Follow.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <script setup lang="ts">
  2. import type { TaskModel } from '@/model/follow.model';
  3. import { FillFollowContentMethod, UploadIFile, FollowContentMethod } from '@/request/api/follow.api';
  4. import { getDictionaryMethod } from '@/request/api/dictionary.api';
  5. import { useRequest } from 'alova/client';
  6. import { PlusOutlined } from '@ant-design/icons-vue';
  7. import { notification } from 'ant-design-vue';
  8. import type { UploadFile } from 'ant-design-vue/es/upload/interface';
  9. import { VxeUI } from 'vxe-pc-ui';
  10. type FormModel = Partial<TaskModel>;
  11. const props = defineProps<{ data: FormModel }>();
  12. const emits = defineEmits<{
  13. submit: [data?: TaskModel];
  14. }>();
  15. const {
  16. data: contentArr,
  17. loading,
  18. send: load,
  19. } = useRequest(() => FollowContentMethod(props.data), {
  20. initialData: [],
  21. }).onSuccess(({ data }) => {
  22. const index = data.findIndex((item) => item.id === props.data.id);
  23. if (index > -1) {
  24. changeTab(data[index], index);
  25. }
  26. });
  27. const statusList = ref<string[]>([]);
  28. onBeforeMount(() => {
  29. getDictionaryMethod('followup_syndrome_change').then((res) => {
  30. statusList.value = res;
  31. });
  32. });
  33. const activeKey = ref<number>();
  34. const activeIndex = ref<number>();
  35. const activeObj = ref<any>({ fillin: {}, symptomsData: [] });
  36. // 切换侧边栏任务
  37. const changeTab = (data: any, index: number) => {
  38. activeKey.value = data.id;
  39. activeObj.value = { ...data, fillin: { ...data.fillin } };
  40. activeIndex.value = index;
  41. activeObj.value.symptomsData = [];
  42. downImageList.value = [];
  43. const upImg = data.fillin?.upImg;
  44. const downImg = data.fillin?.downImg;
  45. const faceImg = data.fillin?.faceImg;
  46. if (activeObj.value?.syndromeList && activeObj.value.syndromeList.length > 0) {
  47. activeObj.value.syndromeList.forEach((syndrome) => {
  48. activeObj.value.symptomsData.push({ name: syndrome });
  49. });
  50. activeObj.value.symptomsData.forEach((item) => {
  51. item.child = statusList.value;
  52. item.selectedValue = '';
  53. item.selectedId = null;
  54. item.id = data.id;
  55. });
  56. }
  57. symptomsValue.value.parent = '';
  58. upImgList.value = upImg
  59. ? [
  60. {
  61. uid: '-1',
  62. status: 'done',
  63. url: upImg,
  64. thumbUrl: upImg,
  65. response: { url: upImg },
  66. },
  67. ]
  68. : [];
  69. downImageList.value = downImg
  70. ? [
  71. {
  72. uid: '-1',
  73. status: 'done',
  74. url: downImg,
  75. thumbUrl: downImg,
  76. response: { url: downImg },
  77. },
  78. ]
  79. : [];
  80. faceImageList.value = faceImg
  81. ? [
  82. {
  83. uid: '-1',
  84. status: 'done',
  85. url: faceImg,
  86. thumbUrl: faceImg,
  87. response: { url: faceImg },
  88. },
  89. ]
  90. : [];
  91. uploadProps.showRemoveIcon = data.progress === '1';
  92. };
  93. // 存储所有选择的症状
  94. const selectedSymptomsList = ref<{ name: string; value: string }[]>([]);
  95. // 存储症状
  96. const symptomsList = ref<{ name: string; type: string }[]>([]);
  97. // 症状选择的值
  98. const symptomsValue = ref({
  99. parent: '',
  100. child: '',
  101. });
  102. // 处理父级点击
  103. const handleParentClick = (name: string) => {
  104. if (symptomsValue.value.parent === name) {
  105. // 如果点击的是当前选中的父级,则清空选择
  106. symptomsValue.value = {
  107. parent: '',
  108. child: '',
  109. };
  110. } else {
  111. // 选择新的父级
  112. symptomsValue.value = {
  113. parent: name,
  114. child: '',
  115. };
  116. }
  117. };
  118. // 处理子级选择变化
  119. const handleChildChange = (e: any) => {
  120. const selectedValue = e.target.value;
  121. const currentParent = symptomsValue.value.parent;
  122. // 找到当前症状
  123. const symptom = activeObj.value.symptomsData.find((item) => item.name === currentParent);
  124. if (symptom) {
  125. // 如果点击的是当前选中的值,则取消选择
  126. if (symptom.selectedId === selectedValue) {
  127. // 取消选择
  128. symptom.selectedValue = '';
  129. symptom.selectedId = null;
  130. // 从已选择的症状列表中移除
  131. const index = selectedSymptomsList.value.findIndex((item) => item.name === currentParent);
  132. if (index > -1) {
  133. selectedSymptomsList.value.splice(index, 1);
  134. }
  135. } else {
  136. // 选择新的值
  137. const child = symptom.child.find((item) => item.value === selectedValue);
  138. if (child) {
  139. symptom.selectedValue = selectedValue;
  140. symptom.selectedId = selectedValue;
  141. // 更新已选择的症状列表
  142. const existingIndex = selectedSymptomsList.value.findIndex((item) => item.name === currentParent);
  143. if (existingIndex > -1) {
  144. selectedSymptomsList.value[existingIndex].value = selectedValue;
  145. } else {
  146. selectedSymptomsList.value.push({
  147. name: currentParent,
  148. value: selectedValue,
  149. });
  150. }
  151. }
  152. }
  153. }
  154. symptomsValue.value.parent = '';
  155. };
  156. // 是否出现新症状
  157. const selectSymptomsData = reactive([
  158. { name: '有', id: 'Y' },
  159. { name: '没有', id: 'N' },
  160. ]);
  161. const uploadProps = reactive({ showRemoveIcon: true });
  162. const changeTag = (item: any) => {
  163. activeObj.value.fillin.isHaveNewSyndrome = item.id;
  164. };
  165. const upImgList = ref<UploadFile[]>([]);
  166. const downImageList = ref<UploadFile[]>([]);
  167. const faceImageList = ref<UploadFile[]>([]);
  168. // 预览图片
  169. const handlePreview = async (file: UploadFile) => {
  170. previewImg.value = file.response?.url ?? file.thumbUrl;
  171. visible.value = true;
  172. };
  173. // 填写随访内容
  174. function subFollowContent() {
  175. activeObj.value.fillin.upImg = upImgList.value[0]?.response?.url;
  176. activeObj.value.fillin.downImg = downImageList.value[0]?.response?.url;
  177. activeObj.value.fillin.faceImg = faceImageList.value[0]?.response?.url;
  178. symptomsList.value = [];
  179. activeObj.value.symptomsData.forEach((item) => {
  180. symptomsList.value.push({ name: item.name, type: item.selectedValue });
  181. });
  182. activeObj.value.fillin.symptomsList = symptomsList.value;
  183. FillFollowContentMethod(activeObj.value).then(() => {
  184. notification.success({
  185. message: '',
  186. description: '提交成功!',
  187. });
  188. emits('submit');
  189. load();
  190. });
  191. }
  192. // 取消提交
  193. function cancelFollowContent() {
  194. VxeUI.modal.close(`follow-modal`);
  195. }
  196. function customUpload(e: any) {
  197. // uploadApi 你的二次封装上传接口
  198. UploadIFile(e.file)
  199. .then((res) => {
  200. // 调用实例的成功方法通知组件该文件上传成功
  201. e.onSuccess(res, e);
  202. })
  203. .catch((err) => {
  204. // 调用实例的失败方法通知组件该文件上传失败
  205. e.onError(err);
  206. });
  207. }
  208. const visible = ref<boolean>(false);
  209. const setVisible = (value: boolean): void => {
  210. visible.value = value;
  211. };
  212. const previewImg = ref<string>('');
  213. </script>
  214. <template>
  215. <div>
  216. <div class="flex font-bold">
  217. <!-- 左边-->
  218. <div class="animated-vertical-tabs">
  219. <div class="tab-list">
  220. <div class="font-bold h-8 pt-3 mb-3 ml-2">{{ activeObj?.followupPlanName }}</div>
  221. <div
  222. style="font-size: 14px"
  223. v-for="(content, index) in contentArr"
  224. :key="content.id"
  225. class="tab-item mb-3"
  226. :class="{ active: activeKey === content.id }"
  227. @click="changeTab(content, index)"
  228. >
  229. <div>{{ content.followupTaskName }}</div>
  230. <span class="tab-label">{{ content.arrangeTime }}</span>
  231. <div :class="content.progress == 1 ? 'text-red-600' : content.progress == 2 ? 'text-green-900' : content.progress == 3 ? 'text-blue-900' : ''">
  232. {{ content.progress === '1' ? '未完成' : content.progress === '2' ? '已完成' : '未开始' }}
  233. </div>
  234. </div>
  235. </div>
  236. </div>
  237. <!-- 右边-->
  238. <div :key="activeObj.id">
  239. <div class="h-8 text-center">
  240. {{ activeObj?.followupTaskName }}
  241. </div>
  242. <div class="mb-2 text-center">预定随访时间:{{ activeObj?.arrangeTime }}</div>
  243. <div class="mb-2 ml-2">
  244. 您好,您于<span class="text-blue-600">【{{ activeObj?.medicalTime }}】</span>在我院<span class="text-blue-600">【{{ activeObj?.institutionName }}】</span>因为<span
  245. class="text-blue-600"
  246. >【{{ activeObj?.diagnosis }}】</span
  247. >就诊。接下来我们将对您进行一个随访,请根据目前的实际情况回答。
  248. </div>
  249. <div
  250. class="border-1 border-solid border-gray:50 pl-2 pd-10 ml-2"
  251. v-if="(activeObj?.progress === '1' && activeObj?.symptomsData?.length > 0) || (activeObj.progress === '2' && activeObj?.fillin?.symptomsList?.length > 0)"
  252. >
  253. <div class="mb-3 border-b-0">
  254. 1、请问您的症状有没有<span class="text-red-600">好转</span>或者<span class="text-red-600">恶化</span>?请先点击症状,再选择好转还是恶化。(没有操作的症状默认没有变化)
  255. </div>
  256. <div class="ml-4" v-if="activeObj.progress === '1'">
  257. <!-- 症状选择器 -->
  258. <div class="symptom-container flex flex-wrap">
  259. <div v-for="item in activeObj?.symptomsData" :key="item.name" class="symptom-item">
  260. <div class="symptom-button" @click="activeObj.progress !== '0' ? handleParentClick(item.name) : ''" :class="{ 'disabled': activeObj.progress === '0' }">
  261. <span>{{ item.name }}</span>
  262. <span v-if="item.selectedValue" class="selected-value">: {{ item.selectedValue }}</span>
  263. </div>
  264. <div v-show="symptomsValue.parent === item.name" class="symptom-options">
  265. <a-radio-group :model-value="item.selectedId" @change="activeObj.progress !== '0' ? handleChildChange : ''" class="flex flex-wrap" :disabled="activeObj.progress === '0'">
  266. <a-radio :value="tag.value" v-for="tag in item.child" :key="tag.value" class="mr-4">
  267. {{ tag.label }}
  268. </a-radio>
  269. </a-radio-group>
  270. </div>
  271. </div>
  272. </div>
  273. </div>
  274. <!-- 已经评估过 -->
  275. <div v-else>
  276. <div class="symptom-container flex flex-wrap">
  277. <div v-for="item in activeObj?.fillin?.symptomsList" :key="item.name" class="symptom-item">
  278. <div class="symptom-button">
  279. <span>{{ item.name }}</span>
  280. <span class="selected-value">: {{ item.type }}</span>
  281. </div>
  282. </div>
  283. </div>
  284. </div>
  285. </div>
  286. <!-- 第二个-->
  287. <div class="border-1 border-solid border-gray:50 pl-2 pd-10 ml-2">
  288. <div class="mb-3">2、请问有没有出现<span class="text-red-600">新</span>的症状?</div>
  289. <div class="mb-8 ml-4 flex">
  290. <div v-for="symptoms in selectSymptomsData" :key="symptoms.name" class="mr-4" @click="activeObj?.progress === '1' ? changeTag(symptoms) : ''">
  291. <div>
  292. <div class="border-solid b-1 w-20 text-center" :class="[activeObj.fillin.isHaveNewSyndrome === symptoms.id ? 'bg-blue text-#fff' : '', activeObj.progress === '0' ? 'disabled' : '']">
  293. {{ symptoms.name }}
  294. </div>
  295. </div>
  296. </div>
  297. </div>
  298. </div>
  299. <!-- 第三个-->
  300. <div class="border-1 border-solid border-gray:50 pl-2 pd-10 ml-2" v-if="activeObj.fillin?.isHaveNewSyndrome === 'Y'">
  301. <div class="mb-3">3、请描述新的症状</div>
  302. <div class="mb-4 ml-4">
  303. <a-input v-model:value="activeObj.fillin.newSyndrome" placeholder="请输入" :auto-size="{ minRows: 2, maxRows: 5 }" :disabled="activeObj.progress === '2' || activeObj.progress === '0'" />
  304. </div>
  305. </div>
  306. <!-- 第四个-->
  307. <div class="border-1 border-solid border-gray:50 pl-2 pd-10 ml-2">
  308. <div class="mb-3">4、如果有其他情况,请留言</div>
  309. <div class="mb-4 ml-4">
  310. <a-input v-model:value="activeObj.fillin.otherDesc" placeholder="请输入" :auto-size="{ minRows: 2, maxRows: 5 }" :disabled="activeObj.progress === '2' || activeObj.progress === '0'" />
  311. </div>
  312. </div>
  313. <!-- 第五个-->
  314. <div class="border-1 border-solid border-gray:50 pl-2 pd-10 ml-2">
  315. <div class="mb-3">5、为了医生更好地了解您的恢复情况,需要您上传舌面象照片</div>
  316. <div class="mb-4 ml-3">
  317. <!-- 上传图片-->
  318. <div class="flex">
  319. <!-- 舌面-->
  320. <div class="flex flex-col items-center mr-4">
  321. <a-upload
  322. :showUploadList="uploadProps"
  323. v-model:file-list="upImgList"
  324. list-type="picture-card"
  325. @preview="handlePreview"
  326. :maxCount="1"
  327. :customRequest="customUpload"
  328. :disabled="activeObj.progress === '2' || activeObj.progress === '0'"
  329. >
  330. <div v-if="upImgList.length < 1">
  331. <plus-outlined />
  332. </div>
  333. </a-upload>
  334. <div class="font-bold">舌面</div>
  335. </div>
  336. <!-- 舌下-->
  337. <div class="flex flex-col items-center mr-4">
  338. <a-upload
  339. :showUploadList="uploadProps"
  340. v-model:file-list="downImageList"
  341. list-type="picture-card"
  342. @preview="handlePreview"
  343. :maxCount="1"
  344. :customRequest="customUpload"
  345. :disabled="activeObj.progress === '2' || activeObj.progress === '0'"
  346. >
  347. <div v-if="downImageList.length < 1">
  348. <plus-outlined />
  349. </div>
  350. </a-upload>
  351. <div class="font-bold">舌下</div>
  352. </div>
  353. <!-- 面部-->
  354. <div class="flex flex-col items-center mr-4">
  355. <a-upload
  356. :showUploadList="uploadProps"
  357. v-model:file-list="faceImageList"
  358. list-type="picture-card"
  359. @preview="handlePreview"
  360. :maxCount="1"
  361. :customRequest="customUpload"
  362. :disabled="activeObj.progress === '2' || activeObj.progress === '0'"
  363. >
  364. <div v-if="faceImageList.length < 1">
  365. <plus-outlined />
  366. </div>
  367. </a-upload>
  368. <div class="font-bold">面部</div>
  369. </div>
  370. <!-- --------end-->
  371. </div>
  372. <a-image
  373. :width="200"
  374. :style="{ display: 'none' }"
  375. :preview="{
  376. visible,
  377. onVisibleChange: setVisible,
  378. }"
  379. :src="previewImg"
  380. />
  381. </div>
  382. </div>
  383. <!-- -->
  384. <div class="ml-2 mt-1">
  385. 感谢您的配合,为了更好地了解您的回复情况,我们将会在<span class="text-blue-600"> {{ activeObj?.arrangeTime }} </span
  386. >再次对您进行随访,届时请点击随访链接参与,再次感谢您!
  387. </div>
  388. </div>
  389. </div>
  390. <div class="flex items-center justify-center mt-6 mb-6">
  391. <a-button size="small" class="mr-4" @click="cancelFollowContent">取消</a-button>
  392. <a-button type="primary" size="small" @click="subFollowContent" v-show="activeObj.progress === '1'">提交 </a-button>
  393. </div>
  394. </div>
  395. </template>
  396. <style scoped lang="scss">
  397. .ant-upload-select-picture-card i {
  398. font-size: 32px;
  399. color: #999;
  400. }
  401. .ant-upload-select-picture-card .ant-upload-text {
  402. margin-top: 8px;
  403. color: #666;
  404. }
  405. .mesh-grid {
  406. border-collapse: collapse;
  407. }
  408. .mesh-grid td {
  409. border: 1px solid black;
  410. width: 100px;
  411. padding: 20px 20px;
  412. text-align: center;
  413. }
  414. .animated-vertical-tabs {
  415. display: flex;
  416. height: 730px;
  417. width: 17%;
  418. overflow: auto;
  419. }
  420. .tab-list {
  421. border-right: 1px solid #f0f0f0;
  422. }
  423. .tab-item {
  424. position: relative;
  425. padding: 10px;
  426. cursor: pointer;
  427. transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  428. font-weight: bold;
  429. }
  430. .tab-item:hover {
  431. background-color: rgba(24, 144, 255, 0.06);
  432. }
  433. .tab-item.active {
  434. background: lightgray;
  435. }
  436. .tab-indicator {
  437. position: absolute;
  438. top: 0;
  439. right: -1px;
  440. width: 2px;
  441. height: 100%;
  442. background-color: #1890ff;
  443. transform: scaleY(0);
  444. transform-origin: center top;
  445. transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  446. }
  447. .tab-item.active .tab-indicator {
  448. transform: scaleY(1);
  449. }
  450. .tab-content {
  451. flex: 1;
  452. padding: 0 24px;
  453. overflow: auto;
  454. }
  455. .fade-enter-active,
  456. .fade-leave-active {
  457. transition: opacity 0.3s ease;
  458. }
  459. .fade-enter-from,
  460. .fade-leave-to {
  461. opacity: 0;
  462. }
  463. // 症状选择器样式
  464. .symptom-container {
  465. .symptom-item {
  466. position: relative;
  467. margin-right: 16px;
  468. margin-bottom: 16px;
  469. .symptom-button {
  470. min-width: 100px;
  471. padding: 8px 16px;
  472. border: 1px solid #d9d9d9;
  473. border-radius: 4px;
  474. text-align: center;
  475. cursor: pointer;
  476. transition: all 0.3s;
  477. background: #fff;
  478. display: flex;
  479. align-items: center;
  480. justify-content: center;
  481. &:hover {
  482. border-color: #40a9ff;
  483. color: #40a9ff;
  484. }
  485. &.active {
  486. border-color: #1890ff;
  487. color: #1890ff;
  488. background: rgba(24, 144, 255, 0.1);
  489. }
  490. &.has-value {
  491. border-color: #52c41a;
  492. color: #52c41a;
  493. }
  494. .selected-value {
  495. margin-left: 4px;
  496. font-weight: 500;
  497. }
  498. }
  499. .symptom-options {
  500. position: absolute;
  501. top: 100%;
  502. left: 0;
  503. z-index: 1;
  504. margin-top: 8px;
  505. padding: 8px;
  506. background: #fff;
  507. border: 1px solid #d9d9d9;
  508. border-radius: 4px;
  509. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  510. animation: fadeIn 0.3s;
  511. .ant-radio-group {
  512. display: flex;
  513. flex-direction: column;
  514. gap: 8px;
  515. }
  516. .ant-radio-wrapper {
  517. padding: 4px 8px;
  518. border-radius: 4px;
  519. transition: all 0.3s;
  520. &:hover {
  521. background: rgba(24, 144, 255, 0.1);
  522. }
  523. }
  524. }
  525. }
  526. }
  527. // 已选择症状标签样式
  528. .selected-symptoms {
  529. display: flex;
  530. flex-wrap: wrap;
  531. gap: 8px;
  532. margin-bottom: 16px;
  533. .ant-tag {
  534. margin: 0;
  535. padding: 4px 8px;
  536. border-radius: 4px;
  537. transition: all 0.3s;
  538. &:hover {
  539. background: rgba(24, 144, 255, 0.1);
  540. }
  541. }
  542. }
  543. // 动画
  544. @keyframes fadeIn {
  545. from {
  546. opacity: 0;
  547. transform: translateY(-10px);
  548. }
  549. to {
  550. opacity: 1;
  551. transform: translateY(0);
  552. }
  553. }
  554. // 整体布局优化
  555. .border-1 {
  556. margin-bottom: 16px;
  557. padding: 16px;
  558. border-radius: 8px;
  559. background: #fff;
  560. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  561. }
  562. .mb-8 {
  563. margin-bottom: 32px;
  564. }
  565. .ml-4 {
  566. margin-left: 16px;
  567. }
  568. .text-blue-600 {
  569. color: #1890ff;
  570. }
  571. .text-red-600 {
  572. color: #ff4d4f;
  573. }
  574. .font-bold {
  575. font-weight: 600;
  576. }
  577. // 禁用状态样式
  578. .disabled {
  579. opacity: 0.5;
  580. cursor: not-allowed !important;
  581. pointer-events: none;
  582. }
  583. .symptom-button.disabled {
  584. background-color: #f5f5f5;
  585. color: #999;
  586. border-color: #d9d9d9;
  587. cursor: not-allowed;
  588. }
  589. .symptom-button.disabled:hover {
  590. border-color: #d9d9d9;
  591. color: #999;
  592. background-color: #f5f5f5;
  593. }
  594. </style>