EditSystemService.vue 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
  1. <script setup lang="ts">
  2. import { ref, computed, nextTick, h, watch, onMounted, useTemplateRef } from 'vue';
  3. import { notification } from 'ant-design-vue';
  4. import { getDictionaryMethod } from '@/request/api/dictionary.api';
  5. import { UploadIFile } from '@/request/api/follow.api';
  6. import type { UploadFile } from 'ant-design-vue/es/upload/interface';
  7. import { branchMethod } from '@/request/api/system.api';
  8. import { message } from 'ant-design-vue';
  9. import {
  10. pageMedicineMethod,
  11. pageDiagnoseTypeMethod,
  12. addSystemCwMethod,
  13. addOrgCwMethod,
  14. getConditioningRecordDetailMethod,
  15. getAllSystemCpMethod,
  16. getCpContentListMethod,
  17. } from '@/request/api/care.api';
  18. import RemoteSelect from '@/libs/v-select-page/RemoteSelect.vue';
  19. import { usePagination, useRequest } from 'alova/client';
  20. import 'ant-design-vue/dist/reset.css';
  21. import type { SystemCwModel } from '@/model/care.model';
  22. import AcupointEdit from '@/service/AcupointEdit.vue';
  23. import ServiceDetail from '@/service/ServiceDetail.vue';
  24. import ServicePackageList from '@/service/ServicePackageList.vue';
  25. import { MinusCircleOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons-vue';
  26. import { VxeUI } from 'vxe-pc-ui';
  27. type FollowModel = Partial<SystemCwModel>;
  28. const props = defineProps<{ data: FollowModel }>();
  29. const fileList = ref<UploadFile[]>([]);
  30. const uploadProps = reactive({ showRemoveIcon: true });
  31. const emit = defineEmits<{ submit: [data?: SystemCwModel] }>();
  32. const deptId = ref<string>('');
  33. (notification.config as any)({
  34. zIndex: 10000, // 直接设置层级
  35. });
  36. // 获取所有的机构
  37. const branch = ref<any[]>([]);
  38. const { loading: branchLoading } = useRequest(branchMethod).onSuccess(({ data }) => {
  39. const to = (data?: any[]): any[] => {
  40. return Array.isArray(data)
  41. ? data.map((item) => {
  42. return {
  43. ...item,
  44. value: item.id,
  45. key: item.id.toString(),
  46. children: to(item.children),
  47. };
  48. })
  49. : [];
  50. };
  51. branch.value = to(data);
  52. });
  53. // 递归查找机构名称(优先 label,其次 title),找不到返回空字符串
  54. function findInstitutionLabelById(nodes: any[], targetId: string | number | undefined): string {
  55. if (!Array.isArray(nodes) || targetId === undefined || targetId === null) return '';
  56. for (const node of nodes) {
  57. if (node?.id === targetId || node?.value === targetId) {
  58. return node?.label ?? node?.title ?? '';
  59. }
  60. const childResult = findInstitutionLabelById(node?.children ?? [], targetId);
  61. if (childResult) return childResult;
  62. }
  63. return '';
  64. }
  65. const { loading: addSystemCwLoading, send: addSystemCw } = useRequest(addSystemCwMethod, {
  66. immediate: false,
  67. }).onSuccess(({ data }) => {
  68. emit('submit');
  69. });
  70. const { loading: addOrgCwLoading, send: addOrgCw } = useRequest(addOrgCwMethod, {
  71. immediate: false,
  72. }).onSuccess(({ data }) => {
  73. emit('submit');
  74. });
  75. const formData = reactive<FollowModel>({
  76. name: '', //服务包名称
  77. price: 0, //总计价格
  78. conditioningWrapPatientMatchRule: {
  79. sex: '',
  80. age: '',
  81. diagnoseDiseaseNames: [],
  82. diagnoseSyndromeNames: [],
  83. constitutionGroupNames: [],
  84. willillStateNames: [],
  85. },
  86. items: [],
  87. });
  88. const emptyRow = {
  89. id: '',
  90. conditioningWrapId: '',
  91. conditioningProgramId: 0,
  92. days: '',
  93. frequencyType: '',
  94. frequencyTypeing: [],
  95. frequencyMeasure: '',
  96. totalMeasure: '',
  97. totalPrice: '',
  98. initialDay: '',
  99. conditioningProgramDetail: {
  100. id: '',
  101. name: '',
  102. conditioningProgramType: '',
  103. pricingType: '',
  104. cpFixedPricingRule: {
  105. unitPrice: 0,
  106. pricingUnit: '',
  107. convertDose: 0,
  108. convertUnit: '',
  109. },
  110. cpDynamicPricingRule: [],
  111. cpMedicines: [],
  112. effect: '',
  113. isOffline: null,
  114. isDelivery: null,
  115. photo: '',
  116. institutionId: '',
  117. institutionName: '',
  118. remark: '',
  119. },
  120. cwcpAcuMeridians: [],
  121. cwcpAcuPoints: [],
  122. conditioningProgramSupplierName: '',
  123. };
  124. function getInstitutionProjectList() {
  125. const {
  126. loading: projectLoading,
  127. onSuccess,
  128. replace,
  129. refresh,
  130. remove,
  131. } = usePagination(() => getCpContentListMethod(), {
  132. initialData: { data: [] },
  133. immediate: true,
  134. });
  135. onSuccess(({ data }: any) => {
  136. allProjects.value = data;
  137. });
  138. }
  139. function getSystemProjectList() {
  140. const { loading: projectLoading, onSuccess } = useRequest(getAllSystemCpMethod, {
  141. immediate: true,
  142. });
  143. onSuccess(({ data }: any) => {
  144. allProjects.value = data;
  145. });
  146. }
  147. async function getProjectList() {
  148. if (props.data?.types === 'system') {
  149. getSystemProjectList();
  150. } else {
  151. getInstitutionProjectList();
  152. }
  153. }
  154. const totalPrice = computed(() => {
  155. return (formData.items ?? []).reduce((sum, row) => {
  156. const price = Number(row?.totalPrice) || 0;
  157. return sum + price;
  158. }, 0);
  159. });
  160. watch(totalPrice, (val) => {
  161. formData.price = val;
  162. });
  163. const projectSearchRef = useTemplateRef<any>('projectSearchRef');
  164. const projectSearchFocus = (visible: boolean) => {
  165. if (visible) setTimeout(() => projectSearchRef.value?.focus?.(), 300);
  166. };
  167. const handleSearchInput = () => {
  168. console.log('搜索输入事件触发,当前值:', projectSearch.value);
  169. nextTick();
  170. };
  171. // 安全获取浮层挂载容器:优先 body,无法获取则回退到父节点
  172. const getPopoverContainer = (triggerNode: any) => {
  173. try {
  174. // @ts-ignore
  175. if (typeof document !== 'undefined' && document?.body) return document.body;
  176. // @ts-ignore
  177. return triggerNode?.ownerDocument?.body || triggerNode?.parentNode;
  178. } catch (e) {
  179. return triggerNode?.parentNode;
  180. }
  181. };
  182. const projectSearch = ref('');
  183. const showProjectPopover = ref(false);
  184. // 监听搜索文本变化
  185. watch(projectSearch, (newValue, oldValue) => {
  186. console.log('搜索文本变化:', { newValue, oldValue });
  187. console.log('当前项目数据:', allProjects.value);
  188. console.log('过滤后的结果:', filteredProjects.value);
  189. });
  190. const allProjects = ref<
  191. Array<{
  192. name: string;
  193. conditioningProgramType?: string;
  194. effect?: string;
  195. }>
  196. >([]);
  197. watch(showProjectPopover, (v) => {
  198. if (v) nextTick(() => projectSearchRef.value?.focus?.());
  199. });
  200. const filteredProjects = computed(() => {
  201. const searchText = projectSearch.value.trim().toLowerCase();
  202. if (!searchText) return allProjects.value;
  203. return allProjects.value.filter((p) => p.name?.toLowerCase().includes(searchText));
  204. });
  205. function onSelectProject({ row }: any) {
  206. if ((formData.items ?? []).some((item) => item.conditioningProgramDetail?.name === row.name)) {
  207. notification.warning({
  208. message: '不能重复添加该项目',
  209. });
  210. return;
  211. }
  212. // 添加新行到主表格
  213. if (!formData.items) formData.items = [];
  214. formData.items.push({
  215. id: '',
  216. conditioningWrapId: '',
  217. conditioningProgramId: row.id,
  218. unitPrice: '-',
  219. days: '',
  220. frequencyType: '',
  221. frequencyMeasure: '',
  222. totalMeasure: '',
  223. totalPrice: '',
  224. initialDay: '',
  225. // 经络
  226. cwcpAcuMeridians: row?.cwcpAcuMeridians ?? [],
  227. // 穴位
  228. cwcpAcuPoints: row?.cwcpAcuPoints ?? [],
  229. conditioningProgramDetail: {
  230. ...row,
  231. id: row?.id || '',
  232. name: row?.name,
  233. conditioningProgramType: row?.conditioningProgramType,
  234. effect: row?.effect,
  235. pricingType: row?.pricingType,
  236. cpFixedPricingRule: {
  237. unitPrice: row?.cpFixedPricingRule?.unitPrice,
  238. pricingUnit: row?.cpFixedPricingRule?.pricingUnit,
  239. convertDose: row?.cpFixedPricingRule?.convertDose,
  240. convertUnit: row?.cpFixedPricingRule?.convertUnit,
  241. },
  242. cpDynamicPricingRule: row?.cpDynamicPricingRule,
  243. cpMedicines: row?.cpMedicines,
  244. isOffline: row?.isOffline,
  245. isDelivery: row?.isDelivery,
  246. photo: row?.photo,
  247. conditioningProgramSupplierName: row?.conditioningProgramSupplierName,
  248. },
  249. // 备注
  250. remark: row?.remark,
  251. });
  252. // 关闭弹窗
  253. showProjectPopover.value = false;
  254. // 清空搜索
  255. projectSearch.value = '';
  256. }
  257. // 预览
  258. function onPreview(row: any) {
  259. if (row.conditioningProgramDetail.id) {
  260. // 这里写你的预览逻辑
  261. VxeUI.modal.open({
  262. title: `预览`,
  263. height: 600,
  264. width: 950,
  265. escClosable: true,
  266. destroyOnClose: true,
  267. id: `preview-modal`,
  268. remember: true,
  269. storage: true,
  270. slots: {
  271. default() {
  272. return h(ServiceDetail, {
  273. data: row.conditioningProgramDetail,
  274. onSubmit: (data: any) => {
  275. VxeUI.modal.close(`preview-modal`);
  276. },
  277. });
  278. },
  279. },
  280. });
  281. } else {
  282. message.warning('请先添加服务包');
  283. }
  284. }
  285. function detailPreview(row: any) {
  286. if (row.id) {
  287. // 这里写你的预览逻辑
  288. VxeUI.modal.open({
  289. title: `预览`,
  290. height: 600,
  291. width: 950,
  292. escClosable: true,
  293. destroyOnClose: true,
  294. id: `preview-modal`,
  295. remember: true,
  296. storage: true,
  297. slots: {
  298. default() {
  299. return h(ServiceDetail, {
  300. data: row,
  301. onSubmit: (data) => {
  302. VxeUI.modal.close(`preview-modal`);
  303. },
  304. });
  305. },
  306. },
  307. onHide() {
  308. showProjectPopover.value = true;
  309. },
  310. });
  311. showProjectPopover.value = false;
  312. } else {
  313. notification.warning({ message: '请先添加服务包' });
  314. }
  315. }
  316. function editPart(row: any) {
  317. VxeUI.modal.open({
  318. title: `编辑部位`,
  319. height: 700,
  320. width: 750,
  321. escClosable: true,
  322. destroyOnClose: true,
  323. id: `edit-part-modal`,
  324. remember: true,
  325. storage: true,
  326. slots: {
  327. default() {
  328. return h(AcupointEdit, {
  329. data: row,
  330. onSubmit: (data: any) => {
  331. VxeUI.modal.close(`edit-part-modal`);
  332. },
  333. });
  334. },
  335. },
  336. });
  337. }
  338. // 添加计算数量的函数
  339. function calculateCount(row: any) {
  340. const pricingType = row.conditioningProgramDetail.pricingType;
  341. const period = Number(row.days) || 0;
  342. const frequency = Number(row.frequencyMeasure) || 0;
  343. const maxCount = row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.max;
  344. const acCount = (row.acuMeridianNames?.length ?? 0) + (row.acuPointNames?.length ?? 0);
  345. // 一口价
  346. if (pricingType === '0') {
  347. // 检查是否选择了"不限"
  348. if (row.frequencyType === '不限') {
  349. row.frequencyMeasure = ''; // 重置 frequencyMeasure
  350. row.totalMeasure = 1;
  351. } else {
  352. const convertDose = Number(row.conditioningProgramDetail.cpFixedPricingRule.convertDose) || 0;
  353. const frequencyType = Number(row.frequencyType) || 0;
  354. row.totalMeasure = Math.ceil(((period / frequencyType) * frequency) / convertDose);
  355. }
  356. // 获取单价
  357. const unitPrice = Number(row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice) || 0;
  358. // 计算总价
  359. row.totalPrice = (row.totalMeasure * unitPrice).toFixed(2);
  360. } else if (pricingType === '1') {
  361. // 按穴位计价
  362. const frequencyType = Number(row.frequencyType) || 0;
  363. row.totalMeasure = Math.ceil((period / frequencyType)) * frequency;
  364. // console.log(frequencyType, 'frequencyType', frequency, period, row.totalMeasure);
  365. // console.log(acCount, maxCount, 'acCount, maxCount', row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType);
  366. if (acCount > maxCount) {
  367. if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 0) {
  368. // 单价
  369. if (acCount > 0) {
  370. let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * acCount;
  371. row.unitPrice = unitPrice;
  372. row.totalPrice = row.totalMeasure * unitPrice;
  373. } else {
  374. row.unitPrice = '-';
  375. row.totalPrice = 0;
  376. }
  377. } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[1]?.priceType === 1) {
  378. // 一口价
  379. let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[1].price;
  380. row.unitPrice = unitPrice;
  381. // row.unitPrice = '-';
  382. row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[1].price * row.totalMeasure;
  383. }
  384. } else {
  385. if (row.conditioningProgramDetail.cpDynamicPricingRule?.length > 0) {
  386. if (row.conditioningProgramDetail.cpDynamicPricingRule?.[0]?.priceType === 0) {
  387. // 单价
  388. if (acCount > 0) {
  389. let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * acCount;
  390. row.unitPrice = unitPrice;
  391. row.totalPrice = row.totalMeasure * unitPrice;
  392. } else {
  393. row.unitPrice = '-';
  394. row.totalPrice = 0;
  395. }
  396. } else if (row.conditioningProgramDetail.cpDynamicPricingRule?.[0]?.priceType === 1) {
  397. // 一口价
  398. let unitPrice: number = row.conditioningProgramDetail.cpDynamicPricingRule[0].price;
  399. row.unitPrice = unitPrice;
  400. // row.unitPrice = '-';
  401. row.totalPrice = row.conditioningProgramDetail.cpDynamicPricingRule[0].price * row.totalMeasure;
  402. }
  403. }
  404. }
  405. }
  406. }
  407. // 添加监听器
  408. watch(
  409. () => formData.items,
  410. (newData) => {
  411. if (!newData) return;
  412. newData.forEach((row: any) => {
  413. if (row?.days || row?.frequencyType || row?.frequencyMeasure) {
  414. calculateCount(row);
  415. }
  416. });
  417. },
  418. { deep: true }
  419. );
  420. function cancel() {
  421. VxeUI.modal.close(`edit-system-service-modal`);
  422. }
  423. function confirm() {
  424. const isValid = (formData.items ?? []).every((item: any) => {
  425. delete item.id; // 删除 id 属性
  426. if (item?.conditioningProgramDetail && item?.conditioningProgramDetail?.pricingType === '1') {
  427. // 确保 cwcpAcuMeridians 和 cwcpAcuPoints 存在
  428. const hasMeridians = item.cwcpAcuMeridians?.length > 0; // 使用可选链
  429. const hasPoints = item.cwcpAcuPoints?.length > 0; // 使用可选链
  430. // 只要有一个长度大于0,就满足条件
  431. if (!hasMeridians && !hasPoints) {
  432. // 如果两个都没有,显示提示
  433. notification.warning({ message: '请至少选择一个穴位或经络' });
  434. return false; // 返回 false 表示条件不满足
  435. }
  436. }
  437. return true; // 返回 true 表示条件满足
  438. });
  439. formData?.items?.forEach((row: any) => {
  440. if (row.frequencyTypeing && row.frequencyTypeing.length > 0) {
  441. row.frequencyType = row.frequencyTypeing[0];
  442. }
  443. });
  444. // 如果所有条件都满足,继续执行后续代码
  445. if (isValid) {
  446. // 系统服务包
  447. if (props.data.types === 'system') {
  448. delete formData.types;
  449. addSystemCw({
  450. ...formData,
  451. }).then((res) => {
  452. notification.success({ message: '操作成功' });
  453. VxeUI.modal.close(`edit-system-service-modal`);
  454. });
  455. } else if (props.data.types === 'institution') {
  456. if (fileList.value.length > 0) {
  457. const upImg = fileList.value[0]?.response?.url || fileList.value[0]?.thumbUrl;
  458. formData.photo = upImg;
  459. fileList.value = upImg
  460. ? [
  461. {
  462. uid: '-1',
  463. name: 'image.png',
  464. status: 'done',
  465. url: upImg,
  466. thumbUrl: upImg,
  467. response: { url: upImg },
  468. },
  469. ]
  470. : [];
  471. }
  472. addOrgCw({
  473. ...formData,
  474. }).then((res) => {
  475. notification.success({ message: '操作成功' });
  476. VxeUI.modal.close(`edit-system-service-modal`);
  477. });
  478. }
  479. }
  480. }
  481. // 欲病状态
  482. const desiredConditions = ref<{ id: string; name: string }[]>([]);
  483. const constitutionGroups = ref<{ id: string; name: string }[]>([]);
  484. async function getDesiredConditions() {
  485. const res = await getDictionaryMethod('conditioning_wrap_willill_state');
  486. if (res?.length > 0) {
  487. desiredConditions.value = res.map((item: any) => ({
  488. id: item.value,
  489. name: item.label,
  490. }));
  491. }
  492. }
  493. // 获取性别
  494. const genders = ref<{ id: string; name: string }[]>([]);
  495. const gendersLoading = ref(false);
  496. async function getGender() {
  497. gendersLoading.value = true;
  498. const res = await getDictionaryMethod('sys_user_sex');
  499. if (res && res.length > 0) {
  500. genders.value = res.map((item: any) => ({
  501. id: item.label,
  502. name: item.label,
  503. }));
  504. }
  505. gendersLoading.value = false;
  506. }
  507. // 获取年龄
  508. const ages = ref<{ id: string; name: string }[]>([]);
  509. async function getAge() {
  510. const res = await getDictionaryMethod('conditioning_wrap_rule_age');
  511. if (res && res.length > 0) {
  512. ages.value = res.map((item: any) => ({
  513. id: item.label,
  514. name: item.label,
  515. }));
  516. }
  517. }
  518. async function getConstitutionGroup() {
  519. const res = await getDictionaryMethod('constitution_group');
  520. if (res && res.length > 0) {
  521. constitutionGroups.value = res.map((item: any) => ({
  522. id: item.value,
  523. name: item.label,
  524. }));
  525. }
  526. }
  527. onMounted(async () => {
  528. // 获取欲病状态
  529. getDesiredConditions();
  530. // 获取性别
  531. getGender();
  532. // 获取年龄
  533. getAge();
  534. // 获取体质
  535. getConstitutionGroup();
  536. // 获取机构项目列表 服务包内容点击 弹窗里的内容
  537. await getProjectList();
  538. // 有id说明是编辑
  539. if (props.data.id) {
  540. props.data.types = 'institution';
  541. // 调编辑接口获取调理包详情 机构服务包和系统服务包的详情接口是一样的
  542. const res: any = await getConditioningRecordDetailMethod(props.data);
  543. Object.assign(formData, res); // Use the response to update formData
  544. await nextTick(); // 确保视图更新
  545. // 获取适用情况
  546. const matchRule = res?.conditioningWrapPatientMatchRule || {};
  547. Object.assign(formData.conditioningWrapPatientMatchRule!, {
  548. sex: matchRule.sex ?? '',
  549. age: matchRule.age ?? '',
  550. diagnoseDiseaseNames: Array.isArray(matchRule.diagnoseDiseaseNames) ? matchRule.diagnoseDiseaseNames : [],
  551. diagnoseSyndromeNames: Array.isArray(matchRule.diagnoseSyndromeNames) ? matchRule.diagnoseSyndromeNames : [],
  552. constitutionGroupNames: Array.isArray(matchRule.constitutionGroupNames) ? matchRule.constitutionGroupNames : [],
  553. willillStateNames: Array.isArray(matchRule.willillStateNames) ? matchRule.willillStateNames : [],
  554. });
  555. formData.items = res?.items ?? [];
  556. fileList.value = res?.photo
  557. ? [
  558. {
  559. uid: '-1',
  560. name: 'image.png',
  561. status: 'done',
  562. url: res?.photo,
  563. thumbUrl: res?.photo,
  564. },
  565. ]
  566. : [];
  567. formData?.items?.forEach((row: any) => {
  568. row.frequencyTypeing = row.frequencyType ? [row.frequencyType] : [];
  569. });
  570. }
  571. });
  572. const tableData = computed(() => {
  573. return [...(formData.items ?? []), { ...emptyRow }];
  574. });
  575. function removeTableRow(idx: number) {
  576. if (idx < (formData.items ?? []).length) {
  577. formData.items?.splice(idx, 1);
  578. }
  579. }
  580. // 引入服务包
  581. function addInstitution() {
  582. deptId.value = localStorage.getItem('deptId') || '';
  583. // console.log(formData.institutionId, 'formData.institutionId', branch, 'deptId.value');
  584. // 若未有机构名称且已选择机构ID,则从树中递归查找名称
  585. if (formData.institutionId) {
  586. formData.institutionName = findInstitutionLabelById(branch.value, formData.institutionId) || '';
  587. }
  588. // console.log(formData.institutionName, 'formData.institutionName');
  589. VxeUI.modal.open({
  590. title: '选择引入',
  591. width: 1000,
  592. height: 700,
  593. escClosable: true,
  594. destroyOnClose: true,
  595. id: `systemService-list-modal`,
  596. remember: true,
  597. storage: true,
  598. slots: {
  599. default() {
  600. return h(ServicePackageList, {
  601. data: formData,
  602. institutionId: formData.institutionId ? formData.institutionId : deptId.value,
  603. institutionName: formData.institutionName ? formData.institutionName : '',
  604. onSubmit(data: SystemCwModel) {
  605. VxeUI.modal.close(`systemService-list-modal`);
  606. },
  607. });
  608. },
  609. },
  610. });
  611. }
  612. function customUpload(e: any) {
  613. // 二次封装上传接口
  614. UploadIFile(e.file)
  615. .then((res) => {
  616. // 文件上传成功
  617. e.onSuccess(res, e);
  618. })
  619. .catch((err) => {
  620. // 文件上传失败
  621. e.onError(err);
  622. });
  623. }
  624. const visible = ref<boolean>(false);
  625. const setVisible = (value: boolean): void => {
  626. visible.value = value;
  627. };
  628. const previewImg = ref<string>('');
  629. // 预览图片
  630. const handlePreview = async (file: UploadFile) => {
  631. previewImg.value = file.response?.url ?? file.thumbUrl;
  632. visible.value = true;
  633. };
  634. function set() {
  635. formData.items = [];
  636. formData.name = '';
  637. formData.conditioningWrapPatientMatchRule = {
  638. sex: '',
  639. age: '',
  640. diagnoseDiseaseNames: [],
  641. diagnoseSyndromeNames: [],
  642. constitutionGroupNames: [],
  643. willillStateNames: [],
  644. };
  645. }
  646. let multiple = ref<boolean>(true);
  647. function handleSelect(value: string, node: any, extra: any) {
  648. set();
  649. formData.institutionId = value;
  650. formData.institutionName = node.label;
  651. }
  652. function openPopover() {
  653. showProjectPopover.value = true;
  654. }
  655. </script>
  656. <template>
  657. <div style="padding: 0 24px">
  658. <div class="flex" style="align-items: center">
  659. <!-- 机构名称 -->
  660. <div class="mr-10" v-if="props.data?.types === 'institution'" style="display: flex; align-items: center; margin-bottom: 0">
  661. <span style="white-space: nowrap; margin-right: 8px">机构名称:</span>
  662. <a-tree-select
  663. v-model:value="formData.institutionId"
  664. style="width: 280px"
  665. :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
  666. placeholder="请选择"
  667. tree-default-expand-all
  668. :tree-data="branch"
  669. @select="handleSelect"
  670. :loading="branchLoading"
  671. :getPopupContainer="(triggerNode: any) => triggerNode.parentNode"
  672. />
  673. </div>
  674. <!-- 服务包名称 -->
  675. <div class="mr-6" style="display: flex; align-items: center; margin-bottom: 0">
  676. <span style="white-space: nowrap; margin-right: 8px">服务包名称:</span>
  677. <a-input style="width: 200px" v-model:value="formData.name" />
  678. <a-button type="primary" @click="addInstitution" class="ml-4" v-if="props.data?.types === 'institution'" :disabled="!formData.institutionId">引入</a-button>
  679. </div>
  680. <!-- 服务形象照 -->
  681. <div class="flex" v-if="props.data?.types === 'institution'" style="align-items: center; margin-bottom: 0">
  682. <div class="w-35" style="white-space: nowrap; margin-right: 8px">服务形象照:</div>
  683. <a-upload
  684. :showUploadList="uploadProps"
  685. v-model:file-list="fileList"
  686. name="avatar"
  687. list-type="picture-card"
  688. class="avatar-uploader"
  689. @preview="handlePreview"
  690. :maxCount="1"
  691. action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
  692. :customRequest="customUpload"
  693. >
  694. <div v-if="fileList.length < 1">
  695. <plus-outlined />
  696. </div>
  697. </a-upload>
  698. </div>
  699. </div>
  700. <div style="margin-bottom: 16px">
  701. <div class="mb-3">适用情况</div>
  702. <div class="flex items-center mb-3">
  703. <span class="w-20">使用限制</span>
  704. <div class="mr-10">
  705. <span>性别:</span>
  706. <a-select
  707. placeholder="请选择"
  708. v-model:value="formData.conditioningWrapPatientMatchRule.sex"
  709. style="width: 150px; margin: 0 5px"
  710. allowClear
  711. :loading="gendersLoading"
  712. :getPopupContainer="(triggerNode: any) => triggerNode.parentNode"
  713. >
  714. <a-select-option v-for="option in genders" :key="option.id" :value="option.id" placeholder="请选择">
  715. {{ option.name }}
  716. </a-select-option>
  717. </a-select>
  718. </div>
  719. <div>
  720. <span>年龄:</span>
  721. <a-select
  722. v-model:value="formData.conditioningWrapPatientMatchRule.age"
  723. style="width: 150px; margin: 0 8px"
  724. placeholder="请选择"
  725. allowClear
  726. :getPopupContainer="(triggerNode: any) => triggerNode.parentNode"
  727. >
  728. <a-select-option v-for="option in ages" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
  729. </a-select>
  730. </div>
  731. </div>
  732. <div class="flex items-center">
  733. <span class="w-20">适用</span>
  734. <div class="mr-10 flex items-center">
  735. <span>专病:</span>
  736. <RemoteSelect
  737. :load="pageMedicineMethod"
  738. key-prop="name"
  739. v-model:value="formData.conditioningWrapPatientMatchRule.diagnoseDiseaseNames"
  740. style="margin: 0 8px"
  741. :multiple="multiple"
  742. />
  743. </div>
  744. <div class="mr-10 flex items-center">
  745. <span>证型:</span>
  746. <RemoteSelect
  747. :load="pageDiagnoseTypeMethod"
  748. key-prop="name"
  749. v-model:value="formData.conditioningWrapPatientMatchRule.diagnoseSyndromeNames"
  750. style="margin: 0 8px"
  751. :multiple="multiple"
  752. />
  753. </div>
  754. <div class="flex items-center mr-10">
  755. <span>体质:</span>
  756. <a-select
  757. v-model:value="formData.conditioningWrapPatientMatchRule.constitutionGroupNames"
  758. style="width: 120px; margin: 0 8px"
  759. placeholder="请选择"
  760. allowClear
  761. mode="multiple"
  762. :getPopupContainer="(triggerNode: any) => triggerNode.parentNode"
  763. >
  764. <a-select-option v-for="option in constitutionGroups" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
  765. </a-select>
  766. </div>
  767. <div>
  768. <span>欲病状态:</span>
  769. <a-select
  770. v-model:value="formData.conditioningWrapPatientMatchRule.willillStateNames"
  771. style="width: 120px; margin: 0 8px"
  772. placeholder="请选择"
  773. allowClear
  774. mode="multiple"
  775. :getPopupContainer="(triggerNode: any) => triggerNode.parentNode"
  776. @change="(value) => (formData.conditioningWrapPatientMatchRule.willillStateNames = value)"
  777. >
  778. <a-select-option v-for="option in desiredConditions" :key="option.id" :value="option.id" placeholder="请选择">{{ option.name }}</a-select-option>
  779. </a-select>
  780. </div>
  781. </div>
  782. </div>
  783. <div style="margin-bottom: 16px">
  784. <span>服务包内容</span>
  785. <vxe-table :data="tableData" border style="margin-top: 8px">
  786. <vxe-column width="60" title="">
  787. <template #default="{ rowIndex }">
  788. <a-button type="text" danger @click="removeTableRow(rowIndex)" :disabled="rowIndex === tableData.length - 1">
  789. <MinusCircleOutlined />
  790. </a-button>
  791. </template>
  792. </vxe-column>
  793. <vxe-column field="conditioningProgramDetail.name" title="项目名称" width="180">
  794. <template #default="{ row, rowIndex }">
  795. <template v-if="rowIndex === tableData.length - 1">
  796. <a-popover v-model:open="showProjectPopover"
  797. trigger="click"
  798. placement="bottomLeft"
  799. :overlayStyle="{ width: '350px', padding: 0, zIndex: 4000 }"
  800. :getPopupContainer="getPopoverContainer"
  801. :destroyTooltipOnHide="false"
  802. :arrow="false"
  803. @openChange="projectSearchFocus">
  804. <template #content>
  805. <vxe-input
  806. ref="projectSearchRef"
  807. v-model="projectSearch"
  808. placeholder="输入项目名称搜索"
  809. style="margin: 8px; width: 90%"
  810. @change="handleSearchInput"
  811. ></vxe-input>
  812. <vxe-table :data="filteredProjects" border size="small" style="max-height: 240px; overflow-y: auto" @cell-click="onSelectProject">
  813. <vxe-column field="name" title="项目名称" />
  814. <vxe-column field="conditioningProgramType" title="方案类型" />
  815. <vxe-column field="effect" title="功效" />
  816. <vxe-column title="预览" width="60">
  817. <template #default="{ row }">
  818. <EyeOutlined style="font-size: 18px; color: #1890ff; cursor: pointer" @click.stop="detailPreview(row)" />
  819. </template>
  820. </vxe-column>
  821. </vxe-table>
  822. </template>
  823. <vxe-input v-model="row.name" placeholder="请搜索" style="width: 120px" @click="openPopover" readonly></vxe-input>
  824. </a-popover>
  825. </template>
  826. <template v-else>
  827. {{ row.conditioningProgramDetail?.name }}
  828. </template>
  829. </template>
  830. </vxe-column>
  831. <vxe-column title="预览" width="50">
  832. <template #default="{ row }">
  833. <EyeOutlined style="font-size: 18px; color: #1890ff; cursor: pointer" @click="onPreview(row)" v-if="row?.conditioningProgramDetail?.id" />
  834. </template>
  835. </vxe-column>
  836. <vxe-column field="days" title="周期" width="100">
  837. <template #default="{ row }">
  838. <div style="display: flex; align-items: center">
  839. <a-input v-model:value="row.days" @change="() => calculateCount(row)" />
  840. <span>天</span>
  841. </div>
  842. </template>
  843. </vxe-column>
  844. <vxe-column field="frequencyType" title="频率" width="auto">
  845. <template #default="{ row }">
  846. <div v-if="row.conditioningProgramDetail?.name === '健康咨询' || row.conditioningProgramDetail?.name === '健康评估'" class="flex items-center">
  847. <div class="flex items-center mr-4">
  848. <span>每</span>
  849. <a-input v-model:value="row.frequencyType" style="width: 50px" @change="() => calculateCount(row)" :disabled="row.frequencyType === '不限'" />
  850. <span>天</span>
  851. <a-input v-model:value="row.frequencyMeasure" style="width: 50px" @change="() => calculateCount(row)" :disabled="row.frequencyType === '不限'" />
  852. <span>{{ row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit ? row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit : '次' }}</span>
  853. </div>
  854. <div>
  855. <a-checkbox-group
  856. v-model:value="row.frequencyTypeing"
  857. @change="
  858. (value) => {
  859. row.frequencyTypeing = value.includes('不限') ? ['不限'] : [];
  860. row.frequencyType = value.includes('不限') ? '不限' : '';
  861. calculateCount(row);
  862. }
  863. "
  864. >
  865. <a-checkbox value="不限">不限</a-checkbox>
  866. </a-checkbox-group>
  867. </div>
  868. </div>
  869. <div class="flex items-center" v-else>
  870. <span>每</span>
  871. <a-input v-model:value="row.frequencyType" style="width: 50px" @change="() => calculateCount(row)" />
  872. <span>天</span>
  873. <a-input v-model:value="row.frequencyMeasure" style="width: 50px" @change="() => calculateCount(row)" />
  874. <span>{{ row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit ? row.conditioningProgramDetail?.cpFixedPricingRule?.convertUnit : '次' }}</span>
  875. </div>
  876. </template>
  877. </vxe-column>
  878. <vxe-column field="row?.conditioningProgramDetail?.conditioningProgramType" title="方案类型" width="120">
  879. <template #default="{ row }">
  880. {{ row?.conditioningProgramDetail?.conditioningProgramType || '-' }}
  881. </template>
  882. </vxe-column>
  883. <vxe-column field="row.totalMeasure" title="数量" width="60">
  884. <template #default="{ row }">
  885. {{ row.totalMeasure || 0 }}
  886. </template>
  887. </vxe-column>
  888. <vxe-column field="row?.conditioningProgramDetail?.cpFixedPricingRule?.unit" title="单位" width="60">
  889. <template #default="{ row }">
  890. {{ row?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit ? row?.conditioningProgramDetail?.cpFixedPricingRule?.pricingUnit : '次' }}
  891. </template>
  892. </vxe-column>
  893. <vxe-column field="row.conditioningProgramDetail.cpFixedPricingRule.unitPrice" title="单价(元)" width="100">
  894. <template #default="{ row }">
  895. {{ row?.conditioningProgramDetail?.pricingType === '0' ? row.conditioningProgramDetail?.cpFixedPricingRule?.unitPrice : row?.unitPrice }}
  896. </template>
  897. </vxe-column>
  898. <vxe-column field="row.totalPrice" title="总价(元)" width="auto" @change="() => calculateCount(row)">
  899. <template #default="{ row }">
  900. <span>{{ row.totalPrice || 0 }}</span>
  901. </template>
  902. </vxe-column>
  903. <vxe-column field="start" title="开始时间" width="190">
  904. <template #default="{ row }">
  905. <div style="display: flex; align-items: center">
  906. <span>调养开始第</span>
  907. <a-input v-model:value="row.initialDay" style="width: 60px" />
  908. <span>天</span>
  909. </div>
  910. </template>
  911. </vxe-column>
  912. <vxe-column field="conditioningProgramDetail.isOffline" title="线下项目" width="100">
  913. <template #default="{ row }">
  914. <span v-if="row.conditioningProgramDetail?.isOffline">
  915. {{ row.conditioningProgramDetail.isOffline === 'Y' ? '是' : '否' }}
  916. </span>
  917. <span v-else>-</span>
  918. </template>
  919. </vxe-column>
  920. <vxe-column field="conditioningProgramDetail.pricingType" title="穴位/经络/部位" width="120">
  921. <template #default="{ row }">
  922. <a @click="editPart(row)" style="color: #1890ff; cursor: pointer"
  923. v-if="row?.conditioningProgramDetail?.pricingType === '1'">{{
  924. (((row?.acuPointNames || []).concat(row?.acuMeridianNames || []).filter(Boolean).join('、')) || '编辑')
  925. }}</a>
  926. </template>
  927. </vxe-column>
  928. <vxe-column field="remark" title="说明" width="180">
  929. <template #default="{ row }">
  930. <!-- <div>说明star:{{ row.remark }}:说明结束</div> -->
  931. <a-textarea v-model:value="row.remark" style="max-width: 180px; width: 100%; height: 50px" :rows="2" show-count :maxLength="200" />
  932. </template>
  933. </vxe-column>
  934. </vxe-table>
  935. </div>
  936. <a-image
  937. :width="200"
  938. :style="{ display: 'none' }"
  939. :preview="{
  940. visible,
  941. onVisibleChange: setVisible,
  942. }"
  943. :src="previewImg"
  944. />
  945. <div style="display: flex; justify-content: flex-end">
  946. <span style="font-weight: bold">合计:{{ formData.price }}元</span>
  947. </div>
  948. <div style="display: flex; justify-content: center">
  949. <a-button style="margin-right: 24px" @click="cancel">取消</a-button>
  950. <a-button type="primary" @click="confirm">确认</a-button>
  951. </div>
  952. </div>
  953. </template>
  954. <style scoped lang="scss">
  955. /* 可根据实际需求自定义样式 */
  956. .v-dropdown-trigger {
  957. width: 160px !important;
  958. }
  959. .sp-trigger-container .sp-trigger.sp-select .sp-select-content {
  960. overflow: hidden !important;
  961. width: 100px !important;
  962. white-space: nowrap !important;
  963. text-overflow: ellipsis !important;
  964. }
  965. .avatar-uploader > .ant-upload {
  966. width: 128px;
  967. height: 128px;
  968. }
  969. .ant-upload-select-picture-card i {
  970. font-size: 32px;
  971. color: #999;
  972. }
  973. .ant-upload-select-picture-card .ant-upload-text {
  974. margin-top: 8px;
  975. color: #666;
  976. }
  977. </style>