ReportCardWidget.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <script setup lang="ts">
  2. import type { ReportModel } from '@/model';
  3. import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
  4. import { h } from 'vue';
  5. const props = withDefaults(defineProps<{
  6. dataset: ReportModel;
  7. loading?: boolean;
  8. title?: string;
  9. collapsible?: boolean;
  10. hideTitle?: boolean;
  11. }>(), {
  12. title: '健康分析报告',
  13. collapsible: true,
  14. hideTitle: false,
  15. });
  16. const container = ref<HTMLDivElement | null>(null);
  17. const collapsed = defineModel('collapsed', { required: false, default: false });
  18. const collapse = (value: boolean) => {
  19. collapsed.value = value;
  20. container.value?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
  21. };
  22. const tongueExceptionPanelHeight = window.innerHeight * 0.8;
  23. const openTongueExceptionAnalysis = ref(false);
  24. const tongueExceptionPanelKeys = ref<string[]>([]);
  25. const tongueExceptionData = computed(() => {
  26. const keys = [ 'tongueColor', 'tongueCoatingColor', 'tongueShape', 'tongueCoating', 'bodyFluid', 'sublingualVein' ];
  27. const data: Record<string, string>[] = [];
  28. for ( const key of keys ) {
  29. const list = props.dataset?.[ key ]?.actualList ?? [];
  30. for ( const item of list ) if ( item?.contrast && item.contrast !== 's' ) data.push(item);
  31. }
  32. tongueExceptionPanelKeys.value = data.map(t => t.actualValue);
  33. return data;
  34. });
  35. defineExpose({
  36. elementTarget: container,
  37. });
  38. </script>
  39. <template>
  40. <div class="card report-card" ref="container">
  41. <div v-if="!props.hideTitle" class="card__header sticky flex justify-between items-center">
  42. <div class="card__title">
  43. <span>{{ props.title }}</span>
  44. <a-spin v-if="props.loading" size="small" style="margin-left: 4px;" />
  45. <a-space style="margin-left: 8px" v-if="collapsible">
  46. <a-tooltip :title="collapsed ? '展开' : '收起'">
  47. <a-button
  48. shape="circle" size="small" :icon="collapsed ? h(FullscreenOutlined) : h(FullscreenExitOutlined)"
  49. @click="collapse(!collapsed)"
  50. />
  51. </a-tooltip>
  52. </a-space>
  53. <a-space style="margin-left: 8px">
  54. <slot name="tool-bar" :reportId="props.dataset.id"></slot>
  55. </a-space>
  56. </div>
  57. </div>
  58. <div class="card__content" v-if="props.dataset.id">
  59. <div style="padding: 8px 0;background-color:#fff;" class="sticky top-0 z-10">
  60. <span>报告日期:</span>
  61. <slot name="select">
  62. <span style="margin-right: 8px;">{{ props.dataset.time }}</span>
  63. </slot>
  64. <slot name="analysis"></slot>
  65. <a-spin v-if="props.hideTitle && props.loading" size="small" style="margin-left: 4px;" />
  66. </div>
  67. <a-card class="card no-bordered background" size="small">
  68. <a-descriptions :column="3">
  69. <a-descriptions-item v-if="props.dataset.willillStateName" label="健康状态">
  70. {{ props.dataset.willillStateName }}
  71. </a-descriptions-item>
  72. <a-descriptions-item v-if="props.dataset.willillDegreeName" label="程度" :span="2">
  73. {{ props.dataset.willillDegreeName }}
  74. </a-descriptions-item>
  75. <a-descriptions-item v-if="props.dataset.willillFunctionName" label="表现">
  76. {{ props.dataset.willillFunctionName }}
  77. </a-descriptions-item>
  78. <a-descriptions-item v-if="props.dataset.constitutionGroupName" label="体质" :span="2">
  79. {{ props.dataset.constitutionGroupName }}
  80. </a-descriptions-item>
  81. </a-descriptions>
  82. </a-card>
  83. <template v-if="!props.collapsible || !collapsed">
  84. <a-card class="card background symptom-card" size="small">
  85. <p v-if="props.dataset.constitutionGroupDefinition">体质: {{ props.dataset.constitutionGroupDefinition }}</p>
  86. <a-descriptions :column="1" bordered size="small" :label-style="{width: '120px'}">
  87. <a-descriptions-item v-if="props.dataset.constitutionGroupGeneralCharacteristics" label="总体特征">
  88. {{ props.dataset.constitutionGroupGeneralCharacteristics }}
  89. </a-descriptions-item>
  90. <a-descriptions-item v-if="props.dataset.constitutionGroupPhysicalCharacteristics" label="形体特征">
  91. {{ props.dataset.constitutionGroupPhysicalCharacteristics }}
  92. </a-descriptions-item>
  93. <a-descriptions-item v-if="props.dataset.constitutionGroupPsychicCharacteristics" label="精神特征">
  94. {{ props.dataset.constitutionGroupPsychicCharacteristics }}
  95. </a-descriptions-item>
  96. <a-descriptions-item v-if="props.dataset.constitutionGroupCommonManifestations" label="常见表现">
  97. {{ props.dataset.constitutionGroupCommonManifestations }}
  98. </a-descriptions-item>
  99. <a-descriptions-item v-if="props.dataset.constitutionGroupDiseaseTendency" label="发病倾向">
  100. {{ props.dataset.constitutionGroupDiseaseTendency }}
  101. </a-descriptions-item>
  102. <a-descriptions-item v-if="props.dataset.constitutionGroupAdaptability" label="环境适应能力">
  103. {{ props.dataset.constitutionGroupAdaptability }}
  104. </a-descriptions-item>
  105. </a-descriptions>
  106. </a-card>
  107. <a-card class="card background" size="small" title="舌象分析" :loading="props.loading">
  108. <template #extra v-if="tongueExceptionData.length">
  109. <a-button type="dashed" size="small" danger @click="openTongueExceptionAnalysis=true">异常舌象分析
  110. </a-button>
  111. <vxe-modal
  112. title="异常舌象分析" v-model="openTongueExceptionAnalysis"
  113. resize show-maximize :width="600" :height="tongueExceptionPanelHeight"
  114. >
  115. <a-collapse :bordered="false" v-model:activeKey="tongueExceptionPanelKeys">
  116. <a-collapse-panel
  117. v-for="(panel, index) in tongueExceptionData" :key="panel.actualValue"
  118. :header="panel.actualValue"
  119. >
  120. <a-descriptions :column="1" bordered size="small" :label-style="{width: '120px',textAlign:'center' }">
  121. <a-descriptions-item>
  122. <div class="flex flex-row">
  123. <a-image v-if="panel.splitImage" :src="panel.splitImage" :width="200" :preview="true" />
  124. <a-space v-if="panel.attrs" direction="vertical">
  125. <a-tag v-for="v in panel.attrs" color="#cd201f" style="margin-left: 8px;margin-right: 0;">
  126. {{ v }}
  127. </a-tag>
  128. </a-space>
  129. </div>
  130. </a-descriptions-item>
  131. <a-descriptions-item label="特征">{{ panel.features }}</a-descriptions-item>
  132. <a-descriptions-item label="临床意义">{{ panel.clinicalSignificance }}</a-descriptions-item>
  133. </a-descriptions>
  134. </a-collapse-panel>
  135. </a-collapse>
  136. </vxe-modal>
  137. </template>
  138. <a-space class="w-full analysis-wrapper">
  139. <a-descriptions :column="3" bordered size="small">
  140. <a-descriptions-item style="font-size: 14px;font-weight: 500;">舌象维度</a-descriptions-item>
  141. <a-descriptions-item style="font-size: 14px;font-weight: 500;">检测结果</a-descriptions-item>
  142. <a-descriptions-item style="font-size: 14px;font-weight: 500;">标准值</a-descriptions-item>
  143. <template v-if="props.dataset.tongueColor?.actualList">
  144. <a-descriptions-item>舌色</a-descriptions-item>
  145. <a-descriptions-item>
  146. <span class="tongue-value" v-for="item in props.dataset.tongueColor.actualList" :key="item?.actualValue">
  147. <span>{{ item.actualValue }}</span>
  148. <span v-if="item.contrast !== 's'">({{ item.contrast }})</span>
  149. </span>
  150. </a-descriptions-item>
  151. <a-descriptions-item>{{ props.dataset.tongueColor.standardValue }}</a-descriptions-item>
  152. </template>
  153. <template v-if="props.dataset.tongueCoatingColor?.actualList">
  154. <a-descriptions-item>苔色</a-descriptions-item>
  155. <a-descriptions-item>
  156. <span class="tongue-value" v-for="item in props.dataset.tongueCoatingColor.actualList"
  157. :key="item?.actualValue"
  158. >
  159. <span>{{ item.actualValue }}</span>
  160. <span v-if="item.contrast !== 's'">({{ item.contrast }})</span>
  161. </span>
  162. </a-descriptions-item>
  163. <a-descriptions-item>{{ props.dataset.tongueCoatingColor.standardValue }}</a-descriptions-item>
  164. </template>
  165. <template v-if="props.dataset.tongueShape?.actualList">
  166. <a-descriptions-item>舌形</a-descriptions-item>
  167. <a-descriptions-item>
  168. <span class="tongue-value" v-for="item in props.dataset.tongueShape.actualList" :key="item?.actualValue">
  169. <span>{{ item.actualValue }}</span>
  170. <span v-if="item.contrast !== 's'">({{ item.contrast }})</span>
  171. </span>
  172. </a-descriptions-item>
  173. <a-descriptions-item>{{ props.dataset.tongueShape.standardValue }}</a-descriptions-item>
  174. </template>
  175. <template v-if="props.dataset.tongueCoating?.actualList">
  176. <a-descriptions-item>苔质</a-descriptions-item>
  177. <a-descriptions-item>
  178. <span class="tongue-value" v-for="item in props.dataset.tongueCoating.actualList" :key="item?.actualValue">
  179. <span>{{ item.actualValue }}</span>
  180. <span v-if="item.contrast !== 's'">({{ item.contrast }})</span>
  181. </span>
  182. </a-descriptions-item>
  183. <a-descriptions-item>{{ props.dataset.tongueCoating.standardValue }}</a-descriptions-item>
  184. </template>
  185. <template v-if="props.dataset.bodyFluid?.actualList">
  186. <a-descriptions-item>津液</a-descriptions-item>
  187. <a-descriptions-item>
  188. <span class="tongue-value" v-for="item in props.dataset.bodyFluid.actualList" :key="item?.actualValue">
  189. <span>{{ item.actualValue }}</span>
  190. <span v-if="item.contrast !== 's'">({{ item.contrast }})</span>
  191. </span>
  192. </a-descriptions-item>
  193. <a-descriptions-item>{{ props.dataset.bodyFluid.standardValue }}</a-descriptions-item>
  194. </template>
  195. <template v-if="props.dataset.sublingualVein?.actualList">
  196. <a-descriptions-item>舌下</a-descriptions-item>
  197. <a-descriptions-item>
  198. <span class="" v-for="item in props.dataset.sublingualVein.actualList" :key="item?.actualValue">
  199. <span>{{ item.actualValue }}</span>
  200. <span v-if="item.contrast !== 's'">({{ item.contrast }})</span>
  201. </span>
  202. </a-descriptions-item>
  203. <a-descriptions-item>{{ props.dataset.sublingualVein.standardValue }}</a-descriptions-item>
  204. </template>
  205. </a-descriptions>
  206. <a-image :width="200" :height="200" :src="props.dataset.upImg" :preview="true" v-if="props.dataset.upImg" />
  207. <a-image :width="200" :height="200" :src="props.dataset.downImg" :preview="true" v-if="props.dataset.downImg" />
  208. </a-space>
  209. </a-card>
  210. <template v-if="props.dataset?.faceAnalysisResult">
  211. <a-card class="card background" size="small" title="面象分析" :loading="props.loading">
  212. <a-space align="start" class="w-full analysis-wrapper">
  213. <div>{{ props.dataset.faceAnalysisResult }}</div>
  214. <a-image :width="200" :height="200" :src="props.dataset.faceImg" :preview="true" v-if="props.dataset.faceImg" />
  215. </a-space>
  216. </a-card>
  217. </template>
  218. <a-card class="card background symptom-card" size="small" title="中医证素"
  219. v-if="props.dataset.factorItems?.length"
  220. >
  221. <a-descriptions :column="1" bordered size="small">
  222. <a-descriptions-item
  223. v-for="item in props.dataset.factorItems" :key="item.factorItemName"
  224. :label="item.factorItemName"
  225. >
  226. {{ item.factorItemDescription }}
  227. </a-descriptions-item>
  228. </a-descriptions>
  229. </a-card>
  230. <a-card class="card background symptom-card" size="small" title="中医证型"
  231. v-if="props.dataset.diagnoseSyndromes?.length"
  232. >
  233. <a-descriptions :column="1" bordered size="small">
  234. <a-descriptions-item v-for="item in props.dataset.diagnoseSyndromes" :key="item.diagnoseSyndromeName"
  235. :label="item.diagnoseSyndromeName"
  236. >
  237. {{ item.diagnoseSyndromeAnalysis }}
  238. </a-descriptions-item>
  239. </a-descriptions>
  240. </a-card>
  241. <slot></slot>
  242. </template>
  243. </div>
  244. <div v-else style="height: 40px;"></div>
  245. </div>
  246. </template>
  247. <style scoped lang="scss">
  248. @import "@/themes/report-card";
  249. .card__content {
  250. > .card {
  251. transition: all 200ms ease;
  252. &:not(:last-of-type) {
  253. margin-bottom: 12px;
  254. }
  255. }
  256. }
  257. .tongue-value {
  258. margin: 0 4px;
  259. &:first-of-type {
  260. margin-left: 0;
  261. }
  262. &:last-of-type {
  263. margin-right: 0;
  264. }
  265. }
  266. .symptom-card {
  267. :deep(.ant-descriptions-item-label) {
  268. padding: 8px 12px;
  269. width: 120px;
  270. text-align: center;
  271. }
  272. :deep(.ant-descriptions-item-content) {
  273. background-color: #fff;
  274. }
  275. }
  276. .analysis-wrapper {
  277. :deep(.ant-space-item) {
  278. &:first-of-type {
  279. flex: auto;
  280. }
  281. }
  282. }
  283. </style>