MaterialInOutConfirm.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  1. <!-- 出库确认 - 智能仓储风格 -->
  2. <template>
  3. <dv-border-box-8>
  4. <div class="stock-requisition-page">
  5. <!-- 背景层 -->
  6. <div class="bg-layer" :style="{ backgroundImage: `url(${bgImg})` }" />
  7. <!-- 顶部标题区域 -->
  8. <div class="header-section">
  9. <button class="logout-btn" @click="goHome">
  10. <i class="fas fa-home" />
  11. <span>主页</span>
  12. </button>
  13. <h1 class="page-title">离开</h1>
  14. <!-- 右侧操作按钮 -->
  15. <div class="header-actions">
  16. <button class="action-btn refresh-btn" @click="resetView()">
  17. <i class="fas fa-sync-alt" />
  18. <span>重新校验</span>
  19. </button>
  20. </div>
  21. </div>
  22. <!-- 主内容区域 -->
  23. <main class="main-content">
  24. <div v-if="needConfirmStockInMaterialList.length > 0" class="stats-section">
  25. <span class="stats-text">
  26. 您共有 {{ needConfirmStockInMaterialList.length }} 个物料入库,请确认物料已经放置到对应的货位,确认无误以后,再点击【入库离开】按钮。
  27. </span>
  28. <span v-if="confirmInvalidCount > 0" style="color: red; font-weight: bolder;">
  29. 其中 {{ confirmInvalidCount }} 个物料被闸机读取到了,请把物料放到对应的位置,再点击【重新校验】按钮。
  30. </span>
  31. </div>
  32. <!-- 卡片网格区域 -->
  33. <div class="card-grid-wrapper">
  34. <!-- 空状态 -->
  35. <div v-if=" needConfirmStockInMaterialList.length === 0 && !loading" class="empty-state">
  36. <i class="fas fa-inbox empty-icon" />
  37. <p>暂无数据</p>
  38. </div>
  39. <!-- 卡片网格 -->
  40. <div v-else class="card-grid">
  41. <div
  42. v-for="(item, index) in needConfirmStockInMaterialList"
  43. :key="item.id || index"
  44. class="inventory-card"
  45. :class="getCardClass(item)"
  46. >
  47. <!-- 卡片序号 -->
  48. <div class="card-index" :class="getCardClass(item)">{{ index + 1 }}</div>
  49. <!-- 图片区域 -->
  50. <div class="card-image">
  51. <div class="image-placeholder" :class="getCardClass(item)">
  52. <i :class="getInventoryIcon(item.inventoryType)" />
  53. </div>
  54. </div>
  55. <!-- 信息区域 -->
  56. <div class="card-info">
  57. <div class="info-row">
  58. <span class="info-label">类型:</span>
  59. <span class="info-value">{{ item.inventoryType || '-' }}</span>
  60. </div>
  61. <div class="info-row">
  62. <span class="info-label">名称:</span>
  63. <span class="info-value">{{ item.inventoryName || '-' }}</span>
  64. </div>
  65. <div class="info-row location-row">
  66. <i class="fas fa-map-marker-alt location-icon" />
  67. <span class="info-value">{{ item.positionName || '-' }}</span>
  68. </div>
  69. </div>
  70. <!-- 状态区域(固定高度) -->
  71. <div class="card-status-section">
  72. <div class="status-badge" :class="getCardClass(item)">
  73. <i class="fas fa-exclamation-circle" />
  74. {{ item.remarks || '-' }}
  75. </div>
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. <!-- 统计信息 -->
  81. <div v-if="materialList.length > 0" class="stats-section">
  82. <span class="stats-text">共 {{ materialList.length }} 条数据,已完成 {{ completedCount }} 条, 不识别 {{ unrecognizedCount }} 条</span>
  83. </div>
  84. <!-- 卡片网格区域 -->
  85. <div class="card-grid-wrapper">
  86. <!-- 空状态 -->
  87. <div v-if="materialList.length === 0 && !loading" class="empty-state">
  88. <i class="fas fa-inbox empty-icon" />
  89. <p>暂无数据</p>
  90. </div>
  91. <!-- 卡片网格 -->
  92. <div v-else class="card-grid">
  93. <template v-for="(item, index) in materialList" :key="item.epc">
  94. <div
  95. v-if="item.inventoryId != null && item.isValid === true"
  96. class="inventory-card"
  97. :class="{ 'completed': item.status === 1 }"
  98. >
  99. <!-- 卡片序号 -->
  100. <div class="card-index" :class="{ 'completed': item.status === 1 }">{{ index + 1 }}</div>
  101. <!-- 图片区域 -->
  102. <div class="card-image">
  103. <div class="image-placeholder" :style="{ backgroundColor: getStatusColor(item.status) }">
  104. <i :class="getInventoryIcon(item.inventoryType)" />
  105. </div>
  106. </div>
  107. <!-- 信息区域 -->
  108. <div class="card-info">
  109. <div class="info-row">
  110. <span class="info-label">名称:</span>
  111. <span class="info-value">{{ item.inventoryName || '-' }}</span>
  112. </div>
  113. <div class="info-row">
  114. <span class="info-label">编号:</span>
  115. <span class="info-value">{{ item.inventoryNo || '-' }}</span>
  116. </div>
  117. <div class="info-row location-row">
  118. <i class="fas fa-map-marker-alt location-icon" />
  119. <span class="info-value">{{ item.positionName || '-' }} / {{ item.warehouseName || '-' }}</span>
  120. </div>
  121. </div>
  122. <!-- 操作区域(固定高度) -->
  123. <div class="card-action-section">
  124. <template v-if="item.positionName !== '不在库' && item.inventoryName !== '未识别epc'">
  125. <button v-if="item.status === 1" class="status-btn completed-btn" disabled>
  126. <i class="fas fa-check" /> 领料完成
  127. </button>
  128. <button v-if="item.status === 2" class="status-btn start-btn" @click.stop="handleStart(item)">
  129. <i class="fas fa-play" /> 开始领料
  130. </button>
  131. <button v-if="item.status === 3" class="status-btn apply-btn" @click.stop="handleApply(item)">
  132. <i class="fas fa-hand-paper" /> 申请领料
  133. </button>
  134. </template>
  135. <template v-else>
  136. <span class="status-placeholder">无需操作</span>
  137. </template>
  138. </div>
  139. </div>
  140. </template>
  141. </div>
  142. </div>
  143. </main>
  144. <!-- 底部操作按钮 -->
  145. <div class="bottom-actions">
  146. <button class="submit-btn" :disabled="confirmInvalidCount > 0 || !isCanLeave" @click="handleLeave">
  147. 确认离开
  148. </button>
  149. </div>
  150. <!-- Loading -->
  151. <div v-if="loading" class="loading-overlay">
  152. <div class="loading-dots">
  153. <div class="dot" />
  154. <div class="dot" />
  155. <div class="dot" />
  156. </div>
  157. <span class="loading-text">加载中...</span>
  158. </div>
  159. </div>
  160. </dv-border-box-8>
  161. </template>
  162. <script setup>
  163. import { useRouter } from 'vue-router';
  164. import { ref, computed, onMounted, onUnmounted } from 'vue';
  165. import { showNotify } from 'vant';
  166. import { cfStockOut, createStockOut, cfStockOutLeave,leaveCFWarehouse } from '../api/stockOut.js';
  167. import { areArraysEqual } from '../common/utils.js';
  168. import { gateController } from '../hardware/GateOperate.js';
  169. import { queryCFStockInByUser, backfillCFStockInTime, cfStockInLeave } from '../api/stockIn.js';
  170. // 图片资源
  171. import bgImg from '../assets/images/bj.png';
  172. // 路由
  173. const router = useRouter();
  174. // 返回主页
  175. const goHome = () => {
  176. router.push('/home');
  177. };
  178. // 表格加载状态
  179. const loading = ref(false);
  180. // 物料列表数据
  181. const materialList = ref([]);
  182. const epcs = ref([]);
  183. // 完成数量统计
  184. const confirmInvalidCount = computed(() => {
  185. return needConfirmStockInMaterialList.value.filter(item => item.isValid === false).length;
  186. });
  187. // 完成数量统计
  188. const completedCount = computed(() => {
  189. return materialList.value.filter(item => item.status === 1 && item.inventoryId != null).length;
  190. });
  191. // 不识别的数据统计
  192. const unrecognizedCount = computed(() => {
  193. return materialList.value.filter(item => item.inventoryId == null).length;
  194. });
  195. // 获取设备类型图标
  196. const getInventoryIcon = type => {
  197. const iconMap = {
  198. '工装': 'fas fa-cube',
  199. '设备': 'fas fa-cogs',
  200. '成品': 'fas fa-box',
  201. };
  202. return iconMap[type] || 'fas fa-cube';
  203. };
  204. // 获取状态颜色
  205. const getStatusColor = status => {
  206. const colorMap = {
  207. 1: '#10b981', // 完成 - 绿色
  208. 2: '#f59e0b', // 进行中 - 橙色
  209. 3: '#3b82f6', // 待开始 - 蓝色
  210. };
  211. return colorMap[status] || '#6b7280';
  212. };
  213. // 判断是否可以离开
  214. const isCanLeave = computed(() => {
  215. // 1. 先判断 materialList 是否存在且是数组
  216. // if (!materialList.value || !Array.isArray(materialList.value)) {
  217. // return false;
  218. // }
  219. // 2. 判断是否有数据(空数组不能离开)
  220. // if (materialList.value.length === 0) {
  221. // return false;
  222. // }
  223. // 3. 判断所有项的 status 是否都为 1
  224. // 使用 every 时需要确保 item 和 item.status 都存在
  225. // return materialList.value.every(item => {
  226. // return item && typeof item.status !== 'undefined' && item.status === 1;
  227. // });
  228. return true;
  229. });
  230. // 开始领料
  231. const handleStart = record => {
  232. generateCFStockOut(record.stockOutPrepareLineId);
  233. };
  234. // 申请领料
  235. const handleApply = async record => {
  236. showNotify({ type: 'success', message: '申请领料成功' });
  237. try {
  238. const res = await leaveCFWarehouse(record.inventoryId);
  239. if (res.errorCode === 0) {
  240. showNotify({ type: 'success', message: '申请领料成功' });
  241. getStockOutList();
  242. } else {
  243. showNotify({ type: 'danger', message: res.errorMessage });
  244. }
  245. } catch (error) {
  246. console.error('申请领料API调用失败:', error);
  247. showNotify({ type: 'danger', message: '申请领料API调用失败' });
  248. } finally {
  249. loading.value = false;
  250. }
  251. };
  252. // 领料完成后离开
  253. const handleLeave = async () => {
  254. //----------------------------------- 还料数据处理 -----------------------------------//
  255. await getCFStockInByUser();
  256. let invalidCount = 0;
  257. needConfirmStockInMaterialList.value.map(item => {
  258. if(item.isValid === false) {
  259. invalidCount++;
  260. }
  261. });
  262. if(invalidCount > 0) {
  263. showNotify({
  264. type: 'danger',
  265. message: '请检查有' + invalidCount + '条物料已经入库,但是被闸机扫描到了。请自己检查以后,点击【重新校验】按钮' });
  266. return;
  267. }
  268. // 还料离开,回填入库单中的离开时间
  269. const stockInIds = needConfirmStockInMaterialList.value.map(item => {
  270. return item.stockInId;
  271. });
  272. if(stockInIds.length > 0){
  273. await generateCFStockIn(stockInIds);
  274. }
  275. //----------------------------------- 领料数据处理 -----------------------------------//
  276. const params = [];
  277. materialList.value.map(item => {
  278. if(item.isValid === true && item.inventoryId != null){
  279. params.push({
  280. stockOutPrepareLineId: item.stockOutPrepareLineId,
  281. stockOutId: item.stockOutId,
  282. inventoryId: item.inventoryId,
  283. });
  284. }
  285. });
  286. if(params.length == 0){
  287. // 直接开门离开
  288. gateController('SHOTOPEN');
  289. router.push('/');
  290. return;
  291. }
  292. loading.value = true;
  293. try {
  294. const res = await cfStockOutLeave(params);
  295. if (res.errorCode === 0) {
  296. gateController('SHOTOPEN');
  297. showNotify({ type: 'success', message: '领料离开成功' });
  298. router.push('/');
  299. } else {
  300. showNotify({ type: 'danger', message: res.errorMessage });
  301. }
  302. } catch (error) {
  303. console.error('领料离开API调用失败:', error);
  304. showNotify({ type: 'danger', message: '领料离开API调用失败' });
  305. } finally {
  306. loading.value = false;
  307. }
  308. };
  309. // 扫描到EPC以后,获取领料的数据
  310. const getStockOutList = async () => {
  311. const params = {
  312. epcList: [],
  313. };
  314. const tempMaterialList = [];
  315. for(let i = 0; i < materialList.value.length; i++) {
  316. if(materialList.value[i].queryStatus === 0) {
  317. params.epcList.push(materialList.value[i].epc);
  318. materialList.value[i].queryStatus = 1;
  319. tempMaterialList.push(materialList.value[i]);
  320. }
  321. }
  322. if(params.epcList.length == 0){
  323. return;
  324. }
  325. try {
  326. const res = await cfStockOut(params);
  327. if (res.errorCode === 0) {
  328. if (res.datas && res.datas.length > 0) {
  329. res.datas.forEach(i => {
  330. materialList.value.forEach(j => {
  331. if (j.epc === i.epc) {
  332. j.stockOutId = i.stockOutId;
  333. j.inventoryName = i.inventoryName;
  334. j.inventoryNo = i.inventoryNo;
  335. j.inventoryType = i.inventoryType;
  336. j.warehouseName = i.warehouseName;
  337. j.positionName = i.positionName;
  338. j.stockOutPrepareNo = i.stockOutPrepareNo;
  339. j.stockOutNo = i.stockOutNo;
  340. j.inventoryId = i.inventoryId;
  341. j.stockOutPrepareLineId = i.stockOutPrepareLineId;
  342. j.queryStatus = 2;
  343. j.status = i.status;
  344. }
  345. });
  346. });
  347. }
  348. } else {
  349. showNotify({ type: 'danger', message: res.errorMessage });
  350. }
  351. computerErrorData();
  352. } catch (error) {
  353. console.error('获取扫描到的领料数据API调用失败:', error);
  354. showNotify({ type: 'danger', message: '获取扫描到的领料数据API调用失败' });
  355. // 处理查询失败的物料
  356. tempMaterialList.forEach(item => {
  357. item.queryStatus = 0;
  358. });
  359. } finally {
  360. loading.value = false;
  361. }
  362. };
  363. // 生成出库单
  364. const generateCFStockOut = async id => {
  365. loading.value = true;
  366. const params = [
  367. {
  368. stockOutPrepareLineId: id,
  369. deliveryMethod: 'Manual_Delivery',
  370. },
  371. ];
  372. try {
  373. const res = await createStockOut(params);
  374. if (res.errorCode === 0) {
  375. getStockOutList();
  376. } else {
  377. showNotify({ type: 'danger', message: res.errorMessage });
  378. }
  379. } catch (error) {
  380. console.error('生成出库单API调用失败:', error);
  381. showNotify({ type: 'danger', message: '生成出库单API调用失败' });
  382. } finally {
  383. loading.value = false;
  384. }
  385. };
  386. let timer = null; // 用于保存当前定时器引用
  387. const addEpc = data => {
  388. const newEpcs = data.map(item => item.epc);
  389. // 将新的EPC数据添加到临时数组中,使用Set进行去重
  390. const uniqueEpcs = new Set([...newEpcs]);
  391. if(newEpcs != null && newEpcs.length > 0) {
  392. newEpcs.forEach(item => {
  393. let exist = false;
  394. for(let i =0; i < materialList.value.length; i++) {
  395. if(materialList.value[i].epc === item) {
  396. exist = true;
  397. break;
  398. }
  399. }
  400. if(exist == false) {
  401. materialList.value.push({
  402. epc: item,
  403. queryStatus: 0, // 0: 未查询, 1: 查询中, 2: 已查询, 3: 未查询到
  404. });
  405. }
  406. });
  407. }
  408. };
  409. onMounted(() => {
  410. getCFStockInByUser();
  411. plugin.gateConfig.sendEpc = function(data){
  412. if (typeof(GATE_CONFIG) == 'undefined') {
  413. console.log('设备不支持读写器功能。');
  414. } else {
  415. addEpc(data);
  416. }
  417. };
  418. // 创建全局唯一定时器,每2秒执行一次
  419. timer = setInterval(() => {
  420. // 执行获取出库列表
  421. // addEpc([{
  422. // 'epc': 'FFF000000000000000000001',
  423. // }]);
  424. getStockOutList();
  425. }, 1000);
  426. });
  427. onUnmounted(() => {
  428. plugin.gateConfig.sendEpc = null;
  429. // 清除定时器
  430. if (timer) {
  431. clearInterval(timer);
  432. timer = null;
  433. }
  434. });
  435. // 已入库但是未确认的工装设备列表数据
  436. const needConfirmStockInMaterialList = ref([]);
  437. // 查询用户已经入库但是未确认的工装设备信息
  438. const getCFStockInByUser = async () => {
  439. const info = localStorage.getItem('#LoginInfo');
  440. const userId = JSON.parse(info).userId;
  441. try {
  442. const res = await queryCFStockInByUser(userId);
  443. if (res.errorCode === 0) {
  444. if (res.datas && res.datas.length > 0) {
  445. needConfirmStockInMaterialList.value = res.datas;
  446. computerErrorData();
  447. // userMaterialList.value = res.datas;
  448. // getInnerList();
  449. } else {
  450. needConfirmStockInMaterialList.value = [];
  451. computerErrorData();
  452. // userMaterialList.value = [];
  453. }
  454. } else {
  455. showNotify({ type: 'danger', message: res.errorMessage,zIndex: 99999999 });
  456. // getInnerList();
  457. }
  458. } catch (error) {
  459. console.error('获取物料列表API调用失败:', error);
  460. showNotify({ type: 'danger', message: '获取物料列表API调用失败' });
  461. }
  462. };
  463. // 还料离开(回填归还入库单入库时间)
  464. const generateCFStockIn = async params => {
  465. loading.value = true;
  466. try {
  467. const res = await backfillCFStockInTime(params);
  468. if (res.errorCode === 0) {
  469. // 调用开门操作
  470. //gateController('SHOTOPEN');
  471. //showNotify({ type: 'success', message: '还料离开已完成!' });
  472. //router.push('/home');
  473. } else {
  474. showNotify({ type: 'danger', message: res.errorMessage });
  475. }
  476. } catch (error) {
  477. console.error('生成CF离开单API调用失败:', error);
  478. showNotify({ type: 'danger', message: '生成CF离开单API调用失败' });
  479. } finally {
  480. loading.value = false;
  481. }
  482. };
  483. // 待入库确认的数据,与扫描到的数据进行比对,待入库确认的数据不能被扫描到
  484. const computerErrorData = data => {
  485. if(needConfirmStockInMaterialList.value != null){
  486. needConfirmStockInMaterialList.value.forEach(item => {
  487. item.isValid = true;
  488. });
  489. }
  490. if(materialList.value != null){
  491. materialList.value.forEach(i => {
  492. i.isValid = true;
  493. });
  494. }
  495. if (!needConfirmStockInMaterialList.value || needConfirmStockInMaterialList.value.length === 0) {
  496. return;
  497. }
  498. if (!materialList.value || materialList.value.length === 0) {
  499. return;
  500. }
  501. needConfirmStockInMaterialList.value.forEach(item => {
  502. materialList.value.forEach(i => {
  503. if (i.epc === item.epc) {
  504. i.isValid = false;
  505. item.isValid = false;
  506. }
  507. });
  508. });
  509. };
  510. // 根据 isValid 返回卡片样式类
  511. const getCardClass = card => {
  512. if (card.isValid == null || card.isValid === true) {
  513. return 'in-stock-card';
  514. } else if (card.isValid === false) {
  515. return 'not-in-stock-card';
  516. }
  517. return '';
  518. };
  519. const resetView = () => {
  520. epcs.value = [];
  521. materialList.value = [];
  522. needConfirmStockInMaterialList.value = [];
  523. getCFStockInByUser();
  524. };
  525. </script>
  526. <style scoped>
  527. /* ========== 基础样式 ========== */
  528. .stock-requisition-page {
  529. width: 100%;
  530. height: 100vh;
  531. max-height: 100vh;
  532. position: relative;
  533. font-family: 'Microsoft YaHei', sans-serif;
  534. color: #fff;
  535. overflow: hidden;
  536. display: flex;
  537. flex-direction: column;
  538. }
  539. /* 背景层 */
  540. .bg-layer {
  541. position: fixed;
  542. top: 0;
  543. left: 0;
  544. width: 100%;
  545. height: 100%;
  546. background-color: #041c3d;
  547. background-size: cover;
  548. background-position: center;
  549. background-repeat: no-repeat;
  550. z-index: 0;
  551. }
  552. /* ========== 主内容区域 ========== */
  553. .main-content {
  554. flex: 1;
  555. display: flex;
  556. flex-direction: column;
  557. padding: 0 30px;
  558. position: relative;
  559. z-index: 10;
  560. min-height: 0;
  561. overflow: hidden;
  562. }
  563. /* 统计信息 */
  564. .stats-section {
  565. background: rgba(9, 61, 140, 0.5);
  566. border: 1px solid #049FD8;
  567. border-radius: 12px;
  568. padding: 15px 20px;
  569. margin-bottom: 15px;
  570. flex-shrink: 0;
  571. }
  572. .stats-text {
  573. color: #7ec8ff;
  574. font-size: 16px;
  575. }
  576. /* ========== 卡片网格区域 ========== */
  577. .card-grid-wrapper {
  578. flex: 1;
  579. display: flex;
  580. flex-direction: column;
  581. min-height: 0;
  582. overflow-y: auto;
  583. overflow-x: hidden;
  584. padding-bottom: 10px;
  585. scrollbar-width: none;
  586. -ms-overflow-style: none;
  587. }
  588. .card-grid-wrapper::-webkit-scrollbar {
  589. display: none;
  590. }
  591. /* 空状态 */
  592. .empty-state {
  593. flex: 1;
  594. display: flex;
  595. flex-direction: column;
  596. align-items: center;
  597. justify-content: center;
  598. color: #7ec8ff;
  599. padding: 60px 0;
  600. }
  601. .empty-icon {
  602. font-size: 80px;
  603. margin-bottom: 20px;
  604. opacity: 0.5;
  605. }
  606. .empty-state p {
  607. font-size: 18px;
  608. margin: 0;
  609. }
  610. /* 卡片网格 - 4列布局 */
  611. .card-grid {
  612. display: grid;
  613. grid-template-columns: repeat(4, 1fr);
  614. gap: 20px;
  615. }
  616. /* ========== 设备卡片样式 ========== */
  617. .inventory-card {
  618. background: rgba(5, 30, 60, 0.8);
  619. border: 2px solid #0d4a8a;
  620. border-radius: 12px;
  621. overflow: hidden;
  622. transition: all 0.3s ease;
  623. position: relative;
  624. }
  625. .inventory-card:hover {
  626. border-color: #1e90ff;
  627. box-shadow: 0 0 20px rgba(30, 144, 255, 0.3);
  628. transform: translateY(-3px);
  629. }
  630. .inventory-card.completed {
  631. border-color: #10b981;
  632. box-shadow: 0 0 15px rgba(16, 185, 129, 0.3);
  633. }
  634. /* 卡片序号 - 左上角 */
  635. .card-index {
  636. position: absolute;
  637. top: 10px;
  638. left: 10px;
  639. width: 36px;
  640. height: 36px;
  641. background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
  642. border-radius: 50%;
  643. display: flex;
  644. align-items: center;
  645. justify-content: center;
  646. font-size: 16px;
  647. font-weight: bold;
  648. color: #fff;
  649. z-index: 2;
  650. box-shadow: 0 2px 8px rgba(0, 191, 255, 0.4);
  651. }
  652. .card-index.completed {
  653. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  654. box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
  655. }
  656. /* 图片区域 */
  657. .card-image {
  658. width: 100%;
  659. aspect-ratio: 3 / 4;
  660. display: flex;
  661. align-items: center;
  662. justify-content: center;
  663. overflow: hidden;
  664. margin: 15px;
  665. margin-bottom: 10px;
  666. border-radius: 8px;
  667. width: calc(100% - 30px);
  668. }
  669. .image-placeholder {
  670. display: flex;
  671. align-items: center;
  672. justify-content: center;
  673. width: 100%;
  674. height: 100%;
  675. background: linear-gradient(135deg, #f0f4f8 0%, #e8ecf0 100%);
  676. color: #fff;
  677. font-size: 48px;
  678. }
  679. /* 信息区域 */
  680. .card-info {
  681. padding: 10px 15px 15px;
  682. }
  683. .info-row {
  684. display: flex;
  685. align-items: baseline;
  686. margin-bottom: 6px;
  687. font-size: 14px;
  688. }
  689. .info-row:last-child {
  690. margin-bottom: 0;
  691. }
  692. .info-label {
  693. color: #7ec8ff;
  694. white-space: nowrap;
  695. margin-right: 5px;
  696. }
  697. .info-value {
  698. color: #fff;
  699. flex: 1;
  700. overflow: hidden;
  701. text-overflow: ellipsis;
  702. white-space: nowrap;
  703. }
  704. /* 位置行样式 */
  705. .location-row {
  706. display: flex;
  707. align-items: center;
  708. margin-top: 4px;
  709. padding-top: 4px;
  710. border-top: 1px solid rgba(30, 144, 255, 0.2);
  711. }
  712. .location-icon {
  713. color: #00bfff;
  714. font-size: 12px;
  715. margin-right: 6px;
  716. }
  717. /* ========== 操作区域(固定高度) ========== */
  718. .card-action-section {
  719. padding: 12px 15px;
  720. border-top: 1px solid rgba(30, 144, 255, 0.3);
  721. background: rgba(9, 61, 140, 0.3);
  722. min-height: 50px;
  723. display: flex;
  724. align-items: center;
  725. justify-content: center;
  726. }
  727. .status-placeholder {
  728. color: #5a8abf;
  729. font-size: 12px;
  730. opacity: 0.7;
  731. }
  732. /* 状态按钮 */
  733. .status-btn {
  734. width: 100%;
  735. padding: 10px 15px;
  736. border-radius: 6px;
  737. font-size: 13px;
  738. font-weight: 500;
  739. cursor: pointer;
  740. transition: all 0.3s;
  741. border: none;
  742. display: flex;
  743. align-items: center;
  744. justify-content: center;
  745. gap: 6px;
  746. }
  747. .completed-btn {
  748. background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
  749. color: #fff;
  750. opacity: 0.7;
  751. cursor: not-allowed;
  752. }
  753. .start-btn {
  754. background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%);
  755. color: #fff;
  756. }
  757. .start-btn:hover {
  758. box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
  759. }
  760. .apply-btn {
  761. background: linear-gradient(90deg, #1e90ff 0%, #00bfff 100%);
  762. color: #fff;
  763. }
  764. .apply-btn:hover {
  765. box-shadow: 0 0 15px rgba(30, 144, 255, 0.5);
  766. }
  767. /* ========== 底部操作按钮 ========== */
  768. .bottom-actions {
  769. width: 100%;
  770. padding: 15px 30px;
  771. box-sizing: border-box;
  772. background: rgba(4, 28, 61, 0.95);
  773. z-index: 20;
  774. flex-shrink: 0;
  775. }
  776. .submit-btn {
  777. width: 100%;
  778. padding: 18px 0;
  779. background: #4a99e2;
  780. border: none;
  781. border-radius: 12px;
  782. font-size: 24px;
  783. font-weight: bold;
  784. color: #fff;
  785. cursor: pointer;
  786. transition: all 0.3s;
  787. letter-spacing: 8px;
  788. text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  789. }
  790. .submit-btn:hover:not(:disabled) {
  791. box-shadow: 0 0 30px rgba(16, 185, 129, 0.6);
  792. transform: translateY(-2px);
  793. }
  794. .submit-btn:disabled {
  795. opacity: 0.5;
  796. cursor: not-allowed;
  797. background: linear-gradient(90deg, #6b7280 0%, #9ca3af 100%);
  798. }
  799. /* ========== 响应式 - 横屏 ========== */
  800. @media screen and (orientation: landscape) {
  801. .header-section {
  802. padding: 30px 20px 10px 20px;
  803. }
  804. .page-title {
  805. font-size: 22px;
  806. }
  807. .logout-btn {
  808. padding: 6px 14px;
  809. font-size: 13px;
  810. left: 20px;
  811. }
  812. .logout-btn i {
  813. font-size: 14px;
  814. }
  815. .header-actions {
  816. right: 20px;
  817. }
  818. .action-btn {
  819. padding: 6px 14px;
  820. font-size: 13px;
  821. }
  822. .action-btn i {
  823. font-size: 14px;
  824. }
  825. .main-content {
  826. padding: 0 20px;
  827. }
  828. .stats-section {
  829. padding: 10px 15px;
  830. margin-bottom: 10px;
  831. }
  832. .stats-text {
  833. font-size: 14px;
  834. }
  835. /* 卡片网格响应式 */
  836. .card-grid {
  837. grid-template-columns: repeat(6, 1fr);
  838. gap: 15px;
  839. }
  840. .card-index {
  841. width: 24px;
  842. height: 24px;
  843. font-size: 11px;
  844. top: 6px;
  845. left: 6px;
  846. }
  847. .card-image {
  848. margin: 10px;
  849. margin-bottom: 8px;
  850. width: calc(100% - 20px);
  851. }
  852. .image-placeholder {
  853. font-size: 36px;
  854. }
  855. .card-info {
  856. padding: 6px 10px 8px;
  857. }
  858. .info-row {
  859. font-size: 11px;
  860. margin-bottom: 3px;
  861. }
  862. .location-icon {
  863. font-size: 10px;
  864. }
  865. .card-action-section {
  866. padding: 8px 10px;
  867. min-height: 40px;
  868. }
  869. .status-btn {
  870. padding: 6px 10px;
  871. font-size: 11px;
  872. gap: 4px;
  873. }
  874. .status-placeholder {
  875. font-size: 10px;
  876. }
  877. .bottom-actions {
  878. padding: 10px 20px;
  879. }
  880. .submit-btn {
  881. padding: 12px 0;
  882. font-size: 18px;
  883. letter-spacing: 6px;
  884. border-radius: 8px;
  885. }
  886. }
  887. </style>
  888. <style scoped>
  889. /* ========== 设备卡片样式 ========== */
  890. .inventory-card {
  891. background: rgba(5, 30, 60, 0.8);
  892. border: 2px solid #0d4a8a;
  893. border-radius: 12px;
  894. overflow: hidden;
  895. transition: all 0.3s ease;
  896. position: relative;
  897. }
  898. .inventory-card:hover {
  899. border-color: #1e90ff;
  900. box-shadow: 0 0 20px rgba(30, 144, 255, 0.3);
  901. transform: translateY(-3px);
  902. }
  903. /* 校验失败卡片 - 红色警告 */
  904. .inventory-card.not-in-stock-card {
  905. border-color: #ef4444;
  906. }
  907. /* 校验通过卡片 - 绿色 */
  908. .inventory-card.in-stock-card {
  909. border-color: #10b981;
  910. }
  911. /* 卡片序号 - 左上角 */
  912. .card-index {
  913. position: absolute;
  914. top: 10px;
  915. left: 10px;
  916. width: 36px;
  917. height: 36px;
  918. background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
  919. border-radius: 50%;
  920. display: flex;
  921. align-items: center;
  922. justify-content: center;
  923. font-size: 16px;
  924. font-weight: bold;
  925. color: #fff;
  926. z-index: 2;
  927. box-shadow: 0 2px 8px rgba(0, 191, 255, 0.4);
  928. }
  929. .card-index.not-in-stock-card {
  930. background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
  931. box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
  932. }
  933. .card-index.in-stock-card {
  934. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  935. box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
  936. }
  937. /* 图片区域 */
  938. .card-image {
  939. width: 100%;
  940. aspect-ratio: 3 / 4;
  941. display: flex;
  942. align-items: center;
  943. justify-content: center;
  944. overflow: hidden;
  945. margin: 15px;
  946. margin-bottom: 10px;
  947. border-radius: 8px;
  948. width: calc(100% - 30px);
  949. }
  950. .image-placeholder {
  951. display: flex;
  952. align-items: center;
  953. justify-content: center;
  954. width: 100%;
  955. height: 100%;
  956. background: linear-gradient(135deg, #f0f4f8 0%, #e8ecf0 100%);
  957. color: #94a3b8;
  958. font-size: 48px;
  959. }
  960. .image-placeholder.not-in-stock-card {
  961. background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
  962. color: #fff;
  963. }
  964. .image-placeholder.in-stock-card {
  965. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  966. color: #fff;
  967. }
  968. /* 信息区域 */
  969. .card-info {
  970. padding: 10px 15px 15px;
  971. }
  972. .info-row {
  973. display: flex;
  974. align-items: baseline;
  975. margin-bottom: 6px;
  976. font-size: 14px;
  977. }
  978. .info-row:last-child {
  979. margin-bottom: 0;
  980. }
  981. .info-label {
  982. color: #7ec8ff;
  983. white-space: nowrap;
  984. margin-right: 5px;
  985. }
  986. .info-value {
  987. color: #fff;
  988. flex: 1;
  989. overflow: hidden;
  990. text-overflow: ellipsis;
  991. white-space: nowrap;
  992. }
  993. /* 位置行样式 */
  994. .location-row {
  995. display: flex;
  996. align-items: center;
  997. margin-top: 4px;
  998. padding-top: 4px;
  999. border-top: 1px solid rgba(30, 144, 255, 0.2);
  1000. }
  1001. .location-icon {
  1002. color: #00bfff;
  1003. font-size: 12px;
  1004. margin-right: 6px;
  1005. }
  1006. /* ========== 状态区域(固定高度) ========== */
  1007. .card-status-section {
  1008. padding: 12px 15px;
  1009. border-top: 1px solid rgba(30, 144, 255, 0.3);
  1010. background: rgba(9, 61, 140, 0.3);
  1011. min-height: 50px;
  1012. display: flex;
  1013. align-items: center;
  1014. justify-content: center;
  1015. }
  1016. /* 状态标签 */
  1017. .status-badge {
  1018. padding: 8px 20px;
  1019. border-radius: 20px;
  1020. font-size: 13px;
  1021. font-weight: 500;
  1022. display: flex;
  1023. align-items: center;
  1024. gap: 6px;
  1025. }
  1026. .status-badge.not-in-stock-card {
  1027. background: rgba(239, 68, 68, 0.3);
  1028. border: 1px solid #ef4444;
  1029. color: #fca5a5;
  1030. }
  1031. .status-badge.in-stock-card {
  1032. background: rgba(16, 185, 129, 0.3);
  1033. border: 1px solid #10b981;
  1034. color: #6ee7b7;
  1035. }
  1036. </style>