AppointmentConfirm.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  1. <template>
  2. <div class="confirm-layout">
  3. <!-- 顶部状态栏 -->
  4. <StatusBar :title="robotName" />
  5. <!-- 主内容区 -->
  6. <div class="layout-main">
  7. <div class="confirm-page">
  8. <!-- 动态背景层 -->
  9. <div class="bg-layer">
  10. <div class="bg-orb orb-1"></div>
  11. <div class="bg-orb orb-2"></div>
  12. <div class="bg-orb orb-3"></div>
  13. <div class="bg-grid-overlay"></div>
  14. </div>
  15. <!-- 内容区 -->
  16. <div class="confirm-content">
  17. <!-- 标题区 -->
  18. <div class="confirm-hero">
  19. <h1 class="page-title">预约信息确认</h1>
  20. <p class="page-subtitle">请核对预约信息,如需调整可修改被访人与来访事由</p>
  21. </div>
  22. <!-- 表单卡片 -->
  23. <div class="form-card">
  24. <!-- 预约单号:只读 -->
  25. <div class="form-row form-row-readonly">
  26. <span class="form-label">预约单号</span>
  27. <span class="form-value">{{ form.appointmentNo || '--' }}</span>
  28. </div>
  29. <!-- 访客姓名:只读 -->
  30. <div class="form-row form-row-readonly">
  31. <span class="form-label">访客姓名</span>
  32. <span class="form-value">{{ form.visitorName }}</span>
  33. </div>
  34. <!-- 身份证号:只读 -->
  35. <div class="form-row form-row-readonly">
  36. <span class="form-label">身份证号</span>
  37. <span class="form-value">{{ maskIdCard(form.idCardNo || appointment?.idCardNo) }}</span>
  38. </div>
  39. <!-- 手机号码:只读 -->
  40. <div class="form-row form-row-readonly">
  41. <span class="form-label">手机号码</span>
  42. <span class="form-value">{{ form.mobile }}</span>
  43. </div>
  44. <!-- 被访人:可编辑 -->
  45. <div class="form-row form-row-editable" @click="openTextModal('visitedPerson', '修改被访人', form.visitedPerson)">
  46. <span class="form-label">被访人</span>
  47. <div class="form-value-editable-wrapper">
  48. <span v-if="form.visitedPerson" class="form-value form-value-editable">{{ form.visitedPerson }}</span>
  49. <span v-else class="form-value form-value-placeholder">请输入被访人</span>
  50. <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  51. <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
  52. <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
  53. </svg>
  54. </div>
  55. </div>
  56. <!-- 预约时间:只读 -->
  57. <div class="form-row form-row-readonly">
  58. <span class="form-label">预约时间</span>
  59. <span class="form-value">{{ form.appointmentTime || '--' }}</span>
  60. </div>
  61. <!-- 来访事由:可编辑 -->
  62. <div class="form-row form-row-editable" @click="openReasonModal">
  63. <span class="form-label">来访事由</span>
  64. <div class="form-value-editable-wrapper">
  65. <span v-if="currentReasonLabel" class="form-value form-value-editable">{{ currentReasonLabel }}</span>
  66. <span v-else class="form-value form-value-placeholder">请选择来访事由</span>
  67. <svg class="edit-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  68. <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
  69. <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
  70. </svg>
  71. </div>
  72. </div>
  73. </div>
  74. <!-- 表单卡片底部提示 -->
  75. <div class="form-hint">蓝色标记项可点击修改</div>
  76. <!-- 底部按钮 -->
  77. <div class="confirm-actions">
  78. <button class="btn-back" @click="goBack">
  79. <svg class="back-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
  80. <path d="M15 18l-6-6 6-6" />
  81. </svg>
  82. <span>返回核验</span>
  83. </button>
  84. <button class="btn-confirm" :disabled="loading" @click="handleConfirm">
  85. <span v-if="loading" class="loading-spinner"></span>
  86. <span v-else>确认登记</span>
  87. </button>
  88. </div>
  89. </div>
  90. </div>
  91. </div>
  92. <!-- ===== 文本编辑弹窗 ===== -->
  93. <Teleport to="body">
  94. <div v-if="showTextModal" class="modal-overlay" @click.self="closeTextModal">
  95. <div class="modal-card">
  96. <div class="modal-header">
  97. <h2>{{ textModalTitle }}</h2>
  98. </div>
  99. <div class="modal-input-wrapper">
  100. <input
  101. ref="textInputRef"
  102. v-model="tempText"
  103. class="modal-text-input"
  104. type="text"
  105. :placeholder="textModalPlaceholder"
  106. @keydown.enter="confirmTextEdit"
  107. />
  108. </div>
  109. <div class="modal-text-actions">
  110. <button class="btn-modal-cancel" @click="closeTextModal">取消</button>
  111. <button class="btn-modal-save" @click="confirmTextEdit">保存</button>
  112. </div>
  113. </div>
  114. </div>
  115. </Teleport>
  116. <!-- ===== 来访事由选项弹窗 ===== -->
  117. <Teleport to="body">
  118. <div v-if="showReasonModal" class="modal-overlay" @click.self="closeReasonModal">
  119. <div class="modal-card modal-reason-card">
  120. <div class="modal-header">
  121. <h2>选择来访事由</h2>
  122. </div>
  123. <div class="reason-options">
  124. <button
  125. v-for="opt in visitReasonOptions"
  126. :key="opt.value"
  127. class="reason-btn"
  128. :class="{ 'reason-btn-active': tempReason === opt.value }"
  129. @click="tempReason = opt.value"
  130. >
  131. {{ opt.label }}
  132. </button>
  133. </div>
  134. <div class="modal-reason-actions">
  135. <button class="btn-modal-cancel" @click="closeReasonModal">取消</button>
  136. <button class="btn-modal-save" @click="confirmReasonEdit">保存</button>
  137. </div>
  138. </div>
  139. </div>
  140. </Teleport>
  141. </div>
  142. </template>
  143. <script setup>
  144. import { ref, reactive, computed, nextTick } from 'vue'
  145. import { useRouter } from 'vue-router'
  146. import StatusBar from '@/components/StatusBar.vue'
  147. import { useScreenStore } from '@/stores/screen'
  148. import { useVisitorStore } from '@/stores/visitor'
  149. import { maskIdCard } from '@/utils/device'
  150. const router = useRouter()
  151. const screenStore = useScreenStore()
  152. const visitorStore = useVisitorStore()
  153. const robotName = computed(() => screenStore.screenTheme?.robotName || '迎宾巡逻机器人')
  154. const loading = ref(false)
  155. const visitReasonOptions = visitorStore.visitReasonOptions
  156. const appointment = visitorStore.appointmentInfo
  157. // 表单数据
  158. const form = reactive({
  159. appointmentNo: '',
  160. visitorName: '',
  161. idCardNo: '',
  162. mobile: '',
  163. visitedPerson: '',
  164. appointmentTime: '',
  165. visitPurpose: ''
  166. })
  167. // 初始化表单数据
  168. const initForm = () => {
  169. if (appointment) {
  170. form.appointmentNo = appointment.appointmentNo || ''
  171. form.visitorName = appointment.visitorName || ''
  172. form.idCardNo = appointment.idCardNo || ''
  173. form.mobile = appointment.mobile || ''
  174. form.visitedPerson = appointment.visitedPerson || ''
  175. form.appointmentTime = appointment.appointmentTime || ''
  176. form.visitPurpose = appointment.visitPurpose || appointment.visitReason || ''
  177. }
  178. }
  179. initForm()
  180. // 来访事由标签
  181. const currentReasonLabel = computed(() => {
  182. const found = visitReasonOptions.find(o => o.value === form.visitPurpose)
  183. return found ? found.label : ''
  184. })
  185. // ===== 文本编辑弹窗 =====
  186. const showTextModal = ref(false)
  187. const textModalField = ref('')
  188. const textModalTitle = ref('')
  189. const textModalPlaceholder = ref('')
  190. const tempText = ref('')
  191. const textInputRef = ref(null)
  192. const openTextModal = (field, title, currentValue) => {
  193. textModalField.value = field
  194. textModalTitle.value = title
  195. textModalPlaceholder.value = title.replace('修改', '请输入')
  196. tempText.value = currentValue
  197. showTextModal.value = true
  198. nextTick(() => {
  199. if (textInputRef.value) {
  200. textInputRef.value.focus()
  201. }
  202. })
  203. }
  204. const closeTextModal = () => {
  205. showTextModal.value = false
  206. textModalField.value = ''
  207. }
  208. const confirmTextEdit = () => {
  209. if (!tempText.value.trim()) {
  210. screenStore.showAlert({
  211. type: 'warning',
  212. message: `${textModalTitle.value.replace('修改', '')}不能为空`,
  213. duration: 3000
  214. })
  215. return
  216. }
  217. if (textModalField.value === 'visitedPerson') {
  218. form.visitedPerson = tempText.value
  219. }
  220. closeTextModal()
  221. }
  222. // ===== 来访事由选项弹窗 =====
  223. const showReasonModal = ref(false)
  224. const tempReason = ref('')
  225. const openReasonModal = () => {
  226. tempReason.value = form.visitPurpose
  227. showReasonModal.value = true
  228. }
  229. const closeReasonModal = () => {
  230. showReasonModal.value = false
  231. }
  232. const confirmReasonEdit = () => {
  233. if (!tempReason.value) {
  234. screenStore.showAlert({
  235. type: 'warning',
  236. message: '请选择来访事由',
  237. duration: 3000
  238. })
  239. return
  240. }
  241. form.visitPurpose = tempReason.value
  242. closeReasonModal()
  243. }
  244. // ===== 导航 =====
  245. const goBack = () => {
  246. router.push('/visitor/appointment')
  247. }
  248. // ===== 确认登记 =====
  249. const handleConfirm = async () => {
  250. if (loading.value) return
  251. // 手机号校验
  252. if (!form.mobile || !/^1[3-9]\d{9}$/.test(form.mobile)) {
  253. screenStore.showAlert({
  254. type: 'warning',
  255. message: '请输入正确的11位手机号',
  256. duration: 3000
  257. })
  258. return
  259. }
  260. // 被访人非空校验
  261. if (!form.visitedPerson || !form.visitedPerson.trim()) {
  262. screenStore.showAlert({
  263. type: 'warning',
  264. message: '请填写被访人姓名',
  265. duration: 3000
  266. })
  267. return
  268. }
  269. // 来访事由非空校验
  270. if (!form.visitPurpose) {
  271. screenStore.showAlert({
  272. type: 'warning',
  273. message: '请选择来访事由',
  274. duration: 3000
  275. })
  276. return
  277. }
  278. loading.value = true
  279. try {
  280. visitorStore.appointmentInfo = {
  281. ...visitorStore.appointmentInfo,
  282. ...form,
  283. visitReason: form.visitPurpose
  284. }
  285. await visitorStore.submitRegistration()
  286. router.push('/visitor/success')
  287. } catch (error) {
  288. screenStore.showAlert({
  289. type: 'error',
  290. message: '登记失败,请重试',
  291. duration: 3000
  292. })
  293. } finally {
  294. loading.value = false
  295. }
  296. }
  297. </script>
  298. <style scoped>
  299. /* ===== 布局结构 ===== */
  300. .confirm-layout {
  301. width: 100vw;
  302. height: 100vh;
  303. display: flex;
  304. flex-direction: column;
  305. overflow: hidden;
  306. background: linear-gradient(155deg, #e8f4fd 0%, #dbeafe 40%, #eff6ff 100%);
  307. }
  308. .layout-main {
  309. flex: 1;
  310. display: flex;
  311. flex-direction: column;
  312. overflow: hidden;
  313. }
  314. .confirm-page {
  315. flex: 1;
  316. display: flex;
  317. flex-direction: column;
  318. position: relative;
  319. overflow: hidden;
  320. }
  321. /* ===== 动态背景层 ===== */
  322. .bg-layer {
  323. position: absolute;
  324. inset: 0;
  325. overflow: hidden;
  326. pointer-events: none;
  327. z-index: 0;
  328. }
  329. .bg-orb {
  330. position: absolute;
  331. border-radius: 50%;
  332. filter: blur(60px);
  333. opacity: 0.45;
  334. }
  335. .orb-1 {
  336. width: 380px;
  337. height: 380px;
  338. background: radial-gradient(circle, rgba(59, 130, 246, 0.35) 0%, transparent 70%);
  339. top: -80px;
  340. right: -80px;
  341. animation: float1 18s ease-in-out infinite;
  342. }
  343. .orb-2 {
  344. width: 300px;
  345. height: 300px;
  346. background: radial-gradient(circle, rgba(99, 102, 241, 0.30) 0%, transparent 70%);
  347. bottom: -40px;
  348. left: -60px;
  349. animation: float2 22s ease-in-out infinite;
  350. }
  351. .orb-3 {
  352. width: 240px;
  353. height: 240px;
  354. background: radial-gradient(circle, rgba(14, 165, 233, 0.28) 0%, transparent 70%);
  355. top: 50%;
  356. left: 50%;
  357. transform: translate(-50%, -50%);
  358. animation: float3 16s ease-in-out infinite;
  359. }
  360. .bg-grid-overlay {
  361. position: absolute;
  362. inset: 0;
  363. background-image:
  364. linear-gradient(rgba(147, 197, 253, 0.15) 1px, transparent 1px),
  365. linear-gradient(90deg, rgba(147, 197, 253, 0.15) 1px, transparent 1px);
  366. background-size: 48px 48px;
  367. mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 40%, transparent 100%);
  368. -webkit-mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 40%, transparent 100%);
  369. }
  370. @keyframes float1 {
  371. 0%, 100% { transform: translate(0, 0) scale(1); }
  372. 33% { transform: translate(-20px, 25px) scale(1.05); }
  373. 66% { transform: translate(15px, -15px) scale(0.96); }
  374. }
  375. @keyframes float2 {
  376. 0%, 100% { transform: translate(0, 0) scale(1); }
  377. 40% { transform: translate(25px, -20px) scale(1.08); }
  378. 70% { transform: translate(-10px, 15px) scale(0.95); }
  379. }
  380. @keyframes float3 {
  381. 0%, 100% { transform: translate(-50%, -50%) scale(1); }
  382. 50% { transform: translate(-45%, -55%) scale(1.1); }
  383. }
  384. /* ===== 内容区 ===== */
  385. .confirm-content {
  386. flex: 1;
  387. display: flex;
  388. flex-direction: column;
  389. align-items: center;
  390. padding: 0 32px 32px;
  391. position: relative;
  392. z-index: 1;
  393. overflow-y: auto;
  394. }
  395. /* ===== 标题区 ===== */
  396. .confirm-hero {
  397. text-align: center;
  398. margin: 28px 0 24px;
  399. flex-shrink: 0;
  400. }
  401. .page-title {
  402. font-size: 40px;
  403. font-weight: 900;
  404. color: var(--text-primary);
  405. margin: 0 0 10px;
  406. letter-spacing: 3px;
  407. }
  408. .page-subtitle {
  409. font-size: 20px;
  410. color: var(--text-secondary);
  411. margin: 0;
  412. font-weight: 500;
  413. }
  414. /* ===== 表单卡片 ===== */
  415. .form-card {
  416. width: 100%;
  417. max-width: 640px;
  418. background: rgba(255, 255, 255, 0.88);
  419. border-radius: 32px;
  420. padding: 8px 0;
  421. box-shadow:
  422. 0 24px 64px rgba(30, 64, 175, 0.10),
  423. 0 8px 24px rgba(0, 0, 0, 0.06);
  424. backdrop-filter: blur(20px);
  425. -webkit-backdrop-filter: blur(20px);
  426. border: 1px solid rgba(255, 255, 255, 0.60);
  427. margin-bottom: 12px;
  428. }
  429. .form-hint {
  430. text-align: center;
  431. font-size: 16px;
  432. color: #94a3b8;
  433. font-weight: 500;
  434. letter-spacing: 0.5px;
  435. margin-bottom: 16px;
  436. }
  437. .form-row {
  438. display: flex;
  439. align-items: center;
  440. padding: 0 32px;
  441. min-height: 80px;
  442. border-bottom: 1px solid rgba(226, 232, 240, 0.80);
  443. }
  444. .form-row:last-child {
  445. border-bottom: none;
  446. }
  447. .form-row-readonly {
  448. cursor: default;
  449. }
  450. .form-row-editable {
  451. cursor: pointer;
  452. transition: background 0.15s;
  453. }
  454. .form-row-editable:hover {
  455. background: rgba(59, 130, 246, 0.04);
  456. }
  457. .form-row-editable:active {
  458. background: rgba(59, 130, 246, 0.08);
  459. }
  460. .form-label {
  461. font-size: 22px;
  462. font-weight: 600;
  463. color: var(--text-secondary);
  464. width: 140px;
  465. flex-shrink: 0;
  466. letter-spacing: 0.5px;
  467. }
  468. .form-value {
  469. flex: 1;
  470. font-size: 24px;
  471. font-weight: 700;
  472. color: var(--text-primary);
  473. text-align: right;
  474. letter-spacing: 1px;
  475. }
  476. .form-value-editable-wrapper {
  477. flex: 1;
  478. display: flex;
  479. align-items: center;
  480. justify-content: flex-end;
  481. gap: 8px;
  482. }
  483. .form-value-editable {
  484. color: var(--primary);
  485. }
  486. .form-value-placeholder {
  487. color: #94a3b8;
  488. font-weight: 500;
  489. font-size: 22px;
  490. }
  491. .edit-icon {
  492. width: 22px;
  493. height: 22px;
  494. color: var(--primary);
  495. flex-shrink: 0;
  496. opacity: 0.7;
  497. transition: opacity 0.15s;
  498. }
  499. .form-row-editable:hover .edit-icon {
  500. opacity: 1;
  501. }
  502. /* ===== 底部按钮 ===== */
  503. .confirm-actions {
  504. display: grid;
  505. grid-template-columns: 1fr 1.8fr;
  506. gap: 16px;
  507. width: 100%;
  508. max-width: 640px;
  509. flex-shrink: 0;
  510. }
  511. .btn-back,
  512. .btn-confirm {
  513. height: 78px;
  514. font-size: 24px;
  515. font-weight: 800;
  516. border-radius: 999px;
  517. cursor: pointer;
  518. transition: all 0.22s ease;
  519. letter-spacing: 1.5px;
  520. display: flex;
  521. align-items: center;
  522. justify-content: center;
  523. }
  524. .btn-back {
  525. background: rgba(255, 255, 255, 0.80);
  526. color: var(--text-secondary);
  527. border: 2px solid var(--border-light);
  528. backdrop-filter: blur(10px);
  529. -webkit-backdrop-filter: blur(10px);
  530. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
  531. gap: 8px;
  532. }
  533. .btn-back:hover {
  534. background: rgba(255, 255, 255, 0.95);
  535. border-color: var(--text-muted);
  536. transform: translateY(-2px);
  537. box-shadow: 0 12px 32px rgba(0, 0, 0, 0.10);
  538. }
  539. .btn-back:active {
  540. transform: scale(0.97);
  541. }
  542. .back-icon {
  543. width: 22px;
  544. height: 22px;
  545. flex-shrink: 0;
  546. }
  547. .btn-confirm {
  548. background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
  549. color: white;
  550. border: none;
  551. box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
  552. }
  553. .btn-confirm:hover:not(:disabled) {
  554. box-shadow: 0 16px 36px rgba(37, 99, 235, 0.36);
  555. transform: translateY(-2px);
  556. }
  557. .btn-confirm:active {
  558. transform: scale(0.97);
  559. }
  560. .btn-confirm:disabled {
  561. background: linear-gradient(135deg, #93c5fd 0%, #60a5fa 100%);
  562. cursor: not-allowed;
  563. box-shadow: none;
  564. transform: none;
  565. }
  566. .loading-spinner {
  567. width: 26px;
  568. height: 26px;
  569. border: 3px solid rgba(255, 255, 255, 0.35);
  570. border-top-color: white;
  571. border-radius: 50%;
  572. animation: spin 1s linear infinite;
  573. }
  574. @keyframes spin {
  575. to { transform: rotate(360deg); }
  576. }
  577. /* ===== 弹窗遮罩 ===== */
  578. .modal-overlay {
  579. position: fixed;
  580. inset: 0;
  581. background: rgba(0, 0, 0, 0.50);
  582. backdrop-filter: blur(4px);
  583. -webkit-backdrop-filter: blur(4px);
  584. display: flex;
  585. align-items: center;
  586. justify-content: center;
  587. z-index: 1000;
  588. animation: fadeIn 0.2s ease;
  589. }
  590. @keyframes fadeIn {
  591. from { opacity: 0; }
  592. to { opacity: 1; }
  593. }
  594. /* ===== 弹窗卡片 ===== */
  595. .modal-card {
  596. width: 560px;
  597. background: rgba(255, 255, 255, 0.96);
  598. border-radius: 32px;
  599. padding: 40px 36px 36px;
  600. box-shadow:
  601. 0 40px 80px rgba(0, 0, 0, 0.20),
  602. 0 16px 40px rgba(0, 0, 0, 0.12);
  603. backdrop-filter: blur(20px);
  604. -webkit-backdrop-filter: blur(20px);
  605. animation: slideUp 0.22s ease;
  606. }
  607. @keyframes slideUp {
  608. from { transform: translateY(20px) scale(0.98); opacity: 0; }
  609. to { transform: translateY(0) scale(1); opacity: 1; }
  610. }
  611. /* ===== 弹窗标题 ===== */
  612. .modal-header {
  613. text-align: center;
  614. margin-bottom: 24px;
  615. }
  616. .modal-header h2 {
  617. font-size: 30px;
  618. font-weight: 800;
  619. color: var(--text-primary);
  620. margin: 0 0 8px;
  621. letter-spacing: 2px;
  622. }
  623. /* ===== 文本输入弹窗 ===== */
  624. .modal-input-wrapper {
  625. background: linear-gradient(160deg, #f0f7ff 0%, #e8f4fd 100%);
  626. border: 2px solid rgba(59, 130, 246, 0.18);
  627. border-radius: 20px;
  628. padding: 0 24px;
  629. height: 96px;
  630. display: flex;
  631. align-items: center;
  632. margin-bottom: 16px;
  633. }
  634. .modal-text-input {
  635. width: 100%;
  636. height: 100%;
  637. font-size: 30px;
  638. font-weight: 700;
  639. color: var(--text-primary);
  640. background: transparent;
  641. border: none;
  642. outline: none;
  643. letter-spacing: 1px;
  644. }
  645. .modal-text-input::placeholder {
  646. font-size: 26px;
  647. font-weight: 500;
  648. color: #b0bec5;
  649. }
  650. .modal-text-actions {
  651. display: grid;
  652. grid-template-columns: 1fr 1.5fr;
  653. gap: 14px;
  654. }
  655. /* ===== 来访事由选项弹窗 ===== */
  656. .modal-reason-card {
  657. width: 560px;
  658. }
  659. .reason-options {
  660. display: grid;
  661. grid-template-columns: repeat(2, 1fr);
  662. gap: 14px;
  663. margin-bottom: 20px;
  664. }
  665. .reason-btn {
  666. height: 72px;
  667. font-size: 22px;
  668. font-weight: 700;
  669. color: var(--text-primary);
  670. background: rgba(255, 255, 255, 0.80);
  671. border: 2px solid var(--border-light);
  672. border-radius: 20px;
  673. cursor: pointer;
  674. transition: all 0.18s ease;
  675. letter-spacing: 0.5px;
  676. display: flex;
  677. align-items: center;
  678. justify-content: center;
  679. }
  680. .reason-btn:hover {
  681. background: rgba(59, 130, 246, 0.06);
  682. border-color: rgba(59, 130, 246, 0.30);
  683. transform: translateY(-1px);
  684. }
  685. .reason-btn:active {
  686. transform: scale(0.97);
  687. }
  688. .reason-btn-active {
  689. background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
  690. color: white;
  691. border-color: transparent;
  692. box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
  693. }
  694. .reason-btn-active:hover {
  695. background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
  696. border-color: transparent;
  697. box-shadow: 0 10px 24px rgba(37, 99, 235, 0.30);
  698. }
  699. .modal-reason-actions {
  700. display: grid;
  701. grid-template-columns: 1fr 1.5fr;
  702. gap: 14px;
  703. }
  704. /* ===== 弹窗通用按钮 ===== */
  705. .btn-modal-cancel,
  706. .btn-modal-save {
  707. height: 72px;
  708. font-size: 24px;
  709. font-weight: 800;
  710. border-radius: 999px;
  711. cursor: pointer;
  712. transition: all 0.20s ease;
  713. letter-spacing: 1.5px;
  714. display: flex;
  715. align-items: center;
  716. justify-content: center;
  717. }
  718. .btn-modal-cancel {
  719. background: rgba(255, 255, 255, 0.80);
  720. color: var(--text-secondary);
  721. border: 2px solid var(--border-light);
  722. box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
  723. }
  724. .btn-modal-cancel:hover {
  725. background: rgba(255, 255, 255, 0.95);
  726. border-color: var(--text-muted);
  727. transform: translateY(-1px);
  728. }
  729. .btn-modal-save {
  730. background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
  731. color: white;
  732. border: none;
  733. box-shadow: 0 10px 24px rgba(37, 99, 235, 0.26);
  734. }
  735. .btn-modal-save:hover {
  736. box-shadow: 0 14px 32px rgba(37, 99, 235, 0.34);
  737. transform: translateY(-1px);
  738. }
  739. .btn-modal-cancel:active,
  740. .btn-modal-save:active {
  741. transform: scale(0.97);
  742. }
  743. /* ===== 响应式适配 ===== */
  744. @media (max-height: 700px) {
  745. .confirm-content {
  746. padding: 0 24px 20px;
  747. }
  748. .confirm-hero {
  749. margin: 16px 0 16px;
  750. }
  751. .page-title {
  752. font-size: 32px;
  753. }
  754. .page-subtitle {
  755. font-size: 18px;
  756. }
  757. .form-row {
  758. min-height: 68px;
  759. }
  760. .form-label {
  761. font-size: 20px;
  762. width: 120px;
  763. }
  764. .form-value {
  765. font-size: 22px;
  766. }
  767. .confirm-actions {
  768. grid-template-columns: 1fr 1.5fr;
  769. }
  770. .btn-back,
  771. .btn-confirm {
  772. height: 68px;
  773. font-size: 22px;
  774. }
  775. }
  776. </style>