WXL (wul)
5 天以前 bc9538a9e071259ca8bbb6de52c60c9d7a55a03b
测试完成
已重命名1个文件
已修改12个文件
已添加1个文件
2637 ■■■■■ 文件已修改
src/api/AiCentre/SingleTask.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/patient/record.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/smartor/patouthosp.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CallCenterLs/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/configurationall/index.vue 2308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/DataOverview.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/tasklist/index.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/AwaitingAdmission.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/behospitalized.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/patient/hospital.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
随访通用922.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/SingleTask.js
@@ -86,6 +86,14 @@
    data: data,
  });
}
// æŸ¥è¯¢ä»»åŠ¡æ‰§è¡Œæƒ…å†µ
export function countByCondition(data) {
  return request({
    url: "/smartor/serviceSubtask/countByCondition",
    method: "post",
    data: data,
  });
}
// ä»»åŠ¡è§„åˆ™åˆ é™¤
export function deltaskrule(taskid) {
src/api/patient/record.js
@@ -3,10 +3,13 @@
// æŸ¥è¯¢ä½é™¢è®°å½•列表
export function listpatient(data) {
  const params = { ...data };
  // bedSort ä¸ä¼ å³ä¸æŒ‰åºŠå·æŽ’序
  if (params.bedSort == null || params.bedSort === "") delete params.bedSort;
  return request({
    url: '/smartor/patinhosp/selectPatMedInhospList',
    method: 'post',
    data: data
    data: params
  })
};
// ä½é™¢è®°å½•详情
src/api/smartor/patouthosp.js
@@ -10,10 +10,13 @@
}
// æŸ¥è¯¢å¾…入院记录列表
export function listPatMedInhosp(query) {
  const data = { ...query };
  // bedSort ä¸ä¼ å³ä¸æŒ‰åºŠå·æŽ’序
  if (data.bedSort == null || data.bedSort === "") delete data.bedSort;
  return request({
    url: '/smartor/patinhosp/selectPatMedInhospList',
    method: 'post',
    data: query
    data: data
  })
}
src/components/CallCenterLs/index.vue
@@ -267,9 +267,14 @@
    },
    onCallStatusChange(status) {
      const wasConnected = this.callStatus === "connected";
      this.callStatus = status.status;
      if (status.status == "connected") {
        this.startCallTimer();
        // ä»…在从非接通状态切换到接通时通知父组件(避免重复计数)
        if (!wasConnected) {
          this.$emit("call-connected");
        }
      } else if (status.status == "idle") {
        this.stopCallTimer();
      }
src/views/Satisfaction/configurationall/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2308 @@
<template>
  <div class="question-exception-config">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="header-content">
        <h2 class="page-title">题目异常处理配置</h2>
        <p class="page-description">
          åŸºäºŽæ¨¡æ¿é…ç½®å…¨éƒ¨é¢˜ç›®çš„责任科室和报备科室
        </p>
      </div>
    </div>
    <!-- æ¨¡æ¿é€‰æ‹©åŒºåŸŸ -->
    <div class="template-section">
      <el-card shadow="never">
        <div class="template-header">
          <h3 class="template-title">模板选择</h3>
          <p class="template-tip">请先选择模板类型和具体模板</p>
        </div>
        <el-form
          :model="templateForm"
          :rules="templateRules"
          ref="templateForm"
          label-width="120px"
          size="medium"
        >
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="模板类型" prop="templateType">
                <el-select
                  v-model="templateForm.templateType"
                  placeholder="请选择模板类型"
                  clearable
                  @change="handleTemplateTypeChange"
                  style="width: 100%"
                >
                  <el-option label="问卷模板" :value="1" />
                  <el-option label="语音模板" :value="2" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item
                label="选择模板"
                prop="templateId"
                :rules="
                  templateForm.templateType
                    ? [
                        {
                          required: true,
                          message: '请选择模板',
                          trigger: 'change',
                        },
                      ]
                    : []
                "
              >
                <el-select
                  v-model="templateForm.templateId"
                  placeholder="请选择模板"
                  clearable
                  filterable
                  @change="handleTemplateChange"
                  style="width: 100%"
                >
                  <el-option
                    v-for="template in filteredTemplateOptions"
                    :key="template.id"
                    :label="template.templateName"
                    :value="template.id"
                  />
                  <div
                    v-if="templateOptionsLoading"
                    slot="empty"
                    class="select-loading"
                  >
                    <i class="el-icon-loading"></i>
                    <span>加载中...</span>
                  </div>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item>
                <el-button
                  type="primary"
                  icon="el-icon-search"
                  @click="handleLoadTemplate"
                  :loading="templateLoading"
                  :disabled="!templateForm.templateId"
                >
                  åŠ è½½æ¨¡æ¿é¢˜ç›®
                </el-button>
                <el-button icon="el-icon-refresh" @click="handleResetTemplate">
                  é‡ç½®
                </el-button>
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
      </el-card>
    </div>
    <!-- æ¨¡æ¿ä¿¡æ¯ -->
    <div v-if="currentTemplateInfo" class="template-info-section">
      <el-card shadow="never">
        <div class="template-info">
          <div class="info-left">
            <h3 class="template-name">
              {{ currentTemplateInfo.templateName }}
            </h3>
            <div class="template-meta">
              <span class="meta-item">
                <i class="el-icon-s-order"></i>
                æ¨¡æ¿ç±»åž‹ï¼š{{
                  templateForm.templateType == 1 ? "问卷模板" : "语音模板"
                }}
              </span>
              <span class="meta-item">
                <i class="el-icon-s-management"></i>
                é¢˜ç›®æ€»æ•°ï¼š{{ currentTemplateInfo.questionCount || 0 }}
              </span>
            </div>
          </div>
          <div class="info-right">
            <el-tag
              :type="
                currentTemplateInfo.templateStatus == 1 ? 'success' : 'info'
              "
              size="medium"
            >
              {{ currentTemplateInfo.templateStatus == 1 ? "启用" : "停用" }}
            </el-tag>
          </div>
        </div>
      </el-card>
    </div>
    <!-- æœç´¢åŒºåŸŸï¼ˆé¢˜ç›®ç­›é€‰ï¼‰ -->
    <div v-show="questionList.length > 0" class="search-section">
      <el-card shadow="never" class="search-container">
        <el-form :model="queryParams" size="medium">
          <el-form-item label="问题主题">
            <el-input
              v-model="queryParams.scriptTopic"
              placeholder="请输入问题主题"
              @keyup.enter.native="handleQuery"
            />
          </el-form-item>
          <el-form-item label="问题内容">
            <el-input
              v-model="queryParams.scriptContent"
              placeholder="请输入问题内容"
              @keyup.enter.native="handleQuery"
            />
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              icon="el-icon-search"
              @click="handleQuery"
            >
              ç­›é€‰é¢˜ç›®
            </el-button>
            <el-button icon="el-icon-refresh" @click="resetQuery">
              é‡ç½®ç­›é€‰
            </el-button>
          </el-form-item>
        </el-form>
      </el-card>
    </div>
    <!-- é…ç½®åˆ—表 -->
    <div class="config-content">
      <!-- æ‰¹é‡æ“ä½œæ  -->
      <div v-if="questionList.length > 0" class="batch-actions-card">
        <el-card shadow="never">
          <div class="batch-actions">
            <el-button
              type="success"
              icon="el-icon-check"
              :loading="batchSaving"
              :disabled="!hasChanges || batchSaving"
              @click="handleBatchSave"
              size="medium"
            >
              {{ batchSaving ? "批量保存中..." : "批量保存配置" }}
            </el-button>
            <span v-if="changedCount > 0" class="change-count">
              æœ‰ {{ changedCount }} é¡¹é…ç½®éœ€è¦ä¿å­˜
            </span>
            <div class="total-count">
              å…± {{ filteredQuestionList.length }} æ¡è®°å½•
            </div>
          </div>
        </el-card>
      </div>
      <div v-if="loading" class="loading-wrapper">
        <div class="loading-spinner">
          <i class="el-icon-loading"></i>
          <span>加载中...</span>
        </div>
      </div>
      <div
        v-else-if="questionList.length == 0 && templateForm.templateId"
        class="empty-wrapper"
      >
        <el-empty description="该模板中暂无题目">
          <p class="empty-tip">
            è¯·é€‰æ‹©å…¶ä»–模板或检查模板中是否包含题目
          </p>
        </el-empty>
      </div>
      <!-- ä¸€è¡Œä¸€è¡Œçš„卡片列表 -->
      <div v-else-if="filteredQuestionList.length > 0" class="question-list">
        <div
          v-for="(question, index) in filteredQuestionList"
          :key="question.id"
          class="question-item"
        >
          <el-card
            shadow="hover"
            class="question-card"
            :class="{ 'has-changes': question.hasChanges }"
          >
            <!-- å¡ç‰‡å¤´éƒ¨ -->
            <div class="card-header">
              <div class="header-left">
                <div class="question-index">
                  <span class="index-number">{{ index + 1 }}</span>
                  <div class="index-line"></div>
                </div>
                <div class="question-basic-info">
                  <div class="question-title-section">
                    <h3 class="question-topic" :title="question.scriptTopic">
                      {{ question.scriptTopic || "无主题" }}
                    </h3>
                    <div class="question-tags">
                      <dict-tag
                        :options="askvaluetype"
                        :value="question.scriptType"
                        size="small"
                      />
                      <el-tag
                        v-if="question.targetname"
                        size="small"
                        type="info"
                      >
                        {{ question.targetname }}
                      </el-tag>
                    </div>
                  </div>
                  <div class="question-content-section">
                    <span class="content-label">题目内容:</span>
                    <span class="content-text">{{
                      question.scriptContent
                    }}</span>
                  </div>
                </div>
              </div>
              <div class="header-right">
                <!-- å¼‚常选项状态 -->
                <div
                  class="option-status"
                  v-if="
                    templateForm.templateType != 3 &&
                    templateForm.templateType != 4
                  "
                >
                  <el-tooltip
                    :content="
                      checkHasAbnormalOptions(question)
                        ? '已有异常选项'
                        : '暂无异常选项'
                    "
                    placement="top"
                  >
                    <el-tag
                      :type="
                        checkHasAbnormalOptions(question) ? 'success' : 'danger'
                      "
                      size="small"
                      class="status-tag"
                    >
                      <i
                        :class="
                          checkHasAbnormalOptions(question)
                            ? 'el-icon-success'
                            : 'el-icon-warning'
                        "
                      ></i>
                      {{
                        checkHasAbnormalOptions(question)
                          ? "异常选项已配置"
                          : "无异常选项"
                      }}
                    </el-tag>
                  </el-tooltip>
                </div>
                <el-button
                  type="text"
                  icon="el-icon-view"
                  @click="previewQuestion(question)"
                  size="small"
                >
                  é¢„览
                </el-button>
                <!-- æ·»åŠ é…ç½®é€‰é¡¹æŒ‰é’® -->
                <el-button
                  v-if="
                    templateForm.templateType != 3 &&
                    templateForm.templateType != 4
                  "
                  type="text"
                  icon="el-icon-setting"
                  @click="openOptionDialog(question)"
                  size="small"
                >
                  é…ç½®é€‰é¡¹
                </el-button>
              </div>
            </div>
            <!-- å¼‚常处理配置 -->
            <div class="config-section">
              <div class="config-title">
                <i class="el-icon-setting"></i>
                <span>异常处理配置</span>
              </div>
              <el-form
                :model="question.exceptionConfig"
                :rules="configRules"
                ref="configForm"
                label-width="100px"
                size="small"
                class="config-form"
              >
                <div class="config-fields">
                  <!-- è´£ä»»ç§‘室(多选) -->
                  <div class="config-field">
                    <el-form-item
                      label="责任科室"
                      prop="responsibilityDept"
                      class="config-item"
                    >
                      <el-select
                        v-model="question.exceptionConfig.responsibilityDept"
                        placeholder="请选择责任科室"
                        filterable
                        clearable
                        multiple
                        style="width: 100%"
                        @change="handleConfigChange(question)"
                      >
                        <el-option
                          v-for="dept in deptOptions"
                          :key="dept.id"
                          :label="dept.label"
                          :value="dept.deptCode"
                        />
                      </el-select>
                      <div class="config-tip">
                        è´Ÿè´£å¤„理该题目反馈的科室,可多选
                      </div>
                    </el-form-item>
                  </div>
                  <div class="config-field">
                    <el-form-item label="责任人工号">
                      <el-input
                        v-model="question.exceptionConfig.dutyDeptPersonCode"
                        placeholder="请输入责任人工号"
                        @input="handleConfigChange(question)"
                      />
                    </el-form-item>
                  </div>
                  <!-- æŠ¥å¤‡ç§‘室(多选) -->
                  <div class="config-field">
                    <el-form-item
                      label="报备科室"
                      prop="reportDept"
                      class="config-item"
                    >
                      <el-select
                        v-model="question.exceptionConfig.reportDept"
                        placeholder="请选择报备科室"
                        filterable
                        clearable
                        multiple
                        style="width: 100%"
                        @change="handleConfigChange(question)"
                      >
                        <el-option
                          v-for="dept in deptOptions"
                          :key="dept.id"
                          :label="dept.label"
                          :value="dept.deptCode"
                        />
                      </el-select>
                      <div class="config-tip">
                        éœ€è¦æŽ¥æ”¶å¼‚常反馈的科室,可多选
                      </div>
                    </el-form-item>
                  </div>
                </div>
                <!-- å½“前配置信息 -->
                <div v-if="question.hasChanges" class="current-config">
                  <div class="config-preview">
                    <div class="preview-item">
                      <span class="preview-label">责任科室:</span>
                      <span class="preview-value">
                        {{
                          getDeptNames(
                            question.exceptionConfig.responsibilityDept || []
                          ).join(", ")
                        }}
                      </span>
                    </div>
                    <div class="preview-item">
                      <span class="preview-label">报备科室:</span>
                      <span class="preview-value">
                        {{
                          getDeptNames(
                            question.exceptionConfig.reportDept || []
                          ).join(", ")
                        }}
                      </span>
                    </div>
                  </div>
                </div>
                <!-- é…ç½®çŠ¶æ€å’Œæ“ä½œæŒ‰é’® -->
                <div class="config-footer">
                  <div v-if="question.saveStatus" class="save-status">
                    <el-alert
                      :type="question.saveStatus.type"
                      :title="question.saveStatus.message"
                      :closable="false"
                      show-icon
                      :effect="
                        question.saveStatus.type == 'success' ? 'dark' : 'light'
                      "
                      size="small"
                    />
                  </div>
                  <div class="config-actions">
                    <el-button
                      type="primary"
                      :loading="question.saving"
                      :disabled="!question.hasChanges"
                      @click="saveSingleConfig(question)"
                      size="small"
                      icon="el-icon-check"
                    >
                      {{ question.saving ? "保存中..." : "保存配置" }}
                    </el-button>
                    <el-button
                      v-if="question.hasChanges"
                      type="text"
                      @click="resetSingleConfig(question)"
                      size="small"
                    >
                      é‡ç½®
                    </el-button>
                  </div>
                </div>
              </el-form>
            </div>
          </el-card>
        </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>
        <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>
                <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="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
        type="primary"
        icon="el-icon-plus"
        @click="addNewOption"
        size="small"
        plain
        style="width: 100%; margin-top: 10px;"
      >
        æ·»åР选项
      </el-button> -->
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="optionDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="saveOptions"> ä¿å­˜é…ç½® </el-button>
      </span>
    </el-dialog>
    <!-- é¢˜ç›®é¢„览对话框 -->
    <el-dialog
      title="题目预览"
      :visible.sync="previewVisible"
      width="600px"
      center
    >
      <div v-if="currentPreview" class="preview-wrapper">
        <div class="preview-header">
          <h4>{{ currentPreview.scriptTopic || "无主题" }}</h4>
          <div class="preview-tags">
            <dict-tag
              :options="askvaluetype"
              :value="currentPreview.scriptType"
              size="small"
            />
            <el-tag v-if="currentPreview.targetname" size="small" type="info">
              {{ currentPreview.targetname }}
            </el-tag>
          </div>
        </div>
        <!-- æ¨¡æ¿é¢˜ç›®å±•示 -->
        <div class="preview-content" v-if="templateForm.templateType == 1">
          <p class="preview-question">{{ currentPreview.scriptContent }}</p>
          <div
            v-if="
              currentPreview.scriptType != 3 && currentPreview.scriptType != 4
            "
            class="preview-options"
          >
            <el-radio-group v-model="previewAnswer">
              <el-radio
                v-for="(
                  option, idx
                ) in currentPreview.svyLibTemplateTargetoptions || []"
                :key="idx"
                :label="option.optioncontent"
                class="option-item"
              >
                {{ option.optioncontent }}
              </el-radio>
            </el-radio-group>
          </div>
          <div v-else class="preview-textarea">
            <el-input
              type="textarea"
              placeholder="请输入回答"
              v-model="previewAnswer"
              :rows="4"
            />
          </div>
        </div>
        <!-- è¯­éŸ³é¢˜ç›®å±•示 -->
        <div class="preview-content" v-else>
          <p class="preview-question">{{ currentPreview.scriptContent }}</p>
          <div
            v-if="
              currentPreview.scriptType != 3 && currentPreview.scriptType != 4
            "
            class="preview-options"
          >
            <el-radio-group v-model="previewAnswer">
              <el-radio
                v-for="(
                  option, idx
                ) in currentPreview.ivrLibaScriptTargetoptionList || []"
                :key="idx"
                :label="option.targetvalue"
                class="option-item"
              >
                {{ option.targetvalue }}
              </el-radio>
            </el-radio-group>
          </div>
          <div v-else class="preview-textarea">
            <el-input
              type="textarea"
              placeholder="请输入回答"
              v-model="previewAnswer"
              :rows="4"
            />
          </div>
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="previewVisible = false">关闭</el-button>
      </span>
    </el-dialog>
    <!-- ä¿å­˜æˆåŠŸæç¤º -->
    <el-dialog
      title="保存成功"
      :visible.sync="saveSuccessVisible"
      width="400px"
      center
    >
      <div class="success-content">
        <i class="el-icon-success success-icon"></i>
        <p class="success-text">配置已成功保存!</p>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" @click="saveSuccessVisible = false"
          >确定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>
<script>
import {
  compileQtemplate,
  compileFollowup,
  getQtemplatelist,
  getFollowuplist,
  getvFollowup,
  getQtemplateobj,
} from "@/api/AiCentre/index";
import { deptTreeSelect } from "@/api/system/user";
import store from "@/store";
import Pagination from "@/components/Pagination";
export default {
  name: "QuestionExceptionConfig",
  components: { Pagination },
  data() {
    return {
      // æ¨¡æ¿è¡¨å•
      templateForm: {
        templateType: "",
        templateId: "",
      },
      templateRules: {
        templateType: [
          { required: true, message: "请选择模板类型", trigger: "change" },
        ],
      },
      // é€‰é¡¹ç®¡ç†ç›¸å…³
      optionDialogVisible: false,
      currentOptions: [],
      editingQuestion: null,
      optionRules: {
        targetvalue: [
          { required: true, message: "请输入选项内容", trigger: "blur" },
        ],
        isabnormal: [
          { required: true, message: "请选择异常状态", trigger: "change" },
        ],
      },
      // å¼‚常状态选项
      abnormalOptions: [
        { label: "正常", value: 0, type: "success" },
        { label: "异常", value: 1, type: "danger" },
        { label: "警告", value: 2, type: "warning" },
      ],
      // æ¨¡æ¿é€‰é¡¹
      questionnaireTemplates: [], // é—®å·æ¨¡æ¿åˆ—表
      followupTemplates: [], // è¯­éŸ³æ¨¡æ¿åˆ—表
      templateOptionsLoading: false,
      // å½“前模板信息
      currentTemplateInfo: null,
      templateLoading: false,
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        scriptTopic: null,
        scriptContent: null,
      },
      // æ•°æ®åˆ—表
      questionList: [],
      loading: false,
      batchSaving: false,
      // å­—典数据
      askvaluetype: store.getters.askvaluetype || [],
      qyoptions: store.getters.usable || [],
      // ç§‘室选项
      deptOptions: [],
      // é¢„览相关
      previewVisible: false,
      currentPreview: null,
      previewAnswer: "",
      // ä¿å­˜ç›¸å…³
      saveSuccessVisible: false,
      hasChanges: false,
      changedCount: 0,
      // è¡¨å•验证规则
      configRules: {
        responsibilityDept: [
          {
            required: true,
            message: "请至少选择一个责任科室",
            trigger: "change",
          },
          {
            validator: (rule, value, callback) => {
              if (!value || value.length == 0) {
                callback(new Error("请至少选择一个责任科室"));
              } else {
                callback();
              }
            },
            trigger: "change",
          },
        ],
        reportDept: [
          {
            required: true,
            message: "请至少选择一个报备科室",
            trigger: "change",
          },
          {
            validator: (rule, value, callback) => {
              if (!value || value.length == 0) {
                callback(new Error("请至少选择一个报备科室"));
              } else {
                callback();
              }
            },
            trigger: "change",
          },
        ],
      },
    };
  },
  computed: {
    // æ ¹æ®æ¨¡æ¿ç±»åž‹è¿‡æ»¤æ¨¡æ¿é€‰é¡¹
    filteredTemplateOptions() {
      if (this.templateForm.templateType == 1) {
        return this.questionnaireTemplates;
      } else if (this.templateForm.templateType == 2) {
        return this.followupTemplates;
      }
      return [];
    },
    // ç­›é€‰åŽçš„题目列表
    filteredQuestionList() {
      let filtered = this.questionList;
      // åº”用搜索条件
      if (this.queryParams.scriptTopic) {
        const keyword = this.queryParams.scriptTopic.toLowerCase();
        filtered = filtered.filter(
          (q) => q.scriptTopic && q.scriptTopic.toLowerCase().includes(keyword)
        );
      }
      if (this.queryParams.scriptContent) {
        const keyword = this.queryParams.scriptContent.toLowerCase();
        filtered = filtered.filter(
          (q) =>
            q.scriptContent && q.scriptContent.toLowerCase().includes(keyword)
        );
      }
      return filtered;
    },
  },
  created() {
    this.getDeptOptions();
    this.loadAllTemplates();
  },
  methods: {
    /** åŠ è½½æ‰€æœ‰æ¨¡æ¿åˆ—è¡¨ */
    loadAllTemplates() {
      this.templateOptionsLoading = true;
      // å¹¶è¡ŒåŠ è½½é—®å·æ¨¡æ¿å’Œè¯­éŸ³æ¨¡æ¿
      Promise.all([
        this.loadQuestionnaireTemplates(),
        this.loadFollowupTemplates(),
      ]).finally(() => {
        this.templateOptionsLoading = false;
      });
    },
    /** æŸ¥è¯¢ç§‘室列表 */
    getDeptOptions() {
      deptTreeSelect()
        .then((res) => {
          if (res.code == 200) {
            this.deptOptions = 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;
    },
    /** æ ¹æ®ç§‘室编码获取科室名称 */
    getDeptName(deptCode) {
      if (!deptCode) return "";
      const dept = this.deptOptions.find((d) => d.deptCode == deptCode);
      return dept ? dept.label : deptCode;
    },
    /** æ ¹æ®ç§‘室编码数组获取科室名称数组 */
    getDeptNames(deptCodes) {
      if (!Array.isArray(deptCodes) || deptCodes.length == 0) return [];
      return deptCodes
        .map((code) => this.getDeptName(code))
        .filter((name) => name && name.trim());
    },
    /** æ¨¡æ¿ç±»åž‹å˜æ›´ */
    handleTemplateTypeChange() {
      this.templateForm.templateId = "";
      this.currentTemplateInfo = null;
      this.questionList = [];
    },
    /** åŠ è½½é—®å·æ¨¡æ¿åˆ—è¡¨ */
    loadQuestionnaireTemplates() {
      return new Promise((resolve) => {
        getQtemplatelist({ pageSize: 1000 })
          .then((res) => {
            console.log(res.rows, "res.rows");
            if (res.code == 200) {
              console.log(res.rows, 2);
              this.questionnaireTemplates = (res.rows || []).map((item) => ({
                id: item.svyid,
                templateName: item.svyname,
                isavailable: item.isavailable,
              }));
              console.log(this.followupTemplates, 3);
            } else {
              this.$message.error(res.msg || "加载问卷模板失败");
            }
            resolve();
          })
          .catch((error) => {
            console.error("加载问卷模板失败:", error);
            this.$message.error("加载问卷模板失败");
            resolve();
          });
      });
    },
    /** åŠ è½½è¯­éŸ³æ¨¡æ¿åˆ—è¡¨ */
    loadFollowupTemplates() {
      return new Promise((resolve) => {
        getFollowuplist({ pageSize: 1000 })
          .then((res) => {
            console.log(res.rows, "res.rows");
            if (res.code == 200) {
              console.log(res.rows, 2);
              this.followupTemplates = (res.rows || []).map((item) => ({
                id: item.id,
                templateName: item.templateName,
                isavailable: item.isavailable,
              }));
              console.log(this.followupTemplates, 3);
            } else {
              this.$message.error(res.msg || "加载语音模板失败");
            }
            resolve();
          })
          .catch((error) => {
            console.error("加载语音模板失败:", error);
            this.$message.error("加载语音模板失败");
            resolve();
          });
      });
    },
    /** æ¨¡æ¿é€‰æ‹©å˜æ›´ */
    handleTemplateChange(templateId) {
      if (templateId) {
        const selectedTemplate = this.filteredTemplateOptions.find(
          (t) => t.id == templateId
        );
        if (selectedTemplate) {
          this.currentTemplateInfo = {
            templateName: selectedTemplate.templateName,
            templateStatus: selectedTemplate.isavailable,
            questionCount: 0,
          };
        }
        // åŠ è½½æ¨¡æ¿è¯¦æƒ…æ•°æ®
        this.templateLoading = true;
        this.loading = true;
        this.questionList = [];
        if (this.templateForm.templateType == 1) {
          this.loadQuestionnaireTemplateDetail();
        } else if (this.templateForm.templateType == 2) {
          this.loadFollowupTemplateDetail();
        }
      } else {
        this.currentTemplateInfo = null;
        this.questionList = [];
      }
    },
    /** åŠ è½½æ¨¡æ¿è¯¦æƒ…å’Œé¢˜ç›® */
    handleLoadTemplate() {
      this.$refs.templateForm.validate((valid) => {
        if (!valid) {
          this.$message.warning("请先选择模板");
          return;
        }
        this.templateLoading = true;
        this.loading = true;
        this.questionList = [];
        if (this.templateForm.templateType == 1) {
          this.loadQuestionnaireTemplateDetail();
        } else if (this.templateForm.templateType == 2) {
          this.loadFollowupTemplateDetail();
        }
      });
    },
    /** åŠ è½½é—®å·æ¨¡æ¿è¯¦æƒ… */
    loadQuestionnaireTemplateDetail() {
      getQtemplateobj({ svyid: this.templateForm.templateId })
        .then((res) => {
          this.templateLoading = false;
          this.loading = false;
          if (res.code == 200 && res.rows && res.rows.length > 0) {
            const templateDetail = res.rows[0];
            // æ›´æ–°æ¨¡æ¿ä¿¡æ¯
            this.currentTemplateInfo = {
              ...templateDetail,
              templateName: templateDetail.svyname,
              templateStatus: templateDetail.isavailable,
              questionCount: templateDetail.svyTemplateLibScripts?.length || 0,
            };
            // æå–题目列表
            const questions = templateDetail.svyTemplateLibScripts || [];
            this.processQuestions(questions);
            this.$message.success(`成功加载 ${questions.length} ä¸ªé¢˜ç›®`);
          } else {
            this.$message.error(res.msg || "加载模板详情失败");
          }
        })
        .catch((error) => {
          this.templateLoading = false;
          this.loading = false;
          console.error("加载问卷模板详情失败:", error);
          this.$message.error("加载模板详情失败");
        });
    },
    /** åŠ è½½è¯­éŸ³æ¨¡æ¿è¯¦æƒ… */
    loadFollowupTemplateDetail() {
      getvFollowup({ id: this.templateForm.templateId })
        .then((res) => {
          this.templateLoading = false;
          this.loading = false;
          if (res.code == 200) {
            const templateDetail = res.data;
            // æ›´æ–°æ¨¡æ¿ä¿¡æ¯
            this.currentTemplateInfo = {
              ...this.currentTemplateInfo,
              templateName: templateDetail.templateName,
              templateStatus: templateDetail.isavailable,
              questionCount:
                templateDetail.ivrLibaTemplateScriptVOList?.length || 0,
            };
            // æå–题目列表
            const questions = templateDetail.ivrLibaTemplateScriptVOList || [];
            this.processQuestions(questions);
            this.$message.success(`成功加载 ${questions.length} ä¸ªé¢˜ç›®`);
          } else {
            this.$message.error(res.msg || "加载模板详情失败");
          }
        })
        .catch((error) => {
          this.templateLoading = false;
          this.loading = false;
          console.error("加载语音模板详情失败:", error);
          this.$message.error("加载模板详情失败");
        });
    },
    /** å¤„理题目数据 */
    processQuestions(questions) {
      this.questionList = questions.map((question) => {
        // è§£æžè´£ä»»ç§‘室和报备科室
        let exceptionConfig = {
          responsibilityDept: [], // è´£ä»»ç§‘室编码数组
          reportDept: [], // æŠ¥å¤‡ç§‘室编码数组
          dutyDeptPersonCode: question.dutyDeptPersonCode || "", // è´£ä»»äººå·¥å·
        };
        // ä»Žé¢˜ç›®é¡¶å±‚字段读取数据
        if (question.dutyDeptCode) {
          // ä»Žé€—号分隔的字符串转为数组
          exceptionConfig.responsibilityDept = question.dutyDeptCode
            .split(",")
            .map((code) => code.trim())
            .filter((code) => code);
        }
        if (question.reportDeptCode) {
          exceptionConfig.reportDept = question.reportDeptCode
            .split(",")
            .map((code) => code.trim())
            .filter((code) => code);
        }
        return {
          ...question,
          // ç»Ÿä¸€å­—段名
          id: question.id || question.scriptId,
          scriptTopic: question.scriptTopic || question.scriptTopic,
          scriptContent: question.scriptContent || question.scriptContent,
          scriptType: question.scriptType,
          isavailable: question.isavailable,
          targetname: question.targetname,
          categoryid: question.categoryid || question.categoryid,
          originalConfig: JSON.parse(JSON.stringify(exceptionConfig)),
          exceptionConfig: exceptionConfig,
          hasChanges: false,
          saving: false,
          saveStatus: null,
        };
      });
      this.updateChangedStatus();
    },
    /** é‡ç½®æ¨¡æ¿é€‰æ‹© */
    handleResetTemplate() {
      this.templateForm = {
        templateType: "",
        templateId: "",
      };
      this.currentTemplateInfo = null;
      this.questionList = [];
      this.resetQuery();
      this.$refs.templateForm?.clearValidate();
    },
    /** é…ç½®å˜æ›´å¤„理 */
    handleConfigChange(question) {
      // ç›´æŽ¥å¯¹æ¯”配置是否变化,不依赖表单校验(避免只改一个字段时校验不通过导致 hasChanges æ— æ³•更新)
      question.hasChanges = !this.isConfigEqual(
        question.exceptionConfig,
        question.originalConfig
      );
      this.updateChangedStatus();
    },
    /** æ¯”较配置是否改变 */
    isConfigEqual(config1, config2) {
      if (!config1 || !config2) return false;
      const responsibility1 = [...(config1.responsibilityDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const responsibility2 = [...(config2.responsibilityDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const report1 = [...(config1.reportDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const report2 = [...(config2.reportDept || [])]
        .sort()
        .join(",")
        .toLowerCase();
      const person1 = (config1.dutyDeptPersonCode || "").trim();
      const person2 = (config2.dutyDeptPersonCode || "").trim();
      return responsibility1 == responsibility2
        && report1 == report2
        && person1 == person2;
    },
    /** æ›´æ–°å˜æ›´çŠ¶æ€ */
    updateChangedStatus() {
      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) {
        this.$message.info("当前配置无变化");
        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;
      // éªŒè¯è¡¨å•
      try {
        await formRef.validate();
      } catch (error) {
        this.$message.warning("请先完成必填项");
        return;
      }
      question.saving = true;
      question.saveStatus = null;
      try {
        // èŽ·å–å½“å‰æ¨¡æ¿è¯¦æƒ…
        let templateDetail;
        if (this.templateForm.templateType == 1) {
          // é—®å·æ¨¡æ¿
          const res = await getQtemplateobj({
            svyid: this.templateForm.templateId,
          });
          if (res.code !== 200 || !res.rows || res.rows.length == 0) {
            throw new Error(res.msg || "获取模板详情失败");
          }
          templateDetail = res.rows[0];
        } else if (this.templateForm.templateType == 2) {
          // è¯­éŸ³æ¨¡æ¿
          const res = await getvFollowup({ id: this.templateForm.templateId });
          if (res.code !== 200) {
            throw new Error(res.msg || "获取模板详情失败");
          }
          templateDetail = res.data;
        }
        // æ›´æ–°é¢˜ç›®é…ç½®
        let updatedTemplateDetail = { ...templateDetail };
        let questionsField =
          this.templateForm.templateType == 1
            ? "svyTemplateLibScripts"
            : "ivrLibaTemplateScriptVOList";
        const questions = updatedTemplateDetail[questionsField] || [];
        const questionIndex = questions.findIndex((q) => q.id == question.id);
        if (questionIndex == -1) {
          throw new Error("未找到题目");
        }
        // èŽ·å–ç§‘å®¤åç§°
        const responsibilityDeptNames = this.getDeptNames(
          question.exceptionConfig.responsibilityDept
        );
        const reportDeptNames = this.getDeptNames(
          question.exceptionConfig.reportDept
        );
        // ç›´æŽ¥æ›´æ–°é¢˜ç›®é¡¶å±‚字段
        questions[questionIndex] = {
          ...questions[questionIndex],
          // è®¾ç½®Excel要求的字段
          dutyDeptCode: question.exceptionConfig.responsibilityDept.join(","),
          dutyDeptName: responsibilityDeptNames.join(","),
          reportDeptCode: question.exceptionConfig.reportDept.join(","),
          reportDeptName: reportDeptNames.join(","),
          dutyDeptPersonCode: question.exceptionConfig.dutyDeptPersonCode || "",
        };
        // å¦‚果需要,也更新选项数据
        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;
        // ä¿å­˜æ¨¡æ¿
        let response;
        if (this.templateForm.templateType == 1) {
          response = await compileQtemplate({
            ...updatedTemplateDetail,
            id: this.templateForm.templateId,
            isoperation: 2,
          });
        } else {
          response = await compileFollowup({
            ...updatedTemplateDetail,
            id: this.templateForm.templateId,
            isoperation: 2,
          });
        }
        if (response.code == 200) {
          this.handleSaveSuccess(question);
        } else {
          throw new Error(response.msg || "保存失败");
        }
      } catch (error) {
        console.error("保存失败:", error);
        question.saveStatus = {
          type: "error",
          message: error.message || "保存失败,请稍后重试",
        };
        this.$message.error(error.message || "保存失败,请稍后重试");
      } finally {
        question.saving = false;
      }
    },
    /** å¤„理保存成功 */
    handleSaveSuccess(question) {
      // åŒæ—¶æ›´æ–°é¢˜ç›®é¡¶å±‚字段
      const responsibilityDeptNames = this.getDeptNames(
        question.exceptionConfig.responsibilityDept
      );
      const reportDeptNames = this.getDeptNames(
        question.exceptionConfig.reportDept
      );
      // æ›´æ–°é¢˜ç›®æœ¬èº«çš„字段
      question.dutyDeptCode =
        question.exceptionConfig.responsibilityDept.join(",");
      question.dutyDeptName = responsibilityDeptNames.join(",");
      question.reportDeptCode = question.exceptionConfig.reportDept.join(",");
      question.reportDeptName = reportDeptNames.join(",");
      question.dutyDeptPersonCode = question.exceptionConfig.dutyDeptPersonCode || "";
      // æ›´æ–°åŽŸå§‹é…ç½®
      question.originalConfig = JSON.parse(
        JSON.stringify(question.exceptionConfig)
      );
      question.hasChanges = false;
      question.saveStatus = {
        type: "success",
        message: "配置保存成功",
      };
      this.updateChangedStatus();
      this.$message.success("配置保存成功");
      // 5秒后清除成功提示
      setTimeout(() => {
        question.saveStatus = null;
      }, 5000);
    },
    /** é‡ç½®å•个题目配置 */
    resetSingleConfig(question) {
      this.$confirm("确定要重置当前题目的配置吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          question.exceptionConfig = JSON.parse(
            JSON.stringify(question.originalConfig)
          );
          // åŒæ—¶é‡ç½®é¢˜ç›®é¡¶å±‚字段
          const responsibilityDeptNames = this.getDeptNames(
            question.exceptionConfig.responsibilityDept
          );
          const reportDeptNames = this.getDeptNames(
            question.exceptionConfig.reportDept
          );
          question.dutyDeptCode =
            question.exceptionConfig.responsibilityDept.join(",");
          question.dutyDeptName = responsibilityDeptNames.join(",");
          question.reportDeptCode =
            question.exceptionConfig.reportDept.join(",");
          question.reportDeptName = reportDeptNames.join(",");
          question.hasChanges = false;
          question.saveStatus = null;
          this.updateChangedStatus();
          this.$message.success("配置已重置");
        })
        .catch(() => {});
    },
    /** æ‰¹é‡ä¿å­˜é…ç½® */
    async handleBatchSave() {
      if (!this.hasChanges || this.batchSaving) return;
      // èŽ·å–æœ‰å˜æ›´çš„é¢˜ç›®
      const changedQuestions = this.questionList.filter((q) => q.hasChanges);
      if (changedQuestions.length == 0) {
        this.$message.info("没有需要保存的配置变更");
        return;
      }
      this.$confirm("确定要保存所有修改过的配置吗?", "批量保存", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(async () => {
          this.batchSaving = true;
          const results = [];
          for (const question of changedQuestions) {
            try {
              await this.saveSingleConfig(question);
              results.push({
                id: question.id,
                success: !question.hasChanges,
              });
            } catch (error) {
              results.push({
                id: question.id,
                success: false,
                error: error.message,
              });
            }
          }
          this.batchSaving = false;
          const successCount = results.filter((r) => r.success).length;
          const failCount = results.length - successCount;
          if (failCount == 0) {
            this.saveSuccessVisible = true;
            this.$message.success(`成功保存 ${successCount} ä¸ªé…ç½®`);
          } else {
            this.$message.warning(
              `成功保存 ${successCount} ä¸ªï¼Œå¤±è´¥ ${failCount} ä¸ª`
            );
            // å¯ä»¥æ˜¾ç¤ºå…·ä½“哪些失败了
            const failedQuestions = results
              .filter((r) => !r.success)
              .map((r) => r.id);
            console.error("保存失败的题目ID:", failedQuestions);
          }
        })
        .catch(() => {
          this.batchSaving = false;
        });
    },
    /** é¢„览题目 */
    previewQuestion(question) {
      this.currentPreview = { ...question };
      this.previewAnswer = "";
      this.previewVisible = true;
    },
    /** ä¿®æ”¹é€‰é¡¹ç®¡ç†å¯¹è¯æ¡†çš„æ‰“开方法,保存原始选项 */
    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, index) => ({
          ...opt,
          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, index) => ({
          ...opt,
          id: opt.id || `temp_${Date.now()}_${index}`,
          targetvalue: opt.targetvalue || "",
          isabnormal: opt.isabnormal || 0,
        }));
      }
      this.optionDialogVisible = true;
    },
    /** æ·»åŠ æ–°é€‰é¡¹ */
    addNewOption() {
      this.currentOptions.push({
        id: `temp_${Date.now()}_${this.currentOptions.length}`,
        targetvalue: "",
        isabnormal: 0,
        isNew: true,
      });
    },
    /** åˆ é™¤é€‰é¡¹ */
    removeOption(index) {
      this.$confirm("确定要删除这个选项吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.currentOptions.splice(index, 1);
        })
        .catch(() => {});
    },
    /** ä¿å­˜é€‰é¡¹é…ç½® */
    async saveOptions() {
      try {
        // éªŒè¯å¿…填项
        for (let i = 0; i < this.currentOptions.length; i++) {
          const option = this.currentOptions[i];
          if (!option.targetvalue || option.targetvalue.trim() == "") {
            this.$message.warning(`第 ${i + 1} ä¸ªé€‰é¡¹å†…容不能为空`);
            return;
          }
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰å¼‚常选项
        const hasAbnormal = this.currentOptions.some(
          (opt) => opt.isabnormal == 1
        );
        if (!hasAbnormal) {
          this.$message.warning("请至少设置一个异常选项(isabnormal=1)");
          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 =
            this.currentOptions.map((opt) => ({
              ...opt,
              optioncontent: opt.targetvalue,
              isabnormal: opt.isabnormal,
              // æ¸…除临时字段
              targetvalue: undefined,
              isNew: undefined,
            }));
        } else if (this.templateForm.templateType == 2) {
          this.editingQuestion.ivrLibaScriptTargetoptionList =
            this.currentOptions.map((opt) => ({
              ...opt,
              // æ¸…除临时字段
              isNew: undefined,
            }));
        }
        // å¦‚果选项有变化,则设置题目为有变更状态
        if (isOptionsChanged) {
          this.editingQuestion.hasChanges = true;
          this.updateChangedStatus();
        }
        this.$message.success("选项配置保存成功");
        this.optionDialogVisible = false;
      } catch (error) {
        console.error("保存选项失败:", error);
        this.$message.error("保存选项失败");
      }
    },
    /** æ£€æŸ¥é€‰é¡¹æ˜¯å¦å‘生变化 */
    checkOptionsChanged(originalOptions, newOptions, templateType) {
      // å¦‚果数量不同,则一定变化了
      if (originalOptions.length !== newOptions.length) {
        return true;
      }
      // æ¯”较每个选项的内容和异常状态
      for (let i = 0; i < originalOptions.length; i++) {
        const original = originalOptions[i];
        const current = newOptions[i];
        if (templateType == "questionnaire") {
          // é—®å·æ¨¡æ¿æ¯”较
          if (
            original.optioncontent !== current.targetvalue ||
            original.isabnormal !== current.isabnormal
          ) {
            return true;
          }
        } else if (templateType == "voice") {
          // è¯­éŸ³æ¨¡æ¿æ¯”较
          if (
            original.targetvalue !== current.targetvalue ||
            original.isabnormal !== current.isabnormal
          ) {
            return true;
          }
        }
      }
      return false;
    },
    /** èŽ·å–å¼‚å¸¸é€‰é¡¹ç»Ÿè®¡ */
    getAbnormalStats(question) {
      if (this.templateForm.templateType == 1) {
        const options = question.svyLibTemplateTargetoptions || [];
        return {
          total: options.length,
          abnormal: options.filter((opt) => opt.isabnormal == 1).length,
          warning: options.filter((opt) => opt.isabnormal == 2).length,
          normal: options.filter((opt) => opt.isabnormal == 0).length,
        };
      } else if (this.templateForm.templateType == 2) {
        const options = question.ivrLibaScriptTargetoptionList || [];
        return {
          total: options.length,
          abnormal: options.filter((opt) => opt.isabnormal == 1).length,
          warning: options.filter((opt) => opt.isabnormal == 2).length,
          normal: options.filter((opt) => opt.isabnormal == 0).length,
        };
      }
      return { total: 0, abnormal: 0, warning: 0, normal: 0 };
    },
    /** æœç´¢ */
    handleQuery() {
      // ä»…筛选显示,不需要重新加载
    },
    /** é‡ç½®æœç´¢ */
    resetQuery() {
      this.queryParams = {
        scriptTopic: "",
        scriptContent: "",
      };
    },
  },
};
</script>
<style lang="scss" scoped>
.question-exception-config {
  min-height: 100%;
  background-color: #f5f7fa;
  padding: 20px;
  .page-header {
    margin-bottom: 20px;
    padding: 20px;
    background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
    border-radius: 8px;
    color: white;
    .header-content {
      .page-title {
        margin: 0 0 8px 0;
        font-size: 20px;
        font-weight: 600;
      }
      .page-description {
        margin: 0;
        opacity: 0.9;
        font-size: 14px;
      }
    }
  }
  .template-section {
    margin-bottom: 20px;
    .template-header {
      margin-bottom: 20px;
      .template-title {
        margin: 0 0 8px 0;
        font-size: 16px;
        font-weight: 600;
        color: #303133;
      }
      .template-tip {
        margin: 0;
        color: #909399;
        font-size: 13px;
      }
    }
    .select-loading {
      text-align: center;
      padding: 10px;
      color: #909399;
      i {
        margin-right: 8px;
      }
    }
  }
  .template-info-section {
    margin-bottom: 20px;
    .template-info {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 5px 0;
      .info-left {
        .template-name {
          margin: 0 0 10px 0;
          font-size: 18px;
          font-weight: 600;
          color: #303133;
        }
        .template-meta {
          display: flex;
          gap: 20px;
          flex-wrap: wrap;
          .meta-item {
            display: flex;
            align-items: center;
            gap: 5px;
            font-size: 13px;
            color: #606266;
            i {
              font-size: 14px;
            }
          }
        }
      }
    }
  }
  .search-section {
    margin-bottom: 20px;
    .search-container {
      border-radius: 8px;
      .el-form {
        display: flex;
        flex-wrap: wrap;
        gap: 16px;
        align-items: center;
      }
    }
  }
  .config-content {
    .batch-actions-card {
      margin-bottom: 20px;
      .batch-actions {
        display: flex;
        align-items: center;
        gap: 20px;
        padding: 8px 0;
        .change-count {
          color: #e6a23c;
          font-size: 14px;
          font-weight: 500;
        }
        .total-count {
          margin-left: auto;
          color: #909399;
          font-size: 14px;
        }
      }
    }
    .loading-wrapper {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 400px;
      .loading-spinner {
        text-align: center;
        color: #409eff;
        i {
          font-size: 24px;
          margin-right: 8px;
        }
        span {
          font-size: 16px;
        }
      }
    }
    .empty-wrapper {
      min-height: 400px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      .empty-tip {
        margin-top: 10px;
        color: #909399;
        font-size: 13px;
        text-align: center;
      }
    }
    .question-list {
      display: flex;
      flex-direction: column;
      gap: 16px;
    }
    .question-item {
      .question-card {
        border-radius: 8px;
        border: 1px solid #ebeef5;
        transition: all 0.3s ease;
        &.has-changes {
          border-color: #409eff;
          box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.1);
        }
        &:hover {
          box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
        }
        .card-header {
          display: flex;
          justify-content: space-between;
          align-items: flex-start;
          margin-bottom: 20px;
          padding-bottom: 20px;
          border-bottom: 1px solid #f0f0f0;
          .header-left {
            display: flex;
            gap: 20px;
            flex: 1;
            .question-index {
              display: flex;
              flex-direction: column;
              align-items: center;
              min-width: 40px;
              .index-number {
                display: flex;
                align-items: center;
                justify-content: center;
                width: 32px;
                height: 32px;
                background: #409eff;
                color: white;
                border-radius: 50%;
                font-size: 14px;
                font-weight: 600;
                margin-bottom: 8px;
              }
              .index-line {
                width: 2px;
                height: 100%;
                background: #e0e0e0;
                border-radius: 1px;
              }
            }
            .question-basic-info {
              flex: 1;
              .question-title-section {
                margin-bottom: 12px;
                .question-topic {
                  margin: 0 0 8px 0;
                  font-size: 16px;
                  font-weight: 600;
                  color: #303133;
                  line-height: 1.4;
                }
                .question-tags {
                  display: flex;
                  gap: 8px;
                  flex-wrap: wrap;
                }
              }
              .question-content-section {
                display: flex;
                align-items: flex-start;
                gap: 8px;
                .content-label {
                  color: #606266;
                  font-size: 13px;
                  font-weight: 500;
                  min-width: 80px;
                }
                .content-text {
                  color: #303133;
                  font-size: 13px;
                  line-height: 1.6;
                  flex: 1;
                }
              }
            }
          }
          .header-right {
            display: flex;
            flex-direction: column;
            align-items: flex-end;
            gap: 8px;
            .option-status {
              .status-tag {
                cursor: default;
                i {
                  margin-right: 4px;
                }
              }
            }
          }
        }
        .config-section {
          .config-title {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 20px;
            padding: 8px 12px;
            background: #f8f9fa;
            border-radius: 4px;
            i {
              color: #409eff;
              font-size: 16px;
            }
            span {
              color: #303133;
              font-weight: 600;
              font-size: 14px;
            }
          }
          .config-form {
            .config-fields {
              display: grid;
              grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
              gap: 20px;
              margin-bottom: 20px;
              .config-field {
                .config-item {
                  margin-bottom: 0;
                  :deep(.el-form-item__label) {
                    font-weight: 500;
                    color: #606266;
                    padding-right: 12px;
                  }
                  .config-tip {
                    font-size: 12px;
                    color: #909399;
                    margin-top: 4px;
                    line-height: 1.4;
                  }
                }
              }
            }
            .current-config {
              margin-bottom: 20px;
              padding: 15px;
              background: #f0f9ff;
              border-radius: 6px;
              border: 1px solid #d0ebff;
              .config-preview {
                .preview-item {
                  display: flex;
                  align-items: flex-start;
                  margin-bottom: 8px;
                  &:last-child {
                    margin-bottom: 0;
                  }
                  .preview-label {
                    font-size: 13px;
                    color: #606266;
                    font-weight: 500;
                    min-width: 80px;
                  }
                  .preview-value {
                    font-size: 13px;
                    color: #303133;
                    line-height: 1.5;
                    flex: 1;
                  }
                }
              }
            }
            .config-footer {
              display: flex;
              justify-content: space-between;
              align-items: center;
              padding-top: 20px;
              border-top: 1px dashed #dcdfe6;
              .save-status {
                flex: 1;
                margin-right: 20px;
                .el-alert {
                  padding: 8px 16px;
                  border-radius: 4px;
                }
              }
              .config-actions {
                display: flex;
                align-items: center;
                gap: 12px;
                flex-shrink: 0;
                .el-button {
                  min-width: 100px;
                }
              }
            }
          }
        }
      }
    }
  }
  .preview-wrapper {
    .preview-header {
      margin-bottom: 20px;
      h4 {
        margin: 0 0 12px 0;
        color: #303133;
        font-size: 18px;
        font-weight: 600;
      }
      .preview-tags {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
      }
    }
    .preview-content {
      .preview-question {
        margin-bottom: 20px;
        padding: 16px;
        background: #f8f9fa;
        border-radius: 4px;
        color: #606266;
        line-height: 1.6;
      }
      .preview-options {
        .option-item {
          display: block;
          margin-bottom: 12px;
          padding: 12px;
          border-radius: 4px;
          border: 1px solid #ebeef5;
          transition: all 0.3s;
          &:hover {
            background: #f5f7fa;
            border-color: #409eff;
          }
          &:last-child {
            margin-bottom: 0;
          }
        }
      }
      .preview-textarea {
        .el-textarea__inner {
          resize: none;
        }
      }
    }
  }
  .success-content {
    text-align: center;
    padding: 20px 0;
    .success-icon {
      color: #67c23a;
      font-size: 48px;
      margin-bottom: 20px;
    }
    .success-text {
      font-size: 16px;
      color: #606266;
      margin: 0;
    }
  }
}
.option-config-wrapper {
  .dialog-header {
    margin-bottom: 20px;
    padding-bottom: 15px;
    border-bottom: 1px solid #ebeef5;
    h4 {
      margin: 0 0 8px 0;
      color: #303133;
      font-size: 16px;
      font-weight: 600;
    }
    .dialog-subtitle {
      margin: 0;
      color: #606266;
      font-size: 13px;
      line-height: 1.4;
    }
  }
  .option-list {
    .option-item {
      margin-bottom: 12px;
      padding: 12px;
      background: #f8f9fa;
      border-radius: 4px;
      border: 1px solid #ebeef5;
      &:hover {
        border-color: #dcdfe6;
      }
      .option-form {
        .option-index {
          display: flex;
          align-items: center;
          justify-content: center;
          height: 100%;
          color: #909399;
          font-weight: 500;
        }
      }
    }
  }
}
@media (max-width: 768px) {
  .question-exception-config {
    padding: 12px;
    .page-header {
      padding: 16px;
      margin-bottom: 16px;
    }
    .template-info {
      flex-direction: column;
      align-items: flex-start;
      gap: 10px;
    }
    .search-card {
      margin-bottom: 16px;
    }
    .config-content {
      .batch-actions-card {
        margin-bottom: 16px;
      }
      .question-item {
        .question-card {
          .card-header {
            flex-direction: column;
            gap: 12px;
            .header-left {
              flex-direction: column;
              gap: 12px;
              .question-index {
                flex-direction: row;
                align-items: center;
                min-width: auto;
                .index-number {
                  margin-bottom: 0;
                  margin-right: 12px;
                }
                .index-line {
                  width: 100%;
                  height: 2px;
                }
              }
            }
          }
          .config-section {
            .config-form {
              .config-fields {
                grid-template-columns: 1fr;
                gap: 16px;
              }
              .current-config {
                padding: 12px;
              }
              .config-footer {
                flex-direction: column;
                align-items: stretch;
                gap: 12px;
                .save-status {
                  margin-right: 0;
                  margin-bottom: 8px;
                }
              }
            }
          }
        }
      }
    }
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/ChartsPanel.vue
@@ -371,6 +371,11 @@
      if (!den) return '0.00';
      return (num / den).toFixed(2);
    },
    // ç»¼åˆå¾—分展示:直接使用后端 avgScore,格式化为两位小数
    formatScore(val) {
      const n = Number(val);
      return isNaN(n) ? '0.00' : n.toFixed(2);
    },
    // è§£æžæ¯”例字符串:"14.29%"、"0"、"0.5" â†’ æ•°å€¼ï¼ˆç™¾åˆ†æ¯”);无效返回 null
    parsePercent(value) {
      if (value == null || value === '') return null;
@@ -382,7 +387,13 @@
      const arr = Array.isArray(list) ? list : (list && (list.list || list.rows)) || [];
      const options = [];
      let total = 0;
      let avgScore = null;
      arr.forEach((item) => {
        // ç»¼åˆå¾—分:后端返回的题目级 avgScore(与题目同级)
        if (avgScore == null) {
          const rawAvg = Number(item && item.avgScore);
          if (!isNaN(rawAvg)) avgScore = rawAvg;
        }
        const details = Array.isArray(item && item.subtaskDetailRatioExportList)
          ? item.subtaskDetailRatioExportList
          : [];
@@ -395,7 +406,7 @@
          total += count;
        });
      });
      return { options, total };
      return { options, total, avgScore };
    },
    // å•题选项统计(真实接口 /smartor/serviceSubtaskDetail/sltdMydTotalByScore)
    async fetchSingleQuestion() {
@@ -425,7 +436,7 @@
        const res = await sltdMydTotalByScore(payload);
        console.log('[sltdMydTotalByScore å•题] code=', res && res.code, 'data=', JSON.stringify(res && res.data).slice(0, 1500));
        const list = res && res.code === 200 ? res.data : [];
        const { options, total } = this.normalizeSingleQuestion(list);
        const { options, total, avgScore } = this.normalizeSingleQuestion(list);
        this.singleQuestionData = options.map((o) => ({
          option: o.option,
          count: o.count,
@@ -435,7 +446,7 @@
        }));
        this.singleQuestionMeta = {
          total: total.toLocaleString(),
          score: this.scoreOfOptions(options),
          score: this.formatScore(avgScore),
        };
        if (!options.length) this.notify('当前条件下暂无该题的选项统计数据', 'warning');
      } catch (e) {
src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -445,8 +445,13 @@
    const details = pickDetails(item);
    let node = map.get(title);
    if (!node) {
      node = { title, type: "", options: new Map() };
      node = { title, type: "", options: new Map(), avgScore: null };
      map.set(title, node);
    }
    // ç»¼åˆå¾—分:后端返回的题目级 avgScore(与题目同级)
    if (node.avgScore == null) {
      const rawAvg = Number(item && item.avgScore);
      node.avgScore = isNaN(rawAvg) ? null : rawAvg;
    }
    details.forEach((d) => {
@@ -480,7 +485,7 @@
  return Array.from(map.values()).map((n) => {
    const options = Array.from(n.options.values());
    const item = { title: n.title, type: n.type, options };
    const item = { title: n.title, type: n.type, options, avgScore: n.avgScore };
    // å¤šé€‰é¢˜ï¼šç”¨å„选项数量之和作为填报总量(接口未单独提供人次)
    if (n.type === "multiple")
      item.total = options.reduce((s, o) => s + o.count, 0);
@@ -595,7 +600,8 @@
          veryBadRate: this.rateOf(veryBad.count, total),
          naCount: na.count,
          naRate: this.rateOf(na.count, total),
          score: this.scoreOfItems(items),
          total,
          score: this.formatScore(q.avgScore),
        };
      });
@@ -609,6 +615,17 @@
        });
      });
      const total = sum.reduce((s, it) => s + it.count, 0);
      // æ€»è®¡ç»¼åˆå¾—分:按各题填报量加权平均各题 avgScore
      let totalScoreNum = 0;
      let totalScoreDen = 0;
      this.satisfactionQuestions.forEach((q, i) => {
        const avg = Number(q.avgScore);
        const qTotal = rows[i] ? rows[i].total : 0;
        if (!isNaN(avg) && qTotal) {
          totalScoreNum += avg * qTotal;
          totalScoreDen += qTotal;
        }
      });
      rows.push({
        category: "总计",
        veryGoodCount: sum[0].count,
@@ -623,7 +640,7 @@
        veryBadRate: this.rateOf(sum[4].count, total),
        naCount: sum[5].count,
        naRate: this.rateOf(sum[5].count, total),
        score: this.scoreOfItems(sum),
        score: totalScoreDen ? (totalScoreNum / totalScoreDen).toFixed(2) : "0.00",
      });
      return rows;
    },
@@ -772,6 +789,11 @@
      if (!denominator) return "0.00";
      return (numerator / denominator).toFixed(2);
    },
    // ç»¼åˆå¾—分展示:直接使用后端 avgScore,格式化为两位小数
    formatScore(val) {
      const n = Number(val);
      return isNaN(n) ? "0.00" : n.toFixed(2);
    },
    defaultRange() {
      const end = new Date();
      const start = new Date();
src/views/followvisit/discharge/index.vue
@@ -586,6 +586,29 @@
          prop="phone"
        />
        <el-table-column
          v-if="
            orgname == '景宁畲族自治县人民医院' ||
            orgname == '丽水市中医院'
          "
          label="拨号状态"
          width="120"
          align="center"
          key="isManuallyCalled"
          prop="isManuallyCalled"
        >
          <template slot-scope="scope">
            <el-tag
              v-if="Number(scope.row.isManuallyCalled) > 0"
              type="success"
              :disable-transitions="false"
              >拨通{{ scope.row.isManuallyCalled }}次</el-tag
            >
            <el-tag v-else type="info" :disable-transitions="false"
              >未拨通</el-tag
            >
          </template>
        </el-table-column>
        <el-table-column
          label="身份证号码"
          width="200"
          align="center"
src/views/followvisit/record/detailpage/index.vue
@@ -128,6 +128,29 @@
            </template>
          </el-table-column>
          <el-table-column
            v-if="
              orgname == '景宁畲族自治县人民医院' ||
              orgname == '丽水市中医院'
            "
            label="拨号状态"
            width="120"
            align="center"
            key="isManuallyCalled"
            prop="isManuallyCalled"
          >
            <template slot-scope="scope">
              <el-tag
                v-if="Number(scope.row.isManuallyCalled) > 0"
                type="success"
                :disable-transitions="false"
                >拨通{{ scope.row.isManuallyCalled }}次</el-tag
              >
              <el-tag v-else type="info" :disable-transitions="false"
                >未拨通</el-tag
              >
            </template>
          </el-table-column>
          <el-table-column
            prop="finishtime"
            align="center"
            label="随访完成时间"
@@ -282,6 +305,8 @@
            show-overflow-tooltip
          >
          </el-table-column>
          <el-table-column
            prop="bankcardno"
@@ -1312,7 +1337,11 @@
        :visible.sync="CaldialogVisible"
        width="60%"
      >
        <CallCenterLs ref="CallCenterLs" :initial-phone="currentPhoneNumber" />
        <CallCenterLs
          ref="CallCenterLs"
          :initial-phone="currentPhoneNumber"
          @call-connected="recordCallConnected"
        />
      </el-dialog>
    </div>
  </div>
@@ -2349,7 +2378,7 @@
        this.CaldialogVisible = true;
        return;
      }
      // ä¸‹æ–¹æ˜¯ä¸­åŒ»é™¢å‘¼å«
      // ä¸‹æ–¹æ˜¯ä¸½æ°´å¸‚中医院
      if (this.isSipRegistering) {
        this.$message.warning("系统正在初始化,请稍候...");
        return;
@@ -2379,11 +2408,15 @@
      this.callStatus = status.type;
      if (status.type === "connected") {
        if (!this.currentCall) {
        this.currentCall = {
          phone: this.currentPhoneNumber,
          type: this.callType,
          startTime: new Date(),
        };
          // ä¸½æ°´å¸‚中医院:电话接通后记录拨打成功次数
          this.recordCallConnected();
        }
      } else if (status.type === "ended" || status.type === "failed") {
        this.currentCall = null;
      }
@@ -2393,6 +2426,39 @@
        this.$message.error(`呼叫失败: ${status.text}`);
      }
    },
    // ç”µè¯æ‹¨æ‰“成功后(接通)次数 +1,并通过 Editsingletaskson æ›´æ–°æœåŠ¡æ•°æ®
    // ä»…限景宁畲族自治县人民医院与丽水市中医院
    recordCallConnected() {
      const allowedOrgs = ["景宁畲族自治县人民医院", "丽水市中医院"];
      if (!allowedOrgs.includes(this.orgname)) return;
      getTaskservelist({
        patid: this.patid,
        subId: this.id,
      }).then((res) => {
        if (res.code == 200) {
          const objson = res.rows[0].serviceSubtaskList.find(
            (item) => item.id == this.id
          );
          if (!objson) return;
          objson.isManuallyCalled = (Number(objson.isManuallyCalled) || 0) + 1;
          Editsingletaskson(objson).then((r) => {
            if (r.code == 200) {
              // åŠæ—¶æ›´æ–°é¡¶éƒ¨æœåŠ¡åˆ—è¡¨ä¸Žè¡¨å•ä¸­çš„æ‹¨å·çŠ¶æ€
              const localRow = (this.logsheetlist || []).find(
                (item) => item.id == this.id
              );
              if (localRow) {
                localRow.isManuallyCalled = objson.isManuallyCalled;
              }
              if (this.form && this.form.id == this.id) {
                this.form.isManuallyCalled = objson.isManuallyCalled;
              }
            }
          });
        }
      });
    },
    // ç»“束当前通话
    endCurrentCall() {
      if (!this.currentCall) return;
src/views/followvisit/tasklist/index.vue
@@ -130,19 +130,19 @@
          :show-overflow-tooltip="true"
        />
        <el-table-column
          label="总任务/已随访"
          label="执行详情"
          align="center"
          key="nickName"
          key="taskDetail"
          width="120"
          prop="nickName"
        >
          <template slot-scope="scope">
            <span
              >{{
                scope.row.wfs || scope.row.wfs == 0
                  ? scope.row.wfs + scope.row.yfs
                  : ""
              }}/{{ scope.row.yfs }}</span
            <el-button
              size="medium"
              type="text"
              @click="showCountDetail(scope.row)"
              ><span class="button-xq"
                ><i class="el-icon-s-data"></i>执行详情</span
              ></el-button
            >
          </template>
        </el-table-column>
@@ -385,6 +385,26 @@
        >
      </div>
    </el-dialog>
    <!-- ä»»åŠ¡æ‰§è¡Œè¯¦æƒ…å¼¹çª— -->
    <el-dialog
      title="任务执行详情"
      :visible.sync="countDialogVisible"
      width="600px"
      :before-close="handleCountClose"
    >
      <div v-loading="countLoading" class="count-detail">
        <div class="count-task-name">任务名称:{{ countTaskName || "-" }}</div>
        <div class="count-grid">
          <div v-for="f in countFields" :key="f.key" class="count-item">
            <span class="count-label">{{ f.label }}</span>
            <span class="count-value">{{ countVal(f) }}</span>
          </div>
        </div>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="countDialogVisible = false">关 é—­</el-button>
      </span>
    </el-dialog>
    <!-- é€‰æ‹©æ‚£è€…弹框 -->
    <Patient-Selection
      ref="Patient"
@@ -407,6 +427,7 @@
import {
  getTasklist,
  delTaskInfo,
  countByCondition,
  TaskTemplateSendExecution,
  scanGenerateSubtask,
  goSfRelay,
@@ -450,6 +471,25 @@
      qrDialogVisible: false,
      qrCodeUrl: "", // ä¸­é—´é¡µé“¾æŽ¥
      qrCodeCanvas: null, // å­˜å‚¨canvas引用
      // ä»»åŠ¡æ‰§è¡Œè¯¦æƒ…å¼¹çª—
      countDialogVisible: false,
      countLoading: false,
      countTaskName: "",
      countData: {},
      countFields: [
        { key: "total", label: "任务总量" },
        { key: "blq", label: "待领取" },
        { key: "wfs", label: "未发送" },
        { key: "yfs", label: "已发送", altKey: "ysf" },
        { key: "fssb", label: "发送失败" },
        { key: "dsf", label: "待随访" },
        { key: "xsf", label: "需随访" },
        { key: "wxsf", label: "无需随访" },
        { key: "wzx", label: "未执行" },
        { key: "ywc", label: "已完成" },
        { key: "yc", label: "异常" },
        { key: "jg", label: "警告" },
      ],
      // æ—¥æœŸèŒƒå›´
      dateRange: [],
      // å²—位选项
@@ -1196,6 +1236,44 @@
      const container = this.$refs.qrCodeContainer;
      if (container) container.innerHTML = "";
    },
    // æŸ¥çœ‹ä»»åŠ¡æ‰§è¡Œè¯¦æƒ…ï¼ˆcountByCondition ç›´æŽ¥è¿”回聚合 Map)
    showCountDetail(row) {
      this.countTaskName = row.taskName || "";
      this.countData = {};
      this.countDialogVisible = true;
      this.countLoading = true;
      countByCondition({ taskid: row.taskid })
        .then((res) => {
          this.countData = this.normalizeCount(res || {});
        })
        .catch(() => {
          this.countData = {};
          this.$modal.msgError("获取任务执行详情失败");
        })
        .finally(() => {
          this.countLoading = false;
        });
    },
    // å½’一键名(聚合 SQL åˆ«åå¯èƒ½å¸¦ç©ºæ ¼ï¼‰
    normalizeCount(data) {
      const out = {};
      Object.keys(data || {}).forEach((k) => {
        out[String(k).trim()] = data[k];
      });
      return out;
    },
    // è¯»å–统计值(key ä¼˜å…ˆï¼ŒaltKey å…œåº•,缺失显示 0)
    countVal(f) {
      const d = this.countData || {};
      if (d[f.key] != null && d[f.key] !== "") return d[f.key];
      if (f.altKey && d[f.altKey] != null && d[f.altKey] !== "")
        return d[f.altKey];
      return 0;
    },
    handleCountClose() {
      this.countDialogVisible = false;
      this.countData = {};
    },
  },
};
</script>
@@ -1358,4 +1436,35 @@
    font-size: 24px;
  }
}
.count-detail {
  .count-task-name {
    font-size: 14px;
    color: #303133;
    font-weight: 600;
    margin-bottom: 16px;
  }
  .count-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
  }
  .count-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 14px;
    background: #f5f7fa;
    border-radius: 4px;
    .count-label {
      font-size: 13px;
      color: #606266;
    }
    .count-value {
      font-size: 16px;
      font-weight: 600;
      color: #303133;
    }
  }
}
</style>
src/views/patient/patient/AwaitingAdmission.vue
@@ -54,6 +54,17 @@
              <el-option label="未安排" :value="2"> </el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="床号排序" prop="bedSort">
            <el-select
              v-model="queryParams.bedSort"
              placeholder="不按床号排序"
              style="width: 160px"
            >
              <el-option label="不按床号排序" :value="null"></el-option>
              <el-option label="按床号正序" :value="1"></el-option>
              <el-option label="按床号倒序" :value="2"></el-option>
            </el-select>
          </el-form-item>
          <el-row>
            <el-form-item>
              <el-button
@@ -704,6 +715,7 @@
        pageNum: 1,
        pageSize: 10,
        inhospstate: "3",
        bedSort: null,
      },
      // è¡¨å•校验
      rules: {
@@ -893,6 +905,7 @@
      this.queryParams = {
        pageNum: 1,
        pageSize: 10,
        bedSort: null,
        // leavehospitaldistrictcodes: [],
      };
src/views/patient/patient/behospitalized.vue
@@ -66,6 +66,17 @@
              end-placeholder="结束日期"
            ></el-date-picker>
          </el-form-item>
          <el-form-item label="床号排序" prop="bedSort">
            <el-select
              v-model="queryParams.bedSort"
              placeholder="不按床号排序"
              style="width: 160px"
            >
              <el-option label="不按床号排序" :value="null"></el-option>
              <el-option label="按床号正序" :value="1"></el-option>
              <el-option label="按床号倒序" :value="2"></el-option>
            </el-select>
          </el-form-item>
          <el-row>
            <el-form-item>
@@ -767,6 +778,7 @@
        pageNum: 1,
        pageSize: 10,
        cry: 0,
        bedSort: null,
        searchscope: 3,
        scopetype: [],
        leaveldeptcodes: [],
@@ -1118,6 +1130,7 @@
        pageNum: 1,
        pageSize: 10,
        cry: 0,
        bedSort: null,
        searchscope: 3,
        scopetype: [],
        leaveldeptcodes: [],
src/views/patient/patient/hospital.vue
@@ -77,6 +77,17 @@
              end-placeholder="结束日期"
            ></el-date-picker>
          </el-form-item>
          <el-form-item label="床号排序" prop="bedSort">
            <el-select
              v-model="queryParams.bedSort"
              placeholder="不按床号排序"
              style="width: 160px"
            >
              <el-option label="不按床号排序" :value="null"></el-option>
              <el-option label="按床号正序" :value="1"></el-option>
              <el-option label="按床号倒序" :value="2"></el-option>
            </el-select>
          </el-form-item>
          <el-row>
            <el-form-item>
@@ -794,6 +805,7 @@
        pageNum: 1,
        pageSize: 10,
        cry: 1,
        bedSort: null,
        searchscope: 3,
        scopetype: [],
        leaveldeptcodes: [],
@@ -1147,6 +1159,7 @@
        pageNum: 1,
        pageSize: 10,
        cry: 1,
        bedSort: null,
        searchscope: 3,
        scopetype: [],
        leaveldeptcodes: [],
Ëæ·ÃͨÓÃ922.zip
Binary files differ