ソースを参照

优化预约登记相关页面

yawuga 4 日 前
コミット
45dad57fe2

+ 33 - 44
src/components/NumericKeyboard.vue

@@ -5,7 +5,6 @@
       <span class="input-label">{{ label }}</span>
       <div class="input-value">
         <span class="value-text">{{ displayValue || placeholder }}</span>
-        <span v-if="showCursor" class="cursor">|</span>
       </div>
     </div>
 
@@ -38,7 +37,7 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
+import { computed } from 'vue'
 
 const props = defineProps({
   modelValue: {
@@ -76,23 +75,6 @@ const displayValue = computed(() => {
   return props.modelValue
 })
 
-const showCursor = ref(true)
-
-// 光标闪烁
-let cursorTimer = null
-
-onMounted(() => {
-  cursorTimer = setInterval(() => {
-    showCursor.value = !showCursor.value
-  }, 500)
-})
-
-onUnmounted(() => {
-  if (cursorTimer) {
-    clearInterval(cursorTimer)
-  }
-})
-
 const isActionKey = (key) => {
   return ['DEL'].includes(key)
 }
@@ -129,18 +111,23 @@ const handleKeyClick = (key) => {
 <style scoped>
 .numeric-keyboard {
   width: 100%;
-  max-width: 400px;
+  max-width: 480px;
   margin: 0 auto;
 }
 
 .keyboard-input {
   display: flex;
-  flex-direction: column;
-  gap: 8px;
-  padding: 16px 20px;
-  background: var(--bg-card);
-  border: 2px solid var(--border-light);
-  border-radius: var(--radius-lg);
+  flex-direction: row;
+  align-items: center;
+  gap: 16px;
+  padding: 0 24px;
+  height: 108px;
+  min-height: 108px;
+  max-height: 108px;
+  box-sizing: border-box;
+  background: linear-gradient(160deg, #f0f7ff 0%, #e8f4fd 100%);
+  border: 2px solid rgba(59, 130, 246, 0.18);
+  border-radius: 20px;
   margin-bottom: 20px;
   cursor: pointer;
   transition: border-color var(--transition-fast);
@@ -151,37 +138,39 @@ const handleKeyClick = (key) => {
 }
 
 .input-label {
-  font-size: 14px;
+  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;
-  min-height: 40px;
+  flex: 1;
+  height: 100%;
+  justify-content: flex-end;
 }
 
 .value-text {
-  font-size: 28px;
-  font-weight: 500;
+  font-size: 38px;
+  font-weight: 700;
   color: var(--text-primary);
-  letter-spacing: 2px;
+  letter-spacing: 4px;
+  font-family: 'Courier New', monospace;
+  line-height: 1;
+  white-space: nowrap;
 }
 
 .placeholder {
-  color: var(--text-light);
-}
-
-.cursor {
-  font-size: 28px;
-  color: var(--primary);
-  animation: blink 1s step-end infinite;
-}
-
-@keyframes blink {
-  50% {
-    opacity: 0;
-  }
+  font-size: 38px;
+  font-weight: 700;
+  color: #b0bec5;
+  letter-spacing: 2px;
+  line-height: 1;
+  white-space: nowrap;
 }
 
 .keyboard-keys {

+ 34 - 1
src/stores/visitor.js

@@ -2,6 +2,11 @@ import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import * as api from '@/api/screen'
 
+// ===== 开发调试开关 =====
+// 正式联调前请关闭此开关
+const ENABLE_APPOINTMENT_CONFIRM_MOCK = import.meta.env.DEV
+// =========================
+
 export const useVisitorStore = defineStore('visitor', () => {
   // 当前访客信息
   const currentVisitor = ref({
@@ -97,6 +102,31 @@ export const useVisitorStore = defineStore('visitor', () => {
     }
   }
 
+  /**
+   * Mock 预约数据,用于开发调试
+   * 正式联调前请关闭 ENABLE_APPOINTMENT_CONFIRM_MOCK
+   */
+  function setMockAppointment(mobile = '13800000000') {
+    const now = new Date()
+    const dateStr = now.toISOString().slice(0, 10)
+    const timeStr = `${dateStr} 14:30`
+    const mockData = {
+      appointmentNo: `APT${dateStr.replace(/-/g, '')}0001`,
+      visitorName: '张三',
+      mobile: mobile,
+      idCardNo: '320***********1234',
+      visitedPerson: '李经理',
+      visitedDepartment: '综合管理部',
+      appointmentTime: timeStr,
+      visitPurpose: '业务拜访',
+      companyName: '测试单位',
+      remark: '前端调试预约数据',
+      _isMock: true
+    }
+    appointmentInfo.value = mockData
+    return mockData
+  }
+
   async function submitRegistration() {
     if (!validateForm()) {
       throw new Error('表单验证失败')
@@ -157,6 +187,9 @@ export const useVisitorStore = defineStore('visitor', () => {
     queryAppointment,
     submitRegistration,
     readIdCard,
-    clearVisitorData
+    clearVisitorData,
+    // 开发调试用
+    setMockAppointment,
+    ENABLE_APPOINTMENT_CONFIRM_MOCK
   }
 })

+ 711 - 119
src/views/visitor/Appointment.vue

@@ -1,268 +1,860 @@
 <template>
-  <ScreenLayout :show-back-btn="true" back-text="返回登记" back-target="/visitor">
-    <div class="page-appointment">
-      <h1 class="page-title">预约核验</h1>
-
-      <div class="verify-options">
-        <!-- 身份证读取 -->
-        <div class="verify-card" @click="handleIdCard">
-          <div class="verify-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>
-          <h3>身份证读取</h3>
-          <p>请将身份证放置读卡区</p>
+  <div class="appointment-layout">
+    <!-- 顶部状态栏 -->
+    <StatusBar :title="robotName" />
+
+    <!-- 主内容区 -->
+    <div class="layout-main">
+      <div class="appointment-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="verify-card" @click="showPhoneInput = true">
-          <div class="verify-icon">
-            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
-              <line x1="12" y1="18" x2="12.01" y2="18" />
-            </svg>
+        <!-- 内容区 -->
+        <div class="appointment-content">
+          <!-- 标题区 -->
+          <div class="appointment-hero">
+            <h1 class="page-title">预约核验</h1>
+            <p class="page-subtitle">请选择预约信息核验方式</p>
+          </div>
+
+          <!-- 核验入口卡片 -->
+          <div class="verify-options">
+            <!-- 身份证读取 -->
+            <div
+              class="verify-card"
+              role="button"
+              tabindex="0"
+              aria-label="身份证读取"
+              @click="handleIdCard"
+              @keydown.enter="handleIdCard"
+            >
+              <div class="card-body">
+                <div class="verify-icon idcard-icon">
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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>
+                </div>
+                <div class="verify-text">
+                  <h3>身份证读取</h3>
+                  <p>请将身份证放置在读卡区,系统将自动核验</p>
+                </div>
+              </div>
+            </div>
+
+            <!-- 手机号查询 -->
+            <div
+              class="verify-card"
+              role="button"
+              tabindex="0"
+              aria-label="手机号查询"
+              @click="showPhoneInputModal"
+              @keydown.enter="showPhoneInputModal"
+            >
+              <div class="card-body">
+                <div class="verify-icon phone-icon">
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
+                    <rect x="5" y="2" width="14" height="20" rx="3" />
+                    <line x1="12" y1="17" x2="12.01" y2="17" stroke-width="2.5" />
+                    <line x1="9" y1="8" x2="15" y2="8" />
+                    <line x1="9" y1="12" x2="15" y2="12" />
+                  </svg>
+                </div>
+                <div class="verify-text">
+                  <h3>手机号查询</h3>
+                  <p>输入预约手机号,快速查询您的到访预约</p>
+                </div>
+              </div>
+            </div>
           </div>
-          <h3>手机号查询</h3>
-          <p>输入预约手机号查询</p>
         </div>
+
+        <!-- 悬浮返回按钮 -->
+        <button class="btn-back" @click="goToVisitor">
+          <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>
+
+        <!-- 开发调试入口:仅 import.meta.env.DEV 显示 -->
+        <Teleport to="body">
+          <button
+            v-if="visitorStore.ENABLE_APPOINTMENT_CONFIRM_MOCK"
+            class="btn-debug-entry"
+            @click="handleDebugEnter"
+          >
+            调试进入预约确认页
+          </button>
+        </Teleport>
       </div>
+    </div>
+
+    <!-- 手机号输入弹窗 -->
+    <Teleport to="body">
+      <div v-if="showPhoneInput" class="phone-modal" @click.self="closePhoneModal">
+        <div class="modal-card">
+          <!-- 弹窗标题 -->
+          <div class="modal-header">
+            <h2>手机号查询</h2>
+            <p>请输入预约时预留的手机号</p>
+          </div>
 
-      <!-- 手机号输入弹窗 -->
-      <div v-if="showPhoneInput" class="phone-modal">
-        <div class="modal-content">
-          <h2>请输入预约手机号</h2>
+          <!-- 数字键盘 -->
           <NumericKeyboard
             v-model="phoneNumber"
-            label="手机号"
-            placeholder="请输入11位手机号"
             :max-length="11"
             type="phone"
+            :class="{ 'keyboard-error': phoneError }"
             @confirm="handlePhoneConfirm"
           />
+
+          <!-- 操作按钮 -->
           <div class="modal-actions">
-            <button class="btn-cancel" @click="showPhoneInput = false">取消</button>
-            <button class="btn-confirm" @click="handlePhoneConfirm(phoneNumber)">查询</button>
+            <button class="btn-cancel" @click="closePhoneModal">取消</button>
+            <button
+              class="btn-confirm"
+              :disabled="loading"
+              @click="handlePhoneConfirm"
+            >
+              {{ loading ? '查询中...' : '查询' }}
+            </button>
           </div>
+
+          <!-- 转现场登记 -->
+          <button class="btn-switch" @click="goToWalkIn">
+            未查询到?转现场登记
+          </button>
         </div>
       </div>
-    </div>
-  </ScreenLayout>
+    </Teleport>
+  </div>
 </template>
 
 <script setup>
-import { ref } from 'vue'
+import { ref, computed } 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 robotName = computed(() => screenStore.screenTheme?.robotName || '迎宾巡逻机器人')
+
 const showPhoneInput = ref(false)
 const phoneNumber = ref('')
+const phoneError = ref(false)
+const loading = ref(false)
+
+const showPhoneInputModal = () => {
+  phoneNumber.value = ''
+  phoneError.value = false
+  showPhoneInput.value = true
+}
+
+const closePhoneModal = () => {
+  showPhoneInput.value = false
+  phoneNumber.value = ''
+  phoneError.value = false
+}
+
+const goToVisitor = () => {
+  router.push('/visitor')
+}
+
+const goToWalkIn = () => {
+  closePhoneModal()
+  router.push('/visitor/walk-in')
+}
+
+// ===== 开发调试用 =====
+const handleDebugEnter = () => {
+  visitorStore.setMockAppointment()
+  screenStore.showAlert({
+    type: 'info',
+    message: '【调试】已进入预约确认页',
+    duration: 2000
+  })
+  router.push('/visitor/appointment-confirm')
+}
+// =========================
 
 const handleIdCard = async () => {
+  if (loading.value) return
+  loading.value = true
   try {
     const result = await visitorStore.readIdCard()
     screenStore.showAlert({
       type: 'success',
-      message: '身份证读取成功'
+      message: '身份证读取成功,正在核验...'
     })
-    // 查询预约
     if (result.idCardNo) {
       try {
         await visitorStore.queryAppointment({ idCardNo: result.idCardNo })
         router.push('/visitor/appointment-confirm')
       } catch {
-        screenStore.showAlert({
-          type: 'info',
-          message: '未查询到预约信息,请选择现场登记'
-        })
+        // 开发环境:身份证读取成功但预约查询失败,兜底进入确认页
+        if (visitorStore.ENABLE_APPOINTMENT_CONFIRM_MOCK) {
+          visitorStore.setMockAppointment()
+          closePhoneModal()
+          screenStore.showAlert({
+            type: 'info',
+            message: '【调试】身份证读取成功但预约查询无数据,已进入预约确认页'
+          })
+          router.push('/visitor/appointment-confirm')
+        } else {
+          screenStore.showAlert({
+            type: 'info',
+            message: '未查询到预约信息,您可以转现场登记',
+            duration: 4000
+          })
+        }
       }
     }
   } catch {
     screenStore.showAlert({
       type: 'error',
-      message: '身份证读取失败,请重试或选择其他方式'
+      message: '身份证读取失败,请重新放置证件,或使用手机号查询',
+      duration: 4000
     })
+  } finally {
+    loading.value = false
   }
 }
 
-const handlePhoneConfirm = async (phone) => {
-  if (!phone || phone.length !== 11) {
+const handlePhoneConfirm = async () => {
+  if (loading.value) return
+
+  if (!phoneNumber.value || phoneNumber.value.length !== 11) {
+    phoneError.value = true
     screenStore.showAlert({
       type: 'warning',
-      message: '请输入正确的11位手机号'
+      message: '请输入正确的11位手机号',
+      duration: 3000
     })
     return
   }
 
+  phoneError.value = false
+  loading.value = true
   try {
-    await visitorStore.queryAppointment({ mobile: phone })
-    showPhoneInput.value = false
+    await visitorStore.queryAppointment({ mobile: phoneNumber.value })
+    closePhoneModal()
     router.push('/visitor/appointment-confirm')
   } catch {
-    screenStore.showAlert({
-      type: 'info',
-      message: '未查询到预约信息'
-    })
+    // 开发环境:预约查询失败,兜底进入确认页
+    if (visitorStore.ENABLE_APPOINTMENT_CONFIRM_MOCK) {
+      visitorStore.setMockAppointment(phoneNumber.value)
+      closePhoneModal()
+      screenStore.showAlert({
+        type: 'info',
+        message: '【调试】预约查询无数据,已进入预约确认页',
+        duration: 3000
+      })
+      router.push('/visitor/appointment-confirm')
+    } else {
+      screenStore.showAlert({
+        type: 'info',
+        message: '未查询到预约信息,您可以转现场登记',
+        duration: 4000
+      })
+    }
+  } finally {
+    loading.value = false
   }
 }
 </script>
 
 <style scoped>
-.page-appointment {
+/* 页面布局 */
+.appointment-layout {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* 主内容 */
+.layout-main {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  position: relative;
+}
+
+/* 预约页面容器 */
+.appointment-page {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  overflow: hidden;
+}
+
+/* 动态背景 */
+.bg-layer {
+  position: absolute;
+  inset: 0;
+  background: linear-gradient(160deg, #e8f1fd 0%, #dbeeff 30%, #eef5ff 60%, #e5f0f8 100%);
+  overflow: hidden;
+}
+
+.bg-orb {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(80px);
+  pointer-events: none;
+}
+
+.orb-1 {
+  top: -180px;
+  right: -120px;
+  width: 560px;
+  height: 560px;
+  background: radial-gradient(ellipse, rgba(59, 130, 246, 0.14) 0%, transparent 65%);
+  animation: orbFloat 18s ease-in-out infinite;
+}
+
+.orb-2 {
+  bottom: -140px;
+  left: -100px;
+  width: 480px;
+  height: 480px;
+  background: radial-gradient(ellipse, rgba(6, 182, 212, 0.11) 0%, transparent 65%);
+  animation: orbFloat 22s ease-in-out infinite reverse;
+}
+
+.orb-3 {
+  top: 40%;
+  left: 45%;
+  transform: translate(-50%, -50%);
+  width: 320px;
+  height: 320px;
+  background: radial-gradient(ellipse, rgba(47, 142, 229, 0.06) 0%, transparent 65%);
+  animation: orbFloat 15s ease-in-out infinite;
+  animation-delay: -5s;
+}
+
+.bg-grid-overlay {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(47, 142, 229, 0.025) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(47, 142, 229, 0.025) 1px, transparent 1px);
+  background-size: 48px 48px;
+  pointer-events: none;
+}
+
+@keyframes orbFloat {
+  0%, 100% { transform: translate(0, 0) scale(1); }
+  33% { transform: translate(20px, -15px) scale(1.05); }
+  66% { transform: translate(-15px, 20px) scale(0.97); }
+}
+
+/* 内容区 */
+.appointment-content {
+  position: relative;
+  z-index: 1;
   width: 100%;
-  height: 100%;
+  max-width: 1200px;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 20px 0;
+}
+
+/* 标题区 */
+.appointment-hero {
+  text-align: center;
+  flex-shrink: 0;
+  padding-top: 64px;
+  margin-bottom: 40px;
 }
 
 .page-title {
-  font-size: 32px;
-  font-weight: 600;
+  font-size: 54px;
+  font-weight: 900;
   color: var(--text-primary);
-  margin: 0 0 40px;
+  letter-spacing: 3px;
+  margin: 0 0 6px;
+  line-height: 1.05;
+}
+
+.page-subtitle {
+  font-size: 22px;
+  color: var(--text-secondary);
+  margin: 0;
+  font-weight: 500;
+  line-height: 1.25;
 }
 
+/* 核验卡片 */
 .verify-options {
   display: grid;
   grid-template-columns: repeat(2, 1fr);
-  gap: 40px;
-  max-width: 700px;
+  gap: 36px;
   width: 100%;
 }
 
 .verify-card {
+  height: 420px;
+  padding: 48px 52px 44px;
+  border-radius: 44px;
+  background: rgba(255, 255, 255, 0.92);
+  border: 1px solid rgba(255, 255, 255, 0.80);
+  box-shadow:
+    0 28px 80px rgba(30, 64, 175, 0.14),
+    0 2px 10px rgba(255, 255, 255, 0.72) inset;
+  backdrop-filter: blur(16px);
+  -webkit-backdrop-filter: blur(16px);
+  cursor: pointer;
+  transition: all 0.22s ease;
   display: flex;
   flex-direction: column;
-  align-items: center;
-  gap: 16px;
-  padding: 40px 24px;
-  background: var(--bg-card);
-  border-radius: var(--radius-xl);
-  box-shadow: var(--shadow-md);
-  cursor: pointer;
-  transition: all var(--transition-normal);
+  justify-content: center;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: transparent;
+  user-select: none;
 }
 
 .verify-card:hover {
   transform: translateY(-6px);
-  box-shadow: var(--shadow-xl);
+  box-shadow:
+    0 34px 88px rgba(30, 64, 175, 0.18),
+    0 2px 10px rgba(255, 255, 255, 0.78) inset;
+}
+
+.verify-card:active {
+  transform: scale(0.984);
 }
 
+.card-body {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  padding-top: 4px;
+}
+
+/* 卡片图标 */
 .verify-icon {
-  width: 64px;
-  height: 64px;
+  width: 116px;
+  height: 116px;
+  border-radius: 32px;
+  color: #fff;
   display: flex;
   align-items: center;
   justify-content: center;
-  background: var(--primary-soft);
-  border-radius: 16px;
-  color: var(--primary);
+  margin-bottom: 30px;
+  flex-shrink: 0;
 }
 
 .verify-icon svg {
-  width: 36px;
-  height: 36px;
+  width: 58px;
+  height: 58px;
 }
 
-.verify-card h3 {
-  font-size: 24px;
-  font-weight: 600;
+.idcard-icon {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  box-shadow: 0 18px 36px rgba(37, 99, 235, 0.24);
+}
+
+.phone-icon {
+  background: linear-gradient(135deg, #20b7c7 0%, #0ea5e9 100%);
+  box-shadow: 0 18px 36px rgba(32, 183, 199, 0.28);
+}
+
+/* 卡片文字 */
+.verify-text {
+  display: flex;
+  flex-direction: column;
+}
+
+.verify-text h3 {
+  font-size: 44px;
+  font-weight: 900;
   color: var(--text-primary);
-  margin: 0;
+  margin: 0 0 14px;
+  letter-spacing: 1.5px;
+  line-height: 1.15;
 }
 
-.verify-card p {
-  font-size: 15px;
-  color: var(--text-muted);
+.verify-text p {
+  font-size: 22px;
+  color: var(--text-secondary);
+  line-height: 1.50;
   margin: 0;
+  font-weight: 500;
+}
+
+/* 悬浮返回按钮 */
+.btn-back {
+  position: absolute;
+  left: 42px;
+  bottom: 30px;
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  height: 66px;
+  padding: 0 30px;
+  background: rgba(255, 255, 255, 0.88);
+  border: 1px solid rgba(47, 142, 229, 0.20);
+  border-radius: 999px;
+  box-shadow:
+    0 12px 36px rgba(47, 142, 229, 0.14),
+    0 2px 8px rgba(0, 0, 0, 0.06);
+  color: var(--text-secondary);
+  font-size: 23px;
+  font-weight: 800;
+  cursor: pointer;
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
+  backdrop-filter: blur(12px);
+  -webkit-backdrop-filter: blur(12px);
+  z-index: 10;
+}
+
+.btn-back:hover {
+  background: rgba(255, 255, 255, 0.97);
+  color: var(--primary);
+  box-shadow:
+    0 18px 48px rgba(47, 142, 229, 0.22),
+    0 4px 12px rgba(0, 0, 0, 0.08);
+  transform: translateY(-3px);
+}
+
+.btn-back:active {
+  transform: scale(0.97);
+  box-shadow: 0 8px 24px rgba(47, 142, 229, 0.12);
+}
+
+.back-icon {
+  width: 22px;
+  height: 22px;
+  flex-shrink: 0;
 }
 
 /* 手机号输入弹窗 */
 .phone-modal {
   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.48);
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 100;
+  z-index: 200;
   animation: fadeIn 0.2s ease-out;
+  backdrop-filter: blur(6px);
+  -webkit-backdrop-filter: blur(6px);
 }
 
-.modal-content {
-  width: 90%;
-  max-width: 450px;
-  padding: 32px;
-  background: var(--bg-card);
-  border-radius: var(--radius-xl);
-  box-shadow: var(--shadow-xl);
+.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;
 }
 
-.modal-content h2 {
-  font-size: 24px;
-  font-weight: 600;
-  color: var(--text-primary);
+.modal-header {
   text-align: center;
-  margin: 0 0 24px;
+  margin-bottom: 28px;
+  flex-shrink: 0;
+}
+
+.modal-header h2 {
+  font-size: 36px;
+  font-weight: 900;
+  color: var(--text-primary);
+  margin: 0 0 8px;
+  letter-spacing: 2px;
 }
 
+.modal-header p {
+  font-size: 20px;
+  color: var(--text-secondary);
+  margin: 0;
+  font-weight: 500;
+}
+
+/* 操作按钮 */
 .modal-actions {
   display: grid;
-  grid-template-columns: 1fr 1fr;
+  grid-template-columns: 1fr 1.5fr;
   gap: 16px;
+  width: 100%;
   margin-top: 24px;
+  flex-shrink: 0;
 }
 
 .btn-cancel,
 .btn-confirm {
-  height: 56px;
-  font-size: 18px;
-  border-radius: var(--radius-lg);
+  height: 74px;
+  font-size: 26px;
+  font-weight: 800;
+  border-radius: 999px;
   cursor: pointer;
-  transition: all var(--transition-fast);
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
 }
 
 .btn-cancel {
-  background: var(--bg-page);
-  color: var(--text-secondary);
-  border: 2px solid var(--border-light);
+  background: rgba(107, 114, 128, 0.10);
+  color: #4b5563;
+  border: 1px solid rgba(107, 114, 128, 0.15);
 }
 
 .btn-cancel:hover {
-  border-color: var(--text-muted);
+  background: rgba(107, 114, 128, 0.16);
+}
+
+.btn-cancel:active {
+  transform: scale(0.97);
 }
 
 .btn-confirm {
-  background: var(--primary);
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
   color: white;
   border: none;
+  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
 }
 
 .btn-confirm:hover {
-  background: var(--primary-dark);
+  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;
+}
+
+/* 转现场登记 */
+.btn-switch {
+  margin-top: 20px;
+  padding: 0;
+  background: none;
+  border: none;
+  color: var(--primary);
+  font-size: 17px;
+  font-weight: 600;
+  cursor: pointer;
+  text-decoration: underline;
+  text-underline-offset: 3px;
+  transition: color 0.2s;
+  flex-shrink: 0;
+}
+
+.btn-switch:hover {
+  color: var(--primary-dark);
+}
+
+/* 开发调试入口按钮 */
+.btn-debug-entry {
+  position: fixed;
+  right: 16px;
+  bottom: 16px;
+  padding: 8px 16px;
+  background: rgba(245, 158, 11, 0.15);
+  border: 1px solid rgba(245, 158, 11, 0.40);
+  border-radius: 999px;
+  color: #d97706;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  z-index: 999;
+  transition: all 0.2s ease;
+  letter-spacing: 0.5px;
+}
+
+.btn-debug-entry:hover {
+  background: rgba(245, 158, 11, 0.25);
+  border-color: rgba(245, 158, 11, 0.60);
+  transform: translateY(-1px);
+}
+
+.btn-debug-entry:active {
+  transform: scale(0.97);
+}
+
+/* 键盘内部输入框错误态 */
+:deep(.keyboard-error .keyboard-input) {
+  border-color: #ef4444 !important;
+  background: linear-gradient(160deg, #fef2f2 0%, #fee2e2 100%) !important;
+}
+
+:deep(.keyboard-error .input-label) {
+  color: #dc2626 !important;
+}
+
+:deep(.keyboard-error .placeholder) {
+  color: #fca5a5 !important;
+}
+
+/* 键盘宽屏大按钮覆盖 */
+:deep(.numeric-keyboard) {
+  width: 100%;
+  max-width: 100%;
+}
+
+:deep(.numeric-keyboard .keyboard-input) {
+  height: 108px;
+  min-height: 108px;
+  max-height: 108px;
+  padding: 0 24px;
+  margin-bottom: 16px;
+}
+
+:deep(.numeric-keyboard .input-value) {
+  height: 100%;
+  min-height: 0;
+}
+
+:deep(.numeric-keyboard .keyboard-keys) {
+  gap: 14px;
+}
+
+:deep(.numeric-keyboard .key-btn) {
+  height: 82px;
+  font-size: 32px;
+  font-weight: 700;
+  border-radius: 20px;
+}
+
+:deep(.numeric-keyboard .key-btn svg) {
+  width: 34px;
+  height: 34px;
+}
+
+:deep(.numeric-keyboard .key-action) {
+  background: rgba(107, 114, 128, 0.08);
+}
+
+:deep(.numeric-keyboard .key-delete) {
+  color: var(--text-secondary);
+}
+
+:deep(.numeric-keyboard .key-delete:hover) {
+  background: rgba(239, 68, 68, 0.06);
+  border-color: rgba(239, 68, 68, 0.20);
+  color: #dc2626;
 }
 
 @keyframes fadeIn {
-  from {
-    opacity: 0;
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+/* 响应式 */
+@media (max-width: 900px), (max-height: 660px) {
+  .appointment-hero {
+    padding-top: 50px;
+    margin-bottom: 32px;
   }
-  to {
-    opacity: 1;
+
+  .page-title {
+    font-size: 46px;
+    letter-spacing: 2px;
+    margin-bottom: 5px;
+  }
+
+  .page-subtitle {
+    font-size: 20px;
+  }
+
+  .verify-options {
+    gap: 36px;
+  }
+
+  .verify-card {
+    height: 360px;
+    padding: 38px 42px 34px;
+    border-radius: 38px;
+  }
+
+  .verify-icon {
+    width: 94px;
+    height: 94px;
+    border-radius: 28px;
+    margin-bottom: 24px;
+  }
+
+  .verify-icon svg {
+    width: 48px;
+    height: 48px;
+  }
+
+  .verify-text h3 {
+    font-size: 36px;
+    margin-bottom: 12px;
+  }
+
+  .verify-text p {
+    font-size: 18px;
+    line-height: 1.42;
+  }
+
+  .btn-back {
+    left: 30px;
+    bottom: 24px;
+    height: 62px;
+    padding: 0 28px;
+    font-size: 21px;
+  }
+
+  .modal-card {
+    width: 560px;
+    padding: 36px 40px 30px;
+  }
+
+  .modal-header h2 {
+    font-size: 30px;
+  }
+
+  .modal-header p {
+    font-size: 18px;
+  }
+
+  .phone-value {
+    font-size: 28px;
+  }
+
+  .btn-cancel,
+  .btn-confirm {
+    height: 60px;
+    font-size: 22px;
   }
 }
 </style>

+ 776 - 87
src/views/visitor/AppointmentConfirm.vue

@@ -1,81 +1,330 @@
 <template>
-  <ScreenLayout :show-back-btn="true" back-text="返回" back-target="/visitor/appointment">
-    <div class="page-confirm">
-      <h1 class="page-title">预约信息确认</h1>
-
-      <div v-if="appointment" class="info-card">
-        <div class="info-row">
-          <span class="info-label">访客姓名</span>
-          <span class="info-value">{{ appointment.visitorName }}</span>
-        </div>
-        <div class="info-row">
-          <span class="info-label">手机号码</span>
-          <span class="info-value">{{ maskMobile(appointment.mobile) }}</span>
-        </div>
-        <div class="info-row">
-          <span class="info-label">身份证号</span>
-          <span class="info-value">{{ maskIdCard(appointment.idCardNo) }}</span>
-        </div>
-        <div class="info-row">
-          <span class="info-label">被访人</span>
-          <span class="info-value">{{ appointment.visitedPerson }}</span>
-        </div>
-        <div class="info-row">
-          <span class="info-label">被访部门</span>
-          <span class="info-value">{{ appointment.visitedDepartment }}</span>
+  <div class="confirm-layout">
+    <!-- 顶部状态栏 -->
+    <StatusBar :title="robotName" />
+
+    <!-- 主内容区 -->
+    <div class="layout-main">
+      <div class="confirm-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="info-row">
-          <span class="info-label">预约时间</span>
-          <span class="info-value">{{ appointment.appointmentTime }}</span>
+
+        <!-- 内容区 -->
+        <div class="confirm-content">
+          <!-- 标题区 -->
+          <div class="confirm-hero">
+            <h1 class="page-title">预约信息确认</h1>
+            <p class="page-subtitle">请核对预约信息,如需调整可修改被访人与来访事由</p>
+          </div>
+
+          <!-- 表单卡片 -->
+          <div class="form-card">
+            <!-- 预约单号:只读 -->
+            <div class="form-row form-row-readonly">
+              <span class="form-label">预约单号</span>
+              <span class="form-value">{{ form.appointmentNo || '--' }}</span>
+            </div>
+
+            <!-- 访客姓名:只读 -->
+            <div class="form-row form-row-readonly">
+              <span class="form-label">访客姓名</span>
+              <span class="form-value">{{ form.visitorName }}</span>
+            </div>
+
+            <!-- 身份证号:只读 -->
+            <div class="form-row form-row-readonly">
+              <span class="form-label">身份证号</span>
+              <span class="form-value">{{ maskIdCard(form.idCardNo || appointment?.idCardNo) }}</span>
+            </div>
+
+            <!-- 手机号码:只读 -->
+            <div class="form-row form-row-readonly">
+              <span class="form-label">手机号码</span>
+              <span class="form-value">{{ form.mobile }}</span>
+            </div>
+
+            <!-- 被访人:可编辑 -->
+            <div class="form-row form-row-editable" @click="openTextModal('visitedPerson', '修改被访人', form.visitedPerson)">
+              <span class="form-label">被访人</span>
+              <div class="form-value-editable-wrapper">
+                <span v-if="form.visitedPerson" class="form-value form-value-editable">{{ form.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-readonly">
+              <span class="form-label">预约时间</span>
+              <span class="form-value">{{ form.appointmentTime || '--' }}</span>
+            </div>
+
+            <!-- 来访事由:可编辑 -->
+            <div class="form-row form-row-editable" @click="openReasonModal">
+              <span class="form-label">来访事由</span>
+              <div class="form-value-editable-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-hint">蓝色标记项可点击修改</div>
+
+          <!-- 底部按钮 -->
+          <div class="confirm-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="loading" @click="handleConfirm">
+              <span v-if="loading" class="loading-spinner"></span>
+              <span v-else>确认登记</span>
+            </button>
+          </div>
         </div>
-        <div class="info-row">
-          <span class="info-label">来访事由</span>
-          <span class="info-value">{{ appointment.visitPurpose }}</span>
+      </div>
+    </div>
+
+    <!-- ===== 文本编辑弹窗 ===== -->
+    <Teleport to="body">
+      <div v-if="showTextModal" class="modal-overlay" @click.self="closeTextModal">
+        <div class="modal-card">
+          <div class="modal-header">
+            <h2>{{ textModalTitle }}</h2>
+          </div>
+          <div class="modal-input-wrapper">
+            <input
+              ref="textInputRef"
+              v-model="tempText"
+              class="modal-text-input"
+              type="text"
+              :placeholder="textModalPlaceholder"
+              @keydown.enter="confirmTextEdit"
+            />
+          </div>
+          <div class="modal-text-actions">
+            <button class="btn-modal-cancel" @click="closeTextModal">取消</button>
+            <button class="btn-modal-save" @click="confirmTextEdit">保存</button>
+          </div>
         </div>
       </div>
+    </Teleport>
 
-      <div class="confirm-actions">
-        <button class="btn-back" @click="goBack">信息有误</button>
-        <button class="btn-confirm" @click="handleConfirm" :disabled="loading">
-          <span v-if="loading" class="loading-spinner"></span>
-          <span v-else>确认登记</span>
-        </button>
+    <!-- ===== 来访事由选项弹窗 ===== -->
+    <Teleport to="body">
+      <div v-if="showReasonModal" class="modal-overlay" @click.self="closeReasonModal">
+        <div class="modal-card modal-reason-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="modal-reason-actions">
+            <button class="btn-modal-cancel" @click="closeReasonModal">取消</button>
+            <button class="btn-modal-save" @click="confirmReasonEdit">保存</button>
+          </div>
+        </div>
       </div>
-    </div>
-  </ScreenLayout>
+    </Teleport>
+  </div>
 </template>
 
 <script setup>
-import { ref } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
+import StatusBar from '@/components/StatusBar.vue'
 import { useScreenStore } from '@/stores/screen'
 import { useVisitorStore } from '@/stores/visitor'
-import { maskMobile, maskIdCard } from '@/utils/device'
-import ScreenLayout from '@/layouts/ScreenLayout.vue'
+import { maskIdCard } from '@/utils/device'
 
 const router = useRouter()
 const screenStore = useScreenStore()
 const visitorStore = useVisitorStore()
 
+const robotName = computed(() => screenStore.screenTheme?.robotName || '迎宾巡逻机器人')
 const loading = ref(false)
+const visitReasonOptions = visitorStore.visitReasonOptions
 
 const appointment = visitorStore.appointmentInfo
 
+// 表单数据
+const form = reactive({
+  appointmentNo: '',
+  visitorName: '',
+  idCardNo: '',
+  mobile: '',
+  visitedPerson: '',
+  appointmentTime: '',
+  visitPurpose: ''
+})
+
+// 初始化表单数据
+const initForm = () => {
+  if (appointment) {
+    form.appointmentNo = appointment.appointmentNo || ''
+    form.visitorName = appointment.visitorName || ''
+    form.idCardNo = appointment.idCardNo || ''
+    form.mobile = appointment.mobile || ''
+    form.visitedPerson = appointment.visitedPerson || ''
+    form.appointmentTime = appointment.appointmentTime || ''
+    form.visitPurpose = appointment.visitPurpose || appointment.visitReason || ''
+  }
+}
+initForm()
+
+// 来访事由标签
+const currentReasonLabel = computed(() => {
+  const found = visitReasonOptions.find(o => o.value === form.visitPurpose)
+  return found ? found.label : ''
+})
+
+// ===== 文本编辑弹窗 =====
+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.replace('修改', '请输入')
+  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: `${textModalTitle.value.replace('修改', '')}不能为空`,
+      duration: 3000
+    })
+    return
+  }
+  if (textModalField.value === 'visitedPerson') {
+    form.visitedPerson = tempText.value
+  }
+  closeTextModal()
+}
+
+// ===== 来访事由选项弹窗 =====
+const showReasonModal = ref(false)
+const tempReason = ref('')
+
+const openReasonModal = () => {
+  tempReason.value = form.visitPurpose
+  showReasonModal.value = true
+}
+
+const closeReasonModal = () => {
+  showReasonModal.value = false
+}
+
+const confirmReasonEdit = () => {
+  if (!tempReason.value) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请选择来访事由',
+      duration: 3000
+    })
+    return
+  }
+  form.visitPurpose = tempReason.value
+  closeReasonModal()
+}
+
+// ===== 导航 =====
 const goBack = () => {
-  router.push('/visitor/walk-in')
+  router.push('/visitor/appointment')
 }
 
+// ===== 确认登记 =====
 const handleConfirm = async () => {
   if (loading.value) return
+
+  // 手机号校验
+  if (!form.mobile || !/^1[3-9]\d{9}$/.test(form.mobile)) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请输入正确的11位手机号',
+      duration: 3000
+    })
+    return
+  }
+
+  // 被访人非空校验
+  if (!form.visitedPerson || !form.visitedPerson.trim()) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请填写被访人姓名',
+      duration: 3000
+    })
+    return
+  }
+
+  // 来访事由非空校验
+  if (!form.visitPurpose) {
+    screenStore.showAlert({
+      type: 'warning',
+      message: '请选择来访事由',
+      duration: 3000
+    })
+    return
+  }
+
   loading.value = true
 
   try {
+    visitorStore.appointmentInfo = {
+      ...visitorStore.appointmentInfo,
+      ...form,
+      visitReason: form.visitPurpose
+    }
+
     await visitorStore.submitRegistration()
     router.push('/visitor/success')
   } catch (error) {
     screenStore.showAlert({
       type: 'error',
-      message: '登记失败,请重试'
+      message: '登记失败,请重试',
+      duration: 3000
     })
   } finally {
     loading.value = false
@@ -84,116 +333,556 @@ const handleConfirm = async () => {
 </script>
 
 <style scoped>
-.page-confirm {
-  width: 100%;
-  height: 100%;
+/* ===== 布局结构 ===== */
+.confirm-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;
+}
+
+.confirm-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); }
+}
+
+/* ===== 内容区 ===== */
+.confirm-content {
+  flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 20px 0;
+  padding: 0 32px 32px;
+  position: relative;
+  z-index: 1;
+  overflow-y: auto;
+}
+
+/* ===== 标题区 ===== */
+.confirm-hero {
+  text-align: center;
+  margin: 28px 0 24px;
+  flex-shrink: 0;
 }
 
 .page-title {
-  font-size: 32px;
-  font-weight: 600;
+  font-size: 40px;
+  font-weight: 900;
   color: var(--text-primary);
-  margin: 0 0 32px;
+  margin: 0 0 10px;
+  letter-spacing: 3px;
 }
 
-.info-card {
+.page-subtitle {
+  font-size: 20px;
+  color: var(--text-secondary);
+  margin: 0;
+  font-weight: 500;
+}
+
+/* ===== 表单卡片 ===== */
+.form-card {
   width: 100%;
-  max-width: 500px;
-  background: var(--bg-card);
-  border-radius: var(--radius-xl);
-  box-shadow: var(--shadow-md);
-  padding: 24px;
-  margin-bottom: 32px;
+  max-width: 640px;
+  background: rgba(255, 255, 255, 0.88);
+  border-radius: 32px;
+  padding: 8px 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: 12px;
+}
+
+.form-hint {
+  text-align: center;
+  font-size: 16px;
+  color: #94a3b8;
+  font-weight: 500;
+  letter-spacing: 0.5px;
+  margin-bottom: 16px;
 }
 
-.info-row {
+.form-row {
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  padding: 16px 0;
-  border-bottom: 1px solid var(--border-light);
+  padding: 0 32px;
+  min-height: 80px;
+  border-bottom: 1px solid rgba(226, 232, 240, 0.80);
 }
 
-.info-row:last-child {
+.form-row:last-child {
   border-bottom: none;
 }
 
-.info-label {
-  font-size: 16px;
-  color: var(--text-muted);
+.form-row-readonly {
+  cursor: default;
 }
 
-.info-value {
-  font-size: 18px;
-  font-weight: 500;
+.form-row-editable {
+  cursor: pointer;
+  transition: background 0.15s;
+}
+
+.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: 22px;
+  font-weight: 600;
+  color: var(--text-secondary);
+  width: 140px;
+  flex-shrink: 0;
+  letter-spacing: 0.5px;
+}
+
+.form-value {
+  flex: 1;
+  font-size: 24px;
+  font-weight: 700;
   color: var(--text-primary);
+  text-align: right;
+  letter-spacing: 1px;
+}
+
+.form-value-editable-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+.form-value-editable {
+  color: var(--primary);
+}
+
+.form-value-placeholder {
+  color: #94a3b8;
+  font-weight: 500;
+  font-size: 22px;
 }
 
+.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: grid;
-  grid-template-columns: 1fr 2fr;
-  gap: 20px;
+  grid-template-columns: 1fr 1.8fr;
+  gap: 16px;
   width: 100%;
-  max-width: 500px;
+  max-width: 640px;
+  flex-shrink: 0;
 }
 
 .btn-back,
 .btn-confirm {
-  height: 64px;
-  font-size: 20px;
-  font-weight: 500;
-  border-radius: var(--radius-lg);
+  height: 78px;
+  font-size: 24px;
+  font-weight: 800;
+  border-radius: 999px;
   cursor: pointer;
-  transition: all var(--transition-fast);
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
   display: flex;
   align-items: center;
   justify-content: center;
 }
 
 .btn-back {
-  background: var(--bg-card);
+  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;
 }
 
 .btn-back:hover {
+  background: rgba(255, 255, 255, 0.95);
   border-color: var(--text-muted);
+  transform: translateY(-2px);
+  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.10);
+}
+
+.btn-back:active {
+  transform: scale(0.97);
+}
+
+.back-icon {
+  width: 22px;
+  height: 22px;
+  flex-shrink: 0;
 }
 
 .btn-confirm {
-  background: var(--primary);
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
   color: white;
   border: none;
-  box-shadow: var(--shadow-md);
+  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
 }
 
 .btn-confirm:hover:not(:disabled) {
-  background: var(--primary-dark);
+  box-shadow: 0 16px 36px rgba(37, 99, 235, 0.36);
   transform: translateY(-2px);
-  box-shadow: var(--shadow-lg);
+}
+
+.btn-confirm:active {
+  transform: scale(0.97);
 }
 
 .btn-confirm:disabled {
-  opacity: 0.7;
+  background: linear-gradient(135deg, #93c5fd 0%, #60a5fa 100%);
   cursor: not-allowed;
+  box-shadow: none;
+  transform: none;
 }
 
 .loading-spinner {
-  width: 24px;
-  height: 24px;
-  border: 3px solid rgba(255, 255, 255, 0.3);
+  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);
+  to { transform: rotate(360deg); }
+}
+
+/* ===== 弹窗遮罩 ===== */
+.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;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+/* ===== 弹窗卡片 ===== */
+.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;
+}
+
+@keyframes slideUp {
+  from { transform: translateY(20px) scale(0.98); opacity: 0; }
+  to { transform: translateY(0) scale(1); opacity: 1; }
+}
+
+/* ===== 弹窗标题 ===== */
+.modal-header {
+  text-align: center;
+  margin-bottom: 24px;
+}
+
+.modal-header h2 {
+  font-size: 30px;
+  font-weight: 800;
+  color: var(--text-primary);
+  margin: 0 0 8px;
+  letter-spacing: 2px;
+}
+
+/* ===== 文本输入弹窗 ===== */
+.modal-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;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.modal-text-input {
+  width: 100%;
+  height: 100%;
+  font-size: 30px;
+  font-weight: 700;
+  color: var(--text-primary);
+  background: transparent;
+  border: none;
+  outline: none;
+  letter-spacing: 1px;
+}
+
+.modal-text-input::placeholder {
+  font-size: 26px;
+  font-weight: 500;
+  color: #b0bec5;
+}
+
+.modal-text-actions {
+  display: grid;
+  grid-template-columns: 1fr 1.5fr;
+  gap: 14px;
+}
+
+/* ===== 来访事由选项弹窗 ===== */
+.modal-reason-card {
+  width: 560px;
+}
+
+.reason-options {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 14px;
+  margin-bottom: 20px;
+}
+
+.reason-btn {
+  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: 20px;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  letter-spacing: 0.5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.reason-btn:hover {
+  background: rgba(59, 130, 246, 0.06);
+  border-color: rgba(59, 130, 246, 0.30);
+  transform: translateY(-1px);
+}
+
+.reason-btn:active {
+  transform: scale(0.97);
+}
+
+.reason-btn-active {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  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);
+}
+
+.modal-reason-actions {
+  display: grid;
+  grid-template-columns: 1fr 1.5fr;
+  gap: 14px;
+}
+
+/* ===== 弹窗通用按钮 ===== */
+.btn-modal-cancel,
+.btn-modal-save {
+  height: 72px;
+  font-size: 24px;
+  font-weight: 800;
+  border-radius: 999px;
+  cursor: pointer;
+  transition: all 0.20s ease;
+  letter-spacing: 1.5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.btn-modal-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-modal-cancel:hover {
+  background: rgba(255, 255, 255, 0.95);
+  border-color: var(--text-muted);
+  transform: translateY(-1px);
+}
+
+.btn-modal-save {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  color: white;
+  border: none;
+  box-shadow: 0 10px 24px rgba(37, 99, 235, 0.26);
+}
+
+.btn-modal-save:hover {
+  box-shadow: 0 14px 32px rgba(37, 99, 235, 0.34);
+  transform: translateY(-1px);
+}
+
+.btn-modal-cancel:active,
+.btn-modal-save:active {
+  transform: scale(0.97);
+}
+
+/* ===== 响应式适配 ===== */
+@media (max-height: 700px) {
+  .confirm-content {
+    padding: 0 24px 20px;
+  }
+
+  .confirm-hero {
+    margin: 16px 0 16px;
+  }
+
+  .page-title {
+    font-size: 32px;
+  }
+
+  .page-subtitle {
+    font-size: 18px;
+  }
+
+  .form-row {
+    min-height: 68px;
+  }
+
+  .form-label {
+    font-size: 20px;
+    width: 120px;
+  }
+
+  .form-value {
+    font-size: 22px;
+  }
+
+  .confirm-actions {
+    grid-template-columns: 1fr 1.5fr;
+  }
+
+  .btn-back,
+  .btn-confirm {
+    height: 68px;
+    font-size: 22px;
   }
 }
 </style>

+ 387 - 63
src/views/visitor/Index.vue

@@ -1,46 +1,107 @@
 <template>
-  <ScreenLayout :show-back-btn="true" back-text="返回菜单" back-target="/menu">
-    <div class="page-visitor">
-      <h1 class="page-title">访客登记</h1>
-
-      <div class="visitor-options">
-        <!-- 预约到访 -->
-        <div class="option-card" @click="goToAppointment">
-          <div class="option-icon" style="background: linear-gradient(135deg, #2f8ee5 0%, #1a6fc9 100%);">
-            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
-              <line x1="16" y1="2" x2="16" y2="6" />
-              <line x1="8" y1="2" x2="8" y2="6" />
-              <line x1="3" y1="10" x2="21" y2="10" />
-            </svg>
-          </div>
-          <h3>预约到访</h3>
-          <p>请出示预约信息</p>
+  <div class="visitor-layout">
+    <!-- 顶部状态栏 -->
+    <StatusBar :title="robotName" />
+
+    <!-- 主内容区 -->
+    <div class="layout-main">
+      <div class="visitor-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="option-card" @click="goToWalkIn">
-          <div class="option-icon" style="background: linear-gradient(135deg, #20b7c7 0%, #0ea5a5 100%);">
-            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
-              <circle cx="9" cy="7" r="4" />
-              <line x1="19" y1="8" x2="19" y2="14" />
-              <line x1="22" y1="11" x2="16" y2="11" />
-            </svg>
+        <!-- 内容区 -->
+        <div class="visitor-content">
+          <!-- 标题区 -->
+          <div class="visitor-hero">
+            <h1 class="page-title">访客登记</h1>
+            <p class="page-subtitle">请选择您的到访方式</p>
+          </div>
+
+          <!-- 服务入口区 -->
+          <div class="visitor-service-area">
+            <!-- 入口卡片 -->
+            <div class="visitor-options">
+            <!-- 预约到访 -->
+            <div
+              class="option-card appointment-card"
+              role="button"
+              tabindex="0"
+              aria-label="预约到访"
+              @click="goToAppointment"
+              @keydown.enter="goToAppointment"
+            >
+              <div class="card-body">
+                <div class="option-icon appointment-icon">
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+                    <line x1="16" y1="2" x2="16" y2="6" />
+                    <line x1="8" y1="2" x2="8" y2="6" />
+                    <line x1="3" y1="10" x2="21" y2="10" />
+                    <circle cx="12" cy="15" r="2" />
+                  </svg>
+                </div>
+                <div class="option-content">
+                  <h3>预约到访</h3>
+                  <p>已提前预约,可通过手机号或身份证核验</p>
+                </div>
+              </div>
+            </div>
+
+            <!-- 现场登记 -->
+            <div
+              class="option-card walkin-card"
+              role="button"
+              tabindex="0"
+              aria-label="现场登记"
+              @click="goToWalkIn"
+              @keydown.enter="goToWalkIn"
+            >
+              <div class="card-body">
+                <div class="option-icon walkin-icon">
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
+                    <circle cx="12" cy="7" r="4" />
+                    <line x1="20" y1="8" x2="20" y2="14" />
+                    <line x1="23" y1="11" x2="17" y2="11" />
+                  </svg>
+                </div>
+                <div class="option-content">
+                  <h3>现场登记</h3>
+                  <p>未预约访客,可现场填写信息完成登记</p>
+                </div>
+              </div>
+            </div>
+          </div>
           </div>
-          <h3>现场登记</h3>
-          <p>填写个人信息</p>
         </div>
+
+        <!-- 悬浮返回按钮 -->
+        <button class="btn-back" @click="goToMenu">
+          <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>
       </div>
     </div>
-  </ScreenLayout>
+  </div>
 </template>
 
 <script setup>
+import { computed } from 'vue'
 import { useRouter } from 'vue-router'
-import ScreenLayout from '@/layouts/ScreenLayout.vue'
+import StatusBar from '@/components/StatusBar.vue'
+import { useScreenStore } from '@/stores/screen'
 
 const router = useRouter()
+const screenStore = useScreenStore()
+
+const robotName = computed(() => screenStore.screenTheme?.robotName || '迎宾巡逻机器人')
 
 const goToAppointment = () => {
   router.push('/visitor/appointment')
@@ -49,80 +110,343 @@ const goToAppointment = () => {
 const goToWalkIn = () => {
   router.push('/visitor/walk-in')
 }
+
+const goToMenu = () => {
+  router.push('/menu')
+}
 </script>
 
 <style scoped>
-.page-visitor {
-  width: 100%;
-  height: 100%;
+/* 页面布局:与 /menu 完全一致 */
+.visitor-layout {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* 主内容 */
+.layout-main {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  position: relative;
+}
+
+/* 访客页面容器 */
+.visitor-page {
+  flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 20px 0;
+  position: relative;
+  overflow: hidden;
+  padding: 0;
+}
+
+/* 动态背景 */
+.bg-layer {
+  position: absolute;
+  inset: 0;
+  background: linear-gradient(160deg, #e8f1fd 0%, #dbeeff 30%, #eef5ff 60%, #e5f0f8 100%);
+  overflow: hidden;
+}
+
+/* 光晕球 */
+.bg-orb {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(80px);
+  pointer-events: none;
+}
+
+.orb-1 {
+  top: -180px;
+  right: -120px;
+  width: 560px;
+  height: 560px;
+  background: radial-gradient(ellipse, rgba(59, 130, 246, 0.14) 0%, transparent 65%);
+  animation: orbFloat 18s ease-in-out infinite;
+}
+
+.orb-2 {
+  bottom: -140px;
+  left: -100px;
+  width: 480px;
+  height: 480px;
+  background: radial-gradient(ellipse, rgba(6, 182, 212, 0.11) 0%, transparent 65%);
+  animation: orbFloat 22s ease-in-out infinite reverse;
+}
+
+.orb-3 {
+  top: 40%;
+  left: 45%;
+  transform: translate(-50%, -50%);
+  width: 320px;
+  height: 320px;
+  background: radial-gradient(ellipse, rgba(47, 142, 229, 0.06) 0%, transparent 65%);
+  animation: orbFloat 15s ease-in-out infinite;
+  animation-delay: -5s;
+}
+
+/* 网格叠加 */
+.bg-grid-overlay {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(47, 142, 229, 0.025) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(47, 142, 229, 0.025) 1px, transparent 1px);
+  background-size: 48px 48px;
+  pointer-events: none;
+}
+
+@keyframes orbFloat {
+  0%, 100% { transform: translate(0, 0) scale(1); }
+  33% { transform: translate(20px, -15px) scale(1.05); }
+  66% { transform: translate(-15px, 20px) scale(0.97); }
+}
+
+/* 标题区 */
+.visitor-hero {
+  text-align: center;
+  flex-shrink: 0;
+  position: relative;
+  z-index: 1;
+  margin-bottom: 40px;
+  padding-top: 64px;
 }
 
 .page-title {
-  font-size: 32px;
-  font-weight: 600;
+  font-size: 54px;
+  font-weight: 900;
   color: var(--text-primary);
-  margin: 0 0 40px;
+  letter-spacing: 3px;
+  margin: 0 0 6px;
+  line-height: 1.05;
+}
+
+.page-subtitle {
+  font-size: 22px;
+  color: var(--text-secondary);
+  margin: 0;
+  font-weight: 500;
+  line-height: 1.25;
+}
+
+/* 服务入口区 */
+.visitor-service-area {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  max-width: 1100px;
+  position: relative;
+  z-index: 1;
 }
 
+/* 入口卡片 */
 .visitor-options {
   display: grid;
   grid-template-columns: repeat(2, 1fr);
-  gap: 40px;
-  max-width: 700px;
+  gap: 48px;
   width: 100%;
 }
 
 .option-card {
+  height: 440px;
+  padding: 52px 56px 48px;
+  border-radius: 44px;
+  background: rgba(255, 255, 255, 0.92);
+  border: 1px solid rgba(255, 255, 255, 0.80);
+  box-shadow:
+    0 28px 80px rgba(30, 64, 175, 0.14),
+    0 2px 10px rgba(255, 255, 255, 0.72) inset;
+  backdrop-filter: blur(16px);
+  -webkit-backdrop-filter: blur(16px);
+  cursor: pointer;
+  transition: all 0.22s ease;
   display: flex;
   flex-direction: column;
-  align-items: center;
-  gap: 16px;
-  padding: 48px 32px;
-  background: var(--bg-card);
-  border-radius: var(--radius-xl);
-  box-shadow: var(--shadow-md);
-  cursor: pointer;
-  transition: all var(--transition-normal);
+  justify-content: center;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: transparent;
+  user-select: none;
 }
 
 .option-card:hover {
-  transform: translateY(-8px);
-  box-shadow: var(--shadow-xl);
+  transform: translateY(-6px);
+  box-shadow:
+    0 34px 88px rgba(30, 64, 175, 0.18),
+    0 2px 10px rgba(255, 255, 255, 0.78) inset;
 }
 
 .option-card:active {
-  transform: translateY(-4px);
+  transform: scale(0.984);
+}
+
+/* 卡片内容容器 */
+.card-body {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
 }
 
+/* 卡片图标 */
 .option-icon {
-  width: 80px;
-  height: 80px;
+  width: 120px;
+  height: 120px;
+  border-radius: 34px;
+  color: #fff;
   display: flex;
   align-items: center;
   justify-content: center;
-  border-radius: 20px;
-  color: white;
+  margin-bottom: 28px;
+  flex-shrink: 0;
 }
 
 .option-icon svg {
-  width: 44px;
-  height: 44px;
+  width: 62px;
+  height: 62px;
 }
 
-.option-card h3 {
-  font-size: 26px;
-  font-weight: 600;
+.appointment-icon {
+  background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+  box-shadow: 0 20px 38px rgba(37, 99, 235, 0.24);
+}
+
+.walkin-icon {
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  box-shadow: 0 20px 38px rgba(16, 185, 129, 0.24);
+}
+
+/* 卡片文字 */
+.option-content {
+  display: flex;
+  flex-direction: column;
+}
+
+.option-content h3 {
+  font-size: 46px;
+  font-weight: 900;
   color: var(--text-primary);
-  margin: 0;
+  margin: 0 0 12px;
+  letter-spacing: 1.5px;
+  line-height: 1.15;
 }
 
-.option-card p {
-  font-size: 16px;
-  color: var(--text-muted);
+.option-content p {
+  font-size: 24px;
+  color: var(--text-secondary);
+  line-height: 1.50;
   margin: 0;
+  max-width: 380px;
+  font-weight: 500;
+}
+
+/* 悬浮返回按钮 */
+.btn-back {
+  position: absolute;
+  left: 42px;
+  bottom: 30px;
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  height: 66px;
+  padding: 0 30px;
+  background: rgba(255, 255, 255, 0.88);
+  border: 1px solid rgba(47, 142, 229, 0.20);
+  border-radius: 999px;
+  box-shadow:
+    0 12px 36px rgba(47, 142, 229, 0.14),
+    0 2px 8px rgba(0, 0, 0, 0.06);
+  color: var(--text-secondary);
+  font-size: 23px;
+  font-weight: 800;
+  cursor: pointer;
+  transition: all 0.22s ease;
+  letter-spacing: 1.5px;
+  backdrop-filter: blur(12px);
+  -webkit-backdrop-filter: blur(12px);
+  z-index: 10;
+}
+
+.btn-back:hover {
+  background: rgba(255, 255, 255, 0.97);
+  color: var(--primary);
+  box-shadow:
+    0 18px 48px rgba(47, 142, 229, 0.22),
+    0 4px 12px rgba(0, 0, 0, 0.08);
+  transform: translateY(-3px);
+}
+
+.btn-back:active {
+  transform: scale(0.97);
+  box-shadow: 0 8px 24px rgba(47, 142, 229, 0.12);
+}
+
+.back-icon {
+  width: 22px;
+  height: 22px;
+  flex-shrink: 0;
+}
+
+/* 响应式 */
+@media (max-width: 900px), (max-height: 660px) {
+  .visitor-hero {
+    margin-bottom: 30px;
+    padding-top: 50px;
+  }
+
+  .page-title {
+    font-size: 46px;
+    letter-spacing: 2px;
+    margin-bottom: 5px;
+  }
+
+  .page-subtitle {
+    font-size: 20px;
+  }
+
+  .visitor-options {
+    gap: 36px;
+  }
+
+  .option-card {
+    height: 370px;
+    padding: 40px 44px 36px;
+    border-radius: 38px;
+  }
+
+  .option-icon {
+    width: 98px;
+    height: 98px;
+    border-radius: 30px;
+    margin-bottom: 28px;
+  }
+
+  .option-icon svg {
+    width: 52px;
+    height: 52px;
+  }
+
+  .option-content h3 {
+    font-size: 38px;
+    margin-bottom: 12px;
+  }
+
+  .option-content p {
+    font-size: 20px;
+    line-height: 1.42;
+  }
+
+  .btn-back {
+    left: 30px;
+    bottom: 24px;
+    height: 62px;
+    padding: 0 28px;
+    font-size: 21px;
+  }
 }
 </style>