ConfirmItems.vue 18 KB

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