issueService.vue 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. <script setup lang="ts">
  2. import { useRoute } from 'vue-router';
  3. import { ref, computed, nextTick, h, watch, onMounted } from 'vue';
  4. import { MinusCircleOutlined, EyeOutlined } from '@ant-design/icons-vue';
  5. import { notification } from 'ant-design-vue';
  6. import { message } from 'ant-design-vue';
  7. import type { OpenConditioningSchemeModel, SystemCwModel } from '@/model/care.model';
  8. import PatientTagWidget from '@/widgets/PatientTagWidget.vue';
  9. import AcupointEdit from '@/service/AcupointEdit.vue';
  10. import ServiceDetail from '@/service/ServiceDetail.vue';
  11. import {
  12. getCpContentListMethod,
  13. getPatientListMethod,
  14. getConditioningRecordDetailMethod,
  15. getPatientConditioningRecordMethod,
  16. addConditioningSchemeMethod,
  17. getCpDetailMethod,
  18. getProvinceMethod,
  19. getCityMethod,
  20. getAreaMethod,
  21. getAvailableCwMethod,
  22. } from '@/request/api/care.api';
  23. import { patientMethod, patientTags } from '@/request/api/patient.api';
  24. import { useRequest, usePagination } from 'alova/client';
  25. import ServicePackageDetail from '@/service/ServicePackageDetail.vue';
  26. import { VxeUI } from 'vxe-pc-ui';
  27. import type { PatientTagModel } from '@/model/patient.model';
  28. import dayjs from 'dayjs';
  29. type FollowModel = Partial<OpenConditioningSchemeModel>;
  30. const props = defineProps<{ data: FollowModel }>();
  31. const form = reactive<FollowModel>({
  32. id: 0,
  33. patientId: '',
  34. patientName: '',
  35. patientSex: '',
  36. patientAge: 0,
  37. diagnosis: '',
  38. symptom: '',
  39. conditioningWrapId: '',
  40. conditioningWrapName: '',
  41. estimatedStartDate: '',
  42. estimatedEndDate: '',
  43. isDelivery: null,
  44. cost: 0,
  45. healthAnalysisReport: {
  46. willillStateName: '',
  47. willillDegreeName: '',
  48. willillSocialName: '',
  49. willillFunctionName: '',
  50. },
  51. patientMedicalRecord: {
  52. patientId: '', // 患者ID
  53. institutionId: '', // 机构ID
  54. institutionName: '', // 机构名称
  55. diagnosis: '', // 疾病
  56. symptom: '', // 症型
  57. syndrome: '', // 证状
  58. },
  59. items: [],
  60. provinceName: '',
  61. cityName: '',
  62. districtName: '',
  63. areaName: '',
  64. detailAddress: '',
  65. phone: '',
  66. progress: '0',
  67. photo: '',
  68. });
  69. const searchName = ref('');
  70. const selectedPackage = ref('');
  71. const currentSelectedPackage = ref<any>(null);
  72. const deliveryChecked = ref(true);
  73. const address = ref({
  74. province: '',
  75. city: '',
  76. district: '',
  77. detail: '',
  78. phone: '',
  79. });
  80. // 机构调理包
  81. const lifeCwData = ref([]);
  82. // 获取可用的服务包
  83. const suggestCwData = ref<string[]>([]);
  84. async function loadAvailableCw(patientId: string) {
  85. const res: any = await getAvailableCwMethod(patientId);
  86. if (res.length > 0) {
  87. lifeCwData.value = res;
  88. // 筛选出可推荐的
  89. suggestCwData.value = lifeCwData.value.filter((item: any) => item.isSuggest);
  90. }
  91. }
  92. export interface PatientModel {
  93. id: string;
  94. patientId: string;
  95. patientName: string;
  96. patientSex: string;
  97. patientAge: number;
  98. diagnosis: string;
  99. symptom: string;
  100. healthAnalysisReport: {
  101. willillStateName: string;
  102. willillDegreeName: string;
  103. willillSocialName: string;
  104. willillFunctionName: string;
  105. };
  106. constitutionGroupName: string;
  107. status?: string;
  108. }
  109. // 患者列表
  110. const patients = ref([]);
  111. // 患者详细信息
  112. interface CpDetail {
  113. womenSpecialPeriod?: string;
  114. cardno?: string;
  115. }
  116. let cpDetail = ref<CpDetail>({});
  117. async function getCpDetail(id: string) {
  118. await patientMethod(id).then((res2) => {
  119. cpDetail.value = res2;
  120. });
  121. }
  122. interface PatientRecord {
  123. id: string;
  124. estimatedStartDate: string;
  125. estimatedEndDate: string;
  126. }
  127. const patientRecord = ref<PatientRecord[]>([]);
  128. // 获取患者调养记录
  129. function getPatientRecord(id: any) {
  130. patientRecord.value = [];
  131. getPatientConditioningRecordMethod(id).then((res) => {
  132. if (res && res.length > 0) {
  133. patientRecord.value = res;
  134. }
  135. });
  136. }
  137. // const report=ref({})
  138. async function getCpRecordDetail(id: string) {
  139. await getCpDetailMethod({ id }).then((res) => {
  140. formData.items = res?.items ?? [];
  141. // form = res;
  142. form.conditioningWrapName = res?.conditioningWrapName;
  143. form.estimatedStartDate = res?.estimatedStartDate;
  144. form.estimatedEndDate = res?.estimatedEndDate;
  145. form.provinceName = res?.provinceName;
  146. form.cityName = res?.cityName;
  147. form.districtName = res?.districtName;
  148. form.areaName = res?.areaName;
  149. form.detailAddress = res?.detailAddress;
  150. form.phone = res?.phone;
  151. form.healthAnalysisReport = res.healthAnalysisReport;
  152. });
  153. }
  154. async function getPatientList(id: string) {
  155. console.log(id, '切换');
  156. if (id) {
  157. getCpDetail(id);
  158. loadTags(id);
  159. getPatientRecord(id);
  160. }
  161. }
  162. const route = useRoute();
  163. onMounted(async () => {
  164. const id = Number(route.query.id); // 获取查询参数 id
  165. // 获取患者列表
  166. const res: any = await getPatientListMethod();
  167. if (res && res.length > 0) {
  168. if (id) {
  169. let index = res.findIndex((item: any) => item.patientId === id);
  170. currentPatient.value = res[index];
  171. } else {
  172. currentPatient.value = res[0];
  173. }
  174. patients.value = res;
  175. }
  176. if (currentPatient.value?.patientId) {
  177. // 获取患者列表
  178. await getPatientList(currentPatient.value?.patientId || '');
  179. // 获取服务包选择列表
  180. await loadAvailableCw(currentPatient.value?.patientId || '');
  181. }
  182. if (currentPatient.value?.id) {
  183. // 获取调养记录
  184. await getCpRecordDetail(currentPatient.value?.id || '');
  185. }
  186. // 获取省份
  187. await loadProvinces();
  188. });
  189. // 患者标签
  190. const tags = ref<PatientTagModel>({ id: '', tags: [] });
  191. function loadTags(patientId: string) {
  192. patientTags(patientId).then((res) => {
  193. tags.value = res;
  194. });
  195. }
  196. // 展示的患者数据
  197. const filteredPatients: any = computed(() => patients.value.filter((p: any) => p.patientName.includes(searchName.value)));
  198. // 默认显示第一个患者
  199. const currentPatient = ref<PatientModel>();
  200. // 点击切换患者
  201. async function selectPatient(item: any) {
  202. currentPatient.value = item;
  203. // 获取患者信息
  204. await getPatientList(item.patientId);
  205. await loadAvailableCw(item.patientId || '');
  206. // 获取患者调养记录
  207. await getCpRecordDetail(item.id);
  208. // 清空服务包选择
  209. selectedPackage.value = '';
  210. currentSelectedPackage.value = null;
  211. }
  212. // 打开调养记录
  213. function openRecord(item: any) {
  214. const types = 'record';
  215. VxeUI.modal.open({
  216. id: 'servicePackageDetail-modal',
  217. title: '调养记录',
  218. height: 700,
  219. width: 1200,
  220. escClosable: true,
  221. destroyOnClose: true,
  222. slots: {
  223. default() {
  224. return h(ServicePackageDetail, <any>{
  225. data: { ...item, types },
  226. });
  227. },
  228. },
  229. });
  230. }
  231. const formData = reactive<FollowModel>({
  232. price: 0, //总计价格
  233. cwPatientMatchRules: [
  234. {
  235. diagnoseDiseaseNames: [],
  236. diagnoseSyndromeNames: [],
  237. constitutionGroupNames: [],
  238. diagnoseDisease: {
  239. id: '',
  240. code: '',
  241. name: '',
  242. optionalWords: '',
  243. attributes: [],
  244. children: [],
  245. },
  246. diagnoseSyndrome: {
  247. code: '',
  248. name: '',
  249. analysis: '',
  250. remark: '',
  251. },
  252. constitutionGroup: {
  253. id: '',
  254. code: '',
  255. name: '',
  256. definition: '',
  257. remark: '',
  258. },
  259. },
  260. ], // 适用情况
  261. items: [], // Initialize as empty array
  262. });
  263. const emptyRow = {
  264. id: '',
  265. conditioningWrapId: '',
  266. conditioningProgramId: 0,
  267. days: '',
  268. frequencyType: '',
  269. frequencyMeasure: '',
  270. totalMeasure: '',
  271. totalPrice: '',
  272. initialDay: '',
  273. conditioningProgramDetail: {
  274. id: '',
  275. name: '',
  276. conditioningProgramType: '',
  277. pricingType: '',
  278. cpFixedPricingRule: {
  279. unitPrice: 0,
  280. pricingUnit: '',
  281. convertDose: 0,
  282. convertUnit: '',
  283. },
  284. cpDynamicPricingRule: [],
  285. cpMedicines: [],
  286. effect: '',
  287. isOffline: null,
  288. isDelivery: null,
  289. photo: '',
  290. institutionId: '',
  291. institutionName: '',
  292. remark: '',
  293. },
  294. cwcpAcuMeridians: [],
  295. cwcpAcuPoints: [],
  296. conditioningProgramSupplierName: '',
  297. };
  298. const projectSearch = ref('');
  299. const showProjectPopover = ref(false);
  300. const displayTableData = computed(() => {
  301. return [...(formData.items ?? []), { ...emptyRow }];
  302. });
  303. const isShowDelivery = ref<boolean>(false);
  304. // 监听 displayTableData 的变化
  305. watch(displayTableData, (newValue, oldValue) => {
  306. console.log('displayTableData 变化了:', newValue);
  307. if (newValue.length > 0) {
  308. isShowDelivery.value = newValue.some((item) => {
  309. return item.conditioningProgramDetail?.isDelivery === 'Y';
  310. });
  311. }
  312. });
  313. const totalPrice = computed(() => {
  314. return (formData.items ?? []).reduce((sum, row) => {
  315. const price = Number(row?.totalPrice) || 0;
  316. return sum + price;
  317. }, 0);
  318. });
  319. function onSelectProject({ row }: any) {
  320. if ((formData.items ?? []).some((item) => item.conditioningProgramDetail?.name === row.name)) {
  321. message.warning('不能重复添加该项目');
  322. return;
  323. }
  324. // 添加新行到主表格
  325. if (!formData.items) formData.items = [];
  326. formData.items.push({
  327. id: '',
  328. conditioningProgramId: row.id || '',
  329. // conditioningProgramId: 0,
  330. days: '',
  331. frequencyType: '',
  332. frequencyMeasure: '',
  333. totalMeasure: '',
  334. totalPrice: '',
  335. initialDay: '',
  336. cwcpAcuMeridians: [
  337. {
  338. id: 0,
  339. name: '',
  340. code: '',
  341. type: '',
  342. photo: '',
  343. },
  344. ],
  345. cwcpAcuPoints: [
  346. {
  347. id: 0,
  348. name: '',
  349. code: '',
  350. type: '',
  351. merName: '',
  352. photo: '',
  353. },
  354. ],
  355. conditioningProgramDetail: {
  356. ...row,
  357. id: row.id || '',
  358. name: row.name || '',
  359. conditioningProgramType: row.conditioningProgramType || '',
  360. effect: row.effect || '',
  361. pricingType: row.pricingType || '',
  362. cpFixedPricingRule: {
  363. unitPrice: row?.cpFixedPricingRule?.unitPrice || 0,
  364. pricingUnit: row?.cpFixedPricingRule?.pricingUnit || '',
  365. convertDose: row?.cpFixedPricingRule?.convertDose || 0,
  366. convertUnit: row?.cpFixedPricingRule?.convertUnit || '',
  367. },
  368. cpDynamicPricingRule: row?.cpDynamicPricingRule || [],
  369. cpMedicines: row?.cpMedicines || [],
  370. isOffline: row?.isOffline || null,
  371. isDelivery: row?.isDelivery || null,
  372. photo: row?.photo || '',
  373. conditioningProgramSupplierName: row?.conditioningProgramSupplierName || '',
  374. },
  375. remark: '',
  376. });
  377. // 关闭弹窗
  378. showProjectPopover.value = false;
  379. // 清空搜索
  380. projectSearch.value = '';
  381. }
  382. function removeTableRow(idx: number) {
  383. if (idx < (formData.items ?? []).length) {
  384. formData.items?.splice(idx, 1);
  385. }
  386. }
  387. function onPreview(row) {
  388. if (row.conditioningProgramDetail.id) {
  389. // 这里写你的预览逻辑
  390. VxeUI.modal.open({
  391. title: `预览`,
  392. height: 600,
  393. width: 950,
  394. escClosable: true,
  395. destroyOnClose: true,
  396. id: `preview-modal`,
  397. remember: true,
  398. storage: true,
  399. slots: {
  400. default() {
  401. return h(ServiceDetail, {
  402. data: row.conditioningProgramDetail,
  403. onSubmit(data: SystemCwModel) {
  404. VxeUI.modal.close(`preview-modal`);
  405. },
  406. });
  407. },
  408. },
  409. });
  410. } else {
  411. message.warning('请先添加服务包');
  412. }
  413. }
  414. function detailPreview(row) {
  415. if (row.id) {
  416. // 这里写你的预览逻辑
  417. VxeUI.modal.open({
  418. title: `预览`,
  419. height: 600,
  420. width: 950,
  421. escClosable: true,
  422. destroyOnClose: true,
  423. id: `preview-modal`,
  424. remember: true,
  425. storage: true,
  426. slots: {
  427. default() {
  428. return h(ServiceDetail, {
  429. data: row,
  430. onSubmit: (data) => {
  431. VxeUI.modal.close(`preview-modal`);
  432. },
  433. });
  434. },
  435. },
  436. });
  437. showProjectPopover.value = false;
  438. } else {
  439. message.warning('请先添加服务包');
  440. }
  441. }
  442. function editPart(row) {
  443. if (row.conditioningProgramDetail.id) {
  444. VxeUI.modal.open({
  445. title: `编辑部位`,
  446. height: 700,
  447. width: 750,
  448. escClosable: true,
  449. destroyOnClose: true,
  450. id: `edit-part-modal`,
  451. remember: true,
  452. storage: true,
  453. slots: {
  454. default() {
  455. return h(AcupointEdit, <any>{
  456. data: row,
  457. onSubmit(data: PlanModel) {
  458. refresh(page.value);
  459. VxeUI.modal.close(`edit-part-modal`);
  460. },
  461. });
  462. },
  463. },
  464. });
  465. } else {
  466. message.warning('请先添加服务包');
  467. }
  468. }
  469. const allProjects = ref<
  470. Array<{
  471. name: string;
  472. conditioningProgramType?: string;
  473. effect?: string;
  474. }>
  475. >([]);
  476. const {
  477. loading: projectLoading,
  478. onSuccess,
  479. refresh,
  480. remove,
  481. } = usePagination(() => getCpContentListMethod(), {
  482. initialData: { data: [], total: 0 },
  483. immediate: true,
  484. });
  485. onSuccess(({ data }) => {
  486. allProjects.value = data;
  487. });
  488. const filteredProjects = computed(() => {
  489. const searchText = projectSearch.value.toLowerCase();
  490. return allProjects.value.filter(
  491. (p) => p.name.toLowerCase().includes(searchText) || p?.conditioningProgramType?.toLowerCase().includes(searchText) || p.effect?.toLowerCase().includes(searchText)
  492. );
  493. });
  494. // 添加计算数量的函数
  495. function calculateCount(row: any) {
  496. const period = Number(row.days) || 0;
  497. const frequency = Number(row.frequencyMeasure) || 0;
  498. let count = 0;
  499. count = Math.ceil(period / (Number(row.frequencyType) || 1)) * frequency;
  500. row.totalMeasure = count;
  501. // 取单价
  502. const unitPrice = Number(row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice) || 0;
  503. // 计算总价
  504. row.totalPrice = (count * unitPrice).toFixed(2);
  505. }
  506. // 添加监听器
  507. watch(totalPrice, (val) => {
  508. formData.price = val;
  509. form.cost = val;
  510. });
  511. // 获取调理包详情
  512. async function selectCw(item: any) {
  513. currentSelectedPackage.value = item;
  514. selectedPackage.value = item.id;
  515. item.types = 'institution';
  516. const res: any = await getConditioningRecordDetailMethod(item);
  517. Object.assign(formData, res);
  518. formData.items = res?.items ?? [];
  519. form.conditioningWrapName = item.name;
  520. form.conditioningWrapId = item.id;
  521. form.phone = item.phone;
  522. }
  523. function handleCancel() {
  524. console.log('取消');
  525. }
  526. // 添加电话号码验证函数
  527. function isValidPhone(phone: string): boolean {
  528. // 中国大陆手机号码正则表达式
  529. const phoneRegex = /^1[3-9]\d{9}$/;
  530. return phoneRegex.test(phone);
  531. }
  532. async function handleSubmit() {
  533. if (!currentPatient.value) {
  534. message.error('请选择患者');
  535. return;
  536. }
  537. if (formData.items) {
  538. formData.items.forEach((item) => {
  539. delete item.id;
  540. });
  541. }
  542. form.id = Number(currentPatient.value.id);
  543. form.patientId = currentPatient.value.patientId;
  544. form.patientName = currentPatient.value.patientName;
  545. form.patientSex = currentPatient.value.patientSex;
  546. form.patientAge = currentPatient.value.patientAge;
  547. form.diagnosis = currentPatient.value.diagnosis;
  548. form.symptom = currentPatient.value.symptom;
  549. form.items = formData.items;
  550. // 添加电话号码验证
  551. if (form.phone && !isValidPhone(form.phone)) {
  552. message.error('请输入有效的手机号码');
  553. return;
  554. }
  555. await addConditioningSchemeMethod(form).then(async () => {
  556. notification.success({ message: '开立成功' });
  557. // 开立成功之后 刷新列表
  558. // 刷新患者列表,并保持当前患者高亮
  559. const patientList = await getPatientListMethod();
  560. patients.value = patientList;
  561. // 重新设置 currentPatient 为刚才的患者
  562. if (currentPatient.value) {
  563. const newCurrent = patientList.find((p) => p.patientId === currentPatient.value.patientId);
  564. if (newCurrent) {
  565. currentPatient.value = newCurrent;
  566. // 获取患者列表
  567. await getPatientList(currentPatient.value?.patientId || '');
  568. // 获取服务包选择列表
  569. await loadAvailableCw(currentPatient.value?.patientId || '');
  570. // 获取调养记录
  571. await getCpRecordDetail(currentPatient.value?.id || '');
  572. }
  573. }
  574. });
  575. }
  576. // 省市区的数据
  577. const provinceOptions = ref([]);
  578. const cityOptions = ref([]);
  579. const areaOptions = ref([]);
  580. const selectedProvince = ref('');
  581. const selectedCity = ref('');
  582. const selectedArea = ref('');
  583. async function loadProvinces() {
  584. const res: any = await getProvinceMethod();
  585. provinceOptions.value = res.map((item) => ({
  586. value: item.code,
  587. label: item.name,
  588. }));
  589. }
  590. async function loadCities(name: string, provincecode: string) {
  591. if (!provincecode) {
  592. cityOptions.value = [];
  593. areaOptions.value = [];
  594. selectedCity.value = '';
  595. selectedArea.value = '';
  596. return;
  597. }
  598. const res: any = await getCityMethod(name, provincecode);
  599. cityOptions.value = res.map((item) => ({
  600. value: item.code,
  601. label: item.name,
  602. }));
  603. }
  604. async function loadAreas(name: string, citycode: string) {
  605. if (!citycode) {
  606. areaOptions.value = [];
  607. selectedArea.value = '';
  608. return;
  609. }
  610. const res: any = await getAreaMethod(name, citycode);
  611. areaOptions.value = res.map((item) => ({
  612. value: item.code,
  613. label: item.name,
  614. }));
  615. }
  616. function handleProvinceChange(value: string) {
  617. selectedProvince.value = value;
  618. const selectedProvinceOption = provinceOptions.value.find((p) => p.value === value);
  619. form.provinceName = selectedProvinceOption?.label || '';
  620. loadCities(selectedProvinceOption?.name, value);
  621. }
  622. function handleCityChange(value: string) {
  623. selectedCity.value = value;
  624. const selectedCityOption = cityOptions.value.find((c) => c.value === value);
  625. form.cityName = selectedCityOption?.label || '';
  626. loadAreas(selectedCityOption?.name, value);
  627. }
  628. function handleAreaChange(value: string) {
  629. selectedArea.value = value;
  630. const selectedAreaOption = areaOptions.value.find((a) => a.value === value);
  631. form.districtName = selectedAreaOption?.label || '';
  632. }
  633. interface PatientInfo {
  634. patientName?: string;
  635. patientSex?: string;
  636. patientAge?: number;
  637. }
  638. interface CpDetailInfo {
  639. womenSpecialPeriod?: string;
  640. cardno?: string;
  641. }
  642. function formatPatientInfo(patient: PatientInfo | null, cpDetail: CpDetailInfo | null): string {
  643. if (!patient) return '';
  644. const parts: string[] = [];
  645. // 姓名
  646. if (patient.patientName) {
  647. parts.push(patient.patientName);
  648. }
  649. // 性别
  650. if (patient.patientSex) {
  651. const gender = patient.patientSex === '1' ? '女' : patient.patientSex === '0' ? '男' : '';
  652. if (gender) {
  653. parts.push(gender);
  654. }
  655. }
  656. // 年龄
  657. if (patient.patientAge) {
  658. parts.push(`${patient.patientAge}岁`);
  659. }
  660. // 特殊时期
  661. parts.push(getWomenSpecialPeriod(cpDetail?.womenSpecialPeriod || '0'));
  662. // 身份证号
  663. if (cpDetail?.cardno) {
  664. parts.push(`身份证号:${cpDetail.cardno}`);
  665. }
  666. return parts.join(',');
  667. }
  668. function getWomenSpecialPeriod(type: string) {
  669. if (type === '0') {
  670. return '无';
  671. } else if (type === '1') {
  672. return '月经期';
  673. } else if (type === '2') {
  674. return '孕期';
  675. } else if (type === '3') {
  676. return '产后';
  677. } else if (type === '4') {
  678. return '哺乳期';
  679. }
  680. }
  681. // 处理日期选择
  682. const handleDateChange = (date: any) => {
  683. if (date) {
  684. form.estimatedStartDate = dayjs(date).format('YYYY-MM-DD');
  685. } else {
  686. form.estimatedStartDate = '';
  687. }
  688. };
  689. watch(showProjectPopover, (val) => {
  690. if (!val) {
  691. projectSearch.value = '';
  692. }
  693. });
  694. </script>
  695. <template>
  696. <div class="issue-service-page">
  697. <!-- 左侧患者列表 -->
  698. <div class="left-panel">
  699. <a-input v-model:value="searchName" placeholder="输入姓名搜索" style="margin-bottom: 12px" />
  700. <div v-if="filteredPatients.length > 0">
  701. <div
  702. class="patient-item"
  703. v-for="item in filteredPatients"
  704. :key="item.patientId"
  705. @click="selectPatient(item)"
  706. :class="{ active: currentPatient?.patientId === item.patientId }"
  707. style="cursor: pointer"
  708. >
  709. {{ item.patientName }} {{ item.patientAge }}岁
  710. <span v-if="item.status === '0'" style="color: #aaa">(已开)</span>
  711. </div>
  712. </div>
  713. <div v-else style="padding-bottom: 8px; text-align: center; margin-top: 40px">暂无数据</div>
  714. </div>
  715. <!-- 中间主内容 -->
  716. <div class="main-panel">
  717. <!-- 顶部患者信息 -->
  718. <div class="patient-info">
  719. <div style="font-size: 12px">
  720. {{ formatPatientInfo(currentPatient || null, cpDetail) }}
  721. </div>
  722. <div style="margin: 12px 0 12px 0; font-size: 12px">
  723. <!-- <div>{{ form.healthAnalysisReport }}</div> -->
  724. <span style="color: lightgray" v-if="currentPatient?.diagnosis">诊断:</span><span v-if="currentPatient?.diagnosis">{{ currentPatient?.diagnosis }}</span>
  725. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillStateName">欲病状态:</span
  726. ><span>{{ form?.healthAnalysisReport?.willillStateName }}</span>
  727. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillDegreeName">欲病程度:</span
  728. ><span>{{ form?.healthAnalysisReport?.willillDegreeName }}</span>
  729. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillSocialName">欲病类型:</span
  730. ><span>{{ form?.healthAnalysisReport?.willillSocialName }}</span>
  731. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillFunctionName">欲病表现:</span
  732. ><span>{{ form?.healthAnalysisReport?.willillFunctionName }}</span>
  733. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.constitutionGroupName">体质:</span>
  734. <span>{{ form?.healthAnalysisReport?.constitutionGroupName }}</span>
  735. </div>
  736. </div>
  737. <!-- 服务包选择 -->
  738. <div v-if="filteredPatients.length > 0">
  739. <div class="service-select-row">
  740. <span>服务包选择:</span>
  741. <template v-if="currentPatient?.status === '0'">
  742. <span>{{ form.conditioningWrapName }}</span>
  743. </template>
  744. <template v-else>
  745. <a-select v-model:value="selectedPackage" style="width: 180px" placeholder="请选择服务包">
  746. <a-select-option v-for="item in lifeCwData" :key="item.id" :value="item.id" @click="selectCw(item)">
  747. {{ item.name }}
  748. </a-select-option>
  749. </a-select>
  750. </template>
  751. <div v-if="currentPatient?.status === '1' && lifeCwData.length > 0">
  752. <span style="margin-left: 16px; color: #1890ff">推荐:</span>
  753. <a class="suggest-cw" v-for="item in suggestCwData" :key="item.id" @click="selectCw(item)">{{ item.name }}</a>
  754. </div>
  755. </div>
  756. <!-- 服务包内容表格 -->
  757. <div class="table-section">
  758. <div class="table-title">服务包内容</div>
  759. <vxe-table :data="displayTableData" border style="margin-top: 8px">
  760. <vxe-column width="60" title="">
  761. <template #default="{ rowIndex }">
  762. <a-button type="text" danger @click="removeTableRow(rowIndex)" :disabled="rowIndex === displayTableData.length - 1">
  763. <MinusCircleOutlined />
  764. </a-button>
  765. </template>
  766. </vxe-column>
  767. <vxe-column field="conditioningProgramDetail.name" title="项目名称" width="180">
  768. <template #default="{ row, rowIndex }">
  769. <template v-if="rowIndex === displayTableData.length - 1">
  770. <a-popover v-model:open="showProjectPopover" trigger="click" placement="bottomLeft" :overlayStyle="{ width: '350px', padding: 0 }">
  771. <template #content>
  772. <a-input v-model:value="projectSearch" placeholder="输入项目名称搜索" style="margin: 8px; width: 90%" @input="() => nextTick()" />
  773. <vxe-table :data="filteredProjects" border size="small" style="max-height: 240px; overflow-y: auto" @cell-click="onSelectProject">
  774. <vxe-column field="name" title="项目名称" />
  775. <vxe-column field="conditioningProgramType" title="方案类型" />
  776. <vxe-column field="effect" title="功效" />
  777. <vxe-column title="预览" width="60">
  778. <template #default="{ row }">
  779. <EyeOutlined style="font-size: 18px; color: #1890ff; cursor: pointer" @click.stop="detailPreview(row)" />
  780. </template>
  781. </vxe-column>
  782. </vxe-table>
  783. </template>
  784. <a-input v-model:value="row.name" placeholder="请搜索" style="width: 120px" @click="showProjectPopover = true" />
  785. </a-popover>
  786. </template>
  787. <template v-else>
  788. {{ row.conditioningProgramDetail?.name }}
  789. </template>
  790. </template>
  791. </vxe-column>
  792. <vxe-column title="预览" width="60">
  793. <template #default="{ row }">
  794. <EyeOutlined style="font-size: 18px; color: #1890ff; cursor: pointer" @click="onPreview(row)" />
  795. </template>
  796. </vxe-column>
  797. <vxe-column field="days" title="周期" width="120">
  798. <template #default="{ row }">
  799. <div style="display: flex; align-items: center">
  800. <a-input v-model:value="row.days" @change="() => calculateCount(row)" :disabled="currentPatient?.status === '0' ? true : false" />
  801. <span>天</span>
  802. </div>
  803. </template>
  804. </vxe-column>
  805. <vxe-column field="frequencyType" title="频率" width="auto">
  806. <template #default="{ row }">
  807. <div style="display: flex; align-items: center">
  808. <span>每</span>
  809. <a-input v-model:value="row.frequencyType" style="width: 60px" @change="() => calculateCount(row)" :disabled="currentPatient?.status === '0' ? true : false" />
  810. <span>天</span>
  811. <a-input v-model:value="row.frequencyMeasure" style="width: 60px" @change="() => calculateCount(row)" :disabled="currentPatient?.status === '0' ? true : false" />
  812. <span>{{ row.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit }}</span>
  813. </div>
  814. </template>
  815. </vxe-column>
  816. <vxe-column field="row?.conditioningProgramDetail?.conditioningProgramType" title="方案类型" width="120">
  817. <template #default="{ row }">
  818. {{ row?.conditioningProgramDetail?.conditioningProgramType || '-' }}
  819. </template>
  820. </vxe-column>
  821. <vxe-column field="row.totalMeasure" title="数量" width="60">
  822. <template #default="{ row }">
  823. {{ row.totalMeasure || 0 }}
  824. </template>
  825. </vxe-column>
  826. <vxe-column field="row?.conditioningProgramDetail?.cpFixedPricingRule?.unit" title="单位" width="60">
  827. <template #default="{ row }">
  828. {{ row?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '-' }}
  829. </template>
  830. </vxe-column>
  831. <vxe-column field="row.conditioningProgramDetail.cpFixedPricingRule.unitPrice" title="单价(元)" width="100">
  832. <template #default="{ row }">
  833. {{ row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice ?? '-' }}
  834. </template>
  835. </vxe-column>
  836. <vxe-column field="row.totalPrice" title="总价(元)" width="auto" @change="() => calculateCount(row)">
  837. <template #default="{ row }">
  838. <span>{{ row.totalPrice || 0 }}</span>
  839. </template>
  840. </vxe-column>
  841. <vxe-column field="start" title="开始时间" width="190">
  842. <template #default="{ row }">
  843. <div style="display: flex; align-items: center">
  844. <span>调养开始第</span>
  845. <a-input v-model:value="row.initialDay" style="width: 80px" :disabled="currentPatient?.status === '0' ? true : false" />
  846. <span>天</span>
  847. </div>
  848. </template>
  849. </vxe-column>
  850. <vxe-column field="conditioningProgramDetail.isOffline" title="线下项目" width="100">
  851. <template #default="{ row }">
  852. <span v-if="row.conditioningProgramDetail?.isOffline">
  853. {{ row.conditioningProgramDetail.isOffline === 'Y' ? '是' : '否' }}
  854. </span>
  855. <span v-else>-</span>
  856. </template>
  857. </vxe-column>
  858. <vxe-column field="conditioningProgramDetail.pricingType" title="穴位/经络/部位" width="160">
  859. <template #default="{ row }">
  860. <!-- <a-input v-model:value="row.desc" style="width: 120px" :disabled="currentPatient?.status === '0' ? true : false" /> -->
  861. <span>
  862. <a @click="editPart(row)" style="color: #1890ff; cursor: pointer" v-if="row.conditioningProgramDetail.pricingType === '1'">编辑</a>
  863. </span>
  864. </template>
  865. </vxe-column>
  866. <vxe-column field="remark" title="说明" width="180">
  867. <template #default="{ row }">
  868. <a-input v-model:value="row.remark" style="width: 120px" :disabled="currentPatient?.status === '0' ? true : false" />
  869. </template>
  870. </vxe-column>
  871. </vxe-table>
  872. </div>
  873. <div style="display: flex; justify-content: flex-end; margin-top: 16px">
  874. <span style="font-weight: bold">合计:{{ totalPrice }}元</span>
  875. </div>
  876. <!-- 调养日期 -->
  877. <div class="delivery-row">
  878. <span>开始调养日期:</span>
  879. <span v-if="currentPatient?.status === '0'">
  880. {{ form.estimatedStartDate }}
  881. </span>
  882. <span v-else>
  883. <a-date-picker
  884. :value="form.estimatedStartDate ? dayjs(form.estimatedStartDate) : null"
  885. @change="handleDateChange"
  886. placeholder="请选择日期"
  887. :disabledDate="(current) => current && current < dayjs().startOf('day')"
  888. />
  889. </span>
  890. </div>
  891. <!-- 配送信息 -->
  892. <div class="delivery-row" v-if="isShowDelivery">
  893. <div v-if="!form.progress || currentPatient?.status === '1'">
  894. <a-checkbox v-model:checked="deliveryChecked">配送</a-checkbox>
  895. <template v-if="deliveryChecked">
  896. <span>地址:</span>
  897. <a-select v-model:value="selectedProvince" :options="provinceOptions" placeholder="请选择省" style="width: 100px; margin-right: 4px" @change="handleProvinceChange" />
  898. <a-select
  899. v-model:value="selectedCity"
  900. :options="cityOptions"
  901. placeholder="请选择市"
  902. style="width: 100px; margin-right: 4px"
  903. :disabled="!selectedProvince"
  904. @change="handleCityChange"
  905. />
  906. <a-select
  907. v-model:value="selectedArea"
  908. :options="areaOptions"
  909. placeholder="请选择区"
  910. style="width: 100px; margin-right: 4px"
  911. :disabled="!selectedCity"
  912. @change="handleAreaChange"
  913. />
  914. <a-input v-model:value="form.detailAddress" placeholder="详细地址" style="width: 120px; margin-right: 4px" />
  915. <span>电话:</span>
  916. <a-input v-model:value="form.phone" placeholder="请输入" style="width: 120px" />
  917. </template>
  918. </div>
  919. <div v-else>
  920. <a-checkbox v-model:checked="deliveryChecked" disabled>配送</a-checkbox>
  921. <span>地址:</span>
  922. <span>{{ form.provinceName }}{{ form.cityName }}{{ form.districtName }}{{ form.detailAddress }}</span>
  923. <span style="margin-left: 16px" v-if="form.phone">电话:</span>
  924. <span>{{ form.phone }}</span>
  925. </div>
  926. </div>
  927. <!-- 操作按钮 -->
  928. <div class="footer-btns" v-if="currentPatient?.status === '1'">
  929. <a-button @click="handleCancel">取消</a-button>
  930. <a-button type="primary" style="margin-left: 24px" @click="handleSubmit">确认</a-button>
  931. </div>
  932. </div>
  933. <a-result class="area" v-else style="background-color: #fff" status="warning" title="暂无数据" />
  934. </div>
  935. <!-- 右侧调养记录 -->
  936. <div class="right-panel">
  937. <PatientTagWidget
  938. style="min-height: 112px; flex: none"
  939. :style="{ height: `${height}px` }"
  940. :dataset="tags"
  941. editable
  942. @refresh="loadTags(currentPatient.patientId)"
  943. :data="currentPatient"
  944. />
  945. <!-- <a-button
  946. type="primary"
  947. size="small"
  948. :disabled="!reportId"
  949. :loading="reportLoading || historyReportPreviewOpening"
  950. @click="
  951. historyReportPreviewOpening = true;
  952. openHistoryPreviewHandle();
  953. "
  954. >
  955. 健康档案
  956. </a-button> -->
  957. <div class="record-title">调养记录</div>
  958. <div class="record-list" v-if="patientRecord.length > 0">
  959. <div class="record-item" v-for="item in patientRecord" :key="item.id" @click="openRecord(item)">{{ item.estimatedStartDate }}~{{ item.estimatedEndDate }}</div>
  960. </div>
  961. <div v-else style="padding-bottom: 8px; text-align: center; margin-top: 40px">暂无数据</div>
  962. </div>
  963. </div>
  964. </template>
  965. <style scoped>
  966. .issue-service-page {
  967. display: flex;
  968. flex-direction: row;
  969. width: 100vw;
  970. min-height: 100vh;
  971. background: #fff;
  972. }
  973. .left-panel {
  974. flex: none;
  975. width: 180px;
  976. border-right: 1px solid #eee;
  977. padding: 16px 8px 0 16px;
  978. background: #fafbfc;
  979. min-height: 100vh;
  980. box-sizing: border-box;
  981. }
  982. .patient-item {
  983. padding: 10px 0;
  984. font-size: 14px;
  985. color: #333;
  986. }
  987. .patient-item.active {
  988. background: #e6f7ff;
  989. color: #1890ff;
  990. }
  991. .main-panel {
  992. flex: 1 1 0;
  993. min-width: 0;
  994. padding: 16px 32px 0 32px;
  995. display: flex;
  996. flex-direction: column;
  997. box-sizing: border-box;
  998. }
  999. .patient-info {
  1000. font-size: 14px;
  1001. color: #333;
  1002. }
  1003. .service-select-row {
  1004. margin-bottom: 16px;
  1005. display: flex;
  1006. align-items: center;
  1007. gap: 8px;
  1008. font-size: 14px;
  1009. }
  1010. .table-section {
  1011. margin-bottom: 16px;
  1012. }
  1013. .table-title {
  1014. font-weight: bold;
  1015. margin-bottom: 8px;
  1016. }
  1017. .table-total {
  1018. text-align: right;
  1019. margin-top: 8px;
  1020. font-weight: bold;
  1021. }
  1022. .delivery-row {
  1023. display: flex;
  1024. align-items: center;
  1025. gap: 4px;
  1026. margin-bottom: 24px;
  1027. margin-top: 16px;
  1028. }
  1029. .footer-btns {
  1030. display: flex;
  1031. justify-content: flex-end;
  1032. gap: 24px;
  1033. }
  1034. .right-panel {
  1035. flex: none;
  1036. width: 190px;
  1037. border-left: 1px solid #eee;
  1038. padding: 16px 8px 0 8px;
  1039. background: #fafbfc;
  1040. min-height: 100vh;
  1041. box-sizing: border-box;
  1042. }
  1043. .record-title {
  1044. font-weight: bold;
  1045. margin-bottom: 8px;
  1046. text-align: center;
  1047. }
  1048. .record-list {
  1049. display: flex;
  1050. flex-direction: column;
  1051. gap: 8px;
  1052. }
  1053. .record-item {
  1054. background: #fff;
  1055. border: 1px solid #eee;
  1056. border-radius: 4px;
  1057. padding: 8px;
  1058. text-align: center;
  1059. font-size: 12px;
  1060. font-weight: bold;
  1061. cursor: pointer;
  1062. }
  1063. .suggest-cw {
  1064. color: #1890ff;
  1065. cursor: pointer;
  1066. margin-right: 10px;
  1067. }
  1068. </style>