feat: Redesign the contact page by adding a contact image and updating the form layout and styling.

This commit is contained in:
2026-03-01 12:51:36 +08:00
parent 173905ecd3
commit 84b5a498e6

View File

@@ -4,314 +4,349 @@
* Pixel-perfect implementation based on Webflow design * Pixel-perfect implementation based on Webflow design
* Includes form validation, submission handling, and responsive layout * Includes form validation, submission handling, and responsive layout
*/ */
import Layout from '../layouts/Layout.astro' import Layout from "../layouts/Layout.astro";
// Metadata for SEO // Metadata for SEO
const title = '聯絡我們 | 恩群數位行銷' const title = "聯絡我們 | 恩群數位行銷";
const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我們的專業團隊將竭誠為您服務。電話: 02 5570 0527Email: enchuntaiwan@gmail.com' const description =
"有任何問題嗎?歡迎聯絡恩群數位行銷,我們的專業團隊將竭誠為您服務。電話: 02 5570 0527Email: enchuntaiwan@gmail.com";
--- ---
<Layout title={title} description={description}> <Layout title={title} description={description}>
<section class="py-16 bg-background scroll-mt-20 lg:py-8 md:py-8" id="contact"> <section class="py-20 bg-[#f4f5f7] scroll-mt-20 lg:py-10" id="contact">
<div class="grid grid-cols-2 gap-12 items-center max-w-[1200px] mx-auto px-5 lg:grid-cols-1 lg:gap-8"> <div
class="grid grid-cols-2 gap-16 items-start max-w-3xl mx-auto px-5 lg:grid-cols-2 lg:gap-10"
>
<!-- Contact Image Side -->
<div class="flex justify-center items-center w-full lg:order-1 pt-4">
<img
src="https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b532e942d09_Contact%20us-bro%201.svg"
alt="聯絡我們插圖"
class="w-full h-auto object-contain drop-shadow-sm"
/>
</div>
<!-- Contact Form Side --> <!-- Contact Form Side -->
<div class="w-full"> <div class="w-full lg:order-2">
<h1 class="text-[2.5rem] font-bold leading-tight text-text-primary mb-4 lg:text-2xl md:text-[1.5rem]">聯絡我們</h1> <h2
<p class="text-lg font-normal leading-relaxed text-slate-600 mb-2"> class="text-4xl font-bold leading-snug text-[#2a5b83] mb-3 lg:text-3xl"
有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。 >
與我們聯絡
</h2>
<p
class="text-lg font-medium text-[#2a5b83] mb-2 tracking-wide lg:text-base"
>
任何關於行銷的相關訊息或是詢價,歡迎與我們聯絡
</p> </p>
<p class="text-sm italic text-text-muted mb-8"> <p class="text-sm italic text-slate-500 mb-8">
* 標註欄位為必填 有星號的地方 (*) 是必填欄位
</p> </p>
<!-- Contact Form --> <!-- Contact Form -->
<form id="contact-form" class="p-8 bg-surface rounded-lg shadow-lg lg:p-6 md:p-4" novalidate> <form id="contact-form" class="w-full flex flex-col gap-6" novalidate>
<!-- Success Message --> <!-- Success Message -->
<div id="form-success" class="p-4 px-6 rounded-md mt-4 text-center font-medium bg-[#d4edda] text-[#155724] border border-[#c3e6cb] animate-[slideUp_0.3s_ease-out]" style="display: none;"> <div
id="form-success"
class="p-4 px-6 rounded-lg text-center font-medium bg-[#d4edda] text-[#155724] border border-[#c3e6cb] animate-[slideUp_0.3s_ease-out]"
style="display: none;"
>
感謝您的留言!我們會盡快回覆您。 感謝您的留言!我們會盡快回覆您。
</div> </div>
<!-- Error Message --> <!-- Error Message -->
<div id="form-error" class="p-4 px-6 rounded-md mt-4 text-center font-medium bg-[#f8d7da] text-[#721c24] border border-[#f5c6cb] animate-[slideUp_0.3s_ease-out]" style="display: none;"> <div
id="form-error"
class="p-4 px-6 rounded-lg text-center font-medium bg-[#f8d7da] text-[#721c24] border border-[#f5c6cb] animate-[slideUp_0.3s_ease-out]"
style="display: none;"
>
送出失敗,請稍後再試或直接來電。 送出失敗,請稍後再試或直接來電。
</div> </div>
<!-- Form Fields -->
<div class="grid grid-cols-2 gap-6 mb-6 md:grid-cols-1 md:gap-4">
<!-- Name Field --> <!-- Name Field -->
<div class="flex flex-col mb-6"> <div class="flex flex-col">
<label for="Name" class="text-sm font-semibold text-text-primary mb-2 block"> <label
姓名 <span class="text-primary">*</span> for="Name"
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
>
姓名*
</label> </label>
<input <input
type="text" type="text"
id="Name" id="Name"
name="Name" name="Name"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]" class="w-full px-4 py-2.5 border border-[#4a90e2] rounded-lg text-base bg-white focus:outline-none focus:ring-2 focus:ring-[#4a90e2]/20 transition-all shadow-sm [&.error]:border-[#dc3545]"
required required
minlength="2" minlength="2"
maxlength="256" maxlength="256"
placeholder="請輸入您的姓名"
/> />
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Name-error"></span> <span
class="error-message text-[#dc3545] text-sm mt-1 hidden"
id="Name-error"></span>
</div> </div>
<!-- Phone Field --> <!-- Phone Field -->
<div class="flex flex-col mb-6"> <div class="flex flex-col">
<label for="Phone" class="text-sm font-semibold text-text-primary mb-2 block"> <label
聯絡電話 <span class="text-primary">*</span> for="Phone"
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
>
聯絡電話*
</label> </label>
<input <input
type="tel" type="tel"
id="Phone" id="Phone"
name="Phone" name="Phone"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]" class="w-full px-4 py-2.5 border border-white shadow-sm rounded-lg text-base bg-white focus:outline-none focus:border-[#4a90e2] focus:ring-2 focus:ring-[#4a90e2]/20 transition-all [&.error]:border-[#dc3545]"
required required
placeholder="請輸入您的電話號碼"
/> />
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Phone-error"></span> <span
</div> class="error-message text-[#dc3545] text-sm mt-1 hidden"
id="Phone-error"></span>
</div> </div>
<!-- Email Field --> <!-- Email Field -->
<div class="flex flex-col mb-6"> <div class="flex flex-col">
<label for="Email" class="text-sm font-semibold text-text-primary mb-2 block"> <label
Email <span class="text-primary">*</span> for="Email"
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
>
Email *
</label> </label>
<input <input
type="email" type="email"
id="Email" id="Email"
name="Email" name="Email"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]" class="w-full px-4 py-2.5 border border-white shadow-sm rounded-lg text-base bg-white focus:outline-none focus:border-[#4a90e2] focus:ring-2 focus:ring-[#4a90e2]/20 transition-all [&.error]:border-[#dc3545]"
required required
placeholder="請輸入您的 Email"
/> />
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Email-error"></span> <span
class="error-message text-[#dc3545] text-sm mt-1 hidden"
id="Email-error"></span>
</div> </div>
<!-- Message Field --> <!-- Message Field -->
<div class="flex flex-col mb-8"> <div class="flex flex-col">
<label for="Message" class="text-sm font-semibold text-text-primary mb-2 block"> <label
聯絡訊息 <span class="text-primary">*</span> for="Message"
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
>
聯絡訊息
</label> </label>
<textarea <textarea
id="Message" id="Message"
name="Message" name="Message"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted min-h-[120px] resize-y [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]" class="w-full px-4 py-3 border border-white shadow-sm rounded-lg text-base bg-white focus:outline-none focus:border-[#4a90e2] focus:ring-2 focus:ring-[#4a90e2]/20 transition-all resize-y min-h-[120px] [&.error]:border-[#dc3545]"
minlength="10"
maxlength="5000"
required required
placeholder="請輸入您的訊息(至少 10 個字元)" minlength="10"
></textarea> maxlength="5000"></textarea>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Message-error"></span> <span
class="error-message text-[#dc3545] text-sm mt-1 hidden"
id="Message-error"></span>
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<button type="submit" class="bg-primary text-white border-none rounded-md px-8 py-[0.875rem] text-base font-semibold cursor-pointer transition-all duration-150 text-center inline-flex items-center justify-center min-w-[200px] w-full hover:bg-primary-hover hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 disabled:bg-slate-500 disabled:cursor-not-allowed disabled:opacity-80" id="submit-btn"> <div class="flex justify-end mt-2">
<span class="button-text">送出訊息</span> <button
<span class="button-loading" style="display: none;">送出中...</span> type="submit"
class="bg-[#2a5b83] text-white border-none rounded-lg px-10 py-[0.6rem] text-[1.05rem] font-medium cursor-pointer transition-all duration-200 hover:bg-[#1f4666] shadow-sm transform hover:-translate-y-0.5 min-w-[120px] disabled:bg-slate-400 disabled:cursor-not-allowed disabled:transform-none"
id="submit-btn"
>
<span class="button-text">送出</span>
<span class="button-loading" style="display: none;"
>送出中...</span
>
</button> </button>
</div>
</form> </form>
</div> </div>
<!-- Contact Image Side -->
<div class="flex flex-col gap-8 justify-center items-center">
<div class="w-full rounded-lg overflow-hidden shadow-md bg-slate-100 aspect-[3/2]">
<img
src="/placeholder-contact.jpg"
alt="聯絡恩群數位"
width="600"
height="400"
class="w-full h-full object-cover"
/>
</div>
<!-- Contact Info Card -->
<div class="w-full p-6 bg-white rounded-lg shadow-lg">
<h3 class="text-xl font-semibold text-text-primary mb-4">聯絡資訊</h3>
<div class="flex items-center gap-3 mb-4 text-[0.95rem] text-text-secondary">
<svg class="w-5 h-5 flex-shrink-0 text-primary" viewBox="0 0 24 24"><path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47-1.14.75-1.02L2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02L2.05 21.05zM19.95 2.95c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36.11.74-.47-1.14-.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.02.75 1.02L2.05 21.05c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36-.11-.74.47-1.14.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11-.74.47 1.14.75 1.02L2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02z"/></svg>
<span>諮詢電話: 02 5570 0527</span>
</div>
<div class="flex items-center gap-3 mb-4 text-[0.95rem] text-text-secondary">
<svg class="w-5 h-5 flex-shrink-0 text-primary" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
<span>Email: <a href="mailto:enchuntaiwan@gmail.com" class="text-primary no-underline hover:underline">enchuntaiwan@gmail.com</a></span>
</div>
</div>
</div>
</div> </div>
</section> </section>
</Layout> </Layout>
<script> <script>
// Form validation and submission handler // Form validation and submission handler
function initContactForm() { function initContactForm() {
const form = document.getElementById('contact-form') as HTMLFormElement const form = document.getElementById("contact-form") as HTMLFormElement;
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement const submitBtn = document.getElementById(
const successMsg = document.getElementById('form-success') as HTMLElement "submit-btn",
const errorMsg = document.getElementById('form-error') as HTMLElement ) as HTMLButtonElement;
const successMsg = document.getElementById("form-success") as HTMLElement;
const errorMsg = document.getElementById("form-error") as HTMLElement;
if (!form) return if (!form) return;
// Validation patterns // Validation patterns
const patterns = { const patterns = {
Name: /^[\u4e00-\u9fa5a-zA-Z\s]{2,256}$/, Name: /^[\u4e00-\u9fa5a-zA-Z\s]{2,256}$/,
Phone: /^[0-9\-\s\+]{6,20}$/, Phone: /^[0-9\-\s\+]{6,20}$/,
Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
Message: /^.{10,5000}$/ Message: /^.{10,5000}$/,
} };
// Validation function // Validation function
function validateField(input: HTMLInputElement | HTMLTextAreaElement): boolean { function validateField(
const name = input.name input: HTMLInputElement | HTMLTextAreaElement,
const value = input.value.trim() ): boolean {
const errorSpan = document.getElementById(`${name}-error`) as HTMLElement const name = input.name;
const value = input.value.trim();
const errorSpan = document.getElementById(`${name}-error`) as HTMLElement;
if (!input.hasAttribute('required') && !value) { if (!input.hasAttribute("required") && !value) {
clearError(input, errorSpan) clearError(input, errorSpan);
return true return true;
} }
let isValid = true let isValid = true;
let errorMessage = '' let errorMessage = "";
// Required check // Required check
if (input.hasAttribute('required') && !value) { if (input.hasAttribute("required") && !value) {
isValid = false isValid = false;
errorMessage = '此欄位為必填' errorMessage = "此欄位為必填";
} }
// Pattern validation // Pattern validation
else if (patterns[name as keyof typeof patterns] && !patterns[name as keyof typeof patterns].test(value)) { else if (
isValid = false patterns[name as keyof typeof patterns] &&
!patterns[name as keyof typeof patterns].test(value)
) {
isValid = false;
switch (name) { switch (name) {
case 'Name': case "Name":
errorMessage = '請輸入有效的姓名(至少 2 個字元)' errorMessage = "請輸入有效的姓名(至少 2 個字元)";
break break;
case 'Phone': case "Phone":
errorMessage = '請輸入有效的電話號碼' errorMessage = "請輸入有效的電話號碼";
break break;
case 'Email': case "Email":
errorMessage = '請輸入有效的 Email 格式' errorMessage = "請輸入有效的 Email 格式";
break break;
case 'Message': case "Message":
errorMessage = '訊息至少需要 10 個字元' errorMessage = "訊息至少需要 10 個字元";
break break;
} }
} }
// Show/hide error // Show/hide error
if (!isValid) { if (!isValid) {
input.classList.add('error') input.classList.add("error");
if (errorSpan) { if (errorSpan) {
errorSpan.textContent = errorMessage errorSpan.textContent = errorMessage;
errorSpan.style.display = 'block' errorSpan.style.display = "block";
} }
} else { } else {
clearError(input, errorSpan) clearError(input, errorSpan);
} }
return isValid return isValid;
} }
function clearError(input: HTMLInputElement | HTMLTextAreaElement, errorSpan: HTMLElement) { function clearError(
input.classList.remove('error') input: HTMLInputElement | HTMLTextAreaElement,
errorSpan: HTMLElement,
) {
input.classList.remove("error");
if (errorSpan) { if (errorSpan) {
errorSpan.textContent = '' errorSpan.textContent = "";
errorSpan.style.display = 'none' errorSpan.style.display = "none";
} }
} }
// Real-time validation on blur // Real-time validation on blur
form.querySelectorAll('input, textarea').forEach((field) => { form.querySelectorAll("input, textarea").forEach((field) => {
field.addEventListener('blur', () => { field.addEventListener("blur", () => {
validateField(field as HTMLInputElement | HTMLTextAreaElement) validateField(field as HTMLInputElement | HTMLTextAreaElement);
}) });
field.addEventListener('input', () => { field.addEventListener("input", () => {
const input = field as HTMLInputElement | HTMLTextAreaElement const input = field as HTMLInputElement | HTMLTextAreaElement;
if (input.classList.contains('error')) { if (input.classList.contains("error")) {
validateField(input) validateField(input);
} }
}) });
}) });
// Form submission // Form submission
form.addEventListener('submit', async (e) => { form.addEventListener("submit", async (e) => {
e.preventDefault() e.preventDefault();
// Validate all fields // Validate all fields
const inputs = form.querySelectorAll('input, textarea') as NodeListOf<HTMLInputElement | HTMLTextAreaElement> const inputs = form.querySelectorAll("input, textarea") as NodeListOf<
let isFormValid = true HTMLInputElement | HTMLTextAreaElement
>;
let isFormValid = true;
inputs.forEach((input) => { inputs.forEach((input) => {
if (!validateField(input)) { if (!validateField(input)) {
isFormValid = false isFormValid = false;
} }
}) });
if (!isFormValid) { if (!isFormValid) {
// Scroll to first error // Scroll to first error
const firstError = form.querySelector('.input_field.error') as HTMLElement const firstError = form.querySelector(
".input_field.error",
) as HTMLElement;
if (firstError) { if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }) firstError.scrollIntoView({ behavior: "smooth", block: "center" });
} }
return return;
} }
// Show loading state // Show loading state
submitBtn.disabled = true submitBtn.disabled = true;
const buttonText = submitBtn.querySelector('.button-text') as HTMLElement const buttonText = submitBtn.querySelector(".button-text") as HTMLElement;
const buttonLoading = submitBtn.querySelector('.button-loading') as HTMLElement const buttonLoading = submitBtn.querySelector(
if (buttonText) buttonText.style.display = 'none' ".button-loading",
if (buttonLoading) buttonLoading.style.display = 'inline' ) as HTMLElement;
if (buttonText) buttonText.style.display = "none";
if (buttonLoading) buttonLoading.style.display = "inline";
// Hide previous messages // Hide previous messages
successMsg.style.display = 'none' successMsg.style.display = "none";
errorMsg.style.display = 'none' errorMsg.style.display = "none";
// Collect form data // Collect form data
const formData = new FormData(form) const formData = new FormData(form);
const data = { const data = {
name: formData.get('Name'), name: formData.get("Name"),
phone: formData.get('Phone'), phone: formData.get("Phone"),
email: formData.get('Email'), email: formData.get("Email"),
message: formData.get('Message') message: formData.get("Message"),
} };
try { try {
// Submit to backend (via API proxy) // Submit to backend (via API proxy)
const response = await fetch('/api/contact', { const response = await fetch("/api/contact", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
}) });
if (response.ok) { if (response.ok) {
// Success // Success
successMsg.style.display = 'block' successMsg.style.display = "block";
form.reset() form.reset();
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: "smooth" });
} else { } else {
throw new Error('Submission failed') throw new Error("Submission failed");
} }
} catch (error) { } catch (error) {
console.error('Form submission error:', error) console.error("Form submission error:", error);
errorMsg.style.display = 'block' errorMsg.style.display = "block";
} finally { } finally {
// Reset button state // Reset button state
submitBtn.disabled = false submitBtn.disabled = false;
if (buttonText) buttonText.style.display = 'inline' if (buttonText) buttonText.style.display = "inline";
if (buttonLoading) buttonLoading.style.display = 'none' if (buttonLoading) buttonLoading.style.display = "none";
} }
}) });
} }
// Initialize when DOM is ready // Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initContactForm) document.addEventListener("DOMContentLoaded", initContactForm);
if (document.readyState !== 'loading') { if (document.readyState !== "loading") {
initContactForm() initContactForm();
} }
</script> </script>