a2c10da81668de1f5b7d38f5962d46d795e3cc7e..c65b90aaa3477a90ebc325024927d80227c0c841
7 小时以前 WXL (wul)
测试完成
c65b90 对比 | 目录
12 小时以前 WXL (wul)
测试完成
61f0c5 对比 | 目录
已删除1个文件
已修改9个文件
已添加1个文件
4455 ■■■■■ 文件已修改
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
sltd.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/satisfactionse.js 25 ●●●●● 补丁 | 查看 | 原始文档 | 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 546 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/HistoricalFollow/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vue.config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip
Binary files differ
sltd.zip
Binary files differ
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/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)
        );
      }
@@ -1212,9 +1200,9 @@
    /** é…ç½®å˜æ›´å¤„理 */
    handleConfigChange(question) {
      this.$nextTick(() => {
        const index = this.filteredQuestionList.findIndex(
          (q) => q.id === question.id
        );
        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 +1249,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 +1364,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 +1412,6 @@
      }
    },
    /** å¤„理保存成功 */
    /** å¤„理保存成功 */
    handleSaveSuccess(question) {
      // åŒæ—¶æ›´æ–°é¢˜ç›®é¡¶å±‚字段
@@ -1450,6 +1486,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 +1524,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 +1554,11 @@
            this.$message.warning(
              `成功保存 ${successCount} ä¸ªï¼Œå¤±è´¥ ${failCount} ä¸ª`
            );
            // å¯ä»¥æ˜¾ç¤ºå…·ä½“哪些失败了
            const failedQuestions = results
              .filter((r) => !r.success)
              .map((r) => r.id);
            console.error("保存失败的题目ID:", failedQuestions);
          }
        })
        .catch(() => {
@@ -1505,39 +1572,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 +1615,7 @@
    /** æ·»åŠ æ–°é€‰é¡¹ */
    addNewOption() {
      this.currentOptions.push({
        id: Date.now(), // ä¸´æ—¶ID
        id: `temp_${Date.now()}_${this.currentOptions.length}`,
        targetvalue: "",
        isabnormal: 0,
        isNew: true,
@@ -1558,16 +1624,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 +1657,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 +1685,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 +1711,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 +1765,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",
vue.config.js
@@ -37,9 +37,9 @@
      [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://localhost:8095`,
        // target:`http://35z1t16164.qicp.vip`,
        // target: `http://192.168.100.172:8095`,
        // target: `http://192.168.101.166:8093`,