liuyanpeng 7 bulan lalu
melakukan
63a76cda04
54 mengubah file dengan 8300 tambahan dan 0 penghapusan
  1. 8 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 4 0
      .eslintignore
  4. 82 0
      .eslintrc.js
  5. 6 0
      .gitignore
  6. 16 0
      README.MD
  7. 6 0
      bat/build.bat
  8. 5 0
      bat/debug.bat
  9. 5 0
      bat/install.bat
  10. 45 0
      maintance.sh
  11. 68 0
      package.json
  12. 19 0
      postcss.config.js
  13. 5 0
      public/font-awesome.min.css
  14. 16 0
      public/index.html
  15. 8 0
      public/pacifico.css
  16. TEMPAT SAMPAH
      public/webfonts/fa-solid-900.woff2
  17. 27 0
      src/App.vue
  18. 337 0
      src/agv-process/DeliveryManagement.vue
  19. 404 0
      src/agv-process/ReturnManagement.vue
  20. 1 0
      src/agv-process/car.svg
  21. 42 0
      src/api/agv.js
  22. 9 0
      src/api/login.js
  23. 17 0
      src/api/stock.js
  24. 26 0
      src/api/stockIn.js
  25. 108 0
      src/api/stockOut.js
  26. 57 0
      src/assets/main.css
  27. 508 0
      src/common/Common.js
  28. 299 0
      src/common/CommonTable.vue
  29. 98 0
      src/common/FilterPanel.vue
  30. 164 0
      src/common/InventoryCard.vue
  31. 121 0
      src/common/PageHeader.vue
  32. 26 0
      src/common/tableScroll.js
  33. 10 0
      src/common/utils.js
  34. 518 0
      src/login/UserHome.vue
  35. 391 0
      src/login/UserLogin.vue
  36. 70 0
      src/main.js
  37. 4 0
      src/public-path.js
  38. 40 0
      src/router/routes.js
  39. 494 0
      src/stock-in/InConfirm.vue
  40. 352 0
      src/stock-out/AgvRfidRecognition.vue
  41. 830 0
      src/stock-out/OrderPicking.vue
  42. 487 0
      src/stock-out/OutboundConfirm.vue
  43. 656 0
      src/stock/RegularRequisition.vue
  44. 650 0
      src/stock/StockPickingCar.vue
  45. 621 0
      src/stock/StockRequisition.vue
  46. 14 0
      src/util/Uuid.js
  47. 36 0
      src/util/axios.js
  48. 53 0
      src/util/common.js
  49. 143 0
      src/util/loading.js
  50. 83 0
      src/util/request.js
  51. 63 0
      tailwind.config.js
  52. 90 0
      webpack.base.js
  53. 73 0
      webpack.dev.js
  54. 76 0
      webpack.prod.js

+ 8 - 0
.babelrc

@@ -0,0 +1,8 @@
+{
+  "presets": [
+    "@babel/env"
+    // ["env", { "modules": false }],
+    // "stage-3", 
+    // "es2015"
+  ]
+}

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+/build
+/dist
+/node_modules
+/src/slam/lib/ros3d.esm.js

+ 82 - 0
.eslintrc.js

@@ -0,0 +1,82 @@
+module.exports = {
+  // 如果想要在不同的目录中使用不同的 .eslintrc, 就需要在该目录中添加如下的配置项:
+  // 告诉eslint找.eslintrc配置文件不能往父级查找
+  // root: true,
+  // 此项是用来提供插件的,插件名称省略了eslint-plugin-,下面这个配置是用来规范vue的
+  // plugins: ['vue'],
+  extends: [
+    // add more generic rulesets here, such as:
+    'eslint:recommended', // eslint推荐规则预设
+    'plugin:vue/vue3-recommended', // eslint-plugin-vue推荐的适用于vue3的规则预设
+  ],
+  parser: 'vue-eslint-parser',
+  // 自定义 parser
+  parserOptions: {
+    parser: '@babel/eslint-parser',
+    sourceType: 'module',
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+    jquery: true,
+  },
+  rules: {
+    // override/add rules settings here, such as:
+    'vue/no-unused-vars': 'error',
+    // 此规则禁用不必要的分号。
+    'no-extra-semi': 'off',
+    // 该规则强制使用一致的分号
+    semi: ['error', 'always'],
+    // 该规则强制使用一致的反勾号、双引号或单引号。
+    quotes: ['error', 'single'],
+    // 该规则旨在强制使用一致的缩进风格。默认是 4个空格。
+    indent: ['error', 4],
+    // 该规则旨在通过限制代码行的长度来提高代码的可读性和可维护性。
+    // 一行的长度为行中的 Unicode 字符的数量。
+    'max-len': ['error', { code: 185 }],
+    // 这个规则强制在对象和数组字面量中使用一致的拖尾逗号。
+    // "always-multiline" 当最后一个元素或属性与闭括号 ] 或 } 在 不同的行时,要求使用拖尾逗号;当在 同一行时,禁止使用拖尾逗号。
+    'comma-dangle': ['error', 'always-multiline'],
+    // 该规则强制箭头函数单个参数是否要使用圆括号括起来
+    // "as-needed":在可以省略括号的地方强制不使用括号
+    'arrow-parens': ['error', 'as-needed'],
+    // 此规则在单行元素的内容之前和之后强制换行。
+    'vue/singleline-html-element-content-newline': 'off',
+    // 限制每行最多能写多少个属性
+    'vue/max-attributes-per-line': 'off',
+    // 标签自闭合相关设置
+    'vue/html-self-closing': [
+      'warn',
+      {
+        html: {
+          void: 'always',
+          normal: 'always',
+          component: 'always',
+        },
+      },
+    ],
+
+    'no-unused-vars': [0, {
+      // function 参数未使用不检查
+      'args': 'none'
+    }],
+
+    "vue/v-on-event-hyphenation": ["warn", "always", {
+      "autofix": true,
+      "ignore": []
+    }],
+
+  },
+  "globals":{
+    "document": true,
+    "localStorage": true,
+    "window": true,
+    "BootstrapDialog": true,
+    "moment": true,
+    "echarts": true,
+    "layer": true,
+    "Handsontable": true,
+    "dd": true
+  }
+}

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+/node_modules/
+/dist/
+*.log
+yarn.lock
+/*.project
+/package-lock.json

+ 16 - 0
README.MD

@@ -0,0 +1,16 @@
+# 程序打包
+
+sh maintance.sh build
+
+程序打包以后会生成dist.zip文件,可以手工拷贝到slam_startup_py/www目录下面,然后解压缩。
+
+
+# 程序发布
+
+sh maintance.sh publish
+
+程序打包以后会发布到平台上,进入到slam_startup_py目录,然后运行sh download.sh,此时会自动的下载对应版本的程序包。
+
+# 访问地址
+端口后跟/board.html
+http://192.168.1.107:10026/board.html

+ 6 - 0
bat/build.bat

@@ -0,0 +1,6 @@
+set current_path="%~dp0"
+cd %current_path%
+cd ..
+rmdir /s/q dist
+npm run build
+pause

+ 5 - 0
bat/debug.bat

@@ -0,0 +1,5 @@
+set current_path="%~dp0"
+cd %current_path%
+cd ..
+npm run dev
+pause

+ 5 - 0
bat/install.bat

@@ -0,0 +1,5 @@
+set current_path="%~dp0"
+cd %current_path%
+cd ..
+npm install -registry=http://wuzhixin.vip:4873 --force
+pause

+ 45 - 0
maintance.sh

@@ -0,0 +1,45 @@
+#!/bin/sh
+
+packageClient(){
+    rm -rf dist*.zip
+    rm -rf ./dist
+    npm run build
+    zip -r dist.zip ./dist/
+}
+
+
+publish(){
+    packageClient
+    npm publish --registry=http://wuzhixin.vip:4873
+    rm -rf dist*.zip
+}
+
+
+
+
+# 帮助说明,用于提示输入参数信息
+usage() {
+    echo "Usage: sh maintance.sh [ packageClient | publish ]"
+    exit 1
+}
+
+
+
+###################################
+# 读取脚本的第一个参数($1),进行判断
+# 参数取值范围:{ stop | start }
+# 如参数不在指定范围之内,则打印帮助信息
+###################################
+#根据输入参数,选择执行对应方法,不输入则执行使用说明
+case "$1" in
+  'packageClient')
+    packageClient
+    ;;
+  'publish')
+    publish
+    ;;
+  *)
+    usage
+    ;;
+esac
+exit 0

+ 68 - 0
package.json

@@ -0,0 +1,68 @@
+{
+  "name": "client-wms-board",
+  "version": "0.0.1",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "ins": "npm install --registry=http://wuzhixin.vip:4873 -force",
+    "dev": "webpack serve --config ./webpack.dev.js",
+    "build": "webpack --mode=production --config ./webpack.prod.js --progress",
+    "verify": "node verify-build.js"
+  },
+  "files": [
+    "package.json",
+    "dist"
+  ],
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "devDependencies": {
+    "@babel/core": "^7.16.7",
+    "@babel/eslint-parser": "^7.17.0",
+    "@babel/preset-env": "^7.16.7",
+    "autoprefixer": "^10.4.21",
+    "babel-loader": "^8.2.3",
+    "copy-webpack-plugin": "^13.0.1",
+    "cross-env": "^7.0.3",
+    "css-loader": "^6.5.1",
+    "eslint": "^8.10.0",
+    "eslint-plugin-vue": "^8.5.0",
+    "eslint-webpack-plugin": "^3.1.1",
+    "file-loader": "^6.2.0",
+    "html-loader": "^3.1.0",
+    "html-webpack-plugin": "^5.5.0",
+    "mini-css-extract-plugin": "^2.4.6",
+    "pc-component-v3": "2.0.3",
+    "postcss": "^8.5.6",
+    "postcss-loader": "^8.2.0",
+    "style-loader": "^3.3.1",
+    "tailwindcss": "^3.4.18",
+    "terser-webpack-plugin": "^5.3.6",
+    "vue-loader": "^17.0.0",
+    "webpack": "^5.70.0",
+    "webpack-cli": "^4.9.2",
+    "webpack-dev-server": "^4.7.4",
+    "webpack-merge": "^5.8.0"
+  },
+  "dependencies": {
+    "ant-design-vue": "^4.2.1",
+    "axios": "^1.2.2",
+    "brace-expansion": "^4.0.1",
+    "dayjs": "^1.10.7",
+    "jquery": "^3.6.0",
+    "vue": "^3.4.25",
+    "vue-request": "^1.2.3",
+    "vue-router": "^4.0.12"
+  },
+  "publishConfig": {
+    "access": "public",
+    "registry": "http://wuzhixin.vip:4873/"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead",
+    "Android >= 4.4",
+    "iOS >=5"
+  ]
+}

+ 19 - 0
postcss.config.js

@@ -0,0 +1,19 @@
+// PostCSS 是一个 CSS 转换工具,通过插件来处理 CSS
+
+module.exports = {
+  plugins: {
+    tailwindcss: {}, // 将 tailwind 指令转换为实际的 CSS 代码
+    autoprefixer: {}, // 自动添加浏览器前缀(如 -webkit-、-moz-)
+  },
+}
+// 处理示例:
+
+// 原始代码 :
+// .flex { display: flex; }
+
+// autoprefixer 处理后 :
+// .flex { 
+//   display: -webkit-box;
+//   display: -ms-flexbox;
+//   display: flex; 
+// }

File diff ditekan karena terlalu besar
+ 5 - 0
public/font-awesome.min.css


+ 16 - 0
public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>领料管理</title>
+	<!-- 本地 Font Awesome -->
+	<link rel="stylesheet" href="./font-awesome.min.css">
+	<!-- 本地 Pacifico 字体 -->
+	<link rel="stylesheet" href="./pacifico.css">
+</head>
+<body>
+    <div id="app"></div>
+</body>
+</html>

+ 8 - 0
public/pacifico.css

@@ -0,0 +1,8 @@
+/* Pacifico 字体 - 本地版本 */
+@font-face {
+  font-family: 'Pacifico';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(./fonts/pacifico/pacifico-latin.woff2) format('woff2');
+}

TEMPAT SAMPAH
public/webfonts/fa-solid-900.woff2


+ 27 - 0
src/App.vue

@@ -0,0 +1,27 @@
+<template>
+  <a-config-provider :locale="locale">
+    <router-view />
+  </a-config-provider>
+</template>
+
+<script>
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+dayjs.locale('zh-cn');
+
+export default {
+
+    components: {
+
+    },
+    data() {
+        return {
+            locale: zhCN,
+
+        };
+    },
+};
+</script>
+
+<style></style>

+ 337 - 0
src/agv-process/DeliveryManagement.vue

@@ -0,0 +1,337 @@
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>取料区管理</h2>
+      </div>
+
+      <div class="stations-container">
+        <div class="stations-grid">
+          <div
+            v-for="station in stations" :key="station.positionNo" class="station-card"
+            :class="{ 'station-occupied': station.positionStatus === '占用', 'station-idle': station.positionStatus === '空闲中' }"
+            @click="openStation(station)"
+          >
+            <div class="card-header">
+              <div class="card-title">{{ station.positionName }}</div>
+              <a-tag v-if="station.positionStatus === '空闲中'" class="card-status" color="green">
+                {{ station.positionStatus }}
+              </a-tag>
+              <a-tag v-if="station.positionStatus === '已占用'" class="card-status" color="red">
+                {{ station.positionStatus }}
+              </a-tag>
+              <a-tag v-if="station.positionStatus === '待入库'" class="card-status" color="orange">
+                {{ station.positionStatus }}
+              </a-tag>
+            </div>
+
+            <!-- 卡片内容 -->
+            <div class="card-content">
+              <!-- 占用状态 - 显示物料图片 -->
+              <div v-if="station.positionStatus === '已占用'" class="material-display">
+                <div class="material-image">
+                  <img src="./car.svg" :alt="station.materialName" />
+                </div>
+                <!-- <div v-if="station.positionName" class="material-info">
+                  <div class="material-name">{{ station.positionName }}</div>
+                  <div class="material-code">{{ station.positionNo }}</div>
+                </div> -->
+              </div>
+
+              <!-- 空闲状态 - 显示空闲图标 -->
+              <a-empty v-else description="暂无物料车" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </main>
+    <a-modal v-model:open="visible" title="提示" @ok="takeAway" @cancel="cancelTask">
+      <p>请确认您是否要取走【{{ positionName }}】的料车,如果确认请点击【确认】按钮。</p>
+    </a-modal>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { message } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import { getLocatorFeedAreaStatus, takeAwaySkip } from '../api/agv.js';
+
+
+// 加载状态
+const loading = ref(false);
+
+// 还料位数据
+const stations = ref([]);
+
+const visible = ref(false);
+
+const positionName = ref('');
+const positionNo = ref('');
+
+// 打开弹窗
+const openStation = station => {
+    if (station.positionStatus === '空闲中' || station.positionStatus === '待入库') {
+        message.warning('请选择占用中的还料区');
+    } else {
+        visible.value = true;
+        positionName.value = station.positionName;
+        positionNo.value = station.positionNo;
+    }
+};
+
+// 根据还料货位和料车编号生成归还单和调度任务
+const takeAway = async () => {
+
+    loading.value = true;
+    try {
+        const res = await takeAwaySkip(positionNo.value);
+        if (res.errorCode === 0) {
+            message.success('料车取走成功');
+            visible.value = false;
+            await getStations();
+        } else {
+            message.warning(res.errorMessage || '料车取走失败');
+        }
+    } catch (error) {
+        console.error('料车取走失败:', error);
+        message.error('料车取走失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 取消任务
+const cancelTask = () => {
+    visible.value = false;
+};
+// 获取还料位数据
+const getStations = async () => {
+    loading.value = true;
+    try {
+        const res = await getLocatorFeedAreaStatus();
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                stations.value = res.datas;
+            } else {
+                stations.value = [];
+            }
+        } else {
+            message.warn(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('获取还料位数据失败:', error);
+        message.error('获取还料位数据失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+onMounted(() => {
+    getStations();
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    background-color: #f9fafb;
+    overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+    flex: 1;
+    overflow-y: auto;
+    padding: 1.5rem;
+    display: flex;
+    flex-direction: column;
+}
+
+/* 页面标题 */
+.page-title {
+    margin-bottom: 1.5rem;
+}
+
+.page-title h2 {
+    font-size: 1.5rem;
+    font-weight: 700;
+    color: #111827;
+    margin: 0;
+}
+
+/* 还料位容器 */
+.stations-container {
+    flex: 1;
+    background-color: white;
+    border-radius: 0.5rem;
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+    padding: 2rem;
+    overflow-y: auto;
+}
+
+/* 网格布局 */
+.stations-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+    gap: 1.5rem;
+}
+
+/* 响应式布局 */
+@media (min-width: 768px) {
+    .stations-grid {
+        grid-template-columns: repeat(2, 1fr);
+    }
+}
+
+@media (min-width: 1280px) {
+    .stations-grid {
+        grid-template-columns: repeat(3, 1fr);
+    }
+}
+
+@media (min-width: 1536px) {
+    .stations-grid {
+        grid-template-columns: repeat(4, 1fr);
+    }
+}
+
+/* 还料位卡片 */
+.station-card {
+    background-color: #ffffff;
+    border-radius: 0.75rem;
+    border: 3px solid #97b5f9;
+    padding: 1.5rem;
+    transition: all 0.3s ease;
+    min-height: 320px;
+    display: flex;
+    flex-direction: column;
+}
+
+.station-card:hover {
+    transform: translateY(-4px);
+    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+}
+
+/* 占用状态卡片 */
+.station-occupied {
+    border-color: #3b82f6;
+    background: linear-gradient(to bottom, #ffffff 0%, #eff6ff 100%);
+}
+
+/* 空闲状态卡片 */
+.station-idle {
+    border-color: #d1d5db;
+    background-color: #fafafa;
+}
+
+/* 卡片头部 */
+.card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 1.5rem;
+    padding-bottom: 1rem;
+    border-bottom: 2px solid #e5e7eb;
+}
+
+.card-title {
+    font-size: 1.25rem;
+    font-weight: 700;
+    color: #111827;
+}
+
+/* 状态标签 */
+.card-status {
+    padding: 0.375rem 0.875rem;
+    border-radius: 0.5rem;
+    font-size: 0.875rem;
+    font-weight: 600;
+}
+
+/* 卡片内容 */
+.card-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+/* 物料显示区域 */
+.material-display {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 1rem;
+}
+
+.material-image {
+    width: 180px;
+    height: 180px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #f9fafb;
+    border-radius: 0.5rem;
+    padding: 1rem;
+}
+
+.material-image img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+}
+
+.material-info {
+    text-align: center;
+    width: 100%;
+}
+
+.material-name {
+    font-size: 1rem;
+    font-weight: 600;
+    color: #111827;
+    margin-bottom: 0.25rem;
+}
+
+.material-code {
+    font-size: 0.875rem;
+    color: #6b7280;
+}
+
+.station-idle:hover {
+    background-color: #e5e7eb;
+}
+
+.idle-text {
+    font-size: 1rem;
+    font-weight: 500;
+    color: #9ca3af;
+}
+
+/* 工具类 */
+.mr-1 {
+    margin-right: 0.25rem;
+}
+
+.mr-2 {
+    margin-right: 0.5rem;
+}
+
+.ml-2 {
+    margin-left: 0.5rem;
+}
+</style>

+ 404 - 0
src/agv-process/ReturnManagement.vue

@@ -0,0 +1,404 @@
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>还料区管理</h2>
+      </div>
+
+      <div class="stations-container">
+        <div class="stations-grid">
+          <div
+            v-for="station in stations" :key="station.positionNo" class="station-card"
+            :class="{ 'station-occupied': station.positionStatus === '占用', 'station-idle': station.positionStatus === '空闲中' }"
+            @click="openStation(station)"
+          >
+            <div class="card-header">
+              <div class="card-title">{{ station.positionName }}</div>
+              <a-tag v-if="station.positionStatus === '空闲中'" class="card-status" color="green">
+                {{ station.positionStatus }}
+              </a-tag>
+              <a-tag v-if="station.positionStatus === '已占用'" class="card-status" color="red">
+                {{ station.positionStatus }}
+              </a-tag>
+            </div>
+
+            <!-- 卡片内容 -->
+            <div class="card-content">
+              <!-- 占用状态 - 显示物料图片 -->
+              <div v-if="station.positionStatus === '已占用'" class="material-display">
+                <div class="material-image">
+                  <img src="./car.svg" :alt="station.materialName" />
+                </div>
+                <!-- <div v-if="station.positionName" class="material-info">
+                  <div class="material-name">{{ station.positionName }}</div>
+                  <div class="material-code">{{ station.positionNo }}</div>
+                </div> -->
+              </div>
+
+              <!-- 空闲状态 - 显示空闲图标 -->
+              <a-empty v-else description="暂无物料车" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </main>
+    <a-modal v-model:open="visible" title="提示" @ok="generateTask" @cancel="cancelTask">
+      <p>请确认您已经把待归还的料车推到【 {{ positionName }} 】上,请输入料车编号,完成以后点击【确认】按钮。</p>
+      <br />
+      <a-select
+        v-model:value="truckNo" show-search placeholder="请输入料车编号搜索" :default-active-first-option="false"
+        :filter-option="false" :not-found-content="null" :options="truckNos" style="width: 100%;"
+        @search="handleSearch" @change="handleChange"
+      />
+    </a-modal>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { message } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import { getLocatorStatus, getNoInStorageTruck, generateTaskByBlankNo } from '../api/agv.js';
+
+
+// 加载状态
+const loading = ref(false);
+
+// 还料位数据
+const stations = ref([]);
+
+const visible = ref(false);
+
+const positionName = ref('');
+const positionNo = ref('');
+
+const truckNo = ref('');
+
+const truckNos = ref([]);
+let searchTimeout = null;
+
+// 远程搜索料车(带防抖)
+const handleSearch = value => {
+    if (!value || value.trim() === '') {
+        truckNos.value = [];
+        return;
+    }
+
+    // 清除之前的定时器
+    if (searchTimeout) {
+        clearTimeout(searchTimeout);
+    }
+
+    // 设置新的定时器,500ms 后执行搜索
+    searchTimeout = setTimeout(async () => {
+        try {
+            const res = await getNoInStorageTruck(value);
+            if (res.errorCode === 0) {
+                if (res.datas && res.datas.length > 0) {
+                    // 将数据转换为 a-select 需要的格式
+                    truckNos.value = res.datas.map(item => ({
+                        label: item,
+                        value: item,
+                    }));
+                } else {
+                    truckNos.value = [];
+                }
+            } else {
+                message.warning(res.errorMessage || '搜索料车失败');
+                truckNos.value = [];
+            }
+        } catch (error) {
+            console.error('搜索料车失败:', error);
+            truckNos.value = [];
+        }
+    }, 500);
+};
+
+// 选择料车
+const handleChange = value => {
+    console.log('选择的料车编号:', value);
+    truckNo.value = value;
+};
+
+// 打开弹窗
+const openStation = station => {
+    if (station.positionStatus === '已占用') {
+        message.warning('请选择空闲的还料区');
+    } else {
+        visible.value = true;
+        positionName.value = station.positionName;
+        positionNo.value = station.positionNo;
+        // 重置选择
+        truckNo.value = '';
+        truckNos.value = [];
+    }
+};
+
+// 根据还料货位和料车编号生成归还单和调度任务
+const generateTask = async () => {
+    if (!truckNo.value) {
+        message.warning('请输入料车编号');
+        return;
+    }
+
+    const params = {
+        truckNo: truckNo.value,
+        positionNo: positionNo.value,
+    };
+    console.log('生成任务参数:', params);
+    loading.value = true;
+    try {
+        const res = await generateTaskByBlankNo(params);
+        if (res.errorCode === 0) {
+            message.success('绑定成功,等待AGV归还');
+            visible.value = false;
+            truckNo.value = '';
+            truckNos.value = [];
+            await getStations();
+        } else {
+            message.warning(res.errorMessage || '绑定失败');
+        }
+    } catch (error) {
+        console.error('绑定失败:', error);
+        message.error('绑定失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 取消任务
+const cancelTask = () => {
+    truckNo.value = '';
+    truckNos.value = [];
+    visible.value = false;
+};
+// 获取还料位数据
+const getStations = async () => {
+    loading.value = true;
+    try {
+        const res = await getLocatorStatus();
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                stations.value = res.datas;
+            } else {
+                stations.value = [];
+            }
+        } else {
+            message.warn(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('获取还料位数据失败:', error);
+        message.error('获取还料位数据失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+onMounted(() => {
+    getStations();
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    background-color: #f9fafb;
+    overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+    flex: 1;
+    overflow-y: auto;
+    padding: 1.5rem;
+    display: flex;
+    flex-direction: column;
+}
+
+/* 页面标题 */
+.page-title {
+    margin-bottom: 1.5rem;
+}
+
+.page-title h2 {
+    font-size: 1.5rem;
+    font-weight: 700;
+    color: #111827;
+    margin: 0;
+}
+
+/* 还料位容器 */
+.stations-container {
+    flex: 1;
+    background-color: white;
+    border-radius: 0.5rem;
+    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+    padding: 2rem;
+    overflow-y: auto;
+}
+
+/* 网格布局 */
+.stations-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+    gap: 1.5rem;
+}
+
+/* 响应式布局 */
+@media (min-width: 768px) {
+    .stations-grid {
+        grid-template-columns: repeat(2, 1fr);
+    }
+}
+
+@media (min-width: 1280px) {
+    .stations-grid {
+        grid-template-columns: repeat(3, 1fr);
+    }
+}
+
+@media (min-width: 1536px) {
+    .stations-grid {
+        grid-template-columns: repeat(4, 1fr);
+    }
+}
+
+/* 还料位卡片 */
+.station-card {
+    background-color: #ffffff;
+    border-radius: 0.75rem;
+    border: 3px solid #97b5f9;
+    padding: 1.5rem;
+    transition: all 0.3s ease;
+    min-height: 320px;
+    display: flex;
+    flex-direction: column;
+}
+
+.station-card:hover {
+    transform: translateY(-4px);
+    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+}
+
+/* 占用状态卡片 */
+.station-occupied {
+    border-color: #3b82f6;
+    background: linear-gradient(to bottom, #ffffff 0%, #eff6ff 100%);
+}
+
+/* 空闲状态卡片 */
+.station-idle {
+    border-color: #d1d5db;
+    background-color: #fafafa;
+}
+
+/* 卡片头部 */
+.card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 1.5rem;
+    padding-bottom: 1rem;
+    border-bottom: 2px solid #e5e7eb;
+}
+
+.card-title {
+    font-size: 1.25rem;
+    font-weight: 700;
+    color: #111827;
+}
+
+/* 状态标签 */
+.card-status {
+    padding: 0.375rem 0.875rem;
+    border-radius: 0.5rem;
+    font-size: 0.875rem;
+    font-weight: 600;
+}
+
+/* 卡片内容 */
+.card-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+/* 物料显示区域 */
+.material-display {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 1rem;
+}
+
+.material-image {
+    width: 180px;
+    height: 180px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #f9fafb;
+    border-radius: 0.5rem;
+    padding: 1rem;
+}
+
+.material-image img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+}
+
+.material-info {
+    text-align: center;
+    width: 100%;
+}
+
+.material-name {
+    font-size: 1rem;
+    font-weight: 600;
+    color: #111827;
+    margin-bottom: 0.25rem;
+}
+
+.material-code {
+    font-size: 0.875rem;
+    color: #6b7280;
+}
+
+.station-idle:hover {
+    background-color: #e5e7eb;
+}
+
+.idle-text {
+    font-size: 1rem;
+    font-weight: 500;
+    color: #9ca3af;
+}
+
+/* 工具类 */
+.mr-1 {
+    margin-right: 0.25rem;
+}
+
+.mr-2 {
+    margin-right: 0.5rem;
+}
+
+.ml-2 {
+    margin-left: 0.5rem;
+}
+</style>

+ 1 - 0
src/agv-process/car.svg

@@ -0,0 +1 @@
+<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><rect width="200" height="200" fill="#f3f4f6"/><path d="M100 50L150 80V150H50V80Z" fill="#d1d1d1"/><path d="M100 50L50 80V150L100 180V110Z" fill="#a8a8a8"/><path d="M100 50L150 80L100 110L50 80Z" fill="#ffffff"/></svg>

+ 42 - 0
src/api/agv.js

@@ -0,0 +1,42 @@
+import request from '../util/request.js';
+
+// 查询还料区货位状态
+export function getLocatorStatus() {
+    return request({
+        url: '/api/LocatorResource/queryBlankAreaStatus',
+        method: 'get',
+    });
+}
+
+// 查询不在库的料车
+export function getNoInStorageTruck(no) {
+    return request({
+        url: '/api/TruckResource/queryNoInStorageTruck?no=' + no,
+        method: 'get',
+    });
+}
+
+// 根据还料货位和料车编号生成归还单和调度任务
+export function generateTaskByBlankNo(params) {
+    return request({
+        url: '/api/SchedulingTasksResource/generateTaskByBlankNo',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 查询送料区货位状态
+export function getLocatorFeedAreaStatus() {
+    return request({
+        url: '/api/LocatorResource/queryFeedAreaStatus',
+        method: 'get',
+    });
+}
+
+// 取走送料区料车
+export function takeAwaySkip(positionNo) {
+    return request({
+        url: '/api/LocatorResource/takeAwaySkip?positionNo=' + positionNo,
+        method: 'get',
+    });
+}

+ 9 - 0
src/api/login.js

@@ -0,0 +1,9 @@
+import request  from '../util/request.js';
+
+export function loginApi(params) {
+    return request({
+        url: '/authApi/LoginResource/login',
+        method: 'post',
+        data: params,
+    });
+}

+ 17 - 0
src/api/stock.js

@@ -0,0 +1,17 @@
+import request  from '../util/request.js';
+
+// 获取仓库列表
+export function getWarehouseList() {
+    return request({
+        url: '/api/WarehouseResource/queryByCondition',
+        method: 'get',
+    });
+}
+
+// 查询送料区空闲货位
+export function queryIdleLocator() {
+    return request({
+        url: '/api/LocatorResource/queryIdleLocator',
+        method: 'get',
+    });
+}

+ 26 - 0
src/api/stockIn.js

@@ -0,0 +1,26 @@
+import request  from '../util/request.js';
+
+// 闸机外侧扫描查询工装设备信息
+export function cfStockIn() {
+    return request({
+        url: '/api/stockInResource/queryInventoryByEPC',
+        method: 'get',
+    });
+}
+
+// 闸机外侧确认生成入库单
+export function createStockIn(params) {
+    return request({
+        url: '/api/stockInResource/generateCFStockIn',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 闸机内侧扫描查询工装设备信息
+export function cfStockInLeave() {
+    return request({
+        url: '/api/stockInResource/validationInsideTurnstile',
+        method: 'get',
+    });
+}

+ 108 - 0
src/api/stockOut.js

@@ -0,0 +1,108 @@
+import request  from '../util/request.js';
+
+
+// 可领料数据查询
+export function findInventory(params) {
+    return request({
+        url: '/api/InventoryResource/findInventoryByCondition',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 生成领料车
+export function createStockOutPrepareLine(params) {
+    return request({
+        url: '/api/PickingCarResource/generateCFPickCar',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 查询领料车数量
+export function queryPickingCarNumber() {
+    return request({
+        url: '/api/PickingCarResource/queryPickingCarCount',
+        method: 'get',
+    });
+}
+
+// 查询领料车物料列表
+export function queryCFPickCar(params) {
+    return request({
+        url: '/api/PickingCarResource/queryCFPickCar',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 将设备从领料车内删除
+export function deleteCFPickCar(params) {
+    return request({
+        url: '/api/PickingCarResource/deleteCFPickCar',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 生成领料单
+export function saveCFStockOutPrepare(params) {
+    return request({
+        url: '/api/StockOutPrepareResource/saveCFStockOutPrepare',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 查询常用领料
+export function queryCommonUse(params) {
+    return request({
+        url: '/api/BorroweLogResource/queryCommonUse',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 闸机外侧查询领料明细
+export function list(params) {
+    return request({
+        url: '/api/StockOutPrepareLineResource/queryCFStockOutPrepareLine',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 闸机外侧选择领料单明细生成出库单
+export function createStockOut(params) {
+    return request({
+        url: '/api/StockOutResource/generateCFStockOut',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 闸机内侧人工领料出库验证工装设备是否一致
+export function cfStockOut() {
+    return request({
+        url: '/api/StockOutResource/verificationCFStockOut',
+        method: 'get',
+    });
+}
+
+
+// 领料完成离开
+export function cfStockOutLeave(params) {
+    return request({
+        url: '/api/StockOutResource/leaveCFStockOut',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 查看AGV RFID识别记录
+export function getAgvRfidLogByTaskId() {
+    return request({
+        url: '/api/AGVRfidRecordLogResource/queryLastAgvRfidLog',
+        method: 'get',
+    });
+}

+ 57 - 0
src/assets/main.css

@@ -0,0 +1,57 @@
+/* 这三个 tailwind指令告诉 Tailwind CSS 注入三类样式: */
+/*  基础样式重置(如 margin、padding 的默认值)*/
+@tailwind base;
+/*  组件样式(如按钮、输入框等)*/
+@tailwind components;
+/*  工具类样式(如 bg-blue-500、flex、p-4 等)*/
+@tailwind utilities;
+
+body {
+  /* min-height: 1024px; */
+  font-family: ui-sans-serif, system-ui, sans-serif;
+}
+
+.circle-button {
+  width: 120px;
+  height: 120px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+}
+
+.circle-button:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
+}
+
+.circle-button i {
+  font-size: 32px;
+  margin-bottom: 8px;
+}
+
+.circle-button span {
+  font-weight: 500;
+}
+
+.user-info {
+  background-color: #f9fafb;
+  padding: 8px 16px;
+  border-radius: 9999px;
+  display: flex;
+  align-items: center;
+}
+
+.action-btn {
+  padding: 8px 16px;
+  border-radius: 9999px;
+  font-weight: 500;
+  transition: all 0.2s ease;
+}
+
+.action-btn:hover {
+  opacity: 0.9;
+}

+ 508 - 0
src/common/Common.js

@@ -0,0 +1,508 @@
+import { Notify } from 'pc-component-v3';
+import { notification } from 'ant-design-vue';
+
+export default {
+    pageSize: 20,
+
+
+    // 异常处理
+    processException: function (XMLHttpRequest, textStatus, errorThrown) {
+        var _self = this;
+        console.log(XMLHttpRequest);
+        if (XMLHttpRequest.status == 400) {
+            // 400 Bad Request
+            Notify.error('400', XMLHttpRequest.responseText, true);
+        } else if (XMLHttpRequest.status == 401) {
+            var currentUrl = window.location;
+            var href = window.location.href;
+            // 当前未处于登陆的界面
+            // 系统未登录
+            if (href.indexOf('login') < 0 && href.indexOf('redirectUrl=') < 0) {
+                // 处理钉钉免登陆
+                const clientId = this.getRouteParam('clientId');
+                const appName = this.getRouteParam('appName');
+                const corpId = this.getRouteParam('corpId');
+
+                let newUrl;
+                if(clientId != null && clientId.length > 0 && appName != null && appName.length > 0 && corpId != null && corpId.length > 0){
+                    newUrl = _self.getRedirectUrl('#/login?clientId=' + clientId + '&appName=' + appName + '&corpId=' + corpId + '&redirectUrl=' + encodeURIComponent(currentUrl));
+                }else{
+                    newUrl = _self.getRedirectUrl('#/login?redirectUrl=' + encodeURIComponent(currentUrl));
+                }
+                window.location = newUrl;
+            }
+        } else if (XMLHttpRequest.status == 500) {
+            // 500 Internal Server Error
+            Notify.error('500', XMLHttpRequest.responseText, true);
+            if (XMLHttpRequest.responseText.indexOf('登录超时') > 0) {
+                // 如果异常信息包含“登录超时”,则2秒后跳转到登录页面
+                setTimeout(function () {
+                    window.location = _self.getRedirectUrl('#/login');
+                }, 2 * 1000);
+            }
+        } else {
+            Notify.error('服务器异常', XMLHttpRequest.responseText, true);
+        }
+    },
+
+
+    /**
+   * 获取主机地址
+   */
+    getRootPath: function () {
+    //获取当前网址,如: http://localhost:8083/myproj/view/my.jsp
+        var curWwwPath = window.document.location.href;
+        //获取主机地址之后的目录,如: myproj/view/my.jsp
+        var pathName = window.document.location.pathname;
+        var pos = curWwwPath.indexOf(pathName);
+        //获取主机地址,如: http://localhost:8083
+        var localhostPaht = curWwwPath.substring(0, pos);
+        return localhostPaht;
+    },
+
+    getHostPageBaseURL: function () {
+        return this.getRootPath() + '/';
+    },
+
+    // 获取图片路径url
+    getFileServerUrl: function () {
+        return this.getRootPath() + '/';
+    },
+
+    // 获取API的地址
+    getApiURL: function (apiName) {
+        return this.getHostPageBaseURL() + 'api/' + apiName;
+    },
+
+    // 获取datacenterAPI的地址
+    getDataCenterURL: function (apiName) {
+        return this.getHostPageBaseURL() + apiName;
+    },
+
+    // 获取测试API的地址
+    getTestApiURL: function (apiName) {
+        return 'http://xxx/' + 'api/' + apiName;
+    },
+
+    // 获取中间件API的地址
+    getMidURL: function (apiName) {
+        var that = this;
+        if (window.middlewareUrl == undefined) {
+            $.ajax({
+                url: that.getApiURL('MiddleareResource/getApiUrl'),
+                type: 'GET',
+                dataType: 'text',
+                async: false,
+                beforeSend: function (request) {
+                    that.addTokenToRequest(request);
+                },
+                success: function (data) {
+                    if (data.indexOf('http') == 0) {
+                        window.middlewareUrl = data;
+                    } else {
+                        window.middlewareUrl = that.getHostPageBaseURL() + 'mid';
+                    }
+                    if (window.middlewareUrl.indexOf(window.middlewareUrl.length() - 1) == '/') {
+                        window.middlewareUrl = window.middlewareUrl.substring(0, window.middlewareUrl.length() - 2);
+                    }
+                },
+                error: function (XMLHttpRequest, textStatus, errorThrown) {
+                    window.middlewareUrl = that.getHostPageBaseURL() + 'mid';
+                },
+            });
+        }
+        if (apiName.indexOf(0) == '/') {
+            apiName = apiName.substring(1, apiName.length() - 2);
+        }
+        return window.middlewareUrl + apiName;
+    },
+
+    // 获取微信测试api地址
+    getWeixinApiURL: function (apiName) {
+        return this.getHostPageBaseURL() + '/api/' + apiName;
+    },
+
+    // 获取图片路径
+    getImageUrl: function (imageName) {
+        if (imageName == null || imageName == '') {
+            return this.getFileServerUrl() + 'notFound.png';
+        } else {
+            return this.getFileServerUrl() + imageName;
+        }
+    },
+
+    // 获取图片路径
+    getImageSrc: function (className, imageName) {  
+        if (imageName == null) {
+            return null;
+        }
+        return '/api/file/imageDownload?className=' + className + '&fileName=' + encodeURIComponent(imageName);
+    },
+
+
+    // 获取略缩图图片路径
+    getThumbnailImageSrc: function (className, imageName) {
+        if (imageName == null) {
+            return null;
+        }
+        return '/api/file/thumbnailImageDownload?className=' + className + '&fileName=' + encodeURIComponent(imageName);
+    },
+
+
+    /**
+   * 获取附件的路径
+   * @param  {[type]} className [description]
+   * @param  {[type]} imageName [description]
+   * @return {[type]}           [description]
+   */
+    getAttachmentsSrc: function (className, imageName) {
+        var accountId = localStorage.getItem('#accountId');
+        return this.getFileServerUrl() + 'Files/' + accountId + '/Attachments/' + className + '/' + imageName;
+    },
+
+    // 获取图片路径
+    getVideoSrc: function (className, imageName) {
+        var accountId = localStorage.getItem('#accountId');
+        if (imageName == undefined || imageName == '') {
+            return this.getHostPageBaseURL() + 'static/image/noImage.jpg';
+        }
+
+        return this.getFileServerUrl() + 'Files/' + accountId + '/Video/' + className + '/' + imageName;
+    },
+
+
+    //获取资源路径   type: 图片image,视频video,文件file,
+    getResourceUrl: function (type, className, resourceName) {
+        var accountId = localStorage.getItem('#accountId');
+        if (resourceName == undefined || className == undefined || type == undefined || resourceName == '' || className == '' || type == '') {
+            return;
+        }
+        if (type == 'image') {
+            return this.getFileServerUrl() + 'Files/' + accountId + '/Images/' + className + '/' + resourceName;
+        }
+        if (type == 'video') {
+            return this.getFileServerUrl() + 'Files/' + accountId + '/Video/' + className + '/' + resourceName;
+        }
+        if (type == 'file') {
+            return this.getFileServerUrl() + 'Files/' + accountId + '/Files/' + className + '/' + resourceName;
+        }
+
+    },
+
+    getApsURL: function (apiName) {
+        var apsBaseUrl = localStorage.getItem('apsBaseUrl');
+        if (apsBaseUrl == undefined) {
+            Notify.error('错误', '系统参数"apsBaseUrl"未设置,请联系系统管理员设置参数', false);
+            return;
+        }
+        return apsBaseUrl + apiName;
+    },
+
+    getActivitiURL: function (apiName) {
+        var apsBaseUrl = localStorage.getItem('activitiUrl');
+        if (apsBaseUrl == undefined) {
+            Notify.error('错误', '系统参数"activitiUrl"未设置,请联系系统管理员设置参数', false);
+            return;
+        }
+        return apsBaseUrl + apiName;
+    },
+
+    getSchedulingURL: function (apiName) {
+        var _self = this;
+        var apsBaseUrl = localStorage.getItem('schedulingUrl');
+        if (apsBaseUrl == undefined) {
+            _self.loadSystemParam('schedulingUrl', function () {
+                apsBaseUrl = localStorage.getItem('schedulingUrl');
+                if (apsBaseUrl == undefined) {
+                    Notify.error('错误', '系统参数"schedulingUrl"未设置,请联系系统管理员设置参数', false);
+                    return;
+                }
+            });
+        }
+        return apsBaseUrl + apiName;
+    },
+
+    getCameraURL: function (apiName) {
+        var apsBaseUrl = localStorage.getItem('cameraBaseURL');
+        if (apsBaseUrl == undefined) {
+            Notify.error('错误', '系统参数"cameraBaseURL"未设置,请联系系统管理员设置参数', false);
+            return;
+        }
+        return apsBaseUrl + apiName;
+    },
+
+    //设置路径到localStorage
+    setHref: function () {
+        var href = window.location.href;
+        if (href.indexOf('http') == 0) {
+            href = href.substring(0, href.indexOf('#') + 2);
+            localStorage.setItem('href', href);
+        } else {
+            var hostPageBaseURL = localStorage.getItem('hostPageBaseURL');
+            if (hostPageBaseURL == undefined) {
+                var href1 = window.location.href;
+                href1 = href1.substring(0, href1.indexOf('#') + 2);
+                localStorage.setItem('href', href1);
+            }
+        }
+    },
+
+    //加载系统参数到localStorage
+    loadSystemParam: function (systemParamName, success) {
+        var that = this;
+        $.ajax({
+            url: that.getApiURL('SystemParamResource/loadSystemParam'),
+            type: 'GET',
+            dataType: 'text',
+            data: {
+                'systemParamName': systemParamName,
+            },
+            beforeSend: function (request) {
+                that.addTokenToRequest(request);
+            },
+            success: function (data) {
+                localStorage.setItem(systemParamName, data);
+                success();
+            },
+            error: function (XMLHttpRequest, textStatus, errorThrown) {
+                that.processException(XMLHttpRequest, textStatus, errorThrown);
+            },
+        });
+    },
+
+    // 给请求头中加上account和token信息
+    addTokenToRequest: function (request) {
+        request.setRequestHeader('token', localStorage.getItem('#token'));
+
+    },
+
+    /**
+   * 获取Token
+   */
+    getToken: function () {
+        return localStorage.getItem('#token');
+    },
+
+    // 获取新建对象的Id
+    getNewRecordId: function () {
+        window.CRUDId++;
+        return window.CRUDId;
+    },
+
+    // 清空 Cookie
+    clearCookie: function () {
+        var keys = document.cookie.match(/[^ =;]+(?=\\=)/g);
+        if (keys) {
+            for (var i = keys.length; i--;) {
+                // 清除当前域名路径的有限日期
+                document.cookie = keys[i] + '=0;path=/;expires=' + new Date(0).toUTCString();
+                // Domain Name域名 清除当前域名的
+                document.cookie = keys[i] + '=0;path=/;domain=' + document.domain + ';expires=' + new Date(0).toUTCString();
+                // 清除一级域名下的或指定的
+                document.cookie = keys[i] + '=0;path=/;domain=baidu.com;expires=' + new Date(0).toUTCString();
+            }
+        }
+    },
+
+    clearLocalStorage: function () {
+    // 清理localStorage时需要保留的参数列表
+        var reserveParams = ['hostPageBaseURL', 'workShopId', 'resourceInstanceId',
+            'resourceInstanceName', 'apsBaseUrl', 'cameraBaseURL'];
+        //存放的信息
+        var reserveParamValues = [];
+
+        //获取参数信息 
+        var len = reserveParams.length;
+        for (var i = 0; i < len; i++) {
+            var reserveParam = reserveParams[i];
+            var reserveParamValue = '';
+            if (localStorage.getItem(reserveParam) != undefined) {
+                reserveParamValue = localStorage.getItem(reserveParam);
+            }
+            reserveParamValues.push(reserveParamValue);
+        }
+
+        //清理localStorage
+        window.localStorage.clear();
+
+        //还原参数信息
+        for (var j = 0; j < len; j++) {
+            localStorage.setItem(reserveParams[j], reserveParamValues[j]);
+        }
+
+    },
+
+    showDialog: function (title, content, type) {
+        if (type == 'success') {
+            Notify.success(title, content, 4000);
+        }
+        else if (type == 'error') {
+            Notify.error(title, content, -1);
+        }
+        else if (type == 'info') {
+            Notify.info(title, content, 2000);
+        }
+        else if (type == 'notice') {
+            Notify.notice(title, content, 2000);
+        }
+    },
+
+    //合并路由数组
+    mergeArray: function (arr1, arr2) {
+        var arr = [];
+        for (var k = 0, len = arr1.length; k < len; k++) {
+            arr.push(arr1[k]);
+        }
+        for (var i = 0, len2 = arr2.length; i < len2; i++) {
+            //是否添加该元素
+            var flag = true;
+            for (var j = 0, len1 = arr.length; j < len1; j++) {
+                //如果path相同,则合并children;如果children不存在,则不合并;
+                if (arr[j].path == arr2[i].path) {
+                    flag = false;
+                    if (arr[j].children instanceof Array && arr2[i].children instanceof Array) {
+                        arr[j].children = this.mergeArray(arr[j].children, arr2[i].children);
+                    }
+                }
+            }
+            if (flag) {
+                arr.push(arr2[i]);
+            }
+        }
+        //将path为"*"的项移到最后
+        var temp;
+        var index;
+        for (var n = 0, len3 = arr.length; n < len3; n++) {
+            if (arr[n].path == '*' || arr[n].path == '/*') {
+                index = n;
+            }
+        }
+        if (index != undefined) {
+            temp = arr[index];
+            arr[index] = arr[len3 - 1];
+            arr[len3 - 1] = temp;
+        }
+        return arr;
+    },
+
+    // 获取路由中的参数
+    getRouteParam: function (name) {
+        var reg = new RegExp('(^|\\?|&)' + name + '=([^&]*)(\\s|&|$)', 'i');
+        if (reg.test(location.href)) return unescape(RegExp.$2.replace(/\+/g, ' '));
+        return '';
+    },
+
+    /**
+   * 获取跳转的路径
+   * @param {*} url 
+   */
+    getRedirectUrl: function (url) {
+        var href = window.location.href;
+        if (href.indexOf('pcapp') >= 0) {
+            return this.getRootPath() + '/pcapp/' + url;
+        } else {
+            return this.getRootPath() +'/'+ url;
+        }
+    },
+
+    /**
+   * 可以修改url的参数
+   * 例如将
+   *    www.baidu.com
+   * 修改为
+   *    www.baidu.com?name=123
+   * 操作为
+   *    window.location.href = changeURLArg(window.location.href,'name',123)
+   */
+    changeURLArg: function (url, arg, value) {
+        var pattern = arg + '=([^&]*)';
+        var replaceText = arg + '=' + value;
+        if (url.match(pattern)) {
+            var tmp = '/(' + arg + '=)([^&]*)/gi';
+            tmp = url.replace(eval(tmp), replaceText);
+            return tmp;
+        } else {
+            if (url.match('[\\?]')) {
+                return url + '&' + replaceText;
+            } else {
+                return url + '?' + replaceText;
+            }
+        }
+    },
+
+};
+
+
+/**
+ * 请求错误
+ * @param {} err 
+ */
+export function requestFailed(err) {
+    console.error(err);
+    notification['error']({
+        message: '错误',
+        description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
+        duration: 8,
+    });
+}
+
+/**
+ * 请求正常
+ * @param {} response 
+ */
+export function requestSuccess(response) {
+    if (response.errorCode !== 0) {
+        notification['error']({
+            message: '错误',
+            description: response.errorMessage,
+            duration: 8,
+        });
+    }
+}
+
+/**
+ * 错误提示
+ * @param {} response 
+ */
+export function notificationError(message, title) {
+    notification['error']({
+        message: title || '操作失败',
+        description: message,
+        duration: 8,
+    });
+}
+
+
+/**
+ * 错误提示
+ * @param {} response 
+ */
+export function notificationSuccess(message, title) {
+    notification['success']({
+        message: title || '操作成功',
+        description: message,
+        duration: 8,
+    });
+}
+
+// 获取IP
+export function getRootPath() {
+    var protocol = window.location.protocol;
+    var host = window.location.host;
+    var localhostPath = `${protocol}//${host}`;
+    return localhostPath;
+}
+
+// 格式化时间为 YYYY-MM-DD HH:mm:ss
+export function getFormattedDateTime() {
+    const now = new Date();
+  
+    const year = now.getFullYear();
+    const month = String(now.getMonth() + 1).padStart(2, '0');
+    const day = String(now.getDate()).padStart(2, '0');
+    const hours = String(now.getHours()).padStart(2, '0');
+    const minutes = String(now.getMinutes()).padStart(2, '0');
+    const seconds = String(now.getSeconds()).padStart(2, '0');
+  
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+}

+ 299 - 0
src/common/CommonTable.vue

@@ -0,0 +1,299 @@
+<template>
+  <div class="tablePaganations">
+    <!-- <a-config-provider :locale="locale"> -->
+    <a-table
+      id="commonTable" class="ant-table-striped" bordered size="small" height="1000px" :loading="isLoading"
+      :data-source="dataSource" :columns="columns" :row-key="(record) => record.id" :scroll="{ y: yScroll }"
+      :pagination="havePage ? pagination : false" :row-class-name="(_record, index) => (index % 2 === 1 ? 'table-striped' : null)
+      " :row-selection="isSelect
+        ? {
+          type: selectType === 'radio' ? 'radio' : 'checkbox',
+          selectedRowKeys: state.selectedRowKeys,
+          onSelect: selectEvent,
+          onSelectAll: selectAllEvent,
+          hideSelectAll: hideSelectAll,
+        }
+        : null
+      " :custom-row="isCustomRowClick ? customRowClick : null" @change="tableChange"
+      @resize-column="handleResizeColumn"
+    >
+      <template v-for="(item, index) in renderArr" #[item]="scope" :key="index">
+        <slot :name="item" :scope="scope" v-bind="scope || {}" />
+      </template>
+    </a-table>
+    <!-- </a-config-provider> -->
+  </div>
+</template>
+
+<script setup>
+import {
+    useSlots,
+    ref,
+    reactive,
+    defineProps,
+    defineEmits,
+    defineExpose,
+    watch,
+    onMounted,
+} from 'vue';
+import { getTableScroll } from '../common/tableScroll.js';
+// import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN';
+// const locale = ref(zhCN);
+
+const props = defineProps({
+    // 表格数据
+    dataSource: {
+        type: Object,
+        required: true,
+    },
+    // 表头数据
+    columns: {
+        type: Object,
+        required: true,
+    },
+    // 是否可选择
+    isSelect: {
+        type: Boolean,
+    },
+    // 表格loading
+    isLoading: {
+        type: Boolean,
+    },
+    // 数据总数
+    total: {
+        type: Number,
+        default: 0,
+    },
+    // 是否分页
+    havePage: {
+        type: Boolean,
+        default: true,
+    },
+    // 是否隐藏全选
+    hideSelectAll: {
+        type: Boolean,
+        default: false,
+    },
+    // 单选多选
+    selectType: {
+        type: String,
+        default: 'checkbox',
+    },
+    // 分页在右上角
+    topRight: {
+        type: Boolean,
+        default: false,
+    },
+    // 表格距底部高度
+    extraHeight: {
+        type: Number,
+        default: undefined,
+    },
+    // 选择的key值
+    selectedKeys: {
+        type: Array,
+        default: () => [],
+    },
+    // 是否自定义行点击
+    isCustomRowClick: {
+        type: Boolean,
+        default: false,
+    },
+    // 自定义默认数量
+    defaultPageSize: {
+        type: Number,
+        default: 20,
+    },
+});
+
+const emit = defineEmits(['getPager', 'getSorter', 'getSelected', 'customRowClick']);
+
+// 分页配置
+const pagination = reactive({
+    showQuickJumper: true,
+    current: 1,
+    pageSize: props.defaultPageSize, // 默认每页显示数量
+    showSizeChanger: true, // 显示可改变每页数量
+    pageSizeOptions: ['10', '20', '50', '100', '200', '500'], // 每页数量选项值
+    showTotal: (total, range) =>
+        range[0] + '-' + range[1] + '条' + ' 共' + total + '条', // 显示总数
+    onShowSizeChange: (current, pageSize) => showSizeChange(current, pageSize),
+    onChange: (current, pageSize) => changePage(current, pageSize), //点击页码事件
+    total: props.total,
+    position: props.topRight ? ['topRight'] : ['bottomRight'],
+});
+
+const yScroll = ref(400); //默认滚动高度
+const extraHeight = ref(undefined); //表格距离底部值
+
+// 最后一次排序信息
+const lastSorter = reactive({ field: '', order: '' });
+
+//  选择的数据
+const state = reactive({
+    selectedRows: [],
+    selectedRowKeys: [],
+});
+
+onMounted(() => {
+    if (!props.havePage) {
+        extraHeight.value = 30;
+    } else {
+        extraHeight.value = props.extraHeight;
+    }
+    if (props.extraHeight) {
+        extraHeight.value = props.extraHeight;
+    }
+    onResizeTable();
+    window.onresize = () => {
+        onResizeTable();
+    };
+});
+
+// 表格位置
+const onResizeTable = () => {
+    yScroll.value = getTableScroll({
+        extraHeight: extraHeight.value,
+        id: 'commonTable',
+    });
+};
+
+//点击页码事件
+const changePage = (current, size) => {
+    pagination.current = current;
+    emit('getPager', pagination.current, size);
+};
+
+// 改变每页数量时更新显示
+const showSizeChange = (current, pageSize) => {
+    setTimeout(() => {
+        pagination.current = 1;
+        emit('getPager', pagination.current, pageSize);
+    });
+    pagination.pageSize = pageSize;
+};
+
+// 回到第一页
+const backFirstPage = () => {
+    pagination.current = 1;
+    emit('getPager', pagination.current, pagination.pageSize);
+};
+
+// 伸缩列
+const handleResizeColumn = (w, col) => {
+    col.width = w;
+};
+
+// 选择每一项操作
+const selectEvent = (record, selected) => {
+    if (props.selectType === 'radio') {
+        state.selectedRows = [record];
+        state.selectedRowKeys = [record.id];
+    } else {
+        if (selected) {
+            state.selectedRows.push(record);
+            state.selectedRowKeys.push(record.id);
+        } else {
+            let index = state.selectedRowKeys.indexOf(record.id);
+            if (index >= 0) {
+                state.selectedRows.splice(index, 1);
+                state.selectedRowKeys.splice(index, 1);
+            }
+        }
+    }
+    emit('getSelected', state);
+};
+
+// 点击以后全选当前分页数据
+const selectAllEvent = (selected, selectedRows, changeRows) => {
+    if (selected) {
+        changeRows.forEach(item => {
+            state.selectedRows.push(item);
+            state.selectedRowKeys.push(item.id);
+        });
+    } else {
+        changeRows.forEach(item => {
+            let index = state.selectedRowKeys.indexOf(item.id);
+            if (index >= 0) {
+                state.selectedRows.splice(index, 1);
+                state.selectedRowKeys.splice(index, 1);
+            }
+        });
+    }
+    emit('getSelected', state);
+};
+
+// 清空选择
+const clear = () => {
+    state.selectedRowKeys = [];
+    state.selectedRows = [];
+    emit('getSelected', state);
+};
+
+// 获取排序信息
+const tableChange = (pagination, filters, sorter) => {
+    // pagination, filters 变化时也会触发所以对sorter进行判断限制执行
+    if (Object.keys(sorter).length > 0) {
+        if (sorter.field != lastSorter.field && sorter.order != lastSorter.order) {
+            lastSorter.field = sorter.field;
+            lastSorter.order = sorter.order;
+            emit('getSorter', sorter);
+        }
+        if (sorter.field != lastSorter.field && sorter.order == lastSorter.order) {
+            lastSorter.field = sorter.field;
+            lastSorter.order = sorter.order;
+            emit('getSorter', sorter);
+        }
+        if (sorter.field == lastSorter.field && sorter.order != lastSorter.order) {
+            lastSorter.field = sorter.field;
+            lastSorter.order = sorter.order;
+            emit('getSorter', sorter);
+        }
+    }
+};
+
+// 自定义行点击
+const customRowClick = record => {
+    return {
+        onDblclick() {
+            emit('customRowClick', record);
+        },
+    };
+};
+
+// 暴露出方法
+defineExpose({ backFirstPage, clear });
+
+// 监听total变化
+watch(
+    props,
+    newData => {
+        pagination.total = newData.total;
+        extraHeight.value = newData.extraHeight;
+    },
+    { immediate: true, deep: true },
+);
+watch(
+    () => props.selectedKeys,
+    newData => {
+        state.selectedRowKeys = newData;
+        state.selectedRows = newData.map(item => props.dataSource.find(data => data.id === item));
+        emit('getSelected', state);
+        console.log(state.selectedRows);
+    },
+    { immediate: true, deep: true },
+);
+// 插槽的实例
+const slots = useSlots();
+const renderArr = Object.keys(slots);
+</script>
+<style scoped>
+.tablePaganations {
+  width: 100%;
+  /* margin-top: 8px; */
+}
+
+.ant-table-striped :deep(.table-striped) td {
+  background-color: #fafafa;
+}
+</style>

+ 98 - 0
src/common/FilterPanel.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="filter-panel">
+    <div class="filter-header" @click="toggleCollapse">
+      <div class="filter-title">
+        <i class="fas fa-filter mr-2" />
+        <span>筛选条件</span>
+        <a-tag v-if="collapsed" color="blue" class="ml-2">{{ activeFilterCount }} 个条件</a-tag>
+      </div>
+      <div class="filter-toggle">
+        <span class="text-sm text-gray-500 mr-2">{{ collapsed ? '展开' : '收起' }}</span>
+        <i :class="collapsed ? 'fas fa-chevron-down' : 'fas fa-chevron-up'" class="text-gray-500" />
+      </div>
+    </div>
+    
+    <div v-show="!collapsed" class="filter-content">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, defineProps } from 'vue';
+
+const props = defineProps({
+    defaultCollapsed: {
+        type: Boolean,
+        default: false,
+    },
+    activeCount: {
+        type: Number,
+        default: 0,
+    },
+});
+
+const collapsed = ref(props.defaultCollapsed);
+
+const activeFilterCount = computed(() => props.activeCount);
+
+const toggleCollapse = () => {
+    collapsed.value = !collapsed.value;
+};
+</script>
+
+<style scoped>
+.filter-panel {
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  margin-bottom: 1.5rem;
+  overflow: hidden;
+}
+
+.filter-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 1rem 1.5rem;
+  cursor: pointer;
+  user-select: none;
+  transition: background-color 0.2s;
+  border-bottom: 1px solid #f3f4f6;
+}
+
+.filter-header:hover {
+  background-color: #f9fafb;
+}
+
+.filter-title {
+  display: flex;
+  align-items: center;
+  font-weight: 600;
+  color: #374151;
+  font-size: 0.95rem;
+}
+
+.filter-toggle {
+  display: flex;
+  align-items: center;
+  color: #6b7280;
+  font-size: 0.875rem;
+}
+
+.filter-content {
+  padding: 1.5rem;
+  animation: slideDown 0.3s ease-out;
+}
+
+@keyframes slideDown {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+</style>

+ 164 - 0
src/common/InventoryCard.vue

@@ -0,0 +1,164 @@
+<template>
+  <a-card
+    :hoverable="hoverable"
+    :class="{ 'selected-card': isSelected }"
+    class="inventory-card"
+    @click="$emit('card-click')"
+  >
+    <template #title>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center flex-1">
+          <a-checkbox
+            v-if="selectable"
+            :checked="isSelected"
+            @click.stop
+            @change="e => $emit('checkbox-change', e)"
+          />
+          <div :class="selectable ? 'ml-3' : ''" class="flex flex-col">
+            <div class="card-title-line">
+              <span class="card-name">{{ item.inventoryName || item.name }}</span>
+            </div>
+            <div class="card-subtitle">
+              编号: {{ item.inventoryNo || item.no }}
+            </div>
+          </div>
+        </div>
+        <a-avatar :size="36" :style="{ backgroundColor: iconColor }">
+          <template #icon>
+            <i :class="icon" style="font-size: 22px;" />
+          </template>
+        </a-avatar>
+      </div>
+    </template>
+
+    <div class="card-content">
+      <div class="card-main-info">
+        <i class="fas fa-map-marker-alt text-blue-500 mr-2" />
+        <span class="location-text">
+          {{ location }}
+        </span>
+      </div>
+      <slot></slot>
+    </div>
+  </a-card>
+</template>
+
+<script setup>
+import { defineProps, defineEmits, computed } from 'vue';
+
+const props = defineProps({
+  item: {
+    type: Object,
+    required: true,
+  },
+  isSelected: {
+    type: Boolean,
+    default: false,
+  },
+  selectable: {
+    type: Boolean,
+    default: true,
+  },
+  hoverable: {
+    type: Boolean,
+    default: true,
+  },
+  iconColor: {
+    type: String,
+    default: '#3b82f6',
+  },
+});
+
+const emit = defineEmits(['card-click', 'checkbox-change']);
+
+const icon = computed(() => {
+  const type = props.item.inventoryType || props.item.type;
+  const iconMap = {
+    '工装': 'fas fa-cube',
+    '设备': 'fas fa-cogs',
+    '成品': 'fas fa-box',
+  };
+  return iconMap[type] || 'fas fa-cube';
+});
+
+const location = computed(() => {
+  const position = props.item.inventoryActulPosition || props.item.inventoryPosition || props.item.positionName || '-';
+  const warehouse = props.item.inventoryWarehouse || props.item.warehouseName || '-';
+  return `${position} / ${warehouse}`;
+});
+</script>
+
+<style scoped>
+.inventory-card {
+  cursor: pointer;
+  transition: all 0.3s ease;
+  border: 2px solid transparent;
+  background-color: #eef4ff;
+}
+
+.inventory-card:hover {
+  transform: translateY(-2px);
+}
+
+.inventory-card.selected-card {
+  border-color: #3b82f6 !important;
+  background-color: #dbeafe;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
+}
+
+.card-content {
+  margin-top: -8px;
+}
+
+.card-title-line {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+}
+
+.card-name {
+  font-size: 1.25rem;
+  font-weight: 700;
+  color: #1f2937;
+  line-height: 1.4;
+}
+
+.card-subtitle {
+  font-size: 0.8rem;
+  color: #9ca3af;
+  margin-top: 2px;
+  line-height: 1.2;
+}
+
+.card-main-info {
+  display: flex;
+  align-items: center;
+  padding: 8px 0;
+  color: #6b7280;
+}
+
+.card-main-info i {
+  font-size: 14px;
+  flex-shrink: 0;
+}
+
+.location-text {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.5;
+  margin-left: 6px;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 16px 0 2px 0;
+}
+
+:deep(.ant-card-body) {
+  padding: 12px 20px 16px 26px;
+}
+
+:deep(.ant-card-head) {
+  border-bottom: none;
+  min-height: auto;
+}
+</style>

+ 121 - 0
src/common/PageHeader.vue

@@ -0,0 +1,121 @@
+<template>
+  <header class="page-header">
+    <div class="header-left">
+      <button v-if="showBack" class="action-btn back-btn" @click="handleBack">
+        <i class="fas fa-arrow-left mr-1" /> 返回
+      </button>
+      <div class="user-info">
+        <i class="fas fa-user text-blue-500 mr-2" />
+        <span class="font-medium">操作员:{{ operatorName }} (ID: {{ operatorId }})</span>
+      </div>
+    </div>
+    <div class="header-right">
+      <!-- 自定义插槽,用于插入额外按钮(如领料车) -->
+      <slot name="actions" />
+      
+      <button class="action-btn logout-btn" @click="handleLogout">
+        <i class="fas fa-sign-out-alt mr-1" /> 登出
+      </button>
+    </div>
+  </header>
+</template>
+
+<script setup>
+import { ref, defineProps, defineEmits } from 'vue';
+import { useRouter } from 'vue-router';
+
+const props = defineProps({
+    showBack: {
+        type: Boolean,
+        default: true,
+    },
+    isGoHome: {
+        type: Boolean,
+        default: true,
+    },
+});
+
+const emit = defineEmits(['back', 'logout']);
+const router = useRouter();
+
+const operatorName = ref('管理员');
+const operatorId = ref('lw001');
+
+const handleBack = () => {
+    if(props.isGoHome){
+        router.push('/home');
+    }else{
+        router.back();
+    }
+    emit('back');
+};
+
+const handleLogout = () => {
+    emit('logout');
+    localStorage.removeItem('#LoginInfo');
+    localStorage.removeItem('#token');
+    localStorage.removeItem('#accountId');
+    router.push('/login');
+};
+</script>
+
+<style scoped>
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 1rem 1.5rem;
+  background-color: white;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  position: sticky;
+  top: 0;
+  z-index: 100;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+}
+
+.action-btn {
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+  border: none;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.back-btn {
+  background-color: #f3f4f6;
+  color: #374151;
+}
+
+.back-btn:hover {
+  background-color: #e5e7eb;
+}
+
+.logout-btn {
+  background-color: #fee2e2;
+  color: #dc2626;
+}
+
+.logout-btn:hover {
+  background-color: #fecaca;
+}
+</style>

+ 26 - 0
src/common/tableScroll.js

@@ -0,0 +1,26 @@
+/**
+* 获取表格的可视化高度
+* @param {*} extraHeight 额外的高度(表格底部的内容高度) 
+* @param {*} id 当前页面中有多个table时需要制定table的id
+*/
+export const getTableScroll = ({ extraHeight, id }) => {
+    if (typeof extraHeight == 'undefined') {
+    //  默认底部分页高度
+        extraHeight = 60;
+    }
+    let tHeader = null;
+    if (id) {
+        tHeader = document.getElementById(id) ?
+            document.getElementById(id).getElementsByClassName('ant-table-thead')[0] : null;
+    } else {
+        tHeader = document.getElementsByClassName('ant-table-thead')[0];
+    }
+    //表格内容距离顶部的距离
+    let tHeaderBottom = 0;
+    if (tHeader) {
+        tHeaderBottom = tHeader.getBoundingClientRect().bottom;
+    }
+    //窗体高度-表格内容顶部的高度-表格内容底部的高度
+    let height = `calc(100vh - ${tHeaderBottom + extraHeight}px)`;
+    return height;
+};

+ 10 - 0
src/common/utils.js

@@ -0,0 +1,10 @@
+// 防抖函数
+export const debounce = (fn, wait = 1000) => {
+  let timer;
+  return function (...args) {
+    clearTimeout(timer);
+    timer = setTimeout(() => {
+      fn.call(this, args);
+    }, wait);
+  };
+};

+ 518 - 0
src/login/UserHome.vue

@@ -0,0 +1,518 @@
+<!-- 主页 -->
+<template>
+  <div class="bg-gray-50 min-h-screen">
+    <!-- 顶部信息栏 -->
+    <PageHeader :show-back="false" />
+
+    <!-- 主内容区域 -->
+    <main class="container mx-auto px-6 py-12 pb-40">
+      <div class="text-center mb-16">
+        <h1 class="text-4xl font-bold text-gray-800 mb-4">智能仓储管理系统</h1>
+        <p class="text-xl text-gray-600 max-w-2xl mx-auto">高效管理物料流转,实时监控库存状态,优化仓储作业流程</p>
+      </div>
+
+      <!-- 数据概览卡片 -->
+      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
+        <div
+          v-for="stat in statistics" :key="stat.title" class="bg-white rounded-xl shadow-md p-6 border-l-4"
+          :class="getStatColorClasses(stat.color, 'border')"
+        >
+          <div class="flex justify-between items-start">
+            <div>
+              <p class="text-gray-500 text-sm">{{ stat.title }}</p>
+              <p class="text-3xl font-bold mt-2">{{ stat.value }}</p>
+            </div>
+            <div class="p-3 rounded-lg" :class="getStatColorClasses(stat.color, 'bg')">
+              <i class="text-xl" :class="['fas', stat.icon, getStatColorClasses(stat.color, 'text')]" />
+            </div>
+          </div>
+          <div class="mt-4">
+            <p class="text-sm" :class="stat.trend > 0 ? 'text-green-500' : 'text-red-500'">
+              <i class="mr-1" :class="stat.trend > 0 ? 'fas fa-arrow-up' : 'fas fa-arrow-down'" />{{ stat.changeText }}
+            </p>
+          </div>
+        </div>
+      </div>
+
+      <!-- 物料分类区域 -->
+      <div class="mb-16">
+        <h2 class="text-2xl font-bold text-gray-800 mb-6">物料分类</h2>
+        <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
+          <div
+            v-for="category in materialCategories" :key="category.name"
+            class="bg-white rounded-lg shadow-sm p-4 text-center cursor-pointer hover:shadow-md transition-shadow"
+          >
+            <div class="w-full h-20 overflow-hidden mb-3 rounded-lg">
+              <img :src="category.image" :alt="category.name" class="w-full h-full object-cover object-top" />
+            </div>
+            <p class="font-medium">{{ category.name }}</p>
+            <p class="text-gray-500 text-sm">{{ category.count }} 种</p>
+          </div>
+        </div>
+      </div>
+
+      <!-- 最近操作记录 -->
+      <div>
+        <div class="flex justify-between items-center mb-6">
+          <h2 class="text-2xl font-bold text-gray-800">最近操作记录</h2>
+          <button class="text-blue-500 hover:text-blue-700 font-medium" @click="viewAllRecords">
+            查看全部 <i class="fas fa-chevron-right ml-1 text-xs" />
+          </button>
+        </div>
+        <div class="bg-white rounded-xl shadow-sm overflow-hidden">
+          <a-table :columns="columns" :data-source="recentRecords" :pagination="false" :scroll="{ x: true }">
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'operationType'">
+                <span class="px-3 py-1 rounded-full text-sm" :class="getOperationTypeClass(record.operationType)">
+                  {{ record.operationType }}
+                </span>
+              </template>
+              <template v-if="column.key === 'status'">
+                <span class="text-green-500">
+                  <i class="fas fa-check-circle mr-1" />{{ record.status }}
+                </span>
+              </template>
+            </template>
+          </a-table>
+        </div>
+      </div>
+    </main>
+
+    <!-- 底部圆形功能按钮 -->
+    <footer class="fixed bottom-0 left-0 right-0 bg-white shadow-lg py-6 z-50">
+      <div class="container mx-auto px-6">
+        <div class="flex justify-center space-x-12">
+          <template v-if="isOut">
+            <button
+              v-for="action in outButtons" :key="action.label" class="circle-button text-white"
+              :class="getButtonClass(action.color)" @click="handleAction(action.action)"
+            >
+              <i :class="`fas ${action.icon}`" />
+              <span>{{ action.label }}</span>
+            </button>
+          </template>
+          <template v-if="!isOut">
+            <button
+              v-for="action in inButtons" :key="action.label" class="circle-button text-white"
+              :class="getButtonClass(action.color)" @click="handleAction(action.action)"
+            >
+              <i :class="`fas ${action.icon}`" />
+              <span>{{ action.label }}</span>
+            </button>
+          </template>
+        </div>
+      </div>
+    </footer>
+
+    <!-- 拣货确认弹窗 -->
+    <a-modal
+      v-model:open="pickingModalVisible" title="请确认您要进行的操作?" :closable="true" :mask-closable="false"
+      @ok="handlePickingConfirm" @cancel="pickingModalVisible = false"
+    >
+      <template #footer>
+        <a-button @click="handlePickingCancel">否</a-button>
+        <a-button type="primary" danger @click="handlePickingConfirm">是</a-button>
+      </template>
+      <p>您是否已经申请了领料。</p>
+    </a-modal>
+  </div>
+  <Loading v-if="loading" />
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
+import PageHeader from '../common/PageHeader.vue';
+import { cfStockInLeave } from '../api/stockIn.js';
+import { message } from 'ant-design-vue';
+
+// 路由
+const router = useRouter();
+
+
+// 加载状态
+const loading = ref(false);
+
+// 弹窗控制
+const pickingModalVisible = ref(false);
+
+// 是否为外侧屏幕
+const isOut = ref(true);
+
+// 统计数据
+const statistics = reactive([
+    {
+        title: '总库存量',
+        value: '12,847',
+        icon: 'fa-boxes',
+        color: 'blue',
+        trend: 1,
+        changeText: '较昨日 +2.3%',
+    },
+    {
+        title: '今日入库',
+        value: '1,248',
+        icon: 'fa-truck-loading',
+        color: 'green',
+        trend: 1,
+        changeText: '较昨日 +5.7%',
+    },
+    {
+        title: '今日出库',
+        value: '36',
+        icon: 'fa-tasks',
+        color: 'yellow',
+        trend: -1,
+        changeText: '较昨日 -1.2%',
+    },
+    {
+        title: '库存周转率',
+        value: '4.2',
+        icon: 'fa-sync-alt',
+        color: 'purple',
+        trend: 1,
+        changeText: '较上月 +0.8%',
+    },
+]);
+
+// 物料分类
+const materialCategories = reactive([
+    {
+        name: '电子元件',
+        count: 248,
+        image: 'https://ai-public.mastergo.com/ai/img_res/b752a3a8cb35d2b915a23b12981693d8.jpg',
+    },
+    {
+        name: '机械零件',
+        count: 186,
+        image: 'https://ai-public.mastergo.com/ai/img_res/1d4cf39dc61dadac88754be3bd525e3c.jpg',
+    },
+    {
+        name: '化工原料',
+        count: 92,
+        image: 'https://ai-public.mastergo.com/ai/img_res/4f12a387c9413be821c1d203bda09622.jpg',
+    },
+    {
+        name: '包装材料',
+        count: 64,
+        image: 'https://ai-public.mastergo.com/ai/img_res/c2d2f76aebbef43067223fd11e120c49.jpg',
+    },
+    {
+        name: '办公用品',
+        count: 127,
+        image: 'https://ai-public.mastergo.com/ai/img_res/404ab042346c15d0722e9d2eae6bda99.jpg',
+    },
+    {
+        name: '维修工具',
+        count: 89,
+        image: 'https://ai-public.mastergo.com/ai/img_res/048d0b638d5cb1f1bd11916983979fed.jpg',
+    },
+]);
+
+// 表格列定义
+const columns = [
+    {
+        title: '时间',
+        dataIndex: 'time',
+        key: 'time',
+        width: 150,
+    },
+    {
+        title: '操作类型',
+        dataIndex: 'operationType',
+        key: 'operationType',
+        width: 120,
+    },
+    {
+        title: '物料名称',
+        dataIndex: 'materialName',
+        key: 'materialName',
+        width: 200,
+    },
+    {
+        title: '数量',
+        dataIndex: 'quantity',
+        key: 'quantity',
+        width: 100,
+    },
+    {
+        title: '操作员',
+        dataIndex: 'operator',
+        key: 'operator',
+        width: 100,
+    },
+    {
+        title: '状态',
+        dataIndex: 'status',
+        key: 'status',
+        width: 120,
+    },
+];
+
+// 最近操作记录
+const recentRecords = reactive([
+    {
+        key: '1',
+        time: '2023-12-15 14:32',
+        operationType: '入库',
+        materialName: '集成电路芯片 IC-2023',
+        quantity: '+500',
+        operator: '张伟',
+        status: '已完成',
+    },
+    {
+        key: '2',
+        time: '2023-12-15 13:45',
+        operationType: '领料',
+        materialName: '电阻 R-1KΩ 1/4W',
+        quantity: '-200',
+        operator: '李娜',
+        status: '已完成',
+    },
+    {
+        key: '3',
+        time: '2023-12-15 11:20',
+        operationType: '还料',
+        materialName: '电容 C-100μF 50V',
+        quantity: '+50',
+        operator: '王强',
+        status: '已完成',
+    },
+    {
+        key: '4',
+        time: '2023-12-15 09:15',
+        operationType: '出库',
+        materialName: '连接器 CONN-USB-C',
+        quantity: '-150',
+        operator: '赵敏',
+        status: '已完成',
+    },
+    {
+        key: '5',
+        time: '2023-12-14 16:40',
+        operationType: '入库',
+        materialName: '传感器 SHT-30',
+        quantity: '+300',
+        operator: '孙磊',
+        status: '已完成',
+    },
+]);
+
+// 底部操作按钮
+const outButtons = reactive([
+    {
+        label: '领料',
+        icon: 'fa-hand-holding',
+        color: 'blue',
+        action: 'materialOut',
+    },
+    {
+        label: '拣货',
+        icon: 'fa-undo',
+        color: 'green',
+        action: 'materialReturn',
+    },
+    {
+        label: '出库确认',
+        icon: 'fa-arrow-down',
+        color: 'yellow',
+        action: 'materialOutLeave',
+    },
+
+    {
+        label: '还料',
+        icon: 'fa-arrow-up',
+        color: 'red',
+        action: 'materialIn',
+    },
+    {
+        label: '还料离开',
+        icon: 'fa-arrow-down',
+        color: 'yellow',
+        action: 'materialInLeave',
+    },
+    {
+        label: 'AGV RFID校验',
+        icon: 'fa-wifi',
+        color: 'blue',
+        action: 'agvRfidRecognition',
+    },
+
+    //  测试待删除
+    {
+        label: '还料区管理',
+        icon: 'fa-undo',
+        color: 'green',
+        action: 'returnManagement',
+    },
+    {
+        label: '取料区管理',
+        icon: 'fa-arrow-down',
+        color: 'red',
+        action: 'deliverManagement',
+    },
+]);
+const inButtons = reactive([
+    {
+        label: '还料',
+        icon: 'fa-hand-holding',
+        color: 'blue',
+        action: 'materialIn',
+    },
+    {
+        label: '拣货',
+        icon: 'fa-undo',
+        color: 'green',
+        action: 'materialReturn',
+    },
+    {
+        label: '还料离开',
+        icon: 'fa-arrow-down',
+        color: 'yellow',
+        action: 'materialInLeave',
+    },
+    {
+        label: 'AGV RFID校验',
+        icon: 'fa-wifi',
+        color: 'blue',
+        action: 'agvRfidRecognition',
+    },
+]);
+
+// 查看全部记录逻辑
+const viewAllRecords = () => {
+    console.log('查看全部');
+};
+
+// 拣货确认弹窗处理
+const handlePickingConfirm = () => {
+    pickingModalVisible.value = false;
+    router.push('/order-picking');
+};
+
+// 去领料
+const handlePickingCancel = () => {
+    pickingModalVisible.value = false;
+    router.push('/stock-requisition');
+};
+
+// 根据不同的action执行不同的逻辑
+const handleAction = action => {
+    console.log('Action clicked:', action);
+    switch (action) {
+    case 'materialOut':
+        // 领料 - 跳转到领料界面
+        router.push('/stock-requisition');
+        break;
+    case 'materialReturn':
+        // 拣货 - 显示确认弹窗
+        pickingModalVisible.value = true;
+        break;
+    case 'materialOutLeave':
+        // 领料出库离开
+        router.push('/outbound-confirm');
+        break;
+    case 'materialReturnLeave':
+        // 还料出库离开
+        break;
+    case 'agvRfidRecognition':
+        // RFID识别
+        router.push('/agv-rfid-recognition');
+        break;
+    case 'materialIn':
+        // 还料 - 跳转到还料界面
+        router.push('/in-confirm');
+        break;
+    case 'materialInLeave':
+        // 还料出库离开
+        validateMaterialInLeave();
+        break;
+
+
+    // 测试待删除
+    case 'returnManagement':
+        router.push('/return-management');
+        break;
+    case 'deliverManagement':
+        router.push('/delivery-management');
+        break;
+    }
+
+
+};
+
+// 校验还料离开时是否带有工装设备信息
+const validateMaterialInLeave = async () => {
+    loading.value = true;
+    try {
+        const res = await cfStockInLeave();
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                router.push('/in-confirm?isLeave=true');
+            } else {
+                message.success('您已完成还料操作,请在闸机开门后离开');
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('校验还料离开时是否带有工装设备信息失败:', error);
+        message.error('校验还料离开时是否带有工装设备信息失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 渲染样式
+const getOperationTypeClass = type => {
+    const classMap = {
+        '入库': 'bg-green-100 text-green-800',
+        '领料': 'bg-blue-100 text-blue-800',
+        '还料': 'bg-yellow-100 text-yellow-800',
+        '出库': 'bg-red-100 text-red-800',
+    };
+    return classMap[type] || 'bg-gray-100 text-gray-800';
+};
+
+// 按钮样式
+const getButtonClass = color => {
+    const colorMap = {
+        blue: 'bg-blue-500 hover:bg-blue-600',
+        green: 'bg-green-500 hover:bg-green-600',
+        yellow: 'bg-yellow-500 hover:bg-yellow-600',
+        red: 'bg-red-500 hover:bg-red-600',
+    };
+    return colorMap[color] || 'bg-gray-500 hover:bg-gray-600';
+};
+
+// 统计样式
+const getStatColorClasses = (color, type) => {
+    const colorMap = {
+        blue: {
+            border: 'border-blue-500',
+            bg: 'bg-blue-100',
+            text: 'text-blue-500',
+        },
+        green: {
+            border: 'border-green-500',
+            bg: 'bg-green-100',
+            text: 'text-green-500',
+        },
+        yellow: {
+            border: 'border-yellow-500',
+            bg: 'bg-yellow-100',
+            text: 'text-yellow-500',
+        },
+        purple: {
+            border: 'border-purple-500',
+            bg: 'bg-purple-100',
+            text: 'text-purple-500',
+        },
+    };
+    return colorMap[color]?.[type] || '';
+};
+</script>
+
+<style scoped>
+/* Additional component-specific styles if needed */
+</style>

+ 391 - 0
src/login/UserLogin.vue

@@ -0,0 +1,391 @@
+<!-- 智能仓储管理系统登录页面 -->
+
+<template>
+  <div class="login-container">
+    <!-- 背景装饰 -->
+    <div class="background-decoration" />
+
+    <!-- 主内容区域 -->
+    <div class="login-content">
+      <!-- 左侧信息区 -->
+      <div class="login-left">
+        <div class="system-info">
+          <h1 class="system-title">智能仓储管理系统</h1>
+          <div class="feature-list">
+            <div class="feature-item">
+              <i class="fas fa-check-circle" />
+              <span>高效的库存管理</span>
+            </div>
+            <div class="feature-item">
+              <i class="fas fa-check-circle" />
+              <span>智能化物料追踪</span>
+            </div>
+            <div class="feature-item">
+              <i class="fas fa-check-circle" />
+              <span>实时数据分析</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧登录表单 -->
+      <div class="login-right">
+        <div class="login-form-wrapper">
+          <div class="form-header">
+            <h2 class="form-title">欢迎登录</h2>
+            <p class="form-subtitle">请输入您的账号信息</p>
+          </div>
+
+          <div class="form-content">
+            <!-- 用户名输入框 -->
+            <div class="form-item">
+              <a-input v-model:value="username" placeholder="请输入用户名" size="large" @keyup.enter="handleLogin">
+                <template #prefix>
+                  <UserOutlined class="input-icon" />
+                </template>
+              </a-input>
+            </div>
+
+            <!-- 密码输入框 -->
+            <div class="form-item">
+              <a-input-password v-model:value="password" placeholder="请输入密码" size="large" @keyup.enter="handleLogin">
+                <template #prefix>
+                  <LockOutlined class="input-icon" />
+                </template>
+              </a-input-password>
+            </div>
+
+            <!-- 登录按钮 -->
+            <a-button type="primary" block size="large" class="login-button" :loading="loading" @click="handleLogin">
+              登录
+            </a-button>
+
+            <!-- 忘记密码链接 -->
+            <div class="form-footer">
+              <a href="#" class="forgot-link" @click.prevent="handleForgotPassword">
+                忘记密码?
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部版权信息 -->
+    <!-- <footer class="login-footer">
+      © 2023 智能仓储管理系统. 保留所有权利.
+    </footer> -->
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { message } from 'ant-design-vue';
+import { loginApi } from '../api/login.js';
+import { getFormattedDateTime } from '../common/Common.js';
+import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
+
+const router = useRouter();
+
+// 表单数据
+const username = ref('');
+const password = ref('');
+const loading = ref(false);
+
+// 登录处理函数
+const handleLogin = async () => {
+    if (!username.value || !password.value) {
+        message.error('请输入完整的登录信息');
+        return;
+    }
+
+    loading.value = true;
+    try {
+        const params = {
+            username: username.value,
+            password: password.value,
+            accountDateTime: getFormattedDateTime(),
+            languageId: 'zh-CN',
+        };
+        const formData = new FormData();
+        formData.append('userName', params.username);
+        formData.append('password', params.password);
+        formData.append('accountDateTime', params.accountDateTime);
+        formData.append('languageId', params.languageId);
+        const res = await loginApi(formData);
+        if (res.errorCode === 0) {
+            if (res.data) {
+                localStorage.setItem('#token', res.data.token);
+                localStorage.setItem('#accountId', res.data.accountId);
+                localStorage.setItem('#LoginInfo', JSON.stringify(res.data));
+            }
+            message.success('欢迎登录,' + res.data.userName);
+            const redirectUrl = window.location.href.split('redirectUrl=')[1];
+
+            if (redirectUrl) {
+                window.location = decodeURIComponent(redirectUrl);
+            } else {
+                router.push('/home');
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        message.error('登录失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 忘记密码处理函数
+const handleForgotPassword = () => {
+    message.info('请联系管理员重置密码');
+};
+</script>
+
+<style scoped>
+/* 登录容器 */
+.login-container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  position: relative;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 背景装饰 */
+.background-decoration {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  /* background-image: url('./background.svg'); */
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: cover;
+  opacity: 0.1;
+  pointer-events: none;
+}
+
+/* 主内容区域 */
+.login-content {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  position: relative;
+  z-index: 1;
+}
+
+/* 左侧信息区 */
+.login-left {
+  flex: 1;
+  max-width: 500px;
+  padding: 0 40px;
+  color: white;
+}
+
+.system-info {
+  animation: fadeInLeft 0.8s ease-out;
+}
+
+.system-title {
+  font-size: 42px;
+  font-weight: 700;
+  margin-bottom: 16px;
+  line-height: 1.2;
+  color: #fff
+}
+
+.system-subtitle {
+  font-size: 18px;
+  opacity: 0.9;
+  margin-bottom: 40px;
+  font-weight: 300;
+}
+
+.feature-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.feature-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  font-size: 16px;
+  opacity: 0.95;
+}
+
+.feature-item i {
+  font-size: 20px;
+  color: #4ade80;
+}
+
+/* 右侧登录表单 */
+.login-right {
+  flex: 0 0 450px;
+  padding: 0 20px;
+}
+
+.login-form-wrapper {
+  background: white;
+  border-radius: 16px;
+  padding: 48px 40px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+  animation: fadeInRight 0.8s ease-out;
+}
+
+.form-header {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.form-title {
+  font-size: 28px;
+  font-weight: 700;
+  color: #1f2937;
+  margin-bottom: 8px;
+}
+
+.form-subtitle {
+  font-size: 14px;
+  color: #6b7280;
+}
+
+.form-content {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.form-item {
+  margin-bottom: 4px;
+}
+
+.input-icon {
+  color: #667eea;
+  font-size: 16px;
+}
+
+.login-button {
+  height: 48px;
+  font-size: 16px;
+  font-weight: 600;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  border-radius: 8px;
+  margin-top: 8px;
+  transition: all 0.3s ease;
+}
+
+.login-button:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
+}
+
+.form-footer {
+  text-align: center;
+  margin-top: 8px;
+}
+
+.forgot-link {
+  color: #667eea;
+  font-size: 14px;
+  text-decoration: none;
+  transition: color 0.3s ease;
+}
+
+.forgot-link:hover {
+  color: #764ba2;
+  text-decoration: underline;
+}
+
+/* 底部版权 */
+.login-footer {
+  text-align: center;
+  padding: 20px;
+  color: white;
+  font-size: 14px;
+  opacity: 0.8;
+  position: relative;
+  z-index: 1;
+}
+
+/* 动画效果 */
+@keyframes fadeInLeft {
+  from {
+    opacity: 0;
+    transform: translateX(-30px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes fadeInRight {
+  from {
+    opacity: 0;
+    transform: translateX(30px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+/* 响应式设计 */
+@media (max-width: 1024px) {
+  .login-left {
+    display: none;
+  }
+
+  .login-right {
+    flex: 0 0 100%;
+    max-width: 450px;
+  }
+}
+
+@media (max-width: 640px) {
+  .login-form-wrapper {
+    padding: 32px 24px;
+  }
+
+  .form-title {
+    font-size: 24px;
+  }
+
+  .login-right {
+    padding: 0 16px;
+  }
+}
+
+/* Ant Design 样式覆盖 */
+:deep(.ant-input-affix-wrapper) {
+  border-radius: 8px;
+  border: 1px solid #e5e7eb;
+  padding: 12px 16px;
+}
+
+:deep(.ant-input-affix-wrapper:focus),
+:deep(.ant-input-affix-wrapper-focused) {
+  border-color: #667eea;
+  box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
+}
+
+:deep(.ant-input) {
+  font-size: 15px;
+}
+
+:deep(.ant-input-password) {
+  border-radius: 8px;
+}
+</style>

+ 70 - 0
src/main.js

@@ -0,0 +1,70 @@
+import './public-path.js';
+
+import { createApp } from 'vue/dist/vue.esm-bundler.js';
+import Antd from 'ant-design-vue';
+import App from './App.vue';
+import { createRouter, createWebHashHistory } from 'vue-router';
+
+import * as PcComponentV3 from 'pc-component-v3';
+import 'pc-component-v3/dist/pc-component-v3.css';
+import 'ant-design-vue/dist/reset.css';
+import './assets/main.css';
+import routes from './router/routes.js';
+import $ from 'jquery';
+
+window.$ = $;
+window.jQuery = $;
+window.PcComponentV3 = PcComponentV3;
+
+
+let instance = null;
+const router = createRouter({
+    // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
+    history: createWebHashHistory(),
+    routes, // `routes: routes` 的缩写
+});
+
+function render(props = {}) {
+    const { container } = props;
+  
+    instance = createApp(App);
+    instance.use(Antd);
+    instance.use(PcComponentV3);
+    instance.use(router);
+    instance.mount(container ? container.querySelector('#app') : '#app');
+}
+  
+  
+// 独立运行时
+if (!window.__POWERED_BY_QIANKUN__) {
+    render();
+}
+  
+/**
+   * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
+   * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
+   */
+export async function bootstrap() {
+    console.log('[client-role-v3] bootstraped');
+}
+  
+/**
+   * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
+   */
+export async function mount(props) {
+    console.log('[client-role-v3] props from main framework', props);
+   
+    render(props);
+    instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
+    instance.config.globalProperties.$setGlobalState = props.setGlobalState;
+}
+  
+/**
+   * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
+   */
+export async function unmount() {
+    instance.unmount();
+    instance._container.innerHTML = '';
+    instance = null;
+}
+  

+ 4 - 0
src/public-path.js

@@ -0,0 +1,4 @@
+if (window.__POWERED_BY_QIANKUN__) {
+    // eslint-disable-next-line
+  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
+}

+ 40 - 0
src/router/routes.js

@@ -0,0 +1,40 @@
+// 登录
+const UserLogin = () => import('../login/UserLogin.vue');
+// 操作员界面
+const UserHome = () => import('../login/UserHome.vue');
+// 领料管理
+const StockRequisition = () => import('../stock/StockRequisition.vue');
+// 常用领料
+const RegularRequisition = () => import('../stock/RegularRequisition.vue');
+// 领料车
+const StockPickingCar = () => import('../stock/StockPickingCar.vue');
+// 拣货管理
+const OrderPicking = () => import('../stock-out/OrderPicking.vue');
+// 出库确认
+const OutboundConfirm = () => import('../stock-out/OutboundConfirm.vue');
+// AGV RFID识别
+const AgvRfidRecognition = () => import('../stock-out/AgvRfidRecognition.vue');
+// 入库确认
+const InConfirm = () => import('../stock-in/InConfirm.vue');
+
+// 还料区管理
+const ReturnManagement = () => import('../agv-process/ReturnManagement.vue');
+
+// 送料区管理
+const DeliveryManagement = () => import('../agv-process/DeliveryManagement.vue');
+
+const routes = [
+    { path: '/', redirect: '/login' },
+    { path: '/login', component: UserLogin },
+    { path: '/home', component: UserHome },
+    { path: '/order-picking', component: OrderPicking },
+    { path: '/outbound-confirm', component: OutboundConfirm },
+    { path: '/stock-picking-car', component: StockPickingCar },
+    { path: '/stock-requisition', component: StockRequisition },
+    { path: '/regular-requisition', component: RegularRequisition },
+    { path: '/agv-rfid-recognition', component: AgvRfidRecognition },
+    { path: '/in-confirm', component: InConfirm },
+    { path: '/return-management', component: ReturnManagement },
+    { path: '/delivery-management', component: DeliveryManagement },
+];
+export default routes;

+ 494 - 0
src/stock-in/InConfirm.vue

@@ -0,0 +1,494 @@
+<!-- 还料入库 -->
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-header-row">
+        <div class="page-title">
+          <h2>{{ isLeave == 'true' ? '还料离开' : '还料入库' }}</h2>
+        </div>
+        <a-button type="primary" @click="verify">
+          <i class="fas fa-sync-alt mr-1" /> 重新校验
+        </a-button>
+      </div>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <a-empty v-if="materialList.length === 0" description="暂无数据" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="(item, index) in materialList" :key="item.id || index" :class="getCardClass(item.remarks)"
+              class="inventory-card"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-avatar :size="42" :style="{ backgroundColor: '#3b82f6' }">
+                      <template #icon>
+                        <i :class="getInventoryIcon(item.inventoryType)" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ item.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ item.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ item.positionName || '-' }}</span>
+                      <span class="mx-2">/</span>
+                      <span>{{ item.remarks || '-' }}</span>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 分页区域(固定底部) -->
+        <div v-if="materialList.length > 0" class="pagination-wrapper">
+          <span class="text-gray-600">共 {{ materialList.length }} 条数据</span>
+        </div>
+      </div>
+
+      <!-- 底部完成按钮 -->
+      <div v-if="!isLeave" class="bottom-actions">
+        <a-button
+          type="primary" size="large" :disabled="notInStockCount === 0" class="shadow-sm"
+          @click="handleComplete"
+        >
+          <template #icon>
+            <i class="fas fa-check-circle mr-2" />
+          </template>
+          还料入库 ({{ notInStockCount }})
+        </a-button>
+      </div>
+    </main>
+  </div>
+  <Loading v-if="loading" />
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { message, Empty } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import { ref, reactive, onMounted, computed } from 'vue';
+import { cfStockIn, createStockIn, cfStockInLeave } from '../api/stockIn.js';
+
+const router = useRouter();
+
+// 表格加载状态
+const loading = ref(false);
+
+// 物料列表数据
+const materialList = ref([]);
+
+const isLeave = ref(false);
+
+// 分页配置
+const pagination = reactive({
+    start: 1,
+    lang: 10,
+    total: 0,
+});
+
+// 计算不在库的数量
+const notInStockCount = computed(() => {
+    return materialList.value.filter(item => item.remarks === '不在库').length;
+});
+
+// 根据 remarks 返回卡片样式类
+const getCardClass = remarks => {
+    if (remarks === '不在库') {
+        return 'not-in-stock-card';
+    } else if (remarks === '在库') {
+        return 'in-stock-card';
+    }
+    return '';
+};
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 还料入库
+const handleComplete = async () => {
+    // 只处理不在库的数据
+    const notInStockItems = materialList.value.filter(item => item.remarks === '不在库');
+
+    if (notInStockItems.length === 0) {
+        message.warning('没有需要入库的物料!');
+        return;
+    }
+
+    const params = [];
+    notInStockItems.forEach(item => {
+        if (item.remarks === '不在库') {
+            params.push({
+                inventoryId: item.inventoryId,
+            });
+        }
+    });
+
+    console.log('提交参数:', params);
+    await generateCFStockIn(params);
+};
+
+// 校验
+const verify = () => {
+    if (isLeave.value == 'true') {
+        getInnerList();
+    } else {
+        getList();
+    }
+};
+
+// 获取物料列表(外侧校验)
+const getList = async () => {
+    loading.value = true;
+    try {
+
+        const res = await cfStockIn();
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                materialList.value = res.datas;
+                pagination.total = materialList.value.length;
+            } else {
+                materialList.value = [];
+            }
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取物料列表API调用失败:', error);
+        message.error('获取物料列表API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+// 获取物料列表(内侧校验)
+const getInnerList = async () => {
+    loading.value = true;
+    try {
+        const res = await cfStockInLeave();
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                materialList.value = res.datas;
+                pagination.total = materialList.value.length;
+                message.warning('检测到您已携带工装设备,请将工装设备全部还料后,再次点击【重新校验】按钮,待校验通过后,请在闸机开门后离开', 8);
+            } else {
+                message.success('校验成功,请在闸机开门后离开');
+            }
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取物料列表API调用失败:', error);
+        message.error('获取物料列表API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+// 生成CF入库单
+const generateCFStockIn = async params => {
+    loading.value = true;
+    try {
+        const res = await createStockIn(params);
+
+        if (res.errorCode === 0) {
+            message.success('入库申请已完成,还料任务已创建!');
+            router.push('/home');
+        } else {
+            message.error(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('生成CF入库单API调用失败:', error);
+        message.error('生成CF入库单API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+onMounted(() => {
+    isLeave.value = router.currentRoute.value.query.isLeave;
+    if (isLeave.value == 'true') {
+        getInnerList();
+    } else {
+        getList();
+    }
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f9fafb;
+  overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 页面标题行 */
+.page-header-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  /* margin-bottom: 1.5rem; */
+}
+
+/* 页面标题 */
+.page-title {
+  margin-bottom: 1.5rem;
+}
+
+/* 页面标题 */
+.page-title h2 {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #111827;
+  margin: 0;
+}
+
+/* 卡片容器 */
+.card-container {
+  flex: 1;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
+/* 卡片列表包装器(可滚动区域) */
+.card-list-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  min-height: 0;
+}
+
+/* 卡片列表(单列布局) */
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 1rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  border-bottom: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+/* 底部操作按钮 */
+.bottom-actions {
+  position: sticky;
+  bottom: 0;
+  padding: 1rem 1rem 0 1rem;
+  background-color: #f9fafb;
+  display: flex;
+  justify-content: flex-end;
+  z-index: 10;
+}
+
+/* 筛选表单 */
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+}
+
+:deep(.ant-form-item) {
+  margin-bottom: 0 !important;
+}
+
+:deep(.ant-form-item-label > label) {
+  font-size: 14px !important;
+  font-weight: 600 !important;
+}
+
+/* 库存卡片样式 */
+.inventory-card {
+  transition: all 0.25s ease;
+  border: 2px solid #e5e7eb;
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+}
+
+/* 不在库卡片 */
+.not-in-stock-card {
+  background-color: #eff6ff !important;
+  border-color: #93c5fd !important;
+}
+
+/* 在库卡片 */
+.in-stock-card {
+  background-color: #f5f5f5 !important;
+  border-color: #d1d5db !important;
+}
+
+/* 卡片标题区域 */
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.5rem;
+}
+
+.card-header-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.card-header-right {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+/* 卡片标题信息 */
+.card-title-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  min-width: 0;
+}
+
+.card-name {
+  font-size: 1.125rem;
+  font-weight: 600;
+  color: #111827;
+  line-height: 1.5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.card-subtitle {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+/* 卡片位置信息 */
+.card-location {
+  display: flex;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background-color: #f3f4f6;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  color: #374151;
+  white-space: nowrap;
+}
+
+.card-location i {
+  color: #3b82f6;
+}
+
+/* 卡片标题样式 */
+:deep(.ant-card-head) {
+  border-bottom: none;
+  padding: 1rem 1.5rem;
+  min-height: auto;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 0;
+}
+
+/* 卡片内容区域 - 选中时显示 */
+:deep(.ant-card-body) {
+  padding: 0;
+  display: block;
+}
+
+/* 卡片配送方式选择区域 */
+.card-delivery-section {
+  padding: 1rem 1.5rem;
+  background-color: #f8fafc;
+  border-top: 1px solid #e5e7eb;
+  margin-top: 0;
+}
+
+.option-label {
+  font-size: 0.875rem;
+  font-weight: 600;
+  color: #374151;
+  min-width: 80px;
+  margin: 0;
+}
+
+/* Radio Group 样式优化 */
+:deep(.ant-radio-button-wrapper) {
+  display: inline-flex;
+  align-items: center;
+  height: 32px;
+  padding: 0 15px;
+  font-size: 14px;
+}
+
+:deep(.ant-radio-button-wrapper i) {
+  font-size: 14px;
+}
+
+/* Select 样式优化 */
+:deep(.ant-select) {
+  font-size: 14px;
+}
+
+/* 按钮样式 */
+:deep(.ant-btn-primary) {
+  background-color: #3b82f6;
+  border-color: #3b82f6;
+}
+
+:deep(.ant-btn-primary:hover) {
+  background-color: #2563eb;
+  border-color: #2563eb;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+</style>

+ 352 - 0
src/stock-out/AgvRfidRecognition.vue

@@ -0,0 +1,352 @@
+<!-- AGV RFID识别界面 -->
+<template>
+  <div class="bg-gray-50 min-h-screen">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <div class="max-w-[1440px] mx-auto px-8 py-12" style="padding-top: 1rem;">
+      <!-- 任务状态区域 -->
+      <section class="mb-16">
+        <button
+          class="px-6 py-3 bg-primary text-white font-medium rounded hover:bg-indigo-700 transition duration-300 flex items-center"
+          style="margin-bottom: 1rem;background-color: #3c97e7;" @click="getAgvRfidLog"
+        >
+          <i class="fas fa-sync-alt mr-2" />刷新结果
+        </button>
+        <div class="task-card bg-white rounded-xl p-8 border border-gray-200">
+          <div class="flex items-start justify-between mb-6">
+            <div>
+              <h2 class="text-2xl font-semibold mb-2">任务状态</h2>
+              <p class="text-gray-500">当前 AGV 执行任务的实时状态信息</p>
+            </div>
+            <span
+              class="px-4 py-2 rounded-full text-sm font-medium"
+              :class="taskStatus.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
+            >
+              <i class="fas mr-2" :class="taskStatus.success ? 'fa-check-circle' : 'fa-exclamation-circle'" />
+              {{ taskStatus.text }}
+            </span>
+          </div>
+
+          <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
+            <div class="bg-gray-50 p-5 rounded-lg">
+              <p class="text-gray-500 text-sm mb-1">任务 ID</p>
+              <p class="text-lg font-mono">{{ taskInfo.taskId }}</p>
+            </div>
+            <div class="bg-gray-50 p-5 rounded-lg">
+              <p class="text-gray-500 text-sm mb-1">执行次数</p>
+              <p v-if="taskStatus.success" class="text-lg">第 {{ taskInfo.executionCount }} 次</p>
+              <p v-else class="text-lg" />
+            </div>
+            <div class="bg-gray-50 p-5 rounded-lg">
+              <p class="text-sm mb-1" :class="taskStatus.success ? 'text-green-500' : 'text-red-500'">
+                信息
+              </p>
+              <p class="text-lg" :class="taskStatus.success ? 'text-green-600' : 'text-red-600'">
+                {{ taskInfo.message }}
+              </p>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      <!-- 设备信息区域 -->
+      <section class="mb-16">
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
+          <!-- 目标设备卡片 -->
+          <div class="task-card bg-white rounded-xl p-8 border border-gray-200 target-device-card">
+            <div class="flex items-center mb-6">
+              <div class="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mr-4">
+                <i class="fas fa-box-open text-xl" />
+              </div>
+              <div>
+                <h2 class="text-2xl font-semibold">目标搬运工装</h2>
+                <p class="text-gray-500">AGV 应当搬运的设备信息</p>
+              </div>
+            </div>
+
+            <div class="mt-6 p-6 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100">
+              <div class="flex justify-between items-center">
+                <div>
+                  <p class="text-gray-500 text-sm">工装名称</p>
+                  <p class="text-xl font-semibold mt-1">{{ targetDevice.name }}</p>
+                </div>
+                <span class="px-4 py-2 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
+                  目标
+                </span>
+              </div>
+            </div>
+            <div
+              v-if="targetDevice2.name"
+              class="mt-6 p-6 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100"
+            >
+              <div class="flex justify-between items-center">
+                <div>
+                  <p class="text-gray-500 text-sm">工装名称</p>
+                  <p class="text-xl font-semibold mt-1">{{ targetDevice2.name }}</p>
+                </div>
+                <span class="px-4 py-2 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
+                  目标
+                </span>
+              </div>
+            </div>
+          </div>
+
+          <!-- 已识别设备列表卡片 -->
+          <div class="task-card bg-white rounded-xl p-8 border border-gray-200">
+            <div class="flex items-center mb-6">
+              <div class="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center text-purple-600 mr-4">
+                <i class="fas fa-tags text-xl" />
+              </div>
+              <div>
+                <h2 class="text-2xl font-semibold">已识别工装列表</h2>
+                <p class="text-gray-500">RFID 实际扫描到的设备信息</p>
+              </div>
+            </div>
+
+            <div class="space-y-5 mt-6">
+              <div
+                v-for="(device) in identifiedDevices" :key="device.code"
+                class="device-item bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl p-6 border border-purple-100"
+              >
+                <div class="flex justify-between items-center">
+                  <div>
+                    <p class="text-gray-500 text-sm">工装编号</p>
+                    <p class="font-mono text-lg">{{ device.code }}</p>
+                  </div>
+                  <span class="px-4 py-2 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
+                    已识别
+                  </span>
+                </div>
+                <div class="mt-4 pt-4 border-t border-purple-100">
+                  <p class="text-gray-500 text-sm">工装名称</p>
+                  <p class="text-lg font-semibold">{{ device.name }}</p>
+                </div>
+              </div>
+
+              <!-- 空状态 -->
+              <div v-if="identifiedDevices.length === 0" class="text-center py-8 text-gray-400">
+                <i class="fas fa-inbox text-4xl mb-3" />
+                <p>暂无识别到的工装设备</p>
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      <!-- 操作建议区域 -->
+      <!-- <section class="text-center py-12">
+        <h3 class="text-2xl font-semibold mb-4">操作建议</h3>
+        <p class="text-gray-600 max-w-2xl mx-auto mb-8">
+          {{ operationTip }}
+        </p>
+        <div class="flex justify-center space-x-4">
+          <button
+            class="px-6 py-3 bg-primary text-white font-medium rounded hover:bg-indigo-700 transition duration-300 flex items-center"
+            @click="handleReIdentify"
+          >
+            <i class="fas fa-sync-alt mr-2" />重新识别
+          </button>
+          <button
+            class="px-6 py-3 bg-secondary text-white font-medium rounded hover:bg-purple-700 transition duration-300 flex items-center"
+            @click="handleManualConfirm"
+          >
+            <i class="fas fa-check-circle mr-2" />手动确认
+          </button>
+          <button
+            class="px-6 py-3 bg-gray-200 text-gray-800 font-medium rounded hover:bg-gray-300 transition duration-300 flex items-center"
+            @click="handleCancelTask"
+          >
+            <i class="fas fa-times-circle mr-2" />取消任务
+          </button>
+        </div>
+      </section> -->
+    </div>
+  </div>
+  <Loading v-if="loading" />
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { message } from 'ant-design-vue';
+import { ref, computed, onMounted } from 'vue';
+import PageHeader from '../common/PageHeader.vue';
+import { getAgvRfidLogByTaskId } from '../api/stockOut.js';
+
+// 路由
+const router = useRouter();
+
+// 操作员信息
+const operatorName = ref('管理员');
+const operatorId = ref('lw001');
+
+// 加载状态
+const loading = ref(false);
+
+// 任务信息
+const taskInfo = ref({
+    taskId: '',
+    executionCount: 0,
+    message: '',
+});
+
+// 任务状态
+const taskStatus = ref({
+    success: false,
+    text: '待识别',
+});
+
+// 目标设备
+const targetDevice = ref({
+    name: '',
+});
+
+// 目标设备2
+const targetDevice2 = ref({
+    name: '',
+});
+
+// 已识别设备列表(支持多个)
+const identifiedDevices = ref([]);
+
+// 获取 AGV 的 RFID 识别记录
+const getAgvRfidLog = async () => {
+    loading.value = true;
+    try {
+        const res = await getAgvRfidLogByTaskId();
+
+        if (res.errorCode === 0 && res.datas && res.datas.length > 0) {
+            // 取最后一条数据(最新的记录)
+            const latestData = res.datas[res.datas.length - 1];
+
+            // 更新任务信息
+            taskInfo.value = {
+                taskId: latestData.scheduleTaskId.toString(),
+                executionCount: latestData.countExecute,
+                message: latestData.message,
+            };
+
+            // 更新目标设备
+            targetDevice.value = {
+                name: latestData.inventoryName,
+            };
+            targetDevice2.value = {
+                name: latestData.inventoryName2,
+            };
+
+            // 更新已识别设备列表
+            if (latestData.rfidInventoryResponseList && latestData.rfidInventoryResponseList.length > 0) {
+                identifiedDevices.value = latestData.rfidInventoryResponseList.map(item => ({
+                    code: item.inventoryNo,
+                    name: item.inventoryName,
+                }));
+            } else {
+                identifiedDevices.value = [];
+            }
+
+            // 更新任务状态(根据message判断)
+            const isSuccess = latestData.success;
+            taskStatus.value = {
+                success: isSuccess,
+                text: isSuccess ? '校验通过' : '校验失败',
+            };
+
+            // message.success('获取识别结果成功');
+        } else {
+            message.warning('暂无识别记录');
+        }
+    } catch (error) {
+        console.error('获取RFID识别记录失败:', error);
+        message.error('获取识别结果失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+onMounted(() => {
+    getAgvRfidLog();
+});
+
+// 操作建议(根据识别结果动态显示)
+const operationTip = computed(() => {
+    if (identifiedDevices.value.length === 0) {
+        return '未识别到任何工装设备,请检查 RFID 标签是否正确贴附,或重新进行识别。';
+    }
+
+    // 检查是否所有识别的设备都与目标一致
+    const allMatch = identifiedDevices.value.every(
+        device => device.name === targetDevice.value.name,
+    );
+
+    if (allMatch && identifiedDevices.value.length === 1) {
+        return '当前识别到的工装与目标一致,可以继续执行任务。';
+    } else if (!allMatch) {
+        return '当前识别到的工装与目标不符,请检查 RFID 标签是否正确贴附,或手动确认后重试任务。';
+    } else {
+        return `识别到 ${identifiedDevices.value.length} 个工装设备,请确认是否符合任务要求。`;
+    }
+});
+
+// 返回
+const handleBack = () => {
+    router.push('/home');
+};
+
+// 重新识别
+const handleReIdentify = () => {
+    message.info('正在重新识别 RFID 标签...');
+    // 示例:重置识别结果
+    // identifiedDevices.value = [];
+    // taskStatus.value = { success: false, text: '识别中...' };
+};
+
+// 手动确认
+const handleManualConfirm = () => {
+    message.success('手动确认成功,任务继续执行');
+    router.back();
+};
+
+// 取消任务
+const handleCancelTask = () => {
+    message.warning('任务已取消');
+    router.back();
+};
+</script>
+
+<style scoped>
+.task-card {
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+}
+
+.device-item {
+  transition: all 0.2s ease-in-out;
+}
+
+.device-item:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+}
+
+.bg-primary {
+  background-color: #4F46E5;
+}
+
+.bg-secondary {
+  background-color: #7C3AED;
+}
+
+.action-btn {
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+}
+
+/* 目标设备卡片固定高度 */
+.target-device-card {
+  height: fit-content;
+  min-height: 250px;
+}
+</style>

+ 830 - 0
src/stock-out/OrderPicking.vue

@@ -0,0 +1,830 @@
+<!-- 拣货管理 -->
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>拣货管理</h2>
+      </div>
+
+      <!-- 筛选区域 -->
+      <FilterPanel :default-collapsed="false" :active-count="getActiveFilterCount()">
+        <a-form layout="inline" class="filter-form">
+          <a-form-item label="设备名称">
+            <a-input
+              v-model:value="filterForm.inventoryName" placeholder="请输入" style="width: 180px"
+              @keyup.enter="getList"
+            />
+          </a-form-item>
+          <a-form-item label="设备编号">
+            <a-input
+              v-model:value="filterForm.inventoryNo" placeholder="请输入" style="width: 180px"
+              @keyup.enter="getList"
+            />
+          </a-form-item>
+          <a-form-item label="设备类型">
+            <a-select
+              v-model:value="filterForm.inventoryType" placeholder="选择类型" allow-clear style="width: 180px"
+              :options="inventoryTypeList" @change="getList"
+            />
+          </a-form-item>
+          <a-form-item label="仓库名称">
+            <a-select
+              v-model:value="filterForm.warehouseId" placeholder="选择仓库" allow-clear style="width: 180px"
+              @change="getList"
+            >
+              <a-select-option v-for="item in warehouseList" :key="item.id" :value="item.id">
+                {{ item.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="货位名称">
+            <a-input
+              v-model:value="filterForm.positionName" placeholder="请输入" style="width: 180px"
+              @keyup.enter="getList"
+            />
+          </a-form-item>
+          <a-form-item>
+            <a-button type="primary" @click="getList">
+              <i class="fas fa-search mr-1" /> 搜索
+            </a-button>
+            <a-button class="ml-2" @click="handleReset">
+              <i class="fas fa-redo mr-1" /> 重置
+            </a-button>
+          </a-form-item>
+        </a-form>
+      </FilterPanel>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <a-empty v-if="materialList.length === 0" description="暂无数据" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="(item, index) in materialList" :key="item.id || index" :hoverable="true"
+              :class="{ 'selected-card': selectedIds.includes(item.id) }" class="inventory-card"
+              @click="toggleSelect(item.id)"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-checkbox
+                      :checked="selectedIds.includes(item.id)" @click.stop
+                      @change="e => handleCheckboxChange(e, item.id)"
+                    />
+                    <a-avatar :size="42" :style="{ backgroundColor: '#3b82f6' }" class="ml-3">
+                      <template #icon>
+                        <i :class="getInventoryIcon(item.inventoryType)" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ item.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ item.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ item.positionName || '-' }}</span>
+                      <span class="mx-2">/</span>
+                      <span>{{ item.warehouseName || '-' }}</span>
+                    </div>
+                  </div>
+                </div>
+              </template>
+
+              <!-- 卡片展开内容:配送方式选择(只有选中时显示) -->
+              <template v-if="selectedIds.includes(item.id)">
+                <div class="card-delivery-section" @click.stop>
+                  <!-- <div class="delivery-section-title">
+                    <i class="fas fa-truck mr-2" />
+                    配送方式设置
+                  </div> -->
+
+                  <div class="delivery-options">
+                    <!-- 配送方式选择 -->
+                    <div class="delivery-option-item">
+                      <label class="option-label">配送方式:</label>
+                      <a-radio-group
+                        v-model:value="item.deliveryMethod" button-style="solid"
+                        @change="handleDeliveryMethodChange(item)"
+                      >
+                        <a-radio-button value="AGV_Delivery" :disabled="item.deliveryType === '人工配送'">
+                          <i class="fas fa-robot mr-1" /> AGV 配送
+                        </a-radio-button>
+                        <a-radio-button value="Manual_Delivery" :disabled="item.deliveryType === '强制AGV配送'">
+                          <i class="fas fa-user mr-1" /> 人工配送
+                        </a-radio-button>
+                      </a-radio-group>
+                      <span
+                        v-if="item.deliveryType" class="delivery-type-badge"
+                        :class="getDeliveryTypeBadgeClass(item.deliveryType)"
+                      >
+                        {{ item.deliveryType }}
+                      </span>
+                    </div>
+
+                    <!-- 配送位置选择(仅 AGV 配送时显示) -->
+                    <div v-if="item.deliveryMethod === 'AGV_Delivery'" class="delivery-location-item">
+                      <label class="option-label">配送位置:</label>
+                      <a-select
+                        v-model:value="item.selectedLocation" placeholder="请选择" style="width: 200px"
+                        :options="locator"
+                      />
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 分页区域(固定底部) -->
+        <div v-if="materialList.length > 0" class="pagination-wrapper">
+          <span class="text-gray-600">共 {{ materialList.length }} 条数据</span>
+        </div>
+      </div>
+
+      <!-- 底部完成按钮 -->
+      <div class="bottom-actions">
+        <a-button
+          type="primary" size="large" :disabled="selectedIds.length === 0" class="shadow-sm"
+          @click="handleComplete"
+        >
+          <template #icon>
+            <i class="fas fa-check-circle mr-2" />
+          </template>
+          拣货 ({{ selectedIds.length }})
+        </a-button>
+      </div>
+    </main>
+  </div>
+  <Loading v-if="loading" />
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { ref, reactive, onMounted } from 'vue';
+import { message, Empty } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import FilterPanel from '../common/FilterPanel.vue';
+import { list, createStockOut } from '../api/stockOut.js';
+import { getWarehouseList, queryIdleLocator } from '../api/stock.js';
+
+const router = useRouter();
+
+const warehouseList = ref([]);
+
+// 筛选表单
+const filterForm = reactive({
+    inventoryName: '',
+    inventoryNo: '',
+    inventoryType: undefined,
+    warehouseId: undefined,
+    positionName: '',
+});
+const inventoryTypeList = ref([
+    { value: 'Clamp', label: '工装' },
+    { value: 'Instrument', label: '设备' },
+    { value: 'FinishProduct', label: '成品' },
+]);
+// 表格加载状态
+const loading = ref(false);
+
+// 物料列表数据
+const materialList = ref([]);
+
+// 分页配置
+const pagination = reactive({
+    start: 1,
+    lang: 10,
+    total: 0,
+});
+
+//  所选集合
+const selectedIds = ref([]);
+const selectedRows = ref([]);
+
+// 可配送位置
+const locator = ref([]);
+
+// 计算激活的筛选条件数量
+const getActiveFilterCount = () => {
+    let count = 0;
+    if (filterForm.inventoryName) count++;
+    if (filterForm.inventoryNo) count++;
+    if (filterForm.inventoryType) count++;
+    if (filterForm.warehouseId) count++;
+    if (filterForm.positionName) count++;
+    return count;
+};
+
+// 根据 deliveryType 初始化配送方式
+const initDeliveryMethod = item => {
+    if (item.deliveryType === '人工配送') {
+    // 人工配送:只能人工,不可切换
+        item.deliveryMethod = 'Manual_Delivery';
+        item.selectedLocation = '';
+    } else if (item.deliveryType === '强制AGV配送') {
+    // 强制AGV配送:只能AGV,不可切换为人工
+        item.deliveryMethod = 'AGV_Delivery';
+        if (!item.selectedLocation) {
+            item.selectedLocation = '';
+        }
+    } else if (item.deliveryType === '可选AGV配送') {
+    // 可选AGV配送:默认人工,可切换为AGV
+        item.deliveryMethod = item.deliveryMethod || 'Manual_Delivery';
+        item.selectedLocation = item.selectedLocation || '';
+    }
+};
+
+// 切换选择
+const toggleSelect = id => {
+    const index = selectedIds.value.indexOf(id);
+    if (index > -1) {
+        selectedIds.value.splice(index, 1);
+        // 同时移除 selectedRows 中对应的项
+        selectedRows.value = selectedRows.value.filter(item => item.id !== id);
+    } else {
+        selectedIds.value.push(id);
+        // 添加到 selectedRows 并初始化配送字段
+        const item = materialList.value.find(item => item.id === id);
+        if (item) {
+            // 初始化配送字段
+            initDeliveryMethod(item);
+            selectedRows.value.push(item);
+        }
+    }
+};
+
+// 处理复选框变化
+const handleCheckboxChange = (e, id) => {
+    if (e.target.checked) {
+        if (!selectedIds.value.includes(id)) {
+            selectedIds.value.push(id);
+            const item = materialList.value.find(item => item.id === id);
+            if (item) {
+                // 初始化配送字段
+                initDeliveryMethod(item);
+                selectedRows.value.push(item);
+            }
+        }
+    } else {
+        const index = selectedIds.value.indexOf(id);
+        if (index > -1) {
+            selectedIds.value.splice(index, 1);
+            selectedRows.value = selectedRows.value.filter(item => item.id !== id);
+        }
+    }
+};
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 获取配送类型徽章样式
+const getDeliveryTypeBadgeClass = deliveryType => {
+    const classMap = {
+        '人工配送': 'badge-manual',
+        '强制AGV配送': 'badge-agv-force',
+        '可选AGV配送': 'badge-agv-optional',
+    };
+    return classMap[deliveryType] || '';
+};
+
+// 重置筛选条件
+const handleReset = () => {
+    filterForm.inventoryName = '';
+    filterForm.inventoryNo = '';
+    filterForm.inventoryType = undefined;
+    filterForm.warehouseId = undefined;
+    filterForm.positionName = '';
+    getList();
+};
+
+// 加入领料申请
+const addToRequisition = record => {
+    if (!selectedIds.value.includes(record.id)) {
+        selectedIds.value.push(record.id);
+        // 初始化配送字段
+        initDeliveryMethod(record);
+        selectedRows.value.push(record);
+    }
+};
+
+// 领料申请,直接验证并提交
+const handleComplete = async () => {
+    if (selectedIds.value.length === 0) {
+        message.warning('请选择物料加入领料申请!');
+        return;
+    }
+
+    // 验证选择了 AGV 配送的物料是否都选择了配送位置
+    const agvItems = selectedRows.value.filter(item => item.deliveryMethod === 'AGV_Delivery');
+    const hasEmptyLocation = agvItems.some(item => !item.selectedLocation);
+
+    if (hasEmptyLocation) {
+        message.warning('请为所有 AGV 配送的物料选择配送位置');
+        return;
+    }
+
+    const params = [];
+    selectedRows.value.forEach(item => {
+        params.push({
+            stockOutPrepareLineId: item.id,
+            deliveryMethod: item.deliveryMethod,
+            positionEndNo: item.selectedLocation,
+        });
+    });
+
+    console.log('提交参数:', params);
+    await generateCFStockOut(params);
+};
+
+
+// 配送方式变更处理
+const handleDeliveryMethodChange = record => {
+    // 当选择人工配送时,清空配送位置
+    if (record.deliveryMethod === 'Manual_Delivery') {
+        record.selectedLocation = '';
+    }
+    console.log('配送方式变更:', record.inventoryName, record.deliveryMethod);
+};
+
+
+// 获取物料列表
+const getList = async () => {
+    loading.value = true;
+    try {
+        const params = {
+            ...filterForm,
+            userId: JSON.parse(localStorage.getItem('#LoginInfo')).userId,
+        };
+        const res = await list(params);
+        loading.value = false;
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                const testData = res.datas;
+                // ========== 测试数据模拟开始 ==========
+                // const testData = res.datas.map((item, index) => {
+                //     const types = ['人工配送', '强制AGV配送', '可选AGV配送'];
+                //     return {
+                //         ...item,
+                //         // 循环分配不同的配送类型用于测试
+                //         deliveryType: types[index % 3] || item.deliveryType,
+                //     };
+                // });
+                // ========== 测试数据模拟结束 ==========
+
+                // 保留已选中物料的配送方式设置
+                materialList.value = testData.map(item => {
+                    // 查找该物料是否在已选中列表中
+                    const selectedItem = selectedRows.value.find(selected => selected.id === item.id);
+
+                    if (selectedItem) {
+                        // 如果已选中,保留其配送方式和位置设置,并更新 selectedRows 中的引用
+                        const updatedItem = {
+                            ...item,
+                            deliveryMethod: selectedItem.deliveryMethod,
+                            selectedLocation: selectedItem.selectedLocation,
+                        };
+
+                        // 更新 selectedRows 中的对应项,保持引用同步
+                        const index = selectedRows.value.findIndex(row => row.id === item.id);
+                        if (index !== -1) {
+                            selectedRows.value[index] = updatedItem;
+                        }
+
+                        return updatedItem;
+                    } else {
+                        // 未选中,初始化为空
+                        return {
+                            ...item,
+                            deliveryMethod: '',
+                            selectedLocation: '',
+                        };
+                    }
+                });
+                pagination.total = materialList.value.length;
+            } else {
+                materialList.value = [];
+            }
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        loading.value = false;
+        console.error('获取物料列表API调用失败:', error);
+        return;
+    }
+};
+// 生成CF出库单
+const generateCFStockOut = async params => {
+    loading.value = true;
+    try {
+        const res = await createStockOut(params);
+
+        if (res.errorCode === 0) {
+            message.success('领料申请已完成,配送任务已创建!');
+            selectedIds.value = [];
+            selectedRows.value = [];
+            // getList();
+            router.push('/home');
+        } else {
+            message.error(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('生成CF出库单API调用失败:', error);
+        message.error('生成CF出库单API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 获取仓库列表
+const getWarehouse = async () => {
+    try {
+        const res = await getWarehouseList();
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                warehouseList.value = res.datas;
+            } else {
+                warehouseList.value = [];
+            }
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取仓库列表API调用失败:', error);
+        message.error('获取仓库列表API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+// 获取空闲货位
+const getIdleLocator = async () => {
+    try {
+        const res = await queryIdleLocator();
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                locator.value = res.datas.map(item => {
+                    return {
+                        label: item.positionName,
+                        value: item.positionNo,
+                    };
+                });
+            } else {
+                locator.value = [];
+            }
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取空闲货位API调用失败:', error);
+        message.error('获取空闲货位API调用失败');
+    }
+};
+
+onMounted(() => {
+    getWarehouse();
+    getList();
+    getIdleLocator();
+});
+
+
+//  获取分页参数
+// const getPageParams = (current, pageSize) => {
+//     pagination.start = current;
+//     pagination.lang = pageSize;
+//     getList();
+// };
+
+// 查询回到第一页
+// const getDatas = () => {
+//     commonTableRef.value.backFirstPage();
+// };
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f9fafb;
+  overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 页面标题 */
+.page-title {
+  margin-bottom: 1.5rem;
+}
+
+.page-title h2 {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #111827;
+  margin: 0;
+}
+
+/* 卡片容器 */
+.card-container {
+  flex: 1;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
+/* 卡片列表包装器(可滚动区域) */
+.card-list-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  min-height: 0;
+}
+
+/* 卡片列表(单列布局) */
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 1rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  border-bottom: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+/* 底部操作按钮 */
+.bottom-actions {
+  position: sticky;
+  bottom: 0;
+  padding: 1rem 1rem 0 1rem;
+  background-color: #f9fafb;
+  display: flex;
+  justify-content: flex-end;
+  z-index: 10;
+}
+
+/* 筛选表单 */
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+}
+
+:deep(.ant-form-item) {
+  margin-bottom: 0 !important;
+}
+
+:deep(.ant-form-item-label > label) {
+  font-size: 14px !important;
+  font-weight: 600 !important;
+}
+
+/* 库存卡片样式 */
+.inventory-card {
+  cursor: pointer;
+  transition: all 0.25s ease;
+  border: 2px solid #e5e7eb;
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+}
+
+.inventory-card:hover {
+  border-color: #93c5fd;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
+}
+
+.inventory-card.selected-card {
+  border-color: #3b82f6 !important;
+  background-color: #eff6ff;
+  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2);
+}
+
+/* 卡片标题区域 */
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.5rem;
+}
+
+.card-header-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.card-header-right {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+/* 卡片标题信息 */
+.card-title-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  min-width: 0;
+}
+
+.card-name {
+  font-size: 1.125rem;
+  font-weight: 600;
+  color: #111827;
+  line-height: 1.5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.card-subtitle {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+/* 卡片位置信息 */
+.card-location {
+  display: flex;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background-color: #f3f4f6;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  color: #374151;
+  white-space: nowrap;
+}
+
+.card-location i {
+  color: #3b82f6;
+}
+
+/* 卡片标题样式 */
+:deep(.ant-card-head) {
+  border-bottom: none;
+  padding: 1rem 1.5rem;
+  min-height: auto;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 0;
+}
+
+/* 卡片内容区域 - 选中时显示 */
+:deep(.ant-card-body) {
+  padding: 0;
+  display: block;
+}
+
+/* 卡片配送方式选择区域 */
+.card-delivery-section {
+  padding: 1rem 1.5rem;
+  background-color: #f8fafc;
+  border-top: 1px solid #e5e7eb;
+  margin-top: 0;
+}
+
+.delivery-section-title {
+  font-size: 0.9375rem;
+  font-weight: 600;
+  color: #1f2937;
+  margin-bottom: 1rem;
+  display: flex;
+  align-items: center;
+}
+
+.delivery-section-title i {
+  color: #3b82f6;
+  font-size: 1rem;
+}
+
+.delivery-options {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 2rem;
+  flex-wrap: wrap;
+}
+
+.delivery-option-item {
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+}
+
+.delivery-location-item {
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+}
+
+.option-label {
+  font-size: 0.875rem;
+  font-weight: 600;
+  color: #374151;
+  min-width: 80px;
+  margin: 0;
+}
+
+.delivery-type-badge {
+  margin-left: 0.75rem;
+  padding: 0.25rem 0.625rem;
+  border-radius: 0.375rem;
+  font-size: 0.8125rem;
+  font-weight: 500;
+  white-space: nowrap;
+}
+
+/* 人工配送徽章 - 灰色 */
+.delivery-type-badge.badge-manual {
+  background-color: #eff6ff;
+  color: #4b5563;
+}
+
+/* 强制AGV配送徽章 - 蓝色 */
+.delivery-type-badge.badge-agv-force {
+  background-color: #82b0ec;
+  color: #1e40af;
+}
+
+/* 可选AGV配送徽章 - 绿色 */
+.delivery-type-badge.badge-agv-optional {
+  background-color: #adf8d1;
+  color: #065f46;
+}
+
+/* Radio Group 样式优化 */
+:deep(.ant-radio-button-wrapper) {
+  display: inline-flex;
+  align-items: center;
+  height: 32px;
+  padding: 0 15px;
+  font-size: 14px;
+}
+
+:deep(.ant-radio-button-wrapper i) {
+  font-size: 14px;
+}
+
+/* Select 样式优化 */
+:deep(.ant-select) {
+  font-size: 14px;
+}
+
+/* 按钮样式 */
+:deep(.ant-btn-primary) {
+  background-color: #3b82f6;
+  border-color: #3b82f6;
+}
+
+:deep(.ant-btn-primary:hover) {
+  background-color: #2563eb;
+  border-color: #2563eb;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+</style>

+ 487 - 0
src/stock-out/OutboundConfirm.vue

@@ -0,0 +1,487 @@
+<!-- 出库确认 -->
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题和操作按钮 -->
+      <div class="page-header-row">
+        <div class="page-title">
+          <h2>出库确认</h2>
+        </div>
+        <a-button type="primary" @click="getStockOutList(false)">
+          <i class="fas fa-sync-alt mr-1" /> 重新校验
+        </a-button>
+      </div>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <a-empty v-if="materialList.length === 0" description="暂无数据" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="(item, index) in materialList" :key="item.inventoryId || index"
+              :hoverable="item.positionName !== '不在库' && item.inventoryName !== '未识别epc'"
+              :class="{ 'status-card': true, 'completed-card': item.status === 1 }" class="inventory-card"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-avatar :size="42" :style="{ backgroundColor: getStatusColor(item.status) }" class="ml-0">
+                      <template #icon>
+                        <i :class="getInventoryIcon(item.inventoryType)" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ item.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ item.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ item.positionName || '-' }}</span>
+                    </div>
+                    <!-- <div class="status-badge" :class="getStatusClass(item.status)">
+                      {{ getStatusText(item.status) }}
+                    </div> -->
+                    <template v-if="item.positionName !== '不在库' && item.inventoryName !== '未识别epc'">
+                      <a-button v-if="item.status === 1" type="primary" size="small" disabled class="btn-complete">
+                        领料完成
+                      </a-button>
+                      <a-button v-if="item.status === 2" type="primary" size="small" @click="handleStart(item)">
+                        开始领料
+                      </a-button>
+                      <a-button v-if="item.status === 3" type="primary" size="small" @click="handleApply(item)">
+                        申请领料
+                      </a-button>
+                    </template>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 统计信息(固定底部) -->
+        <div v-if="materialList.length > 0" class="pagination-wrapper">
+          <span class="text-gray-600">共 {{ materialList.length }} 条数据,已完成 {{ completedCount }} 条</span>
+        </div>
+      </div>
+
+      <!-- 底部操作按钮 -->
+      <div class="bottom-actions">
+        <a-button
+          type="primary" size="large" class="shadow-sm btn-complete" :disabled="!isCanLeave"
+          @click="handleLeave"
+        >
+          <template #icon>
+            <i class="fas fa-check-circle mr-2" />
+          </template>
+          领料离开
+        </a-button>
+      </div>
+    </main>
+  </div>
+  <Loading v-if="loading" />
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { ref, computed, onMounted } from 'vue';
+import { message, Empty } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import { cfStockOut, createStockOut, cfStockOutLeave } from '../api/stockOut.js';
+
+// 路由
+const router = useRouter();
+
+// 表格加载状态
+const loading = ref(false);
+
+// 物料列表数据
+const materialList = ref([]);
+
+// 完成数量统计
+const completedCount = computed(() => {
+    return materialList.value.filter(item => item.status === 1).length;
+});
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 获取状态颜色
+const getStatusColor = status => {
+    const colorMap = {
+        1: '#10b981', // 完成 - 绿色
+        2: '#f59e0b', // 进行中 - 橙色
+        3: '#3b82f6', // 待开始 - 蓝色
+    };
+    return colorMap[status] || '#6b7280';
+};
+
+// 判断是否可以离开
+const isCanLeave = computed(() => {
+    // 1. 先判断 materialList 是否存在且是数组
+    if (!materialList.value || !Array.isArray(materialList.value)) {
+        return false;
+    }
+
+    // 2. 判断是否有数据(空数组不能离开)
+    if (materialList.value.length === 0) {
+        return false;
+    }
+
+    // 3. 判断所有项的 status 是否都为 1
+    // 使用 every 时需要确保 item 和 item.status 都存在
+    return materialList.value.every(item => {
+        return item && typeof item.status !== 'undefined' && item.status === 1;
+    });
+});
+
+// 开始领料
+const handleStart = record => {
+    generateCFStockOut(record.stockOutPrepareLineId);
+};
+
+// 申请领料
+const handleApply = record => {
+    message.success('申请领料成功');
+};
+
+// 领料完成后离开
+const handleLeave = async () => {
+    const params = materialList.value.map(item => {
+        return {
+            stockOutPrepareLineId: item.stockOutPrepareLineId,
+            stockOutId: item.stockOutNo,
+            inventoryId: item.inventoryId,
+        };
+    });
+    loading.value = true;
+    try {
+        const res = await cfStockOutLeave(params);
+
+        if (res.errorCode === 0) {
+            message.success('领料离开成功');
+            router.push('/home');
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('领料离开API调用失败:', error);
+        message.error('领料离开API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 获取扫描到的领料数据
+const getStockOutList = async (isOne = false) => {
+    loading.value = true;
+
+    try {
+        const res = await cfStockOut();
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                if (!isOne) {
+                    materialList.value = [];
+                    materialList.value = res.datas;
+                } else {
+                    res.datas.forEach(i => {
+                        materialList.value.forEach(j => {
+                            if (i.inventoryNo === j.inventoryNo) {
+                                j.status = 1;
+                                j.stockOutNo = i.stockOutNo;
+                                j.positionName = i.positionName;
+                            }
+                        });
+                    });
+                }
+            } else {
+                materialList.value = [];
+            }
+        } else {
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取扫描到的领料数据API调用失败:', error);
+        message.error('获取扫描到的领料数据API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 生成出库单
+const generateCFStockOut = async id => {
+    loading.value = true;
+    const params = [
+        {
+            stockOutPrepareLineId: id,
+            deliveryMethod: 'Manual_Delivery',
+        },
+    ];
+    try {
+        const res = await createStockOut(params);
+
+        if (res.errorCode === 0) {
+            getStockOutList(true);
+        } else {
+            message.error(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('生成出库单API调用失败:', error);
+        message.error('生成出库单API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+onMounted(() => {
+    getStockOutList(false);
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f9fafb;
+  overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 页面标题行 */
+.page-header-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 1.5rem;
+}
+
+/* 页面标题 */
+.page-title h2 {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #111827;
+  margin: 0;
+}
+
+/* 卡片容器 */
+.card-container {
+  flex: 1;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
+/* 卡片列表包装器(可滚动区域) */
+.card-list-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  min-height: 0;
+}
+
+/* 卡片列表(单列布局) */
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 1rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  border-bottom: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+/* 底部操作按钮 */
+.bottom-actions {
+  position: sticky;
+  bottom: 0;
+  padding: 1rem 1rem 0 1rem;
+  background-color: #f9fafb;
+  display: flex;
+  justify-content: flex-end;
+  z-index: 10;
+}
+
+/* 库存卡片样式 */
+.inventory-card {
+  transition: all 0.25s ease;
+  border: 2px solid #e5e7eb;
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+}
+
+.inventory-card:hover {
+  border-color: #93c5fd;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
+}
+
+.inventory-card.completed-card {
+  border-color: #10b981 !important;
+  background-color: #f0fdf4;
+}
+
+/* 卡片标题区域 */
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.5rem;
+}
+
+.card-header-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.card-header-right {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+/* 卡片标题信息 */
+.card-title-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  min-width: 0;
+}
+
+.card-name {
+  font-size: 1.125rem;
+  font-weight: 600;
+  color: #111827;
+  line-height: 1.5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.card-subtitle {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+/* 卡片位置信息 */
+.card-location {
+  display: flex;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background-color: #f3f4f6;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  color: #374151;
+  white-space: nowrap;
+}
+
+.card-location i {
+  color: #3b82f6;
+}
+
+/* 状态徽章 */
+.status-badge {
+  padding: 0.375rem 0.875rem;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  font-weight: 600;
+  white-space: nowrap;
+}
+
+.status-completed {
+  background-color: #d1fae5;
+  color: #065f46;
+}
+
+.status-processing {
+  background-color: #fed7aa;
+  color: #92400e;
+}
+
+.status-pending {
+  background-color: #dbeafe;
+  color: #1e40af;
+}
+
+/* 卡片标题样式 */
+:deep(.ant-card-head) {
+  border-bottom: none;
+  padding: 1rem 1.5rem;
+  min-height: auto;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 0;
+}
+
+:deep(.ant-card-body) {
+  display: none;
+}
+
+/* 按钮样式 */
+:deep(.ant-btn-primary) {
+  background-color: #3b82f6;
+  border-color: #3b82f6;
+}
+
+:deep(.ant-btn-primary:hover) {
+  background-color: #2563eb;
+  border-color: #2563eb;
+}
+
+.btn-complete {
+  background-color: #10b981 !important;
+  border-color: #10b981 !important;
+}
+
+.btn-complete:hover {
+  background-color: #059669 !important;
+  border-color: #059669 !important;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+</style>

+ 656 - 0
src/stock/RegularRequisition.vue

@@ -0,0 +1,656 @@
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader :is-go-home="false">
+      <template #actions>
+        <button class="action-btn cart-btn" @click="openStockOutCar">
+          <a-badge :count="count" show-zero :number-style="{ backgroundColor: '#10b981' }">
+            <i class="fas fa-shopping-cart text-xl" />
+          </a-badge>
+          <span class="ml-2">领料车</span>
+        </button>
+      </template>
+    </PageHeader>
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>常用领料</h2>
+      </div>
+
+      <!-- 筛选区域 -->
+      <FilterPanel :default-collapsed="true" :active-count="getActiveFilterCount()">
+        <a-form layout="inline" class="filter-form">
+          <a-form-item label="仓库">
+            <a-select
+              v-model:value="warehouseId" placeholder="选择仓库" allow-clear style="width: 150px"
+              @change="getDatas"
+            >
+              <a-select-option v-for="item in warehouseList" :key="item.id" :value="item.id">
+                {{ item.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="名称">
+            <a-input v-model:value="inventoryName" placeholder="输入名称" style="width: 150px" @keyup.enter="getDatas" />
+          </a-form-item>
+          <a-form-item label="编号">
+            <a-input v-model:value="inventoryNo" placeholder="输入编号" style="width: 150px" @keyup.enter="getDatas" />
+          </a-form-item>
+          <a-form-item label="类型">
+            <a-select
+              v-model:value="inventoryType" placeholder="选择类型" allow-clear :options="inventoryTypeList"
+              style="width: 150px" @change="getDatas"
+            />
+          </a-form-item>
+          <a-form-item>
+            <a-button type="primary" @click="getDatas">
+              <i class="fas fa-search mr-1" /> 搜索
+            </a-button>
+            <a-button class="ml-2" @click="handleReset">
+              <i class="fas fa-redo mr-1" /> 重置
+            </a-button>
+          </a-form-item>
+        </a-form>
+      </FilterPanel>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <!-- 空状态 -->
+          <a-empty v-if="stockRequisitionList.length === 0" description="暂无数据" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="(item, index) in stockRequisitionList" :key="item.id || index" :hoverable="true"
+              :class="{ 'selected-card': selectedIds.includes(item.id) }" class="inventory-card"
+              @click="toggleSelect(item.id)"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-checkbox
+                      :checked="selectedIds.includes(item.id)" @click.stop
+                      @change="e => handleCheckboxChange(e, item.id)"
+                    />
+                    <a-avatar :size="42" :style="{ backgroundColor: '#3b82f6' }" class="ml-3">
+                      <template #icon>
+                        <i :class="getInventoryIcon(item.inventoryType)" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ item.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ item.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-center">
+                    <div class="card-stats">
+                      <div class="stat-item">
+                        <i class="fas fa-history" />
+                        <span class="stat-label">借用次数</span>
+                        <span class="stat-value">{{ item.borrowTotal || 0 }}次</span>
+                      </div>
+                      <div class="stat-item">
+                        <i class="fas fa-clock" />
+                        <span class="stat-label">上次借用</span>
+                        <span class="stat-value">{{ item.lastBorrowTime || '暂无记录' }}</span>
+                      </div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ item.inventoryActulPosition || item.inventoryPosition || '-' }}</span>
+                      <span class="mx-2">/</span>
+                      <span>{{ item.inventoryWarehouse || '-' }}</span>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 分页区域(固定底部) -->
+        <div v-if="stockRequisitionList.length > 0" class="pagination-wrapper">
+          <AntdPagination
+            ref="paginationRef" :pagination="pagination" :options="options"
+            @get-page-params="getPageParams"
+          />
+        </div>
+      </div>
+    </main>
+
+    <!-- 底部操作按钮 -->
+    <div class="bottom-actions">
+      <a-button
+        type="primary" size="large" :disabled="selectedIds.length === 0" class="add-to-cart-btn"
+        @click="submitStock"
+      >
+        <i class="fas fa-cart-plus mr-2" />
+        加入领料车
+      </a-button>
+    </div>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import { message, Empty } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import FilterPanel from '../common/FilterPanel.vue';
+import { getWarehouseList } from '../api/stock.js';
+import { queryCommonUse, createStockOutPrepareLine, queryPickingCarNumber } from '../api/stockOut.js';
+
+
+const router = useRouter();
+
+const warehouseId = ref(undefined);
+const inventoryName = ref('');
+const inventoryNo = ref('');
+const inventoryType = ref(undefined);
+const count = ref(0);
+const warehouseList = ref([]);
+const stockRequisitionList = ref([]);
+const selectedIds = ref([]);
+const loading = ref(false);
+const inventoryTypeList = ref([
+    { value: 'Clamp', label: '工装' },
+    { value: 'Instrument', label: '设备' },
+    { value: 'FinishProduct', label: '成品' },
+]);
+const pagination = ref({
+    total: 0,
+    current_page: 1,
+    per_page: 20,
+});
+const options = {
+    showTotal: true,
+    showSizeChanger: true,
+    showQuickJumper: false,
+    pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
+};
+
+// 计算激活的筛选条件数量
+const getActiveFilterCount = () => {
+    let count = 0;
+    if (warehouseId.value) count++;
+    if (inventoryName.value) count++;
+    if (inventoryNo.value) count++;
+    if (inventoryType.value) count++;
+    return count;
+};
+
+// 打开领料车
+const openStockOutCar = () => {
+    router.push('/stock-picking-car?isRegular=true');
+};
+
+// 切换选择
+const toggleSelect = id => {
+    const index = selectedIds.value.indexOf(id);
+    if (index > -1) {
+        selectedIds.value.splice(index, 1);
+    } else {
+        selectedIds.value.push(id);
+    }
+};
+
+// 处理复选框变化
+const handleCheckboxChange = (e, id) => {
+    if (e.target.checked) {
+        if (!selectedIds.value.includes(id)) {
+            selectedIds.value.push(id);
+        }
+    } else {
+        const index = selectedIds.value.indexOf(id);
+        if (index > -1) {
+            selectedIds.value.splice(index, 1);
+        }
+    }
+};
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 分页改变
+const getPageParams = (page, pageSize) => {
+    pagination.value.current_page = page;
+    pagination.value.per_page = pageSize;
+    getStockRequisitionList();
+};
+
+// 查询物料数据
+const getStockRequisitionList = async () => {
+    loading.value = true;
+    const params = {
+        inventoryName: inventoryName.value,
+        inventoryNo: inventoryNo.value,
+        inventoryType: inventoryType.value,
+        warehouseId: warehouseId.value,
+        range: {
+            start: (pagination.value.current_page - 1) * pagination.value.per_page,
+            length: pagination.value.per_page,
+        },
+    };
+
+    try {
+        const res = await queryCommonUse(params);
+        if (res.errorCode == 0) {
+            if (res.datas && res.datas.length > 0) {
+                stockRequisitionList.value = res.datas;
+                pagination.value.total = res.total;
+            } else {
+                stockRequisitionList.value = [];
+                pagination.value.total = 0;
+            }
+        }
+    } catch (error) {
+        message.error('查询常用物料API失败');
+        console.error('查询常用物料API失败', error);
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 获取仓库列表
+const getWarehouses = async () => {
+    try {
+        const res = await getWarehouseList();
+        if (res.errorCode == 0) {
+            if (res.datas && res.datas.length > 0) {
+                warehouseList.value = res.datas;
+            } else {
+                warehouseList.value = [];
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取仓库数据失败:', error);
+        message.error('获取仓库数据失败');
+    }
+};
+
+
+// 重置筛选条件
+const handleReset = () => {
+    warehouseId.value = undefined;
+    inventoryName.value = '';
+    inventoryNo.value = '';
+    inventoryType.value = undefined;
+    getDatas();
+};
+
+// 查询
+const getDatas = () => {
+    pagination.value.current_page = 1;
+    getStockRequisitionList();
+};
+
+// 加入领料车
+const submitStock = async () => {
+    if (selectedIds.value.length == 0) {
+        message.warning('请至少选择一个工装设备');
+        return;
+    }
+    loading.value = true;
+    const params = {
+        inventoryIds: selectedIds.value,
+    };
+
+    try {
+        const res = await createStockOutPrepareLine(params);
+        if (res.errorCode == 0) {
+            selectedIds.value = [];
+            getDatas();
+            queryPickingCarCount();
+            message.success('添加领料车成功');
+        }
+    } catch (error) {
+        console.error('添加领料车失败:', error);
+        message.error('添加领料车失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+/**
+     * 查询领料车中的数量
+     */
+const queryPickingCarCount = async () => {
+    try {
+        const res = await queryPickingCarNumber();
+        if (res.errorCode == 0) {
+            if (res.data) {
+                count.value = res.data;
+            } else {
+                count.value = 0;
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('查询领料车数量失败:', error);
+        message.error('查询领料车数量失败');
+    }
+};
+onMounted(() => {
+    getDatas();
+    getWarehouses();
+    queryPickingCarCount();
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f9fafb;
+  overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 页面标题 */
+.page-title {
+  margin-bottom: 1.5rem;
+}
+
+.page-title h2 {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #111827;
+  margin: 0;
+}
+
+/* 卡片容器 */
+.card-container {
+  flex: 1;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
+/* 卡片列表包装器(可滚动区域) */
+.card-list-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  min-height: 0;
+}
+
+/* 卡片列表(单列布局) */
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 1rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+/* 底部操作按钮 */
+.bottom-actions {
+  position: sticky;
+  bottom: 0;
+  padding: 0 1rem 1rem 1rem;
+  background-color: #f9fafb;
+  display: flex;
+  justify-content: flex-end;
+  z-index: 10;
+}
+
+/* 领料车按钮样式 */
+.cart-btn {
+  background-color: #d1fae5;
+  color: #065f46;
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+  border: none;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.cart-btn:hover {
+  background-color: #a7f3d0;
+}
+
+/* 筛选表单 */
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+}
+
+:deep(.ant-form-item) {
+  margin-bottom: 0 !important;
+}
+
+:deep(.ant-form-item-label > label) {
+  font-size: 14px !important;
+  font-weight: 600 !important;
+}
+
+/* 库存卡片样式 */
+.inventory-card {
+  cursor: pointer;
+  transition: all 0.25s ease;
+  border: 2px solid #e5e7eb;
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+}
+
+.inventory-card:hover {
+  border-color: #93c5fd;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
+}
+
+.inventory-card.selected-card {
+  border-color: #3b82f6 !important;
+  background-color: #eff6ff;
+  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2);
+}
+
+/* 卡片标题区域 */
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.5rem;
+}
+
+.card-header-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.card-header-center {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.card-header-right {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+  flex-shrink: 0;
+}
+
+/* 卡片标题信息 */
+.card-title-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  min-width: 0;
+}
+
+.card-name {
+  font-size: 1.125rem;
+  font-weight: 600;
+  color: #111827;
+  line-height: 1.5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.card-subtitle {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+/* 卡片位置信息 */
+.card-location {
+  display: flex;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background-color: #f3f4f6;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  color: #374151;
+  white-space: nowrap;
+}
+
+.card-location i {
+  color: #3b82f6;
+}
+
+.card-location {
+  flex-shrink: 0;
+}
+
+/* 卡片统计信息 */
+.card-stats {
+  display: flex;
+  align-items: center;
+  gap: 1.5rem;
+  padding: 0.4rem 1.25rem;
+  background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+  border-radius: 0.5rem;
+  border: 1px solid #bae6fd;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  color: #0369a1;
+}
+
+.stat-item i {
+  font-size: 0.875rem;
+  color: #0284c7;
+}
+
+.stat-label {
+  font-size: 0.75rem;
+  color: #64748b;
+  font-weight: 500;
+}
+
+.stat-value {
+  font-size: 0.875rem;
+  font-weight: 600;
+  color: #0c4a6e;
+}
+
+/* 卡片标题样式 */
+:deep(.ant-card-head) {
+  border-bottom: none;
+  padding: 1rem 1.5rem;
+  min-height: auto;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 0;
+}
+
+:deep(.ant-card-body) {
+  display: none;
+}
+
+/* 自定义样式 */
+:deep(.ant-table-cell) {
+  padding: 12px 16px;
+}
+
+:deep(.ant-btn-primary) {
+  background-color: #3b82f6;
+  border-color: #3b82f6;
+}
+
+:deep(.ant-btn-primary:hover) {
+  background-color: #2563eb;
+  border-color: #2563eb;
+}
+
+.add-to-cart-btn {
+  background-color: #10b981 !important;
+  border-color: #10b981 !important;
+}
+
+.add-to-cart-btn:hover {
+  background-color: #059669 !important;
+  border-color: #059669 !important;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.action-btn {
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+  border: none;
+  cursor: pointer;
+}
+</style>

+ 650 - 0
src/stock/StockPickingCar.vue

@@ -0,0 +1,650 @@
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader :is-go-home="false">
+      <template #actions>
+        <button class="action-btn home-btn" @click="goToHome">
+          <i class="fas fa-home mr-1" /> {{ isRegular == 'true' ? '返回常用领料' : '返回领料' }}
+        </button>
+      </template>
+    </PageHeader>
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>领料车管理</h2>
+      </div>
+
+      <!-- 筛选区域 -->
+      <FilterPanel :default-collapsed="true" :active-count="getActiveFilterCount()">
+        <a-form layout="inline" class="filter-form">
+          <a-form-item label="仓库">
+            <a-select
+              v-model:value="warehouseId" placeholder="选择仓库" allow-clear style="width: 150px"
+              @change="getDatas"
+            >
+              <a-select-option v-for="item in warehouseList" :key="item.id" :value="item.id">
+                {{ item.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="名称">
+            <a-input v-model:value="inventoryName" placeholder="输入名称" style="width: 150px" @keyup.enter="getDatas" />
+          </a-form-item>
+          <a-form-item label="编号">
+            <a-input v-model:value="inventoryNo" placeholder="输入编号" style="width: 150px" @keyup.enter="getDatas" />
+          </a-form-item>
+          <a-form-item label="类型">
+            <a-select
+              v-model:value="inventoryType" placeholder="选择类型" allow-clear :options="inventoryTypeList"
+              style="width: 150px" @change="getDatas"
+            />
+          </a-form-item>
+          <a-form-item>
+            <a-button type="primary" @click="getDatas">
+              <i class="fas fa-search mr-1" /> 搜索
+            </a-button>
+            <a-button class="ml-2" @click="handleReset">
+              <i class="fas fa-redo mr-1" /> 重置
+            </a-button>
+          </a-form-item>
+        </a-form>
+      </FilterPanel>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <a-empty v-if="carList.length === 0" description="领料车为空" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="(item, index) in carList" :key="item.id || index" :hoverable="true"
+              :class="{ 'selected-card': selectedIds.includes(item.id) }" class="inventory-card"
+              @click="toggleSelect(item.id)"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-checkbox
+                      :checked="selectedIds.includes(item.id)" @click.stop
+                      @change="e => handleCheckboxChange(e, item.id)"
+                    />
+                    <a-avatar :size="42" :style="{ backgroundColor: '#3b82f6' }" class="ml-3">
+                      <template #icon>
+                        <i :class="getInventoryIcon(item.inventoryType)" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ item.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ item.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ item.positionName || '-' }}</span>
+                      <span class="mx-2">/</span>
+                      <span>{{ item.warehouseName || '-' }}</span>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 分页区域(固定底部) -->
+        <div v-if="carList.length > 0" class="pagination-wrapper">
+          <AntdPagination
+            ref="paginationRef" :pagination="pagination" :options="options"
+            @get-page-params="getPageParams"
+          />
+        </div>
+      </div>
+
+      <!-- 底部操作按钮 -->
+      <div class="bottom-actions">
+        <a-button danger size="large" :disabled="selectedIds.length === 0" class="mr-3" @click="deleteStock">
+          <i class="fas fa-trash mr-2" />
+          删除选中
+        </a-button>
+        <a-button
+          type="primary" size="large" :disabled="selectedIds.length === 0" class="submit-btn"
+          @click="submitStock"
+        >
+          <i class="fas fa-check-circle mr-2" />
+          提交领料
+        </a-button>
+      </div>
+    </main>
+
+    <!-- 拣货确认弹窗 -->
+    <a-modal
+      v-model:open="confirmModalVisible" title="请您确认要进行的操作" :closable="true" :mask-closable="false"
+      @ok="handleConfirmOk" @cancel="confirmModalVisible = false"
+    >
+      <template #footer>
+        <a-button @click="handleConfirmCancel">取消</a-button>
+        <a-button type="primary" danger @click="handleConfirmOk">确认</a-button>
+      </template>
+      <p>您是否要进入仓库,并开始拣货。</p>
+    </a-modal>
+
+    <!-- 删除确认弹窗 -->
+    <a-modal
+      v-model:open="deleteModalVisible" title="确认将工装设备从领料车中删除吗?" :closable="true" :mask-closable="false"
+      @ok="handleDeleteOk" @cancel="handleDeleteCancel"
+    >
+      <template #footer>
+        <a-button @click="handleDeleteCancel">取消</a-button>
+        <a-button type="primary" danger @click="handleDeleteOk">确认</a-button>
+      </template>
+      <p>确认的话请点击【确认】按钮,否则请点击【取消】按钮</p>
+    </a-modal>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import Common from '../common/Common.js';
+import { message, Empty } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import FilterPanel from '../common/FilterPanel.vue';
+import { getWarehouseList } from '../api/stock.js';
+import { queryCFPickCar, deleteCFPickCar, saveCFStockOutPrepare } from '../api/stockOut.js';
+
+const router = useRouter();
+
+// 弹窗控制
+const confirmModalVisible = ref(false);
+const deleteModalVisible = ref(false);
+
+const userId = ref(undefined);
+const warehouseId = ref(undefined);
+const inventoryName = ref('');
+const inventoryNo = ref('');
+const inventoryType = ref(undefined);
+const warehouseList = ref([]);
+const carList = ref([]);
+const selectedIds = ref([]);
+const loading = ref(false);
+const isRegular = ref(false);
+const inventoryTypeList = ref([
+    { value: 'Clamp', label: '工装' },
+    { value: 'Instrument', label: '设备' },
+    { value: 'FinishProduct', label: '成品' },
+]);
+const pagination = ref({
+    total: 0,
+    current_page: 1,
+    per_page: 20,
+});
+const options = {
+    showTotal: true,
+    showSizeChanger: true,
+    showQuickJumper: false,
+    pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
+};
+
+// 计算激活的筛选条件数量
+const getActiveFilterCount = () => {
+    let count = 0;
+    if (warehouseId.value) count++;
+    if (inventoryName.value) count++;
+    if (inventoryNo.value) count++;
+    if (inventoryType.value) count++;
+    return count;
+};
+
+// 切换选择
+const toggleSelect = id => {
+    const index = selectedIds.value.indexOf(id);
+    if (index > -1) {
+        selectedIds.value.splice(index, 1);
+    } else {
+        selectedIds.value.push(id);
+    }
+};
+
+// 处理复选框变化
+const handleCheckboxChange = (e, id) => {
+    if (e.target.checked) {
+        if (!selectedIds.value.includes(id)) {
+            selectedIds.value.push(id);
+        }
+    } else {
+        const index = selectedIds.value.indexOf(id);
+        if (index > -1) {
+            selectedIds.value.splice(index, 1);
+        }
+    }
+};
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 返回领料
+const goToHome = () => {
+    if (isRegular.value == 'true') {
+        router.push('/regular-requisition');
+    } else {
+        router.push('/stock-requisition');
+    }
+};
+
+// 重置筛选条件
+const handleReset = () => {
+    warehouseId.value = undefined;
+    inventoryName.value = '';
+    inventoryNo.value = '';
+    inventoryType.value = undefined;
+    getDatas();
+};
+
+// 获取分页参数
+const getPageParams = (page, pageSize) => {
+    pagination.value.current_page = page;
+    pagination.value.per_page = pageSize;
+    getPickerCarList();
+};
+
+// 从领料车里删除工装设备
+const deleteStock = () => {
+    if (selectedIds.value.length == 0) {
+        message.warning('请至少选择一个工装设备');
+        return;
+    }
+    // 显示删除确认弹窗
+    deleteModalVisible.value = true;
+};
+
+// 查询物料数据
+const getPickerCarList = async () => {
+    loading.value = true;
+    const loginInfo = localStorage.getItem('#LoginInfo');
+    if (loginInfo) {
+        const loginInfoObj = JSON.parse(loginInfo);
+        userId.value = loginInfoObj.userId;
+    }
+    const params = {
+        inventoryName: inventoryName.value,
+        inventoryNo: inventoryNo.value,
+        inventoryType: inventoryType.value,
+        warehouseId: warehouseId.value,
+        userId: userId.value,
+        start: (pagination.value.current_page - 1) * pagination.value.per_page,
+        length: pagination.value.per_page,
+    };
+
+    try {
+        const res = await queryCFPickCar(params);
+        if (res.errorCode == 0) {
+            if (res.datas && res.datas.length > 0) {
+                carList.value = res.datas;
+                pagination.value.total = res.total;
+            } else {
+                carList.value = [];
+                pagination.value.total = 0;
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.log('获取领料车列表API调用失败', error);
+        message.error('获取领料车列表API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+//  删除接口
+const deleteStockCarApi = async () => {
+    loading.value = true;
+    const params = {
+        pickingCarIds: selectedIds.value,
+    };
+    try {
+        const res = await deleteCFPickCar(params);
+        if (res.errorCode == 0) {
+            message.success('工装设备从领料车中删除成功');
+            selectedIds.value = [];
+            getDatas();
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.log('删除领料车API调用失败', error);
+        message.error('删除领料车API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+// 获取仓库列表
+const getWarehouses = async () => {
+    try {
+        const res = await getWarehouseList();
+        if (res.errorCode == 0) {
+            if (res.datas && res.datas.length > 0) {
+                warehouseList.value = res.datas;
+            } else {
+                warehouseList.value = [];
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取仓库数据失败:', error);
+        message.error('获取仓库数据失败');
+    }
+};
+
+// 拣货确认弹窗处理
+const handleConfirmOk = () => {
+    confirmModalVisible.value = false;
+    router.push('/order-picking');
+};
+
+const handleConfirmCancel = () => {
+    confirmModalVisible.value = false;
+    getDatas();
+};
+
+// 删除确认弹窗处理
+const handleDeleteOk = () => {
+    deleteModalVisible.value = false;
+    deleteStockCarApi();
+};
+
+const handleDeleteCancel = () => {
+    deleteModalVisible.value = false;
+};
+
+// 查询回到第一页
+const getDatas = () => {
+    pagination.value.current_page = 1;
+    getPickerCarList();
+};
+
+// 领料
+const submitStock = async () => {
+    if (selectedIds.value.length == 0) {
+        message.warning('请至少选择一个工装设备');
+        return;
+    }
+    loading.value = true;
+    const params = {
+        pickingCarIds: selectedIds.value,
+    };
+
+    try {
+        const res = await saveCFStockOutPrepare(params);
+        if (res.errorCode == 0) {
+            confirmModalVisible.value = true;
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.log('生成领料单API调用失败', error);
+        message.error('生成领料单API调用失败');
+    } finally {
+        loading.value = false;
+    }
+};
+onMounted(() => {
+    getDatas();
+    getWarehouses();
+    isRegular.value = router.currentRoute.value.query.isRegular;
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f9fafb;
+  overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 页面标题 */
+.page-title {
+  margin-bottom: 1.5rem;
+}
+
+.page-title h2 {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #111827;
+  margin: 0;
+}
+
+/* 卡片容器 */
+.card-container {
+  flex: 1;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
+/* 卡片列表包装器(可滚动区域) */
+.card-list-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  min-height: 0;
+}
+
+/* 卡片列表(单列布局) */
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 1rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  border-bottom: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+/* 底部操作按钮 */
+.bottom-actions {
+  position: sticky;
+  bottom: 0;
+  padding: 1rem 0;
+  background-color: #f9fafb;
+  display: flex;
+  justify-content: flex-end;
+  z-index: 10;
+}
+
+/* 返回首页按钮 */
+.home-btn {
+  background-color: #dbeafe;
+  color: #1e40af;
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+  border: none;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.home-btn:hover {
+  background-color: #bfdbfe;
+}
+
+/* 筛选表单 */
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+}
+
+:deep(.ant-form-item) {
+  margin-bottom: 0 !important;
+}
+
+:deep(.ant-form-item-label > label) {
+  font-size: 14px !important;
+  font-weight: 600 !important;
+}
+
+/* 库存卡片样式 */
+.inventory-card {
+  cursor: pointer;
+  transition: all 0.25s ease;
+  border: 2px solid #e5e7eb;
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+}
+
+.inventory-card:hover {
+  border-color: #93c5fd;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
+}
+
+.inventory-card.selected-card {
+  border-color: #3b82f6 !important;
+  background-color: #eff6ff;
+  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2);
+}
+
+/* 卡片标题区域 */
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.5rem;
+}
+
+.card-header-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.card-header-right {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+/* 卡片标题信息 */
+.card-title-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  min-width: 0;
+}
+
+.card-name {
+  font-size: 1.125rem;
+  font-weight: 600;
+  color: #111827;
+  line-height: 1.5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.card-subtitle {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+/* 卡片位置信息 */
+.card-location {
+  display: flex;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background-color: #f3f4f6;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  color: #374151;
+  white-space: nowrap;
+}
+
+.card-location i {
+  color: #3b82f6;
+}
+
+/* 卡片标题样式 */
+:deep(.ant-card-head) {
+  border-bottom: none;
+  padding: 1rem 1.5rem;
+  min-height: auto;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 0;
+}
+
+:deep(.ant-card-body) {
+  display: none;
+}
+
+/* 按钮样式 */
+:deep(.ant-btn-primary) {
+  background-color: #3b82f6;
+  border-color: #3b82f6;
+}
+
+:deep(.ant-btn-primary:hover) {
+  background-color: #2563eb;
+  border-color: #2563eb;
+}
+
+.submit-btn {
+  background-color: #10b981 !important;
+  border-color: #10b981 !important;
+}
+
+.submit-btn:hover {
+  background-color: #059669 !important;
+  border-color: #059669 !important;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+</style>

+ 621 - 0
src/stock/StockRequisition.vue

@@ -0,0 +1,621 @@
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader>
+      <template #actions>
+        <button class="action-btn regular-btn" @click="openRegularRequisition">
+          <i class="fas fa-star text-xl" />
+          <span class="ml-2">常用领料</span>
+        </button>
+        <button class="action-btn cart-btn" @click="openStockOutCar">
+          <a-badge :count="count" show-zero :number-style="{ backgroundColor: '#10b981' }">
+            <i class="fas fa-shopping-cart text-xl" />
+          </a-badge>
+          <span class="ml-2">领料车</span>
+        </button>
+      </template>
+    </PageHeader>
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>领料管理</h2>
+      </div>
+
+      <!-- 筛选区域 -->
+      <FilterPanel :default-collapsed="false" :active-count="getActiveFilterCount()">
+        <a-form layout="inline" class="filter-form">
+          <a-form-item label="仓库">
+            <a-select
+              v-model:value="warehouseId" placeholder="选择仓库" allow-clear style="width: 150px"
+              @change="getDatas"
+            >
+              <a-select-option v-for="item in warehouseList" :key="item.id" :value="item.id">
+                {{ item.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="名称">
+            <a-input v-model:value="inventoryName" placeholder="输入名称" style="width: 150px" @keyup.enter="getDatas" />
+          </a-form-item>
+          <a-form-item label="编号">
+            <a-input v-model:value="inventoryNo" placeholder="输入编号" style="width: 150px" @keyup.enter="getDatas" />
+          </a-form-item>
+          <a-form-item label="类型">
+            <a-select
+              v-model:value="inventoryType" placeholder="选择类型" allow-clear :options="inventoryTypeList"
+              style="width: 150px" @change="getDatas"
+            />
+          </a-form-item>
+          <a-form-item>
+            <a-button type="primary" @click="getDatas">
+              <i class="fas fa-search mr-1" /> 搜索
+            </a-button>
+            <a-button class="ml-2" @click="handleReset">
+              <i class="fas fa-redo mr-1" /> 重置
+            </a-button>
+          </a-form-item>
+        </a-form>
+      </FilterPanel>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <!-- 空状态 -->
+          <a-empty v-if="stockRequisitionList.length === 0" description="暂无数据" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="(item, index) in stockRequisitionList" :key="item.id || index" :hoverable="true"
+              :class="{ 'selected-card': selectedIds.includes(item.id) }" class="inventory-card"
+              @click="toggleSelect(item.id)"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-checkbox
+                      :checked="selectedIds.includes(item.id)" @click.stop
+                      @change="e => handleCheckboxChange(e, item.id)"
+                    />
+                    <a-avatar :size="42" :style="{ backgroundColor: '#3b82f6' }" class="ml-3">
+                      <template #icon>
+                        <i :class="getInventoryIcon(item.inventoryType)" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ item.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ item.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ item.inventoryActulPosition || item.inventoryPosition || '-' }}</span>
+                      <span class="mx-2">/</span>
+                      <span>{{ item.inventoryWarehouse || '-' }}</span>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 分页区域(固定底部) -->
+        <div v-if="stockRequisitionList.length > 0" class="pagination-wrapper">
+          <AntdPagination
+            ref="paginationRef" :pagination="pagination" :options="options"
+            @get-page-params="getPageParams"
+          />
+        </div>
+      </div>
+    </main>
+
+    <!-- 底部操作按钮 -->
+    <div class="bottom-actions">
+      <a-button
+        type="primary" size="large" :disabled="selectedIds.length === 0" class="add-to-cart-btn"
+        @click="submitStock"
+      >
+        <i class="fas fa-cart-plus mr-2" />
+        加入领料车
+      </a-button>
+    </div>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import Common from '../common/Common.js';
+import { message, Empty } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import FilterPanel from '../common/FilterPanel.vue';
+import { getWarehouseList } from '../api/stock.js';
+import { findInventory, createStockOutPrepareLine, queryPickingCarNumber } from '../api/stockOut.js';
+
+const router = useRouter();
+
+const warehouseId = ref(undefined);
+const inventoryName = ref('');
+const inventoryNo = ref('');
+const inventoryType = ref(undefined);
+const count = ref(0);
+const warehouseList = ref([]);
+const stockRequisitionList = ref([]);
+const selectedIds = ref([]);
+const loading = ref(false);
+
+const inventoryTypeList = ref([
+    { value: 'Clamp', label: '工装' },
+    { value: 'Instrument', label: '设备' },
+    { value: 'FinishProduct', label: '成品' },
+]);
+const pagination = ref({
+    total: 0,
+    current_page: 1,
+    per_page: 20,
+});
+const options = {
+    showTotal: true,
+    showSizeChanger: true,
+    showQuickJumper: false,
+    pageSizeOptions: ['2', '4', '50', '100', '200', '500'],
+};
+
+// 计算激活的筛选条件数量
+const getActiveFilterCount = () => {
+    let count = 0;
+    if (warehouseId.value) count++;
+    if (inventoryName.value) count++;
+    if (inventoryNo.value) count++;
+    if (inventoryType.value) count++;
+    return count;
+};
+
+// 打开常用领料
+const openRegularRequisition = () => {
+    router.push('/regular-requisition');
+
+};
+
+// 打开领料车
+const openStockOutCar = () => {
+    router.push('/stock-picking-car?isRegular=false');
+};
+
+// 切换选择
+const toggleSelect = id => {
+    const index = selectedIds.value.indexOf(id);
+    if (index > -1) {
+        selectedIds.value.splice(index, 1);
+    } else {
+        selectedIds.value.push(id);
+    }
+};
+
+// 处理复选框变化
+const handleCheckboxChange = (e, id) => {
+    if (e.target.checked) {
+        if (!selectedIds.value.includes(id)) {
+            selectedIds.value.push(id);
+        }
+    } else {
+        const index = selectedIds.value.indexOf(id);
+        if (index > -1) {
+            selectedIds.value.splice(index, 1);
+        }
+    }
+};
+
+// 获取设备类型图标
+const getInventoryIcon = type => {
+    const iconMap = {
+        '工装': 'fas fa-cube',
+        '设备': 'fas fa-cogs',
+        '成品': 'fas fa-box',
+    };
+    return iconMap[type] || 'fas fa-cube';
+};
+
+// 分页改变
+const getPageParams = (page, pageSize) => {
+    pagination.value.current_page = page;
+    pagination.value.per_page = pageSize;
+    getStockRequisitionList();
+};
+
+// 查询物料数据
+const getStockRequisitionList = async () => {
+    loading.value = true;
+    const params = {
+        inventoryName: inventoryName.value,
+        inventoryNo: inventoryNo.value,
+        inventoryType: inventoryType.value,
+        warehouseId: warehouseId.value,
+        range: {
+            start: (pagination.value.current_page - 1) * pagination.value.per_page,
+            length: pagination.value.per_page,
+        },
+    };
+
+    try {
+        const res = await findInventory(params);
+        if (res.errorCode == 0) {
+            if (res.datas && res.datas.length > 0) {
+                stockRequisitionList.value = res.datas;
+                pagination.value.total = res.total;
+            } else {
+                stockRequisitionList.value = [];
+                pagination.value.total = 0;
+            }
+        }
+
+    } catch (error) {
+        console.error('获取物料数据失败:', error);
+        message.error('获取物料数据失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 获取仓库列表
+const getWarehouses = async () => {
+    try {
+        const res = await getWarehouseList();
+        if (res.errorCode == 0) {
+            if (res.datas && res.datas.length > 0) {
+                warehouseList.value = res.datas;
+            } else {
+                warehouseList.value = [];
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('获取仓库数据失败:', error);
+        message.error('获取仓库数据失败');
+    }
+};
+
+// 登出
+const handleLogout = () => {
+    router.push('/login');
+    localStorage.clear();
+};
+
+// 重置筛选条件
+const handleReset = () => {
+    warehouseId.value = undefined;
+    inventoryName.value = '';
+    inventoryNo.value = '';
+    inventoryType.value = undefined;
+    getDatas();
+};
+
+// 查询
+const getDatas = () => {
+    pagination.value.current_page = 1;
+    getStockRequisitionList();
+};
+
+// 加入领料车
+const submitStock = async () => {
+    if (selectedIds.value.length == 0) {
+        message.warning('请至少选择一个工装设备');
+        return;
+    }
+    loading.value = true;
+    const params = {
+        inventoryIds: selectedIds.value,
+    };
+
+    try {
+        const res = await createStockOutPrepareLine(params);
+        if (res.errorCode == 0) {
+            selectedIds.value = [];
+            getDatas();
+            queryPickingCarCount();
+            message.success('添加领料车成功');
+        }
+    } catch (error) {
+        console.error('添加领料车失败:', error);
+        message.error('添加领料车失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+/**
+     * 查询领料车中的数量
+     */
+const queryPickingCarCount = async () => {
+    try {
+        const res = await queryPickingCarNumber();
+        if (res.errorCode == 0) {
+            if (res.data) {
+                count.value = res.data;
+            } else {
+                count.value = 0;
+            }
+        } else {
+            message.warning(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.error('查询领料车数量失败:', error);
+        message.error('查询领料车数量失败');
+    }
+};
+onMounted(() => {
+    getDatas();
+    getWarehouses();
+    queryPickingCarCount();
+});
+</script>
+
+<style scoped>
+/* 页面容器 - 固定高度布局 */
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #f9fafb;
+  overflow: hidden;
+}
+
+/* 主内容区 - 可滚动 */
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 页面标题 */
+.page-title {
+  margin-bottom: 1.5rem;
+}
+
+.page-title h2 {
+  font-size: 1.5rem;
+  font-weight: 700;
+  color: #111827;
+  margin: 0;
+}
+
+/* 卡片容器 */
+.card-container {
+  flex: 1;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
+/* 卡片列表包装器(可滚动区域) */
+.card-list-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  padding: 1.5rem;
+  min-height: 0;
+}
+
+/* 卡片列表(单列布局) */
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 1rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+/* 底部操作按钮 */
+.bottom-actions {
+  position: sticky;
+  bottom: 0;
+  padding: 0 1rem 1rem 1rem;
+  background-color: #f9fafb;
+  display: flex;
+  justify-content: flex-end;
+  z-index: 10;
+}
+
+/* 领料车按钮样式 */
+.regular-btn,
+.cart-btn {
+  background-color: #d1fae5;
+  color: #065f46;
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+  border: none;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.regular-btn {
+  padding: 1rem;
+  background-color: #f3e8ff !important;
+}
+
+.regular-btn,
+.cart-btn:hover {
+  background-color: #a7f3d0;
+}
+
+/* 筛选表单 */
+.filter-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+}
+
+:deep(.ant-form-item) {
+  margin-bottom: 0 !important;
+}
+
+:deep(.ant-form-item-label > label) {
+  font-size: 14px !important;
+  font-weight: 600 !important;
+}
+
+/* 库存卡片样式 */
+.inventory-card {
+  cursor: pointer;
+  transition: all 0.25s ease;
+  border: 2px solid #e5e7eb;
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+}
+
+.inventory-card:hover {
+  border-color: #93c5fd;
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
+}
+
+.inventory-card.selected-card {
+  border-color: #3b82f6 !important;
+  background-color: #eff6ff;
+  box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2);
+}
+
+/* 卡片标题区域 */
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 1.5rem;
+}
+
+.card-header-left {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.card-header-right {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+/* 卡片标题信息 */
+.card-title-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+  min-width: 0;
+}
+
+.card-name {
+  font-size: 1.125rem;
+  font-weight: 600;
+  color: #111827;
+  line-height: 1.5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.card-subtitle {
+  font-size: 0.875rem;
+  color: #6b7280;
+  line-height: 1.4;
+}
+
+/* 卡片位置信息 */
+.card-location {
+  display: flex;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background-color: #f3f4f6;
+  border-radius: 0.375rem;
+  font-size: 0.875rem;
+  color: #374151;
+  white-space: nowrap;
+}
+
+.card-location i {
+  color: #3b82f6;
+}
+
+/* 卡片标题样式 */
+:deep(.ant-card-head) {
+  border-bottom: none;
+  padding: 1rem 1.5rem;
+  min-height: auto;
+}
+
+:deep(.ant-card-head-title) {
+  padding: 0;
+}
+
+:deep(.ant-card-body) {
+  display: none;
+}
+
+/* 自定义样式 */
+:deep(.ant-table-cell) {
+  padding: 12px 16px;
+}
+
+:deep(.ant-btn-primary) {
+  background-color: #3b82f6;
+  border-color: #3b82f6;
+}
+
+:deep(.ant-btn-primary:hover) {
+  background-color: #2563eb;
+  border-color: #2563eb;
+}
+
+.add-to-cart-btn {
+  background-color: #10b981 !important;
+  border-color: #10b981 !important;
+}
+
+.add-to-cart-btn:hover {
+  background-color: #059669 !important;
+  border-color: #059669 !important;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.action-btn {
+  padding: 0.5rem 1rem;
+  border-radius: 0.5rem;
+  font-weight: 500;
+  transition: all 0.2s;
+  display: inline-flex;
+  align-items: center;
+  border: none;
+  cursor: pointer;
+}
+</style>

+ 14 - 0
src/util/Uuid.js

@@ -0,0 +1,14 @@
+function uuid() {
+    let res = '';
+    const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
+
+    for (let i = 0, len = template.length; i < len; i += 1) {
+        const s = template[i];
+        const r = (Math.random() * 16) | 0;
+        const v = s === 'x' ? r : s === 'y' ? (r & 0x3) | 0x8 : s;
+        res += v.toString(16);
+    }
+    return res;
+}
+
+export default uuid;

+ 36 - 0
src/util/axios.js

@@ -0,0 +1,36 @@
+
+const VueAxios = {
+  vm: {},
+  // eslint-disable-next-line no-unused-vars
+  install (Vue, instance) {
+    if (this.installed) {
+      return
+    }
+    this.installed = true
+
+    if (!instance) {
+      // eslint-disable-next-line no-console
+      console.error('You have to install axios')
+      return
+    }
+
+    Vue.axios = instance
+
+    Object.defineProperties(Vue.prototype, {
+      axios: {
+        get: function get () {
+          return instance
+        }
+      },
+      $http: {
+        get: function get () {
+          return instance
+        }
+      }
+    })
+  }
+}
+
+export {
+  VueAxios
+}

+ 53 - 0
src/util/common.js

@@ -0,0 +1,53 @@
+import { notification } from 'ant-design-vue';
+
+/**
+ * 请求错误
+ * @param {} err 
+ */
+export function requestFailed(err) {
+    console.error(err);
+    notification['error']({
+        message: '错误',
+        description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
+        duration: 8,
+    });
+}
+
+/**
+ * 请求正常
+ * @param {} response 
+ */
+export function requestSuccess(response) {
+    if (response.errorCode !== 0) {
+        notification['error']({
+            message: '错误',
+            description: response.errorMessage,
+            duration: 8,
+        });
+    }
+}
+
+/**
+ * 错误提示
+ * @param {} response 
+ */
+export function notificationError(message, title) {
+    notification['error']({
+        message: title || '操作失败',
+        description: message,
+        duration: 8,
+    });
+}
+
+
+/**
+ * 错误提示
+ * @param {} response 
+ */
+export function notificationSuccess(message, title) {
+    notification['success']({
+        message: title || '操作成功',
+        description: message,
+        duration: 8,
+    });
+}

+ 143 - 0
src/util/loading.js

@@ -0,0 +1,143 @@
+// 实现 loading 效果
+export const showLoading = (text = '加载中...') => {
+    // 先移除之前的loading(如果存在)
+    hideLoading();
+
+    // 创建全屏遮罩
+    const overlay = document.createElement('div');
+    overlay.style.position = 'fixed';
+    overlay.style.top = '0';
+    overlay.style.left = '0';
+    overlay.style.width = '100vw';
+    overlay.style.height = '100vh';
+    overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
+    overlay.style.zIndex = '9999';
+    overlay.style.display = 'flex';
+    overlay.style.justifyContent = 'center';
+    overlay.style.alignItems = 'center';
+    overlay.style.flexDirection = 'column';
+    overlay.id = 'fullscreen-loading';
+
+    // 创建loading容器
+    const loadingContainer = document.createElement('div');
+    loadingContainer.style.display = 'flex';
+    loadingContainer.style.flexDirection = 'column';
+    loadingContainer.style.alignItems = 'center';
+    loadingContainer.style.justifyContent = 'center';
+    loadingContainer.style.padding = '30px';
+    loadingContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.95)';
+    loadingContainer.style.borderRadius = '12px';
+    loadingContainer.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
+    loadingContainer.style.backdropFilter = 'blur(10px)';
+
+    // 创建旋转动画元素
+    const spinner = document.createElement('div');
+    spinner.style.width = '50px';
+    spinner.style.height = '50px';
+    spinner.style.border = '4px solid #e3e3e3';
+    spinner.style.borderRadius = '50%';
+    spinner.style.borderTop = '4px solid #1890ff';
+    spinner.style.animation = 'spin 1s linear infinite';
+    spinner.style.marginBottom = '20px';
+
+    // 创建文字元素
+    const textElement = document.createElement('div');
+    textElement.textContent = text;
+    textElement.style.fontSize = '16px';
+    textElement.style.fontWeight = '500';
+    textElement.style.color = '#333';
+    textElement.style.textAlign = 'center';
+    textElement.style.lineHeight = '1.5';
+    textElement.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
+
+    // 添加CSS动画关键帧,给style标签添加特定ID
+    const style = document.createElement('style');
+    style.id = 'fullscreen-loading-style';
+    style.textContent = `
+    @keyframes spin {
+      to { transform: rotate(360deg); }
+    }
+  `;
+
+    // 组装元素
+    loadingContainer.appendChild(spinner);
+    loadingContainer.appendChild(textElement);
+    overlay.appendChild(loadingContainer);
+
+    document.body.appendChild(style);
+    document.body.appendChild(overlay);
+};
+
+export const hideLoading = () => {
+    const overlay = document.getElementById('fullscreen-loading');
+    if (overlay) {
+        overlay.remove();
+    }
+
+    // 只移除我们创建的特定style标签
+    const loadingStyle = document.getElementById('fullscreen-loading-style');
+    if (loadingStyle) {
+        loadingStyle.remove();
+    }
+};
+
+/**
+ * 生成严谨的UUID
+ * 使用crypto API或时间戳+随机数的组合方式确保唯一性
+ * 即使在多人同时生成的情况下也能保证每个UUID都是唯一的
+ * @returns {string} 格式为 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 的UUID字符串
+ */
+export function generateUUID() {
+    // 优先使用crypto API(现代浏览器支持)
+    if (typeof crypto !== 'undefined' && crypto.randomUUID) {
+        return crypto.randomUUID();
+    }
+
+    // 如果crypto.randomUUID不可用,使用crypto.getRandomValues
+    if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
+        const array = new Uint8Array(16);
+        crypto.getRandomValues(array);
+
+        // 设置版本号(4)和变体位
+        array[6] = (array[6] & 0x0f) | 0x40; // 版本4
+        array[8] = (array[8] & 0x3f) | 0x80; // 变体位
+
+        // 转换为UUID格式字符串
+        const hex = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
+        return [
+            hex.slice(0, 8),
+            hex.slice(8, 12),
+            hex.slice(12, 16),
+            hex.slice(16, 20),
+            hex.slice(20, 32),
+        ].join('-');
+    }
+
+    // 兜底方案:使用时间戳+高精度随机数+计数器的组合
+    const timestamp = Date.now().toString(36); // 时间戳转36进制
+    const highResTime = (performance.now() * 1000).toString(36); // 高精度时间
+    const randomPart1 = Math.random().toString(36).substring(2, 15); // 随机数1
+    const randomPart2 = Math.random().toString(36).substring(2, 15); // 随机数2
+
+    // 添加一个全局计数器确保同一毫秒内的唯一性
+    if (!window._uuidCounter) {
+        window._uuidCounter = 0;
+    }
+    window._uuidCounter = (window._uuidCounter + 1) % 10000;
+    const counter = window._uuidCounter.toString(36).padStart(3, '0');
+
+    // 组合所有部分并格式化为标准UUID格式
+    const combined = (timestamp + highResTime + randomPart1 + randomPart2 + counter).replace(/[^a-z0-9]/g, '');
+    const padded = (combined + '0'.repeat(32)).substring(0, 32);
+
+    // 格式化为标准UUID格式,并设置版本和变体位
+    const formatted = [
+        padded.slice(0, 8),
+        padded.slice(8, 12),
+        '4' + padded.slice(13, 16), // 版本4
+        ((parseInt(padded.slice(16, 17), 16) & 0x3) | 0x8).toString(16) + padded.slice(17, 20), // 变体位
+        padded.slice(20, 32),
+    ].join('-');
+
+    return formatted;
+}

+ 83 - 0
src/util/request.js

@@ -0,0 +1,83 @@
+import axios from 'axios';
+import { notification } from 'ant-design-vue';
+import Common from '../common/Common.js';
+
+// 创建 axios 实例
+const request = axios.create({
+    // API 请求的默认前缀
+    baseURL: '/',
+    timeout: 60000, // 请求超时时间
+});
+
+// 异常拦截处理器
+const errorHandler = error => {
+    console.log(error);
+    if (error.response) {
+        const data = error.response.data;
+        // 从 localstorage 获取 token
+        const token = localStorage.getItem('#token');
+        if (error.response.status === 403) {
+            notification.error({
+                message: 'Forbidden',
+                description: data.message,
+            });
+        }
+        if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
+            console.log('401');
+            notification.error({
+                message: 'Unauthorized',
+                description: 'Authorization verification failed',
+            });
+            if (token) {
+                // store.dispatch('Logout').then(() => {
+                //   setTimeout(() => {
+                //     window.location.reload();
+                //   }, 1500);
+                // });
+            }
+        }
+    }
+    return Promise.reject(error);
+};
+
+// request interceptor
+request.interceptors.request.use(config => {
+    const token = localStorage.getItem('#token');
+    // 如果 token 存在
+    // 让每个请求携带自定义 token 请根据实际情况自行修改
+    if (token) {
+        config.headers['token'] = token;
+    }
+    return config;
+}, errorHandler);
+
+
+// 异常拦截处理器
+const responseErrorHandler = error => {
+    console.log(error);
+    if (error.response.status === 401) {
+        const currentUrl = window.location.href;
+        if (currentUrl.indexOf('login') < 0 && currentUrl.indexOf('redirectUrl=') < 0) {
+
+            window.location = Common.getRedirectUrl('#/login?redirectUrl=' + encodeURIComponent(currentUrl));
+
+        }
+    }
+    if (error.response.status === 504) {
+        notification.error({
+            message: '504',
+            description: error.response.data,
+        });
+    }
+    return Promise.reject(error);
+};
+
+
+// response interceptor
+request.interceptors.response.use(response => {
+    return response.data;
+}, responseErrorHandler);
+
+
+export default request;
+

+ 63 - 0
tailwind.config.js

@@ -0,0 +1,63 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: [
+    // Tailwind 会扫描这些文件,找出所有使用的工具类(如 bg-blue-500)
+    // 只生成用到的样式,未使用的不会打包(Tree-shaking)
+    "./public/index.html",
+    "./src/**/*.{vue,js,ts,jsx,tsx}",
+  ],
+
+  // 这些类名强制保留,即使扫描时没发现使用
+  // 用于动态类名(如 :class="getButtonClass(color)")
+  // 使用原因:因为类名是动态拼接的,Tailwind 扫描时可能识别不出来,所以需要在 safelist 中手动声明。
+  safelist: [
+    // 统计卡片颜色
+    'border-blue-500',
+    'border-green-500',
+    'border-yellow-500',
+    'border-purple-500',
+    'bg-blue-100',
+    'bg-green-100',
+    'bg-yellow-100',
+    'bg-purple-100',
+    'text-blue-500',
+    'text-green-500',
+    'text-yellow-500',
+    'text-purple-500',
+    'text-red-500',
+    // 底部按钮颜色
+    'bg-blue-500',
+    'bg-green-500',
+    'bg-yellow-500',
+    'bg-red-500',
+    'hover:bg-blue-600',
+    'hover:bg-green-600',
+    'hover:bg-yellow-600',
+    'hover:bg-red-600',
+  ],
+
+  // theme(主题扩展)
+  // 自定义颜色和圆角大小
+  // 可以使用 bg-primary、rounded-button 等自定义类名
+  theme: {
+    extend: {
+      colors: {
+        primary: '#3b82f6',
+        secondary: '#60a5fa'
+      },
+      borderRadius: {
+        'none': '0px',
+        'sm': '2px',
+        DEFAULT: '4px',
+        'md': '8px',
+        'lg': '12px',
+        'xl': '16px',
+        '2xl': '20px',
+        '3xl': '24px',
+        'full': '9999px',
+        'button': '4px'
+      }
+    },
+  },
+  plugins: [],
+}

+ 90 - 0
webpack.base.js

@@ -0,0 +1,90 @@
+var path = require('path');
+var webpack = require('webpack');
+const { VueLoaderPlugin } = require('vue-loader');
+const ESLintPlugin = require('eslint-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+
+module.exports = {
+    module: {
+        rules: [
+            {
+                test: /\.vue$/,
+                loader: 'vue-loader',
+            },
+            {
+                test: /\.js$/,
+                loader: 'babel-loader',
+                // include: [        
+                //   path.resolve(__dirname, './node_modules/pc-component-v3/dist/pc-component-v3.js'),      
+                // ],
+                // exclude: file => (
+                //   /node_modules/.test(file) &&
+                //   !/\.vue\.js/.test(file)
+                // ),
+            },
+            {
+                test: /\.css$/,
+                use: (process.env.NODE_ENV === 'production') ? [
+                    {
+                        loader: MiniCssExtractPlugin.loader,
+                        options: {
+                            publicPath: '../',
+                        },
+                    },
+                    'css-loader',
+                    'postcss-loader'] : ['style-loader', 'css-loader', 'postcss-loader'],
+                    // ‌‌PostCSS**的核心作用‌是转换CSS和处理CSS。它通过插件机制来处理CSS文件,支持各种转换任务,如自动添加浏览器前缀、压缩代码、使用未来CSS语法等。‌
+            },
+            // 处理链条(从右到左)
+            /**
+             * 1.postcss-loader:执行 PostCSS 插件(Tailwind + Autoprefixer)
+             * 2.css-loader:解析 CSS 中的 import 和 url()
+             * 3.style-loader(开发环境):将 CSS 注入到 <style> 标签
+             * 4.MiniCssExtractPlugin.loader(生产环境):提取 CSS 到单独文件
+             */
+            {
+                test: /\.(png|jpg|gif|svg)$/,
+                loader: 'file-loader',
+                options: {
+                    name: './client-wms-board.image/[name].[ext]?[hash]',
+                },
+            },
+            {
+                test: /\.(eot|woff|woff2|ttf)$/,
+                loader: 'file-loader',
+                options: {
+                    name: './font/[name].[ext]?[hash]',
+                },
+            },
+        ],
+    },
+    resolve: {
+
+        alias: {
+            //   'vue$': 'vue/dist/vue.esm.js',
+            '@static': path.resolve('static'),
+        },
+        extensions: ['*', '.js', '.vue', '.json'],
+    },
+    performance: {
+        hints: false,
+    },
+  
+    externals: {
+        bootstrap: 'bootstrap',
+        BootstrapDialog: 'BootstrapDialog',
+        d3: 'd3',
+        echarts: 'echarts',
+        moment: 'moment',
+    },
+  
+    plugins: [
+        new VueLoaderPlugin(),
+        new ESLintPlugin({
+            extensions: ['js', 'vue'],
+            // 自动修复。
+            // 自从eslint推出--fix命令后,如果觉得eslint格式化规则已经够用的话,其实也可以不用prettier了。
+            fix: true,
+        }),
+    ],
+};

+ 73 - 0
webpack.dev.js

@@ -0,0 +1,73 @@
+const path = require('path');
+const webpack = require('webpack');
+const fs = require('fs');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const { VueLoaderPlugin } = require('vue-loader');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ESLintPlugin = require('eslint-webpack-plugin');
+const WebpackMerge = require('webpack-merge');
+const baseConfig = require('./webpack.base.js');
+const { name } = require('./package');
+
+
+module.exports = WebpackMerge.merge(baseConfig, {
+    mode: 'development',
+    //开发环境下默认启用cache,在内存中对已经构建的部分进行缓存
+    //避免其他模块修改,但是该模块未修改时候,重新构建,能够更快的进行增量构建
+    //属于空间换时间的做法
+    cache: true,
+    // 代码入口
+    entry: {
+    // 注册界面
+        main: './src/main.js',
+    },
+
+    // output: {
+    //   path: path.resolve(__dirname, '../dist'),
+    //   publicPath: '/',
+    //   filename: 'client-role-v3-[name].js',
+    //   chunkFilename: 'client-role-v3-chunk-[name].js',
+    // },
+    output: {
+        publicPath: '/',
+        library: `${name}-[name]`,
+        libraryTarget: 'umd', // 把微应用打包成 umd 库格式
+        // chunkLoadingGlobal: `webpackJsonp_${name}`,
+        chunkFilename: 'chunk/client-wms-board-chunk-[name].js',
+    },
+
+    devServer: {
+        port: 8089,
+        // static: {
+        //   directory: path.join(__dirname, ''),
+        // },
+        proxy: {
+            '/api': {
+                target: 'http://192.168.1.107:10026/',
+                ws: false,
+                changeOrigin: true,
+            },
+            '/authApi': {
+                target: 'http://192.168.1.107:10026/',
+                ws: false,
+                changeOrigin: true,
+            },
+        },
+
+        headers: {
+            'Access-Control-Allow-Origin': '*',
+        },
+    },
+
+    devtool: 'source-map',
+
+
+    plugins: (module.exports.plugins || []).concat([
+        new HtmlWebpackPlugin({
+            title: 'client-div-v3',
+            template: './public/index.html',  // 源模板文件
+            filename: './index.html', // 输出文件【注意:这里的根路径是module.exports.output.path】
+            chunks: ['main'],
+        }),
+    ]),
+});

+ 76 - 0
webpack.prod.js

@@ -0,0 +1,76 @@
+const path = require('path');
+const webpack = require('webpack');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const WebpackMerge = require('webpack-merge');
+const baseConfig = require('./webpack.base.js');
+const TerserPlugin = require('terser-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+
+module.exports =  WebpackMerge.merge(baseConfig, {
+    mode: 'production',
+    //开发环境下默认启用cache,在内存中对已经构建的部分进行缓存
+    //避免其他模块修改,但是该模块未修改时候,重新构建,能够更快的进行增量构建
+    //属于空间换时间的做法
+    cache: true,
+    // 代码入口
+    entry: {
+    // 注册界面
+        main: './src/main.js',
+    },
+
+    output: {
+        path: path.resolve(__dirname, './dist'),
+        publicPath: './',
+        filename: './client-wms-board-js-bundle/[name].[contenthash:8].js',
+        chunkFilename: './client-wms-board-js-chunk/[name].[contenthash:8].js',
+    },
+
+
+    devtool: 'source-map',
+
+    plugins: (module.exports.plugins || []).concat([
+
+
+        new HtmlWebpackPlugin({
+            title: 'Board',
+            template: './public/index.html',  // 源模板文件
+            filename: './board.html', // 输出文件【注意:这里的根路径是module.exports.output.path】
+            chunks: ['main'],
+        }),
+
+        new webpack.LoaderOptionsPlugin({
+            minimize: true,
+        }),
+
+        // CSS 提取
+        new MiniCssExtractPlugin({
+            filename: './client-wms-board-style/[name].[contenthash:8].css',
+        }),
+
+        // 复制静态资源(字体和CSS文件)
+        new CopyWebpackPlugin({
+            patterns: [
+                {
+                    from: 'public/font-awesome.min.css',
+                    to: 'font-awesome.min.css'
+                },
+                {
+                    from: 'public/pacifico.css',
+                    to: 'pacifico.css'
+                },
+                {
+                    from: 'public/webfonts',
+                    to: 'webfonts',
+                    noErrorOnMissing: true
+                },
+                {
+                    from: 'public/fonts',
+                    to: 'fonts',
+                    noErrorOnMissing: true
+                }
+            ]
+        }),
+
+    ]),
+});

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini