ソースを参照

优化手动登记及登记成功页面及开发文档

yawuga 1 日 前
コミット
5e8ca06214

+ 46 - 57
src/components/NumericKeyboard.vue

@@ -1,10 +1,10 @@
 <template>
-  <div class="numeric-keyboard">
+  <div class="numeric-keyboard" :class="`numeric-keyboard-${type}`">
     <!-- 输入框显示 -->
     <div class="keyboard-input" @click="$emit('focus')">
-      <span class="input-label">{{ label }}</span>
       <div class="input-value">
-        <span class="value-text">{{ displayValue || placeholder }}</span>
+        <span v-if="modelValue" class="value-text">{{ displayFormatted }}</span>
+        <span v-else class="value-placeholder">{{ placeholder }}</span>
       </div>
     </div>
 
@@ -16,8 +16,7 @@
         class="key-btn"
         :class="{
           'key-action': isActionKey(key),
-          'key-delete': key === 'DEL',
-          'key-confirm': key === '确认'
+          'key-delete': key === 'DEL'
         }"
         @click="handleKeyClick(key)"
       >
@@ -48,17 +47,13 @@ const props = defineProps({
     type: Number,
     default: 11
   },
-  label: {
-    type: String,
-    default: '请输入'
-  },
   placeholder: {
     type: String,
     default: ''
   },
   type: {
     type: String,
-    default: 'phone' // 'phone' | 'idcard'
+    default: 'phone'
   }
 })
 
@@ -71,7 +66,7 @@ const keys = computed(() => {
   return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'DEL']
 })
 
-const displayValue = computed(() => {
+const displayFormatted = computed(() => {
   return props.modelValue
 })
 
@@ -81,23 +76,16 @@ const isActionKey = (key) => {
 
 const handleKeyClick = (key) => {
   if (key === 'DEL') {
-    // 删除
     const newValue = props.modelValue.slice(0, -1)
     emit('update:modelValue', newValue)
-  } else if (key === '确认') {
-    // 确认
-    emit('confirm', props.modelValue)
   } else {
-    // 输入
     if (props.modelValue.length < props.maxLength) {
       let newValue = props.modelValue + key
-      // X 要大写
       if (key === 'X') {
         newValue = props.modelValue + 'X'
       }
       emit('update:modelValue', newValue)
 
-      // 自动触发确认(身份证号18位、手机号11位)
       if (newValue.length === props.maxLength) {
         setTimeout(() => {
           emit('confirm', newValue)
@@ -111,20 +99,14 @@ const handleKeyClick = (key) => {
 <style scoped>
 .numeric-keyboard {
   width: 100%;
-  max-width: 480px;
-  margin: 0 auto;
 }
 
 .keyboard-input {
   display: flex;
-  flex-direction: row;
   align-items: center;
-  gap: 16px;
-  padding: 0 24px;
-  height: 108px;
-  min-height: 108px;
-  max-height: 108px;
+  width: 100%;
   box-sizing: border-box;
+  height: 108px;
   background: linear-gradient(160deg, #f0f7ff 0%, #e8f4fd 100%);
   border: 2px solid rgba(59, 130, 246, 0.18);
   border-radius: 20px;
@@ -137,24 +119,21 @@ const handleKeyClick = (key) => {
   border-color: var(--primary);
 }
 
-.input-label {
-  font-size: 30px;
-  color: var(--text-muted);
-  font-weight: 700;
-  flex-shrink: 0;
-  letter-spacing: 0.5px;
-  line-height: 1;
-}
-
 .input-value {
   display: flex;
   align-items: center;
-  flex: 1;
-  height: 100%;
-  justify-content: flex-end;
+  width: 100%;
+  min-width: 0;
+  max-width: 100%;
+  overflow: hidden;
+  box-sizing: border-box;
+  padding: 0 24px;
 }
 
 .value-text {
+  width: 100%;
+  min-width: 0;
+  max-width: 100%;
   font-size: 38px;
   font-weight: 700;
   color: var(--text-primary);
@@ -162,27 +141,48 @@ const handleKeyClick = (key) => {
   font-family: 'Courier New', monospace;
   line-height: 1;
   white-space: nowrap;
+  overflow: hidden;
+  text-overflow: clip;
+  box-sizing: border-box;
+  text-align: right;
 }
 
-.placeholder {
-  font-size: 38px;
-  font-weight: 700;
+.value-placeholder {
+  width: 100%;
+  min-width: 0;
+  max-width: 100%;
+  font-size: 28px;
+  font-weight: 600;
   color: #b0bec5;
-  letter-spacing: 2px;
+  letter-spacing: 1px;
   line-height: 1;
   white-space: nowrap;
+  overflow: hidden;
+  text-overflow: clip;
+  box-sizing: border-box;
+  text-align: right;
+}
+
+/* 身份证号输入:字号缩小防溢出 */
+.numeric-keyboard-idcard .value-text {
+  font-size: 30px;
+  letter-spacing: 2px;
+}
+
+.numeric-keyboard-idcard .value-placeholder {
+  font-size: 26px;
 }
 
 .keyboard-keys {
   display: grid;
   grid-template-columns: repeat(3, 1fr);
-  gap: 12px;
+  gap: 10px;
 }
 
 .key-btn {
-  height: 72px;
-  font-size: 28px;
-  font-weight: 500;
+  height: 76px;
+  font-size: 30px;
+  font-weight: 600;
   color: var(--text-primary);
   background: var(--bg-card);
   border: 1px solid var(--border-light);
@@ -218,15 +218,4 @@ const handleKeyClick = (key) => {
 .key-delete {
   color: var(--text-secondary);
 }
-
-.key-confirm {
-  background: var(--primary);
-  color: white;
-  border-color: var(--primary);
-}
-
-.key-confirm:hover {
-  background: var(--primary-dark);
-  color: white;
-}
 </style>

+ 11 - 0
src/stores/visitor.js

@@ -131,6 +131,17 @@ export const useVisitorStore = defineStore('visitor', () => {
     if (!validateForm()) {
       throw new Error('表单验证失败')
     }
+    // 开发环境:Mock 提交成功
+    if (import.meta.env.DEV) {
+      const now = new Date()
+      const pad = (n) => String(n).padStart(2, '0')
+      const dateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
+      registrationInfo.value = {
+        ...registrationInfo.value,
+        registerTime: dateTime
+      }
+      return registrationInfo.value
+    }
     try {
       const res = await api.submitVisitorRegistration(registrationInfo.value)
       return res

+ 293 - 116
src/views/visitor/Success.vue

@@ -1,209 +1,386 @@
 <template>
-  <ScreenLayout :show-back-btn="false">
-    <div class="page-success">
-      <div class="success-content">
-        <div class="success-icon">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
-            <polyline points="22 4 12 14.01 9 11.01" />
-          </svg>
+  <div class="success-layout">
+    <StatusBar :title="robotName" />
+
+    <div class="layout-main">
+      <div class="success-page">
+        <!-- 动态背景层 -->
+        <div class="bg-layer">
+          <div class="bg-orb orb-1"></div>
+          <div class="bg-orb orb-2"></div>
+          <div class="bg-orb orb-3"></div>
+          <div class="bg-grid-overlay"></div>
         </div>
 
-        <h1 class="success-title">登记成功</h1>
-        <p class="success-message">欢迎您的到来</p>
+        <!-- 内容区 -->
+        <div class="success-content">
+          <!-- 成功图标 -->
+          <div class="success-icon-wrap">
+            <svg class="success-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+              <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
+              <polyline points="22 4 12 14.01 9 11.01" />
+            </svg>
+          </div>
+
+          <!-- 标题 -->
+          <h1 class="success-title">登记成功</h1>
+          <p class="success-subtitle">欢迎您的到来</p>
 
-        <div class="visitor-info">
-          <div class="info-item">
-            <span class="info-label">访客姓名</span>
-            <span class="info-value">{{ visitorName }}</span>
+          <!-- 信息卡片 -->
+          <div class="info-card">
+            <div class="info-row">
+              <span class="info-label">访客姓名</span>
+              <span class="info-value">{{ visitorName }}</span>
+            </div>
+            <div class="info-row">
+              <span class="info-label">手机号码</span>
+              <span class="info-value">{{ maskedMobile }}</span>
+            </div>
+            <div class="info-row">
+              <span class="info-label">被访人</span>
+              <span class="info-value">{{ visitedPerson }}</span>
+            </div>
+            <div class="info-row">
+              <span class="info-label">登记时间</span>
+              <span class="info-value">{{ registerTime }}</span>
+            </div>
           </div>
-          <div class="info-item">
-            <span class="info-label">登记时间</span>
-            <span class="info-value">{{ currentTime }}</span>
+
+          <!-- 温馨提示 -->
+          <div class="hint-text">
+            请前往 {{ visitedPerson || '被访人' }} 处办理来访事项
+          </div>
+
+          <!-- 自动返回提示 -->
+          <div v-if="!isDev && showCountdown" class="auto-hint">
+            {{ countdown }} 秒后自动返回待机
           </div>
-        </div>
 
-        <div class="next-hint">
-          <p>请前往{{ visitedPerson || '被访人' }}处</p>
-          <p>感谢您的配合,祝您访问愉快</p>
+          <!-- 底部按钮 -->
+          <button class="btn-return" @click="goToIdle">
+            返回待机
+          </button>
         </div>
       </div>
-
-      <button class="btn-return" @click="goToIdle">
-        返回首页
-      </button>
     </div>
-  </ScreenLayout>
+  </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { useVisitorStore } from '@/stores/visitor'
+import { useScreenStore } from '@/stores/screen'
 import { formatDateTime } from '@/utils/time'
-import ScreenLayout from '@/layouts/ScreenLayout.vue'
+import StatusBar from '@/components/StatusBar.vue'
 
 const router = useRouter()
 const visitorStore = useVisitorStore()
+const screenStore = useScreenStore()
+
+const robotName = computed(() => screenStore.robotName || '智能迎宾机器人')
+const isDev = import.meta.env.DEV
+
+// 开发环境 Mock 数据
+const mockInfo = {
+  visitorName: '张三',
+  mobile: '13800000000',
+  visitedPerson: '李经理',
+  registerTime: formatDateTime(),
+  registerType: 'walk_in'
+}
+
+// 获取登记信息(优先真实数据,DEV 环境兜底 Mock)
+const info = computed(() => {
+  const real = visitorStore.registrationInfo
+  if (real?.visitorName) return real
+  if (isDev) return mockInfo
+  return { visitorName: '访客', mobile: '', visitedPerson: '--', registerTime: formatDateTime() }
+})
 
-const currentTime = ref(formatDateTime())
+const visitorName = computed(() => info.value.visitorName || '访客')
+const maskedMobile = computed(() => {
+  const mobile = info.value.mobile || ''
+  if (!mobile) return '--'
+  if (mobile.length === 11) {
+    return `${mobile.slice(0, 3)}****${mobile.slice(7)}`
+  }
+  return mobile
+})
+const visitedPerson = computed(() => info.value.visitedPerson || '--')
+const registerTime = computed(() => info.value.registerTime || info.value.registeredTime || formatDateTime())
 
-const visitorName = computed(() => visitorStore.registrationInfo.visitorName || '访客')
-const visitedPerson = computed(() => visitorStore.registrationInfo.visitedPerson || '')
+// 自动返回计时
+const countdown = ref(10)
+const showCountdown = ref(false)
+let countdownTimer = null
+let autoReturnTimer = null
 
 const goToIdle = () => {
   visitorStore.clearVisitorData()
   router.push('/idle')
 }
 
-// 更新时间
-let timer = null
+const startCountdown = () => {
+  showCountdown.value = true
+  countdown.value = 10
+  countdownTimer = setInterval(() => {
+    countdown.value--
+    if (countdown.value <= 0) {
+      clearInterval(countdownTimer)
+    }
+  }, 1000)
+}
 
 onMounted(() => {
-  timer = setInterval(() => {
-    currentTime.value = formatDateTime()
-  }, 1000)
+  // PROD 环境:10 秒后自动返回待机
+  if (!isDev) {
+    startCountdown()
+    autoReturnTimer = setTimeout(() => {
+      goToIdle()
+    }, 10000)
+  }
+})
 
-  // 3秒后自动返回待机页
-  setTimeout(() => {
-    goToIdle()
-  }, 10000)
+onUnmounted(() => {
+  if (countdownTimer) clearInterval(countdownTimer)
+  if (autoReturnTimer) clearTimeout(autoReturnTimer)
 })
 </script>
 
 <style scoped>
-.page-success {
-  width: 100%;
-  height: 100%;
+/* ===== 整体布局 ===== */
+.success-layout {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.layout-main {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  position: relative;
+}
+
+/* ===== 页面容器 ===== */
+.success-page {
+  flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #e9f8ff 0%, #dff8f5 50%, #f7fbff 100%);
-  padding: 40px;
+  padding: 0 32px 28px;
+  position: relative;
+  z-index: 1;
+  overflow-y: auto;
+}
+
+/* ===== 动态背景层 ===== */
+.bg-layer {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  pointer-events: none;
+  z-index: 0;
+}
+
+.bg-orb {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(60px);
+  opacity: 0.5;
 }
 
+.orb-1 {
+  width: 400px;
+  height: 400px;
+  background: radial-gradient(circle, rgba(59, 130, 246, 0.35) 0%, transparent 70%);
+  top: -100px;
+  right: -80px;
+  animation: float 8s ease-in-out infinite;
+}
+
+.orb-2 {
+  width: 350px;
+  height: 350px;
+  background: radial-gradient(circle, rgba(16, 185, 129, 0.28) 0%, transparent 70%);
+  bottom: -60px;
+  left: -60px;
+  animation: float 10s ease-in-out infinite reverse;
+}
+
+.orb-3 {
+  width: 280px;
+  height: 280px;
+  background: radial-gradient(circle, rgba(99, 102, 241, 0.22) 0%, transparent 70%);
+  top: 40%;
+  left: 30%;
+  animation: float 12s ease-in-out infinite;
+}
+
+@keyframes float {
+  0%, 100% { transform: translateY(0) scale(1); }
+  50% { transform: translateY(-20px) scale(1.05); }
+}
+
+.bg-grid-overlay {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(59, 130, 246, 0.04) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(59, 130, 246, 0.04) 1px, transparent 1px);
+  background-size: 48px 48px;
+}
+
+/* ===== 内容区 ===== */
 .success-content {
-  flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
-  justify-content: center;
+  width: 100%;
+  max-width: 680px;
   gap: 20px;
-  text-align: center;
-  animation: fadeInUp 0.6s ease-out;
+  animation: fadeInUp 0.5s ease-out;
 }
 
-.success-icon {
-  width: 100px;
-  height: 100px;
+@keyframes fadeInUp {
+  from { opacity: 0; transform: translateY(24px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+/* ===== 成功图标 ===== */
+.success-icon-wrap {
+  width: 130px;
+  height: 130px;
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
-  background: var(--success);
-  border-radius: 50%;
-  color: white;
-  animation: scaleIn 0.5s ease-out 0.2s backwards;
+  box-shadow: 0 16px 40px rgba(16, 185, 129, 0.30);
+  animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s backwards;
+}
+
+@keyframes scaleIn {
+  from { opacity: 0; transform: scale(0.4); }
+  to { opacity: 1; transform: scale(1); }
 }
 
-.success-icon svg {
-  width: 60px;
-  height: 60px;
+.success-icon {
+  width: 70px;
+  height: 70px;
+  color: white;
+  stroke-width: 2.5;
 }
 
+/* ===== 标题 ===== */
 .success-title {
-  font-size: 48px;
-  font-weight: 700;
+  font-size: 56px;
+  font-weight: 900;
   color: var(--text-primary);
   margin: 0;
+  letter-spacing: 3px;
+  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
 }
 
-.success-message {
-  font-size: 24px;
+.success-subtitle {
+  font-size: 26px;
   color: var(--text-secondary);
   margin: 0;
+  font-weight: 500;
+  letter-spacing: 1px;
 }
 
-.visitor-info {
-  background: var(--bg-card);
-  border-radius: var(--radius-xl);
-  padding: 24px 40px;
-  box-shadow: var(--shadow-md);
-  margin: 20px 0;
+/* ===== 信息卡片 ===== */
+.info-card {
+  width: 100%;
+  background: rgba(255, 255, 255, 0.90);
+  border-radius: 28px;
+  padding: 8px 0;
+  box-shadow:
+    0 12px 40px rgba(30, 64, 175, 0.10),
+    0 4px 12px rgba(0, 0, 0, 0.06);
+  backdrop-filter: blur(12px);
+  -webkit-backdrop-filter: blur(12px);
+  border: 1px solid rgba(255, 255, 255, 0.6);
+  margin-top: 8px;
 }
 
-.info-item {
+.info-row {
   display: flex;
-  justify-content: space-between;
-  gap: 40px;
-  padding: 12px 0;
+  align-items: center;
+  padding: 18px 36px;
+  gap: 24px;
 }
 
-.info-item:not(:last-child) {
-  border-bottom: 1px solid var(--border-light);
+.info-row + .info-row {
+  border-top: 1px solid rgba(0, 0, 0, 0.05);
 }
 
 .info-label {
-  font-size: 16px;
+  font-size: 24px;
   color: var(--text-muted);
+  font-weight: 600;
+  flex-shrink: 0;
+  min-width: 100px;
 }
 
 .info-value {
-  font-size: 18px;
-  font-weight: 500;
+  font-size: 26px;
   color: var(--text-primary);
+  font-weight: 700;
+  text-align: right;
+  flex: 1;
+  word-break: break-all;
 }
 
-.next-hint {
-  margin-top: 20px;
+/* ===== 温馨提示 ===== */
+.hint-text {
+  font-size: 22px;
+  color: var(--text-secondary);
+  font-weight: 500;
+  text-align: center;
+  margin-top: 4px;
 }
 
-.next-hint p {
+/* ===== 自动返回提示 ===== */
+.auto-hint {
   font-size: 18px;
-  color: var(--text-secondary);
-  margin: 8px 0;
+  color: var(--text-muted);
+  text-align: center;
+  margin-top: -8px;
 }
 
+/* ===== 返回按钮 ===== */
 .btn-return {
-  width: 280px;
-  height: 64px;
-  font-size: 22px;
-  font-weight: 500;
-  background: var(--bg-card);
-  color: var(--primary);
-  border: 2px solid var(--primary);
-  border-radius: var(--radius-lg);
+  width: 380px;
+  height: 80px;
+  font-size: 28px;
+  font-weight: 800;
+  letter-spacing: 2px;
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  border: none;
+  border-radius: 999px;
   cursor: pointer;
-  transition: all var(--transition-fast);
-  box-shadow: var(--shadow-md);
+  transition: all 0.22s ease;
+  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.30);
+  margin-top: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
 .btn-return:hover {
-  background: var(--primary);
-  color: white;
+  box-shadow: 0 16px 36px rgba(37, 99, 235, 0.38);
   transform: translateY(-2px);
-  box-shadow: var(--shadow-lg);
-}
-
-@keyframes fadeInUp {
-  from {
-    opacity: 0;
-    transform: translateY(30px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
 }
 
-@keyframes scaleIn {
-  from {
-    opacity: 0;
-    transform: scale(0.5);
-  }
-  to {
-    opacity: 1;
-    transform: scale(1);
-  }
+.btn-return:active {
+  transform: scale(0.97);
 }
 </style>

+ 1067 - 278
src/views/visitor/WalkIn.vue

@@ -1,172 +1,404 @@
 <template>
-  <ScreenLayout :show-back-btn="true" back-text="返回登记" back-target="/visitor">
-    <div class="page-walk-in">
-      <h1 class="page-title">现场登记</h1>
-
-      <div class="form-container">
-        <!-- 身份证读取 -->
-        <div class="idcard-section">
-          <button class="btn-idcard" @click="handleReadIdCard" :disabled="reading">
-            <div class="idcard-icon">
-              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                <rect x="2" y="4" width="20" height="16" rx="2" />
-                <line x1="6" y1="8" x2="10" y2="8" />
-                <line x1="6" y1="12" x2="18" y2="12" />
-                <line x1="6" y1="16" x2="14" y2="16" />
-              </svg>
-            </div>
-            <span v-if="reading">读取中...</span>
-            <span v-else>读取身份证</span>
-          </button>
-        </div>
+  <div class="walkin-layout">
+    <!-- 顶部状态栏 -->
+    <StatusBar :title="robotName" />
 
-        <div class="form-divider">
-          <span>或手动填写</span>
+    <!-- 主内容区 -->
+    <div class="layout-main">
+      <div class="walkin-page">
+        <!-- 动态背景层 -->
+        <div class="bg-layer">
+          <div class="bg-orb orb-1"></div>
+          <div class="bg-orb orb-2"></div>
+          <div class="bg-orb orb-3"></div>
+          <div class="bg-grid-overlay"></div>
         </div>
 
-        <!-- 表单 -->
-        <div class="form-fields">
-          <!-- 姓名 -->
-          <div class="form-field">
-            <label>访客姓名</label>
-            <input
-              type="text"
-              v-model="formData.visitorName"
-              placeholder="请输入姓名"
-              class="input"
-            />
+        <!-- 内容区 -->
+        <div class="walkin-content">
+          <!-- 标题区 -->
+          <div class="walkin-hero">
+            <h1 class="page-title">现场登记</h1>
+            <p class="page-subtitle">请填写访客信息,完成现场登记</p>
           </div>
 
-          <!-- 手机号 -->
-          <div class="form-field">
-            <label>手机号码</label>
-            <div class="input-with-keyboard" @click="showKeyboard('mobile')">
-              <span class="input-value">{{ formData.mobile || '请输入手机号' }}</span>
+          <!-- 登记信息卡片 -->
+          <div class="form-card">
+            <!-- 访客姓名 -->
+            <div class="form-row form-row-editable" @click="openTextModal('visitorName', '输入访客姓名', formData.visitorName)">
+              <span class="form-label">访客姓名</span>
+              <div class="form-value-wrapper">
+                <span v-if="formData.visitorName" class="form-value form-value-editable">{{ formData.visitorName }}</span>
+                <span v-else class="form-value form-value-placeholder">请输入访客姓名</span>
+                <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+                </svg>
+              </div>
+            </div>
+
+            <!-- 手机号码 -->
+            <div class="form-row form-row-editable" @click="openPhoneModal">
+              <span class="form-label">手机号码</span>
+              <div class="form-value-wrapper">
+                <span v-if="formData.mobile" class="form-value form-value-editable">{{ formData.mobile }}</span>
+                <span v-else class="form-value form-value-placeholder">请输入手机号</span>
+                <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+                </svg>
+              </div>
+            </div>
+
+            <!-- 身份证号 -->
+            <div class="form-row form-row-editable" @click="openIdCardModal">
+              <span class="form-label">身份证号</span>
+              <div class="form-value-wrapper">
+                <span v-if="formData.idCardNo" class="form-value form-value-editable">{{ formData.idCardNo }}</span>
+                <span v-else class="form-value form-value-placeholder">请输入身份证号</span>
+                <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+                </svg>
+              </div>
+            </div>
+
+            <!-- 来访单位 -->
+            <div class="form-row form-row-editable" @click="openTextModal('visitorCompany', '输入来访单位', formData.visitorCompany)">
+              <span class="form-label">来访单位</span>
+              <div class="form-value-wrapper">
+                <span v-if="formData.visitorCompany" class="form-value form-value-editable">{{ formData.visitorCompany }}</span>
+                <span v-else class="form-value form-value-placeholder">请输入来访单位</span>
+                <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+                </svg>
+              </div>
+            </div>
+
+            <!-- 被访人 -->
+            <div class="form-row form-row-editable" @click="openTextModal('visitedPerson', '输入被访人', formData.visitedPerson)">
+              <span class="form-label">被访人</span>
+              <div class="form-value-wrapper">
+                <span v-if="formData.visitedPerson" class="form-value form-value-editable">{{ formData.visitedPerson }}</span>
+                <span v-else class="form-value form-value-placeholder">请输入被访人</span>
+                <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+                </svg>
+              </div>
+            </div>
+
+            <!-- 来访事由 -->
+            <div class="form-row form-row-editable" @click="openReasonModal">
+              <span class="form-label">来访事由</span>
+              <div class="form-value-wrapper">
+                <span v-if="currentReasonLabel" class="form-value form-value-editable">{{ currentReasonLabel }}</span>
+                <span v-else class="form-value form-value-placeholder">请选择来访事由</span>
+                <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
+                </svg>
+              </div>
             </div>
           </div>
 
-          <!-- 身份证号 -->
-          <div class="form-field">
-            <label>身份证号</label>
-            <div class="input-with-keyboard" @click="showKeyboard('idCardNo')">
-              <span class="input-value">{{ formData.idCardNo || '请输入身份证号' }}</span>
+          <!-- 底部操作区 -->
+          <div class="confirm-actions">
+            <button class="btn-idcard" :disabled="reading" @click="handleReadIdCard">
+              <svg class="idcard-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <rect x="2" y="4" width="20" height="16" rx="2" />
+                <circle cx="8.5" cy="10" r="2.5" />
+                <path d="M2 16h5" />
+                <path d="M2 19h3" />
+                <line x1="13" y1="9" x2="20" y2="9" />
+                <line x1="13" y1="13" x2="20" y2="13" />
+                <line x1="13" y1="17" x2="17" y2="17" />
+              </svg>
+              <span v-if="reading">读取中...</span>
+              <span v-else>读取身份证</span>
+            </button>
+            <div class="primary-actions">
+              <button class="btn-back" @click="goBack">
+                <svg class="back-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
+                  <path d="M15 18l-6-6 6-6" />
+                </svg>
+                <span>返回登记</span>
+              </button>
+              <button class="btn-confirm" :disabled="submitting" @click="handleSubmit">
+                <span v-if="submitting" class="loading-spinner"></span>
+                <span v-else>提交登记</span>
+              </button>
             </div>
           </div>
+        </div>
+      </div>
+    </div>
 
-          <!-- 被访人 -->
-          <div class="form-field">
-            <label>被访人</label>
+    <!-- ===== 文本输入弹窗 ===== -->
+    <!-- 触摸屏文本输入依赖系统虚拟键盘(如 Ubuntu onboard),前端通过 focus 尽量触发键盘弹出 -->
+    <Teleport to="body">
+      <div v-if="showTextModal" class="text-modal-overlay" @click.self="closeTextModal">
+        <div class="text-modal-card">
+          <div class="modal-header">
+            <h2>{{ textModalTitle }}</h2>
+          </div>
+          <div class="text-input-wrapper">
             <input
+              ref="textInputRef"
+              v-model="tempText"
+              class="text-modal-input"
               type="text"
-              v-model="formData.visitedPerson"
-              placeholder="请输入被访人姓名"
-              class="input"
+              :placeholder="textModalPlaceholder"
+              @keydown.enter="confirmTextEdit"
             />
           </div>
-
-          <!-- 来访事由 -->
-          <div class="form-field">
-            <label>来访事由</label>
-            <div class="reason-options">
-              <button
-                v-for="option in visitReasons"
-                :key="option.value"
-                class="reason-btn"
-                :class="{ active: formData.visitReason === option.value }"
-                @click="formData.visitReason = option.value"
-              >
-                {{ option.label }}
-              </button>
-            </div>
+          <div class="text-modal-actions">
+            <button class="text-btn-cancel" @click="closeTextModal">取消</button>
+            <button class="text-btn-save" @click="confirmTextEdit">保存</button>
           </div>
         </div>
+      </div>
+    </Teleport>
 
-        <!-- 提交按钮 -->
-        <button class="btn-submit" @click="handleSubmit" :disabled="submitting">
-          <span v-if="submitting" class="loading-spinner"></span>
-          <span v-else>提交登记</span>
-        </button>
+    <!-- ===== 手机号 NumericKeyboard 弹窗 ===== -->
+    <Teleport to="body">
+      <div v-if="showPhoneModal" class="phone-modal" @click.self="closePhoneModal">
+        <div class="modal-card">
+          <div class="modal-header">
+            <h2>输入手机号</h2>
+            <p>请输入访客手机号码</p>
+          </div>
+          <NumericKeyboard
+            v-model="tempPhone"
+            :max-length="11"
+            type="phone"
+            placeholder="请输入手机号"
+          />
+          <div class="modal-actions">
+            <button class="btn-cancel" @click="closePhoneModal">取消</button>
+            <button class="btn-confirm" @click="confirmPhoneEdit">保存</button>
+          </div>
+        </div>
       </div>
+    </Teleport>
 
-      <!-- 数字键盘弹窗 -->
-      <div v-if="showKeyboardFlag" class="keyboard-modal" @click.self="hideKeyboard">
-        <div class="keyboard-content">
-          <div class="keyboard-header">
-            <span>{{ keyboardLabel }}</span>
-            <button class="keyboard-close" @click="hideKeyboard">完成</button>
+    <!-- ===== 身份证号 NumericKeyboard 弹窗 ===== -->
+    <Teleport to="body">
+      <div v-if="showIdCardModal" class="phone-modal" @click.self="closeIdCardModal">
+        <div class="modal-card">
+          <div class="modal-header">
+            <h2>输入身份证号</h2>
+            <p>请输入访客身份证号码</p>
           </div>
           <NumericKeyboard
-            v-model="currentInput"
-            :label="keyboardLabel"
-            :max-length="keyboardMaxLength"
-            :type="keyboardType"
-            @confirm="handleKeyboardConfirm"
+            v-model="tempIdCard"
+            :max-length="18"
+            type="idcard"
+            placeholder="请输入身份证号"
           />
+          <div class="modal-actions">
+            <button class="btn-cancel" @click="closeIdCardModal">取消</button>
+            <button class="btn-confirm" @click="confirmIdCardEdit">保存</button>
+          </div>
         </div>
       </div>
-    </div>
-  </ScreenLayout>
+    </Teleport>
+
+    <!-- ===== 来访事由选项弹窗 ===== -->
+    <Teleport to="body">
+      <div v-if="showReasonModal" class="reason-modal-overlay" @click.self="closeReasonModal">
+        <div class="reason-modal-card">
+          <div class="modal-header">
+            <h2>选择来访事由</h2>
+          </div>
+          <div class="reason-options">
+            <button
+              v-for="opt in visitReasonOptions"
+              :key="opt.value"
+              class="reason-btn"
+              :class="{ 'reason-btn-active': tempReason === opt.value }"
+              @click="tempReason = opt.value"
+            >
+              {{ opt.label }}
+            </button>
+          </div>
+          <div class="reason-modal-actions">
+            <button class="reason-btn-cancel" @click="closeReasonModal">取消</button>
+            <button class="reason-btn-save" @click="confirmReasonEdit">保存</button>
+          </div>
+        </div>
+      </div>
+    </Teleport>
+  </div>
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
+import StatusBar from '@/components/StatusBar.vue'
+import NumericKeyboard from '@/components/NumericKeyboard.vue'
 import { useScreenStore } from '@/stores/screen'
 import { useVisitorStore } from '@/stores/visitor'
-import ScreenLayout from '@/layouts/ScreenLayout.vue'
-import NumericKeyboard from '@/components/NumericKeyboard.vue'
 
 const router = useRouter()
 const screenStore = useScreenStore()
 const visitorStore = useVisitorStore()
 
-const reading = ref(false)
+const robotName = computed(() => screenStore.screenTheme?.robotName || '迎宾巡逻机器人')
 const submitting = ref(false)
+const reading = ref(false)
+const visitReasonOptions = visitorStore.visitReasonOptions
 
-// 键盘相关
-const showKeyboardFlag = ref(false)
-const currentInput = ref('')
-const currentField = ref('')
-const keyboardLabel = ref('')
-const keyboardMaxLength = ref(11)
-const keyboardType = ref('phone')
-
-const visitReasons = visitorStore.visitReasonOptions
-
+// 表单数据
 const formData = reactive({
   visitorName: '',
   mobile: '',
   idCardNo: '',
+  visitorCompany: '',
   visitedPerson: '',
   visitReason: ''
 })
 
-const showKeyboard = (field) => {
-  currentField.value = field
-  currentInput.value = formData[field] || ''
-
-  if (field === 'mobile') {
-    keyboardLabel.value = '手机号'
-    keyboardMaxLength.value = 11
-    keyboardType.value = 'phone'
-  } else if (field === 'idCardNo') {
-    keyboardLabel.value = '身份证号'
-    keyboardMaxLength.value = 18
-    keyboardType.value = 'idcard'
+// 来访事由标签
+const currentReasonLabel = computed(() => {
+  const found = visitReasonOptions.find(o => o.value === formData.visitReason)
+  return found ? found.label : ''
+})
+
+// ===== 导航 =====
+const goBack = () => {
+  router.push('/visitor')
+}
+
+// ===== 文本输入弹窗 =====
+const showTextModal = ref(false)
+const textModalField = ref('')
+const textModalTitle = ref('')
+const textModalPlaceholder = ref('')
+const tempText = ref('')
+const textInputRef = ref(null)
+
+const openTextModal = (field, title, currentValue) => {
+  textModalField.value = field
+  textModalTitle.value = title
+  textModalPlaceholder.value = title
+  tempText.value = currentValue
+  showTextModal.value = true
+  nextTick(() => {
+    if (textInputRef.value) {
+      textInputRef.value.focus()
+    }
+  })
+}
+
+const closeTextModal = () => {
+  showTextModal.value = false
+  textModalField.value = ''
+}
+
+const confirmTextEdit = () => {
+  if (!tempText.value.trim()) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '该项不能为空',
+      duration: 3000
+    })
+    return
+  }
+  if (textModalField.value === 'visitorName') {
+    formData.visitorName = tempText.value
+  } else if (textModalField.value === 'visitorCompany') {
+    formData.visitorCompany = tempText.value
+  } else if (textModalField.value === 'visitedPerson') {
+    formData.visitedPerson = tempText.value
+  }
+  closeTextModal()
+}
+
+// ===== 手机号弹窗 =====
+const showPhoneModal = ref(false)
+const tempPhone = ref('')
+const phoneEditError = ref(false)
+
+const openPhoneModal = () => {
+  tempPhone.value = formData.mobile
+  phoneEditError.value = false
+  showPhoneModal.value = true
+}
+
+const closePhoneModal = () => {
+  showPhoneModal.value = false
+  phoneEditError.value = false
+}
+
+const confirmPhoneEdit = () => {
+  if (!tempPhone.value || !/^1[3-9]\d{9}$/.test(tempPhone.value)) {
+    phoneEditError.value = true
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请输入正确的11位手机号',
+      duration: 3000
+    })
+    return
+  }
+  formData.mobile = tempPhone.value
+  closePhoneModal()
+}
+
+// ===== 身份证号弹窗 =====
+const showIdCardModal = ref(false)
+const tempIdCard = ref('')
+
+const openIdCardModal = () => {
+  tempIdCard.value = formData.idCardNo
+  showIdCardModal.value = true
+}
+
+const closeIdCardModal = () => {
+  showIdCardModal.value = false
+}
+
+const confirmIdCardEdit = () => {
+  if (!tempIdCard.value || tempIdCard.value.length < 15) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请输入正确的身份证号',
+      duration: 3000
+    })
+    return
   }
+  formData.idCardNo = tempIdCard.value
+  closeIdCardModal()
+}
+
+// ===== 来访事由弹窗 =====
+const showReasonModal = ref(false)
+const tempReason = ref('')
 
-  showKeyboardFlag.value = true
+const openReasonModal = () => {
+  tempReason.value = formData.visitReason
+  showReasonModal.value = true
 }
 
-const hideKeyboard = () => {
-  showKeyboardFlag.value = false
+const closeReasonModal = () => {
+  showReasonModal.value = false
 }
 
-const handleKeyboardConfirm = (value) => {
-  formData[currentField.value] = value
-  hideKeyboard()
+const confirmReasonEdit = () => {
+  if (!tempReason.value) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请选择来访事由',
+      duration: 3000
+    })
+    return
+  }
+  formData.visitReason = tempReason.value
+  closeReasonModal()
 }
 
+// ===== 身份证读取 =====
 const handleReadIdCard = async () => {
   reading.value = true
   try {
@@ -180,44 +412,82 @@ const handleReadIdCard = async () => {
   } catch {
     screenStore.showAlert({
       type: 'error',
-      message: '身份证读取失败,请手动输入'
+      message: '身份证读取失败,请手动填写'
     })
   } finally {
     reading.value = false
   }
 }
 
+// ===== 提交登记 =====
 const handleSubmit = async () => {
-  // 验证
+  if (submitting.value) return
+
   if (!formData.visitorName?.trim()) {
-    screenStore.showAlert({ type: 'warning', message: '请输入访客姓名' })
+    screenStore.showAlert({ type: 'warning', message: '请输入访客姓名', duration: 3000 })
     return
   }
   if (!formData.mobile?.trim()) {
-    screenStore.showAlert({ type: 'warning', message: '请输入手机号' })
+    screenStore.showAlert({ type: 'warning', message: '请输入手机号', duration: 3000 })
     return
   }
   if (!/^1[3-9]\d{9}$/.test(formData.mobile)) {
-    screenStore.showAlert({ type: 'warning', message: '手机号格式不正确' })
+    screenStore.showAlert({ type: 'warning', message: '请输入正确的11位手机号', duration: 3000 })
+    return
+  }
+  if (!formData.visitorCompany?.trim()) {
+    screenStore.showAlert({ type: 'warning', message: '请输入来访单位', duration: 3000 })
+    return
+  }
+  if (!formData.visitedPerson?.trim()) {
+    screenStore.showAlert({ type: 'warning', message: '请输入被访人', duration: 3000 })
+    return
+  }
+  if (!formData.visitReason) {
+    screenStore.showAlert({ type: 'warning', message: '请选择来访事由', duration: 3000 })
     return
   }
 
   submitting.value = true
   try {
-    // 更新 store
     visitorStore.setFormField('visitorName', formData.visitorName)
     visitorStore.setFormField('mobile', formData.mobile)
     visitorStore.setFormField('idCardNo', formData.idCardNo)
+    visitorStore.setFormField('visitorCompany', formData.visitorCompany)
     visitorStore.setFormField('visitedPerson', formData.visitedPerson)
     visitorStore.setFormField('visitReason', formData.visitReason)
+    visitorStore.setFormField('visitType', 'walk_in')
 
     await visitorStore.submitRegistration()
     router.push('/visitor/success')
-  } catch {
-    screenStore.showAlert({
-      type: 'error',
-      message: '登记失败,请重试'
-    })
+  } catch (error) {
+    console.error('[WalkIn] submitRegistration failed:', error)
+    if (import.meta.env.DEV) {
+      const now = new Date()
+      const pad = (n) => String(n).padStart(2, '0')
+      const dateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
+      visitorStore.registrationInfo = {
+        ...visitorStore.registrationInfo,
+        visitorName: formData.visitorName,
+        mobile: formData.mobile,
+        idCardNo: formData.idCardNo,
+        visitorCompany: formData.visitorCompany,
+        visitorSource: formData.visitorCompany,
+        visitedPerson: formData.visitedPerson,
+        visitReason: formData.visitReason,
+        visitType: 'walk_in',
+        registerTime: dateTime,
+        _mockSubmit: true
+      }
+      screenStore.showAlert({ type: 'success', message: '调试提交成功,正在跳转...', duration: 2000 })
+      setTimeout(() => router.push('/visitor/success'), 500)
+    } else {
+      screenStore.showAlert({
+        type: 'error',
+        message: '登记失败,请重试',
+        duration: 3000
+      })
+    }
   } finally {
     submitting.value = false
   }
@@ -225,274 +495,793 @@ const handleSubmit = async () => {
 </script>
 
 <style scoped>
-.page-walk-in {
-  width: 100%;
-  height: 100%;
+/* ===== 布局结构 ===== */
+.walkin-layout {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: linear-gradient(155deg, #e8f4fd 0%, #dbeafe 40%, #eff6ff 100%);
+}
+
+.layout-main {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.walkin-page {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  overflow: hidden;
+}
+
+/* ===== 动态背景层 ===== */
+.bg-layer {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  pointer-events: none;
+  z-index: 0;
+}
+
+.bg-orb {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(60px);
+  opacity: 0.45;
+}
+
+.orb-1 {
+  width: 380px;
+  height: 380px;
+  background: radial-gradient(circle, rgba(59, 130, 246, 0.35) 0%, transparent 70%);
+  top: -80px;
+  right: -80px;
+  animation: float1 18s ease-in-out infinite;
+}
+
+.orb-2 {
+  width: 300px;
+  height: 300px;
+  background: radial-gradient(circle, rgba(99, 102, 241, 0.30) 0%, transparent 70%);
+  bottom: -40px;
+  left: -60px;
+  animation: float2 22s ease-in-out infinite;
+}
+
+.orb-3 {
+  width: 240px;
+  height: 240px;
+  background: radial-gradient(circle, rgba(14, 165, 233, 0.28) 0%, transparent 70%);
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  animation: float3 16s ease-in-out infinite;
+}
+
+.bg-grid-overlay {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(147, 197, 253, 0.15) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(147, 197, 253, 0.15) 1px, transparent 1px);
+  background-size: 48px 48px;
+  mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 40%, transparent 100%);
+  -webkit-mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 40%, transparent 100%);
+}
+
+@keyframes float1 {
+  0%, 100% { transform: translate(0, 0) scale(1); }
+  33% { transform: translate(-20px, 25px) scale(1.05); }
+  66% { transform: translate(15px, -15px) scale(0.96); }
+}
+
+@keyframes float2 {
+  0%, 100% { transform: translate(0, 0) scale(1); }
+  40% { transform: translate(25px, -20px) scale(1.08); }
+  70% { transform: translate(-10px, 15px) scale(0.95); }
+}
+
+@keyframes float3 {
+  0%, 100% { transform: translate(-50%, -50%) scale(1); }
+  50% { transform: translate(-45%, -55%) scale(1.1); }
+}
+
+/* ===== 内容区 ===== */
+.walkin-content {
+  flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 20px 0;
+  padding: 0 32px 28px;
+  position: relative;
+  z-index: 1;
   overflow-y: auto;
 }
 
-.page-title {
-  font-size: 32px;
-  font-weight: 600;
-  color: var(--text-primary);
-  margin: 0 0 24px;
+/* ===== 标题区 ===== */
+.walkin-hero {
+  text-align: center;
+  margin: 16px 0 14px;
+  flex-shrink: 0;
 }
 
-.form-container {
-  width: 100%;
-  max-width: 500px;
-  padding: 0 16px;
+.page-title {
+  font-size: 44px;
+  font-weight: 900;
+  color: var(--text-primary);
+  margin: 0 0 8px;
+  letter-spacing: 3px;
 }
 
-.idcard-section {
-  margin-bottom: 24px;
+.page-subtitle {
+  font-size: 22px;
+  color: var(--text-secondary);
+  margin: 0;
+  font-weight: 500;
 }
 
 .btn-idcard {
   width: 100%;
-  height: 80px;
+  height: 72px;
+  background: rgba(255, 255, 255, 0.75);
+  color: var(--primary);
+  border: 2px solid rgba(59, 130, 246, 0.30);
+  border-radius: 999px;
+  font-size: 24px;
+  font-weight: 700;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  letter-spacing: 0.5px;
   display: flex;
   align-items: center;
   justify-content: center;
-  gap: 16px;
-  background: var(--primary-soft);
-  color: var(--primary);
-  border: 2px dashed var(--primary);
-  border-radius: var(--radius-lg);
-  font-size: 20px;
-  font-weight: 500;
-  cursor: pointer;
-  transition: all var(--transition-fast);
+  gap: 12px;
 }
 
 .btn-idcard:hover:not(:disabled) {
-  background: var(--primary);
-  color: white;
-  border-style: solid;
+  background: rgba(255, 255, 255, 0.92);
+  border-color: rgba(59, 130, 246, 0.45);
+  transform: translateY(-1px);
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
+}
+
+.btn-idcard:active {
+  transform: scale(0.97);
 }
 
 .btn-idcard:disabled {
-  opacity: 0.6;
+  opacity: 0.5;
   cursor: not-allowed;
 }
 
 .idcard-icon {
-  width: 32px;
-  height: 32px;
+  width: 26px;
+  height: 26px;
+  flex-shrink: 0;
 }
 
-.idcard-icon svg {
+/* ===== 登记信息卡片 ===== */
+.form-card {
   width: 100%;
-  height: 100%;
+  max-width: 700px;
+  background: rgba(255, 255, 255, 0.88);
+  border-radius: 32px;
+  padding: 6px 0;
+  box-shadow:
+    0 24px 64px rgba(30, 64, 175, 0.10),
+    0 8px 24px rgba(0, 0, 0, 0.06);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+  border: 1px solid rgba(255, 255, 255, 0.60);
+  margin-bottom: 18px;
 }
 
-.form-divider {
+.form-row {
   display: flex;
   align-items: center;
-  gap: 16px;
-  margin: 24px 0;
+  padding: 0 32px;
+  min-height: 86px;
+  border-bottom: 1px solid rgba(226, 232, 240, 0.80);
+}
+
+.form-row:last-child {
+  border-bottom: none;
+}
+
+.form-row-editable {
+  cursor: pointer;
+  transition: background 0.15s;
 }
 
-.form-divider::before,
-.form-divider::after {
-  content: '';
+.form-row-editable:hover {
+  background: rgba(59, 130, 246, 0.04);
+}
+
+.form-row-editable:active {
+  background: rgba(59, 130, 246, 0.08);
+}
+
+.form-label {
+  font-size: 25px;
+  font-weight: 600;
+  color: var(--text-secondary);
+  width: 150px;
+  flex-shrink: 0;
+  letter-spacing: 0.5px;
+}
+
+.form-value-wrapper {
   flex: 1;
-  height: 1px;
-  background: var(--border-light);
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+.form-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: var(--text-primary);
+  letter-spacing: 1px;
+}
+
+.form-value-editable {
+  color: var(--primary);
+}
+
+.form-value-placeholder {
+  color: #94a3b8;
+  font-weight: 500;
+  font-size: 25px;
+}
+
+.edit-icon {
+  width: 26px;
+  height: 26px;
+  color: var(--primary);
+  flex-shrink: 0;
+  opacity: 0.65;
+  transition: opacity 0.15s;
 }
 
-.form-divider span {
-  font-size: 14px;
-  color: var(--text-muted);
+.form-row-editable:hover .edit-icon {
+  opacity: 0.95;
 }
 
-.form-fields {
+.edit-icon {
+  width: 22px;
+  height: 22px;
+  color: var(--primary);
+  flex-shrink: 0;
+  opacity: 0.7;
+  transition: opacity 0.15s;
+}
+
+.form-row-editable:hover .edit-icon {
+  opacity: 1;
+}
+
+/* ===== 底部操作区 ===== */
+.confirm-actions {
   display: flex;
   flex-direction: column;
-  gap: 20px;
-  margin-bottom: 32px;
+  align-items: center;
+  gap: 14px;
+  width: 100%;
+  max-width: 700px;
+  flex-shrink: 0;
+}
+
+.primary-actions {
+  display: grid;
+  grid-template-columns: 1fr 2.2fr;
+  gap: 14px;
+  width: 100%;
+  align-items: stretch;
 }
 
-.form-field {
+.btn-back,
+.btn-confirm {
+  height: 82px;
+  min-height: 82px;
+  font-size: 26px;
+  font-weight: 800;
+  border-radius: 999px;
+  cursor: pointer;
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
   display: flex;
-  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  box-sizing: border-box;
+  line-height: 1;
+}
+
+.btn-back span,
+.btn-confirm span {
+  line-height: 1;
+}
+
+.btn-back {
+  background: rgba(255, 255, 255, 0.80);
+  color: var(--text-secondary);
+  border: 2px solid var(--border-light);
+  backdrop-filter: blur(10px);
+  -webkit-backdrop-filter: blur(10px);
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
   gap: 8px;
 }
 
-.form-field label {
-  font-size: 16px;
+.btn-back:hover {
+  background: rgba(255, 255, 255, 0.95);
+  border-color: var(--text-muted);
+  transform: translateY(-2px);
+}
+
+.btn-back:active {
+  transform: scale(0.97);
+}
+
+.back-icon {
+  width: 22px;
+  height: 22px;
+  flex-shrink: 0;
+}
+
+.btn-confirm {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  border: 2px solid transparent;
+  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
+}
+
+.btn-confirm:hover:not(:disabled) {
+  box-shadow: 0 16px 36px rgba(37, 99, 235, 0.36);
+  transform: translateY(-2px);
+}
+
+.btn-confirm:active {
+  transform: scale(0.97);
+}
+
+.btn-confirm:disabled {
+  background: linear-gradient(135deg, #93c5fd 0%, #60a5fa 100%);
+  cursor: not-allowed;
+  box-shadow: none;
+  transform: none;
+}
+
+.loading-spinner {
+  width: 26px;
+  height: 26px;
+  border: 3px solid rgba(255, 255, 255, 0.35);
+  border-top-color: white;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+/* ===== 手机号/身份证号弹窗 ===== */
+.phone-modal {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.48);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 200;
+  animation: fadeIn 0.2s ease-out;
+  backdrop-filter: blur(6px);
+  -webkit-backdrop-filter: blur(6px);
+}
+
+.phone-modal .modal-card {
+  width: 800px;
+  max-width: calc(100vw - 48px);
+  max-height: calc(100vh - 48px);
+  overflow-y: auto;
+  padding: 44px 56px 40px;
+  background: rgba(255, 255, 255, 0.97);
+  border-radius: 40px;
+  box-shadow:
+    0 40px 100px rgba(30, 64, 175, 0.22),
+    0 4px 16px rgba(0, 0, 0, 0.08);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  scrollbar-width: thin;
+}
+
+.phone-modal .modal-header {
+  text-align: center;
+  margin-bottom: 28px;
+  flex-shrink: 0;
+  width: 100%;
+}
+
+.phone-modal .modal-header h2 {
+  font-size: 36px;
+  font-weight: 900;
+  color: var(--text-primary);
+  margin: 0 0 8px;
+  letter-spacing: 2px;
+}
+
+.phone-modal .modal-header p {
+  font-size: 20px;
   color: var(--text-secondary);
+  margin: 0;
+  font-weight: 500;
 }
 
-.input {
+/* 操作按钮 */
+.modal-actions {
+  display: grid;
+  grid-template-columns: 1fr 1.5fr;
+  gap: 16px;
   width: 100%;
-  height: 56px;
-  padding: 0 16px;
-  font-size: 18px;
-  background: var(--bg-card);
-  border: 2px solid var(--border-light);
-  border-radius: var(--radius-lg);
-  transition: border-color var(--transition-fast);
+  margin-top: 24px;
+  flex-shrink: 0;
+}
+
+.btn-cancel,
+.btn-confirm {
+  height: 74px;
+  font-size: 26px;
+  font-weight: 800;
+  border-radius: 999px;
+  cursor: pointer;
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
+}
+
+.btn-cancel {
+  background: rgba(107, 114, 128, 0.10);
+  color: #4b5563;
+  border: 1px solid rgba(107, 114, 128, 0.15);
 }
 
-.input:focus {
-  border-color: var(--primary);
+.btn-cancel:hover {
+  background: rgba(107, 114, 128, 0.16);
 }
 
-.input::placeholder {
-  color: var(--text-light);
+.btn-cancel:active {
+  transform: scale(0.97);
 }
 
-.input-with-keyboard {
-  height: 56px;
-  padding: 0 16px;
+.btn-confirm {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  border: none;
+  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
   display: flex;
   align-items: center;
-  background: var(--bg-card);
-  border: 2px solid var(--border-light);
-  border-radius: var(--radius-lg);
-  cursor: pointer;
-  transition: border-color var(--transition-fast);
+  justify-content: center;
 }
 
-.input-with-keyboard:hover {
-  border-color: var(--primary);
+.btn-confirm:hover {
+  box-shadow: 0 16px 36px rgba(37, 99, 235, 0.36);
+  transform: translateY(-1px);
 }
 
-.input-value {
-  font-size: 18px;
-  color: var(--text-light);
+.btn-confirm:active {
+  transform: scale(0.97);
 }
 
-.input-with-keyboard:has(.input-value:not(:empty)) .input-value {
-  color: var(--text-primary);
+/* ===== 来访事由选项弹窗 ===== */
+.reason-modal-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.50);
+  backdrop-filter: blur(4px);
+  -webkit-backdrop-filter: blur(4px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  animation: fadeIn 0.2s ease;
+}
+
+.reason-modal-card {
+  width: 560px;
+  background: rgba(255, 255, 255, 0.96);
+  border-radius: 32px;
+  padding: 40px 36px 36px;
+  box-shadow:
+    0 40px 80px rgba(0, 0, 0, 0.20),
+    0 16px 40px rgba(0, 0, 0, 0.12);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+  animation: slideUp 0.22s ease;
 }
 
 .reason-options {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 10px;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 14px;
+  margin-bottom: 20px;
 }
 
 .reason-btn {
-  padding: 10px 18px;
-  font-size: 15px;
-  background: var(--bg-card);
+  height: 72px;
+  font-size: 22px;
+  font-weight: 700;
+  color: var(--text-primary);
+  background: rgba(255, 255, 255, 0.80);
   border: 2px solid var(--border-light);
-  border-radius: var(--radius-full);
-  color: var(--text-secondary);
+  border-radius: 20px;
   cursor: pointer;
-  transition: all var(--transition-fast);
+  transition: all 0.18s ease;
+  letter-spacing: 0.5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
 .reason-btn:hover {
-  border-color: var(--primary);
-  color: var(--primary);
+  background: rgba(59, 130, 246, 0.06);
+  border-color: rgba(59, 130, 246, 0.30);
+  transform: translateY(-1px);
 }
 
-.reason-btn.active {
-  background: var(--primary);
-  border-color: var(--primary);
-  color: white;
+.reason-btn:active {
+  transform: scale(0.97);
 }
 
-.btn-submit {
-  width: 100%;
-  height: 64px;
-  font-size: 22px;
-  font-weight: 600;
-  background: var(--primary);
+.reason-btn-active {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
   color: white;
-  border: none;
-  border-radius: var(--radius-lg);
+  border-color: transparent;
+  box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
+}
+
+.reason-btn-active:hover {
+  background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
+  border-color: transparent;
+  box-shadow: 0 10px 24px rgba(37, 99, 235, 0.30);
+}
+
+.reason-modal-actions {
+  display: grid;
+  grid-template-columns: 1fr 1.5fr;
+  gap: 14px;
+}
+
+.reason-btn-cancel,
+.reason-btn-save {
+  height: 72px;
+  font-size: 24px;
+  font-weight: 800;
+  border-radius: 999px;
   cursor: pointer;
-  box-shadow: var(--shadow-md);
-  transition: all var(--transition-fast);
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
   display: flex;
   align-items: center;
   justify-content: center;
 }
 
-.btn-submit:hover:not(:disabled) {
-  background: var(--primary-dark);
-  transform: translateY(-2px);
-  box-shadow: var(--shadow-lg);
+.reason-btn-cancel {
+  background: rgba(255, 255, 255, 0.80);
+  color: var(--text-secondary);
+  border: 2px solid var(--border-light);
+  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
 }
 
-.btn-submit:disabled {
-  opacity: 0.7;
-  cursor: not-allowed;
+.reason-btn-cancel:hover {
+  background: rgba(255, 255, 255, 0.95);
+  border-color: var(--text-muted);
+  transform: translateY(-1px);
 }
 
-.loading-spinner {
-  width: 24px;
-  height: 24px;
-  border: 3px solid rgba(255, 255, 255, 0.3);
-  border-top-color: white;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
+.reason-btn-save {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  border: none;
+  box-shadow: 0 10px 24px rgba(37, 99, 235, 0.26);
 }
 
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
-  }
+.reason-btn-save:hover {
+  box-shadow: 0 14px 32px rgba(37, 99, 235, 0.34);
+  transform: translateY(-1px);
 }
 
-/* 键盘弹窗 */
-.keyboard-modal {
+.reason-btn-cancel:active,
+.reason-btn-save:active {
+  transform: scale(0.97);
+}
+
+/* ===== 文本输入弹窗 ===== */
+.text-modal-overlay {
   position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
+  inset: 0;
+  background: rgba(0, 0, 0, 0.50);
+  backdrop-filter: blur(4px);
+  -webkit-backdrop-filter: blur(4px);
   display: flex;
-  align-items: flex-end;
-  z-index: 100;
-  animation: fadeIn 0.2s ease-out;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  animation: fadeIn 0.2s ease;
 }
 
-.keyboard-content {
-  width: 100%;
-  background: var(--bg-card);
-  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
-  padding: 20px;
-  padding-bottom: 40px;
+.text-modal-card {
+  width: 560px;
+  background: rgba(255, 255, 255, 0.96);
+  border-radius: 32px;
+  padding: 40px 36px 36px;
+  box-shadow:
+    0 40px 80px rgba(0, 0, 0, 0.20),
+    0 16px 40px rgba(0, 0, 0, 0.12);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+  animation: slideUp 0.22s ease;
 }
 
-.keyboard-header {
+.text-input-wrapper {
+  background: linear-gradient(160deg, #f0f7ff 0%, #e8f4fd 100%);
+  border: 2px solid rgba(59, 130, 246, 0.18);
+  border-radius: 20px;
+  padding: 0 24px;
+  height: 96px;
   display: flex;
-  justify-content: space-between;
   align-items: center;
   margin-bottom: 16px;
 }
 
-.keyboard-header span {
-  font-size: 18px;
-  font-weight: 500;
+.text-modal-input {
+  width: 100%;
+  height: 100%;
+  font-size: 30px;
+  font-weight: 700;
   color: var(--text-primary);
+  background: transparent;
+  border: none;
+  outline: none;
+  letter-spacing: 1px;
 }
 
-.keyboard-close {
-  padding: 8px 20px;
-  font-size: 16px;
-  color: var(--primary);
-  background: none;
-  border: none;
+.text-modal-input::placeholder {
+  font-size: 26px;
+  font-weight: 500;
+  color: #b0bec5;
+}
+
+.text-modal-actions {
+  display: grid;
+  grid-template-columns: 1fr 1.5fr;
+  gap: 14px;
+}
+
+.text-btn-cancel,
+.text-btn-save {
+  height: 72px;
+  font-size: 24px;
+  font-weight: 800;
+  border-radius: 999px;
   cursor: pointer;
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.text-btn-cancel {
+  background: rgba(255, 255, 255, 0.80);
+  color: var(--text-secondary);
+  border: 2px solid var(--border-light);
+  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
+}
+
+.text-btn-cancel:hover {
+  background: rgba(255, 255, 255, 0.95);
+  border-color: var(--text-muted);
+  transform: translateY(-1px);
 }
 
-@keyframes fadeIn {
-  from {
-    opacity: 0;
+.text-btn-save {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  border: none;
+  box-shadow: 0 10px 24px rgba(37, 99, 235, 0.26);
+}
+
+.text-btn-save:hover {
+  box-shadow: 0 14px 32px rgba(37, 99, 235, 0.34);
+  transform: translateY(-1px);
+}
+
+.text-btn-cancel:active,
+.text-btn-save:active {
+  transform: scale(0.97);
+}
+
+/* ===== 响应式适配 ===== */
+@media (max-height: 700px) {
+  .walkin-content {
+    padding: 0 24px 16px;
+  }
+
+  .walkin-hero {
+    margin: 12px 0 12px;
+  }
+
+  .page-title {
+    font-size: 32px;
+  }
+
+  .page-subtitle {
+    font-size: 18px;
   }
-  to {
-    opacity: 1;
+
+  .form-row {
+    min-height: 68px;
+  }
+
+  .form-label {
+    font-size: 20px;
+    width: 110px;
+  }
+
+  .form-value,
+  .form-value-editable {
+    font-size: 22px;
+  }
+
+  .form-value-placeholder {
+    font-size: 20px;
+  }
+
+  .form-card {
+    max-width: 700px;
+  }
+
+  .edit-icon {
+    width: 24px;
+    height: 24px;
+  }
+
+  .confirm-actions {
+    max-width: 700px;
+    gap: 12px;
+  }
+
+  .primary-actions {
+    grid-template-columns: 1fr 2fr;
+  }
+
+  .btn-back,
+  .btn-confirm {
+    height: 70px;
+    min-height: 70px;
+    font-size: 22px;
+    box-sizing: border-box;
+  }
+
+  .btn-confirm {
+    border: 2px solid transparent;
+  }
+
+  .btn-idcard {
+    height: 62px;
+    font-size: 22px;
+  }
+
+  .idcard-icon {
+    width: 24px;
+    height: 24px;
   }
 }
 </style>

+ 45 - 30
迎宾巡逻安防机器人机身屏交互系统详细设计开发文档(一期).html

@@ -201,13 +201,14 @@ audio ended 或 audio error
 选择身份证读取或手动填写
-填写/回填访客姓名、手机号、身份证号、被访人、来访事由
+填写/回填访客姓名、手机号、身份证号、来访单位、被访人、来访事由
 提交登记前确认
 提交成功
 显示登记成功页</div>
+    <div class="note">现场登记页字段顺序:访客姓名 → 手机号码 → 身份证号 → 来访单位 → 被访人 → 来访事由。被访部门不在机身屏现场登记页展示。</div>
     <h3>6.5 人脸识别结果进入流程</h3>
     <div class="flow">机器人侧完成人脸识别
@@ -275,10 +276,37 @@ audio ended 或 audio error
       <tr><td>访客登记首页</td><td>展示预约到访、现场登记两个入口。</td></tr>
       <tr><td>预约到访</td><td>支持身份证读取查询、手机号查询;人脸识别命中预约用户时可直接进入预约确认页。</td></tr>
       <tr><td>现场登记</td><td>支持身份证读取自动填充和手动填写。</td></tr>
-      <tr><td>登记字段</td><td>访客姓名、手机号、身份证号、到访类型、登记方式、访客来源、来访事由、被访对象、预约单号、来访时间、访客照片。</td></tr>
-      <tr><td>输入方式</td><td>手机号、身份证号使用前端内置数字键盘;姓名、被访人、事由使用输入框、预置选项或系统软键盘。</td></tr>
+      <tr><td>登记字段</td><td>访客姓名、手机号、身份证号、来访单位、被访人、来访事由。</td></tr>
+      <tr><td>输入方式</td><td>手机号、身份证号使用前端内置数字键盘;访客姓名、来访单位、被访人使用文本输入弹窗(依赖系统软键盘);来访事由使用枚举选项弹窗。</td></tr>
       <tr><td>超时规则</td><td>登记页面长时间无操作后清空敏感信息并返回待机。</td></tr>
     </tbody></table>
+    <h4>7.3.1 现场登记页字段说明</h4>
+    <table><thead><tr><th>字段</th><th>类型</th><th>说明</th></tr></thead><tbody>
+      <tr><td>访客姓名</td><td>文本输入</td><td>依赖系统软键盘,不做枚举。</td></tr>
+      <tr><td>手机号码</td><td>数字输入</td><td>前端 NumericKeyboard,11 位,格式校验。</td></tr>
+      <tr><td>身份证号</td><td>数字输入</td><td>前端 NumericKeyboard,支持末位 X,选填。</td></tr>
+      <tr><td>来访单位</td><td>文本输入</td><td>依赖系统软键盘,不做枚举选择,用于填写访客来自的公司、单位、团队、施工队或客户机构等。</td></tr>
+      <tr><td>被访人</td><td>文本输入</td><td>依赖系统软键盘,不做枚举。被访部门不在机身屏现场登记页展示。</td></tr>
+      <tr><td>来访事由</td><td>枚举选择</td><td>枚举选项弹窗,不做自由文本输入。</td></tr>
+    </tbody></table>
+    <h4>7.3.2 预约确认页字段规则</h4>
+    <table><thead><tr><th>字段</th><th>是否可编辑</th><th>说明</th></tr></thead><tbody>
+      <tr><td>预约单号</td><td>不可修改</td><td>系统自动生成。</td></tr>
+      <tr><td>访客姓名</td><td>不可修改</td><td>来源于预约数据。</td></tr>
+      <tr><td>身份证号</td><td>不可修改</td><td>来源于预约数据或读取。</td></tr>
+      <tr><td>手机号码</td><td>不可修改</td><td>来源于预约数据。</td></tr>
+      <tr><td>被访人</td><td>可修改</td><td>通过蓝色标记和编辑图标提示可点击编辑。被访部门不在预约确认页展示。</td></tr>
+      <tr><td>预约时间</td><td>不可修改</td><td>来源于预约数据。</td></tr>
+      <tr><td>来访事由</td><td>可修改</td><td>通过蓝色标记和编辑图标提示可点击编辑,枚举选项弹窗。</td></tr>
+    </tbody></table>
+    <h4>7.3.3 登记成功页字段规则</h4>
+    <table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody>
+      <tr><td>访客姓名</td><td>展示登记的访客姓名。</td></tr>
+      <tr><td>手机号码</td><td>展示登记的手机号,脱敏显示,如 138****0000。不展示完整号码。</td></tr>
+      <tr><td>被访人</td><td>展示登记的被访人。</td></tr>
+      <tr><td>登记时间</td><td>展示提交登记的时间。</td></tr>
+    </tbody></table>
+    <div class="note">登记成功页不展示身份证号,不展示来访单位(来访单位为后台登记字段,不在成功结果页展示)。成功页展示"登记成功"标题、欢迎语和后续指引文案,提供"返回待机"按钮。生产环境可 10 秒后自动返回待机页;开发环境不自动跳转,方便调试。</div>
     <h3>7.4 路线引导页面</h3>
     <table><thead><tr><th>模块</th><th>详细设计</th></tr></thead><tbody>
       <tr><td>页面目标</td><td>让访客选择目的地,展示机器人带路或路线引导流程。</td></tr>
@@ -367,11 +395,11 @@ audio ended 或 audio error
     <h3>10.1 输入策略</h3>
     <table><thead><tr><th>输入内容</th><th>推荐方式</th><th>说明</th></tr></thead><tbody>
       <tr><td>手机号</td><td>前端内置数字键盘</td><td>11 位手机号输入,支持清空、删除、确认。</td></tr>
-      <tr><td>身份证号</td><td>前端内置数字键盘 + X</td><td>支持 18 位身份证号和末位 X。</td></tr>
-      <tr><td>预约查询</td><td>前端内置数字键盘</td><td>优先支持手机号或身份证号查询。</td></tr>
-      <tr><td>访客姓名</td><td>系统软键盘/普通输入框</td><td>一期可依赖 Ubuntu 软键盘,后续优化中文输入。</td></tr>
-      <tr><td>被访对象</td><td>搜索选择/手动输入</td><td>后续如有被访人列表接口,优先做搜索选择。</td></tr>
-      <tr><td>来访事由</td><td>预置选项 + 其他输入</td><td>减少中文自由输入,提高触摸屏体验。</td></tr>
+      <tr><td>身份证号</td><td>前端内置数字键盘 + X</td><td>支持 18 位身份证号和末位 X,可选填。</td></tr>
+      <tr><td>访客姓名</td><td>文本输入弹窗 + 系统软键盘</td><td>点击字段弹出文本输入弹窗,依赖 Ubuntu 系统软键盘。</td></tr>
+      <tr><td>来访单位</td><td>文本输入弹窗 + 系统软键盘</td><td>文本输入弹窗,依赖系统软键盘,不做枚举选择。</td></tr>
+      <tr><td>被访人</td><td>文本输入弹窗 + 系统软键盘</td><td>文本输入弹窗,依赖系统软键盘。被访部门不在机身屏现场登记页展示。</td></tr>
+      <tr><td>来访事由</td><td>枚举选项弹窗</td><td>预置选项弹窗(商务洽谈、面试应聘、快递/外卖、设备维修、探亲访友、咨询业务、其他事宜),不做自由文本输入。</td></tr>
     </tbody></table>
     <h3>10.2 防误触与超时规则</h3>
     <ul><li>长时间无操作自动返回待机页,建议默认 60 秒,可配置。</li><li>访客登记、身份证读取、手机号查询等页面超时后自动清空敏感信息。</li><li>退出登记流程时需弹窗确认,避免误触导致信息丢失。</li><li>提交登记前展示确认页,避免误提交。</li><li>重启、关机、开始巡逻、停止巡逻等高风险动作不在普通屏幕端开放。</li></ul>
@@ -448,6 +476,14 @@ audio ended 或 audio error
 └─ App.vue</div>
     <h3>12.4 Mock 数据范围</h3>
     <div><span class="tag">机器人状态</span><span class="tag">播放方案</span><span class="tag">素材列表</span><span class="tag">播报内容</span><span class="tag">语音指令</span><span class="tag">预约记录</span><span class="tag">身份证读取结果</span><span class="tag">人脸识别结果</span><span class="tag">目的地列表</span><span class="tag">通知公告</span></div>
+    <h4>12.5 访客登记 Mock 开发说明</h4>
+    <ul>
+      <li>开发环境允许现场登记提交失败时使用 Mock 成功数据跳转 <code class="inline">/visitor/success</code>,便于前端调试登记成功页。</li>
+      <li>正式生产环境不启用 Mock 提交兜底,接口失败时正常提示"登记失败,请重试"。</li>
+      <li>登记成功页(<code class="inline">Success.vue</code>)支持开发环境无 <code class="inline">registrationInfo</code> 时使用 Mock 数据展示,Mock 数据包含访客姓名、手机号、被访人、登记时间。</li>
+      <li>预约确认页(<code class="inline">AppointmentConfirm.vue</code>)可在开发环境通过 <code class="inline">setMockAppointment()</code> 方法注入 Mock 预约数据进行调试。</li>
+      <li>开发阶段所有 Mock 数据仅在前端内存中,不持久化,不上报真实后端接口。</li>
+    </ul>
     <h4>本地播放素材 Mock 约定</h4>
     <ul>
       <li>开发阶段播放方案素材放置于 <code class="inline">src/assets/media/play-plan/images/</code> 与 <code class="inline">src/assets/media/play-plan/videos/</code> 目录。</li>
@@ -469,7 +505,7 @@ audio ended 或 audio error
       <tr><td>/screen/command/ack</td><td>POST</td><td>指令处理回执</td><td>commandId、resultStatus、resultMsg</td></tr>
       <tr><td>/screen/id-card/read</td><td>POST</td><td>读取身份证</td><td>name、idCardNo、gender、nation、address、photoUrl</td></tr>
       <tr><td>/screen/appointment/query</td><td>GET</td><td>预约查询</td><td>mobile、idCardNo;返回 appointmentNo、visitorName、visitedPerson、appointmentTime</td></tr>
-      <tr><td>/screen/visitor/register</td><td>POST</td><td>提交访客登记</td><td>visitorName、mobile、idCardNo、visitType、registerType、visitorSource、visitReason、visitedPerson、appointmentNo</td></tr>
+      <tr><td>/screen/visitor/register</td><td>POST</td><td>提交访客登记</td><td>visitorName、mobile、idCardNo、visitorCompany(或 visitorSource 承接"来访单位")、visitedPerson、visitReason、visitType、registerType、appointmentNo</td></tr>
       <tr><td>/screen/recognition/latest</td><td>GET</td><td>获取最新识别结果</td><td>personType、matchStatus、visitorName、appointmentNo、confidence</td></tr>
       <tr><td>/screen/destination/list</td><td>GET</td><td>目的地列表</td><td>destinationId、name、category、floor、description</td></tr>
       <tr><td>/screen/navigation/start</td><td>POST</td><td>发起导航</td><td>destinationId</td></tr>
@@ -834,24 +870,3 @@ audio ended 或 audio error
 </div>
 </body>
 </html>
-
-    <h3>12.5 Cursor 实现提示(前端:播报插播/Idle 播放器)</h3>
-    <div class="info">
-      <strong>Cursor 实现提示:</strong><br>
-      <ul>
-        <li>本阶段需实现 <strong>播报插播功能</strong> 的前端 Cursor/Vue 组件。</li>
-        <li>参考 <code>BroadcastOverlay.vue</code> 组件拆分建议,实现如下要点:</li>
-        <ul>
-          <li>每 2 秒轮询 <code>/robot-ops/screen/broadcast/current</code>,判断是否有播报任务。</li>
-          <li>当 <code>broadcasting=true</code> 且 <code>audioUrl</code> 不为空时,显示播报浮层,暂停 Idle 播放器,播放 MP3。</li>
-          <li>监听 audio 播放结束,自动隐藏浮层并恢复 Idle 播放。</li>
-          <li>如有播放失败、audioUrl 为空等情况,需记录 warning 日志,并恢复 Idle 播放。</li>
-          <li>仅在 <code>/idle</code> 待机页生效,业务办理页不强制打断。</li>
-          <li>支持 <code>BroadcastOverlay.vue</code> 作为全局蒙层组件,动画渐入/渐出,展示播报标题、正文、状态。</li>
-        </ul>
-        <li>接口示例、字段说明与前端处理规则详见本章节 <code>13.2 当前播报状态接口详细设计</code>。</li>
-        <li>如需 Idle 播放器与播报插播协同,请确保 Idle 播放器(<code>IdlePlayer.vue</code>)支持暂停/恢复控制。</li>
-        <li>所有状态由 Pinia 管理,避免多处状态冲突。</li>
-      </ul>
-      <strong>实现目标:</strong> 实现播报插播浮层组件(BroadcastOverlay.vue),并集成到 Idle 播放器页面,确保播报流程、暂停/恢复、异常兜底、动画效果等完整体验。
-    </div>