FinishProductOut.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. <!-- 成品出库 - 智能仓储风格 -->
  2. <template>
  3. <div class="stock-requisition-page">
  4. <!-- 背景层 -->
  5. <div class="bg-layer" :style="{ backgroundImage: `url(${bgImg})` }" />
  6. <!-- 顶部标题区域 -->
  7. <div class="header-section">
  8. <button class="logout-btn" @click="goHome">
  9. <i class="fas fa-home" />
  10. <span>主页</span>
  11. </button>
  12. <h1 class="page-title">成品出库</h1>
  13. </div>
  14. <!-- 主内容区域 -->
  15. <main class="main-content">
  16. <!-- 筛选区域 -->
  17. <div class="filter-section">
  18. <div class="filter-row">
  19. <div class="filter-item">
  20. <label class="filter-label">名称</label>
  21. <input
  22. v-model="searchForm.inventoryName" type="text" placeholder="输入名称"
  23. class="filter-input dark-input" @keyup.enter="handleSearch"
  24. />
  25. </div>
  26. <div class="filter-item">
  27. <label class="filter-label">图号</label>
  28. <input
  29. v-model="searchForm.inventoryDrawNo" type="text" placeholder="输入图号"
  30. class="filter-input dark-input" @keyup.enter="handleSearch"
  31. />
  32. </div>
  33. <!-- <div class="filter-item">
  34. <label class="filter-label">仓库</label>
  35. <v-select
  36. v-model="searchForm.warehouseId" :options="warehouseList" :reduce="item => item.id" label="name"
  37. placeholder="选择仓库" :clearable="true" :filterable="true" class="filter-select dark-select"
  38. @update:model-value="handleSearch"
  39. >
  40. <template #no-options>
  41. <span>无该选项数据</span>
  42. </template>
  43. </v-select>
  44. </div> -->
  45. <div class="filter-buttons">
  46. <button class="search-btn" @click="handleSearch">搜索</button>
  47. <button class="reset-btn" @click="handleReset">重置</button>
  48. </div>
  49. </div>
  50. </div>
  51. <!-- 全选控制栏 -->
  52. <div v-if="finishProductList.length > 0" class="select-all-bar">
  53. <van-checkbox :model-value="isAllSelected" @update:model-value="toggleSelectAll">
  54. <span class="select-all-text">全选当前页(已选 {{ selectedProducts.length }} 项)</span>
  55. </van-checkbox>
  56. </div>
  57. <!-- 卡片网格区域 -->
  58. <div ref="cardGridWrapper" class="card-grid-wrapper" @scroll="handleScroll">
  59. <!-- 空状态 -->
  60. <div v-if="finishProductList.length === 0 && !loading" class="empty-state">
  61. <i class="fas fa-box-open empty-icon" />
  62. <p>暂无成品</p>
  63. </div>
  64. <!-- 卡片网格 -->
  65. <div v-else class="card-grid">
  66. <div
  67. v-for="(product, index) in finishProductList" :key="product.id"
  68. class="inventory-card"
  69. :class="{ 'selected': selectedProducts.includes(product.id) }"
  70. @click="toggleSelect(product.id)"
  71. >
  72. <!-- 卡片序号 -->
  73. <div class="card-index">{{ index + 1 }}</div>
  74. <!-- 图片区域 -->
  75. <div class="card-image">
  76. <div class="image-placeholder">
  77. <i class="fas fa-box" />
  78. </div>
  79. </div>
  80. <!-- 信息区域 -->
  81. <div class="card-info">
  82. <div class="info-row">
  83. <span class="info-label">名称:</span>
  84. <span class="info-value">{{ product.inventoryName || '-' }}</span>
  85. </div>
  86. <div class="info-row">
  87. <span class="info-label">图号:</span>
  88. <span class="info-value">{{ product.inventoryDrawNo || '-' }}</span>
  89. </div>
  90. <div class="info-row location-row">
  91. <i class="fas fa-map-marker-alt location-icon" />
  92. <span class="info-value">{{ product.inventoryPosition || '-' }} / {{ product.inventoryWarehouse || '-' }}</span>
  93. </div>
  94. </div>
  95. <!-- 选中状态指示 -->
  96. <div v-if="selectedProducts.includes(product.id)" class="selected-indicator">
  97. <i class="fas fa-check" />
  98. </div>
  99. </div>
  100. </div>
  101. <!-- 加载更多提示 -->
  102. <div v-if="loadingMore" class="loading-more">
  103. <div class="loading-more-spinner" />
  104. <span>加载中...</span>
  105. </div>
  106. <div v-else-if="noMoreData && finishProductList.length > 0" class="no-more-data">
  107. <span>没有更多数据了</span>
  108. </div>
  109. </div>
  110. </main>
  111. <!-- 底部操作按钮 -->
  112. <div class="bottom-actions">
  113. <button class="submit-btn" :disabled="selectedProducts.length === 0" @click="confirmOutbound">
  114. 确认出库
  115. </button>
  116. </div>
  117. <!-- 确认出库弹窗 -->
  118. <div v-if="showConfirmModal" class="tech-modal-overlay" @click.self="showConfirmModal = false">
  119. <div class="tech-modal success-modal">
  120. <!-- <button class="modal-close-btn" @click="showConfirmModal = false">
  121. <i class="fas fa-times" />
  122. </button> -->
  123. <div class="modal-content-row">
  124. <div class="modal-text">
  125. <h3>确认出库</h3>
  126. <p>您确定要将选中的 <span class="highlight">{{ selectedProducts.length }}</span> 个成品进行出库操作吗?</p>
  127. </div>
  128. <div class="modal-icon">
  129. <div class="icon-box">
  130. <i class="fas fa-box" />
  131. <i class="fas fa-check-circle check-icon" />
  132. </div>
  133. </div>
  134. </div>
  135. <div class="modal-footer">
  136. <button class="modal-btn cancel" @click="showConfirmModal = false">取消</button>
  137. <button class="modal-btn confirm" @click="executeOutbound">确认出库</button>
  138. </div>
  139. </div>
  140. </div>
  141. <!-- Loading -->
  142. <div v-if="loading" class="loading-overlay">
  143. <div class="loading-dots">
  144. <div class="dot" />
  145. <div class="dot" />
  146. <div class="dot" />
  147. </div>
  148. <span class="loading-text">加载中...</span>
  149. </div>
  150. </div>
  151. </template>
  152. <script setup>
  153. import { ref, computed, onMounted, nextTick } from 'vue';
  154. import { useRouter } from 'vue-router';
  155. import { showNotify } from 'vant';
  156. import { getWarehouseList } from '../api/stock.js';
  157. import { queryFinishProduct, generateStockOut } from '../api/finishProduct.js';
  158. import vSelect from 'vue-select';
  159. import 'vue-select/dist/vue-select.css';
  160. import { gateController } from '../hardware/GateOperate.js';
  161. // 图片资源
  162. import bgImg from '../assets/images/bj.png';
  163. const router = useRouter();
  164. // 返回主页
  165. const goHome = () => {
  166. router.push('/home');
  167. };
  168. // 滚动容器ref
  169. const cardGridWrapper = ref(null);
  170. // 加载状态
  171. const loading = ref(false);
  172. const loadingMore = ref(false);
  173. const noMoreData = ref(false);
  174. // 仓库列表
  175. const warehouseList = ref([]);
  176. // 筛选表单
  177. const searchForm = ref({
  178. inventoryName: '',
  179. inventoryDrawNo: '',
  180. // warehouseId: undefined,
  181. });
  182. // 无限滚动分页参数
  183. const pageSize = 20;
  184. const currentStart = ref(0);
  185. const total = ref(0);
  186. const finishProductList = ref([]);
  187. // 选中项
  188. const selectedProducts = ref([]); // 存储选中的 product.id
  189. // 计算是否全选
  190. const isAllSelected = computed(() => {
  191. if (finishProductList.value.length === 0) return false;
  192. return finishProductList.value.every(item => selectedProducts.value.includes(item.id));
  193. });
  194. // 弹窗与提示
  195. const showConfirmModal = ref(false);
  196. // 滚动事件处理 - 无限滚动
  197. const handleScroll = e => {
  198. const container = e.target;
  199. const { scrollTop, scrollHeight, clientHeight } = container;
  200. // 距离底部100px时触发加载
  201. if (scrollHeight - scrollTop - clientHeight < 100) {
  202. loadMore();
  203. }
  204. };
  205. // 检查容器是否需要继续加载(当内容不足以产生滚动条时自动加载更多)
  206. const checkAndLoadMore = async () => {
  207. await nextTick();
  208. const container = cardGridWrapper.value;
  209. if (!container) return;
  210. // 如果内容高度不足以产生滚动条,且还有更多数据,继续加载
  211. if (container.scrollHeight <= container.clientHeight && !noMoreData.value && !loadingMore.value) {
  212. loadMore();
  213. }
  214. };
  215. // 是否正在执行初次加载(防止重复请求)
  216. let isInitialLoading = false;
  217. // 加载更多数据
  218. const loadMore = async () => {
  219. // 增加 isInitialLoading 检查,防止初次加载时触发 loadMore
  220. if (loadingMore.value || noMoreData.value || loading.value || isInitialLoading) return;
  221. loadingMore.value = true;
  222. const params = {
  223. ...searchForm.value,
  224. range: {
  225. start: currentStart.value,
  226. length: pageSize,
  227. },
  228. };
  229. try {
  230. const res = await queryFinishProduct(params);
  231. if (res.errorCode === 0) {
  232. if (res.datas && res.datas.length > 0) {
  233. finishProductList.value = [...finishProductList.value, ...res.datas];
  234. currentStart.value += res.datas.length;
  235. total.value = res.total;
  236. // 检查是否还有更多数据
  237. if (finishProductList.value.length >= res.total) {
  238. noMoreData.value = true;
  239. } else {
  240. // 加载完成后检查是否需要继续加载
  241. checkAndLoadMore();
  242. }
  243. } else {
  244. noMoreData.value = true;
  245. }
  246. }
  247. } catch (error) {
  248. console.error('加载更多数据失败:', error);
  249. } finally {
  250. loadingMore.value = false;
  251. }
  252. };
  253. // 搜索
  254. const handleSearch = () => {
  255. getList();
  256. };
  257. // 重置
  258. const handleReset = () => {
  259. searchForm.value = {
  260. warehouseId: undefined,
  261. inventoryName: '',
  262. inventoryDrawNo: '',
  263. };
  264. getList();
  265. };
  266. // 卡片勾选
  267. const toggleSelect = id => {
  268. const index = selectedProducts.value.indexOf(id);
  269. if (index > -1) {
  270. selectedProducts.value.splice(index, 1);
  271. } else {
  272. selectedProducts.value.push(id);
  273. }
  274. };
  275. // 处理复选框变化
  276. const handleCheckboxChange = (checked, id) => {
  277. if (checked) {
  278. if (!selectedProducts.value.includes(id)) {
  279. selectedProducts.value.push(id);
  280. }
  281. } else {
  282. const index = selectedProducts.value.indexOf(id);
  283. if (index > -1) {
  284. selectedProducts.value.splice(index, 1);
  285. }
  286. }
  287. };
  288. // 全选/取消全选
  289. const toggleSelectAll = checked => {
  290. if (checked) {
  291. // 全选当前页所有项(累加模式)
  292. finishProductList.value.forEach(item => {
  293. if (!selectedProducts.value.includes(item.id)) {
  294. selectedProducts.value.push(item.id);
  295. }
  296. });
  297. } else {
  298. // 取消全选当前页
  299. const currentPageIds = finishProductList.value.map(item => item.id);
  300. selectedProducts.value = selectedProducts.value.filter(id => !currentPageIds.includes(id));
  301. }
  302. };
  303. // 打开发起出库确认
  304. const confirmOutbound = () => {
  305. if (selectedProducts.value.length === 0) {
  306. showNotify({ type: 'danger', message: '请选择要确认出库的成品' });
  307. return;
  308. }
  309. showConfirmModal.value = true;
  310. };
  311. // 提交中状态(与列表加载分开管理)
  312. const submitting = ref(false);
  313. // 执行出库
  314. const executeOutbound = async () => {
  315. showConfirmModal.value = false;
  316. if (selectedProducts.value.length === 0) return;
  317. submitting.value = true;
  318. try {
  319. const res = await generateStockOut(selectedProducts.value);
  320. if (res.errorCode === 0) {
  321. // 调用开门操作
  322. gateController('SHOTOPEN');
  323. showNotify({ type: 'success', message: '出库成功' });
  324. selectedProducts.value = [];
  325. await getList(); // 重新加载列表
  326. } else {
  327. showNotify({ type: 'danger', message: res.errorMessage });
  328. }
  329. } catch (error) {
  330. console.log(error);
  331. showNotify({ type: 'danger', message: '出库失败' });
  332. } finally {
  333. submitting.value = false;
  334. }
  335. };
  336. // 初次加载成品列表
  337. const getList = async () => {
  338. // 防止重复调用
  339. if (isInitialLoading) return;
  340. isInitialLoading = true;
  341. loading.value = true;
  342. currentStart.value = 0;
  343. noMoreData.value = false;
  344. // 立即清空列表,防止数据重复
  345. finishProductList.value = [];
  346. const params = {
  347. ...searchForm.value,
  348. range: {
  349. start: 0,
  350. length: pageSize,
  351. },
  352. };
  353. try {
  354. const res = await queryFinishProduct(params);
  355. if (res.errorCode === 0) {
  356. if (res.datas && res.datas.length > 0) {
  357. finishProductList.value = res.datas;
  358. currentStart.value = res.datas.length;
  359. total.value = res.total;
  360. if (finishProductList.value.length >= res.total) {
  361. noMoreData.value = true;
  362. } else {
  363. // 初次加载完成后检查是否需要继续加载
  364. checkAndLoadMore();
  365. }
  366. } else {
  367. finishProductList.value = [];
  368. total.value = 0;
  369. noMoreData.value = true;
  370. }
  371. } else {
  372. finishProductList.value = [];
  373. total.value = 0;
  374. showNotify({ type: 'danger', message: res.errorMessage });
  375. }
  376. } catch (error) {
  377. console.error('获取成品列表失败:', error);
  378. showNotify({ type: 'danger', message: '获取成品列表失败' });
  379. } finally {
  380. loading.value = false;
  381. isInitialLoading = false;
  382. }
  383. };
  384. // 获取仓库列表
  385. const getWarehouse = async () => {
  386. try {
  387. const res = await getWarehouseList();
  388. if (res.errorCode === 0) {
  389. if (res.datas && res.datas.length > 0) {
  390. warehouseList.value = res.datas;
  391. } else {
  392. warehouseList.value = [];
  393. }
  394. } else {
  395. showNotify({ type: 'danger', message: res.errorMessage });
  396. }
  397. } catch (error) {
  398. console.error('获取仓库列表API调用失败:', error);
  399. showNotify({ type: 'danger', message: '获取仓库数据失败' });
  400. } finally {
  401. loading.value = false;
  402. }
  403. };
  404. onMounted(() => {
  405. getWarehouse();
  406. getList();
  407. });
  408. </script>
  409. <style scoped>
  410. /* ========== 基础样式 ========== */
  411. .stock-requisition-page {
  412. width: 100%;
  413. height: 100vh;
  414. max-height: 100vh;
  415. position: relative;
  416. font-family: 'Microsoft YaHei', sans-serif;
  417. color: #fff;
  418. overflow: hidden;
  419. display: flex;
  420. flex-direction: column;
  421. }
  422. /* 背景层 */
  423. .bg-layer {
  424. position: fixed;
  425. top: 0;
  426. left: 0;
  427. width: 100%;
  428. height: 100%;
  429. background-color: #041c3d;
  430. background-size: cover;
  431. background-position: center;
  432. background-repeat: no-repeat;
  433. z-index: 0;
  434. }
  435. /* ========== 主内容区域 ========== */
  436. .main-content {
  437. flex: 1;
  438. display: flex;
  439. flex-direction: column;
  440. padding: 0 30px;
  441. position: relative;
  442. z-index: 10;
  443. min-height: 0;
  444. overflow: hidden;
  445. }
  446. /* ========== 筛选区域 ========== */
  447. .filter-section {
  448. background: rgba(9, 61, 140, 0.5);
  449. border: 1px solid #049FD8;
  450. border-radius: 12px;
  451. padding: 20px;
  452. margin-bottom: 15px;
  453. flex-shrink: 0;
  454. }
  455. .filter-row {
  456. display: flex;
  457. flex-wrap: wrap;
  458. align-items: center;
  459. gap: 20px;
  460. }
  461. .filter-item {
  462. display: flex;
  463. align-items: center;
  464. gap: 10px;
  465. }
  466. .filter-label {
  467. font-size: 16px;
  468. color: #7ec8ff;
  469. white-space: nowrap;
  470. font-weight: 500;
  471. }
  472. .filter-select {
  473. width: 200px;
  474. }
  475. .filter-input.dark-input {
  476. width: 200px;
  477. padding: 10px 15px;
  478. background: rgba(13, 58, 106, 0.8);
  479. border: 1px solid #2a7fff;
  480. border-radius: 6px;
  481. color: #fff;
  482. font-size: 14px;
  483. outline: none;
  484. transition: all 0.3s;
  485. }
  486. .filter-input.dark-input::placeholder {
  487. color: #7ec8ff;
  488. opacity: 0.7;
  489. }
  490. .filter-input.dark-input:focus {
  491. border-color: #00bfff;
  492. box-shadow: 0 0 10px rgba(0, 191, 255, 0.3);
  493. }
  494. .filter-buttons {
  495. display: flex;
  496. gap: 15px;
  497. margin-left: auto;
  498. }
  499. .search-btn,
  500. .reset-btn {
  501. padding: 10px 30px;
  502. border-radius: 6px;
  503. font-size: 16px;
  504. font-weight: 500;
  505. cursor: pointer;
  506. transition: all 0.3s;
  507. border: none;
  508. }
  509. .search-btn {
  510. background: linear-gradient(90deg, #1e90ff 0%, #00bfff 100%);
  511. color: #fff;
  512. }
  513. .search-btn:hover {
  514. box-shadow: 0 0 15px rgba(30, 144, 255, 0.6);
  515. transform: translateY(-2px);
  516. }
  517. .reset-btn {
  518. background: rgba(13, 58, 106, 0.8);
  519. border: 1px solid #2a7fff;
  520. color: #fff;
  521. }
  522. .reset-btn:hover {
  523. background: rgba(26, 74, 122, 0.8);
  524. box-shadow: 0 0 10px rgba(30, 144, 255, 0.3);
  525. }
  526. /* Vue Select 深色主题 */
  527. :deep(.dark-select .vs__dropdown-toggle) {
  528. background: rgba(13, 58, 106, 0.8);
  529. border: 1px solid #2a7fff;
  530. border-radius: 6px;
  531. padding: 6px 10px;
  532. min-height: 40px;
  533. }
  534. :deep(.dark-select .vs__selected) {
  535. color: #fff;
  536. }
  537. :deep(.dark-select .vs__search) {
  538. color: #fff;
  539. }
  540. :deep(.dark-select .vs__search::placeholder) {
  541. color: #7ec8ff;
  542. opacity: 0.7;
  543. }
  544. :deep(.dark-select .vs__actions svg) {
  545. fill: #7ec8ff;
  546. }
  547. :deep(.dark-select .vs__dropdown-menu) {
  548. background: rgba(13, 58, 106, 0.95);
  549. border: 1px solid #2a7fff;
  550. border-radius: 6px;
  551. z-index: 9999;
  552. }
  553. :deep(.dark-select .vs__dropdown-option) {
  554. color: #fff;
  555. padding: 10px 15px;
  556. }
  557. :deep(.dark-select .vs__dropdown-option--highlight) {
  558. background: rgba(30, 144, 255, 0.3);
  559. }
  560. :deep(.filter-input .van-cell::after) {
  561. display: none !important;
  562. }
  563. /* ========== 全选控制栏 ========== */
  564. .select-all-bar {
  565. background: rgba(9, 61, 140, 0.3);
  566. border: 1px solid #049FD8;
  567. border-radius: 8px;
  568. padding: 10px 20px;
  569. margin-bottom: 10px;
  570. display: flex;
  571. align-items: center;
  572. flex-shrink: 0;
  573. }
  574. .select-all-text {
  575. font-size: 14px;
  576. color: #7ec8ff;
  577. font-weight: 500;
  578. }
  579. :deep(.select-all-bar .van-checkbox__label) {
  580. color: #7ec8ff;
  581. }
  582. /* ========== 卡片网格区域 ========== */
  583. .card-grid-wrapper {
  584. flex: 1;
  585. display: flex;
  586. flex-direction: column;
  587. min-height: 0;
  588. overflow-y: auto;
  589. overflow-x: hidden;
  590. padding-bottom: 10px;
  591. scrollbar-width: none;
  592. -ms-overflow-style: none;
  593. }
  594. .card-grid-wrapper::-webkit-scrollbar {
  595. display: none;
  596. }
  597. /* 空状态 */
  598. .empty-state {
  599. flex: 1;
  600. display: flex;
  601. flex-direction: column;
  602. align-items: center;
  603. justify-content: center;
  604. color: #7ec8ff;
  605. padding: 60px 0;
  606. }
  607. .empty-icon {
  608. font-size: 80px;
  609. margin-bottom: 20px;
  610. opacity: 0.5;
  611. }
  612. .empty-state p {
  613. font-size: 18px;
  614. margin: 0;
  615. }
  616. /* 卡片网格 - 4列布局 */
  617. .card-grid {
  618. display: grid;
  619. grid-template-columns: repeat(4, 1fr);
  620. gap: 20px;
  621. }
  622. /* ========== 设备卡片样式 ========== */
  623. .inventory-card {
  624. background: rgba(5, 30, 60, 0.8);
  625. border: 2px solid #0d4a8a;
  626. border-radius: 12px;
  627. overflow: hidden;
  628. transition: all 0.3s ease;
  629. position: relative;
  630. cursor: pointer;
  631. }
  632. .inventory-card:hover {
  633. border-color: #1e90ff;
  634. box-shadow: 0 0 20px rgba(30, 144, 255, 0.3);
  635. transform: translateY(-3px);
  636. }
  637. .inventory-card.selected {
  638. border-color: #00bfff;
  639. box-shadow: 0 0 25px rgba(0, 191, 255, 0.4);
  640. }
  641. /* 卡片序号 - 左上角 */
  642. .card-index {
  643. position: absolute;
  644. top: 10px;
  645. left: 10px;
  646. width: 36px;
  647. height: 36px;
  648. background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
  649. border-radius: 50%;
  650. display: flex;
  651. align-items: center;
  652. justify-content: center;
  653. font-size: 16px;
  654. font-weight: bold;
  655. color: #fff;
  656. z-index: 2;
  657. box-shadow: 0 2px 8px rgba(0, 191, 255, 0.4);
  658. }
  659. /* 图片区域 */
  660. .card-image {
  661. width: 100%;
  662. aspect-ratio: 3 / 4;
  663. display: flex;
  664. align-items: center;
  665. justify-content: center;
  666. overflow: hidden;
  667. margin: 15px;
  668. margin-bottom: 10px;
  669. border-radius: 8px;
  670. width: calc(100% - 30px);
  671. }
  672. .image-placeholder {
  673. display: flex;
  674. align-items: center;
  675. justify-content: center;
  676. width: 100%;
  677. height: 100%;
  678. background: linear-gradient(135deg, #f0f4f8 0%, #e8ecf0 100%);
  679. color: #fff;
  680. font-size: 48px;
  681. }
  682. /* 信息区域 */
  683. .card-info {
  684. padding: 10px 15px 15px;
  685. }
  686. .info-row {
  687. display: flex;
  688. align-items: baseline;
  689. margin-bottom: 6px;
  690. font-size: 14px;
  691. }
  692. .info-row:last-child {
  693. margin-bottom: 0;
  694. }
  695. .info-label {
  696. color: #7ec8ff;
  697. white-space: nowrap;
  698. margin-right: 5px;
  699. }
  700. .info-value {
  701. color: #fff;
  702. flex: 1;
  703. overflow: hidden;
  704. text-overflow: ellipsis;
  705. white-space: nowrap;
  706. }
  707. /* 位置行样式 */
  708. .location-row {
  709. display: flex;
  710. align-items: center;
  711. margin-top: 4px;
  712. padding-top: 4px;
  713. border-top: 1px solid rgba(30, 144, 255, 0.2);
  714. }
  715. .location-icon {
  716. color: #00bfff;
  717. font-size: 12px;
  718. margin-right: 6px;
  719. }
  720. /* 选中状态指示 */
  721. .selected-indicator {
  722. position: absolute;
  723. top: 10px;
  724. right: 10px;
  725. width: 36px;
  726. height: 36px;
  727. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  728. border-radius: 50%;
  729. display: flex;
  730. align-items: center;
  731. justify-content: center;
  732. color: #fff;
  733. font-size: 18px;
  734. z-index: 2;
  735. box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
  736. }
  737. /* 加载更多样式 */
  738. .loading-more {
  739. display: flex;
  740. align-items: center;
  741. justify-content: center;
  742. gap: 10px;
  743. padding: 20px 0;
  744. color: #7ec8ff;
  745. font-size: 14px;
  746. }
  747. .loading-more-spinner {
  748. width: 20px;
  749. height: 20px;
  750. border: 2px solid rgba(30, 144, 255, 0.3);
  751. border-top-color: #00bfff;
  752. border-radius: 50%;
  753. animation: spin 1s linear infinite;
  754. }
  755. /* 没有更多数据 */
  756. .no-more-data {
  757. display: flex;
  758. align-items: center;
  759. justify-content: center;
  760. padding: 20px 0;
  761. color: #5a8cba;
  762. font-size: 14px;
  763. }
  764. /* 分页信息栏 */
  765. .pagination-info-bar {
  766. background: rgba(9, 61, 140, 0.3);
  767. border: 1px solid #049FD8;
  768. border-radius: 8px;
  769. padding: 10px 20px;
  770. display: flex;
  771. justify-content: space-between;
  772. align-items: center;
  773. flex-shrink: 0;
  774. margin-top: 10px;
  775. color: #7ec8ff;
  776. font-size: 14px;
  777. }
  778. .pagination-controls {
  779. display: flex;
  780. align-items: center;
  781. gap: 10px;
  782. }
  783. .page-btn {
  784. width: 32px;
  785. height: 32px;
  786. background: rgba(13, 58, 106, 0.8);
  787. border: 1px solid #2a7fff;
  788. border-radius: 6px;
  789. color: #7ec8ff;
  790. font-size: 14px;
  791. cursor: pointer;
  792. transition: all 0.3s;
  793. }
  794. .page-btn:hover:not(:disabled) {
  795. background: rgba(30, 144, 255, 0.3);
  796. border-color: #00bfff;
  797. }
  798. .page-btn:disabled {
  799. opacity: 0.5;
  800. cursor: not-allowed;
  801. }
  802. .page-current {
  803. color: #00bfff;
  804. font-weight: bold;
  805. }
  806. /* ========== 底部操作按钮 ========== */
  807. .bottom-actions {
  808. width: 100%;
  809. padding: 15px 30px;
  810. box-sizing: border-box;
  811. background: rgba(4, 28, 61, 0.95);
  812. z-index: 20;
  813. flex-shrink: 0;
  814. }
  815. .submit-btn {
  816. width: 100%;
  817. padding: 18px 0;
  818. background: #4a99e2;
  819. border: none;
  820. border-radius: 12px;
  821. font-size: 24px;
  822. font-weight: bold;
  823. color: #fff;
  824. cursor: pointer;
  825. transition: all 0.3s;
  826. letter-spacing: 8px;
  827. text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  828. }
  829. .submit-btn:hover:not(:disabled) {
  830. box-shadow: 0 0 30px rgba(74, 153, 226, 0.6);
  831. transform: translateY(-2px);
  832. }
  833. .submit-btn:disabled {
  834. opacity: 0.5;
  835. cursor: not-allowed;
  836. }
  837. /* ========== 科技感弹窗样式 ========== */
  838. .tech-modal-overlay {
  839. position: fixed;
  840. top: 0;
  841. left: 0;
  842. width: 100%;
  843. height: 100%;
  844. background: rgba(4, 28, 61, 0.9);
  845. display: flex;
  846. align-items: center;
  847. justify-content: center;
  848. z-index: 2000;
  849. }
  850. .tech-modal {
  851. background: linear-gradient(135deg, #0a3d6d 0%, #041c3d 100%);
  852. border: 1px solid #049FD8;
  853. border-radius: 16px;
  854. width: 90%;
  855. max-width: 500px;
  856. position: relative;
  857. box-shadow: 0 0 40px rgba(4, 159, 216, 0.3);
  858. }
  859. .modal-close-btn {
  860. position: absolute;
  861. top: 15px;
  862. right: 15px;
  863. width: 36px;
  864. height: 36px;
  865. background: rgba(255, 255, 255, 0.1);
  866. border: none;
  867. border-radius: 50%;
  868. color: #7ec8ff;
  869. font-size: 18px;
  870. cursor: pointer;
  871. transition: all 0.3s;
  872. display: flex;
  873. align-items: center;
  874. justify-content: center;
  875. }
  876. .modal-close-btn:hover {
  877. background: rgba(255, 255, 255, 0.2);
  878. color: #fff;
  879. }
  880. .modal-content-row {
  881. padding: 30px;
  882. display: flex;
  883. align-items: center;
  884. gap: 30px;
  885. }
  886. .modal-text {
  887. flex: 1;
  888. }
  889. .modal-text h3 {
  890. font-size: 22px;
  891. font-weight: bold;
  892. color: #fff;
  893. margin: 0 0 12px 0;
  894. }
  895. .modal-text p {
  896. font-size: 15px;
  897. color: #7ec8ff;
  898. margin: 0;
  899. line-height: 1.6;
  900. }
  901. .modal-text .highlight {
  902. color: #00bfff;
  903. font-weight: bold;
  904. }
  905. .modal-icon {
  906. flex-shrink: 0;
  907. }
  908. .icon-box {
  909. width: 80px;
  910. height: 80px;
  911. background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
  912. border-radius: 12px;
  913. display: flex;
  914. align-items: center;
  915. justify-content: center;
  916. font-size: 36px;
  917. color: #fff;
  918. position: relative;
  919. transform: perspective(100px) rotateY(-10deg);
  920. box-shadow: 0 10px 30px rgba(0, 191, 255, 0.3);
  921. }
  922. .icon-box .check-icon {
  923. position: absolute;
  924. bottom: -5px;
  925. right: -5px;
  926. font-size: 24px;
  927. color: #10b981;
  928. background: #041c3d;
  929. border-radius: 50%;
  930. padding: 2px;
  931. }
  932. .modal-footer {
  933. padding: 20px 30px 30px;
  934. display: flex;
  935. gap: 15px;
  936. }
  937. .modal-btn {
  938. flex: 1;
  939. padding: 14px 0;
  940. border-radius: 8px;
  941. font-size: 16px;
  942. font-weight: 600;
  943. cursor: pointer;
  944. transition: all 0.3s;
  945. }
  946. .modal-btn.cancel {
  947. background: transparent;
  948. border: 1px solid #2a7fff;
  949. color: #7ec8ff;
  950. }
  951. .modal-btn.cancel:hover {
  952. background: rgba(42, 127, 255, 0.2);
  953. }
  954. .modal-btn.confirm {
  955. background: linear-gradient(90deg, #1e90ff 0%, #00bfff 100%);
  956. border: none;
  957. color: #fff;
  958. }
  959. .modal-btn.confirm:hover {
  960. box-shadow: 0 0 20px rgba(30, 144, 255, 0.5);
  961. }
  962. /* ========== 响应式 - 横屏 ========== */
  963. @media screen and (orientation: landscape) {
  964. .header-section {
  965. padding: 10px 20px;
  966. }
  967. .page-title {
  968. font-size: 22px;
  969. }
  970. .logout-btn {
  971. padding: 6px 14px;
  972. font-size: 13px;
  973. left: 20px;
  974. }
  975. .main-content {
  976. padding: 0 20px;
  977. }
  978. .filter-section {
  979. padding: 12px 15px;
  980. margin-bottom: 10px;
  981. }
  982. .filter-label {
  983. font-size: 13px;
  984. }
  985. .filter-input.dark-input {
  986. width: 150px;
  987. padding: 6px 10px;
  988. font-size: 12px;
  989. }
  990. .filter-select {
  991. width: 150px;
  992. }
  993. .search-btn,
  994. .reset-btn {
  995. padding: 6px 20px;
  996. font-size: 13px;
  997. }
  998. .card-grid {
  999. grid-template-columns: repeat(6, 1fr);
  1000. gap: 15px;
  1001. }
  1002. .card-index {
  1003. width: 24px;
  1004. height: 24px;
  1005. font-size: 11px;
  1006. top: 6px;
  1007. left: 6px;
  1008. }
  1009. .card-image {
  1010. margin: 10px;
  1011. margin-bottom: 8px;
  1012. width: calc(100% - 20px);
  1013. }
  1014. .image-placeholder {
  1015. font-size: 36px;
  1016. }
  1017. .card-info {
  1018. padding: 6px 10px 8px;
  1019. }
  1020. .info-row {
  1021. font-size: 11px;
  1022. margin-bottom: 3px;
  1023. }
  1024. .selected-indicator {
  1025. width: 24px;
  1026. height: 24px;
  1027. font-size: 12px;
  1028. top: 6px;
  1029. right: 6px;
  1030. }
  1031. .bottom-actions {
  1032. padding: 10px 20px;
  1033. }
  1034. .submit-btn {
  1035. padding: 12px 0;
  1036. font-size: 18px;
  1037. letter-spacing: 6px;
  1038. border-radius: 8px;
  1039. }
  1040. }
  1041. </style>
  1042. <style>
  1043. /* 全局样式:确保 v-select 下拉菜单在模态框之上 */
  1044. .vs__dropdown-menu {
  1045. z-index: 99999 !important;
  1046. }
  1047. .v-select .vs__dropdown-menu {
  1048. z-index: 99999 !important;
  1049. }
  1050. /* 确保附加到 body 的下拉菜单在最上层 */
  1051. body > .v-select-menu {
  1052. z-index: 99999 !important;
  1053. }
  1054. body > .vs__dropdown-menu {
  1055. z-index: 99999 !important;
  1056. }
  1057. /* vue-select 的下拉菜单容器 */
  1058. .vs--open .vs__dropdown-menu {
  1059. z-index: 99999 !important;
  1060. }
  1061. </style>