WXL (wul)
2026-04-15 15371b329484632c987e349e50f41cab90d3ae8c
测试完成
已删除1个文件
已修改13个文件
已添加9个文件
14063 ■■■■■ 文件已修改
src/api/AiCentre/satisfactionse.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AudioPlayer/index.vue 964 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/batch.vue 1004 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/components/DetailsAnomaly.vue 1266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/dispose.vue 570 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationmyd/index.vue 554 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/HistoricalFollow/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/again/index.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/ChartDialog.vue 579 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/ContinuedCare.vue 633 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/DetailDialog.vue 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/FirstFollowUp.vue 728 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/SecondFollowUp.vue 679 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/TimelyRateDialog.vue 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/components/styles.scss 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/index copy.vue 2898 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/index.vue 3338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vue.config.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xhyy.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/satisfactionse.js
@@ -35,6 +35,31 @@
    method: "get",
  });
}
// å¼‚常处理列表
export function tracedeallist(data) {
  return request({
    url: "/smartor/trace/tracedeallist",
    method: "post",
    data: data,
  });
}
// æ‰¹é‡å¤„理列表
export function tracelist(data) {
  return request({
    url: "/smartor/trace/list",
    method: "post",
    data: data,
  });
}
// ä»»åŠ¡åˆ—è¡¨ä¿®æ”¹å¤„ç†
export function traceedit(data) {
  return request({
    url: "/smartor/trace/edit",
    method: "post",
    data: data,
  });
}
src/api/system/user.js
@@ -102,6 +102,14 @@
    data: data,
  });
}
// å»¶ç»­æŠ¤ç†ç»Ÿè®¡
export function getContinueNerseCount(data) {
  return request({
    url: "/smartor/serviceSubtask/getContinueNerseCount",
    method: "post",
    data: data,
  });
}
// æ»¡æ„åº¦ç»Ÿè®¡è¯¦æƒ…
export function getSfStatisticsJoyInfo(data) {
  return request({
src/components/AudioPlayer/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,964 @@
<template>
  <div class="audio-player">
    <!-- ä¸»æ’­æ”¾å™¨ -->
    <div class="audio-container" v-if="showDefaultPlayer">
      <audio
        ref="audioElement"
        :src="audioSource"
        preload="metadata"
        @timeupdate="updateTime"
        @loadedmetadata="updateDuration"
        @play="handlePlay"
        @pause="handlePause"
        @ended="handleEnded"
        @error="handleError"
        style="display: none"
      ></audio>
      <!-- æ’­æ”¾æŽ§åˆ¶ -->
      <div class="audio-controls">
        <el-button
          v-if="!isPlaying"
          type="text"
          icon="el-icon-video-play"
          class="play-btn"
          @click="playAudio"
          :loading="loading"
        ></el-button>
        <el-button
          v-else
          type="text"
          icon="el-icon-video-pause"
          class="pause-btn"
          @click="pauseAudio"
        ></el-button>
        <!-- è¿›åº¦æ¡ -->
        <div class="progress-container" @click="seekAudio" @mousemove="updateHoverTime">
          <div class="progress-background">
            <!-- ç¼“冲进度 -->
            <div
              v-if="buffered.length > 0"
              class="buffered-progress"
              :style="{ width: `${bufferedPercent}%` }"
            ></div>
            <!-- æ’­æ”¾è¿›åº¦ -->
            <div
              class="played-progress"
              :style="{ width: `${progressPercent}%` }"
            ></div>
            <!-- æ‚¬åœé¢„览 -->
            <div
              v-if="showHoverPreview"
              class="hover-preview"
              :style="{ left: `${hoverPercent}%` }"
            >
              <span class="hover-time">{{ formatTime(hoverTime) }}</span>
            </div>
            <!-- æ’­æ”¾ç‚¹ -->
            <div
              class="playhead"
              :style="{ left: `${progressPercent}%` }"
            ></div>
          </div>
        </div>
        <!-- æ—¶é—´æ˜¾ç¤º -->
        <div class="time-display">
          <span class="current-time">{{ formatTime(currentTime) }}</span>
          <span class="time-separator">/</span>
          <span class="duration">{{ formatTime(duration) }}</span>
        </div>
        <!-- éŸ³é‡æŽ§åˆ¶ -->
        <div class="volume-control" v-if="showVolumeControl">
          <el-popover
            placement="top"
            width="40"
            trigger="hover"
            popper-class="volume-popover"
          >
            <div class="volume-slider-container" @click.stop>
              <el-slider
                v-model="volume"
                vertical
                height="100px"
                :show-tooltip="false"
                @input="changeVolume"
              ></el-slider>
            </div>
            <el-button
              slot="reference"
              type="text"
              :icon="volumeIcon"
              class="volume-btn"
              @click.stop
            ></el-button>
          </el-popover>
        </div>
        <!-- æ’­æ”¾é€Ÿåº¦ -->
        <el-select
          v-if="showPlaybackRate"
          v-model="playbackRate"
          size="mini"
          class="playback-rate"
          @change="changePlaybackRate"
        >
          <el-option
            v-for="rate in playbackRates"
            :key="rate"
            :label="`${rate}x`"
            :value="rate"
          ></el-option>
        </el-select>
        <!-- ä¸‹è½½æŒ‰é’® -->
        <el-button
          v-if="showDownload"
          type="text"
          icon="el-icon-download"
          class="download-btn"
          @click="downloadAudio"
          title="下载音频"
        ></el-button>
        <!-- æ›´å¤šåŠŸèƒ½æŒ‰é’® -->
        <el-dropdown
          v-if="showMoreOptions"
          trigger="click"
          class="more-options"
        >
          <el-button type="text" icon="el-icon-more" class="more-btn"></el-button>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="loopAudio = !loopAudio">
              <i :class="loopAudio ? 'el-icon-refresh' : 'el-icon-refresh-left'"></i>
              {{ loopAudio ? '关闭循环' : '开启循环' }}
            </el-dropdown-item>
            <el-dropdown-item @click.native="resetAudio">
              <i class="el-icon-refresh-right"></i>
              é‡ç½®
            </el-dropdown-item>
            <el-dropdown-item v-if="showDetails" @click.native="showAudioInfo">
              <i class="el-icon-info"></i>
              éŸ³é¢‘信息
            </el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </div>
    </div>
    <!-- éŸ³é¢‘信息对话框 -->
    <el-dialog
      v-if="showDetailsDialog"
      title="音频信息"
      :visible.sync="showDetailsDialog"
      width="400px"
    >
      <div class="audio-info">
        <div class="info-item">
          <span class="label">文件大小:</span>
          <span class="value">{{ fileSize }}</span>
        </div>
        <div class="info-item">
          <span class="label">音频时长:</span>
          <span class="value">{{ formatTime(duration) }}</span>
        </div>
        <div class="info-item">
          <span class="label">文件格式:</span>
          <span class="value">{{ audioFormat }}</span>
        </div>
        <div class="info-item">
          <span class="label">音频地址:</span>
          <a :href="audioSource" target="_blank" class="audio-url">{{ truncateUrl(audioSource) }}</a>
        </div>
      </div>
    </el-dialog>
    <!-- ç®€æ˜“播放器(仅显示播放按钮) -->
    <div v-else class="simple-player">
      <el-button
        v-if="!isPlaying"
        type="text"
        icon="el-icon-video-play"
        size="small"
        class="simple-play-btn"
        @click="playAudio"
        :loading="loading"
      >
        æ’­æ”¾å½•音
      </el-button>
      <el-button
        v-else
        type="text"
        icon="el-icon-video-pause"
        size="small"
        class="simple-pause-btn"
        @click="pauseAudio"
      >
        æš‚停播放
      </el-button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'AudioPlayer',
  props: {
    // éŸ³é¢‘源地址
    audioSource: {
      type: String,
      default: ''
    },
    // æ˜¯å¦æ˜¾ç¤ºå®Œæ•´æ’­æ”¾å™¨
    showDefaultPlayer: {
      type: Boolean,
      default: true
    },
    // æ˜¯å¦æ˜¾ç¤ºéŸ³é‡æŽ§åˆ¶
    showVolumeControl: {
      type: Boolean,
      default: true
    },
    // æ˜¯å¦æ˜¾ç¤ºæ’­æ”¾é€Ÿåº¦æŽ§åˆ¶
    showPlaybackRate: {
      type: Boolean,
      default: true
    },
    // æ˜¯å¦æ˜¾ç¤ºä¸‹è½½æŒ‰é’®
    showDownload: {
      type: Boolean,
      default: true
    },
    // æ˜¯å¦æ˜¾ç¤ºæ›´å¤šé€‰é¡¹
    showMoreOptions: {
      type: Boolean,
      default: true
    },
    // æ˜¯å¦æ˜¾ç¤ºéŸ³é¢‘信息
    showDetails: {
      type: Boolean,
      default: true
    },
    // åˆå§‹éŸ³é‡ï¼ˆ0-100)
    initialVolume: {
      type: Number,
      default: 80,
      validator: (value) => value >= 0 && value <= 100
    },
    // åˆå§‹æ’­æ”¾é€Ÿåº¦
    initialPlaybackRate: {
      type: Number,
      default: 1.0
    },
    // è‡ªåŠ¨æ’­æ”¾
    autoplay: {
      type: Boolean,
      default: false
    },
    // å¾ªçŽ¯æ’­æ”¾
    loop: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      // æ’­æ”¾çŠ¶æ€
      isPlaying: false,
      isLoading: false,
      loading: false,
      error: false,
      // æ—¶é—´ç›¸å…³
      currentTime: 0,
      duration: 0,
      buffered: [],
      // éŸ³é‡
      volume: this.initialVolume,
      isMuted: false,
      // æ’­æ”¾é€Ÿåº¦
      playbackRate: this.initialPlaybackRate,
      playbackRates: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
      // å¾ªçŽ¯æ’­æ”¾
      loopAudio: this.loop,
      // è¿›åº¦æ¡æ‚¬åœ
      hoverTime: 0,
      hoverPercent: 0,
      showHoverPreview: false,
      // éŸ³é¢‘信息
      showDetailsDialog: false,
      fileSize: '未知',
      audioFormat: '未知',
      // ç›‘听器引用
      resizeObserver: null
    };
  },
  computed: {
    // æ’­æ”¾è¿›åº¦ç™¾åˆ†æ¯”
    progressPercent() {
      if (this.duration <= 0) return 0;
      return (this.currentTime / this.duration) * 100;
    },
    // ç¼“冲进度百分比
    bufferedPercent() {
      if (this.duration <= 0) return 0;
      if (this.buffered.length === 0) return 0;
      const bufferedEnd = this.buffered.end(this.buffered.length - 1);
      return (bufferedEnd / this.duration) * 100;
    },
    // éŸ³é‡å›¾æ ‡
    volumeIcon() {
      if (this.isMuted || this.volume === 0) {
        return 'el-icon-turn-off-microphone';
      } else if (this.volume <= 30) {
        return 'el-icon-microphone';
      } else if (this.volume <= 70) {
        return 'el-icon-microphone';
      } else {
        return 'el-icon-microphone';
      }
    }
  },
  watch: {
    // ç›‘听音频源变化
    audioSource(newSource) {
      if (newSource) {
        this.resetAudio();
        this.loadAudio();
      }
    },
    // ç›‘听自动播放
    autoplay(newVal) {
      if (newVal && this.audioSource) {
        this.playAudio();
      }
    },
    // ç›‘听循环播放
    loopAudio(newVal) {
      this.$emit('loop-change', newVal);
    }
  },
  mounted() {
    if (this.audioSource) {
      this.loadAudio();
    }
    // ç›‘听窗口大小变化
    this.resizeObserver = new ResizeObserver(() => {
      this.updateBuffered();
    });
    if (this.$refs.audioElement) {
      this.resizeObserver.observe(this.$refs.audioElement);
    }
  },
  beforeDestroy() {
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
    }
    // åœæ­¢éŸ³é¢‘播放
    this.pauseAudio();
  },
  methods: {
    // åŠ è½½éŸ³é¢‘
    async loadAudio() {
      this.loading = true;
      this.error = false;
      try {
        const audio = this.$refs.audioElement;
        if (!audio) return;
        // è®¾ç½®éŸ³é‡
        audio.volume = this.volume / 100;
        audio.playbackRate = this.playbackRate;
        audio.loop = this.loopAudio;
        // å°è¯•获取文件信息
        this.getAudioInfo();
        // å¦‚果设置了自动播放,开始播放
        if (this.autoplay) {
          await this.playAudio();
        }
      } catch (error) {
        console.error('加载音频失败:', error);
        this.error = true;
        this.$emit('error', error);
      } finally {
        this.loading = false;
      }
    },
    // æ’­æ”¾éŸ³é¢‘
    async playAudio() {
      try {
        const audio = this.$refs.audioElement;
        if (!audio) return;
        await audio.play();
        this.isPlaying = true;
        this.$emit('play');
      } catch (error) {
        console.error('播放失败:', error);
        this.error = true;
        this.isPlaying = false;
        this.$emit('error', error);
        // å¦‚果是用户交互引起的错误,提示用户
        if (error.name === 'NotAllowedError') {
          this.$message.warning('请手动点击播放按钮开始播放');
        }
      }
    },
    // æš‚停音频
    pauseAudio() {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      audio.pause();
      this.isPlaying = false;
      this.$emit('pause');
    },
    // è·³è½¬åˆ°æŒ‡å®šæ—¶é—´
    seekAudio(event) {
      const audio = this.$refs.audioElement;
      if (!audio || this.duration <= 0) return;
      const progressContainer = event.currentTarget;
      const rect = progressContainer.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const width = rect.width;
      const percent = Math.max(0, Math.min(1, x / width));
      const time = percent * this.duration;
      audio.currentTime = time;
      this.currentTime = time;
      this.$emit('seek', time);
    },
    // æ›´æ–°æ‚¬åœæ—¶é—´
    updateHoverTime(event) {
      const progressContainer = event.currentTarget;
      const rect = progressContainer.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const width = rect.width;
      const percent = Math.max(0, Math.min(1, x / width));
      this.hoverPercent = percent * 100;
      this.hoverTime = percent * this.duration;
      this.showHoverPreview = true;
    },
    // éšè—æ‚¬åœé¢„览
    hideHoverPreview() {
      this.showHoverPreview = false;
    },
    // æ›´æ–°æ—¶é—´
    updateTime() {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      this.currentTime = audio.currentTime;
      this.$emit('timeupdate', this.currentTime);
    },
    // æ›´æ–°æ€»æ—¶é•¿
    updateDuration() {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      this.duration = audio.duration;
      this.updateBuffered();
      this.$emit('loadedmetadata', this.duration);
    },
    // æ›´æ–°ç¼“冲数据
    updateBuffered() {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      this.buffered = audio.buffered;
    },
    // å¤„理播放
    handlePlay() {
      this.isPlaying = true;
      this.$emit('play');
    },
    // å¤„理暂停
    handlePause() {
      this.isPlaying = false;
      this.$emit('pause');
    },
    // å¤„理播放结束
    handleEnded() {
      this.isPlaying = false;
      this.currentTime = 0;
      this.$emit('ended');
    },
    // å¤„理错误
    handleError(event) {
      console.error('音频播放错误:', event);
      this.error = true;
      this.isPlaying = false;
      this.$emit('error', event);
    },
    // æ”¹å˜éŸ³é‡
    changeVolume(value) {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      audio.volume = value / 100;
      this.volume = value;
      this.isMuted = value === 0;
      this.$emit('volume-change', value);
    },
    // é™éŸ³/取消静音
    toggleMute() {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      this.isMuted = !this.isMuted;
      audio.muted = this.isMuted;
      this.$emit('mute-change', this.isMuted);
    },
    // æ”¹å˜æ’­æ”¾é€Ÿåº¦
    changePlaybackRate(rate) {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      audio.playbackRate = rate;
      this.playbackRate = rate;
      this.$emit('playbackrate-change', rate);
    },
    // é‡ç½®éŸ³é¢‘
    resetAudio() {
      const audio = this.$refs.audioElement;
      if (!audio) return;
      audio.currentTime = 0;
      this.currentTime = 0;
      this.pauseAudio();
      this.$emit('reset');
    },
    // ä¸‹è½½éŸ³é¢‘
    downloadAudio() {
      if (!this.audioSource) {
        this.$message.warning('音频地址无效');
        return;
      }
      const link = document.createElement('a');
      link.href = this.audioSource;
      link.download = this.getFileNameFromUrl(this.audioSource);
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      this.$emit('download');
    },
    // æ˜¾ç¤ºéŸ³é¢‘信息
    async showAudioInfo() {
      this.showDetailsDialog = true;
      await this.getAudioInfo();
    },
    // èŽ·å–éŸ³é¢‘ä¿¡æ¯
    async getAudioInfo() {
      if (!this.audioSource) return;
      try {
        // èŽ·å–æ–‡ä»¶å¤§å°
        const response = await fetch(this.audioSource, { method: 'HEAD' });
        const contentLength = response.headers.get('content-length');
        if (contentLength) {
          this.fileSize = this.formatFileSize(contentLength);
        }
        // èŽ·å–æ–‡ä»¶æ ¼å¼
        const url = this.audioSource.toLowerCase();
        if (url.endsWith('.mp3')) this.audioFormat = 'MP3';
        else if (url.endsWith('.wav')) this.audioFormat = 'WAV';
        else if (url.endsWith('.ogg')) this.audioFormat = 'OGG';
        else if (url.endsWith('.m4a')) this.audioFormat = 'M4A';
        else if (url.endsWith('.aac')) this.audioFormat = 'AAC';
        else this.audioFormat = '未知格式';
      } catch (error) {
        console.error('获取音频信息失败:', error);
        this.fileSize = '未知';
        this.audioFormat = '未知';
      }
    },
    // æ ¼å¼åŒ–æ—¶é—´
    formatTime(time) {
      if (isNaN(time) || time < 0) return '00:00';
      const hours = Math.floor(time / 3600);
      const minutes = Math.floor((time % 3600) / 60);
      const seconds = Math.floor(time % 60);
      if (hours > 0) {
        return `${hours.toString().padStart(2, '0')}:${minutes
          .toString()
          .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
      } else {
        return `${minutes.toString().padStart(2, '0')}:${seconds
          .toString()
          .padStart(2, '0')}`;
      }
    },
    // æ ¼å¼åŒ–文件大小
    formatFileSize(bytes) {
      if (bytes === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    },
    // æˆªæ–­URL显示
    truncateUrl(url, maxLength = 40) {
      if (!url) return '';
      if (url.length <= maxLength) return url;
      const start = url.substring(0, maxLength / 2 - 3);
      const end = url.substring(url.length - maxLength / 2 + 3);
      return start + '...' + end;
    },
    // ä»ŽURL获取文件名
    getFileNameFromUrl(url) {
      if (!url) return 'audio';
      const fileName = url.split('/').pop();
      return fileName || 'audio';
    }
  }
};
</script>
<style lang="scss" scoped>
.audio-player {
  width: 100%;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  .audio-container {
    background: #fff;
    border-radius: 8px;
    padding: 12px 16px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    border: 1px solid #ebeef5;
    .audio-controls {
      display: flex;
      align-items: center;
      gap: 12px;
      .play-btn,
      .pause-btn {
        width: 36px;
        height: 36px;
        padding: 0;
        font-size: 20px;
        color: #409EFF;
        &:hover {
          color: #66b1ff;
        }
        &:active {
          color: #3a8ee6;
        }
      }
      .progress-container {
        flex: 1;
        height: 36px;
        display: flex;
        align-items: center;
        cursor: pointer;
        .progress-background {
          position: relative;
          width: 100%;
          height: 6px;
          background: #e4e7ed;
          border-radius: 3px;
          overflow: visible;
          .buffered-progress {
            position: absolute;
            top: 0;
            left: 0;
            height: 100%;
            background: #c0c4cc;
            border-radius: 3px;
            transition: width 0.2s ease;
          }
          .played-progress {
            position: absolute;
            top: 0;
            left: 0;
            height: 100%;
            background: #409EFF;
            border-radius: 3px;
            transition: width 0.2s ease;
            z-index: 2;
          }
          .hover-preview {
            position: absolute;
            top: -30px;
            width: 1px;
            height: 20px;
            background: #909399;
            transform: translateX(-50%);
            z-index: 3;
            pointer-events: none;
            .hover-time {
              position: absolute;
              bottom: 100%;
              left: 50%;
              transform: translateX(-50%);
              background: rgba(0, 0, 0, 0.8);
              color: white;
              padding: 2px 6px;
              border-radius: 3px;
              font-size: 12px;
              white-space: nowrap;
            }
            &::after {
              content: '';
              position: absolute;
              top: 100%;
              left: 50%;
              transform: translateX(-50%);
              width: 0;
              height: 0;
              border-left: 4px solid transparent;
              border-right: 4px solid transparent;
              border-top: 4px solid rgba(0, 0, 0, 0.8);
            }
          }
          .playhead {
            position: absolute;
            top: 50%;
            width: 12px;
            height: 12px;
            background: #fff;
            border: 2px solid #409EFF;
            border-radius: 50%;
            transform: translate(-50%, -50%);
            z-index: 4;
            cursor: pointer;
            transition: all 0.2s ease;
            &:hover {
              transform: translate(-50%, -50%) scale(1.2);
              box-shadow: 0 0 8px rgba(64, 158, 255, 0.5);
            }
          }
        }
        &:hover {
          .progress-background {
            height: 8px;
            .playhead {
              transform: translate(-50%, -50%) scale(1.1);
            }
          }
        }
      }
      .time-display {
        min-width: 100px;
        font-size: 12px;
        color: #606266;
        display: flex;
        align-items: center;
        gap: 2px;
        .current-time {
          color: #303133;
          font-weight: 500;
        }
        .time-separator {
          opacity: 0.6;
        }
        .duration {
          opacity: 0.8;
        }
      }
      .volume-control {
        .volume-btn {
          padding: 0;
          width: 24px;
          height: 24px;
          font-size: 16px;
          color: #606266;
          &:hover {
            color: #409EFF;
          }
        }
      }
      .playback-rate {
        width: 60px;
        ::v-deep .el-input__inner {
          height: 24px;
          line-height: 24px;
          padding: 0 5px;
          font-size: 12px;
        }
      }
      .download-btn,
      .more-btn {
        padding: 0;
        width: 24px;
        height: 24px;
        font-size: 16px;
        color: #606266;
        &:hover {
          color: #409EFF;
        }
      }
    }
  }
  .simple-player {
    .simple-play-btn,
    .simple-pause-btn {
      padding: 4px 8px;
      font-size: 12px;
      color: #409EFF;
      &:hover {
        color: #66b1ff;
      }
    }
  }
  .audio-info {
    .info-item {
      margin-bottom: 12px;
      display: flex;
      .label {
        width: 80px;
        font-weight: 500;
        color: #303133;
      }
      .value {
        flex: 1;
        color: #606266;
      }
      .audio-url {
        color: #409EFF;
        text-decoration: none;
        &:hover {
          text-decoration: underline;
        }
      }
    }
  }
}
// éŸ³é‡å¼¹çª—样式
::v-deep .volume-popover {
  padding: 10px;
  min-width: 40px;
  .volume-slider-container {
    height: 100px;
    display: flex;
    justify-content: center;
    .el-slider {
      height: 100%;
      .el-slider__runway {
        background: #e4e7ed;
      }
      .el-slider__bar {
        background: #409EFF;
      }
      .el-slider__button {
        border-color: #409EFF;
        width: 12px;
        height: 12px;
      }
    }
  }
}
</style>
src/views/Satisfaction/configurationmyd/batch.vue
@@ -11,8 +11,9 @@
            icon="el-icon-check"
            @click="handleBatchSubmit"
            :loading="batchProcessing"
            :disabled="selectedExceptionIds.length === 0"
          >
            æ‰¹é‡æäº¤å¤„理
            æ‰¹é‡æäº¤å¤„理 ({{ selectedExceptionIds.length }})
          </el-button>
          <el-button type="warning" icon="el-icon-back" @click="handleGoBack">
            è¿”回异常列表
@@ -33,29 +34,40 @@
          >
            <el-form-item label="负责科室">
              <el-select
                v-model="filterParams.deptId"
                v-model="filterParams.todeptcode"
                placeholder="请选择科室"
                clearable
                filterable
                style="width: 200px"
              >
                <el-option
                  v-for="dept in deptList"
                  :key="dept.id"
                  :label="dept.name"
                  :value="dept.id"
                  :key="dept.deptCode"
                  :label="dept.label"
                  :value="dept.deptCode"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="处理状态">
              <el-select
                v-model="filterParams.status"
                v-model="filterParams.handleFlag"
                placeholder="请选择状态"
                clearable
                style="width: 200px"
              >
                <el-option label="待处理" :value="0" />
                <el-option label="处理中" :value="1" />
                <el-option label="已处理" :value="2" />
                <el-option label="未处理" :value="'0'" />
                <el-option label="已处理" :value="'1'" />
              </el-select>
            </el-form-item>
            <el-form-item label="满意度类型">
              <el-select
                v-model="filterParams.templateType"
                placeholder="请选择模板类型"
                clearable
                style="width: 200px"
              >
                <el-option label="语音模板" :value="1" />
                <el-option label="问卷模板" :value="2" />
              </el-select>
            </el-form-item>
            <el-form-item>
@@ -79,6 +91,7 @@
          :border="true"
          style="width: 100%"
          @selection-change="handleSelectionChange"
          row-key="id"
          class="exception-table"
        >
          <el-table-column type="selection" width="55" align="center" />
@@ -92,24 +105,30 @@
          <el-table-column
            label="负责科室"
            prop="responsibilityDept"
            width="120"
            prop="todeptname"
            width="200"
            align="center"
          >
            <template slot-scope="{ row }">
              <el-tag type="primary">{{ row.responsibilityDept }}</el-tag>
              <el-tag type="primary" v-if="row.todeptname">{{
                row.todeptname
              }}</el-tag>
              <span v-else class="no-data">未分配</span>
            </template>
          </el-table-column>
          <el-table-column
            label="不满意详情"
            prop="unsatisfactoryDetail"
            min-width="200"
            align="center"
          >
          <el-table-column label="不满意详情" min-width="250" align="center">
            <template slot-scope="{ row }">
              <div class="detail-content">
                {{ row.unsatisfactoryDetail }}
                <div class="question-text">
                  <strong>问题:</strong>{{ row.questiontext }}
                </div>
                <div class="answer-text">
                  <strong>回答:</strong>{{ row.asrtext || "无回答" }}
                </div>
                <div class="matched-text" v-if="row.matchedtext">
                  <strong>解析值:</strong>{{ row.matchedtext }}
                </div>
              </div>
            </template>
          </el-table-column>
@@ -117,42 +136,50 @@
          <el-table-column label="患者信息" width="300" align="center">
            <template slot-scope="{ row }">
              <div class="patient-info">
                <div class="patient-item">
                  <span class="label">姓名:</span>
                  <span class="value">{{ row.patientName }}</span>
                <div class="patient-row">
                  <div class="patient-item">
                    <span class="label">姓名:</span>
                    <span class="value">{{ row.patdescJson.sendname }}</span>
                  </div>
                  <div class="patient-item">
                    <span class="label">性别:</span>
                    <span class="value">{{
                      row.patdescJson.sex
                    }}</span>
                  </div>
                  <div class="patient-item">
                    <span class="label">年龄:</span>
                    <span class="value">{{ row.patdescJson.age }}岁</span>
                  </div>
                </div>
                <div class="patient-item">
                  <span class="label">性别:</span>
                  <span class="value">{{
                    row.gender === 1 ? "男" : "女"
                  }}</span>
                </div>
                <div class="patient-item">
                  <span class="label">年龄:</span>
                  <span class="value">{{ row.age }}岁</span>
                </div>
                <div class="patient-item">
                  <span class="label">电话:</span>
                  <span class="value">{{ row.phone }}</span>
                <div class="patient-row">
                  <div class="patient-item full-width">
                    <span class="label">电话:</span>
                    <span class="value">{{ row.patdescJson.phone }}</span>
                  </div>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="出院信息" width="250" align="center">
          <el-table-column label="填写信息" width="180" align="center">
            <template slot-scope="{ row }">
              <div class="discharge-info">
              <div class="fill-info">
                <div class="info-item">
                  <span class="label">科室:</span>
                  <span class="value">{{ row.dischargeDept }}</span>
                  <span class="label">填报时间:</span>
                  <span class="value time">{{
                    formatDateTime(row.createTime)
                  }}</span>
                </div>
                <div class="info-item">
                  <span class="label">病区:</span>
                  <span class="value">{{ row.dischargeWard }}</span>
                </div>
                <div class="info-item">
                  <span class="label">填写时间:</span>
                  <span class="value time">{{ row.fillTime }}</span>
                <div v-if="row.recordurl" class="info-item">
                  <el-button
                    type="text"
                    size="small"
                    @click="handlePlayAudio(row.recordurl)"
                    icon="el-icon-headset"
                  >
                    æ’­æ”¾å½•音
                  </el-button>
                </div>
              </div>
            </template>
@@ -160,14 +187,36 @@
          <el-table-column
            label="处理状态"
            prop="processStatus"
            prop="handleFlag"
            width="100"
            align="center"
          >
            <template slot-scope="{ row }">
              <el-tag :type="getStatusTagType(row.processStatus)" effect="dark">
                {{ getStatusText(row.processStatus) }}
              <el-tag :type="getStatusTagType(row.handleFlag)" effect="dark">
                {{ getStatusText(row.handleFlag) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="最新处理信息" width="180" align="center">
            <template slot-scope="{ row }">
              <div v-if="row.handleTime" class="handle-info">
                <div class="info-item">
                  <span class="label">处理人:</span>
                  <span class="value">{{ row.handleBy || "系统" }}</span>
                </div>
                <div class="info-item">
                  <span class="label">处理时间:</span>
                  <span class="value time">{{
                    formatDateTime(row.handleTime)
                  }}</span>
                </div>
                <div class="info-item">
                  <span class="label">处理说明:</span>
                  <span class="value">{{ row.handledesc }}</span>
                </div>
              </div>
              <span v-else class="no-data">未处理</span>
            </template>
          </el-table-column>
@@ -191,7 +240,7 @@
                size="small"
                icon="el-icon-edit"
                @click="handleProcess(row)"
                :disabled="row.processStatus === 2"
                :disabled="row.handleFlag === '1'"
              >
                å¤„理
              </el-button>
@@ -229,63 +278,76 @@
        label-width="100px"
        size="medium"
      >
        <el-form-item label="处理状态" prop="status">
        <el-form-item label="处理状态" prop="handleFlag">
          <el-select
            v-model="processForm.status"
            v-model="processForm.handleFlag"
            placeholder="请选择处理状态"
            style="width: 100%"
          >
            <el-option label="处理中" :value="1" />
            <el-option label="已处理" :value="2" />
            <el-option label="已驳回" :value="3" />
            <el-option label="已处理" :value="'1'" />
            <el-option label="取消处理" :value="'0'" />
          </el-select>
        </el-form-item>
        <el-form-item label="报备科室" prop="reportDepts">
        <el-form-item label="报备科室" prop="ccdepts">
          <el-select
            v-model="processForm.reportDepts"
            v-model="processForm.ccdepts"
            placeholder="请选择报备科室"
            multiple
            filterable
            collapse-tags
            style="width: 100%"
            :disabled="processForm.handleFlag !== '1'"
          >
            <el-option
              v-for="dept in deptList"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
              :key="dept.deptCode"
              :label="dept.label"
              :value="dept.deptCode"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="处理备注" prop="remark">
        <el-form-item label="处理结果" prop="handleresult">
          <el-select
            v-model="processForm.handleresult"
            placeholder="请选择处理结果"
            style="width: 100%"
            :disabled="processForm.handleFlag !== '1'"
          >
            <el-option label="已解决" value="resolved" />
            <el-option label="已解释" value="explained" />
            <el-option label="已转交" value="transferred" />
            <el-option label="需改进" value="improvement" />
            <el-option label="已驳回" value="rejected" />
          </el-select>
        </el-form-item>
        <el-form-item label="处理说明" prop="handledesc">
          <el-input
            v-model="processForm.remark"
            v-model="processForm.handledesc"
            type="textarea"
            :rows="4"
            placeholder="请输入处理备注(最多500字)"
            placeholder="请输入处理说明(最多500字)"
            maxlength="500"
            show-word-limit
            :disabled="processForm.handleFlag !== '1'"
          />
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            class="upload-demo"
            action="#"
            :on-preview="handlePreview"
            :on-remove="handleRemove"
            :before-remove="beforeRemove"
            :limit="3"
            :on-exceed="handleExceed"
            :file-list="fileList"
          >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">
              æ”¯æŒä¸Šä¼ å›¾ç‰‡ã€æ–‡æ¡£ç­‰é™„件,单个文件不超过10MB
            </div>
          </el-upload>
        <el-form-item
          label="最终意见"
          prop="finaloption"
          v-if="hasQualityPermission"
        >
          <el-input
            v-model="processForm.finaloption"
            type="textarea"
            :rows="3"
            placeholder="请输入最终处理意见(最多300字)"
            maxlength="300"
            show-word-limit
          />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
@@ -310,44 +372,60 @@
        label-width="100px"
        size="medium"
      >
        <el-form-item label="处理状态" prop="status">
        <el-form-item label="处理状态" prop="handleFlag">
          <el-select
            v-model="batchProcessForm.status"
            v-model="batchProcessForm.handleFlag"
            placeholder="请选择处理状态"
            style="width: 100%"
          >
            <el-option label="处理中" :value="1" />
            <el-option label="已处理" :value="2" />
            <el-option label="已驳回" :value="3" />
            <el-option label="已处理" :value="'1'" />
            <el-option label="取消处理" :value="'0'" />
          </el-select>
        </el-form-item>
        <el-form-item label="报备科室" prop="reportDepts">
        <el-form-item label="报备科室" prop="ccdepts">
          <el-select
            v-model="batchProcessForm.reportDepts"
            v-model="batchProcessForm.ccdepts"
            placeholder="请选择报备科室"
            multiple
            filterable
            collapse-tags
            style="width: 100%"
            :disabled="batchProcessForm.handleFlag !== '1'"
          >
            <el-option
              v-for="dept in deptList"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
              :key="dept.deptCode"
              :label="dept.label"
              :value="dept.deptCode"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="处理备注" prop="remark">
        <el-form-item label="处理结果" prop="handleresult">
          <el-select
            v-model="batchProcessForm.handleresult"
            placeholder="请选择处理结果"
            style="width: 100%"
            :disabled="batchProcessForm.handleFlag !== '1'"
          >
            <el-option label="已解决" value="resolved" />
            <el-option label="已解释" value="explained" />
            <el-option label="已转交" value="transferred" />
            <el-option label="需改进" value="improvement" />
            <el-option label="已驳回" value="rejected" />
          </el-select>
        </el-form-item>
        <el-form-item label="处理说明" prop="handledesc">
          <el-input
            v-model="batchProcessForm.remark"
            v-model="batchProcessForm.handledesc"
            type="textarea"
            :rows="4"
            placeholder="请输入处理备注(最多500字)"
            placeholder="请输入处理说明(最多500字)"
            maxlength="500"
            show-word-limit
            :disabled="batchProcessForm.handleFlag !== '1'"
          />
        </el-form-item>
      </el-form>
@@ -358,24 +436,57 @@
          @click="submitBatchProcess"
          :loading="batchProcessing"
        >
          æ‰¹é‡æäº¤
          æ‰¹é‡æäº¤ ({{ selectedExceptionIds.length }})
        </el-button>
      </span>
    </el-dialog>
    <!-- è¿›åº¦å¯¹è¯æ¡† -->
    <el-dialog
      title="批量处理进度"
      :visible.sync="batchProgress.visible"
      width="400px"
      :close-on-click-modal="false"
      :show-close="false"
      :close-on-press-escape="false"
    >
      <div class="progress-content">
        <el-progress
          :percentage="batchProgress.percentage"
          :status="batchProgress.percentage === 100 ? 'success' : ''"
        />
        <div class="progress-info">
          å·²å¤„理 {{ batchProgress.processed }}/{{ batchProgress.total }} æ¡è®°å½•
        </div>
      </div>
    </el-dialog>
    <!-- å¼‚常详情弹框 -->
    <Details-anomaly
      :visible="detailDialogVisible"
      :record-id="selectedRecordId"
      :title="detailDialogTitle"
      :record-data="selectedRecordData"
      @update:visible="handleDetailDialogClose"
      @processed="handleProcessed"
      @close="handleDetailDialogClose"
    />
    <!-- å½•音播放器 -->
    <audio
      v-if="audioUrl"
      :src="audioUrl"
      ref="audioPlayer"
      controls
      style="display: none"
    />
  </div>
</template>
<script>
import DetailsAnomaly from "./components/DetailsAnomaly.vue";
import { tracelist, traceedit } from "@/api/AiCentre/index";
import dayjs from "dayjs";
import { deptTreeSelect } from "@/api/system/user";
export default {
  name: "BatchProcess",
  components: {
@@ -383,10 +494,15 @@
  },
  data() {
    return {
      // æ·»åŠ ä»¥ä¸‹æ•°æ®
      // è¯¦æƒ…弹框相关
      detailDialogVisible: false,
      selectedRecordId: null,
      selectedRecordData: null,
      detailDialogTitle: "异常反馈详情",
      // éŸ³é¢‘播放
      audioUrl: "",
      // å½“前处理的异常ID
      currentExceptionId: null,
@@ -395,8 +511,10 @@
      // è¿‡æ»¤å‚æ•°
      filterParams: {
        deptId: "",
        status: "",
        todeptcode: "",
        handleFlag: "",
        templateType: null,
        scriptids: null,
        pageNum: 1,
        pageSize: 10,
      },
@@ -406,19 +524,11 @@
      processing: false,
      batchProcessing: false,
      // æƒé™æŽ§åˆ¶
      hasQualityPermission: false, // æ˜¯å¦å…·æœ‰è´¨ç®¡æƒé™
      // ç§‘室列表
      deptList: [
        { id: 1, name: "心血管内科" },
        { id: 2, name: "神经内科" },
        { id: 3, name: "普外科" },
        { id: 4, name: "骨科" },
        { id: 5, name: "妇产科" },
        { id: 6, name: "儿科" },
        { id: 7, name: "急诊科" },
        { id: 8, name: "呼吸内科" },
        { id: 9, name: "消化内科" },
        { id: 10, name: "内分泌科" },
      ],
      deptList: [],
      // å¼‚常列表数据
      exceptionList: [],
@@ -427,183 +537,169 @@
      // å¤„理对话框
      processDialogVisible: false,
      processForm: {
        status: "",
        reportDepts: [],
        remark: "",
        handleFlag: "",
        ccdepts: [],
        handleresult: "",
        handledesc: "",
        finaloption: "",
      },
      batchProgress: {
        visible: false,
        percentage: 0,
        processed: 0,
        total: 0,
      },
      processRules: {
        status: [
        handleFlag: [
          { required: true, message: "请选择处理状态", trigger: "change" },
        ],
        remark: [
          { required: true, message: "请输入处理备注", trigger: "blur" },
        handleresult: [
          {
            min: 5,
            max: 500,
            message: "备注长度在 5 åˆ° 500 ä¸ªå­—符",
            required: true,
            message: "请选择处理结果",
            trigger: "change",
            validator: (rule, value, callback) => {
              if (this.processForm.handleFlag === "1" && !value) {
                callback(new Error("请选择处理结果"));
              } else {
                callback();
              }
            },
          },
        ],
        handledesc: [
          {
            required: true,
            message: "请输入处理说明",
            trigger: "blur",
            validator: (rule, value, callback) => {
              if (
                this.processForm.handleFlag === "1" &&
                (!value || value.trim().length < 3)
              ) {
                callback(new Error("处理说明至少3个字符"));
              } else {
                callback();
              }
            },
          },
        ],
      },
      fileList: [],
      // æ‰¹é‡å¤„理对话框
      batchDialogVisible: false,
      batchProcessForm: {
        status: "",
        reportDepts: [],
        remark: "",
        handleFlag: "",
        ccdepts: [],
        handleresult: "",
        handledesc: "",
      },
    };
  },
  created() {
    // ä»Žè·¯ç”±å‚数获取问题ID
    this.filterParams.scriptids = this.$route.query.questionId || this.$route.query.questionIds||null;
    // if (this.$route.query.questionId) {
    // } else if (this.$route.query.questionIds) {
    //   console.log(
    //     this.$route.query.questionIds,
    //     "this.$route.query.questionIds"
    //   );
      this.filterParams.templateType = Number(this.$route.query.type)||null;
    //   this.filterParams.scriptid = null;
    // }
    this.hasQualityPermission = this.checkQualityPermission();
  },
  mounted() {
    this.loadExceptionList();
    this.getDeptOptions();
  },
  methods: {
    // åŠ è½½å¼‚å¸¸åˆ—è¡¨
    async loadExceptionList() {
      this.loading = true;
    // æ ¼å¼åŒ–日期时间
    formatDateTime(dateTime) {
      if (!dateTime) return "";
      try {
        // Mock æ•°æ®
        await new Promise((resolve) => {
          setTimeout(() => {
            this.exceptionList = [
              {
                id: 1,
                responsibilityDept: "心血管内科",
                unsatisfactoryDetail:
                  "医生查房时间太短,沟通不够充分,对病情解释不够详细",
                patientName: "张先生",
                gender: 1,
                age: 45,
                phone: "138****1234",
                dischargeDept: "心血管内科",
                dischargeWard: "内科一病区",
                fillTime: "2024-01-15 10:30:25",
                processStatus: 0,
                questionnaireId: 1001,
              },
              {
                id: 2,
                responsibilityDept: "神经内科",
                unsatisfactoryDetail:
                  "护士打针技术不佳,扎了三次才成功,且态度不够耐心",
                patientName: "李女士",
                gender: 0,
                age: 38,
                phone: "139****5678",
                dischargeDept: "神经内科",
                dischargeWard: "内科二病区",
                fillTime: "2024-01-14 16:20:10",
                processStatus: 0,
                questionnaireId: 1002,
              },
              {
                id: 3,
                responsibilityDept: "普外科",
                unsatisfactoryDetail: "术后换药不及时,伤口疼痛时没有及时处理",
                patientName: "王先生",
                gender: 1,
                age: 52,
                phone: "137****9012",
                dischargeDept: "普外科",
                dischargeWard: "外科一病区",
                fillTime: "2024-01-13 09:15:45",
                processStatus: 1,
                questionnaireId: 1003,
              },
              {
                id: 4,
                responsibilityDept: "骨科",
                unsatisfactoryDetail: "康复指导不够专业,对恢复过程描述不清楚",
                patientName: "刘女士",
                gender: 0,
                age: 65,
                phone: "136****3456",
                dischargeDept: "骨科",
                dischargeWard: "外科二病区",
                fillTime: "2024-01-12 14:40:30",
                processStatus: 0,
                questionnaireId: 1004,
              },
              {
                id: 5,
                responsibilityDept: "妇产科",
                unsatisfactoryDetail:
                  "产前检查排队时间过长,等待期间没有休息座位",
                patientName: "陈女士",
                gender: 0,
                age: 28,
                phone: "135****7890",
                dischargeDept: "妇产科",
                dischargeWard: "妇产科病区",
                fillTime: "2024-01-11 11:25:15",
                processStatus: 2,
                questionnaireId: 1005,
              },
              {
                id: 6,
                responsibilityDept: "儿科",
                unsatisfactoryDetail:
                  "儿童用药剂量交代不清晰,用药注意事项没有说明",
                patientName: "赵宝宝",
                gender: 1,
                age: 5,
                phone: "134****1234",
                dischargeDept: "儿科",
                dischargeWard: "儿科病区",
                fillTime: "2024-01-10 15:50:20",
                processStatus: 0,
                questionnaireId: 1006,
              },
              {
                id: 7,
                responsibilityDept: "急诊科",
                unsatisfactoryDetail: "急诊等待时间过长,病情没有得到及时评估",
                patientName: "孙先生",
                gender: 1,
                age: 40,
                phone: "133****5678",
                dischargeDept: "急诊科",
                dischargeWard: "急诊病区",
                fillTime: "2024-01-09 10:15:40",
                processStatus: 0,
                questionnaireId: 1007,
              },
              {
                id: 8,
                responsibilityDept: "呼吸内科",
                unsatisfactoryDetail: "医生开药较多,费用较高,没有说明必要性",
                patientName: "周女士",
                gender: 0,
                age: 55,
                phone: "132****9012",
                dischargeDept: "呼吸内科",
                dischargeWard: "内科一病区",
                fillTime: "2024-01-08 13:30:55",
                processStatus: 1,
                questionnaireId: 1008,
              },
            ];
            this.total = this.exceptionList.length;
            resolve();
          }, 500);
        const date = new Date(dateTime);
        if (isNaN(date.getTime())) {
          return dateTime;
        }
        return (
          date.toLocaleDateString().replace(/\//g, "-") +
          " " +
          date.toTimeString().split(" ")[0]
        );
      } catch (error) {
        console.error("日期格式化错误:", error);
        return dateTime;
      }
    },
    /** æŸ¥è¯¢ç§‘室列表 */
    getDeptOptions() {
      deptTreeSelect()
        .then((res) => {
          if (res.code == 200) {
            this.deptList = this.flattenArray(res.data) || [];
          }
        })
        .catch((error) => {
          console.error("获取科室列表失败:", error);
          this.$message.error("获取科室列表失败");
        });
      } finally {
        this.loading = false;
    },
    flattenArray(multiArray) {
      let result = [];
      function flatten(element) {
        if (element.children && element.children.length > 0) {
          element.children.forEach((child) => flatten(child));
        } else {
          let item = JSON.parse(JSON.stringify(element));
          result.push(item);
        }
      }
      multiArray.forEach((element) => flatten(element));
      return result;
    },
    // è§£æžæ‚£è€…描述信息
    parsePatDesc(patdesc) {
      if (!patdesc) return [];
      try {
        const parts = patdesc.split("|");
        const items = [];
        if (parts[0]) items.push({ label: "姓名", value: parts[0] });
        if (parts[1]) items.push({ label: "电话", value: parts[1] });
        if (parts[2]) items.push({ label: "科室", value: parts[2] });
        return items;
      } catch (error) {
        console.error("解析患者信息失败:", error);
        return [];
      }
    },
    // æ£€æŸ¥è´¨ç®¡æƒé™
    checkQualityPermission() {
      // è¿™é‡Œå¯ä»¥æ ¹æ®å®žé™…权限系统实现
      const userRoles = this.$store.getters.roles || [];
      return (
        userRoles.includes("quality_manager") || userRoles.includes("admin")
      );
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      switch (status) {
        case 0:
          return "warning"; // å¾…处理
        case 1:
          return "primary"; // å¤„理中
        case 2:
    getStatusTagType(handleFlag) {
      switch (handleFlag) {
        case "0":
          return "warning"; // æœªå¤„理
        case "1":
          return "success"; // å·²å¤„理
        default:
          return "info";
@@ -611,16 +707,82 @@
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      switch (status) {
        case 0:
          return "待处理";
        case 1:
          return "处理中";
        case 2:
    getStatusText(handleFlag) {
      switch (handleFlag) {
        case "0":
          return "未处理";
        case "1":
          return "已处理";
        default:
          return "未知";
      }
    },
    // æ’­æ”¾å½•音
    handlePlayAudio(url) {
      this.audioUrl = url;
      this.$nextTick(() => {
        const audioPlayer = this.$refs.audioPlayer;
        if (audioPlayer) {
          audioPlayer.play().catch((error) => {
            console.error("播放失败:", error);
            this.$message.error("音频播放失败");
          });
        }
      });
    },
    // æž„建查询参数
    buildQueryParams() {
      const params = {
        pageNum: this.filterParams.pageNum,
        pageSize: this.filterParams.pageSize,
      };
      if (this.filterParams.todeptcode) {
        params.todeptcode = this.filterParams.todeptcode;
      }
      if (this.filterParams.handleFlag !== "") {
        params.handleFlag = this.filterParams.handleFlag;
      }
      if (this.filterParams.templateType) {
        params.templateType = this.filterParams.templateType;
      }
      // if (this.filterParams.scriptid) {
      //   params.scriptid = this.filterParams.scriptid;
      // }
      if (this.filterParams.scriptids) {
        params.scriptids = this.filterParams.scriptids.split(",");
      }
      return params;
    },
    // åŠ è½½å¼‚å¸¸åˆ—è¡¨
    async loadExceptionList() {
      this.loading = true;
      try {
        const params = this.buildQueryParams();
        const response = await tracelist(params);
        if (response && response.code === 200) {
          this.exceptionList = response.rows || [];
          this.total = response.total || 0;
        } else {
          this.exceptionList = [];
          this.total = 0;
          this.$message.error(response?.msg || "加载异常列表失败");
        }
      } catch (error) {
        console.error("加载异常列表失败:", error);
        this.$message.error("加载异常列表失败,请稍后重试");
        this.exceptionList = [];
        this.total = 0;
      } finally {
        this.loading = false;
      }
    },
@@ -633,11 +795,14 @@
    // é‡ç½®ç­›é€‰
    handleResetFilter() {
      this.filterParams = {
        deptId: "",
        status: "",
        todeptcode: "",
        handleFlag: "",
        templateType: "",
        scriptids: null, // ä¿ç•™é—®é¢˜ID
        pageNum: 1,
        pageSize: 10,
      };
      this.selectedExceptionIds = [];
      this.loadExceptionList();
    },
@@ -652,6 +817,15 @@
        this.$message.warning("请先选择要处理的异常反馈");
        return;
      }
      // é‡ç½®æ‰¹é‡å¤„理表单
      this.batchProcessForm = {
        handleFlag: "",
        ccdepts: [],
        handleresult: "",
        handledesc: "",
      };
      this.batchDialogVisible = true;
    },
@@ -663,44 +837,86 @@
    // æŸ¥çœ‹è¯¦æƒ…
    handleViewDetail(row) {
      this.selectedRecordId = row.id;
      this.detailDialogTitle = `${row.patientName} - å¼‚常反馈详情`;
      this.selectedRecordData = row;
      // ç”Ÿæˆå¼¹æ¡†æ ‡é¢˜
      let title = "异常反馈详情";
      if (row.patdesc) {
        const patientName = row.patdescJson.sendname;
        if (patientName) {
          title = `${patientName} - ${title}`;
        }
      }
      this.detailDialogTitle = title;
      this.detailDialogVisible = true;
    },
    // å¤„理详情弹框关闭
    handleDetailDialogClose() {
      this.detailDialogVisible = false;
      this.selectedRecordId = null;
    }, // å¤„理完成后的回调
      this.selectedRecordData = null;
    },
    // å¤„理完成后的回调
    handleProcessed() {
      // é‡æ–°åŠ è½½æ•°æ®
      this.loadExceptionList();
    },
    // å¤„理单个异常
    handleProcess(row) {
      this.currentExceptionId = row.id;
      // åˆå§‹åŒ–表单数据
      this.processForm = {
        status: row.processStatus === 0 ? 1 : row.processStatus,
        reportDepts: [],
        remark: "",
        handleFlag: row.handleFlag === "0" ? "1" : "0",
        ccdepts: row.ccdepts ? row.ccdepts.split(",") : [],
        handleresult: row.handleresult || "",
        handledesc: row.handledesc || "",
        finaloption: row.finaloption || "",
      };
      this.processDialogVisible = true;
    },
    // æäº¤å¤„理
    async submitProcess() {
      this.$refs.processForm.validate(async (valid) => {
        if (valid) {
          this.processing = true;
          try {
            // Mock API调用
            await new Promise((resolve) => setTimeout(resolve, 1000));
        if (!valid) {
          return;
        }
            this.$message.success("处理提交成功");
            this.processDialogVisible = false;
            this.loadExceptionList();
          } finally {
            this.processing = false;
          }
        this.processing = true;
        try {
          // å‡†å¤‡æäº¤æ•°æ®
          const submitData = {
            id: this.currentExceptionId,
            handleFlag: this.processForm.handleFlag,
            handleresult: this.processForm.handleresult,
            handledesc: this.processForm.handledesc,
            finaloption: this.processForm.finaloption,
            handleBy: this.$store.state.user.nickName,
            handleTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
            // å°†æ•°ç»„转换为逗号分隔的字符串
            ccdepts: Array.isArray(this.processForm.ccdepts)
              ? this.processForm.ccdepts.join(",")
              : this.processForm.ccdepts,
          };
          // TODO: è¿™é‡Œéœ€è¦è°ƒç”¨å®žé™…的处理接口
          await traceedit(submitData);
          // await new Promise((resolve) => setTimeout(resolve, 1000));
          this.$message.success("处理提交成功");
          this.processDialogVisible = false;
          this.loadExceptionList();
        } catch (error) {
          console.error("处理提交失败:", error);
          this.$message.error("处理提交失败,请稍后重试");
        } finally {
          this.processing = false;
        }
      });
    },
@@ -708,21 +924,111 @@
    // æäº¤æ‰¹é‡å¤„理
    async submitBatchProcess() {
      this.$refs.batchProcessForm.validate(async (valid) => {
        if (valid) {
          this.batchProcessing = true;
          try {
            // Mock API调用
            await new Promise((resolve) => setTimeout(resolve, 1500));
        if (!valid) {
          return;
        }
            this.$message.success(
              `已批量处理 ${this.selectedExceptionIds.length} æ¡å¼‚常反馈`
        this.batchProcessing = true;
        // æ˜¾ç¤ºè¿›åº¦æ¡
        this.batchProgress = {
          visible: true,
          percentage: 0,
          processed: 0,
          total: this.selectedExceptionIds.length,
        };
        try {
          // å‡†å¤‡æ‰¹é‡æäº¤æ•°æ®
          const processData = {
            handleFlag: this.batchProcessForm.handleFlag,
            handleresult: this.batchProcessForm.handleresult,
            handledesc: this.batchProcessForm.handledesc,
            ccdepts: Array.isArray(this.batchProcessForm.ccdepts)
              ? this.batchProcessForm.ccdepts.join(",")
              : this.batchProcessForm.ccdepts,
          };
          // æŽ§åˆ¶å¹¶å‘æ•°
          const CONCURRENT_LIMIT = 10; // åŒæ—¶æœ€å¤š3个请求
          const totalCount = this.selectedExceptionIds.length;
          const results = [];
          let successCount = 0;
          let failCount = 0;
          this.$message.info(`开始批量处理 ${totalCount} æ¡è®°å½•...`);
          // åˆ†ç»„处理
          for (
            let i = 0;
            i < this.selectedExceptionIds.length;
            i += CONCURRENT_LIMIT
          ) {
            const batchIds = this.selectedExceptionIds.slice(
              i,
              i + CONCURRENT_LIMIT
            );
            this.batchDialogVisible = false;
            this.selectedExceptionIds = [];
            this.loadExceptionList();
          } finally {
            this.batchProcessing = false;
            // å¹¶å‘处理当前批次
            const batchPromises = batchIds.map((id) =>
              traceedit({
                id: id,
                ...processData,
              })
                .then((result) => ({
                  id,
                  success: result && result.code === 200,
                  error: result?.msg,
                }))
                .catch((error) => ({
                  id,
                  success: false,
                  error: error.message,
                }))
            );
            const batchResults = await Promise.all(batchPromises);
            results.push(...batchResults);
            // æ›´æ–°ç»Ÿè®¡
            batchResults.forEach((result) => {
              if (result.success) {
                successCount++;
              } else {
                failCount++;
                console.error(`处理记录 ${result.id} å¤±è´¥:`, result.error);
              }
            });
            // æ›´æ–°è¿›åº¦
            this.batchProgress.processed = i + 1;
            this.batchProgress.percentage = Math.round(
              ((i + 1) / totalCount) * 100
            );
            // æ˜¾ç¤ºè¿›åº¦
            console.log(
              `进度: ${Math.min(
                i + CONCURRENT_LIMIT,
                totalCount
              )}/${totalCount}`
            );
          }
          // å¤„理结果提示
          if (successCount === totalCount) {
            this.$message.success(`已成功处理全部 ${totalCount} æ¡å¼‚常反馈`);
          } else {
            this.$message.warning(
              `已处理 ${successCount} æ¡ï¼Œå¤±è´¥ ${failCount} æ¡å¼‚常反馈`
            );
          }
          this.batchDialogVisible = false;
          this.selectedExceptionIds = [];
          this.loadExceptionList();
        } catch (error) {
          console.error("批量处理失败:", error);
          this.$message.error("批量处理失败,请稍后重试");
        } finally {
          this.batchProcessing = false;
          this.batchProgress.visible = false;
        }
      });
    },
@@ -738,27 +1044,6 @@
    handlePageChange(page) {
      this.filterParams.pageNum = page;
      this.loadExceptionList();
    },
    // æ–‡ä»¶ä¸Šä¼ ç›¸å…³æ–¹æ³•
    handlePreview(file) {
      console.log("预览文件:", file);
    },
    handleRemove(file, fileList) {
      console.log("移除文件:", file, fileList);
    },
    beforeRemove(file) {
      return this.$confirm(`确定移除 ${file.name}?`);
    },
    handleExceed(files, fileList) {
      this.$message.warning(
        `当前限制选择 3 ä¸ªæ–‡ä»¶ï¼Œæœ¬æ¬¡é€‰æ‹©äº† ${files.length} ä¸ªæ–‡ä»¶ï¼Œå…±é€‰æ‹©äº† ${
          files.length + fileList.length
        } ä¸ªæ–‡ä»¶`
      );
    },
  },
};
@@ -827,64 +1112,109 @@
      }
      .detail-content {
        font-size: 13px;
        color: #606266;
        line-height: 1.5;
        text-align: left;
        font-size: 12px;
        line-height: 1.5;
        .question-text {
          color: #303133;
          margin-bottom: 5px;
          font-weight: 500;
        }
        .answer-text {
          color: #f56c6c;
          margin-bottom: 5px;
        }
        .matched-text {
          color: #e6a23c;
          font-style: italic;
        }
        strong {
          color: #606266;
          font-weight: 600;
        }
      }
      .patient-info {
        .patient-item {
        .patient-row {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 5px;
          padding: 2px 0;
          margin-bottom: 8px;
          .label {
            font-size: 12px;
            color: #606266;
            min-width: 40px;
          &:last-child {
            margin-bottom: 0;
          }
          .value {
            font-size: 13px;
            color: #333;
            font-weight: 500;
            text-align: right;
          .patient-item {
            flex: 1;
            display: flex;
            justify-content: flex-start;
            align-items: center;
            padding: 0 5px;
            &.full-width {
              flex: 1 0 100%;
              margin-left: 0;
              margin-right: 0;
            }
            .label {
              font-size: 12px;
              color: #606266;
              margin-right: 5px;
              white-space: nowrap;
            }
            .value {
              font-size: 12px;
              color: #333;
              font-weight: 500;
              text-align: right;
              word-break: break-all;
            }
          }
        }
      }
      .discharge-info {
      .fill-info,
      .handle-info {
        font-size: 12px;
        .info-item {
          display: flex;
          justify-content: space-between;
          justify-content: flex-start;
          align-items: center;
          margin-bottom: 5px;
          padding: 2px 0;
          .label {
            font-size: 12px;
            color: #606266;
            min-width: 50px;
          }
          .value {
            font-size: 13px;
            color: #333;
            font-weight: 500;
            text-align: right;
            // text-align: right;
            flex: 1;
            &.time {
              font-size: 12px;
              color: #909399;
              font-size: 11px;
            }
          }
        }
      }
      .no-data {
        color: #909399;
        font-style: italic;
        font-size: 12px;
      }
    }
    .pagination-section {
src/views/Satisfaction/configurationmyd/components/DetailsAnomaly.vue
@@ -13,147 +13,280 @@
      <el-row :gutter="20">
        <el-col :span="8">
          <div class="info-item">
            <span class="label">患者姓名:</span>
            <span class="value">{{ currentRecord.patientName }}</span>
            <span class="label">问题内容:</span>
            <span class="value">{{ currentRecord.questiontext || '无' }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">性别:</span>
            <span class="value">{{ currentRecord.gender === 1 ? '男' : '女' }}</span>
            <span class="label">回答内容:</span>
            <span class="value">{{ currentRecord.asrtext || '无回答' }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">年龄:</span>
            <span class="value">{{ currentRecord.age }}岁</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">联系方式:</span>
            <span class="value">{{ currentRecord.phone }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">出院科室:</span>
            <span class="value">{{ currentRecord.dischargeDept }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">出院病区:</span>
            <span class="value">{{ currentRecord.dischargeWard }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">填写时间:</span>
            <span class="value">{{ currentRecord.fillTime }}</span>
            <span class="label">解析值:</span>
            <span class="value">{{ currentRecord.matchedtext || '无' }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">负责科室:</span>
            <el-tag type="primary">{{ currentRecord.responsibilityDept }}</el-tag>
            <el-tag v-if="currentRecord.todeptname" type="primary">{{ currentRecord.todeptname }}</el-tag>
            <span v-else class="value">未分配</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">处理状态:</span>
            <el-tag
              :type="getStatusTagType(currentRecord.processStatus)"
              :type="getStatusTagType(currentRecord.handleFlag)"
              effect="dark"
            >
              {{ getStatusText(currentRecord.processStatus) }}
              {{ getStatusText(currentRecord.handleFlag) }}
            </el-tag>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">模板类型:</span>
            <el-tag :type="currentRecord.templateType === 1 ? 'primary' : 'success'">
              {{ currentRecord.templateType === 1 ? '语音模板' : '问卷模板' }}
            </el-tag>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">创建时间:</span>
            <span class="value">{{ formatDateTime(currentRecord.createTime) }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">处理时间:</span>
            <span class="value">{{ currentRecord.handleTime ? formatDateTime(currentRecord.handleTime) : '未处理' }}</span>
          </div>
        </el-col>
        <el-col :span="8">
          <div class="info-item">
            <span class="label">处理人:</span>
            <span class="value">{{ currentRecord.handleBy || '未处理' }}</span>
          </div>
        </el-col>
        <el-col :span="8" v-if="currentRecord.handleresult">
          <div class="info-item">
            <span class="label">处理结果:</span>
            <span class="value">{{ getHandleresultText(currentRecord.handleresult) }}</span>
          </div>
        </el-col>
        <el-col :span="16" v-if="currentRecord.handledesc">
          <div class="info-item">
            <span class="label">处理说明:</span>
            <span class="value">{{ currentRecord.handledesc }}</span>
          </div>
        </el-col>
        <el-col :span="24" v-if="currentRecord.finaloption">
          <div class="info-item">
            <span class="label">最终意见:</span>
            <span class="value">{{ currentRecord.finaloption }}</span>
          </div>
        </el-col>
        <el-col :span="8" v-if="currentRecord.recordurl">
          <div class="info-item">
            <span class="label">录音地址:</span>
            <el-button
              type="text"
              size="small"
              icon="el-icon-headset"
              @click="handlePlayAudio(currentRecord.recordurl)"
            >
              æ’­æ”¾å½•音
            </el-button>
          </div>
        </el-col>
        <el-col :span="8" v-if="currentRecord.ccdepts">
          <div class="info-item">
            <span class="label">抄送科室:</span>
            <span class="value">{{ currentRecord.ccdepts }}</span>
          </div>
        </el-col>
      </el-row>
    </div>
    <!-- é—®å·è¯¦æƒ… -->
    <div class="questionnaire-section">
      <div class="section-title">问卷填写详情</div>
      <div class="questionnaire-content">
        <div class="question-item" v-for="(question, index) in questionnaireData" :key="index">
          <div class="question-header">
            <span class="question-index">{{ index + 1 }}.</span>
            <span class="question-text">{{ question.question }}</span>
            <el-tag
              size="mini"
              :type="question.type === 1 ? 'primary' : 'success'"
              class="question-type"
            >
              {{ question.type === 1 ? '单选题' : '多选题' }}
            </el-tag>
          </div>
          <div class="question-options">
            <el-radio-group
              v-model="question.answer"
              v-if="question.type === 1"
              disabled
            >
              <el-radio
                v-for="option in question.options"
                :key="option.value"
                :label="option.value"
                :class="{ 'unsatisfactory-option': isUnsatisfactoryOption(option.value) }"
    <!-- é—®å·/语音详情 -->
    <div class="content-container" v-if="templateData.length > 0">
      <el-tabs v-model="activeName" type="border-card">
        <!-- é—®å·éšè®¿è¯¦æƒ… -->
        <el-tab-pane name="wj" v-if="currentRecord.templateType === 2">
          <span slot="label"><i class="el-icon-notebook-1"></i> é—®å·éšè®¿è¯¦æƒ…</span>
          <div class="CONTENT">
            <div class="title">{{ currentRecord.questiontext || '问卷详情' }}</div>
            <div class="preview-left" v-if="!isVoiceTemplate">
              <div
                class="topic-dev"
                v-for="(item, index) in templateData"
                :key="item.id"
              >
                {{ option.text }}
              </el-radio>
            </el-radio-group>
            <el-checkbox-group
              v-model="question.answer"
              v-else
              disabled
            >
              <el-checkbox
                v-for="option in question.options"
                :key="option.value"
                :label="option.value"
                :class="{ 'unsatisfactory-option': isUnsatisfactoryOption(option.value) }"
              >
                {{ option.text }}
              </el-checkbox>
            </el-checkbox-group>
                <!-- å•选 -->
                <div
                  :class="getTopicClass(item)"
                  :key="index"
                  v-if="item.scriptType == 1 && !item.astrict"
                >
                  <div class="dev-text">
                    {{ index + 1 }}、[单选]<span>{{ item.scriptContent }}</span>
                  </div>
                  <div class="dev-xx">
                    <el-radio-group v-model="item.scriptResult" disabled>
                      <el-radio
                        v-for="(option, optionIndex) in item.svyTaskTemplateTargetoptions"
                        :class="getOptionClass(option)"
                        :key="optionIndex"
                        :label="option.optioncontent"
                      >{{ option.optioncontent }}</el-radio>
                    </el-radio-group>
                  </div>
                  <div
                    v-if="item.showAppendInput || item.answerps"
                    class="append-input-container"
                  >
                    <el-input
                      type="textarea"
                      :rows="2"
                      placeholder="请输入具体信息"
                      v-model="item.answerps"
                      readonly
                    ></el-input>
                  </div>
                  <div v-show="item.prompt">
                    <el-alert :title="item.prompt" type="warning"></el-alert>
                  </div>
                </div>
                <!-- å¤šé€‰ -->
                <div
                  :class="item.isabnormal ? 'scriptTopic-isabnormal' : 'scriptTopic-dev'"
                  :key="index"
                  v-if="item.scriptType == 2 && !item.astrict"
                >
                  <div class="dev-text">
                    {{ index + 1 }}、[多选]<span>{{ item.scriptContent }}</span>
                  </div>
                  <div class="dev-xx">
                    <el-checkbox-group v-model="item.scriptResult" disabled>
                      <el-checkbox
                        :class="option.isabnormal ? 'red-star' : ''"
                        v-for="(option, optionIndex) in item.svyTaskTemplateTargetoptions"
                        :key="optionIndex"
                        :label="option.optioncontent"
                      >
                        {{ option.optioncontent }}
                      </el-checkbox>
                    </el-checkbox-group>
                  </div>
                  <div v-show="item.prompt && item.scriptResult[0]">
                    <el-alert :title="item.prompt" type="warning"></el-alert>
                  </div>
                </div>
                <!-- å¡«ç©º -->
                <div
                  class="scriptTopic-dev"
                  :key="index"
                  v-if="item.scriptType == 4 && !item.astrict"
                >
                  <div class="dev-text">
                    {{ index + 1 }}、[问答]<span>{{ item.scriptContent }}</span>
                    <span v-if="item.valueType == 3">(只能输入数字)</span>
                  </div>
                  <div class="dev-xx" v-if="item.valueType == 3">
                    <el-input
                      type="text"
                      placeholder="请输入答案"
                      v-model="item.scriptResult"
                      readonly
                    ></el-input>
                  </div>
                  <div class="dev-xx" v-else>
                    <el-input
                      type="textarea"
                      :rows="2"
                      placeholder="请输入答案"
                      v-model="item.scriptResult"
                      readonly
                    ></el-input>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div v-if="question.additional" class="additional-remark">
            <div class="remark-label">补充说明:</div>
            <div class="remark-content">{{ question.additional }}</div>
        </el-tab-pane>
        <!-- è¯­éŸ³éšè®¿è¯¦æƒ… -->
        <el-tab-pane name="yy" v-if="currentRecord.templateType === 1">
          <span slot="label"><i class="el-icon-headset"></i> è¯­éŸ³éšè®¿è¯¦æƒ…</span>
          <div class="borderdiv">
            <div class="title">{{ taskName }}</div>
            <div class="voice-audio" v-if="voiceAudioUrl">
              å®Œæ•´è¯­éŸ³ï¼š
              <audio-player
                :audio-source="voiceAudioUrl"
              ></audio-player>
            </div>
            <div class="preview-left" v-if="voiceData.length > 0">
              <div v-for="(item, index) in voiceData" :key="index">
                <div class="leftside">
                  <i class="el-icon-phone-outline"></i>
                  <span>{{ item.questiontext || '问题内容' }}</span>
                </div>
                <div class="offside">
                  <i class="el-icon-user"></i>
                  <div class="offside-value">
                    <el-input
                      type="textarea"
                      :autosize="{ minRows: 1 }"
                      v-model="item.asrtext"
                      readonly
                    ></el-input>
                    <div v-if="item.questionvoice">
                      <audio-player
                        :audio-source="item.questionvoice"
                      ></audio-player>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
        </el-tab-pane>
      </el-tabs>
    </div>
    <!-- å¤„理记录 -->
    <div class="process-section">
    <div class="process-section" v-if="processRecords.length > 0">
      <div class="section-title">处理记录</div>
      <div class="process-timeline" v-if="processRecords.length > 0">
      <div class="process-timeline">
        <el-timeline>
          <el-timeline-item
            v-for="(record, index) in processRecords"
            :key="index"
            :timestamp="record.time"
            :timestamp="formatDateTime(record.handleTime || record.createTime)"
            placement="top"
          >
            <el-card>
              <div class="process-item">
                <div class="process-header">
                  <span class="process-user">{{ record.user }}</span>
                  <span class="process-user">{{ record.handleBy || '系统' }}</span>
                  <el-tag
                    size="small"
                    :type="getStatusTagType(record.status)"
                    :type="getStatusTagType(record.handleFlag)"
                  >
                    {{ getStatusText(record.status) }}
                    {{ getStatusText(record.handleFlag) }}
                  </el-tag>
                </div>
                <div class="process-content">
                  <div v-if="record.reportDepts && record.reportDepts.length > 0" class="process-depts">
                    <span class="label">报备科室:</span>
                  <div v-if="record.ccdepts" class="process-depts">
                    <span class="label">抄送科室:</span>
                    <el-tag
                      v-for="dept in record.reportDepts"
                      v-for="dept in getDeptArray(record.ccdepts)"
                      :key="dept"
                      size="small"
                      type="info"
@@ -162,31 +295,23 @@
                      {{ dept }}
                    </el-tag>
                  </div>
                  <div v-if="record.remark" class="process-remark">
                    <span class="label">处理备注:</span>
                    <span class="content">{{ record.remark }}</span>
                  <div v-if="record.handleresult" class="process-remark">
                    <span class="label">处理结果:</span>
                    <span class="content">{{ getHandleresultText(record.handleresult) }}</span>
                  </div>
                  <div v-if="record.attachments && record.attachments.length > 0" class="process-attachments">
                    <span class="label">附件:</span>
                    <el-button
                      v-for="file in record.attachments"
                      :key="file.id"
                      type="text"
                      size="small"
                      icon="el-icon-document"
                      @click="handlePreviewFile(file)"
                    >
                      {{ file.name }}
                    </el-button>
                  <div v-if="record.handledesc" class="process-remark">
                    <span class="label">处理说明:</span>
                    <span class="content">{{ record.handledesc }}</span>
                  </div>
                  <div v-if="record.finaloption" class="process-remark">
                    <span class="label">最终意见:</span>
                    <span class="content">{{ record.finaloption }}</span>
                  </div>
                </div>
              </div>
            </el-card>
          </el-timeline-item>
        </el-timeline>
      </div>
      <div v-else class="no-record">
        æš‚无处理记录
      </div>
    </div>
@@ -195,7 +320,7 @@
        type="primary"
        icon="el-icon-edit"
        @click="handleProcess"
        v-if="currentRecord.processStatus !== 2"
        v-if="currentRecord.handleFlag !== '1'"
      >
        å¤„理异常
      </el-button>
@@ -217,61 +342,72 @@
        label-width="100px"
        size="medium"
      >
        <el-form-item label="处理状态" prop="status">
        <el-form-item label="处理状态" prop="handleFlag">
          <el-select
            v-model="processForm.status"
            v-model="processForm.handleFlag"
            placeholder="请选择处理状态"
            style="width: 100%"
          >
            <el-option label="处理中" :value="1" />
            <el-option label="已处理" :value="2" />
            <el-option label="已驳回" :value="3" />
            <el-option label="已处理" :value="'1'" />
            <el-option label="取消处理" :value="'0'" />
          </el-select>
        </el-form-item>
        <el-form-item label="报备科室" prop="reportDepts">
        <el-form-item label="抄送科室" prop="ccdepts">
          <el-select
            v-model="processForm.reportDepts"
            placeholder="请选择报备科室"
            v-model="processForm.ccdepts"
            placeholder="请选择抄送科室"
            multiple
            filterable
            collapse-tags
            style="width: 100%"
            :disabled="processForm.handleFlag !== '1'"
          >
            <el-option
              v-for="dept in deptList"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
              :key="dept.deptCode"
              :label="dept.label"
              :value="dept.deptCode"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="处理备注" prop="remark">
        <el-form-item label="处理结果" prop="handleresult">
          <el-select
            v-model="processForm.handleresult"
            placeholder="请选择处理结果"
            style="width: 100%"
            :disabled="processForm.handleFlag !== '1'"
          >
            <el-option label="已解决" value="resolved" />
            <el-option label="已解释" value="explained" />
            <el-option label="已转交" value="transferred" />
            <el-option label="需改进" value="improvement" />
            <el-option label="已驳回" value="rejected" />
          </el-select>
        </el-form-item>
        <el-form-item label="处理说明" prop="handledesc">
          <el-input
            v-model="processForm.remark"
            v-model="processForm.handledesc"
            type="textarea"
            :rows="4"
            placeholder="请输入处理备注(最多500字)"
            placeholder="请输入处理说明(最多500字)"
            maxlength="500"
            show-word-limit
            :disabled="processForm.handleFlag !== '1'"
          />
        </el-form-item>
        <el-form-item label="附件上传">
          <el-upload
            class="upload-demo"
            action="#"
            :on-preview="handleFilePreview"
            :on-remove="handleFileRemove"
            :before-remove="beforeFileRemove"
            :limit="3"
            :on-exceed="handleFileExceed"
            :file-list="fileList"
          >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">支持上传图片、文档等附件,单个文件不超过10MB</div>
          </el-upload>
        <el-form-item label="最终意见" prop="finaloption" v-if="hasQualityPermission">
          <el-input
            v-model="processForm.finaloption"
            type="textarea"
            :rows="3"
            placeholder="请输入最终处理意见(最多300字)"
            maxlength="300"
            show-word-limit
          />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
@@ -285,24 +421,42 @@
        </el-button>
      </span>
    </el-dialog>
    <!-- å½•音播放器 -->
    <audio
      v-if="audioUrl"
      :src="audioUrl"
      ref="audioPlayer"
      controls
      style="display: none"
    />
  </el-dialog>
</template>
<script>
import { traceedit } from "@/api/AiCentre/index";
import { getsearchrResults, getPersonVoices, getTaskservelist } from "@/api/AiCentre/index";
import { deptTreeSelect } from "@/api/system/user";
import AudioPlayer from "@/components/AudioPlayer"; // éœ€è¦åˆ›å»ºè¿™ä¸ªéŸ³é¢‘播放组件
export default {
  name: 'ExceptionDetailDialog',
  components: {
    AudioPlayer
  },
  props: {
    // æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
    visible: {
      type: Boolean,
      default: false
    },
    // è®°å½•ID
    recordId: {
      type: [Number, String],
      default: null
    },
    // å¯¹è¯æ¡†æ ‡é¢˜
    recordData: {
      type: Object,
      default: () => ({})
    },
    title: {
      type: String,
      default: '异常反馈详情'
@@ -313,45 +467,71 @@
      // å½“前记录
      currentRecord: {},
      // é—®å·æ•°æ®
      questionnaireData: [],
      // é—®å·/语音数据
      activeName: 'wj',
      taskName: '',
      templateData: [],
      voiceData: [],
      voiceAudioUrl: '',
      // å¤„理记录
      processRecords: [],
      // ç§‘室列表
      deptList: [
        { id: 1, name: '心血管内科' },
        { id: 2, name: '神经内科' },
        { id: 3, name: '普外科' },
        { id: 4, name: '骨科' },
        { id: 5, name: '妇产科' },
        { id: 6, name: '儿科' },
        { id: 7, name: '急诊科' },
        { id: 8, name: '呼吸内科' }
      ],
      deptList: [],
      // å¤„理对话框
      processDialogVisible: false,
      processing: false,
      processForm: {
        status: '',
        reportDepts: [],
        remark: ''
        handleFlag: '',
        ccdepts: [],
        handleresult: '',
        handledesc: '',
        finaloption: ''
      },
      processRules: {
        status: [
        handleFlag: [
          { required: true, message: '请选择处理状态', trigger: 'change' }
        ],
        remark: [
          { required: true, message: '请输入处理备注', trigger: 'blur' },
          { min: 5, max: 500, message: '备注长度在 5 åˆ° 500 ä¸ªå­—符', trigger: 'blur' }
        handleresult: [
          {
            required: true,
            message: '请选择处理结果',
            trigger: 'change',
            validator: (rule, value, callback) => {
              if (this.processForm.handleFlag === '1' && !value) {
                callback(new Error('请选择处理结果'));
              } else {
                callback();
              }
            }
          }
        ],
        handledesc: [
          {
            required: true,
            message: '请输入处理说明',
            trigger: 'blur',
            validator: (rule, value, callback) => {
              if (this.processForm.handleFlag === '1' && (!value || value.trim().length < 3)) {
                callback(new Error('处理说明至少3个字符'));
              } else {
                callback();
              }
            }
          }
        ]
      },
      fileList: [],
      // éŸ³é¢‘播放
      audioUrl: '',
      // åŠ è½½çŠ¶æ€
      loading: false
      loading: false,
      // æƒé™æŽ§åˆ¶
      hasQualityPermission: false
    };
  },
@@ -363,6 +543,10 @@
      set(val) {
        this.$emit('update:visible', val);
      }
    },
    isVoiceTemplate() {
      return this.currentRecord.templateType === 1;
    }
  },
@@ -370,237 +554,293 @@
    visible: {
      immediate: true,
      handler(val) {
        if (val && this.recordId) {
        if (val) {
          this.loadData();
        } else {
          this.resetData();
        }
      }
    },
    recordData: {
      immediate: true,
      handler(val) {
        if (val && Object.keys(val).length > 0) {
          this.currentRecord = { ...val };
        }
      }
    }
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
    // æ ¼å¼åŒ–日期时间
    formatDateTime(dateTime) {
      if (!dateTime) return '';
      try {
        await Promise.all([
          this.loadRecordDetail(),
          this.loadQuestionnaireData(),
          this.loadProcessRecords()
        ]);
      } finally {
        this.loading = false;
        const date = new Date(dateTime);
        if (isNaN(date.getTime())) {
          return dateTime;
        }
        return date.toLocaleDateString().replace(/\//g, '-') + ' ' +
               date.toTimeString().split(' ')[0];
      } catch (error) {
        console.error('日期格式化错误:', error);
        return dateTime;
      }
    },
    // åŠ è½½è®°å½•è¯¦æƒ…
    async loadRecordDetail() {
      return new Promise(resolve => {
        setTimeout(() => {
          // æ ¹æ®ä¸åŒçš„recordId返回不同的mock数据
          const mockRecords = {
            1: {
              id: 1,
              patientName: '张先生',
              gender: 1,
              age: 45,
              phone: '13800138000',
              dischargeDept: '心血管内科',
              dischargeWard: '内科一病区',
              fillTime: '2024-01-15 10:30:25',
              responsibilityDept: '心血管内科',
              processStatus: 0
            },
            2: {
              id: 2,
              patientName: '李女士',
              gender: 0,
              age: 38,
              phone: '13900139000',
              dischargeDept: '神经内科',
              dischargeWard: '内科二病区',
              fillTime: '2024-01-14 16:20:10',
              responsibilityDept: '神经内科',
              processStatus: 0
            },
            3: {
              id: 3,
              patientName: '王先生',
              gender: 1,
              age: 52,
              phone: '13700137000',
              dischargeDept: '普外科',
              dischargeWard: '外科一病区',
              fillTime: '2024-01-13 09:15:45',
              responsibilityDept: '普外科',
              processStatus: 1
            }
          };
          this.currentRecord = mockRecords[this.recordId] || {
            id: 1,
            patientName: '张先生',
            gender: 1,
            age: 45,
            phone: '13800138000',
            dischargeDept: '心血管内科',
            dischargeWard: '内科一病区',
            fillTime: '2024-01-15 10:30:25',
            responsibilityDept: '心血管内科',
            processStatus: 0
          };
          resolve();
        }, 300);
      });
    // æ£€æŸ¥è´¨ç®¡æƒé™
    checkQualityPermission() {
      const userRoles = this.$store.getters.roles || [];
      return userRoles.includes('quality_manager') || userRoles.includes('admin');
    },
    // åŠ è½½é—®å·æ•°æ®
    async loadQuestionnaireData() {
      return new Promise(resolve => {
        setTimeout(() => {
          this.questionnaireData = [
            {
              question: '您对医护人员的服务态度是否满意?',
              type: 1,
              options: [
                { value: '非常满意', text: '非常满意' },
                { value: '满意', text: '满意' },
                { value: '一般', text: '一般' },
                { value: '不满意', text: '不满意' },
                { value: '非常不满意', text: '非常不满意' }
              ],
              answer: '不满意',
              additional: '医生查房时间太短,沟通不够充分,对病情解释不够详细'
            },
            {
              question: '您对医生的诊疗水平和技术能力评价如何?',
              type: 1,
              options: [
                { value: '非常专业', text: '非常专业' },
                { value: '比较专业', text: '比较专业' },
                { value: '一般', text: '一般' },
                { value: '不够专业', text: '不够专业' },
                { value: '非常不专业', text: '非常不专业' }
              ],
              answer: '比较专业',
              additional: ''
            },
            {
              question: '您对医院的环境和卫生状况是否满意?',
              type: 1,
              options: [
                { value: '非常满意', text: '非常满意' },
                { value: '满意', text: '满意' },
                { value: '一般', text: '一般' },
                { value: '不满意', text: '不满意' },
                { value: '非常不满意', text: '非常不满意' }
              ],
              answer: '一般',
              additional: ''
            },
            {
              question: '您认为医护人员与您的沟通是否充分?',
              type: 1,
              options: [
                { value: '非常充分', text: '非常充分' },
                { value: '比较充分', text: '比较充分' },
                { value: '一般', text: '一般' },
                { value: '不够充分', text: '不够充分' },
                { value: '非常不充分', text: '非常不充分' }
              ],
              answer: '不够充分',
              additional: '医生讲解病情时语速太快,没有给足够的时间提问'
            },
            {
              question: '您对等待就诊和治疗的时间是否满意?',
              type: 1,
              options: [
                { value: '非常满意', text: '非常满意' },
                { value: '满意', text: '满意' },
                { value: '一般', text: '一般' },
                { value: '不满意', text: '不满意' },
                { value: '非常不满意', text: '非常不满意' }
              ],
              answer: '不满意',
              additional: '预约的9点,实际10点才见到医生'
            }
          ];
          resolve();
        }, 300);
      });
    // èŽ·å–ç§‘å®¤åˆ—è¡¨
    async getDeptOptions() {
      try {
        const res = await deptTreeSelect();
        if (res.code == 200) {
          this.deptList = this.flattenArray(res.data) || [];
        }
      } catch (error) {
        console.error('获取科室列表失败:', error);
      }
    },
    // åŠ è½½å¤„ç†è®°å½•
    async loadProcessRecords() {
      return new Promise(resolve => {
        setTimeout(() => {
          this.processRecords = [
            {
              id: 1,
              time: '2024-01-15 14:20:30',
              user: '张医生',
              status: 1, // å¤„理中
              reportDepts: ['医务科', '护理部'],
              remark: '已收到反馈,正在安排相关人员核查情况',
              attachments: [
                { id: 1, name: '初步调查记录.docx' },
                { id: 2, name: '患者沟通记录.jpg' }
              ]
            },
            {
              id: 2,
              time: '2024-01-15 10:45:12',
              user: '系统',
              status: 0, // å¾…处理
              remark: '系统自动识别为异常反馈,已分配到责任科室',
              attachments: []
            }
          ];
          resolve();
        }, 300);
      });
    },
    // å±•平数组
    flattenArray(multiArray) {
      let result = [];
    // åˆ¤æ–­æ˜¯å¦ä¸ºä¸æ»¡æ„é€‰é¡¹
    isUnsatisfactoryOption(value) {
      const unsatisfactoryValues = [
        '不满意',
        '非常不满意',
        '不够专业',
        '非常不专业',
        '不够充分',
        '非常不充分'
      ];
      return unsatisfactoryValues.includes(value);
      function flatten(element) {
        if (element.children && element.children.length > 0) {
          element.children.forEach((child) => flatten(child));
        } else {
          let item = JSON.parse(JSON.stringify(element));
          result.push(item);
        }
      }
      multiArray.forEach((element) => flatten(element));
      return result;
    },
    // èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
    getStatusTagType(status) {
      switch (status) {
        case 0: return 'warning'; // å¾…处理
        case 1: return 'primary'; // å¤„理中
        case 2: return 'success'; // å·²å¤„理
        case 3: return 'danger';  // å·²é©³å›ž
    getStatusTagType(handleFlag) {
      switch (handleFlag) {
        case '0': return 'warning'; // æœªå¤„理
        case '1': return 'success'; // å·²å¤„理
        default: return 'info';
      }
    },
    // èŽ·å–çŠ¶æ€æ–‡æœ¬
    getStatusText(status) {
      switch (status) {
        case 0: return '待处理';
        case 1: return '处理中';
        case 2: return '已处理';
        case 3: return '已驳回';
    getStatusText(handleFlag) {
      switch (handleFlag) {
        case '0': return '未处理';
        case '1': return '已处理';
        default: return '未知';
      }
    },
    // èŽ·å–å¤„ç†ç»“æžœæ–‡æœ¬
    getHandleresultText(handleresult) {
      const map = {
        'resolved': '已解决',
        'explained': '已解释',
        'transferred': '已转交',
        'improvement': '需改进',
        'rejected': '已驳回'
      };
      return map[handleresult] || handleresult;
    },
    // èŽ·å–ç§‘å®¤æ•°ç»„
    getDeptArray(ccdepts) {
      if (!ccdepts) return [];
      return ccdepts.split(',');
    },
    // æ’­æ”¾éŸ³é¢‘
    handlePlayAudio(url) {
      this.audioUrl = url;
      this.$nextTick(() => {
        const audioPlayer = this.$refs.audioPlayer;
        if (audioPlayer) {
          audioPlayer.play().catch(error => {
            console.error('播放失败:', error);
            this.$message.error('音频播放失败');
          });
        }
      });
    },
    // èŽ·å–ä¸»é¢˜æ ·å¼ç±»
    getTopicClass(item) {
      if (item.isabnormal == 1) {
        return "scriptTopic-isabnormal";
      } else if (item.isabnormal == 2) {
        return "scriptTopic-warning";
      } else {
        return "scriptTopic-dev";
      }
    },
    // èŽ·å–é€‰é¡¹æ ·å¼ç±»
    getOptionClass(items) {
      if (items.isabnormal == 1) {
        return "red-star";
      } else if (items.isabnormal == 2) {
        return "yellow-star";
      }
      return "";
    },
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        this.hasQualityPermission = this.checkQualityPermission();
        await this.getDeptOptions();
        if (Object.keys(this.currentRecord).length === 0) {
          this.currentRecord = this.recordData || {};
        }
        // å¦‚果当前记录是语音模板,加载语音数据
        if (this.currentRecord.templateType === 1) {
          await this.loadVoiceData();
          this.activeName = 'yy';
        } else if (this.currentRecord.templateType === 2) {
          await this.loadQuestionnaireData();
          this.activeName = 'wj';
        }
        await this.loadProcessRecords();
      } catch (error) {
        console.error('加载详情数据失败:', error);
        this.$message.error('加载数据失败');
      } finally {
        this.loading = false;
      }
    },
    // é‡ç½®æ•°æ®
    resetData() {
      this.currentRecord = {};
      this.templateData = [];
      this.voiceData = [];
      this.processRecords = [];
      this.voiceAudioUrl = '';
      this.taskName = '';
      this.activeName = 'wj';
    },
    // åŠ è½½é—®å·æ•°æ®
    async loadQuestionnaireData() {
      try {
        // è¿™é‡Œéœ€è¦æ ¹æ®å®žé™…情况调用接口获取问卷数据
        // å¦‚æžœrecordData中已经包含了问卷数据,可以直接使用
        if (this.currentRecord.taskid && this.currentRecord.patid) {
          const params = {
            taskid: this.currentRecord.taskid,
            patid: this.currentRecord.patid,
            subId: this.currentRecord.subId || this.currentRecord.id,
            isFinish: true
          };
          const res = await getsearchrResults(params);
          if (res.code === 200 && res.data) {
            this.templateData = res.data.scriptResult || [];
            this.taskName = res.data.taskName || '';
            // å¤„理数据格式
            this.templateData.forEach((item) => {
              if (item.scriptType == 2) item.scriptResult = [];
              if (item.scriptResultId && item.scriptType != 2) {
                item.isoption = 3;
                item.scriptResult = item.scriptResult;
              } else if (item.scriptResultId && item.scriptType == 2) {
                item.scriptResult = item.scriptResult.split("&");
                item.isoption = 3;
              }
            });
            this.overdata();
          }
        }
      } catch (error) {
        console.error('加载问卷数据失败:', error);
      }
    },
    // å¤„理异常数据
    overdata() {
      this.templateData.forEach((item, index) => {
        var obj = item.svyTaskTemplateTargetoptions.find(
          (items) => items.optioncontent == item.scriptResult
        );
        if (obj && obj.isabnormal) {
          this.templateData[index].isabnormal = obj.isabnormal;
        }
        this.$forceUpdate();
      });
    },
    // åŠ è½½è¯­éŸ³æ•°æ®
    async loadVoiceData() {
      try {
        if (this.currentRecord.taskid && this.currentRecord.patid) {
          const params = {
            taskid: this.currentRecord.taskid,
            patid: this.currentRecord.patid,
            subId: this.currentRecord.subId || this.currentRecord.id
          };
          const res = await getPersonVoices(params);
          if (res.code == 200) {
            this.voiceData = res.data.serviceSubtaskDetails || [];
            this.voiceAudioUrl = res.data.voice || '';
            this.taskName = res.data.taskName || '';
            this.templateData = res.data.filteredDetails || [];
            this.templateData.forEach((item) => {
              if (item.targetvalue) {
                item.scriptResult = item.targetvalue.split("&");
              } else {
                item.scriptResult = [];
              }
            });
          }
        }
      } catch (error) {
        console.error('加载语音数据失败:', error);
      }
    },
    // åŠ è½½å¤„ç†è®°å½•
    async loadProcessRecords() {
      try {
        // è¿™é‡Œå¯ä»¥æ ¹æ®recordId加载处理历史
        // æš‚时使用当前记录的处理信息
        if (this.currentRecord.handleTime) {
          this.processRecords = [{
            ...this.currentRecord,
            time: this.currentRecord.handleTime
          }];
        }
      } catch (error) {
        console.error('加载处理记录失败:', error);
      }
    },
    // å¤„理异常
    handleProcess() {
      this.processForm = {
        status: this.currentRecord.processStatus === 0 ? 1 : this.currentRecord.processStatus,
        reportDepts: [],
        remark: ''
        handleFlag: this.currentRecord.handleFlag === '0' ? '1' : '0',
        ccdepts: this.currentRecord.ccdepts ? this.currentRecord.ccdepts.split(',') : [],
        handleresult: this.currentRecord.handleresult || '',
        handledesc: this.currentRecord.handledesc || '',
        finaloption: this.currentRecord.finaloption || ''
      };
      this.processDialogVisible = true;
    },
@@ -608,52 +848,56 @@
    // æäº¤å¤„理
    async submitProcess() {
      this.$refs.processForm.validate(async (valid) => {
        if (valid) {
          this.processing = true;
          try {
            // Mock API调用
            await new Promise(resolve => setTimeout(resolve, 1000));
        if (!valid) {
          return;
        }
            this.$message.success('处理提交成功');
        this.processing = true;
        try {
          const submitData = {
            id: this.currentRecord.id,
            handleFlag: this.processForm.handleFlag,
            handleresult: this.processForm.handleresult,
            handledesc: this.processForm.handledesc,
            finaloption: this.processForm.finaloption,
            ccdepts: Array.isArray(this.processForm.ccdepts)
              ? this.processForm.ccdepts.join(",")
              : this.processForm.ccdepts
          };
          const res = await traceedit(submitData);
          if (res.code === 200) {
            this.$message.success("处理提交成功");
            this.processDialogVisible = false;
            // é‡æ–°åŠ è½½æ•°æ®
            await this.loadData();
            // æ›´æ–°å½“前记录
            this.currentRecord = {
              ...this.currentRecord,
              ...submitData,
              handleBy: this.$store.getters.name, // å½“前用户
              handleTime: new Date().toISOString().replace('T', ' ').substr(0, 19)
            };
            // é‡æ–°åŠ è½½å¤„ç†è®°å½•
            await this.loadProcessRecords();
            // è§¦å‘父组件刷新
            this.$emit('processed');
          } finally {
            this.processing = false;
          } else {
            this.$message.error(res.msg || "处理提交失败");
          }
        } catch (error) {
          console.error("处理提交失败:", error);
          this.$message.error("处理提交失败,请稍后重试");
        } finally {
          this.processing = false;
        }
      });
    },
    // é¢„览文件
    handlePreviewFile(file) {
      this.$message.info(`预览文件: ${file.name}`);
    },
    // å¤„理对话框关闭
    handleClose() {
      this.$emit('close');
    },
    // æ–‡ä»¶ä¸Šä¼ ç›¸å…³æ–¹æ³•
    handleFilePreview(file) {
      console.log('预览文件:', file);
    },
    handleFileRemove(file, fileList) {
      console.log('移除文件:', file, fileList);
    },
    beforeFileRemove(file) {
      return this.$confirm(`确定移除 ${file.name}?`);
    },
    handleFileExceed(files, fileList) {
      this.$message.warning(`当前限制选择 3 ä¸ªæ–‡ä»¶ï¼Œæœ¬æ¬¡é€‰æ‹©äº† ${files.length} ä¸ªæ–‡ä»¶ï¼Œå…±é€‰æ‹©äº† ${files.length + fileList.length} ä¸ªæ–‡ä»¶`);
    }
  }
};
@@ -705,116 +949,137 @@
        font-size: 14px;
        color: #303133;
        font-weight: 500;
        flex: 1;
      }
    }
  }
  .questionnaire-section {
  .content-container {
    margin-bottom: 20px;
    padding: 20px;
    background: #fff;
    border-radius: 8px;
    border: 1px solid #ebeef5;
    .section-title {
      font-size: 16px;
      font-weight: 600;
      color: #303133;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 2px solid #409EFF;
    ::v-deep .el-tabs__content {
      padding: 20px;
      background: #fff;
      border-radius: 0 0 4px 4px;
    }
    .questionnaire-content {
      .question-item {
        margin-bottom: 20px;
        padding: 15px;
        border-radius: 6px;
        border: 1px solid #ebeef5;
        transition: all 0.3s;
    .CONTENT, .borderdiv {
      padding: 20px;
      background: #fff;
      border-radius: 6px;
        &:hover {
          border-color: #409EFF;
          box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.1);
      .title {
        font-size: 18px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 20px;
        padding-bottom: 10px;
        border-bottom: 2px solid #409EFF;
      }
      .voice-audio {
        margin-bottom: 20px;
        display: flex;
        align-items: center;
        gap: 10px;
        .audio-player {
          flex: 1;
        }
      }
      .preview-left {
        .topic-dev {
          margin-bottom: 20px;
          padding: 15px;
          border-radius: 6px;
          border: 1px solid #ebeef5;
          background: #fff;
          .dev-text {
            font-size: 15px;
            font-weight: 500;
            color: #303133;
            margin-bottom: 15px;
            line-height: 1.5;
            span {
              color: #606266;
            }
          }
          .dev-xx {
            ::v-deep .el-radio-group,
            ::v-deep .el-checkbox-group {
              display: flex;
              flex-direction: column;
              gap: 10px;
            }
            ::v-deep .el-radio,
            ::v-deep .el-checkbox {
              margin: 0;
              padding: 8px 12px;
              border-radius: 4px;
              border: 1px solid #ebeef5;
              transition: all 0.3s;
              &:hover {
                background: #f5f7fa;
              }
              &.red-star {
                border-color: #f56c6c;
                background: #fef0f0;
              }
              &.yellow-star {
                border-color: #e6a23c;
                background: #fdf6ec;
              }
            }
          }
          .append-input-container {
            margin-top: 15px;
          }
          .el-alert {
            margin-top: 10px;
          }
        }
        .question-header {
        .leftside {
          display: flex;
          align-items: center;
          margin-bottom: 15px;
          padding-bottom: 10px;
          border-bottom: 1px dashed #dcdfe6;
          gap: 10px;
          margin-bottom: 10px;
          font-size: 15px;
          font-weight: 500;
          color: #303133;
          .question-index {
            font-weight: 600;
          i {
            color: #409EFF;
            margin-right: 8px;
            font-size: 15px;
          }
        }
        .offside {
          display: flex;
          align-items: flex-start;
          gap: 10px;
          margin-bottom: 20px;
          i {
            color: #67C23A;
            margin-top: 8px;
          }
          .question-text {
          .offside-value {
            flex: 1;
            font-size: 15px;
            color: #303133;
            font-weight: 500;
            line-height: 1.5;
          }
          .question-type {
            margin-left: 10px;
          }
        }
        .question-options {
          ::v-deep .el-radio-group {
            display: flex;
            flex-direction: column;
            gap: 10px;
          }
          ::v-deep .el-checkbox-group {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
          }
          ::v-deep .el-radio,
          ::v-deep .el-checkbox {
            margin: 0;
            padding: 8px 12px;
            border-radius: 4px;
            border: 1px solid #ebeef5;
            transition: all 0.3s;
            &:hover {
              background: #f5f7fa;
            .el-textarea {
              margin-bottom: 10px;
            }
            &.unsatisfactory-option {
              border-color: #e6a23c;
              background: #fdf6ec;
            }
          }
        }
        .additional-remark {
          margin-top: 15px;
          padding: 12px;
          background: #f0f9ff;
          border-radius: 6px;
          border-left: 4px solid #409EFF;
          .remark-label {
            font-size: 13px;
            color: #606266;
            font-weight: 500;
            margin-bottom: 5px;
          }
          .remark-content {
            font-size: 14px;
            color: #303133;
            line-height: 1.6;
          }
        }
      }
@@ -886,30 +1151,8 @@
              line-height: 1.5;
            }
          }
          .process-attachments {
            .label {
              font-size: 13px;
              color: #606266;
              margin-right: 5px;
            }
            ::v-deep .el-button {
              margin-right: 8px;
              margin-bottom: 5px;
            }
          }
        }
      }
    }
    .no-record {
      text-align: center;
      padding: 40px 0;
      color: #909399;
      font-style: italic;
      background: #f8f9fa;
      border-radius: 6px;
    }
  }
@@ -920,4 +1163,15 @@
    gap: 10px;
  }
}
// å¼‚常样式
.scriptTopic-isabnormal {
  border-color: #f56c6c !important;
  background: #fef0f0 !important;
}
.scriptTopic-warning {
  border-color: #e6a23c !important;
  background: #fdf6ec !important;
}
</style>
src/views/Satisfaction/configurationmyd/dispose.vue
@@ -19,9 +19,9 @@
          label-width="120px"
          class="search-form"
        >
          <el-form-item label="满意度模板" prop="templateId">
          <el-form-item label="满意度类型" prop="templateid">
            <el-select
              v-model="queryParams.templateId"
              v-model="queryParams.templateType"
              placeholder="请选择模板"
              clearable
              style="width: 200px"
@@ -35,9 +35,9 @@
            </el-select>
          </el-form-item>
          <el-form-item label="责任科室" prop="deptIds">
          <el-form-item label="责任科室" prop="todeptcode">
            <el-select
              v-model="queryParams.deptIds"
              v-model="queryParams.todeptcode"
              placeholder="请选择责任科室"
              clearable
              filterable
@@ -47,21 +47,21 @@
            >
              <el-option
                v-for="dept in deptList"
                :key="dept.id"
                :label="dept.name"
                :value="dept.id"
                :key="dept.deptCode"
                :label="dept.label"
                :value="dept.deptCode"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="统计时间" prop="dateRange">
          <el-form-item label="处理时间" prop="handleTimeRange">
            <el-date-picker
              v-model="queryParams.dateRange"
              type="daterange"
              v-model="queryParams.handleTimeRange"
              type="datetimerange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="yyyy-MM-dd"
              start-placeholder="开始时间"
              end-placeholder="结束时间"
              value-format="yyyy-MM-dd HH:mm:ss"
              :picker-options="pickerOptions"
              style="width: 380px"
            />
@@ -96,11 +96,7 @@
          >
            æ‰¹é‡å¤„理 ({{ selectedIds.length }})
          </el-button>
          <el-button
            type="info"
            icon="el-icon-download"
            @click="handleExport"
          >
          <el-button type="info" icon="el-icon-download" @click="handleExport">
            å¯¼å‡ºå¼‚常数据
          </el-button>
          <el-button
@@ -117,24 +113,26 @@
    <!-- å¼‚常统计概览 -->
    <div class="overview-section">
      <el-row :gutter="20">
        <el-col :span="6">
        <el-col :span="8">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-s-claim" style="color: #5788FE;"></i>
              <div class="stat-icon" style="background: #f0f9ff">
                <i class="el-icon-s-claim" style="color: #5788fe"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">总异常数量</div>
                <div class="stat-value">{{ overviewData.totalExceptionCount }}</div>
                <div class="stat-value">
                  {{ overviewData.totalExceptionCount }}
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
        <el-col :span="8">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-s-flag" style="color: #E6A23C;"></i>
              <div class="stat-icon" style="background: #f0f9ff">
                <i class="el-icon-s-flag" style="color: #e6a23c"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">待处理异常</div>
@@ -143,28 +141,15 @@
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
        <el-col :span="8">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-check" style="color: #67C23A;"></i>
              <div class="stat-icon" style="background: #f0f9ff">
                <i class="el-icon-check" style="color: #67c23a"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">已处理异常</div>
                <div class="stat-value">{{ overviewData.processedCount }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card shadow="never" class="stat-card">
            <div class="stat-content">
              <div class="stat-icon" style="background: #f0f9ff;">
                <i class="el-icon-s-order" style="color: #909399;"></i>
              </div>
              <div class="stat-info">
                <div class="stat-title">今日处理数</div>
                <div class="stat-value">{{ overviewData.todayProcessedCount }}</div>
              </div>
            </div>
          </el-card>
@@ -183,11 +168,7 @@
          @selection-change="handleSelectionChange"
          class="exception-table"
        >
          <el-table-column
            type="selection"
            width="55"
            align="center"
          />
          <el-table-column type="selection" width="55" align="center" />
          <el-table-column
            label="序号"
@@ -198,25 +179,19 @@
          <el-table-column
            label="题目内容"
            prop="questionContent"
            prop="questiontext"
            min-width="300"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="question-content">
                <span class="question-text">{{ row.questionContent }}</span>
                <span class="question-text">{{ row.questiontext }}</span>
                <div class="question-tags">
                  <el-tag
                    size="mini"
                    :type="getQuestionTypeTag(row.questionType)"
                    :type="getTemplateTypeTag(row.templateType)"
                  >
                    {{ row.questionType === 1 ? '单选题' : '多选题' }}
                  </el-tag>
                  <el-tag
                    size="mini"
                    type="info"
                  >
                    {{ row.templateName }}
                    {{ row.templateType === 1 ? "语音模板" : "问卷模板" }}
                  </el-tag>
                </div>
              </div>
@@ -225,62 +200,64 @@
          <el-table-column
            label="负责科室"
            prop="responsibilityDepts"
            prop="responsibleDept"
            width="180"
            align="center"
          >
            <template slot-scope="{ row }">
              <div class="dept-list">
                <el-tag
                  v-for="dept in row.responsibilityDepts"
                  :key="dept.id"
                  v-for="dept in row.responsibleDept"
                  :key="dept.deptCode"
                  size="small"
                  type="primary"
                  class="dept-tag"
                >
                  {{ dept.name }}
                  {{ dept.deptName }}
                </el-tag>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="填写情况"
            width="200"
            align="center"
          >
          <el-table-column label="填写情况" width="200" align="center">
            <template slot-scope="{ row }">
              <div class="fill-statistics">
                <div class="stat-item">
                  <span class="stat-label">有效填写:</span>
                  <span class="stat-value">{{ row.validFillCount }}</span>
                  <span class="stat-value">{{
                    row.fillSituation.effectiveFillNum
                  }}</span>
                </div>
                <div class="stat-item">
                  <span class="stat-label">异常填写:</span>
                  <span class="stat-value exception-count">{{ row.exceptionFillCount }}</span>
                  <span class="stat-value exception-count">{{
                    row.fillSituation.exceptionFillNum
                  }}</span>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column
            label="异常任务"
            width="280"
            align="center"
          >
          <el-table-column label="异常任务" width="280" align="center">
            <template slot-scope="{ row }">
              <div class="exception-tasks">
                <div class="task-category">
                  <div class="task-title">已处理</div>
                  <div class="task-count processed">{{ row.processedCount }}</div>
                  <div class="task-count processed">
                    {{ row.exceptionQuesNum.yesDeal }}
                  </div>
                </div>
                <div class="task-category">
                  <div class="task-title">待处理</div>
                  <div class="task-count pending">{{ row.pendingCount }}</div>
                  <div class="task-count pending">
                    {{ row.exceptionQuesNum.noDeal }}
                  </div>
                </div>
                <div class="task-category">
                  <div class="task-title">异常总数</div>
                  <div class="task-count total">{{ row.totalExceptionCount }}</div>
                  <div class="task-count total">
                    {{ row.exceptionQuesNum.all }}
                  </div>
                </div>
              </div>
            </template>
@@ -288,14 +265,16 @@
          <el-table-column
            label="最近处理"
            prop="lastProcessTime"
            prop="handleTime"
            width="180"
            align="center"
          >
            <template slot-scope="{ row }">
              <div v-if="row.lastProcessTime" class="last-process">
                <div class="process-time">{{ row.lastProcessTime }}</div>
                <div class="process-user">{{ row.lastProcessUser }}</div>
              <div v-if="row.handleTime" class="last-process">
                <div class="process-time">
                  {{ formatDateTime(row.handleTime) }}
                </div>
                <div class="process-user">{{ row.handleBy || "系统处理" }}</div>
              </div>
              <span v-else class="no-process">暂无处理记录</span>
            </template>
@@ -308,14 +287,6 @@
            fixed="right"
          >
            <template slot-scope="{ row }">
              <!-- <el-button
                type="primary"
                size="small"
                icon="el-icon-view"
                @click="handleViewDetail(row)"
              >
                è¯¦æƒ…
              </el-button> -->
              <el-button
                type="warning"
                size="small"
@@ -347,17 +318,23 @@
</template>
<script>
import { tracedeallist } from "@/api/AiCentre/index";
import { deptTreeSelect } from "@/api/system/user";
export default {
  name: 'ExceptionList',
  name: "ExceptionList",
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        templateId: '',
        deptIds: [],
        dateRange: [],
        todeptcode: [], // å¤„理科室编号数组
        todeptname: "", // å¤„理科室名称
        templateType: 2, // ä»»åŠ¡æ¨¡æ¿ID
        handleStartTime: "", // å¤„理开始时间
        handleEndTime: "", // å¤„理结束时间
        handleTimeRange: [], // æ—¶é—´èŒƒå›´ï¼Œç”¨äºŽç•Œé¢å±•示
        pageNum: 1,
        pageSize: 10
        pageSize: 10,
      },
      // åŠ è½½çŠ¶æ€
@@ -368,27 +345,13 @@
      // æ¨¡æ¿åˆ—表
      templateList: [
        { id: 1, name: '出院满意度问卷' },
        { id: 2, name: '住院满意度问卷' },
        { id: 3, name: '门诊满意度问卷' },
        { id: 4, name: '常用满意度问卷' }
        { id: 1, name: "语音满意度" },
        { id: 2, name: "问卷满意度" },
        // ä½ å¯ä»¥æ ¹æ®å®žé™…情况从接口获取模板列表
      ],
      // ç§‘室列表
      deptList: [
        { id: 1, name: '心血管内科' },
        { id: 2, name: '神经内科' },
        { id: 3, name: '普外科' },
        { id: 4, name: '骨科' },
        { id: 5, name: '妇产科' },
        { id: 6, name: '儿科' },
        { id: 7, name: '急诊科' },
        { id: 8, name: '呼吸内科' },
        { id: 9, name: '消化内科' },
        { id: 10, name: '内分泌科' },
        { id: 11, name: '肾内科' },
        { id: 12, name: '肿瘤科' }
      ],
      deptList: [],
      // å¼‚常列表数据
      exceptionList: [],
@@ -399,60 +362,134 @@
        totalExceptionCount: 0,
        pendingCount: 0,
        processedCount: 0,
        todayProcessedCount: 0
        todayProcessedCount: 0,
      },
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      pickerOptions: {
        shortcuts: [
          {
            text: '最近一周',
            text: "最近一周",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
              picker.$emit('pick', [start, end]);
            }
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: '最近一个月',
            text: "最近一个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
              picker.$emit('pick', [start, end]);
            }
              picker.$emit("pick", [start, end]);
            },
          },
          {
            text: '最近三个月',
            text: "最近三个月",
            onClick(picker) {
              const end = new Date();
              const start = new Date();
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
              picker.$emit('pick', [start, end]);
            }
          }
              picker.$emit("pick", [start, end]);
            },
          },
        ],
        disabledDate(time) {
          return time.getTime() > Date.now();
        }
      }
        },
      },
    };
  },
  mounted() {
    this.loadData();
    this.getDeptOptions();
  },
  methods: {
    // æ ¼å¼åŒ–日期时间
    formatDateTime(dateTime) {
      if (!dateTime) return "";
      const date = new Date(dateTime);
      return (
        date.toLocaleDateString().replace(/\//g, "-") +
        " " +
        date.toTimeString().split(" ")[0]
      );
    },
    // èŽ·å–æ¨¡æ¿ç±»åž‹æ ‡ç­¾æ ·å¼
    getTemplateTypeTag(type) {
      return type === 1 ? "primary" : "success";
    },
    // æž„建查询参数
    buildQueryParams() {
      const params = {
        pageNum: this.queryParams.pageNum,
        pageSize: this.queryParams.pageSize,
      };
      // å¤„理科室编号
      if (
        this.queryParams.todeptcode &&
        this.queryParams.todeptcode.length > 0
      ) {
        // æŽ¥å£å¯èƒ½éœ€è¦å­—符串格式的科室编号,根据实际情况调整
        params.todeptcode = this.queryParams.todeptcode.join(",");
      }
      // æ¨¡æ¿ID
      if (this.queryParams.templateType) {
        params.templateType = this.queryParams.templateType;
      }
      // å¤„理时间范围
      if (
        this.queryParams.handleTimeRange &&
        this.queryParams.handleTimeRange.length === 2
      ) {
        params.handleStartTime = this.queryParams.handleTimeRange[0];
        params.handleEndTime = this.queryParams.handleTimeRange[1];
      }
      return params;
    },
    /** æŸ¥è¯¢ç§‘室列表 */
    getDeptOptions() {
      deptTreeSelect()
        .then((res) => {
          if (res.code == 200) {
            this.deptList = this.flattenArray(res.data) || [];
          }
        })
        .catch((error) => {
          console.error("获取科室列表失败:", error);
          this.$message.error("获取科室列表失败");
        });
    },
    flattenArray(multiArray) {
      let result = [];
      function flatten(element) {
        if (element.children && element.children.length > 0) {
          element.children.forEach((child) => flatten(child));
        } else {
          let item = JSON.parse(JSON.stringify(element));
          result.push(item);
        }
      }
      multiArray.forEach((element) => flatten(element));
      return result;
    },
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await Promise.all([
          this.loadExceptionList(),
          this.loadOverviewData()
        ]);
        await Promise.all([this.loadExceptionList(), this.loadOverviewData()]);
      } finally {
        this.loading = false;
      }
@@ -460,185 +497,70 @@
    // åŠ è½½å¼‚å¸¸åˆ—è¡¨
    async loadExceptionList() {
      return new Promise((resolve) => {
        setTimeout(() => {
          // Mock æ•°æ®
          this.exceptionList = [
            {
              id: 1,
              questionId: 101,
              questionContent: '您对医护人员的服务态度是否满意?',
              questionType: 1, // 1: å•选题, 2: å¤šé€‰é¢˜
              templateName: '出院满意度问卷',
              responsibilityDepts: [
                { id: 1, name: '心血管内科' },
                { id: 2, name: '神经内科' }
              ],
              validFillCount: 145,
              exceptionFillCount: 8,
              processedCount: 5,
              pendingCount: 3,
              totalExceptionCount: 8,
              lastProcessTime: '2024-01-15 10:30:25',
              lastProcessUser: '张医生'
            },
            {
              id: 2,
              questionId: 102,
              questionContent: '您对医生的诊疗水平和技术能力评价如何?',
              questionType: 1,
              templateName: '住院满意度问卷',
              responsibilityDepts: [
                { id: 3, name: '普外科' },
                { id: 4, name: '骨科' }
              ],
              validFillCount: 120,
              exceptionFillCount: 12,
              processedCount: 8,
              pendingCount: 4,
              totalExceptionCount: 12,
              lastProcessTime: '2024-01-14 16:20:10',
              lastProcessUser: '李护士长'
            },
            {
              id: 3,
              questionId: 103,
              questionContent: '您对医院的环境和卫生状况是否满意?',
              questionType: 1,
              templateName: '门诊满意度问卷',
              responsibilityDepts: [
                { id: 5, name: '妇产科' },
                { id: 6, name: '儿科' },
                { id: 7, name: '急诊科' }
              ],
              validFillCount: 180,
              exceptionFillCount: 15,
              processedCount: 10,
              pendingCount: 5,
              totalExceptionCount: 15,
              lastProcessTime: '2024-01-13 09:15:45',
              lastProcessUser: '王主任'
            },
            {
              id: 4,
              questionId: 104,
              questionContent: '您认为医护人员与您的沟通是否充分?',
              questionType: 1,
              templateName: '常用满意度问卷',
              responsibilityDepts: [
                { id: 8, name: '呼吸内科' },
                { id: 9, name: '消化内科' }
              ],
              validFillCount: 95,
              exceptionFillCount: 6,
              processedCount: 4,
              pendingCount: 2,
              totalExceptionCount: 6,
              lastProcessTime: '2024-01-12 14:40:30',
              lastProcessUser: '赵医生'
            },
            {
              id: 5,
              questionId: 105,
              questionContent: '您对等待就诊和治疗的时间是否满意?',
              questionType: 1,
              templateName: '住院满意度问卷',
              responsibilityDepts: [
                { id: 10, name: '内分泌科' },
                { id: 11, name: '肾内科' }
              ],
              validFillCount: 200,
              exceptionFillCount: 25,
              processedCount: 15,
              pendingCount: 10,
              totalExceptionCount: 25,
              lastProcessTime: '2024-01-11 11:25:15',
              lastProcessUser: '孙护士'
            },
            {
              id: 6,
              questionId: 106,
              questionContent: '您对医院收费的透明度和合理性评价如何?',
              questionType: 1,
              templateName: '门诊满意度问卷',
              responsibilityDepts: [
                { id: 12, name: '肿瘤科' }
              ],
              validFillCount: 160,
              exceptionFillCount: 18,
              processedCount: 12,
              pendingCount: 6,
              totalExceptionCount: 18,
              lastProcessTime: '2024-01-10 15:50:20',
              lastProcessUser: '周医生'
            },
            {
              id: 7,
              questionId: 107,
              questionContent: '您会向亲友推荐我们医院吗?',
              questionType: 1,
              templateName: '出院满意度问卷',
              responsibilityDepts: [
                { id: 1, name: '心血管内科' },
                { id: 8, name: '呼吸内科' }
              ],
              validFillCount: 110,
              exceptionFillCount: 7,
              processedCount: 5,
              pendingCount: 2,
              totalExceptionCount: 7,
              lastProcessTime: '2024-01-09 10:15:40',
              lastProcessUser: '吴主任'
            },
            {
              id: 8,
              questionId: 108,
              questionContent: '您对以下哪些方面比较满意(多选)?',
              questionType: 2,
              templateName: '常用满意度问卷',
              responsibilityDepts: [
                { id: 2, name: '神经内科' },
                { id: 3, name: '普外科' },
                { id: 5, name: '妇产科' }
              ],
              validFillCount: 135,
              exceptionFillCount: 9,
              processedCount: 6,
              pendingCount: 3,
              totalExceptionCount: 9,
              lastProcessTime: '2024-01-08 13:30:55',
              lastProcessUser: '郑医生'
            }
          ];
          this.total = this.exceptionList.length;
          resolve();
        }, 500);
      });
      try {
        const params = this.buildQueryParams();
        const response = await tracedeallist(params);
        if (response.code == 200) {
          this.exceptionList = response.rows.detailTraceDealDTOList || [];
          this.overviewData.totalExceptionCount = response.rows.totalException;
          this.overviewData.pendingCount = response.rows.noDealException;
          this.overviewData.processedCount = response.rows.yesDealException;
          this.total = response.total || 0;
        } else {
          this.exceptionList = [];
          this.total = 0;
        }
      } catch (error) {
        console.error("加载异常列表失败:", error);
        this.$message.error("加载异常列表失败,请稍后重试");
        this.exceptionList = [];
        this.total = 0;
      }
    },
    // åŠ è½½æ¦‚è§ˆæ•°æ®
    async loadOverviewData() {
      return new Promise((resolve) => {
        setTimeout(() => {
          // è®¡ç®—统计数据
          const totalExceptionCount = this.exceptionList.reduce((sum, item) => sum + item.totalExceptionCount, 0);
          const pendingCount = this.exceptionList.reduce((sum, item) => sum + item.pendingCount, 0);
          const processedCount = this.exceptionList.reduce((sum, item) => sum + item.processedCount, 0);
      try {
        // ä»ŽæŽ¥å£æ•°æ®è®¡ç®—统计数据
        const totalExceptionCount = this.exceptionList.reduce(
          (sum, item) => sum + (item.exceptionQuesNum?.all || 0),
          0
        );
        const pendingCount = this.exceptionList.reduce(
          (sum, item) => sum + (item.exceptionQuesNum?.noDeal || 0),
          0
        );
        const processedCount = this.exceptionList.reduce(
          (sum, item) => sum + (item.exceptionQuesNum?.yesDeal || 0),
          0
        );
          this.overviewData = {
            totalExceptionCount,
            pendingCount,
            processedCount,
            todayProcessedCount: 8 // ä»Šæ—¥å¤„理数 mock
          };
          resolve();
        }, 300);
      });
    },
        // è®¡ç®—今日处理数(这里可以根据实际需求调整逻辑)
        const today = new Date().toISOString().split("T")[0];
        const todayProcessedCount = this.exceptionList.filter((item) => {
          if (!item.handleTime) return false;
          const handleDate = new Date(item.handleTime)
            .toISOString()
            .split("T")[0];
          return handleDate === today;
        }).length;
    // èŽ·å–é¢˜ç›®ç±»åž‹æ ‡ç­¾æ ·å¼
    getQuestionTypeTag(type) {
      return type === 1 ? 'primary' : 'success';
        this.overviewData = {
          totalExceptionCount,
          pendingCount,
          processedCount,
          todayProcessedCount,
        };
      } catch (error) {
        console.error("加载概览数据失败:", error);
        this.overviewData = {
          totalExceptionCount: 0,
          pendingCount: 0,
          processedCount: 0,
          todayProcessedCount: 0,
        };
      }
    },
    // å¤„理查询
@@ -650,60 +572,53 @@
    // å¤„理重置
    handleReset() {
      this.$refs.queryForm.resetFields();
      this.queryParams.dateRange = [];
      this.queryParams.handleTimeRange = [];
      this.queryParams.pageNum = 1;
      this.queryParams.todeptcode = []; // é‡ç½®ç§‘室选择
      this.loadData();
    },
    // å¤„理批量处理
    handleBatchProcess() {
      if (this.selectedIds.length === 0) {
        this.$message.warning('请先选择要处理的异常题目');
        this.$message.warning("请先选择要处理的异常题目");
        return;
      }
      // è·³è½¬åˆ°æ‰¹é‡å¤„理页面
      this.$router.push({
        path: '/satisfaction/exception/batch-process',
        path: "/Intelligentcenter/batch",
        query: {
          questionIds: this.selectedIds.join(',')
        }
          questionIds: this.selectedIds.join(","),
          type: this.queryParams.templateType,
        },
      });
    },
    // å¤„理导出
    handleExport() {
      this.$message.success('导出功能开发中...');
      this.$message.success("导出功能开发中...");
    },
    // åˆ·æ–°æ•°æ®
    refreshData() {
      this.loadData();
      this.$message.success('数据已刷新');
      this.$message.success("数据已刷新");
    },
    // å¤„理选择变化
    handleSelectionChange(selection) {
      this.selectedIds = selection.map(item => item.questionId);
    },
    // å¤„理查看详情
    handleViewDetail(row) {
      this.$router.push({
        path: '/satisfaction/exception/detail',
        query: {
          id: row.questionId
        }
      });
      this.selectedIds = selection.map((item) => item.scriptid);
    },
    // å¤„理单个题目批量处理
    handleBatchQuestion(row) {
      this.$router.push({
        path: '/Intelligentcenter/batch',
        path: "/Intelligentcenter/batch",
        query: {
          questionId: row.questionId
        }
          questionId: row.scriptid,
          type: this.queryParams.templateType,
        },
      });
    },
@@ -718,8 +633,8 @@
    handlePageChange(page) {
      this.queryParams.pageNum = page;
      this.loadExceptionList();
    }
  }
    },
  },
};
</script>
@@ -732,7 +647,7 @@
  .page-header {
    margin-bottom: 20px;
    padding: 20px;
    background: linear-gradient(135deg, #5788FE 0%, #66b1ff 100%);
    background: linear-gradient(135deg, #5788fe 0%, #66b1ff 100%);
    border-radius: 8px;
    color: white;
@@ -838,12 +753,13 @@
          color: #303133;
          margin-bottom: 8px;
          line-height: 1.5;
          text-align: left;
        }
        .question-tags {
          display: flex;
          gap: 5px;
          justify-content: center;
          justify-content: flex-start;
        }
      }
@@ -915,7 +831,7 @@
            }
            &.total {
              color: #5788FE;
              color: #5788fe;
            }
          }
        }
@@ -930,7 +846,7 @@
        .process-user {
          font-size: 13px;
          color: #5788FE;
          color: #5788fe;
          font-weight: 500;
        }
      }
src/views/Satisfaction/configurationmyd/index.vue
@@ -481,89 +481,95 @@
        </div>
      </div>
    </div>
<!-- é€‰é¡¹é…ç½®å¯¹è¯æ¡† -->
<el-dialog
  title="选项异常状态配置"
  :visible.sync="optionDialogVisible"
  width="700px"
  center
  :close-on-click-modal="false"
>
  <div v-if="editingQuestion" class="option-config-wrapper">
    <div class="dialog-header">
      <h4>{{ editingQuestion.scriptTopic || '无主题' }}</h4>
      <p class="dialog-subtitle">{{ editingQuestion.scriptContent }}</p>
    </div>
    <!-- é€‰é¡¹é…ç½®å¯¹è¯æ¡† -->
    <el-dialog
      title="选项异常状态配置"
      :visible.sync="optionDialogVisible"
      width="700px"
      center
      :close-on-click-modal="false"
    >
      <div v-if="editingQuestion" class="option-config-wrapper">
        <div class="dialog-header">
          <h4>{{ editingQuestion.scriptTopic || "无主题" }}</h4>
          <p class="dialog-subtitle">{{ editingQuestion.scriptContent }}</p>
        </div>
    <div class="option-list">
      <el-alert
        v-if="!currentOptions.some(opt => opt.isabnormal === 1)"
        title="请至少设置一个异常选项(标记为异常)"
        type="warning"
        :closable="false"
        show-icon
        style="margin-bottom: 20px;"
      />
        <div class="option-list">
          <el-alert
            v-if="!currentOptions.some((opt) => opt.isabnormal === 1)"
            title="请至少设置一个异常选项(标记为异常)"
            type="warning"
            :closable="false"
            show-icon
            style="margin-bottom: 20px"
          />
      <div v-for="(option, index) in currentOptions" :key="index" class="option-item">
        <el-form
          :model="option"
          :rules="optionRules"
          ref="optionForm"
          size="small"
          class="option-form"
        >
          <el-row :gutter="12" align="middle">
            <el-col :span="2">
              <div class="option-index">#{{ index + 1 }}</div>
            </el-col>
          <div
            v-for="(option, index) in currentOptions"
            :key="index"
            class="option-item"
          >
            <el-form
              :model="option"
              :rules="optionRules"
              ref="optionForm"
              size="small"
              class="option-form"
            >
              <el-row :gutter="12" align="middle">
                <el-col :span="2">
                  <div class="option-index">#{{ index + 1 }}</div>
                </el-col>
            <el-col :span="12">
              <el-form-item prop="targetvalue">
                <el-input
                  v-model="option.targetvalue"
                  placeholder="请输入选项内容"
                  clearable
                  maxlength="200"
                  show-word-limit
                />
              </el-form-item>
            </el-col>
                <el-col :span="12">
                  <el-form-item prop="targetvalue">
                    <el-input
                      v-model="option.targetvalue"
                      placeholder="请输入选项内容"
                      clearable
                      maxlength="200"
                      show-word-limit
                    />
                  </el-form-item>
                </el-col>
            <el-col :span="6">
              <el-form-item prop="isabnormal">
                <el-select
                  v-model="option.isabnormal"
                  placeholder="选择状态"
                  style="width: 100%"
                >
                  <el-option
                    v-for="status in abnormalOptions"
                    :key="status.value"
                    :label="status.label"
                    :value="status.value"
                  >
                    <el-tag :type="status.type" size="small">{{ status.label }}</el-tag>
                  </el-option>
                </el-select>
              </el-form-item>
            </el-col>
                <el-col :span="6">
                  <el-form-item prop="isabnormal">
                    <el-select
                      v-model="option.isabnormal"
                      placeholder="选择状态"
                      style="width: 100%"
                    >
                      <el-option
                        v-for="status in abnormalOptions"
                        :key="status.value"
                        :label="status.label"
                        :value="status.value"
                      >
                        <el-tag :type="status.type" size="small">{{
                          status.label
                        }}</el-tag>
                      </el-option>
                    </el-select>
                  </el-form-item>
                </el-col>
            <el-col :span="4">
              <el-button
                type="danger"
                icon="el-icon-delete"
                @click="removeOption(index)"
                size="small"
                circle
                plain
              />
            </el-col>
          </el-row>
        </el-form>
      </div>
                <el-col :span="4">
                  <el-button
                    type="danger"
                    icon="el-icon-delete"
                    @click="removeOption(index)"
                    size="small"
                    circle
                    plain
                  />
                </el-col>
              </el-row>
            </el-form>
          </div>
      <!-- <el-button
          <!-- <el-button
        type="primary"
        icon="el-icon-plus"
        @click="addNewOption"
@@ -573,16 +579,16 @@
      >
        æ·»åР选项
      </el-button> -->
    </div>
  </div>
        </div>
      </div>
  <span slot="footer" class="dialog-footer">
    <el-button @click="optionDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="saveOptions" :loading="savingOptions">
      ä¿å­˜é…ç½®
    </el-button>
  </span>
</el-dialog>
      <span slot="footer" class="dialog-footer">
        <el-button @click="optionDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveOptions" :loading="savingOptions">
          ä¿å­˜é…ç½®
        </el-button>
      </span>
    </el-dialog>
    <!-- é¢˜ç›®é¢„览对话框 -->
    <el-dialog
      title="题目预览"
@@ -848,30 +854,12 @@
          this.voiceCategories.includes(q.scriptAssortid)
        ).length;
      }
      return 0;
    },
    // æ£€æŸ¥é¢˜ç›®æ˜¯å¦æœ‰å¼‚常选项
    hasAbnormalOption(question) {
      return (question) => {
        if (!question) return false;
        // é—®å·æ¨¡æ¿
        if (this.templateForm.templateType === 1) {
          const options = question.svyLibTemplateTargetoptions || [];
          return options.some((opt) => opt.isabnormal === 1);
        }
        // è¯­éŸ³æ¨¡æ¿
        else if (this.templateForm.templateType === 2) {
          const options = question.ivrLibaScriptTargetoptionList || [];
          return options.some((opt) => opt.isabnormal === 1);
        }
        return false;
      };
    },
    // ç­›é€‰åŽçš„题目列表
    filteredQuestionList() {
      let filtered = this.questionList;
      console.log(this.questionnaireCategorys);
      // ç­›é€‰æ»¡æ„åº¦é¢˜ç›®
      if (this.templateForm.templateType === 1) {
@@ -888,7 +876,7 @@
      if (this.queryParams.scriptTopic) {
        const keyword = this.queryParams.scriptTopic.toLowerCase();
        filtered = filtered.filter(
          (q) => q.scriptTopic && q.scriptTopic.toLowerCase().includes(keyword)
          (q) => q.scriptTopic && q.criptTopic.toLowerCase().includes(keyword)
        );
      }
@@ -988,12 +976,16 @@
      return new Promise((resolve) => {
        getQtemplatelist({ pageSize: 1000 })
          .then((res) => {
            if (res.code === 200) {
            console.log(res.rows, "res.rows");
            if (res.code == 200) {
              console.log(res.rows, 2);
              this.questionnaireTemplates = (res.rows || []).map((item) => ({
                id: item.svyid,
                templateName: item.svyname,
                isavailable: item.isavailable,
              }));
              console.log(this.followupTemplates, 3);
            } else {
              this.$message.error(res.msg || "加载问卷模板失败");
            }
@@ -1012,12 +1004,16 @@
      return new Promise((resolve) => {
        getFollowuplist({ pageSize: 1000 })
          .then((res) => {
            if (res.code === 200) {
            console.log(res.rows, "res.rows");
            if (res.code == 200) {
              console.log(res.rows, 2);
              this.followupTemplates = (res.rows || []).map((item) => ({
                id: item.id,
                templateName: item.templateName,
                isavailable: item.isavailable,
              }));
              console.log(this.followupTemplates, 3);
            } else {
              this.$message.error(res.msg || "加载语音模板失败");
            }
@@ -1215,6 +1211,8 @@
        const index = this.filteredQuestionList.findIndex(
          (q) => q.id === question.id
        );
        console.log(index, "index");
        if (index !== -1) {
          const formRef = this.$refs.configForm && this.$refs.configForm[index];
          if (formRef) {
@@ -1261,24 +1259,61 @@
      const changedItems = this.questionList.filter((q) => q.hasChanges);
      this.changedCount = changedItems.length;
      this.hasChanges = changedItems.length > 0;
      // å¼ºåˆ¶æ›´æ–°è§†å›¾
      this.$forceUpdate();
    },
    /** æ£€æŸ¥é¢˜ç›®æ˜¯å¦æœ‰å¼‚常选项 */
    checkHasAbnormalOptions(question) {
      if (this.templateForm.templateType === 1) {
        return (question.svyLibTemplateTargetoptions || []).some(
          (opt) => opt.isabnormal === 1
        );
      } else if (this.templateForm.templateType === 2) {
        return (question.ivrLibaScriptTargetoptionList || []).some(
          (opt) => opt.isabnormal === 1
        );
      }
      return false;
    },
    /** ä¿å­˜å•个题目配置 */
    async saveSingleConfig(question) {
      if (!question.hasChanges) return;
    async saveSingleConfig(question, skipAbnormalCheck = false) {
      // æ£€æŸ¥æ˜¯å¦æœ‰å˜æ›´
      if (!question.hasChanges && !skipAbnormalCheck) {
        this.$message.info("当前配置无变化");
        return;
      }
      const index = this.filteredQuestionList.findIndex(
        (q) => q.id === question.id
      );
      console.log(index, "filteredQuestionList");
      // æ£€æŸ¥æ˜¯å¦æœ‰å¼‚常选项
      if (!skipAbnormalCheck && !this.checkHasAbnormalOptions(question)) {
        this.$confirm(
          "该题目没有设置异常选项,必须先配置异常选项才能保存。是否立即配置?",
          "提示",
          {
            confirmButtonText: "去配置",
            cancelButtonText: "取消",
            type: "warning",
          }
        )
          .then(() => {
            this.openOptionDialog(question);
          })
          .catch(() => {});
        return;
      }
      const index = this.questionList.findIndex((q) => q.id === question.id);
      if (index === -1) return;
      const formRef = this.$refs.configForm && this.$refs.configForm[index];
      if (!formRef) return;
      const valid = await formRef.validate();
      if (!valid) {
      // éªŒè¯è¡¨å•
      try {
        await formRef.validate();
      } catch (error) {
        this.$message.warning("请先完成必填项");
        return;
      }
@@ -1339,6 +1374,18 @@
          reportDeptName: reportDeptNames.join(","),
        };
        // å¦‚果需要,也更新选项数据
        if (question.hasChanges && this.templateForm.templateType === 1) {
          questions[questionIndex].svyLibTemplateTargetoptions =
            question.svyLibTemplateTargetoptions || [];
        } else if (
          question.hasChanges &&
          this.templateForm.templateType === 2
        ) {
          questions[questionIndex].ivrLibaScriptTargetoptionList =
            question.ivrLibaScriptTargetoptionList || [];
        }
        // æ›´æ–°æ¨¡æ¿
        updatedTemplateDetail[questionsField] = questions;
@@ -1375,7 +1422,6 @@
      }
    },
    /** å¤„理保存成功 */
    /** å¤„理保存成功 */
    handleSaveSuccess(question) {
      // åŒæ—¶æ›´æ–°é¢˜ç›®é¡¶å±‚字段
@@ -1450,6 +1496,36 @@
    async handleBatchSave() {
      if (!this.hasChanges || this.batchSaving) return;
      // èŽ·å–æœ‰å˜æ›´çš„é¢˜ç›®
      const changedQuestions = this.questionList.filter((q) => q.hasChanges);
      if (changedQuestions.length === 0) {
        this.$message.info("没有需要保存的配置变更");
        return;
      }
      // æ£€æŸ¥æ˜¯å¦æœ‰é¢˜ç›®ç¼ºå°‘异常选项
      const questionsWithoutAbnormal = changedQuestions.filter(
        (q) => !this.checkHasAbnormalOptions(q)
      );
      if (questionsWithoutAbnormal.length > 0) {
        this.$confirm(
          `有 ${questionsWithoutAbnormal.length} ä¸ªé¢˜ç›®æ²¡æœ‰è®¾ç½®å¼‚常选项,必须配置异常选项后才能保存。是否先去配置?`,
          "提示",
          {
            confirmButtonText: "去配置",
            cancelButtonText: "取消",
            type: "warning",
          }
        )
          .then(() => {
            // æ‰“开第一个没有异常选项的题目的配置对话框
            this.openOptionDialog(questionsWithoutAbnormal[0]);
          })
          .catch(() => {});
        return;
      }
      this.$confirm("确定要保存所有修改过的配置吗?", "批量保存", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
@@ -1458,24 +1534,20 @@
        .then(async () => {
          this.batchSaving = true;
          const changedQuestions = this.questionList.filter(
            (q) => q.hasChanges
          );
          const results = [];
          for (const question of changedQuestions) {
            try {
              await this.saveSingleConfig(question);
              // è·³è¿‡å¼‚常检查,因为在上面已经检查过了
              await this.saveSingleConfig(question, true);
              results.push({
                id: question.id,
                success:
                  !question.hasChanges &&
                  question.saveStatus?.type === "success",
                success: !question.hasChanges,
              });
            } catch (error) {
              results.push({
                id: question.id,
                success: false,
                error: error.message,
              });
            }
          }
@@ -1492,6 +1564,11 @@
            this.$message.warning(
              `成功保存 ${successCount} ä¸ªï¼Œå¤±è´¥ ${failCount} ä¸ª`
            );
            // å¯ä»¥æ˜¾ç¤ºå…·ä½“哪些失败了
            const failedQuestions = results
              .filter((r) => !r.success)
              .map((r) => r.id);
            console.error("保存失败的题目ID:", failedQuestions);
          }
        })
        .catch(() => {
@@ -1505,39 +1582,38 @@
      this.previewAnswer = "";
      this.previewVisible = true;
    },
    /** æ£€æŸ¥é¢˜ç›®æ˜¯å¦æœ‰å¼‚常选项 */
    checkHasAbnormalOptions(question) {
      if (this.templateForm.templateType === 1) {
        return (question.svyLibTemplateTargetoptions || []).some(
          (opt) => opt.isabnormal === 1
        );
      } else if (this.templateForm.templateType === 2) {
        return (question.ivrLibaScriptTargetoptionList || []).some(
          (opt) => opt.isabnormal === 1
        );
      }
      return false;
    },
    /** æ‰“开选项管理对话框 */
    /** ä¿®æ”¹é€‰é¡¹ç®¡ç†å¯¹è¯æ¡†çš„æ‰“开方法,保存原始选项 */
    openOptionDialog(question) {
      this.editingQuestion = question;
      // ä¿å­˜åŽŸå§‹é€‰é¡¹çš„å¿«ç…§
      if (this.templateForm.templateType === 1) {
        this.editingQuestion.originalOptions = JSON.parse(
          JSON.stringify(question.svyLibTemplateTargetoptions || [])
        );
      } else if (this.templateForm.templateType === 2) {
        this.editingQuestion.originalOptions = JSON.parse(
          JSON.stringify(question.ivrLibaScriptTargetoptionList || [])
        );
      }
      // å¤åˆ¶é€‰é¡¹æ•°æ®
      if (this.templateForm.templateType === 1) {
        this.currentOptions = JSON.parse(
          JSON.stringify(question.svyLibTemplateTargetoptions || [])
        ).map((opt) => ({
        ).map((opt, index) => ({
          ...opt,
          id: opt.id,
          id: opt.id || `temp_${Date.now()}_${index}`,
          targetvalue: opt.optioncontent || "",
          isabnormal: opt.isabnormal || 0,
        }));
      } else if (this.templateForm.templateType === 2) {
        this.currentOptions = JSON.parse(
          JSON.stringify(question.ivrLibaScriptTargetoptionList || [])
        ).map((opt) => ({
        ).map((opt, index) => ({
          ...opt,
          id: opt.id || `temp_${Date.now()}_${index}`,
          targetvalue: opt.targetvalue || "",
          isabnormal: opt.isabnormal || 0,
        }));
@@ -1549,7 +1625,7 @@
    /** æ·»åŠ æ–°é€‰é¡¹ */
    addNewOption() {
      this.currentOptions.push({
        id: Date.now(), // ä¸´æ—¶ID
        id: `temp_${Date.now()}_${this.currentOptions.length}`,
        targetvalue: "",
        isabnormal: 0,
        isNew: true,
@@ -1558,16 +1634,25 @@
    /** åˆ é™¤é€‰é¡¹ */
    removeOption(index) {
      this.currentOptions.splice(index, 1);
      this.$confirm("确定要删除这个选项吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.currentOptions.splice(index, 1);
        })
        .catch(() => {});
    },
    /** ä¿å­˜é€‰é¡¹é…ç½® */
    async saveOptions() {
      try {
        // éªŒè¯å¿…填项
        for (const option of this.currentOptions) {
        for (let i = 0; i < this.currentOptions.length; i++) {
          const option = this.currentOptions[i];
          if (!option.targetvalue || option.targetvalue.trim() === "") {
            this.$message.warning("请填写所有选项内容");
            this.$message.warning(`第 ${i + 1} ä¸ªé€‰é¡¹å†…容不能为空`);
            return;
          }
        }
@@ -1582,6 +1667,27 @@
          return;
        }
        // åˆ¤æ–­é€‰é¡¹æ˜¯å¦å‘生变化
        let isOptionsChanged = false;
        if (this.templateForm.templateType === 1) {
          const originalOptions =
            this.editingQuestion.svyLibTemplateTargetoptions || [];
          isOptionsChanged = this.checkOptionsChanged(
            originalOptions,
            this.currentOptions,
            "questionnaire"
          );
        } else if (this.templateForm.templateType === 2) {
          const originalOptions =
            this.editingQuestion.ivrLibaScriptTargetoptionList || [];
          isOptionsChanged = this.checkOptionsChanged(
            originalOptions,
            this.currentOptions,
            "voice"
          );
        }
        // ä¿å­˜é€»è¾‘ - æ›´æ–°é¢˜ç›®å¯¹è±¡çš„选项数据
        if (this.templateForm.templateType === 1) {
          this.editingQuestion.svyLibTemplateTargetoptions =
@@ -1589,14 +1695,24 @@
              ...opt,
              optioncontent: opt.targetvalue,
              isabnormal: opt.isabnormal,
              // æ¸…除临时字段
              targetvalue: undefined,
              isNew: undefined,
            }));
        } else if (this.templateForm.templateType === 2) {
          this.editingQuestion.ivrLibaScriptTargetoptionList =
            this.currentOptions;
            this.currentOptions.map((opt) => ({
              ...opt,
              // æ¸…除临时字段
              isNew: undefined,
            }));
        }
        // è§¦å‘配置变更检查
        this.handleConfigChange(this.editingQuestion);
        // å¦‚果选项有变化,则设置题目为有变更状态
        if (isOptionsChanged) {
          this.editingQuestion.hasChanges = true;
          this.updateChangedStatus();
        }
        this.$message.success("选项配置保存成功");
        this.optionDialogVisible = false;
@@ -1605,126 +1721,39 @@
        this.$message.error("保存选项失败");
      }
    },
    /** ä¿®æ”¹ä¿å­˜å•个题目配置方法,添加异常选项检查 */
    async saveSingleConfig(question) {
      // æ£€æŸ¥æ˜¯å¦æœ‰å¼‚常选项
      if (!this.checkHasAbnormalOptions(question)) {
        this.$confirm("该题目没有设置异常选项,是否先配置选项?", "提示", {
          confirmButtonText: "去配置",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            this.openOptionDialog(question);
          })
          .catch(() => {});
        return;
    /** æ£€æŸ¥é€‰é¡¹æ˜¯å¦å‘生变化 */
    checkOptionsChanged(originalOptions, newOptions, templateType) {
      // å¦‚果数量不同,则一定变化了
      if (originalOptions.length !== newOptions.length) {
        return true;
      }
      // åŽŸæœ‰çš„ä¿å­˜é€»è¾‘...
      if (!question.hasChanges) return;
      // æ¯”较每个选项的内容和异常状态
      for (let i = 0; i < originalOptions.length; i++) {
        const original = originalOptions[i];
        const current = newOptions[i];
      const index = this.filteredQuestionList.findIndex(
        (q) => q.id === question.id
      );
      if (index === -1) return;
      const formRef = this.$refs.configForm && this.$refs.configForm[index];
      if (!formRef) return;
      const valid = await formRef.validate();
      if (!valid) {
        this.$message.warning("请先完成必填项");
        return;
      }
      // ç»§ç»­åŽŸæœ‰çš„ä¿å­˜é€»è¾‘...
      question.saving = true;
      question.saveStatus = null;
      try {
        // ... åŽŸæœ‰çš„ä¿å­˜é€»è¾‘ä¸å˜
      } catch (error) {
        // ... é”™è¯¯å¤„理不变
      } finally {
        question.saving = false;
      }
    },
    /** æ‰¹é‡ä¿å­˜æ—¶ä¹Ÿè¦æ£€æŸ¥ */
    async handleBatchSave() {
      if (!this.hasChanges || this.batchSaving) return;
      // æ£€æŸ¥æ‰€æœ‰æœ‰å˜æ›´çš„题目是否都有异常选项
      const changedQuestions = this.questionList.filter((q) => q.hasChanges);
      const questionsWithoutAbnormal = changedQuestions.filter(
        (q) => !this.checkHasAbnormalOptions(q)
      );
      if (questionsWithoutAbnormal.length > 0) {
        this.$confirm(
          `有 ${questionsWithoutAbnormal.length} ä¸ªé¢˜ç›®æ²¡æœ‰è®¾ç½®å¼‚常选项,请先配置选项。是否继续?`,
          "提示",
          {
            confirmButtonText: "ç»§ç»­",
            cancelButtonText: "去配置",
            type: "warning",
        if (templateType === "questionnaire") {
          // é—®å·æ¨¡æ¿æ¯”较
          if (
            original.optioncontent !== current.targetvalue ||
            original.isabnormal !== current.isabnormal
          ) {
            return true;
          }
        )
          .then(() => {
            // ç»§ç»­æ‰§è¡Œæ‰¹é‡ä¿å­˜
            this.executeBatchSave(changedQuestions);
          })
          .catch(() => {
            // å¯ä»¥åœ¨è¿™é‡Œè·³è½¬åˆ°ç¬¬ä¸€ä¸ªæ²¡æœ‰å¼‚常选项的题目
            if (questionsWithoutAbnormal.length > 0) {
              this.openOptionDialog(questionsWithoutAbnormal[0]);
            }
          });
      } else {
        this.executeBatchSave(changedQuestions);
      }
    },
    /** æ‰§è¡Œæ‰¹é‡ä¿å­˜ */
    async executeBatchSave(changedQuestions) {
      this.$confirm("确定要保存所有修改过的配置吗?", "批量保存", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(async () => {
          this.batchSaving = true;
          const results = [];
          for (const question of changedQuestions) {
            try {
              // è¿™é‡Œè°ƒç”¨ä¿®æ”¹åŽçš„saveSingleConfig方法
              await this.saveSingleConfig(question);
              results.push({
                id: question.id,
                success:
                  !question.hasChanges &&
                  question.saveStatus?.type === "success",
              });
            } catch (error) {
              results.push({
                id: question.id,
                success: false,
              });
            }
        } else if (templateType === "voice") {
          // è¯­éŸ³æ¨¡æ¿æ¯”较
          if (
            original.targetvalue !== current.targetvalue ||
            original.isabnormal !== current.isabnormal
          ) {
            return true;
          }
        }
      }
          this.batchSaving = false;
          // ... åŽç»­å¤„理不变
        })
        .catch(() => {
          this.batchSaving = false;
        });
      return false;
    },
    /** èŽ·å–å¼‚å¸¸é€‰é¡¹ç»Ÿè®¡ */
    getAbnormalStats(question) {
      if (this.templateForm.templateType === 1) {
@@ -1746,6 +1775,7 @@
      }
      return { total: 0, abnormal: 0, warning: 0, normal: 0 };
    },
    /** æœç´¢ */
    handleQuery() {
      // ä»…筛选显示,不需要重新加载
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
@@ -414,7 +414,7 @@
                  </template>
                </el-table-column>
                <el-table-column
                <!-- <el-table-column
                  label="趋势"
                  prop="trend"
                  align="center"
@@ -446,7 +446,7 @@
                      }}</span>
                    </div>
                  </template>
                </el-table-column>
                </el-table-column> -->
                <el-table-column
                  label="操作"
@@ -790,11 +790,11 @@
      Object.entries(apiData.rows).forEach(([typeName, typeStat]) => {
        const sendCount = typeStat.subidAll || 0;
        const receiveCount = typeStat.fillCountAll || 0;
        const recoveryRate = typeStat.receiveRate || 0;
        const recoveryRate = typeStat.receiveRate.toFixed(2) || 0;
        chartData.push({
          name: typeName,
          value: recoveryRate * 100, // è½¬æ¢ä¸ºç™¾åˆ†æ¯”
          value: (recoveryRate * 100).toFixed(2), // è½¬æ¢ä¸ºç™¾åˆ†æ¯”
          sendCount: sendCount,
          receiveCount: receiveCount,
          averageScore: typeStat.averageScore || 0,
@@ -914,7 +914,9 @@
        const response = await satisfactionGraph(params);
        if (response.code === 200) {
          this.processTypeDetailData(response.data);
          this.processTypeDetailData(response);
          console.log(11);
        } else {
          this.$message.error(response.msg || "获取类型明细数据失败");
          const mockData = await this.generateMockTypeDetail();
@@ -944,7 +946,7 @@
      Object.entries(apiData.rows).forEach(([typeName, typeStat], index) => {
        const sendCount = typeStat.subidAll || 0;
        const receiveCount = typeStat.fillCountAll || 0;
        const recoveryRate = typeStat.receiveRate || 0;
        const recoveryRate = typeStat.receiveRate.toFixed(2) || 0;
        const averageScore = typeStat.averageScore || 0;
        typeDetail.push({
@@ -963,6 +965,8 @@
      });
      this.typeDetailData = typeDetail;
      console.log(this.typeDetailData,'this.typeDetailData');
      this.calculateTypeSummary(typeDetail);
    },
@@ -972,7 +976,7 @@
      if (score >= 4.0) return "良好";
      if (score >= 3.0) return "一般";
      if (score >= 2.0) return "较差";
      return "å·®";
      return "未知";
    },
    // èŽ·å–è¶‹åŠ¿
@@ -1041,24 +1045,24 @@
    // æ¸²æŸ“图表
    renderChart(chartData) {
      if (!this.barChart) return;
 if (!chartData || chartData.length === 0) {
    const emptyOption = {
      title: {
        text: "暂无数据",
        left: "center",
        top: "center",
        textStyle: {
          color: "#999",
          fontSize: 16,
          fontWeight: "normal"
        }
      },
      xAxis: { show: false },
      yAxis: { show: false }
    };
    this.barChart.setOption(emptyOption);
    return;
  }
      if (!chartData || chartData.length === 0) {
        const emptyOption = {
          title: {
            text: "暂无数据",
            left: "center",
            top: "center",
            textStyle: {
              color: "#999",
              fontSize: 16,
              fontWeight: "normal",
            },
          },
          xAxis: { show: false },
          yAxis: { show: false },
        };
        this.barChart.setOption(emptyOption);
        return;
      }
      const option = {
        title: {
          text: "",
@@ -1079,7 +1083,7 @@
                <span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${
                  data.color
                };margin-right:5px;"></span>
                å¡«æŠ¥æ¯”例: <strong>${data.value.toFixed(1)}%</strong>
                å¡«æŠ¥æ¯”例: <strong>${data.value}%</strong>
              </div>
              <div style="margin: 2px 0;">
                <span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#eee;margin-right:5px;"></span>
@@ -1437,12 +1441,12 @@
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
   if (value === null || value === undefined) return "-";
  const num = parseFloat(value);
  if (isNaN(num)) return "-";
  // å¦‚果值小于1,认为是小数比例,需要乘以100
  const percentValue = num < 1 ? num * 100 : num;
  return `${percentValue.toFixed(2)}%`;
      if (value === null || value === undefined) return "-";
      const num = parseFloat(value);
      if (isNaN(num)) return "-";
      // å¦‚果值小于1,认为是小数比例,需要乘以100
      const percentValue = num < 1 ? num * 100 : num;
      return `${percentValue.toFixed(2)}%`;
    },
    // èŽ·å–å›žæ”¶çŽ‡æ ·å¼ç±»
@@ -1459,7 +1463,7 @@
        è‰¯å¥½: "primary",
        ä¸€èˆ¬: "warning",
        è¾ƒå·®: "danger",
        å·®: "info",
        æœªçŸ¥: "info",
      };
      return levelMap[level] || "info";
    },
src/views/followvisit/HistoricalFollow/index.vue
@@ -662,7 +662,7 @@
          value: 4,
          label: "不执行",
        },
         {
        {
          value: 5,
          label: "发送失败",
        },
@@ -670,7 +670,7 @@
          value: 6,
          label: "已完成",
        },
         {
        {
          value: 7,
          label: "超时",
        },
@@ -1147,7 +1147,7 @@
    /** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
    handleExport() {
      console.log(this.topqueryParams);
      this.topqueryParams.pageSize = null;
      this.download(
        // "smartor/serviceSubtask/export",
        "smartor/serviceSubtask/getSubtaskByDiagnameExport",
src/views/followvisit/again/index.vue
@@ -180,7 +180,7 @@
        <el-col :span="1.5">
          <el-button
            type="primary"
                        icon="el-icon-plus"
            icon="el-icon-plus"
            size="medium"
            @click="handleAdd"
            >新增</el-button
@@ -289,6 +289,15 @@
          </template>
        </el-table-column>
        <el-table-column
          label="诊断名称"
          align="center"
          key="leavediagname"
          prop="leavediagname"
          width="120"
          :show-overflow-tooltip="true"
        >
        </el-table-column>
        <el-table-column
          label="任务状态"
          align="center"
          key="sendstate"
@@ -325,15 +334,13 @@
                  >发送失败</el-tag
                >
              </div>
            <div v-if="scope.row.sendstate == 6">
              <div v-if="scope.row.sendstate == 6">
                <el-tag type="success" :disable-transitions="false"
                  >已完成</el-tag
                >
              </div>
              <div v-if="scope.row.sendstate == 7">
                <el-tag type="danger" :disable-transitions="false"
                  >超时</el-tag
                >
                <el-tag type="danger" :disable-transitions="false">超时</el-tag>
              </div>
            </el-tooltip>
          </template>
@@ -472,15 +479,6 @@
        >
        </el-table-column>
        <el-table-column
          label="诊断名称"
          align="center"
          key="leavediagname"
          prop="leavediagname"
          width="120"
          :show-overflow-tooltip="true"
        >
        </el-table-column>
        <el-table-column
          label="随访人员"
          align="center"
@@ -649,7 +647,7 @@
            </el-form-item>
          </el-col>
        </el-row>
<el-row >
        <el-row>
          <el-col :span="8">
            <el-form-item label="过滤医生" width="100" prop="filterDrname">
              <el-input
@@ -1035,7 +1033,7 @@
          value: 4,
          label: "不执行",
        },
         {
        {
          value: 5,
          label: "发送失败",
        },
@@ -1043,7 +1041,7 @@
          value: 6,
          label: "已完成",
        },
         {
        {
          value: 7,
          label: "超时",
        },
@@ -1214,7 +1212,7 @@
      });
    },
    affiliation() {
      this.topqueryParams.managementDoctorCode= store.getters.hisUserId;
      this.topqueryParams.managementDoctorCode = store.getters.hisUserId;
      this.getList(1);
    },
@@ -1488,7 +1486,7 @@
            .then((response) => {
              console.log(response);
            })
              .then(() => {
            .then(() => {
              this.getList(1);
              this.$modal.msgSuccess("患者过滤成功");
            });
@@ -1557,11 +1555,11 @@
    },
    // è·³è½¬è¯¦æƒ…页
    Seedetails(row) {
    let type = "";
      let type = "";
      console.log(row, "rwo");
        if (row.type == 1) {
          type = 1;
        }
      if (row.type == 1) {
        type = 1;
      }
      this.$router.push({
        path: "/followvisit/record/detailpage/",
        query: {
@@ -1787,11 +1785,11 @@
  }
}
::v-deep.leftvlue .el-card__body {
  background: #F2F8FF;
  color: #324A9B;
  background: #f2f8ff;
  color: #324a9b;
}
::v-deep.leftvlue .el-card__body:hover {
  background: #3664D9;
  background: #3664d9;
  color: #fff;
  cursor: pointer; /* é¼ æ ‡æ‚¬æµ®æ—¶å˜ä¸ºæ‰‹å½¢ */
}
src/views/followvisit/discharge/index.vue
@@ -210,7 +210,7 @@
      </el-form>
      <el-divider></el-divider>
      <el-row :gutter="10" class="mb8">
        <!-- <el-col :span="1.5">
        <el-col :span="1.5">
          <div class="documentf">
            <div class="document">
              <el-button
@@ -223,7 +223,7 @@
              >
            </div>
          </div>
        </el-col> -->
        </el-col>
        <el-col :span="1.5">
          <el-button
            type="primary"
@@ -2047,7 +2047,7 @@
        {
          ...this.topqueryParams,
        },
        `user_${new Date().getTime()}.xlsx`
        `user_${new Date().getTime()}.xlsx`,
      );
    },
    // å¼‚常列渲染
src/views/followvisit/record/detailpage/index.vue
@@ -2286,35 +2286,7 @@
        })
        .catch(() => {});
    },
    aahandleOptionChange(a, b, c) {
      const result = c.find((item) => item.optioncontent == a);
      if (result.nextQuestion == 0) {
        this.tableDatatop = this.tableDatatop.reduce((acc, item, i) => {
          acc.push(i > b ? { ...item, astrict: 1 } : item);
          return acc;
        }, []);
      } else {
        this.tableDatatop = this.tableDatatop.reduce((acc, item, i) => {
          acc.push(i > b ? { ...item, astrict: 0 } : item);
          return acc;
        }, []);
      }
      if (this.Voicetype) {
        var obj = this.tableDatatop[b].ivrTaskScriptTargetoptionList.find(
          (item) => item.optioncontent == a
        );
      } else {
        var obj = this.tableDatatop[b].svyTaskTemplateTargetoptions.find(
          (item) => item.optioncontent == a
        );
      }
      if (obj.isabnormal) {
        this.tableDatatop[b].isabnormal = true;
      } else {
        this.tableDatatop[b].isabnormal = false;
      }
      this.$forceUpdate();
    },
    handleRadioToggles(questionItem, optionValue) {
      if (!questionItem.matchedtext) {
        questionItem.matchedtext == "";
@@ -2373,7 +2345,13 @@
      this.tableDatatop[questionIndex].showAppendInput =
        selectedOptionObj.appendflag == 1;
      console.log(this.tableDatatop);
      if (
        selectedOptionObj.nextQuestion !== undefined &&
        selectedOptionObj.nextQuestion !== null
      ) {
        this.tableDatatop[questionIndex].nextScriptno =
          selectedOptionObj.nextQuestion;
      }
      // if (!this.tableDatatop[questionIndex].showAppendInput) {
      //   this.tableDatatop[questionIndex].answerps = ""; // æ¸…除附加信息
      // }
@@ -2443,7 +2421,6 @@
          hiddenByEnd: index === questionIndex + 1 ? false : item.hiddenByEnd,
        }));
      }
      2;
      this.$forceUpdate();
    },
src/views/sfstatistics/percentage/components/ChartDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,579 @@
<template>
  <el-dialog
    title="随访统计趋势图"
    :visible.sync="visible"
    width="80%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
    <div class="chart-container">
      <el-row :gutter="20">
        <el-col :span="12">
          <div class="chart-title">随访状态分布</div>
          <div id="pieChart" style="width: 100%; height: 400px"></div>
        </el-col>
        <el-col :span="12">
          <div class="chart-title">随访趋势分析</div>
          <div id="barLineChart" style="width: 100%; height: 400px"></div>
        </el-col>
      </el-row>
    </div>
  </el-dialog>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'ChartDialog',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    data: {
      type: Array,
      default: () => []
    },
    activeTab: {
      type: String,
      default: 'first'
    }
  },
  data() {
    return {
      pieChart: null,
      barLineChart: null
    }
  },
  watch: {
    visible(newVal) {
      if (newVal) {
        this.$nextTick(() => {
          this.initCharts()
        })
      } else {
        this.destroyCharts()
      }
    }
  },
  mounted() {
    if (this.visible) {
      this.$nextTick(() => {
        this.initCharts()
      })
    }
  },
  beforeDestroy() {
    this.destroyCharts()
  },
  methods: {
    initCharts() {
      this.initPieChart()
      this.initBarLineChart()
    },
    initPieChart() {
      const pieDom = document.getElementById('pieChart')
      if (!pieDom) return
      if (this.pieChart) {
        this.pieChart.dispose()
      }
      this.pieChart = echarts.init(pieDom)
      // æ ¹æ®å½“前tab计算饼图数据
      const pieData = this.getPieChartData()
      const pieOption = {
        title: {
          text: '随访状态分布',
          left: 'center',
          textStyle: {
            color: '#333',
            fontSize: 16
          }
        },
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          left: 'left',
          data: pieData.legendData,
          textStyle: {
            color: '#666'
          }
        },
        color: ['#FF9D4D', '#36B37E', '#FF5C5C'],
        series: [
          {
            name: '随访状态',
            type: 'pie',
            radius: ['40%', '70%'],
            avoidLabelOverlap: true,
            itemStyle: {
              borderRadius: 10,
              borderColor: '#fff',
              borderWidth: 2
            },
            label: {
              show: true,
              formatter: '{b}: {c} ({d}%)',
              color: '#333'
            },
            emphasis: {
              label: {
                show: true,
                fontSize: '18',
                fontWeight: 'bold'
              },
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.5)'
              }
            },
            data: pieData.seriesData
          }
        ]
      }
      this.pieChart.setOption(pieOption)
      window.addEventListener('resize', this.resizePieChart)
    },
    getPieChartData() {
      let legendData = []
      let seriesData = []
      if (this.activeTab === 'first') {
        legendData = ['待随访', '随访成功', '随访失败']
        const followUpData = {
          pending: 0,
          success: 0,
          fail: 0
        }
        this.data.forEach((item) => {
          followUpData.pending += item.pendingFollowUp || 0
          followUpData.success += item.followUpSuccess || 0
          followUpData.fail += item.followUpFail || 0
        })
        seriesData = [
          { value: followUpData.pending, name: '待随访' },
          { value: followUpData.success, name: '随访成功' },
          { value: followUpData.fail, name: '随访失败' }
        ]
      } else if (this.activeTab === 'second') {
        legendData = ['待随访(再次)', '随访成功(再次)', '随访失败(再次)']
        const followUpData = {
          pending: 0,
          success: 0,
          fail: 0
        }
        this.data.forEach((item) => {
          followUpData.pending += item.pendingFollowUpAgain || 0
          followUpData.success += item.followUpSuccessAgain || 0
          followUpData.fail += item.followUpFailAgain || 0
        })
        seriesData = [
          { value: followUpData.pending, name: '待随访(再次)' },
          { value: followUpData.success, name: '随访成功(再次)' },
          { value: followUpData.fail, name: '随访失败(再次)' }
        ]
      } else if (this.activeTab === 'continued') {
        legendData = ['护理完成', '护理进行中', '护理未开始']
        const careData = {
          completed: 0,
          inProgress: 0,
          notStarted: 0
        }
        this.data.forEach((item) => {
          careData.completed += item.careCompleted || 0
          careData.inProgress += item.careInProgress || 0
          careData.notStarted += item.careNotStarted || 0
        })
        seriesData = [
          { value: careData.completed, name: '护理完成' },
          { value: careData.inProgress, name: '护理进行中' },
          { value: careData.notStarted, name: '护理未开始' }
        ]
      }
      return { legendData, seriesData }
    },
    initBarLineChart() {
      const barDom = document.getElementById('barLineChart')
      if (!barDom) return
      if (this.barLineChart) {
        this.barLineChart.dispose()
      }
      this.barLineChart = echarts.init(barDom)
      // å‡†å¤‡æ•°æ®
      const chartData = this.getBarLineChartData()
      const option = {
        title: {
          text: `${chartData.title}趋势`,
          left: 'center',
          textStyle: {
            color: '#333',
            fontSize: 16
          }
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross',
            crossStyle: {
              color: '#999'
            }
          }
        },
        legend: {
          data: chartData.legendData,
          top: 'bottom',
          textStyle: {
            color: '#666'
          }
        },
        color: chartData.colors,
        xAxis: {
          type: 'category',
          data: chartData.categories,
          axisLabel: {
            interval: 0,
            rotate: 30,
            color: '#666'
          },
          axisLine: {
            lineStyle: {
              color: '#ddd'
            }
          }
        },
        yAxis: [
          {
            type: 'value',
            name: chartData.yAxisName1,
            min: 0,
            axisLabel: {
              color: '#666'
            },
            axisLine: {
              lineStyle: {
                color: '#ddd'
              }
            },
            splitLine: {
              lineStyle: {
                color: '#f0f0f0'
              }
            }
          },
          {
            type: 'value',
            name: '百分比(%)',
            min: 0,
            max: 100,
            axisLabel: {
              color: '#666',
              formatter: '{value}%'
            },
            axisLine: {
              lineStyle: {
                color: '#ddd'
              }
            },
            splitLine: {
              show: false
            }
          }
        ],
        series: chartData.series,
        grid: {
          top: '15%',
          left: '3%',
          right: '4%',
          bottom: '15%',
          containLabel: true
        }
      }
      this.barLineChart.setOption(option)
      window.addEventListener('resize', this.resizeBarLineChart)
    },
    getBarLineChartData() {
      const categories = this.data.map(
        (item) => item.leavehospitaldistrictname || item.deptname
      )
      let title = '科室/病区'
      let yAxisName1 = '人次'
      let legendData = []
      let colors = []
      let series = []
      if (this.activeTab === 'first') {
        title = '首次随访'
        yAxisName1 = '人次'
        legendData = ['出院人次', '应随访人次', '随访率(%)', '及时率(%)']
        colors = ['#5470C6', '#91CC75', '#EE6666', '#9A60B4']
        const dischargeData = this.data.map((item) => item.dischargeCount || 0)
        const followUpData = this.data.map((item) => item.followUpNeeded || 0)
        const followUpRateData = this.data.map((item) => {
          if (!item.followUpRate) return 0
          const rateStr = String(item.followUpRate).replace('%', '')
          return parseFloat(rateStr) || 0
        })
        const timelyRateData = this.data.map((item) =>
          item.rate ? (Number(item.rate) * 100).toFixed(2) : 0
        )
        series = [
          {
            name: '出院人次',
            type: 'bar',
            barWidth: '25%',
            data: dischargeData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0]
            }
          },
          {
            name: '应随访人次',
            type: 'bar',
            barWidth: '25%',
            data: followUpData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0]
            }
          },
          {
            name: '随访率(%)',
            type: 'line',
            yAxisIndex: 1,
            data: followUpRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 80,
                  lineStyle: {
                    color: '#EE6666',
                    type: 'dashed'
                  }
                }
              ]
            }
          },
          {
            name: '及时率(%)',
            type: 'line',
            yAxisIndex: 1,
            data: timelyRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3,
              type: 'dotted'
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 90,
                  lineStyle: {
                    color: '#9A60B4',
                    type: 'dashed'
                  }
                }
              ]
            }
          }
        ]
      } else if (this.activeTab === 'second') {
        title = '再次随访'
        yAxisName1 = '人次'
        legendData = ['出院人次', '应随访人次', '随访率(%)']
        colors = ['#5470C6', '#91CC75', '#EE6666']
        const dischargeData = this.data.map((item) => item.dischargeCount || 0)
        const followUpData = this.data.map((item) => item.followUpNeeded || 0)
        const followUpRateAgainData = this.data.map((item) => {
          if (!item.followUpRateAgain) return 0
          const rateStr = String(item.followUpRateAgain).replace('%', '')
          return parseFloat(rateStr) || 0
        })
        series = [
          {
            name: '出院人次',
            type: 'bar',
            barWidth: '25%',
            data: dischargeData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0]
            }
          },
          {
            name: '应随访人次',
            type: 'bar',
            barWidth: '25%',
            data: followUpData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0]
            }
          },
          {
            name: '随访率(%)',
            type: 'line',
            yAxisIndex: 1,
            data: followUpRateAgainData,
            symbolSize: 8,
            lineStyle: {
              width: 3
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 80,
                  lineStyle: {
                    color: '#EE6666',
                    type: 'dashed'
                  }
                }
              ]
            }
          }
        ]
      } else if (this.activeTab === 'continued') {
        title = '延续护理'
        yAxisName1 = '人次'
        legendData = ['延续护理人次', '护理完成', '完成率(%)']
        colors = ['#5470C6', '#91CC75', '#EE6666']
        const continuedCareData = this.data.map((item) => item.continuedCareCount || 0)
        const careCompletedData = this.data.map((item) => item.careCompleted || 0)
        const completionRateData = this.data.map((item) => {
          if (!item.completionRate) return 0
          const rateStr = String(item.completionRate).replace('%', '')
          return parseFloat(rateStr) || 0
        })
        series = [
          {
            name: '延续护理人次',
            type: 'bar',
            barWidth: '25%',
            data: continuedCareData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0]
            }
          },
          {
            name: '护理完成',
            type: 'bar',
            barWidth: '25%',
            data: careCompletedData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0]
            }
          },
          {
            name: '完成率(%)',
            type: 'line',
            yAxisIndex: 1,
            data: completionRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 85,
                  lineStyle: {
                    color: '#EE6666',
                    type: 'dashed'
                  }
                }
              ]
            }
          }
        ]
      }
      return {
        title,
        yAxisName1,
        categories,
        legendData,
        colors,
        series
      }
    },
    resizePieChart() {
      if (this.pieChart) {
        this.pieChart.resize()
      }
    },
    resizeBarLineChart() {
      if (this.barLineChart) {
        this.barLineChart.resize()
      }
    },
    destroyCharts() {
      if (this.pieChart) {
        this.pieChart.dispose()
        this.pieChart = null
      }
      if (this.barLineChart) {
        this.barLineChart.dispose()
        this.barLineChart = null
      }
      window.removeEventListener('resize', this.resizePieChart)
      window.removeEventListener('resize', this.resizeBarLineChart)
    },
    handleClose() {
      this.destroyCharts()
      this.$emit('close')
    }
  }
}
</script>
<style lang="scss" scoped>
.chart-container {
  .chart-title {
    text-align: center;
    font-size: 16px;
    font-weight: bold;
    margin-bottom: 20px;
    color: #333;
  }
}
</style>
src/views/sfstatistics/percentage/components/ContinuedCare.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,633 @@
<template>
  <div class="continued-care">
    <div class="your-table-container">
      <el-table
        v-loading="loading"
        :data="tableData"
        :border="true"
        show-summary
        :summary-method="getSummaries"
      >
        <!-- è¡¨æ ¼åˆ—定义 -->
        <el-table-column
          label="序号"
          type="index"
          align="center"
          width="60"
        />
        <el-table-column
          label="病区名称"
          align="center"
          prop="wardName"
          width="200"
          :show-overflow-tooltip="true"
        />
        <el-table-column
          label="已延续数量"
          align="center"
          prop="continuedCount"
        >
          <template slot-scope="scope">
            <el-button
              size="medium"
              type="text"
              @click="handleViewDetails(scope.row, 'continued', '已延续列表')"
            >
              <span class="button-zx">{{ scope.row.continuedCount }}</span>
            </el-button>
          </template>
        </el-table-column>
        <el-table-column
          label="未延续数量"
          align="center"
          prop="unContinuedCount"
        >
          <template slot-scope="scope">
            <el-button
              size="medium"
              type="text"
              @click="handleViewDetails(scope.row, 'uncontinued', '未延续列表')"
            >
              <span class="button-zx">{{ scope.row.unContinuedCount }}</span>
            </el-button>
          </template>
        </el-table-column>
        <el-table-column
          label="延续率"
          align="center"
          prop="continuedRate"
          width="120"
        />
        <!-- <el-table-column
          label="操作"
          align="center"
          width="150"
        >
          <template slot-scope="scope">
            <el-button
              size="small"
              type="primary"
              @click="handleRowClick(scope.row)"
            >
              æŸ¥çœ‹æŠ¤å£«è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column> -->
      </el-table>
    </div>
    <!-- æŠ¤å£«è¯¦æƒ…弹窗 -->
    <el-dialog
      title="护士延续护理详情"
      :visible.sync="nurseDialogVisible"
      width="80%"
      :close-on-click-modal="false"
    >
      <div v-if="currentWardData">
        <el-table
          v-loading="nurseLoading"
          :data="nurseData"
          :border="true"
        >
          <el-table-column
            label="护士姓名"
            prop="nurseName"
            align="center"
            width="120"
          />
          <el-table-column
            label="科室"
            prop="deptName"
            align="center"
            width="150"
          />
          <el-table-column
            label="已延续数量"
            prop="continuedCount"
            align="center"
          />
          <el-table-column
            label="未延续数量"
            prop="unContinuedCount"
            align="center"
          />
          <el-table-column
            label="延续率"
            prop="continuedRate"
            align="center"
            width="120"
          />
        </el-table>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import { getContinueNerseCount, getNurseContinuedDetail } from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
export default {
  name: "ContinuedCare",
  props: {
    queryParams: {
      type: Object,
      required: true,
    },
    flatArrayhospit: {
      type: Array,
      default: () => [],
    },
    flatArraydept: {
      type: Array,
      default: () => [],
    },
    options: {
      type: Array,
      default: () => [],
    },
    orgname: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      tableData: [],
      loading: false,
      nurseDialogVisible: false,
      nurseLoading: false,
      currentWardData: null,
      nurseData: [],
      originalData: {},
      totalData: {
        continued: 0,
        uncontinued: 0
      }
    };
  },
  methods: {
    loadData() {
      this.loading = true;
      const params = {
        leavehospitaldistrictcodes:
          this.queryParams.leavehospitaldistrictcodes.includes("all")
            ? this.getAllWardCodes()
            : this.queryParams.leavehospitaldistrictcodes,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.getAllDeptCodes()
          : this.queryParams.deptcodes,
      };
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      getContinueNerseCount(params)
        .then((response) => {
          this.originalData = response.data;
          this.processData(response.data);
        })
        .catch((error) => {
          console.error("获取延续护理数据失败:", error);
          this.$message.error("获取延续护理数据失败");
        })
        .finally(() => {
          this.loading = false;
        });
    },
    processData(data) {
      this.totalData = {
        continued: data.已延续总数量 || 0,
        uncontinued: data.未延续总数量 || 0
      };
      const processedData = [];
      if (data.详情 && Array.isArray(data.详情)) {
        data.详情.forEach((item) => {
          // æå–病区名称和数据
          Object.keys(item).forEach(key => {
            if (key.startsWith('已延续_')) {
              const wardName = key.replace('已延续_', '');
              const continuedCount = item[key];
              const unContinuedCount = item[`未延续_${wardName}`] || 0;
              const total = continuedCount + unContinuedCount;
              const continuedRate = total > 0 ? ((continuedCount / total) * 100).toFixed(2) + '%' : '0.00%';
              processedData.push({
                wardName,
                continuedCount,
                unContinuedCount,
                continuedRate,
                originalData: item
              });
            }
          });
        });
      }
      // æŽ’序
      this.tableData = this.customSort(processedData);
    },
    getAllWardCodes() {
      return this.flatArrayhospit
        .filter((item) => item.value !== "all")
        .map((item) => item.value);
    },
    getAllDeptCodes() {
      return this.flatArraydept
        .filter((item) => item.value !== "all")
        .map((item) => item.value);
    },
    customSort(data) {
      const order = [
        "一", "二", "三", "四", "五", "六", "七", "八", "九", "十",
        "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
        "二十一", "二十二", "二十三", "二十四", "二十五", "二十六", "二十七", "二十八", "二十九", "三十",
        "三十一", "三十二", "三十三", "三十四", "三十五", "三十六", "三十七", "三十八", "三十九", "四十",
        "四十一", "四十二", "四十三", "四十四", "四十五"
      ];
      return data.sort((a, b) => {
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          const chineseMatch = name.match(/^(\d+)-/);
          if (chineseMatch && chineseMatch[1]) {
            const num = parseInt(chineseMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1;
            }
          }
          // å°è¯•匹配中文数字
          for (let i = 0; i < order.length; i++) {
            if (name.includes(order[i])) {
              return i;
            }
          }
          return -1;
        };
        const indexA = getIndex(a.wardName);
        const indexB = getIndex(b.wardName);
        if (indexA === -1 && indexB === -1) {
          return (a.wardName || "").localeCompare(b.wardName || "");
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    getSummaries(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计";
          return;
        }
        if (index === 1) { // ç—…区名称列
          sums[index] = "/";
          return;
        }
        if (index === 2) { // å·²å»¶ç»­æ•°é‡åˆè®¡
          const totalContinued = this.tableData.reduce((sum, item) => sum + item.continuedCount, 0);
          sums[index] = this.formatNumber(totalContinued);
        } else if (index === 3) { // æœªå»¶ç»­æ•°é‡åˆè®¡
          const totalUnContinued = this.tableData.reduce((sum, item) => sum + item.unContinuedCount, 0);
          sums[index] = this.formatNumber(totalUnContinued);
        } else if (index === 4) { // å»¶ç»­çŽ‡å¹³å‡å€¼
          const totalContinued = this.tableData.reduce((sum, item) => sum + item.continuedCount, 0);
          const totalUnContinued = this.tableData.reduce((sum, item) => sum + item.unContinuedCount, 0);
          const total = totalContinued + totalUnContinued;
          const avgRate = total > 0 ? ((totalContinued / total) * 100).toFixed(2) + '%' : '0.00%';
          sums[index] = avgRate;
        } else {
          sums[index] = "";
        }
      });
      return sums;
    },
    formatNumber(num) {
      if (isNaN(num)) return "-";
      return Number.isInteger(num) ? num.toString() : num.toFixed(0);
    },
    handleViewDetails(row, type, titleSuffix) {
      const title = `${row.wardName}${titleSuffix}`;
      // è¿™é‡Œéœ€è¦æ ¹æ®ä½ çš„实际情况获取详情数据
      // ä½ å¯ä»¥ä»Žrow.originalData中获取,或者调用新的API
      const detailData = this.getMockDetailData(row, type);
      this.$emit("view-details", detailData, title);
    },
    handleRowClick(row) {
      this.currentWardData = row;
      this.nurseDialogVisible = true;
      this.loadNurseData(row.wardName);
    },
    async loadNurseData(wardName) {
      this.nurseLoading = true;
      try {
        const params = {
          wardName: wardName,
          startTime: this.queryParams.startTime,
          endTime: this.queryParams.endTime
        };
        const response = await getNurseContinuedDetail(params);
        this.nurseData = this.processNurseData(response.data);
      } catch (error) {
        console.error("获取护士详情失败:", error);
        this.$message.error("获取护士详情失败");
      } finally {
        this.nurseLoading = false;
      }
    },
    processNurseData(data) {
      // æ ¹æ®ä½ çš„实际API响应结构处理数据
      // è¿™é‡Œæ˜¯ä¸€ä¸ªç¤ºä¾‹
      return [
        {
          nurseName: "护士A",
          deptName: this.currentWardData.wardName,
          continuedCount: Math.floor(this.currentWardData.continuedCount * 0.3),
          unContinuedCount: Math.floor(this.currentWardData.unContinuedCount * 0.3),
          continuedRate: "75.00%"
        },
        {
          nurseName: "护士B",
          deptName: this.currentWardData.wardName,
          continuedCount: Math.floor(this.currentWardData.continuedCount * 0.4),
          unContinuedCount: Math.floor(this.currentWardData.unContinuedCount * 0.4),
          continuedRate: "80.00%"
        },
        {
          nurseName: "护士C",
          deptName: this.currentWardData.wardName,
          continuedCount: Math.floor(this.currentWardData.continuedCount * 0.3),
          unContinuedCount: Math.floor(this.currentWardData.unContinuedCount * 0.3),
          continuedRate: "70.00%"
        }
      ];
    },
    getMockDetailData(row, type) {
      // æ¨¡æ‹Ÿè¯¦æƒ…数据,实际应该调用API
      const detailData = [];
      const count = type === 'continued' ? row.continuedCount : row.unContinuedCount;
      for (let i = 1; i <= Math.min(count, 10); i++) {
        detailData.push({
          sendname: `患者${i}`,
          taskName: `${row.wardName}延续护理`,
          sendstate: type === 'continued' ? 6 : 2,
          preachform: ["人工随访", "电话随访"],
          visitTime: "2024-01-15 10:00:00",
          finishtime: type === 'continued' ? "2024-01-15 11:00:00" : "",
          endtime: "2024-01-10 09:00:00",
          nurseName: "护士A",
          drname: "医生A",
          excep: 1,
          suggest: 2,
          templatename: "延续护理服务模板",
          remark: type === 'continued' ? "已完成延续护理" : "未开始延续护理",
          bankcardno: "已完成"
        });
      }
      return detailData;
    },
    async exportTable() {
      try {
        let dateRangeString = "";
        let sheetNameSuffix = "";
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateStr = this.queryParams.dateRange[0];
          const endDateStr = this.queryParams.dateRange[1];
          const formatDateForDisplay = (dateTimeStr) => {
            return dateTimeStr.split(" ")[0];
          };
          const startDateFormatted = formatDateForDisplay(startDateStr);
          const endDateFormatted = formatDateForDisplay(endDateStr);
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
        } else {
          const now = new Date();
          const currentMonth = now.getMonth() + 1;
          dateRangeString = `${currentMonth}月`;
          sheetNameSuffix = `${currentMonth}月`;
        }
        const excelName = `延续护理统计表_${dateRangeString}.xlsx`;
        const worksheetName = `延续护理统计_${sheetNameSuffix}`;
        if (!this.tableData || this.tableData.length === 0) {
          this.$message.warning("暂无延续护理数据可导出");
          return false;
        }
        const workbook = new ExcelJS.Workbook();
        const worksheet = workbook.addWorksheet(worksheetName);
        this.buildExportSheet(worksheet, sheetNameSuffix);
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
        this.$message.success("导出成功");
        return true;
      } catch (error) {
        console.error("导出失败:", error);
        this.$message.error(`导出失败: ${error.message}`);
        return false;
      }
    },
    buildExportSheet(worksheet, sheetNameSuffix) {
      const titleStyle = {
        font: {
          name: "微软雅黑",
          size: 16,
          bold: true,
          color: { argb: "FF000000" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFE6F3FF" },
        },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const headerStyle = {
        font: {
          name: "微软雅黑",
          size: 11,
          bold: true,
          color: { argb: "FF000000" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF5F7FA" },
        },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const cellStyle = {
        font: { name: "宋体", size: 10, color: { argb: "FF000000" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const summaryStyle = {
        font: {
          name: "宋体",
          size: 10,
          bold: true,
          color: { argb: "FF409EFF" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF5F7FA" },
        },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      // æ·»åŠ æ ‡é¢˜è¡Œ
      worksheet.mergeCells(1, 1, 1, 6);
      const titleCell = worksheet.getCell(1, 1);
      titleCell.value = `延续护理统计表_${sheetNameSuffix}`;
      titleCell.style = titleStyle;
      worksheet.getRow(1).height = 35;
      // è¡¨å¤´
      const headers = ["序号", "病区名称", "已延续数量", "未延续数量", "延续率", "操作"];
      headers.forEach((header, index) => {
        const cell = worksheet.getCell(2, index + 1);
        cell.value = header;
        cell.style = headerStyle;
      });
      worksheet.getRow(2).height = 25;
      // æ•°æ®è¡Œ
      this.tableData.forEach((item, rowIndex) => {
        const dataRow = worksheet.addRow(
          [
            rowIndex + 1,
            item.wardName,
            item.continuedCount,
            item.unContinuedCount,
            item.continuedRate,
            "查看护士详情"
          ],
          rowIndex + 3
        );
        dataRow.eachCell((cell) => {
          cell.style = cellStyle;
        });
        dataRow.height = 24;
      });
      // åˆè®¡è¡Œ
      const totalContinued = this.tableData.reduce((sum, item) => sum + item.continuedCount, 0);
      const totalUnContinued = this.tableData.reduce((sum, item) => sum + item.unContinuedCount, 0);
      const total = totalContinued + totalUnContinued;
      const totalRate = total > 0 ? ((totalContinued / total) * 100).toFixed(2) + '%' : '0.00%';
      const summaryRow = worksheet.addRow([
        "合计",
        "/",
        totalContinued,
        totalUnContinued,
        totalRate,
        "/"
      ]);
      summaryRow.eachCell((cell, colNumber) => {
        cell.style = summaryStyle;
      });
      summaryRow.height = 28;
      // åˆ—宽
      worksheet.columns = [
        { width: 8 },
        { width: 30 },
        { width: 15 },
        { width: 15 },
        { width: 12 },
        { width: 15 }
      ];
    }
  }
};
</script>
<style lang="scss" scoped>
.continued-care {
  .your-table-container {
    margin-top: 10px;
  }
  .button-zx {
    color: rgb(70, 204, 238);
  }
}
</style>
src/views/sfstatistics/percentage/components/DetailDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,302 @@
<template>
  <el-dialog
    :title="title"
    :visible.sync="visible"
    v-loading="loading"
    width="70%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
    <div class="detail-dialog">
      <div style="margin-bottom: 16px; display: flex; align-items: center">
        <span style="margin-right: 10px; font-weight: bold">患者姓名查询:</span>
        <el-input
          v-model="searchName"
          placeholder="请输入患者姓名进行筛选"
          clearable
          style="width: 300px"
          @input="handleSearch"
          @clear="handleSearch"
        />
        <span style="margin-left: 10px; color: rgb(35, 81, 233); font-size: 16px">
          å…± {{ displayList.length }} æ¡è®°å½•
        </span>
      </div>
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <div class="data-list" ref="dataList" @scroll="handleScroll" v-loading="loading">
              <el-table :data="currentDisplayList" height="660" style="width: 100%">
                <el-table-column prop="sendname" align="center" label="姓名" width="100" />
                <el-table-column prop="taskName" align="center" width="200" show-overflow-tooltip label="任务名称" />
                <el-table-column prop="sendstate" align="center" width="200" label="任务状态">
                  <template slot-scope="scope">
                    <el-tag
                      :type="getStateTagType(scope.row.sendstate)"
                      :disable-transitions="false"
                    >
                      {{ getStateText(scope.row.sendstate) }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column
                  label="任务执行方式"
                  align="center"
                  key="preachform"
                  prop="preachform"
                  width="160"
                  :show-overflow-tooltip="true"
                >
                  <template slot-scope="scope">
                    <span v-for="(item, index) in scope.row.preachform" :key="index">
                      {{ item }}{{ index < scope.row.preachform.length - 1 ? '、' : '' }}
                    </span>
                  </template>
                </el-table-column>
                <el-table-column
                  prop="visitTime"
                  align="center"
                  label="应随访时间"
                  width="200"
                  show-overflow-tooltip
                />
                <el-table-column
                  prop="finishtime"
                  align="center"
                  label="随访完成时间"
                  width="200"
                  show-overflow-tooltip
                />
                <el-table-column label="出院日期" width="200" align="center" key="endtime" prop="endtime">
                  <template slot-scope="scope">
                    <span>{{ formatTime(scope.row.endtime) }}</span>
                  </template>
                </el-table-column>
                <el-table-column label="责任护士" width="120" align="center" key="nurseName" prop="nurseName" />
                <el-table-column label="主治医生" width="120" align="center" key="drname" prop="drname" />
                <el-table-column label="结果状态" align="center" key="excep" prop="excep" width="120">
                  <template slot-scope="scope">
                    <dict-tag :options="dict.type.sys_yujing" :value="scope.row.excep" />
                  </template>
                </el-table-column>
                <el-table-column label="处理意见" align="center" key="suggest" prop="suggest" width="120">
                  <template slot-scope="scope">
                    <dict-tag :options="dict.type.sys_suggest" :value="scope.row.suggest" />
                  </template>
                </el-table-column>
                <el-table-column prop="templatename" align="center" label="服务模板" width="200" show-overflow-tooltip />
                <el-table-column prop="remark" align="center" label="服务记录" width="200" show-overflow-tooltip />
                <el-table-column prop="bankcardno" align="center" label="呼叫状态" width="210" />
                <el-table-column label="操作" fixed="right" align="center" width="200" class-name="small-padding fixed-width">
                  <template slot-scope="scope">
                    <el-button size="medium" type="text" @click="handleDetailsGo(scope.row)">
                      <span class="button-zx">
                        <i class="el-icon-s-order"></i>查看
                      </span>
                    </el-button>
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </el-row>
        </div>
      </div>
    </div>
  </el-dialog>
</template>
<script>
export default {
  name: 'DetailDialog',
  dicts: ['sys_yujing', 'sys_suggest'],
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: ''
    },
    data: {
      type: Array,
      default: () => []
    },
    searchName: {
      type: String,
      default: ''
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      localSearchName: '',
      displayList: [],
      currentDisplayList: [],
      loadIndex: 0,
      pageSize: 100,
      isLoading: false
    }
  },
  watch: {
    data: {
      immediate: true,
      handler(newData) {
        this.initializeData(newData)
      }
    },
    searchName(newVal) {
      this.localSearchName = newVal
      this.handleSearch()
    }
  },
  mounted() {
    if (this.data && this.data.length > 0) {
      this.initializeData(this.data)
    }
  },
  methods: {
    initializeData(data) {
      this.displayList = [...data]
      this.formatPreachformData()
      this.loadIndex = 0
      this.currentDisplayList = []
      this.$nextTick(() => {
        this.loadMoreData()
      })
    },
    formatPreachformData() {
      this.displayList.forEach((item) => {
        if (item.preachform) {
          if (item.endtime) {
            item.preachformson = item.preachform
            const idArray = item.preachform.split(',')
            item.preachform = idArray.map((value) => {
              const checkboxlist = this.$store.getters.checkboxlist
              const foundItem = checkboxlist.find((item) => item.value == value)
              return foundItem ? foundItem.label : null
            }).filter(label => label !== null)
          }
        }
      })
    },
    handleSearch() {
      if (!this.localSearchName.trim()) {
        this.displayList = [...this.data]
        this.formatPreachformData()
      } else {
        const keyword = this.localSearchName.toLowerCase()
        this.displayList = this.data.filter((item) => {
          return item.sendname && item.sendname.toLowerCase().includes(keyword)
        })
        this.formatPreachformData()
      }
      this.loadIndex = 0
      this.currentDisplayList = []
      this.$nextTick(() => {
        this.loadMoreData()
      })
      this.$emit('search', this.localSearchName)
    },
    loadMoreData() {
      if (this.isLoading || this.loadIndex >= this.displayList.length) return
      this.isLoading = true
      setTimeout(() => {
        const nextChunk = this.displayList.slice(
          this.loadIndex,
          this.loadIndex + this.pageSize
        )
        this.currentDisplayList = this.currentDisplayList.concat(nextChunk)
        this.loadIndex += this.pageSize
        this.isLoading = false
      }, 200)
    },
    handleScroll(event) {
      const scrollContainer = event.target
      const isAtBottom =
        scrollContainer.scrollTop + scrollContainer.clientHeight >=
        scrollContainer.scrollHeight - 10
      if (
        isAtBottom &&
        !this.isLoading &&
        this.loadIndex < this.displayList.length
      ) {
        this.loadMoreData()
      }
    },
    getStateTagType(state) {
      const stateMap = {
        1: 'primary',  // è¡¨å•已领取
        2: 'primary',  // å¾…随访
        3: 'success',  // è¡¨å•已发送
        4: 'info',     // ä¸æ‰§è¡Œ
        5: 'danger',   // å‘送失败
        6: 'success'   // å·²å®Œæˆ
      }
      return stateMap[state] || 'info'
    },
    getStateText(state) {
      const stateTextMap = {
        1: '表单已领取',
        2: '待随访',
        3: '表单已发送',
        4: '不执行',
        5: '发送失败',
        6: '已完成'
      }
      return stateTextMap[state] || '未知状态'
    },
    formatTime(time) {
      if (!time) return ''
      return this.parseTime(time)
    },
    handleDetailsGo(row) {
      this.$emit('details-go', row)
    },
    handleClose() {
      this.$emit('close')
    }
  }
}
</script>
<style lang="scss" scoped>
.detail-dialog {
  .data-list {
    max-height: 800px;
    overflow-y: auto;
  }
  .button-zx {
    color: rgb(70, 204, 238);
  }
}
</style>
src/views/sfstatistics/percentage/components/FirstFollowUp.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,728 @@
<template>
  <div class="first-follow-up">
    <div class="your-table-container">
      <el-table
        ref="exportTable"
        id="exportTableid"
        v-loading="loading"
        :data="tableData"
        :border="true"
        @selection-change="handleSelectionChange"
        @expand-change="handleRowClick"
        :row-key="getRowKey"
        show-summary
        :summary-method="getSummaries"
        :expand-row-keys="expands"
      >
        <!-- å±•开行箭头列 -->
        <el-table-column type="expand">
          <template slot-scope="props">
            <el-table
              :data="props.row.doctorStats"
              border
              style="width: 95%; margin: 0 auto"
              class="inner-table"
              show-summary
              :summary-method="getInnerSummaries"
            >
              <el-table-column label="医生姓名" prop="drname" align="center" />
              <el-table-column label="科室" width="120" prop="deptname" align="center" />
              <el-table-column label="出院人次" prop="dischargeCount" align="center" />
              <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount" />
              <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp" prop="nonFollowUp" />
              <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded" prop="followUpNeeded" />
              <el-table-column align="center" label="首次出院随访">
                <el-table-column label="需随访" align="center" key="needFollowUp" prop="needFollowUp" />
                <el-table-column label="待随访" align="center" key="pendingFollowUp" prop="pendingFollowUp" />
                <el-table-column label="随访成功" align="center" key="followUpSuccess" prop="followUpSuccess" />
                <el-table-column label="随访失败" align="center" key="followUpFail" prop="followUpFail" />
                <el-table-column label="随访率" align="center" width="120" key="followUpRate" prop="followUpRate" />
                <el-table-column v-if="orgname != '丽水市中医院'" label="及时率" align="center" width="120" key="rate" prop="rate">
                  <template slot-scope="scope">
                    <el-button size="medium" type="text" @click="handleSeeDetails(scope.row)">
                      <span class="button-zx">{{ (Number(scope.row.rate) * 100).toFixed(2) }}%</span>
                    </el-button>
                  </template>
                </el-table-column>
                <el-table-column label="人工" align="center" key="manual" prop="manual" />
                <el-table-column label="短信" align="center" key="sms" prop="sms" />
                <el-table-column label="微信" align="center" key="weChat" prop="weChat" />
              </el-table-column>
            </el-table>
          </template>
        </el-table-column>
        <!-- è¡¨æ ¼åˆ—定义 -->
        <el-table-column
          label="出院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          width="150"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
        />
        <el-table-column label="科室" align="center" key="deptname" prop="deptname" :show-overflow-tooltip="true" />
        <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount" />
        <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp" prop="nonFollowUp" />
        <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded" prop="followUpNeeded" />
        <el-table-column align="center" label="首次出院随访">
          <el-table-column label="需随访" align="center" key="needFollowUp" prop="needFollowUp">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'needFollowUpInfo', '需随访列表')">
                <span class="button-zx">{{ scope.row.needFollowUp }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="待随访" align="center" key="pendingFollowUp" prop="pendingFollowUp">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'pendingFollowUpInfo', '待随访列表')">
                <span class="button-zx">{{ scope.row.pendingFollowUp }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="随访成功" align="center" key="followUpSuccess" prop="followUpSuccess">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'followUpSuccessInfo', '随访成功列表')">
                <span class="button-zx">{{ scope.row.followUpSuccess }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="随访失败" align="center" key="followUpFail" prop="followUpFail">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'followUpFailInfo', '随访失败列表')">
                <span class="button-zx">{{ scope.row.followUpFail }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="随访率" align="center" width="120" key="followUpRate" prop="followUpRate" />
          <el-table-column v-if="orgname != '丽水市中医院'" label="及时率" align="center" width="120" key="rate" prop="rate">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleSeeDetails(scope.row)">
                <span class="button-zx">{{ (Number(scope.row.rate) * 100).toFixed(2) }}%</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="人工" align="center" key="manual" prop="manual">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'manualInfo', '人工随访列表')">
                <span class="button-zx">{{ scope.row.manual }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="短信" align="center" key="sms" prop="sms">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'smsInfo', '短信随访列表')">
                <span class="button-zx">{{ scope.row.sms }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="微信" align="center" key="weChat" prop="weChat">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'weChatInfo', '微信随访列表')">
                <span class="button-zx">{{ scope.row.weChat }}</span>
              </el-button>
            </template>
          </el-table-column>
        </el-table-column>
        <!-- éšè®¿æƒ…况列(仅丽水市中医院显示) -->
        <el-table-column v-if="orgname == '丽水市中医院'" align="center" label="随访情况">
          <el-table-column label="正常语音" align="center" width="100" key="taskSituation1" prop="taskSituation1" />
          <el-table-column label="患者拒接或拒访" align="center" width="100" key="taskSituation2" prop="taskSituation2" />
          <el-table-column label="面访或者接诊" align="center" width="100" key="taskSituation3" prop="taskSituation3" />
          <el-table-column label="微信随访" align="center" width="100" key="taskSituation4" prop="taskSituation4" />
          <el-table-column label="随访电话不正确" align="center" width="100" key="taskSituation5" prop="taskSituation5" />
          <el-table-column label="其他情况不宜随访" align="center" width="100" key="taskSituation6" prop="taskSituation6" />
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
import { getSfStatistics, selectTimelyRate } from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
export default {
  name: 'FirstFollowUp',
  props: {
    queryParams: {
      type: Object,
      required: true
    },
    flatArrayhospit: {
      type: Array,
      default: () => []
    },
    flatArraydept: {
      type: Array,
      default: () => []
    },
    options: {
      type: Array,
      default: () => []
    },
    orgname: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      tableData: [],
      loading: false,
      expands: [],
      ids: []
    }
  },
  methods: {
    loadData() {
      this.loading = true
      const params = {
        ...this.queryParams,
        visitCount: 1,
        leavehospitaldistrictcodes: this.queryParams.leavehospitaldistrictcodes.includes("all")
          ? this.getAllWardCodes()
          : this.queryParams.leavehospitaldistrictcodes,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.getAllDeptCodes()
          : this.queryParams.deptcodes
      }
      delete params.leavehospitaldistrictcodes.all
      delete params.deptcodes.all
      getSfStatistics(params)
        .then(response => {
          this.tableData = this.customSort(response.data)
        })
        .catch(error => {
          console.error("获取首次随访数据失败:", error)
          this.$message.error("获取首次随访数据失败")
        })
        .finally(() => {
          this.loading = false
        })
    },
    getAllWardCodes() {
      return this.flatArrayhospit
        .filter(item => item.value !== 'all')
        .map(item => item.value)
    },
    getAllDeptCodes() {
      return this.flatArraydept
        .filter(item => item.value !== 'all')
        .map(item => item.value)
    },
    customSort(data) {
      const order = [
        "一","二","三","四","五","六","七","八","九","十",
        "十一","十二","十三","十四","十五","十六","十七","十八","十九","二十",
        "二十一","二十二","二十三","二十四","二十五","二十六","二十七","二十八","二十九","三十",
        "三十一","三十二","三十三","三十四","三十五","三十六","三十七","三十八","三十九","四十",
        "四十一","四十二","四十三","四十四","四十五"
      ]
      return data.sort((a, b) => {
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/)
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1])
          }
          const arabicMatch = name.match(/^(\d+)/)
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10)
            if (num >= 1 && num <= 45) {
              return num - 1
            }
          }
          return -1
        }
        const indexA = getIndex(a.leavehospitaldistrictname)
        const indexB = getIndex(b.leavehospitaldistrictname)
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(b.leavehospitaldistrictname || "")
        }
        if (indexA === -1) return 1
        if (indexB === -1) return -1
        return indexA - indexB
      })
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname
      const b = bRow.leavehospitaldistrictname
      const chineseNumMap = {
        ä¸€:1,二:2,三:3,四:4,五:5,六:6,七:7,八:8,九:9,十:10,
        åä¸€:11,十二:12,十三:13,十四:14,十五:15,十六:16,十七:17,十八:18,十九:19,二十:20,
        äºŒåä¸€:21,二十二:22,二十三:23,二十四:24,二十五:25,二十六:26,二十七:27,二十八:28,二十九:29,三十:30,
        ä¸‰åä¸€:31,三十二:32,三十三:33,三十四:34,三十五:35,三十六:36,三十七:37,三十八:38,三十九:39,四十:40,
        å››åä¸€:41,四十二:42,四十三:43,四十四:44,四十五:45
      }
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1
        const match = text.match(/^([一二三四五六七八九十]+)/)
        if (match && match[1]) {
          const chineseNum = match[1]
          return chineseNumMap[chineseNum] !== undefined ? chineseNumMap[chineseNum] : -1
        }
        const arabicMatch = text.match(/^(\d+)/)
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10)
          return num >= 1 && num <= 45 ? num : -1
        }
        return -1
      }
      const numA = getNumberFromText(a)
      const numB = getNumberFromText(b)
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "")
      }
      if (numA === -1) return 1
      if (numB === -1) return -1
      return numA - numB
    },
    getRowKey(row) {
      return row.statisticaltype === 1 ? row.leavehospitaldistrictcode : row.deptcode
    },
    handleRowClick(row) {
      if (this.expands.includes(this.getRowKey(row))) {
        this.expands = []
        return
      }
      const params = {
        ...this.queryParams,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.getAllDeptCodes()
          : this.queryParams.deptcodes,
        leavehospitaldistrictcodes: [row.leavehospitaldistrictcode],
        drcode: "1",
        visitCount: 1
      }
      delete params.leavehospitaldistrictcodes.all
      delete params.deptcodes.all
      if (!row.doctorStats) {
        this.loading = true
        getSfStatistics(params).then((res) => {
          this.$set(row, "doctorStats", res.data)
          this.expands = [this.getRowKey(row)]
          this.loading = false
        })
      } else {
        this.expands = [this.getRowKey(row)]
      }
    },
    getSummaries(param) {
      const { columns, data } = param
      const sums = []
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计"
          return
        }
        if (index === 1 || index === 2) {
          sums[index] = "/"
          return
        }
        if (column.property === "followUpRate" || column.property === "rate") {
          const percentageValues = data
            .map((item) => {
              const value = item[column.property]
              if (!value || value === "-" || value === "0%") return null
              if (typeof value === "string" && value.includes("%")) {
                const numValue = parseFloat(value.replace("%", "")) / 100
                return isNaN(numValue) ? null : numValue
              } else {
                const numValue = parseFloat(value)
                return isNaN(numValue) ? null : numValue
              }
            })
            .filter((value) => value !== null && value !== 0)
          if (percentageValues.length > 0) {
            const average = percentageValues.reduce((sum, value) => sum + value, 0) / percentageValues.length
            sums[index] = (average * 100).toFixed(2) + "%"
          } else {
            sums[index] = "0.00%"
          }
        } else {
          const values = data.map((item) => {
            const value = item[column.property]
            if (value === "-" || value === "" || value === null) return 0
            return Number(value) || 0
          })
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0)
            sums[index] = this.formatNumber(sums[index])
          } else {
            sums[index] = "-"
          }
        }
      })
      return sums
    },
    getInnerSummaries(param) {
      const { columns, data } = param
      const sums = []
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "小计"
          return
        }
        if (column.property === "drname" || column.property === "deptname") {
          sums[index] = "-"
          return
        }
        if (column.property === "followUpRate" || column.property === "rate") {
          const percentageValues = data
            .map((item) => {
              const value = item[column.property]
              if (!value || value === "-" || value === "0%") return null
              if (typeof value === "string" && value.includes("%")) {
                const numValue = parseFloat(value.replace("%", "")) / 100
                return isNaN(numValue) ? null : numValue
              } else {
                const numValue = parseFloat(value)
                return isNaN(numValue) ? null : numValue
              }
            })
            .filter((value) => value !== null && value !== 0)
          if (percentageValues.length > 0) {
            const average = percentageValues.reduce((sum, value) => sum + value, 0) / percentageValues.length
            sums[index] = (average * 100).toFixed(2) + "%"
          } else {
            sums[index] = "0.00%"
          }
        } else {
          const values = data.map((item) => {
            const value = item[column.property]
            if (value === "-" || value === "" || value === null) return 0
            return Number(value) || 0
          })
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0)
            sums[index] = this.formatNumber(sums[index])
          } else {
            sums[index] = "-"
          }
        }
      })
      return sums
    },
    formatNumber(num) {
      if (isNaN(num)) return "-"
      return Number.isInteger(num) ? num.toString() : num.toFixed(0)
    },
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.tagid)
    },
    handleViewDetails(row, infoKey, titleSuffix) {
      const title = `${row.leavehospitaldistrictname || row.deptname}${titleSuffix}`
      this.$emit('view-details', row[infoKey], title)
    },
    handleSeeDetails(row) {
      this.$emit('see-details', row)
    },
    async exportTable() {
      try {
        let dateRangeString = ""
        let sheetNameSuffix = ""
        if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
          const startDateStr = this.queryParams.dateRange[0]
          const endDateStr = this.queryParams.dateRange[1]
          const formatDateForDisplay = (dateTimeStr) => {
            return dateTimeStr.split(" ")[0]
          }
          const startDateFormatted = formatDateForDisplay(startDateStr)
          const endDateFormatted = formatDateForDisplay(endDateStr)
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`
        } else {
          const now = new Date()
          const currentMonth = now.getMonth() + 1
          dateRangeString = `${currentMonth}月`
          sheetNameSuffix = `${currentMonth}月`
        }
        const excelName = `首次出院随访统计表_${dateRangeString}.xlsx`
        const worksheetName = `首次随访统计_${sheetNameSuffix}`
        if (!this.tableData || this.tableData.length === 0) {
          this.$message.warning("暂无首次随访数据可导出")
          return false
        }
        const workbook = new ExcelJS.Workbook()
        const worksheet = workbook.addWorksheet(worksheetName)
        // æž„建表格
        this.buildExportSheet(worksheet, sheetNameSuffix)
        const buffer = await workbook.xlsx.writeBuffer()
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        })
        saveAs(blob, excelName)
        this.$message.success("导出成功")
        return true
      } catch (error) {
        console.error("导出失败:", error)
        this.$message.error(`导出失败: ${error.message}`)
        return false
      }
    },
    buildExportSheet(worksheet, sheetNameSuffix) {
      const titleStyle = {
        font: { name: "微软雅黑", size: 16, bold: true, color: { argb: "FF000000" } },
        fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" } },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      const headerStyle = {
        font: { name: "微软雅黑", size: 11, bold: true, color: { argb: "FF000000" } },
        fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      const cellStyle = {
        font: { name: "宋体", size: 10, color: { argb: "FF000000" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      const summaryStyle = {
        font: { name: "宋体", size: 10, bold: true, color: { argb: "FF409EFF" } },
        fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      // æ·»åŠ æ ‡é¢˜è¡Œ
      worksheet.mergeCells(1, 1, 1, 16)
      const titleCell = worksheet.getCell(1, 1)
      titleCell.value = `首次出院随访统计表_${sheetNameSuffix}`
      titleCell.style = titleStyle
      worksheet.getRow(1).height = 35
      // è¡¨å¤´
      const secondRowHeaders = [
        "", "出院病区", "科室", "出院人次", "无需随访人次", "应随访人次",
        "需随访", "待随访", "随访成功", "随访失败", "随访率", "及时率", "人工", "短信", "微信"
      ]
      secondRowHeaders.forEach((header, index) => {
        const cell = worksheet.getCell(3, index + 1)
        cell.value = header
        cell.style = headerStyle
      })
      // åˆå¹¶å•元格
      for (let i = 1; i <= 6; i++) {
        worksheet.mergeCells(2, i, 3, i)
        const cell = worksheet.getCell(2, i)
        cell.style = headerStyle
      }
      worksheet.getCell(2, 1).value = ""
      worksheet.getCell(2, 2).value = "出院病区"
      worksheet.getCell(2, 3).value = "科室"
      worksheet.getCell(2, 4).value = "出院人次"
      worksheet.getCell(2, 5).value = "无需随访人次"
      worksheet.getCell(2, 6).value = "应随访人次"
      worksheet.mergeCells(2, 7, 2, 15)
      worksheet.getCell(2, 7).value = "首次出院随访"
      worksheet.getCell(2, 7).style = headerStyle
      worksheet.getRow(2).height = 28
      worksheet.getRow(3).height = 25
      // æ•°æ®è¡Œ
      this.tableData.forEach((item, rowIndex) => {
        const dataRow = worksheet.addRow([
          "",
          item.leavehospitaldistrictname || "",
          item.deptname || "",
          item.dischargeCount || 0,
          item.nonFollowUp || 0,
          item.followUpNeeded || 0,
          item.needFollowUp || 0,
          item.pendingFollowUp || 0,
          item.followUpSuccess || 0,
          item.followUpFail || 0,
          item.followUpRate || "0%",
          item.rate ? (Number(item.rate) * 100).toFixed(2) + "%" : "0%",
          item.manual || 0,
          item.sms || 0,
          item.weChat || 0
        ], rowIndex + 4)
        dataRow.eachCell((cell) => {
          cell.style = cellStyle
        })
        dataRow.height = 24
      })
      // åˆè®¡è¡Œ
      const summaries = this.getExportSummaries()
      const summaryRow = worksheet.addRow(summaries)
      summaryRow.eachCell((cell, colNumber) => {
        cell.style = summaryStyle
        if (colNumber === 1) {
          cell.value = "合计"
        }
      })
      summaryRow.height = 28
      // åˆ—宽
      worksheet.columns = [
        { width: 8 }, { width: 20 }, { width: 15 }, { width: 12 }, { width: 12 }, { width: 12 },
        { width: 10 }, { width: 10 }, { width: 10 }, { width: 10 }, { width: 12 }, { width: 12 },
        { width: 8 }, { width: 8 }, { width: 8 }
      ]
    },
    getExportSummaries() {
      const summaries = ["合计", "/", "/", 0, 0, 0, 0, 0, 0, 0, "0%", "0%", 0, 0, 0]
      this.tableData.forEach((item) => {
        summaries[3] += Number(item.dischargeCount) || 0
        summaries[4] += Number(item.nonFollowUp) || 0
        summaries[5] += Number(item.followUpNeeded) || 0
        summaries[6] += Number(item.needFollowUp) || 0
        summaries[7] += Number(item.pendingFollowUp) || 0
        summaries[8] += Number(item.followUpSuccess) || 0
        summaries[9] += Number(item.followUpFail) || 0
        summaries[12] += Number(item.manual) || 0
        summaries[13] += Number(item.sms) || 0
        summaries[14] += Number(item.weChat) || 0
      })
      const followUpRateValues = this.tableData
        .map((item) => this.extractPercentageValue(item.followUpRate))
        .filter((value) => value !== null)
      const rateValues = this.tableData
        .map((item) => this.extractPercentageValue(item.rate))
        .filter((value) => value !== null)
      if (followUpRateValues.length > 0) {
        const avgFollowUpRate = followUpRateValues.reduce((sum, val) => sum + val, 0) / followUpRateValues.length
        summaries[10] = (avgFollowUpRate * 100).toFixed(2) + "%"
      }
      if (rateValues.length > 0) {
        const avgRate = rateValues.reduce((sum, val) => sum + val, 0) / rateValues.length
        summaries[11] = (avgRate * 100).toFixed(2) + "%"
      }
      summaries[3] = this.formatNumber(summaries[3])
      summaries[4] = this.formatNumber(summaries[4])
      summaries[5] = this.formatNumber(summaries[5])
      summaries[6] = this.formatNumber(summaries[6])
      summaries[7] = this.formatNumber(summaries[7])
      summaries[8] = this.formatNumber(summaries[8])
      summaries[9] = this.formatNumber(summaries[9])
      summaries[12] = this.formatNumber(summaries[12])
      summaries[13] = this.formatNumber(summaries[13])
      summaries[14] = this.formatNumber(summaries[14])
      return summaries
    },
    extractPercentageValue(value) {
      if (!value) return null
      if (typeof value === "string" && value.includes("%")) {
        const num = parseFloat(value.replace("%", ""))
        return isNaN(num) ? null : num / 100
      }
      const num = parseFloat(value)
      return isNaN(num) ? null : num
    },
    selectTimelyRate(row, dateRange) {
      const params = {
        ...this.patientqueryParams,
        starttime: this.parseTime(dateRange[0]),
        endtime: this.parseTime(dateRange[1]),
        deptcode: row.deptcode
      }
      return selectTimelyRate(params)
    }
  }
}
</script>
<style lang="scss" scoped>
.first-follow-up {
  .your-table-container {
    margin-top: 10px;
  }
  .button-zx {
    color: rgb(70, 204, 238);
  }
}
</style>
src/views/sfstatistics/percentage/components/SecondFollowUp.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,679 @@
<template>
  <div class="second-follow-up">
    <div class="your-table-container">
      <el-table
        ref="exportTableSecond"
        id="exportTableidSecond"
        v-loading="loading"
        :data="tableData"
        :border="true"
        @selection-change="handleSelectionChange"
        @expand-change="handleRowClick"
        :row-key="getRowKey"
        show-summary
        :summary-method="getSummaries"
        :expand-row-keys="expands"
      >
        <!-- å±•开行箭头列 -->
        <el-table-column type="expand">
          <template slot-scope="props">
            <el-table
              :data="props.row.doctorStats"
              border
              style="width: 95%; margin: 0 auto"
              class="inner-table"
              show-summary
              :summary-method="getInnerSummaries"
            >
              <el-table-column label="医生姓名" prop="drname" align="center" />
              <el-table-column label="科室" width="120" prop="deptname" align="center" />
              <el-table-column label="出院人次" prop="dischargeCount" align="center" />
              <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp" prop="nonFollowUp" />
              <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded" prop="followUpNeeded" />
              <el-table-column align="center" label="再次出院随访">
                <el-table-column label="需随访" align="center" key="needFollowUpAgain" prop="needFollowUpAgain" />
                <el-table-column label="待随访" align="center" key="pendingFollowUpAgain" prop="pendingFollowUpAgain" />
                <el-table-column label="随访成功" align="center" key="followUpSuccessAgain" prop="followUpSuccessAgain" />
                <el-table-column label="随访失败" align="center" key="followUpFailAgain" prop="followUpFailAgain" />
                <el-table-column label="随访率" align="center" width="120" key="followUpRateAgain" prop="followUpRateAgain" />
                <el-table-column label="人工" align="center" key="manualAgain" prop="manualAgain" />
                <el-table-column label="短信" align="center" key="smsAgain" prop="smsAgain" />
                <el-table-column label="微信" align="center" key="weChatAgain" prop="weChatAgain" />
              </el-table-column>
            </el-table>
          </template>
        </el-table-column>
        <!-- è¡¨æ ¼åˆ—定义 -->
        <el-table-column
          label="出院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          width="150"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
        />
        <el-table-column label="科室" align="center" key="deptname" prop="deptname" :show-overflow-tooltip="true" />
        <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount" />
        <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp" prop="nonFollowUp" />
        <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded" prop="followUpNeeded" />
        <el-table-column align="center" label="再次出院随访">
          <el-table-column label="需随访" align="center" key="needFollowUpAgain" prop="needFollowUpAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'needFollowUpAgainInfo', '再次随访需随访列表')">
                <span class="button-zx">{{ scope.row.needFollowUpAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="待随访" align="center" key="pendingFollowUpAgain" prop="pendingFollowUpAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'pendingFollowUpAgainInfo', '再次随访待随访列表')">
                <span class="button-zx">{{ scope.row.pendingFollowUpAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="随访成功" align="center" key="followUpSuccessAgain" prop="followUpSuccessAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'followUpSuccessAgainInfo', '再次随访随访成功列表')">
                <span class="button-zx">{{ scope.row.followUpSuccessAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="随访失败" align="center" key="followUpFailAgain" prop="followUpFailAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'followUpFailAgainInfo', '再次随访随访失败列表')">
                <span class="button-zx">{{ scope.row.followUpFailAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="随访率" align="center" width="120" key="followUpRateAgain" prop="followUpRateAgain" />
          <el-table-column label="人工" align="center" key="manualAgain" prop="manualAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'manualAgainInfo', '再次随访人工随访列表')">
                <span class="button-zx">{{ scope.row.manualAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="短信" align="center" key="smsAgain" prop="smsAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'smsAgainInfo', '再次随访短信随访列表')">
                <span class="button-zx">{{ scope.row.smsAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="微信" align="center" key="weChatAgain" prop="weChatAgain">
            <template slot-scope="scope">
              <el-button size="medium" type="text" @click="handleViewDetails(scope.row, 'weChatAgainInfo', '再次随访微信随访列表')">
                <span class="button-zx">{{ scope.row.weChatAgain }}</span>
              </el-button>
            </template>
          </el-table-column>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
import { getSfStatistics } from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
export default {
  name: 'SecondFollowUp',
  props: {
    queryParams: {
      type: Object,
      required: true
    },
    flatArrayhospit: {
      type: Array,
      default: () => []
    },
    flatArraydept: {
      type: Array,
      default: () => []
    },
    options: {
      type: Array,
      default: () => []
    },
    orgname: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      tableData: [],
      loading: false,
      expands: [],
      ids: []
    }
  },
  methods: {
    loadData() {
      this.loading = true
      const params = {
        ...this.queryParams,
        visitCount: 2,
        leavehospitaldistrictcodes: this.queryParams.leavehospitaldistrictcodes.includes("all")
          ? this.getAllWardCodes()
          : this.queryParams.leavehospitaldistrictcodes,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.getAllDeptCodes()
          : this.queryParams.deptcodes
      }
      delete params.leavehospitaldistrictcodes.all
      delete params.deptcodes.all
      getSfStatistics(params)
        .then(response => {
          this.tableData = this.customSort(response.data)
        })
        .catch(error => {
          console.error("获取再次随访数据失败:", error)
          this.$message.error("获取再次随访数据失败")
        })
        .finally(() => {
          this.loading = false
        })
    },
    getAllWardCodes() {
      return this.flatArrayhospit
        .filter(item => item.value !== 'all')
        .map(item => item.value)
    },
    getAllDeptCodes() {
      return this.flatArraydept
        .filter(item => item.value !== 'all')
        .map(item => item.value)
    },
    customSort(data) {
      const order = [
        "一","二","三","四","五","六","七","八","九","十",
        "十一","十二","十三","十四","十五","十六","十七","十八","十九","二十",
        "二十一","二十二","二十三","二十四","二十五","二十六","二十七","二十八","二十九","三十",
        "三十一","三十二","三十三","三十四","三十五","三十六","三十七","三十八","三十九","四十",
        "四十一","四十二","四十三","四十四","四十五"
      ]
      return data.sort((a, b) => {
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/)
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1])
          }
          const arabicMatch = name.match(/^(\d+)/)
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10)
            if (num >= 1 && num <= 45) {
              return num - 1
            }
          }
          return -1
        }
        const indexA = getIndex(a.leavehospitaldistrictname)
        const indexB = getIndex(b.leavehospitaldistrictname)
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(b.leavehospitaldistrictname || "")
        }
        if (indexA === -1) return 1
        if (indexB === -1) return -1
        return indexA - indexB
      })
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname
      const b = bRow.leavehospitaldistrictname
      const chineseNumMap = {
        ä¸€:1,二:2,三:3,四:4,五:5,六:6,七:7,八:8,九:9,十:10,
        åä¸€:11,十二:12,十三:13,十四:14,十五:15,十六:16,十七:17,十八:18,十九:19,二十:20,
        äºŒåä¸€:21,二十二:22,二十三:23,二十四:24,二十五:25,二十六:26,二十七:27,二十八:28,二十九:29,三十:30,
        ä¸‰åä¸€:31,三十二:32,三十三:33,三十四:34,三十五:35,三十六:36,三十七:37,三十八:38,三十九:39,四十:40,
        å››åä¸€:41,四十二:42,四十三:43,四十四:44,四十五:45
      }
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1
        const match = text.match(/^([一二三四五六七八九十]+)/)
        if (match && match[1]) {
          const chineseNum = match[1]
          return chineseNumMap[chineseNum] !== undefined ? chineseNumMap[chineseNum] : -1
        }
        const arabicMatch = text.match(/^(\d+)/)
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10)
          return num >= 1 && num <= 45 ? num : -1
        }
        return -1
      }
      const numA = getNumberFromText(a)
      const numB = getNumberFromText(b)
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "")
      }
      if (numA === -1) return 1
      if (numB === -1) return -1
      return numA - numB
    },
    getRowKey(row) {
      return row.statisticaltype === 1 ? row.leavehospitaldistrictcode : row.deptcode
    },
    handleRowClick(row) {
      if (this.expands.includes(this.getRowKey(row))) {
        this.expands = []
        return
      }
      const params = {
        ...this.queryParams,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.getAllDeptCodes()
          : this.queryParams.deptcodes,
        leavehospitaldistrictcodes: [row.leavehospitaldistrictcode],
        drcode: "1",
        visitCount: 2
      }
      delete params.leavehospitaldistrictcodes.all
      delete params.deptcodes.all
      if (!row.doctorStats) {
        this.loading = true
        getSfStatistics(params).then((res) => {
          this.$set(row, "doctorStats", res.data)
          this.expands = [this.getRowKey(row)]
          this.loading = false
        })
      } else {
        this.expands = [this.getRowKey(row)]
      }
    },
    getSummaries(param) {
      const { columns, data } = param
      const sums = []
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计"
          return
        }
        if (index === 1 || index === 2) {
          sums[index] = "/"
          return
        }
        if (column.property === "followUpRateAgain") {
          const percentageValues = data
            .map((item) => {
              const value = item[column.property]
              if (!value || value === "-" || value === "0%") return null
              if (typeof value === "string" && value.includes("%")) {
                const numValue = parseFloat(value.replace("%", "")) / 100
                return isNaN(numValue) ? null : numValue
              } else {
                const numValue = parseFloat(value)
                return isNaN(numValue) ? null : numValue
              }
            })
            .filter((value) => value !== null && value !== 0)
          if (percentageValues.length > 0) {
            const average = percentageValues.reduce((sum, value) => sum + value, 0) / percentageValues.length
            sums[index] = (average * 100).toFixed(2) + "%"
          } else {
            sums[index] = "0.00%"
          }
        } else {
          const values = data.map((item) => {
            const value = item[column.property]
            if (value === "-" || value === "" || value === null) return 0
            return Number(value) || 0
          })
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0)
            sums[index] = this.formatNumber(sums[index])
          } else {
            sums[index] = "-"
          }
        }
      })
      return sums
    },
    getInnerSummaries(param) {
      const { columns, data } = param
      const sums = []
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "小计"
          return
        }
        if (column.property === "drname" || column.property === "deptname") {
          sums[index] = "-"
          return
        }
        if (column.property === "followUpRateAgain") {
          const percentageValues = data
            .map((item) => {
              const value = item[column.property]
              if (!value || value === "-" || value === "0%") return null
              if (typeof value === "string" && value.includes("%")) {
                const numValue = parseFloat(value.replace("%", "")) / 100
                return isNaN(numValue) ? null : numValue
              } else {
                const numValue = parseFloat(value)
                return isNaN(numValue) ? null : numValue
              }
            })
            .filter((value) => value !== null && value !== 0)
          if (percentageValues.length > 0) {
            const average = percentageValues.reduce((sum, value) => sum + value, 0) / percentageValues.length
            sums[index] = (average * 100).toFixed(2) + "%"
          } else {
            sums[index] = "0.00%"
          }
        } else {
          const values = data.map((item) => {
            const value = item[column.property]
            if (value === "-" || value === "" || value === null) return 0
            return Number(value) || 0
          })
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0)
            sums[index] = this.formatNumber(sums[index])
          } else {
            sums[index] = "-"
          }
        }
      })
      return sums
    },
    formatNumber(num) {
      if (isNaN(num)) return "-"
      return Number.isInteger(num) ? num.toString() : num.toFixed(0)
    },
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.tagid)
    },
    handleViewDetails(row, infoKey, titleSuffix) {
      const title = `${row.leavehospitaldistrictname || row.deptname}${titleSuffix}`
      this.$emit('view-details', row[infoKey], title)
    },
    async exportTable() {
      try {
        let dateRangeString = ""
        let sheetNameSuffix = ""
        if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
          const startDateStr = this.queryParams.dateRange[0]
          const endDateStr = this.queryParams.dateRange[1]
          const formatDateForDisplay = (dateTimeStr) => {
            return dateTimeStr.split(" ")[0]
          }
          const startDateFormatted = formatDateForDisplay(startDateStr)
          const endDateFormatted = formatDateForDisplay(endDateStr)
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`
        } else {
          const now = new Date()
          const currentMonth = now.getMonth() + 1
          dateRangeString = `${currentMonth}月`
          sheetNameSuffix = `${currentMonth}月`
        }
        const excelName = `再次出院随访统计表_${dateRangeString}.xlsx`
        const worksheetName = `再次随访统计_${sheetNameSuffix}`
        if (!this.tableData || this.tableData.length === 0) {
          this.$message.warning("暂无再次随访数据可导出")
          return false
        }
        const workbook = new ExcelJS.Workbook()
        const worksheet = workbook.addWorksheet(worksheetName)
        // æž„建表格
        this.buildExportSheet(worksheet, sheetNameSuffix)
        const buffer = await workbook.xlsx.writeBuffer()
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        })
        saveAs(blob, excelName)
        this.$message.success("导出成功")
        return true
      } catch (error) {
        console.error("导出失败:", error)
        this.$message.error(`导出失败: ${error.message}`)
        return false
      }
    },
    buildExportSheet(worksheet, sheetNameSuffix) {
      const titleStyle = {
        font: { name: "微软雅黑", size: 16, bold: true, color: { argb: "FF000000" } },
        fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" } },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      const headerStyle = {
        font: { name: "微软雅黑", size: 11, bold: true, color: { argb: "FF000000" } },
        fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      const cellStyle = {
        font: { name: "宋体", size: 10, color: { argb: "FF000000" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      const summaryStyle = {
        font: { name: "宋体", size: 10, bold: true, color: { argb: "FF409EFF" } },
        fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } }
        }
      }
      // æ·»åŠ æ ‡é¢˜è¡Œ
      worksheet.mergeCells(1, 1, 1, 15)
      const titleCell = worksheet.getCell(1, 1)
      titleCell.value = `再次出院随访统计表_${sheetNameSuffix}`
      titleCell.style = titleStyle
      worksheet.getRow(1).height = 35
      // è¡¨å¤´
      const secondRowHeaders = [
        "", "出院病区", "科室", "出院人次", "无需随访人次", "应随访人次",
        "需随访", "待随访", "随访成功", "随访失败", "随访率", "人工", "短信", "微信"
      ]
      secondRowHeaders.forEach((header, index) => {
        const cell = worksheet.getCell(3, index + 1)
        cell.value = header
        cell.style = headerStyle
      })
      // åˆå¹¶å•元格
      for (let i = 1; i <= 6; i++) {
        worksheet.mergeCells(2, i, 3, i)
        const cell = worksheet.getCell(2, i)
        cell.style = headerStyle
      }
      worksheet.getCell(2, 1).value = ""
      worksheet.getCell(2, 2).value = "出院病区"
      worksheet.getCell(2, 3).value = "科室"
      worksheet.getCell(2, 4).value = "出院人次"
      worksheet.getCell(2, 5).value = "无需随访人次"
      worksheet.getCell(2, 6).value = "应随访人次"
      worksheet.mergeCells(2, 7, 2, 14)
      worksheet.getCell(2, 7).value = "再次出院随访"
      worksheet.getCell(2, 7).style = headerStyle
      worksheet.getRow(2).height = 28
      worksheet.getRow(3).height = 25
      // æ•°æ®è¡Œ
      this.tableData.forEach((item, rowIndex) => {
        const dataRow = worksheet.addRow([
          "",
          item.leavehospitaldistrictname || "",
          item.deptname || "",
          item.dischargeCount || 0,
          item.nonFollowUp || 0,
          item.followUpNeeded || 0,
          item.needFollowUpAgain || 0,
          item.pendingFollowUpAgain || 0,
          item.followUpSuccessAgain || 0,
          item.followUpFailAgain || 0,
          item.followUpRateAgain || "0%",
          item.manualAgain || 0,
          item.smsAgain || 0,
          item.weChatAgain || 0
        ], rowIndex + 4)
        dataRow.eachCell((cell) => {
          cell.style = cellStyle
        })
        dataRow.height = 24
      })
      // åˆè®¡è¡Œ
      const summaries = this.getExportSummaries()
      const summaryRow = worksheet.addRow(summaries)
      summaryRow.eachCell((cell, colNumber) => {
        cell.style = summaryStyle
        if (colNumber === 1) {
          cell.value = "合计"
        }
      })
      summaryRow.height = 28
      // åˆ—宽
      worksheet.columns = [
        { width: 8 }, { width: 20 }, { width: 15 }, { width: 12 }, { width: 12 }, { width: 12 },
        { width: 10 }, { width: 10 }, { width: 10 }, { width: 10 }, { width: 12 },
        { width: 8 }, { width: 8 }, { width: 8 }
      ]
    },
    getExportSummaries() {
      const summaries = ["合计", "/", "/", 0, 0, 0, 0, 0, 0, 0, "0%", 0, 0, 0]
      this.tableData.forEach((item) => {
        summaries[3] += Number(item.dischargeCount) || 0
        summaries[4] += Number(item.nonFollowUp) || 0
        summaries[5] += Number(item.followUpNeeded) || 0
        summaries[6] += Number(item.needFollowUpAgain) || 0
        summaries[7] += Number(item.pendingFollowUpAgain) || 0
        summaries[8] += Number(item.followUpSuccessAgain) || 0
        summaries[9] += Number(item.followUpFailAgain) || 0
        summaries[11] += Number(item.manualAgain) || 0
        summaries[12] += Number(item.smsAgain) || 0
        summaries[13] += Number(item.weChatAgain) || 0
      })
      const followUpRateAgainValues = this.tableData
        .map((item) => this.extractPercentageValue(item.followUpRateAgain))
        .filter((value) => value !== null)
      if (followUpRateAgainValues.length > 0) {
        const avgFollowUpRateAgain = followUpRateAgainValues.reduce((sum, val) => sum + val, 0) / followUpRateAgainValues.length
        summaries[10] = (avgFollowUpRateAgain * 100).toFixed(2) + "%"
      }
      summaries[3] = this.formatNumber(summaries[3])
      summaries[4] = this.formatNumber(summaries[4])
      summaries[5] = this.formatNumber(summaries[5])
      summaries[6] = this.formatNumber(summaries[6])
      summaries[7] = this.formatNumber(summaries[7])
      summaries[8] = this.formatNumber(summaries[8])
      summaries[9] = this.formatNumber(summaries[9])
      summaries[11] = this.formatNumber(summaries[11])
      summaries[12] = this.formatNumber(summaries[12])
      summaries[13] = this.formatNumber(summaries[13])
      return summaries
    },
    extractPercentageValue(value) {
      if (!value) return null
      if (typeof value === "string" && value.includes("%")) {
        const num = parseFloat(value.replace("%", ""))
        return isNaN(num) ? null : num / 100
      }
      const num = parseFloat(value)
      return isNaN(num) ? null : num
    }
  }
}
</script>
<style lang="scss" scoped>
.second-follow-up {
  .your-table-container {
    margin-top: 10px;
  }
  .button-zx {
    color: rgb(70, 204, 238);
  }
}
</style>
src/views/sfstatistics/percentage/components/TimelyRateDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,249 @@
<template>
  <el-dialog
    title="未及时随访患者服务"
    :visible.sync="visible"
    v-loading="loading"
    width="70%"
    :close-on-click-modal="false"
    @close="handleClose"
  >
    <div class="timely-rate-dialog">
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <!-- æœç´¢è¡¨å• -->
            <el-form
              :model="queryParams"
              ref="queryForm"
              size="small"
              :inline="true"
              label-width="98px"
              class="search-form"
            >
              <el-form-item label="患者:">
                <el-input
                  v-model="queryParams.name"
                  placeholder="请输入患者姓名"
                  @keyup.enter.native="handleSearch"
                />
              </el-form-item>
              <el-form-item label="患者诊断:">
                <el-input
                  v-model="queryParams.leavediagname"
                  placeholder="请输入患者诊断"
                  @keyup.enter.native="handleSearch"
                />
              </el-form-item>
              <el-form-item>
                <el-button
                  type="primary"
                  icon="el-icon-search"
                  size="medium"
                  @click="handleSearch"
                >
                  æœç´¢
                </el-button>
                <el-button
                  icon="el-icon-refresh"
                  size="medium"
                  @click="resetQuery"
                >
                  é‡ç½®
                </el-button>
              </el-form-item>
            </el-form>
            <!-- æ‚£è€…列表 -->
            <el-table :data="data" style="width: 100%" v-loading="loading">
              <el-table-column prop="sendname" align="center" label="姓名" width="100" />
              <el-table-column prop="taskName" align="center" width="200" show-overflow-tooltip label="任务名称" />
              <el-table-column prop="sendstate" align="center" width="200" label="任务状态">
                <template slot-scope="scope">
                  <el-tag
                    :type="getStateTagType(scope.row.sendstate)"
                    :disable-transitions="false"
                  >
                    {{ getStateText(scope.row.sendstate) }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="visitTime" align="center" label="应随访时间" width="200" show-overflow-tooltip />
              <el-table-column prop="finishtime" align="center" label="随访完成时间" width="200" show-overflow-tooltip />
              <el-table-column label="出院日期" width="200" align="center" key="endtime" prop="endtime">
                <template slot-scope="scope">
                  <span>{{ formatTime(scope.row.endtime) }}</span>
                </template>
              </el-table-column>
              <el-table-column label="责任护士" width="120" align="center" key="nurseName" prop="nurseName" />
              <el-table-column label="主治医生" width="120" align="center" key="drname" prop="drname" />
              <el-table-column label="结果状态" align="center" key="excep" prop="excep" width="120">
                <template slot-scope="scope">
                  <dict-tag :options="dict.type.sys_yujing" :value="scope.row.excep" />
                </template>
              </el-table-column>
              <el-table-column label="处理意见" align="center" key="suggest" prop="suggest" width="120">
                <template slot-scope="scope">
                  <dict-tag :options="dict.type.sys_suggest" :value="scope.row.suggest" />
                </template>
              </el-table-column>
              <el-table-column prop="templatename" align="center" label="服务模板" width="200" show-overflow-tooltip />
              <el-table-column prop="remark" align="center" label="服务记录" width="200" show-overflow-tooltip />
              <el-table-column prop="bankcardno" align="center" label="呼叫状态" width="210" />
              <el-table-column label="操作" fixed="right" align="center" width="200" class-name="small-padding fixed-width">
                <template slot-scope="scope">
                  <el-button size="medium" type="text" @click="handleDetailsGo(scope.row)">
                    <span class="button-zx">
                      <i class="el-icon-s-order"></i>查看
                    </span>
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
          </el-row>
          <!-- åˆ†é¡µ -->
          <pagination
            v-show="total > 0"
            :total="total"
            :page.sync="queryParams.pn"
            :limit.sync="queryParams.ps"
            @pagination="handlePagination"
          />
        </div>
      </div>
    </div>
  </el-dialog>
</template>
<script>
import Pagination from '@/components/Pagination'
export default {
  name: 'TimelyRateDialog',
  components: {
    Pagination
  },
  dicts: ['sys_yujing', 'sys_suggest'],
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    loading: {
      type: Boolean,
      default: false
    },
    data: {
      type: Array,
      default: () => []
    },
    total: {
      type: Number,
      default: 0
    },
    queryParams: {
      type: Object,
      default: () => ({
        pn: 1,
        ps: 10
      })
    }
  },
  data() {
    return {
      localQueryParams: { ...this.queryParams }
    }
  },
  watch: {
    queryParams: {
      deep: true,
      handler(newParams) {
        this.localQueryParams = { ...newParams }
      }
    }
  },
  mounted() {
    this.localQueryParams = { ...this.queryParams }
  },
  methods: {
    handleSearch() {
      this.$emit('search', this.localQueryParams)
    },
    resetQuery() {
      this.localQueryParams = {
        pn: 1,
        ps: 10,
        name: '',
        leavediagname: ''
      }
      this.$emit('search', this.localQueryParams)
    },
    handlePagination(pagination) {
      this.localQueryParams.pn = pagination.page
      this.localQueryParams.ps = pagination.limit
      this.$emit('search', this.localQueryParams)
    },
    getStateTagType(state) {
      const stateMap = {
        1: 'primary',
        2: 'primary',
        3: 'success',
        4: 'info',
        5: 'danger',
        6: 'success'
      }
      return stateMap[state] || 'info'
    },
    getStateText(state) {
      const stateTextMap = {
        1: '表单已领取',
        2: '待随访',
        3: '表单已发送',
        4: '不执行',
        5: '发送失败',
        6: '已完成'
      }
      return stateTextMap[state] || '未知状态'
    },
    formatTime(time) {
      if (!time) return ''
      return this.parseTime(time)
    },
    handleDetailsGo(row) {
      this.$emit('details-go', row)
    },
    handleClose() {
      this.$emit('close')
    }
  }
}
</script>
<style lang="scss" scoped>
.timely-rate-dialog {
  .search-form {
    margin-bottom: 20px;
  }
  .button-zx {
    color: rgb(70, 204, 238);
  }
}
</style>
src/views/sfstatistics/percentage/components/styles.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
// å…¨å±€æ ·å¼
.follow-up-statistics,
.first-follow-up,
.second-follow-up,
.continued-care {
  .your-table-container {
    margin-top: 10px;
  }
  .button-zx {
    color: rgb(70, 204, 238);
  }
  // ç¾ŽåŒ–合计行样式
  ::v-deep .el-table__footer {
    .el-table__cell {
      background-color: #f5f7fa;
      font-weight: 600;
      color: #409eff;
      .cell {
        font-weight: 600;
        color: #409eff;
      }
    }
  }
  // å†…部表格合计行样式
  ::v-deep .inner-table .el-table__footer {
    .el-table__cell {
      background-color: #ecf5ff;
      font-weight: 500;
      color: #67c23a;
      .cell {
        font-weight: 500;
        color: #67c23a;
      }
    }
  }
  // ç™¾åˆ†æ¯”字段特殊样式
  ::v-deep .el-table__footer .el-table__cell[data-field="followUpRate"] .cell,
  ::v-deep .el-table__footer .el-table__cell[data-field="rate"] .cell,
  ::v-deep .el-table__footer .el-table__cell[data-field="followUpRateAgain"] .cell,
  ::v-deep .el-table__footer .el-table__cell[data-field="completionRate"] .cell {
    color: #e6a23c !important;
    font-weight: 700 !important;
  }
  // å†…层医生表格样式
  .inner-table {
    ::v-deep .el-table__header-wrapper {
      background-color: #f0f7ff !important;
      th {
        background-color: #f0f7ff !important;
      }
    }
    ::v-deep .el-table__body-wrapper {
      tr {
        background-color: #f9fbfe !important;
        &:hover {
          background-color: #e6f1ff !important;
        }
      }
    }
    ::v-deep .el-table--border {
      border-color: #d9e8ff !important;
      td, th {
        border-color: #d9e8ff !important;
      }
    }
  }
  /* ä½¿è¡Œæœ‰æ‰‹åž‹æŒ‡é’ˆ */
  ::v-deep .el-table__row {
    cursor: pointer;
  }
  /* å±•开行样式 */
  ::v-deep .el-table__expanded-cell {
    padding: 10px 0 !important;
    background: #f8f8f8;
  }
}
src/views/sfstatistics/percentage/index copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2898 @@
<template>
  <div class="Questionnairemanagement">
    <div class="leftvlue">
      <div class="leftvlue-bg">
        <el-row :gutter="20">
          <!--标签数据-->
          <el-col :span="24" :xs="24">
            <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch"
              label-width="98px">
              <el-form-item label="统计类型" prop="userName">
                <el-select v-model="queryParams.statisticaltype" placeholder="请选择统计类型">
                  <el-option v-for="item in Statisticallist" :key="item.value" :label="item.label" :value="item.value">
                  </el-option>
                </el-select>
                <el-select style="margin-left: 10px" v-if="queryParams.statisticaltype == 1"
                  v-model="queryParams.leavehospitaldistrictcodes" size="medium" multiple filterable
                  placeholder="请选择病区">
                  <el-option v-for="item in flatArrayhospit" :key="item.value" :label="item.label" :value="item.value">
                  </el-option>
                </el-select>
                <el-select v-else-if="queryParams.statisticaltype == 2" v-model="queryParams.deptcodes" size="medium"
                  multiple filterable placeholder="请选择科室">
                  <el-option v-for="item in flatArraydept" :key="item.value" :label="item.label" :value="item.value">
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="服务类型" prop="userName">
                <el-select v-model="queryParams.serviceType" multiple placeholder="请选择">
                  <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label-width="200" label="应随访时间范围" prop="userName">
                <el-date-picker v-model="queryParams.dateRange" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
                  range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
                  :default-time="['00:00:00', '23:59:59']">
                </el-date-picker>
              </el-form-item>
              <el-form-item>
                <el-button type="primary" icon="el-icon-search" size="medium" @click="handleQuery">搜索</el-button>
                <el-button icon="el-icon-refresh" size="medium" @click="resetQuery">重置</el-button>
              </el-form-item>
              <el-button type="warning" plain icon="el-icon-download" size="medium" @click="exportTable">导出</el-button>
              <el-button type="primary" plain icon="el-icon-data-line" size="medium"
                @click="showChartDialog">统计趋势图</el-button>
            </el-form>
            <!-- æ–°å¢žï¼šTab标签页 -->
            <el-tabs v-model="activeTab" @tab-click="handleTabClick">
              <el-tab-pane label="首次随访" name="first">
                <div class="your-table-container">
                  <el-table ref="exportTable" id="exportTableid" v-loading="loading" :data="firstFollowUpList"
                    :border="true" @selection-change="handleSelectionChange" @expand-change="handleRowClick"
                    :row-key="getRowKey" show-summary :summary-method="getSummaries" :expand-row-keys="expands">
                    <!-- å±•开行箭头列 -->
                    <el-table-column type="expand">
                      <template slot-scope="props">
                        <el-table :data="props.row.doctorStats" border style="width: 95%; margin: 0 auto"
                          class="inner-table" show-summary :summary-method="getInnerSummaries">
                          <el-table-column label="医生姓名" prop="drname" align="center" />
                          <el-table-column label="科室" width="120" prop="deptname" align="center" />
                          <el-table-column label="出院人次" prop="dischargeCount" align="center" />
                          <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount">
                          </el-table-column>
                          <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp"
                            prop="nonFollowUp">
                          </el-table-column>
                          <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded"
                            prop="followUpNeeded">
                          </el-table-column>
                          <el-table-column align="center" label="首次出院随访">
                            <el-table-column label="需随访" align="center" key="needFollowUp" prop="needFollowUp">
                            </el-table-column>
                            <el-table-column label="待随访" align="center" key="pendingFollowUp" prop="pendingFollowUp">
                            </el-table-column>
                            <el-table-column label="随访成功" align="center" key="followUpSuccess" prop="followUpSuccess">
                            </el-table-column>
                            <el-table-column label="随访失败" align="center" key="followUpFail" prop="followUpFail">
                            </el-table-column>
                            <el-table-column label="随访率" align="center" width="120" key="followUpRate"
                              prop="followUpRate">
                            </el-table-column>
                            <el-table-column v-if="orgname != '丽水市中医院'" label="及时率" align="center" width="120"
                              key="rate" prop="rate">
                              <template slot-scope="scope">
                                <el-button size="medium" type="text" @click="Seedetails(scope.row)"><span
                                    class="button-zx">{{
                                      (Number(scope.row.rate) * 100).toFixed(2)
                                    }}%</span></el-button>
                              </template>
                            </el-table-column>
                            <el-table-column label="人工" align="center" key="manual" prop="manual">
                            </el-table-column>
                            <el-table-column label="短信" align="center" key="sms" prop="sms">
                            </el-table-column>
                            <el-table-column label="微信" align="center" key="weChat" prop="weChat">
                            </el-table-column>
                          </el-table-column>
                        </el-table>
                      </template>
                    </el-table-column>
                    <el-table-column label="出院病区" align="center" sortable key="leavehospitaldistrictname"
                      prop="leavehospitaldistrictname" width="150" :show-overflow-tooltip="true"
                      :sort-method="sortChineseNumber" />
                    <el-table-column label="科室" align="center" key="deptname" prop="deptname"
                      :show-overflow-tooltip="true" />
                    <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount">
                    </el-table-column>
                    <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp" prop="nonFollowUp">
                    </el-table-column>
                    <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded"
                      prop="followUpNeeded">
                    </el-table-column>
                    <el-table-column align="center" label="首次出院随访">
                      <el-table-column label="需随访" align="center" key="needFollowUp" prop="needFollowUp">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.needFollowUpInfo,
                              scope.row.leavehospitaldistrictname +
                              '需随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.needFollowUp
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="待随访" align="center" key="pendingFollowUp" prop="pendingFollowUp">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.pendingFollowUpInfo,
                              scope.row.leavehospitaldistrictname +
                              '待随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.pendingFollowUp
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="随访成功" align="center" key="followUpSuccess" prop="followUpSuccess">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.followUpSuccessInfo,
                              scope.row.leavehospitaldistrictname +
                              '随访成功列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.followUpSuccess
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="随访失败" align="center" key="followUpFail" prop="followUpFail">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.followUpFailInfo,
                              scope.row.leavehospitaldistrictname +
                              '随访失败列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.followUpFail
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="随访率" align="center" width="120" key="followUpRate" prop="followUpRate">
                      </el-table-column>
                      <el-table-column v-if="orgname != '丽水市中医院'" label="及时率" align="center" width="120" key="rate"
                        prop="rate">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="Seedetails(scope.row)"><span class="button-zx">{{
                            (Number(scope.row.rate) * 100).toFixed(2)
                          }}%</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="人工" align="center" key="manual" prop="manual">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.manualInfo,
                              scope.row.leavehospitaldistrictname +
                              '人工随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.manual
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="短信" align="center" key="sms" prop="sms">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.smsInfo,
                              scope.row.leavehospitaldistrictname +
                              '短信随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.sms
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="微信" align="center" key="weChat" prop="weChat">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.weChatInfo,
                              scope.row.leavehospitaldistrictname +
                              '微信随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.weChat
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                    </el-table-column>
                    <!-- éšè®¿æƒ…况列(仅丽水市中医院显示) -->
                    <el-table-column v-if="orgname == '丽水市中医院'" align="center" label="随访情况">
                      <el-table-column label="正常语音" align="center" width="100" key="taskSituation1"
                        prop="taskSituation1">
                      </el-table-column><el-table-column label="患者拒接或拒访" align="center" width="100" key="taskSituation2"
                        prop="taskSituation2">
                      </el-table-column><el-table-column label="面访或者接诊" align="center" width="100" key="taskSituation3"
                        prop="taskSituation3">
                      </el-table-column><el-table-column label="微信随访" align="center" width="100" key="taskSituation4"
                        prop="taskSituation4">
                      </el-table-column><el-table-column label="随访电话不正确" align="center" width="100" key="taskSituation5"
                        prop="taskSituation5">
                      </el-table-column><el-table-column label="其他情况不宜随访" align="center" width="100"
                        key="taskSituation6" prop="taskSituation6">
                      </el-table-column>
                    </el-table-column>
                  </el-table>
                </div>
              </el-tab-pane>
              <el-tab-pane label="再次随访" name="second">
                <div class="your-table-container">
                  <el-table ref="exportTableSecond" id="exportTableidSecond" v-loading="loadingSecond"
                    :data="secondFollowUpList" :border="true" @selection-change="handleSelectionChangeSecond"
                    @expand-change="handleRowClickSecond" :row-key="getRowKey" show-summary
                    :summary-method="getSummariesSecond" :expand-row-keys="expandsSecond">
                    <!-- å±•开行箭头列 -->
                    <el-table-column type="expand">
                      <template slot-scope="props">
                        <el-table :data="props.row.doctorStats" border style="width: 95%; margin: 0 auto"
                          class="inner-table" show-summary :summary-method="getInnerSummariesSecond">
                          <el-table-column label="医生姓名" prop="drname" align="center" />
                          <el-table-column label="科室" width="120" prop="deptname" align="center" />
                          <el-table-column label="出院人次" prop="dischargeCount" align="center" />
                          <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount">
                          </el-table-column>
                          <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp"
                            prop="nonFollowUp">
                          </el-table-column>
                          <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded"
                            prop="followUpNeeded">
                          </el-table-column>
                          <el-table-column align="center" label="再次出院随访">
                            <el-table-column label="需随访" align="center" key="needFollowUpAgain"
                              prop="needFollowUpAgain">
                            </el-table-column>
                            <el-table-column label="待随访" align="center" key="pendingFollowUpAgain"
                              prop="pendingFollowUpAgain">
                            </el-table-column>
                            <el-table-column label="随访成功" align="center" key="followUpSuccessAgain"
                              prop="followUpSuccessAgain">
                            </el-table-column>
                            <el-table-column label="随访失败" align="center" key="followUpFailAgain"
                              prop="followUpFailAgain">
                            </el-table-column>
                            <el-table-column label="随访率" align="center" width="120" key="followUpRateAgain"
                              prop="followUpRateAgain">
                            </el-table-column>
                            <el-table-column label="人工" align="center" key="manualAgain" prop="manualAgain">
                            </el-table-column>
                            <el-table-column label="短信" align="center" key="smsAgain" prop="smsAgain">
                            </el-table-column>
                            <el-table-column label="微信" align="center" key="weChatAgain" prop="weChatAgain">
                            </el-table-column>
                          </el-table-column>
                        </el-table>
                      </template>
                    </el-table-column>
                    <el-table-column label="出院病区" align="center" sortable key="leavehospitaldistrictname"
                      prop="leavehospitaldistrictname" width="150" :show-overflow-tooltip="true"
                      :sort-method="sortChineseNumber" />
                    <el-table-column label="科室" align="center" key="deptname" prop="deptname"
                      :show-overflow-tooltip="true" />
                    <el-table-column label="出院人次" align="center" key="dischargeCount" prop="dischargeCount">
                    </el-table-column>
                    <el-table-column label="无需随访人次" align="center" width="100" key="nonFollowUp" prop="nonFollowUp">
                    </el-table-column>
                    <el-table-column label="应随访人次" align="center" width="100" key="followUpNeeded"
                      prop="followUpNeeded">
                    </el-table-column>
                    <el-table-column align="center" label="再次出院随访">
                      <el-table-column label="需随访" align="center" key="needFollowUpAgain" prop="needFollowUpAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.needFollowUpAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访需随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.needFollowUpAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="待随访" align="center" key="pendingFollowUpAgain"
                        prop="pendingFollowUpAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.pendingFollowUpAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访待随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.pendingFollowUpAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="随访成功" align="center" key="followUpSuccessAgain"
                        prop="followUpSuccessAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.followUpSuccessAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访随访成功列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.followUpSuccessAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="随访失败" align="center" key="followUpFailAgain" prop="followUpFailAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.followUpFailAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访随访失败列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.followUpFailAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="随访率" align="center" width="120" key="followUpRateAgain"
                        prop="followUpRateAgain">
                      </el-table-column>
                      <el-table-column label="人工" align="center" key="manualAgain" prop="manualAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.manualAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访人工随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.manualAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="短信" align="center" key="smsAgain" prop="smsAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.smsAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访短信随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.smsAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                      <el-table-column label="微信" align="center" key="weChatAgain" prop="weChatAgain">
                        <template slot-scope="scope">
                          <el-button size="medium" type="text" @click="
                            viewDetails(
                              scope.row.weChatAgainInfo,
                              scope.row.leavehospitaldistrictname +
                              '再次随访微信随访列表'
                            )
                            "><span class="button-zx">{{
                              scope.row.weChatAgain
                            }}</span></el-button>
                        </template>
                      </el-table-column>
                    </el-table-column>
                  </el-table>
                </div>
              </el-tab-pane>
            </el-tabs>
          </el-col>
        </el-row>
      </div>
    </div>
    <!-- ç»Ÿè®¡è¶‹åŠ¿å›¾å¼¹çª— -->
    <el-dialog title="随访统计趋势图" :visible.sync="chartDialogVisible" width="80%" :close-on-click-modal="false">
      <div class="chart-container">
        <el-row :gutter="20">
          <el-col :span="12">
            <div class="chart-title">随访状态分布</div>
            <div id="pieChart" style="width: 100%; height: 400px"></div>
          </el-col>
          <el-col :span="12">
            <div class="chart-title">随访趋势分析</div>
            <div id="barLineChart" style="width: 100%; height: 400px"></div>
          </el-col>
        </el-row>
      </div>
    </el-dialog>
    <el-dialog title="未及时随访患者服务" :visible.sync="SeedetailsVisible" v-loading="Seedloading" width="70%"
      :close-on-click-modal="false">
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <!--用户数据-->
            <el-form :model="patientqueryParams" ref="queryForm" size="small" :inline="true" label-width="98px">
              <el-form-item label="患者:">
                <el-input v-model="patientqueryParams.name" @keyup.enter.native="handleQuery"></el-input>
              </el-form-item>
              <el-form-item label="患者诊断:">
                <el-input v-model="patientqueryParams.leavediagname" @keyup.enter.native="handleQuery"></el-input>
              </el-form-item>
              <el-form-item>
                <el-button type="primary" icon="el-icon-search" size="medium" @click="handleQuery">搜索</el-button>
                <el-button icon="el-icon-refresh" size="medium" @click="resetQuery">取消创建</el-button>
              </el-form-item>
            </el-form>
            <!-- é€‰æ‹©æ‚£è€…列表 -->
            <el-table :data="logsheetlist" style="width: 100%">
              <el-table-column prop="sendname" align="center" label="姓名" width="100">
              </el-table-column>
              <el-table-column prop="taskName" align="center" width="200" show-overflow-tooltip label="任务名称">
              </el-table-column>
              <el-table-column prop="sendstate" align="center" width="200" label="任务状态">
                <template slot-scope="scope">
                  <div v-if="scope.row.sendstate == 1">
                    <el-tag type="primary" :disable-transitions="false">表单已领取</el-tag>
                  </div>
                  <div v-if="scope.row.sendstate == 2">
                    <el-tag type="primary" :disable-transitions="false">待随访</el-tag>
                  </div>
                  <div v-if="scope.row.sendstate == 3">
                    <el-tag type="success" :disable-transitions="false">表单已发送</el-tag>
                  </div>
                  <div v-if="scope.row.sendstate == 4">
                    <el-tag type="info" :disable-transitions="false">不执行</el-tag>
                  </div>
                  <div v-if="scope.row.sendstate == 5">
                    <el-tag type="danger" :disable-transitions="false">发送失败</el-tag>
                  </div>
                  <div v-if="scope.row.sendstate == 6">
                    <el-tag type="success" :disable-transitions="false">已完成</el-tag>
                  </div>
                </template>
              </el-table-column>
              <el-table-column prop="visitTime" align="center" label="应随访时间" width="200" show-overflow-tooltip>
              </el-table-column>
              <el-table-column prop="finishtime" align="center" label="随访完成时间" width="200" show-overflow-tooltip>
              </el-table-column>
              <el-table-column label="出院日期" width="200" align="center" key="endtime" prop="endtime">
                <template slot-scope="scope">
                  <span>{{ formatTime(scope.row.endtime) }}</span>
                </template></el-table-column>
              <el-table-column label="责任护士" width="120" align="center" key="nurseName" prop="nurseName" />
              <el-table-column label="主治医生" width="120" align="center" key="drname" prop="drname" />
              <el-table-column label="结果状态" align="center" key="excep" prop="excep" width="120">
                <template slot-scope="scope">
                  <dict-tag :options="dict.type.sys_yujing" :value="scope.row.excep" />
                </template>
              </el-table-column>
              <el-table-column label="处理意见" align="center" key="suggest" prop="suggest" width="120">
                <template slot-scope="scope">
                  <dict-tag :options="dict.type.sys_suggest" :value="scope.row.suggest" />
                </template>
              </el-table-column>
              <el-table-column prop="templatename" align="center" label="服务模板" width="200" show-overflow-tooltip>
              </el-table-column>
              <el-table-column prop="remark" align="center" label="服务记录" width="200" show-overflow-tooltip>
              </el-table-column>
              <el-table-column prop="bankcardno" align="center" label="呼叫状态" width="210">
              </el-table-column>
              <el-table-column label="操作" fixed="right" align="center" width="200"
                class-name="small-padding fixed-width">
                <template slot-scope="scope">
                  <el-button size="medium" type="text" @click="SeedetailsgGo(scope.row)"><span class="button-zx"><i
                        class="el-icon-s-order"></i>查看</span></el-button>
                </template>
              </el-table-column>
            </el-table>
          </el-row>
          <pagination v-show="patienttotal > 0 && this.patientqueryParams.allhosp != 6" :total="patienttotal"
            :page.sync="patientqueryParams.pn" :limit.sync="patientqueryParams.ps" @pagination="Seedetailstion" />
        </div>
      </div>
    </el-dialog>
    <!-- å„类详情 -->
    <el-dialog :title="infotitle" :visible.sync="infotitleVisible" v-loading="infotitloading" width="70%"
      :close-on-click-modal="false">
      <div style="margin-bottom: 16px; display: flex; align-items: center">
        <span style="margin-right: 10px; font-weight: bold">患者姓名查询:</span>
        <el-input v-model="searchName" placeholder="请输入患者姓名进行筛选" clearable style="width: 300px" @input="handleSearch"
          @clear="handleSearch">
        </el-input>
        <span style="margin-left: 10px; color: rgb(35, 81, 233); font-size: 16px">
          å…± {{ infotitlelist.length }} æ¡è®°å½•
        </span>
      </div>
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <!-- é€‰æ‹©æ‚£è€…列表 -->
            <div class="data-list" ref="dataList" @scroll="handleScroll" v-loading="infotitloading">
              <el-table :data="currentDisplayList" height="660" style="width: 100%">
                <el-table-column prop="sendname" align="center" label="姓名" width="100">
                </el-table-column>
                <el-table-column prop="taskName" align="center" width="200" show-overflow-tooltip label="任务名称">
                </el-table-column>
                <el-table-column prop="sendstate" align="center" width="200" label="任务状态">
                  <template slot-scope="scope">
                    <div v-if="scope.row.sendstate == 1">
                      <el-tag type="primary" :disable-transitions="false">表单已领取</el-tag>
                    </div>
                    <div v-if="scope.row.sendstate == 2">
                      <el-tag type="primary" :disable-transitions="false">待随访</el-tag>
                    </div>
                    <div v-if="scope.row.sendstate == 3">
                      <el-tag type="success" :disable-transitions="false">表单已发送</el-tag>
                    </div>
                    <div v-if="scope.row.sendstate == 4">
                      <el-tag type="info" :disable-transitions="false">不执行</el-tag>
                    </div>
                    <div v-if="scope.row.sendstate == 5">
                      <el-tag type="danger" :disable-transitions="false">发送失败</el-tag>
                    </div>
                    <div v-if="scope.row.sendstate == 6">
                      <el-tag type="success" :disable-transitions="false">已完成</el-tag>
                    </div>
                  </template>
                </el-table-column>
                <el-table-column label="任务执行方式" align="center" key="preachform" prop="preachform" width="160"
                  :show-overflow-tooltip="true">
                  <template slot-scope="scope">
                    <span v-for="item in scope.row.preachform">{{ item }}、
                    </span>
                  </template>
                </el-table-column>
                <el-table-column prop="visitTime" align="center" label="应随访时间" width="200" show-overflow-tooltip>
                </el-table-column>
                <el-table-column prop="finishtime" align="center" label="随访完成时间" width="200" show-overflow-tooltip>
                </el-table-column>
                <el-table-column label="出院日期" width="200" align="center" key="endtime" prop="endtime">
                  <template slot-scope="scope">
                    <span>{{ formatTime(scope.row.endtime) }}</span>
                  </template></el-table-column>
                <el-table-column label="责任护士" width="120" align="center" key="nurseName" prop="nurseName" />
                <el-table-column label="主治医生" width="120" align="center" key="drname" prop="drname" />
                <el-table-column label="结果状态" align="center" key="excep" prop="excep" width="120">
                  <template slot-scope="scope">
                    <dict-tag :options="dict.type.sys_yujing" :value="scope.row.excep" />
                  </template>
                </el-table-column>
                <el-table-column label="处理意见" align="center" key="suggest" prop="suggest" width="120">
                  <template slot-scope="scope">
                    <dict-tag :options="dict.type.sys_suggest" :value="scope.row.suggest" />
                  </template>
                </el-table-column>
                <el-table-column prop="templatename" align="center" label="服务模板" width="200" show-overflow-tooltip>
                </el-table-column>
                <el-table-column prop="remark" align="center" label="服务记录" width="200" show-overflow-tooltip>
                </el-table-column>
                <el-table-column prop="bankcardno" align="center" label="呼叫状态" width="210">
                </el-table-column>
                <el-table-column label="操作" fixed="right" align="center" width="200"
                  class-name="small-padding fixed-width">
                  <template slot-scope="scope">
                    <el-button size="medium" type="text" @click="SeedetailsgGo(scope.row)"><span class="button-zx"><i
                          class="el-icon-s-order"></i>查看</span></el-button>
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </el-row>
        </div>
      </div>
    </el-dialog>
  </div>
</template>
<script>
import {
  toamendtag,
  addapitag,
  deletetag,
  changetagcategory,
} from "@/api/system/label";
import store from "@/store";
import { getSfStatistics, selectTimelyRate } from "@/api/system/user";
import * as XLSX from "xlsx";
import FileSaver from "file-saver";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
const shortcuts = [
  {
    text: "今天",
    onClick(picker) {
      picker.$emit("pick", new Date());
    },
  },
  {
    text: "昨天",
    onClick(picker) {
      const date = new Date();
      date.setTime(date.getTime() - 3600 * 1000 * 24);
      picker.$emit("pick", date);
    },
  },
  {
    text: "一周前",
    onClick(picker) {
      const date = new Date();
      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
      picker.$emit("pick", date);
    },
  },
];
export default {
  name: "Percentage",
  dicts: ["sys_normal_disable", "sys_user_sex"],
  components: { Treeselect },
  data() {
    return {
      // æ–°å¢žï¼šTab标签页控制
      activeTab: "first", // å½“前激活的tab,first-首次随访,second-再次随访
      // åˆ†ç¦»çš„æ•°æ®åˆ—表
      firstFollowUpList: [], // é¦–次随访数据
      secondFollowUpList: [], // å†æ¬¡éšè®¿æ•°æ®
      // åˆ†ç¦»çš„加载状态
      loading: false, // é¦–次随访表格加载状态
      loadingSecond: false, // å†æ¬¡éšè®¿è¡¨æ ¼åŠ è½½çŠ¶æ€
      // åˆ†ç¦»çš„展开状态
      expands: [], // é¦–次随访表格展开行
      expandsSecond: [], // å†æ¬¡éšè®¿è¡¨æ ¼å±•开行
      // åˆ†ç¦»çš„选择状态
      ids: [], // é¦–次随访选中项
      idsSecond: [], // å†æ¬¡éšè®¿é€‰ä¸­é¡¹
      orgname: "",
      infotitlelist: [],
      currentDisplayList: [],
      loadIndex: 0,
      pageSize: 100,
      isLoading: false,
      Seedloading: false,
      chartDialogVisible: false,
      infotitleVisible: false,
      searchName: "",
      infotitloading: false,
      infotitle: "",
      pieChart: null,
      barLineChart: null,
      single: true,
      multiple: true,
      showSearch: true,
      idds: "",
      total: 0,
      flatArrayhospit: [],
      flatArraydept: [],
      patienttotal: 0,
      logsheetlist: [],
      Statisticallist: [
        {
          label: "病区统计",
          value: 1,
        },
        {
          label: "科室统计",
          value: 2,
        },
      ],
      patientqueryParams: {
        pn: 1,
        ps: 10,
      },
      amendtag: false,
      lstamendtag: false,
      scavisible: false,
      deleteVisible: false,
      deletefenl: "高血压",
      tagform: {
        isupload: "",
        tagname: "",
        tagcategoryid: "",
        tagdescription: "",
      },
      classifyform: {
        categoryname: "",
      },
      title: "",
      open: false,
      dateRange: [],
      postOptions: [],
      roleOptions: [],
      allDeptCodes: [],
      allWardCodes: [],
      checkboxlist: [],
      form: {},
      forms: {
        name: "",
      },
      numberlb: 22,
      dialogFormVisible: false,
      lstamendtagVisible: false,
      goQRCodeVisible: false,
      sidecolumnval: "",
      propss: { multiple: true },
      SeedetailsVisible: false,
      options: store.getters.tasktypes,
      pickerOptions: {
        disabledDate(time) {
          return time.getTime() < Date.now() - 3600 * 1000 * 24;
        },
        shortcuts: shortcuts,
      },
      pickerOptionsa: {
        disabledDate(time) {
          return time.getTime() > Date.now();
        },
        shortcuts: shortcuts,
      },
      queryParams: {
        serviceType: [2],
        dateRange: [],
        statisticaltype: 1,
        leavehospitaldistrictcodes: ["all"],
        deptcodes: [],
        visitCount: 1, // æ–°å¢žï¼šéšè®¿æ¬¡æ•°å‚数,1-首次,2-再次
      },
      columns: [
        { key: 0, label: `标签编号`, visible: true },
        { key: 1, label: `标签名称`, visible: true },
        { key: 2, label: `标签昵称`, visible: true },
        { key: 3, label: `部门`, visible: true },
        { key: 4, label: `手机号码`, visible: true },
        { key: 5, label: `状态`, visible: true },
        { key: 6, label: `创建时间`, visible: true },
      ],
    };
  },
  watch: {},
  created() {
    this.getDeptTree();
    this.getFirstFollowUpList(); // é»˜è®¤åŠ è½½é¦–æ¬¡éšè®¿æ•°æ®
    this.checkboxlist = store.getters.checkboxlist;
    this.orgname = localStorage.getItem("orgname");
  },
  methods: {
    /** æŸ¥è¯¢é¦–次随访列表 */
    async getFirstFollowUpList() {
      this.loading = true;
      this.queryParams.visitCount = 1; // è®¾ç½®éšè®¿æ¬¡æ•°ä¸ºé¦–次
      const params = {
        ...this.queryParams,
        leavehospitaldistrictcodes:
          this.queryParams.leavehospitaldistrictcodes.includes("all")
            ? this.allWardCodes
            : this.queryParams.leavehospitaldistrictcodes,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.allDeptCodes
          : this.queryParams.deptcodes,
      };
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      try {
        const response = await getSfStatistics(params);
        this.firstFollowUpList = this.customSort(response.data);
        this.total = response.total;
      } catch (error) {
        console.error("获取首次随访数据失败:", error);
        this.$message.error("获取首次随访数据失败");
      } finally {
        this.loading = false;
      }
    },
    /** æŸ¥è¯¢å†æ¬¡éšè®¿åˆ—表 */
    async getSecondFollowUpList() {
      this.loadingSecond = true;
      this.queryParams.visitCount = 2; // è®¾ç½®éšè®¿æ¬¡æ•°ä¸ºå†æ¬¡
      const params = {
        ...this.queryParams,
        leavehospitaldistrictcodes:
          this.queryParams.leavehospitaldistrictcodes.includes("all")
            ? this.allWardCodes
            : this.queryParams.leavehospitaldistrictcodes,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.allDeptCodes
          : this.queryParams.deptcodes,
      };
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      try {
        const response = await getSfStatistics(params);
        this.secondFollowUpList = this.customSort(response.data);
        this.total = response.total;
      } catch (error) {
        console.error("获取再次随访数据失败:", error);
        this.$message.error("获取再次随访数据失败");
      } finally {
        this.loadingSecond = false;
      }
    },
    /** Tab切换事件 */
    handleTabClick(tab) {
      if (tab.name === "first") {
        if (this.firstFollowUpList.length === 0) {
          this.getFirstFollowUpList();
        }
      } else if (tab.name === "second") {
        if (this.secondFollowUpList.length === 0) {
          this.getSecondFollowUpList();
        }
      }
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
    // æœç´¢å¤„理函数
    handleSearch() {
      if (!this.searchName.trim()) {
        // å¦‚果搜索框为空,显示所有数据
        this.currentDisplayList = [...this.infotitlelist];
      } else {
        // æ ¹æ®æ‚£è€…姓名进行筛选(不区分大小写)
        const keyword = this.searchName.toLowerCase();
        this.currentDisplayList = this.infotitlelist.filter((item) => {
          return item.sendname && item.sendname.toLowerCase().includes(keyword);
        });
      }
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
        "十三",
        "十四",
        "十五",
        "十六",
        "十七",
        "十八",
        "十九",
        "二十",
        "二十一",
        "二十二",
        "二十三",
        "二十四",
        "二十五",
        "二十六",
        "二十七",
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    getRowKey(row) {
      return row.statisticaltype === 1
        ? row.leavehospitaldistrictcode
        : row.deptcode;
    },
    // å¤„理行点击展开
    handleRowClick(row) {
      console.log(row, "row");
      // å¦‚果已经展开则收起
      if (this.expands.includes(this.getRowKey(row))) {
        this.expands = [];
        return;
      }
      // å¤„理查询参数
      const params = {
        ...this.queryParams,
        // å¦‚果选择了"全部",则传所有病区/科室代码
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.allDeptCodes
          : this.queryParams.deptcodes,
        leavehospitaldistrictcodes: [row.leavehospitaldistrictcode],
        drcode: "1",
        visitCount: 1, // è®¾ç½®ä¸ºé¦–次随访
      };
      // ç§»é™¤å¯èƒ½å­˜åœ¨çš„"all"值
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      // å¦‚果该行还没有加载医生数据,则加载
      if (!row.doctorStats) {
        this.loading = true;
        getSfStatistics(params).then((res) => {
          this.$set(row, "doctorStats", res.data);
          this.expands = [this.getRowKey(row)];
          this.loading = false;
        });
      } else {
        this.expands = [this.getRowKey(row)];
      }
    },
    getSummaries(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计";
          return;
        }
        if (index === 1 || index === 2) {
          sums[index] = "/";
          return;
        }
        // å¯¹ç™¾åˆ†æ¯”字段特殊处理 - å–平均值
        if (
          column.property === "followUpRate" ||
          column.property === "rate" ||
          column.property === "followUpRateAgain"
        ) {
          // æå–所有有效百分比值并转换为小数
          const percentageValues = data
            .map((item) => {
              const value = item[column.property];
              if (!value || value === "-" || value === "0%") return null;
              // å¤„理带百分号的数据
              if (typeof value === "string" && value.includes("%")) {
                // åŽ»é™¤ç™¾åˆ†å·å¹¶è½¬æ¢ä¸ºå°æ•°
                const numValue = parseFloat(value.replace("%", "")) / 100;
                return isNaN(numValue) ? null : numValue;
              } else {
                // å¤„理已经是小数的数据
                const numValue = parseFloat(value);
                return isNaN(numValue) ? null : numValue;
              }
            })
            .filter((value) => value !== null && value !== 0); // è¿‡æ»¤æŽ‰null和0值
          if (percentageValues.length > 0) {
            const average =
              percentageValues.reduce((sum, value) => sum + value, 0) /
              percentageValues.length;
            sums[index] = (average * 100).toFixed(2) + "%";
          } else {
            sums[index] = "0.00%";
          }
        } else {
          // æ™®é€šæ•°å­—字段 - æ±‚å’Œ
          const values = data.map((item) => {
            const value = item[column.property];
            if (value === "-" || value === "" || value === null) return 0;
            return Number(value) || 0;
          });
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0);
            sums[index] = this.formatNumber(sums[index]);
          } else {
            sums[index] = "-";
          }
        }
      });
      return sums;
    },
    // å†…部表格合计行计算方法
    getInnerSummaries(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "小计";
          return;
        }
        if (column.property === "drname" || column.property === "deptname") {
          sums[index] = "-";
          return;
        }
        // å¯¹ç™¾åˆ†æ¯”字段特殊处理 - å–平均值
        if (column.property === "followUpRate" || column.property === "rate") {
          // æå–所有有效百分比值并转换为小数
          const percentageValues = data
            .map((item) => {
              const value = item[column.property];
              if (!value || value === "-" || value === "0%") return null;
              // å¤„理带百分号的数据
              if (typeof value === "string" && value.includes("%")) {
                // åŽ»é™¤ç™¾åˆ†å·å¹¶è½¬æ¢ä¸ºå°æ•°
                const numValue = parseFloat(value.replace("%", "")) / 100;
                return isNaN(numValue) ? null : numValue;
              } else {
                // å¤„理已经是小数的数据
                const numValue = parseFloat(value);
                return isNaN(numValue) ? null : numValue;
              }
            })
            .filter((value) => value !== null && value !== 0);
          if (percentageValues.length > 0) {
            const average =
              percentageValues.reduce((sum, value) => sum + value, 0) /
              percentageValues.length;
            sums[index] = (average * 100).toFixed(2) + "%";
          } else {
            sums[index] = "0.00%";
          }
        } else {
          // æ™®é€šæ•°å­—字段 - æ±‚å’Œ
          const values = data.map((item) => {
            const value = item[column.property];
            if (value === "-" || value === "" || value === null) return 0;
            return Number(value) || 0;
          });
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0);
            sums[index] = this.formatNumber(sums[index]);
          } else {
            sums[index] = "-";
          }
        }
      });
      return sums;
    },
    // è¾…助方法:提取百分比数值
    extractPercentageValue(value) {
      if (!value) return null;
      if (typeof value === "string") {
        // å¤„理带百分号的字符串
        if (value.includes("%")) {
          const num = parseFloat(value.replace("%", ""));
          return isNaN(num) ? null : num / 100;
        }
        // å¤„理纯数字字符串
        const num = parseFloat(value);
        return isNaN(num) ? null : num;
      }
      // å¤„理数字类型
      return typeof value === "number" ? value : null;
    },
    // æ•°å­—格式化方法
    formatNumber(num) {
      if (isNaN(num)) return "-";
      return Number.isInteger(num) ? num.toString() : num.toFixed(0);
    },
    /** ä¿®æ”¹æ ‡ç­¾ */
    handleUpdate(row) {
      console.log(row, "修改标签");
      this.lstamendtagVisible = true;
      this.lstamendtag = true;
      this.tagform = {
        isupload: row.isupload,
        tagname: row.tagname,
        tagcategoryid: row.tagcategoryid,
        tagdescription: row.tagdescription,
        tagid: row.tagid,
      };
    },
    // èŽ·å–ç§‘å®¤æ ‘
    getDeptTree() {
      // ç§‘室列表
      this.flatArraydept = store.getters.belongDepts.map((dept) => {
        return {
          label: dept.deptName,
          value: dept.deptCode,
        };
      });
      // å­˜å‚¨æ‰€æœ‰ç§‘室代码
      this.allDeptCodes = store.getters.belongDepts.map(
        (dept) => dept.deptCode
      );
      // ç—…区列表
      this.flatArrayhospit = store.getters.belongWards.map((ward) => {
        return {
          label: ward.districtName,
          value: ward.districtCode,
        };
      });
      // å­˜å‚¨æ‰€æœ‰ç—…区代码
      this.allWardCodes = store.getters.belongWards.map(
        (ward) => ward.districtCode
      );
      this.flatArraydept.push({ label: "全部", value: "all" });
      this.flatArrayhospit.push({ label: "全部", value: "all" });
    },
    flattenArray(multiArray) {
      let result = [];
      // é€’归函数,用于将多级数组转换为一维数组,只包含最底层的元素
      function flatten(element) {
        // å¦‚果当前元素有子元素,继续递归
        if (element.children && element.children.length > 0) {
          element.children.forEach((child) => flatten(child));
        } else {
          // å…‹éš†å…ƒç´ ä»¥é¿å…ä¿®æ”¹åŽŸå§‹æ•°æ®
          let item = JSON.parse(JSON.stringify(element));
          result.push(item); // å°†æœ€åº•层的元素添加到结果数组
        }
      }
      // ä»Žé¡¶å±‚元素开始递归
      multiArray.forEach((element) => flatten(element));
      return result; // è¿”回只包含最底层元素的一维数组
    },
    addladeltag() {
      this.lstamendtagVisible = true;
      this.lstamendtag = false;
      this.tagform = {
        isupload: "",
        tagname: "",
        tagcategoryid: "",
        tagdescription: "",
        tagid: "",
      };
    },
    Seedetails(row) {
      this.SeedetailsVisible = true;
      this.Seedloading = true;
      this.patientqueryParams.starttime = this.parseTime(
        this.queryParams.dateRange[0]
      );
      this.patientqueryParams.endtime = this.parseTime(
        this.queryParams.dateRange[1]
      );
      this.patientqueryParams.deptcode = row.deptcode;
      selectTimelyRate(this.patientqueryParams).then((response) => {
        this.logsheetlist = response.data.detail;
        this.patienttotal = response.data.total;
        this.Seedloading = false;
      });
    },
    Seedetailstion() {
      selectTimelyRate(this.patientqueryParams).then((response) => {
        this.logsheetlist = response.data.detail;
        this.patienttotal = response.data.total;
        this.Seedloading = false;
      });
    },
    viewDetails(row, title) {
      this.infotitleVisible = true;
      this.infotitle = title;
      this.infotitlelist = row; // å‡è®¾row就是需要展示的详细数组
      console.log(this.infotitlelist, "this.infotitlelist");
      this.infotitlelist.forEach((item) => {
        let idArray = null;
        if (item.preachform) {
          if (item.endtime) {
            item.preachformson = item.preachform;
            idArray = item.preachform.split(",");
          }
          item.preachform = idArray.map((value) => {
            // æŸ¥æ‰¾id对应的对象
            const item = this.checkboxlist.find((item) => item.value == value);
            // å¦‚果找到对应的id,返回label值,否则返回null
            return item ? item.label : null;
          });
        }
      });
      // åˆå§‹åŒ–加载
      this.loadIndex = 0;
      this.currentDisplayList = [];
      this.$nextTick(() => {
        this.loadMoreData();
      });
    },
    loadMoreData() {
      if (this.isLoading) return;
      this.isLoading = true;
      // æ¨¡æ‹Ÿå¼‚步加载,实际可能是直接切片本地数据
      setTimeout(() => {
        console.log(this.infotitlelist, "this.infotitlelist");
        const nextChunk = this.infotitlelist.slice(
          this.loadIndex,
          this.loadIndex + this.pageSize
        );
        this.currentDisplayList = this.currentDisplayList.concat(nextChunk);
        this.loadIndex += this.pageSize;
        this.isLoading = false;
      }, 200);
    },
    handleScroll(event) {
      const scrollContainer = event.target;
      // åˆ¤æ–­æ˜¯å¦æ»šåŠ¨åˆ°åº•éƒ¨
      const isAtBottom =
        scrollContainer.scrollTop + scrollContainer.clientHeight >=
        scrollContainer.scrollHeight - 10;
      if (
        isAtBottom &&
        !this.isLoading &&
        this.loadIndex < this.infotitlelist.length
      ) {
        this.loadMoreData();
      }
    },
    SeedetailsgGo(row) {
      this.SeedetailsVisible = false;
      let type = "";
      if (row.preachformson && row.preachformson.includes("3")) {
        type = 1;
      }
      setTimeout(() => {
        this.$router.push({
          path: "/followvisit/record/detailpage/",
          query: {
            taskid: row.taskid,
            patid: row.patid,
            id: row.id,
            Voicetype: type,
            // visitCount: this.topqueryParams.visitCount,
          },
        });
      }, 300);
    },
    // æ·»åŠ /修改标签
    Maintenancetag() {
      if (this.lstamendtag) {
        toamendtag(this.addDateRange(this.tagform)).then((response) => {
          console.log(response);
          this.getList();
        });
      } else {
        addapitag(this.addDateRange(this.tagform)).then((response) => {
          console.log(response);
          this.getList();
        });
      }
      this.tagform = {
        isupload: "",
        tagname: "",
        tagcategoryid: "",
        tagdescription: "",
        tagid: "",
      };
    },
    routerErr(row) {
      console.log(row, "跳转异常");
      this.$router.push({
        path: "/followvisit/discharge",
        query: {
          errtype: 1,
          leavehospitaldistrictcode: row.leavehospitaldistrictcode,
        },
      });
    },
    // è¡¨å•重置
    reset() {
      this.form = {
        userId: undefined,
        deptId: undefined,
        userName: undefined,
        nickName: undefined,
        password: undefined,
        phonenumber: undefined,
        email: undefined,
        sex: undefined,
        status: "0",
        remark: undefined,
        postIds: [],
        roleIds: [],
      };
      this.resetForm("form");
    },
    // æ ‡ç­¾çŠ¶æ€ä¿®æ”¹
    handleStatusChange(row) {
      console.log(row.isupload);
      let text = row.isupload === "0" ? "启用" : "停用";
      this.$modal
        .confirm('确认要"' + text + '""' + row.tagname + '"标签吗?')
        .then(function () {
          return changetagcategory(row.tagid, row.isupload);
        })
        .then(() => {
          this.$modal.msgSuccess(text + "成功");
        })
        .catch(function () {
          row.isupload = row.isupload === "0" ? "1" : "0";
        });
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ - ä¿®æ”¹ä¸ºæœç´¢å½“前激活的tab数据 */
    handleQuery() {
      this.queryParams.pageNum = 1;
      if (!this.queryParams.dateRange) this.queryParams.dateRange = [];
      if (this.queryParams.statisticaltype == 1) {
        this.queryParams.deptcodes = [];
      } else if (this.queryParams.statisticaltype == 2) {
        this.queryParams.leavehospitaldistrictcodes = [];
      }
      this.queryParams.startTime = this.parseTime(
        this.queryParams.dateRange[0]
      );
      this.queryParams.endTime = this.parseTime(this.queryParams.dateRange[1]);
      // æ ¹æ®å½“前激活的tab加载对应数据
      if (this.activeTab === "first") {
        this.getFirstFollowUpList();
      } else {
        this.getSecondFollowUpList();
      }
    },
    /** é‡ç½®æŒ‰é’®æ“ä½œ */
    resetQuery() {
      this.queryParams.dateRange = [];
      this.queryParams.leavehospitaldistrictcodes = [];
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.tagid);
      this.single = selection.length != 1;
      this.multiple = !selection.length;
    },
    /** åˆ é™¤æŒ‰é’®æ“ä½œ */
    handleDelete(row) {
      console.log(row, "删除弹窗");
      const tagids = row.tagid || this.ids;
      console.log(tagids);
      const tagname = row.tagname;
      this.$modal
        .confirm(
          tagname
            ? '是否确认删除标签名称为"' + tagname + '"的数据项?'
            : "是否确认删除选中的数据项?"
        )
        .then(function () {
          return deletetag(tagids);
        })
        .then(() => {
          this.getList();
          this.$modal.msgSuccess("删除成功");
        })
        .catch(() => { });
    },
    // å¯¼å‡ºæ–¹æ³•
    async exportTable() {
      try {
        // 1. èŽ·å–å¹¶æ ¼å¼åŒ–æ—¥æœŸèŒƒå›´
        let dateRangeString = "";
        let sheetNameSuffix = "";
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateStr = this.queryParams.dateRange[0];
          const endDateStr = this.queryParams.dateRange[1];
          const formatDateForDisplay = (dateTimeStr) => {
            return dateTimeStr.split(" ")[0];
          };
          const startDateFormatted = formatDateForDisplay(startDateStr);
          const endDateFormatted = formatDateForDisplay(endDateStr);
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
        } else {
          const now = new Date();
          const currentMonth = now.getMonth() + 1;
          dateRangeString = `${currentMonth}月`;
          sheetNameSuffix = `${currentMonth}月`;
        }
        // 2. æ ¹æ®å½“前激活的tab确定导出的数据
        const isFirstFollowUp = this.activeTab === "first";
        let excelName, worksheetName, dataToExport;
        if (isFirstFollowUp) {
          excelName = `首次出院随访统计表_${dateRangeString}.xlsx`;
          worksheetName = `首次随访统计_${sheetNameSuffix}`;
          dataToExport = this.firstFollowUpList;
          if (!dataToExport || dataToExport.length === 0) {
            this.$message.warning("暂无首次随访数据可导出");
            return false;
          }
        } else {
          excelName = `再次出院随访统计表_${dateRangeString}.xlsx`;
          worksheetName = `再次随访统计_${sheetNameSuffix}`;
          dataToExport = this.secondFollowUpList;
          if (!dataToExport || dataToExport.length === 0) {
            this.$message.warning("暂无再次随访数据可导出");
            return false;
          }
        }
        // 3. åˆ›å»ºå·¥ä½œç°¿å’Œå·¥ä½œè¡¨
        const workbook = new ExcelJS.Workbook();
        const worksheet = workbook.addWorksheet(worksheetName);
        // 4. æž„建表格
        if (isFirstFollowUp) {
          this.buildFirstFollowUpExportSheet(
            worksheet,
            dataToExport,
            sheetNameSuffix
          );
        } else {
          this.buildSecondFollowUpExportSheet(
            worksheet,
            dataToExport,
            sheetNameSuffix
          );
        }
        // 5. ç”Ÿæˆå¹¶ä¸‹è½½æ–‡ä»¶
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
        this.$message.success("导出成功");
        return true;
      } catch (error) {
        console.error("导出失败:", error);
        this.$message.error(`导出失败: ${error.message}`);
        return false;
      }
    },
    /** æž„建首次随访导出表格 */
    buildFirstFollowUpExportSheet(worksheet, data, sheetNameSuffix) {
      const titleStyle = {
        font: {
          name: "微软雅黑",
          size: 16,
          bold: true,
          color: { argb: "FF000000" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFE6F3FF" },
        },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const headerStyle = {
        font: {
          name: "微软雅黑",
          size: 11,
          bold: true,
          color: { argb: "FF000000" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF5F7FA" },
        },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const cellStyle = {
        font: { name: "宋体", size: 10, color: { argb: "FF000000" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const summaryStyle = {
        font: {
          name: "宋体",
          size: 10,
          bold: true,
          color: { argb: "FF409EFF" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF5F7FA" },
        },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      // 1. æ·»åŠ æ€»æ ‡é¢˜è¡Œ
      worksheet.mergeCells(1, 1, 1, 16); // åˆå¹¶A1到P1
      const titleCell = worksheet.getCell(1, 1);
      titleCell.value = `首次出院随访统计表_${sheetNameSuffix}`;
      titleCell.style = titleStyle;
      worksheet.getRow(1).height = 35;
      // 2. åˆ›å»ºè¡¨å¤´
      const secondRowHeaders = [
        "", // A2 å±•开列占位
        "出院病区",
        "科室",
        "出院人次",
        "无需随访人次",
        "应随访人次", // B2 to F2
        // é¦–次出院随访子表头
        "需随访",
        "待随访",
        "随访成功",
        "随访失败",
        "随访率",
        "及时率",
        "人工",
        "短信",
        "微信",
      ];
      // æ·»åŠ ç¬¬äºŒè¡Œ
      secondRowHeaders.forEach((header, index) => {
        const cell = worksheet.getCell(3, index + 1);
        cell.value = header;
        cell.style = headerStyle;
      });
      // 3. åˆå¹¶å•元格
      // åˆå¹¶ A2:A3, B2:B3, C2:C3, D2:D3, E2:E3, F2:F3
      for (let i = 1; i <= 6; i++) {
        worksheet.mergeCells(2, i, 3, i);
        const cell = worksheet.getCell(2, i);
        cell.style = headerStyle;
      }
      // è®¾ç½®ç¬¬ä¸€è¡Œåˆå¹¶å•元格的值
      worksheet.getCell(2, 1).value = "";
      worksheet.getCell(2, 2).value = "出院病区";
      worksheet.getCell(2, 3).value = "科室";
      worksheet.getCell(2, 4).value = "出院人次";
      worksheet.getCell(2, 5).value = "无需随访人次";
      worksheet.getCell(2, 6).value = "应随访人次";
      // 4. åˆå¹¶"首次出院随访"标题
      worksheet.mergeCells(2, 7, 2, 15); // G2:O2
      worksheet.getCell(2, 7).value = "首次出院随访";
      worksheet.getCell(2, 7).style = headerStyle;
      // 5. è®¾ç½®è¡Œé«˜
      worksheet.getRow(2).height = 28;
      worksheet.getRow(3).height = 25;
      // 6. æ·»åŠ æ•°æ®è¡Œ
      data.forEach((item, rowIndex) => {
        const dataRow = worksheet.addRow(
          [
            "", // å±•开列
            item.leavehospitaldistrictname || "",
            item.deptname || "",
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
            // é¦–次出院随访数据
            item.needFollowUp || 0,
            item.pendingFollowUp || 0,
            item.followUpSuccess || 0,
            item.followUpFail || 0,
            item.followUpRate || "0%",
            item.rate ? (Number(item.rate) * 100).toFixed(2) + "%" : "0%",
            item.manual || 0,
            item.sms || 0,
            item.weChat || 0,
          ],
          rowIndex + 4
        );
        // åº”用数据行样式
        dataRow.eachCell((cell) => {
          cell.style = cellStyle;
        });
        dataRow.height = 24;
      });
      // 7. æ·»åŠ åˆè®¡è¡Œ
      const summaries = this.getFirstFollowUpSummaries(data);
      const summaryRow = worksheet.addRow(summaries);
      summaryRow.eachCell((cell, colNumber) => {
        cell.style = summaryStyle;
        if (colNumber === 1) {
          cell.value = "合计";
        }
      });
      summaryRow.height = 28;
      // 8. è®¾ç½®åˆ—宽
      worksheet.columns = [
        { width: 8 }, // å±•开列
        { width: 20 }, // å‡ºé™¢ç—…区
        { width: 15 }, // ç§‘室
        { width: 12 }, // å‡ºé™¢äººæ¬¡
        { width: 12 }, // æ— éœ€éšè®¿äººæ¬¡
        { width: 12 }, // åº”随访人次
        // é¦–次出院随访列
        { width: 10 }, // éœ€éšè®¿
        { width: 10 }, // å¾…随访
        { width: 10 }, // éšè®¿æˆåŠŸ
        { width: 10 }, // éšè®¿å¤±è´¥
        { width: 12 }, // éšè®¿çއ
        { width: 12 }, // åŠæ—¶çއ
        { width: 8 }, // äººå·¥
        { width: 8 }, // çŸ­ä¿¡
        { width: 8 }, // å¾®ä¿¡
      ];
    },
    /** é¦–次随访数据合计行计算 */
    getFirstFollowUpSummaries(data) {
      const summaries = [
        "合计",
        "/",
        "/",
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        "0%",
        "0%",
        0,
        0,
        0,
      ];
      data.forEach((item) => {
        // æ•°å€¼å­—段求和
        summaries[3] += Number(item.dischargeCount) || 0;
        summaries[4] += Number(item.nonFollowUp) || 0;
        summaries[5] += Number(item.followUpNeeded) || 0;
        summaries[6] += Number(item.needFollowUp) || 0;
        summaries[7] += Number(item.pendingFollowUp) || 0;
        summaries[8] += Number(item.followUpSuccess) || 0;
        summaries[9] += Number(item.followUpFail) || 0;
        summaries[12] += Number(item.manual) || 0;
        summaries[13] += Number(item.sms) || 0;
        summaries[14] += Number(item.weChat) || 0;
      });
      // è®¡ç®—百分比字段的平均值
      const followUpRateValues = data
        .map((item) => this.extractPercentageValue(item.followUpRate))
        .filter((value) => value !== null);
      const rateValues = data
        .map((item) => this.extractPercentageValue(item.rate))
        .filter((value) => value !== null);
      if (followUpRateValues.length > 0) {
        const avgFollowUpRate =
          followUpRateValues.reduce((sum, val) => sum + val, 0) /
          followUpRateValues.length;
        summaries[10] = (avgFollowUpRate * 100).toFixed(2) + "%";
      }
      if (rateValues.length > 0) {
        const avgRate =
          rateValues.reduce((sum, val) => sum + val, 0) / rateValues.length;
        summaries[11] = (avgRate * 100).toFixed(2) + "%";
      }
      // æ ¼å¼åŒ–æ•°å­—
      summaries[3] = this.formatNumber(summaries[3]);
      summaries[4] = this.formatNumber(summaries[4]);
      summaries[5] = this.formatNumber(summaries[5]);
      summaries[6] = this.formatNumber(summaries[6]);
      summaries[7] = this.formatNumber(summaries[7]);
      summaries[8] = this.formatNumber(summaries[8]);
      summaries[9] = this.formatNumber(summaries[9]);
      summaries[12] = this.formatNumber(summaries[12]);
      summaries[13] = this.formatNumber(summaries[13]);
      summaries[14] = this.formatNumber(summaries[14]);
      return summaries;
    },
    /** æž„建再次随访导出表格 */
    buildSecondFollowUpExportSheet(worksheet, data, sheetNameSuffix) {
      const titleStyle = {
        font: {
          name: "微软雅黑",
          size: 16,
          bold: true,
          color: { argb: "FF000000" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFE6F3FF" },
        },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const headerStyle = {
        font: {
          name: "微软雅黑",
          size: 11,
          bold: true,
          color: { argb: "FF000000" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF5F7FA" },
        },
        alignment: { vertical: "middle", horizontal: "center", wrapText: true },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const cellStyle = {
        font: { name: "宋体", size: 10, color: { argb: "FF000000" } },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      const summaryStyle = {
        font: {
          name: "宋体",
          size: 10,
          bold: true,
          color: { argb: "FF409EFF" },
        },
        fill: {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF5F7FA" },
        },
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin", color: { argb: "FFD0D0D0" } },
          left: { style: "thin", color: { argb: "FFD0D0D0" } },
          bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
          right: { style: "thin", color: { argb: "FFD0D0D0" } },
        },
      };
      // 1. æ·»åŠ æ€»æ ‡é¢˜è¡Œ
      worksheet.mergeCells(1, 1, 1, 15); // åˆå¹¶A1到O1
      const titleCell = worksheet.getCell(1, 1);
      titleCell.value = `再次出院随访统计表_${sheetNameSuffix}`;
      titleCell.style = titleStyle;
      worksheet.getRow(1).height = 35;
      // 2. åˆ›å»ºè¡¨å¤´
      const secondRowHeaders = [
        "", // A2 å±•开列占位
        "出院病区",
        "科室",
        "出院人次",
        "无需随访人次",
        "应随访人次", // B2 to F2
        // å†æ¬¡å‡ºé™¢éšè®¿å­è¡¨å¤´
        "需随访",
        "待随访",
        "随访成功",
        "随访失败",
        "随访率",
        "人工",
        "短信",
        "微信",
      ];
      // æ·»åŠ ç¬¬äºŒè¡Œ
      secondRowHeaders.forEach((header, index) => {
        const cell = worksheet.getCell(3, index + 1);
        cell.value = header;
        cell.style = headerStyle;
      });
      // 3. åˆå¹¶å•元格
      // åˆå¹¶ A2:A3, B2:B3, C2:C3, D2:D3, E2:E3, F2:F3
      for (let i = 1; i <= 6; i++) {
        worksheet.mergeCells(2, i, 3, i);
        const cell = worksheet.getCell(2, i);
        cell.style = headerStyle;
      }
      // è®¾ç½®ç¬¬ä¸€è¡Œåˆå¹¶å•元格的值
      worksheet.getCell(2, 1).value = "";
      worksheet.getCell(2, 2).value = "出院病区";
      worksheet.getCell(2, 3).value = "科室";
      worksheet.getCell(2, 4).value = "出院人次";
      worksheet.getCell(2, 5).value = "无需随访人次";
      worksheet.getCell(2, 6).value = "应随访人次";
      // 4. åˆå¹¶"再次出院随访"标题
      worksheet.mergeCells(2, 7, 2, 14); // G2:N2
      worksheet.getCell(2, 7).value = "再次出院随访";
      worksheet.getCell(2, 7).style = headerStyle;
      // 5. è®¾ç½®è¡Œé«˜
      worksheet.getRow(2).height = 28;
      worksheet.getRow(3).height = 25;
      // 6. æ·»åŠ æ•°æ®è¡Œ
      data.forEach((item, rowIndex) => {
        const dataRow = worksheet.addRow(
          [
            "", // å±•开列
            item.leavehospitaldistrictname || "",
            item.deptname || "",
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
            // å†æ¬¡å‡ºé™¢éšè®¿æ•°æ®
            item.needFollowUpAgain || 0,
            item.pendingFollowUpAgain || 0,
            item.followUpSuccessAgain || 0,
            item.followUpFailAgain || 0,
            item.followUpRateAgain || "0%",
            item.manualAgain || 0,
            item.smsAgain || 0,
            item.weChatAgain || 0,
          ],
          rowIndex + 4
        );
        // åº”用数据行样式
        dataRow.eachCell((cell) => {
          cell.style = cellStyle;
        });
        dataRow.height = 24;
      });
      // 7. æ·»åŠ åˆè®¡è¡Œ
      const summaries = this.getSecondFollowUpSummaries(data);
      const summaryRow = worksheet.addRow(summaries);
      summaryRow.eachCell((cell, colNumber) => {
        cell.style = summaryStyle;
        if (colNumber === 1) {
          cell.value = "合计";
        }
      });
      summaryRow.height = 28;
      // 8. è®¾ç½®åˆ—宽
      worksheet.columns = [
        { width: 8 }, // å±•开列
        { width: 20 }, // å‡ºé™¢ç—…区
        { width: 15 }, // ç§‘室
        { width: 12 }, // å‡ºé™¢äººæ¬¡
        { width: 12 }, // æ— éœ€éšè®¿äººæ¬¡
        { width: 12 }, // åº”随访人次
        // å†æ¬¡å‡ºé™¢éšè®¿åˆ—
        { width: 10 }, // éœ€éšè®¿
        { width: 10 }, // å¾…随访
        { width: 10 }, // éšè®¿æˆåŠŸ
        { width: 10 }, // éšè®¿å¤±è´¥
        { width: 12 }, // éšè®¿çއ
        { width: 8 }, // äººå·¥
        { width: 8 }, // çŸ­ä¿¡
        { width: 8 }, // å¾®ä¿¡
      ];
    },
    /** å†æ¬¡éšè®¿æ•°æ®åˆè®¡è¡Œè®¡ç®— */
    getSecondFollowUpSummaries(data) {
      const summaries = ["合计", "/", "/", 0, 0, 0, 0, 0, 0, 0, "0%", 0, 0, 0];
      data.forEach((item) => {
        // æ•°å€¼å­—段求和
        summaries[3] += Number(item.dischargeCount) || 0;
        summaries[4] += Number(item.nonFollowUp) || 0;
        summaries[5] += Number(item.followUpNeeded) || 0;
        summaries[6] += Number(item.needFollowUpAgain) || 0;
        summaries[7] += Number(item.pendingFollowUpAgain) || 0;
        summaries[8] += Number(item.followUpSuccessAgain) || 0;
        summaries[9] += Number(item.followUpFailAgain) || 0;
        summaries[11] += Number(item.manualAgain) || 0;
        summaries[12] += Number(item.smsAgain) || 0;
        summaries[13] += Number(item.weChatAgain) || 0;
      });
      // è®¡ç®—随访率百分比字段的平均值
      const followUpRateAgainValues = data
        .map((item) => this.extractPercentageValue(item.followUpRateAgain))
        .filter((value) => value !== null);
      if (followUpRateAgainValues.length > 0) {
        const avgFollowUpRateAgain =
          followUpRateAgainValues.reduce((sum, val) => sum + val, 0) /
          followUpRateAgainValues.length;
        summaries[10] = (avgFollowUpRateAgain * 100).toFixed(2) + "%";
      }
      // æ ¼å¼åŒ–æ•°å­—
      summaries[3] = this.formatNumber(summaries[3]);
      summaries[4] = this.formatNumber(summaries[4]);
      summaries[5] = this.formatNumber(summaries[5]);
      summaries[6] = this.formatNumber(summaries[6]);
      summaries[7] = this.formatNumber(summaries[7]);
      summaries[8] = this.formatNumber(summaries[8]);
      summaries[9] = this.formatNumber(summaries[9]);
      summaries[11] = this.formatNumber(summaries[11]);
      summaries[12] = this.formatNumber(summaries[12]);
      summaries[13] = this.formatNumber(summaries[13]);
      return summaries;
    },
    /** å†æ¬¡éšè®¿è¡¨æ ¼çš„合计行计算方法 */
    getSummariesSecond(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计";
          return;
        }
        if (index === 1 || index === 2) {
          sums[index] = "/";
          return;
        }
        if (column.property === "followUpRateAgain") {
          const percentageValues = data
            .map((item) => {
              const value = item[column.property];
              if (!value || value === "-" || value === "0%") return null;
              if (typeof value === "string" && value.includes("%")) {
                const numValue = parseFloat(value.replace("%", "")) / 100;
                return isNaN(numValue) ? null : numValue;
              } else {
                const numValue = parseFloat(value);
                return isNaN(numValue) ? null : numValue;
              }
            })
            .filter((value) => value !== null && value !== 0);
          if (percentageValues.length > 0) {
            const average =
              percentageValues.reduce((sum, value) => sum + value, 0) /
              percentageValues.length;
            sums[index] = (average * 100).toFixed(2) + "%";
          } else {
            sums[index] = "0.00%";
          }
        } else {
          const values = data.map((item) => {
            const value = item[column.property];
            if (value === "-" || value === "" || value === null) return 0;
            return Number(value) || 0;
          });
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0);
            sums[index] = this.formatNumber(sums[index]);
          } else {
            sums[index] = "-";
          }
        }
      });
      return sums;
    },
    /** å†æ¬¡éšè®¿å†…部表格合计行计算方法 */
    getInnerSummariesSecond(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "小计";
          return;
        }
        if (column.property === "drname" || column.property === "deptname") {
          sums[index] = "-";
          return;
        }
        if (column.property === "followUpRateAgain") {
          const percentageValues = data
            .map((item) => {
              const value = item[column.property];
              if (!value || value === "-" || value === "0%") return null;
              if (typeof value === "string" && value.includes("%")) {
                const numValue = parseFloat(value.replace("%", "")) / 100;
                return isNaN(numValue) ? null : numValue;
              } else {
                const numValue = parseFloat(value);
                return isNaN(numValue) ? null : numValue;
              }
            })
            .filter((value) => value !== null && value !== 0);
          if (percentageValues.length > 0) {
            const average =
              percentageValues.reduce((sum, value) => sum + value, 0) /
              percentageValues.length;
            sums[index] = (average * 100).toFixed(2) + "%";
          } else {
            sums[index] = "0.00%";
          }
        } else {
          const values = data.map((item) => {
            const value = item[column.property];
            if (value === "-" || value === "" || value === null) return 0;
            return Number(value) || 0;
          });
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0);
            sums[index] = this.formatNumber(sums[index]);
          } else {
            sums[index] = "-";
          }
        }
      });
      return sums;
    },
    /** å†æ¬¡éšè®¿è¡¨æ ¼çš„行点击展开 */
    handleRowClickSecond(row) {
      if (this.expandsSecond.includes(this.getRowKey(row))) {
        this.expandsSecond = [];
        return;
      }
      const params = {
        ...this.queryParams,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.allDeptCodes
          : this.queryParams.deptcodes,
        leavehospitaldistrictcodes: [row.leavehospitaldistrictcode],
        drcode: "1",
        visitCount: 2, // è®¾ç½®ä¸ºå†æ¬¡éšè®¿
      };
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      if (!row.doctorStats) {
        this.loadingSecond = true;
        getSfStatistics(params).then((res) => {
          this.$set(row, "doctorStats", res.data);
          this.expandsSecond = [this.getRowKey(row)];
          this.loadingSecond = false;
        });
      } else {
        this.expandsSecond = [this.getRowKey(row)];
      }
    },
    /** å†æ¬¡éšè®¿è¡¨æ ¼çš„多选框选中数据 */
    handleSelectionChangeSecond(selection) {
      this.idsSecond = selection.map((item) => item.tagid);
      this.single = selection.length != 1;
      this.multiple = !selection.length;
    },
    // æ˜¾ç¤ºå›¾è¡¨å¼¹çª—
    showChartDialog() {
      this.chartDialogVisible = true;
      this.$nextTick(() => {
        this.initPieChart();
        this.initBarLineChart();
      });
    },
    // åœ¨methods中修改统计方法
    showChartDialog() {
      this.chartDialogVisible = true;
      this.$nextTick(() => {
        console.log(this.userList, "this.userList");
        this.initCharts();
      });
    },
    // æ–°å¢žåˆå§‹åŒ–图表方法
    initCharts() {
      this.initPieChart();
      this.initBarLineChart();
    },
    // åˆå§‹åŒ–饼图
    initPieChart() {
      const echarts = require("echarts");
      const pieDom = document.getElementById("pieChart");
      if (!pieDom) return;
      if (this.pieChart) {
        this.pieChart.dispose();
      }
      this.pieChart = echarts.init(pieDom);
      // è®¡ç®—饼图数据
      const followUpData = {
        pending: 0,
        success: 0,
        fail: 0,
      };
      this.userList.forEach((item) => {
        followUpData.pending += item.pendingFollowUp || 0;
        followUpData.success += item.followUpSuccess || 0;
        followUpData.fail += item.followUpFail || 0;
      });
      // ä½¿ç”¨æ›´ç¾Žè§‚的颜色方案
      const pieOption = {
        title: {
          text: "随访状态分布",
          left: "center",
          textStyle: {
            color: "#333",
            fontSize: 16,
          },
        },
        tooltip: {
          trigger: "item",
          formatter: "{a} <br/>{b}: {c} ({d}%)",
        },
        legend: {
          orient: "vertical",
          left: "left",
          data: ["待随访", "随访成功", "随访失败"],
          textStyle: {
            color: "#666",
          },
        },
        color: ["#FF9D4D", "#36B37E", "#FF5C5C"], // æ–°çš„配色方案
        series: [
          {
            name: "随访状态",
            type: "pie",
            radius: ["40%", "70%"],
            avoidLabelOverlap: true,
            itemStyle: {
              borderRadius: 10,
              borderColor: "#fff",
              borderWidth: 2,
            },
            label: {
              show: true,
              formatter: "{b}: {c} ({d}%)",
              color: "#333",
            },
            emphasis: {
              label: {
                show: true,
                fontSize: "18",
                fontWeight: "bold",
              },
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: "rgba(0, 0, 0, 0.5)",
              },
            },
            data: [
              {
                value: followUpData.pending,
                name: "待随访",
              },
              {
                value: followUpData.success,
                name: "随访成功",
              },
              {
                value: followUpData.fail,
                name: "随访失败",
              },
            ],
          },
        ],
      };
      this.pieChart.setOption(pieOption);
      window.addEventListener("resize", this.resizePieChart);
    },
    // åˆå§‹åŒ–柱状折线图
    initBarLineChart() {
      const echarts = require("echarts");
      const barDom = document.getElementById("barLineChart");
      if (!barDom) return;
      if (this.barLineChart) {
        this.barLineChart.dispose();
      }
      this.barLineChart = echarts.init(barDom);
      // å‡†å¤‡æ•°æ®
      const categories = this.userList.map(
        (item) => item.leavehospitaldistrictname || item.deptname
      );
      const dischargeData = this.userList.map(
        (item) => item.dischargeCount || 0
      );
      const followUpData = this.userList.map(
        (item) => item.followUpNeeded || 0
      );
      // æ–°å¢žä¸¤æ¡æŠ˜çº¿æ•°æ®
      const followUpRateData = this.userList.map((item) => {
        if (!item.followUpRate) return 0;
        // åŽ»æŽ‰ç™¾åˆ†å·å¹¶è½¬ä¸ºæ•°å­—
        const rateStr = String(item.followUpRate).replace("%", "");
        return parseFloat(rateStr) || 0;
      });
      const timelyRateData = this.userList.map((item) =>
        item.rate ? (Number(item.rate) * 100).toFixed(2) : 0
      );
      const option = {
        title: {
          text: "科室/病区随访趋势",
          left: "center",
          textStyle: {
            color: "#333",
            fontSize: 16,
          },
        },
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "cross",
            crossStyle: {
              color: "#999",
            },
          },
        },
        legend: {
          data: ["出院人次", "应随访人次", "随访率(%)", "及时率(%)"],
          top: "bottom",
          textStyle: {
            color: "#666",
          },
        },
        color: ["#5470C6", "#91CC75", "#EE6666", "#9A60B4"], // æ–°å¢žç´«è‰²ç”¨äºŽåŠæ—¶çއ
        xAxis: {
          type: "category",
          data: categories,
          axisLabel: {
            interval: 0,
            rotate: 30,
            color: "#666",
          },
          axisLine: {
            lineStyle: {
              color: "#ddd",
            },
          },
        },
        yAxis: [
          {
            type: "value",
            name: "人次",
            min: 0,
            axisLabel: {
              color: "#666",
            },
            axisLine: {
              lineStyle: {
                color: "#ddd",
              },
            },
            splitLine: {
              lineStyle: {
                color: "#f0f0f0",
              },
            },
          },
          {
            type: "value",
            name: "百分比(%)",
            min: 0,
            max: 100,
            axisLabel: {
              color: "#666",
              formatter: "{value}%",
            },
            axisLine: {
              lineStyle: {
                color: "#ddd",
              },
            },
            splitLine: {
              show: false,
            },
          },
        ],
        series: [
          {
            name: "出院人次",
            type: "bar",
            barWidth: "25%",
            data: dischargeData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0],
            },
          },
          {
            name: "应随访人次",
            type: "bar",
            barWidth: "25%",
            data: followUpData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0],
            },
          },
          {
            name: "随访率(%)",
            type: "line",
            yAxisIndex: 1,
            data: followUpRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3,
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 80,
                  lineStyle: {
                    color: "#EE6666",
                    type: "dashed",
                  },
                  // label: {
                  //   position: 'end',
                  //   formatter: '目标80%'
                  // }
                },
              ],
            },
          },
          {
            name: "及时率(%)",
            type: "line",
            yAxisIndex: 1,
            data: timelyRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3,
              type: "dotted", // ä½¿ç”¨è™šçº¿åŒºåˆ†
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 90,
                  lineStyle: {
                    color: "#9A60B4",
                    type: "dashed",
                  },
                  // label: {
                  //   position: 'end',
                  //   formatter: '目标90%'
                  // }
                },
              ],
            },
          },
        ],
        grid: {
          top: "15%",
          left: "3%",
          right: "4%",
          bottom: "15%",
          containLabel: true,
        },
      };
      this.barLineChart.setOption(option);
      window.addEventListener("resize", this.resizeBarLineChart);
    },
    // å›¾è¡¨å“åº”式调整方法
    resizePieChart() {
      if (this.pieChart) {
        this.pieChart.resize();
      }
    },
    resizeBarLineChart() {
      if (this.barLineChart) {
        this.barLineChart.resize();
      }
    },
    // åœ¨ç»„件销毁时清理
    beforeDestroy() {
      // ç§»é™¤äº‹ä»¶ç›‘听
      window.removeEventListener("resize", this.resizePieChart);
      window.removeEventListener("resize", this.resizeBarLineChart);
      // é”€æ¯å›¾è¡¨å®žä¾‹
      if (this.pieChart) {
        this.pieChart.dispose();
        this.pieChart = null;
      }
      if (this.barLineChart) {
        this.barLineChart.dispose();
        this.barLineChart = null;
      }
    },
  },
};
</script>
<style lang="scss" scoped>
::v-deep .el-tabs__header {
  margin-bottom: 20px;
}
::v-deep .el-tabs__item {
  font-size: 16px;
  padding: 0 20px;
  height: 40px;
  line-height: 40px;
}
::v-deep .el-tabs__active-bar {
  height: 3px;
}
/* Tab内容区域样式 */
.el-tab-pane {
  .your-table-container {
    margin-top: 10px;
  }
}
.sidecolumn {
  width: 180px;
  min-height: 100vh;
  text-align: center;
  //   display: flex;
  margin-top: 20px;
  margin: 20px;
  padding: 30px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .sidecolumn-top {
    display: flex;
    justify-content: space-between;
    .top-wj {
      font-size: 20px;
    }
    .top-tj {
      font-size: 18px;
      color: rgb(0, 89, 255);
      cursor: pointer;
    }
  }
  .center-ss {
    margin-top: 30px;
    .input-with-select {
      height: 40px !important;
    }
  }
  .bottom-fl {
    margin-top: 30px;
    display: center !important;
  }
}
.qrcode-dialo {
  text-align: center;
  //   display: flex;
  margin: 20px;
  padding: 30px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .qrcode-text {
    font-size: 20px;
    span {
      margin-left: 20px;
    }
  }
  .qrcode-img {
    width: 300px;
    height: 400px;
  }
}
::v-deep.el-tabs--left,
.el-tabs--right {
  overflow: hidden;
  align-items: center;
  display: flex;
}
::v-deep.el-input--medium .el-input__inner {
  height: 40px !important;
}
::v-deep.el-tabs--right .el-tabs__active-bar.is-right {
  height: 40px;
  width: 5px;
  left: 0;
}
::v-deep.el-tabs--right .el-tabs__item.is-right {
  display: block;
  text-align: left;
  font-size: 20px;
}
// ç¾ŽåŒ–合计行样式
::v-deep .el-table__footer {
  .el-table__cell {
    background-color: #f5f7fa;
    font-weight: 600;
    color: #409eff;
    .cell {
      font-weight: 600;
      color: #409eff;
    }
  }
}
// å†…部表格合计行样式
::v-deep .inner-table .el-table__footer {
  .el-table__cell {
    background-color: #ecf5ff;
    font-weight: 500;
    color: #67c23a;
    .cell {
      font-weight: 500;
      color: #67c23a;
    }
  }
}
// ç™¾åˆ†æ¯”字段特殊样式
.your-table-container ::v-deep .el-table__footer .el-table__cell[data-field="followUpRate"] .cell,
.your-table-container ::v-deep .el-table__footer .el-table__cell[data-field="rate"] .cell,
.your-table-container ::v-deep .el-table__footer .el-table__cell[data-field="followUpRateAgain"] .cell {
  color: #e6a23c !important;
  font-weight: 700 !important;
}
.leftvlue {
  //   display: flex;
  //   flex: 1;
  // width: 80%;
  // margin-top: 20px;
  margin: 20px;
  padding: 30px;
  background: #ffff;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .mulsz {
    font-size: 20px;
  }
}
/* ä½¿è¡Œæœ‰æ‰‹åž‹æŒ‡é’ˆ */
.el-table__row {
  cursor: pointer;
}
/* å†…层医生表格样式 */
.inner-table {
  // è¡¨å¤´èƒŒæ™¯è‰²
  ::v-deep .el-table__header-wrapper {
    background-color: #f0f7ff !important;
    th {
      background-color: #f0f7ff !important;
    }
  }
  // è¡¨æ ¼è¡ŒèƒŒæ™¯è‰²
  ::v-deep .el-table__body-wrapper {
    tr {
      background-color: #f9fbfe !important;
      &:hover {
        background-color: #e6f1ff !important;
      }
    }
  }
  // è¾¹æ¡†é¢œè‰²
  ::v-deep .el-table--border {
    border-color: #d9e8ff !important;
    td,
    th {
      border-color: #d9e8ff !important;
    }
  }
  // æ–‘马纹效果
  ::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
    background-color: #f5f9ff !important;
  }
}
/* å±•开行样式 */
.el-table__expanded-cell {
  padding: 10px 0 !important;
  background: #f8f8f8;
}
.document {
  width: 100px;
  height: 50px;
}
.data-list {
  max-height: 800px;
  overflow-y: auto;
}
.documentf {
  display: flex;
  justify-content: flex-end;
}
.button-text {
  color: rgb(70, 204, 238);
}
.button-textck {
  color: rgb(39, 167, 67);
}
.button-textxg {
  color: rgb(35, 81, 233);
}
.button-textsc {
  color: rgb(235, 23, 23);
}
</style>
src/views/sfstatistics/percentage/index.vue
@@ -1,3103 +1,369 @@
<template>
  <div class="Questionnairemanagement">
    <div class="leftvlue">
      <div class="leftvlue-bg">
        <el-row :gutter="20">
          <!--标签数据-->
          <el-col :span="24" :xs="24">
            <el-form
              :model="queryParams"
              ref="queryForm"
              size="small"
              :inline="true"
              v-show="showSearch"
              label-width="98px"
            >
              <el-form-item label="统计类型" prop="userName">
                <el-select
                  v-model="queryParams.statisticaltype"
                  placeholder="请选择统计类型"
                >
                  <el-option
                    v-for="item in Statisticallist"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
                <el-select
                  style="margin-left: 10px"
                  v-if="queryParams.statisticaltype == 1"
                  v-model="queryParams.leavehospitaldistrictcodes"
                  size="medium"
                  multiple
                  filterable
                  placeholder="请选择病区"
                >
                  <el-option
                    v-for="item in flatArrayhospit"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
                <el-select
                  v-else-if="queryParams.statisticaltype == 2"
                  v-model="queryParams.deptcodes"
                  size="medium"
                  multiple
                  filterable
                  placeholder="请选择科室"
                >
                  <el-option
                    v-for="item in flatArraydept"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
              </el-form-item>
  <div class="follow-up-statistics">
    <!-- æœç´¢è¡¨å•区域 -->
    <div class="search-section">
      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="98px">
        <el-form-item label="统计类型" prop="userName">
          <el-select v-model="queryParams.statisticaltype" placeholder="请选择统计类型">
            <el-option v-for="item in Statisticallist" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
          <el-select
            style="margin-left: 10px"
            v-if="queryParams.statisticaltype == 1"
            v-model="queryParams.leavehospitaldistrictcodes"
            size="medium"
            multiple
            filterable
            placeholder="请选择病区"
          >
            <el-option v-for="item in flatArrayhospit" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
          <el-select
            v-else-if="queryParams.statisticaltype == 2"
            v-model="queryParams.deptcodes"
            size="medium"
            multiple
            filterable
            placeholder="请选择科室"
          >
            <el-option v-for="item in flatArraydept" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
        </el-form-item>
              <el-form-item label="统计类型" prop="userName">
                <el-select
                  v-model="queryParams.serviceType"
                  multiple
                  placeholder="请选择"
                >
                  <el-option
                    v-for="item in options"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  >
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item
                label-width="200"
                label="应随访时间范围"
                prop="userName"
              >
                <el-date-picker
                  v-model="queryParams.dateRange"
                  value-format="yyyy-MM-dd HH:mm:ss"
                  type="daterange"
                  range-separator="至"
                  start-placeholder="开始日期"
                  end-placeholder="结束日期"
                  :default-time="['00:00:00', '23:59:59']"
                >
                  >
                </el-date-picker>
              </el-form-item>
        <el-form-item label="服务类型" prop="userName">
          <el-select v-model="queryParams.serviceType" multiple placeholder="请选择">
            <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
        </el-form-item>
              <el-form-item>
                <el-button
                  type="primary"
                  icon="el-icon-search"
                  size="medium"
                  @click="handleQuery"
                  >搜索</el-button
                >
                <el-button
                  icon="el-icon-refresh"
                  size="medium"
                  @click="resetQuery"
                  >重置</el-button
                >
              </el-form-item>
              <el-col :span="19">
                <el-button
                  type="warning"
                  plain
                  icon="el-icon-download"
                  size="medium"
                  @click="exportTable"
                  >导出</el-button
                >
                <el-button
                  type="primary"
                  plain
                  icon="el-icon-data-line"
                  size="medium"
                  @click="showChartDialog"
                  >统计趋势图</el-button
                >
              </el-col>
            </el-form>
            <div class="your-table-container">
              <el-table
                ref="exportTable"
                id="exportTableid"
                v-loading="loading"
                :data="userList"
                :border="true"
                @selection-change="handleSelectionChange"
                @expand-change="handleRowClick"
                :row-key="getRowKey"
                show-summary
                :summary-method="getSummaries"
                :expand-row-keys="expands"
              >
                <!-- å±•开行箭头列 -->
                <el-table-column type="expand">
                  <template slot-scope="props">
                    <el-table
                      :data="props.row.doctorStats"
                      border
                      style="width: 95%; margin: 0 auto"
                      class="inner-table"
                      show-summary
                      :summary-method="getInnerSummaries"
                    >
                      <el-table-column
                        label="医生姓名"
                        prop="drname"
                        align="center"
                      />
                      <el-table-column
                        label="科室"
                        width="120"
                        prop="deptname"
                        align="center"
                      />
                      <el-table-column
                        label="出院人次"
                        prop="dischargeCount"
                        align="center"
                      />
                      <el-table-column
                        label="出院人次"
                        align="center"
                        key="dischargeCount"
                        prop="dischargeCount"
                      >
                      </el-table-column>
                      <el-table-column
                        label="无需随访人次"
                        align="center"
                        width="100"
                        key="nonFollowUp"
                        prop="nonFollowUp"
                      >
                      </el-table-column>
                      <el-table-column
                        label="应随访人次"
                        align="center"
                        width="100"
                        key="followUpNeeded"
                        prop="followUpNeeded"
                      >
                      </el-table-column>
                      <el-table-column align="center" label="首次出院随访">
                        <el-table-column
                          label="需随访"
                          align="center"
                          key="needFollowUp"
                          prop="needFollowUp"
                        >
                        </el-table-column>
                        <el-table-column
                          label="待随访"
                          align="center"
                          key="pendingFollowUp"
                          prop="pendingFollowUp"
                        >
                        </el-table-column>
                        <el-table-column
                          label="随访成功"
                          align="center"
                          key="followUpSuccess"
                          prop="followUpSuccess"
                        >
                        </el-table-column>
                        <el-table-column
                          label="随访失败"
                          align="center"
                          key="followUpFail"
                          prop="followUpFail"
                        >
                        </el-table-column>
                        <el-table-column
                          label="随访率"
                          align="center"
                          width="120"
                          key="followUpRate"
                          prop="followUpRate"
                        >
                          <!-- <template slot-scope="scope">
                      <span
                        >{{
                          (Number(scope.row.followUpRate) * 100).toFixed(2)
                        }}%</span
                      >
                    </template> -->
                        </el-table-column>
                        <el-table-column
                          label="及时率"
                          align="center"
                          width="120"
                          key="rate"
                          prop="rate"
                        >
                          <template slot-scope="scope">
                            <el-button
                              size="medium"
                              type="text"
                              @click="Seedetails(scope.row)"
                              ><span class="button-zx"
                                >{{
                                  (Number(scope.row.rate) * 100).toFixed(2)
                                }}%</span
                              ></el-button
                            >
                          </template>
                        </el-table-column>
                        <el-table-column
                          label="人工"
                          align="center"
                          key="manual"
                          prop="manual"
                        >
                        </el-table-column>
                        <el-table-column
                          label="短信"
                          align="center"
                          key="sms"
                          prop="sms"
                        >
                        </el-table-column>
                        <el-table-column
                          label="微信"
                          align="center"
                          key="weChat"
                          prop="weChat"
                        >
                        </el-table-column>
                      </el-table-column>
                      <el-table-column align="center" label="再次出院随访">
                        <el-table-column
                          label="需随访"
                          align="center"
                          key="needFollowUpAgain"
                          prop="needFollowUpAgain"
                        >
                        </el-table-column>
                        <el-table-column
                          label="待随访"
                          align="center"
                          key="pendingFollowUpAgain"
                          prop="pendingFollowUpAgain"
                        >
                        </el-table-column>
                        <el-table-column
                          label="随访成功"
                          align="center"
                          key="followUpSuccessAgain"
                          prop="followUpSuccessAgain"
                        >
                        </el-table-column>
                        <el-table-column
                          label="随访失败"
                          align="center"
                          key="followUpFailAgain"
                          prop="followUpFailAgain"
                        >
                        </el-table-column>
                        <el-table-column
                          label="随访率"
                          align="center"
                          width="120"
                          key="followUpRateAgain"
                          prop="followUpRateAgain"
                        >
                          <!-- <template slot-scope="scope">
                      <span
                        >{{
                          (Number(scope.row.FollowUpRateAgain) * 100).toFixed(2)
                        }}%</span
                      >
                    </template> -->
                        </el-table-column>
                        <el-table-column
                          label="人工"
                          align="center"
                          key="manualAgain"
                          prop="manualAgain"
                        >
                        </el-table-column>
                        <el-table-column
                          label="短信"
                          align="center"
                          key="smsAgain"
                          prop="smsAgain"
                        >
                        </el-table-column>
                        <el-table-column
                          label="微信"
                          align="center"
                          key="weChatAgain"
                          prop="weChatAgain"
                        >
                        </el-table-column>
                      </el-table-column>
                    </el-table>
                  </template>
                </el-table-column>
                <el-table-column
                  label="出院病区"
                  align="center"
                  sortable
                  key="leavehospitaldistrictname"
                  prop="leavehospitaldistrictname"
                  width="150"
                  :show-overflow-tooltip="true"
                  :sort-method="sortChineseNumber"
                />
                <el-table-column
                  label="科室"
                  align="center"
                  key="deptname"
                  prop="deptname"
                  :show-overflow-tooltip="true"
                />
                <el-table-column
                  label="出院人次"
                  align="center"
                  key="dischargeCount"
                  prop="dischargeCount"
                >
                </el-table-column>
                <el-table-column
                  label="无需随访人次"
                  align="center"
                  width="100"
                  key="nonFollowUp"
                  prop="nonFollowUp"
                >
                </el-table-column>
                <el-table-column
                  label="应随访人次"
                  align="center"
                  width="100"
                  key="followUpNeeded"
                  prop="followUpNeeded"
                >
                </el-table-column>
                <el-table-column align="center" label="首次出院随访">
                  <el-table-column
                    label="需随访"
                    align="center"
                    key="needFollowUp"
                    prop="needFollowUp"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.needFollowUpInfo,
                            scope.row.leavehospitaldistrictname + '需随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.needFollowUp
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="待随访"
                    align="center"
                    key="pendingFollowUp"
                    prop="pendingFollowUp"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.pendingFollowUpInfo,
                            scope.row.leavehospitaldistrictname + '待随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.pendingFollowUp
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="随访成功"
                    align="center"
                    key="followUpSuccess"
                    prop="followUpSuccess"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.followUpSuccessInfo,
                            scope.row.leavehospitaldistrictname + '随访成功列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.followUpSuccess
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="随访失败"
                    align="center"
                    key="followUpFail"
                    prop="followUpFail"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.followUpFailInfo,
                            scope.row.leavehospitaldistrictname + '随访失败列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.followUpFail
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="随访率"
                    align="center"
                    width="120"
                    key="followUpRate"
                    prop="followUpRate"
                  >
                    <!-- <template slot-scope="scope">
                      <span
                        >{{
                          (Number(scope.row.followUpRate) * 100).toFixed(2)
                        }}%</span
                      >
                    </template> -->
                  </el-table-column>
                  <el-table-column
                    label="及时率"
                    align="center"
                    width="120"
                    key="rate"
                    prop="rate"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="Seedetails(scope.row)"
                        ><span class="button-zx"
                          >{{
                            (Number(scope.row.rate) * 100).toFixed(2)
                          }}%</span
                        ></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="人工"
                    align="center"
                    key="manual"
                    prop="manual"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.manualInfo,
                            scope.row.leavehospitaldistrictname + '人工随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.manual
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="短信"
                    align="center"
                    key="sms"
                    prop="sms"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.smsInfo,
                            scope.row.leavehospitaldistrictname + '短信随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.sms
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="微信"
                    align="center"
                    key="weChat"
                    prop="weChat"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.weChatInfo,
                            scope.row.leavehospitaldistrictname + '微信随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.weChat
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                </el-table-column>
                <el-table-column align="center" label="再次出院随访">
                  <el-table-column
                    label="需随访"
                    align="center"
                    key="needFollowUpAgain"
                    prop="needFollowUpAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.needFollowUpAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访需随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.needFollowUpAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="待随访"
                    align="center"
                    key="pendingFollowUpAgain"
                    prop="pendingFollowUpAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.pendingFollowUpAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访待随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.pendingFollowUpAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="随访成功"
                    align="center"
                    key="followUpSuccessAgain"
                    prop="followUpSuccessAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.followUpSuccessAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访随访成功列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.followUpSuccessAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="随访失败"
                    align="center"
                    key="followUpFailAgain"
                    prop="followUpFailAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.followUpFailAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访随访失败列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.followUpFailAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="随访率"
                    align="center"
                    width="120"
                    key="followUpRateAgain"
                    prop="followUpRateAgain"
                  >
                    <!-- <template slot-scope="scope">
                      <span
                        >{{
                          (Number(scope.row.FollowUpRateAgain) * 100).toFixed(2)
                        }}%</span
                      >
                    </template> -->
                  </el-table-column>
                  <el-table-column
                    label="人工"
                    align="center"
                    key="manualAgain"
                    prop="manualAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.manualAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访人工随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.manualAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="短信"
                    align="center"
                    key="smsAgain"
                    prop="smsAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.smsAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访短信随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.smsAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                  <el-table-column
                    label="微信"
                    align="center"
                    key="weChatAgain"
                    prop="weChatAgain"
                  >
                    <template slot-scope="scope">
                      <el-button
                        size="medium"
                        type="text"
                        @click="
                          viewDetails(
                            scope.row.weChatAgainInfo,
                            scope.row.leavehospitaldistrictname +
                              '再次随访微信随访列表'
                          )
                        "
                        ><span class="button-zx">{{
                          scope.row.weChatAgain
                        }}</span></el-button
                      >
                    </template>
                  </el-table-column>
                </el-table-column>
                <el-table-column
                  v-if="orgname == '丽水市中医院'"
                  align="center"
                  label="随访情况"
                >
                  <el-table-column
                    label="正常语音"
                    align="center"
                    width="100"
                    key="taskSituation1"
                    prop="taskSituation1"
                  >
                  </el-table-column
                  ><el-table-column
                    label="患者拒接或拒访"
                    align="center"
                    width="100"
                    key="taskSituation2"
                    prop="taskSituation2"
                  >
                  </el-table-column
                  ><el-table-column
                    label="面访或者接诊"
                    align="center"
                    width="100"
                    key="taskSituation3"
                    prop="taskSituation3"
                  >
                  </el-table-column
                  ><el-table-column
                    label="微信随访"
                    align="center"
                    width="100"
                    key="taskSituation4"
                    prop="taskSituation4"
                  >
                  </el-table-column
                  ><el-table-column
                    label="随访电话不正确"
                    align="center"
                    width="100"
                    key="taskSituation5"
                    prop="taskSituation5"
                  >
                  </el-table-column
                  ><el-table-column
                    label="其他情况不宜随访"
                    align="center"
                    width="100"
                    key="taskSituation6"
                    prop="taskSituation6"
                  >
                  </el-table-column>
                </el-table-column>
              </el-table>
            </div>
            <!-- <pagination
              v-show="total > 0"
              :total="total"
              :page.sync="queryParams.pageNum"
              :limit.sync="queryParams.pageSize"
              @pagination="getList"
            /> -->
          </el-col>
        </el-row>
      </div>
    </div>
    <!-- ç»Ÿè®¡è¶‹åŠ¿å›¾å¼¹çª— -->
    <el-dialog
      title="随访统计趋势图"
      :visible.sync="chartDialogVisible"
      width="80%"
      :close-on-click-modal="false"
    >
      <div class="chart-container">
        <el-row :gutter="20">
          <el-col :span="12">
            <div class="chart-title">随访状态分布</div>
            <div id="pieChart" style="width: 100%; height: 400px"></div>
          </el-col>
          <el-col :span="12">
            <div class="chart-title">随访趋势分析</div>
            <div id="barLineChart" style="width: 100%; height: 400px"></div>
          </el-col>
        </el-row>
      </div>
    </el-dialog>
    <el-dialog
      title="未及时随访患者服务"
      :visible.sync="SeedetailsVisible"
      v-loading="Seedloading"
      width="70%"
      :close-on-click-modal="false"
    >
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <!--用户数据-->
            <el-form
              :model="patientqueryParams"
              ref="queryForm"
              size="small"
              :inline="true"
              label-width="98px"
            >
              <el-form-item label="患者:">
                <el-input
                  v-model="patientqueryParams.name"
                  @keyup.enter.native="handleQuery"
                ></el-input>
              </el-form-item>
              <el-form-item label="患者诊断:">
                <el-input
                  v-model="patientqueryParams.leavediagname"
                  @keyup.enter.native="handleQuery"
                ></el-input>
              </el-form-item>
              <el-form-item>
                <el-button
                  type="primary"
                  icon="el-icon-search"
                  size="medium"
                  @click="handleQuery"
                  >搜索</el-button
                >
                <el-button
                  icon="el-icon-refresh"
                  size="medium"
                  @click="resetQuery"
                  >取消创建</el-button
                >
              </el-form-item>
            </el-form>
            <!-- é€‰æ‹©æ‚£è€…列表 -->
            <el-table :data="logsheetlist" style="width: 100%">
              <el-table-column
                prop="sendname"
                align="center"
                label="姓名"
                width="100"
              >
              </el-table-column>
              <el-table-column
                prop="taskName"
                align="center"
                width="200"
                show-overflow-tooltip
                label="任务名称"
              >
              </el-table-column>
              <el-table-column
                prop="sendstate"
                align="center"
                width="200"
                label="任务状态"
              >
                <template slot-scope="scope">
                  <div v-if="scope.row.sendstate == 1">
                    <el-tag type="primary" :disable-transitions="false"
                      >表单已领取</el-tag
                    >
                  </div>
                  <div v-if="scope.row.sendstate == 2">
                    <el-tag type="primary" :disable-transitions="false"
                      >待随访</el-tag
                    >
                  </div>
                  <div v-if="scope.row.sendstate == 3">
                    <el-tag type="success" :disable-transitions="false"
                      >表单已发送</el-tag
                    >
                  </div>
                  <div v-if="scope.row.sendstate == 4">
                    <el-tag type="info" :disable-transitions="false"
                      >不执行</el-tag
                    >
                  </div>
                  <div v-if="scope.row.sendstate == 5">
                    <el-tag type="danger" :disable-transitions="false"
                      >发送失败</el-tag
                    >
                  </div>
                  <div v-if="scope.row.sendstate == 6">
                    <el-tag type="success" :disable-transitions="false"
                      >已完成</el-tag
                    >
                  </div>
                </template>
              </el-table-column>
              <el-table-column
                prop="visitTime"
                align="center"
                label="应随访时间"
                width="200"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="finishtime"
                align="center"
                label="随访完成时间"
                width="200"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                label="出院日期"
                width="200"
                align="center"
                key="endtime"
                prop="endtime"
              >
                <template slot-scope="scope">
                  <span>{{ formatTime(scope.row.endtime) }}</span>
                </template></el-table-column
              >
              <el-table-column
                label="责任护士"
                width="120"
                align="center"
                key="nurseName"
                prop="nurseName"
              />
              <el-table-column
                label="主治医生"
                width="120"
                align="center"
                key="drname"
                prop="drname"
              />
              <el-table-column
                label="结果状态"
                align="center"
                key="excep"
                prop="excep"
                width="120"
              >
                <template slot-scope="scope">
                  <dict-tag
                    :options="dict.type.sys_yujing"
                    :value="scope.row.excep"
                  />
                </template>
              </el-table-column>
              <el-table-column
                label="处理意见"
                align="center"
                key="suggest"
                prop="suggest"
                width="120"
              >
                <template slot-scope="scope">
                  <dict-tag
                    :options="dict.type.sys_suggest"
                    :value="scope.row.suggest"
                  />
                </template>
              </el-table-column>
              <el-table-column
                prop="templatename"
                align="center"
                label="服务模板"
                width="200"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="remark"
                align="center"
                label="服务记录"
                width="200"
                show-overflow-tooltip
              >
              </el-table-column>
              <el-table-column
                prop="bankcardno"
                align="center"
                label="呼叫状态"
                width="210"
              >
              </el-table-column>
              <el-table-column
                label="操作"
                fixed="right"
                align="center"
                width="200"
                class-name="small-padding fixed-width"
              >
                <template slot-scope="scope">
                  <el-button
                    size="medium"
                    type="text"
                    @click="SeedetailsgGo(scope.row)"
                    ><span class="button-zx"
                      ><i class="el-icon-s-order"></i>查看</span
                    ></el-button
                  >
                </template>
              </el-table-column>
            </el-table>
          </el-row>
          <pagination
            v-show="patienttotal > 0 && this.patientqueryParams.allhosp != 6"
            :total="patienttotal"
            :page.sync="patientqueryParams.pn"
            :limit.sync="patientqueryParams.ps"
            @pagination="Seedetailstion"
        <el-form-item label-width="200" label="应随访时间范围" prop="userName">
          <el-date-picker
            v-model="queryParams.dateRange"
            value-format="yyyy-MM-dd HH:mm:ss"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            :default-time="['00:00:00', '23:59:59']"
          />
        </div>
      </div>
    </el-dialog>
    <!-- å„类详情 -->
    <el-dialog
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" size="medium" @click="handleQuery">搜索</el-button>
          <el-button icon="el-icon-refresh" size="medium" @click="resetQuery">重置</el-button>
        </el-form-item>
        <el-button type="warning" plain icon="el-icon-download" size="medium" @click="handleExport">导出</el-button>
        <el-button type="primary" plain icon="el-icon-data-line" size="medium" @click="showChartDialog">统计趋势图</el-button>
      </el-form>
    </div>
    <!-- Tab切换区域 -->
    <div class="tab-section">
      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
        <el-tab-pane label="首次随访" name="first">
          <FirstFollowUp
            ref="firstFollowUp"
            :query-params="queryParams"
            :flat-array-hospit="flatArrayhospit"
            :flat-array-dept="flatArraydept"
            :options="options"
            :orgname="orgname"
            @view-details="viewDetails"
            @see-details="Seedetails"
          />
        </el-tab-pane>
        <el-tab-pane label="再次随访" name="second">
          <SecondFollowUp
            ref="secondFollowUp"
            :query-params="queryParams"
            :flat-array-hospit="flatArrayhospit"
            :flat-array-dept="flatArraydept"
            :options="options"
            :orgname="orgname"
            @view-details="viewDetails"
          />
        </el-tab-pane>
        <el-tab-pane label="延续护理统计" name="continued" v-if="orgname == '省立同德翠苑院区'">
          <ContinuedCare
            ref="continuedCare"
            :query-params="queryParams"
            :flat-array-hospit="flatArrayhospit"
            :flat-array-dept="flatArraydept"
            :options="options"
            :orgname="orgname"
            @view-details="viewDetails"
          />
        </el-tab-pane>
      </el-tabs>
    </div>
    <!-- å¼¹çª—区域 -->
    <ChartDialog
      :visible="chartDialogVisible"
      :data="chartData"
      :active-tab="activeTab"
      @close="chartDialogVisible = false"
    />
    <DetailDialog
      :visible="infotitleVisible"
      :title="infotitle"
      :visible.sync="infotitleVisible"
      v-loading="infotitloading"
      width="70%"
      :close-on-click-modal="false"
    >
      <div style="margin-bottom: 16px; display: flex; align-items: center">
        <span style="margin-right: 10px; font-weight: bold">患者姓名查询:</span>
        <el-input
          v-model="searchName"
          placeholder="请输入患者姓名进行筛选"
          clearable
          style="width: 300px"
          @input="handleSearch"
          @clear="handleSearch"
        >
        </el-input>
        <span
          style="margin-left: 10px; color: rgb(35, 81, 233); font-size: 16px"
        >
          å…± {{ infotitlelist.length }} æ¡è®°å½•
        </span>
      </div>
      <div class="examine-jic">
        <div class="jic-value">
          <el-row :gutter="20">
            <!-- é€‰æ‹©æ‚£è€…列表 -->
            <div
              class="data-list"
              ref="dataList"
              @scroll="handleScroll"
              v-loading="infotitloading"
            >
              <el-table
                :data="currentDisplayList"
                height="660"
                style="width: 100%"
              >
                <el-table-column
                  prop="sendname"
                  align="center"
                  label="姓名"
                  width="100"
                >
                </el-table-column>
                <el-table-column
                  prop="taskName"
                  align="center"
                  width="200"
                  show-overflow-tooltip
                  label="任务名称"
                >
                </el-table-column>
                <el-table-column
                  prop="sendstate"
                  align="center"
                  width="200"
                  label="任务状态"
                >
                  <template slot-scope="scope">
                    <div v-if="scope.row.sendstate == 1">
                      <el-tag type="primary" :disable-transitions="false"
                        >表单已领取</el-tag
                      >
                    </div>
                    <div v-if="scope.row.sendstate == 2">
                      <el-tag type="primary" :disable-transitions="false"
                        >待随访</el-tag
                      >
                    </div>
                    <div v-if="scope.row.sendstate == 3">
                      <el-tag type="success" :disable-transitions="false"
                        >表单已发送</el-tag
                      >
                    </div>
                    <div v-if="scope.row.sendstate == 4">
                      <el-tag type="info" :disable-transitions="false"
                        >不执行</el-tag
                      >
                    </div>
                    <div v-if="scope.row.sendstate == 5">
                      <el-tag type="danger" :disable-transitions="false"
                        >发送失败</el-tag
                      >
                    </div>
                    <div v-if="scope.row.sendstate == 6">
                      <el-tag type="success" :disable-transitions="false"
                        >已完成</el-tag
                      >
                    </div>
                  </template>
                </el-table-column>
                <el-table-column
                  label="任务执行方式"
                  align="center"
                  key="preachform"
                  prop="preachform"
                  width="160"
                  :show-overflow-tooltip="true"
                >
                  <template slot-scope="scope">
                    <span v-for="item in scope.row.preachform"
                      >{{ item }}、
                    </span>
                  </template>
                </el-table-column>
                <el-table-column
                  prop="visitTime"
                  align="center"
                  label="应随访时间"
                  width="200"
                  show-overflow-tooltip
                >
                </el-table-column>
                <el-table-column
                  prop="finishtime"
                  align="center"
                  label="随访完成时间"
                  width="200"
                  show-overflow-tooltip
                >
                </el-table-column>
                <el-table-column
                  label="出院日期"
                  width="200"
                  align="center"
                  key="endtime"
                  prop="endtime"
                >
                  <template slot-scope="scope">
                    <span>{{ formatTime(scope.row.endtime) }}</span>
                  </template></el-table-column
                >
                <el-table-column
                  label="责任护士"
                  width="120"
                  align="center"
                  key="nurseName"
                  prop="nurseName"
                />
                <el-table-column
                  label="主治医生"
                  width="120"
                  align="center"
                  key="drname"
                  prop="drname"
                />
      :data="infotitlelist"
      :search-name="searchName"
      @close="infotitleVisible = false"
      @search="handleSearch"
      @details-go="SeedetailsgGo"
    />
                <el-table-column
                  label="结果状态"
                  align="center"
                  key="excep"
                  prop="excep"
                  width="120"
                >
                  <template slot-scope="scope">
                    <dict-tag
                      :options="dict.type.sys_yujing"
                      :value="scope.row.excep"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  label="处理意见"
                  align="center"
                  key="suggest"
                  prop="suggest"
                  width="120"
                >
                  <template slot-scope="scope">
                    <dict-tag
                      :options="dict.type.sys_suggest"
                      :value="scope.row.suggest"
                    />
                  </template>
                </el-table-column>
                <el-table-column
                  prop="templatename"
                  align="center"
                  label="服务模板"
                  width="200"
                  show-overflow-tooltip
                >
                </el-table-column>
                <el-table-column
                  prop="remark"
                  align="center"
                  label="服务记录"
                  width="200"
                  show-overflow-tooltip
                >
                </el-table-column>
                <el-table-column
                  prop="bankcardno"
                  align="center"
                  label="呼叫状态"
                  width="210"
                >
                </el-table-column>
                <el-table-column
                  label="操作"
                  fixed="right"
                  align="center"
                  width="200"
                  class-name="small-padding fixed-width"
                >
                  <template slot-scope="scope">
                    <el-button
                      size="medium"
                      type="text"
                      @click="SeedetailsgGo(scope.row)"
                      ><span class="button-zx"
                        ><i class="el-icon-s-order"></i>查看</span
                      ></el-button
                    >
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </el-row>
        </div>
      </div>
    </el-dialog>
    <TimelyRateDialog
      :visible="SeedetailsVisible"
      :loading="Seedloading"
      :data="logsheetlist"
      :total="patienttotal"
      :query-params="patientqueryParams"
      @close="SeedetailsVisible = false"
      @search="Seedetailstion"
      @details-go="SeedetailsgGo"
    />
  </div>
</template>
<script>
import {
  toamendtag,
  addapitag,
  deletetag,
  changetagcategory,
} from "@/api/system/label";
import store from "@/store";
import { getSfStatistics, selectTimelyRate } from "@/api/system/user";
import * as XLSX from "xlsx";
import FileSaver from "file-saver";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
const shortcuts = [
  {
    text: "今天",
    onClick(picker) {
      picker.$emit("pick", new Date());
    },
  },
  {
    text: "昨天",
    onClick(picker) {
      const date = new Date();
      date.setTime(date.getTime() - 3600 * 1000 * 24);
      picker.$emit("pick", date);
    },
  },
  {
    text: "一周前",
    onClick(picker) {
      const date = new Date();
      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
      picker.$emit("pick", date);
    },
  },
];
import FirstFollowUp from './components/FirstFollowUp.vue'
import SecondFollowUp from './components/SecondFollowUp.vue'
import ContinuedCare from './components/ContinuedCare.vue'
import ChartDialog from './components/ChartDialog.vue'
import DetailDialog from './components/DetailDialog.vue'
import TimelyRateDialog from './components/TimelyRateDialog.vue'
export default {
  name: "Percentage",
  dicts: ["sys_normal_disable", "sys_user_sex"],
  components: { Treeselect },
  name: 'FollowUpStatistics',
  components: {
    FirstFollowUp,
    SecondFollowUp,
    ContinuedCare,
    ChartDialog,
    DetailDialog,
    TimelyRateDialog
  },
  data() {
    return {
      topactiveName: "Local", //顶部选择
      activeName: "first", //侧边选择
      orgname: "",
      expands: [],
      infotitlelist: [],
      currentDisplayList: [], // å½“前显示的数据
      loadIndex: 0, // å½“前已加载的数据索引
      pageSize: 100, // æ¯æ¬¡åŠ è½½çš„æ•°æ®é‡
      isLoading: false, // é˜²æ­¢æ»šåŠ¨æ—¶é‡å¤åŠ è½½
      // é®ç½©å±‚
      loading: false,
      Seedloading: false,
      chartDialogVisible: false,
      infotitleVisible: false,
      searchName: "", // æœç´¢å…³é”®è¯
      infotitloading: false,
      infotitle: "",
      pieChart: null,
      barLineChart: null,
      // é€‰ä¸­æ•°ç»„
      ids: [],
      // éžå•个禁用
      single: true,
      // éžå¤šä¸ªç¦ç”¨
      multiple: true,
      // æ˜¾ç¤ºæœç´¢æ¡ä»¶
      showSearch: true,
      idds: "", //分类id
      // æ€»æ¡æ•°
      total: 0,
      flatArrayhospit: [],
      flatArraydept: [],
      patienttotal: 0,
      logsheetlist: [],
      activeTab: 'first',
      orgname: localStorage.getItem('orgname') || '',
      Statisticallist: [
        {
          label: "病区统计",
          value: 1,
        },
        {
          label: "科室统计",
          value: 2,
        },
        { label: '病区统计', value: 1 },
        { label: '科室统计', value: 2 }
      ],
      patientqueryParams: {
        pn: 1,
        ps: 10,
      },
      amendtag: false, //是否修改类别
      lstamendtag: false, //是否修改标签
      scavisible: false, //删除弹框
      deleteVisible: false, //分类删除弹框
      deletefenl: "高血压", //删除项
      //修改添加标签弹框数据
      tagform: {
        isupload: "",
        tagname: "",
        tagcategoryid: "",
        tagdescription: "",
      },
      classifyform: {
        categoryname: "",
      },
      // æ ‡ç­¾è¡¨æ ¼æ•°æ®
      userList: [],
      // å¼¹å‡ºå±‚标题
      title: "",
      // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
      open: false,
      // æ—¥æœŸèŒƒå›´
      dateRange: [],
      // å²—位选项
      postOptions: [],
      // è§’色选项
      roleOptions: [],
      // å­˜å‚¨æ‰€æœ‰ç§‘室代码
      allDeptCodes: [],
      // å­˜å‚¨æ‰€æœ‰ç—…区代码
      allWardCodes: [],
      checkboxlist: [],
      // è¡¨å•参数
      form: {},
      forms: {
        name: "",
      },
      numberlb: 22,
      dialogFormVisible: false, //添加、修改类别弹框
      lstamendtagVisible: false, //添加、修改标签弹框
      goQRCodeVisible: false, //二维码弹框
      sidecolumnval: "", //类别搜索
      propss: { multiple: true },
      SeedetailsVisible: false,
      options: store.getters.tasktypes,
      pickerOptions: {
        disabledDate(time) {
          return time.getTime() < Date.now() - 3600 * 1000 * 24;
        },
        shortcuts: shortcuts,
      },
      pickerOptionsa: {
        disabledDate(time) {
          return time.getTime() > Date.now();
        },
        shortcuts: shortcuts,
      },
      // æŸ¥è¯¢æ ‡ç­¾åˆ—表参数
      options: this.$store.getters.tasktypes,
      queryParams: {
        serviceType: [2],
        dateRange: [],
        statisticaltype: 1,
        leavehospitaldistrictcodes: ["all"], // é»˜è®¤é€‰ä¸­å…¨éƒ¨ç—…区
        deptcodes: [], // é»˜è®¤é€‰ä¸­å…¨éƒ¨ç§‘室
        leavehospitaldistrictcodes: ['all'],
        deptcodes: []
      },
      // åˆ—信息
      columns: [
        { key: 0, label: `标签编号`, visible: true },
        { key: 1, label: `标签名称`, visible: true },
        { key: 2, label: `标签昵称`, visible: true },
        { key: 3, label: `部门`, visible: true },
        { key: 4, label: `手机号码`, visible: true },
        { key: 5, label: `状态`, visible: true },
        { key: 6, label: `创建时间`, visible: true },
      ],
    };
      flatArrayhospit: [],
      flatArraydept: [],
      allDeptCodes: [],
      allWardCodes: [],
      showSearch: true,
      // å¼¹çª—相关状态
      chartDialogVisible: false,
      chartData: [],
      infotitleVisible: false,
      SeedetailsVisible: false,
      searchName: '',
      infotitle: '',
      infotitlelist: [],
      patienttotal: 0,
      logsheetlist: [],
      Seedloading: false,
      patientqueryParams: {
        pn: 1,
        ps: 10
      }
    }
  },
  watch: {},
  created() {
    this.getDeptTree();
    this.getList();
    this.checkboxlist = store.getters.checkboxlist;
    this.orgname = localStorage.getItem("orgname");
    this.getDeptTree()
  },
  methods: {
    /** æŸ¥è¯¢æ ‡ç­¾åˆ—表 */
    getList() {
      // å¤„理查询参数
      const params = {
        ...this.queryParams,
        // å¦‚果选择了"全部",则传所有病区/科室代码
        leavehospitaldistrictcodes:
          this.queryParams.leavehospitaldistrictcodes.includes("all")
            ? this.allWardCodes
            : this.queryParams.leavehospitaldistrictcodes,
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.allDeptCodes
          : this.queryParams.deptcodes,
      };
      this.loading = true;
      // ç§»é™¤å¯èƒ½å­˜åœ¨çš„"all"值
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      getSfStatistics(params).then((response) => {
        this.loading = false;
        this.total = response.total;
        // this.userList = response.data;
        this.userList = this.customSort(response.data);
      });
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
    // æœç´¢å¤„理函数
    handleSearch() {
      if (!this.searchName.trim()) {
        // å¦‚果搜索框为空,显示所有数据
        this.currentDisplayList = [...this.infotitlelist];
      } else {
        // æ ¹æ®æ‚£è€…姓名进行筛选(不区分大小写)
        const keyword = this.searchName.toLowerCase();
        this.currentDisplayList = this.infotitlelist.filter((item) => {
          return item.sendname && item.sendname.toLowerCase().includes(keyword);
        });
      }
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
        "十三",
        "十四",
        "十五",
        "十六",
        "十七",
        "十八",
        "十九",
        "二十",
        "二十一",
        "二十二",
        "二十三",
        "二十四",
        "二十五",
        "二十六",
        "二十七",
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    getRowKey(row) {
      return row.statisticaltype === 1
        ? row.leavehospitaldistrictcode
        : row.deptcode;
    },
    // å¤„理行点击展开
    handleRowClick(row) {
      console.log(row, "row");
      // å¦‚果已经展开则收起
      if (this.expands.includes(this.getRowKey(row))) {
        this.expands = [];
        return;
      }
      // å¤„理查询参数
      const params = {
        ...this.queryParams,
        // å¦‚果选择了"全部",则传所有病区/科室代码
        deptcodes: this.queryParams.deptcodes.includes("all")
          ? this.allDeptCodes
          : this.queryParams.deptcodes,
        leavehospitaldistrictcodes: [row.leavehospitaldistrictcode],
        drcode: "1",
      };
      // ç§»é™¤å¯èƒ½å­˜åœ¨çš„"all"值
      delete params.leavehospitaldistrictcodes.all;
      delete params.deptcodes.all;
      // å¦‚果该行还没有加载医生数据,则加载
      if (!row.doctorStats) {
        this.loading = true;
        getSfStatistics(params).then((res) => {
          this.$set(row, "doctorStats", res.data);
          this.expands = [this.getRowKey(row)];
          this.loading = false;
        });
      } else {
        this.expands = [this.getRowKey(row)];
      }
    },
    getSummaries(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计";
          return;
        }
        if (index === 1 || index === 2) {
          sums[index] = "/";
          return;
        }
        // å¯¹ç™¾åˆ†æ¯”字段特殊处理 - å–平均值
        if (
          column.property === "followUpRate" ||
          column.property === "rate" ||
          column.property === "followUpRateAgain"
        ) {
          // æå–所有有效百分比值并转换为小数
          const percentageValues = data
            .map((item) => {
              const value = item[column.property];
              if (!value || value === "-" || value === "0%") return null;
              // å¤„理带百分号的数据
              if (typeof value === "string" && value.includes("%")) {
                // åŽ»é™¤ç™¾åˆ†å·å¹¶è½¬æ¢ä¸ºå°æ•°
                const numValue = parseFloat(value.replace("%", "")) / 100;
                return isNaN(numValue) ? null : numValue;
              } else {
                // å¤„理已经是小数的数据
                const numValue = parseFloat(value);
                return isNaN(numValue) ? null : numValue;
              }
            })
            .filter((value) => value !== null && value !== 0); // è¿‡æ»¤æŽ‰null和0值
          if (percentageValues.length > 0) {
            const average =
              percentageValues.reduce((sum, value) => sum + value, 0) /
              percentageValues.length;
            sums[index] = (average * 100).toFixed(2) + "%";
          } else {
            sums[index] = "0.00%";
          }
        } else {
          // æ™®é€šæ•°å­—字段 - æ±‚å’Œ
          const values = data.map((item) => {
            const value = item[column.property];
            if (value === "-" || value === "" || value === null) return 0;
            return Number(value) || 0;
          });
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0);
            sums[index] = this.formatNumber(sums[index]);
          } else {
            sums[index] = "-";
          }
        }
      });
      return sums;
    },
    // å†…部表格合计行计算方法
    getInnerSummaries(param) {
      const { columns, data } = param;
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "小计";
          return;
        }
        if (column.property === "drname" || column.property === "deptname") {
          sums[index] = "-";
          return;
        }
        // å¯¹ç™¾åˆ†æ¯”字段特殊处理 - å–平均值
        if (column.property === "followUpRate" || column.property === "rate") {
          // æå–所有有效百分比值并转换为小数
          const percentageValues = data
            .map((item) => {
              const value = item[column.property];
              if (!value || value === "-" || value === "0%") return null;
              // å¤„理带百分号的数据
              if (typeof value === "string" && value.includes("%")) {
                // åŽ»é™¤ç™¾åˆ†å·å¹¶è½¬æ¢ä¸ºå°æ•°
                const numValue = parseFloat(value.replace("%", "")) / 100;
                return isNaN(numValue) ? null : numValue;
              } else {
                // å¤„理已经是小数的数据
                const numValue = parseFloat(value);
                return isNaN(numValue) ? null : numValue;
              }
            })
            .filter((value) => value !== null && value !== 0);
          if (percentageValues.length > 0) {
            const average =
              percentageValues.reduce((sum, value) => sum + value, 0) /
              percentageValues.length;
            sums[index] = (average * 100).toFixed(2) + "%";
          } else {
            sums[index] = "0.00%";
          }
        } else {
          // æ™®é€šæ•°å­—字段 - æ±‚å’Œ
          const values = data.map((item) => {
            const value = item[column.property];
            if (value === "-" || value === "" || value === null) return 0;
            return Number(value) || 0;
          });
          if (!values.every((value) => isNaN(value))) {
            sums[index] = values.reduce((prev, curr) => prev + curr, 0);
            sums[index] = this.formatNumber(sums[index]);
          } else {
            sums[index] = "-";
          }
        }
      });
      return sums;
    },
    // è¾…助方法:提取百分比数值
    extractPercentageValue(value) {
      if (!value) return null;
      if (typeof value === "string") {
        // å¤„理带百分号的字符串
        if (value.includes("%")) {
          const num = parseFloat(value.replace("%", ""));
          return isNaN(num) ? null : num / 100;
        }
        // å¤„理纯数字字符串
        const num = parseFloat(value);
        return isNaN(num) ? null : num;
      }
      // å¤„理数字类型
      return typeof value === "number" ? value : null;
    },
    // æ•°å­—格式化方法
    formatNumber(num) {
      if (isNaN(num)) return "-";
      return Number.isInteger(num) ? num.toString() : num.toFixed(0);
    },
    /** ä¿®æ”¹æ ‡ç­¾ */
    handleUpdate(row) {
      console.log(row, "修改标签");
      this.lstamendtagVisible = true;
      this.lstamendtag = true;
      this.tagform = {
        isupload: row.isupload,
        tagname: row.tagname,
        tagcategoryid: row.tagcategoryid,
        tagdescription: row.tagdescription,
        tagid: row.tagid,
      };
    },
    // èŽ·å–ç§‘å®¤æ ‘
    getDeptTree() {
      // ç§‘室列表
      this.flatArraydept = store.getters.belongDepts.map((dept) => {
      this.flatArraydept = this.$store.getters.belongDepts.map((dept) => {
        return {
          label: dept.deptName,
          value: dept.deptCode,
        };
      });
      // å­˜å‚¨æ‰€æœ‰ç§‘室代码
      this.allDeptCodes = store.getters.belongDepts.map(
        (dept) => dept.deptCode
      );
          value: dept.deptCode
        }
      })
      this.allDeptCodes = this.$store.getters.belongDepts.map((dept) => dept.deptCode)
      // ç—…区列表
      this.flatArrayhospit = store.getters.belongWards.map((ward) => {
      this.flatArrayhospit = this.$store.getters.belongWards.map((ward) => {
        return {
          label: ward.districtName,
          value: ward.districtCode,
        };
      });
      // å­˜å‚¨æ‰€æœ‰ç—…区代码
      this.allWardCodes = store.getters.belongWards.map(
        (ward) => ward.districtCode
      );
      this.flatArraydept.push({ label: "全部", value: "all" });
      this.flatArrayhospit.push({ label: "全部", value: "all" });
    },
    flattenArray(multiArray) {
      let result = [];
      // é€’归函数,用于将多级数组转换为一维数组,只包含最底层的元素
      function flatten(element) {
        // å¦‚果当前元素有子元素,继续递归
        if (element.children && element.children.length > 0) {
          element.children.forEach((child) => flatten(child));
        } else {
          // å…‹éš†å…ƒç´ ä»¥é¿å…ä¿®æ”¹åŽŸå§‹æ•°æ®
          let item = JSON.parse(JSON.stringify(element));
          result.push(item); // å°†æœ€åº•层的元素添加到结果数组
          value: ward.districtCode
        }
      })
      this.allWardCodes = this.$store.getters.belongWards.map((ward) => ward.districtCode)
      this.flatArraydept.push({ label: '全部', value: 'all' })
      this.flatArrayhospit.push({ label: '全部', value: 'all' })
    },
    handleTabClick(tab) {
      this.activeTab = tab.name
      this.loadCurrentTabData()
    },
    loadCurrentTabData() {
      switch (this.activeTab) {
        case 'first':
          this.$refs.firstFollowUp.loadData()
          break
        case 'second':
          this.$refs.secondFollowUp.loadData()
          break
        case 'continued':
          this.$refs.continuedCare.loadData()
          break
      }
    },
    handleQuery() {
      this.queryParams.startTime = this.parseTime(this.queryParams.dateRange[0])
      this.queryParams.endTime = this.parseTime(this.queryParams.dateRange[1])
      if (this.queryParams.statisticaltype == 1) {
        this.queryParams.deptcodes = []
      } else if (this.queryParams.statisticaltype == 2) {
        this.queryParams.leavehospitaldistrictcodes = []
      }
      // ä»Žé¡¶å±‚元素开始递归
      multiArray.forEach((element) => flatten(element));
      return result; // è¿”回只包含最底层元素的一维数组
      this.loadCurrentTabData()
    },
    addladeltag() {
      this.lstamendtagVisible = true;
      this.lstamendtag = false;
      this.tagform = {
        isupload: "",
        tagname: "",
        tagcategoryid: "",
        tagdescription: "",
        tagid: "",
      };
    resetQuery() {
      this.queryParams.dateRange = []
      this.queryParams.leavehospitaldistrictcodes = []
      this.handleQuery()
    },
    Seedetails(row) {
      this.SeedetailsVisible = true;
      this.Seedloading = true;
      this.patientqueryParams.starttime = this.parseTime(
        this.queryParams.dateRange[0]
      );
      this.patientqueryParams.endtime = this.parseTime(
        this.queryParams.dateRange[1]
      );
      this.patientqueryParams.deptcode = row.deptcode;
      selectTimelyRate(this.patientqueryParams).then((response) => {
        this.logsheetlist = response.data.detail;
        this.patienttotal = response.data.total;
        this.Seedloading = false;
      });
    async handleExport() {
      switch (this.activeTab) {
        case 'first':
          await this.$refs.firstFollowUp.exportTable()
          break
        case 'second':
          await this.$refs.secondFollowUp.exportTable()
          break
        case 'continued':
          await this.$refs.continuedCare.exportTable()
          break
      }
    },
    Seedetailstion() {
      selectTimelyRate(this.patientqueryParams).then((response) => {
        this.logsheetlist = response.data.detail;
        this.patienttotal = response.data.total;
        this.Seedloading = false;
      });
    showChartDialog() {
      this.chartData = this.getCurrentTabData()
      this.chartDialogVisible = true
    },
    getCurrentTabData() {
      switch (this.activeTab) {
        case 'first':
          return this.$refs.firstFollowUp.tableData
        case 'second':
          return this.$refs.secondFollowUp.tableData
        case 'continued':
          return this.$refs.continuedCare.tableData
        default:
          return []
      }
    },
    viewDetails(row, title) {
      this.infotitleVisible = true;
      this.infotitle = title;
      this.infotitlelist = row; // å‡è®¾row就是需要展示的详细数组
      console.log(this.infotitlelist, "this.infotitlelist");
      this.infotitlelist.forEach((item) => {
        let idArray = null;
        if (item.preachform) {
          if (item.endtime) {
            item.preachformson = item.preachform;
            idArray = item.preachform.split(",");
          }
          item.preachform = idArray.map((value) => {
            // æŸ¥æ‰¾id对应的对象
            const item = this.checkboxlist.find((item) => item.value == value);
            // å¦‚果找到对应的id,返回label值,否则返回null
            return item ? item.label : null;
          });
        }
      });
      // åˆå§‹åŒ–加载
      this.loadIndex = 0;
      this.currentDisplayList = [];
      this.$nextTick(() => {
        this.loadMoreData();
      });
      this.infotitle = title
      this.infotitlelist = row
      this.infotitleVisible = true
    },
    loadMoreData() {
      if (this.isLoading) return;
      this.isLoading = true;
      // æ¨¡æ‹Ÿå¼‚步加载,实际可能是直接切片本地数据
      setTimeout(() => {
        console.log(this.infotitlelist, "this.infotitlelist");
    Seedetails(row) {
      this.SeedetailsVisible = true
      this.Seedloading = true
        const nextChunk = this.infotitlelist.slice(
          this.loadIndex,
          this.loadIndex + this.pageSize
        );
        this.currentDisplayList = this.currentDisplayList.concat(nextChunk);
        this.loadIndex += this.pageSize;
        this.isLoading = false;
      }, 200);
      this.$refs.firstFollowUp.selectTimelyRate(row, this.queryParams.dateRange)
        .then(response => {
          this.logsheetlist = response.data.detail
          this.patienttotal = response.data.total
          this.Seedloading = false
        })
    },
    handleScroll(event) {
      const scrollContainer = event.target;
      // åˆ¤æ–­æ˜¯å¦æ»šåŠ¨åˆ°åº•éƒ¨
      const isAtBottom =
        scrollContainer.scrollTop + scrollContainer.clientHeight >=
        scrollContainer.scrollHeight - 10;
      if (
        isAtBottom &&
        !this.isLoading &&
        this.loadIndex < this.infotitlelist.length
      ) {
        this.loadMoreData();
      }
    Seedetailstion() {
      this.$refs.firstFollowUp.selectTimelyRate(this.patientqueryParams)
        .then(response => {
          this.logsheetlist = response.data.detail
          this.patienttotal = response.data.total
        })
    },
    SeedetailsgGo(row) {
      this.SeedetailsVisible = false;
      let type = "";
      if (row.preachformson && row.preachformson.includes("3")) {
        type = 1;
      this.SeedetailsVisible = false
      let type = ''
      if (row.preachformson && row.preachformson.includes('3')) {
        type = 1
      }
      setTimeout(() => {
        this.$router.push({
          path: "/followvisit/record/detailpage/",
          path: '/followvisit/record/detailpage/',
          query: {
            taskid: row.taskid,
            patid: row.patid,
            id: row.id,
            Voicetype: type,
            // visitCount: this.topqueryParams.visitCount,
          },
        });
      }, 300);
    },
    // æ·»åŠ /修改标签
    Maintenancetag() {
      if (this.lstamendtag) {
        toamendtag(this.addDateRange(this.tagform)).then((response) => {
          console.log(response);
          this.getList();
        });
      } else {
        addapitag(this.addDateRange(this.tagform)).then((response) => {
          console.log(response);
          this.getList();
        });
      }
      this.tagform = {
        isupload: "",
        tagname: "",
        tagcategoryid: "",
        tagdescription: "",
        tagid: "",
      };
    },
    routerErr(row) {
      console.log(row, "跳转异常");
      this.$router.push({
        path: "/followvisit/discharge",
        query: {
          errtype: 1,
          leavehospitaldistrictcode: row.leavehospitaldistrictcode,
        },
      });
    },
    // è¡¨å•重置
    reset() {
      this.form = {
        userId: undefined,
        deptId: undefined,
        userName: undefined,
        nickName: undefined,
        password: undefined,
        phonenumber: undefined,
        email: undefined,
        sex: undefined,
        status: "0",
        remark: undefined,
        postIds: [],
        roleIds: [],
      };
      this.resetForm("form");
    },
    // æ ‡ç­¾çŠ¶æ€ä¿®æ”¹
    handleStatusChange(row) {
      console.log(row.isupload);
      let text = row.isupload === "0" ? "启用" : "停用";
      this.$modal
        .confirm('确认要"' + text + '""' + row.tagname + '"标签吗?')
        .then(function () {
          return changetagcategory(row.tagid, row.isupload);
        })
        .then(() => {
          this.$modal.msgSuccess(text + "成功");
        })
        .catch(function () {
          row.isupload = row.isupload === "0" ? "1" : "0";
        });
    },
    /** æœç´¢æŒ‰é’®æ“ä½œ */
    handleQuery() {
      this.queryParams.pageNum = 1;
      if (!this.queryParams.dateRange) this.queryParams.dateRange = [];
      if (this.queryParams.statisticaltype == 1) {
        this.queryParams.deptcodes = [];
      } else if (this.queryParams.statisticaltype == 2) {
        this.queryParams.leavehospitaldistrictcodes = [];
      }
      console.log(this.queryParams.dateRange);
      this.queryParams.startTime = this.parseTime(
        this.queryParams.dateRange[0]
      );
      this.queryParams.endTime = this.parseTime(this.queryParams.dateRange[1]);
      this.getList();
    },
    /** é‡ç½®æŒ‰é’®æ“ä½œ */
    resetQuery() {
      this.queryParams.dateRange = [];
      this.queryParams.leavehospitaldistrictcodes = [];
      this.handleQuery();
    },
    // å¤šé€‰æ¡†é€‰ä¸­æ•°æ®
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.tagid);
      this.single = selection.length != 1;
      this.multiple = !selection.length;
    },
    /** åˆ é™¤æŒ‰é’®æ“ä½œ */
    handleDelete(row) {
      console.log(row, "删除弹窗");
      const tagids = row.tagid || this.ids;
      console.log(tagids);
      const tagname = row.tagname;
      this.$modal
        .confirm(
          tagname
            ? '是否确认删除标签名称为"' + tagname + '"的数据项?'
            : "是否确认删除选中的数据项?"
        )
        .then(function () {
          return deletetag(tagids);
        })
        .then(() => {
          this.getList();
          this.$modal.msgSuccess("删除成功");
        })
        .catch(() => {});
    },
    // å¯¼å‡ºæ–¹æ³•
    // æ›¿æ¢æ‚¨åŽŸæ¥çš„ exportTable æ–¹æ³•
    async exportTable() {
      try {
        // 1. èŽ·å–å¹¶æ ¼å¼åŒ–æ—¥æœŸèŒƒå›´
        let dateRangeString = ""; // ç”¨äºŽæ–‡ä»¶å
        let sheetNameSuffix = ""; // ç”¨äºŽå·¥ä½œè¡¨å
        // æ£€æŸ¥æ˜¯å¦å­˜åœ¨é€‰ä¸­çš„æ—¥æœŸèŒƒå›´
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateStr = this.queryParams.dateRange[0]; // å¼€å§‹æ—¥æœŸå­—符串,例如 "2026-01-01 00:00:00"
          const endDateStr = this.queryParams.dateRange[1]; // ç»“束日期字符串
          // æ ¼å¼åŒ–日期为 YYYY-MM-DD(去掉时间部分)
          const formatDateForDisplay = (dateTimeStr) => {
            return dateTimeStr.split(" ")[0]; // å–空格前的部分,即 "YYYY-MM-DD"
          };
          const startDateFormatted = formatDateForDisplay(startDateStr);
          const endDateFormatted = formatDateForDisplay(endDateStr);
          // æž„建日期范围字符串
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
        } else {
          // å¦‚果没有选择日期范围,则使用当前月份作为备选方案
          const now = new Date();
          const currentMonth = now.getMonth() + 1;
          dateRangeString = `${currentMonth}月`;
          sheetNameSuffix = `${currentMonth}月`;
        }
        // 2. åŠ¨æ€æž„å»ºæ–‡ä»¶åå’Œå·¥ä½œè¡¨å
        const excelName = `出院随访统计表_${dateRangeString}.xlsx`;
        const worksheetName = `随访统计_${sheetNameSuffix}`; // å·¥ä½œè¡¨åä¸èƒ½è¶…过31个字符[2](@ref)
        // åˆ›å»ºæ–°çš„工作簿和工作表
        const workbook = new ExcelJS.Workbook();
        const worksheet = workbook.addWorksheet(worksheetName); // ä½¿ç”¨åŠ¨æ€å·¥ä½œè¡¨å
        // å®šä¹‰æ ·å¼ï¼ˆæ–°å¢žæ€»æ ‡é¢˜æ ·å¼ï¼‰
        const titleStyle = {
          font: {
            name: "微软雅黑",
            size: 16,
            bold: true,
            color: { argb: "FF000000" },
          },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFE6F3FF" },
          },
          alignment: {
            vertical: "middle",
            horizontal: "center",
            wrapText: true,
          },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        // å®šä¹‰æ ·å¼
        const headerStyle = {
          font: {
            name: "微软雅黑",
            size: 11,
            bold: true,
            color: { argb: "FF000000" },
          },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: {
            vertical: "middle",
            horizontal: "center",
            wrapText: true,
          },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const cellStyle = {
          font: {
            name: "宋体",
            size: 10,
            color: { argb: "FF000000" },
          },
          alignment: {
            vertical: "middle",
            horizontal: "center",
          },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const summaryStyle = {
          font: {
            name: "宋体",
            size: 10,
            bold: true,
            color: { argb: "FF409EFF" },
          },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: {
            vertical: "middle",
            horizontal: "center",
          },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        // 1. æ·»åŠ æ€»æ ‡é¢˜è¡Œï¼ˆç¬¬ä¸€è¡Œï¼‰
        worksheet.mergeCells(1, 1, 1, 23); // åˆå¹¶A1到W1的所有列[1,4](@ref)
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `${sheetNameSuffix}出院随访统计表`; // ä½¿ç”¨æ–‡ä»¶åä½œä¸ºæ€»æ ‡é¢˜
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35; // è®¾ç½®æ€»æ ‡é¢˜è¡Œé«˜
        // 1. é¦–先,创建并设置第二行(子表头)的所有单元格
        const secondRowHeaders = [
          "", // A2 å±•开列占位(其值将由第一行合并后的主单元格决定)
          "出院病区",
          "科室",
          "出院人次",
          "无需随访人次",
          "应随访人次", // B2 to F2
          // é¦–次出院随访子表头
          "需随访",
          "待随访",
          "随访成功",
          "随访失败",
          "随访率",
          "及时率",
          "人工",
          "短信",
          "微信",
          // å†æ¬¡å‡ºé™¢éšè®¿å­è¡¨å¤´
          "需随访",
          "待随访",
          "随访成功",
          "随访失败",
          "随访率",
          "人工",
          "短信",
          "微信",
        ];
        // æ·»åŠ ç¬¬äºŒè¡Œï¼ˆåŽŸç¬¬ä¸€è¡Œä¸‹ç§»ï¼‰
        secondRowHeaders.forEach((header, index) => {
          const cell = worksheet.getCell(3, index + 1); // æ”¹ä¸ºç¬¬3行
          cell.value = header;
          cell.style = headerStyle;
        });
        // 3. è°ƒæ•´åŽŸåˆå¹¶å•å…ƒæ ¼ä½ç½®ï¼ˆåŽŸç¬¬1行合并单元格下移到第2行)
        // åˆå¹¶ A2:A3
        worksheet.mergeCells(2, 1, 3, 1);
        worksheet.getCell(2, 1).value = "";
        worksheet.getCell(2, 1).style = headerStyle;
        // åˆå¹¶ B2:B3
        worksheet.mergeCells(2, 2, 3, 2);
        worksheet.getCell(2, 2).value = "出院病区";
        worksheet.getCell(2, 2).style = headerStyle;
        // åˆå¹¶ C2:C3
        worksheet.mergeCells(2, 3, 3, 3);
        worksheet.getCell(2, 3).value = "科室";
        worksheet.getCell(2, 3).style = headerStyle;
        // åˆå¹¶ D2:D3
        worksheet.mergeCells(2, 4, 3, 4);
        worksheet.getCell(2, 4).value = "出院人次";
        worksheet.getCell(2, 4).style = headerStyle;
        // åˆå¹¶ E2:E3
        worksheet.mergeCells(2, 5, 3, 5);
        worksheet.getCell(2, 5).value = "无需随访人次";
        worksheet.getCell(2, 5).style = headerStyle;
        // åˆå¹¶ F2:F3
        worksheet.mergeCells(2, 6, 3, 6);
        worksheet.getCell(2, 6).value = "应随访人次";
        worksheet.getCell(2, 6).style = headerStyle;
        // 4. è°ƒæ•´æ¨ªå‘合并标题位置(下移到第2行)
        // é¦–次出院随访(合并G2:O2)
        worksheet.mergeCells(2, 7, 2, 15); // G2:O2
        worksheet.getCell(2, 7).value = "首次出院随访";
        worksheet.getCell(2, 7).style = headerStyle;
        // å†æ¬¡å‡ºé™¢éšè®¿ï¼ˆåˆå¹¶P2:W2)
        worksheet.mergeCells(2, 16, 2, 23); // P2:W2
        worksheet.getCell(2, 16).value = "再次出院随访";
        worksheet.getCell(2, 16).style = headerStyle;
        // 5. è®¾ç½®è¡Œé«˜
        worksheet.getRow(1).height = 35; // æ€»æ ‡é¢˜è¡Œé«˜
        worksheet.getRow(2).height = 28; // åŽŸç¬¬ä¸€è¡Œä¸‹ç§»
        worksheet.getRow(3).height = 25; // åŽŸç¬¬äºŒè¡Œä¸‹ç§»
        // 6. æ·»åŠ æ•°æ®è¡Œï¼ˆæ³¨æ„è¡Œç´¢å¼•éœ€è¦+1,因为上面插入了一行)
        this.userList.forEach((item, rowIndex) => {
          const dataRow = worksheet.addRow(
            [
              "", // å±•开列
              item.leavehospitaldistrictname || "",
              item.deptname || "",
              item.dischargeCount || 0,
              item.nonFollowUp || 0,
              item.followUpNeeded || 0,
              // é¦–次出院随访数据
              item.needFollowUp || 0,
              item.pendingFollowUp || 0,
              item.followUpSuccess || 0,
              item.followUpFail || 0,
              item.followUpRate || "0%",
              item.rate ? (Number(item.rate) * 100).toFixed(2) + "%" : "0%",
              item.manual || 0,
              item.sms || 0,
              item.weChat || 0,
              // å†æ¬¡å‡ºé™¢éšè®¿æ•°æ®
              item.needFollowUpAgain || 0,
              item.pendingFollowUpAgain || 0,
              item.followUpSuccessAgain || 0,
              item.followUpFailAgain || 0,
              item.followUpRateAgain || "0%",
              item.manualAgain || 0,
              item.smsAgain || 0,
              item.weChatAgain || 0,
            ],
            rowIndex + 4
          ); // ä»Žç¬¬4行开始添加数据(原第3行)
          // åº”用数据行样式
          dataRow.eachCell((cell) => {
            cell.style = cellStyle;
          });
          dataRow.height = 24;
        });
        // æ·»åŠ åˆè®¡è¡Œ
        const summaries = this.getSummaries({
          columns: [
            { property: "" },
            { property: "leavehospitaldistrictname" },
            { property: "deptname" },
            { property: "dischargeCount" },
            { property: "nonFollowUp" },
            { property: "followUpNeeded" },
            { property: "needFollowUp" },
            { property: "pendingFollowUp" },
            { property: "followUpSuccess" },
            { property: "followUpFail" },
            { property: "followUpRate" },
            { property: "rate" },
            { property: "manual" },
            { property: "sms" },
            { property: "weChat" },
            { property: "needFollowUpAgain" },
            { property: "pendingFollowUpAgain" },
            { property: "followUpSuccessAgain" },
            { property: "followUpFailAgain" },
            { property: "followUpRateAgain" },
            { property: "manualAgain" },
            { property: "smsAgain" },
            { property: "weChatAgain" },
          ],
          data: this.userList,
        });
        const summaryRow = worksheet.addRow(summaries);
        summaryRow.eachCell((cell, colNumber) => {
          cell.style = summaryStyle;
          // ç¬¬ä¸€åˆ—显示"合计"
          if (colNumber === 1) {
            cell.value = "合计";
            Voicetype: type
          }
        });
        summaryRow.height = 28;
        // è®¾ç½®åˆ—宽
        worksheet.columns = [
          { width: 8 }, // å±•开列
          { width: 20 }, // å‡ºé™¢ç—…区
          { width: 15 }, // ç§‘室
          { width: 12 }, // å‡ºé™¢äººæ¬¡
          { width: 12 }, // æ— éœ€éšè®¿äººæ¬¡
          { width: 12 }, // åº”随访人次
          // é¦–次出院随访列
          { width: 10 },
          { width: 10 },
          { width: 10 },
          { width: 10 },
          { width: 12 },
          { width: 12 },
          { width: 8 },
          { width: 8 },
          { width: 8 },
          // å†æ¬¡å‡ºé™¢éšè®¿åˆ—
          { width: 10 },
          { width: 10 },
          { width: 10 },
          { width: 10 },
          { width: 12 },
          { width: 8 },
          { width: 8 },
          { width: 8 },
        ];
        // ç”Ÿæˆå¹¶ä¸‹è½½æ–‡ä»¶
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
        this.$message.success("导出成功");
        return true;
      } catch (error) {
        console.error("导出失败:", error);
        this.$message.error(`导出失败: ${error.message}`);
        return false;
      }
        })
      }, 300)
    },
    // æ˜¾ç¤ºå›¾è¡¨å¼¹çª—
    showChartDialog() {
      this.chartDialogVisible = true;
      this.$nextTick(() => {
        this.initPieChart();
        this.initBarLineChart();
      });
    },
    // åœ¨methods中修改统计方法
    showChartDialog() {
      this.chartDialogVisible = true;
      this.$nextTick(() => {
        console.log(this.userList, "this.userList");
        this.initCharts();
      });
    },
    // æ–°å¢žåˆå§‹åŒ–图表方法
    initCharts() {
      this.initPieChart();
      this.initBarLineChart();
    },
    // åˆå§‹åŒ–饼图
    initPieChart() {
      const echarts = require("echarts");
      const pieDom = document.getElementById("pieChart");
      if (!pieDom) return;
      if (this.pieChart) {
        this.pieChart.dispose();
      }
      this.pieChart = echarts.init(pieDom);
      // è®¡ç®—饼图数据
      const followUpData = {
        pending: 0,
        success: 0,
        fail: 0,
      };
      this.userList.forEach((item) => {
        followUpData.pending += item.pendingFollowUp || 0;
        followUpData.success += item.followUpSuccess || 0;
        followUpData.fail += item.followUpFail || 0;
      });
      // ä½¿ç”¨æ›´ç¾Žè§‚的颜色方案
      const pieOption = {
        title: {
          text: "随访状态分布",
          left: "center",
          textStyle: {
            color: "#333",
            fontSize: 16,
          },
        },
        tooltip: {
          trigger: "item",
          formatter: "{a} <br/>{b}: {c} ({d}%)",
        },
        legend: {
          orient: "vertical",
          left: "left",
          data: ["待随访", "随访成功", "随访失败"],
          textStyle: {
            color: "#666",
          },
        },
        color: ["#FF9D4D", "#36B37E", "#FF5C5C"], // æ–°çš„配色方案
        series: [
          {
            name: "随访状态",
            type: "pie",
            radius: ["40%", "70%"],
            avoidLabelOverlap: true,
            itemStyle: {
              borderRadius: 10,
              borderColor: "#fff",
              borderWidth: 2,
            },
            label: {
              show: true,
              formatter: "{b}: {c} ({d}%)",
              color: "#333",
            },
            emphasis: {
              label: {
                show: true,
                fontSize: "18",
                fontWeight: "bold",
              },
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: "rgba(0, 0, 0, 0.5)",
              },
            },
            data: [
              {
                value: followUpData.pending,
                name: "待随访",
              },
              {
                value: followUpData.success,
                name: "随访成功",
              },
              {
                value: followUpData.fail,
                name: "随访失败",
              },
            ],
          },
        ],
      };
      this.pieChart.setOption(pieOption);
      window.addEventListener("resize", this.resizePieChart);
    },
    // åˆå§‹åŒ–柱状折线图
    initBarLineChart() {
      const echarts = require("echarts");
      const barDom = document.getElementById("barLineChart");
      if (!barDom) return;
      if (this.barLineChart) {
        this.barLineChart.dispose();
      }
      this.barLineChart = echarts.init(barDom);
      // å‡†å¤‡æ•°æ®
      const categories = this.userList.map(
        (item) => item.leavehospitaldistrictname || item.deptname
      );
      const dischargeData = this.userList.map(
        (item) => item.dischargeCount || 0
      );
      const followUpData = this.userList.map(
        (item) => item.followUpNeeded || 0
      );
      // æ–°å¢žä¸¤æ¡æŠ˜çº¿æ•°æ®
      const followUpRateData = this.userList.map((item) => {
        if (!item.followUpRate) return 0;
        // åŽ»æŽ‰ç™¾åˆ†å·å¹¶è½¬ä¸ºæ•°å­—
        const rateStr = String(item.followUpRate).replace("%", "");
        return parseFloat(rateStr) || 0;
      });
      const timelyRateData = this.userList.map((item) =>
        item.rate ? (Number(item.rate) * 100).toFixed(2) : 0
      );
      const option = {
        title: {
          text: "科室/病区随访趋势",
          left: "center",
          textStyle: {
            color: "#333",
            fontSize: 16,
          },
        },
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "cross",
            crossStyle: {
              color: "#999",
            },
          },
        },
        legend: {
          data: ["出院人次", "应随访人次", "随访率(%)", "及时率(%)"],
          top: "bottom",
          textStyle: {
            color: "#666",
          },
        },
        color: ["#5470C6", "#91CC75", "#EE6666", "#9A60B4"], // æ–°å¢žç´«è‰²ç”¨äºŽåŠæ—¶çއ
        xAxis: {
          type: "category",
          data: categories,
          axisLabel: {
            interval: 0,
            rotate: 30,
            color: "#666",
          },
          axisLine: {
            lineStyle: {
              color: "#ddd",
            },
          },
        },
        yAxis: [
          {
            type: "value",
            name: "人次",
            min: 0,
            axisLabel: {
              color: "#666",
            },
            axisLine: {
              lineStyle: {
                color: "#ddd",
              },
            },
            splitLine: {
              lineStyle: {
                color: "#f0f0f0",
              },
            },
          },
          {
            type: "value",
            name: "百分比(%)",
            min: 0,
            max: 100,
            axisLabel: {
              color: "#666",
              formatter: "{value}%",
            },
            axisLine: {
              lineStyle: {
                color: "#ddd",
              },
            },
            splitLine: {
              show: false,
            },
          },
        ],
        series: [
          {
            name: "出院人次",
            type: "bar",
            barWidth: "25%",
            data: dischargeData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0],
            },
          },
          {
            name: "应随访人次",
            type: "bar",
            barWidth: "25%",
            data: followUpData,
            itemStyle: {
              borderRadius: [4, 4, 0, 0],
            },
          },
          {
            name: "随访率(%)",
            type: "line",
            yAxisIndex: 1,
            data: followUpRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3,
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 80,
                  lineStyle: {
                    color: "#EE6666",
                    type: "dashed",
                  },
                  // label: {
                  //   position: 'end',
                  //   formatter: '目标80%'
                  // }
                },
              ],
            },
          },
          {
            name: "及时率(%)",
            type: "line",
            yAxisIndex: 1,
            data: timelyRateData,
            symbolSize: 8,
            lineStyle: {
              width: 3,
              type: "dotted", // ä½¿ç”¨è™šçº¿åŒºåˆ†
            },
            markLine: {
              silent: true,
              data: [
                {
                  yAxis: 90,
                  lineStyle: {
                    color: "#9A60B4",
                    type: "dashed",
                  },
                  // label: {
                  //   position: 'end',
                  //   formatter: '目标90%'
                  // }
                },
              ],
            },
          },
        ],
        grid: {
          top: "15%",
          left: "3%",
          right: "4%",
          bottom: "15%",
          containLabel: true,
        },
      };
      this.barLineChart.setOption(option);
      window.addEventListener("resize", this.resizeBarLineChart);
    },
    // å›¾è¡¨å“åº”式调整方法
    resizePieChart() {
      if (this.pieChart) {
        this.pieChart.resize();
      }
    },
    resizeBarLineChart() {
      if (this.barLineChart) {
        this.barLineChart.resize();
      }
    },
    // åœ¨ç»„件销毁时清理
    beforeDestroy() {
      // ç§»é™¤äº‹ä»¶ç›‘听
      window.removeEventListener("resize", this.resizePieChart);
      window.removeEventListener("resize", this.resizeBarLineChart);
      // é”€æ¯å›¾è¡¨å®žä¾‹
      if (this.pieChart) {
        this.pieChart.dispose();
        this.pieChart = null;
      }
      if (this.barLineChart) {
        this.barLineChart.dispose();
        this.barLineChart = null;
      }
    },
  },
};
    handleSearch() {
      // æœç´¢é€»è¾‘
    }
  }
}
</script>
<style lang="scss" scoped>
.sidecolumn {
  width: 180px;
  min-height: 100vh;
  text-align: center;
  //   display: flex;
  margin-top: 20px;
  margin: 20px;
  padding: 30px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .sidecolumn-top {
    display: flex;
    justify-content: space-between;
    .top-wj {
      font-size: 20px;
    }
    .top-tj {
      font-size: 18px;
      color: rgb(0, 89, 255);
      cursor: pointer;
    }
  }
  .center-ss {
    margin-top: 30px;
    .input-with-select {
      height: 40px !important;
    }
  }
  .bottom-fl {
    margin-top: 30px;
    display: center !important;
  }
}
.qrcode-dialo {
  text-align: center;
  //   display: flex;
  margin: 20px;
  padding: 30px;
  background: #edf1f7;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .qrcode-text {
    font-size: 20px;
    span {
      margin-left: 20px;
    }
  }
  .qrcode-img {
    width: 300px;
    height: 400px;
  }
}
::v-deep.el-tabs--left,
.el-tabs--right {
  overflow: hidden;
  align-items: center;
  display: flex;
}
::v-deep.el-input--medium .el-input__inner {
  height: 40px !important;
}
::v-deep.el-tabs--right .el-tabs__active-bar.is-right {
  height: 40px;
  width: 5px;
  left: 0;
}
::v-deep.el-tabs--right .el-tabs__item.is-right {
  display: block;
  text-align: left;
  font-size: 20px;
}
// ç¾ŽåŒ–合计行样式
::v-deep .el-table__footer {
  .el-table__cell {
    background-color: #f5f7fa;
    font-weight: 600;
    color: #409eff;
    .cell {
      font-weight: 600;
      color: #409eff;
    }
  }
}
// å†…部表格合计行样式
::v-deep .inner-table .el-table__footer {
  .el-table__cell {
    background-color: #ecf5ff;
    font-weight: 500;
    color: #67c23a;
    .cell {
      font-weight: 500;
      color: #67c23a;
    }
  }
}
// ç™¾åˆ†æ¯”字段特殊样式
.your-table-container
  ::v-deep
  .el-table__footer
  .el-table__cell[data-field="followUpRate"]
  .cell,
.your-table-container
  ::v-deep
  .el-table__footer
  .el-table__cell[data-field="rate"]
  .cell,
.your-table-container
  ::v-deep
  .el-table__footer
  .el-table__cell[data-field="followUpRateAgain"]
  .cell {
  color: #e6a23c !important;
  font-weight: 700 !important;
}
.leftvlue {
  //   display: flex;
  //   flex: 1;
  // width: 80%;
  // margin-top: 20px;
  margin: 20px;
  padding: 30px;
.follow-up-statistics {
  padding: 20px;
  background: #ffff;
  border: 1px solid #dcdfe6;
  -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12),
    0 0 6px 0 rgba(0, 0, 0, 0.04);
  .mulsz {
    font-size: 20px;
  }
}
/* ä½¿è¡Œæœ‰æ‰‹åž‹æŒ‡é’ˆ */
.el-table__row {
  cursor: pointer;
}
/* å†…层医生表格样式 */
.inner-table {
  // è¡¨å¤´èƒŒæ™¯è‰²
  ::v-deep .el-table__header-wrapper {
    background-color: #f0f7ff !important;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04);
    th {
      background-color: #f0f7ff !important;
  .search-section {
    margin-bottom: 20px;
  }
  .tab-section {
    ::v-deep .el-tabs__header {
      margin-bottom: 20px;
    }
    ::v-deep .el-tabs__item {
      font-size: 16px;
      padding: 0 20px;
      height: 40px;
      line-height: 40px;
    }
    ::v-deep .el-tabs__active-bar {
      height: 3px;
    }
  }
  // è¡¨æ ¼è¡ŒèƒŒæ™¯è‰²
  ::v-deep .el-table__body-wrapper {
    tr {
      background-color: #f9fbfe !important;
      &:hover {
        background-color: #e6f1ff !important;
      }
    }
  }
  // è¾¹æ¡†é¢œè‰²
  ::v-deep .el-table--border {
    border-color: #d9e8ff !important;
    td,
    th {
      border-color: #d9e8ff !important;
    }
  }
  // æ–‘马纹效果
  ::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
    background-color: #f5f9ff !important;
  }
}
/* å±•开行样式 */
.el-table__expanded-cell {
  padding: 10px 0 !important;
  background: #f8f8f8;
}
.document {
  width: 100px;
  height: 50px;
}
.data-list {
  max-height: 800px;
  overflow-y: auto;
}
.documentf {
  display: flex;
  justify-content: flex-end;
}
.button-text {
  color: rgb(70, 204, 238);
}
.button-textck {
  color: rgb(39, 167, 67);
}
.button-textxg {
  color: rgb(35, 81, 233);
}
.button-textsc {
  color: rgb(235, 23, 23);
}
</style>
vue.config.js
@@ -37,9 +37,10 @@
      [process.env.VUE_APP_BASE_API]: {
        // target: `https://www.health-y.cn/lssf`,
        // target: `http://192.168.100.10:8096`,
        // target: `http://192.168.100.10:8094`,//省立同德
        target: `http://192.168.100.10:8094`,//省立同德
        // target: `http://192.168.100.10:8095`,//新华
        target:`http://localhost:8095`,
        // target: `http://192.168.100.10:8098`,//市一
        // target:`http://localhost:8095`,
        // target:`http://35z1t16164.qicp.vip`,
        // target: `http://192.168.100.183:8095`,
        // target: `http://192.168.101.166:8093`,
xhyy.zip
Binary files differ