index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. <template>
  2. <ContentWrap>
  3. <!-- 搜索工作栏 -->
  4. <el-form
  5. class="-mb-15px"
  6. :model="queryParams"
  7. ref="queryFormRef"
  8. :inline="true"
  9. label-width="68px"
  10. >
  11. <el-form-item
  12. :label="t('workOrderMaterial.factory')"
  13. prop="factoryId"
  14. v-if="!shouldHideComponents"
  15. >
  16. <el-select
  17. v-model="queryParams.factoryId"
  18. clearable
  19. filterable
  20. :placeholder="t('faultForm.choose')"
  21. class="!w-240px"
  22. @change="selectedFactoryChange"
  23. >
  24. <el-option
  25. v-for="item in factoryList"
  26. :key="item.id"
  27. :label="item.factoryName"
  28. :value="item.id!"
  29. />
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item
  33. :label="t('workOrderMaterial.costCenter')"
  34. prop="costCenterId"
  35. v-if="!shouldHideComponents"
  36. >
  37. <el-select
  38. v-model="queryParams.costCenterId"
  39. clearable
  40. filterable
  41. :placeholder="t('faultForm.choose')"
  42. class="!w-240px"
  43. >
  44. <el-option
  45. v-for="item in filteredCostCenterList"
  46. :key="item.id"
  47. :label="item.costCenterName"
  48. :value="item.id!"
  49. />
  50. </el-select>
  51. </el-form-item>
  52. <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
  53. <el-input
  54. v-model="queryParams.materialCode"
  55. :placeholder="t('chooseMaintain.materialCode')"
  56. clearable
  57. @keyup.enter="handleQuery"
  58. class="!w-240px"
  59. />
  60. </el-form-item>
  61. <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
  62. <el-input
  63. v-model="queryParams.materialName"
  64. :placeholder="t('chooseMaintain.materialName')"
  65. clearable
  66. @keyup.enter="handleQuery"
  67. class="!w-240px"
  68. />
  69. </el-form-item>
  70. <el-form-item :label="t('chooseMaintain.createTime')" prop="storageTime">
  71. <el-date-picker
  72. v-model="queryParams.storageTime"
  73. value-format="YYYY-MM-DD HH:mm:ss"
  74. type="daterange"
  75. :start-placeholder="t('info.start')"
  76. :end-placeholder="t('info.end')"
  77. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  78. class="!w-220px"
  79. />
  80. </el-form-item>
  81. <el-form-item>
  82. <el-button @click="handleQuery"
  83. ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
  84. >
  85. <el-button @click="resetQuery"
  86. ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
  87. >
  88. <el-button
  89. type="primary"
  90. plain
  91. @click="openForm('create')"
  92. v-hasPermi="['pms:iot-lock-stock:create']"
  93. >
  94. <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
  95. </el-button>
  96. <!-- v-hasPermi="['pms:iot-lock-stock:export']" -->
  97. <el-button type="success" plain @click="handleExport" :loading="exportLoading">
  98. <Icon icon="ep:download" class="mr-5px" /> 导出
  99. </el-button>
  100. </el-form-item>
  101. </el-form>
  102. </ContentWrap>
  103. <!-- ========== 统计信息卡片 ========== -->
  104. <ContentWrap style="margin-bottom: 16px">
  105. <el-card shadow="never" class="stat-card">
  106. <div class="stat-container">
  107. <div class="stat-item">
  108. <span class="stat-label">{{ t('stock.totalQuantity') }}:</span>
  109. <span class="stat-value">{{ totalQuantity.toLocaleString() }}</span>
  110. </div>
  111. <div class="stat-item">
  112. <span class="stat-label">{{ t('stock.totalAmount') }}:</span>
  113. <span class="stat-value"
  114. {{
  115. totalAmount.toLocaleString(undefined, {
  116. minimumFractionDigits: 2,
  117. maximumFractionDigits: 2
  118. })
  119. }}</span
  120. >
  121. </div>
  122. </div>
  123. </el-card>
  124. </ContentWrap>
  125. <!-- 列表 -->
  126. <ContentWrap ref="tableContainerRef" class="table-container">
  127. <el-table
  128. ref="tableRef"
  129. v-loading="loading"
  130. :data="list"
  131. :stripe="true"
  132. :show-overflow-tooltip="false"
  133. style="width: 100%"
  134. >
  135. <el-table-column
  136. :label="t('workOrderMaterial.factory')"
  137. align="center"
  138. prop="factory"
  139. :width="columnWidths.factory"
  140. />
  141. <el-table-column
  142. :label="t('workOrderMaterial.costCenter')"
  143. align="center"
  144. prop="costCenter"
  145. :width="columnWidths.costCenter"
  146. />
  147. <el-table-column
  148. :label="t('chooseMaintain.materialCode')"
  149. align="center"
  150. prop="materialCode"
  151. :width="columnWidths.materialCode"
  152. />
  153. <el-table-column
  154. :label="t('chooseMaintain.materialName')"
  155. align="left"
  156. prop="materialName"
  157. :width="columnWidths.materialName"
  158. />
  159. <el-table-column
  160. :label="t('route.quantity')"
  161. align="center"
  162. prop="quantity"
  163. :formatter="erpPriceTableColumnFormatter"
  164. :width="columnWidths.quantity"
  165. />
  166. <el-table-column
  167. :label="t('workOrderMaterial.unitPrice')"
  168. align="center"
  169. prop="unitPrice"
  170. :formatter="erpPriceTableColumnFormatter"
  171. :width="columnWidths.unitPrice"
  172. />
  173. <el-table-column
  174. :label="t('workOrderMaterial.unit')"
  175. align="center"
  176. prop="unit"
  177. :width="columnWidths.unit"
  178. />
  179. <el-table-column
  180. :label="t('stock.storageTime')"
  181. align="center"
  182. prop="storageTime"
  183. :formatter="dateFormatter"
  184. :width="columnWidths.storageTime"
  185. />
  186. </el-table>
  187. <!-- 分页 -->
  188. <Pagination
  189. :total="total"
  190. v-model:page="queryParams.pageNo"
  191. v-model:limit="queryParams.pageSize"
  192. @pagination="getList"
  193. />
  194. </ContentWrap>
  195. <!-- 表单弹窗:添加/修改 -->
  196. <IotLockStockForm ref="formRef" @success="getList" />
  197. </template>
  198. <script setup lang="ts">
  199. import { dateFormatter } from '@/utils/formatTime'
  200. import download from '@/utils/download'
  201. import { IotLockStockApi, IotLockStockVO } from '@/api/pms/iotlockstock'
  202. import IotLockStockForm from './IotLockStockForm.vue'
  203. import { erpPriceTableColumnFormatter } from '@/utils'
  204. import { useUserStore } from '@/store/modules/user'
  205. import { SapOrgApi, SapOrgVO } from '@/api/system/saporg'
  206. import { checkRole } from '@/utils/permission'
  207. import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
  208. /** PMS 本地 库存 列表 */
  209. defineOptions({ name: 'IotLockStock' })
  210. const message = useMessage() // 消息弹窗
  211. const { t } = useI18n() // 国际化
  212. const { push } = useRouter() // 路由跳转
  213. // 过滤后的成本中心列表(用于动态显示)
  214. const filteredCostCenterList = ref<SapOrgVO[]>([])
  215. const loading = ref(true) // 列表的加载中
  216. const list = ref<IotLockStockVO[]>([]) // 列表的数据
  217. const total = ref(0) // 列表的总页数
  218. const queryParams = reactive({
  219. pageNo: 1,
  220. pageSize: 10,
  221. deptId: undefined,
  222. factory: undefined,
  223. factoryId: undefined,
  224. projectDepartment: undefined,
  225. storageLocationId: undefined,
  226. costCenter: undefined,
  227. costCenterId: undefined,
  228. pickingListNumber: undefined,
  229. materialCode: undefined,
  230. materialName: undefined,
  231. materialGroupName: undefined,
  232. materialGroupId: undefined,
  233. quantity: undefined,
  234. unitPrice: undefined,
  235. unit: undefined,
  236. storageTime: [],
  237. sort: undefined,
  238. status: undefined,
  239. processInstanceId: undefined,
  240. auditStatus: undefined,
  241. remark: undefined,
  242. createTime: []
  243. })
  244. const queryFormRef = ref() // 搜索的表单
  245. const exportLoading = ref(false) // 导出的加载中
  246. const factoryList = ref([] as SapOrgVO[]) // 工厂列表
  247. const storageLocationList = ref([] as SapOrgVO[]) // 库存地点列表
  248. const costCenterList = ref([] as SapOrgVO[]) // SAP成本中心列表
  249. // 统计变量
  250. const totalQuantity = ref(0) // 总数量
  251. const totalAmount = ref(0) // 总金额
  252. // 表格容器和表格的引用
  253. const tableContainerRef = ref()
  254. const tableRef = ref()
  255. const shouldHideComponents = computed(() => {
  256. // 检查用户是否拥有 'A' 或 'B' 角色
  257. return checkRole(['小队队长', '操作员'])
  258. })
  259. const selectedFactoryReqVO = ref({
  260. type: 0, // 类型(1工厂 2成本中心 3库位)
  261. factoryCodes: [] // 已经选择的SAP工厂code 列表
  262. })
  263. // 列宽度配置
  264. const columnWidths = ref({
  265. factory: '120px',
  266. costCenter: '120px',
  267. materialCode: '120px',
  268. materialName: '200px', // 初始宽度,会被计算覆盖
  269. quantity: '100px',
  270. unitPrice: '100px',
  271. unit: '100px',
  272. storageTime: '180px'
  273. })
  274. /** 获取滚动条宽度 */
  275. const getScrollbarWidth = () => {
  276. const outer = document.createElement('div')
  277. outer.style.visibility = 'hidden'
  278. outer.style.overflow = 'scroll'
  279. document.body.appendChild(outer)
  280. const inner = document.createElement('div')
  281. outer.appendChild(inner)
  282. const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
  283. outer.parentNode?.removeChild(outer)
  284. return scrollbarWidth
  285. }
  286. /** 计算文本宽度 */
  287. const getTextWidth = (text: string, fontSize = 14) => {
  288. const span = document.createElement('span')
  289. span.style.visibility = 'hidden'
  290. span.style.position = 'absolute'
  291. span.style.whiteSpace = 'nowrap'
  292. span.style.fontSize = `${fontSize}px`
  293. span.style.fontFamily = 'inherit'
  294. span.innerText = text
  295. document.body.appendChild(span)
  296. const width = span.offsetWidth
  297. document.body.removeChild(span)
  298. return width
  299. }
  300. /** 计算列宽度 */
  301. const calculateColumnWidths = () => {
  302. const MIN_WIDTH = 80 // 最小列宽
  303. const PADDING = 25 // 列内边距
  304. const FLEXIBLE_COLUMN = 'materialName' // 可伸缩列
  305. const scrollbarWidth = getScrollbarWidth() // 动态获取滚动条宽度
  306. if (!tableContainerRef.value?.$el || list.value.length === 0) return
  307. const containerWidth = tableContainerRef.value.$el.clientWidth
  308. // 需要自适应的列配置
  309. const autoColumns = [
  310. { key: 'factory', label: t('workOrderMaterial.factory'), getValue: (row) => row.factory },
  311. {
  312. key: 'costCenter',
  313. label: t('workOrderMaterial.costCenter'),
  314. getValue: (row) => row.costCenter
  315. },
  316. {
  317. key: 'materialCode',
  318. label: t('chooseMaintain.materialCode'),
  319. getValue: (row) => row.materialCode
  320. },
  321. {
  322. key: 'materialName',
  323. label: t('chooseMaintain.materialName'),
  324. getValue: (row) => row.materialName
  325. },
  326. {
  327. key: 'quantity',
  328. label: t('route.quantity'),
  329. getValue: (row) => erpPriceTableColumnFormatter(null, null, row.quantity, null)
  330. },
  331. {
  332. key: 'unitPrice',
  333. label: t('workOrderMaterial.unitPrice'),
  334. getValue: (row) => erpPriceTableColumnFormatter(null, null, row.unitPrice, null)
  335. },
  336. { key: 'unit', label: t('workOrderMaterial.unit'), getValue: (row) => row.unit },
  337. {
  338. key: 'storageTime',
  339. label: t('stock.storageTime'),
  340. getValue: (row) => dateFormatter(null, null, row.storageTime)
  341. }
  342. ]
  343. const newWidths: Record<string, string> = {}
  344. let totalFixedWidth = 0 // 所有固定列的总宽度
  345. // 计算除可伸缩列外的所有列宽度
  346. autoColumns.forEach((col) => {
  347. if (col.key === FLEXIBLE_COLUMN) return
  348. const headerText = col.label
  349. const headerWidth = getTextWidth(headerText) * 1.3 // 表头宽度(加粗效果增加20%)
  350. // 计算内容最大宽度
  351. let contentMaxWidth = 0
  352. list.value.forEach((row) => {
  353. const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '')
  354. const textWidth = getTextWidth(text)
  355. if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
  356. })
  357. // 取表头宽度、内容最大宽度和最小宽度的最大值
  358. const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
  359. newWidths[col.key] = `${finalWidth}px`
  360. totalFixedWidth += finalWidth
  361. })
  362. // 处理可伸缩列(materialName)
  363. const flexibleCol = autoColumns.find((col) => col.key === FLEXIBLE_COLUMN)
  364. if (flexibleCol) {
  365. const headerText = flexibleCol.label
  366. const headerWidth = getTextWidth(headerText) * 1.3
  367. let contentMaxWidth = 0
  368. list.value.forEach((row) => {
  369. const text = flexibleCol.getValue
  370. ? String(flexibleCol.getValue(row))
  371. : String(row[flexibleCol.key] || '')
  372. const textWidth = getTextWidth(text)
  373. if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
  374. })
  375. const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
  376. // 剩余空间 = 容器宽度 - 其他列总宽度 - 垂直滚动条宽度(17px)
  377. const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth
  378. // 可伸缩列的宽度取剩余空间和基础宽度的最大值
  379. const flexibleWidth = Math.max(remainingWidth, baseWidth)
  380. newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`
  381. }
  382. // 更新列宽度
  383. columnWidths.value = newWidths
  384. // 重新布局表格
  385. nextTick(() => {
  386. tableRef.value?.doLayout()
  387. })
  388. }
  389. /** 查询列表 */
  390. const getList = async () => {
  391. loading.value = true
  392. try {
  393. const data = await IotLockStockApi.getIotLockStockPage(queryParams)
  394. list.value = data.list
  395. total.value = data.total
  396. // 从第一条记录中提取统计值
  397. if (data.list && data.list.length > 0) {
  398. // 确保取到有效的数值(第一条记录中的统计值代表整个查询结果)
  399. totalQuantity.value = Number(data.list[0].totalQuantity) || 0
  400. totalAmount.value = Number(data.list[0].totalAmount) || 0
  401. } else {
  402. // 没有数据时重置为0
  403. totalQuantity.value = 0
  404. totalAmount.value = 0
  405. }
  406. // 数据加载完成后计算列宽
  407. nextTick(() => {
  408. calculateColumnWidths()
  409. })
  410. } finally {
  411. loading.value = false
  412. }
  413. // 获取当前登录人的部门id 查询部门及子部门关联的所有SAP组织
  414. // 获取组织数据(移出统计值处理逻辑)
  415. await loadOrgData()
  416. }
  417. // 单独封装组织数据加载方法
  418. const loadOrgData = async () => {
  419. const deptId = useUserStore().getUser.deptId
  420. if (typeof deptId === 'number' && !isNaN(deptId) && deptId > 0) {
  421. try {
  422. const [factories, costCenters] = await Promise.all([
  423. SapOrgApi.filteredSimpleSapOrgList(1, deptId),
  424. SapOrgApi.filteredSimpleSapOrgList(2, deptId)
  425. ])
  426. factoryList.value = factories
  427. costCenterList.value = costCenters
  428. // 初始化时显示全部成本中心
  429. filteredCostCenterList.value = costCenters
  430. } catch (error) {
  431. console.error('获取组织数据失败:', error)
  432. }
  433. } else {
  434. console.warn('无效的部门ID:', deptId)
  435. }
  436. }
  437. /** 搜索按钮操作 */
  438. const handleQuery = () => {
  439. queryParams.pageNo = 1
  440. getList()
  441. }
  442. /** 重置按钮操作 */
  443. const resetQuery = () => {
  444. queryFormRef.value.resetFields()
  445. // 重置后恢复成本中心为完整列表
  446. filteredCostCenterList.value = costCenterList.value
  447. handleQuery()
  448. }
  449. /** 添加/修改操作 */
  450. const formRef = ref()
  451. const openForm = (type: string, id?: number) => {
  452. if (typeof id === 'number') {
  453. formRef.value.open(type, id)
  454. return
  455. }
  456. push({ name: 'LockStockAdd', params: {} })
  457. }
  458. /** 删除按钮操作 */
  459. const handleDelete = async (id: number) => {
  460. try {
  461. // 删除的二次确认
  462. await message.delConfirm()
  463. // 发起删除
  464. await IotLockStockApi.deleteIotLockStock(id)
  465. message.success(t('common.delSuccess'))
  466. // 刷新列表
  467. await getList()
  468. } catch {}
  469. }
  470. /** 已经选择了 SAP工厂 */
  471. /* const selectedFactoryChange = async (selectedId: number | undefined) => {
  472. // 获取选中的factoryCode数组
  473. const selectedFactory = factoryList.value.find(item => item.id === selectedId)
  474. const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
  475. // 获得已经选择的 SAP 工厂 数组
  476. // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 成本中心
  477. selectedFactoryReqVO.value.type = 2
  478. selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
  479. costCenterList.value = await SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
  480. // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
  481. selectedFactoryReqVO.value.type = 3
  482. selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
  483. storageLocationList.value = await SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
  484. } */
  485. /** 已经选择了 SAP工厂 */
  486. const selectedFactoryChange = async (selectedId: number | undefined) => {
  487. // 清空已选择的成本中心
  488. queryParams.costCenterId = undefined
  489. if (!selectedId) {
  490. // 未选择工厂时显示全部成本中心
  491. filteredCostCenterList.value = costCenterList.value
  492. return
  493. }
  494. // 获取选中的工厂对象
  495. const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
  496. if (!selectedFactory) return
  497. // 根据工厂代码过滤成本中心
  498. filteredCostCenterList.value = costCenterList.value.filter(
  499. (item) => item.factoryCode === selectedFactory.factoryCode
  500. )
  501. }
  502. /** 导出按钮操作 */
  503. const handleExport = async () => {
  504. try {
  505. exportLoading.value = true
  506. const data = await IotLockStockApi.exportIotLockStock(queryParams)
  507. download.excel(data, 'PMS 本地库存.xls')
  508. } catch {
  509. } finally {
  510. exportLoading.value = false
  511. }
  512. }
  513. /** 初始化 **/
  514. onMounted(() => {
  515. getList()
  516. // 添加窗口大小变化监听
  517. window.addEventListener('resize', calculateColumnWidths)
  518. })
  519. onUnmounted(() => {
  520. // 移除窗口大小变化监听
  521. window.removeEventListener('resize', calculateColumnWidths)
  522. })
  523. // 监听列表数据变化,重新计算列宽
  524. watch(
  525. list,
  526. () => {
  527. nextTick(calculateColumnWidths)
  528. },
  529. { deep: true }
  530. )
  531. </script>
  532. <style scoped>
  533. /* 统计卡片样式 */
  534. .stat-card {
  535. border-radius: 4px;
  536. border: 1px solid #ebeef5;
  537. }
  538. .stat-container {
  539. display: flex;
  540. padding: 1px;
  541. }
  542. .stat-item {
  543. display: flex;
  544. align-items: center;
  545. margin-right: 40px; /* 控制项间距 */
  546. }
  547. .stat-label {
  548. font-weight: bold;
  549. color: #606266;
  550. margin-right: 8px;
  551. }
  552. .stat-value {
  553. font-size: 18px;
  554. font-weight: bold;
  555. color: #409eff;
  556. }
  557. /* 表格容器样式 - 确保可以水平滚动 */
  558. .table-container {
  559. overflow-x: auto;
  560. }
  561. /* 防止表格内容换行 */
  562. :deep(.el-table) .cell {
  563. white-space: nowrap !important;
  564. overflow: visible !important;
  565. text-overflow: unset !important;
  566. }
  567. /* 确保表格行不换行 */
  568. :deep(.el-table__row) {
  569. white-space: nowrap;
  570. }
  571. /* 防止表格内容换行 */
  572. :deep(.el-table .cell) {
  573. white-space: nowrap !important;
  574. }
  575. /* 表头特别处理 */
  576. :deep(.el-table__header) {
  577. .cell {
  578. display: inline-block;
  579. white-space: nowrap;
  580. width: auto !important;
  581. }
  582. }
  583. /* 表格整体布局优化 */
  584. :deep(.el-table__inner-wrapper) {
  585. min-width: 100% !important;
  586. width: auto !important;
  587. }
  588. /* 单元格内容完全显示 */
  589. :deep(.el-table__body-wrapper) .el-table__cell .cell {
  590. display: block;
  591. overflow: visible;
  592. text-overflow: unset;
  593. }
  594. </style>