Prechádzať zdrojové kódy

适宜技术开具第三期的需求:适宜技术开具体两侧的功能(协定方查询以及推荐的方案和安全合理检测)

张田田 2 týždňov pred
rodič
commit
f829bb24ad

+ 9 - 0
src/api/technology.js

@@ -34,6 +34,15 @@ export function getSuitableTechList(data) {
   });
 }
 
+// 适宜技术协定方分组列表
+export function getSuitableTechGroupedList(data) {
+  return request({
+    url: "/basis/suitableTechnologyAccordMgr/listGrouped",
+    method: "post",
+    data,
+  });
+}
+
 // 适宜技术协定方保存
 export function saveSuitableTech(data) {
   return request({

+ 12 - 1
src/views/business/components/AcupointTable.vue

@@ -839,6 +839,10 @@ export default {
         .map((v) => CUMULATIVE_MAP[v.trim()])
         .filter(Boolean);
     },
+    // 当前激活项目
+    activeProject() {
+      return this.prescriptions[this.activeIndex] || null;
+    },
   },
   watch: {
     detailTables: {
@@ -847,6 +851,10 @@ export default {
       },
       deep: true,
     },
+    // 激活项目变化时通知父组件(右侧「安全合理检测」只跟随当前选中项目)
+    activeProject(val) {
+      this.$emit("active-project-change", val);
+    },
   },
   created() {
     this._prevDetailTypes = [...this.innerDetailTypes];
@@ -1100,6 +1108,8 @@ export default {
         departmentId: detailData.deptId || "",
         stitutionsId: detailData.institutionCode || "",
         stitutionsName: detailData.institutionName || "",
+        //供右侧「安全合理检测」
+        basisStitutionsnondrug: detailData || null,
       });
 
       this.prescriptionDataMap[newIndex] = createPrescriptionData({
@@ -1333,7 +1343,6 @@ export default {
       );
       if (exists) {
         this.$message.error("不可重复选择");
-        // 不重置行数据,保留原有值,让 :value 绑定自动回显
         return;
       }
 
@@ -1650,6 +1659,8 @@ export default {
           minTreatmentDuration:
             item.basisStitutionsnondrug?.minTreatmentDuration || 0,
           detailCumulative: item.basisStitutionsnondrug?.detailCumulative || "",
+          // 右侧「安全合理检测」
+          basisStitutionsnondrug: item.basisStitutionsnondrug || null,
         });
 
         const allDetails = [].concat(

+ 97 - 6
src/views/diagnosis/Prescribing.vue

@@ -83,8 +83,8 @@
           <div>快捷查询</div>
         </div>
       </div>
-      <!-- 协定方 -->
-      <div class="mr-t10 agreement">
+      <!-- 协定方(中药处方/中药制剂时显示,适宜技术隐藏) -->
+      <div class="mr-t10 agreement" v-if="container_i!=2">
         <div class="agree-tab flex-vertical-between">
           <div
             :class="agree_current==0?'flex-center agree-active':'flex-center'"
@@ -183,6 +183,9 @@
         </div>
       </div>
 
+      <!-- 适宜技术 协定方(仅 container_i==2显示) -->
+      <SuitTechAgreement v-if="container_i==2" @turn="onSuitTechTurn" />
+
       <div class="pre-title mr-t10" v-if="false">
         <div class="flex-vertical-center-l title-container">
           <span></span>
@@ -483,11 +486,12 @@
           @save="submitRecipe1()"
           @clear="clearContainer('2')"
           @add-recipe="addRecipeFrom('3')"
+          @active-project-change="onSuitActiveProjectChange"
         />
       </div>
     </div>
 
-    <div class="pre-right" v-show="container_i!=1 && !isShrink">
+    <div class="pre-right" v-show="container_i==0 && !isShrink">
       <!-- 标题信息 -->
       <div class="pre-title flex-vertical-between">
         <div class="flex-vertical-center-l title-container">
@@ -565,6 +569,16 @@
       </div>
     </div>
 
+    <!-- 适宜技术  右栏(推荐方案 + 安全检测,仅 container_i==2显示) -->
+    <div class="pre-right" v-show="container_i==2 && !isShrink">
+      <SuitTechSidePanel
+        :recommendations="rRecomendR"
+        :activeId="tjRecipeId"
+        :safetyItems="suitActiveSafety"
+        @select="onSuitRecommendSelect"
+      />
+    </div>
+
     <!-- 方剂详情 -->
     <Popup
       :showDialog="showDialog"
@@ -1057,7 +1071,6 @@
     </Popup>
 
     <!-- 安全合理用药弹窗 -->
-    <!-- 查询协定方 方剂 页面768 -->
     <Popup
       :showDialog.sync="showSafeD"
       @cancle="showSafeD=false"
@@ -1113,6 +1126,8 @@ import TongueAnalysis from "./components/tongue-analysis.vue";
 import safeDrug from "./components/safeDrug.vue";
 import medAdress from "./components/medAddress.vue";
 import medAdressNew from "./components/medAddressNew.vue";
+import SuitTechAgreement from "./components/SuitTechAgreement.vue";
+import SuitTechSidePanel from "./components/SuitTechSidePanel.vue";
 import {
   getPatiensBasisM,
   refreshElectronicInfo,
@@ -1185,7 +1200,9 @@ export default {
     doctorCase,
     safeDrug,
     medAdress,
-    medAdressNew
+    medAdressNew,
+    SuitTechAgreement,
+    SuitTechSidePanel
   },
   data() {
     return {
@@ -1290,7 +1307,6 @@ export default {
        */
       reasonableSafeMedicinesHTML: '',
       reasonableSafeMedicinesLoading: false,
-      // 保存右侧合理用药信息
       rationalMed: [],
       // 从1-9 分别对应 超剂量 到 19反
       rationalMed1: [],
@@ -3898,6 +3914,81 @@ export default {
         }, 500);
       }
     },
+    // 适宜技术 · 协定方转方(详情弹窗「转方」触发)
+    async onSuitTechTurn(pid) {
+      const loading = this.$loading({
+        lock: true,
+        text: '正在转方',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)',
+      });
+      try {
+        const res = await getRecipeByBasis({
+          sourcePid: pid,
+          recordId: this.getPatiensInfo.pid || undefined,
+          sourceType: 'accord',
+        });
+        if (res.ResultCode === 0 && res.Data) {
+          this.assignRecipe3(res.Data, loading);
+        } else {
+          loading.close();
+        }
+      } catch (e) {
+        loading.close();
+        console.error('适宜技术协定方转方失败', e);
+      }
+      this.addRecipeFrom("2");
+    },
+    // 适宜技术  推荐方案
+    async onSuitRecommendSelect(item) {
+      const sourcePid = item.pid || item.preid || item.acupreid;
+      if (!sourcePid) return;
+      if (this.$refs.suitScience && (this.$refs.suitScience.isPay || this.$refs.suitScience.paystate == 1)) {
+        this.$message.error('当前为已付费处方,暂不支持推导');
+        return;
+      }
+      const loading = this.$loading({
+        lock: true,
+        text: '正在获取适宜技术处方数据',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)',
+      });
+      try {
+        const recipeRes = await getRecipeByBasis({
+          sourcePid,
+          recordId: this.getPatiensInfo.pid || undefined,
+          sourceType: 'localExpert',
+        });
+        if (recipeRes.ResultCode === 0 && recipeRes.Data) {
+          this.assignRecipe3(recipeRes.Data, loading);
+          this.tjRecipeId = sourcePid;
+        } else {
+          loading.close();
+        }
+      } catch (e) {
+        loading.close();
+        console.error('适宜技术推荐载入失败', e);
+      }
+      this.addRecipeFrom("4", 2);
+    },
+    onSuitActiveProjectChange(prescription) {
+      if (!prescription) {
+        this.suitActiveSafety = [];
+        return;
+      }
+      const b = prescription.basisStitutionsnondrug || {};
+      const pick = (key) => b[key] || "";
+      this.suitActiveSafety = [
+        {
+          itemName: prescription.itemName || "",
+          connotation: pick("connotation"),
+          excludedContent: pick("excludedContent"),
+          limitPayScope: pick("limitPayScope"),
+          remark: pick("remark"),
+        },
+      ];
+    },
+
     // 新增处方来源
     async addRecipeFrom(type, isSuit = "") {
       if (type != 3) {

+ 235 - 0
src/views/diagnosis/components/SuitTechAgreement.vue

@@ -0,0 +1,235 @@
+<template>
+  <div class="mr-t10 agreement">
+    <!-- 协定方标题(与中药协定方 agree-tab 样式一致) -->
+    <div class="agree-tab flex-vertical-between">
+      <div class="flex-center agree-active">协定方</div>
+    </div>
+    <!-- 三组折叠(个人/科室/机构,手风琴单展开;每组加载更多三组同步翻页) -->
+    <div class="collapse">
+      <el-collapse v-model="activeNames" accordion>
+        <el-collapse-item
+          v-for="group in groupList"
+          :key="group.key"
+          :title="group.name"
+          :name="group.key"
+        >
+          <div class="collapse-body">
+            <div
+              class="collapse-item flex-vertical-between"
+              v-for="(item, idx) in group.list"
+              :key="item.pid + idx"
+            >
+              <div class="med-name" @click="openDetail(item.pid)">{{ item.name }}</div>
+            </div>
+            <!-- 每组加载更多:按钮常驻不隐藏;点对应分组看对应数组是否为空,空则提示「没有更多数据了」 -->
+            <div class="collapse-more flex-center" @click="loadMore(group)">
+              <img src="../../../assets/point3.png" alt />
+            </div>
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+    </div>
+
+    <!-- 详情弹窗(独立组件) -->
+    <SuitTechAgreementDetail
+      :show.sync="showDetail"
+      :detail="detail"
+      :loading="detailLoading"
+      @turn="onDetailTurn"
+    />
+  </div>
+</template>
+
+<script>
+import { getSuitableTechGroupedList, getSuitableTechInfo } from "@/api/technology.js";
+import SuitTechAgreementDetail from "./SuitTechAgreementDetail.vue";
+
+export default {
+  name: "SuitTechAgreement",
+  components: { SuitTechAgreementDetail },
+  data() {
+    return {
+      activeNames: "personal",
+      // 每组各自持有状态:showType(共享方式 0 个人 / 1 科室 / 2 机构)、列表、是否还有更多
+      groups: {
+        personal: { showType: 0, list: [], loadMore: true },
+        department: { showType: 1, list: [], loadMore: true },
+        institution: { showType: 2, list: [], loadMore: true },
+      },
+      pageNum: 1,
+      pageSize: 10,
+      loading: false,
+      // 本次「加载更多」点的是哪个分组(共享方式 0/1/2),用于判断该分组返回数组是否为空
+      currentShowType: null,
+      showDetail: false,
+      detailLoading: false,
+      detail: {},
+      currentPid: "",
+    };
+  },
+  computed: {
+    groupList() {
+      return [
+        { key: "personal", name: "个人", ...this.groups.personal },
+        { key: "department", name: "科室", ...this.groups.department },
+        { key: "institution", name: "机构", ...this.groups.institution },
+      ];
+    },
+  },
+  mounted() {
+    this.fetchList(true);
+  },
+  methods: {
+    async fetchList(reset = false) {
+      if (this.loading) return;
+      this.loading = true;
+      if (reset) {
+        this.pageNum = 1;
+        // 重置每组列表与「是否还有更多」,showType 保留
+        Object.keys(this.groups).forEach((k) => {
+          this.groups[k].list = [];
+          this.groups[k].loadMore = true;
+        });
+      }
+      try {
+        const res = await getSuitableTechGroupedList({
+          pageNum: this.pageNum,
+          pageSize: this.pageSize,
+        });
+        if (res.ResultCode === 0 && res.Data) {
+          const d = res.Data;
+          // 追加每组列表(三组共用 pageNum,同步翻页)
+          this.groups.personal.list = this.groups.personal.list.concat(d.personal || []);
+          this.groups.department.list = this.groups.department.list.concat(d.department || []);
+          this.groups.institution.list = this.groups.institution.list.concat(d.institution || []);
+          // 按本次点击的分组(currentShowType)判断:对应数组为空 = 该分组没有更多
+          this.checkLoadMore(d);
+        }
+      } catch (e) {
+        console.error("获取适宜技术协定方列表失败", e);
+      } finally {
+        this.loading = false;
+      }
+    },
+    // 根据本次点击的 showType,判断对应分组返回数组是否为空:空则提示并标记该组无更多
+    checkLoadMore(d) {
+      const showTypeKey = { 0: "personal", 1: "department", 2: "institution" };
+      const key = showTypeKey[this.currentShowType];
+      // 初次加载(currentShowType 为 null)不提示
+      if (key == null) return;
+      const empty = !(d[key] || []).length;
+      if (empty) {
+        this.$message({
+          showClose: false,
+          message: "没有更多数据了",
+          type: "warning",
+        });
+        this.groups[key].loadMore = false;
+      }
+      // 消费后复位,避免下次初始加载误判
+      this.currentShowType = null;
+    },
+    loadMore(group) {
+      if (this.loading) return;
+      // 该组已无更多:直接提示,不再重复请求(按钮仍常驻显示,与中药协定方一致)
+      if (!group.loadMore) {
+        this.$message({
+          showClose: false,
+          message: "没有更多数据了",
+          type: "warning",
+        });
+        return;
+      }
+      // 记录本次点的是哪个分组(共享方式 0/1/2)
+      this.currentShowType = group.showType;
+      this.pageNum += 1;
+      this.fetchList();
+    },
+    async openDetail(pid) {
+      this.currentPid = pid;
+      this.detailLoading = true;
+      this.showDetail = true;
+      try {
+        const res = await getSuitableTechInfo(pid);
+        if (res.ResultCode === 0) {
+          this.detail = res.Data || {};
+        }
+      } catch (e) {
+        console.error("获取协定方详情失败", e);
+      } finally {
+        this.detailLoading = false;
+      }
+    },
+    onDetailTurn() {
+      if (!this.currentPid) return;
+      this.$emit("turn", this.currentPid);
+      this.showDetail = false;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.agreement {
+  .agree-tab {
+    div {
+      width: 100%;
+      height: 30px;
+      background: rgba($color: #5386f6, $alpha: 0.1);
+      font-size: 13px;
+      font-family: PingFang SC;
+      font-weight: 400;
+      color: #333333;
+      cursor: pointer;
+    }
+    .agree-active {
+      background: #5386f6;
+      color: #fff;
+    }
+  }
+
+  .collapse {
+    border: 1px solid #ebeef5;
+  }
+
+  .collapse-item {
+    padding: 13px;
+    border-bottom: 1px dashed #ebeef5;
+
+    .med-name {
+      flex: 1;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      font-size: 14px;
+      font-family: PingFang SC;
+      font-weight: 400;
+      color: #333333;
+      cursor: pointer;
+    }
+
+    .med-name:hover {
+      background: #d8d8d8;
+    }
+  }
+
+  .collapse-more {
+    img {
+      width: 26px;
+    }
+  }
+}
+
+.collapse::v-deep .el-collapse-item__header {
+  padding: 0 0 0 10px;
+  border-bottom: 1px dashed #ebeef5;
+  font-size: 14px;
+  font-family: PingFang SC;
+  font-weight: 500;
+  color: #333333;
+}
+
+.collapse ::v-deep .el-collapse-item__content {
+  padding-bottom: 5px;
+}
+</style>

+ 481 - 0
src/views/diagnosis/components/SuitTechAgreementDetail.vue

@@ -0,0 +1,481 @@
+<template>
+  <div class="suit-tech-agreement-detail">
+    <Popup
+      :showDialog.sync="dialogVisible"
+      @cancle="handleClose"
+      distanceTop="5vh"
+      :title="title"
+      :showBtns="false"
+      :loading="loading"
+      width="750px"
+    >
+      <template slot="body">
+        <!-- 基本信息 -->
+        <div class="info-row">
+          <span class="info-label">方 名:</span>
+          <span class="info-value">{{ detail.name }}</span>
+          <el-button
+            type="primary"
+            size="mini"
+            class="turn-btn"
+            @click="handleTurn"
+            >转方</el-button
+          >
+        </div>
+        <div class="info-row">
+          <span class="info-label">中医病名:</span>
+          <span class="info-value">{{ detail.disName }}</span>
+          <span class="info-label ml-40">西医诊断:</span>
+          <span class="info-value">{{ detail.westernDiag }}</span>
+        </div>
+        <div class="info-row">
+          <span class="info-label">功效适应症:</span>
+          <span class="info-value">{{ detail.effect }}</span>
+        </div>
+
+        <!-- 涵盖项目 tabs -->
+        <div class="section-tabs">
+          <span class="tabs-label">涵盖项目</span>
+          <div class="tabs-list">
+            <span
+              v-for="(item, index) in treatmentList"
+              :key="index"
+              class="tab-item"
+              :class="{ active: activeTab === index }"
+              @click="activeTab = index"
+              >{{ item.treatItemName }}</span
+            >
+          </div>
+        </div>
+
+        <!-- 治疗项目详情 -->
+        <div class="treatment-detail" v-if="currentTreatment">
+          <div class="detail-row">
+            <span class="detail-label">分 类:</span>
+            <span class="detail-value">{{
+              getClassifyLabel(currentTreatment.treatClass)
+            }}</span>
+            <span class="detail-label ml-60">项目名称:</span>
+            <span class="detail-value">{{
+              currentTreatment.treatItemName
+            }}</span>
+          </div>
+          <div class="detail-row" v-if="hasAnyDetailData()">
+            <span class="detail-label">明 细:</span>
+            <div class="detail-value detail-group">
+              <div class="detail-sub" v-if="hasDetailData('detailPoint')">
+                <span class="detail-sub-label">穴位:</span>
+                <span class="acupoint-list">
+                  <span
+                    v-for="(point, pIdx) in currentTreatment.detailPoint"
+                    :key="'point-' + pIdx"
+                    class="acupoint-item"
+                  >
+                    {{ point.name }} {{ point.num }}
+                  </span>
+                </span>
+              </div>
+              <div class="detail-sub" v-if="hasDetailData('detailMeridian')">
+                <span class="detail-sub-label">经络:</span>
+                <span class="acupoint-list">
+                  <span
+                    v-for="(item, mIdx) in currentTreatment.detailMeridian"
+                    :key="'meridian-' + mIdx"
+                    class="acupoint-item"
+                  >
+                    {{ item.name }} {{ item.num }}
+                  </span>
+                </span>
+              </div>
+              <div class="detail-sub" v-if="hasDetailData('detailEarPoint')">
+                <span class="detail-sub-label">耳穴:</span>
+                <span class="acupoint-list">
+                  <span
+                    v-for="(item, eIdx) in currentTreatment.detailEarPoint"
+                    :key="'earpoint-' + eIdx"
+                    class="acupoint-item"
+                  >
+                    {{ item.name }} {{ item.num }}
+                  </span>
+                </span>
+              </div>
+              <div class="detail-sub" v-if="hasDetailData('detailBodyPart')">
+                <span class="detail-sub-label">部位:</span>
+                <span class="acupoint-list">
+                  <span
+                    v-for="(item, bIdx) in currentTreatment.detailBodyPart"
+                    :key="'bodypart-' + bIdx"
+                    class="acupoint-item"
+                  >
+                    {{ item.name }} {{ item.num }}
+                  </span>
+                </span>
+              </div>
+              <div class="detail-sub" v-if="hasDetailData('detailOther')">
+                <span class="detail-sub-label">其他详情:</span>
+                <span class="acupoint-list">
+                  <span
+                    v-for="(item, oIdx) in currentTreatment.detailOther"
+                    :key="'other-' + oIdx"
+                    class="acupoint-item"
+                  >
+                    {{ item.name }} {{ item.num }}
+                  </span>
+                </span>
+              </div>
+            </div>
+          </div>
+          <div class="detail-row">
+            <span class="detail-label">操作指南:</span>
+            <span class="detail-value">{{ currentTreatment.useExplain }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="detail-label">单 次:</span>
+            <span class="detail-value"
+              >{{ currentTreatment.singleQty
+              }}{{
+                (currentTreatment.basisStitutionsnondrug &&
+                  currentTreatment.basisStitutionsnondrug.pricingUnit) ||
+                  ""
+              }}</span
+            >
+          </div>
+          <div class="detail-row">
+            <span class="detail-label">治疗次数:</span>
+            <span class="detail-value">{{ currentTreatment.treatNum }}次</span>
+          </div>
+          <div class="detail-row">
+            <span class="detail-label">频 次:</span>
+            <span class="detail-value">{{ currentTreatment.frequency }}</span>
+          </div>
+        </div>
+
+        <!-- 底部信息:agreement=分享医生;recommend=方解+出处 -->
+        <div class="share-doctor" v-if="mode === 'agreement'">
+          <span class="share-label">分享医生:</span>
+          <span class="share-value">{{ detail.createUserName }}</span>
+        </div>
+        <template v-else>
+          <div class="share-doctor">
+            <span class="share-label">方 解:</span>
+            <span class="share-value">{{ detail.mechanismPre }}</span>
+          </div>
+          <div class="share-doctor">
+            <span class="share-label">出 处:</span>
+            <span class="share-value"
+              >{{ detail.expert }}{{ detail.expertTitle }}</span
+            >
+          </div>
+        </template>
+
+        <!-- 底部返回按钮(右侧推荐方案详情) -->
+        <div class="detail-back" v-if="showBackBtn">
+          <el-button type="primary" @click="handleClose">返回</el-button>
+        </div>
+      </template>
+    </Popup>
+  </div>
+</template>
+
+<script>
+import Popup from "@/components/Propup.vue";
+
+export default {
+  name: "SuitTechAgreementDetail",
+  components: { Popup },
+  props: {
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    detail: {
+      type: Object,
+      default: () => ({}),
+    },
+    loading: {
+      type: Boolean,
+      default: false,
+    },
+    // 弹窗标题(左侧协定方默认「我的协定方」,右侧推荐传「方案详情」)
+    title: {
+      type: String,
+      default: "我的协定方",
+    },
+    // 模式:agreement(左侧协定方,底部显示分享医生) | recommend(右侧推荐,底部显示方解/出处)
+    mode: {
+      type: String,
+      default: "agreement",
+    },
+    // 是否显示底部「返回」按钮(右侧推荐需要)
+    showBackBtn: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      activeTab: 0,
+      classifyOptions: [],
+    };
+  },
+  computed: {
+    dialogVisible: {
+      get() {
+        return this.show;
+      },
+      set(val) {
+        this.$emit("update:show", val);
+      },
+    },
+    treatmentList() {
+      return this.detail.treatmentList || [];
+    },
+    currentTreatment() {
+      return this.treatmentList[this.activeTab] || null;
+    },
+  },
+  mounted() {
+    this.loadClassifyOptions();
+  },
+  watch: {
+    show(val) {
+      if (val) {
+        this.activeTab = 0;
+      }
+    },
+  },
+  methods: {
+    handleClose() {
+      this.$emit("update:show", false);
+    },
+    handleTurn() {
+      this.$emit("turn");
+    },
+    async loadClassifyOptions() {
+      try {
+        const res = await fetch(`${window.BASE_URL}data/classify.json`);
+        const data = await res.json();
+        this.classifyOptions = data.classify || [];
+      } catch (e) {
+        console.error("加载分类选项失败", e);
+      }
+    },
+    getClassifyLabel(value) {
+      if (
+        (value === "" || value === null || value === undefined) &&
+        value !== 0
+      )
+        return "-";
+      const sv = String(value);
+      const item = this.classifyOptions.find((o) => String(o.value) === sv);
+      return item ? item.label : value;
+    },
+    hasDetailData(detailType) {
+      if (!this.currentTreatment) return false;
+      const detail = this.currentTreatment[detailType];
+      return detail && Array.isArray(detail) && detail.length > 0;
+    },
+    hasAnyDetailData() {
+      if (!this.currentTreatment) return false;
+      return (
+        this.hasDetailData("detailPoint") ||
+        this.hasDetailData("detailMeridian") ||
+        this.hasDetailData("detailEarPoint") ||
+        this.hasDetailData("detailBodyPart") ||
+        this.hasDetailData("detailOther")
+      );
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+/* 仅覆盖本弹窗的容器高度,不影响其他 Popup 组件 */
+.suit-tech-agreement-detail ::v-deep .popup-container {
+  height: auto;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.ml-40 {
+  margin-left: 40px;
+}
+
+.ml-60 {
+  margin-left: 60px;
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 14px;
+  font-size: 14px;
+  line-height: 22px;
+
+  .info-label {
+    color: black;
+    white-space: nowrap;
+  }
+
+  .info-value {
+    color: black;
+  }
+
+  .turn-btn {
+    margin-left: 16px;
+  }
+}
+
+.section-tabs {
+  display: flex;
+  align-items: center;
+  margin-top: 24px;
+  margin-bottom: 0;
+
+  .tabs-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: black;
+    margin-right: 16px;
+    white-space: nowrap;
+  }
+
+  .tabs-list {
+    display: flex;
+    gap: 0;
+    overflow-x: auto;
+    flex-wrap: nowrap;
+
+    &::-webkit-scrollbar {
+      height: 0;
+    }
+
+    .tab-item {
+      display: inline-block;
+      padding: 5px 14px;
+      font-size: 13px;
+      color: #1684fc;
+      background: transparent;
+      border: 1px solid #1684fc;
+      border-radius: 0;
+      cursor: pointer;
+      transition: all 0.2s;
+      white-space: nowrap;
+      flex-shrink: 0;
+      // 项目之间无间隙:负边距合并相邻边框,避免接缝双线
+      margin-left: -1px;
+
+      &:first-child {
+        margin-left: 0;
+        border-top-left-radius: 5px;
+        border-bottom-left-radius: 5px;
+      }
+
+      &:last-child {
+        border-top-right-radius: 5px;
+        border-bottom-right-radius: 5px;
+      }
+
+      &:hover {
+        color: #1684fc;
+      }
+
+      // 只有选中的才有背景色;置顶层覆盖相邻边框
+      &.active {
+        position: relative;
+        color: #fff;
+        background: #1684fc;
+        border-color: #1684fc;
+      }
+    }
+  }
+}
+
+.treatment-detail {
+  // margin-top: 16px;
+  padding: 20px 20px 28px;
+  background: #fff;
+  border: 2px solid #dedede;
+  // border-radius: 4px;
+  min-height: 420px;
+
+  .detail-row {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 18px;
+    font-size: 14px;
+    line-height: 22px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .detail-label {
+      color: black;
+      white-space: nowrap;
+    }
+
+    .detail-value {
+      color: black;
+
+      &.acupoint-list {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+
+        .acupoint-item {
+          white-space: nowrap;
+        }
+      }
+
+      &.detail-group {
+        flex: 1;
+
+        .detail-sub {
+          display: flex;
+          align-items: flex-start;
+          margin-bottom: 8px;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+
+          .detail-sub-label {
+            white-space: nowrap;
+          }
+
+          .acupoint-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 20px;
+
+            .acupoint-item {
+              white-space: nowrap;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.share-doctor {
+  margin-top: 24px;
+  font-size: 14px;
+  line-height: 22px;
+
+  .share-label {
+    color: black;
+  }
+
+  .share-value {
+    color: black;
+  }
+}
+
+.detail-back {
+  display: flex;
+  justify-content: center;
+  margin-top: 24px;
+}
+</style>

+ 252 - 0
src/views/diagnosis/components/SuitTechSidePanel.vue

@@ -0,0 +1,252 @@
+<template>
+  <div>
+    <!-- 推荐的方案 -->
+    <div class="pre-title flex-vertical-between">
+      <div class="flex-vertical-center-l title-container">
+        <span></span>
+        <div>推荐的方案</div>
+      </div>
+    </div>
+    <div class="recommend flex-center" v-if="displayRecommendations && displayRecommendations.length">
+      <div class="banner">
+        <div class="r-banner-body">
+          <div
+            class="r-body-item flex-vertical-between"
+            v-for="(item, index) in displayRecommendations"
+            :key="'suit-r' + index"
+          >
+            <!-- 适宜技术推荐:只取 schemes(businesstype=7) 的 pres,渲染 pres.name;点击弹详情窗 -->
+            <div
+              class="med-name med-name-bg"
+              :class="{ active: activeId && (activeId == item.pid || activeId == item.preid || activeId == item.acupreid) }"
+              @click="openRecommendDetail(item)"
+            >
+              <span>{{ item.name }}</span>
+            </div>
+            <!-- 原始渲染(保留备查,不再启用):showType 分支 + 直接 $emit('select') 载入
+            <div class="med-name med-name-bg" :class="{ active: activeId && (activeId == item.preid || activeId == item.pid || activeId == item.acupreid) }" v-if="item.showType == 0" @click.stop="$emit('select', item)">
+              <span v-if="item.prename">{{ item.prename }}</span>
+              <span v-else>{{ item.acupretype == 1 ? '针灸' : item.acupretype == 2 ? '艾灸' : item.acupretype == 3 ? '推拿' : '' }}</span>
+            </div>
+            <div class="med-name med-name-bg" :class="{ active: activeId && (activeId == item.preid || activeId == item.pid) }" v-else @click="$emit('select', item)">
+              <span>{{ item.name }}({{ item.experttitle }})</span>
+            </div>
+            -->
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 安全合理检测:标题常驻;内容仅在 visibleSafetyItems 有数据时展示 -->
+    <div class="pre-title mr-t10">
+      <div class="flex-vertical-center-l title-container">
+        <span></span>
+        <div>安全合理检测</div>
+      </div>
+      <div class="patiens-msg reasonable-safe-medicines-wrapper" v-if="visibleSafetyItems.length">
+        <div
+          class="safety-project"
+          v-for="(item, index) in visibleSafetyItems"
+          :key="'safety-' + index"
+        >
+          <div class="safety-project-name">
+            {{ item.itemName || "项目" + (index + 1) }}
+          </div>
+          <div
+            class="safety-field"
+            v-for="field in safetyFields"
+            v-if="item[field.key]"
+            :key="field.key"
+          >
+            <span class="safety-label">{{ field.label }}:</span>
+            <span class="safety-value">{{ item[field.key] }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 推荐方案详情弹窗(与左侧协定方共用 SuitTechAgreementDetail,mode=recommend) -->
+    <SuitTechAgreementDetail
+      :show.sync="showDetail"
+      :detail="detail"
+      :loading="detailLoading"
+      title="方案详情"
+      mode="recommend"
+      :showBackBtn="true"
+      @turn="onDetailTurn"
+    />
+  </div>
+</template>
+
+<script>
+import { getLocalSuitableTechInfo } from "@/api/technology.js";
+import SuitTechAgreementDetail from "./SuitTechAgreementDetail.vue";
+
+export default {
+  name: "SuitTechSidePanel",
+  components: { SuitTechAgreementDetail },
+  props: {
+    recommendations: { type: Array, default: () => [] },
+    activeId: { type: [String, Number], default: "" },
+    // 适宜技术每个项目的「安全合理检测」字段(父组件从 basisStitutionsnondrug 提取后传入)
+    safetyItems: { type: Array, default: () => [] },
+  },
+  data() {
+    return {
+      // 四个字段:有值才渲染,无值连标题一起隐藏
+      safetyFields: [
+        { key: "connotation", label: "内涵" },
+        { key: "excludedContent", label: "除外内容" },
+         { key: "remark", label: "备注" },
+        { key: "limitPayScope", label: "限定支付范围" },
+      ],
+      // 推荐方案详情弹窗
+      showDetail: false,
+      detailLoading: false,
+      detail: {},
+      currentItem: null,
+    };
+  },
+  computed: {
+    // 只保留至少有一个字段有值的项目;全部为空时内容不展示(标题仍常驻显示)
+    visibleSafetyItems() {
+      return (this.safetyItems || []).filter((item) =>
+        this.safetyFields.some((f) => item && item[f.key])
+      );
+    },
+    // 适宜技术推荐:只取 schemes(businesstype=7) 的 pres(showType=0),不含 expList
+    displayRecommendations() {
+      return (this.recommendations || []).filter((item) => item.showType == 0);
+    },
+  },
+  methods: {
+    // 点击推荐 name:先弹详情窗(调本地专家适宜技术详情),不再直接载入
+    async openRecommendDetail(item) {
+      const pid = item.pid || item.preid || item.acupreid;
+      if (!pid) return;
+      this.currentItem = item;
+      this.detailLoading = true;
+      this.showDetail = true;
+      try {
+        const res = await getLocalSuitableTechInfo(pid);
+        if (res.ResultCode === 0) {
+          this.detail = res.Data || {};
+        }
+      } catch (e) {
+        console.error("获取本地专家适宜技术详情失败", e);
+      } finally {
+        this.detailLoading = false;
+      }
+    },
+    // 详情窗「转方」:冒泡给父级执行载入处方(沿用原 onSuitRecommendSelect 逻辑)
+    onDetailTurn() {
+      if (!this.currentItem) return;
+      this.$emit("select", this.currentItem);
+      this.showDetail = false;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.pre-title {
+  margin-bottom: 10px;
+
+  .title-container {
+    span {
+      width: 3px;
+      height: 14px;
+      background: #5386f6;
+      border-radius: 1px;
+      display: inline-block;
+    }
+
+    div {
+      font-size: 18px;
+      font-family: PingFang SC;
+      font-weight: 400;
+      color: #5386f6;
+      margin-left: 9px;
+    }
+  }
+
+  .patiens-msg {
+    font-size: 14px;
+    font-family: PingFang SC;
+    font-weight: 400;
+    color: #333333;
+  }
+}
+
+// 推荐方剂
+.recommend {
+  cursor: pointer;
+
+  .banner {
+    flex: 1;
+    height: 80px;
+    margin: 0 0px;
+    overflow-y: auto;
+
+    .r-banner-body {
+      cursor: pointer;
+
+      .r-body-item {
+        margin-bottom: 10px;
+
+        .med-name {
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          font-size: 14px;
+          font-family: PingFang SC;
+          font-weight: 400;
+          color: #333333;
+
+          span {
+            cursor: pointer;
+          }
+        }
+
+        .med-name:hover {
+          background: #d8d8d8;
+        }
+      }
+    }
+  }
+
+  .active {
+    color: #5386f6 !important;
+  }
+}
+
+// 安全合理检测
+.reasonable-safe-medicines-wrapper {
+  .safety-project {
+    padding: 10px 0;
+    border-bottom: 1px dashed #dcdcdc;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .safety-project-name {
+      font-size: 15px;
+      font-weight: 700;
+      color: #5386f6;
+      margin-bottom: 6px;
+    }
+
+    .safety-field {
+      font-size: 14px;
+      font-family: PingFang SC;
+      line-height: 1.75;
+      color: #333333;
+      margin-bottom: 3px;
+
+      .safety-label {
+        color: black;
+      }
+    }
+  }
+}
+</style>