ConfirmItems.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. <script setup lang="ts">
  2. import { ref, reactive, watch, onMounted, defineEmits, nextTick } from 'vue';
  3. import { notification } from 'ant-design-vue';
  4. import { PlusOutlined } from '@ant-design/icons-vue';
  5. import { branchMethod } from '@/request/api/system.api';
  6. import { useRequest } from 'alova/client';
  7. import { cpMedicinesMethod } from '@/request/api/dictionary.api';
  8. import { VxeUI } from 'vxe-pc-ui';
  9. import RemoteSelect from '@/libs/v-select-page/RemoteSelect.vue';
  10. import { confirmOrgConfirmMethod, getConditioningSchemeDetailMethod, getAllSupplierMethod } from '@/request/api/care.api';
  11. import type { SystemItemModel } from '@/model/care.model';
  12. import { UploadIFile } from '@/request/api/follow.api';
  13. import type { UploadFile } from 'ant-design-vue/es/upload/interface';
  14. type SystemModel = Partial<SystemItemModel>;
  15. const props = defineProps<{ data: SystemModel }>();
  16. const unitOptions = [
  17. { label: '袋', value: '袋' },
  18. { label: '包', value: '包' },
  19. { label: '贴', value: '贴' },
  20. { label: '次', value: '次' },
  21. ];
  22. const fileList = ref<UploadFile[]>([]);
  23. // 获取所有的机构
  24. const branch = ref<any[]>([]);
  25. const { loading: branchLoading } = useRequest(branchMethod).onSuccess(({ data }) => {
  26. const to = (data?: any[]): any[] => {
  27. return Array.isArray(data)
  28. ? data.map((item) => {
  29. return {
  30. ...item,
  31. value: item.id,
  32. key: item.id.toString(),
  33. children: to(item.children),
  34. };
  35. })
  36. : [];
  37. };
  38. branch.value = to(data);
  39. });
  40. const supplierOptions = ref<any[]>([]);
  41. // 获取所有的供应商
  42. const supplierArr = ref<any[]>([]);
  43. supplierOptions.value = [];
  44. function getSupplier(params: any) {
  45. getAllSupplierMethod(params).then((res: unknown) => {
  46. if (Array.isArray(res) && res.length > 0) {
  47. supplierArr.value = res as any[];
  48. supplierOptions.value = (res as any[]).map((item: any) => ({
  49. label: item.name,
  50. value: item.id,
  51. }));
  52. }
  53. });
  54. }
  55. watch(
  56. [() => props.data.conditioningProgramType, () => props.data.institutionId],
  57. ([newType, newInstitutionId]) => {
  58. if (newType) {
  59. props.data.conditioningProgramSupplierId = '';
  60. props.data.isOffline = null;
  61. }
  62. getSupplier({
  63. conditioningProgramTypes: newType ? [newType] : [],
  64. collaborateDeptId: newInstitutionId ? newInstitutionId : '',
  65. });
  66. },
  67. { immediate: true }
  68. );
  69. let showOffLine = ref<boolean>(false);
  70. function getIsonline(newSupplierId: any) {
  71. if (!supplierArr.value?.length || !newSupplierId || !props.data?.conditioningProgramType) {
  72. return;
  73. }
  74. const supplier = supplierArr.value.find((item) => item.id === newSupplierId);
  75. if (!supplier) {
  76. return;
  77. }
  78. const { offlineCPTypes, onlineCPTypes } = supplier;
  79. const programType = props.data.conditioningProgramType;
  80. // 检查是否同时支持线上和线下
  81. const supportsOffline = offlineCPTypes?.includes(programType);
  82. const supportsOnline = onlineCPTypes?.includes(programType);
  83. if (supportsOffline && supportsOnline) {
  84. showOffLine.value = true;
  85. return;
  86. }
  87. // 设置显示状态
  88. showOffLine.value = false;
  89. // 根据支持情况设置项目类型
  90. if (supportsOffline) {
  91. props.data.isOffline = 'Y';
  92. } else if (supportsOnline) {
  93. props.data.isOffline = 'N';
  94. } else {
  95. props.data.isOffline = null;
  96. }
  97. }
  98. watch(
  99. () => props.data.conditioningProgramSupplierId,
  100. async (newSupplierId: any) => {
  101. if (newSupplierId) {
  102. await nextTick();
  103. if (supplierArr.value && supplierArr.value.length > 0) {
  104. getIsonline(newSupplierId);
  105. } else {
  106. // 如果数据还没加载,等待一段时间后重试
  107. setTimeout(() => {
  108. if (supplierArr.value && supplierArr.value.length > 0) {
  109. getIsonline(newSupplierId);
  110. }
  111. }, 200);
  112. }
  113. }
  114. }
  115. );
  116. const uploadProps = reactive({ showRemoveIcon: true });
  117. function customUpload(e: any) {
  118. // uploadApi 你的二次封装上传接口
  119. UploadIFile(e.file)
  120. .then((res) => {
  121. // 调用实例的成功方法通知组件该文件上传成功
  122. e.onSuccess(res, e);
  123. })
  124. .catch((err) => {
  125. // 调用实例的失败方法通知组件该文件上传失败
  126. e.onError(err);
  127. });
  128. }
  129. const visible = ref<boolean>(false);
  130. const setVisible = (value: boolean): void => {
  131. visible.value = value;
  132. };
  133. const previewImg = ref<string>('');
  134. // 预览图片
  135. const handlePreview = async (file: UploadFile) => {
  136. previewImg.value = file.response?.url ?? file.thumbUrl;
  137. visible.value = true;
  138. };
  139. function removeHerb(idx: number) {
  140. if (Array.isArray(props.data.cpMedicines)) {
  141. props.data.cpMedicines.splice(idx, 1);
  142. }
  143. }
  144. function addHerb() {
  145. if (!Array.isArray(props.data.cpMedicines)) {
  146. props.data.cpMedicines = [] as any[];
  147. }
  148. (props.data.cpMedicines as any[]).push({ name: '', dosage: '' } as any);
  149. }
  150. function handleCancel() {
  151. VxeUI.modal.close('confirm-item-modal');
  152. }
  153. // Define emits
  154. const emit = defineEmits<{
  155. (e: 'submit', data: SystemItemModel): void;
  156. }>();
  157. function validate(): boolean {
  158. const d = props.data as any;
  159. // 计价规则 必填
  160. if (d.pricingType === undefined || d.pricingType === null || d.pricingType === '') {
  161. notification.error({
  162. message: '请选择计价规则',
  163. description: '请选择计价规则',
  164. });
  165. return false;
  166. }
  167. // 单价 必填(当一口价时)
  168. if (d.pricingType === '0') {
  169. const unitPrice = d.cpFixedPricingRule?.unitPrice;
  170. if (unitPrice === undefined || unitPrice === null || unitPrice === '') {
  171. notification.error({
  172. message: '请填写单价',
  173. description: '请填写单价',
  174. });
  175. return false;
  176. }
  177. // 计价单位 必填
  178. if (!d.cpFixedPricingRule?.pricingUnit) {
  179. notification.error({
  180. message: '请填写计价单位',
  181. description: '请填写计价单位',
  182. });
  183. return false;
  184. }
  185. // 转换剂量 必填
  186. if (!d.cpFixedPricingRule?.convertDose) {
  187. notification.error({
  188. message: '请填写转换剂量',
  189. description: '请填写转换剂量',
  190. });
  191. return false;
  192. }
  193. // 转换单位 必填
  194. if (!d.cpFixedPricingRule?.convertUnit) {
  195. notification.error({
  196. message: '请选择转换单位',
  197. description: '请选择转换单位',
  198. });
  199. return false;
  200. }
  201. }
  202. // 穴位/经络/部位 范围 必填(当动态计价时)
  203. if (d.pricingType === '1') {
  204. const rules = d.cpDynamicPricingRule;
  205. if (!Array.isArray(rules) || rules.length < 2) {
  206. notification.error({
  207. message: '请完善穴位/经络/部位的计价规则',
  208. description: '请完善穴位/经络/部位的计价规则',
  209. });
  210. return false;
  211. }
  212. // 验证计价1
  213. const rule1 = rules[0];
  214. if (!rule1 || rule1.min === undefined || rule1.min === '') {
  215. notification.error({
  216. message: '请填写计价1的穴位/经络/部位范围',
  217. description: '请填写计价1的穴位/经络/部位范围',
  218. });
  219. return false;
  220. }
  221. if (rule1.priceType === undefined || rule1.priceType === null || rule1.priceType === '') {
  222. notification.error({
  223. message: '请选择计价1的计价类型',
  224. description: '请选择计价1的计价类型',
  225. });
  226. return false;
  227. }
  228. if (!rule1.price || rule1.price === '') {
  229. notification.error({
  230. message: '请填写计价1的价格',
  231. description: '请填写计价1的价格',
  232. });
  233. return false;
  234. }
  235. // 验证计价2
  236. const rule2 = rules[1];
  237. if (!rule2 || rule2.max === undefined || rule2.max === '') {
  238. notification.error({
  239. message: '请填写计价2的穴位/经络/部位范围',
  240. description: '请填写计价2的穴位/经络/部位范围',
  241. });
  242. return false;
  243. }
  244. if (rule2.priceType === undefined || rule2.priceType === null || rule2.priceType === '') {
  245. notification.error({
  246. message: '请选择计价2的计价类型',
  247. description: '请选择计价2的计价类型',
  248. });
  249. return false;
  250. }
  251. if (!rule2.price || rule2.price === '') {
  252. notification.error({
  253. message: '请填写计价2的价格',
  254. description: '请填写计价2的价格',
  255. });
  256. return false;
  257. }
  258. }
  259. // 机构 必填
  260. if (!d.institutionId) {
  261. notification.error({
  262. message: '请选择机构名称',
  263. description: '请选择机构名称',
  264. });
  265. return false;
  266. }
  267. // 供应商 必填
  268. if (!d.conditioningProgramSupplierId) {
  269. notification.error({
  270. message: '请选择供应商',
  271. description: '请选择供应商',
  272. });
  273. return false;
  274. }
  275. return true;
  276. }
  277. function handleOk() {
  278. if (!validate()) return;
  279. if (showOffLine.value && !props.data.isOffline) {
  280. props.data.isOffline = 'N';
  281. }
  282. confirmOrgConfirmMethod(props.data as SystemItemModel).then(() => {
  283. emit('submit', props.data as SystemItemModel);
  284. VxeUI.modal.close('confirm-item-modal');
  285. notification.success({
  286. message: '提交成功',
  287. description: '提交成功',
  288. });
  289. });
  290. }
  291. const isOffline = ref<boolean>(false);
  292. function toggleOnlineStatus() {
  293. props.data.isOffline = isOffline.value ? 'Y' : 'N';
  294. }
  295. onMounted(async () => {
  296. if (props.data.id) {
  297. const res = await getConditioningSchemeDetailMethod(props.data);
  298. Object.assign(props.data, res);
  299. getIsonline(props.data.conditioningProgramSupplierId);
  300. }
  301. fileList.value = props.data?.photo
  302. ? [
  303. {
  304. uid: '-1',
  305. name: 'image.png',
  306. status: 'done',
  307. url: props.data?.photo,
  308. thumbUrl: props.data?.photo,
  309. },
  310. ]
  311. : [];
  312. isOffline.value = props.data.isOffline === 'Y';
  313. });
  314. // 安全获取弹窗容器
  315. function getSafePopupContainer(triggerNode: HTMLElement) {
  316. return document?.body || triggerNode?.parentNode || triggerNode;
  317. }
  318. // 处理机构选择变化
  319. function handleInstitutionChange(value: string | number, node: any) {
  320. props.data.institutionId = value;
  321. props.data.institutionName = node?.label || node?.title || '';
  322. }
  323. </script>
  324. <template>
  325. <div class="form-container">
  326. <div class="form-row">
  327. <label>项目名称:</label>
  328. <span>{{ data.name }}</span>
  329. </div>
  330. <div class="form-row">
  331. <label>方案类型:</label>
  332. <div>{{ data.conditioningProgramType }}</div>
  333. </div>
  334. <div class="form-row">
  335. <label><span class="required-star">*</span>计价规则:</label>
  336. <div>{{ data.pricingType === '0' ? '一口价' : '按穴位/经络/部位' }}</div>
  337. </div>
  338. <div class="form-row" v-if="data.pricingType === '0'">
  339. <label><span class="required-star">*</span>单价:</label>
  340. <a-input v-model:value="data.cpFixedPricingRule!.unitPrice" placeholder="请输入" style="width: 100px; margin-left: 8px" />
  341. <span style="margin-left: 20px">元</span>
  342. <span style="margin-left: 20px">计价单位:</span>
  343. <a-input v-model:value="data.cpFixedPricingRule!.pricingUnit" placeholder="请输入" style="width: 100px; margin-left: 8px" />
  344. <span style="margin-left: 20px">相当于</span>
  345. <a-input v-model:value="data.cpFixedPricingRule!.convertDose" placeholder="请输入" style="width: 100px; margin-left: 8px" />
  346. <vxe-select v-model="data.cpFixedPricingRule!.convertUnit" style="width: 100px; margin-left: 8px" :options="unitOptions" placeholder="请选择" clearable transfer />
  347. <span style="color: #aaa; margin-left: 10px">(使用单位)</span>
  348. </div>
  349. <div v-if="data.pricingType === '1'" class="per-rule">
  350. <div class="price-row">
  351. <span><span class="required-star">*</span>计价1:</span>
  352. <span class="flex items-center">当"穴位/经络/部位" ≤ <a-input placeholder="请输入" class="w-20 ml-2 mr-2" v-model:value="data.cpDynamicPricingRule![0].min" />个时,</span>
  353. <a-select
  354. v-model:value="data.cpDynamicPricingRule![0].priceType"
  355. :options="[
  356. { label: '单价', value: 0, priceType: 0 },
  357. { label: '一口价', value: 1, priceType: 1 },
  358. ]"
  359. style="width: 100px; margin: 0 4px"
  360. placeholder="请选择"
  361. />
  362. <span>=</span>
  363. <a-input v-model:value="data.cpDynamicPricingRule![0].price" style="width: 80px; margin: 0 4px" placeholder="请输入" />
  364. <span>元</span>
  365. </div>
  366. <div class="price-row">
  367. <span><span class="required-star">*</span>计价2:</span>
  368. <span class="flex items-center"
  369. >当"穴位/经络/部位" &gt; <a-input placeholder="请输入" class="w-20 ml-2 mr-2" v-model:value="data.cpDynamicPricingRule![1].max" disabled />个时,</span
  370. >
  371. <a-select
  372. v-model:value="data.cpDynamicPricingRule![1].priceType"
  373. :options="[
  374. { label: '单价', value: 0, priceType: 0 },
  375. { label: '一口价', value: 1, priceType: 1 },
  376. ]"
  377. style="width: 100px; margin: 0 4px"
  378. placeholder="请选择"
  379. />
  380. <span>=</span>
  381. <a-input v-model:value="data.cpDynamicPricingRule![1].price" style="width: 80px; margin: 0 4px" placeholder="请输入" />
  382. <span>元</span>
  383. </div>
  384. </div>
  385. <div class="form-row">
  386. <label>功效:</label>
  387. <input v-model="data.effect" style="width: 400px" />
  388. </div>
  389. <div class="form-row">
  390. <label>中药组成:</label>
  391. <div class="herb-list">
  392. <template v-for="(herb, idx) in data.cpMedicines || []" :key="idx">
  393. <div class="herb-item">
  394. <button class="herb-remove" @click="removeHerb(idx)" type="button">×</button>
  395. <RemoteSelect :load="cpMedicinesMethod" key-prop="name" v-model:value="herb.name" />
  396. <a-input v-model:value="herb.dosage" class="herb-dosage" placeholder="剂量" />
  397. <span>g</span>
  398. </div>
  399. </template>
  400. <button style="margin-left: 8px" @click="addHerb" type="button">+</button>
  401. </div>
  402. </div>
  403. <div class="form-row">
  404. <label><span class="required-star">*</span>机构名称:</label>
  405. <a-tree-select
  406. v-model:value="data.institutionId"
  407. style="width: 400px"
  408. :dropdown-style="{ maxHeight: '400px', overflow: 'auto', zIndex: 4000 }"
  409. placeholder="请选择"
  410. allow-clear
  411. tree-default-expand-all
  412. :tree-data="branch"
  413. :getPopupContainer="getSafePopupContainer"
  414. :dropdownMatchSelectWidth="false"
  415. @change="handleInstitutionChange"
  416. ></a-tree-select>
  417. </div>
  418. <div class="form-row">
  419. <label><span class="required-star">*</span>供应商:</label>
  420. <vxe-select v-model="data.conditioningProgramSupplierId" :options="supplierOptions" placeholder="请选择供应商" style="width: 400px" clearable class="mr-10" />
  421. <a-checkbox v-model:checked="isOffline" style="margin-right: 8px" @change="toggleOnlineStatus" v-show="showOffLine"> 线下项目 </a-checkbox>
  422. </div>
  423. <div class="form-row">
  424. <label>图片:</label>
  425. <a-upload
  426. :showUploadList="uploadProps"
  427. v-model:file-list="fileList"
  428. name="avatar"
  429. list-type="picture-card"
  430. class="avatar-uploader"
  431. @preview="handlePreview"
  432. :maxCount="1"
  433. action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
  434. :customRequest="customUpload"
  435. >
  436. <div v-if="fileList.length < 1">
  437. <PlusOutlined />
  438. <div style="margin-top: 8px">上传</div>
  439. </div>
  440. </a-upload>
  441. <a-image
  442. :width="200"
  443. :style="{ display: 'none' }"
  444. :preview="{
  445. visible,
  446. onVisibleChange: setVisible,
  447. }"
  448. :src="previewImg"
  449. />
  450. </div>
  451. <div class="form-actions-center">
  452. <button @click="handleCancel" class="cancel">取消</button>
  453. <button class="primary" @click="handleOk">确定</button>
  454. </div>
  455. </div>
  456. </template>
  457. <style scoped lang="scss">
  458. body,
  459. html,
  460. .form-container {
  461. overflow-x: hidden !important;
  462. }
  463. .per-rule {
  464. margin-bottom: 16px;
  465. }
  466. .price-row {
  467. display: flex;
  468. align-items: center;
  469. margin-bottom: 10px;
  470. }
  471. .label {
  472. font-weight: 500;
  473. color: #222;
  474. margin-right: 8px;
  475. .required-star {
  476. color: #ff4d4f;
  477. margin-right: 4px;
  478. }
  479. }
  480. .form-container {
  481. position: relative;
  482. min-height: 500px; /* 根据实际内容调整 */
  483. padding-bottom: 70px; /* 给底部按钮留空间 */
  484. margin: 0 auto;
  485. padding: 32px 0px 32px 30px;
  486. }
  487. .form-row {
  488. width: 100%;
  489. overflow: visible;
  490. display: flex;
  491. align-items: center;
  492. margin-bottom: 18px;
  493. label {
  494. width: 110px;
  495. font-weight: 500;
  496. color: #222;
  497. .required-star {
  498. color: #ff4d4f;
  499. margin-right: 4px;
  500. }
  501. }
  502. input,
  503. select {
  504. margin-right: 8px;
  505. padding: 4px 8px;
  506. border: 1px solid #d9d9d9;
  507. border-radius: 4px;
  508. }
  509. .herb-list {
  510. display: flex;
  511. align-items: center;
  512. flex-wrap: wrap;
  513. }
  514. .herb-item {
  515. position: relative;
  516. display: flex;
  517. align-items: center;
  518. margin-right: 8px;
  519. margin-bottom: 8px;
  520. padding-left: 16px; /* 给左上角按钮留空间 */
  521. }
  522. .herb-name {
  523. width: 80px;
  524. padding: 2px 6px;
  525. font-size: 14px;
  526. }
  527. .herb-dosage {
  528. width: 100px;
  529. // padding: 2px 6px;
  530. font-size: 14px;
  531. }
  532. .herb-remove {
  533. position: absolute;
  534. left: 0;
  535. top: 0;
  536. background: #fff;
  537. border: none;
  538. color: #ff4d4f;
  539. font-size: 14px;
  540. cursor: pointer;
  541. line-height: 1;
  542. width: 16px;
  543. height: 16px;
  544. padding: 0;
  545. display: flex;
  546. align-items: center;
  547. justify-content: center;
  548. }
  549. }
  550. .form-actions-center {
  551. display: flex;
  552. justify-content: center;
  553. gap: 16px;
  554. margin-top: 32px;
  555. }
  556. .primary {
  557. background: #1890ff;
  558. color: #fff;
  559. border: none;
  560. padding: 0 24px;
  561. height: 36px;
  562. border-radius: 4px;
  563. }
  564. .cancel {
  565. background: #fff;
  566. color: #222;
  567. border: 1px solid #d9d9d9;
  568. border-radius: 4px;
  569. padding: 0 24px;
  570. height: 36px;
  571. margin-right: 10px;
  572. }
  573. .required-star {
  574. color: #ff4d4f;
  575. margin-right: 4px;
  576. }
  577. </style>