MaterialTable.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. <template>
  2. <div class="material-table">
  3. <el-table :data="tableData" border :max-height="600">
  4. <el-table-column type="index" label="组号" width="60" align="center" />
  5. <el-table-column label="耗材" min-width="300">
  6. <template #default="{row}">
  7. <div class="material-group">
  8. <div class="material-row" style="margin-bottom: 10px">
  9. <el-select v-model="row.consumableId" placeholder="请选择" @change="changeConsumableSelect">
  10. <el-option v-for="pn in suppliesList" :key="pn.id" :label="pn.suppliesName" :value="pn.id + '_' + row.sn"></el-option>
  11. </el-select>
  12. </div>
  13. </div>
  14. </template>
  15. </el-table-column>
  16. <el-table-column label="规格" width="100" align="center">
  17. <template #default="{row}">
  18. <div class="spec-group">
  19. <span>{{ row.specification }}</span>
  20. </div>
  21. </template>
  22. </el-table-column>
  23. <el-table-column label="用量/次" width="200" align="center">
  24. <template #default="{row}">
  25. <div class="dosage-group">
  26. <div class="dosage-input">
  27. <el-input v-model="row.dosePerTime" placeholder="请输入" class="input-center" @input="dosePerTimeInput(row.dosePerTime, row.sn)">
  28. <template #append>袋</template>
  29. </el-input>
  30. </div>
  31. </div>
  32. </template>
  33. </el-table-column>
  34. <el-table-column label="频次" width="100" align="center">
  35. <template #default="{row}">
  36. <span>一天{{ row.frequency }}次</span>
  37. </template>
  38. </el-table-column>
  39. <el-table-column label="用量/日" width="100" align="center">
  40. <template #default="{row}">
  41. <span>{{ row.dosePerDay }}</span>
  42. <span class="unit">袋</span>
  43. </template>
  44. </el-table-column>
  45. <el-table-column label="使用天数" width="100" align="center">
  46. <template #default="{row}">
  47. <span>{{ row.usageDays }}天</span>
  48. </template>
  49. </el-table-column>
  50. <el-table-column label="首日" width="80" align="center">
  51. <template #default="{row}">
  52. <span>{{ row.firstDay }}次</span>
  53. </template>
  54. </el-table-column>
  55. <el-table-column label="数量" width="80" align="center">
  56. <template #default="{row}">
  57. <span>{{ row.quantity }}袋</span>
  58. </template>
  59. </el-table-column>
  60. <el-table-column label="处方备注" width="200" align="center">
  61. <template #default="{row}">
  62. <el-input v-model="row.prescriptionRemark" :maxlength="100" :show-word-limit="true" @change="prescriptionRemark" />
  63. </template>
  64. </el-table-column>
  65. <el-table-column label="金额" width="80" align="center">
  66. <template #default="{row}">
  67. <span>{{ row.amount }}</span>
  68. </template>
  69. </el-table-column>
  70. </el-table>
  71. <div class="table-footer">
  72. <div class="left-buttons">
  73. <el-button type="primary" plain>登为个人模板</el-button>
  74. <el-button type="primary" plain>批量添加营养品</el-button>
  75. <el-button type="primary" plain @click="openTemplate">处方模板</el-button>
  76. </div>
  77. <div class="right-summary">
  78. <div class="summary-list">
  79. <div class="summary-item">
  80. <span class="label">处方费用:</span>
  81. <span class="value">¥{{ nutritionalFee + otherlFee }}</span>
  82. </div>
  83. <div class="summary-item">
  84. <span class="label">营养配置费:</span>
  85. <span class="value">¥{{ prescriptionFee }}</span>
  86. </div>
  87. <div class="summary-item total">
  88. <span class="label">总计:</span>
  89. <span class="value">¥{{ totalFee + otherlFee }}</span>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. <!-- 处方模板弹窗 -->
  95. <el-dialog v-model="dialogVisible" title="选择处方模板" width="60%" :close-on-click-modal="false" class="template-dialog">
  96. <!-- 提示信息 -->
  97. <div class="tip-box">如部分模版未显示请检查预包装库位产品库存,无库存则含该产品的协定方模板将不再显示</div>
  98. <div class="search-box">
  99. <el-input v-model="searchValue" placeholder="请输入名称" class="search-input">
  100. <template #append>
  101. <el-button type="primary">查询</el-button>
  102. </template>
  103. </el-input>
  104. </div>
  105. <div class="dialog-content">
  106. <!-- 左侧树形控件 -->
  107. <div class="left-tree">
  108. <el-tree
  109. ref="treeRef"
  110. :data="treeData"
  111. :props="defaultProps"
  112. node-key="id"
  113. :default-expanded-keys="['1']"
  114. show-checkbox
  115. @check="handleCheck"
  116. />
  117. </div>
  118. <!-- 右侧表格 -->
  119. <div class="right-table">
  120. <div class="table-wrapper">
  121. <table class="custom-table">
  122. <thead>
  123. <tr>
  124. <th width="60">组号</th>
  125. <th>产品名称</th>
  126. <th width="120">用量/次</th>
  127. </tr>
  128. </thead>
  129. <tbody>
  130. <tr v-for="(item, index) in templateTableData" :key="index">
  131. <td>{{ index + 1 }}</td>
  132. <td>{{ item.productName }}</td>
  133. <td>{{ item.dosePerTime }}{{ item.unit }}</td>
  134. </tr>
  135. </tbody>
  136. </table>
  137. </div>
  138. </div>
  139. </div>
  140. <div class="selected-count">已选 ({{ selectedCount }})</div>
  141. <template #footer>
  142. <span class="dialog-footer">
  143. <el-button @click="dialogVisible = false">取消</el-button>
  144. <el-button type="primary" @click="handleConfirm">确定</el-button>
  145. </span>
  146. </template>
  147. </el-dialog>
  148. </div>
  149. </template>
  150. <script setup lang="ts">
  151. import {ref, defineProps} from 'vue';
  152. import {listAll} from '@/api/warehouse/suppliesManage';
  153. import {SuppliesManageVO} from '@/api/warehouse/suppliesManage/types';
  154. import {
  155. listNutritionConsumable,
  156. getNutritionConsumable,
  157. delNutritionConsumable,
  158. addNutritionConsumable,
  159. updateNutritionConsumable
  160. } from '@/api/patients/nutritionConsumable';
  161. import {NutritionConsumableForm} from '@/api/patients/nutritionConsumable/types';
  162. let prescriptionFee = ref(1.0);
  163. let nutritionalFee = ref(0.0);
  164. let otherlFee = ref(0.0);
  165. let totalFee = ref(1.0);
  166. const emit = defineEmits(['change']);
  167. const prop = defineProps({
  168. modelValue: {
  169. type: String,
  170. default: () => undefined
  171. }
  172. });
  173. interface TableRow extends NutritionConsumableForm {
  174. sn?: number;
  175. tableId?: number;
  176. }
  177. // 示例数据
  178. const tableData = ref<TableRow[]>([
  179. {
  180. firstDay: 1,
  181. frequency: 3,
  182. dosePerTime: 1,
  183. dosePerDay: 3,
  184. usageDays: 1,
  185. quantity: 1,
  186. sn: 0,
  187. tableId: 0
  188. }
  189. ]);
  190. let suppliesList = ref<SuppliesManageVO[]>([]);
  191. const getSuppliesList = async () => {
  192. const res = await listAll();
  193. suppliesList.value = res.rows;
  194. };
  195. const changeConsumableSelect = async (id: string) => {
  196. let arr = id.split('_');
  197. for (let item of suppliesList.value) {
  198. let row = tableData.value[Number(arr[1])];
  199. if (item.id.toString() != arr[0]) {
  200. otherlFee.value += item.sellPrice * row.quantity;
  201. continue;
  202. }
  203. let dosePerTime = 0;
  204. if (row.dosePerTime) {
  205. dosePerTime = row.dosePerTime;
  206. }
  207. row.specification = item.suppliesSpec;
  208. row.dosePerDay = row.frequency * dosePerTime;
  209. row.quantity = ((row.usageDays - 1) * row.frequency + row.firstDay) * dosePerTime;
  210. if (row.consumableId) {
  211. let consumableId = row.consumableId.split('_')[0];
  212. for (let item of suppliesList.value) {
  213. if (item.id.toString() != consumableId) {
  214. continue;
  215. }
  216. row.amount = item.sellPrice * row.quantity;
  217. break;
  218. }
  219. }
  220. break;
  221. }
  222. otherlFee.value = 0;
  223. tableData.value.forEach((row) => {
  224. if (row.amount) {
  225. otherlFee.value += row.amount;
  226. }
  227. });
  228. emit('change', JSON.stringify(tableData.value));
  229. };
  230. watch(
  231. () => prop.modelValue,
  232. async (val: string) => {
  233. if (!val || val.trim().length == 0) {
  234. return;
  235. }
  236. let configData = JSON.parse(val);
  237. let tableDataMap = {};
  238. tableData.value.forEach((item: any) => {
  239. tableDataMap[item.tableId] = item;
  240. });
  241. const newTableList = ref<TableRow[]>([]);
  242. prescriptionFee.value = 0;
  243. totalFee.value = 0;
  244. otherlFee.value = 0;
  245. nutritionalFee.value = 0;
  246. prescriptionFee.value += configData.length;
  247. totalFee.value += configData.length;
  248. configData.forEach((item: any) => {
  249. item.products.forEach((pd) => {
  250. if (pd.amount) {
  251. nutritionalFee.value += pd.amount;
  252. totalFee.value += pd.amount;
  253. }
  254. });
  255. let row: TableRow = {
  256. frequency: Number(item.frequency),
  257. usageDays: Number(item.usageDays),
  258. firstDay: Number(item.firstDay),
  259. dosePerTime: 1,
  260. tableId: item.tableId,
  261. sn: newTableList.value.length
  262. };
  263. let tmpData = tableDataMap[item.tableId];
  264. if (tmpData) {
  265. row = tmpData;
  266. }
  267. let dosePerTime = 0;
  268. if (row.dosePerTime) {
  269. dosePerTime = row.dosePerTime;
  270. }
  271. row.frequency = Number(item.frequency);
  272. row.usageDays = Number(item.usageDays);
  273. row.firstDay = Number(item.firstDay);
  274. row.dosePerDay = item.frequency * dosePerTime;
  275. row.quantity = ((row.usageDays - 1) * row.frequency + row.firstDay) * dosePerTime;
  276. if (row.consumableId) {
  277. let consumableId = row.consumableId.split('_')[0];
  278. for (let item of suppliesList.value) {
  279. if (item.id.toString() != consumableId) {
  280. continue;
  281. }
  282. row.amount = item.sellPrice * row.quantity;
  283. break;
  284. }
  285. }
  286. if (row.amount) {
  287. otherlFee.value += row.amount;
  288. }
  289. newTableList.value.push(row);
  290. });
  291. tableData.value = newTableList.value;
  292. emit('change', JSON.stringify(tableData.value));
  293. },
  294. {deep: true, immediate: true}
  295. );
  296. const dosePerTimeInput = (value: string, sn: number) => {
  297. let row = tableData.value[sn];
  298. let dosePerTime = 0;
  299. if (row.dosePerTime) {
  300. dosePerTime = row.dosePerTime;
  301. }
  302. row.dosePerDay = row.frequency * dosePerTime;
  303. row.quantity = ((row.usageDays - 1) * row.frequency + row.firstDay) * dosePerTime;
  304. otherlFee.value = 0;
  305. if (row.consumableId) {
  306. let consumableId = row.consumableId.split('_')[0];
  307. for (let item of suppliesList.value) {
  308. if (item.id.toString() != consumableId) {
  309. otherlFee.value += item.sellPrice * row.quantity;
  310. continue;
  311. }
  312. row.amount = item.sellPrice * row.quantity;
  313. otherlFee.value += row.amount;
  314. break;
  315. }
  316. }
  317. otherlFee.value = 0;
  318. tableData.value.forEach((row) => {
  319. if (row.amount) {
  320. otherlFee.value += row.amount;
  321. }
  322. });
  323. emit('change', JSON.stringify(tableData.value));
  324. };
  325. const prescriptionRemark = (value: string) => {
  326. emit('change', JSON.stringify(tableData.value));
  327. };
  328. // 弹窗显示控制
  329. const dialogVisible = ref(false);
  330. // 搜索值
  331. const searchValue = ref('');
  332. // 选中的节点
  333. const selectedNode = ref('');
  334. // 已选数量
  335. const selectedCount = ref(2);
  336. // 树形控件数据
  337. const treeData = ref([
  338. {
  339. id: '1',
  340. label: '基础公共模板',
  341. children: [
  342. {
  343. id: '1-1',
  344. label: '精氨酸组件'
  345. },
  346. {
  347. id: '1-2',
  348. label: '谷氨酰胺组件'
  349. },
  350. {
  351. id: '1-3',
  352. label: '免疫营养组件'
  353. },
  354. {
  355. id: '1-4',
  356. label: '肠内营养组件'
  357. },
  358. {
  359. id: '1-5',
  360. label: '肠外营养组件'
  361. }
  362. ]
  363. },
  364. {
  365. id: '2',
  366. label: '万颗星营养智能医院',
  367. children: [
  368. {
  369. id: '2-1',
  370. label: '中链甘油三酯MCT组件'
  371. },
  372. {
  373. id: '2-2',
  374. label: '口服补糖盐饮品'
  375. },
  376. {
  377. id: '2-3',
  378. label: '膳食纤维组件'
  379. },
  380. {
  381. id: '2-4',
  382. label: 'ERAS专用碳水化合物饮品'
  383. },
  384. {
  385. id: '2-5',
  386. label: '白蛋白肽组件'
  387. },
  388. {
  389. id: '2-6',
  390. label: '乳清蛋白组件'
  391. },
  392. {
  393. id: '2-7',
  394. label: '益生菌组件'
  395. },
  396. {
  397. id: '2-8',
  398. label: '鱼油蛋白组件'
  399. }
  400. ]
  401. },
  402. {
  403. id: '3',
  404. label: '个人模板',
  405. children: [
  406. {
  407. id: '3-1',
  408. label: '术前营养支持方案'
  409. },
  410. {
  411. id: '3-2',
  412. label: '术后恢复营养方案'
  413. },
  414. {
  415. id: '3-3',
  416. label: '重症营养支持方案'
  417. }
  418. ]
  419. },
  420. {
  421. id: '4',
  422. label: '科室模板',
  423. children: [
  424. {
  425. id: '4-1',
  426. label: '外科营养支持方案'
  427. },
  428. {
  429. id: '4-2',
  430. label: '内科营养支持方案'
  431. },
  432. {
  433. id: '4-3',
  434. label: 'ICU营养支持方案'
  435. },
  436. {
  437. id: '4-4',
  438. label: '肿瘤科营养支持方案'
  439. }
  440. ]
  441. }
  442. ]);
  443. // 树形控件配置
  444. const defaultProps = {
  445. children: 'children',
  446. label: 'label'
  447. };
  448. // 模板表格数据
  449. const templateTableData = ref([
  450. {
  451. productName: '氨基酸型复配营养粉2',
  452. dosePerTime: '5.0000',
  453. unit: 'g'
  454. },
  455. {
  456. productName: '肠内营养全营养复合膜',
  457. dosePerTime: '1.0000',
  458. unit: '袋'
  459. }
  460. ]);
  461. // 树节点选中事件
  462. const handleCheck = (data: any, checked: any) => {
  463. console.log(data, checked);
  464. // 这里处理节点选中逻辑
  465. };
  466. // 打开处方模板
  467. const openTemplate = () => {
  468. dialogVisible.value = true;
  469. };
  470. // 确认选择模板
  471. const handleConfirm = () => {
  472. dialogVisible.value = false;
  473. // 这里处理选中模板后的逻辑
  474. };
  475. onMounted(() => {
  476. getSuppliesList();
  477. });
  478. </script>
  479. <style lang="scss" scoped>
  480. .material-table {
  481. :deep(.el-table) {
  482. // 表头样式
  483. .el-table__header {
  484. th {
  485. background-color: #f5f7fa;
  486. color: #606266;
  487. font-weight: 500;
  488. padding: 8px 0;
  489. height: 40px;
  490. }
  491. }
  492. // 表格行样式
  493. .el-table__row {
  494. td {
  495. padding: 4px 8px;
  496. }
  497. }
  498. }
  499. // 耗材输入框组样式
  500. .material-group {
  501. display: flex;
  502. flex-direction: column;
  503. gap: 8px;
  504. .material-row {
  505. display: flex;
  506. align-items: center;
  507. gap: 8px;
  508. .el-input {
  509. flex: 1;
  510. }
  511. }
  512. }
  513. // 规格组样式
  514. .spec-group {
  515. display: flex;
  516. flex-direction: column;
  517. gap: 20px;
  518. padding: 8px 0;
  519. span {
  520. color: #606266;
  521. font-size: 14px;
  522. }
  523. }
  524. // 用量输入框组样式
  525. .dosage-group {
  526. display: flex;
  527. flex-direction: column;
  528. gap: 8px;
  529. .dosage-input {
  530. display: flex;
  531. align-items: center;
  532. justify-content: center;
  533. gap: 4px;
  534. .input-center {
  535. width: 100px;
  536. :deep(.el-input__inner) {
  537. text-align: center;
  538. }
  539. }
  540. .unit {
  541. color: #909399;
  542. font-size: 14px;
  543. flex-shrink: 0;
  544. }
  545. }
  546. }
  547. // 餐次时间样式
  548. .time-slots {
  549. display: flex;
  550. flex-direction: column;
  551. gap: 8px;
  552. .time-row {
  553. display: flex;
  554. gap: 8px;
  555. .time-group {
  556. display: flex;
  557. align-items: center;
  558. gap: 4px;
  559. :deep(.el-checkbox) {
  560. margin-right: 0;
  561. .el-checkbox__label {
  562. display: none;
  563. }
  564. }
  565. :deep(.el-time-select) {
  566. .el-input {
  567. width: 120px;
  568. }
  569. .el-input__wrapper {
  570. padding: 0 8px;
  571. border-radius: 2px;
  572. box-shadow: 0 0 0 1px #dcdfe6 inset;
  573. &:hover:not(.is-disabled) {
  574. box-shadow: 0 0 0 1px #c0c4cc inset;
  575. }
  576. &.is-disabled {
  577. background-color: #f5f7fa;
  578. box-shadow: 0 0 0 1px #e4e7ed inset;
  579. .el-input__inner {
  580. color: #909399;
  581. -webkit-text-fill-color: #909399;
  582. }
  583. }
  584. }
  585. .el-input__inner {
  586. height: 32px;
  587. text-align: center;
  588. font-size: 14px;
  589. color: #606266;
  590. }
  591. }
  592. }
  593. }
  594. }
  595. // 操作按钮样式
  596. :deep(.el-button--link) {
  597. height: 28px;
  598. padding: 0 8px;
  599. font-size: 13px;
  600. &.el-button--primary {
  601. color: var(--el-color-primary);
  602. &:hover {
  603. color: var(--el-color-primary-light-3);
  604. }
  605. }
  606. &.el-button--danger {
  607. color: var(--el-color-danger);
  608. &:hover {
  609. color: var(--el-color-danger-light-3);
  610. }
  611. }
  612. }
  613. .table-footer {
  614. display: flex;
  615. justify-content: space-between;
  616. align-items: flex-start;
  617. margin-top: 16px;
  618. .left-buttons {
  619. display: flex;
  620. gap: 12px;
  621. .el-button {
  622. height: 32px;
  623. padding: 0 16px;
  624. font-size: 14px;
  625. }
  626. }
  627. .right-summary {
  628. min-width: 200px;
  629. .summary-list {
  630. display: flex;
  631. flex-direction: column;
  632. align-items: flex-end;
  633. gap: 4px;
  634. .summary-item {
  635. display: flex;
  636. align-items: center;
  637. justify-content: flex-end;
  638. gap: 8px;
  639. text-align: right;
  640. .label {
  641. font-size: 16px;
  642. color: #606266;
  643. }
  644. .value {
  645. font-size: 20px;
  646. color: #606266;
  647. min-width: 80px;
  648. text-align: right;
  649. }
  650. &.total {
  651. margin-top: 4px;
  652. .label,
  653. .value {
  654. color: #f56c6c;
  655. font-weight: 500;
  656. }
  657. }
  658. }
  659. }
  660. }
  661. }
  662. }
  663. .template-dialog {
  664. :deep(.el-dialog__body) {
  665. padding: 0 20px;
  666. }
  667. .tip-box {
  668. background-color: #e6f7ff;
  669. padding: 12px 16px;
  670. color: #606266;
  671. font-size: 14px;
  672. margin-bottom: 16px;
  673. }
  674. .search-box {
  675. margin-bottom: 16px;
  676. .search-input {
  677. width: 300px;
  678. }
  679. }
  680. .dialog-content {
  681. display: flex;
  682. min-height: 350px;
  683. border: 1px solid #ebeef5;
  684. .left-tree {
  685. width: 220px;
  686. padding: 12px;
  687. border-right: 1px solid #ebeef5;
  688. max-height: 350px;
  689. overflow-y: auto;
  690. :deep(.el-tree-node__content) {
  691. height: 32px;
  692. }
  693. :deep(.el-tree-node__content:hover) {
  694. background-color: #f5f7fa;
  695. }
  696. :deep(.el-tree-node.is-current > .el-tree-node__content) {
  697. background-color: #f5f7fa;
  698. }
  699. }
  700. .right-table {
  701. flex: 1;
  702. padding: 12px;
  703. overflow: auto;
  704. .table-wrapper {
  705. margin-bottom: 0;
  706. }
  707. }
  708. }
  709. .selected-count {
  710. margin: 16px 0;
  711. color: #409eff;
  712. font-size: 14px;
  713. }
  714. }
  715. .custom-table {
  716. width: 100%;
  717. border-collapse: collapse;
  718. border: 1px solid #ebeef5;
  719. th,
  720. td {
  721. padding: 12px 8px;
  722. border: 1px solid #ebeef5;
  723. text-align: center;
  724. font-size: 14px;
  725. white-space: nowrap;
  726. overflow: hidden;
  727. text-overflow: ellipsis;
  728. }
  729. th {
  730. background-color: #f5f7fa;
  731. color: #303133;
  732. font-weight: 600;
  733. }
  734. tbody tr:hover {
  735. background-color: #f5f7fa;
  736. }
  737. }
  738. </style>