management.vue 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813
  1. <template>
  2. <div>
  3. <q-toolbar class="row items-center">
  4. <q-btn-group push class="btn-group">
  5. <q-btn
  6. :label="$t('stock.shelf.shelf_up')"
  7. icon="upload"
  8. @click="handleShelfUp()"
  9. />
  10. <div class="self-center text-center q-px-sm">
  11. {{ $t("stock.layertip") }}
  12. </div>
  13. <q-input
  14. dense
  15. color="primary"
  16. v-model="shelf.layer_now"
  17. style="width: 50px"
  18. />
  19. <q-btn
  20. :label="$t('stock.shelf.shelf_down')"
  21. icon="download"
  22. @click="handleShelfDown()"
  23. />
  24. <q-btn :label="$t('refresh')" icon="refresh" @click="reFresh()">
  25. <q-tooltip
  26. content-class="bg-amber text-black shadow-4"
  27. :offset="[10, 10]"
  28. content-style="font-size: 12px"
  29. >
  30. {{ $t("refreshtip") }}
  31. </q-tooltip>
  32. </q-btn>
  33. <q-separator />
  34. <!-- <q-btn
  35. :label="$t('stock.edit')"
  36. icon="edit"
  37. v-if="hasPermission('edit')"
  38. @click="handle_edit()"
  39. /> -->
  40. <q-toggle
  41. v-model="showInventoryStatus"
  42. checked-icon="check"
  43. unchecked-icon="close"
  44. :label="showInventoryStatus ? '显示质检状态' : '显示库位状态'"
  45. size="sm"
  46. color="primary"
  47. />
  48. </q-btn-group>
  49. <goodscard
  50. v-if="showInventoryDetails"
  51. ref="goodscard"
  52. :col-index="select_Inventory.colIndex"
  53. :row-index="select_Inventory.rowIndex"
  54. :layer-index="select_Inventory.layerIndex"
  55. :selected-shelf-type="select_Inventory.shelf_type"
  56. :goods-data="select_Inventory.goods_data"
  57. @close="showInventoryDetails = false"
  58. />
  59. </q-toolbar>
  60. <q-page class="q-pa-md">
  61. <div class="layout-container">
  62. <!-- 货架部分 -->
  63. <div class="shelf-section">
  64. <div class="grid-system">
  65. <!-- Y轴 -->
  66. <div class="axis y-axis">
  67. <div class="axis-numbers">
  68. <div v-for="index in shelf.rows" :key="'y' + index">
  69. {{ index }}
  70. </div>
  71. </div>
  72. <div class="axis-arrow"></div>
  73. </div>
  74. <!-- X轴 -->
  75. <div class="axis x-axis">
  76. <div class="axis-arrow"></div>
  77. <div class="axis-numbers">
  78. <div
  79. v-for="col in shelf.cols"
  80. :key="'x' + col"
  81. class="axis-label"
  82. >
  83. {{ col }}
  84. </div>
  85. <div class="axis-label"></div>
  86. </div>
  87. </div>
  88. <!-- 网格系统 -->
  89. <div class="grid-container">
  90. <!-- 内容层 -->
  91. <div class="grid-content">
  92. <div
  93. v-for="(row, rowIndex) in shelf.rows"
  94. :key="`row-${rowIndex}|${shelf.layer_now}`"
  95. class="grid-row"
  96. :style="{ cursor: 'pointer' }"
  97. >
  98. <div
  99. v-for="(col, colIndex) in shelf.cols"
  100. :key="`col-${colIndex}|${shelf.layer_now}`"
  101. class="grid-item"
  102. :style="{ cursor: 'pointer' }"
  103. >
  104. <div
  105. class="select-item"
  106. v-if="
  107. shouldShowButton(
  108. shelf.rows - rowIndex,
  109. colIndex + 1,
  110. shelf.layer_now
  111. )
  112. "
  113. :key="`${shelf.rows - rowIndex}-${colIndex}-${
  114. shelf.layer_now
  115. }`"
  116. :style="{
  117. border: '1px solid #ccc',
  118. borderRadius: '5px',
  119. width: 'var(--cell-d)',
  120. height: 'var(--cell-d)',
  121. backgroundColor: getBinColor(
  122. shelf.rows - rowIndex,
  123. colIndex + 1,
  124. shelf.layer_now
  125. ),
  126. cursor: 'pointer',
  127. }"
  128. @click="
  129. handleBinClick(
  130. shelf.rows - rowIndex,
  131. colIndex + 1,
  132. shelf.layer_now
  133. )
  134. "
  135. ></div>
  136. <q-tooltip
  137. v-if="
  138. shouldShowButton(
  139. shelf.rows - rowIndex,
  140. colIndex + 1,
  141. shelf.layer_now
  142. )
  143. "
  144. content-class="bg-amber text-black shadow-4"
  145. :offset="[20, 20]"
  146. content-style="font-size: 12px; max-width: 300px;"
  147. >
  148. <div class="tooltip-header">
  149. {{ $t("stock.rowtip") }} {{ shelf.rows - rowIndex }}
  150. {{ $t("stock.coltip") }} {{ colIndex + 1 }}
  151. </div>
  152. <q-separator color="dark" inset />
  153. <div class="tooltip-content">
  154. <div
  155. v-if="
  156. getBinbatch(
  157. shelf.rows - rowIndex,
  158. colIndex + 1,
  159. shelf.layer_now
  160. ).length
  161. "
  162. >
  163. <div
  164. v-for="(batch, index) in getBinbatch(
  165. shelf.rows - rowIndex,
  166. colIndex + 1,
  167. shelf.layer_now
  168. )"
  169. :key="index"
  170. class="batch-item"
  171. :class="{
  172. 'batch-pending': batch.status === 0,
  173. 'batch-approved': batch.status === 1,
  174. 'batch-rejected': batch.status === 2,
  175. 'batch-no-goods': batch.status === 404,
  176. }"
  177. >
  178. <div class="batch-status">
  179. <q-icon
  180. :name="getStatusIcon(batch.status)"
  181. size="sm"
  182. :color="getStatusColor(batch.status)"
  183. class="q-mr-xs"
  184. />
  185. {{ getStatusLabel(batch.status) }}
  186. </div>
  187. <div
  188. v-if="
  189. batch.batchId !== 'no_batch' && batch.batchId
  190. "
  191. >
  192. {{ "批次号" }}: {{ batch.batchId }}
  193. </div>
  194. <div
  195. v-if="
  196. batch.time !== 'no_check_time' && batch.time
  197. "
  198. >
  199. {{ "库位数目" }}: {{ batch.qty }}
  200. </div>
  201. <div
  202. v-if="
  203. batch.time !== 'no_check_time' && batch.time
  204. "
  205. >
  206. {{ "时间" }}: {{ formatDateTime(batch.time) }}
  207. </div>
  208. </div>
  209. </div>
  210. <div v-else class="batch-no-goods">
  211. <q-icon name="inventory" size="sm" class="q-mr-xs" />
  212. {{ $t("stock.batch.no_goods") }}
  213. </div>
  214. </div>
  215. </q-tooltip>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <!-- 统计面板部分 -->
  223. <div class="stats-section">
  224. <div class="stats-panel" role="region" aria-label="统计面板">
  225. <div v-if="statsLoaded" class="stats-cards">
  226. <q-card class="stat-card" @click="reFreshStatistics">
  227. <q-card-section>
  228. <div class="card-title">总体统计</div>
  229. <div class="big-num">{{ stats.total_locations }}</div>
  230. <div class="row items-center q-mt-sm">
  231. <div class="col">
  232. <div class="muted">已占用</div>
  233. <div class="small-num used">{{ stats.total_used }}</div>
  234. </div>
  235. <div class="col">
  236. <div class="muted">可用</div>
  237. <div class="small-num available">
  238. {{ stats.total_available }}
  239. </div>
  240. </div>
  241. </div>
  242. <div class="util-row q-mt-md">
  243. <div class="muted">使用率</div>
  244. <div class="util-number">
  245. {{ formatPercent(stats.utilization_rate) }}
  246. </div>
  247. </div>
  248. <q-linear-progress
  249. :value="(Number(stats.utilization_rate) || 0) / 100"
  250. class="q-mt-xs"
  251. track-color="grey-3"
  252. size="12px"
  253. />
  254. </q-card-section>
  255. </q-card>
  256. <q-card class="stat-card" @click="openGroupStatsDialog">
  257. <q-card-section>
  258. <div class="card-title">各类型统计</div>
  259. <div class="type-list">
  260. <div v-for="t in typeList" :key="t.key" class="type-item">
  261. <div class="type-info">
  262. <div class="type-name">{{ t.label }}</div>
  263. <div class="type-total">
  264. 共 {{ stats[t.totalKey] }} 个
  265. </div>
  266. </div>
  267. <div class="type-details">
  268. <div class="detail-item">
  269. <span class="detail-label">占用</span>
  270. <span class="detail-value used">{{
  271. stats[t.usedKey]
  272. }}</span>
  273. </div>
  274. <div class="detail-item">
  275. <span class="detail-label">可用</span>
  276. <span class="detail-value available">{{
  277. stats[t.availKey]
  278. }}</span>
  279. </div>
  280. </div>
  281. </div>
  282. </div>
  283. </q-card-section>
  284. </q-card>
  285. <q-card class="stat-card small">
  286. <q-card-section>
  287. <div class="card-title">数据管理</div>
  288. <div class="muted q-mt-xs">
  289. {{ stats.statistic_time_display || "-" }}
  290. </div>
  291. <div class="action-buttons q-mt-sm">
  292. <q-btn
  293. icon="refresh"
  294. label="刷新统计"
  295. @click="getStatistics()"
  296. flat
  297. dense
  298. class="full-width q-mb-sm"
  299. />
  300. <q-btn
  301. icon="verified"
  302. :label="$t('validate') || '数据校验'"
  303. @click="runCheck()"
  304. :disable="checking"
  305. flat
  306. dense
  307. class="full-width"
  308. :color="checking ? 'grey' : 'primary'"
  309. >
  310. <q-tooltip
  311. v-if="!checking"
  312. content-class="bg-amber text-black shadow-4"
  313. :offset="[10, 10]"
  314. content-style="font-size:12px"
  315. >
  316. 执行数据一致性校验
  317. </q-tooltip>
  318. </q-btn>
  319. </div>
  320. </q-card-section>
  321. </q-card>
  322. </div>
  323. <div v-else class="stats-loading">
  324. <q-spinner-dots size="36px" color="primary" />
  325. <div class="muted q-mt-sm">正在加载统计数据...</div>
  326. </div>
  327. </div>
  328. </div>
  329. </div>
  330. <!-- 校验结果弹窗 -->
  331. <q-dialog
  332. v-model="showCheckDialog"
  333. maximized
  334. transition-show="scale"
  335. transition-hide="scale"
  336. >
  337. <q-card class="check-dialog">
  338. <q-card-section class="dialog-header">
  339. <div class="header-content">
  340. <div>
  341. <div class="dialog-title">数据一致性校验结果</div>
  342. <div class="dialog-subtitle">
  343. {{
  344. checkResult?.summary?.check_time
  345. ? formatDateTime(checkResult.summary.check_time)
  346. : "-"
  347. }}
  348. </div>
  349. </div>
  350. <q-btn
  351. icon="close"
  352. flat
  353. round
  354. dense
  355. @click="showCheckDialog = false"
  356. />
  357. </div>
  358. </q-card-section>
  359. <q-separator />
  360. <q-card-section class="dialog-content">
  361. <div class="summary-cards">
  362. <q-card flat class="summary-card">
  363. <q-card-section>
  364. <div class="summary-value">
  365. {{ checkResult?.summary?.total_checked?.locations ?? "-" }}
  366. </div>
  367. <div class="summary-label">已校验位置</div>
  368. </q-card-section>
  369. </q-card>
  370. <q-card flat class="summary-card error">
  371. <q-card-section>
  372. <div class="summary-value">
  373. {{ checkResult?.summary?.errors_found?.locations ?? "-" }}
  374. </div>
  375. <div class="summary-label">发现错误</div>
  376. </q-card-section>
  377. </q-card>
  378. <q-card flat class="summary-card success">
  379. <q-card-section>
  380. <div class="summary-value">
  381. {{ checkResult?.summary?.fixed?.locations ?? "-" }}
  382. </div>
  383. <div class="summary-label">已修复</div>
  384. </q-card-section>
  385. </q-card>
  386. </div>
  387. <q-separator class="q-my-md" />
  388. <div class="error-sections">
  389. <div class="error-section">
  390. <div class="section-header">
  391. <q-icon name="place" class="q-mr-sm" />
  392. <span>位置级别错误</span>
  393. <q-badge color="negative" class="q-ml-sm">
  394. {{ checkResult?.details?.location_errors?.length || 0 }}
  395. </q-badge>
  396. </div>
  397. <q-table
  398. v-if="checkResult?.details?.location_errors?.length"
  399. :rows="checkResult.details.location_errors"
  400. :columns="checkColumns"
  401. row-key="location_id"
  402. flat
  403. bordered
  404. dense
  405. class="error-table q-mt-md"
  406. >
  407. <template v-slot:body-cell-actions="props">
  408. <div class="action-buttons">
  409. <q-btn
  410. icon="place"
  411. size="sm"
  412. flat
  413. dense
  414. @click="highlightLocation(props.row)"
  415. title="定位到该位置"
  416. />
  417. <q-btn
  418. icon="open_in_new"
  419. size="sm"
  420. flat
  421. dense
  422. @click="openLocationDetail(props.row)"
  423. title="查看详情"
  424. />
  425. </div>
  426. </template>
  427. </q-table>
  428. <div v-else class="no-errors">未发现位置级别错误</div>
  429. </div>
  430. <div class="error-section q-mt-lg">
  431. <div class="section-header">
  432. <q-icon name="folder" class="q-mr-sm" />
  433. <span>分组级别错误</span>
  434. <q-badge color="warning" class="q-ml-sm">
  435. {{ checkResult?.details?.group_errors?.length || 0 }}
  436. </q-badge>
  437. </div>
  438. <div
  439. v-if="checkResult?.details?.group_errors?.length"
  440. class="group-errors q-mt-md"
  441. >
  442. <div
  443. v-for="(error, index) in checkResult.details.group_errors"
  444. :key="index"
  445. class="group-error-item"
  446. >
  447. <div class="group-info">
  448. <strong>{{
  449. error.group_name || error.group_code
  450. }}</strong>
  451. <span class="error-type">{{ error.error_type }}</span>
  452. </div>
  453. <div class="group-details">
  454. 当前: {{ error.current_status }} → 期望:
  455. {{ error.expected_status }}
  456. </div>
  457. </div>
  458. </div>
  459. <div v-else class="no-errors">未发现分组级别错误</div>
  460. </div>
  461. </div>
  462. </q-card-section>
  463. <q-card-actions align="right" class="dialog-actions">
  464. <q-btn label="关闭" flat @click="showCheckDialog = false" />
  465. <q-btn
  466. label="导出报告"
  467. icon="download"
  468. @click="exportCheckReport()"
  469. v-if="checkResult"
  470. color="primary"
  471. />
  472. </q-card-actions>
  473. </q-card>
  474. </q-dialog>
  475. <!-- 货位组统计查询弹窗 -->
  476. <q-dialog v-model="showGroupStatsDialog" persistent>
  477. <q-card style="min-width: 500px">
  478. <q-card-section>
  479. <div class="text-h6">货位组统计查询</div>
  480. </q-card-section>
  481. <q-card-section class="q-pt-none">
  482. <div class="q-gutter-md">
  483. <q-input
  484. filled
  485. type="number"
  486. v-model="groupStatsParams.min_utilization"
  487. label="最小使用率 (%)"
  488. hint="输入0-100之间的数值"
  489. :rules="[
  490. (val) => (val >= 0 && val <= 100) || '请输入0-100之间的数值',
  491. ]"
  492. />
  493. <q-input
  494. filled
  495. type="number"
  496. v-model="groupStatsParams.min_used_locations"
  497. label="最小使用数目"
  498. hint="输入最小已使用货位数"
  499. :rules="[(val) => val >= 0 || '请输入非负数']"
  500. />
  501. </div>
  502. </q-card-section>
  503. <q-card-actions align="right">
  504. <q-btn
  505. flat
  506. label="取消"
  507. color="primary"
  508. @click="showGroupStatsDialog = false"
  509. />
  510. <q-btn
  511. flat
  512. label="查询"
  513. color="primary"
  514. @click="getGroupStatistics()"
  515. :loading="loadingGroupStats"
  516. />
  517. </q-card-actions>
  518. </q-card>
  519. </q-dialog>
  520. <!-- 货位组统计结果弹窗 -->
  521. <q-dialog v-model="showGroupStatsResult" maximized>
  522. <q-card>
  523. <q-card-section>
  524. <div class="row items-center">
  525. <div class="text-h6">货位组统计结果</div>
  526. <q-space />
  527. <q-btn
  528. icon="close"
  529. flat
  530. round
  531. dense
  532. @click="showGroupStatsResult = false"
  533. />
  534. </div>
  535. <div class="text-caption text-grey">
  536. 查询时间:
  537. {{
  538. groupStatsResult?.timestamp
  539. ? formatDateTime(groupStatsResult.timestamp)
  540. : "-"
  541. }}
  542. | 筛选条件: 使用率≥{{ groupStatsParams.min_utilization }}%,
  543. 使用数≥{{ groupStatsParams.min_used_locations }} | 共
  544. {{ groupStatsResult?.filters?.total_records || 0 }} 条记录
  545. </div>
  546. </q-card-section>
  547. <q-separator />
  548. <q-card-section>
  549. <q-table
  550. flat
  551. bordered
  552. :data="groupStatsResult?.data || []"
  553. :columns="groupStatsColumns"
  554. row-key="id"
  555. :loading="loadingGroupStats"
  556. @request="onGroupStatsRequest"
  557. >
  558. <template v-slot:body-cell-utilization_rate="props">
  559. <q-td :props="props">
  560. <div class="row items-center">
  561. <div class="col">{{ props.value }}%</div>
  562. <div class="col-auto">
  563. <q-linear-progress
  564. :value="parseFloat(props.value) / 100"
  565. :color="getUtilizationColor(props.value)"
  566. style="width: 60px; height: 8px"
  567. />
  568. </div>
  569. </div>
  570. </q-td>
  571. </template>
  572. <template v-slot:body-cell-actions="props">
  573. <q-td :props="props">
  574. <q-btn
  575. icon="visibility"
  576. size="sm"
  577. flat
  578. dense
  579. @click="highlightGroupLocation(props.row)"
  580. title="查看该货位组"
  581. />
  582. </q-td>
  583. </template>
  584. </q-table>
  585. </q-card-section>
  586. <q-card-actions align="right">
  587. <q-btn flat label="关闭" @click="showGroupStatsResult = false" />
  588. <q-btn
  589. flat
  590. label="导出数据"
  591. icon="download"
  592. @click="exportGroupStats()"
  593. v-if="groupStatsResult?.data?.length"
  594. color="primary"
  595. />
  596. </q-card-actions>
  597. </q-card>
  598. </q-dialog>
  599. </q-page>
  600. </div>
  601. </template>
  602. <script>
  603. import goodscard from "components/goodscard.vue";
  604. import { LocalStorage } from "quasar";
  605. import { date } from "quasar";
  606. import {
  607. getauth,
  608. postauth,
  609. post,
  610. putauth,
  611. deleteauth,
  612. getfile,
  613. } from "boot/axios_request";
  614. export default {
  615. name: "LocationManagement",
  616. components: { goodscard },
  617. data() {
  618. return {
  619. pathname: "bin/",
  620. warehouse_code: "",
  621. warehouse_name: "",
  622. shelf_name: "A区货架",
  623. shelf: {
  624. rows: 17,
  625. cols: 29,
  626. layers: 3,
  627. layer_now: 1,
  628. },
  629. filter: "",
  630. auth_edit: false,
  631. goodsMap: {},
  632. goodsMatrix: [],
  633. binColors: {
  634. T1: "#FFD700",
  635. T2: "#FFA500",
  636. T4: "#FF7300",
  637. T5: "#FF4100",
  638. S4: "#FF7300",
  639. M1: "#C8C8C8",
  640. E1: "#80620B",
  641. C1: "#808780",
  642. B1: "#00C300",
  643. reserved: "rgba(20, 125, 200, 0.3)",
  644. occupied: "rgba(20, 125, 255, 0.6)",
  645. default: "rgba(200, 200, 200, 0.3)",
  646. },
  647. showInventoryDetails: false,
  648. select_Inventory: {
  649. rowIndex: 0,
  650. colIndex: 0,
  651. layerIndex: 0,
  652. shelf_type: "storage",
  653. goods_data: {},
  654. },
  655. showInventoryStatus: true,
  656. userComponentPermissions: [],
  657. login_mode: LocalStorage.getItem("login_mode"),
  658. stats: {},
  659. statsLoaded: false,
  660. typeList: [
  661. {
  662. key: "t5",
  663. label: "T5",
  664. totalKey: "t5_total",
  665. usedKey: "t5_used",
  666. availKey: "t5_available",
  667. },
  668. {
  669. key: "t4",
  670. label: "T4",
  671. totalKey: "t4_total",
  672. usedKey: "t4_used",
  673. availKey: "t4_available",
  674. },
  675. {
  676. key: "s4",
  677. label: "S4",
  678. totalKey: "s4_total",
  679. usedKey: "s4_used",
  680. availKey: "s4_available",
  681. },
  682. {
  683. key: "t2",
  684. label: "T2",
  685. totalKey: "t2_total",
  686. usedKey: "t2_used",
  687. availKey: "t2_available",
  688. },
  689. {
  690. key: "t1",
  691. label: "T1",
  692. totalKey: "t1_total",
  693. usedKey: "t1_used",
  694. availKey: "t1_available",
  695. },
  696. ],
  697. checking: false,
  698. showCheckDialog: false,
  699. checkResult: null,
  700. checkColumns: [
  701. {
  702. name: "location_code",
  703. label: "位置编码",
  704. field: "location_code",
  705. align: "left",
  706. },
  707. {
  708. name: "layer",
  709. label: "层",
  710. field: "layer",
  711. align: "center",
  712. style: "width:60px",
  713. },
  714. {
  715. name: "row",
  716. label: "行",
  717. field: "row",
  718. align: "center",
  719. style: "width:60px",
  720. },
  721. {
  722. name: "col",
  723. label: "列",
  724. field: "col",
  725. align: "center",
  726. style: "width:60px",
  727. },
  728. {
  729. name: "current_status",
  730. label: "当前状态",
  731. field: "current_status",
  732. align: "left",
  733. },
  734. {
  735. name: "expected_status",
  736. label: "期望状态",
  737. field: "expected_status",
  738. align: "left",
  739. },
  740. {
  741. name: "detected_at",
  742. label: "检测时间",
  743. field: "detected_at",
  744. align: "left",
  745. },
  746. {
  747. name: "actions",
  748. label: "操作",
  749. field: "actions",
  750. align: "center",
  751. style: "width:100px",
  752. },
  753. ],
  754. showGroupStatsDialog: false,
  755. showGroupStatsResult: false,
  756. loadingGroupStats: false,
  757. groupStatsParams: {
  758. min_utilization: 100,
  759. min_used_locations: 2,
  760. },
  761. groupStatsResult: null,
  762. groupStatsColumns: [
  763. {
  764. name: "location_group",
  765. label: "货位组",
  766. field: "location_group",
  767. align: "left",
  768. sortable: true,
  769. },
  770. {
  771. name: "warehouse_display",
  772. label: "仓库",
  773. field: "warehouse_display",
  774. align: "left",
  775. sortable: true,
  776. },
  777. {
  778. name: "layer_display",
  779. label: "楼层",
  780. field: "layer_display",
  781. align: "center",
  782. sortable: true,
  783. },
  784. {
  785. name: "total_locations",
  786. label: "总货位数",
  787. field: "total_locations",
  788. align: "center",
  789. sortable: true,
  790. },
  791. {
  792. name: "used_locations",
  793. label: "已使用",
  794. field: "used_locations",
  795. align: "center",
  796. sortable: true,
  797. },
  798. {
  799. name: "available_locations",
  800. label: "可用数",
  801. field: "available_locations",
  802. align: "center",
  803. sortable: true,
  804. },
  805. {
  806. name: "utilization_rate",
  807. label: "使用率",
  808. field: "utilization_rate",
  809. align: "center",
  810. sortable: true,
  811. },
  812. {
  813. name: "statistic_time",
  814. label: "统计时间",
  815. field: "statistic_time",
  816. align: "center",
  817. sortable: true,
  818. },
  819. {
  820. name: "actions",
  821. label: "操作",
  822. field: "actions",
  823. align: "center",
  824. },
  825. ],
  826. groupStatsPagination: {
  827. page: 1,
  828. rowsPerPage: 10,
  829. rowsNumber: 0,
  830. },
  831. };
  832. },
  833. methods: {
  834. openGroupStatsDialog() {
  835. this.showGroupStatsDialog = true;
  836. },
  837. getGroupStatistics() {
  838. const layer = this.shelf.layer_now || 1;
  839. const params = new URLSearchParams({
  840. layer: layer,
  841. min_utilization: this.groupStatsParams.min_utilization || 0,
  842. min_used_locations: this.groupStatsParams.min_used_locations || 0,
  843. });
  844. const url = `location_statistics/group-statistics/?${params}`;
  845. this.loadingGroupStats = true;
  846. getauth(url)
  847. .then((res) => {
  848. this.groupStatsResult = res || {};
  849. this.groupStatsPagination.rowsNumber =
  850. this.groupStatsResult.filters?.total_records || 0;
  851. this.showGroupStatsDialog = false;
  852. this.showGroupStatsResult = true;
  853. })
  854. .catch((err) => {
  855. console.error("获取货位组统计失败", err);
  856. this.$q.notify({
  857. type: "negative",
  858. message: "获取货位组统计失败: " + (err?.message || "未知错误"),
  859. });
  860. })
  861. .finally(() => {
  862. this.loadingGroupStats = false;
  863. });
  864. },
  865. onGroupStatsRequest(props) {
  866. const { page, rowsPerPage } = props.pagination;
  867. this.groupStatsPagination.page = page;
  868. this.groupStatsPagination.rowsPerPage = rowsPerPage;
  869. if (this.groupStatsResult?.data) {
  870. const startRow = (page - 1) * rowsPerPage;
  871. const endRow = startRow + rowsPerPage;
  872. this.groupStatsResult.displayData = this.groupStatsResult.data.slice(
  873. startRow,
  874. endRow
  875. );
  876. }
  877. },
  878. highlightGroupLocation(group) {
  879. this.$q.notify({
  880. message: `定位到货位组: ${group.location_group}`,
  881. color: "primary",
  882. });
  883. },
  884. exportGroupStats() {
  885. if (!this.groupStatsResult?.data) return;
  886. const dataStr = JSON.stringify(this.groupStatsResult, null, 2);
  887. const dataBlob = new Blob([dataStr], { type: "application/json" });
  888. const link = document.createElement("a");
  889. link.href = URL.createObjectURL(dataBlob);
  890. link.download = `group-statistics-${
  891. new Date().toISOString().split("T")[0]
  892. }.json`;
  893. link.click();
  894. },
  895. getUtilizationColor(rate) {
  896. const utilization = Number(rate) || 0;
  897. if (utilization < 30) return "positive";
  898. if (utilization < 70) return "warning";
  899. return "negative";
  900. },
  901. formatPercent(val) {
  902. if (val === undefined || val === null || val === "") return "0%";
  903. const v = Number(val);
  904. if (Number.isNaN(v)) return "0%";
  905. return `${v.toFixed(1)}%`;
  906. },
  907. exportCheckReport() {
  908. if (!this.checkResult) return;
  909. const dataStr = JSON.stringify(this.checkResult, null, 2);
  910. const dataBlob = new Blob([dataStr], { type: "application/json" });
  911. const link = document.createElement("a");
  912. link.href = URL.createObjectURL(dataBlob);
  913. link.download = `location-check-report-${
  914. new Date().toISOString().split("T")[0]
  915. }.json`;
  916. link.click();
  917. },
  918. highlightLocation(row) {
  919. if (!row) return;
  920. this.select_Inventory.rowIndex = row.row;
  921. this.select_Inventory.colIndex = row.col;
  922. this.select_Inventory.layerIndex = row.layer;
  923. this.select_Inventory.goods_data =
  924. this.goodsMap[`${row.row}-${row.col}-${row.layer}`] || {};
  925. this.select_Inventory.shelf_type = this.select_Inventory.goods_data
  926. ?.location_type
  927. ? "storage"
  928. : "corridor";
  929. this.showInventoryDetails = true;
  930. },
  931. openLocationDetail(row) {
  932. this.$q.notify({
  933. message: `打开位置 ${row.location_code} 的详情`,
  934. });
  935. },
  936. loadUserPermissions() {
  937. postauth("staff/role-comPermissions/" + this.login_mode + "/", {
  938. page: "/stock/management",
  939. }).then(
  940. (response) => {
  941. this.userComponentPermissions = response;
  942. },
  943. (error) => {
  944. this.$q.notify({
  945. type: "negative",
  946. message: "加载用户权限失败," + error.message,
  947. });
  948. }
  949. );
  950. },
  951. hasPermission(components) {
  952. if (!this.userComponentPermissions) return false;
  953. const permission = this.userComponentPermissions.find(
  954. (perm) => perm.component === components
  955. );
  956. return permission && permission.enabled;
  957. },
  958. runCheck() {
  959. const layer = this.shelf.layer_now || 1;
  960. const url = `/location_statistics/CheckView/?layer=${layer}`;
  961. this.checking = true;
  962. postauth(url, {})
  963. .then((res) => {
  964. if (res && res.data) {
  965. if (res.data.success === false) {
  966. this.$q.notify({ type: "negative", message: "校验返回失败" });
  967. this.checkResult = null;
  968. } else {
  969. this.checkResult = res.data.data || res.data;
  970. this.showCheckDialog = true;
  971. }
  972. } else {
  973. this.$q.notify({ type: "warning", message: "校验无返回内容" });
  974. }
  975. })
  976. .catch((err) => {
  977. this.$q.notify({
  978. type: "negative",
  979. message: "校验请求失败: " + (err && err.message),
  980. });
  981. })
  982. .finally(() => {
  983. this.checking = false;
  984. });
  985. },
  986. shouldShowButton(row, col, layer) {
  987. const bin = this.goodsMap[`${row}-${col}-${layer}`];
  988. return ["T1", "T2", "T4", "T5", "S4", "M1", "E1", "C1", "B1"].includes(
  989. bin?.location_type
  990. );
  991. },
  992. getList() {
  993. var _this = this;
  994. postauth(_this.pathname + "check/", {
  995. layer: _this.shelf.layer_now,
  996. warehouse_code: _this.warehouse_code,
  997. }).then((res) => {
  998. _this.goodsMap = {};
  999. res.data.forEach((item) => {
  1000. const key = `${item.row}-${item.col}-${item.layer}`;
  1001. _this.goodsMap[key] = {
  1002. id: item.id,
  1003. location_type: item.location_type,
  1004. shelf_type: item.shelf_type,
  1005. status: item.status,
  1006. check: item.batch_statuses,
  1007. };
  1008. });
  1009. _this.$q.notify({
  1010. message: "刷新成功",
  1011. icon: "done",
  1012. color: "positive",
  1013. });
  1014. });
  1015. },
  1016. handle_setting() {
  1017. if (LocalStorage.has("warehouse_code")) {
  1018. this.warehouse_code = LocalStorage.getItem("warehouse_code");
  1019. }
  1020. if (LocalStorage.has("warehouse_name")) {
  1021. this.warehouse_name = LocalStorage.getItem("warehouse_name");
  1022. }
  1023. },
  1024. formatDateTime(dateStr) {
  1025. if (!dateStr) return "";
  1026. if (!dateStr.includes("T") || dateStr.includes("no_check_time")) {
  1027. return dateStr;
  1028. }
  1029. return date.formatDate(new Date(dateStr), "YYYY-MM-DD HH:mm:ss");
  1030. },
  1031. getBinbatch(row, col, layer) {
  1032. const bin = this.goodsMap[`${row}-${col}-${layer}`];
  1033. if (!bin || !bin.check || bin.check.length === 0) {
  1034. return [{ status: 404, time: "", batchId: "" }];
  1035. }
  1036. return bin.check.map((item) => {
  1037. if (item[0] === "404") {
  1038. return { status: 404, time: "no_check_time", batchId: "no_batch" };
  1039. } else {
  1040. return {
  1041. status: item[0],
  1042. time: item[1],
  1043. batchId: item[2],
  1044. qty: item[3],
  1045. };
  1046. }
  1047. });
  1048. },
  1049. getStatusIcon(status) {
  1050. switch (status) {
  1051. case 0:
  1052. return "schedule";
  1053. case 1:
  1054. return "check_circle";
  1055. case 2:
  1056. return "cancel";
  1057. case 404:
  1058. return "block";
  1059. default:
  1060. return "help";
  1061. }
  1062. },
  1063. getStatusColor(status) {
  1064. switch (status) {
  1065. case 0:
  1066. return "yellow";
  1067. case 1:
  1068. return "green";
  1069. case 2:
  1070. return "red";
  1071. case 404:
  1072. return "grey";
  1073. default:
  1074. return "blue";
  1075. }
  1076. },
  1077. getStatusLabel(status) {
  1078. switch (status) {
  1079. case 0:
  1080. return "待检";
  1081. case 1:
  1082. return "合格";
  1083. case 2:
  1084. return "不合格";
  1085. case 404:
  1086. return "无货";
  1087. default:
  1088. return "无货";
  1089. }
  1090. },
  1091. getBinColor(row, col, layer) {
  1092. const bin = this.goodsMap[`${row}-${col}-${layer}`];
  1093. if (this.showInventoryStatus) {
  1094. if (!bin || !bin.check || bin.check.length === 0) {
  1095. return "#CCCCCC";
  1096. }
  1097. const status = bin.check[0][0];
  1098. const statusColors = {
  1099. 0: "#FFD700",
  1100. 1: "#00FF00",
  1101. 2: "#FF0000",
  1102. 404: "#CCCCCC",
  1103. };
  1104. return statusColors[status] || "#CCCCCC";
  1105. } else {
  1106. if (!bin) return "#CCCCCC";
  1107. if (bin.status == "reserved" || bin.status == "occupied") {
  1108. return this.binColors[bin.status];
  1109. }
  1110. return this.binColors[bin.location_type] || "#CCCCCC";
  1111. }
  1112. },
  1113. handleBinClick(row, col, layer) {
  1114. this.select_Inventory.rowIndex = row;
  1115. this.select_Inventory.colIndex = col;
  1116. this.select_Inventory.layerIndex = layer;
  1117. const locationType =
  1118. this.goodsMap[`${row}-${col}-${layer}`]?.location_type;
  1119. if (["T1", "T2", "T4", "T5", "S4"].includes(locationType)) {
  1120. this.select_Inventory.shelf_type = "storage";
  1121. } else if (["E1", "C1"].includes(locationType)) {
  1122. this.select_Inventory.shelf_type = "elevator";
  1123. } else {
  1124. this.select_Inventory.shelf_type = "corridor";
  1125. }
  1126. this.select_Inventory.goods_data =
  1127. this.goodsMap[`${row}-${col}-${layer}`];
  1128. this.showInventoryDetails = true;
  1129. },
  1130. handleShelfDown() {
  1131. if (this.shelf.layer_now > this.shelf.layers) {
  1132. this.shelf.layer_now = this.shelf.layers;
  1133. }
  1134. if (this.shelf.layer_now > 1) {
  1135. this.shelf.layer_now -= 1;
  1136. this.reFresh();
  1137. }
  1138. },
  1139. handleShelfUp() {
  1140. if (this.shelf.layer_now < this.shelf.layers) {
  1141. this.shelf.layer_now += 1;
  1142. this.reFresh();
  1143. } else {
  1144. this.shelf.layer_now = this.shelf.layers;
  1145. }
  1146. },
  1147. reFresh() {
  1148. this.handle_setting();
  1149. this.getList();
  1150. this.getStatistics();
  1151. },
  1152. handle_edit() {
  1153. this.auth_edit = !this.auth_edit;
  1154. LocalStorage.set("auth_edit", this.auth_edit);
  1155. },
  1156. getStatistics() {
  1157. const layer = this.shelf.layer_now || 1;
  1158. const url = `location_statistics/refresh-statistics/?layer=${layer}`;
  1159. this.statsLoaded = false;
  1160. getauth(url)
  1161. .then((res) => {
  1162. const arr = res.data && (res.data.data || res.data);
  1163. const item =
  1164. Array.isArray(arr) && arr.length ? arr[0] : res.data || {};
  1165. this._applyStats(item);
  1166. this.statsLoaded = true;
  1167. })
  1168. .catch((err) => {
  1169. console.warn("获取统计失败", err && err.message);
  1170. this.statsLoaded = true;
  1171. });
  1172. },
  1173. reFreshStatistics() {
  1174. const layer = this.shelf.layer_now || 1;
  1175. const url = `location_statistics/refresh-statistics/?layer=${layer}`;
  1176. postauth(url)
  1177. },
  1178. _applyStats(item) {
  1179. this.stats = item || {};
  1180. if (this.stats.statistic_time) {
  1181. try {
  1182. this.stats.statistic_time_display = this.formatDateTime(
  1183. this.stats.statistic_time
  1184. );
  1185. } catch (e) {
  1186. this.stats.statistic_time_display = this.stats.statistic_time;
  1187. }
  1188. } else {
  1189. this.stats.statistic_time_display = "";
  1190. }
  1191. },
  1192. updateCSSVariables() {
  1193. const root = document.documentElement;
  1194. const dwidth = document.documentElement.clientWidth;
  1195. const dheight = document.documentElement.clientHeight;
  1196. const width = dwidth * 0.6;
  1197. const height = dheight * 0.6;
  1198. var cell_d = (width * 8.5) / 10 / this.shelf.cols;
  1199. var cell_x = (cell_d * 1) / 5;
  1200. var cellSize = cell_x / 2;
  1201. var cellGap = height / this.shelf.rows - cell_d;
  1202. if (cellGap < 2) {
  1203. cellGap = 2;
  1204. cell_d = (height - cellGap * this.shelf.rows) / this.shelf.rows;
  1205. cell_x = (cell_d * 3) / 5;
  1206. cellSize = cell_x / 2;
  1207. }
  1208. var cellSize_2 = cellGap / 2;
  1209. var axis_x = cell_x * this.shelf.cols + cell_d * this.shelf.cols;
  1210. root.style.setProperty("--cell-d", `${cell_d}px`);
  1211. root.style.setProperty("--cell-d-x", `${cell_d + cell_x}px`);
  1212. root.style.setProperty("--cell-x-2", `${cellSize}px`);
  1213. root.style.setProperty("--cell-x", `${cell_x}px`);
  1214. root.style.setProperty("--cell-y", `${cellGap + cell_d}px`);
  1215. root.style.setProperty("--cell-y-2", `${cellSize_2}px`);
  1216. root.style.setProperty("--axis-x", `${axis_x}px`);
  1217. },
  1218. handleResize() {
  1219. clearTimeout(this.resizeTimer);
  1220. this.resizeTimer = setTimeout(() => {
  1221. this.updateCSSVariables();
  1222. }, 200);
  1223. },
  1224. },
  1225. mounted() {
  1226. this.updateCSSVariables();
  1227. window.addEventListener("resize", this.handleResize);
  1228. },
  1229. beforeUnmount() {
  1230. this.goodsMap = {};
  1231. this.goodsMatrix = [];
  1232. window.removeEventListener("resize", this.handleResize);
  1233. clearTimeout(this.resizeTimer);
  1234. if (this.$refs.goodscard) {
  1235. this.$refs.goodscard.$destroy();
  1236. }
  1237. },
  1238. created() {
  1239. LocalStorage.set("auth_edit", this.auth_edit);
  1240. this.loadUserPermissions();
  1241. this.handle_setting();
  1242. this.getList();
  1243. this.getStatistics();
  1244. },
  1245. };
  1246. </script>
  1247. <style scoped>
  1248. /* 新增布局容器样式 */
  1249. .layout-container {
  1250. display: flex;
  1251. flex-wrap: wrap;
  1252. gap: 20px;
  1253. width: 100%;
  1254. }
  1255. /* 货架部分 */
  1256. .shelf-section {
  1257. flex: 1;
  1258. min-width: 600px;
  1259. }
  1260. /* 统计面板部分 */
  1261. .stats-section {
  1262. flex: 0 0 300px 300px;
  1263. max-width: 200px;
  1264. }
  1265. :root {
  1266. --cell-d: 40px;
  1267. --cell-d-x: 100px;
  1268. --cell-x-2: 20px;
  1269. --cell-x: 20px;
  1270. --cell-y: 100px;
  1271. --cell-y-2: 20px;
  1272. --axis-x: 20px;
  1273. }
  1274. .btn-group {
  1275. position: absolute;
  1276. left: var(--cell-x-2);
  1277. display: flex;
  1278. gap: 10px;
  1279. }
  1280. /* 网格系统托盘 */
  1281. .grid-system {
  1282. position: relative;
  1283. padding-left: var(--cell-x-2);
  1284. padding-right: 330px;
  1285. /* 左边留出Y轴空间 */
  1286. padding-top: 10px;
  1287. /* 下边留出X轴空间 */
  1288. min-width: max-content;
  1289. }
  1290. /* 坐标轴通用样式 */
  1291. .axis {
  1292. position: absolute;
  1293. background: #333;
  1294. z-index: 2;
  1295. }
  1296. /* 箭头 */
  1297. .axis-arrow {
  1298. position: absolute;
  1299. border-top: 6px solid transparent;
  1300. border-bottom: 6px solid transparent;
  1301. border-left: 12px solid #555;
  1302. }
  1303. .y-axis .axis-arrow {
  1304. top: -10px;
  1305. left: -4px;
  1306. border-left: 5px solid transparent;
  1307. border-right: 5px solid transparent;
  1308. border-bottom: 10px solid #333;
  1309. }
  1310. .x-axis .axis-arrow {
  1311. right: -3px;
  1312. top: -5px;
  1313. border-left-color: #333;
  1314. }
  1315. /* Y轴样式 */
  1316. .y-axis {
  1317. left: 30px;
  1318. top: 0;
  1319. bottom: -10px;
  1320. /* 留出X轴空间 */
  1321. width: 2px;
  1322. }
  1323. .y-axis .axis-numbers {
  1324. position: absolute;
  1325. right: 6px;
  1326. height: 100%;
  1327. display: flex;
  1328. flex-direction: column-reverse;
  1329. }
  1330. .y-axis .axis-numbers div {
  1331. position: relative;
  1332. height: var(--cell-y);
  1333. /* 与网格行高一致 */
  1334. line-height: var(--cell-x);
  1335. top: -0.5em;
  1336. /* 垂直居中 */
  1337. color: #111;
  1338. font-size: larger;
  1339. }
  1340. /* X轴样式 */
  1341. .x-axis {
  1342. position: absolute;
  1343. left: 30px;
  1344. width: var(--axis-x);
  1345. /* 直接使用变量控制宽度 */
  1346. right: auto;
  1347. bottom: -10px;
  1348. height: 2px;
  1349. }
  1350. .x-axis .axis-numbers {
  1351. position: absolute;
  1352. top: 10px;
  1353. /* 数字显示在轴线下方 */
  1354. display: flex;
  1355. }
  1356. .x-axis .axis-numbers div {
  1357. width: var(--cell-d-x);
  1358. /* 与网格列宽一致 */
  1359. text-align: center;
  1360. color: #333;
  1361. }
  1362. /* 网格系统 */
  1363. .grid-container {
  1364. position: relative;
  1365. margin-left: 30px;
  1366. /* 与Y轴对齐 */
  1367. }
  1368. /* 网格内容 */
  1369. .grid-content {
  1370. position: relative;
  1371. z-index: 2;
  1372. }
  1373. .grid-row {
  1374. display: flex;
  1375. height: var(--cell-y);
  1376. /* 固定行高上列下行 */
  1377. gap: var(--cell-x);
  1378. }
  1379. .grid-item {
  1380. width: var(--cell-d);
  1381. /* 固定宽度 */
  1382. height: var(--cell-d);
  1383. background: transparent;
  1384. transition: transform 0.2s;
  1385. }
  1386. .select-item:hover {
  1387. transform: translateY(-2px);
  1388. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  1389. }
  1390. .stats-cards {
  1391. display: flex;
  1392. flex-direction: column;
  1393. gap: 12px;
  1394. }
  1395. .stat-card {
  1396. width: 100%;
  1397. border-radius: 12px;
  1398. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  1399. transition: transform 0.2s ease;
  1400. }
  1401. .stat-card:hover {
  1402. transform: translateY(-2px);
  1403. box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
  1404. }
  1405. .card-title {
  1406. font-size: 14px;
  1407. font-weight: 600;
  1408. color: #333;
  1409. margin-bottom: 8px;
  1410. }
  1411. .big-num {
  1412. font-size: 32px;
  1413. font-weight: 700;
  1414. color: #1976d2;
  1415. }
  1416. .small-num {
  1417. font-size: 18px;
  1418. font-weight: 600;
  1419. }
  1420. .small-num.used {
  1421. color: #f57c00;
  1422. }
  1423. .small-num.available {
  1424. color: #4caf50;
  1425. }
  1426. .muted {
  1427. color: #888;
  1428. font-size: 12px;
  1429. }
  1430. .util-row {
  1431. display: flex;
  1432. justify-content: space-between;
  1433. align-items: center;
  1434. margin-top: 12px;
  1435. }
  1436. .util-number {
  1437. font-weight: 700;
  1438. color: #1976d2;
  1439. }
  1440. .type-list {
  1441. space-y: 8px;
  1442. }
  1443. .type-item {
  1444. display: flex;
  1445. justify-content: space-between;
  1446. align-items: center;
  1447. padding: 4px 0;
  1448. border-bottom: 1px solid #f0f0f0;
  1449. }
  1450. .type-info {
  1451. display: flex;
  1452. flex-direction: column;
  1453. }
  1454. .type-name {
  1455. font-weight: 600;
  1456. font-size: 13px;
  1457. }
  1458. .type-total {
  1459. font-size: 11px;
  1460. color: #999;
  1461. }
  1462. .type-details {
  1463. display: flex;
  1464. gap: 12px;
  1465. }
  1466. .detail-item {
  1467. text-align: center;
  1468. }
  1469. .detail-label {
  1470. display: block;
  1471. font-size: 10px;
  1472. color: #666;
  1473. }
  1474. .detail-value {
  1475. display: block;
  1476. font-size: 12px;
  1477. font-weight: 500;
  1478. }
  1479. .detail-value.used {
  1480. color: #f57c00;
  1481. }
  1482. .detail-value.available {
  1483. color: #4caf50;
  1484. }
  1485. .action-buttons {
  1486. display: flex;
  1487. flex-direction: column;
  1488. gap: 8px;
  1489. }
  1490. .stats-loading {
  1491. display: flex;
  1492. flex-direction: column;
  1493. align-items: center;
  1494. justify-content: center;
  1495. padding: 20px;
  1496. gap: 8px;
  1497. color: #666;
  1498. }
  1499. .check-dialog {
  1500. min-width: 800px;
  1501. max-width: 90vw;
  1502. max-height: 90vh;
  1503. }
  1504. .dialog-header {
  1505. padding: 16px 24px;
  1506. background: #f8f9fa;
  1507. }
  1508. .header-content {
  1509. display: flex;
  1510. justify-content: space-between;
  1511. align-items: center;
  1512. }
  1513. .dialog-title {
  1514. font-size: 20px;
  1515. font-weight: 700;
  1516. color: #333;
  1517. }
  1518. .dialog-subtitle {
  1519. font-size: 14px;
  1520. color: #666;
  1521. }
  1522. .dialog-content {
  1523. padding: 24px;
  1524. overflow-y: auto;
  1525. }
  1526. .summary-cards {
  1527. display: grid;
  1528. grid-template-columns: repeat(3, 1fr);
  1529. gap: 16px;
  1530. margin-bottom: 24px;
  1531. }
  1532. .summary-card {
  1533. text-align: center;
  1534. padding: 16px;
  1535. border-radius: 8px;
  1536. transition: transform 0.2s ease;
  1537. }
  1538. .summary-card:hover {
  1539. transform: translateY(-2px);
  1540. }
  1541. .summary-card.error {
  1542. background: #ffebee;
  1543. }
  1544. .summary-card.success {
  1545. background: #e8f5e8;
  1546. }
  1547. .summary-value {
  1548. font-size: 28px;
  1549. font-weight: 700;
  1550. margin-bottom: 4px;
  1551. }
  1552. .summary-card.error .summary-value {
  1553. color: #f44336;
  1554. }
  1555. .summary-card.success .summary-value {
  1556. color: #4caf50;
  1557. }
  1558. .summary-label {
  1559. font-size: 14px;
  1560. color: #666;
  1561. }
  1562. .error-sections {
  1563. space-y: 24px;
  1564. }
  1565. .error-section {
  1566. background: white;
  1567. border-radius: 8px;
  1568. padding: 16px;
  1569. border: 1px solid #e0e0e0;
  1570. }
  1571. .section-header {
  1572. display: flex;
  1573. align-items: center;
  1574. margin-bottom: 16px;
  1575. font-size: 16px;
  1576. font-weight: 600;
  1577. color: #333;
  1578. }
  1579. .error-table {
  1580. margin-top: 12px;
  1581. }
  1582. .no-errors {
  1583. text-align: center;
  1584. padding: 20px;
  1585. color: #666;
  1586. font-style: italic;
  1587. }
  1588. .group-errors {
  1589. space-y: 8px;
  1590. }
  1591. .group-error-item {
  1592. padding: 12px;
  1593. background: #f8f9fa;
  1594. border-radius: 6px;
  1595. border-left: 4px solid #ffa500;
  1596. }
  1597. .group-info {
  1598. display: flex;
  1599. justify-content: space-between;
  1600. align-items: center;
  1601. margin-bottom: 4px;
  1602. }
  1603. .error-type {
  1604. font-size: 12px;
  1605. color: #666;
  1606. background: #ffecb3;
  1607. padding: 2px 6px;
  1608. border-radius: 3px;
  1609. }
  1610. .group-details {
  1611. font-size: 12px;
  1612. color: #666;
  1613. }
  1614. .dialog-actions {
  1615. padding: 16px 24px;
  1616. border-top: 1px solid #e0e0e0;
  1617. }
  1618. @media (max-width: 1024px) {
  1619. .stats-panel {
  1620. position: relative;
  1621. width: 100%;
  1622. right: 0;
  1623. top: 0;
  1624. margin-top: 20px;
  1625. }
  1626. .summary-cards {
  1627. grid-template-columns: 1fr;
  1628. }
  1629. }
  1630. @media (max-width: 768px) {
  1631. .check-dialog {
  1632. min-width: 95vw;
  1633. }
  1634. .stats-panel {
  1635. width: 100%;
  1636. }
  1637. }
  1638. </style>