show_appointment.html 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. {include file="header" /}
  2. <link rel="stylesheet" href="/assets/xtu/appointment.css?v=20260421-1931">
  3. {php}
  4. $appointment_status = 'open';
  5. $now = time();
  6. $starttime = isset($starttime) ? (int)$starttime : 0;
  7. $endtime = isset($endtime) ? (int)$endtime : 0;
  8. if ($starttime > 0 && $now < $starttime) {
  9. $appointment_status = 'upcoming';
  10. } elseif ($endtime > 0 && $now > $endtime) {
  11. $appointment_status = 'ended';
  12. }
  13. $appointment_status_text = $appointment_status === 'upcoming' ? cms_t('appointment_status_upcoming', '预约尚未开始') : ($appointment_status === 'ended' ? cms_t('appointment_status_ended', '预约已结束') : cms_t('appointment_status_open', '预约开启'));
  14. {/php}
  15. <main class="appointment-page">
  16. <section class="appointment-hero{if !empty($video)} appointment-hero-has-video{/if}"{if empty($video)} style="background-image:url({$thumbbig|default=$thumb|default='/assets/xtu/vendor/img/appbanner.jpg'});"{/if}>
  17. {if !empty($video)}
  18. <div class="appointment-hero-media">
  19. <video class="appointment-hero-video" autoplay muted loop playsinline preload="metadata" poster="{$thumb|default=$thumbbig|default='/assets/xtu/vendor/img/appbanner.jpg'}">
  20. <source src="{$video}" type="video/mp4">
  21. </video>
  22. </div>
  23. {/if}
  24. <div class="appointment-hero-overlay"></div>
  25. <div class="content-1300-wrap appointment-hero-inner">
  26. <div class="appointment-hero-copy">
  27. <h1>{$title}</h1>
  28. {if !empty($subtitle)}
  29. <div class="appointment-hero-subtitle">{$subtitle}</div>
  30. {/if}
  31. {if !empty($appointment_status_text)}
  32. <div class="appointment-hero-status">{$appointment_status_text}</div>
  33. {/if}
  34. <form class="appointment-form" id="appointmentForm" action="javascript:;" method="post" novalidate>
  35. <input type="hidden" name="activity_id" value="{$id}">
  36. <input type="hidden" name="activity_title" value="{$title}">
  37. <input type="hidden" name="source_page" id="appointmentSourcePage" value="">
  38. <input type="hidden" name="lang" id="appointmentLang" value="{$lang?:'cn'}">
  39. <label class="visually-hidden" for="appointmentMobile">{:cms_t('appointment_mobile_label','手机号')}</label>
  40. <input id="appointmentMobile" class="appointment-input" type="tel" name="mobile" maxlength="20" placeholder="{:cms_t('appointment_mobile_placeholder','手机号码')}">
  41. <label class="visually-hidden" for="appointmentEmail">{:cms_t('appointment_email_label','邮箱')}</label>
  42. <input id="appointmentEmail" class="appointment-input" type="email" name="email" maxlength="191" placeholder="{:cms_t('appointment_email_placeholder','邮箱地址')}">
  43. <button class="appointment-submit" id="appointmentSubmitBtn" type="submit">{:cms_t('appointment_submit','预约提醒')}</button>
  44. </form>
  45. </div>
  46. </div>
  47. </section>
  48. {if !empty($content)}
  49. <section class="appointment-intro-section">
  50. <div class="content-1300-wrap">
  51. <div class="appointment-intro">{$content|raw}</div>
  52. </div>
  53. </section>
  54. {/if}
  55. {if !empty($images)}
  56. <section class="appointment-gallery-section">
  57. <div class="content-1300-wrap">
  58. <div class="swiper appointment-gallery-swiper">
  59. <div class="swiper-wrapper">
  60. {volist name="images" id="img"}
  61. <div class="swiper-slide appointment-gallery-slide">
  62. <img src="{$img.url|default=$img}" alt="{$title}">
  63. </div>
  64. {/volist}
  65. </div>
  66. <button class="appointment-gallery-arrow appointment-gallery-prev" type="button" aria-label="Prev">
  67. <i class="bi bi-chevron-left"></i>
  68. </button>
  69. <button class="appointment-gallery-arrow appointment-gallery-next" type="button" aria-label="Next">
  70. <i class="bi bi-chevron-right"></i>
  71. </button>
  72. <div class="appointment-gallery-pagination"></div>
  73. </div>
  74. </div>
  75. </section>
  76. {/if}
  77. <section class="appointment-notice-section">
  78. <div class="content-1300-wrap">
  79. <div class="appointment-notice-box">
  80. <div class="appointment-notice-title">{:cms_t('appointment_notice_title','预约须知')}</div>
  81. <div class="appointment-notice-content">
  82. {if !empty($booking_notice)}
  83. {$booking_notice|raw}
  84. {else /}
  85. <p>{:cms_t('appointment_notice_default','提交预约信息后,我们将在合适时间通过短信或邮件与您联系,请确保联系方式填写正确。')}</p>
  86. {/if}
  87. </div>
  88. </div>
  89. </div>
  90. </section>
  91. </main>
  92. <script>
  93. document.getElementById('appointmentSourcePage').value = window.location.href;
  94. const appointmentTexts = {
  95. invalidMobile: "{:cms_t('appointment_invalid_mobile','请输入正确的手机号')}",
  96. invalidEmail: "{:cms_t('appointment_invalid_email','请输入正确的邮箱地址')}",
  97. success: "{:cms_t('appointment_success','预约成功')}",
  98. failed: "{:cms_t('appointment_failed','预约提交失败')}",
  99. retryLater: "{:cms_t('appointment_retry_later','提交失败,请稍后再试')}"
  100. };
  101. const appointmentForm = document.getElementById('appointmentForm');
  102. const appointmentMobile = document.getElementById('appointmentMobile');
  103. const appointmentEmail = document.getElementById('appointmentEmail');
  104. const appointmentLang = document.getElementById('appointmentLang');
  105. function resolveAppointmentLang() {
  106. const raw = (appointmentLang?.value || document.documentElement.getAttribute('lang') || 'cn').toLowerCase();
  107. return raw || 'cn';
  108. }
  109. function isChineseLang(lang) {
  110. return ['cn', 'zh', 'zh-cn'].includes((lang || '').toLowerCase());
  111. }
  112. function syncAppointmentFields() {
  113. const lang = resolveAppointmentLang();
  114. const chinese = isChineseLang(lang);
  115. if (appointmentLang) {
  116. appointmentLang.value = lang;
  117. }
  118. if (appointmentMobile) {
  119. appointmentMobile.style.display = chinese ? '' : 'none';
  120. appointmentMobile.disabled = !chinese;
  121. appointmentMobile.required = chinese;
  122. if (!chinese) appointmentMobile.value = '';
  123. }
  124. if (appointmentEmail) {
  125. appointmentEmail.style.display = chinese ? 'none' : '';
  126. appointmentEmail.disabled = chinese;
  127. appointmentEmail.required = !chinese;
  128. if (chinese) appointmentEmail.value = '';
  129. }
  130. }
  131. syncAppointmentFields();
  132. appointmentForm?.addEventListener('submit', async function (e) {
  133. e.preventDefault();
  134. const form = e.currentTarget;
  135. const submit = form.querySelector('.appointment-submit');
  136. const lang = resolveAppointmentLang();
  137. const chinese = isChineseLang(lang);
  138. const mobileValue = (appointmentMobile?.value || '').trim();
  139. const emailValue = (appointmentEmail?.value || '').trim();
  140. if (appointmentLang) {
  141. appointmentLang.value = lang;
  142. }
  143. if (chinese) {
  144. if (!/^1\d{10}$/.test(mobileValue)) {
  145. appointmentMobile?.focus();
  146. alert(appointmentTexts.invalidMobile);
  147. return;
  148. }
  149. } else {
  150. if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue)) {
  151. appointmentEmail?.focus();
  152. alert(appointmentTexts.invalidEmail);
  153. return;
  154. }
  155. }
  156. if (submit) submit.disabled = true;
  157. try {
  158. const body = new URLSearchParams(new FormData(form));
  159. const res = await fetch("{:url('index/appointment/submit')}", {
  160. method: 'POST',
  161. headers: {
  162. 'X-Requested-With': 'XMLHttpRequest',
  163. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  164. },
  165. body: body.toString()
  166. });
  167. const data = await res.json();
  168. if (data.code === 1) {
  169. alert(data.msg || appointmentTexts.success);
  170. form.reset();
  171. syncAppointmentFields();
  172. } else {
  173. alert(data.msg || appointmentTexts.failed);
  174. }
  175. } catch (err) {
  176. alert(appointmentTexts.retryLater);
  177. } finally {
  178. if (submit) submit.disabled = false;
  179. }
  180. });
  181. </script>
  182. {include file="footer" /}