From 695da012deda23ed37a09c014dbb8af74312edc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E4=BA=9A=E6=98=95?= <1549469775@qq.com> Date: Tue, 9 Sep 2025 15:10:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(profile):=20=E5=A2=9E=E5=8A=A0=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=81=94=E7=B3=BB=E8=A1=A8=E5=8D=95=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在个人资料页新增头像上传及预览组件,支持本地图片文件选择与拖拽上传 - 实现头像上传过程中的类型、大小校验以及上传进度反馈 - 支持输入头像URL,增加图片链接格式有效性验证 - 优化联系表单,新增前端字段验证与错误提示,提升用户体验 - 联系表单提交成功后跳转新增的感谢反馈页面 - 规范联系表单后端接口请求,添加邮箱及内容的服务器端验证和日志记录 - 统一修正服务导入路径,移除未使用的服务统一导出文件 - 调整 vscode 配置,将 pug 关联语言由 pug 修改为 jade - 优化联系页面 UI 及样式,改进反馈类型选择和错误消息显示机制 --- .vscode/settings.json | 2 +- database/development.sqlite3-shm | Bin 32768 -> 32768 bytes database/development.sqlite3-wal | Bin 828152 -> 861112 bytes public/js/profile.js | 182 +++++++++++++++++++++++ src/controllers/Api/AuthController.js | 2 +- src/controllers/Page/AuthPageController.js | 2 +- src/controllers/Page/BasePageController.js | 45 +++++- src/controllers/Page/ProfileController.js | 2 +- src/services/index.js | 36 ----- src/views/page/extra/contact.pug | 225 ++++++++++++++++++++++++++--- src/views/page/extra/contactSuccess.pug | 62 ++++++++ src/views/page/profile/index.pug | 139 +++++++++++++++++- 12 files changed, 628 insertions(+), 69 deletions(-) delete mode 100644 src/services/index.js create mode 100644 src/views/page/extra/contactSuccess.pug diff --git a/.vscode/settings.json b/.vscode/settings.json index 2f47a36..6bb4812 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "typescript.updateImportsOnFileMove.enabled": "always", "javascript.updateImportsOnFileMove.enabled": "always", "files.associations": { - "*.pug": "pug" + "*.pug": "jade" }, "emmet.includeLanguages": { "pug": "html" diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index c77930b3978a4e3719eda76d6e200a28b5a59ea5..46c674f5363662f73369d6a0798f087c7b124bd5 100644 GIT binary patch delta 200 zcmZo@U}|V!s+V}A%K!o_K+MR%AaD^#3jwiPF7M2#)7Pc1l7LFFq7OocVEj&LKNiZ-l2!ZhZAJX&Ryg#5W z!ssOU@X{fb?HiJKayho2t!2K;wEb5u&rX)hH?8cM`jkL z?NNn1k&IxqVs?y&e>@M$U%?BsfSI|KfoD2%EBk@X2LsBPnL8}_wij$ z7G7xv{&>EFd{Vs9K%=>Ng&J!(1Q;Y48d*gd7>td9jMC|i6L^ZJ+exukZSQ)=^^#GQ z71_)8-p<%A+!6Q#Vp;~wvd| z(A?DA#K6$hz{JqdSidN-TraC2Ju$f?vn&;xbE55pr4@6ogwfhL=4KX#7KEL%%qDeZ qa@V14|=IQ%vVPV*~&@-v?g+ delta 33 pcmdmS+~mhmqlOm77N!>F7M2#)7Pc1l7LFFq7OocVEj&LK0RZ5p3~K-Y diff --git a/public/js/profile.js b/public/js/profile.js index 116296d..f3b5713 100644 --- a/public/js/profile.js +++ b/public/js/profile.js @@ -12,6 +12,7 @@ bindInputValidation(); showInitialMessage(); initTabs(); + initAvatarUpload(); } // 绑定表单事件 @@ -133,6 +134,11 @@ return false; } + if (data.avatar && !isValidImageUrl(data.avatar)) { + showMessage('请输入有效的图片链接或路径', 'error', 'profileForm'); + return false; + } + return true; } @@ -206,6 +212,13 @@ } break; + case 'avatar': + if (value && !isValidImageUrl(value)) { + isValid = false; + errorMessage = '请输入有效的图片链接或路径'; + } + break; + case 'newPassword': if (value && value.length < 6) { isValid = false; @@ -252,6 +265,28 @@ return emailRegex.test(email); } + // 验证图片URL格式(支持相对路径和绝对路径) + function isValidImageUrl(url) { + if (!url) return true; // 空值时认为有效 + + // 支持的图片格式 + const imageExtensions = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i; + + // 检查是否以支持的图片格式结尾 + if (imageExtensions.test(url)) { + return true; + } + + // 检查是否为完整的URL(http或https开头) + try { + const urlObj = new URL(url); + return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'; + } catch { + // 如果不是完整URL,检查是否为相对路径(以/开头) + return url.startsWith('/'); + } + } + // 设置按钮加载状态 function setButtonLoading(button, loading, originalText = null) { if (loading) { @@ -445,5 +480,152 @@ window.resetForm = resetForm; window.resetPasswordForm = resetPasswordForm; window.closeMessage = closeMessage; + window.handleAvatarSelect = handleAvatarSelect; + + // 初始化头像上传功能 + function initAvatarUpload() { + const avatarInput = document.getElementById('avatarFile'); + const avatarPreview = document.querySelector('.avatar-preview'); + const avatarUrlInput = document.getElementById('avatar'); + + if (avatarUrlInput) { + avatarUrlInput.addEventListener('input', function() { + if (this.value.trim()) { + updateAvatarPreview(this.value.trim()); + } + }); + } + } + + // 处理头像文件选择 + function handleAvatarSelect(input) { + const file = input.files[0]; + if (!file) return; + + // 验证文件类型 + if (!file.type.startsWith('image/')) { + showMessage('请选择图片文件', 'error', 'profileForm'); + return; + } + + // 验证文件大小 (5MB) + if (file.size > 5 * 1024 * 1024) { + showMessage('图片文件大小不能超过 5MB', 'error', 'profileForm'); + return; + } + + // 预览图片 + const reader = new FileReader(); + reader.onload = function(e) { + updateAvatarPreview(e.target.result); + }; + reader.readAsDataURL(file); + + // 上传文件 + uploadAvatarFile(file); + } + + // 更新头像预览 + function updateAvatarPreview(imageSrc) { + const avatarPreview = document.querySelector('.avatar-preview'); + const sidebarAvatar = document.querySelector('.profile-avatar'); + + if (avatarPreview) { + // 移除现有内容 + avatarPreview.innerHTML = ''; + + // 创建新的图片元素 + const img = document.createElement('img'); + img.src = imageSrc; + img.alt = '头像预览'; + img.id = 'avatarPreviewImg'; + avatarPreview.appendChild(img); + + // 添加覆盖层 + const overlay = document.createElement('div'); + overlay.className = 'upload-overlay'; + overlay.innerHTML = '
📷
点击更换头像
'; + avatarPreview.appendChild(overlay); + } + + // 同时更新侧边栏头像 + if (sidebarAvatar) { + sidebarAvatar.innerHTML = `用户头像`; + } + } + + // 上传头像文件 + async function uploadAvatarFile(file) { + const progressContainer = document.getElementById('uploadProgress'); + const progressBar = document.getElementById('uploadProgressBar'); + + try { + // 显示进度条 + if (progressContainer) { + progressContainer.style.display = 'block'; + progressBar.style.width = '0%'; + } + + const formData = new FormData(); + formData.append('avatar', file); + + // 模拟进度更新 + let progress = 0; + const progressInterval = setInterval(() => { + progress += Math.random() * 30; + if (progress > 90) { + clearInterval(progressInterval); + progress = 90; + } + if (progressBar) { + progressBar.style.width = progress + '%'; + } + }, 200); + + const response = await fetch('/profile/upload-avatar', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + // 清除进度条动画 + clearInterval(progressInterval); + + if (result.success) { + // 完成进度条 + if (progressBar) { + progressBar.style.width = '100%'; + } + + // 更新头像URL输入框 + const avatarUrlInput = document.getElementById('avatar'); + if (avatarUrlInput && result.url) { + avatarUrlInput.value = result.url; + } + + showMessage('头像上传成功!', 'success', 'profileForm'); + + // 隐藏进度条 + setTimeout(() => { + if (progressContainer) { + progressContainer.style.display = 'none'; + } + }, 1000); + + } else { + throw new Error(result.message || '头像上传失败'); + } + + } catch (error) { + showMessage(error.message || '头像上传失败,请重试', 'error', 'profileForm'); + console.error('Avatar upload error:', error); + + // 隐藏进度条 + if (progressContainer) { + progressContainer.style.display = 'none'; + } + } + } })(); diff --git a/src/controllers/Api/AuthController.js b/src/controllers/Api/AuthController.js index 4c4e5cd..c062092 100644 --- a/src/controllers/Api/AuthController.js +++ b/src/controllers/Api/AuthController.js @@ -1,4 +1,4 @@ -import UserService from "services/userService.js" +import UserService from "@/services/UserService.js" import { R } from "utils/helper.js" import Router from "utils/router.js" diff --git a/src/controllers/Page/AuthPageController.js b/src/controllers/Page/AuthPageController.js index 1fd68b0..6270aa5 100644 --- a/src/controllers/Page/AuthPageController.js +++ b/src/controllers/Page/AuthPageController.js @@ -1,5 +1,5 @@ import Router from "utils/router.js" -import UserService from "services/userService.js" +import UserService from "@/services/UserService.js" import svgCaptcha from "svg-captcha" import CommonError from "@/utils/error/CommonError" import { logger } from "@/logger.js" diff --git a/src/controllers/Page/BasePageController.js b/src/controllers/Page/BasePageController.js index 411a431..09c5bba 100644 --- a/src/controllers/Page/BasePageController.js +++ b/src/controllers/Page/BasePageController.js @@ -41,13 +41,45 @@ class BasePageController { return } - // 这里可以添加邮件发送逻辑或数据库存储逻辑 - // 目前只是简单的成功响应 - logger.info(`收到联系表单: ${name} (${email}) - ${subject}: ${message}`) + // 验证邮箱格式 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + ctx.status = 400 + ctx.body = { success: false, message: "请输入正确的邮箱地址" } + return + } + + // 验证内容长度 + if (name.trim().length < 2) { + ctx.status = 400 + ctx.body = { success: false, message: "姓名至少需要 2 个字符" } + return + } + + if (message.trim().length < 10) { + ctx.status = 400 + ctx.body = { success: false, message: "留言内容至少需要 10 个字符" } + return + } + + try { + // 这里可以添加邮件发送逻辑或数据库存储逻辑 + // 目前只是简单的成功响应和日志记录 + logger.info(`收到联系表单: ${name} (${email}) - ${subject}: ${message}`) + + // TODO: 可以在这里添加以下功能: + // 1. 发送邮件通知管理员 + // 2. 将联系信息存储到数据库 + // 3. 发送自动回复邮件给用户 - ctx.body = { - success: true, - message: "感谢您的留言,我们会尽快回复您!", + ctx.body = { + success: true, + message: "感谢您的留言,我们会尽快回复您!", + } + } catch (error) { + logger.error(`联系表单处理失败: ${error.message}`) + ctx.status = 500 + ctx.body = { success: false, message: "系统错误,请稍后再试" } } } @@ -88,6 +120,7 @@ class BasePageController { router.get("/feedback", controller.pageGet("page/extra/feedback"), { auth: false }) router.get("/help", controller.pageGet("page/extra/help"), { auth: false }) router.get("/contact", controller.pageGet("page/extra/contact"), { auth: false }) + router.get("/contact/success", controller.pageGet("page/extra/contactSuccess"), { auth: false }) // 需要登录的页面 router.get("/notice", controller.pageGet("page/notice/index"), { auth: true }) diff --git a/src/controllers/Page/ProfileController.js b/src/controllers/Page/ProfileController.js index 3a3678c..be10d8b 100644 --- a/src/controllers/Page/ProfileController.js +++ b/src/controllers/Page/ProfileController.js @@ -1,5 +1,5 @@ import Router from "utils/router.js" -import UserService from "services/userService.js" +import UserService from "@/services/UserService.js" import formidable from "formidable" import fs from "fs/promises" import path from "path" diff --git a/src/services/index.js b/src/services/index.js deleted file mode 100644 index db42d64..0000000 --- a/src/services/index.js +++ /dev/null @@ -1,36 +0,0 @@ -// 服务层统一导出 -import UserService from "./UserService.js" -import ArticleService from "./ArticleService.js" -import BookmarkService from "./BookmarkService.js" -import SiteConfigService from "./SiteConfigService.js" -import JobService from "./JobService.js" - -// 导出所有服务类 -export { - UserService, - ArticleService, - BookmarkService, - SiteConfigService, - JobService -} - -// 导出默认实例(单例模式) -export const userService = new UserService() -export const articleService = new ArticleService() -export const bookmarkService = new BookmarkService() -export const siteConfigService = new SiteConfigService() -export const jobService = new JobService() - -// 默认导出 -export default { - UserService, - ArticleService, - BookmarkService, - SiteConfigService, - JobService, - userService, - articleService, - bookmarkService, - siteConfigService, - jobService -} diff --git a/src/views/page/extra/contact.pug b/src/views/page/extra/contact.pug index f334074..16752f9 100644 --- a/src/views/page/extra/contact.pug +++ b/src/views/page/extra/contact.pug @@ -12,7 +12,7 @@ block pageContent h2(class="text-2xl font-semibold mb-4 text-blue-600 flex items-center justify-center") span(class="mr-2") 📞 | 联系方式 - .grid.grid-cols-1.md:grid-cols-3.gap-6 + .grid.grid-cols-1.gap-6(class="md:grid-cols-3") .contact-card(class="text-center p-6 bg-blue-50 rounded-lg border border-blue-200 hover:shadow-md transition-shadow") .icon(class="text-4xl mb-3") 📧 h3(class="font-semibold text-blue-800 mb-2") 邮箱联系 @@ -35,27 +35,37 @@ block pageContent span(class="mr-2") ✍️ | 留言反馈 .form-container(class="max-w-2xl mx-auto") - form(action="/contact" method="POST" class="space-y-4") + // 消息提示区域 + .message-container#messageContainer(class="mb-4 hidden") + .message#messageAlert(class="p-4 rounded-lg border") + span#messageText + button.message-close(type="button" onclick="closeMessage()" class="float-right text-lg font-bold cursor-pointer") × + + form#contactForm(action="/contact" method="POST" class="space-y-4") .form-group(class="grid grid-cols-1 md:grid-cols-2 gap-4") .input-group label(for="name" class="block text-sm font-medium text-gray-700 mb-1") 姓名 * - input#name(type="text" name="name" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent") + input#name(type="text" name="name" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors") + .error-message#nameError(class="text-red-500 text-sm mt-1 hidden") .input-group label(for="email" class="block text-sm font-medium text-gray-700 mb-1") 邮箱 * - input#email(type="email" name="email" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent") + input#email(type="email" name="email" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors") + .error-message#emailError(class="text-red-500 text-sm mt-1 hidden") .form-group label(for="subject" class="block text-sm font-medium text-gray-700 mb-1") 主题 * - select#subject(name="subject" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent") + select#subject(name="subject" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors") option(value="") 请选择反馈类型 option(value="bug") 问题反馈 option(value="feature") 功能建议 option(value="content") 内容相关 option(value="other") 其他 + .error-message#subjectError(class="text-red-500 text-sm mt-1 hidden") .form-group label(for="message" class="block text-sm font-medium text-gray-700 mb-1") 留言内容 * - textarea#message(name="message" rows="5" required placeholder="请详细描述您的问题或建议..." class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-vertical") + textarea#message(name="message" rows="5" required placeholder="请详细描述您的问题或建议..." class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-vertical transition-colors") + .error-message#messageError(class="text-red-500 text-sm mt-1 hidden") .form-group(class="text-center") - button(type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors") 提交留言 + button#submitBtn(type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed") 提交留言 // 办公地址 .office-info(class="mb-8") @@ -68,16 +78,197 @@ block pageContent p(class="text-gray-700 mb-2") 北京市朝阳区某某大厦 p(class="text-gray-700 mb-2") 邮编:100000 p(class="text-sm text-gray-500") 工作时间:周一至周五 9:00-18:00 - - // 相关链接 - .contact-links(class="text-center pt-6 border-t border-gray-200") - p(class="text-gray-600 mb-3") 更多帮助资源: - .links(class="flex flex-wrap justify-center gap-4") - a(href="/help" class="text-blue-600 hover:text-blue-800 hover:underline") 帮助中心 - a(href="/faq" class="text-blue-600 hover:text-blue-800 hover:underline") 常见问题 - a(href="/feedback" class="text-blue-600 hover:text-blue-800 hover:underline") 意见反馈 - a(href="/about" class="text-blue-600 hover:text-blue-800 hover:underline") 关于我们 - .contact-footer(class="text-center mt-8 pt-6 border-t border-gray-200") p(class="text-gray-500 text-sm") 我们承诺保护您的隐私,所有联系信息仅用于回复您的反馈 p(class="text-gray-400 text-xs mt-2") 感谢您的支持与信任 + +block pageScripts + script. + document.addEventListener('DOMContentLoaded', function() { + const form = document.getElementById('contactForm'); + const submitBtn = document.getElementById('submitBtn'); + const messageContainer = document.getElementById('messageContainer'); + const messageAlert = document.getElementById('messageAlert'); + const messageText = document.getElementById('messageText'); + + // 表单字段 + const fields = { + name: document.getElementById('name'), + email: document.getElementById('email'), + subject: document.getElementById('subject'), + message: document.getElementById('message') + }; + + // 错误消息元素 + const errors = { + name: document.getElementById('nameError'), + email: document.getElementById('emailError'), + subject: document.getElementById('subjectError'), + message: document.getElementById('messageError') + }; + + // 清除消息函数 + window.closeMessage = function() { + messageContainer.classList.add('hidden'); + }; + + // 显示消息函数 + function showMessage(text, type = 'error') { + messageText.textContent = text; + messageAlert.className = `message p-4 rounded-lg border ${ + type === 'success' + ? 'bg-green-50 border-green-200 text-green-800' + : 'bg-red-50 border-red-200 text-red-800' + }`; + messageContainer.classList.remove('hidden'); + + // 滚动到消息区域 + messageContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + + // 显示字段错误 + function showFieldError(fieldName, message) { + const field = fields[fieldName]; + const errorElement = errors[fieldName]; + + field.classList.add('border-red-500'); + errorElement.textContent = message; + errorElement.classList.remove('hidden'); + } + + // 清除字段错误 + function clearFieldError(fieldName) { + const field = fields[fieldName]; + const errorElement = errors[fieldName]; + + field.classList.remove('border-red-500'); + errorElement.classList.add('hidden'); + } + + // 清除所有错误 + function clearAllErrors() { + Object.keys(fields).forEach(fieldName => { + clearFieldError(fieldName); + }); + closeMessage(); + } + + // 验证邮箱格式 + function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + // 表单验证 + function validateForm() { + clearAllErrors(); + let isValid = true; + + // 验证姓名 + const name = fields.name.value.trim(); + if (!name) { + showFieldError('name', '请输入您的姓名'); + isValid = false; + } else if (name.length < 2) { + showFieldError('name', '姓名至少需要 2 个字符'); + isValid = false; + } + + // 验证邮箱 + const email = fields.email.value.trim(); + if (!email) { + showFieldError('email', '请输入您的邮箱地址'); + isValid = false; + } else if (!isValidEmail(email)) { + showFieldError('email', '请输入正确的邮箱地址'); + isValid = false; + } + + // 验证主题 + if (!fields.subject.value) { + showFieldError('subject', '请选择反馈类型'); + isValid = false; + } + + // 验证留言内容 + const message = fields.message.value.trim(); + if (!message) { + showFieldError('message', '请输入留言内容'); + isValid = false; + } else if (message.length < 10) { + showFieldError('message', '留言内容至少需要 10 个字符'); + isValid = false; + } + + return isValid; + } + + // 设置按钮加载状态 + function setButtonLoading(loading) { + if (loading) { + submitBtn.disabled = true; + submitBtn.textContent = '提交中...'; + submitBtn.classList.add('bg-gray-400', 'cursor-not-allowed'); + submitBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); + } else { + submitBtn.disabled = false; + submitBtn.textContent = '提交留言'; + submitBtn.classList.remove('bg-gray-400', 'cursor-not-allowed'); + submitBtn.classList.add('bg-blue-600', 'hover:bg-blue-700'); + } + } + + // 表单提交事件 + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + setButtonLoading(true); + + try { + const formData = new FormData(form); + const data = Object.fromEntries(formData); + + const response = await fetch('/contact', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + // 成功后跳转到成功页面 + window.location.href = '/contact/success'; + } else { + showMessage(result.message || '提交失败,请重试', 'error'); + } + } catch (error) { + console.error('提交错误:', error); + showMessage('网络错误,请检查网络连接后重试', 'error'); + } finally { + setButtonLoading(false); + } + }); + + // 字段实时验证(去除错误状态) + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + field.addEventListener('input', function() { + if (field.classList.contains('border-red-500')) { + clearFieldError(fieldName); + } + }); + + field.addEventListener('change', function() { + if (field.classList.contains('border-red-500')) { + clearFieldError(fieldName); + } + }); + }); + }); diff --git a/src/views/page/extra/contactSuccess.pug b/src/views/page/extra/contactSuccess.pug new file mode 100644 index 0000000..9a470e5 --- /dev/null +++ b/src/views/page/extra/contactSuccess.pug @@ -0,0 +1,62 @@ +extends /layouts/empty.pug + +block pageHead + +block pageContent + .contact-success.container(class="mt-[20px] bg-white rounded-[12px] shadow p-6 border border-gray-100") + .success-content(class="text-center py-8") + // 成功图标 + .success-icon(class="mb-6") + .icon-container(class="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full") + span(class="text-4xl text-green-600") ✓ + + // 成功标题 + h1(class="text-3xl font-bold mb-4 text-gray-800") 留言提交成功! + + // 成功消息 + .success-message(class="mb-8") + p(class="text-lg text-gray-600 mb-3") 感谢您的留言,我们已经收到了您的反馈。 + p(class="text-gray-500") 我们会在 24小时内 通过邮箱回复您,请注意查收。 + + // 联系信息卡片 + .contact-cards(class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8") + .contact-card(class="text-center p-4 bg-blue-50 rounded-lg border border-blue-200") + .icon(class="text-2xl mb-2") 📧 + h3(class="font-semibold text-blue-800 mb-1") 邮箱回复 + p(class="text-sm text-gray-600") 24小时内回复 + .contact-card(class="text-center p-4 bg-green-50 rounded-lg border border-green-200") + .icon(class="text-2xl mb-2") 💬 + h3(class="font-semibold text-green-800 mb-1") 在线客服 + p(class="text-sm text-gray-600") 工作日 9:00-18:00 + .contact-card(class="text-center p-4 bg-purple-50 rounded-lg border border-purple-200") + .icon(class="text-2xl mb-2") 📱 + h3(class="font-semibold text-purple-800 mb-1") 社交媒体 + p(class="text-sm text-gray-600") 实时互动 + + // 操作按钮 + .actions(class="flex flex-col sm:flex-row justify-center gap-4") + a(href="/" class="inline-flex items-center justify-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors") + span(class="mr-2") 🏠 + | 返回首页 + a(href="/contact" class="inline-flex items-center justify-center px-6 py-3 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors") + span(class="mr-2") ✍️ + | 再次留言 + a(href="/about" class="inline-flex items-center justify-center px-6 py-3 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors") + span(class="mr-2") ℹ️ + | 了解我们 + + // 温馨提示 + .tips(class="mt-8 p-4 bg-yellow-50 rounded-lg border border-yellow-200") + h3(class="font-semibold text-yellow-800 mb-2 flex items-center") + span(class="mr-2") 💡 + | 温馨提示 + ul(class="text-sm text-yellow-700 space-y-1 text-left") + li • 请确保您提供的邮箱地址正确,以便我们及时回复 + li • 如有紧急问题,建议通过在线客服或电话联系我们 + li • 您也可以关注我们的社交媒体获取最新动态和回复 + li • 我们承诺保护您的隐私,不会泄露您的联系信息 + + // 页脚信息 + .contact-footer(class="text-center mt-8 pt-6 border-t border-gray-200") + p(class="text-gray-500 text-sm") 再次感谢您对我们的关注和支持! + p(class="text-gray-400 text-xs mt-2") 如有其他问题,欢迎随时与我们联系 \ No newline at end of file diff --git a/src/views/page/profile/index.pug b/src/views/page/profile/index.pug index f0fc9d0..d5453c2 100644 --- a/src/views/page/profile/index.pug +++ b/src/views/page/profile/index.pug @@ -49,6 +49,106 @@ block pageHead color: rgba(255,255,255,0.8); } + // 头像上传相关样式 + .avatar-upload-section { + position: relative; + margin-bottom: 20px; + } + + .avatar-preview { + width: 120px; + height: 120px; + border-radius: 50%; + border: 3px dashed #d1d5db; + background: #f9fafb; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .avatar-preview:hover { + border-color: #667eea; + background: #f0f4ff; + } + + .avatar-preview img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; + } + + .avatar-preview .avatar-upload-placeholder { + text-align: center; + color: #9ca3af; + font-size: 0.875rem; + } + + .avatar-preview .upload-icon { + font-size: 2rem; + margin-bottom: 8px; + display: block; + } + + .avatar-preview .upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.6); + color: white; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + border-radius: 50%; + font-size: 0.875rem; + text-align: center; + } + + .avatar-preview:hover .upload-overlay { + opacity: 1; + } + + .file-input-hidden { + position: absolute; + opacity: 0; + width: 0; + height: 0; + overflow: hidden; + } + + .avatar-upload-info { + text-align: center; + font-size: 0.8rem; + color: #6b7280; + margin-bottom: 16px; + } + + .upload-progress { + width: 100%; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + overflow: hidden; + margin-top: 8px; + display: none; + } + + .upload-progress-bar { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + width: 0; + transition: width 0.3s ease; + } + .profile-name { font-size: 1.5rem; font-weight: 600; @@ -459,9 +559,6 @@ block pageHead } block pageContent - form(action="/profile/upload-avatar" method="post" enctype="multipart/form-data") - input(type="file", name="avatar", accept="image/*" onchange="document.getElementById('upload-btn').click()") - button#upload-btn(type="submit") 上传头像 .profile-container .profile-sidebar .profile-avatar @@ -553,14 +650,44 @@ block pageContent placeholder="介绍一下自己..." )= user.bio || '' + // 头像上传区域 + .form-group + label.form-label 头像设置 + .avatar-upload-section + .avatar-preview(onclick="document.getElementById('avatarFile').click()") + if user.avatar + img(src=user.avatar alt="当前头像" id="avatarPreviewImg") + .upload-overlay + div + div 📷 + div 点击更换头像 + else + .avatar-upload-placeholder + span.upload-icon 📷 + div 点击上传头像 + + input.file-input-hidden#avatarFile( + type="file" + name="avatarFile" + accept="image/*" + onchange="handleAvatarSelect(this)" + ) + + .avatar-upload-info + | 支持 JPG、PNG、GIF 格式,文件大小不超过 5MB + + .upload-progress#uploadProgress + .upload-progress-bar#uploadProgressBar + .form-group - label.form-label(for="avatar") 头像URL + label.form-label(for="avatar") 头像URL(可选) input.form-input#avatar( - type="url" + type="text" name="avatar" value=user.avatar || '' - placeholder="请输入头像图片链接" + placeholder="或直接输入头像图片链接" ) + .error-message#avatar-error .form-actions button.btn.btn-primary(type="submit") 保存更改