OrderPicking.vue 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444
  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. <v-select
  22. v-model="filterForm.inventoryType" :options="inventoryTypeList" :reduce="item => item.value"
  23. label="label" placeholder="选择类型" :clearable="true" class="filter-select dark-select"
  24. @update:model-value="getList"
  25. />
  26. </div>
  27. <div class="filter-item">
  28. <label class="filter-label">名称</label>
  29. <input
  30. v-model="filterForm.inventoryName" type="text" class="filter-input dark-input" placeholder="输入名称"
  31. @keyup.enter="getList"
  32. />
  33. </div>
  34. <!-- <div class="filter-item">
  35. <label class="filter-label">编号</label>
  36. <input
  37. v-model="filterForm.inventoryNo"
  38. type="text"
  39. class="filter-input dark-input"
  40. placeholder="输入编号"
  41. @keyup.enter="getList"
  42. />
  43. </div>
  44. <div class="filter-item">
  45. <label class="filter-label">仓库</label>
  46. <v-select
  47. v-model="filterForm.warehouseId"
  48. :options="warehouseList"
  49. :reduce="item => item.id"
  50. label="name"
  51. placeholder="选择仓库"
  52. :clearable="true"
  53. class="filter-select dark-select"
  54. />
  55. </div> -->
  56. <div class="filter-buttons">
  57. <button class="search-btn" @click="getList">
  58. 搜索
  59. </button>
  60. <button class="reset-btn" @click="handleReset">
  61. 重置
  62. </button>
  63. </div>
  64. </div>
  65. </div>
  66. <!-- 全选控制栏 -->
  67. <div v-if="materialList.length > 0" class="select-all-bar">
  68. <van-checkbox :model-value="isAllSelected" @update:model-value="toggleSelectAll">
  69. <span class="select-all-text">全选当前页(已选 {{ selectedIds.length }} 项)</span>
  70. </van-checkbox>
  71. </div>
  72. <!-- 卡片网格区域 -->
  73. <div class="card-grid-wrapper">
  74. <!-- 空状态 -->
  75. <div v-if="materialList.length === 0 && !loading" class="empty-state">
  76. <i class="fas fa-inbox empty-icon" />
  77. <p>暂无数据</p>
  78. </div>
  79. <!-- 卡片网格 -->
  80. <div v-else class="card-grid">
  81. <div
  82. v-for="(item, index) in materialList" :key="item.id || index" class="inventory-card"
  83. :class="{ 'selected': selectedIds.includes(item.id) }" @click="toggleSelect(item.id)"
  84. >
  85. <!-- 卡片序号 -->
  86. <div class="card-index">{{ index + 1 }}</div>
  87. <!-- 选中状态指示 -->
  88. <div v-if="selectedIds.includes(item.id)" class="selected-indicator">
  89. <i class="fas fa-check" />
  90. </div>
  91. <!-- 图片区域 -->
  92. <div class="card-image">
  93. <img v-if="item.imageUrl" :src="item.imageUrl" alt="设备图片" />
  94. <div v-else class="image-placeholder">
  95. <i :class="getInventoryIcon(item.inventoryType)" />
  96. </div>
  97. </div>
  98. <!-- 信息区域 -->
  99. <div class="card-info">
  100. <div class="info-row">
  101. <span class="info-label">名称:</span>
  102. <span class="info-value">{{ item.inventoryName || '-' }}</span>
  103. </div>
  104. <div class="info-row">
  105. <span class="info-label">编号:</span>
  106. <span class="info-value">{{ item.inventoryNo || '-' }}</span>
  107. </div>
  108. <div class="info-row location-row">
  109. <i class="fas fa-map-marker-alt location-icon" />
  110. <span class="info-value">{{ item.positionName || '-' }} / {{ item.warehouseName || '-' }}</span>
  111. </div>
  112. <!-- 配送类型徽章 -->
  113. <div class="info-row delivery-type-row">
  114. <span class="delivery-type-badge" :class="getDeliveryTypeBadgeClass(item.deliveryType)">
  115. {{ item.deliveryType || '未指定' }}
  116. </span>
  117. </div>
  118. </div>
  119. <!-- 配送选择区域(固定高度,始终存在) -->
  120. <div class="card-delivery-section" @click.stop>
  121. <!-- 选中时显示配送选择 -->
  122. <template v-if="selectedIds.includes(item.id)">
  123. <div class="delivery-row">
  124. <span class="delivery-label">配送:</span>
  125. <div class="delivery-method-buttons">
  126. <button
  127. :class="['delivery-btn', { 'active': item.deliveryMethod === 'AGV_Delivery', 'disabled': item.deliveryType === '人工配送' }]"
  128. :disabled="item.deliveryType === '人工配送'" @click="changeDeliveryMethod(item, 'AGV_Delivery')"
  129. >
  130. <i class="fas fa-robot" /> AGV
  131. </button>
  132. <button
  133. :class="['delivery-btn', { 'active': item.deliveryMethod === 'Manual_Delivery', 'disabled': item.deliveryType === '强制AGV配送' }]"
  134. :disabled="item.deliveryType === '强制AGV配送'"
  135. @click="changeDeliveryMethod(item, 'Manual_Delivery')"
  136. >
  137. <i class="fas fa-user" /> 人工
  138. </button>
  139. </div>
  140. </div>
  141. <!-- AGV配送位置选择 -->
  142. <div class="delivery-location-row">
  143. <span class="delivery-label">位置:</span>
  144. <v-select
  145. v-if="item.deliveryMethod === 'AGV_Delivery'" v-model="item.selectedLocation"
  146. :options="locator" :clearable="false" class="location-select dark-select" :append-to-body="true"
  147. :calculate-position="withPopper"
  148. />
  149. <span v-else style="font-size: 16px;">人工配送</span>
  150. </div>
  151. </template>
  152. <!-- 未选中时显示占位 -->
  153. <template v-else>
  154. <div class="delivery-placeholder">
  155. <span>点击选择后设置配送方式</span>
  156. </div>
  157. </template>
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. </main>
  163. <!-- 底部操作按钮 -->
  164. <div class="bottom-actions">
  165. <button class="submit-btn" :disabled="selectedIds.length === 0" @click="handleComplete">
  166. 拣货 ({{ selectedIds.length }})
  167. </button>
  168. </div>
  169. <!-- 拣货完成确认弹窗 - 科技感风格 -->
  170. <div v-if="completeModalVisible" class="tech-modal-overlay" @click.self="handleCompleteCancel">
  171. <div class="tech-modal success-modal">
  172. <div class="modal-content-row">
  173. <div class="modal-text">
  174. <h3>拣货完成</h3>
  175. <p>领料申请已完成,配送任务已创建!<br />请确认是否返回主页?</p>
  176. </div>
  177. <div class="modal-icon">
  178. <div class="icon-box success-box">
  179. <i class="fas fa-truck-loading" />
  180. <i class="fas fa-check check-icon" />
  181. </div>
  182. </div>
  183. </div>
  184. <div class="modal-footer">
  185. <button class="modal-btn cancel-btn" @click="handleCompleteCancel">取消</button>
  186. <button class="modal-btn confirm-btn" @click="handleCompleteConfirm">确认</button>
  187. </div>
  188. </div>
  189. </div>
  190. <!-- Loading -->
  191. <div v-if="loading" class="loading-overlay">
  192. <div class="loading-dots">
  193. <div class="dot" />
  194. <div class="dot" />
  195. <div class="dot" />
  196. </div>
  197. <span class="loading-text">加载中...</span>
  198. </div>
  199. </div>
  200. </template>
  201. <script setup>
  202. import { useRouter } from 'vue-router';
  203. import { ref, reactive, computed, onMounted } from 'vue';
  204. import { showNotify } from 'vant';
  205. import vSelect from 'vue-select';
  206. import 'vue-select/dist/vue-select.css';
  207. import { createPopper } from '@popperjs/core';
  208. import { list, createStockOut } from '../api/stockOut.js';
  209. import { getWarehouseList, queryIdleLocator } from '../api/stock.js';
  210. // 图片资源
  211. import bgImg from '../assets/images/bj.png';
  212. const router = useRouter();
  213. // 返回主页
  214. const goHome = () => {
  215. router.push('/home');
  216. };
  217. // Popper.js 配置
  218. const withPopper = (dropdownList, component, { width }) => {
  219. dropdownList.style.width = width;
  220. dropdownList.style.zIndex = '9999';
  221. const popper = createPopper(component.$refs.toggle, dropdownList, {
  222. placement: 'bottom',
  223. modifiers: [
  224. {
  225. name: 'offset',
  226. options: { offset: [0, -1] },
  227. },
  228. {
  229. name: 'toggleClass',
  230. enabled: true,
  231. phase: 'write',
  232. fn({ state }) {
  233. component.$el.classList.toggle('drop-up', state.placement === 'top');
  234. },
  235. },
  236. ],
  237. });
  238. return () => popper.destroy();
  239. };
  240. const warehouseList = ref([]);
  241. // 筛选表单
  242. const filterForm = reactive({
  243. inventoryName: '',
  244. inventoryNo: '',
  245. inventoryType: undefined,
  246. warehouseId: undefined,
  247. positionName: '',
  248. });
  249. const inventoryTypeList = ref([
  250. { value: 'Clamp', label: '工装' },
  251. { value: 'Instrument', label: '设备' },
  252. // { value: 'FinishProduct', label: '成品' },
  253. ]);
  254. // 表格加载状态
  255. const loading = ref(false);
  256. // 完成弹窗控制
  257. const completeModalVisible = ref(false);
  258. // 物料列表数据
  259. const materialList = ref([]);
  260. // 分页配置
  261. const pagination = reactive({
  262. start: 1,
  263. lang: 10,
  264. total: 0,
  265. });
  266. // 所选集合
  267. const selectedIds = ref([]);
  268. const selectedRows = ref([]);
  269. // 计算是否全选
  270. const isAllSelected = computed(() => {
  271. if (materialList.value.length === 0) return false;
  272. return materialList.value.every(item => selectedIds.value.includes(item.id));
  273. });
  274. // 可配送位置
  275. const locator = ref([]);
  276. // 计算激活的筛选条件数量
  277. const getActiveFilterCount = () => {
  278. let count = 0;
  279. if (filterForm.inventoryName) count++;
  280. if (filterForm.inventoryNo) count++;
  281. if (filterForm.inventoryType) count++;
  282. if (filterForm.warehouseId) count++;
  283. if (filterForm.positionName) count++;
  284. return count;
  285. };
  286. // 根据 deliveryType 初始化配送方式
  287. const initDeliveryMethod = item => {
  288. if (item.deliveryType === '人工配送') {
  289. // 人工配送:只能人工,不可切换
  290. item.deliveryMethod = 'Manual_Delivery';
  291. item.selectedLocation = '';
  292. } else if (item.deliveryType === '强制AGV配送') {
  293. // 强制AGV配送:只能AGV,不可切换为人工
  294. item.deliveryMethod = 'AGV_Delivery';
  295. if (!item.selectedLocation) {
  296. item.selectedLocation = '';
  297. }
  298. } else if (item.deliveryType === '可选AGV配送') {
  299. // 可选AGV配送:默认人工,可切换为AGV
  300. item.deliveryMethod = item.deliveryMethod || 'Manual_Delivery';
  301. item.selectedLocation = item.selectedLocation || '';
  302. }
  303. };
  304. // 切换选择
  305. const toggleSelect = id => {
  306. const index = selectedIds.value.indexOf(id);
  307. if (index > -1) {
  308. selectedIds.value.splice(index, 1);
  309. // 同时移除 selectedRows 中对应的项
  310. selectedRows.value = selectedRows.value.filter(item => item.id !== id);
  311. } else {
  312. selectedIds.value.push(id);
  313. // 添加到 selectedRows 并初始化配送字段
  314. const item = materialList.value.find(item => item.id === id);
  315. if (item) {
  316. // 初始化配送字段
  317. initDeliveryMethod(item);
  318. selectedRows.value.push(item);
  319. }
  320. }
  321. };
  322. // 处理复选框变化
  323. const handleCheckboxChange = (checked, id) => {
  324. if (checked) {
  325. if (!selectedIds.value.includes(id)) {
  326. selectedIds.value.push(id);
  327. const item = materialList.value.find(item => item.id === id);
  328. if (item) {
  329. // 初始化配送字段
  330. initDeliveryMethod(item);
  331. selectedRows.value.push(item);
  332. }
  333. }
  334. } else {
  335. const index = selectedIds.value.indexOf(id);
  336. if (index > -1) {
  337. selectedIds.value.splice(index, 1);
  338. selectedRows.value = selectedRows.value.filter(item => item.id !== id);
  339. }
  340. }
  341. };
  342. // 全选/取消全选
  343. const toggleSelectAll = checked => {
  344. if (checked) {
  345. // 全选当前页所有项(累加模式)
  346. materialList.value.forEach(item => {
  347. if (!selectedIds.value.includes(item.id)) {
  348. selectedIds.value.push(item.id);
  349. initDeliveryMethod(item);
  350. selectedRows.value.push(item);
  351. }
  352. });
  353. } else {
  354. // 取消全选当前页
  355. const currentPageIds = materialList.value.map(item => item.id);
  356. selectedIds.value = selectedIds.value.filter(id => !currentPageIds.includes(id));
  357. selectedRows.value = selectedRows.value.filter(row => !currentPageIds.includes(row.id));
  358. }
  359. };
  360. // 获取设备类型图标
  361. const getInventoryIcon = type => {
  362. const iconMap = {
  363. '工装': 'fas fa-cube',
  364. '设备': 'fas fa-cogs',
  365. '成品': 'fas fa-box',
  366. };
  367. return iconMap[type] || 'fas fa-cube';
  368. };
  369. // 获取配送类型徽章样式
  370. const getDeliveryTypeBadgeClass = deliveryType => {
  371. const classMap = {
  372. '人工配送': 'badge-manual',
  373. '强制AGV配送': 'badge-agv-force',
  374. '可选AGV配送': 'badge-agv-optional',
  375. };
  376. return classMap[deliveryType] || '';
  377. };
  378. // 重置筛选条件
  379. const handleReset = () => {
  380. filterForm.inventoryName = '';
  381. filterForm.inventoryNo = '';
  382. filterForm.inventoryType = undefined;
  383. filterForm.warehouseId = undefined;
  384. filterForm.positionName = '';
  385. getList();
  386. };
  387. // 加入领料申请
  388. const addToRequisition = record => {
  389. if (!selectedIds.value.includes(record.id)) {
  390. selectedIds.value.push(record.id);
  391. // 初始化配送字段
  392. initDeliveryMethod(record);
  393. selectedRows.value.push(record);
  394. }
  395. };
  396. // 领料申请,直接验证并提交
  397. const handleComplete = async () => {
  398. if (selectedIds.value.length === 0) {
  399. showNotify({ type: 'danger', message: '请至少选择一个物料' });
  400. return;
  401. }
  402. // 验证选择了 AGV 配送的物料是否都选择了配送位置
  403. const agvItems = selectedRows.value.filter(item => item.deliveryMethod === 'AGV_Delivery');
  404. const hasEmptyLocation = agvItems.some(item => !item.selectedLocation);
  405. if (hasEmptyLocation) {
  406. showNotify({ type: 'danger', message: '请为所有 AGV 配送的物料选择配送位置' });
  407. return;
  408. }
  409. const params = [];
  410. selectedRows.value.forEach(item => {
  411. params.push({
  412. stockOutPrepareLineId: item.id,
  413. deliveryMethod: item.deliveryMethod,
  414. positionEndNo: item.selectedLocation.value,
  415. });
  416. });
  417. console.log('提交参数:', params);
  418. await generateCFStockOut(params);
  419. };
  420. // 配送方式变更处理
  421. const changeDeliveryMethod = (item, method) => {
  422. item.deliveryMethod = method;
  423. // 当选择人工配送时,清空配送位置
  424. if (method === 'Manual_Delivery') {
  425. item.selectedLocation = '';
  426. }
  427. console.log('配送方式变更:', item.inventoryName, item.deliveryMethod);
  428. };
  429. // 获取物料列表
  430. const getList = async () => {
  431. loading.value = true;
  432. try {
  433. const params = {
  434. ...filterForm,
  435. userId: JSON.parse(localStorage.getItem('#LoginInfo')).userId,
  436. };
  437. const res = await list(params);
  438. loading.value = false;
  439. if (res.errorCode === 0) {
  440. if (res.datas && res.datas.length > 0) {
  441. const testData = res.datas;
  442. // ========== 测试数据模拟开始 ==========
  443. // const testData = res.datas.map((item, index) => {
  444. // const types = ['人工配送', '强制AGV配送', '可选AGV配送'];
  445. // return {
  446. // ...item,
  447. // // 循环分配不同的配送类型用于测试
  448. // deliveryType: types[index % 3] || item.deliveryType,
  449. // };
  450. // });
  451. // ========== 测试数据模拟结束 ==========
  452. // 保留已选中物料的配送方式设置
  453. materialList.value = testData.map(item => {
  454. // 查找该物料是否在已选中列表中
  455. const selectedItem = selectedRows.value.find(selected => selected.id === item.id);
  456. if (selectedItem) {
  457. // 如果已选中,保留其配送方式和位置设置,并更新 selectedRows 中的引用
  458. const updatedItem = {
  459. ...item,
  460. deliveryMethod: selectedItem.deliveryMethod,
  461. selectedLocation: selectedItem.selectedLocation,
  462. };
  463. // 更新 selectedRows 中的对应项,保持引用同步
  464. const index = selectedRows.value.findIndex(row => row.id === item.id);
  465. if (index !== -1) {
  466. selectedRows.value[index] = updatedItem;
  467. }
  468. return updatedItem;
  469. } else {
  470. // 未选中,初始化为空
  471. return {
  472. ...item,
  473. deliveryMethod: '',
  474. selectedLocation: '',
  475. };
  476. }
  477. });
  478. pagination.total = materialList.value.length;
  479. } else {
  480. materialList.value = [];
  481. }
  482. } else {
  483. showNotify({ type: 'danger', message: res.errorMessage });
  484. }
  485. } catch (error) {
  486. loading.value = false;
  487. console.error('获取物料列表API调用失败:', error);
  488. showNotify({ type: 'danger', message: '获取物料列表API调用失败' });
  489. return;
  490. }
  491. };
  492. // 生成CF出库单
  493. const generateCFStockOut = async params => {
  494. loading.value = true;
  495. try {
  496. const res = await createStockOut(params);
  497. if (res.errorCode === 0) {
  498. showNotify({ type: 'success', message: '领料完成' });
  499. selectedIds.value = [];
  500. selectedRows.value = [];
  501. // 显示完成确认弹窗
  502. completeModalVisible.value = true;
  503. } else {
  504. showNotify({ type: 'danger', message: res.errorMessage });
  505. }
  506. } catch (error) {
  507. console.error('生成CF出库单API调用失败:', error);
  508. showNotify({ type: 'danger', message: '生成CF出库单API调用失败' });
  509. } finally {
  510. loading.value = false;
  511. }
  512. };
  513. // 处理完成确认
  514. const handleCompleteConfirm = () => {
  515. completeModalVisible.value = false;
  516. router.push('/home');
  517. // showToast('领料申请已完成,配送任务已创建!');
  518. };
  519. // 处理完成取消
  520. const handleCompleteCancel = () => {
  521. completeModalVisible.value = false;
  522. // 留在当前页面,刷新列表
  523. getList();
  524. };
  525. // 获取仓库列表
  526. const getWarehouse = async () => {
  527. try {
  528. const res = await getWarehouseList();
  529. if (res.errorCode === 0) {
  530. if (res.datas && res.datas.length > 0) {
  531. warehouseList.value = res.datas;
  532. } else {
  533. warehouseList.value = [];
  534. }
  535. } else {
  536. showNotify({ type: 'danger', message: res.errorMessage });
  537. }
  538. } catch (error) {
  539. console.error('获取仓库列表API调用失败:', error);
  540. showNotify({ type: 'danger', message: '获取仓库列表API调用失败' });
  541. } finally {
  542. loading.value = false;
  543. }
  544. };
  545. // 获取空闲货位
  546. const getIdleLocator = async () => {
  547. try {
  548. const res = await queryIdleLocator();
  549. if (res.errorCode === 0) {
  550. if (res.datas && res.datas.length > 0) {
  551. locator.value = res.datas.map(item => {
  552. return {
  553. label: item.positionName,
  554. value: item.positionNo,
  555. };
  556. });
  557. } else {
  558. locator.value = [];
  559. }
  560. } else {
  561. showNotify({ type: 'danger', message: res.errorMessage });
  562. }
  563. } catch (error) {
  564. console.error('获取空闲货位API调用失败:', error);
  565. showNotify({ type: 'danger', message: '获取空闲货位API调用失败' });
  566. }
  567. };
  568. onMounted(() => {
  569. getWarehouse();
  570. getList();
  571. getIdleLocator();
  572. });
  573. </script>
  574. <style scoped>
  575. /* ========== 基础样式 ========== */
  576. .stock-requisition-page {
  577. width: 100%;
  578. height: 100vh;
  579. max-height: 100vh;
  580. position: relative;
  581. font-family: 'Microsoft YaHei', sans-serif;
  582. color: #fff;
  583. overflow: hidden;
  584. display: flex;
  585. flex-direction: column;
  586. }
  587. /* 背景层 */
  588. .bg-layer {
  589. position: fixed;
  590. top: 0;
  591. left: 0;
  592. width: 100%;
  593. height: 100%;
  594. background-color: #041c3d;
  595. background-size: cover;
  596. background-position: center;
  597. background-repeat: no-repeat;
  598. z-index: 0;
  599. }
  600. /* ========== 主内容区域 ========== */
  601. .main-content {
  602. flex: 1;
  603. display: flex;
  604. flex-direction: column;
  605. padding: 0 30px;
  606. position: relative;
  607. z-index: 10;
  608. min-height: 0;
  609. overflow: hidden;
  610. }
  611. /* ========== 筛选区域 ========== */
  612. .filter-section {
  613. background: rgba(9, 61, 140, 0.5);
  614. border: 1px solid #049FD8;
  615. border-radius: 12px;
  616. padding: 20px;
  617. margin-bottom: 15px;
  618. flex-shrink: 0;
  619. }
  620. .filter-row {
  621. display: flex;
  622. flex-wrap: wrap;
  623. align-items: center;
  624. gap: 20px;
  625. }
  626. .filter-item {
  627. display: flex;
  628. align-items: center;
  629. gap: 10px;
  630. }
  631. .filter-label {
  632. font-size: 16px;
  633. color: #7ec8ff;
  634. white-space: nowrap;
  635. font-weight: 500;
  636. }
  637. .filter-select {
  638. width: 180px;
  639. }
  640. .filter-input.dark-input {
  641. width: 180px;
  642. padding: 10px 15px;
  643. background: rgba(13, 58, 106, 0.8);
  644. border: 1px solid #2a7fff;
  645. border-radius: 6px;
  646. color: #fff;
  647. font-size: 14px;
  648. outline: none;
  649. transition: all 0.3s;
  650. }
  651. .filter-input.dark-input::placeholder {
  652. color: #7ec8ff;
  653. opacity: 0.7;
  654. }
  655. .filter-input.dark-input:focus {
  656. border-color: #00bfff;
  657. box-shadow: 0 0 10px rgba(0, 191, 255, 0.3);
  658. }
  659. .filter-buttons {
  660. display: flex;
  661. gap: 15px;
  662. margin-left: auto;
  663. }
  664. .search-btn,
  665. .reset-btn {
  666. padding: 10px 30px;
  667. border-radius: 6px;
  668. font-size: 16px;
  669. font-weight: 500;
  670. cursor: pointer;
  671. transition: all 0.3s;
  672. border: none;
  673. display: flex;
  674. align-items: center;
  675. gap: 8px;
  676. }
  677. .search-btn {
  678. background: linear-gradient(90deg, #1e90ff 0%, #00bfff 100%);
  679. color: #fff;
  680. }
  681. .search-btn:hover {
  682. box-shadow: 0 0 15px rgba(30, 144, 255, 0.6);
  683. }
  684. .reset-btn {
  685. background: rgba(13, 58, 106, 0.8);
  686. border: 1px solid #2a7fff;
  687. color: #fff;
  688. }
  689. .reset-btn:hover {
  690. background: rgba(26, 74, 122, 0.8);
  691. }
  692. /* 全选控制栏 */
  693. .select-all-bar {
  694. background: rgba(9, 61, 140, 0.3);
  695. border: 1px solid #049FD8;
  696. border-radius: 8px;
  697. padding: 10px 20px;
  698. margin-bottom: 10px;
  699. display: flex;
  700. align-items: center;
  701. flex-shrink: 0;
  702. }
  703. .select-all-text {
  704. font-size: 14px;
  705. color: #7ec8ff;
  706. font-weight: 500;
  707. }
  708. :deep(.select-all-bar .van-checkbox__label) {
  709. color: #7ec8ff;
  710. }
  711. /* ========== 卡片网格区域 ========== */
  712. .card-grid-wrapper {
  713. flex: 1;
  714. display: flex;
  715. flex-direction: column;
  716. min-height: 0;
  717. overflow-y: auto;
  718. overflow-x: hidden;
  719. padding-bottom: 10px;
  720. scrollbar-width: none;
  721. -ms-overflow-style: none;
  722. }
  723. .card-grid-wrapper::-webkit-scrollbar {
  724. display: none;
  725. }
  726. /* 空状态 */
  727. .empty-state {
  728. flex: 1;
  729. display: flex;
  730. flex-direction: column;
  731. align-items: center;
  732. justify-content: center;
  733. color: #7ec8ff;
  734. padding: 60px 0;
  735. }
  736. .empty-icon {
  737. font-size: 80px;
  738. margin-bottom: 20px;
  739. opacity: 0.5;
  740. }
  741. .empty-state p {
  742. font-size: 18px;
  743. margin: 0;
  744. }
  745. /* 卡片网格 - 4列布局 */
  746. .card-grid {
  747. display: grid;
  748. grid-template-columns: repeat(4, 1fr);
  749. gap: 18px;
  750. }
  751. /* ========== 设备卡片样式 ========== */
  752. .inventory-card {
  753. background: rgba(5, 30, 60, 0.8);
  754. border: 2px solid #0d4a8a;
  755. border-radius: 12px;
  756. overflow: hidden;
  757. cursor: pointer;
  758. transition: all 0.3s ease;
  759. position: relative;
  760. }
  761. .inventory-card:hover {
  762. border-color: #1e90ff;
  763. box-shadow: 0 0 20px rgba(30, 144, 255, 0.3);
  764. transform: translateY(-3px);
  765. }
  766. .inventory-card.selected {
  767. border-color: #00bfff;
  768. box-shadow: 0 0 25px rgba(0, 191, 255, 0.5);
  769. background: rgba(10, 50, 100, 0.9);
  770. }
  771. /* 卡片序号 - 左上角 */
  772. .card-index {
  773. position: absolute;
  774. top: 10px;
  775. left: 10px;
  776. width: 36px;
  777. height: 36px;
  778. background: linear-gradient(135deg, #1e90ff 0%, #00bfff 100%);
  779. border-radius: 50%;
  780. display: flex;
  781. align-items: center;
  782. justify-content: center;
  783. font-size: 16px;
  784. font-weight: bold;
  785. color: #fff;
  786. z-index: 2;
  787. box-shadow: 0 2px 8px rgba(0, 191, 255, 0.4);
  788. }
  789. /* 图片区域 */
  790. .card-image {
  791. width: 100%;
  792. aspect-ratio: 3 / 4;
  793. background: #fff;
  794. display: flex;
  795. align-items: center;
  796. justify-content: center;
  797. overflow: hidden;
  798. margin: 15px;
  799. margin-bottom: 10px;
  800. border-radius: 8px;
  801. width: calc(100% - 30px);
  802. }
  803. .card-image img {
  804. width: 100%;
  805. height: 100%;
  806. object-fit: cover;
  807. }
  808. .image-placeholder {
  809. display: flex;
  810. align-items: center;
  811. justify-content: center;
  812. width: 100%;
  813. height: 100%;
  814. background: linear-gradient(135deg, #f0f4f8 0%, #e8ecf0 100%);
  815. color: #94a3b8;
  816. font-size: 48px;
  817. }
  818. /* 信息区域 */
  819. .card-info {
  820. padding: 10px 15px 15px;
  821. }
  822. .info-row {
  823. display: flex;
  824. align-items: baseline;
  825. margin-bottom: 6px;
  826. font-size: 14px;
  827. }
  828. .info-row:last-child {
  829. margin-bottom: 0;
  830. }
  831. .info-label {
  832. color: #7ec8ff;
  833. white-space: nowrap;
  834. margin-right: 5px;
  835. }
  836. .info-value {
  837. color: #fff;
  838. flex: 1;
  839. overflow: hidden;
  840. text-overflow: ellipsis;
  841. white-space: nowrap;
  842. }
  843. /* 位置行样式 */
  844. .location-row {
  845. display: flex;
  846. align-items: center;
  847. margin-top: 4px;
  848. padding-top: 4px;
  849. border-top: 1px solid rgba(30, 144, 255, 0.2);
  850. }
  851. .location-icon {
  852. color: #00bfff;
  853. font-size: 12px;
  854. margin-right: 6px;
  855. }
  856. /* 配送类型行样式 */
  857. .delivery-type-row {
  858. margin-top: 6px;
  859. padding-top: 6px;
  860. border-top: 1px solid rgba(30, 144, 255, 0.2);
  861. justify-content: center;
  862. }
  863. /* 选中状态指示 */
  864. .selected-indicator {
  865. position: absolute;
  866. top: 10px;
  867. right: 10px;
  868. width: 28px;
  869. height: 28px;
  870. background: linear-gradient(135deg, #00ff88 0%, #00cc6a 100%);
  871. border-radius: 50%;
  872. display: flex;
  873. align-items: center;
  874. justify-content: center;
  875. font-size: 14px;
  876. color: #fff;
  877. z-index: 2;
  878. box-shadow: 0 2px 8px rgba(0, 255, 136, 0.4);
  879. }
  880. /* ========== 配送方式选择区域(固定高度) ========== */
  881. .card-delivery-section {
  882. padding: 10px 15px;
  883. border-top: 1px solid rgba(30, 144, 255, 0.3);
  884. background: rgba(9, 61, 140, 0.3);
  885. min-height: 94px;
  886. /* 固定最小高度 */
  887. display: flex;
  888. flex-direction: column;
  889. justify-content: center;
  890. margin-bottom: 6px;
  891. }
  892. /* 未选中时的占位样式 */
  893. .delivery-placeholder {
  894. display: flex;
  895. align-items: center;
  896. justify-content: center;
  897. height: 100%;
  898. color: #5a8abf;
  899. font-size: 12px;
  900. opacity: 0.7;
  901. }
  902. .delivery-row {
  903. display: flex;
  904. align-items: center;
  905. gap: 8px;
  906. margin-bottom: 8px;
  907. }
  908. .delivery-row:last-child {
  909. margin-bottom: 0;
  910. }
  911. .delivery-location-row {
  912. display: flex;
  913. align-items: center;
  914. gap: 8px;
  915. height: 30px;
  916. margin-top: 6px;
  917. }
  918. .delivery-label {
  919. font-size: 12px;
  920. color: #7ec8ff;
  921. white-space: nowrap;
  922. }
  923. /* 配送方式按钮 */
  924. .delivery-method-buttons {
  925. display: flex;
  926. gap: 4px;
  927. }
  928. .delivery-btn {
  929. display: inline-flex;
  930. align-items: center;
  931. justify-content: center;
  932. gap: 4px;
  933. padding: 5px 10px;
  934. font-size: 11px;
  935. font-weight: 500;
  936. border: 1px solid #2a7fff;
  937. background: rgba(13, 58, 106, 0.8);
  938. color: #7ec8ff;
  939. cursor: pointer;
  940. transition: all 0.2s;
  941. border-radius: 4px;
  942. }
  943. .delivery-btn i {
  944. font-size: 10px;
  945. }
  946. .delivery-btn.active {
  947. background: linear-gradient(90deg, #1e90ff 0%, #00bfff 100%);
  948. color: #fff;
  949. border-color: #00bfff;
  950. }
  951. .delivery-btn.disabled {
  952. opacity: 0.4;
  953. cursor: not-allowed;
  954. }
  955. .delivery-btn:hover:not(.disabled):not(.active) {
  956. background: rgba(30, 144, 255, 0.3);
  957. }
  958. /* 配送类型徽章 */
  959. .delivery-type-badge {
  960. padding: 3px 10px;
  961. border-radius: 12px;
  962. font-size: 10px;
  963. font-weight: 500;
  964. display: inline-block;
  965. }
  966. .delivery-type-badge.badge-manual {
  967. background: rgba(107, 114, 128, 0.4);
  968. border: 1px solid #6b7280;
  969. color: #d1d5db;
  970. }
  971. .delivery-type-badge.badge-agv-force {
  972. background: rgba(234, 88, 12, 0.4);
  973. border: 1px solid #ea580c;
  974. color: #fdba74;
  975. }
  976. .delivery-type-badge.badge-agv-optional {
  977. background: rgba(16, 185, 129, 0.4);
  978. border: 1px solid #10b981;
  979. color: #6ee7b7;
  980. }
  981. /* 配送位置选择器 */
  982. .location-select {
  983. flex: 1;
  984. min-width: 0;
  985. }
  986. /* 禁用状态的选择器 */
  987. .location-select.vs--disabled .vs__dropdown-toggle {
  988. opacity: 0.5;
  989. background: rgba(13, 58, 106, 0.4);
  990. }
  991. /* 统计信息 */
  992. .stats-info {
  993. text-align: center;
  994. padding: 15px 0;
  995. color: #5a8abf;
  996. font-size: 13px;
  997. }
  998. /* ========== 底部操作按钮 ========== */
  999. .bottom-actions {
  1000. width: 100%;
  1001. padding: 15px 30px;
  1002. box-sizing: border-box;
  1003. background: rgba(4, 28, 61, 0.95);
  1004. z-index: 20;
  1005. flex-shrink: 0;
  1006. }
  1007. .submit-btn {
  1008. width: 100%;
  1009. padding: 18px 0;
  1010. background: #4a99e2;
  1011. border: none;
  1012. border-radius: 12px;
  1013. font-size: 24px;
  1014. font-weight: bold;
  1015. color: #fff;
  1016. cursor: pointer;
  1017. transition: all 0.3s;
  1018. letter-spacing: 4px;
  1019. text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  1020. }
  1021. .submit-btn:hover:not(:disabled) {
  1022. box-shadow: 0 0 30px rgba(16, 185, 129, 0.6);
  1023. transform: translateY(-2px);
  1024. }
  1025. .submit-btn:disabled {
  1026. opacity: 0.5;
  1027. cursor: not-allowed;
  1028. background: linear-gradient(90deg, #6b7280 0%, #9ca3af 100%);
  1029. }
  1030. /* ========== 响应式 - 横屏 ========== */
  1031. @media screen and (orientation: landscape) {
  1032. .header-section {
  1033. padding: 10px 20px;
  1034. }
  1035. .page-title {
  1036. font-size: 22px;
  1037. }
  1038. .logout-btn {
  1039. padding: 6px 14px;
  1040. font-size: 13px;
  1041. left: 20px;
  1042. }
  1043. .logout-btn i {
  1044. font-size: 14px;
  1045. }
  1046. .main-content {
  1047. padding: 0 20px;
  1048. }
  1049. .filter-section {
  1050. padding: 10px 15px;
  1051. margin-bottom: 10px;
  1052. }
  1053. .filter-row {
  1054. gap: 12px;
  1055. }
  1056. .filter-label {
  1057. font-size: 13px;
  1058. }
  1059. .filter-select {
  1060. width: 140px;
  1061. }
  1062. .filter-input.dark-input {
  1063. width: 140px;
  1064. padding: 6px 10px;
  1065. font-size: 12px;
  1066. }
  1067. :deep(.dark-select .vs__dropdown-toggle) {
  1068. min-height: 32px;
  1069. padding: 4px 8px;
  1070. }
  1071. .search-btn,
  1072. .reset-btn {
  1073. padding: 6px 20px;
  1074. font-size: 13px;
  1075. }
  1076. .select-all-bar {
  1077. padding: 8px 15px;
  1078. margin-bottom: 8px;
  1079. }
  1080. .select-all-text {
  1081. font-size: 12px;
  1082. }
  1083. /* 卡片网格响应式 */
  1084. .card-grid {
  1085. grid-template-columns: repeat(6, 1fr);
  1086. gap: 15px;
  1087. }
  1088. .card-index {
  1089. width: 24px;
  1090. height: 24px;
  1091. font-size: 11px;
  1092. top: 6px;
  1093. left: 6px;
  1094. }
  1095. .selected-indicator {
  1096. width: 20px;
  1097. height: 20px;
  1098. font-size: 10px;
  1099. top: 6px;
  1100. right: 6px;
  1101. }
  1102. .card-image {
  1103. margin: 10px;
  1104. margin-bottom: 8px;
  1105. width: calc(100% - 20px);
  1106. }
  1107. .image-placeholder {
  1108. font-size: 36px;
  1109. }
  1110. .card-info {
  1111. padding: 6px 10px 8px;
  1112. }
  1113. .info-row {
  1114. font-size: 11px;
  1115. margin-bottom: 3px;
  1116. }
  1117. .location-icon {
  1118. font-size: 10px;
  1119. }
  1120. .delivery-type-row {
  1121. margin-top: 4px;
  1122. padding-top: 4px;
  1123. }
  1124. .delivery-type-badge {
  1125. font-size: 9px;
  1126. padding: 2px 8px;
  1127. }
  1128. .card-delivery-section {
  1129. padding: 8px 10px;
  1130. min-height: 82px;
  1131. }
  1132. .delivery-placeholder {
  1133. font-size: 10px;
  1134. }
  1135. .delivery-label {
  1136. font-size: 10px;
  1137. }
  1138. .delivery-btn {
  1139. padding: 3px 6px;
  1140. font-size: 9px;
  1141. gap: 2px;
  1142. }
  1143. .delivery-btn i {
  1144. font-size: 8px;
  1145. }
  1146. .delivery-location-row {
  1147. margin-top: 6px;
  1148. }
  1149. .bottom-actions {
  1150. padding: 10px 20px;
  1151. }
  1152. .submit-btn {
  1153. padding: 12px 0;
  1154. font-size: 18px;
  1155. letter-spacing: 4px;
  1156. border-radius: 8px;
  1157. }
  1158. }
  1159. /* 大弹窗样式 */
  1160. :deep(.large-dialog) {
  1161. width: 85vw !important;
  1162. max-width: 600px !important;
  1163. border-radius: 16px !important;
  1164. }
  1165. :deep(.large-dialog .van-dialog__header) {
  1166. padding: 2rem 1.5rem 1rem !important;
  1167. font-size: 1.5rem !important;
  1168. font-weight: 700 !important;
  1169. color: #111827 !important;
  1170. }
  1171. .large-dialog-content {
  1172. padding: 1.5rem 2rem 2.5rem !important;
  1173. }
  1174. /* ========== 科技感弹窗样式 ========== */
  1175. .tech-modal-overlay {
  1176. position: fixed;
  1177. top: 0;
  1178. left: 0;
  1179. width: 100%;
  1180. height: 100%;
  1181. background: rgba(4, 28, 61, 0.9);
  1182. display: flex;
  1183. align-items: center;
  1184. justify-content: center;
  1185. z-index: 2000;
  1186. }
  1187. .tech-modal {
  1188. background: linear-gradient(135deg, #0a3d6d 0%, #041c3d 100%);
  1189. border: 1px solid #049FD8;
  1190. border-radius: 16px;
  1191. width: 90%;
  1192. max-width: 500px;
  1193. position: relative;
  1194. box-shadow: 0 0 40px rgba(4, 159, 216, 0.3);
  1195. }
  1196. .modal-content-row {
  1197. padding: 30px;
  1198. display: flex;
  1199. align-items: center;
  1200. gap: 30px;
  1201. }
  1202. .modal-text {
  1203. flex: 1;
  1204. }
  1205. .modal-text h3 {
  1206. font-size: 22px;
  1207. font-weight: bold;
  1208. color: #fff;
  1209. margin: 0 0 12px 0;
  1210. }
  1211. .modal-text p {
  1212. font-size: 15px;
  1213. color: #7ec8ff;
  1214. margin: 0;
  1215. line-height: 1.6;
  1216. }
  1217. .modal-icon {
  1218. flex-shrink: 0;
  1219. }
  1220. .icon-box {
  1221. width: 80px;
  1222. height: 80px;
  1223. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  1224. border-radius: 12px;
  1225. display: flex;
  1226. align-items: center;
  1227. justify-content: center;
  1228. font-size: 36px;
  1229. color: #fff;
  1230. position: relative;
  1231. box-shadow: 0 10px 30px rgba(16, 185, 129, 0.3);
  1232. }
  1233. .icon-box .check-icon {
  1234. position: absolute;
  1235. bottom: -5px;
  1236. right: -5px;
  1237. font-size: 20px;
  1238. color: #10b981;
  1239. background: #041c3d;
  1240. border-radius: 50%;
  1241. padding: 4px;
  1242. }
  1243. .modal-footer {
  1244. padding: 20px 30px 30px;
  1245. display: flex;
  1246. gap: 15px;
  1247. }
  1248. .modal-btn {
  1249. flex: 1;
  1250. padding: 14px 0;
  1251. border-radius: 8px;
  1252. font-size: 16px;
  1253. font-weight: 600;
  1254. cursor: pointer;
  1255. transition: all 0.3s;
  1256. }
  1257. .cancel-btn {
  1258. background: transparent;
  1259. border: 1px solid #2a7fff;
  1260. color: #7ec8ff;
  1261. }
  1262. .cancel-btn:hover {
  1263. background: rgba(42, 127, 255, 0.2);
  1264. }
  1265. .confirm-btn {
  1266. background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
  1267. border: none;
  1268. color: #fff;
  1269. }
  1270. .confirm-btn:hover {
  1271. box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
  1272. }
  1273. </style>