Browse Source

feat(profile): 增加头像上传功能并优化联系表单页面

- 在个人资料页新增头像上传及预览组件,支持本地图片文件选择与拖拽上传
- 实现头像上传过程中的类型、大小校验以及上传进度反馈
- 支持输入头像URL,增加图片链接格式有效性验证
- 优化联系表单,新增前端字段验证与错误提示,提升用户体验
- 联系表单提交成功后跳转新增的感谢反馈页面
- 规范联系表单后端接口请求,添加邮箱及内容的服务器端验证和日志记录
- 统一修正服务导入路径,移除未使用的服务统一导出文件
- 调整 vscode 配置,将 pug 关联语言由 pug 修改为 jade
- 优化联系页面 UI 及样式,改进反馈类型选择和错误消息显示机制
main
谢亚昕 3 months ago
parent
commit
695da012de
  1. 2
      .vscode/settings.json
  2. BIN
      database/development.sqlite3-shm
  3. BIN
      database/development.sqlite3-wal
  4. 182
      public/js/profile.js
  5. 2
      src/controllers/Api/AuthController.js
  6. 2
      src/controllers/Page/AuthPageController.js
  7. 45
      src/controllers/Page/BasePageController.js
  8. 2
      src/controllers/Page/ProfileController.js
  9. 36
      src/services/index.js
  10. 225
      src/views/page/extra/contact.pug
  11. 62
      src/views/page/extra/contactSuccess.pug
  12. 139
      src/views/page/profile/index.pug

2
.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"

BIN
database/development.sqlite3-shm

Binary file not shown.

BIN
database/development.sqlite3-wal

Binary file not shown.

182
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 = '<div><div>📷</div><div>点击更换头像</div></div>';
avatarPreview.appendChild(overlay);
}
// 同时更新侧边栏头像
if (sidebarAvatar) {
sidebarAvatar.innerHTML = `<img src="${imageSrc}" alt="用户头像">`;
}
}
// 上传头像文件
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';
}
}
}
})();

2
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"

2
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"

45
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 })

2
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"

36
src/services/index.js

@ -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
}

225
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);
}
});
});
});

62
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") 我们会在 <strong class="text-green-600">24小时内</strong> 通过邮箱回复您,请注意查收。
// 联系信息卡片
.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") 如有其他问题,欢迎随时与我们联系

139
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") 保存更改

Loading…
Cancel
Save