report.page.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <script setup lang="ts">
  2. import NavMiniProgram from '@/assets/images/mini-program.svg?url';
  3. import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
  4. import NavPrint from '@/assets/images/nav-print.png?url';
  5. import NavScheme from '@/assets/images/nav-scheme.png?url';
  6. import PhysiqueChart from '@/modules/report/PhysiqueChart.vue';
  7. import SyndromeChart from '@/modules/report/SyndromeChart.vue';
  8. import { Notify, platformIsAIO, Toast } from '@/platform';
  9. import { getReportMethod, updateReportMethod } from '@/request/api';
  10. import { useRouteParams } from '@vueuse/router';
  11. import { useRequest, useWatcher } from 'alova/client';
  12. import { useRouter } from 'vue-router';
  13. const hidePulseExceptionTemplate = computed(() => platformIsAIO())
  14. const id = useRouteParams<string>('id');
  15. const { data, loading } = useWatcher(() => getReportMethod(id.value), [ id ], {
  16. initialData: {
  17. descriptionsTable: { column: [], data: [] },
  18. tongue: {},
  19. face: {},
  20. pulse: {},
  21. },
  22. immediate: true,
  23. }).onSuccess(({ data }) => {
  24. if ( data?.miniProgramURL && data.payLock ) panelOpen(100);
  25. })
  26. const { loading: uploading, send: upload } = useRequest(() => updateReportMethod(id.value, data.value), {
  27. immediate: false,
  28. middleware(_, next) {
  29. if (data.value.reportURL) return;
  30. const hasConstitutionGroupImg = data.value.constitutionGroupImg;
  31. const hasFactorItemRadarImg = data.value[ '中医证素' ]?.length ? data.value.factorItemRadarImg : true;
  32. if ( hasConstitutionGroupImg && hasFactorItemRadarImg ) { next(); }
  33. },
  34. }).onSuccess(({ data: url }) => { data.value.reportURL = url; });
  35. let ReportPreview: Component;
  36. const reportPreviewProps = reactive({
  37. show: false,
  38. title: '',
  39. url: '',
  40. mode: '' as 'img' | 'qr',
  41. });
  42. async function print() {
  43. let url = data.value.reportURL;
  44. if ( !url ) url = await upload();
  45. if (!url) {
  46. Notify.warning(`未获取到报告地址,请联系管理员或重试`);
  47. return;
  48. }
  49. try {
  50. try {
  51. await Bridge.print({ url });
  52. } catch (e) {
  53. window.AIO?.print?.(url)
  54. }
  55. Toast.success(`开始打印`);
  56. } catch ( e ) {
  57. Notify.warning(`打印失败 (${ e.message })`, { duration: 1500 });
  58. setTimeout(() => {
  59. ReportPreview = defineAsyncComponent(() => import('./ReportPreview.vue'));
  60. reportPreviewProps.mode = 'qr';
  61. reportPreviewProps.title = '扫一扫 获取报告';
  62. reportPreviewProps.url = url;
  63. reportPreviewProps.show = true;
  64. }, 1500);
  65. }
  66. }
  67. async function miniProgram() {
  68. let url = data.value.miniProgramURL;
  69. if ( !url ) {
  70. Notify.warning(`未获取到小程序地址,请联系管理员或重试`);
  71. return;
  72. }
  73. panelOpen();
  74. }
  75. const router = useRouter();
  76. function toggle() {
  77. const path = `${ router.currentRoute.value.fullPath }/scheme`.replace(/\/{2,}/g, '/');
  78. router.replace({ path });
  79. }
  80. const panelHeight = ref(0);
  81. const panelProps = reactive({
  82. anchors: [0, window.innerWidth],
  83. contentDraggable: false,
  84. lockScroll: true,
  85. });
  86. const panelOpen = (min?: number) => {
  87. if ( min ) panelProps.anchors[ 0 ] = min;
  88. panelHeight.value = panelProps.anchors[1];
  89. };
  90. const scrollable = computed(() => !data.value.payLock &&
  91. panelHeight.value < panelProps.anchors[ 1 ] || panelHeight.value === 0,
  92. );
  93. </script>
  94. <template>
  95. <div class="report-wrapper">
  96. <div class="page-header flex py-4 px-4">
  97. <div class="grow shrink-0 h-full min-w-16"></div>
  98. <div class="grow-[3] shrink mx-2 flex flex-col justify-center overflow-hidden">
  99. <div class="font-bold text-3xl text-nowrap text-center tracking-wide overflow-ellipsis overflow-hidden">
  100. 健康分析报告
  101. </div>
  102. </div>
  103. <div class="grow shrink-0 h-full min-w-16">
  104. <router-link :to="{ path: '/screen' }" replace>
  105. <img class="size-8 object-scale-down" :src="NavHomeSelect" alt="返回首页" />
  106. </router-link>
  107. </div>
  108. </div>
  109. <div class="page-content flex flex-col overflow-hidden">
  110. <van-skeleton class="flex-auto" title :row="3" :loading>
  111. <div class="flex-auto" :class="{ 'overflow-y-auto': scrollable }">
  112. <div class="my-6 text-primary text-2xl text-center">报告日期:{{ data.date }}</div>
  113. <div class="card m-6 text-lg">
  114. <div class="card__title text-primary text-3xl font-bold"></div>
  115. <div class="card__content flex">
  116. <div class="flex-auto">
  117. <div class="flex items-center my-2">
  118. <span class="text-primary">结果显示您是:</span>
  119. <van-button class="decorate !text-primary-400">{{ data[ '结果' ] }}</van-button>
  120. </div>
  121. <div class="flex items-center my-2" v-if="data[ '程度' ]">
  122. <span class="text-grey">程度:</span>
  123. <span class="px-4 py-2 rounded-lg border border-primary-400 text-primary">{{ data[ '程度' ] }}</span>
  124. </div>
  125. <div class="my-2 text-grey" v-if="data[ '表现' ]">表现:{{ data[ '表现' ] }}</div>
  126. <div class="my-2 text-grey" v-if="data[ '体质' ]">体质:{{ data[ '体质' ] }}</div>
  127. </div>
  128. <div class="flex-none size-48 ml-4">
  129. <img class="size-full object-cover" src="@/assets/images/report-cover.png" alt="封面">
  130. </div>
  131. </div>
  132. </div>
  133. <div class="card m-6 text-lg">
  134. <div class="card__title mb-3 text-primary text-2xl font-bold">体质分析</div>
  135. <div class="card__content">
  136. <PhysiqueChart
  137. :dataset="data['体质图表']"
  138. v-model:snapshot="data.constitutionGroupImg"
  139. />
  140. <div class="my-2 text-primary" v-if="data[ '体质描述' ]">{{ data[ '体质描述' ] }}</div>
  141. <table class="mt-8 mb-2 w-full table-auto border border-collapse border-primary">
  142. <thead>
  143. <tr>
  144. <th class="border border-primary min-w-[140px]"
  145. v-for="(value, i) in data.descriptionsTable.column" :key="i"
  146. v-html="value"
  147. ></th>
  148. </tr>
  149. </thead>
  150. <tbody>
  151. <tr v-for="item in data.descriptionsTable?.data" :key="item[0]">
  152. <td class="py-4 px-2 border border-primary min-w-[140px]"
  153. :class="{'text-grey': i, 'text-primary': !i}"
  154. v-for="(value, i) in item" :key="i"
  155. v-html="value"
  156. ></td>
  157. </tr>
  158. </tbody>
  159. </table>
  160. </div>
  161. </div>
  162. <AnalysisComponent title="舌象分析" v-bind="data.tongue" :cover="[]"></AnalysisComponent>
  163. <AnalysisComponent title="面象分析" v-bind="data.face">
  164. <template #content="{result, cover}">
  165. <div class="card__content flex">
  166. <div class="flex-auto text-grey mt-6">{{ result }}</div>
  167. <div class="flex-none w-2/4 max-h-96 ml-4">
  168. <img class="size-full object-scale-down" v-for="src in cover" :key="src" :src="src" alt="面象">
  169. </div>
  170. </div>
  171. </template>
  172. <template #exception><div><!--空占位符--></div></template>
  173. </AnalysisComponent>
  174. <AnalysisPulseComponent title="脉象分析" v-bind="data.pulse" simple>
  175. <template #exception v-if="hidePulseExceptionTemplate"><div><!--空占位符--></div></template>
  176. </AnalysisPulseComponent>
  177. <div class="card m-6 text-lg" v-if="data['中医证素']?.length">
  178. <div class="card__title mb-3 text-primary text-2xl font-bold">中医证素</div>
  179. <div class="card__content">
  180. <SyndromeChart
  181. :dataset="data['中医证素']"
  182. v-model:snapshot="data.factorItemRadarImg"
  183. />
  184. <table class="mt-8 mb-2 w-full table-auto border border-collapse border-primary">
  185. <tbody>
  186. <tr v-for="item in data['中医证素']" :key="item.label">
  187. <td class="py-4 px-2 border border-primary text-primary text-center" v-html="item.label"></td>
  188. <td class="py-4 px-2 border border-primary text-grey" v-html="item.value"></td>
  189. </tr>
  190. </tbody>
  191. </table>
  192. </div>
  193. </div>
  194. <div class="card m-6 text-lg" v-if="data['中医证型']?.length">
  195. <div class="card__title mb-3 text-primary text-2xl font-bold">中医证型</div>
  196. <div class="card__content">
  197. <div class="my-6 text-grey" v-for="item in data['中医证型']" :key="item.label">
  198. <div class="my-2 text-primary" v-html="item.label"></div>
  199. <div style="text-indent: 2em;" v-html="item.value"></div>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. </van-skeleton>
  205. <div class="flex-none flex justify-between py-2 nav-wrapper" style="background-color: #12312c;">
  206. <div class="m-auto min-w-16 text-center hover:text-primary" v-if="data.scheme" @click="toggle()">
  207. <img :src="NavScheme" alt="调理方案">
  208. <div class="mt-2">调理方案</div>
  209. </div>
  210. <div class="m-auto min-w-16 text-center hover:text-primary" v-if="data.miniProgramURL" @click="miniProgram()">
  211. <img :src="NavMiniProgram" alt="小程序">
  212. <div class="mt-2">小程序</div>
  213. </div>
  214. <div class="m-auto min-w-16 text-center hover:text-primary" @click="print()">
  215. <van-loading v-if="uploading" color="#38ff6e" style="font-size: 24px;" />
  216. <img v-else :src="NavPrint" alt="打印">
  217. <div class="mt-2">打印</div>
  218. </div>
  219. </div>
  220. <Component :is="ReportPreview" v-bind="reportPreviewProps" v-model:show="reportPreviewProps.show"></Component>
  221. <van-floating-panel v-model:height="panelHeight" v-bind="panelProps">
  222. <template #header>
  223. <div class="van-floating-panel__header !justify-between">
  224. <div></div>
  225. <div class="van-floating-panel__header-bar"></div>
  226. <div>
  227. <van-icon v-if="!data.payLock" name="cross" @click.stop="panelHeight = panelProps.anchors[0];" />
  228. </div>
  229. </div>
  230. </template>
  231. <Transition>
  232. <div class="panel-content">
  233. <img
  234. class="size-full object-contain"
  235. v-if="panelHeight === panelProps.anchors[1] || panelHeight > panelProps.anchors[0] * 1.5"
  236. :src="data.miniProgramURL"
  237. alt="小程序码"
  238. />
  239. <div class="flex justify-center items-center" v-else @click="panelOpen()">
  240. <img class="h-10 w-10" src="@/assets/images/mini-program.svg" alt="小程序" />
  241. <span class="text-lg ml-2">小程序</span>
  242. </div>
  243. </div>
  244. </Transition>
  245. </van-floating-panel>
  246. </div>
  247. </div>
  248. </template>
  249. <style scoped lang="scss">
  250. .van-button.decorate {
  251. font-size: 20px;
  252. height: 62px;
  253. width: 240px;
  254. background-size: 80%;
  255. letter-spacing: 2px;
  256. }
  257. .text-grey {
  258. color: #e3e3e3;
  259. }
  260. .report-wrapper {
  261. .panel-content {
  262. padding: 0 var(--van-floating-panel-header-height) var(--van-floating-panel-header-height);
  263. }
  264. .v-enter-active,
  265. .v-leave-active {
  266. transition: opacity 0.5s ease;
  267. }
  268. .v-enter-from,
  269. .v-leave-to {
  270. opacity: 0;
  271. }
  272. }
  273. .overflow-y-auto {
  274. overflow-y: auto;
  275. }
  276. </style>
  277. <style lang="scss">
  278. .report-wrapper .card {
  279. padding: 24px;
  280. border-radius: 24px;
  281. box-shadow: inset 0 0 80px 0 #34a76b60;
  282. }
  283. .nav-wrapper {
  284. img {
  285. margin: auto;
  286. width: 36px;
  287. height: 36px;
  288. object-fit: scale-down;
  289. }
  290. }
  291. </style>