issueService.vue 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194
  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. frequencyTypeing: [],
  270. frequencyMeasure: '',
  271. totalMeasure: '',
  272. totalPrice: '',
  273. initialDay: '',
  274. conditioningProgramDetail: {
  275. id: '',
  276. name: '',
  277. conditioningProgramType: '',
  278. pricingType: '',
  279. cpFixedPricingRule: {
  280. unitPrice: 0,
  281. pricingUnit: '',
  282. convertDose: 0,
  283. convertUnit: '',
  284. },
  285. cpDynamicPricingRule: [],
  286. cpMedicines: [],
  287. effect: '',
  288. isOffline: null,
  289. isDelivery: null,
  290. photo: '',
  291. institutionId: '',
  292. institutionName: '',
  293. remark: '',
  294. },
  295. cwcpAcuMeridians: [],
  296. cwcpAcuPoints: [],
  297. conditioningProgramSupplierName: '',
  298. };
  299. const projectSearch = ref('');
  300. const showProjectPopover = ref(false);
  301. const displayTableData = computed(() => {
  302. return [...(formData.items ?? []), { ...emptyRow }];
  303. });
  304. const isShowDelivery = ref<boolean>(false);
  305. // 监听 displayTableData 的变化
  306. watch(displayTableData, (newValue, oldValue) => {
  307. console.log('displayTableData 变化了:', newValue);
  308. if (newValue.length > 0) {
  309. isShowDelivery.value = newValue.some((item) => {
  310. return item.conditioningProgramDetail?.isDelivery === 'Y';
  311. });
  312. newValue.forEach((row: any) => {
  313. row.frequencyTypeing = row.frequencyType ? [row.frequencyType] : [];
  314. });
  315. console.log(newValue, '开过的newValue');
  316. }
  317. });
  318. const totalPrice = computed(() => {
  319. return (formData.items ?? []).reduce((sum, row) => {
  320. const price = Number(row?.totalPrice) || 0;
  321. return sum + price;
  322. }, 0);
  323. });
  324. function onSelectProject({ row }: any) {
  325. if ((formData.items ?? []).some((item) => item.conditioningProgramDetail?.name === row.name)) {
  326. message.warning('不能重复添加该项目');
  327. return;
  328. }
  329. // 添加新行到主表格
  330. if (!formData.items) formData.items = [];
  331. formData.items.push({
  332. id: '',
  333. conditioningProgramId: row.id || '',
  334. // conditioningProgramId: 0,
  335. days: '',
  336. frequencyType: '',
  337. frequencyTypeing: [],
  338. frequencyMeasure: '',
  339. totalMeasure: '',
  340. totalPrice: '',
  341. initialDay: '',
  342. cwcpAcuMeridians: [
  343. {
  344. id: 0,
  345. name: '',
  346. code: '',
  347. type: '',
  348. photo: '',
  349. },
  350. ],
  351. cwcpAcuPoints: [
  352. {
  353. id: 0,
  354. name: '',
  355. code: '',
  356. type: '',
  357. merName: '',
  358. photo: '',
  359. },
  360. ],
  361. conditioningProgramDetail: {
  362. ...row,
  363. id: row.id || '',
  364. name: row.name || '',
  365. conditioningProgramType: row.conditioningProgramType || '',
  366. effect: row.effect || '',
  367. pricingType: row.pricingType || '',
  368. cpFixedPricingRule: {
  369. unitPrice: row?.cpFixedPricingRule?.unitPrice || 0,
  370. pricingUnit: row?.cpFixedPricingRule?.pricingUnit || '',
  371. convertDose: row?.cpFixedPricingRule?.convertDose || 0,
  372. convertUnit: row?.cpFixedPricingRule?.convertUnit || '',
  373. },
  374. cpDynamicPricingRule: row?.cpDynamicPricingRule || [],
  375. cpMedicines: row?.cpMedicines || [],
  376. isOffline: row?.isOffline || null,
  377. isDelivery: row?.isDelivery || null,
  378. photo: row?.photo || '',
  379. conditioningProgramSupplierName: row?.conditioningProgramSupplierName || '',
  380. },
  381. remark: '',
  382. });
  383. // 关闭弹窗
  384. showProjectPopover.value = false;
  385. // 清空搜索
  386. projectSearch.value = '';
  387. }
  388. function removeTableRow(idx: number) {
  389. if (idx < (formData.items ?? []).length) {
  390. formData.items?.splice(idx, 1);
  391. }
  392. }
  393. function onPreview(row) {
  394. if (row.conditioningProgramDetail.id) {
  395. // 这里写你的预览逻辑
  396. VxeUI.modal.open({
  397. title: `预览`,
  398. height: 600,
  399. width: 950,
  400. escClosable: true,
  401. destroyOnClose: true,
  402. id: `preview-modal`,
  403. remember: true,
  404. storage: true,
  405. slots: {
  406. default() {
  407. return h(ServiceDetail, {
  408. data: row.conditioningProgramDetail,
  409. onSubmit(data: SystemCwModel) {
  410. VxeUI.modal.close(`preview-modal`);
  411. },
  412. });
  413. },
  414. },
  415. });
  416. } else {
  417. message.warning('请先添加服务包');
  418. }
  419. }
  420. function detailPreview(row) {
  421. if (row.id) {
  422. // 这里写你的预览逻辑
  423. VxeUI.modal.open({
  424. title: `预览`,
  425. height: 600,
  426. width: 950,
  427. escClosable: true,
  428. destroyOnClose: true,
  429. id: `preview-modal`,
  430. remember: true,
  431. storage: true,
  432. slots: {
  433. default() {
  434. return h(ServiceDetail, {
  435. data: row,
  436. onSubmit: (data) => {
  437. VxeUI.modal.close(`preview-modal`);
  438. },
  439. });
  440. },
  441. },
  442. });
  443. showProjectPopover.value = false;
  444. } else {
  445. message.warning('请先添加服务包');
  446. }
  447. }
  448. function editPart(row) {
  449. if (row.conditioningProgramDetail.id) {
  450. VxeUI.modal.open({
  451. title: `编辑部位`,
  452. height: 700,
  453. width: 750,
  454. escClosable: true,
  455. destroyOnClose: true,
  456. id: `edit-part-modal`,
  457. remember: true,
  458. storage: true,
  459. slots: {
  460. default() {
  461. return h(AcupointEdit, <any>{
  462. data: row,
  463. onSubmit(data: PlanModel) {
  464. refresh(page.value);
  465. VxeUI.modal.close(`edit-part-modal`);
  466. },
  467. });
  468. },
  469. },
  470. });
  471. } else {
  472. message.warning('请先添加服务包');
  473. }
  474. }
  475. const allProjects = ref<
  476. Array<{
  477. name: string;
  478. conditioningProgramType?: string;
  479. effect?: string;
  480. }>
  481. >([]);
  482. const {
  483. loading: projectLoading,
  484. onSuccess,
  485. refresh,
  486. remove,
  487. } = usePagination(() => getCpContentListMethod(), {
  488. initialData: { data: [], total: 0 },
  489. immediate: true,
  490. });
  491. onSuccess(({ data }) => {
  492. allProjects.value = data;
  493. });
  494. const filteredProjects = computed(() => {
  495. const searchText = projectSearch.value.toLowerCase();
  496. return allProjects.value.filter(
  497. (p) => p.name.toLowerCase().includes(searchText) || p?.conditioningProgramType?.toLowerCase().includes(searchText) || p.effect?.toLowerCase().includes(searchText)
  498. );
  499. });
  500. // 添加计算数量的函数
  501. function calculateCount(row: any) {
  502. const pricingType = row.conditioningProgramDetail.pricingType;
  503. const period = Number(row.days) || 0;
  504. const frequency = Number(row.frequencyMeasure) || 0;
  505. // 一口价
  506. if (pricingType === '0') {
  507. // 检查是否选择了"不限"
  508. if (row.frequencyType === '不限') {
  509. row.frequencyMeasure = ''; // 重置 frequencyMeasure
  510. row.totalMeasure = 1;
  511. } else {
  512. const convertDose = Number(row.conditioningProgramDetail.cpFixedPricingRule.convertDose) || 0;
  513. const frequencyType = Number(row.frequencyType) || 0;
  514. row.totalMeasure = Math.ceil(((period / frequencyType) * frequency) / convertDose);
  515. }
  516. // 获取单价
  517. const unitPrice = Number(row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice) || 0;
  518. // 计算总价
  519. row.totalPrice = (row.totalMeasure * unitPrice).toFixed(2);
  520. } else if (pricingType === '1') {
  521. // 按穴位计价
  522. row.totalPrice =
  523. row.conditioningProgramDetail?.cpDynamicPricingRule?.reduce((sum: number, item: any) => {
  524. return sum + (Number(item.price) || 0);
  525. }, 0) || 0;
  526. }
  527. }
  528. // function calculateCount(row: any) {
  529. // const convertDose = Number(row.conditioningProgramDetail.cpFixedPricingRule.convertDose) || 0;
  530. // console.log(convertDose, 'convertDose');
  531. // const period = Number(row.days) || 0;
  532. // const frequency = Number(row.frequencyMeasure) || 0;
  533. // let count = 0;
  534. // count = Math.ceil(((period / (Number(row.frequencyType) || 1)) * frequency)/convertDose);
  535. // row.totalMeasure = count;
  536. // // 取单价
  537. // const unitPrice = Number(row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice) || 0;
  538. // // 计算总价
  539. // row.totalPrice = (count * unitPrice).toFixed(2);
  540. // }
  541. // 添加监听器
  542. watch(totalPrice, (val) => {
  543. formData.price = val;
  544. form.cost = val;
  545. });
  546. // 获取调理包详情
  547. async function selectCw(item: any) {
  548. currentSelectedPackage.value = item;
  549. selectedPackage.value = item.id;
  550. item.types = 'institution';
  551. const res: any = await getConditioningRecordDetailMethod(item);
  552. Object.assign(formData, res);
  553. formData.items = res?.items ?? [];
  554. form.conditioningWrapName = item.name;
  555. form.conditioningWrapId = item.id;
  556. form.phone = item.phone;
  557. }
  558. function handleCancel() {
  559. console.log('取消');
  560. }
  561. // 添加监听器
  562. watch(
  563. () => formData.items,
  564. (newData) => {
  565. if (!newData) return;
  566. newData.forEach((row) => {
  567. if (row?.days || row?.frequencyType || row?.frequencyMeasure) {
  568. calculateCount(row);
  569. }
  570. });
  571. },
  572. { deep: true }
  573. );
  574. // 添加电话号码验证函数
  575. function isValidPhone(phone: string): boolean {
  576. // 中国大陆手机号码正则表达式
  577. const phoneRegex = /^1[3-9]\d{9}$/;
  578. return phoneRegex.test(phone);
  579. }
  580. async function handleSubmit() {
  581. if (!currentPatient.value) {
  582. message.error('请选择患者');
  583. return;
  584. }
  585. if (formData.items) {
  586. formData.items.forEach((item) => {
  587. delete item.id;
  588. });
  589. }
  590. form.id = Number(currentPatient.value.id);
  591. form.patientId = currentPatient.value.patientId;
  592. form.patientName = currentPatient.value.patientName;
  593. form.patientSex = currentPatient.value.patientSex;
  594. form.patientAge = currentPatient.value.patientAge;
  595. form.diagnosis = currentPatient.value.diagnosis;
  596. form.symptom = currentPatient.value.symptom;
  597. form.items = formData.items;
  598. // 添加电话号码验证
  599. if (form.phone && !isValidPhone(form.phone)) {
  600. message.error('请输入有效的手机号码');
  601. return;
  602. }
  603. await addConditioningSchemeMethod(form).then(async () => {
  604. notification.success({ message: '开立成功' });
  605. // 开立成功之后 刷新列表
  606. // 刷新患者列表,并保持当前患者高亮
  607. const patientList = await getPatientListMethod();
  608. patients.value = patientList;
  609. // 重新设置 currentPatient 为刚才的患者
  610. if (currentPatient.value) {
  611. const newCurrent = patientList.find((p) => p.patientId === currentPatient.value.patientId);
  612. if (newCurrent) {
  613. currentPatient.value = newCurrent;
  614. // 获取患者列表
  615. await getPatientList(currentPatient.value?.patientId || '');
  616. // 获取服务包选择列表
  617. await loadAvailableCw(currentPatient.value?.patientId || '');
  618. // 获取调养记录
  619. await getCpRecordDetail(currentPatient.value?.id || '');
  620. }
  621. }
  622. });
  623. }
  624. // 省市区的数据
  625. const provinceOptions = ref([]);
  626. const cityOptions = ref([]);
  627. const areaOptions = ref([]);
  628. const selectedProvince = ref('');
  629. const selectedCity = ref('');
  630. const selectedArea = ref('');
  631. async function loadProvinces() {
  632. const res: any = await getProvinceMethod();
  633. provinceOptions.value = res.map((item) => ({
  634. value: item.code,
  635. label: item.name,
  636. }));
  637. }
  638. async function loadCities(name: string, provincecode: string) {
  639. if (!provincecode) {
  640. cityOptions.value = [];
  641. areaOptions.value = [];
  642. selectedCity.value = '';
  643. selectedArea.value = '';
  644. return;
  645. }
  646. const res: any = await getCityMethod(name, provincecode);
  647. cityOptions.value = res.map((item) => ({
  648. value: item.code,
  649. label: item.name,
  650. }));
  651. }
  652. async function loadAreas(name: string, citycode: string) {
  653. if (!citycode) {
  654. areaOptions.value = [];
  655. selectedArea.value = '';
  656. return;
  657. }
  658. const res: any = await getAreaMethod(name, citycode);
  659. areaOptions.value = res.map((item) => ({
  660. value: item.code,
  661. label: item.name,
  662. }));
  663. }
  664. function handleProvinceChange(value: string) {
  665. selectedProvince.value = value;
  666. const selectedProvinceOption = provinceOptions.value.find((p) => p.value === value);
  667. form.provinceName = selectedProvinceOption?.label || '';
  668. loadCities(selectedProvinceOption?.name, value);
  669. }
  670. function handleCityChange(value: string) {
  671. selectedCity.value = value;
  672. const selectedCityOption = cityOptions.value.find((c) => c.value === value);
  673. form.cityName = selectedCityOption?.label || '';
  674. loadAreas(selectedCityOption?.name, value);
  675. }
  676. function handleAreaChange(value: string) {
  677. selectedArea.value = value;
  678. const selectedAreaOption = areaOptions.value.find((a) => a.value === value);
  679. form.districtName = selectedAreaOption?.label || '';
  680. }
  681. interface PatientInfo {
  682. patientName?: string;
  683. patientSex?: string;
  684. patientAge?: number;
  685. }
  686. interface CpDetailInfo {
  687. womenSpecialPeriod?: string;
  688. cardno?: string;
  689. }
  690. function formatPatientInfo(patient: PatientInfo | null, cpDetail: CpDetailInfo | null): string {
  691. if (!patient) return '';
  692. const parts: string[] = [];
  693. // 姓名
  694. if (patient.patientName) {
  695. parts.push(patient.patientName);
  696. }
  697. // 性别
  698. if (patient.patientSex) {
  699. const gender = patient.patientSex === '1' ? '女' : patient.patientSex === '0' ? '男' : '';
  700. if (gender) {
  701. parts.push(gender);
  702. }
  703. }
  704. // 年龄
  705. if (patient.patientAge) {
  706. parts.push(`${patient.patientAge}岁`);
  707. }
  708. // 特殊时期
  709. parts.push(getWomenSpecialPeriod(cpDetail?.womenSpecialPeriod || '0'));
  710. // 身份证号
  711. if (cpDetail?.cardno) {
  712. parts.push(`身份证号:${cpDetail.cardno}`);
  713. }
  714. return parts.join(',');
  715. }
  716. function getWomenSpecialPeriod(type: string) {
  717. if (type === '0') {
  718. return '无';
  719. } else if (type === '1') {
  720. return '月经期';
  721. } else if (type === '2') {
  722. return '孕期';
  723. } else if (type === '3') {
  724. return '产后';
  725. } else if (type === '4') {
  726. return '哺乳期';
  727. }
  728. }
  729. // 处理日期选择
  730. const handleDateChange = (date: any) => {
  731. if (date) {
  732. form.estimatedStartDate = dayjs(date).format('YYYY-MM-DD');
  733. } else {
  734. form.estimatedStartDate = '';
  735. }
  736. };
  737. watch(showProjectPopover, (val) => {
  738. if (!val) {
  739. projectSearch.value = '';
  740. }
  741. });
  742. </script>
  743. <template>
  744. <div class="issue-service-page">
  745. <!-- 左侧患者列表 -->
  746. <div class="left-panel">
  747. <a-input v-model:value="searchName" placeholder="输入姓名搜索" style="margin-bottom: 12px" />
  748. <div v-if="filteredPatients.length > 0">
  749. <div
  750. class="patient-item"
  751. v-for="item in filteredPatients"
  752. :key="item.patientId"
  753. @click="selectPatient(item)"
  754. :class="{ active: currentPatient?.patientId === item.patientId }"
  755. style="cursor: pointer"
  756. >
  757. {{ item.patientName }} {{ item.patientAge }}岁
  758. <span v-if="item.status === '0'" style="color: #aaa">(已开)</span>
  759. </div>
  760. </div>
  761. <div v-else style="padding-bottom: 8px; text-align: center; margin-top: 40px">暂无数据</div>
  762. </div>
  763. <!-- 中间主内容 -->
  764. <div class="main-panel">
  765. <!-- 顶部患者信息 -->
  766. <div class="patient-info">
  767. <div style="font-size: 12px">
  768. {{ formatPatientInfo(currentPatient || null, cpDetail) }}
  769. </div>
  770. <div style="margin: 12px 0 12px 0; font-size: 12px">
  771. <!-- <div>{{ form.healthAnalysisReport }}</div> -->
  772. <span style="color: lightgray" v-if="currentPatient?.diagnosis">诊断:</span><span v-if="currentPatient?.diagnosis">{{ currentPatient?.diagnosis }}</span>
  773. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillStateName">欲病状态:</span
  774. ><span>{{ form?.healthAnalysisReport?.willillStateName }}</span>
  775. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillDegreeName">欲病程度:</span
  776. ><span>{{ form?.healthAnalysisReport?.willillDegreeName }}</span>
  777. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillSocialName">欲病类型:</span
  778. ><span>{{ form?.healthAnalysisReport?.willillSocialName }}</span>
  779. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.willillFunctionName">欲病表现:</span
  780. ><span>{{ form?.healthAnalysisReport?.willillFunctionName }}</span>
  781. <span style="margin-left: 20px; color: lightgray" v-if="form?.healthAnalysisReport?.constitutionGroupName">体质:</span>
  782. <span>{{ form?.healthAnalysisReport?.constitutionGroupName }}</span>
  783. </div>
  784. </div>
  785. <!-- 服务包选择 -->
  786. <div v-if="filteredPatients.length > 0">
  787. <div class="service-select-row">
  788. <span>服务包选择:</span>
  789. <template v-if="currentPatient?.status === '0'">
  790. <span>{{ form.conditioningWrapName }}</span>
  791. </template>
  792. <template v-else>
  793. <a-select v-model:value="selectedPackage" style="width: 180px" placeholder="请选择服务包">
  794. <a-select-option v-for="item in lifeCwData" :key="item.id" :value="item.id" @click="selectCw(item)">
  795. {{ item.name }}
  796. </a-select-option>
  797. </a-select>
  798. </template>
  799. <div v-if="currentPatient?.status === '1' && lifeCwData.length > 0">
  800. <span style="margin-left: 16px; color: #1890ff">推荐:</span>
  801. <a class="suggest-cw" v-for="item in suggestCwData" :key="item.id" @click="selectCw(item)">{{ item.name }}</a>
  802. </div>
  803. </div>
  804. <!-- 服务包内容表格 -->
  805. <div class="table-section">
  806. <div class="table-title">服务包内容</div>
  807. <vxe-table :data="displayTableData" border style="margin-top: 8px">
  808. <vxe-column width="60" title="">
  809. <template #default="{ rowIndex }">
  810. <a-button type="text" danger @click="removeTableRow(rowIndex)" :disabled="rowIndex === displayTableData.length - 1">
  811. <MinusCircleOutlined />
  812. </a-button>
  813. </template>
  814. </vxe-column>
  815. <vxe-column field="conditioningProgramDetail.name" title="项目名称" width="180">
  816. <template #default="{ row, rowIndex }">
  817. <template v-if="rowIndex === displayTableData.length - 1">
  818. <a-popover v-model:open="showProjectPopover" trigger="click" placement="bottomLeft" :overlayStyle="{ width: '350px', padding: 0 }">
  819. <template #content>
  820. <a-input v-model:value="projectSearch" placeholder="输入项目名称搜索" style="margin: 8px; width: 90%" @input="() => nextTick()" />
  821. <vxe-table :data="filteredProjects" border size="small" style="max-height: 240px; overflow-y: auto" @cell-click="onSelectProject">
  822. <vxe-column field="name" title="项目名称" />
  823. <vxe-column field="conditioningProgramType" title="方案类型" />
  824. <vxe-column field="effect" title="功效" />
  825. <vxe-column title="预览" width="60">
  826. <template #default="{ row }">
  827. <EyeOutlined style="font-size: 18px; color: #1890ff; cursor: pointer" @click.stop="detailPreview(row)" />
  828. </template>
  829. </vxe-column>
  830. </vxe-table>
  831. </template>
  832. <a-input v-model:value="row.name" placeholder="请搜索" style="width: 120px" @click="showProjectPopover = true" />
  833. </a-popover>
  834. </template>
  835. <template v-else>
  836. {{ row.conditioningProgramDetail?.name }}
  837. </template>
  838. </template>
  839. </vxe-column>
  840. <vxe-column title="预览" width="60">
  841. <template #default="{ row }">
  842. <EyeOutlined style="font-size: 18px; color: #1890ff; cursor: pointer" @click="onPreview(row)" />
  843. </template>
  844. </vxe-column>
  845. <vxe-column field="days" title="周期" width="120">
  846. <template #default="{ row }">
  847. <div style="display: flex; align-items: center">
  848. <a-input v-model:value="row.days" @change="() => calculateCount(row)" :disabled="currentPatient?.status === '0' ? true : false" />
  849. <span>天</span>
  850. </div>
  851. </template>
  852. </vxe-column>
  853. <vxe-column field="frequencyType" title="频率" width="auto">
  854. <template #default="{ row }">
  855. <div v-if="row.conditioningProgramDetail?.name === '健康咨询' || row.conditioningProgramDetail?.name === '健康评估'" class="flex items-center">
  856. <div class="flex items-center mr-4">
  857. <span>每</span>
  858. <a-input v-model:value="row.frequencyType" style="width: 50px" @change="() => calculateCount(row)" :disabled="row.frequencyType === '不限'" />
  859. <span>天</span>
  860. <a-input v-model:value="row.frequencyMeasure" style="width: 50px" @change="() => calculateCount(row)" :disabled="row.frequencyType === '不限'" />
  861. <span>{{ row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit }}</span>
  862. </div>
  863. <div>
  864. <a-checkbox-group
  865. v-model:value="row.frequencyTypeing"
  866. @change="
  867. (value) => {
  868. row.frequencyTypeing = value.includes('不限') ? ['不限'] : [];
  869. row.frequencyType = value.includes('不限') ? '不限' : '';
  870. calculateCount(row);
  871. }
  872. "
  873. >
  874. <a-checkbox value="不限">不限</a-checkbox>
  875. </a-checkbox-group>
  876. </div>
  877. </div>
  878. <div class="flex items-center" v-else>
  879. <span>每</span>
  880. <a-input v-model:value="row.frequencyType" style="width: 50px" @change="() => calculateCount(row)" />
  881. <span>天</span>
  882. <a-input v-model:value="row.frequencyMeasure" style="width: 50px" @change="() => calculateCount(row)" />
  883. <span>{{ row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit }}</span>
  884. </div>
  885. </template>
  886. </vxe-column>
  887. <vxe-column field="row?.conditioningProgramDetail?.conditioningProgramType" title="方案类型" width="120">
  888. <template #default="{ row }">
  889. {{ row?.conditioningProgramDetail?.conditioningProgramType || '-' }}
  890. </template>
  891. </vxe-column>
  892. <vxe-column field="row.totalMeasure" title="数量" width="60">
  893. <template #default="{ row }">
  894. {{ row.totalMeasure || 0 }}
  895. </template>
  896. </vxe-column>
  897. <vxe-column field="row?.conditioningProgramDetail?.cpFixedPricingRule?.unit" title="单位" width="60">
  898. <template #default="{ row }">
  899. {{ row?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit || '-' }}
  900. </template>
  901. </vxe-column>
  902. <vxe-column field="row.conditioningProgramDetail.cpFixedPricingRule.unitPrice" title="单价(元)" width="100">
  903. <template #default="{ row }">
  904. {{ row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice ?? '-' }}
  905. </template>
  906. </vxe-column>
  907. <vxe-column field="row.totalPrice" title="总价(元)" width="auto" @change="() => calculateCount(row)">
  908. <template #default="{ row }">
  909. <span>{{ row.totalPrice || 0 }}</span>
  910. </template>
  911. </vxe-column>
  912. <vxe-column field="start" title="开始时间" width="190">
  913. <template #default="{ row }">
  914. <div style="display: flex; align-items: center">
  915. <span>调养开始第</span>
  916. <a-input v-model:value="row.initialDay" style="width: 80px" :disabled="currentPatient?.status === '0' ? true : false" />
  917. <span>天</span>
  918. </div>
  919. </template>
  920. </vxe-column>
  921. <vxe-column field="conditioningProgramDetail.isOffline" title="线下项目" width="100">
  922. <template #default="{ row }">
  923. <span v-if="row.conditioningProgramDetail?.isOffline">
  924. {{ row.conditioningProgramDetail.isOffline === 'Y' ? '是' : '否' }}
  925. </span>
  926. <span v-else>-</span>
  927. </template>
  928. </vxe-column>
  929. <vxe-column field="conditioningProgramDetail.pricingType" title="穴位/经络/部位" width="160">
  930. <template #default="{ row }">
  931. <!-- <a-input v-model:value="row.desc" style="width: 120px" :disabled="currentPatient?.status === '0' ? true : false" /> -->
  932. <span>
  933. <a @click="editPart(row)" style="color: #1890ff; cursor: pointer" v-if="row.conditioningProgramDetail.pricingType === '1'">编辑</a>
  934. </span>
  935. </template>
  936. </vxe-column>
  937. <vxe-column field="remark" title="说明" width="180">
  938. <template #default="{ row }">
  939. <!-- <a-input v-model:value="row.remark" style="width: 120px" :disabled="currentPatient?.status === '0' ? true : false" /> -->
  940. <a-textarea
  941. v-model:value="row.remark"
  942. style="max-width: 180px; width: 100%; height: 50px"
  943. :rows="2"
  944. show-count
  945. :maxLength="200"
  946. :disabled="currentPatient?.status === '0' ? true : false"
  947. />
  948. </template>
  949. </vxe-column>
  950. </vxe-table>
  951. </div>
  952. <div style="display: flex; justify-content: flex-end; margin-top: 16px">
  953. <span style="font-weight: bold">合计:{{ totalPrice }}元</span>
  954. </div>
  955. <!-- 调养日期 -->
  956. <div class="delivery-row">
  957. <span>开始调养日期:</span>
  958. <span v-if="currentPatient?.status === '0'">
  959. {{ form.estimatedStartDate }}
  960. </span>
  961. <span v-else>
  962. <a-date-picker
  963. :value="form.estimatedStartDate ? dayjs(form.estimatedStartDate) : null"
  964. @change="handleDateChange"
  965. placeholder="请选择日期"
  966. :disabledDate="(current) => current && current < dayjs().startOf('day')"
  967. />
  968. </span>
  969. </div>
  970. <!-- 配送信息 -->
  971. <div class="delivery-row" v-if="isShowDelivery">
  972. <div v-if="!form.progress || currentPatient?.status === '1'">
  973. <a-checkbox v-model:checked="deliveryChecked">配送</a-checkbox>
  974. <template v-if="deliveryChecked">
  975. <span>地址:</span>
  976. <a-select v-model:value="selectedProvince" :options="provinceOptions" placeholder="请选择省" style="width: 100px; margin-right: 4px" @change="handleProvinceChange" />
  977. <a-select
  978. v-model:value="selectedCity"
  979. :options="cityOptions"
  980. placeholder="请选择市"
  981. style="width: 100px; margin-right: 4px"
  982. :disabled="!selectedProvince"
  983. @change="handleCityChange"
  984. />
  985. <a-select
  986. v-model:value="selectedArea"
  987. :options="areaOptions"
  988. placeholder="请选择区"
  989. style="width: 100px; margin-right: 4px"
  990. :disabled="!selectedCity"
  991. @change="handleAreaChange"
  992. />
  993. <a-input v-model:value="form.detailAddress" placeholder="详细地址" style="width: 120px; margin-right: 4px" />
  994. <span>电话:</span>
  995. <a-input v-model:value="form.phone" placeholder="请输入" style="width: 120px" />
  996. </template>
  997. </div>
  998. <div v-else>
  999. <a-checkbox v-model:checked="deliveryChecked" disabled>配送</a-checkbox>
  1000. <span>地址:</span>
  1001. <span>{{ form.provinceName }}{{ form.cityName }}{{ form.districtName }}{{ form.detailAddress }}</span>
  1002. <span style="margin-left: 16px" v-if="form.phone">电话:</span>
  1003. <span>{{ form.phone }}</span>
  1004. </div>
  1005. </div>
  1006. <!-- 操作按钮 -->
  1007. <div class="footer-btns" v-if="currentPatient?.status === '1'">
  1008. <a-button @click="handleCancel">取消</a-button>
  1009. <a-button type="primary" style="margin-left: 24px" @click="handleSubmit">确认</a-button>
  1010. </div>
  1011. </div>
  1012. <a-result class="area" v-else style="background-color: #fff" status="warning" title="暂无数据" />
  1013. </div>
  1014. <!-- 右侧调养记录 -->
  1015. <div class="right-panel">
  1016. <PatientTagWidget style="min-height: 112px; flex: none" :dataset="tags" editable @refresh="loadTags(currentPatient?.patientId)" :data="currentPatient" />
  1017. <!-- <a-button
  1018. type="primary"
  1019. size="small"
  1020. :disabled="!reportId"
  1021. :loading="reportLoading || historyReportPreviewOpening"
  1022. @click="
  1023. historyReportPreviewOpening = true;
  1024. openHistoryPreviewHandle();
  1025. "
  1026. >
  1027. 健康档案
  1028. </a-button> -->
  1029. <div class="record-title">调养记录</div>
  1030. <div class="record-list" v-if="patientRecord.length > 0">
  1031. <div class="record-item" v-for="item in patientRecord" :key="item.id" @click="openRecord(item)">{{ item.estimatedStartDate }}~{{ item.estimatedEndDate }}</div>
  1032. </div>
  1033. <div v-else style="padding-bottom: 8px; text-align: center; margin-top: 40px">暂无数据</div>
  1034. </div>
  1035. </div>
  1036. </template>
  1037. <style scoped>
  1038. .issue-service-page {
  1039. display: flex;
  1040. flex-direction: row;
  1041. width: 100vw;
  1042. /* min-height: 100vh; */
  1043. height: var(--page-main-container);
  1044. overflow: hidden;
  1045. background: #fff;
  1046. }
  1047. .left-panel {
  1048. flex: none;
  1049. width: 180px;
  1050. border-right: 1px solid #eee;
  1051. padding: 16px 8px 0 16px;
  1052. background: #fafbfc;
  1053. /* min-height: 100vh; */
  1054. box-sizing: border-box;
  1055. }
  1056. .patient-item {
  1057. padding: 10px 0;
  1058. font-size: 14px;
  1059. color: #333;
  1060. }
  1061. .patient-item.active {
  1062. background: #e6f7ff;
  1063. color: #1890ff;
  1064. }
  1065. .main-panel {
  1066. flex: 1 1 0;
  1067. min-width: 0;
  1068. padding: 16px 32px 0 32px;
  1069. display: flex;
  1070. flex-direction: column;
  1071. box-sizing: border-box;
  1072. overflow: auto;
  1073. }
  1074. .patient-info {
  1075. font-size: 14px;
  1076. color: #333;
  1077. }
  1078. .service-select-row {
  1079. margin-bottom: 16px;
  1080. display: flex;
  1081. align-items: center;
  1082. gap: 8px;
  1083. font-size: 14px;
  1084. }
  1085. .table-section {
  1086. margin-bottom: 16px;
  1087. }
  1088. .table-title {
  1089. font-weight: bold;
  1090. margin-bottom: 8px;
  1091. }
  1092. .table-total {
  1093. text-align: right;
  1094. margin-top: 8px;
  1095. font-weight: bold;
  1096. }
  1097. .delivery-row {
  1098. display: flex;
  1099. align-items: center;
  1100. gap: 4px;
  1101. margin-bottom: 24px;
  1102. margin-top: 16px;
  1103. }
  1104. .footer-btns {
  1105. display: flex;
  1106. justify-content: flex-end;
  1107. gap: 24px;
  1108. }
  1109. .right-panel {
  1110. flex: none;
  1111. width: 190px;
  1112. border-left: 1px solid #eee;
  1113. padding: 16px 8px 0 8px;
  1114. background: #fafbfc;
  1115. /* min-height: 100vh; */
  1116. box-sizing: border-box;
  1117. }
  1118. .record-title {
  1119. font-weight: bold;
  1120. margin-bottom: 8px;
  1121. text-align: center;
  1122. }
  1123. .record-list {
  1124. display: flex;
  1125. flex-direction: column;
  1126. gap: 8px;
  1127. }
  1128. .record-item {
  1129. background: #fff;
  1130. border: 1px solid #eee;
  1131. border-radius: 4px;
  1132. padding: 8px;
  1133. text-align: center;
  1134. font-size: 12px;
  1135. font-weight: bold;
  1136. cursor: pointer;
  1137. }
  1138. .suggest-cw {
  1139. color: #1890ff;
  1140. cursor: pointer;
  1141. margin-right: 10px;
  1142. }
  1143. .scroll-content {
  1144. /* max-height: 600px; */
  1145. overflow-y: auto;
  1146. /* 可选:让滚动条更美观 */
  1147. scrollbar-width: thin;
  1148. scrollbar-color: #aaa #f5f5f5;
  1149. }
  1150. </style>