Follow.vue 19 KB

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