rd-Inventory-safety.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. <script lang="ts" setup>
  2. import * as echarts from 'echarts'
  3. import {
  4. ANIMATION,
  5. CHART_RENDERER,
  6. createLegend,
  7. createTooltip,
  8. FONT_FAMILY,
  9. THEME
  10. } from '@/utils/kb'
  11. type ActivePanel = 'distribution' | 'trend'
  12. type InventoryItem = {
  13. project: string
  14. mayInventoryAmount: number
  15. yearBeginningBacklog: number
  16. mayBacklogAmount: number
  17. }
  18. const activePanel = ref<ActivePanel>('distribution')
  19. const distributionChartRef = ref<HTMLDivElement>()
  20. const trendChartRef = ref<HTMLDivElement>()
  21. let distributionChart: echarts.ECharts | null = null
  22. let trendChart: echarts.ECharts | null = null
  23. const panelOptions: Array<{ label: string; value: ActivePanel }> = [
  24. {
  25. label: '分布',
  26. value: 'distribution'
  27. },
  28. {
  29. label: '趋势',
  30. value: 'trend'
  31. }
  32. ]
  33. const activeTitle = computed(() => (activePanel.value === 'distribution' ? '存货分布' : '积压趋势'))
  34. const inventoryData: InventoryItem[] = [
  35. {
  36. project: '东营中心仓',
  37. mayInventoryAmount: 7.35,
  38. yearBeginningBacklog: 10.13,
  39. mayBacklogAmount: 7.02
  40. },
  41. {
  42. project: '吉尔吉斯',
  43. mayInventoryAmount: 23.82,
  44. yearBeginningBacklog: 23.82,
  45. mayBacklogAmount: 25.18
  46. },
  47. {
  48. project: '新疆-连油',
  49. mayInventoryAmount: 21.9,
  50. yearBeginningBacklog: 177.02,
  51. mayBacklogAmount: 19.55
  52. },
  53. {
  54. project: '青海',
  55. mayInventoryAmount: 52.03,
  56. yearBeginningBacklog: 177.97,
  57. mayBacklogAmount: 53.46
  58. },
  59. {
  60. project: '东部-陕蒙',
  61. mayInventoryAmount: 5.63,
  62. yearBeginningBacklog: 84.75,
  63. mayBacklogAmount: 7.65
  64. },
  65. {
  66. project: '东部-胜利',
  67. mayInventoryAmount: 8.82,
  68. yearBeginningBacklog: 59.9,
  69. mayBacklogAmount: 10.67
  70. },
  71. {
  72. project: '西南压裂',
  73. mayInventoryAmount: 41.78,
  74. yearBeginningBacklog: 174.18,
  75. mayBacklogAmount: 27.72
  76. },
  77. {
  78. project: '西南连油',
  79. mayInventoryAmount: 24.13,
  80. yearBeginningBacklog: 90.01,
  81. mayBacklogAmount: 23.37
  82. },
  83. {
  84. project: '伊拉克(国内)',
  85. mayInventoryAmount: 0.1,
  86. yearBeginningBacklog: 190.71,
  87. mayBacklogAmount: 0.1
  88. },
  89. {
  90. project: '大庆工具',
  91. mayInventoryAmount: 0.12,
  92. yearBeginningBacklog: 1.79,
  93. mayBacklogAmount: 0.05
  94. },
  95. {
  96. project: '利比亚(国内)',
  97. mayInventoryAmount: 0,
  98. yearBeginningBacklog: 31.33,
  99. mayBacklogAmount: 0
  100. }
  101. ]
  102. function formatProjectName(value: string) {
  103. return value.replace(/项目$/, '')
  104. }
  105. function formatAmount(value: number) {
  106. return Number(value || 0).toFixed(2)
  107. }
  108. function getInventoryChartLayout(chartRef: Ref<HTMLDivElement | undefined>) {
  109. const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
  110. const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
  111. return {
  112. compact,
  113. trendGridTop: compact ? 34 : 40,
  114. trendGridLeft: compact ? 36 : 44,
  115. trendGridRight: compact ? 8 : 16,
  116. trendGridBottom: compact ? 12 : 12,
  117. legendTop: compact ? 0 : 4,
  118. legendRight: compact ? 2 : 6,
  119. legendItemSize: compact ? 8 : 10,
  120. legendGap: compact ? 8 : 12,
  121. legendFontSize: compact ? 10 : 12,
  122. axisFontSize: compact ? 10 : 10,
  123. yAxisLabelMargin: compact ? 6 : 8,
  124. xAxisLabelRotate: compact ? 45 : 36,
  125. distributionTitleTop: compact ? 6 : 4,
  126. distributionTitleFontSize: compact ? 12 : 14,
  127. distributionTitleLineHeight: compact ? 14 : 16,
  128. distributionPieRadius: compact ? ['36%', '54%'] : ['48%', '68%'],
  129. distributionPieCenterY: compact ? '62%' : '57%',
  130. distributionLegendBottom: compact ? 0 : 10,
  131. distributionLegendItemSize: compact ? 9 : 13,
  132. distributionLegendGap: compact ? 8 : 14,
  133. distributionLegendFontSize: compact ? 10 : 14,
  134. pieLabelNameFontSize: compact ? 11 : 13,
  135. pieLabelNameLineHeight: compact ? 18 : 22,
  136. pieLabelValueFontSize: compact ? 16 : 20,
  137. pieLabelValueLineHeight: compact ? 22 : 30,
  138. barWidth: compact ? 10 : 12,
  139. barGap: compact ? '10%' : '8%',
  140. barCategoryGap: compact ? '44%' : '38%',
  141. labelDistance: compact ? 4 : 7,
  142. labelFontSize: compact ? 10 : 12
  143. }
  144. }
  145. function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
  146. const layout = getInventoryChartLayout(distributionChartRef)
  147. const inventoryTotal = data.reduce((total, item) => total + item.mayInventoryAmount, 0)
  148. const backlogTotal = data.reduce((total, item) => total + item.yearBeginningBacklog, 0)
  149. return {
  150. ...ANIMATION,
  151. color: [
  152. THEME.color.blue.line,
  153. THEME.color.orange.line,
  154. THEME.color.green.line,
  155. THEME.color.red.line,
  156. THEME.color.blue.mid,
  157. THEME.color.orange.mid,
  158. THEME.color.green.mid,
  159. THEME.color.red.mid
  160. ],
  161. tooltip: createTooltip({
  162. trigger: 'item',
  163. formatter(params: any) {
  164. return `${params.seriesName}<br/>${params.marker}${params.name}:${formatAmount(
  165. params.value
  166. )}万元<br/>占比:${params.percent}%`
  167. }
  168. }),
  169. title: [
  170. {
  171. text: `5月总积压库存金额\n${formatAmount(inventoryTotal)} 万元`,
  172. left: '26.5%',
  173. top: layout.distributionTitleTop,
  174. textAlign: 'center',
  175. textStyle: {
  176. color: THEME.text.strong,
  177. fontSize: layout.distributionTitleFontSize,
  178. fontWeight: 700,
  179. lineHeight: layout.distributionTitleLineHeight,
  180. fontFamily: FONT_FAMILY
  181. }
  182. },
  183. {
  184. text: `总库存金额\n${formatAmount(backlogTotal)} 万元`,
  185. left: '71.5%',
  186. top: layout.distributionTitleTop,
  187. textAlign: 'center',
  188. textStyle: {
  189. color: THEME.text.strong,
  190. fontSize: layout.distributionTitleFontSize,
  191. fontWeight: 700,
  192. lineHeight: layout.distributionTitleLineHeight,
  193. fontFamily: FONT_FAMILY
  194. }
  195. }
  196. ],
  197. legend: createLegend({
  198. type: 'scroll',
  199. bottom: layout.distributionLegendBottom,
  200. left: 10,
  201. right: 10,
  202. itemWidth: layout.distributionLegendItemSize,
  203. itemHeight: layout.distributionLegendItemSize,
  204. itemGap: layout.distributionLegendGap,
  205. textStyle: {
  206. color: THEME.text.regular,
  207. fontSize: layout.distributionLegendFontSize,
  208. fontWeight: 600,
  209. fontFamily: FONT_FAMILY
  210. }
  211. }),
  212. series: [
  213. {
  214. name: '5月总积压库存金额',
  215. type: 'pie',
  216. radius: layout.distributionPieRadius,
  217. center: ['27%', layout.distributionPieCenterY],
  218. minAngle: 5,
  219. label: { show: false },
  220. data: data.map((item) => ({
  221. name: item.project,
  222. value: item.mayInventoryAmount
  223. }))
  224. },
  225. {
  226. name: '总库存金额',
  227. type: 'pie',
  228. radius: layout.distributionPieRadius,
  229. center: ['73%', layout.distributionPieCenterY],
  230. minAngle: 5,
  231. label: { show: false },
  232. data: data
  233. .filter((item) => item.yearBeginningBacklog > 0)
  234. .map((item) => ({
  235. name: item.project,
  236. value: item.yearBeginningBacklog
  237. }))
  238. }
  239. ]
  240. }
  241. }
  242. function getTrendOption(data: InventoryItem[]): echarts.EChartsOption {
  243. const projects = data.map((item) => formatProjectName(item.project))
  244. const maxBacklog = Math.max(
  245. ...data.map((item) => Math.max(item.mayInventoryAmount, item.mayBacklogAmount)),
  246. 1
  247. )
  248. const backlogAxisMax = Math.ceil((maxBacklog * 1.15) / 50) * 50
  249. const layout = getInventoryChartLayout(trendChartRef)
  250. const barLabel = {
  251. show: false,
  252. position: 'top' as any,
  253. distance: layout.labelDistance,
  254. color: THEME.text.strong,
  255. fontSize: layout.labelFontSize,
  256. fontWeight: 700,
  257. fontFamily: FONT_FAMILY,
  258. formatter(params: any) {
  259. const value = Number(params.value)
  260. return formatAmount(value)
  261. }
  262. }
  263. return {
  264. ...ANIMATION,
  265. grid: {
  266. ...THEME.grid,
  267. top: layout.trendGridTop,
  268. left: layout.trendGridLeft,
  269. right: layout.trendGridRight,
  270. bottom: layout.trendGridBottom
  271. },
  272. color: [THEME.color.blue.line, THEME.color.orange.line],
  273. legend: createLegend(
  274. {
  275. top: layout.legendTop,
  276. right: layout.legendRight,
  277. itemWidth: layout.legendItemSize,
  278. itemHeight: layout.legendItemSize,
  279. itemGap: layout.legendGap,
  280. textStyle: {
  281. color: THEME.text.regular,
  282. fontSize: layout.legendFontSize,
  283. fontWeight: 600,
  284. fontFamily: FONT_FAMILY
  285. }
  286. },
  287. ['2026期初', '5月总积压']
  288. ),
  289. tooltip: createTooltip({
  290. trigger: 'axis',
  291. axisPointer: {
  292. type: 'shadow',
  293. shadowStyle: {
  294. color: THEME.split
  295. }
  296. },
  297. valueFormatter(value: number) {
  298. return `${formatAmount(value)}万元`
  299. }
  300. }),
  301. xAxis: {
  302. type: 'category',
  303. data: projects,
  304. axisLine: {
  305. show: false
  306. },
  307. axisTick: {
  308. show: false
  309. },
  310. axisLabel: {
  311. color: THEME.text.regular,
  312. fontSize: layout.axisFontSize,
  313. fontWeight: 600,
  314. fontFamily: FONT_FAMILY,
  315. interval: 0,
  316. margin: layout.yAxisLabelMargin,
  317. rotate: layout.xAxisLabelRotate
  318. }
  319. },
  320. yAxis: {
  321. type: 'value',
  322. max: backlogAxisMax,
  323. splitNumber: 4,
  324. axisLine: {
  325. show: false
  326. },
  327. axisTick: {
  328. show: false
  329. },
  330. axisLabel: {
  331. color: THEME.text.regular,
  332. fontSize: layout.axisFontSize,
  333. fontFamily: FONT_FAMILY,
  334. formatter(value: number) {
  335. return `${value}`
  336. }
  337. },
  338. splitLine: {
  339. lineStyle: {
  340. color: THEME.split,
  341. type: 'dashed'
  342. }
  343. }
  344. },
  345. series: [
  346. {
  347. name: '2026期初',
  348. type: 'bar',
  349. data: data.map((item) => item.mayBacklogAmount),
  350. barWidth: layout.barWidth,
  351. barGap: layout.barGap,
  352. barCategoryGap: layout.barCategoryGap,
  353. barMinHeight: 0,
  354. showBackground: false,
  355. backgroundStyle: {
  356. color: THEME.split,
  357. borderRadius: 999
  358. },
  359. label: barLabel,
  360. labelLayout: {
  361. hideOverlap: true
  362. },
  363. itemStyle: {
  364. shadowBlur: 10,
  365. shadowColor: THEME.color.blue.bg,
  366. color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
  367. { offset: 0, color: THEME.color.blue.light },
  368. { offset: 0.55, color: THEME.color.blue.mid },
  369. { offset: 1, color: THEME.color.blue.line }
  370. ])
  371. }
  372. },
  373. {
  374. name: '5月总积压',
  375. type: 'bar',
  376. data: data.map((item) => item.mayInventoryAmount),
  377. barWidth: layout.barWidth,
  378. barMinHeight: 0,
  379. barGap: layout.barGap,
  380. barCategoryGap: layout.barCategoryGap,
  381. showBackground: false,
  382. backgroundStyle: {
  383. color: THEME.split,
  384. borderRadius: 999
  385. },
  386. label: barLabel,
  387. labelLayout: {
  388. hideOverlap: true
  389. },
  390. itemStyle: {
  391. shadowBlur: 10,
  392. shadowColor: THEME.color.orange.bg,
  393. color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
  394. { offset: 0, color: THEME.color.orange.light },
  395. { offset: 0.55, color: THEME.color.orange.mid },
  396. { offset: 1, color: THEME.color.orange.line }
  397. ])
  398. }
  399. }
  400. ]
  401. }
  402. }
  403. function initChart(
  404. chartRef: Ref<HTMLDivElement | undefined>,
  405. chart: echarts.ECharts | null,
  406. option: echarts.EChartsOption
  407. ) {
  408. if (!chartRef.value) return chart
  409. chart?.dispose()
  410. const nextChart = echarts.init(chartRef.value, undefined, {
  411. renderer: CHART_RENDERER
  412. })
  413. nextChart.setOption(option, true)
  414. return nextChart
  415. }
  416. function renderDistributionChart() {
  417. distributionChart?.setOption(getDistributionOption(inventoryData), true)
  418. }
  419. function renderTrendChart() {
  420. trendChart?.setOption(getTrendOption(inventoryData), true)
  421. }
  422. function initDistributionChart() {
  423. distributionChart = initChart(
  424. distributionChartRef,
  425. distributionChart,
  426. getDistributionOption(inventoryData)
  427. )
  428. }
  429. function initTrendChart() {
  430. trendChart = initChart(trendChartRef, trendChart, getTrendOption(inventoryData))
  431. }
  432. function resizeCharts() {
  433. distributionChart?.resize()
  434. trendChart?.resize()
  435. renderDistributionChart()
  436. renderTrendChart()
  437. }
  438. function destroyCharts() {
  439. distributionChart?.dispose()
  440. trendChart?.dispose()
  441. distributionChart = null
  442. trendChart = null
  443. }
  444. watch(activePanel, (value) => {
  445. nextTick(() => {
  446. if (value === 'distribution') {
  447. if (!distributionChart) initDistributionChart()
  448. renderDistributionChart()
  449. } else {
  450. if (!trendChart) initTrendChart()
  451. renderTrendChart()
  452. }
  453. resizeCharts()
  454. })
  455. })
  456. onMounted(() => {
  457. initDistributionChart()
  458. window.addEventListener('resize', resizeCharts)
  459. window.addEventListener('rdkb:resize', resizeCharts)
  460. })
  461. onUnmounted(() => {
  462. window.removeEventListener('resize', resizeCharts)
  463. window.removeEventListener('rdkb:resize', resizeCharts)
  464. destroyCharts()
  465. })
  466. </script>
  467. <template>
  468. <div class="panel flex flex-col">
  469. <div class="panel-title flex items-center justify-between">
  470. <div class="kb-panel-title-text flex items-center">
  471. <div class="icon-decorator">
  472. <span></span>
  473. <span></span>
  474. </div>
  475. {{ activeTitle }}
  476. </div>
  477. <el-segmented
  478. v-model="activePanel"
  479. :options="panelOptions"
  480. size="small"
  481. class="inventory-switch" />
  482. </div>
  483. <div class="flex-1 min-h-0">
  484. <div
  485. v-show="activePanel === 'distribution'"
  486. ref="distributionChartRef"
  487. class="inventory-chart"></div>
  488. <div v-show="activePanel === 'trend'" ref="trendChartRef" class="inventory-chart"></div>
  489. </div>
  490. </div>
  491. </template>
  492. <style lang="scss" scoped>
  493. @import url('@/styles/kb.scss');
  494. .inventory-switch {
  495. --el-segmented-item-selected-color: #03409b;
  496. --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
  497. --el-segmented-bg-color: rgb(31 91 184 / 10%);
  498. --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
  499. min-height: calc(26px * var(--kb-scale, 1));
  500. padding: calc(2px * var(--kb-scale, 1));
  501. border: 1px solid rgb(31 91 184 / 12%);
  502. transform: translateY(calc(-2px * var(--kb-scale, 1)));
  503. :deep(.el-segmented__item) {
  504. min-height: calc(22px * var(--kb-scale, 1));
  505. padding: 0 calc(8px * var(--kb-scale, 1));
  506. font-size: calc(13px * var(--kb-scale, 1));
  507. font-weight: 600;
  508. color: #29527f;
  509. }
  510. }
  511. .inventory-chart {
  512. width: 100%;
  513. height: 100%;
  514. min-height: 0;
  515. }
  516. </style>