Success.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <template>
  2. <div class="success-layout">
  3. <StatusBar :title="robotName" />
  4. <div class="layout-main">
  5. <div class="success-page">
  6. <!-- 动态背景层 -->
  7. <div class="bg-layer">
  8. <div class="bg-orb orb-1"></div>
  9. <div class="bg-orb orb-2"></div>
  10. <div class="bg-orb orb-3"></div>
  11. <div class="bg-grid-overlay"></div>
  12. </div>
  13. <!-- 内容区 -->
  14. <div class="success-content">
  15. <!-- 成功图标 -->
  16. <div class="success-icon-wrap">
  17. <svg class="success-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
  18. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
  19. <polyline points="22 4 12 14.01 9 11.01" />
  20. </svg>
  21. </div>
  22. <!-- 标题 -->
  23. <h1 class="success-title">登记成功</h1>
  24. <p class="success-subtitle">欢迎您的到来</p>
  25. <!-- 信息卡片 -->
  26. <div class="info-card">
  27. <div class="info-row">
  28. <span class="info-label">访客姓名</span>
  29. <span class="info-value">{{ visitorName }}</span>
  30. </div>
  31. <div class="info-row">
  32. <span class="info-label">手机号码</span>
  33. <span class="info-value">{{ maskedMobile }}</span>
  34. </div>
  35. <div class="info-row">
  36. <span class="info-label">被访人</span>
  37. <span class="info-value">{{ visitedPerson }}</span>
  38. </div>
  39. <div class="info-row">
  40. <span class="info-label">登记时间</span>
  41. <span class="info-value">{{ registerTime }}</span>
  42. </div>
  43. </div>
  44. <!-- 温馨提示 -->
  45. <div class="hint-text">
  46. 请前往 {{ visitedPerson || '被访人' }} 处办理来访事项
  47. </div>
  48. <!-- 自动返回提示 -->
  49. <div v-if="!isDev && showCountdown" class="auto-hint">
  50. {{ countdown }} 秒后自动返回待机
  51. </div>
  52. <!-- 底部按钮 -->
  53. <button class="btn-return" @click="goToIdle">
  54. 返回待机
  55. </button>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. </template>
  61. <script setup>
  62. import { ref, computed, onMounted, onUnmounted } from 'vue'
  63. import { useRouter } from 'vue-router'
  64. import { useVisitorStore } from '@/stores/visitor'
  65. import { useScreenStore } from '@/stores/screen'
  66. import { formatDateTime } from '@/utils/time'
  67. import StatusBar from '@/components/StatusBar.vue'
  68. const router = useRouter()
  69. const visitorStore = useVisitorStore()
  70. const screenStore = useScreenStore()
  71. const robotName = computed(() => screenStore.robotName || '智能迎宾机器人')
  72. const isDev = import.meta.env.DEV
  73. // 开发环境 Mock 数据
  74. const mockInfo = {
  75. visitorName: '张三',
  76. mobile: '13800000000',
  77. visitedPerson: '李经理',
  78. registerTime: formatDateTime(),
  79. registerType: 'walk_in'
  80. }
  81. // 获取登记信息(优先真实数据,DEV 环境兜底 Mock)
  82. const info = computed(() => {
  83. const real = visitorStore.registrationInfo
  84. if (real?.visitorName) return real
  85. if (isDev) return mockInfo
  86. return { visitorName: '访客', mobile: '', visitedPerson: '--', registerTime: formatDateTime() }
  87. })
  88. const visitorName = computed(() => info.value.visitorName || '访客')
  89. const maskedMobile = computed(() => {
  90. const mobile = info.value.mobile || ''
  91. if (!mobile) return '--'
  92. if (mobile.length === 11) {
  93. return `${mobile.slice(0, 3)}****${mobile.slice(7)}`
  94. }
  95. return mobile
  96. })
  97. const visitedPerson = computed(() => info.value.visitedPerson || '--')
  98. const registerTime = computed(() => info.value.registerTime || info.value.registeredTime || formatDateTime())
  99. // 自动返回计时
  100. const countdown = ref(10)
  101. const showCountdown = ref(false)
  102. let countdownTimer = null
  103. let autoReturnTimer = null
  104. const goToIdle = () => {
  105. visitorStore.clearVisitorData()
  106. router.push('/idle')
  107. }
  108. const startCountdown = () => {
  109. showCountdown.value = true
  110. countdown.value = 10
  111. countdownTimer = setInterval(() => {
  112. countdown.value--
  113. if (countdown.value <= 0) {
  114. clearInterval(countdownTimer)
  115. }
  116. }, 1000)
  117. }
  118. onMounted(() => {
  119. // PROD 环境:10 秒后自动返回待机
  120. if (!isDev) {
  121. startCountdown()
  122. autoReturnTimer = setTimeout(() => {
  123. goToIdle()
  124. }, 10000)
  125. }
  126. })
  127. onUnmounted(() => {
  128. if (countdownTimer) clearInterval(countdownTimer)
  129. if (autoReturnTimer) clearTimeout(autoReturnTimer)
  130. })
  131. </script>
  132. <style scoped>
  133. /* ===== 整体布局 ===== */
  134. .success-layout {
  135. width: 100vw;
  136. height: 100vh;
  137. display: flex;
  138. flex-direction: column;
  139. overflow: hidden;
  140. }
  141. .layout-main {
  142. flex: 1;
  143. display: flex;
  144. flex-direction: column;
  145. min-height: 0;
  146. position: relative;
  147. }
  148. /* ===== 页面容器 ===== */
  149. .success-page {
  150. flex: 1;
  151. display: flex;
  152. flex-direction: column;
  153. align-items: center;
  154. justify-content: center;
  155. padding: 0 32px 28px;
  156. position: relative;
  157. z-index: 1;
  158. overflow-y: auto;
  159. }
  160. /* ===== 动态背景层 ===== */
  161. .bg-layer {
  162. position: absolute;
  163. inset: 0;
  164. overflow: hidden;
  165. pointer-events: none;
  166. z-index: 0;
  167. }
  168. .bg-orb {
  169. position: absolute;
  170. border-radius: 50%;
  171. filter: blur(60px);
  172. opacity: 0.5;
  173. }
  174. .orb-1 {
  175. width: 400px;
  176. height: 400px;
  177. background: radial-gradient(circle, rgba(59, 130, 246, 0.35) 0%, transparent 70%);
  178. top: -100px;
  179. right: -80px;
  180. animation: float 8s ease-in-out infinite;
  181. }
  182. .orb-2 {
  183. width: 350px;
  184. height: 350px;
  185. background: radial-gradient(circle, rgba(16, 185, 129, 0.28) 0%, transparent 70%);
  186. bottom: -60px;
  187. left: -60px;
  188. animation: float 10s ease-in-out infinite reverse;
  189. }
  190. .orb-3 {
  191. width: 280px;
  192. height: 280px;
  193. background: radial-gradient(circle, rgba(99, 102, 241, 0.22) 0%, transparent 70%);
  194. top: 40%;
  195. left: 30%;
  196. animation: float 12s ease-in-out infinite;
  197. }
  198. @keyframes float {
  199. 0%, 100% { transform: translateY(0) scale(1); }
  200. 50% { transform: translateY(-20px) scale(1.05); }
  201. }
  202. .bg-grid-overlay {
  203. position: absolute;
  204. inset: 0;
  205. background-image:
  206. linear-gradient(rgba(59, 130, 246, 0.04) 1px, transparent 1px),
  207. linear-gradient(90deg, rgba(59, 130, 246, 0.04) 1px, transparent 1px);
  208. background-size: 48px 48px;
  209. }
  210. /* ===== 内容区 ===== */
  211. .success-content {
  212. display: flex;
  213. flex-direction: column;
  214. align-items: center;
  215. width: 100%;
  216. max-width: 680px;
  217. gap: 20px;
  218. animation: fadeInUp 0.5s ease-out;
  219. }
  220. @keyframes fadeInUp {
  221. from { opacity: 0; transform: translateY(24px); }
  222. to { opacity: 1; transform: translateY(0); }
  223. }
  224. /* ===== 成功图标 ===== */
  225. .success-icon-wrap {
  226. width: 130px;
  227. height: 130px;
  228. background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  229. border-radius: 50%;
  230. display: flex;
  231. align-items: center;
  232. justify-content: center;
  233. box-shadow: 0 16px 40px rgba(16, 185, 129, 0.30);
  234. animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s backwards;
  235. }
  236. @keyframes scaleIn {
  237. from { opacity: 0; transform: scale(0.4); }
  238. to { opacity: 1; transform: scale(1); }
  239. }
  240. .success-icon {
  241. width: 70px;
  242. height: 70px;
  243. color: white;
  244. stroke-width: 2.5;
  245. }
  246. /* ===== 标题 ===== */
  247. .success-title {
  248. font-size: 56px;
  249. font-weight: 900;
  250. color: var(--text-primary);
  251. margin: 0;
  252. letter-spacing: 3px;
  253. text-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  254. }
  255. .success-subtitle {
  256. font-size: 26px;
  257. color: var(--text-secondary);
  258. margin: 0;
  259. font-weight: 500;
  260. letter-spacing: 1px;
  261. }
  262. /* ===== 信息卡片 ===== */
  263. .info-card {
  264. width: 100%;
  265. background: rgba(255, 255, 255, 0.90);
  266. border-radius: 28px;
  267. padding: 8px 0;
  268. box-shadow:
  269. 0 12px 40px rgba(30, 64, 175, 0.10),
  270. 0 4px 12px rgba(0, 0, 0, 0.06);
  271. backdrop-filter: blur(12px);
  272. -webkit-backdrop-filter: blur(12px);
  273. border: 1px solid rgba(255, 255, 255, 0.6);
  274. margin-top: 8px;
  275. }
  276. .info-row {
  277. display: flex;
  278. align-items: center;
  279. padding: 18px 36px;
  280. gap: 24px;
  281. }
  282. .info-row + .info-row {
  283. border-top: 1px solid rgba(0, 0, 0, 0.05);
  284. }
  285. .info-label {
  286. font-size: 24px;
  287. color: var(--text-muted);
  288. font-weight: 600;
  289. flex-shrink: 0;
  290. min-width: 100px;
  291. }
  292. .info-value {
  293. font-size: 26px;
  294. color: var(--text-primary);
  295. font-weight: 700;
  296. text-align: right;
  297. flex: 1;
  298. word-break: break-all;
  299. }
  300. /* ===== 温馨提示 ===== */
  301. .hint-text {
  302. font-size: 22px;
  303. color: var(--text-secondary);
  304. font-weight: 500;
  305. text-align: center;
  306. margin-top: 4px;
  307. }
  308. /* ===== 自动返回提示 ===== */
  309. .auto-hint {
  310. font-size: 18px;
  311. color: var(--text-muted);
  312. text-align: center;
  313. margin-top: -8px;
  314. }
  315. /* ===== 返回按钮 ===== */
  316. .btn-return {
  317. width: 380px;
  318. height: 80px;
  319. font-size: 28px;
  320. font-weight: 800;
  321. letter-spacing: 2px;
  322. background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
  323. color: white;
  324. border: none;
  325. border-radius: 999px;
  326. cursor: pointer;
  327. transition: all 0.22s ease;
  328. box-shadow: 0 12px 28px rgba(37, 99, 235, 0.30);
  329. margin-top: 12px;
  330. display: flex;
  331. align-items: center;
  332. justify-content: center;
  333. }
  334. .btn-return:hover {
  335. box-shadow: 0 16px 36px rgba(37, 99, 235, 0.38);
  336. transform: translateY(-2px);
  337. }
  338. .btn-return:active {
  339. transform: scale(0.97);
  340. }
  341. </style>