factoryProductionDashboard.vue 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031
  1. <template>
  2. <vue-fullscreen
  3. class="fp-container"
  4. v-cloak
  5. v-model="isFullscreen"
  6. fullscreenClass="fp-container"
  7. :exit-on-click-wrapper="false"
  8. >
  9. <div class="fp-container" v-cloak>
  10. <div class="fp-header">
  11. <div class="fp-header-left">
  12. <el-select
  13. v-model="factoryId"
  14. size="mini"
  15. class="fp-select"
  16. placeholder="工厂名称"
  17. :popper-append-to-body="false"
  18. >
  19. <el-option
  20. v-for="f in factoryList"
  21. :key="f.id"
  22. :label="f.name"
  23. :value="f.id"
  24. />
  25. </el-select>
  26. <el-date-picker
  27. v-model="dateRange"
  28. class="fp-date"
  29. size="mini"
  30. type="daterange"
  31. range-separator="-"
  32. start-placeholder="开始日期"
  33. end-placeholder="结束日期"
  34. value-format="yyyy-MM-dd"
  35. :append-to-body="false"
  36. />
  37. </div>
  38. <div class="fp-title">工厂生产综合看板</div>
  39. <div class="fp-header-right">
  40. <div class="fp-time">
  41. <span class="fp-date-text">{{ date }}</span>
  42. <span class="fp-time-text">{{ time }}</span>
  43. <span class="fp-week-text">{{ week }}</span>
  44. </div>
  45. <span class="fp-fullscreen-toggle" @click.passive="onFullscreen">
  46. <i
  47. v-if="isFullscreen"
  48. title="取消全屏"
  49. class="el-icon-_screen-restore"
  50. ></i>
  51. <i v-else title="全屏" class="el-icon-_screen-full"></i>
  52. </span>
  53. </div>
  54. </div>
  55. <div class="fp-body">
  56. <!-- Left -->
  57. <div class="fp-col fp-col-left">
  58. <chart-card title="生产统计" size="small">
  59. <div class="fp-number-card">
  60. <div class="fp-number-display">
  61. <div class="fp-unit-box">
  62. <span class="fp-unit">十万</span>
  63. <div class="fp-digit-box">
  64. <span class="fp-digit">{{
  65. productionStats.transformQuantity[0]
  66. }}</span>
  67. </div>
  68. </div>
  69. <div class="fp-unit-box">
  70. <span class="fp-unit">万</span>
  71. <div class="fp-digit-box">
  72. <span class="fp-digit">{{
  73. productionStats.transformQuantity[1]
  74. }}</span>
  75. </div>
  76. </div>
  77. <div class="fp-unit-box">
  78. <span class="fp-unit">千</span>
  79. <div class="fp-digit-box">
  80. <span class="fp-digit">{{
  81. productionStats.transformQuantity[2]
  82. }}</span>
  83. </div>
  84. </div>
  85. <div class="fp-separator">,</div>
  86. <div class="fp-unit-box">
  87. <span class="fp-unit"></span>
  88. <div class="fp-digit-box">
  89. <span class="fp-digit">{{
  90. productionStats.transformQuantity[3]
  91. }}</span>
  92. </div>
  93. </div>
  94. <div class="fp-unit-box">
  95. <span class="fp-unit"></span>
  96. <div class="fp-digit-box">
  97. <span class="fp-digit">{{
  98. productionStats.transformQuantity[4]
  99. }}</span>
  100. </div>
  101. </div>
  102. <div class="fp-unit-box">
  103. <span class="fp-unit"></span>
  104. <div class="fp-digit-box">
  105. <span class="fp-digit">{{
  106. productionStats.transformQuantity[5]
  107. }}</span>
  108. </div>
  109. </div>
  110. </div>
  111. <div class="fp-mini-stats">
  112. <div class="fp-mini-stat">
  113. <div class="fp-mini-icon">
  114. <img src="@/assets/png/Group.png" alt="" />
  115. </div>
  116. <div class="fp-mini-info">
  117. <div class="fp-mini-label">要求生产总数量:</div>
  118. <div class="fp-mini-value">{{
  119. productionStats.requiredTotal
  120. }}</div>
  121. </div>
  122. </div>
  123. <div class="fp-mini-stat">
  124. <div class="fp-mini-icon">
  125. <img src="@/assets/png/Group.png" alt="" />
  126. </div>
  127. <div class="fp-mini-info">
  128. <div class="fp-mini-label">生产达成率:</div>
  129. <div class="fp-mini-value"
  130. >{{ productionStats.achievementRate }}%</div
  131. >
  132. </div>
  133. </div>
  134. </div>
  135. </div>
  136. </chart-card>
  137. <chart-card title="五品" size="small">
  138. <div class="fp-chart-box">
  139. <device-status-chart
  140. v-if="isFlag"
  141. :data="fiveCategoryData"
  142. class="fp-chart-fill"
  143. />
  144. </div>
  145. </chart-card>
  146. <chart-card title="四数" size="small">
  147. <div class="fp-chart-box">
  148. <Bar3DChart
  149. v-if="isFlag"
  150. :data="fourNumberBarData"
  151. :colors="['#1163fb', '#1163fb', '#1163fb', '#1163fb']"
  152. :show-label="true"
  153. class="fp-chart-fill"
  154. />
  155. </div>
  156. </chart-card>
  157. </div>
  158. <!-- Middle -->
  159. <div class="fp-col fp-col-middle">
  160. <div class="fp-middle-top">
  161. <div class="fp-middle-title">
  162. <img class="fp-title-icon" src="@/assets/png/icon.png" alt="" />
  163. <span>生产情况</span>
  164. </div>
  165. <div class="fp-kpis">
  166. <div class="fp-kpi">
  167. <div class="fp-kpi-screen">
  168. <div class="fp-kpi-value fp-kpi-glow">{{
  169. productionKpi.delayedCount
  170. }}</div>
  171. </div>
  172. <div class="fp-kpi-base"></div>
  173. <div class="fp-kpi-label">已延期生产计划数</div>
  174. </div>
  175. <div class="fp-kpi">
  176. <div class="fp-kpi-screen">
  177. <div class="fp-kpi-value fp-kpi-glow"
  178. >{{ productionKpi.planAchievementRate }}%</div
  179. >
  180. </div>
  181. <div class="fp-kpi-base"></div>
  182. <div class="fp-kpi-label">计划达成率</div>
  183. </div>
  184. <div class="fp-kpi">
  185. <div class="fp-kpi-screen">
  186. <div class="fp-kpi-value fp-kpi-glow">{{
  187. productionKpi.unpublishedCount
  188. }}</div>
  189. </div>
  190. <div class="fp-kpi-base"></div>
  191. <div class="fp-kpi-label">未发布计划数</div>
  192. </div>
  193. </div>
  194. <div class="fp-middle-note"
  195. >注:生产计划完成率=已完成生产计划数/所有生产计划数</div
  196. >
  197. </div>
  198. <chart-card title="生产计划进度" size="large">
  199. <div class="fp-table-wrap">
  200. <div class="fp-table-tabs">
  201. <div
  202. v-for="t in planTabs"
  203. :key="t.key"
  204. class="fp-tab"
  205. :class="{ active: activePlanTab === t.key }"
  206. @click="activePlanTab = t.key"
  207. >
  208. {{ t.label }}
  209. </div>
  210. </div>
  211. <div class="fp-table">
  212. <table class="fp-main-table">
  213. <colgroup>
  214. <col style="width: 11%" />
  215. <col style="width: 15%" />
  216. <col style="width: 10%" />
  217. <col style="width: 9%" />
  218. <col style="width: 11%" />
  219. <col style="width: 15%" />
  220. <col style="width: 15%" />
  221. <col style="width: 14%" />
  222. </colgroup>
  223. <thead>
  224. <tr>
  225. <th>计划编号</th>
  226. <th>产品名称</th>
  227. <th>批次号</th>
  228. <th>计划数量</th>
  229. <th>工艺路线</th>
  230. <th>计划开始时间</th>
  231. <th>计划结束时间</th>
  232. <th>计划状态</th>
  233. </tr>
  234. </thead>
  235. </table>
  236. <div
  237. ref="planScrollBody"
  238. class="fp-scroll-body"
  239. @mouseenter="stopPlanAutoScroll"
  240. @mouseleave="startPlanAutoScroll"
  241. >
  242. <table class="fp-main-table">
  243. <colgroup>
  244. <col style="width: 11%" />
  245. <col style="width: 15%" />
  246. <col style="width: 10%" />
  247. <col style="width: 9%" />
  248. <col style="width: 11%" />
  249. <col style="width: 15%" />
  250. <col style="width: 15%" />
  251. <col style="width: 14%" />
  252. </colgroup>
  253. <tbody>
  254. <tr
  255. v-for="row in filteredPlanRows"
  256. :key="row.planNo"
  257. class="fp-row"
  258. :style="{
  259. backgroundColor: row.isDelayed ? '#ff4d4f' : '#031d42'
  260. }"
  261. >
  262. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  263. row.planNo
  264. }}</td>
  265. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  266. row.productName
  267. }}</td>
  268. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  269. row.batchNo
  270. }}</td>
  271. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  272. row.planQty
  273. }}</td>
  274. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  275. row.currentProcess
  276. }}</td>
  277. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  278. row.planStart
  279. }}</td>
  280. <td class="fp-cell" @mouseenter="syncOverflowTitle">{{
  281. row.planEnd
  282. }}</td>
  283. <td class="fp-cell" @mouseenter="syncOverflowTitle">
  284. <span
  285. :class="
  286. row.isDelayed ? 'fp-status-red' : 'fp-status-blue'
  287. "
  288. >
  289. {{ row.statusText }}
  290. </span>
  291. </td>
  292. </tr>
  293. <tr v-if="filteredPlanRows.length === 0">
  294. <td class="fp-empty" colspan="8">暂无生产计划数据</td>
  295. </tr>
  296. </tbody>
  297. </table>
  298. </div>
  299. </div>
  300. </div>
  301. </chart-card>
  302. </div>
  303. <!-- Right -->
  304. <div class="fp-col fp-col-right">
  305. <div class="fp-inventory-card chart-card">
  306. <div class="card-header fp-inventory-header">
  307. <div class="card-logo">
  308. <img :src="inventoryIcon" alt="" />
  309. </div>
  310. <div class="card-title">库存台账</div>
  311. <div class="fp-inventory-filters">
  312. <span
  313. class="fp-filter-btn"
  314. :class="{ active: activeInventoryFilter === 'category' }"
  315. @click="setInventoryFilter('category')"
  316. >
  317. 分类维度
  318. </span>
  319. <span
  320. class="fp-filter-btn"
  321. :class="{ active: activeInventoryFilter === 'keeper' }"
  322. @click="setInventoryFilter('keeper')"
  323. >
  324. 仓管
  325. </span>
  326. <i
  327. class="el-icon-setting fp-filter-icon"
  328. @click.stop="showInventoryDropdown = !showInventoryDropdown"
  329. ></i>
  330. <div
  331. v-if="showInventoryDropdown"
  332. class="fp-inventory-dropdown"
  333. @click.stop
  334. >
  335. <div class="fp-dropdown-header">
  336. <el-checkbox
  337. :value="isAllInventoryChecked"
  338. :indeterminate="
  339. checkedInventoryCount > 0 &&
  340. checkedInventoryCount < inventoryXAxisOptions.length
  341. "
  342. @change="toggleAllInventoryXAxis"
  343. >
  344. 展示设置
  345. </el-checkbox>
  346. <span class="fp-dropdown-count"
  347. >({{ checkedInventoryCount }}/{{
  348. inventoryXAxisOptions.length
  349. }})</span
  350. >
  351. </div>
  352. <div class="fp-dropdown-content">
  353. <div
  354. v-for="item in inventoryXAxisOptions"
  355. :key="item.name"
  356. class="fp-dropdown-item"
  357. >
  358. <el-checkbox
  359. v-model="item.checked"
  360. @change="handleInventoryXAxisChange"
  361. >
  362. {{ item.name }}
  363. </el-checkbox>
  364. </div>
  365. </div>
  366. </div>
  367. </div>
  368. </div>
  369. <div class="fp-chart-box card-content">
  370. <Bar3DChart
  371. v-if="isFlag"
  372. :data="inventoryLedgerDisplayData"
  373. :colors="inventoryBarColors"
  374. :show-label="true"
  375. :bar-size="[10, 6]"
  376. class="fp-chart-fill"
  377. />
  378. </div>
  379. </div>
  380. <chart-card title="各类型设备台账" size="small">
  381. <div class="fp-chart-box">
  382. <bar-line-combo-chart
  383. v-if="isFlag"
  384. :categories="deviceLedger.categories"
  385. :bar-data="deviceLedger.barData"
  386. :top-bar-data="deviceLedger.topBarData"
  387. :line-data="deviceLedger.lineData"
  388. bar-name="占用数"
  389. top-bar-name="总数量"
  390. line-name="占比"
  391. :show-bar-label="false"
  392. :show-line-label="false"
  393. :show-legend="false"
  394. :bar-colors="['#1163fb', '#1163fb', '#1163fb', '#1163fb']"
  395. top-bar-color="#f9bb19"
  396. class="fp-chart-fill"
  397. />
  398. </div>
  399. </chart-card>
  400. <chart-card title="质量统计" size="small">
  401. <div class="fp-quality">
  402. <div class="fp-quality-top">
  403. <div class="fp-quality-item">
  404. <div class="fp-quality-label">合格率</div>
  405. <div class="fp-quality-value good"
  406. >{{ quality.passRate }}%</div
  407. >
  408. </div>
  409. <div class="fp-quality-item">
  410. <div class="fp-quality-label">不合格率</div>
  411. <div class="fp-quality-value bad"
  412. >{{ quality.failRate }}%</div
  413. >
  414. </div>
  415. <div class="fp-quality-item">
  416. <div class="fp-quality-label">损耗率</div>
  417. <div class="fp-quality-value warn"
  418. >{{ quality.lossRate }}%</div
  419. >
  420. </div>
  421. </div>
  422. <div class="fp-quality-bars">
  423. <div class="fp-qbar">
  424. <div class="fp-qbar-name">合格率</div>
  425. <div class="fp-qbar-track">
  426. <div
  427. class="fp-qbar-fill good"
  428. :style="{ width: quality.passRate + '%' }"
  429. ></div>
  430. </div>
  431. </div>
  432. <div class="fp-qbar">
  433. <div class="fp-qbar-name">不合格率</div>
  434. <div class="fp-qbar-track">
  435. <div
  436. class="fp-qbar-fill bad"
  437. :style="{ width: quality.failRate + '%' }"
  438. ></div>
  439. </div>
  440. </div>
  441. <div class="fp-qbar">
  442. <div class="fp-qbar-name">损耗率</div>
  443. <div class="fp-qbar-track">
  444. <div
  445. class="fp-qbar-fill warn"
  446. :style="{ width: quality.lossRate + '%' }"
  447. ></div>
  448. </div>
  449. </div>
  450. </div>
  451. </div>
  452. </chart-card>
  453. </div>
  454. </div>
  455. </div>
  456. </vue-fullscreen>
  457. </template>
  458. <script>
  459. import { component as VueFullscreen } from 'vue-fullscreen';
  460. import ChartCard from './components/ChartCard';
  461. import BarChart from './components/charts/BarChart';
  462. import Bar3DChart from './components/charts/Bar3DChart';
  463. import DeviceStatusChart from './components/charts/DeviceStatusChart';
  464. import BarLineComboChart from './components/charts/BarLineComboChart';
  465. import {
  466. getFactoryProductionDashboardData,
  467. getFactoryProductionDataByPlan,
  468. getFactoryProductionDataByStock,
  469. getFactoryProductionDataByDevice,
  470. getFactoryListByUser
  471. } from '@/api/vis/factoryProductionDashboard';
  472. const inventoryIcon = require('@/assets/png/icon.png');
  473. export default {
  474. name: 'FactoryProductionDashboard',
  475. components: {
  476. VueFullscreen,
  477. ChartCard,
  478. BarChart,
  479. Bar3DChart,
  480. DeviceStatusChart,
  481. BarLineComboChart
  482. },
  483. computed: {
  484. contentWidth() {
  485. return this.$store.state.theme.contentWidth || '100%';
  486. },
  487. inventoryLedgerDisplayData() {
  488. const sourceData =
  489. this.activeInventoryFilter === 'keeper'
  490. ? this.inventoryLedgerKeeperData
  491. : this.inventoryLedgerData;
  492. const checkedNames = this.inventoryXAxisOptions
  493. .filter((opt) => opt.checked)
  494. .map((opt) => opt.name);
  495. if (checkedNames.length === 0) return [];
  496. return sourceData.filter((item) => checkedNames.includes(item.name));
  497. },
  498. inventoryBarColors() {
  499. return (this.inventoryLedgerDisplayData || []).map(() => '#1163fb');
  500. },
  501. checkedInventoryCount() {
  502. return this.inventoryXAxisOptions.filter((opt) => opt.checked).length;
  503. },
  504. isAllInventoryChecked() {
  505. return (
  506. this.inventoryXAxisOptions.length > 0 &&
  507. this.checkedInventoryCount === this.inventoryXAxisOptions.length
  508. );
  509. },
  510. filteredPlanRows() {
  511. if (this.activePlanTab === 'all') return this.planRows;
  512. return this.planRows.filter((r) => r.type === this.activePlanTab);
  513. }
  514. },
  515. watch: {
  516. isFullscreen: {
  517. handler(val) {
  518. this.isFlag = false;
  519. this.$nextTick(() => {
  520. const delay = this._initLayout ? 160 : 0;
  521. this._initLayout = true;
  522. const run = () => {
  523. this.applyScreenSize();
  524. this.$nextTick(() => {
  525. this.isFlag = true;
  526. this.triggerChartResize();
  527. });
  528. };
  529. delay ? setTimeout(run, delay) : run();
  530. });
  531. },
  532. immediate: true
  533. },
  534. contentWidth: {
  535. handler() {
  536. clearTimeout(this.resizeTimer);
  537. this.isFlag = false;
  538. this.resizeTimer = setTimeout(() => {
  539. this.$nextTick(() => {
  540. this.applyScreenSize();
  541. this.isFlag = true;
  542. });
  543. }, 300);
  544. },
  545. immediate: true
  546. },
  547. factoryId() {
  548. this.loadDashboardData();
  549. },
  550. dateRange: {
  551. handler() {
  552. this.loadDashboardData();
  553. },
  554. deep: true
  555. },
  556. activePlanTab() {
  557. this.fetchPlanData();
  558. },
  559. activeInventoryFilter() {
  560. this.fetchInventoryData();
  561. },
  562. filteredPlanRows() {
  563. this.$nextTick(() => {
  564. this.restartPlanAutoScroll();
  565. });
  566. }
  567. },
  568. data() {
  569. return {
  570. isFullscreen: false,
  571. isFlag: false,
  572. _initLayout: false,
  573. resizeTimer: null,
  574. updateTimer: null,
  575. planAutoScrollTimer: null,
  576. date: '',
  577. time: '',
  578. week: '',
  579. inventoryIcon,
  580. factoryId: '',
  581. factoryList: [],
  582. dateRange: [],
  583. productionStats: {
  584. quantity: 0,
  585. transformQuantity: [0, 0, 0, 0, 0, 0],
  586. requiredTotal: 0,
  587. achievementRate: 0
  588. },
  589. productionKpi: {
  590. delayedCount: 0,
  591. planAchievementRate: 0,
  592. unpublishedCount: 0
  593. },
  594. fiveCategoryData: [
  595. { name: '成品', value: 0 },
  596. { name: '半成品', value: 0 },
  597. { name: '在制品', value: 0 },
  598. { name: '废品', value: 0 },
  599. { name: '返修品', value: 0 }
  600. ],
  601. fourNumberBarData: [
  602. { name: '投入数', value: 0 },
  603. { name: '产出数', value: 0 },
  604. { name: '废品数', value: 0 },
  605. { name: '周转数', value: 0 }
  606. ],
  607. inventoryLedgerData: [],
  608. inventoryLedgerKeeperData: [],
  609. activeInventoryFilter: 'category',
  610. showInventoryDropdown: false,
  611. inventoryXAxisOptions: [],
  612. deviceLedger: {
  613. categories: [],
  614. barData: [],
  615. topBarData: [],
  616. lineData: []
  617. },
  618. quality: {
  619. passRate: 0,
  620. failRate: 0,
  621. lossRate: 0
  622. },
  623. planTabs: [
  624. { key: 'all', label: '全部' },
  625. { key: 'monthly', label: '月度滚动计划' },
  626. { key: 'temporary', label: '临时生产计划' },
  627. { key: 'research', label: '分厂临时计划' }
  628. ],
  629. activePlanTab: 'all',
  630. planRows: []
  631. };
  632. },
  633. created() {
  634. this.updateTime();
  635. this.updateTimer = setInterval(this.updateTime, 1000);
  636. const now = new Date();
  637. const today = this.formatDate(now);
  638. this.dateRange = [today, today];
  639. this.getFactoryListByData();
  640. document.addEventListener('click', this.handleDocumentClick);
  641. },
  642. mounted() {
  643. this.applyScreenSize();
  644. window.addEventListener('resize', this.handleWindowResize);
  645. document.addEventListener(
  646. 'fullscreenchange',
  647. this.handleFullscreenChange
  648. );
  649. document.addEventListener(
  650. 'webkitfullscreenchange',
  651. this.handleFullscreenChange
  652. );
  653. this.$nextTick(() => {
  654. this.isFlag = true;
  655. this.startPlanAutoScroll();
  656. });
  657. },
  658. beforeDestroy() {
  659. clearInterval(this.updateTimer);
  660. clearTimeout(this.resizeTimer);
  661. this.stopPlanAutoScroll();
  662. window.removeEventListener('resize', this.handleWindowResize);
  663. document.removeEventListener(
  664. 'fullscreenchange',
  665. this.handleFullscreenChange
  666. );
  667. document.removeEventListener(
  668. 'webkitfullscreenchange',
  669. this.handleFullscreenChange
  670. );
  671. document.removeEventListener('click', this.handleDocumentClick);
  672. },
  673. methods: {
  674. handleDocumentClick() {
  675. this.showInventoryDropdown = false;
  676. },
  677. toggleAllInventoryXAxis(val) {
  678. this.inventoryXAxisOptions.forEach((item) => {
  679. item.checked = val;
  680. });
  681. this.handleInventoryXAxisChange();
  682. },
  683. handleInventoryXAxisChange() {
  684. // Force reactivity update by replacing array
  685. this.inventoryXAxisOptions = [...this.inventoryXAxisOptions];
  686. },
  687. updateInventoryXAxisOptions(data) {
  688. const newNames = data.map((item) => item.name);
  689. const updatedOptions = newNames.map((name) => {
  690. const existing = this.inventoryXAxisOptions.find(
  691. (opt) => opt.name === name
  692. );
  693. return { name, checked: existing ? existing.checked : true };
  694. });
  695. this.inventoryXAxisOptions = updatedOptions;
  696. },
  697. handleWindowResize() {
  698. clearTimeout(this.resizeTimer);
  699. this.resizeTimer = setTimeout(() => {
  700. this.applyScreenSize();
  701. }, 120);
  702. },
  703. syncOverflowTitle(event) {
  704. const el = event?.currentTarget;
  705. if (!el) return;
  706. const text = (el.innerText || '').trim();
  707. const isOverflowing = el.scrollWidth > el.clientWidth;
  708. el.title = isOverflowing ? text : '';
  709. },
  710. restartPlanAutoScroll() {
  711. this.stopPlanAutoScroll();
  712. this.startPlanAutoScroll();
  713. },
  714. startPlanAutoScroll() {
  715. this.stopPlanAutoScroll();
  716. const scrollBody = this.$refs.planScrollBody;
  717. if (!scrollBody) return;
  718. if (scrollBody.scrollHeight <= scrollBody.clientHeight + 2) {
  719. scrollBody.scrollTop = 0;
  720. return;
  721. }
  722. this.planAutoScrollTimer = setInterval(() => {
  723. const maxScrollTop =
  724. scrollBody.scrollHeight - scrollBody.clientHeight;
  725. if (maxScrollTop <= 0) return;
  726. if (scrollBody.scrollTop >= maxScrollTop - 1) {
  727. scrollBody.scrollTop = 0;
  728. return;
  729. }
  730. scrollBody.scrollTop += 1;
  731. }, 40);
  732. },
  733. stopPlanAutoScroll() {
  734. if (this.planAutoScrollTimer) {
  735. clearInterval(this.planAutoScrollTimer);
  736. this.planAutoScrollTimer = null;
  737. }
  738. },
  739. handleFullscreenChange() {
  740. this.isFlag = false;
  741. this.$nextTick(() => {
  742. setTimeout(() => {
  743. this.applyScreenSize();
  744. this.$nextTick(() => {
  745. this.isFlag = true;
  746. this.triggerChartResize();
  747. });
  748. }, 150);
  749. });
  750. },
  751. async getFactoryListByData() {
  752. const par = {
  753. type: 1,
  754. size: 9999
  755. };
  756. await getFactoryListByUser(par).then((res) => {
  757. if (res.list && res.list.length > 0) {
  758. this.factoryList = res.list.map((el) => {
  759. return {
  760. id: el.id,
  761. name: el.name
  762. };
  763. });
  764. }
  765. });
  766. this.factoryId = this.$store.state.user.info.factoryId
  767. ? this.$store.state.user.info.factoryId
  768. : this.factoryList.length
  769. ? this.factoryList[0].id
  770. : '';
  771. },
  772. triggerChartResize() {
  773. this.$nextTick(() => {
  774. window.dispatchEvent(new Event('resize'));
  775. });
  776. },
  777. applyScreenSize() {
  778. const isFs = !!(
  779. document.fullscreenElement || document.webkitFullscreenElement
  780. );
  781. let deviceWidth;
  782. let deviceHeight;
  783. if (isFs) {
  784. deviceWidth =
  785. window.innerWidth || document.documentElement.clientWidth;
  786. deviceHeight =
  787. window.innerHeight || document.documentElement.clientHeight;
  788. } else {
  789. deviceWidth = document.documentElement.clientWidth;
  790. deviceHeight = document.documentElement.clientHeight;
  791. }
  792. const eleAdminHeaderHeight =
  793. (!isFs &&
  794. document.getElementsByClassName('ele-admin-header')[0]
  795. ?.offsetHeight) ||
  796. 0;
  797. const eleAdminSidebarWidth =
  798. (!isFs &&
  799. document.getElementsByClassName('ele-admin-sidebar')[0]
  800. ?.offsetWidth) ||
  801. 0;
  802. const eleAdminTabsHeight =
  803. (!isFs &&
  804. document.getElementsByClassName('ele-admin-tabs')[0]
  805. ?.offsetHeight) ||
  806. 0;
  807. const h = isFs
  808. ? deviceHeight
  809. : deviceHeight - eleAdminHeaderHeight - eleAdminTabsHeight;
  810. const w = isFs ? deviceWidth : deviceWidth - eleAdminSidebarWidth;
  811. const setContainerSize = (item) => {
  812. item.style.height = h + 'px';
  813. item.style.width = w + 'px';
  814. };
  815. const containers = [...document.getElementsByClassName('fp-container')];
  816. containers.forEach(setContainerSize);
  817. document.documentElement.style.fontSize = isFs ? '24px' : '16px';
  818. },
  819. onFullscreen() {
  820. this.isFullscreen = !this.isFullscreen;
  821. },
  822. setInventoryFilter(type) {
  823. if (this.activeInventoryFilter === type) return;
  824. this.activeInventoryFilter = type;
  825. },
  826. updateTime() {
  827. const now = new Date();
  828. const pad = (n) => n.toString().padStart(2, '0');
  829. this.time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(
  830. now.getSeconds()
  831. )}`;
  832. this.date = `${now.getFullYear()}年${
  833. now.getMonth() + 1
  834. }月${now.getDate()}日`;
  835. const weekMap = {
  836. 0: '日',
  837. 1: '一',
  838. 2: '二',
  839. 3: '三',
  840. 4: '四',
  841. 5: '五',
  842. 6: '六'
  843. };
  844. this.week = `星期${weekMap[now.getDay()]}`;
  845. },
  846. formatDate(d) {
  847. const pad = (n) => n.toString().padStart(2, '0');
  848. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
  849. d.getDate()
  850. )}`;
  851. },
  852. formatNumber6(num) {
  853. const paddedStr = String(num ?? 0).padStart(6, '0');
  854. return paddedStr.split('').map((c) => Number(c));
  855. },
  856. normalizeNumber(value) {
  857. const num = Number(value);
  858. return Number.isFinite(num) ? num : 0;
  859. },
  860. normalizePercent(value) {
  861. if (value === null || value === undefined || value === '') return 0;
  862. const text = String(value).replace('%', '').trim();
  863. const num = Number(text);
  864. return Number.isFinite(num) ? Number(num.toFixed(2)) : 0;
  865. },
  866. normalizeNameValueList(list) {
  867. if (!Array.isArray(list)) return [];
  868. return list.map((item) => ({
  869. name: item?.name || '--',
  870. value: this.normalizeNumber(item?.value)
  871. }));
  872. },
  873. getBaseParams(extra = {}) {
  874. if (!this.factoryId) return null;
  875. const [rangeStart, rangeEnd] = this.dateRange || [];
  876. const today = this.formatDate(new Date());
  877. const startTime = rangeStart || today;
  878. const endTime = rangeEnd || startTime;
  879. return {
  880. factoryId: this.factoryId,
  881. startTime,
  882. endTime,
  883. ...extra
  884. };
  885. },
  886. getInventoryStockType(filterType = this.activeInventoryFilter) {
  887. return filterType === 'keeper' ? 2 : 1;
  888. },
  889. mapPlanTabType(tab = this.activePlanTab) {
  890. const tabTypeMap = {
  891. all: 0,
  892. monthly: 1,
  893. temporary: 2,
  894. research: 3
  895. };
  896. return tabTypeMap[tab] ?? 0;
  897. },
  898. mapPlanStatus(status) {
  899. const statusMap = {
  900. 1: '待排产',
  901. 2: '待发布',
  902. 3: '发布失败',
  903. 4: '待生产',
  904. 5: '进行中',
  905. 6: '已完成',
  906. 7: '延期',
  907. 8: '待下达'
  908. };
  909. return statusMap[Number(status)] || '--';
  910. },
  911. formatPlanDate(value) {
  912. if (!value) return '--';
  913. const text = String(value).replace('T', ' ');
  914. return text.slice(0, 10);
  915. },
  916. mapPlanRow(row, planTab = this.activePlanTab) {
  917. const status = Number(row?.status || 0);
  918. return {
  919. planNo: row?.code || '--',
  920. productName: row?.productName || '--',
  921. batchNo: row?.batchNo || '--',
  922. planQty: this.normalizeNumber(row?.planQuantity),
  923. currentProcess: row?.produceRoutingName || '--',
  924. planStart: this.formatPlanDate(row?.plannedStartTime),
  925. planEnd: this.formatPlanDate(row?.plannedEndTime),
  926. isDelayed: status === 7,
  927. statusText: this.mapPlanStatus(status),
  928. type: planTab
  929. };
  930. },
  931. buildDeviceLedger(list) {
  932. const normalized = Array.isArray(list)
  933. ? list.map((item) => {
  934. const total = this.normalizeNumber(item?.value);
  935. const occupied = this.normalizeNumber(
  936. item?.value2 !== undefined ? item?.value2 : item?.value
  937. );
  938. return {
  939. name: item?.name || '--',
  940. total,
  941. occupied
  942. };
  943. })
  944. : [];
  945. return {
  946. categories: normalized.map((item) => item.name),
  947. barData: normalized.map((item) => item.occupied),
  948. topBarData: normalized.map((item) => item.total),
  949. lineData: normalized.map((item) =>
  950. item.total
  951. ? Number(((item.occupied / item.total) * 100).toFixed(2))
  952. : 0
  953. )
  954. };
  955. },
  956. applyDashboardSummary(data = {}) {
  957. const quantity = this.normalizeNumber(data?.quantity);
  958. this.productionStats.quantity = quantity;
  959. this.productionStats.transformQuantity = this.formatNumber6(quantity);
  960. this.productionStats.requiredTotal = this.normalizeNumber(
  961. data?.totalQuantity
  962. );
  963. this.productionStats.achievementRate = this.normalizePercent(
  964. data?.quantityAchievementRate
  965. );
  966. this.productionKpi.delayedCount = this.normalizeNumber(
  967. data?.delayNumber
  968. );
  969. this.productionKpi.planAchievementRate = this.normalizePercent(
  970. data?.achievementRate
  971. );
  972. this.productionKpi.unpublishedCount = this.normalizeNumber(
  973. data?.numberToBeProduced
  974. );
  975. this.fiveCategoryData = [
  976. {
  977. name: '成品',
  978. value: this.normalizeNumber(data?.finishedProductQuantity)
  979. },
  980. {
  981. name: '半成品',
  982. value: this.normalizeNumber(data?.semiFinishedProductQuantity)
  983. },
  984. {
  985. name: '在制品',
  986. value: this.normalizeNumber(data?.workInProgressQuantity)
  987. },
  988. {
  989. name: '废品',
  990. value: this.normalizeNumber(data?.scrapProductQuantity)
  991. },
  992. {
  993. name: '返修品',
  994. value: this.normalizeNumber(data?.reworkProductQuantity)
  995. }
  996. ];
  997. this.fourNumberBarData = [
  998. { name: '投入数', value: this.normalizeNumber(data?.inputQuantity) },
  999. { name: '产出数', value: this.normalizeNumber(data?.outputQuantity) },
  1000. { name: '废品数', value: this.normalizeNumber(data?.scrapQuantity) },
  1001. {
  1002. name: '周转数',
  1003. value: this.normalizeNumber(data?.turnoverQuantity)
  1004. }
  1005. ];
  1006. this.quality.passRate = this.normalizePercent(data?.passRate);
  1007. this.quality.failRate = this.normalizePercent(data?.defectiveRate);
  1008. this.quality.lossRate = this.normalizePercent(data?.attritionRate);
  1009. },
  1010. async loadDashboardData() {
  1011. const baseParams = this.getBaseParams();
  1012. if (!baseParams) return;
  1013. await Promise.all([
  1014. this.fetchSummaryData(baseParams),
  1015. this.fetchInventoryData(baseParams),
  1016. this.fetchDeviceData(baseParams),
  1017. this.fetchPlanData(baseParams)
  1018. ]);
  1019. },
  1020. async fetchSummaryData(baseParams = this.getBaseParams()) {
  1021. if (!baseParams) return;
  1022. try {
  1023. const data = await getFactoryProductionDashboardData(baseParams);
  1024. this.applyDashboardSummary(data || {});
  1025. } catch (error) {
  1026. console.error('获取工厂生产综合看板汇总数据失败:', error);
  1027. this.applyDashboardSummary({});
  1028. }
  1029. },
  1030. async fetchInventoryData(
  1031. baseParams = this.getBaseParams(),
  1032. filterType = this.activeInventoryFilter
  1033. ) {
  1034. if (!baseParams) return;
  1035. try {
  1036. const list = await getFactoryProductionDataByStock({
  1037. ...baseParams,
  1038. stockType: this.getInventoryStockType(filterType)
  1039. });
  1040. if (filterType !== this.activeInventoryFilter) return;
  1041. const normalized = this.normalizeNameValueList(list);
  1042. if (filterType === 'keeper') {
  1043. this.inventoryLedgerKeeperData = normalized;
  1044. } else {
  1045. this.inventoryLedgerData = normalized;
  1046. }
  1047. this.updateInventoryXAxisOptions(normalized);
  1048. } catch (error) {
  1049. console.error('获取库存台账数据失败:', error);
  1050. if (filterType === 'keeper') {
  1051. this.inventoryLedgerKeeperData = [];
  1052. } else {
  1053. this.inventoryLedgerData = [];
  1054. }
  1055. }
  1056. },
  1057. async fetchDeviceData(baseParams = this.getBaseParams()) {
  1058. if (!baseParams) return;
  1059. try {
  1060. const list = await getFactoryProductionDataByDevice(baseParams);
  1061. this.deviceLedger = this.buildDeviceLedger(list);
  1062. } catch (error) {
  1063. console.error('获取各类型设备台账数据失败:', error);
  1064. this.deviceLedger = this.buildDeviceLedger([]);
  1065. }
  1066. },
  1067. async fetchPlanData(
  1068. baseParams = this.getBaseParams(),
  1069. planTab = this.activePlanTab
  1070. ) {
  1071. if (!baseParams) return;
  1072. try {
  1073. const list = await getFactoryProductionDataByPlan({
  1074. ...baseParams,
  1075. type: this.mapPlanTabType(planTab)
  1076. });
  1077. if (planTab !== this.activePlanTab) return;
  1078. this.planRows = Array.isArray(list)
  1079. ? list.map((item) => this.mapPlanRow(item, planTab))
  1080. : [];
  1081. } catch (error) {
  1082. console.error('获取生产计划进度数据失败:', error);
  1083. if (planTab === this.activePlanTab) {
  1084. this.planRows = [];
  1085. }
  1086. }
  1087. }
  1088. }
  1089. };
  1090. </script>
  1091. <style lang="scss" scoped>
  1092. [v-cloak] {
  1093. display: none;
  1094. }
  1095. .fp-container {
  1096. font-size: 16px;
  1097. font-family: 'AlibabaPuHuiTi';
  1098. background-image: url('@/assets/png/bj.png');
  1099. background-repeat: no-repeat;
  1100. background-size: 100% 100%;
  1101. background-color: rgb(8, 29, 65);
  1102. height: 100%;
  1103. overflow: hidden;
  1104. display: flex;
  1105. flex-direction: column;
  1106. min-height: 0;
  1107. }
  1108. .fp-header {
  1109. background-image: url('@/assets/border2.png');
  1110. background-repeat: no-repeat;
  1111. background-size: 100% 100%;
  1112. height: 8.5%;
  1113. min-height: 64px;
  1114. max-height: 72px;
  1115. display: flex;
  1116. align-items: center;
  1117. justify-content: space-between;
  1118. padding: 0 14px;
  1119. box-sizing: border-box;
  1120. }
  1121. .fp-header-left {
  1122. width: 24%;
  1123. display: flex;
  1124. gap: 8px;
  1125. align-items: center;
  1126. justify-content: flex-start;
  1127. padding: 0;
  1128. margin-top: 1%;
  1129. }
  1130. .fp-select {
  1131. width: 36%;
  1132. }
  1133. .fp-date {
  1134. width: 64%;
  1135. }
  1136. /* 顶部筛选控件皮肤,贴近原始大屏样式 */
  1137. ::v-deep .fp-header-left .el-input__inner,
  1138. ::v-deep .fp-header-left .el-range-editor.el-input__inner {
  1139. background-color: #011944;
  1140. border: 1px solid #0b6fd5;
  1141. color: #e9f3ff;
  1142. height: 28px;
  1143. line-height: 28px;
  1144. border-radius: 2px;
  1145. box-shadow: 0 0 4px rgba(11, 109, 213, 0.45);
  1146. }
  1147. ::v-deep .fp-header-left .el-input__inner::placeholder,
  1148. ::v-deep .fp-header-left .el-range-input::placeholder {
  1149. color: #6f8fb8;
  1150. }
  1151. ::v-deep .fp-header-left .el-input__suffix,
  1152. ::v-deep .fp-header-left .el-range__icon,
  1153. ::v-deep .fp-header-left .el-range-separator {
  1154. color: #9fc5ff;
  1155. }
  1156. ::v-deep .fp-header-left .el-range-input {
  1157. background-color: transparent;
  1158. color: #e9f3ff;
  1159. }
  1160. .fp-title {
  1161. flex: 1;
  1162. min-width: 0;
  1163. text-align: center;
  1164. font-family: '优设标题黑';
  1165. color: #fff;
  1166. font-size: clamp(2rem, 2.45vw, 2.7rem);
  1167. letter-spacing: clamp(0.16rem, 0.28vw, 0.4rem);
  1168. line-height: 1.08;
  1169. white-space: nowrap;
  1170. overflow: hidden;
  1171. text-overflow: clip;
  1172. transform: none;
  1173. }
  1174. .fp-header-right {
  1175. width: 24%;
  1176. display: flex;
  1177. justify-content: flex-end;
  1178. align-items: center;
  1179. gap: 12px;
  1180. margin-top: 1%;
  1181. color: #7fa7ce;
  1182. font-size: 0.8rem;
  1183. }
  1184. .fp-time {
  1185. color: rgb(210 215 221);
  1186. font-weight: 600;
  1187. }
  1188. .fp-time-text {
  1189. padding: 0 6px;
  1190. }
  1191. .fp-fullscreen-toggle {
  1192. cursor: pointer;
  1193. }
  1194. .fp-body {
  1195. flex: 1;
  1196. display: flex;
  1197. gap: 0.6rem;
  1198. padding: 0.55rem 0.75rem 0.75rem;
  1199. box-sizing: border-box;
  1200. min-height: 0;
  1201. overflow: hidden;
  1202. }
  1203. .fp-col {
  1204. display: flex;
  1205. flex-direction: column;
  1206. gap: 0.8rem;
  1207. min-height: 0;
  1208. }
  1209. .fp-col-left,
  1210. .fp-col-right {
  1211. width: 24.5%;
  1212. min-width: 0;
  1213. }
  1214. .fp-col-middle {
  1215. flex: 1;
  1216. min-width: 0;
  1217. }
  1218. .fp-chart-box {
  1219. flex: 1;
  1220. min-height: 0;
  1221. padding: 6px 10px 10px;
  1222. box-sizing: border-box;
  1223. }
  1224. .fp-chart-fill {
  1225. width: 100%;
  1226. height: 100%;
  1227. }
  1228. /* 库存台账:自定义卡片,标题右侧带筛选 */
  1229. .fp-inventory-card {
  1230. flex: 0.8 !important;
  1231. background-color: rgba(8, 29, 65, 0.8);
  1232. border: 1px solid #124c77;
  1233. overflow: visible;
  1234. display: flex;
  1235. flex-direction: column;
  1236. min-height: 0;
  1237. }
  1238. .fp-inventory-header {
  1239. display: flex;
  1240. align-items: center;
  1241. padding: 0.2rem 0.5rem;
  1242. gap: 0.3rem;
  1243. background-color: rgb(0, 64, 133);
  1244. border-bottom: 1px solid #124c77;
  1245. font-family: '优设标题黑';
  1246. font-size: 1.2rem;
  1247. color: #fff;
  1248. flex-shrink: 0;
  1249. }
  1250. .fp-inventory-header .card-title {
  1251. flex: 1;
  1252. font-size: clamp(1rem, 1.8vw, 1.4rem);
  1253. }
  1254. .fp-inventory-filters {
  1255. display: flex;
  1256. align-items: center;
  1257. gap: 8px;
  1258. font-size: 0.75rem;
  1259. color: #9fc5ff;
  1260. }
  1261. .fp-filter-btn {
  1262. cursor: pointer;
  1263. padding: 2px 6px;
  1264. border: 1px solid rgba(18, 76, 119, 0.8);
  1265. border-radius: 2px;
  1266. white-space: nowrap;
  1267. }
  1268. .fp-filter-btn:hover {
  1269. color: #3bfff2;
  1270. border-color: rgba(20, 205, 201, 0.6);
  1271. }
  1272. .fp-filter-btn.active {
  1273. color: #3bfff2;
  1274. background: linear-gradient(
  1275. 180deg,
  1276. rgba(20, 205, 201, 0.26),
  1277. rgba(17, 99, 251, 0.2)
  1278. );
  1279. border-color: rgba(20, 205, 201, 0.85);
  1280. box-shadow: 0 0 8px rgba(20, 205, 201, 0.35);
  1281. }
  1282. .fp-filter-icon {
  1283. font-size: 1rem;
  1284. cursor: pointer;
  1285. }
  1286. .fp-inventory-card .card-content {
  1287. overflow: hidden;
  1288. flex: 1;
  1289. min-height: 0;
  1290. }
  1291. .fp-inventory-header {
  1292. position: relative;
  1293. z-index: 10;
  1294. }
  1295. .fp-inventory-dropdown {
  1296. position: absolute;
  1297. top: calc(100% + 6px);
  1298. right: 0;
  1299. min-width: 160px;
  1300. background: rgba(1, 25, 68, 0.96);
  1301. border: 1px solid rgba(11, 111, 213, 0.8);
  1302. border-radius: 2px;
  1303. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.6), 0 0 6px rgba(11, 111, 213, 0.3);
  1304. z-index: 1000;
  1305. overflow: hidden;
  1306. }
  1307. .fp-dropdown-header {
  1308. padding: 8px 12px;
  1309. background: rgba(11, 111, 213, 0.25);
  1310. border-bottom: 1px solid rgba(11, 111, 213, 0.4);
  1311. color: #e9f3ff;
  1312. font-size: 0.8rem;
  1313. display: flex;
  1314. align-items: center;
  1315. gap: 4px;
  1316. }
  1317. .fp-dropdown-count {
  1318. color: #9fc5ff;
  1319. font-size: 0.75rem;
  1320. margin-left: auto;
  1321. }
  1322. .fp-dropdown-content {
  1323. max-height: 280px;
  1324. overflow-y: auto;
  1325. padding: 2px 0;
  1326. }
  1327. .fp-dropdown-content::-webkit-scrollbar {
  1328. width: 6px;
  1329. }
  1330. .fp-dropdown-content::-webkit-scrollbar-track {
  1331. background: rgba(3, 29, 66, 0.5);
  1332. }
  1333. .fp-dropdown-content::-webkit-scrollbar-thumb {
  1334. background: rgba(11, 111, 213, 0.6);
  1335. border-radius: 3px;
  1336. }
  1337. .fp-dropdown-item {
  1338. padding: 6px 12px;
  1339. cursor: pointer;
  1340. transition: background-color 0.15s;
  1341. border-bottom: 1px solid rgba(11, 111, 213, 0.1);
  1342. }
  1343. .fp-dropdown-item:last-child {
  1344. border-bottom: none;
  1345. }
  1346. .fp-dropdown-item:hover {
  1347. background-color: rgba(11, 111, 213, 0.15);
  1348. }
  1349. ::v-deep .fp-dropdown-item .el-checkbox,
  1350. ::v-deep .fp-dropdown-header .el-checkbox {
  1351. display: flex;
  1352. align-items: center;
  1353. width: 100%;
  1354. }
  1355. ::v-deep .fp-dropdown-item .el-checkbox__label,
  1356. ::v-deep .fp-dropdown-header .el-checkbox__label {
  1357. color: #d4e4ff;
  1358. font-size: 0.78rem;
  1359. padding-left: 6px;
  1360. }
  1361. ::v-deep .fp-dropdown-item .el-checkbox__input.is-checked .el-checkbox__inner,
  1362. ::v-deep
  1363. .fp-dropdown-header
  1364. .el-checkbox__input.is-checked
  1365. .el-checkbox__inner {
  1366. background-color: #0b6fd5;
  1367. border-color: #0b6fd5;
  1368. }
  1369. ::v-deep
  1370. .fp-dropdown-item
  1371. .el-checkbox__input.is-checked
  1372. + .el-checkbox__label,
  1373. ::v-deep
  1374. .fp-dropdown-header
  1375. .el-checkbox__input.is-checked
  1376. + .el-checkbox__label {
  1377. color: #fff;
  1378. }
  1379. ::v-deep .fp-dropdown-item .el-checkbox__inner,
  1380. ::v-deep .fp-dropdown-header .el-checkbox__inner {
  1381. width: 14px;
  1382. height: 14px;
  1383. background-color: rgba(1, 25, 68, 0.6);
  1384. border-color: rgba(11, 111, 213, 0.6);
  1385. }
  1386. ::v-deep .fp-dropdown-item .el-checkbox__inner:hover,
  1387. ::v-deep .fp-dropdown-header .el-checkbox__inner:hover {
  1388. border-color: #3bfff2;
  1389. }
  1390. /* left number card */
  1391. .fp-number-card {
  1392. height: 100%;
  1393. min-width: 0;
  1394. padding: 0 10px 10px;
  1395. display: flex;
  1396. flex-direction: column;
  1397. gap: 10px;
  1398. box-sizing: border-box;
  1399. }
  1400. .fp-number-display {
  1401. flex: 1;
  1402. min-width: 0;
  1403. display: flex;
  1404. align-items: center;
  1405. justify-content: center;
  1406. gap: 0.3rem;
  1407. }
  1408. .fp-unit-box {
  1409. display: flex;
  1410. flex-direction: column;
  1411. align-items: center;
  1412. flex: 1 1 0;
  1413. min-width: 1.2rem;
  1414. max-width: 3rem;
  1415. height: 5rem;
  1416. position: relative;
  1417. }
  1418. .fp-unit {
  1419. font-size: 14px;
  1420. color: #a4b9d7;
  1421. position: absolute;
  1422. top: -22px;
  1423. left: 50%;
  1424. transform: translateX(-50%);
  1425. width: 35px;
  1426. white-space: nowrap;
  1427. }
  1428. .fp-digit-box {
  1429. background-image: url('@/assets/png/num_bj.png');
  1430. background-repeat: no-repeat;
  1431. background-size: 100% 100%;
  1432. color: #fff;
  1433. padding: 0 0.4rem;
  1434. height: 5rem;
  1435. font-size: clamp(1rem, 3.5vw, 2.2rem);
  1436. display: flex;
  1437. align-items: center;
  1438. justify-content: center;
  1439. font-weight: bold;
  1440. border-radius: 2px;
  1441. width: 100%;
  1442. box-sizing: border-box;
  1443. }
  1444. .fp-separator {
  1445. flex-shrink: 0;
  1446. color: #fff;
  1447. font-size: clamp(1.2rem, 3vw, 2.5rem);
  1448. font-weight: 700;
  1449. margin: 0 2px;
  1450. display: flex;
  1451. align-items: center;
  1452. }
  1453. .fp-mini-stats {
  1454. display: flex;
  1455. gap: 10px;
  1456. }
  1457. .fp-mini-stat {
  1458. flex: 1;
  1459. display: flex;
  1460. gap: 8px;
  1461. padding: 6px 8px;
  1462. border: 1px solid rgba(88, 137, 196, 0.9);
  1463. background-color: rgba(0, 123, 255, 0.1);
  1464. border-radius: 4px;
  1465. align-items: center;
  1466. min-width: 0;
  1467. }
  1468. .fp-mini-icon {
  1469. width: 1.6rem;
  1470. height: 1.6rem;
  1471. padding: 0.35rem;
  1472. background-color: rgba(0, 64, 133, 0.3);
  1473. display: flex;
  1474. align-items: center;
  1475. justify-content: center;
  1476. flex-shrink: 0;
  1477. }
  1478. .fp-mini-icon img {
  1479. width: 100%;
  1480. height: 100%;
  1481. object-fit: contain;
  1482. }
  1483. .fp-mini-info {
  1484. min-width: 0;
  1485. display: flex;
  1486. flex-direction: column;
  1487. align-items: center;
  1488. flex: 1;
  1489. }
  1490. .fp-mini-label {
  1491. color: #fff;
  1492. font-size: 0.75rem;
  1493. letter-spacing: 1px;
  1494. white-space: nowrap;
  1495. }
  1496. .fp-mini-value {
  1497. color: #3bfff2;
  1498. font-weight: bold;
  1499. font-size: 0.75rem;
  1500. white-space: nowrap;
  1501. }
  1502. /* middle top */
  1503. .fp-middle-top {
  1504. border: 1px solid #031a64;
  1505. background: radial-gradient(
  1506. circle at top,
  1507. rgba(50, 197, 225, 0.22),
  1508. transparent 55%
  1509. )
  1510. rgba(5, 18, 86, 0.95);
  1511. height: 21%;
  1512. min-height: 118px;
  1513. display: flex;
  1514. flex-direction: column;
  1515. padding: 8px 12px 10px;
  1516. box-sizing: border-box;
  1517. }
  1518. .fp-middle-title {
  1519. display: flex;
  1520. align-items: center;
  1521. gap: 8px;
  1522. color: #fff;
  1523. font-family: '优设标题黑';
  1524. font-size: 1.2rem;
  1525. }
  1526. .fp-title-icon {
  1527. width: 18px;
  1528. height: 18px;
  1529. }
  1530. .fp-kpis {
  1531. flex: 1;
  1532. display: grid;
  1533. grid-template-columns: repeat(3, 1fr);
  1534. gap: 14px;
  1535. align-items: center;
  1536. padding-top: 6px;
  1537. }
  1538. .fp-kpi {
  1539. height: 100%;
  1540. display: flex;
  1541. flex-direction: column;
  1542. align-items: center;
  1543. justify-content: flex-start;
  1544. color: #fff;
  1545. gap: 4px;
  1546. position: relative;
  1547. overflow: hidden;
  1548. padding-top: 6px;
  1549. }
  1550. .fp-kpi-screen {
  1551. width: 66%;
  1552. min-height: 3.5rem;
  1553. border: 1px solid rgba(43, 150, 255, 0.6);
  1554. border-radius: 6px;
  1555. background: linear-gradient(
  1556. 180deg,
  1557. rgba(118, 255, 255, 0.22),
  1558. rgba(40, 176, 255, 0.26) 54%,
  1559. rgba(10, 36, 121, 0.36)
  1560. );
  1561. box-shadow: 0 0 20px rgba(43, 202, 255, 0.4),
  1562. 0 0 12px rgba(17, 99, 251, 0.28) inset;
  1563. display: flex;
  1564. align-items: center;
  1565. justify-content: center;
  1566. position: relative;
  1567. transform: perspective(280px) rotateX(8deg) skewX(-3deg);
  1568. transform-origin: center bottom;
  1569. }
  1570. .fp-kpi-screen::before,
  1571. .fp-kpi-screen::after {
  1572. content: '';
  1573. position: absolute;
  1574. bottom: -8px;
  1575. width: 24%;
  1576. height: 10px;
  1577. border-bottom: 2px solid rgba(43, 202, 255, 0.62);
  1578. }
  1579. .fp-kpi-screen::before {
  1580. left: 3%;
  1581. transform: skewX(-26deg);
  1582. }
  1583. .fp-kpi-screen::after {
  1584. right: 3%;
  1585. transform: skewX(26deg);
  1586. }
  1587. .fp-kpi-base {
  1588. width: 44%;
  1589. height: 0.32rem;
  1590. border-radius: 999px;
  1591. background: linear-gradient(
  1592. 180deg,
  1593. rgba(142, 255, 255, 0.98),
  1594. rgba(22, 238, 255, 0.95) 48%,
  1595. rgba(17, 99, 251, 0.82)
  1596. );
  1597. box-shadow: 0 0 16px rgba(22, 238, 255, 0.72),
  1598. 0 0 8px rgba(17, 99, 251, 0.45);
  1599. }
  1600. .fp-kpi-value {
  1601. font-family: 'LCD2B';
  1602. font-size: clamp(1.45rem, 3vw, 2.5rem);
  1603. font-weight: 800;
  1604. letter-spacing: 0.06em;
  1605. }
  1606. .fp-kpi-label {
  1607. font-size: 0.92rem;
  1608. color: #cfe4ff;
  1609. letter-spacing: 0.5px;
  1610. margin-top: 4px;
  1611. }
  1612. .fp-kpi-glow {
  1613. text-shadow: 0 0 8px rgba(185, 255, 255, 0.8),
  1614. 0 0 16px rgba(20, 205, 201, 0.65), 0 0 24px rgba(17, 99, 251, 0.42);
  1615. }
  1616. .fp-middle-note {
  1617. font-size: 0.75rem;
  1618. color: rgba(255, 255, 255, 0.85);
  1619. letter-spacing: 0.12rem;
  1620. text-align: right;
  1621. }
  1622. /* table */
  1623. .fp-table-wrap {
  1624. height: 100%;
  1625. display: flex;
  1626. flex-direction: column;
  1627. padding: 4px 8px 8px;
  1628. box-sizing: border-box;
  1629. min-height: 0;
  1630. }
  1631. .fp-table-tabs {
  1632. display: flex;
  1633. gap: 6px;
  1634. padding: 4px 0 8px;
  1635. flex-wrap: wrap;
  1636. }
  1637. .fp-tab {
  1638. padding: 6px 10px;
  1639. border: 1px solid rgba(18, 76, 119, 0.9);
  1640. background-color: rgba(3, 29, 66, 0.6);
  1641. color: #e9ecef;
  1642. border-radius: 4px;
  1643. cursor: pointer;
  1644. font-size: 0.85rem;
  1645. user-select: none;
  1646. }
  1647. .fp-tab.active {
  1648. border-color: rgba(20, 205, 201, 0.95);
  1649. color: #3bfff2;
  1650. box-shadow: 0 0 10px rgba(20, 205, 201, 0.2);
  1651. }
  1652. .fp-table {
  1653. flex: 1;
  1654. min-height: 0;
  1655. display: flex;
  1656. flex-direction: column;
  1657. }
  1658. .fp-main-table {
  1659. width: 100%;
  1660. border-collapse: collapse;
  1661. table-layout: fixed;
  1662. }
  1663. .fp-main-table thead th {
  1664. background-color: rgba(3, 29, 66, 0.95);
  1665. padding: 0.55rem 0.2rem;
  1666. text-align: center;
  1667. font-weight: bold;
  1668. color: #0b6fd5;
  1669. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  1670. font-size: clamp(0.74rem, 0.9vw, 0.82rem);
  1671. line-height: 1.15;
  1672. white-space: normal;
  1673. overflow: hidden;
  1674. text-overflow: clip;
  1675. word-break: keep-all;
  1676. }
  1677. .fp-scroll-body {
  1678. flex: 1;
  1679. min-height: 0;
  1680. overflow: hidden;
  1681. }
  1682. .fp-cell {
  1683. padding: 0.35rem 0.2rem;
  1684. text-align: center;
  1685. color: #e9ecef;
  1686. font-size: 0.82rem;
  1687. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  1688. white-space: nowrap;
  1689. overflow: hidden;
  1690. text-overflow: ellipsis;
  1691. }
  1692. .fp-row:hover .fp-cell {
  1693. background-color: rgba(3, 29, 66, 0.8) !important;
  1694. }
  1695. .fp-empty {
  1696. text-align: center;
  1697. padding: 1rem;
  1698. color: #7fa7ce;
  1699. }
  1700. .fp-status-red {
  1701. color: #fff;
  1702. font-weight: 700;
  1703. }
  1704. .fp-status-blue {
  1705. color: #ffd16c;
  1706. font-weight: 700;
  1707. }
  1708. /* quality */
  1709. .fp-quality {
  1710. height: 100%;
  1711. padding: 8px 10px 10px;
  1712. box-sizing: border-box;
  1713. display: flex;
  1714. flex-direction: column;
  1715. gap: 12px;
  1716. perspective: 700px;
  1717. }
  1718. .fp-quality-top {
  1719. display: grid;
  1720. grid-template-columns: repeat(3, 1fr);
  1721. gap: 10px;
  1722. }
  1723. .fp-quality-item {
  1724. border: 1px solid rgba(18, 76, 119, 0.9);
  1725. border-radius: 6px;
  1726. background: linear-gradient(
  1727. 180deg,
  1728. rgba(22, 120, 201, 0.18),
  1729. rgba(3, 29, 66, 0.72) 62%,
  1730. rgba(2, 21, 54, 0.9)
  1731. );
  1732. padding: 8px 10px;
  1733. display: flex;
  1734. flex-direction: column;
  1735. align-items: center;
  1736. gap: 6px;
  1737. position: relative;
  1738. transform: perspective(260px) rotateX(8deg);
  1739. transform-origin: center bottom;
  1740. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.22),
  1741. 0 0 12px rgba(18, 129, 214, 0.22) inset;
  1742. overflow: hidden;
  1743. }
  1744. .fp-quality-item::before {
  1745. content: '';
  1746. position: absolute;
  1747. top: 0;
  1748. left: 8%;
  1749. right: 8%;
  1750. height: 1px;
  1751. background: linear-gradient(
  1752. 90deg,
  1753. rgba(255, 255, 255, 0),
  1754. rgba(153, 245, 255, 0.95),
  1755. rgba(255, 255, 255, 0)
  1756. );
  1757. }
  1758. .fp-quality-label {
  1759. color: #cfe4ff;
  1760. font-size: 0.85rem;
  1761. letter-spacing: 1px;
  1762. }
  1763. .fp-quality-value {
  1764. font-family: 'LCD2B';
  1765. font-size: clamp(1.25rem, 2.1vw, 1.6rem);
  1766. font-weight: 800;
  1767. text-shadow: 0 0 8px rgba(111, 233, 255, 0.35),
  1768. 0 0 14px rgba(17, 99, 251, 0.28);
  1769. }
  1770. .fp-quality-value.good {
  1771. color: #14cdc9;
  1772. }
  1773. .fp-quality-value.bad {
  1774. color: #ff4d4f;
  1775. }
  1776. .fp-quality-value.warn {
  1777. color: #f9bb19;
  1778. }
  1779. .fp-quality-bars {
  1780. flex: 1;
  1781. display: flex;
  1782. flex-direction: column;
  1783. justify-content: center;
  1784. gap: 10px;
  1785. }
  1786. .fp-qbar {
  1787. display: flex;
  1788. gap: 10px;
  1789. align-items: center;
  1790. }
  1791. .fp-qbar-name {
  1792. width: 72px;
  1793. color: #e9ecef;
  1794. font-size: 0.85rem;
  1795. }
  1796. .fp-qbar-track {
  1797. flex: 1;
  1798. height: 10px;
  1799. border-radius: 999px;
  1800. background: linear-gradient(
  1801. 180deg,
  1802. rgba(6, 34, 82, 0.92),
  1803. rgba(2, 25, 61, 0.88)
  1804. );
  1805. border: 1px solid rgba(18, 76, 119, 0.9);
  1806. overflow: hidden;
  1807. position: relative;
  1808. box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3) inset;
  1809. }
  1810. .fp-qbar-track::before {
  1811. content: '';
  1812. position: absolute;
  1813. left: 4px;
  1814. right: 4px;
  1815. top: 1px;
  1816. height: 2px;
  1817. border-radius: 999px;
  1818. background: linear-gradient(
  1819. 90deg,
  1820. rgba(255, 255, 255, 0),
  1821. rgba(168, 233, 255, 0.5),
  1822. rgba(255, 255, 255, 0)
  1823. );
  1824. pointer-events: none;
  1825. }
  1826. .fp-qbar-fill {
  1827. height: 100%;
  1828. border-radius: 999px;
  1829. box-shadow: 0 0 10px currentColor;
  1830. position: relative;
  1831. }
  1832. .fp-qbar-fill::after {
  1833. content: '';
  1834. position: absolute;
  1835. right: 0;
  1836. top: -1px;
  1837. width: 16px;
  1838. height: calc(100% + 2px);
  1839. border-radius: 999px;
  1840. background: radial-gradient(
  1841. circle at 25% 50%,
  1842. rgba(255, 255, 255, 0.62),
  1843. rgba(255, 255, 255, 0) 70%
  1844. );
  1845. }
  1846. .fp-qbar-fill.good {
  1847. background: linear-gradient(
  1848. 90deg,
  1849. rgba(20, 205, 201, 1),
  1850. rgba(20, 205, 201, 0.25)
  1851. );
  1852. }
  1853. .fp-qbar-fill.bad {
  1854. background: linear-gradient(
  1855. 90deg,
  1856. rgba(255, 77, 79, 1),
  1857. rgba(255, 77, 79, 0.2)
  1858. );
  1859. }
  1860. .fp-qbar-fill.warn {
  1861. background: linear-gradient(
  1862. 90deg,
  1863. rgba(249, 187, 25, 1),
  1864. rgba(249, 187, 25, 0.25)
  1865. );
  1866. }
  1867. /* 覆盖通用 ChartCard 的最小高度,避免低分辨率下被挤出可视区 */
  1868. ::v-deep .chart-container,
  1869. ::v-deep .process-chart-container,
  1870. ::v-deep .product-top10-chart-container,
  1871. ::v-deep .qualification-rate-chart-container {
  1872. min-height: 0 !important;
  1873. }
  1874. ::v-deep .chart-card .card-content {
  1875. min-height: 0;
  1876. }
  1877. </style>
  1878. <style>
  1879. .el-icon-_screen-full,
  1880. .el-icon-_screen-restore {
  1881. font-size: 1.2rem;
  1882. cursor: pointer;
  1883. transition: all 0.3s;
  1884. color: #7fa7ce;
  1885. }
  1886. .el-icon-_screen-full:hover,
  1887. .el-icon-_screen-restore:hover {
  1888. color: #fff;
  1889. transform: scale(1.1);
  1890. }
  1891. </style>