'use strict';
/**
* 个人中心 JavaScript
* 支持pjax初始化和响应式设计
* @version 2.1
* @author 系统管理员
*/
// 常量配置
const CONSTANTS = {
// 页面类型
PAGES: {
PROFILE: 'profile',
ARTICLES: 'articles',
MUMBLES: 'mumbles',
TIMELINE: 'timeline',
PHOTOS: 'photos'
},
// 分页配置
PAGINATION: {
DEFAULT_PAGE_SIZE: 10,
DEFAULT_PAGE_NUM: 1,
MAX_VISIBLE_PAGES: 5
},
// 响应式断点
BREAKPOINTS: {
MOBILE: 768, // 统一使用 768px 作为移动端断点
TABLET: 768,
DESKTOP: 1024
},
// 延迟时间
DELAYS: {
LOADING: 300,
TOAST_AUTO_HIDE: 3000,
TOAST_HIDE_ANIMATION: 300
},
// 消息类型配置
MESSAGE_TYPES: {
SUCCESS: {
type: 'success',
icon: 'fa-check-circle',
color: '#059669',
bgColor: '#ffffff',
borderColor: '#10b981',
textColor: '#065f46'
},
ERROR: {
type: 'error',
icon: 'fa-times-circle',
color: '#dc2626',
bgColor: '#ffffff',
borderColor: '#ef4444',
textColor: '#7f1d1d'
},
WARNING: {
type: 'warning',
icon: 'fa-exclamation-triangle',
color: '#d97706',
bgColor: '#ffffff',
borderColor: '#f59e0b',
textColor: '#92400e'
},
INFO: {
type: 'info',
icon: 'fa-info-circle',
color: '#2563eb',
bgColor: '#ffffff',
borderColor: '#3b82f6',
textColor: '#1e40af'
}
},
// 选择器缓存
SELECTORS: {
// 导航相关
navItem: '.nav-item',
navItemLink: '.nav-item a',
contentPage: '.content-page',
currentPageTitle: '#currentPageTitle',
// 布局相关
menuToggle: '.menu-toggle',
sidebarOverlay: '.sidebar-overlay',
memberSidebar: '.member-sidebar',
tableContainer: '.table-container',
photosGrid: '.photos-grid',
// 模态框相关
publishModal: '#publishModal',
deleteModal: '#deleteModal',
twoFactorModal: '#twoFactorModal',
modalBody: '#modalBody',
modalTitle: '#modalTitle',
confirmPublish: '#confirmPublish',
// 加载相关
loadingOverlay: '#loadingOverlay',
toastContainer: '#toast-container',
progressOverlay: '#progressOverlay'
}
};
/**
* 个人中心主类
*/
class MemberCenter {
constructor() {
this.state = {
currentPage: CONSTANTS.PAGES.PROFILE,
currentTheme: 'light',
currentDeleteItem: null
};
this.cherryInstance = null;
this.photoViewer = null;
this.initialize();
}
initialize() {
try {
this.bindEvents();
this.initPjax();
this.initTheme();
this.initMarkdownEditor();
this.initPhotoViewer();
if ($('#startRecording').length > 0) {
this.initAudioRecorder();
}
} catch (error) {
console.error('初始化失败:', error);
this.showMessage('系统初始化失败,请刷新页面重试', 'error');
}
}
initPjax() {
$.pjax.defaults.timeout = 5000;
const mainSelectors = [
'nav.member-nav > ul > li.nav-item > a',
'#newArticle', '#newMumble', '#newTimeline', '#uploadPhoto',
'.page-header .btn-secondary',
'.list-actions a',
'.photo-actions a'
];
// 绑定主内容区 PJAX
$(document).pjax(mainSelectors.join(', '), '#pjax-container', {scrollTo: 0});
// 绑定列表分页 PJAX
$(document).pjax('#member-article .pagination a', '#member-article', {scrollTo: 0});
$(document).pjax('#member-mumble .pagination a', '#member-mumble', {scrollTo: 0});
$(document).pjax('#member-timeline .pagination a', '#member-timeline', {scrollTo: 0});
$(document).pjax('#member-photo .pagination a', '#member-photo', {scrollTo: 0});
$(document).on('pjax:send', () => {
$(CONSTANTS.SELECTORS.loadingOverlay).addClass('active');
});
$(document).on('pjax:complete', () => {
$(CONSTANTS.SELECTORS.loadingOverlay).removeClass('active');
});
$(document).on('pjax:end', () => {
window.scrollTo(0, 0);
if ($('#startRecording').length > 0) {
this.initAudioRecorder();
}
this.initMarkdownEditor();
this.initPhotoViewer();
// 更新页面标题
this.updatePageTitle();
// 移动端:PJAX加载完成后自动关闭菜单
if (this.isMobile()) {
this.closeMobileMenu();
}
});
// 初始化进度条 DOM
if ($(CONSTANTS.SELECTORS.progressOverlay).length === 0) {
$('body').append(`
<div class="loading-overlay" id="progressOverlay" style="display: none; flex-direction: column; z-index: 9999;">
<div class="loading-spinner" style="width: 80%; max-width: 400px; text-align: center;">
<div style="font-size: 1.1rem; margin-bottom: 15px; color: var(--text-color);" id="progressText">准备上传...</div>
<div style="width: 100%; height: 10px; background: rgba(0,0,0,0.1); border-radius: 5px; overflow: hidden; position: relative;">
<div id="progressBar" class="progress-bar-animated" style="width: 0%; height: 100%; background-color: var(--primary-color); border-radius: 5px; transition: width 0.2s ease;"></div>
</div>
<div id="progressPercent" style="margin-top: 8px; font-size: 0.9rem; color: var(--text-secondary);">0%</div>
</div>
</div>
`);
}
// 初始化进度条 DOM
if ($(CONSTANTS.SELECTORS.progressOverlay).length === 0) {
$('body').append(`
<div class="loading-overlay" id="progressOverlay" style="display: none; flex-direction: column; z-index: 9999;">
<div class="loading-spinner" style="width: 80%; max-width: 400px; text-align: center;">
<div style="font-size: 1.1rem; margin-bottom: 15px; color: var(--text-color);" id="progressText">准备上传...</div>
<div style="width: 100%; height: 10px; background: rgba(0,0,0,0.1); border-radius: 5px; overflow: hidden; position: relative;">
<div id="progressBar" class="progress-bar-animated" style="width: 0%; height: 100%; background-color: var(--primary-color); border-radius: 5px; transition: width 0.2s ease;"></div>
</div>
<div id="progressPercent" style="margin-top: 8px; font-size: 0.9rem; color: var(--text-secondary);">0%</div>
</div>
</div>
`);
}
}
/**
* 更新页面标题
*/
updatePageTitle() {
try {
// 直接获取当前激活的导航菜单文本
const $activeNav = $('nav.member-nav .nav-item.active a');
let title = '';
if ($activeNav.length > 0) {
// 获取纯文本
title = $activeNav.text().trim();
}
// 如果没有获取到(比如在非导航菜单页面),回退到默认标题
if (!title) {
title = '个人中心';
}
// 设置文档标题
document.title = `${title} - 个人中心`;
} catch (error) {
console.error('更新页面标题失败:', error);
}
}
initAudioRecorder() {
if (!this.audioRecorder) {
if (typeof AudioRecorder === 'undefined') return;
this.audioRecorder = new AudioRecorder({
onMessage: (msg, type) => this.showMessage(msg, type),
onLoading: (show, msg) => show ? this.showLoading(msg) : this.hideLoading()
});
}
this.audioRecorder.init();
}
bindEvents() {
this.bindMobileMenuEvents();
this.bindWindowEvents();
this.bindContentEvents();
this.bindModalEvents();
this.bindViewContentEvents();
this.bindKeyboardEvents();
this.bindSidebarActive();
}
bindSidebarActive() {
$(document).on('click', CONSTANTS.SELECTORS.navItem, (e) => {
$(CONSTANTS.SELECTORS.navItem).removeClass('active');
$(e.currentTarget).addClass('active');
});
}
initMarkdownEditor() {
const articleContentEl = document.getElementById('articleContent');
const mumbleContentEl = document.getElementById('mumbleContent');
const timelineContentEl = document.getElementById('timelineContent');
const uploadAddress = document.getElementById('uploadAddress')?.value;
const editorEl = (articleContentEl || mumbleContentEl || timelineContentEl);
if (!editorEl) return;
const markdownValueEl = document.getElementById('txt_' + editorEl.id);
if (!markdownValueEl) return;
const editorId = editorEl.id;
const markdown = markdownValueEl.value;
if (this.cherryInstance) {
this.cherryInstance = null;
}
let that = this;
try {
this.cherryInstance = new Cherry({
id: editorId,
value: markdown,
height: "100%",
defaultModel: "editOnly",
themeSettings: {
mainTheme: this.state.currentTheme,
codeBlockTheme: 'one-dark',
},
engine: {
syntax: {
codeBlock: {
editCode: false,
changeLang: false,
},
}
},
toolbars: {
toolbar: ['bold', 'italic', 'size', '|', 'color', 'header', 'togglePreview', '|', 'theme',
{insert: ['image', 'link', 'hr', 'br', 'code', 'table']}
],
},
fileUpload: async function (file, callback) {
if (uploadAddress && uploadAddress.trim() !== '') {
await that.fileUpload(file, callback, uploadAddress);
} else {
callback('');
}
}
});
} catch (e) {
console.error('Markdown 编辑器初始化失败:', e);
}
markdownValueEl.style.display = 'none';
if (this.cherryInstance) {
this.cherryInstance.switchModel('editOnly');
}
}
// 带进度的上传方法
uploadWithProgress(url, formData, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
// 监听上传进度
if (xhr.upload && onProgress) {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
onProgress(percent);
}
};
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (e) {
reject(new Error('Invalid JSON response'));
}
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send(formData);
});
}
async fileUpload(file, callback, uploadUrl) {
const formData = new FormData();
formData.append("image", file);
try {
this.showProgress('正在上传图片...');
const result = await this.uploadWithProgress(uploadUrl, formData, (percent) => {
this.updateProgress(percent, '正在上传图片...');
});
this.hideProgress();
callback(result["url"]);
} catch (error) {
this.hideProgress();
this.showMessage('图片上传失败: ' + error.message, 'error');
console.error('图片上传失败:', error);
}
}
bindMobileMenuEvents() {
$(document).on('click', CONSTANTS.SELECTORS.menuToggle, () => this.toggleMobileMenu());
$(document).on('click', CONSTANTS.SELECTORS.sidebarOverlay, () => this.closeMobileMenu());
}
bindWindowEvents() {
$(window).on('resize', () => this.handleResize());
}
bindContentEvents() {
$(document).on('click', '#saveProfile', () => this.saveProfile());
$(document).on('click', '#logout', () => this.logout());
$(document).on('click', '#uploadAvatar', () => $('#avatarFile').click());
$(document).on('change', '#avatarFile', (e) => this.handleAvatarUpload(e));
// Articles
$(document).on('click', '#searchArticles', () => {
const title = $('#articleTitleSearch').val();
const tag = $('#articleTagSearch').val();
const url = new URL('/member/articles', window.location.origin);
if (title) url.searchParams.set('title', title);
if (tag) url.searchParams.set('tag', tag);
$.pjax({url: url.href, container: '#member-article', push: true});
});
$(document).on('click', '#clearArticles', () => {
$('#articleTitleSearch').val('');
$('#articleTagSearch').val('');
$('#searchArticles').click();
});
$(document).on('keyup', '#articleTitleSearch', (e) => {
if (e.key === 'Enter') $('#searchArticles').click();
});
// Mumbles
$(document).on('click', '#searchMumbles', () => {
const content = $('#mumbleContentSearch').val();
const url = new URL('/member/mumbles', window.location.origin);
if (content) url.searchParams.set('content', content);
$.pjax({url: url.href, container: '#member-mumble', push: true});
});
$(document).on('click', '#clearMumbles', () => {
$('#mumbleContentSearch').val('');
$('#searchMumbles').click();
});
$(document).on('keyup', '#mumbleContentSearch', (e) => {
if (e.key === 'Enter') $('#searchMumbles').click();
});
// Timeline
$(document).on('click', '#searchTimeline', () => {
const content = $('#timelineSearch').val();
const url = new URL('/member/timeline', window.location.origin);
if (content) url.searchParams.set('content', content);
$.pjax({url: url.href, container: '#member-timeline', push: true});
});
$(document).on('click', '#clearTimeline', () => {
$('#timelineSearch').val('');
$('#searchTimeline').click();
});
$(document).on('keyup', '#timelineSearch', (e) => {
if (e.key === 'Enter') $('#searchTimeline').click();
});
// Photos
$(document).on('click', '#searchPhotos', () => {
const tag = $('#photoTagSearch').val();
const desc = $('#photoDescSearch').val();
const url = new URL('/member/photos', window.location.origin);
if (tag) url.searchParams.set('tag', tag);
if (desc) url.searchParams.set('description', desc);
$.pjax({url: url.href, container: '#member-photo', push: true});
});
$(document).on('click', '#clearPhotos', () => {
$('#photoTagSearch').val('');
$('#photoDescSearch').val('');
$('#searchPhotos').click();
});
$(document).on('keyup', '#photoDescSearch', (e) => {
if (e.key === 'Enter') $('#searchPhotos').click();
});
$(document).on('click', '#confirmPublish', (e) => {
const $btn = $(e.currentTarget);
const action = $btn.data('action');
if (action && typeof this[action] === 'function') {
this.withButtonLoading($btn, async () => {
await this[action]();
});
}
});
$(document).on('change', '#photoFile', function (event) {
const file = event.target.files[0];
if (!file) return;
// 显示预览
const reader = new FileReader();
reader.onload = (e) => {
$('#photoPreview').attr('src', e.target.result);
$('#photoPreview').show();
$('#imageLoadingPlaceholder').remove();
$('#previewContainer').addClass('active');
$('#dropZone').hide();
// 显示文件信息
$('#fileName').text(file.name);
$('#fileSize').text(formatFileSize(file.size));
// 初始化图片查看器
memberCenter.initPhotoViewer();
};
reader.readAsDataURL(file);
});
// 移除文件按钮
$(document).on('click', '#removeFileBtn', function () {
$('#photoFile').val('');
$('#previewContainer').removeClass('active');
$('#dropZone').show();
$('#photoPreview').attr('src', '');
// 清理图片查看器
if (memberCenter && memberCenter.photoViewer) {
// 如果查看器正在显示,先关闭它
try {
if (typeof memberCenter.photoViewer.close === 'function') {
memberCenter.photoViewer.close();
}
} catch (e) {
console.warn('关闭 PhotoSwipe 失败:', e);
}
memberCenter.photoViewer = null;
}
});
// 查看大图按钮
$(document).on('click', '#viewPhotoBtn', function () {
const photoPreview = document.getElementById('photoPreview');
if (memberCenter && photoPreview && photoPreview.src) {
memberCenter.openPhotoSwipe(photoPreview);
}
});
// 拖拽效果
const dropZone = document.getElementById('dropZone');
if (dropZone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
dropZone.classList.add('drag-over');
}
function unhighlight(e) {
dropZone.classList.remove('drag-over');
}
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
const fileInput = document.getElementById('photoFile');
if (files.length > 0) {
fileInput.files = files;
// 手动触发 change 事件
const event = new Event('change', {bubbles: true});
fileInput.dispatchEvent(event);
}
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
bindModalEvents() {
$('#closeDeleteModal').on('click', () => this.hideDeleteModal());
$('#cancelDelete').on('click', () => this.hideDeleteModal());
$('#confirmDelete').on('click', () => this.confirmDelete());
$(document).on('click', '.modal-overlay', (e) => {
if (e.target === e.currentTarget) {
this.hideDeleteModal();
}
});
}
bindViewContentEvents() {
$('#closeViewContentModal, #closeViewContentBtn').on('click', () => {
$('#viewContentModal').hide();
});
$('#viewContentModal').on('click', (e) => {
if (e.target === e.currentTarget) {
$('#viewContentModal').hide();
}
});
$(document).on('click', '.mobile-content-toggle', (e) => {
this.toggleMobileContent(e.target);
});
}
showContent(content, type = 'markdown') {
let htmlContent = content;
if (type === 'markdown' && typeof showdown !== 'undefined') {
const converter = new showdown.Converter();
htmlContent = converter.makeHtml(content);
} else if (type === 'text') {
htmlContent = $('<div>').text(content).html().replace(/\n/g, '<br>');
}
const $content = $('<div>').html(htmlContent);
$content.find('img').css({'max-width': '100%', 'height': 'auto'});
$('#viewContentModalBody').html($content);
$('#viewContentModal').css('display', 'flex');
// 代码高亮
if (typeof Prism !== 'undefined') {
try {
// 对模态框内的代码块进行高亮
const codeBlocks = document.getElementById('viewContentModalBody').querySelectorAll('pre code');
if (codeBlocks.length > 0) {
Prism.highlightAllUnder(document.getElementById('viewContentModalBody'));
}
} catch (e) {
console.error('代码高亮失败:', e);
}
}
}
toggleMobileContent(btn) {
const $btn = $(btn);
const $wrapper = $btn.prev('.mobile-content-wrapper');
if ($wrapper.hasClass('collapsed')) {
$wrapper.removeClass('collapsed');
$btn.html('<i class="fa fa-angle-up"></i> 收起内容');
} else {
$wrapper.addClass('collapsed');
$btn.html('<i class="fa fa-angle-down"></i> 展开全文');
}
}
bindKeyboardEvents() {
$(document).on('keydown', (e) => {
if (e.key === 'Escape') {
this.hideDeleteModal();
}
});
}
// 保存个人资料
async saveProfile() {
try {
const formData = new FormData();
formData.append('name', $('#nickname').val().trim());
formData.append('sex', $('#gender').val());
formData.append('sign', $('#signature').val().trim());
this.showLoading();
const response = await fetch('/Account/UpdateInfo', {
method: 'POST',
body: formData
});
const result = await response.json();
this.hideLoading();
if (!result.success) {
this.showMessage(result.message || '保存失败', 'error');
return;
}
this.showMessage('保存成功', 'success');
} catch (error) {
console.error('保存个人资料失败:', error);
this.hideLoading();
this.showMessage('保存失败,请重试', 'error');
}
}
// 退出登录
async logout() {
if (confirm('确定要退出登录吗?')) {
this.showLoading();
try {
const response = await fetch('/Account/LogOut', {
method: 'POST'
});
if (response.redirected) {
window.location.href = response.url;
} else if (response.ok) {
window.location.href = '/';
} else {
this.showMessage('退出失败', 'error');
}
} catch (error) {
console.error('退出失败:', error);
this.showMessage('退出失败', 'error');
} finally {
this.hideLoading();
}
}
}
handleAvatarUpload(event) {
try {
const file = event.target.files[0];
if (!file) return;
if (!this.validateImageFile(file)) {
return;
}
this.previewImage(file);
this.uploadImage(file);
} catch (error) {
console.error('头像上传失败:', error);
this.showMessage('头像上传失败', 'error');
}
}
validateImageFile(file) {
if (!file.type.startsWith('image/')) {
this.showMessage('请选择图片文件', 'error');
return false;
}
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
this.showMessage('图片文件不能超过5MB', 'error');
return false;
}
return true;
}
previewImage(file) {
const reader = new FileReader();
reader.onload = (e) => {
$('#avatarPreview, #userAvatar').attr('src', e.target.result);
};
reader.readAsDataURL(file);
}
// 上传头像实现
async uploadImage(file) {
this.showLoading();
const form = new FormData();
form.append('avatar', file);
try {
const response = await fetch('/Account/UpdateAvatar', {
method: 'POST',
body: form,
});
const result = await response.json();
this.hideLoading();
if (!result.success) {
this.showMessage(result.msg, 'error');
return;
}
this.showMessage('头像上传成功', 'success');
} catch (error) {
this.hideLoading();
this.showMessage('上传发生错误', 'error');
}
}
deleteItem(type, id) {
this.state.currentDeleteItem = {type, id};
$('#deleteModal').show();
}
confirmDelete() {
if (this.state.currentDeleteItem) {
this.showLoading();
setTimeout(() => {
this.hideLoading();
this.hideDeleteModal();
this.showMessage('删除成功', 'success');
$.pjax.reload('#' + this.getCurrentContainerId());
}, 300); // Small delay for UX
}
}
getCurrentContainerId() {
if (location.pathname.includes('articles')) return 'member-article';
if (location.pathname.includes('mumbles')) return 'member-mumble';
if (location.pathname.includes('timeline')) return 'member-timeline';
if (location.pathname.includes('photos')) return 'member-photo';
return 'pjax-container';
}
async publishArticle() {
// ... (existing publishArticle logic kept as it uses its own endpoints)
const submitAddressEl = document.getElementById('submitAddress');
if (!submitAddressEl) return;
const action = submitAddressEl.value;
const id = document.getElementById('articleId').value;
const title = document.getElementById('articleTitle').value;
const tagsSelect = document.getElementById('articleTags');
const tags = Array.from(tagsSelect.selectedOptions).map(x => x.value);
const introduction = document.getElementById('articleIntroduction').value;
const newTag = document.getElementById('articleExtraTags').value;
let markdown = '';
let content = '';
if (this.cherryInstance) {
markdown = this.cherryInstance.getMarkdown();
content = this.cherryInstance.getHtml();
} else {
const txt = document.getElementById('txt_articleContent');
if (txt) {
markdown = txt.value;
content = markdown;
}
}
if (title.trim() === '') {
this.showMessage('标题不能为空', 'warning');
return;
}
if (newTag.trim() === '' && tags.length === 0) {
this.showMessage('请选择标签或输入补充标签', 'warning');
return;
}
if (introduction.trim() === '') {
this.showMessage('简介不能为空', 'warning');
return;
}
if (markdown.trim() === '' || markdown.length < 5) {
this.showMessage('内容不能为空', 'warning');
return;
}
const formData = new FormData();
if (id.trim() !== '') {
formData.append("id", id);
}
formData.append("title", title);
for (const tag of tags) {
formData.append("tags", tag);
}
formData.append("introduction", introduction);
formData.append("markdown", markdown);
formData.append("content", content);
formData.append("newTag", newTag);
try {
const response = await fetch(action, {method: 'POST', body: formData});
const result = await response.json();
if (!result.success) {
this.showMessage(result.msg || '发布失败', 'error');
return;
}
this.showMessage('文章发布成功', 'success');
$.pjax({url: '/member/articles', container: '#pjax-container'});
} catch (error) {
this.showMessage('发布失败: ' + error.message, 'error');
}
}
async publishMumble() {
const submitAddressEl = document.getElementById('submitAddress');
if (!submitAddressEl) return;
const action = submitAddressEl.value;
const id = document.getElementById('mumbleId')?.value;
let content = '';
let html = '';
if (this.cherryInstance) {
content = this.cherryInstance.getMarkdown();
html = this.cherryInstance.getHtml();
} else {
const el = document.getElementById('mumbleContent');
content = el ? el.value : '';
}
if (!content || content.trim() === '' || content.length < 5) {
this.showMessage('请输入内容', 'warning');
return;
}
const formData = new FormData();
if (id.trim() !== '') {
formData.append("id", id);
}
formData.append("markdown", content);
formData.append("html", html);
try {
const response = await fetch(action, {method: 'POST', body: formData});
const result = await response.json();
if (!result.success) {
this.showMessage(result.msg || '发布失败', 'error');
return;
}
this.showMessage('碎碎念发布成功', 'success');
$.pjax({url: '/member/mumbles', container: '#pjax-container'});
} catch (error) {
this.showMessage('发布失败: ' + error.message, 'error');
}
}
async publishTimeline() {
const submitAddressEl = document.getElementById('submitAddress');
if (!submitAddressEl) return;
const action = submitAddressEl.value;
const id = document.getElementById('timelineId').value;
const title = document.getElementById('timelineTitle').value;
const more = document.getElementById('timelineMore').value;
const date = document.getElementById('timelineDate').value;
let content = '';
if (this.cherryInstance) {
content = this.cherryInstance.getMarkdown();
} else {
const el = document.getElementById('timelineContent');
content = el ? el.value : '';
}
if (title.trim() === '') {
this.showMessage('标题不能为空', 'warning');
return;
}
if (date.trim() === '') {
this.showMessage('时间不能为空', 'warning');
return;
}
if (content.trim() === '' || content.length < 5) {
this.showMessage('内容不能为空', 'warning');
return;
}
const formData = new FormData();
if (id.trim() !== '') {
formData.append("id", id);
}
formData.append("title", title);
formData.append("more", more);
formData.append("date", date);
formData.append("content", content);
try {
const response = await fetch(action, {method: 'POST', body: formData});
const result = await response.json();
if (!result.success) {
this.showMessage(result.msg || '发布失败', 'error');
return;
}
this.showMessage('时间轴发布成功', 'success');
$.pjax({url: '/member/timeline', container: '#pjax-container'});
} catch (error) {
this.showMessage('发布失败: ' + error.message, 'error');
}
}
async publishPhoto() {
const submitAddressEl = document.getElementById('submitAddress');
if (!submitAddressEl) return;
const action = submitAddressEl.value;
const id = document.getElementById('photoId')?.value || '';
const fileInput = document.getElementById('photoFile');
const file = fileInput.files[0];
if (id.trim() === '' && !file) {
this.showMessage('请选择照片', 'warning');
return;
}
const tagsSelect = document.getElementById('photoTags');
const tags = Array.from(tagsSelect.selectedOptions).map(x => x.value).join(',');
const newTag = document.getElementById('photoExtraTags').value;
const description = document.getElementById('photoDescription').value;
const formData = new FormData();
if (id.trim() !== '') {
formData.append("id", id);
}
if (file) {
formData.append("photo", file);
}
formData.append("description", description);
let finalTags = tags;
if (newTag) {
finalTags = finalTags ? (finalTags + ',' + newTag) : newTag;
}
formData.append("tags", finalTags);
try {
// const response = await fetch(action, { method: 'POST', body: formData });
// const result = await response.json();
this.showProgress('正在上传照片...');
const result = await this.uploadWithProgress(action, formData, (percent) => {
this.updateProgress(percent, '正在上传照片...');
});
if (!result.success) {
this.showMessage(result["msg"] || '上传失败', 'error');
return;
}
this.showMessage('照片上传成功', 'success');
$.pjax({url: '/member/photos', container: '#pjax-container'});
} catch (error) {
this.showMessage('上传失败: ' + error.message, 'error');
} finally {
this.hideProgress(); // 确保 loading 被隐藏
}
}
hideDeleteModal() {
$(CONSTANTS.SELECTORS.deleteModal).hide();
this.state.currentDeleteItem = null;
}
showProgress(message = '上传中...') {
const $overlay = $(CONSTANTS.SELECTORS.progressOverlay);
if ($overlay.length > 0) {
$overlay.find('#progressText').text(message);
$overlay.find('#progressBar').css('width', '0%');
$overlay.find('#progressPercent').text('0%');
$overlay.css('display', 'flex').addClass('active');
}
}
updateProgress(percent, message) {
const $overlay = $(CONSTANTS.SELECTORS.progressOverlay);
if ($overlay.length > 0) {
if (message) $overlay.find('#progressText').text(message);
$overlay.find('#progressBar').css('width', `${percent}%`);
$overlay.find('#progressPercent').text(`${percent}%`);
}
}
hideProgress() {
const $overlay = $(CONSTANTS.SELECTORS.progressOverlay);
$overlay.removeClass('active').fadeOut(200);
}
showLoading(message = '加载中...') {
const loadingText = $('.loading-spinner .loading-text');
if (loadingText.length > 0) {
loadingText.text(message);
}
$(CONSTANTS.SELECTORS.loadingOverlay).addClass('active');
}
hideLoading() {
$(CONSTANTS.SELECTORS.loadingOverlay).removeClass('active');
}
showMessage(message, type = 'info') {
try {
const messageConfig = CONSTANTS.MESSAGE_TYPES;
const config = messageConfig[type.toUpperCase()] || messageConfig.INFO;
const messageId = `message-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const alert = this.createToastElement(messageId, type, message, config);
this.appendToastToContainer(alert);
this.animateToast(alert, messageId);
} catch (error) {
console.error('显示消息失败:', error);
}
}
createToastElement(messageId, type, message, config) {
const alert = $(`
<div class="toast-message" id="${messageId}" data-type="${type}">
<div class="toast-content">
<div class="toast-icon">
<i class="fa ${config.icon}"></i>
</div>
<div class="toast-text">
<div class="toast-message-text">${message}</div>
</div>
<button type="button" class="toast-close">
<i class="fa fa-times"></i>
</button>
</div>
<div class="toast-progress">
<div class="toast-progress-bar"></div>
</div>
</div>
`);
alert.css({
'--toast-color': config.color,
'--toast-bg-color': config.bgColor,
'--toast-border-color': config.borderColor,
'--toast-text-color': config.textColor
});
return alert;
}
appendToastToContainer(alert) {
let container = $(CONSTANTS.SELECTORS.toastContainer);
if (container.length === 0) {
container = $('<div id="toast-container"></div>');
$('body').append(container);
}
container.append(alert);
}
animateToast(alert, messageId) {
setTimeout(() => {
alert.addClass('toast-show');
}, 10);
setTimeout(() => {
alert.find('.toast-progress-bar').addClass('toast-progress-active');
}, 100);
alert.find('.toast-close').on('click', () => {
this.hideMessage(messageId);
});
setTimeout(() => {
this.hideMessage(messageId);
}, CONSTANTS.DELAYS.TOAST_AUTO_HIDE);
}
hideMessage(messageId) {
try {
const alert = $(`#${messageId}`);
if (alert.length === 0) return;
alert.addClass('toast-hide');
setTimeout(() => {
alert.remove();
const container = $(CONSTANTS.SELECTORS.toastContainer);
if (container.children().length === 0) {
container.remove();
}
}, CONSTANTS.DELAYS.TOAST_HIDE_ANIMATION);
} catch (error) {
console.error('隐藏消息失败:', error);
}
}
initTheme() {
try {
const savedTheme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
this.setTheme(savedTheme);
} catch (error) {
console.error('初始化主题失败:', error);
this.setTheme('light');
}
}
setTheme(theme) {
try {
if (!['light', 'dark'].includes(theme)) {
theme = 'light';
}
this.state.currentTheme = theme;
$('html').attr('data-theme', theme);
localStorage.setItem('theme', theme);
} catch (error) {
console.error('设置主题失败:', error);
}
}
toggleMobileMenu() {
$(CONSTANTS.SELECTORS.memberSidebar).toggleClass('active');
$(CONSTANTS.SELECTORS.sidebarOverlay).toggleClass('active');
}
closeMobileMenu() {
$(CONSTANTS.SELECTORS.memberSidebar).removeClass('active');
$(CONSTANTS.SELECTORS.sidebarOverlay).removeClass('active');
}
handleResize() {
try {
const width = $(window).width();
if (width > CONSTANTS.BREAKPOINTS.TABLET) {
this.closeMobileMenu();
}
} catch (error) {
console.error('处理窗口大小改变失败:', error);
}
}
async withButtonLoading(btn, asyncAction) {
const $btn = $(btn);
const originalHtml = $btn.html();
// const originalWidth = $btn.outerWidth();
try {
$btn.addClass('btn-loading');
$btn.prop('disabled', true);
$btn.html(`<i class="fa fa-spinner fa-spin"></i> ${originalHtml}`);
await asyncAction();
} finally {
$btn.prop('disabled', false);
$btn.removeClass('btn-loading');
$btn.html(originalHtml);
}
}
isMobile() {
return $(window).width() <= CONSTANTS.BREAKPOINTS.MOBILE;
}
/**
* 初始化照片查看器
*/
initPhotoViewer() {
try {
const photoPreview = document.getElementById('photoPreview');
if (!photoPreview) return;
// 处理已加载的图片(编辑模式)
const loadingPlaceholder = document.getElementById('imageLoadingPlaceholder');
if (loadingPlaceholder && photoPreview.src && photoPreview.src !== '') {
// 检查图片是否已经加载
if (photoPreview.complete && photoPreview.naturalHeight !== 0) {
// 图片已加载完成
loadingPlaceholder.remove();
photoPreview.style.display = 'block';
this.initPhotoSwipe(photoPreview);
} else {
// 图片还在加载中,添加加载事件监听
photoPreview.onload = () => {
if (loadingPlaceholder) {
loadingPlaceholder.remove();
}
photoPreview.style.display = 'block';
this.initPhotoSwipe(photoPreview);
};
photoPreview.onerror = () => {
if (loadingPlaceholder) {
loadingPlaceholder.innerHTML = '<i class="fa fa-exclamation-circle" style="font-size: 40px; color: var(--danger-color);"></i><span style="color: var(--text-secondary); margin-top: 8px;">照片加载失败</span>';
}
};
}
} else if (photoPreview.src && photoPreview.src !== '') {
// 没有加载占位符,直接初始化查看器
this.initPhotoSwipe(photoPreview);
}
} catch (error) {
console.error('初始化照片查看器失败:', error);
}
}
/**
* 初始化 PhotoSwipe 实例
*/
initPhotoSwipe(photoPreview) {
try {
// 检查 PhotoSwipe 是否可用
if (typeof PhotoSwipe === 'undefined' || typeof PhotoSwipeUI_Default === 'undefined') {
console.warn('PhotoSwipe 未加载');
return;
}
// 点击预览图片时显示查看器
$(photoPreview).off('click.photoswipe').on('click.photoswipe', (e) => {
e.preventDefault();
this.openPhotoSwipe(photoPreview);
});
} catch (error) {
console.error('初始化 PhotoSwipe 实例失败:', error);
}
}
/**
* 打开 PhotoSwipe 查看器
*/
openPhotoSwipe(imgElement) {
try {
const pswpElement = document.querySelectorAll('.pswp')[0];
// 构建图片数据
const items = [{
src: imgElement.src,
w: imgElement.naturalWidth || 1920,
h: imgElement.naturalHeight || 1080,
title: imgElement.alt || '照片预览'
}];
// PhotoSwipe 配置选项
const options = {
index: 0,
bgOpacity: 0.85,
showHideOpacity: true,
loop: false,
pinchToClose: true,
closeOnScroll: false,
closeOnVerticalDrag: true,
mouseUsed: false,
escKey: true,
arrowKeys: true,
history: false,
focus: true,
showAnimationDuration: 333,
hideAnimationDuration: 333,
getThumbBoundsFn: function (index) {
const thumbnail = imgElement;
const pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
const rect = thumbnail.getBoundingClientRect();
return {
x: rect.left,
y: rect.top + pageYScroll,
w: rect.width
};
}
};
// 初始化并打开 PhotoSwipe
const gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
gallery.init();
// 保存实例引用
this.photoViewer = gallery;
} catch (error) {
console.error('打开 PhotoSwipe 失败:', error);
}
}
}
let memberCenter = null;
$(document).ready(() => {
try {
memberCenter = new MemberCenter();
window.memberCenter = memberCenter;
console.log('个人中心初始化成功');
} catch (error) {
console.error('个人中心初始化失败:', error);
}
});
$(window).on('beforeunload', () => {
// 清理工作
});
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
try {
if (memberCenter && typeof memberCenter.showMessage === 'function') {
memberCenter.showMessage('系统出现错误,请刷新页面', 'error');
}
} catch (e) {
}
});
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面是对你提供的这段个人中心(MemberCenter)JavaScript 代码的逐项说明,按职责模块化地解释主要功能、关键方法、事件流和依赖。为便于理解,我把内容分成概览、初始化流程、主要模块、辅助方法与注意点几部分。
一、总体概览
- 这是一个面向单页/局部刷新的“个人中心”前端类 MemberCenter,使用 jQuery 与 PJAX 做部分页面加载、并集成了富文本/Markdown 编辑器、图片查看器、上传进度、消息提示、响应式侧栏等常见功能。
- 使用了常量对象 CONSTANTS 保存页面类型、分页、断点、延迟、消息样式与常用 DOM 选择器,便于维护与复用。
- 依赖(显式或隐式):jQuery、jquery-pjax、Cherry(编辑器)、PhotoSwipe、PhotoSwipeUI_Default、showdown(markdown 转 html,可选)、Prism(代码高亮,可选)、可能的 AudioRecorder。还使用浏览器原生 fetch、FormData、XMLHttpRequest(用于上传进度)。
二、初始化流程
- 页面就绪时($(document).ready),创建全局 memberCenter 实例并绑定到 window。
- MemberCenter.constructor 初始化 state(当前页面、主题、待删除项等),并调用 initialize()。
- initialize() 依次调用:
- bindEvents():绑定各种事件(移动菜单、窗口 resize、内容交互、模态框、查看内容、键盘、侧栏激活等)。
- initPjax():配置并绑定 PJAX 行为与加载覆盖层、以及初始化上传进度 DOM(会在 body 中追加 id 为 progressOverlay 的进度弹窗)。
- initTheme():从 localStorage 或系统偏好读取主题并设置(light/dark)。
- initMarkdownEditor():如果当前页面包含编辑器元素,初始化 Cherry 编辑器并隐藏原 textarea。
- initPhotoViewer():初始化图片预览/查看器(PhotoSwipe)。
- 如果页面包含 #startRecording,则 initAudioRecorder() 初始化音频录制器(如果全局存在 AudioRecorder)。
三、PJAX 与页面导航
- initPjax():
- 设置 $.pjax.defaults.timeout。
- 绑定主导航、创建新内容按钮、列表分页链接到不同的容器(#pjax-container、#member-article、#member-mumble、#member-timeline、#member-photo),实现无刷新局部加载。
- 监听 pjax:send / pjax:complete / pjax:end 事件:显示/隐藏 loading overlay,加载完成后重新初始化编辑器、音频、图片查看器并更新页面标题,移动端则自动关闭侧栏菜单。
- 在 body 中追加上传进度 overlay(代码中实际上有重复追加的块——见注意点)。
四、编辑器与文件上传
- initMarkdownEditor():
- 查找 articleContent / mumbleContent / timelineContent 三个编辑器元素之一,使用对应的隐藏 textarea(id 为 txt_ + editor.id)作为初始值。
- 使用 Cherry 编辑器实例(this.cherryInstance)初始化,配置主题、工具栏、fileUpload 回调(调用 memberCenter.fileUpload)。
- 隐藏原 textarea,切换到 editOnly 模式。
- fileUpload(file, callback, uploadUrl):
- 调用 uploadWithProgress(基于 XMLHttpRequest 的上传进度监听),显示进度覆盖层并把上传结果(result.url)传回编辑器回调。
- uploadWithProgress(url, formData, onProgress):
- 使用 XMLHttpRequest 以 POST 上传并在 xhr.upload.onprogress 中回调 percent,最终解析 JSON 响应。
五、照片(Photos)管理与 PhotoSwipe
- publishPhoto():收集表单(文件、tags、description 等),用 uploadWithProgress 上传(先 showProgress),上传完成后根据返回结果提示并用 PJAX 刷新照片列表。
- initPhotoViewer/initPhotoSwipe/openPhotoSwipe():
- 初始化 PhotoSwipe 点击打开逻辑,构建 items(基于 img.src、naturalWidth/naturalHeight),使用 pswp DOM(.pswp)与 PhotoSwipeUI_Default 打开查看器,并保存实例到 this.photoViewer。
- 代码中在处理编辑页面预览时,会监听图片加载完成或失败,移除占位符并显示图片。
六、表单发布(文章/碎碎念/时间轴)流程
- publishArticle(), publishMumble(), publishTimeline():
- 分别收集表单字段(title、tags、markdown、introduction 等),对必填项进行校验(如标题、简介、内容长度等),用 fetch POST 表单数据到 submitAddress(页面隐藏字段),处理 JSON 响应并用 this.showMessage 显示结果、通过 PJAX 刷新对应页面列表。
- publishArticle 会从 cherryInstance 获取 Markdown/HTML 内容(或退回原 textarea)。
七、头像上传与预览
- handleAvatarUpload(event):获取文件,校验类型/大小(validateImageFile),previewImage(FileReader -> 预览 img src),再 uploadImage()。
- uploadImage(file):用 fetch + FormData 提交到 /Account/UpdateAvatar,显示 loading,处理返回并提示成功/失败。
八、UI 与交互辅助函数
- showLoading / hideLoading:控制全局 loading overlay(通过 CONSTANTS.SELECTORS.loadingOverlay 的 active 类)。
- showProgress / updateProgress / hideProgress:控制上传进度覆盖层(progressOverlay)与进度条显示与更新。
- showMessage / createToastElement / appendToastToContainer / animateToast / hideMessage:实现自定义的 toast 消息系统,包括不同类型(success/error/warning/info)的样式(来自 CONSTANTS.MESSAGE_TYPES),自动隐藏与手动关闭。
- withButtonLoading(btn, asyncAction):将按钮置为 loading 状态并执行异步操作,操作完成后恢复按钮状态。
- isMobile / toggleMobileMenu / closeMobileMenu / handleResize:移动端侧栏显示/隐藏与响应式处理。
- getCurrentContainerId:根据当前 location.pathname 决定要刷新哪个 PJAX 容器。
九、事件绑定概览
- 绑定移动菜单开关、侧栏覆盖层关闭。
- 绑定窗口 resize。
- 绑定页面内各种按钮:保存资料、退出登录、上传头像、搜索文章/碎碎念/时间轴/照片、清除搜索、发布按钮(confirmPublish)、文件选择 change 事件(照片预览、拖拽上传)、查看/移除预览按钮等。
- 绑定模态框关闭、删除确认逻辑(deleteItem / confirmDelete,演示中为模拟删除,延迟后刷新列表)。
- 绑定键盘(Esc 隐藏删除模态)。
十、错误处理与全局
- initialize、各方法都用 try/catch 捕获并在控制台打印错误,同时 showMessage 提示用户(若可能)。
- window.addEventListener('error') 全局捕获并通过 memberCenter.showMessage 提示系统错误。
- 页面卸载 beforeunload 处预留清理逻辑(目前为空)。
十一、代码中值得注意 / 可能需要修正的点
- progressOverlay 的 DOM 被追加了两次(在 initPjax() 里出现了重复代码块),这会导致重复元素或冲突,建议去掉重复块,确保只创建一次。
- createToastElement 使用内联样式变量(CSS 自定义属性)但没有在代码中包含对应的样式表,使用者需保证前端 CSS 存在以正确显示 toast。
- PhotoSwipe 的使用假定全局存在 PhotoSwipe & PhotoSwipeUI_Default,并且页面中有 .pswp DOM(必要的 PhotoSwipe DOM 结构需在页面中存在)。
- 部分方法使用了 global variable memberCenter(例如文件上传预览事件中),若在严格模式或模块化环境中需确认作用域。
- publishArticle/publishMumble/publishTimeline 的 id 字段读取时未做空判定(有些地方使用 ?. 有的没),整体可以统一更健壮的 null/undefined 检查。
- load/submit 接口的返回格式期待 JSON 且包含 success 字段,后端需配合该约定。
十二、总结
- 这份代码实现了一个比较完整的“个人中心”前端管理类,功能涵盖:PJAX 局部刷新、Markdown 编辑(含文件上传)、图片上传带进度、图片查看器、头像上传、发布文章/碎碎念/时间轴、响应式侧栏、加载与消息提示、音频录制器集成点以及统一的错误处理。结构清晰、职责分明,便于扩展与维护。只需注意少数重复 DOM 插入、第三方依赖与样式配套即可稳定运行。
如果你希望我继续做任何事情,例如:
- 给出改进建议或重构思路(比如把重复代码抽离、增强错误处理);
- 修复重复 progressOverlay 的代码并返回修改后的片段;
- 为某个功能(如 toast 样式、PhotoSwipe DOM)提供配套 CSS/HTML 示例; 请告诉我你想要的方向。
评论加载中...