page_fankui.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. {include file="header" /}
  2. {php}
  3. $feedbackContactPhone = '';
  4. $feedbackContactTel = '';
  5. foreach (['cooperation_phone', 'contact_phone', 'service_phone', 'phone', 'tel'] as $phoneConfigKey) {
  6. $phoneConfigValue = trim((string)config('site.' . $phoneConfigKey));
  7. if ($phoneConfigValue === '') {
  8. continue;
  9. }
  10. $phoneDigits = '';
  11. $phoneAllowed = true;
  12. $phoneTel = '';
  13. foreach (preg_split('//u', $phoneConfigValue, -1, PREG_SPLIT_NO_EMPTY) as $phoneChar) {
  14. if (ctype_digit($phoneChar)) {
  15. $phoneDigits .= $phoneChar;
  16. $phoneTel .= $phoneChar;
  17. continue;
  18. }
  19. if ($phoneChar === '+') {
  20. $phoneTel .= $phoneChar;
  21. continue;
  22. }
  23. if (!in_array($phoneChar, ['(', ')', '-', ' '], true)) {
  24. $phoneAllowed = false;
  25. break;
  26. }
  27. }
  28. if ($phoneAllowed && strlen($phoneDigits) >= 6 && mb_strlen($phoneConfigValue, 'UTF-8') <= 40) {
  29. $feedbackContactPhone = $phoneConfigValue;
  30. $feedbackContactTel = $phoneTel;
  31. break;
  32. }
  33. }
  34. {/php}
  35. <link rel="stylesheet" href="/assets/xtu/cooperation-topbar.css?v=20260508-5">
  36. <link rel="stylesheet" href="/assets/xtu/cooperation.css?v=20260515-feedback-1">
  37. <main class="cooperation-page feedback-message-page" id="feedbackMessagePage" style="--cooperation-bg:url('{$category.image|default='/assets/xtu/vendor/img/appbanner.jpg'}')">
  38. <div class="cooperation-topbar">
  39. <div class="cooperation-topbar-inner">
  40. <div class="cooperation-topbar-title">{$title|default=cms_t('feedback_message_title','意见反馈')}</div>
  41. </div>
  42. </div>
  43. <div class="cooperation-shell">
  44. <section class="cooperation-copy">
  45. <div class="cooperation-bar-inner">
  46. <h1>{$title|default=cms_t('feedback_message_title','意见反馈')}</h1>
  47. <p>{if getCategory($catid,'description')}{:getCategory($catid,'description')}{elseif !empty($category.description) /}{$category.description}{else /}{:cms_t('feedback_message_intro','欢迎提出您的意见、建议或问题反馈,我们会认真阅读并持续优化服务体验。')}{/if}</p>
  48. </div>
  49. </section>
  50. <section class="cooperation-card" aria-labelledby="feedbackMessageFormTitle">
  51. <div class="cooperation-card-head">
  52. <h2 id="feedbackMessageFormTitle">{:cms_t('feedback_message_form_title','填写反馈')}</h2>
  53. <p>{:cms_t('feedback_message_form_desc','请留下您的联系方式和反馈内容,便于我们进一步沟通处理。')}</p>
  54. </div>
  55. <form id="feedbackMessageForm" class="cooperation-form" data-submit-url="{:url('index/feedback/message')}">
  56. <input type="hidden" name="lang" id="feedbackMessageLang" value="{$site_lang|default='cn'}">
  57. <input type="hidden" name="source_page" id="feedbackMessageSourcePage" value="">
  58. <input type="hidden" name="form_token" id="feedbackMessageFormToken" value="{:md5(session_id().'message_feedback')}">
  59. <div class="cooperation-honeypot" aria-hidden="true"><input type="text" name="website" tabindex="-1" autocomplete="off" value=""></div>
  60. <div class="cooperation-form-grid">
  61. <div class="cooperation-field">
  62. <select name="feedback_type" id="feedbackMessageType" aria-label="{:cms_t('feedback_message_type_label','反馈类型')}" required>
  63. <option value="">{:cms_t('feedback_message_type_placeholder','反馈类型 *')}</option>
  64. <option value="suggestion">{:cms_t('feedback_message_type_suggestion','意见建议')}</option>
  65. <option value="problem">{:cms_t('feedback_message_type_problem','问题反馈')}</option>
  66. <option value="complaint">{:cms_t('feedback_message_type_complaint','投诉建议')}</option>
  67. <option value="other">{:cms_t('feedback_message_type_other','其他')}</option>
  68. </select>
  69. </div>
  70. <div class="cooperation-field"><input type="text" name="name" placeholder="{:cms_t('cooperation_name_placeholder','姓名 *')}" maxlength="80" minlength="2" autocomplete="name" required></div>
  71. <div class="cooperation-field"><input type="tel" name="phone" placeholder="{:cms_t('cooperation_phone_placeholder','电话 *')}" maxlength="40" minlength="6" autocomplete="tel" inputmode="tel" pattern="[0-9+()\-\s]{6,40}" required></div>
  72. <div class="cooperation-field"><input type="email" name="email" placeholder="{:cms_t('cooperation_email_placeholder','电子邮箱 *')}" maxlength="120" autocomplete="email" required></div>
  73. <div class="cooperation-field is-full">
  74. <textarea name="content" id="feedbackMessageContent" placeholder="{:cms_t('feedback_message_content_placeholder','请填写反馈内容 *')}" maxlength="500" required></textarea>
  75. <span class="cooperation-count" id="feedbackMessageContentCount">0/500</span>
  76. </div>
  77. <div class="cooperation-field is-full" id="feedbackMessageCaptchaWrap" style="display:none;">
  78. <label class="screen-reader-text" for="feedbackMessageCaptchaInput">{:cms_t('cooperation_captcha_label','验证码 *')}</label>
  79. <div class="cooperation-captcha-row">
  80. <input type="text" name="verify" id="feedbackMessageCaptchaInput" placeholder="{:cms_t('cooperation_captcha_placeholder','请输入验证码')}" maxlength="6" autocomplete="off">
  81. <a href="javascript:;" class="cooperation-captcha-img-wrap" id="feedbackMessageCaptchaImageLink" aria-label="{:cms_t('cooperation_refresh_captcha','刷新验证码')}"><img src="" alt="{:cms_t('cooperation_captcha_label','验证码')}" id="feedbackMessageCaptchaImage"></a>
  82. </div>
  83. </div>
  84. <div class="cooperation-checks">
  85. <label><input type="checkbox" name="privacy_agree" value="1" required><span>{:cms_t('cooperation_privacy_prefix','已阅读并同意')}<a href="{:getCategory(44,'url')}" target="_blank" rel="noopener">{:cms_t('cooperation_privacy_link','《隐私政策》')}</a></span></label>
  86. </div>
  87. <button class="cooperation-submit" id="feedbackMessageSubmitBtn" type="submit">{:cms_t('cooperation_submit','提交信息')}</button>
  88. <div class="cooperation-message" id="feedbackMessageMessage" role="status" aria-live="polite"></div>
  89. <div class="cooperation-contact">
  90. {if condition="$feedbackContactPhone neq ''"}
  91. <a href="tel:{$feedbackContactTel}" class="cooperation-contact-link"><i class="bi bi-telephone"></i> {$feedbackContactPhone}</a>
  92. {/if}
  93. </div>
  94. </div>
  95. </form>
  96. </section>
  97. </div>
  98. </main>
  99. {if condition="!empty($content)"}
  100. <section class="cooperation-content">
  101. <div class="cooperation-content-inner">{$content|raw}</div>
  102. </section>
  103. {/if}
  104. <script>
  105. document.body.classList.add('cooperation-page-body');
  106. (function () {
  107. const siteHeader = document.getElementById('siteHeader');
  108. const syncTopbarPosition = function () {
  109. if (!siteHeader) return;
  110. const headerRect = siteHeader.getBoundingClientRect();
  111. document.documentElement.style.setProperty('--xtu-header-height', Math.max(0, Math.ceil(headerRect.bottom)) + 'px');
  112. };
  113. syncTopbarPosition();
  114. window.addEventListener('load', syncTopbarPosition);
  115. window.addEventListener('resize', syncTopbarPosition);
  116. window.addEventListener('scroll', syncTopbarPosition, { passive: true });
  117. if (window.ResizeObserver && siteHeader) new ResizeObserver(syncTopbarPosition).observe(siteHeader);
  118. })();
  119. (function () {
  120. const form = document.getElementById('feedbackMessageForm');
  121. if (!form) return;
  122. const submitBtn = document.getElementById('feedbackMessageSubmitBtn');
  123. const messageBox = document.getElementById('feedbackMessageMessage');
  124. const contentInput = document.getElementById('feedbackMessageContent');
  125. const contentCount = document.getElementById('feedbackMessageContentCount');
  126. const sourcePageInput = document.getElementById('feedbackMessageSourcePage');
  127. const submitUrl = form.getAttribute('data-submit-url') || '/index/feedback/message';
  128. const text = {
  129. required: {:json_encode(cms_t('cooperation_required','请完整填写必填信息,并同意隐私政策'), JSON_UNESCAPED_UNICODE)},
  130. invalidEmail: {:json_encode(cms_t('cooperation_invalid_email_hint','请输入正确的电子邮箱格式,如 name@example.com'), JSON_UNESCAPED_UNICODE)},
  131. invalidPhone: {:json_encode(cms_t('cooperation_invalid_phone','请输入正确的电话号码,可包含数字、+、-、空格和括号'), JSON_UNESCAPED_UNICODE)},
  132. fieldTooLong: {:json_encode(cms_t('cooperation_field_too_long','输入内容超过长度限制'), JSON_UNESCAPED_UNICODE)},
  133. captchaHint: {:json_encode(cms_t('cooperation_captcha_hint','当前提交较频繁,请先完成验证码'), JSON_UNESCAPED_UNICODE)},
  134. captchaError: {:json_encode(cms_t('cooperation_captcha_error','验证码错误,请重新输入'), JSON_UNESCAPED_UNICODE)},
  135. sending: {:json_encode(cms_t('cooperation_sending','正在提交...'), JSON_UNESCAPED_UNICODE)},
  136. success: {:json_encode(cms_t('feedback_message_success','提交成功,感谢您的反馈'), JSON_UNESCAPED_UNICODE)},
  137. failed: {:json_encode(cms_t('cooperation_failed','提交失败,请稍后再试'), JSON_UNESCAPED_UNICODE)}
  138. };
  139. function setMessage(msg, type) {
  140. if (!messageBox) return;
  141. messageBox.textContent = msg || '';
  142. messageBox.className = 'cooperation-message' + (type ? ' is-' + type : '');
  143. }
  144. function focusField(input) {
  145. if (!input) return;
  146. input.scrollIntoView({ behavior: 'smooth', block: 'center' });
  147. input.focus({ preventScroll: true });
  148. }
  149. function showSuccessModal(message) {
  150. let modal = document.getElementById('feedbackMessageSuccessModal');
  151. if (!modal) {
  152. modal = document.createElement('div');
  153. modal.id = 'feedbackMessageSuccessModal';
  154. modal.className = 'custom-modal custom-modal-cooperation-success';
  155. modal.setAttribute('aria-hidden', 'true');
  156. modal.innerHTML = '<div class="custom-modal-dialog cooperation-success-modal-dialog"><div class="custom-modal-box cooperation-success-modal-box"><button class="custom-modal-close" type="button" data-close-modal="feedbackMessageSuccessModal" aria-label="关闭">×</button><div class="cooperation-success-icon"><i class="bi bi-check2-circle"></i></div><h3 class="cooperation-success-title">提交成功</h3><p class="cooperation-success-text"></p><div class="cooperation-success-actions"><button type="button" class="cooperation-success-btn" data-close-modal="feedbackMessageSuccessModal">知道了</button></div></div></div>';
  157. document.body.appendChild(modal);
  158. modal.addEventListener('click', function (e) { if (e.target === modal) closeSuccessModal(); });
  159. }
  160. const textNode = modal.querySelector('.cooperation-success-text');
  161. if (textNode) textNode.textContent = message || text.success;
  162. modal.classList.add('is-open');
  163. modal.setAttribute('aria-hidden', 'false');
  164. document.body.classList.add('modal-open');
  165. modal.querySelector('.cooperation-success-btn')?.focus();
  166. }
  167. function closeSuccessModal() {
  168. const modal = document.getElementById('feedbackMessageSuccessModal');
  169. if (!modal) return;
  170. modal.classList.remove('is-open');
  171. modal.setAttribute('aria-hidden', 'true');
  172. if (!document.querySelector('.custom-modal.is-open') && !document.querySelector('.header-inline-search.is-open')) document.body.classList.remove('modal-open');
  173. }
  174. document.addEventListener('click', function (e) {
  175. const closeBtn = e.target.closest && e.target.closest('[data-close-modal="feedbackMessageSuccessModal"]');
  176. if (closeBtn) { e.preventDefault(); closeSuccessModal(); }
  177. });
  178. document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeSuccessModal(); });
  179. function updateContentCount() {
  180. if (!contentInput || !contentCount) return;
  181. const max = Number(contentInput.getAttribute('maxlength') || 500);
  182. const current = contentInput.value.length;
  183. contentCount.textContent = current + '/' + max;
  184. contentCount.style.color = current > max * 0.9 ? '#d92d20' : '#98a2b3';
  185. }
  186. function isValidEmail(value) {
  187. const email = String(value || '').trim();
  188. if (!email || email.length > 120) return false;
  189. return /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(email);
  190. }
  191. function isValidPhone(value) {
  192. const phone = String(value || '').trim();
  193. if (phone.length < 6 || phone.length > 40) return false;
  194. if (!/^[0-9+()\-\s]+$/.test(phone)) return false;
  195. return phone.replace(/\D/g, '').length >= 6;
  196. }
  197. function limitInputLength(input) {
  198. if (!input) return;
  199. const max = Number(input.getAttribute('maxlength') || 0);
  200. if (max > 0 && input.value.length > max) input.value = input.value.slice(0, max);
  201. }
  202. function showCaptcha(force) {
  203. const wrap = document.getElementById('feedbackMessageCaptchaWrap');
  204. const input = document.getElementById('feedbackMessageCaptchaInput');
  205. const image = document.getElementById('feedbackMessageCaptchaImage');
  206. const link = document.getElementById('feedbackMessageCaptchaImageLink');
  207. if (!wrap || !input || !image) return;
  208. wrap.style.display = force ? '' : 'none';
  209. input.required = !!force;
  210. if (force && image.src.indexOf('/captcha') === -1) image.src = '/index/feedback/captcha?t=' + Date.now();
  211. if (link) link.onclick = function () { image.src = '/index/feedback/captcha?t=' + Date.now(); return false; };
  212. }
  213. Array.prototype.forEach.call(form.querySelectorAll('input[maxlength], textarea[maxlength]'), function (input) {
  214. input.addEventListener('input', function () { limitInputLength(input); if (input === contentInput) updateContentCount(); });
  215. });
  216. if (contentInput) { contentInput.addEventListener('input', updateContentCount); updateContentCount(); }
  217. form.addEventListener('submit', async function (e) {
  218. e.preventDefault();
  219. const emailInput = form.querySelector('input[name="email"]');
  220. const phoneInput = form.querySelector('input[name="phone"]');
  221. const privacyInput = form.querySelector('input[name="privacy_agree"]');
  222. if (!form.checkValidity() || !privacyInput || !privacyInput.checked) {
  223. form.reportValidity && form.reportValidity();
  224. setMessage(text.required, 'error');
  225. return;
  226. }
  227. const captchaWrap = document.getElementById('feedbackMessageCaptchaWrap');
  228. const captchaInput = document.getElementById('feedbackMessageCaptchaInput');
  229. if (captchaWrap && captchaWrap.style.display !== 'none' && (!captchaInput || !captchaInput.value.trim())) {
  230. focusField(captchaInput); setMessage(text.captchaHint, 'error'); return;
  231. }
  232. if (!isValidPhone(phoneInput ? phoneInput.value.trim() : '')) { focusField(phoneInput); setMessage(text.invalidPhone, 'error'); return; }
  233. if (!isValidEmail(emailInput ? emailInput.value.trim() : '')) { focusField(emailInput); setMessage(text.invalidEmail, 'error'); return; }
  234. Array.prototype.forEach.call(form.querySelectorAll('input[type="text"], input[type="tel"], input[type="email"], textarea'), function (input) { input.value = input.value.trim(); });
  235. if (sourcePageInput) sourcePageInput.value = window.location.href;
  236. if (submitBtn) submitBtn.disabled = true;
  237. setMessage(text.sending, 'loading');
  238. try {
  239. const body = new URLSearchParams(new FormData(form));
  240. const res = await fetch(submitUrl, { method:'POST', headers:{'X-Requested-With':'XMLHttpRequest','Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}, body: body.toString() });
  241. const data = await res.json();
  242. if (data.code === 1) {
  243. form.reset(); updateContentCount(); showCaptcha(false); setMessage('', ''); showSuccessModal(data.msg || text.success);
  244. } else {
  245. if ((data.data && Number(data.data.need_captcha || 0) === 1) || String(data.msg || '').indexOf(text.captchaError) >= 0) showCaptcha(true);
  246. setMessage(data.msg || text.failed, 'error');
  247. }
  248. } catch (err) {
  249. setMessage(text.failed, 'error');
  250. } finally {
  251. if (submitBtn) submitBtn.disabled = false;
  252. }
  253. });
  254. })();
  255. </script>
  256. {include file="footer-links" /}
  257. {include file="footer" /}