浏览代码

实现指纹识别及成品出入库

liuyanpeng 7 月之前
父节点
当前提交
083016d5fb

+ 2 - 1
.eslintrc.js

@@ -77,6 +77,7 @@ module.exports = {
     "echarts": true,
     "layer": true,
     "Handsontable": true,
-    "dd": true
+    "dd": true,
+    "plugin": true
   }
 }

+ 2 - 1
package.json

@@ -52,7 +52,8 @@
     "jquery": "^3.6.0",
     "vue": "^3.4.25",
     "vue-request": "^1.2.3",
-    "vue-router": "^4.0.12"
+    "vue-router": "^4.0.12",
+    "vant": "^4.9.0"
   },
   "publishConfig": {
     "access": "public",

+ 19 - 14
public/index.html

@@ -1,16 +1,21 @@
 <!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>
+    <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>WMS仓库管理系统</title>
+        <!-- 本地 Font Awesome -->
+        <link rel="stylesheet" href="./font-awesome.min.css" />
+        <!-- 本地 Pacifico 字体 -->
+        <link rel="stylesheet" href="./pacifico.css" />
+        <script
+            nonce="*NONCE_TOKEN*"
+            type="text/javascript"
+            src="/android-device-sdk/FingerprintConfigApi.js"
+        ></script>
+    </head>
+    <body>
+        <div id="app"></div>
+    </body>
+</html>

+ 880 - 0
src/Fingerprint/FingerprintEnroll.vue

@@ -0,0 +1,880 @@
+<template>
+  <div class="fingerprint-wrapper">
+    <PageHeader :show-back="true" :is-go-home="false" :is-custom-back="true" @back="handleBack" />
+    <div class="fingerprint-page">
+      <div class="fingerprint-content">
+        <!-- 步骤内容 -->
+        <div class="step-container">
+          <!-- 第一步输入账号 -->
+          <div v-if="currentStep === 1" class="step-card step-one">
+            <div class="card-header">
+              <div class="step-icon">
+                <i class="icon-user">👤</i>
+              </div>
+              <div>
+                <h2 class="step-title">身份验证</h2>
+                <p class="step-desc">请输入登录账号进行身份验证</p>
+              </div>
+            </div>
+
+            <div class="input-section">
+              <div class="input-wrapper">
+                <van-cell-group inset style="padding: 10px 0 20px;">
+                  <van-field v-model="jobNo" size="large" :border="true" label="登录账号:" placeholder="请输入登录账号" />
+                </van-cell-group>
+              </div>
+
+              <a-button
+                type="primary" size="large" class="next-button" :loading="checking" block
+                @click="handleNextStep"
+              >
+                <span v-if="!checking">验证并继续</span>
+                <span v-else>验证中...</span>
+              </a-button>
+            </div>
+          </div>
+
+          <!-- 第二部 指纹录入 -->
+          <div v-else class="step-card step-two">
+            <div class="card-header">
+              <div class="step-icon">
+                <i class="icon-fingerprint">👆</i>
+              </div>
+              <div class="header-content">
+                <h2 class="step-title">指纹录入</h2>
+                <p class="step-desc">
+                  登录账号:<span class="job-highlight">{{ jobNo }}</span>
+                  <!-- <a-button type="link" size="small" class="edit-link" @click="backToStepOne">
+                    返回
+                  </a-button> -->
+                </p>
+              </div>
+            </div>
+
+            <div class="fingerprint-section">
+              <!-- 设备状态 -->
+              <div class="device-status" @click="handleDeviceClick">
+                <div class="status-indicator" :class="{ connected: isConnected }">
+                  <div class="status-dot" />
+                  <span class="status-text">
+                    {{ isConnected ? '设备已连接' : '设备未连接' }}
+                  </span>
+                </div>
+              </div>
+
+              <!-- 指纹 -->
+              <div class="fingerprint-display">
+                <div class="fingerprint-scanner" :class="{ active: isConnected, scanning: isScanning }">
+                  <div class="scanner-glow" />
+                  <svg class="fingerprint-icon" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+                    <!-- 外侧指纹脊线 -->
+                    <ellipse
+                      cx="100" cy="100" rx="85" ry="90" fill="none" stroke="currentColor" stroke-width="2"
+                      opacity="0.3"
+                    />
+                    <ellipse
+                      cx="100" cy="100" rx="75" ry="80" fill="none" stroke="currentColor" stroke-width="2"
+                      opacity="0.4"
+                    />
+                    <ellipse
+                      cx="100" cy="100" rx="65" ry="70" fill="none" stroke="currentColor" stroke-width="2"
+                      opacity="0.5"
+                    />
+
+                    <!-- 内侧指纹脊线 -->
+                    <path
+                      d="M100 30 C130 30, 150 50, 150 80 C150 110, 130 130, 100 130 C70 130, 50 110, 50 80 C50 50, 70 30, 100 30"
+                      fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.6"
+                    />
+                    <path
+                      d="M100 40 C120 40, 135 55, 135 75 C135 95, 120 110, 100 110 C80 110, 65 95, 65 75 C65 55, 80 40, 100 40"
+                      fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.7"
+                    />
+                    <path
+                      d="M100 50 C110 50, 120 60, 120 70 C120 80, 110 90, 100 90 C90 90, 80 80, 80 70 C80 60, 90 50, 100 50"
+                      fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.8"
+                    />
+
+                    <!-- 指纹脊线 -->
+                    <path
+                      d="M60 60 Q100 40, 140 60" fill="none" stroke="currentColor" stroke-width="1.5"
+                      opacity="0.6"
+                    />
+                    <path
+                      d="M65 70 Q100 50, 135 70" fill="none" stroke="currentColor" stroke-width="1.5"
+                      opacity="0.6"
+                    />
+                    <path
+                      d="M70 80 Q100 60, 130 80" fill="none" stroke="currentColor" stroke-width="1.5"
+                      opacity="0.6"
+                    />
+                    <path
+                      d="M75 90 Q100 70, 125 90" fill="none" stroke="currentColor" stroke-width="1.5"
+                      opacity="0.6"
+                    />
+                    <path
+                      d="M80 100 Q100 80, 120 100" fill="none" stroke="currentColor" stroke-width="1.5"
+                      opacity="0.6"
+                    />
+
+                    <!-- 中央螺旋纹 -->
+                    <circle cx="100" cy="75" r="8" fill="none" stroke="currentColor" stroke-width="2" opacity="0.8" />
+                    <circle cx="100" cy="75" r="4" fill="currentColor" opacity="0.6" />
+                  </svg>
+
+                  <!-- 扫描动画覆盖层 -->
+                  <div v-if="isScanning" class="scan-line" />
+                </div>
+
+                <div class="instruction-text">
+                  {{ isConnected ? statusText : '请先连接指纹设备' }}
+                </div>
+              </div>
+
+              <!-- Action Buttons -->
+              <div class="action-section">
+                <a-button
+                  type="primary" size="large" class="enroll-button btn" :disabled="!isConnected"
+                  :style="isConnected ? 'color: #fff' : 'color: #534a93'" :loading="isScanning" block
+                  @click="handleEnroll"
+                >
+                  <template v-if="!isScanning">
+                    <!-- <span class="button-icon"></span> -->
+                    指纹录入
+                  </template>
+                  <template v-else>
+                    正在录入中...
+                  </template>
+                </a-button>
+
+                <a-button size="large" class="reset-button btn" :disabled="!isConnected" block @click="handleReEnroll">
+                  重新录入
+                </a-button>
+                <!-- <a-button size="large" class="reset-button" block @click="handleDisableConnect">
+                  断开连接
+                </a-button> -->
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { showToast, showFailToast } from 'vant';
+import PageHeader from '../common/PageHeader.vue';
+import { ref, onMounted, onUnmounted } from 'vue';
+import { checkEmployeeByJobNo } from '../api/fingerprint.js';
+
+const router = useRouter();
+
+const currentStep = ref(1);
+const jobNo = ref('');
+const checking = ref(false);
+const isConnected = ref(false);
+const isScanning = ref(false);
+const statusText = ref('');
+
+const handleBack = () => {
+    if (currentStep.value === 2) {
+        currentStep.value = 1;
+        if (plugin.fingerprintConfig) {
+            plugin.fingerprintConfig.disableConnect();
+        }
+        isConnected.value = false;
+        isScanning.value = false;
+        statusText.value = '请点击指纹录入按钮进行录入';
+    } else if (currentStep.value === 1) {
+        router.push('/home');
+    }
+};
+
+// 验证账号是否为系统成员
+const handleNextStep = async () => {
+    if (!jobNo.value) {
+        showToast('请输入账号');
+        return;
+    }
+    checking.value = true;
+
+    try {
+        const res = await checkEmployeeByJobNo(jobNo.value);
+        if (res.errorCode === 0) {
+            if (!res.data) {
+                showToast('该账号不是系统成员');
+            } else {
+                currentStep.value = 2;
+                if (plugin.fingerprintConfig) {
+                    plugin.fingerprintConfig.connect();
+                } else {
+                    isConnected.value = false;
+                    statusText.value = '指纹设备插件未就绪,请检查设备';
+                }
+            }
+        } else {
+            showToast(res.errorMessage);
+        }
+
+    } catch (error) {
+        console.log(error, '账号验证失败');
+    } finally {
+        checking.value = false;
+    }
+};
+
+// 返回输入账号
+const backToStepOne = () => {
+    currentStep.value = 1;
+    if (plugin.fingerprintConfig) {
+        try {
+            plugin.fingerprintConfig.disableConnect();
+        } catch (e) {
+            console.error(e);
+        }
+    }
+    isConnected.value = false;
+};
+
+// 录入指纹
+const handleEnroll = () => {
+    if (!isConnected.value) {
+        showToast('设备未连接或插件异常,请检查指纹设备');
+        return;
+    }
+    if (!jobNo.value) {
+        showToast('账号为空,将返回上一步重新输入');
+        currentStep.value = 1;
+        return;
+    }
+    isScanning.value = true;
+    statusText.value = '请将手指放在指纹采集器上多次按压';
+    plugin.fingerprintConfig.enroll(jobNo.value);
+};
+
+// 重新录入指纹
+const handleReEnroll = async () => {
+    isScanning.value = false;
+    try {
+    // 先断开连接
+        if (plugin.fingerprintConfig) {
+            plugin.fingerprintConfig.disableConnect();
+            isConnected.value = false;
+            statusText.value = '正在准备重新录入指纹,请稍等...';
+
+            // 等待一秒后重新连接
+            setTimeout(() => {
+                plugin.fingerprintConfig.connect();
+                setTimeout(() => {
+                    handleEnroll();
+                }, 200);
+            }, 1000);
+        }
+    } catch (error) {
+        showFailToast('重新连接设备失败');
+        statusText.value = '设备连接失败,请检查设备';
+    }
+};
+
+// 处理指纹响应
+const handleFingerprintResponse = data => {
+    console.log('接收到指纹设备的响应信息', data);
+    if (!data) return;
+    const code = data.code;
+    const msg = data.message || '';
+
+    if (code === 0) {
+        statusText.value = msg || '指纹录入成功!';
+        if (msg === '上传成功' || msg === '当前用户已经录入过指纹,更新成功。') {
+            isScanning.value = false;
+        }
+        if (msg === '连接成功') {
+            isConnected.value = true;
+            isScanning.value = false;
+            statusText.value = '设备已连接,请点击指纹录入按钮进行录入';
+        }
+    } else {
+        if (msg === '连接失败') isConnected.value = false;
+        statusText.value = msg || '指纹录入失败,请重新录入';
+        isScanning.value = false;
+    }
+};
+
+// 设置指纹设备的响应处理函数
+onMounted(() => {
+    if (plugin.fingerprintConfig) {
+        plugin.fingerprintConfig.receiveFingerprintResponse = handleFingerprintResponse;
+    }
+});
+
+// 断开连接
+const handleDisableConnect = () => {
+    isScanning.value = false;
+    isConnected.value = false;
+    statusText.value = '设备已断开连接,请点击连接状态重新连接';
+    if (plugin.fingerprintConfig) {
+        try {
+            plugin.fingerprintConfig.disableConnect();
+        } catch (e) {
+            console.error(e);
+        }
+    }
+};
+
+// 重连or断开
+const handleDeviceClick = () => {
+    if (isConnected.value) {
+        handleDisableConnect();
+        isConnected.value = false;
+        statusText.value = '设备已断开连接,请点击连接状态重新连接';
+    } else {
+        if (plugin.fingerprintConfig) {
+            plugin.fingerprintConfig.connect();
+        }
+    }
+};
+
+// 断开连接
+onUnmounted(() => {
+    handleDisableConnect();
+    plugin.fingerprintConfig.receiveFingerprintResponse = () => { };
+});
+</script>
+
+<style scoped>
+/* 全局重置 - 防止出现滚动条 */
+* {
+  box-sizing: border-box;
+}
+
+html,
+body {
+  overflow: hidden !important;
+  height: 100vh !important;
+  margin: 0 !important;
+  padding: 0 !important;
+}
+
+/* 基础布局 */
+.fingerprint-wrapper {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: #f8fafc;
+  position: relative;
+  overflow: hidden;
+}
+
+.fingerprint-wrapper::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background:
+    radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
+    radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
+  pointer-events: none;
+}
+
+.fingerprint-page {
+  flex: 1;
+  display: flex;
+  align-items: stretch;
+  justify-content: center;
+  padding: 0.5rem;
+  position: relative;
+  z-index: 1;
+  overflow: hidden;
+}
+
+.fingerprint-content {
+  width: 100%;
+  display: flex;
+  align-items: stretch;
+  justify-content: center;
+  overflow: hidden;
+}
+
+/* 步骤卡片 */
+.step-container {
+  display: flex;
+  justify-content: center;
+  align-items: stretch;
+  width: 100%;
+  flex: 1;
+}
+
+.step-card {
+  width: 100%;
+  background: white;
+  border-radius: 12px;
+  padding: clamp(1.5rem, 3vh, 2.5rem) clamp(1.5rem, 3vw, 2.5rem);
+  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
+  border: 1px solid rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  overflow: hidden;
+}
+
+/* 第一步 - 使卡片更大更居中 */
+.step-one {
+  min-height: auto;
+}
+
+/* 第二步 - 防止过度填充,添加适当的间距 */
+.step-two {
+  min-height: auto;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  gap: clamp(0.5rem, 2vw, 1rem);
+  margin-bottom: clamp(0.5rem, 2vh, 1rem);
+  flex-shrink: 0;
+}
+
+.step-icon {
+  width: clamp(2.5rem, 6vw, 4rem);
+  height: clamp(2.5rem, 6vw, 4rem);
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: clamp(1rem, 3vw, 1.8rem);
+  color: white;
+  flex-shrink: 0;
+}
+
+.step-title {
+  font-size: clamp(1.2rem, 4vw, 2rem);
+  font-weight: 700;
+  color: #1f2937;
+  margin-bottom: clamp(0.25rem, 1vh, 0.5rem);
+}
+
+.step-desc {
+  font-size: clamp(0.9rem, 2.5vw, 1.2rem);
+  color: #6b7280;
+  line-height: 1.5;
+}
+
+/* 第一步样式 */
+.input-section {
+  display: flex;
+  flex-direction: column;
+  gap: clamp(1.2rem, 3.5vh, 2.5rem);
+  flex: 1;
+  justify-content: flex-start;
+  padding-top: clamp(2rem, 4vh, 3rem);
+  min-height: 0;
+  max-width: 100%;
+}
+
+.input-wrapper {
+  position: relative;
+}
+
+.job-input {
+  height: clamp(3.5rem, 8vh, 5rem);
+  font-size: clamp(1rem, 2.5vw, 1.3rem);
+  border-radius: clamp(8px, 1.5vw, 12px);
+  border: 2px solid #e5e7eb;
+  transition: all 0.3s ease;
+}
+
+.job-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.input-icon {
+  position: absolute;
+  right: 1rem;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 1.2rem;
+  color: #9ca3af;
+}
+
+.next-button {
+  height: clamp(3.5rem, 8vh, 5rem);
+  font-size: clamp(1rem, 2.5vw, 1.3rem);
+  font-weight: 600;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  border: none;
+  border-radius: clamp(8px, 1.5vw, 12px);
+  transition: all 0.3s ease;
+}
+
+.next-button:hover {
+  background: linear-gradient(135deg, #5a67d8, #6b46c1);
+  transform: translateY(-1px);
+  box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
+}
+
+/* 第二步样式 */
+.header-content {
+  flex: 1;
+}
+
+.job-highlight {
+  color: #667eea;
+  font-weight: 700;
+  background: rgba(102, 126, 234, 0.1);
+  padding: 0.25rem 0.5rem;
+  border-radius: 6px;
+}
+
+.edit-link {
+  color: #667eea;
+  font-weight: 600;
+  margin-left: 0.5rem;
+}
+
+.fingerprint-section {
+  display: flex;
+  flex-direction: column;
+  gap: clamp(0.75rem, 2vh, 1.25rem);
+  flex: 1;
+  justify-content: center;
+  align-items: center;
+  min-height: 0;
+}
+
+/* 设备状态 */
+.device-status {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 20px;
+  cursor: pointer;
+}
+
+.status-indicator {
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+  padding: 0.75rem 1.5rem;
+  border-radius: 50px;
+  background: rgba(239, 68, 68, 0.1);
+  color: #dc2626;
+  font-weight: 600;
+  transition: all 0.3s ease;
+}
+
+.status-indicator.connected {
+  background: rgba(16, 185, 129, 0.1);
+  color: #059669;
+}
+
+.status-dot {
+  width: 0.75rem;
+  height: 0.75rem;
+  border-radius: 50%;
+  background: currentColor;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+
+  0%,
+  100% {
+    opacity: 1;
+  }
+
+  50% {
+    opacity: 0.5;
+  }
+}
+
+/* 指纹显示 */
+.fingerprint-display {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 1rem;
+}
+
+.fingerprint-scanner {
+  position: relative;
+  width: clamp(140px, 25vw, 200px);
+  height: clamp(140px, 25vw, 200px);
+  border-radius: 50%;
+  background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: clamp(2px, 0.5vw, 4px) solid #e5e7eb;
+  transition: all 0.3s ease;
+  overflow: hidden;
+}
+
+.fingerprint-scanner.active {
+  border-color: #667eea;
+  background: linear-gradient(135deg, #f0f4ff, #e0e7ff);
+}
+
+.fingerprint-scanner.scanning {
+  animation: scannerPulse 2s infinite;
+}
+
+@keyframes scannerPulse {
+
+  0%,
+  100% {
+    transform: scale(1);
+    box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
+  }
+
+  50% {
+    transform: scale(1.05);
+    box-shadow: 0 0 0 20px rgba(102, 126, 234, 0);
+  }
+}
+
+.scanner-glow {
+  position: absolute;
+  inset: -10px;
+  border-radius: 50%;
+  background: radial-gradient(circle, rgba(102, 126, 234, 0.2) 0%, transparent 70%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.fingerprint-scanner.active .scanner-glow {
+  opacity: 1;
+}
+
+.fingerprint-icon {
+  width: clamp(100px, 18vw, 140px);
+  height: clamp(100px, 18vw, 140px);
+  color: #9ca3af;
+  transition: color 0.3s ease;
+}
+
+.fingerprint-scanner.active .fingerprint-icon {
+  color: #667eea;
+}
+
+.scan-line {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 3px;
+  background: linear-gradient(90deg, transparent, #667eea, transparent);
+  animation: scanning 2s linear infinite;
+}
+
+@keyframes scanning {
+  0% {
+    transform: translateY(0);
+  }
+
+  100% {
+    transform: translateY(var(--scanner-size, 220px));
+  }
+}
+
+/* 动态扫描动画 */
+.fingerprint-scanner {
+  --scanner-size: clamp(140px, 25vw, 200px);
+}
+
+/* 横屏扫描动画 */
+@media (orientation: landscape) {
+  .fingerprint-scanner {
+    --scanner-size: clamp(160px, 28vw, 220px);
+  }
+}
+
+/* 竖屏大屏幕扫描动画 */
+@media (orientation: portrait) and (min-width: 768px) {
+  .fingerprint-scanner {
+    --scanner-size: clamp(150px, 28vw, 220px);
+  }
+}
+
+.instruction-text {
+  font-size: clamp(0.9rem, 2.5vw, 1.2rem);
+  color: #6b7280;
+  text-align: center;
+  font-weight: 500;
+  max-width: clamp(250px, 50vw, 400px);
+  line-height: 1.5;
+  margin: 10px 0;
+}
+
+/* 按钮 */
+.action-section {
+  display: flex;
+  flex-direction: column;
+  gap: clamp(0.6rem, 1.5vh, 0.9rem);
+  flex-shrink: 0;
+  width: 100%;
+}
+
+.enroll-button {
+  height: clamp(3.5rem, 8vh, 5rem);
+  font-size: clamp(1rem, 2.5vw, 1.3rem);
+  font-weight: 600;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  border: none;
+  border-radius: clamp(8px, 1.5vw, 12px);
+  transition: all 0.3s ease;
+}
+
+.enroll-button:hover:not(:disabled) {
+  background: linear-gradient(135deg, #5a67d8, #6b46c1);
+  transform: translateY(-1px);
+  box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
+}
+
+.reset-button {
+  height: clamp(2.75rem, 5vh, 3.5rem);
+  font-size: clamp(0.9rem, 2vw, 1.1rem);
+  font-weight: 500;
+  background: white;
+  border: 2px solid #e5e7eb;
+  border-radius: clamp(8px, 1.5vw, 12px);
+  color: #6b7280;
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 0.5rem;
+}
+
+.reset-button:hover:not(:disabled) {
+  border-color: #667eea;
+  color: #667eea;
+  background: rgba(102, 126, 234, 0.05);
+}
+
+.button-icon {
+  font-size: 1.2rem;
+}
+
+/* 横屏优化 */
+@media (orientation: landscape) {
+  .input-section {
+    gap: clamp(1.5rem, 4vh, 2.5rem);
+  }
+
+  .job-input {
+    height: clamp(3.5rem, 7vh, 4.5rem);
+  }
+
+  .next-button {
+    height: clamp(3.5rem, 7vh, 4.5rem);
+  }
+
+  .fingerprint-scanner {
+    width: clamp(160px, 28vw, 220px);
+    height: clamp(160px, 28vw, 220px);
+  }
+
+  .fingerprint-icon {
+    width: clamp(120px, 20vw, 160px);
+    height: clamp(120px, 20vw, 160px);
+  }
+}
+
+/* 竖屏优化 - 内容略微升高 */
+@media (orientation: portrait) {
+  .input-section {
+    gap: clamp(1.5rem, 4vh, 3rem);
+  }
+
+  .fingerprint-scanner {
+    width: clamp(150px, 28vw, 220px);
+    height: clamp(150px, 28vw, 220px);
+  }
+
+  .fingerprint-icon {
+    width: clamp(110px, 20vw, 160px);
+    height: clamp(110px, 20vw, 160px);
+  }
+}
+
+/* 竖屏大屏幕优化 */
+@media (orientation: portrait) and (min-width: 768px) and (min-height: 1000px) {
+  .input-section {
+    gap: clamp(2rem, 5vh, 3.5rem);
+  }
+
+  .fingerprint-scanner {
+    width: clamp(170px, 30vw, 240px);
+    height: clamp(170px, 30vw, 240px);
+  }
+
+  .fingerprint-icon {
+    width: clamp(120px, 22vw, 170px);
+    height: clamp(120px, 22vw, 170px);
+  }
+}
+
+/* 针对1080*1920分辨率优化 */
+@media (min-width: 1080px) and (max-width: 1080px) and (min-height: 1920px) and (max-height: 1920px) {
+  .fingerprint-section {
+    justify-content: flex-start;
+    padding-top: 2rem;
+  }
+
+  .card-header {
+    margin-bottom: 1rem;
+  }
+
+  .device-status {
+    margin-bottom: 1rem;
+  }
+
+  .fingerprint-display {
+    margin-top: 0;
+  }
+}
+
+/* 可访问性 */
+@media (prefers-reduced-motion: reduce) {
+  * {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+  }
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+  .step-card {
+    background: #374151;
+    border-color: #4b5563;
+  }
+
+  .step-title {
+    color: #f9fafb;
+  }
+
+  .step-desc {
+    color: #d1d5db;
+  }
+
+  .instruction-text {
+    color: #d1d5db;
+  }
+}
+
+.btn {
+  margin: 10px 0;
+}
+
+:deep(.van-field) {
+  border: 1px solid #ccc;
+  border-radius: 6px;
+}
+</style>

+ 9 - 0
src/api/fingerprint.js

@@ -0,0 +1,9 @@
+import request from '../util/request.js';
+
+// 验证此工号是否为系统成员
+export function checkEmployeeByJobNo(no) {
+    return request({
+        url: '/api/UserFingerprintResource/queryByLoginName?loginName=' + no,
+        method: 'get',
+    });
+}

+ 36 - 0
src/api/finishProduct.js

@@ -0,0 +1,36 @@
+import request from '../util/request.js';
+
+// 生成成品及成品入库单
+export function generateStockIn(params) {
+    return request({
+        url: '/api/InventoryResource/generateFinishedInventory',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 查询在库成品
+export function queryFinishProduct(params) {
+    return request({
+        url: '/api/InventoryResource/findInventoryFinishProduct',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 生成成品出库单
+export function generateStockOut(params) {
+    return request({
+        url: '/api/StockOutResource/inventoryFinishProductStockOut',
+        method: 'post',
+        data: params,
+    });
+}
+
+// 获取货位列表
+export function getLocatorList() {
+    return request({
+        url: '/api/positionResource/positions',
+        method: 'get',
+    });
+}

+ 1 - 1
src/api/stockOut.js

@@ -1,4 +1,4 @@
-import request  from '../util/request.js';
+import request from '../util/request.js';
 
 
 // 可领料数据查询

+ 129 - 9
src/common/PageHeader.vue

@@ -6,13 +6,27 @@
       </button>
       <div class="user-info">
         <i class="fas fa-user text-blue-500 mr-2" />
-        <span class="font-medium">操作员:{{ operatorName }} (ID: {{ operatorId }})</span>
+        <span class="font-medium">{{ operatorName }}</span>
+        <!-- <span class="font-medium">操作员:{{ operatorName }} (ID: {{ operatorId }})</span> -->
       </div>
     </div>
     <div class="header-right">
       <!-- 自定义插槽,用于插入额外按钮(如领料车) -->
       <slot name="actions" />
-      
+
+      <!-- 设置下拉菜单 -->
+      <div ref="settingsDropdown" class="settings-dropdown">
+        <button class="action-btn settings-btn" @click="toggleSettings">
+          <i class="fas fa-cog" />
+        </button>
+        <div v-if="showSettings" class="dropdown-menu">
+          <div class="dropdown-item" @click="goToFingerprintEnroll">
+            <i class="fas fa-fingerprint mr-2" />
+            <span>指纹录入</span>
+          </div>
+        </div>
+      </div>
+
       <button class="action-btn logout-btn" @click="handleLogout">
         <i class="fas fa-sign-out-alt mr-1" /> 登出
       </button>
@@ -21,8 +35,8 @@
 </template>
 
 <script setup>
-import { ref, defineProps, defineEmits } from 'vue';
 import { useRouter } from 'vue-router';
+import { ref, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
 
 const props = defineProps({
     showBack: {
@@ -33,6 +47,10 @@ const props = defineProps({
         type: Boolean,
         default: true,
     },
+    isCustomBack: {
+        type: Boolean,
+        default: false,
+    },
 });
 
 const emit = defineEmits(['back', 'logout']);
@@ -40,11 +58,32 @@ const router = useRouter();
 
 const operatorName = ref('管理员');
 const operatorId = ref('lw001');
+const showSettings = ref(false);
+const settingsDropdown = ref(null);
+
+onMounted(() => {
+    const loginInfo = JSON.parse(localStorage.getItem('#LoginInfo'));
+    if (loginInfo) {
+        operatorName.value = loginInfo.userName;
+        operatorId.value = loginInfo.userId;
+    }
+    // 添加点击外部关闭下拉菜单的事件监听
+    document.addEventListener('click', handleClickOutside);
+});
+
+onUnmounted(() => {
+    // 移除事件监听
+    document.removeEventListener('click', handleClickOutside);
+});
 
 const handleBack = () => {
-    if(props.isGoHome){
+    if (props.isCustomBack) {
+        emit('back');
+        return;
+    }
+    if (props.isGoHome) {
         router.push('/home');
-    }else{
+    } else {
         router.back();
     }
     emit('back');
@@ -55,15 +94,34 @@ const handleLogout = () => {
     localStorage.removeItem('#LoginInfo');
     localStorage.removeItem('#token');
     localStorage.removeItem('#accountId');
-    
+
     // 检查localStorage中的isOut值,如果为true则在登录页面添加isOut参数
     const isOut = localStorage.getItem('isOut') === 'true';
     if (isOut) {
-        router.push({ path: '/login', query: { isOut: 'true' } });
+        router.push({ path: '/fingerprint-login', query: { isOut: 'true' } });
     } else {
-        router.push('/login');
+        router.push('/fingerprint-login');
     }
 };
+
+// 切换设置下拉菜单
+const toggleSettings = event => {
+    event.stopPropagation();
+    showSettings.value = !showSettings.value;
+};
+
+// 点击外部关闭下拉菜单
+const handleClickOutside = event => {
+    if (settingsDropdown.value && !settingsDropdown.value.contains(event.target)) {
+        showSettings.value = false;
+    }
+};
+
+// 跳转到指纹录入页面
+const goToFingerprintEnroll = () => {
+    showSettings.value = false;
+    router.push('/fingerprint-enroll');
+};
 </script>
 
 <style scoped>
@@ -97,7 +155,7 @@ const handleLogout = () => {
 }
 
 .action-btn {
-  padding: 0.5rem 1rem;
+  padding: 0.65rem 1rem;
   border-radius: 0.5rem;
   font-weight: 500;
   transition: all 0.2s;
@@ -125,4 +183,66 @@ const handleLogout = () => {
 .logout-btn:hover {
   background-color: #fecaca;
 }
+
+/* 设置下拉菜单样式 */
+.settings-dropdown {
+  position: relative;
+}
+
+.settings-btn {
+  background-color: #f3f4f6;
+  color: #374151;
+  padding: 0.8rem;
+}
+
+.settings-btn:hover {
+  background-color: #e5e7eb;
+}
+
+.settings-btn i {
+  font-size: 16px;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: calc(100% + 0.5rem);
+  right: 0;
+  background-color: white;
+  border-radius: 0.5rem;
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  min-width: 160px;
+  z-index: 1000;
+  overflow: hidden;
+  animation: dropdownFadeIn 0.2s ease;
+}
+
+@keyframes dropdownFadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.dropdown-item {
+  padding: 0.75rem 1rem;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  transition: background-color 0.2s;
+  color: #374151;
+  font-size: 14px;
+}
+
+.dropdown-item:hover {
+  background-color: #f3f4f6;
+}
+
+.dropdown-item i {
+  color: #3b82f6;
+  font-size: 14px;
+}
 </style>

+ 480 - 0
src/finishProduct/FinishProductIn.vue

@@ -0,0 +1,480 @@
+<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="handleSubmit">
+          <template #icon>
+            <i class="fas fa-upload mr-1" />
+          </template>
+          提交
+        </a-button>
+      </div>
+
+      <!-- 顶部统计与新增按钮 -->
+      <div class="top-actions-row">
+        <div class="top-actions-info">
+          当前已添加 <span class="font-semibold">{{ products.length }}</span> 条成品记录
+        </div>
+        <a-button @click="openModal">
+          <template #icon>
+            <i class="fas fa-plus-circle mr-1" />
+          </template>
+          添加成品
+        </a-button>
+      </div>
+
+      <!-- 卡片容器 -->
+      <div class="card-container">
+        <!-- 卡片列表区域(可滚动) -->
+        <div class="card-list-wrapper">
+          <a-empty v-if="products.length === 0" description="请添加成品" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card v-for="(product, index) in products" :key="product.code || index" 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="fas fa-box" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ product.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ product.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ product.positionName }}</span>
+                    </div>
+                    <a-button type="text" danger size="small" class="delete-btn" @click.stop="removeProduct(index)">
+                      <template #icon>
+                        <i class="fas fa-trash" />
+                      </template>
+                    </a-button>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 底部统计区域(固定) -->
+        <div v-if="products.length > 0" class="pagination-wrapper">
+          <span class="text-gray-600">共 {{ products.length }} 条数据</span>
+        </div>
+      </div>
+    </main>
+
+    <!-- 新增成品弹窗 -->
+    <a-modal v-model:open="isModalOpen" title="新增成品" :mask-closable="false" width="480px" @cancel="closeModal">
+      <a-form layout="vertical" @submit.prevent>
+        <a-form-item label="成品名称">
+          <a-input v-model:value="newProduct.inventoryName" placeholder="请输入成品名称" allow-clear />
+        </a-form-item>
+        <a-form-item label="成品编号">
+          <a-input v-model:value="newProduct.inventoryNo" placeholder="请输入成品编号" allow-clear />
+        </a-form-item>
+        <a-form-item label="货位选择">
+          <a-select
+            v-model:value="newProduct.positionId" placeholder="请选择货位" show-search
+            :options="warehouseLocationOptions" allow-clear
+            :field-names="{ label: 'positionName', value: 'positionId' }" option-filter-prop="positionName"
+          />
+        </a-form-item>
+      </a-form>
+
+      <template #footer>
+        <a-button @click="closeModal">
+          取消
+        </a-button>
+        <a-button type="primary" :disabled="!isFormValid" @click="saveProduct">
+          保存
+        </a-button>
+      </template>
+    </a-modal>
+
+    <a-modal
+      v-model:open="showConfirmModal" title="确认入库" :mask-closable="false" width="420px"
+      @cancel="showConfirmModal = false"
+    >
+      <p class="text-gray-700 mb-4">
+        您确定要将选中的
+        <span class="font-semibold">{{ products.length }}</span>
+        个成品进行入库操作吗?
+      </p>
+      <template #footer>
+        <a-button @click="showConfirmModal = false">取消</a-button>
+        <a-button type="primary" @click="executeInbound">确认入库</a-button>
+      </template>
+    </a-modal>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue';
+import { message, Empty, Modal } from 'ant-design-vue';
+import PageHeader from '../common/PageHeader.vue';
+import { generateStockIn, getLocatorList } from '../api/finishProduct';
+
+// 加载状态(预留给后续真实接口调用)
+const loading = ref(false);
+
+// 仓库货位数据
+const warehouseLocationOptions = ref([]);
+
+// 弹窗显示状态
+const isModalOpen = ref(false);
+const showConfirmModal = ref(false);
+
+// 成品列表
+const products = ref([]);
+
+// 新增成品表单
+const newProduct = reactive({
+    inventoryName: '',
+    inventoryNo: '',
+    positionId: undefined,
+});
+
+// 表单校验
+const isFormValid = computed(() => {
+    return (
+        newProduct.inventoryName.trim() !== '' &&
+    newProduct.inventoryNo.trim() !== '' &&
+    !!newProduct.positionId
+    );
+});
+
+// 打开弹窗
+const openModal = () => {
+    resetForm();
+    isModalOpen.value = true;
+};
+
+// 关闭弹窗
+const closeModal = () => {
+    isModalOpen.value = false;
+};
+
+// 重置表单
+const resetForm = () => {
+    newProduct.inventoryName = '';
+    newProduct.inventoryNo = '';
+    newProduct.positionId = undefined;
+};
+
+// 保存成品
+const saveProduct = () => {
+    if (!isFormValid.value) {
+        message.warning('请完整填写成品信息');
+        return;
+    }
+
+    const location = warehouseLocationOptions.value.find(
+        item => item.positionId === newProduct.positionId,
+    );
+
+    console.log(location);
+
+    products.value.push({
+        inventoryName: newProduct.inventoryName.trim(),
+        inventoryNo: newProduct.inventoryNo.trim(),
+        positionId: newProduct.positionId,
+        positionName: location ? location.positionName + ' / ' + location.warehouseName : '',
+    });
+
+    message.success('成品已添加');
+    isModalOpen.value = false;
+};
+
+// 执行入库
+const handleSubmit = () => {
+    if (products.value.length === 0) {
+        message.warning('暂无可提交的成品,请添加成品');
+        return;
+    }
+    showConfirmModal.value = true;
+};
+
+// 提交入库
+const executeInbound = async () => {
+    const count = products.value.length;
+    loading.value = true;
+
+    const params = products.value.map(item => {
+        return {
+            inventoryName: item.inventoryName,
+            inventoryNo: item.inventoryNo,
+            positionId: item.positionId,
+        };
+    });
+    try {
+        const res = await generateStockIn(params);
+        if (res.errorCode === 0) {
+            message.success(`已提交 ${count} 条成品入库记录`);
+        } else {
+            message.warning(res.errorMessage);
+        }
+        products.value = [];
+        showConfirmModal.value = false;
+    } catch (error) {
+        console.log(error);
+        message.error('提交入库失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 删除成品
+const removeProduct = index => {
+    Modal.confirm({
+        title: '确定删除这条成品吗?',
+        content: '如果确认的话请点击【确定】',
+        okText: '确定',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk() {
+            products.value.splice(index, 1);
+            message.success('成品已删除');
+        },
+        onCancel() {
+            console.log('Cancel');
+        },
+    });
+};
+
+const getLocators = async () => {
+    try {
+        const res = await getLocatorList();
+        if (res && res.length > 0) {
+            warehouseLocationOptions.value = res;
+        } else {
+            message.warning('获取货位列表失败');
+        }
+    } catch (error) {
+        console.log(error);
+        message.error('获取货位列表失败');
+    }
+};
+onMounted(() => {
+    getLocators();
+});
+</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;
+}
+
+/* 顶部统计与按钮行 */
+.top-actions-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 1rem;
+}
+
+.top-actions-info {
+  font-size: 0.875rem;
+  color: #4b5563;
+}
+
+/* 卡片容器 */
+.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: 0.5rem 1.5rem;
+  border-top: 1px solid #e5e7eb;
+  border-bottom: 1px solid #e5e7eb;
+  background-color: #fafafa;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+/* 库存卡片样式 */
+.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);
+}
+
+/* 卡片标题区域 */
+.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;
+}
+
+/* Ant Design 卡片头部样式 */
+: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;
+}
+
+/* 按钮样式与其他页面统一 */
+: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;
+}
+
+/* 删除按钮样式 */
+.delete-btn {
+  margin-left: 0.5rem;
+  opacity: 0.7;
+  transition: opacity 0.2s ease;
+}
+
+.delete-btn:hover {
+  opacity: 1;
+}
+</style>

+ 584 - 0
src/finishProduct/FinishProductOut.vue

@@ -0,0 +1,584 @@
+<template>
+  <div class="page-container">
+    <!-- 顶部信息栏 -->
+    <PageHeader />
+
+    <!-- 主内容区域 -->
+    <main class="main-content">
+      <!-- 页面标题 -->
+      <div class="page-title">
+        <h2>成品出库管理</h2>
+      </div>
+
+      <!-- 筛选区域(沿用 FilterPanel 风格) -->
+      <FilterPanel :default-collapsed="false" :active-count="activeFilterCount">
+        <a-form layout="inline" class="filter-form">
+          <a-form-item label="名称">
+            <a-input
+              v-model:value="searchForm.inventoryName" placeholder="输入名称" style="width: 150px"
+              @keyup.enter="handleSearch"
+            />
+          </a-form-item>
+
+          <a-form-item label="编号">
+            <a-input
+              v-model:value="searchForm.inventoryNo" placeholder="输入编号" style="width: 150px"
+              @keyup.enter="handleSearch"
+            />
+          </a-form-item>
+
+          <a-form-item label="仓库">
+            <a-select
+              v-model:value="searchForm.warehouseId" :options="warehouseList" placeholder="选择仓库"
+              option-filter-prop="name" show-search allow-clear :field-names="{ label: 'name', value: 'id' }"
+              style="width: 150px" @change="handleSearch"
+            />
+          </a-form-item>
+
+          <a-form-item>
+            <a-button type="primary" @click="handleSearch">
+              <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="finishProductList.length === 0" description="暂无成品" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+
+          <!-- 卡片列表 -->
+          <div v-else class="card-list">
+            <a-card
+              v-for="product in finishProductList" :key="product.id" :hoverable="true"
+              :class="{ 'selected-card': selectedProducts.includes(product.id) }" class="inventory-card"
+              @click="toggleSelect(product.id)"
+            >
+              <template #title>
+                <div class="card-header">
+                  <div class="card-header-left">
+                    <a-checkbox
+                      :checked="selectedProducts.includes(product.id)" @click.stop
+                      @change="e => handleCheckboxChange(e, product.id)"
+                    />
+                    <a-avatar :size="42" :style="{ backgroundColor: '#3b82f6' }" class="ml-3">
+                      <template #icon>
+                        <i class="fas fa-box" style="font-size: 20px;" />
+                      </template>
+                    </a-avatar>
+                    <div class="ml-4 card-title-info">
+                      <div class="card-name">{{ product.inventoryName }}</div>
+                      <div class="card-subtitle">编号: {{ product.inventoryNo }}</div>
+                    </div>
+                  </div>
+                  <div class="card-header-right">
+                    <div class="card-location">
+                      <i class="fas fa-map-marker-alt mr-2" />
+                      <span>{{ product.inventoryPosition }}</span>
+                      <span class="mx-2">/</span>
+                      <span>{{ product.inventoryWarehouse }}</span>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </a-card>
+          </div>
+        </div>
+
+        <!-- 分页区域(固定底部) -->
+        <div v-if="finishProductList.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="selectedProducts.length === 0" class="add-to-cart-btn"
+        @click="confirmOutbound"
+      >
+        <i class="fas fa-check-circle mr-2" />
+        确认出库
+      </a-button>
+    </div>
+
+    <!-- 确认出库弹窗 -->
+    <a-modal
+      v-model:open="showConfirmModal" title="确认出库" :mask-closable="false" width="420px"
+      @cancel="showConfirmModal = false"
+    >
+      <p class="text-gray-700 mb-4">
+        您确定要将选中的
+        <span class="font-semibold">{{ selectedProducts.length }}</span>
+        个成品进行出库操作吗?
+      </p>
+      <template #footer>
+        <a-button @click="showConfirmModal = false">取消</a-button>
+        <a-button type="primary" @click="executeOutbound">确认出库</a-button>
+      </template>
+    </a-modal>
+
+    <Loading v-if="loading" />
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+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 { queryFinishProduct, generateStockOut } from '../api/finishProduct.js';
+
+// 加载状态(预留给后续真实接口调用)
+const loading = ref(false);
+
+// 仓库列表
+const warehouseList = ref([]);
+
+// 筛选表单
+const searchForm = ref({
+    inventoryName: '',
+    inventoryNo: '',
+    warehouseId: undefined,
+});
+
+// 分页
+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 finishProductList = ref([]);
+
+// 选中项
+const selectedProducts = ref([]); // 存储选中的 product.id
+
+// 弹窗与提示
+const showConfirmModal = ref(false);
+
+// 激活的筛选条件数量
+const activeFilterCount = computed(() => {
+    let count = 0;
+    if (searchForm.value.warehouseId) count++;
+    if (searchForm.value.inventoryName) count++;
+    if (searchForm.value.inventoryNo) count++;
+    return count;
+});
+
+// 分页改变
+const getPageParams = (page, pageSize) => {
+    pagination.value.current_page = page;
+    pagination.value.per_page = pageSize;
+    getList();
+};
+
+// 搜索
+const handleSearch = () => {
+    pagination.value.current_page = 1;
+    getList();
+};
+
+// 重置
+const handleReset = () => {
+    searchForm.value = {
+        warehouseId: undefined,
+        inventoryName: '',
+        inventoryNo: '',
+    };
+    pagination.value.current_page = 1;
+    getList();
+};
+
+// 卡片勾选
+const toggleSelect = id => {
+    const index = selectedProducts.value.indexOf(id);
+    if (index > -1) {
+        selectedProducts.value.splice(index, 1);
+    } else {
+        selectedProducts.value.push(id);
+    }
+};
+
+// 处理复选框变化
+const handleCheckboxChange = (e, id) => {
+    if (e.target.checked) {
+        if (!selectedProducts.value.includes(id)) {
+            selectedProducts.value.push(id);
+        }
+    } else {
+        const index = selectedProducts.value.indexOf(id);
+        if (index > -1) {
+            selectedProducts.value.splice(index, 1);
+        }
+    }
+};
+
+// 打开发起出库确认
+const confirmOutbound = () => {
+    if (selectedProducts.value.length === 0) {
+        message.warning('请先选择需要出库的成品');
+        return;
+    }
+    showConfirmModal.value = true;
+};
+
+// 执行出库
+const executeOutbound = async () => {
+    showConfirmModal.value = false;
+    if (selectedProducts.value.length === 0) return;
+
+    loading.value = true;
+    const count = selectedProducts.value.length;
+    try {
+        const res = await generateStockOut(selectedProducts.value);
+        if (res.errorCode === 0) {
+            message.success(`已成功出库 ${count} 条成品出库记录`);
+            selectedProducts.value = [];
+            pagination.value.current_page = 1;
+            getList();
+        } else {
+            message.warning(res.errorMessage);
+        }
+    } catch (error) {
+        console.log(error);
+        message.error('提交出库失败');
+    } finally {
+        loading.value = false;
+    }
+};
+// 获取成品列表
+const getList = async () => {
+
+    loading.value = true;
+    const params = {
+        ...searchForm.value, range: {
+            start: (pagination.value.current_page - 1) * pagination.value.per_page,
+            length: pagination.value.per_page,
+        },
+    };
+    try {
+        const res = await queryFinishProduct(params);
+
+        if (res.errorCode === 0) {
+            if (res.datas && res.datas.length > 0) {
+                finishProductList.value = res.datas;
+                pagination.value.total = res.total;
+            } else {
+                finishProductList.value = [];
+                pagination.value.total = 0;
+            }
+        } else {
+            finishProductList.value = [];
+            pagination.value.total = 0;
+            message.error(res.errorMessage);
+        }
+    } catch (error) {
+        console.error('获取成品列表API调用失败:', error);
+        message.error('获取成品列表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;
+    }
+};
+onMounted(() => {
+    getWarehouse();
+    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 1.5rem 1rem 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;
+}
+
+/* 筛选表单 */
+.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;
+}
+
+
+.top-actions-info {
+  font-size: 0.875rem;
+  color: #4b5563;
+}
+
+/* 卡片容器 */
+.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;
+}
+
+/* 卡片样式 */
+.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;
+}
+
+/* 卡片内容区域 */
+.card-body-content {
+  padding: 0 1.5rem 1rem 1.5rem;
+}
+
+.card-body-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 0.875rem;
+  color: #4b5563;
+  margin-top: 0.5rem;
+}
+
+.badge-category {
+  display: inline-flex;
+  align-items: center;
+  padding: 0.25rem 0.75rem;
+  border-radius: 9999px;
+  font-size: 0.75rem;
+  font-weight: 500;
+  background-color: #dbeafe;
+  color: #1d4ed8;
+}
+
+/* 分页包装器(固定底部) */
+.pagination-wrapper {
+  padding: 0.5rem 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;
+}
+
+.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;
+}
+
+/* 成功提示 */
+.success-alert {
+  position: fixed;
+  right: 1.5rem;
+  bottom: 1.5rem;
+  width: 320px;
+  z-index: 50;
+}
+
+/* Ant Design 样式统一 */
+: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;
+}
+
+:deep(.ant-btn[disabled]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+</style>

+ 830 - 0
src/login/FingerprintLogin.vue

@@ -0,0 +1,830 @@
+<template>
+  <div class="recognize-wrapper">
+    <div class="recognize-container">
+      <!-- 状态圆形区域 -->
+      <div class="status-circle" :class="statusClass">
+        <div class="circle-glow" />
+        <div class="fingerprint-icon-wrapper">
+          <svg class="fingerprint-svg" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+            <!-- 外侧指纹脊线 -->
+            <ellipse
+              cx="100" cy="100" rx="85" ry="90" fill="none" stroke="currentColor" stroke-width="2"
+              opacity="0.3"
+            />
+            <ellipse
+              cx="100" cy="100" rx="75" ry="80" fill="none" stroke="currentColor" stroke-width="2"
+              opacity="0.4"
+            />
+            <ellipse
+              cx="100" cy="100" rx="65" ry="70" fill="none" stroke="currentColor" stroke-width="2"
+              opacity="0.5"
+            />
+
+            <!-- 内侧指纹脊线 -->
+            <path
+              d="M100 30 C130 30, 150 50, 150 80 C150 110, 130 130, 100 130 C70 130, 50 110, 50 80 C50 50, 70 30, 100 30"
+              fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.6"
+            />
+            <path
+              d="M100 40 C120 40, 135 55, 135 75 C135 95, 120 110, 100 110 C80 110, 65 95, 65 75 C65 55, 80 40, 100 40"
+              fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.7"
+            />
+            <path
+              d="M100 50 C110 50, 120 60, 120 70 C120 80, 110 90, 100 90 C90 90, 80 80, 80 70 C80 60, 90 50, 100 50"
+              fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.8"
+            />
+
+            <!-- 指纹脊线 -->
+            <path
+              d="M60 60 Q100 40, 140 60" fill="none" stroke="currentColor" stroke-width="1.5"
+              opacity="0.6"
+            />
+            <path
+              d="M65 70 Q100 50, 135 70" fill="none" stroke="currentColor" stroke-width="1.5"
+              opacity="0.6"
+            />
+            <path
+              d="M70 80 Q100 60, 130 80" fill="none" stroke="currentColor" stroke-width="1.5"
+              opacity="0.6"
+            />
+            <path
+              d="M75 90 Q100 70, 125 90" fill="none" stroke="currentColor" stroke-width="1.5"
+              opacity="0.6"
+            />
+            <path
+              d="M80 100 Q100 80, 120 100" fill="none" stroke="currentColor" stroke-width="1.5"
+              opacity="0.6"
+            />
+
+            <!-- 中央螺旋纹 -->
+            <circle
+              cx="100" cy="75" r="8" fill="none" stroke="currentColor" stroke-width="2"
+              opacity="0.8"
+            />
+            <circle cx="100" cy="75" r="4" fill="currentColor" opacity="0.6" />
+          </svg>
+
+          <!-- 扫描线动画 -->
+          <div v-if="status === 'recognizing'" class="scan-line" />
+        </div>
+
+        <!-- 成功图标 -->
+        <div v-if="status === 'success'" class="success-icon">
+          <svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
+            <circle cx="26" cy="26" r="25" fill="none" />
+            <path fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" />
+          </svg>
+        </div>
+
+        <!-- 失败图标 -->
+        <div v-if="status === 'failed'" class="failed-icon">
+          <svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
+            <circle cx="26" cy="26" r="25" fill="none" />
+            <path fill="none" d="M16 16 36 36 M36 16 16 36" />
+          </svg>
+        </div>
+      </div>
+
+      <!-- 状态文本 -->
+      <div class="status-text">
+        {{ isConnected ? statusText : '设备未连接' }}
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="action-buttons">
+        <van-button
+          type="default" size="large" round block class="action-btn password-btn"
+          @click="goToPasswordLogin"
+        >
+          <template #icon>
+            <i class="fas fa-key" />
+          </template>
+          使用密码登录
+        </van-button>
+        <van-button
+          type="default" size="large" round block class="action-btn restart-btn"
+          @click="handleRestart"
+        >
+          <template #icon>
+            <i class="fas fa-redo-alt" />
+          </template>
+          重启指纹识别
+        </van-button>
+      </div>
+
+      <!-- 用户信息(识别成功后显示) -->
+      <!-- <div v-if="status === 'success' && userInfo" class="user-info">
+        <div class="user-avatar">
+          <span class="avatar-text">{{ userInfo.name ? userInfo.name.charAt(0) : '?' }}</span>
+        </div>
+        <div class="user-details">
+          <div class="user-name">{{ userInfo.name || '未知用户' }}</div>
+          <div class="user-role">{{ userInfo.loginName || '' }}</div>
+        </div>
+      </div> -->
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useRouter, useRoute } from 'vue-router';
+import { ref, onMounted, onUnmounted } from 'vue';
+import { showSuccessToast, showFailToast, Button as VanButton } from 'vant';
+
+const route = useRoute();
+const router = useRouter();
+
+// 状态:waiting(等待识别), recognizing(识别中), success(成功), failed(失败)
+const userInfo = ref(null);
+const status = ref('waiting');
+const statusText = ref('');
+const isConnected = ref(false);
+// 计算状态类名
+const statusClass = ref('status-waiting');
+
+// 更新状态
+const updateStatus = (newStatus, text) => {
+    status.value = newStatus;
+    statusText.value = text;
+
+    switch (newStatus) {
+    case 'waiting':
+        statusClass.value = 'status-waiting';
+        break;
+    case 'recognizing':
+        statusClass.value = 'status-recognizing';
+        break;
+    case 'success':
+        statusClass.value = 'status-success';
+        break;
+    case 'failed':
+        statusClass.value = 'status-failed';
+        break;
+    }
+};
+
+// 处理指纹响应
+const handleFingerprintResponse = data => {
+    console.log('接收到指纹设备的响应信息', data);
+
+    if (!data) {
+        updateStatus('failed', '识别失败,未接收到数据');
+        showFailToast('识别失败');
+        return;
+    }
+
+    const code = data.code;
+    const msg = data.message || '';
+    const responseData = data.loginInfo;
+
+    if (code === 0) {
+        // 有数据说明登录成功
+        userInfo.value = responseData;
+        if (msg === '连接成功' || msg === '请按下手指') {
+            isConnected.value = true;
+            updateStatus('recognizing', '请按下手指');
+        }
+        if (msg === '正在同步指纹,请稍后进行指纹登录') {
+            updateStatus('waiting', '正在同步指纹,请稍后进行指纹登录');
+        }
+        if (responseData) {
+            updateStatus('success', '识别成功,登录成功!');
+            setTimeout(() => {
+                localStorage.setItem('#token', responseData.token);
+                localStorage.setItem('#accountId', responseData.accountId);
+                localStorage.setItem('#LoginInfo', JSON.stringify(responseData));
+                showSuccessToast('欢迎登录,' + responseData.userName);
+                const redirectUrl = window.location.href.split('redirectUrl=')[1];
+
+                if (redirectUrl) {
+                    window.location = decodeURIComponent(redirectUrl);
+                } else {
+                    router.push('/home');
+                }
+            }, 1000);
+        }
+    } else {
+        if (msg === '连接失败') {
+            isConnected.value = false;
+        } else {
+            updateStatus('failed', msg || '识别失败,请重试');
+        }
+    }
+};
+
+// 开始识别
+const startRecognize = () => {
+    if (!plugin.fingerprintConfig) {
+        updateStatus('failed', '指纹设备插件未就绪');
+        showFailToast('设备不支持指纹识别功能');
+        return;
+    }
+
+    try {
+        plugin.fingerprintConfig.recognize();
+    } catch (error) {
+        console.error('启动指纹识别失败', error);
+        updateStatus('failed', '启动识别失败');
+        showFailToast('启动识别失败');
+    }
+};
+
+// 重启指纹识别
+const handleRestart = () => {
+    userInfo.value = null;
+    isConnected.value = true;
+    updateStatus('waiting', '指纹重启中,请稍后...');
+
+    setTimeout(() => {
+        startRecognize();
+        updateStatus('recognizing', '请按下手指');
+    }, 1000);
+};
+
+// 跳转到密码登录页面
+const goToPasswordLogin = () => {
+    const isOut = localStorage.getItem('isOut') === 'true';
+    if (isOut) {
+        router.push({ path: '/login', query: { isOut: 'true' } });
+    } else {
+        router.push('/login');
+    }
+};
+
+// 组件挂载
+onMounted(() => {
+    // 检查URL参数中是否包含isOut=true,如果有则存储到localStorage
+    const isOutParam = route.query.isOut;
+    if (isOutParam !== undefined) {
+        const isOutValue = String(isOutParam) === 'true' ? 'true' : 'false';
+        if (isOutValue == 'true') {
+            localStorage.setItem('isOut', isOutValue);
+        } else {
+            localStorage.setItem('isOut', isOutValue);
+        }
+    } else {
+        localStorage.setItem('isOut', 'false');
+    }
+
+    // 设置指纹设备的响应处理函数
+    if (plugin.fingerprintConfig) {
+        plugin.fingerprintConfig.connect();
+        plugin.fingerprintConfig.receiveFingerprintResponse = handleFingerprintResponse;
+    }
+
+    // 延迟500ms后开始识别
+    setTimeout(() => {
+        startRecognize();
+    }, 500);
+});
+
+// 组件卸载
+onUnmounted(() => {
+    // 清理响应处理函数
+    if (plugin.fingerprintConfig) {
+        plugin.fingerprintConfig.receiveFingerprintResponse = () => { };
+        plugin.fingerprintConfig.disableConnect();
+    }
+});
+</script>
+
+<style scoped>
+.recognize-wrapper {
+    width: 100%;
+    height: 100vh;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    overflow: hidden;
+    position: relative;
+}
+
+/* 背景动画 */
+.recognize-wrapper::before {
+    content: '';
+    position: absolute;
+    width: 200%;
+    height: 200%;
+    background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
+    background-size: 50px 50px;
+    animation: backgroundMove 20s linear infinite;
+}
+
+@keyframes backgroundMove {
+    0% {
+        transform: translate(0, 0);
+    }
+
+    100% {
+        transform: translate(50px, 50px);
+    }
+}
+
+.recognize-container {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 2rem;
+    padding: 2rem;
+    max-width: 500px;
+    width: 90%;
+}
+
+/* 状态圆形 */
+.status-circle {
+    position: relative;
+    width: clamp(250px, 40vw, 350px);
+    height: clamp(250px, 40vw, 350px);
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.95);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+    transition: all 0.5s ease;
+    overflow: hidden;
+}
+
+.circle-glow {
+    position: absolute;
+    inset: -20px;
+    border-radius: 50%;
+    opacity: 0;
+    transition: opacity 0.5s ease;
+}
+
+/* 等待状态 */
+.status-waiting {
+    border: 4px solid rgba(102, 126, 234, 0.3);
+}
+
+.status-waiting .circle-glow {
+    background: radial-gradient(circle, rgba(102, 126, 234, 0.2) 0%, transparent 70%);
+    opacity: 0.5;
+}
+
+/* 识别中状态 */
+.status-recognizing {
+    border: 4px solid #667eea;
+    animation: circlePulse 2s infinite;
+}
+
+.status-recognizing .circle-glow {
+    background: radial-gradient(circle, rgba(102, 126, 234, 0.4) 0%, transparent 70%);
+    opacity: 1;
+    animation: glowPulse 2s infinite;
+}
+
+@keyframes circlePulse {
+
+    0%,
+    100% {
+        transform: scale(1);
+        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+    }
+
+    50% {
+        transform: scale(1.05);
+        box-shadow: 0 25px 70px rgba(102, 126, 234, 0.5);
+    }
+}
+
+@keyframes glowPulse {
+
+    0%,
+    100% {
+        opacity: 0.8;
+    }
+
+    50% {
+        opacity: 1;
+    }
+}
+
+/* 成功状态 */
+.status-success {
+    border: 4px solid #10b981;
+    background: rgba(16, 185, 129, 0.1);
+    animation: successBounce 0.6s ease;
+}
+
+.status-success .circle-glow {
+    background: radial-gradient(circle, rgba(16, 185, 129, 0.3) 0%, transparent 70%);
+    opacity: 1;
+}
+
+@keyframes successBounce {
+    0% {
+        transform: scale(1);
+    }
+
+    50% {
+        transform: scale(1.1);
+    }
+
+    100% {
+        transform: scale(1);
+    }
+}
+
+/* 失败状态 */
+.status-failed {
+    border: 4px solid #ef4444;
+    animation: shake 0.5s ease;
+}
+
+.status-failed .circle-glow {
+    background: radial-gradient(circle, rgba(239, 68, 68, 0.3) 0%, transparent 70%);
+    opacity: 1;
+}
+
+@keyframes shake {
+
+    0%,
+    100% {
+        transform: translateX(0);
+    }
+
+    25% {
+        transform: translateX(-10px);
+    }
+
+    75% {
+        transform: translateX(10px);
+    }
+}
+
+/* 指纹图标 */
+.fingerprint-icon-wrapper {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.fingerprint-svg {
+    width: 60%;
+    height: 60%;
+    color: #9ca3af;
+    transition: color 0.5s ease;
+}
+
+.status-waiting .fingerprint-svg {
+    color: #d1d5db;
+}
+
+.status-recognizing .fingerprint-svg {
+    color: #667eea;
+}
+
+.status-success .fingerprint-svg {
+    color: #10b981;
+    opacity: 0.3;
+}
+
+.status-failed .fingerprint-svg {
+    color: #ef4444;
+    opacity: 0.3;
+}
+
+/* 扫描线动画 */
+.scan-line {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 3px;
+    background: linear-gradient(90deg, transparent, #667eea, transparent);
+    animation: scanning 2s linear infinite;
+}
+
+@keyframes scanning {
+    0% {
+        transform: translateY(0);
+    }
+
+    100% {
+        transform: translateY(clamp(250px, 40vw, 350px));
+    }
+}
+
+/* 成功图标 */
+.success-icon {
+    position: absolute;
+    width: 50%;
+    height: 50%;
+    animation: iconAppear 0.5s ease forwards;
+}
+
+.success-icon svg {
+    width: 100%;
+    height: 100%;
+}
+
+.success-icon circle {
+    stroke: #10b981;
+    stroke-width: 3;
+    stroke-dasharray: 166;
+    stroke-dashoffset: 166;
+    animation: checkmarkCircle 0.6s ease-in-out forwards;
+}
+
+.success-icon path {
+    stroke: #10b981;
+    stroke-width: 4;
+    stroke-linecap: round;
+    stroke-linejoin: round;
+    stroke-dasharray: 48;
+    stroke-dashoffset: 48;
+    animation: checkmark 0.3s 0.3s ease-in-out forwards;
+}
+
+@keyframes iconAppear {
+    from {
+        opacity: 0;
+        transform: scale(0.5);
+    }
+
+    to {
+        opacity: 1;
+        transform: scale(1);
+    }
+}
+
+@keyframes checkmarkCircle {
+    to {
+        stroke-dashoffset: 0;
+    }
+}
+
+@keyframes checkmark {
+    to {
+        stroke-dashoffset: 0;
+    }
+}
+
+/* 失败图标 */
+.failed-icon {
+    position: absolute;
+    width: 50%;
+    height: 50%;
+    animation: iconAppear 0.5s ease forwards;
+}
+
+.failed-icon svg {
+    width: 100%;
+    height: 100%;
+}
+
+.failed-icon circle {
+    stroke: #ef4444;
+    stroke-width: 3;
+    stroke-dasharray: 166;
+    stroke-dashoffset: 166;
+    animation: checkmarkCircle 0.6s ease-in-out forwards;
+}
+
+.failed-icon path {
+    stroke: #ef4444;
+    stroke-width: 4;
+    stroke-linecap: round;
+    stroke-dasharray: 50;
+    stroke-dashoffset: 50;
+    animation: crossmark 0.3s 0.3s ease-in-out forwards;
+}
+
+@keyframes crossmark {
+    to {
+        stroke-dashoffset: 0;
+    }
+}
+
+/* 状态文本 */
+.status-text {
+    font-size: clamp(1.2rem, 3vw, 1.8rem);
+    font-weight: 600;
+    color: white;
+    text-align: center;
+    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+    animation: textFadeIn 0.5s ease;
+}
+
+@keyframes textFadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(10px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+/* 用户信息卡片 */
+.user-info {
+    background: rgba(255, 255, 255, 0.95);
+    border-radius: 16px;
+    padding: 1.5rem 2rem;
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+    animation: slideUp 0.5s ease;
+    width: 100%;
+    max-width: 400px;
+}
+
+@keyframes slideUp {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.user-avatar {
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #667eea, #764ba2);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.avatar-text {
+    font-size: 1.5rem;
+    font-weight: 700;
+    color: white;
+}
+
+.user-details {
+    flex: 1;
+}
+
+.user-name {
+    font-size: 1.3rem;
+    font-weight: 700;
+    color: #1f2937;
+    margin-bottom: 0.25rem;
+}
+
+.user-role {
+    font-size: 1rem;
+    color: #6b7280;
+}
+
+/* 操作按钮 */
+.action-buttons {
+    display: flex;
+    gap: 1rem;
+    animation: slideUp 0.5s ease;
+}
+
+.retry-button {
+    height: 3.5rem;
+    padding: 0 2.5rem;
+    font-size: 1.1rem;
+    font-weight: 600;
+    background: white;
+    color: #667eea;
+    border: 2px solid white;
+    border-radius: 50px;
+    transition: all 0.3s ease;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+}
+
+.retry-button:hover {
+    background: rgba(255, 255, 255, 0.9);
+    transform: translateY(-2px);
+    box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .recognize-container {
+        gap: 1.5rem;
+        padding: 1.5rem;
+    }
+
+    .status-circle {
+        width: clamp(200px, 50vw, 280px);
+        height: clamp(200px, 50vw, 280px);
+    }
+
+    .user-info {
+        padding: 1rem 1.5rem;
+    }
+
+    .user-avatar {
+        width: 50px;
+        height: 50px;
+    }
+
+    .avatar-text {
+        font-size: 1.2rem;
+    }
+
+    .user-name {
+        font-size: 1.1rem;
+    }
+
+    .user-role {
+        font-size: 0.9rem;
+    }
+}
+
+/* 暗色模式支持 */
+@media (prefers-color-scheme: dark) {
+    .status-circle {
+        background: rgba(31, 41, 55, 0.95);
+    }
+
+    .user-info {
+        background: rgba(31, 41, 55, 0.95);
+    }
+
+    .user-name {
+        color: #f9fafb;
+    }
+
+    .user-role {
+        color: #d1d5db;
+    }
+}
+
+/* 操作按钮 */
+.action-buttons {
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    width: 100%;
+    max-width: 350px;
+    animation: textFadeIn 0.5s ease;
+}
+
+.action-btn {
+    font-size: 1rem;
+    font-weight: 500;
+    height: 48px !important;
+    transition: all 0.3s ease;
+}
+
+.action-btn i {
+    margin-right: 8px;
+    font-size: 16px;
+}
+
+/* 密码登录按钮样式 */
+:deep(.password-btn) {
+    background: rgba(255, 255, 255, 0.15) !important;
+    border: 2px solid rgba(255, 255, 255, 0.5) !important;
+    color: white !important;
+    backdrop-filter: blur(10px);
+}
+
+:deep(.password-btn:active) {
+    background: rgba(255, 255, 255, 0.25) !important;
+}
+
+/* 重启按钮样式 */
+:deep(.restart-btn) {
+    background: rgba(255, 255, 255, 0.1) !important;
+    border: 2px solid rgba(255, 255, 255, 0.3) !important;
+    color: rgba(255, 255, 255, 0.9) !important;
+    backdrop-filter: blur(10px);
+}
+
+:deep(.restart-btn:active) {
+    background: rgba(255, 255, 255, 0.2) !important;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .action-buttons {
+        max-width: 100%;
+        padding: 0 1rem;
+    }
+}
+
+/* 减少动画(无障碍支持) */
+@media (prefers-reduced-motion: reduce) {
+    * {
+        animation-duration: 0.01ms !important;
+        animation-iteration-count: 1 !important;
+        transition-duration: 0.01ms !important;
+    }
+}
+</style>

+ 8 - 1
src/login/UserHome.vue

@@ -422,7 +422,14 @@ const handleAction = action => {
         // 还料出库离开
         validateMaterialInLeave();
         break;
-
+    case 'finishedProductIn':
+        // 成品入库
+        router.push('/finish-product-in');
+        break;
+    case 'finishedProductOut':
+        // 成品出库
+        router.push('/finish-product-out');
+        break;
 
     // 测试待删除
     case 'returnManagement':

+ 78 - 20
src/login/UserLogin.vue

@@ -39,33 +39,51 @@
           <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 class="native-input-wrapper">
+                <i class="fas fa-user input-icon" />
+                <input
+                  v-model="username"
+                  type="text"
+                  placeholder="请输入用户名"
+                  class="native-input"
+                  @keyup.enter="handleLogin"
+                />
+              </div>
             </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 class="native-input-wrapper">
+                <i class="fas fa-lock input-icon" />
+                <input
+                  v-model="password"
+                  :type="showPassword ? 'text' : 'password'"
+                  placeholder="请输入密码"
+                  class="native-input"
+                  @keyup.enter="handleLogin"
+                />
+                <i
+                  :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"
+                  class="password-toggle-icon"
+                  @click="showPassword = !showPassword"
+                />
+              </div>
             </div>
 
             <!-- 登录按钮 -->
             <a-button type="primary" block size="large" class="login-button" :loading="loading" @click="handleLogin">
               登录
             </a-button>
+            <a-button type="primary" block size="large" class="login-button" @click="handleFingerprintLogin">
+              指纹登录
+            </a-button>
 
             <!-- 忘记密码链接 -->
-            <div class="form-footer">
+            <!-- <div class="form-footer">
               <a href="#" class="forgot-link" @click.prevent="handleForgotPassword">
                 忘记密码?
               </a>
-            </div>
+            </div> -->
           </div>
         </div>
       </div>
@@ -93,6 +111,7 @@ const route = useRoute();
 const username = ref('');
 const password = ref('');
 const loading = ref(false);
+const showPassword = ref(false);
 
 onMounted(() => {
     // 检查URL参数中是否包含isOut=true,如果有则存储到localStorage
@@ -154,6 +173,15 @@ const handleLogin = async () => {
     }
 };
 
+const handleFingerprintLogin = () => {
+    const isOut = localStorage.getItem('isOut') === 'true';
+    if (isOut) {
+        router.push({ path: '/fingerprint-login', query: { isOut: 'true' } });
+    } else {
+        router.push('/fingerprint-login');
+    }
+};
+
 // 忘记密码处理函数
 const handleForgotPassword = () => {
     message.info('请联系管理员重置密码');
@@ -298,6 +326,7 @@ const handleForgotPassword = () => {
   border-radius: 8px;
   margin-top: 8px;
   transition: all 0.3s ease;
+  color: white;
 }
 
 .login-button:hover {
@@ -384,24 +413,53 @@ const handleForgotPassword = () => {
   }
 }
 
-/* Ant Design 样式覆盖 */
-:deep(.ant-input-affix-wrapper) {
-  border-radius: 8px;
+/* 原生输入框样式 */
+.native-input-wrapper {
+  position: relative;
+  display: flex;
+  align-items: center;
   border: 1px solid #e5e7eb;
+  border-radius: 8px;
   padding: 12px 16px;
+  background: white;
+  transition: all 0.3s ease;
 }
 
-:deep(.ant-input-affix-wrapper:focus),
-:deep(.ant-input-affix-wrapper-focused) {
+.native-input-wrapper:focus-within {
   border-color: #667eea;
   box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
 }
 
-:deep(.ant-input) {
+.native-input {
+  flex: 1;
+  border: none;
+  outline: none;
   font-size: 15px;
+  color: #1f2937;
+  background: transparent;
+  padding-left: 8px;
 }
 
-:deep(.ant-input-password) {
-  border-radius: 8px;
+.native-input::placeholder {
+  color: #9ca3af;
+}
+
+.input-icon {
+  color: #667eea;
+  font-size: 16px;
+  flex-shrink: 0;
+}
+
+.password-toggle-icon {
+  color: #9ca3af;
+  font-size: 16px;
+  cursor: pointer;
+  transition: color 0.3s ease;
+  flex-shrink: 0;
+  margin-left: 8px;
+}
+
+.password-toggle-icon:hover {
+  color: #667eea;
 }
 </style>

+ 16 - 0
src/main.js

@@ -12,6 +12,9 @@ import './assets/main.css';
 import routes from './router/routes.js';
 import $ from 'jquery';
 
+import Vant from 'vant';
+import 'vant/lib/index.css';
+
 window.$ = $;
 window.jQuery = $;
 window.PcComponentV3 = PcComponentV3;
@@ -24,11 +27,24 @@ const router = createRouter({
     routes, // `routes: routes` 的缩写
 });
 
+// 路由守卫:动态更新页面标题
+router.beforeEach((to, from, next) => {
+    // 如果路由有 meta.title,则更新页面标题
+    if (to.meta && to.meta.title) {
+        document.title = to.meta.title;
+    } else {
+        // 默认标题
+        document.title = 'WMS仓库管理系统';
+    }
+    next();
+});
+
 function render(props = {}) {
     const { container } = props;
   
     instance = createApp(App);
     instance.use(Antd);
+    instance.use(Vant);
     instance.use(PcComponentV3);
     instance.use(router);
     instance.mount(container ? container.querySelector('#app') : '#app');

+ 25 - 11
src/router/routes.js

@@ -23,18 +23,32 @@ const ReturnManagement = () => import('../agv-process/ReturnManagement.vue');
 // 送料区管理
 const DeliveryManagement = () => import('../agv-process/DeliveryManagement.vue');
 
+// 成品入库
+const FinishProductIn = () => import('../finishProduct/FinishProductIn.vue');
+
+// 成品出库
+const FinishProductOut = () => import('../finishProduct/FinishProductOut.vue');
+
+const FingerprintEnroll = () => import('../Fingerprint/FingerprintEnroll.vue');
+
+const FingerprintLogin = () => import('../login/FingerprintLogin.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 },
+    { path: '/login', component: UserLogin, meta: { title: '用户登录' } },
+    { path: '/home', component: UserHome, meta: { title: '首页' } },
+    { path: '/order-picking', component: OrderPicking, meta: { title: '拣货管理' } },
+    { path: '/outbound-confirm', component: OutboundConfirm, meta: { title: '出库确认' } },
+    { path: '/stock-picking-car', component: StockPickingCar, meta: { title: '领料车' } },
+    { path: '/stock-requisition', component: StockRequisition, meta: { title: '领料管理' } },
+    { path: '/regular-requisition', component: RegularRequisition, meta: { title: '常用领料' } },
+    { path: '/agv-rfid-recognition', component: AgvRfidRecognition, meta: { title: 'AGV RFID识别' } },
+    { path: '/in-confirm', component: InConfirm, meta: { title: '入库确认' } },
+    { path: '/return-management', component: ReturnManagement, meta: { title: '还料区管理' } },
+    { path: '/delivery-management', component: DeliveryManagement, meta: { title: '送料区管理' } },
+    { path: '/finish-product-in', component: FinishProductIn, meta: { title: '成品入库' } },
+    { path: '/finish-product-out', component: FinishProductOut, meta: { title: '成品出库管理' } },
+    { path: '/fingerprint-enroll', component: FingerprintEnroll, meta: { title: '指纹录入' } },
+    { path: '/fingerprint-login', component: FingerprintLogin, meta: { title: '指纹登录' } },
 ];
 export default routes;

+ 1 - 1
src/stock-in/InConfirm.vue

@@ -307,7 +307,7 @@ onMounted(() => {
 
 /* 分页包装器(固定底部) */
 .pagination-wrapper {
-  padding: 1rem 1.5rem;
+  padding: 0.5rem 1.5rem;
   border-top: 1px solid #e5e7eb;
   border-bottom: 1px solid #e5e7eb;
   background-color: #fafafa;

+ 15 - 18
src/stock-out/OrderPicking.vue

@@ -14,37 +14,34 @@
       <!-- 筛选区域 -->
       <FilterPanel :default-collapsed="false" :active-count="getActiveFilterCount()">
         <a-form layout="inline" class="filter-form">
-          <a-form-item label="设备名称">
+          <a-form-item label="名称">
             <a-input
-              v-model:value="filterForm.inventoryName" placeholder="请输入" style="width: 180px"
+              v-model:value="filterForm.inventoryName" placeholder="输入名称" style="width: 150px"
               @keyup.enter="getList"
             />
           </a-form-item>
-          <a-form-item label="设备编号">
+          <a-form-item label="编号">
             <a-input
-              v-model:value="filterForm.inventoryNo" placeholder="请输入" style="width: 180px"
+              v-model:value="filterForm.inventoryNo" placeholder="输入编号" style="width: 150px"
               @keyup.enter="getList"
             />
           </a-form-item>
-          <a-form-item label="设备类型">
+          <a-form-item label="类型">
             <a-select
-              v-model:value="filterForm.inventoryType" placeholder="选择类型" allow-clear style="width: 180px"
-              :options="inventoryTypeList" @change="getList"
+              v-model:value="filterForm.inventoryType" placeholder="选择类型" option-filter-prop="label" show-search
+              allow-clear style="width: 150px" :options="inventoryTypeList" @change="getList"
             />
           </a-form-item>
-          <a-form-item label="仓库名称">
+          <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>
+              v-model:value="filterForm.warehouseId" :options="warehouseList" placeholder="选择仓库"
+              option-filter-prop="name" show-search allow-clear :field-names="{ label: 'name', value: 'id' }"
+              style="width: 150px" @change="getList"
+            />
           </a-form-item>
-          <a-form-item label="货位名称">
+          <a-form-item label="货位">
             <a-input
-              v-model:value="filterForm.positionName" placeholder="请输入" style="width: 180px"
+              v-model:value="filterForm.positionName" placeholder="输入货位名称" style="width: 150px"
               @keyup.enter="getList"
             />
           </a-form-item>
@@ -582,7 +579,7 @@ onMounted(() => {
 
 /* 分页包装器(固定底部) */
 .pagination-wrapper {
-  padding: 1rem 1.5rem;
+  padding: 0.5rem 1.5rem;
   border-top: 1px solid #e5e7eb;
   border-bottom: 1px solid #e5e7eb;
   background-color: #fafafa;

+ 3 - 3
src/stock-out/OutboundConfirm.vue

@@ -166,7 +166,7 @@ const handleLeave = async () => {
     const params = materialList.value.map(item => {
         return {
             stockOutPrepareLineId: item.stockOutPrepareLineId,
-            stockOutId: item.stockOutNo,
+            stockOutId: item.stockOutId,
             inventoryId: item.inventoryId,
         };
     });
@@ -205,7 +205,7 @@ const getStockOutList = async (isOne = false) => {
                         materialList.value.forEach(j => {
                             if (i.inventoryNo === j.inventoryNo) {
                                 j.status = 1;
-                                j.stockOutNo = i.stockOutNo;
+                                j.stockOutId = i.stockOutId;
                                 j.positionName = i.positionName;
                             }
                         });
@@ -320,7 +320,7 @@ onMounted(() => {
 
 /* 分页包装器(固定底部) */
 .pagination-wrapper {
-  padding: 1rem 1.5rem;
+  padding: 0.5rem 1.5rem;
   border-top: 1px solid #e5e7eb;
   border-bottom: 1px solid #e5e7eb;
   background-color: #fafafa;

+ 6 - 10
src/stock/RegularRequisition.vue

@@ -24,13 +24,9 @@
         <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>
+              v-model:value="warehouseId" :options="warehouseList" placeholder="选择仓库" option-filter-prop="name" show-search
+              allow-clear :field-names="{ label: 'name', value: 'id' }" style="width: 150px" @change="getDatas"
+            />
           </a-form-item>
           <a-form-item label="名称">
             <a-input v-model:value="inventoryName" placeholder="输入名称" style="width: 150px" @keyup.enter="getDatas" />
@@ -40,8 +36,8 @@
           </a-form-item>
           <a-form-item label="类型">
             <a-select
-              v-model:value="inventoryType" placeholder="选择类型" allow-clear :options="inventoryTypeList"
-              style="width: 150px" @change="getDatas"
+              v-model:value="inventoryType" placeholder="选择类型" option-filter-prop="label" show-search allow-clear
+              :options="inventoryTypeList" style="width: 150px" @change="getDatas"
             />
           </a-form-item>
           <a-form-item>
@@ -416,7 +412,7 @@ onMounted(() => {
 
 /* 分页包装器(固定底部) */
 .pagination-wrapper {
-  padding: 1rem 1.5rem;
+  padding: 0.5rem 1.5rem;
   border-top: 1px solid #e5e7eb;
   background-color: #fafafa;
   display: flex;

+ 6 - 9
src/stock/StockPickingCar.vue

@@ -21,13 +21,10 @@
         <a-form layout="inline" class="filter-form">
           <a-form-item label="仓库">
             <a-select
-              v-model:value="warehouseId" placeholder="选择仓库" allow-clear style="width: 150px"
+              v-model:value="warehouseId" :options="warehouseList" placeholder="选择仓库" option-filter-prop="name"
+              show-search allow-clear :field-names="{ label: 'name', value: 'id' }" 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" />
@@ -37,8 +34,8 @@
           </a-form-item>
           <a-form-item label="类型">
             <a-select
-              v-model:value="inventoryType" placeholder="选择类型" allow-clear :options="inventoryTypeList"
-              style="width: 150px" @change="getDatas"
+              v-model:value="inventoryType" placeholder="选择类型" option-filter-prop="label" show-search
+              allow-clear :options="inventoryTypeList" style="width: 150px" @change="getDatas"
             />
           </a-form-item>
           <a-form-item>
@@ -470,7 +467,7 @@ onMounted(() => {
 
 /* 分页包装器(固定底部) */
 .pagination-wrapper {
-  padding: 1rem 1.5rem;
+  padding: 0.5rem 1.5rem;
   border-top: 1px solid #e5e7eb;
   border-bottom: 1px solid #e5e7eb;
   background-color: #fafafa;

+ 6 - 9
src/stock/StockRequisition.vue

@@ -28,13 +28,10 @@
         <a-form layout="inline" class="filter-form">
           <a-form-item label="仓库">
             <a-select
-              v-model:value="warehouseId" placeholder="选择仓库" allow-clear style="width: 150px"
+              v-model:value="warehouseId" :options="warehouseList" placeholder="选择仓库" option-filter-prop="name"
+              show-search allow-clear :field-names="{ label: 'name', value: 'id' }" 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" />
@@ -44,8 +41,8 @@
           </a-form-item>
           <a-form-item label="类型">
             <a-select
-              v-model:value="inventoryType" placeholder="选择类型" allow-clear :options="inventoryTypeList"
-              style="width: 150px" @change="getDatas"
+              v-model:value="inventoryType" placeholder="选择类型" option-filter-prop="label" show-search
+              allow-clear :options="inventoryTypeList" style="width: 150px" @change="getDatas"
             />
           </a-form-item>
           <a-form-item>
@@ -420,7 +417,7 @@ onMounted(() => {
 
 /* 分页包装器(固定底部) */
 .pagination-wrapper {
-  padding: 1rem 1.5rem;
+  padding: 0.5rem 1.5rem;
   border-top: 1px solid #e5e7eb;
   background-color: #fafafa;
   display: flex;