containercard.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. <template>
  2. <div :style="{ backgroundColor: bgColor }">
  3. <q-dialog
  4. v-model="storage_dialog"
  5. transition-show="jump-down"
  6. transition-hide="jump-up"
  7. @show="prepareDialog()"
  8. >
  9. <q-card style="min-width: 1000px">
  10. <q-bar
  11. class="bg-light-blue-10 text-white rounded-borders"
  12. style="height: 50px"
  13. >
  14. <div>
  15. {{ "托盘" }}
  16. </div>
  17. <q-space></q-space>
  18. {{ containerCode }}
  19. </q-bar>
  20. <q-card-section class="q-pt-md">
  21. <q-tabs v-model="activeTab">
  22. <q-tab name="tab2" label="托盘信息" />
  23. <q-tab name="tab3" label="托盘详情" />
  24. <q-tab name="tab1" label="操作记录" />
  25. </q-tabs>
  26. </q-card-section>
  27. <q-tab-panels v-model="activeTab" animated>
  28. <q-tab-panel name="tab2" style="height: 600px">
  29. <template>
  30. <div class="text-h6 q-mb-md">{{ "托盘信息" }}</div>
  31. <q-table
  32. v-if="storage_form.length > 0"
  33. :data="storage_form"
  34. :columns="columns_batch"
  35. row-key="id"
  36. flat
  37. bordered
  38. hide-pagination
  39. class="my-sticky-table scrollable-table"
  40. :style="{ 'max-height': '400px' }"
  41. :container-style="{ height: 'auto' }"
  42. :pagination="{ rowsPerPage: 0 }"
  43. >
  44. </q-table>
  45. <div style="float: right; min-width: 100%" flow="row wrap">
  46. <q-card class="q-mb-md" bordered>
  47. <q-card-actions
  48. class="q-px-none"
  49. style="
  50. position: absolute;
  51. right: 20px;
  52. top: 10px;
  53. z-index: 100;
  54. "
  55. >
  56. <q-btn
  57. flat
  58. dense
  59. color="primary"
  60. @click="showInventoryDetails = !showInventoryDetails"
  61. >
  62. {{ showInventoryDetails ? "收起" : "展开" }}
  63. </q-btn>
  64. </q-card-actions>
  65. <q-card-section>
  66. <div class="text-h6 q-mb-md">
  67. {{ "物料详情" }}
  68. </div>
  69. <q-table
  70. v-if="showInventoryDetails"
  71. :data="results"
  72. :columns="columns_results"
  73. row-key="id"
  74. flat
  75. bordered
  76. hide-pagination
  77. class="my-sticky-table scrollable-table"
  78. :style="{ 'max-height': '400px' }"
  79. :container-style="{ height: 'auto' }"
  80. :pagination="{ rowsPerPage: 0 }"
  81. >
  82. </q-table>
  83. </q-card-section>
  84. </q-card>
  85. </div>
  86. </template>
  87. </q-tab-panel>
  88. <q-tab-panel name="tab3" style="height: 600px">
  89. <q-table
  90. :data="container_table"
  91. :columns="coloums_container"
  92. row-key="id"
  93. flat
  94. bordered
  95. hide-pagination
  96. class="my-sticky-table scrollable-table"
  97. :style="{ 'max-height': '600px' }"
  98. :container-style="{ height: 'auto' }"
  99. :pagination="{ rowsPerPage: 0 }"
  100. >
  101. <!-- 入库重量列可编辑 - 添加name属性 -->
  102. <template v-slot:body-cell-goods_qty="props">
  103. <q-td :props="props">
  104. <q-input
  105. v-if="props.row.editing && can_edit_detail"
  106. v-model.number="props.row.goods_qty"
  107. type="number"
  108. dense
  109. outlined
  110. @keyup.enter="handleRowSave(props.row)"
  111. />
  112. <span v-else>
  113. {{ props.row.goods_qty }}
  114. </span>
  115. </q-td>
  116. </template>
  117. <!-- 出库重量列可编辑 - 添加name属性 -->
  118. <template v-slot:body-cell-goods_out_qty="props">
  119. <q-td :props="props">
  120. <q-input
  121. v-if="props.row.editing && can_edit_detail"
  122. v-model.number="props.row.goods_out_qty"
  123. type="number"
  124. dense
  125. outlined
  126. @keyup.enter="handleRowSave(props.row)"
  127. />
  128. <span v-else>
  129. {{ props.row.goods_out_qty }}
  130. </span>
  131. </q-td>
  132. </template>
  133. <!-- 操作列按钮 -->
  134. <template v-slot:body-cell-actions="props">
  135. <q-td :props="props">
  136. <template v-if="props.row.editing">
  137. <q-btn
  138. dense
  139. round
  140. color="positive"
  141. icon="save"
  142. @click="handleRowSave(props.row)"
  143. >
  144. <q-tooltip>保存修改</q-tooltip>
  145. </q-btn>
  146. <q-btn
  147. dense
  148. round
  149. color="negative"
  150. icon="cancel"
  151. @click="handleRowCancel(props.row)"
  152. >
  153. <q-tooltip>取消编辑</q-tooltip>
  154. </q-btn>
  155. </template>
  156. <template v-else>
  157. <q-btn
  158. v-if="hasPermission('edit')"
  159. dense
  160. round
  161. color="primary"
  162. icon="edit"
  163. @click="handleRowEdit(props.row)"
  164. >
  165. <q-tooltip>编辑该行数据</q-tooltip>
  166. </q-btn>
  167. <q-btn
  168. dense
  169. v-if="hasPermission('edit')"
  170. round
  171. color="negative"
  172. icon="delete"
  173. @click="handleRowdelete(props.row)"
  174. >
  175. <q-tooltip>删除该行数据</q-tooltip>
  176. </q-btn>
  177. </template>
  178. </q-td>
  179. </template>
  180. </q-table>
  181. </q-tab-panel>
  182. <q-tab-panel name="tab1" style="height: 600px">
  183. <div
  184. style="
  185. float: right;
  186. padding: 15px 15px 50px 15px;
  187. min-width: 100%;
  188. "
  189. flow="row wrap"
  190. >
  191. <q-card class="q-mb-md" bordered>
  192. <q-card-section>
  193. <template>
  194. <div
  195. class="text-h6 q-mb-md"
  196. style="display: flex; justify-content: space-between"
  197. >
  198. {{ "操作记录" }}
  199. </div>
  200. <q-btn
  201. class="q-ml-sm"
  202. color="primary"
  203. icon="event"
  204. @click="sortoperatedetail"
  205. >近一个月操作记录</q-btn
  206. >
  207. <template v-if="listSize > 0">
  208. <q-list bordered class="rounded-borders">
  209. <q-expansion-item
  210. v-for="(node, index) in nodeList"
  211. v-if="node.value && node.value.length > 0"
  212. :key="index"
  213. group="op-group"
  214. :caption="`操作类型: ${formatType(
  215. node.value[0].operation_type
  216. )} ----- 操作时间:${node.value[0].timestamp}`"
  217. >
  218. <q-card>
  219. <q-card-section>
  220. <q-table
  221. :data="node.value"
  222. :columns="columns_operate"
  223. row-key="id"
  224. flat
  225. bordered
  226. hide-pagination
  227. class="my-sticky-table scrollable-table"
  228. :style="{ 'max-height': '400px' }"
  229. :container-style="{ height: 'auto' }"
  230. :pagination="{ rowsPerPage: 0 }"
  231. >
  232. </q-table>
  233. </q-card-section>
  234. </q-card>
  235. </q-expansion-item>
  236. </q-list>
  237. </template>
  238. <div v-else class="text-grey-8">暂无操作记录</div>
  239. </template>
  240. </q-card-section>
  241. </q-card>
  242. </div>
  243. </q-tab-panel>
  244. </q-tab-panels>
  245. </q-card>
  246. </q-dialog>
  247. </div>
  248. </template>
  249. <script>
  250. class ListNode {
  251. constructor (value) {
  252. this.value = value
  253. this.next = null
  254. }
  255. }
  256. class LinkedList {
  257. constructor () {
  258. this.head = null
  259. this.size = 0
  260. }
  261. append (value) {
  262. const newNode = new ListNode(value)
  263. if (!this.head) {
  264. this.head = newNode
  265. } else {
  266. let current = this.head
  267. while (current.next) {
  268. current = current.next
  269. }
  270. current.next = newNode
  271. }
  272. this.size++
  273. }
  274. delete (value) {
  275. if (!this.head) return
  276. if (this.head.value === value) {
  277. this.head = this.head.next
  278. this.size--
  279. return
  280. }
  281. let current = this.head
  282. while (current.next) {
  283. if (current.next.value === value) {
  284. current.next = current.next.next
  285. this.size--
  286. return
  287. }
  288. current = current.next
  289. }
  290. }
  291. toArray () {
  292. const result = []
  293. let current = this.head
  294. while (current) {
  295. result.push({ value: current.value })
  296. current = current.next
  297. }
  298. return result
  299. }
  300. clear () {
  301. this.head = null
  302. this.size = 0
  303. }
  304. }
  305. import { getauth, putauth, deleteauth, postauth } from 'boot/axios_request'
  306. import { LocalStorage } from 'quasar'
  307. export default {
  308. props: {
  309. containerCode: Number,
  310. containerNumber: Number
  311. },
  312. data () {
  313. return {
  314. pathnamecontainer: 'container/locationdetail/',
  315. pathnamecontainer_detail: 'container/containerdetail/',
  316. container_id: 123456,
  317. results: [],
  318. container_table: [],
  319. storage_form: [],
  320. showInventoryDetails: true,
  321. columns_batch: [
  322. {
  323. name: 'bound_number',
  324. label: '批次',
  325. field: (row) => row.bound_number,
  326. align: 'center'
  327. },
  328. {
  329. name: 'plan_weight',
  330. label: '当前库位容纳重量',
  331. field: (row) => row.total_batch_qty,
  332. align: 'center'
  333. }
  334. ],
  335. columns_results: [
  336. {
  337. label: '物料编码',
  338. field: (row) => row.goods_code,
  339. align: 'center'
  340. },
  341. {
  342. label: '物料名称',
  343. field: (row) => row.goods_desc,
  344. align: 'center'
  345. },
  346. {
  347. label: '物料批次',
  348. field: (row) => row.bound_number,
  349. align: 'center'
  350. },
  351. {
  352. label: '件数',
  353. field: (row) => row.group_qty,
  354. align: 'center'
  355. },
  356. {
  357. label: '每件重量',
  358. field: (row) => row.goods_qty,
  359. align: 'center'
  360. },
  361. {
  362. label: '批次计划重量',
  363. field: (row) => row.batch_total_qty,
  364. align: 'center'
  365. },
  366. {
  367. label: '在库重量',
  368. field: (row) => row.batch_total_in_qty,
  369. align: 'center'
  370. },
  371. {
  372. label: '录入时间',
  373. field: (row) => row.create_time.slice(0, 10),
  374. align: 'center'
  375. }
  376. ],
  377. coloums_container: [
  378. {
  379. label: '物料编码',
  380. field: (row) => row.goods_code,
  381. align: 'center'
  382. },
  383. {
  384. label: '物料名称',
  385. field: (row) => row.goods_desc,
  386. align: 'center'
  387. },
  388. {
  389. label: '物料批次',
  390. field: (row) => row.batch,
  391. align: 'center'
  392. },
  393. {
  394. name: 'goods_qty', // 添加 name 属性
  395. label: '入库重量',
  396. field: (row) => row.goods_qty,
  397. align: 'center',
  398. sortable: true
  399. },
  400. {
  401. name: 'goods_out_qty', // 添加 name 属性
  402. label: '出库重量',
  403. field: (row) => row.goods_out_qty,
  404. align: 'center',
  405. sortable: true
  406. },
  407. {
  408. name: 'actions',
  409. label: '编辑',
  410. align: 'center',
  411. field: 'actions',
  412. sortable: false,
  413. headerStyle: 'width: 80px'
  414. }
  415. ],
  416. columns_operate: [
  417. {
  418. name: 'timestamp',
  419. label: '操作时间',
  420. field: (row) => row.timestamp,
  421. align: 'center'
  422. },
  423. {
  424. name: 'operator',
  425. label: '经手人',
  426. field: (row) => row.operator,
  427. align: 'center'
  428. },
  429. {
  430. name: 'batch',
  431. label: '批次',
  432. field: (row) => row.batch.bound_number,
  433. align: 'center'
  434. },
  435. {
  436. name: 'memo',
  437. label: '备注',
  438. field: (row) => row.memo,
  439. align: 'center'
  440. },
  441. {
  442. label: '当前位置',
  443. field: (row) => row.from_location,
  444. align: 'center'
  445. },
  446. {
  447. label: '目标位置',
  448. field: (row) => row.to_location,
  449. align: 'center'
  450. }
  451. ],
  452. user_id: '',
  453. auth_id: '',
  454. can_edit_detail: false,
  455. storage_dialog: false,
  456. bgColor: 'transparent',
  457. error1: this.$t('stock.shelf.error1'),
  458. shelfLocal: '',
  459. activeTab: 'tab2',
  460. operate_detail: [],
  461. userComponentPermissions: [], // 用户权限
  462. login_mode: LocalStorage.getItem('login_mode'), // 登录模式
  463. linkedList: new LinkedList()
  464. }
  465. },
  466. created () {
  467. this.handleclick()
  468. this.loadUserPermissions()
  469. },
  470. computed: {
  471. nodeList () {
  472. return this.linkedList.toArray()
  473. },
  474. listSize () {
  475. return this.linkedList.size
  476. }
  477. },
  478. methods: {
  479. // 检查用户是否有指定页面的组件访问权限
  480. loadUserPermissions () {
  481. postauth('staff/role-comPermissions/' + this.login_mode + '/', {
  482. page: '/container/containerlist'
  483. }).then(
  484. (response) => {
  485. // 处理分组权限结构
  486. this.userComponentPermissions = response
  487. },
  488. (error) => {
  489. this.$q.notify({
  490. type: 'negative',
  491. message: '加载用户权限失败,' + error.message
  492. })
  493. }
  494. )
  495. },
  496. hasPermission (components) {
  497. if (!this.userComponentPermissions) return false
  498. const permission = this.userComponentPermissions.find(
  499. (perm) => perm.component === components
  500. )
  501. return permission && permission.enabled
  502. },
  503. // 进入编辑模式
  504. handleRowEdit (row) {
  505. if (this.can_edit_detail) {
  506. // 保存原始数据用于取消操作时恢复
  507. row.originalData = {
  508. goods_qty: row.goods_qty,
  509. goods_out_qty: row.goods_out_qty
  510. }
  511. // 设置编辑状态
  512. row.editing = true
  513. // 强制更新视图,确保输入框显示
  514. this.$set(this.container_table, this.container_table.indexOf(row), row)
  515. } else {
  516. this.$q.notify({
  517. message: '权限不足,请联系管理员',
  518. icon: 'close',
  519. color: 'negative'
  520. })
  521. }
  522. },
  523. // 保存修改
  524. async handleRowSave (row) {
  525. try {
  526. const payload = {
  527. goods_code: row.goods_code,
  528. goods_desc: row.goods_desc,
  529. goods_qty: row.goods_qty,
  530. goods_weight: 1,
  531. goods_out_qty: row.goods_out_qty
  532. }
  533. await putauth(`container/detail/${row.id}/`, payload)
  534. this.$q.notify({
  535. message: '更新成功',
  536. color: 'positive',
  537. icon: 'check',
  538. timeout: 1000
  539. })
  540. // 退出编辑模式
  541. row.editing = false
  542. delete row.originalData
  543. // 强制更新视图
  544. this.$set(this.container_table, this.container_table.indexOf(row), row)
  545. } catch (err) {
  546. console.error('更新失败:', err)
  547. this.$q.notify({
  548. message: `更新失败: ${
  549. err.response?.data?.detail || err.message || '服务器错误'
  550. }`,
  551. color: 'negative',
  552. icon: 'warning',
  553. timeout: 3000
  554. })
  555. }
  556. },
  557. // 取消编辑
  558. handleRowCancel (row) {
  559. // 恢复原始数据
  560. if (row.originalData) {
  561. row.goods_qty = row.originalData.goods_qty
  562. row.goods_out_qty = row.originalData.goods_out_qty
  563. delete row.originalData
  564. }
  565. // 退出编辑模式
  566. row.editing = false
  567. // 强制更新视图
  568. this.$set(this.container_table, this.container_table.indexOf(row), row)
  569. },
  570. sortoperatedetail () {
  571. const oneMonthAgo = new Date()
  572. oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)
  573. // 过滤出近一个月的操作记录
  574. const filteredList = this.linkedList.toArray().filter((node) => {
  575. return node.value.some(
  576. (item) => new Date(item.timestamp) > oneMonthAgo
  577. )
  578. })
  579. // 使用Vue的响应式特性更新显示
  580. this.$nextTick(() => {
  581. this.visibleNodes = filteredList
  582. this.$q.notify({
  583. message: `已筛选出近一个月操作记录 (共${filteredList.length}组)`,
  584. color: 'info',
  585. timeout: 1500
  586. })
  587. })
  588. },
  589. formatType (type) {
  590. switch (type) {
  591. case 'container':
  592. return '组盘'
  593. case 'outbound':
  594. return '出库'
  595. case 'inbound':
  596. return '入库'
  597. case 'adjust':
  598. return '移库'
  599. case '5':
  600. return '调拨'
  601. case '6':
  602. return '其他'
  603. default:
  604. return '未知'
  605. }
  606. },
  607. getOperationRecord () {
  608. var _this = this
  609. _this.operate_detail = []
  610. var operate_detail_container = []
  611. _this.linkedList.clear()
  612. getauth('container/operate/?status=1&container=' + _this.containerNumber)
  613. .then((res) => {
  614. _this.operate_detail = res.results
  615. if (_this.operate_detail.length === 0) return
  616. // 初始化第一个元素
  617. operate_detail_container.push(_this.operate_detail[0])
  618. for (let i = 0; i < _this.operate_detail.length - 1; i++) {
  619. const current = _this.operate_detail[i]
  620. const next = _this.operate_detail[i + 1]
  621. if (current.operation_type === next.operation_type) {
  622. operate_detail_container.push(next)
  623. } else {
  624. _this.linkedList.append([...operate_detail_container])
  625. operate_detail_container = [next]
  626. }
  627. }
  628. // 添加最后一个分组
  629. if (operate_detail_container.length > 0) {
  630. _this.linkedList.append([...operate_detail_container])
  631. }
  632. })
  633. .catch((err) => {
  634. _this.$q.notify({
  635. message: err.detail || '获取操作记录失败',
  636. icon: 'close',
  637. color: 'negative'
  638. })
  639. })
  640. },
  641. async deleteContainerData (rowData) {
  642. try {
  643. const res = await deleteauth(`container/detail/${rowData.id}/`)
  644. this.$q.notify({
  645. message: '删除成功',
  646. color: 'positive'
  647. })
  648. this.get_container_table() // 刷新数据
  649. } catch (err) {
  650. this.$q.notify({
  651. message: '删除失败: ' + (err.response?.data?.detail || err.message),
  652. color: 'negative'
  653. })
  654. }
  655. },
  656. handleRowdelete (rowData) {
  657. if (this.can_edit_detail === true) {
  658. this.$q
  659. .dialog({
  660. title: '删除物料信息',
  661. message: `确定删除物料 ${rowData.goods_code} (批次${rowData.batch}) 吗?`,
  662. ok: { label: '确定', color: 'negative' },
  663. cancel: { label: '取消', color: 'primary' }
  664. })
  665. .onOk(() => {
  666. this.deleteContainerData(rowData)
  667. })
  668. } else {
  669. this.$q.notify({
  670. message: '权限不足,请联系管理员',
  671. icon: 'close',
  672. color: 'negative'
  673. })
  674. }
  675. },
  676. prepareDialog () {
  677. if (this.hasPermission('edit')) {
  678. this.can_edit_detail = true
  679. } else {
  680. this.can_edit_detail = false
  681. }
  682. },
  683. handleclick () {
  684. this.getList()
  685. this.get_container_table()
  686. this.getOperationRecord()
  687. this.storage_dialog = true
  688. },
  689. get_container_table () {
  690. var _this = this
  691. getauth(
  692. _this.pathnamecontainer_detail + '?container=' + _this.containerNumber
  693. )
  694. .then((res) => {
  695. // 添加编辑状态属性
  696. _this.container_table = res.data.map((item) => ({
  697. ...item,
  698. editing: false
  699. }))
  700. })
  701. .catch((err) => {
  702. console.error('获取托盘详情失败:', err)
  703. _this.$q.notify({
  704. message:
  705. '获取托盘详情失败:' +
  706. (err.response?.data?.detail || err.message),
  707. color: 'negative'
  708. })
  709. })
  710. },
  711. getList () {
  712. var _this = this
  713. _this.storage_form = []
  714. _this.results = []
  715. getauth(_this.pathnamecontainer + '?container=' + _this.containerNumber)
  716. .then((res) => {
  717. var data = res.data
  718. this.storage_form = data.batch_totals || []
  719. this.results = data.results || []
  720. })
  721. .catch((err) => {
  722. console.error('获取托盘信息失败:', err)
  723. _this.$q.notify({
  724. message:
  725. '获取托盘信息失败:' +
  726. (err.response?.data?.detail || err.message),
  727. color: 'negative'
  728. })
  729. })
  730. }
  731. }
  732. }
  733. </script>
  734. <style scoped>
  735. :deep(.q-field__label) {
  736. margin-top: 8px;
  737. align-self: center;
  738. }
  739. :deep(.q-field__control-container) {
  740. padding-left: 50px;
  741. margin-top: -5px;
  742. }
  743. :deep(.q-table) .q-editable:hover {
  744. background-color: #f0f8ff;
  745. cursor: pointer;
  746. }
  747. :deep(.q-field__native) {
  748. padding: 5px 8px;
  749. }
  750. /* 高亮显示编辑状态的行 */
  751. :deep(.q-table tr.editing) {
  752. background-color: #e8f5e9 !important;
  753. }
  754. </style>