EditSystemService.vue 35 KB

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