import {CONSTANTS} from './constants.js';
import {
closeMobileMenu,
ensureProgressOverlay,
formatFileSize,
hideLoading,
hideProgress,
initTheme,
isMobile,
showLoading,
showMessage,
showProgress,
toggleMobileMenu,
updatePageTitle,
updateProgress,
withButtonLoading
} from './ui.js';
import {MarkdownEditor} from './markdown.js';
import {destroyPhotoViewer, initPhotoViewer, openPhotoSwipe} from './photos.js';
import {AudioRecorder} from './audio-recorder.js';
export class MemberCenter {
constructor() {
this.state = {
currentTheme: 'light',
currentDeleteItem: null
};
this.markdownEditor = null;
this.audioRecorder = null;
this.initialize();
}
initialize() {
ensureProgressOverlay();
this.bindEvents();
this.initPjax();
this.state.currentTheme = initTheme();
this.initMarkdown();
this.initPhotoViewer();
if (document.getElementById('startRecording')) {
this.initAudioRecorder();
}
}
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'
];
$(document).pjax(mainSelectors.join(', '), '#pjax-container', {scrollTo: 0});
$(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 (document.getElementById('startRecording')) {
this.initAudioRecorder();
}
this.initMarkdown();
this.initPhotoViewer();
updatePageTitle();
if (isMobile()) {
closeMobileMenu();
}
});
}
initMarkdown() {
const articleContentEl = document.getElementById('articleContent');
const mumbleContentEl = document.getElementById('mumbleContent');
const timelineContentEl = document.getElementById('timelineContent');
const editorEl = articleContentEl || mumbleContentEl || timelineContentEl;
if (!editorEl) {
this.markdownEditor = null;
return;
}
const markdownValueEl = document.getElementById('txt_' + editorEl.id);
if (!markdownValueEl) {
this.markdownEditor = null;
return;
}
const uploadAddress = document.getElementById('uploadAddress')?.value;
const markdown = markdownValueEl.value || '';
this.markdownEditor = new MarkdownEditor({
elementId: editorEl.id,
markdown,
theme: this.state.currentTheme,
editOnly: true,
uploadAddress,
fileUpload: (file, cb, uploadUrl) => this.fileUpload(file, cb, uploadUrl),
markdownValueEl
});
}
initPhotoViewer() {
destroyPhotoViewer();
const photoPreview = document.getElementById('photoPreview');
const loadingPlaceholder = document.getElementById('imageLoadingPlaceholder');
if (!photoPreview) return;
// 处理已存在的图片(编辑模式)
if (photoPreview.src && photoPreview.src !== '') {
// 检查图片是否已经加载完成
if (photoPreview.complete && photoPreview.naturalHeight !== 0) {
// 图片已加载完成
if (loadingPlaceholder) {
loadingPlaceholder.remove();
}
photoPreview.style.display = 'block';
} else {
// 图片还在加载中,添加加载事件监听
photoPreview.onload = () => {
if (loadingPlaceholder) {
loadingPlaceholder.remove();
}
photoPreview.style.display = 'block';
};
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>';
}
};
}
}
// 初始化照片查看器
initPhotoViewer();
}
initAudioRecorder() {
if (!this.audioRecorder) {
this.audioRecorder = new AudioRecorder({
onMessage: (msg, type) => showMessage(msg, type),
onLoading: (show, msg) => show ? showLoading(msg) : hideLoading()
});
}
this.audioRecorder.init();
}
bindEvents() {
this.bindNavigation();
this.bindSearch();
this.bindPublish();
this.bindTagInput();
this.bindPhotoUpload();
this.bindModal();
this.bindViewContent();
this.bindKeyboard();
$(document).on('click', CONSTANTS.SELECTORS.menuToggle, () => toggleMobileMenu());
$(document).on('click', CONSTANTS.SELECTORS.sidebarOverlay, () => closeMobileMenu());
$(window).on('resize', () => {
if ($(window).width() > CONSTANTS.BREAKPOINTS.TABLET) closeMobileMenu();
});
}
bindNavigation() {
$(document).on('click', CONSTANTS.SELECTORS.navItem, (e) => {
$(CONSTANTS.SELECTORS.navItem).removeClass('active');
$(e.currentTarget).addClass('active');
});
}
bindSearch() {
$(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();
});
$(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();
});
$(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();
});
$(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();
});
}
bindPublish() {
$(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));
$(document).on('click', '#confirmPublish', async (e) => {
const $btn = $(e.currentTarget);
const action = $btn.data('action');
if (action && typeof this[action] === 'function') {
await withButtonLoading($btn, async () => {
await this[action]();
});
}
});
}
bindTagInput() {
$(document).on('click', '#addArticleTag', () => {
this.addTagFromInput('articleExtraTags', 'articleTags');
});
$(document).on('keydown', '#articleExtraTags', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.addTagFromInput('articleExtraTags', 'articleTags');
}
});
$(document).on('click', '#addPhotoTag', () => {
this.addTagFromInput('photoExtraTags', 'photoTags');
});
$(document).on('keydown', '#photoExtraTags', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.addTagFromInput('photoExtraTags', 'photoTags');
}
});
}
addTagFromInput(inputId, containerId) {
const input = document.getElementById(inputId);
if (!input) return;
const value = (input.value || '').trim();
if (!value) return;
this.addTagToSelector(containerId, value);
input.value = '';
}
addTagToSelector(containerId, value) {
const container = document.getElementById(containerId);
if (!container) return;
const existing = Array.from(container.querySelectorAll('input[type="checkbox"]'))
.find((input) => input.value.trim().toLowerCase() === value.toLowerCase());
if (existing) {
existing.checked = true;
return;
}
const label = document.createElement('label');
label.className = 'tag-option';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = value;
checkbox.checked = true;
const span = document.createElement('span');
const icon = document.createElement('i');
icon.className = 'fa fa-check';
const text = document.createElement('em');
text.textContent = value;
span.appendChild(icon);
span.appendChild(text);
label.appendChild(checkbox);
label.appendChild(span);
container.appendChild(label);
}
bindPhotoUpload() {
$(document).on('change', '#photoFile', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const $photoPreview = $('#photoPreview');
$photoPreview.attr('src', e.target.result);
$photoPreview.show();
$('#imageLoadingPlaceholder').remove();
$('#previewContainer').addClass('active');
$('#dropZone').hide();
$('#fileName').text(file.name);
$('#fileSize').text(formatFileSize(file.size));
initPhotoViewer();
};
reader.readAsDataURL(file);
});
$(document).on('click', '#removeFileBtn', () => {
$('#photoFile').val('');
$('#previewContainer').removeClass('active');
$('#dropZone').show();
$('#photoPreview').attr('src', '');
destroyPhotoViewer();
});
$(document).on('click', '#viewPhotoBtn', () => {
const photoPreview = document.getElementById('photoPreview');
if (photoPreview && photoPreview.src) {
openPhotoSwipe(photoPreview);
}
});
const dropZone = document.getElementById('dropZone');
if (dropZone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'), false));
['dragleave', 'drop'].forEach(eventName => dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'), false));
dropZone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
const fileInput = document.getElementById('photoFile');
if (files.length > 0) {
fileInput.files = files;
const event = new Event('change', {bubbles: true});
fileInput.dispatchEvent(event);
}
}, false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
}
bindModal() {
$('#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();
});
}
bindViewContent() {
const $viewContentModal = $('#viewContentModal');
$('#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));
}
bindKeyboard() {
$(document).on('keydown', (e) => {
if (e.key === 'Escape') this.hideDeleteModal();
});
}
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> 展开全文');
}
}
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 container = document.getElementById('viewContentModalBody');
const codeBlocks = container.querySelectorAll('pre code');
if (codeBlocks.length > 0) Prism.highlightAllUnder(container);
} catch (e) {
console.error('代码高亮失败:', e);
}
}
}
// --- data actions ---
async saveProfile() {
try {
const formData = new FormData();
formData.append('name', $('#nickname').val().trim());
formData.append('sex', $('#gender').val());
formData.append('sign', $('#signature').val().trim());
showLoading();
const response = await fetch('/Account/UpdateInfo', {method: 'POST', body: formData});
const result = await response.json();
hideLoading();
if (!result.success) return showMessage(result.message || '保存失败', 'error');
showMessage('保存成功', 'success');
} catch (error) {
console.error('保存个人资料失败:', error);
hideLoading();
showMessage('保存失败,请重试', 'error');
}
}
async logout() {
if (!confirm('确定要退出登录吗?')) return;
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 {
showMessage('退出失败', 'error');
}
} catch (error) {
console.error('退出失败:', error);
showMessage('退出失败', 'error');
} finally {
hideLoading();
}
}
async handleAvatarUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
showMessage('请选择图片文件', 'error');
return;
}
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
showMessage('图片文件不能超过5MB', 'error');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
$('#avatarPreview, #userAvatar').attr('src', e.target.result);
};
reader.readAsDataURL(file);
await this.uploadImage(file);
}
async uploadImage(file) {
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();
hideLoading();
if (!result.success) return showMessage(result.msg, 'error');
showMessage('头像上传成功', 'success');
} catch (error) {
hideLoading();
showMessage('上传发生错误', 'error');
}
}
deleteItem(type, id) {
this.state.currentDeleteItem = {type, id};
$(CONSTANTS.SELECTORS.deleteModal).show();
}
hideDeleteModal() {
$(CONSTANTS.SELECTORS.deleteModal).hide();
this.state.currentDeleteItem = null;
}
confirmDelete() {
if (this.state.currentDeleteItem) {
showLoading();
setTimeout(() => {
hideLoading();
this.hideDeleteModal();
showMessage('删除成功', 'success');
$.pjax.reload('#' + this.getCurrentContainerId());
}, 300);
}
}
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() {
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 tagsContainer = document.getElementById('articleTags');
const tags = Array.from(tagsContainer?.querySelectorAll('input[type="checkbox"]:checked') || [])
.map((x) => x.value);
const introduction = document.getElementById('articleIntroduction').value;
let markdown = '';
let content = '';
if (this.markdownEditor) {
markdown = this.markdownEditor.getMarkdown();
content = this.markdownEditor.getHtml();
} else {
const txt = document.getElementById('txt_articleContent');
markdown = txt ? txt.value : '';
content = markdown;
}
if (!title.trim()) return showMessage('标题不能为空', 'warning');
if (tags.length === 0) return showMessage('请选择标签或输入补充标签', 'warning');
if (!introduction.trim()) return showMessage('简介不能为空', 'warning');
if (!markdown.trim() || markdown.length < 5) return showMessage('内容不能为空', 'warning');
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);
try {
const response = await fetch(action, {method: 'POST', body: formData});
const result = await response.json();
if (!result.success) return showMessage(result.msg || '发布失败', 'error');
showMessage('文章发布成功', 'success');
$.pjax({url: '/member/articles', container: '#pjax-container'});
} catch (error) {
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.markdownEditor) {
content = this.markdownEditor.getMarkdown();
html = this.markdownEditor.getHtml();
} else {
const el = document.getElementById('mumbleContent');
content = el ? el.value : '';
}
if (!content || !content.trim() || content.length < 5) return showMessage('请输入内容', 'warning');
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) return showMessage(result.msg || '发布失败', 'error');
showMessage('碎碎念发布成功', 'success');
$.pjax({url: '/member/mumbles', container: '#pjax-container'});
} catch (error) {
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.markdownEditor) {
content = this.markdownEditor.getMarkdown();
} else {
const el = document.getElementById('timelineContent');
content = el ? el.value : '';
}
if (!title.trim()) return showMessage('标题不能为空', 'warning');
if (!date.trim()) return showMessage('时间不能为空', 'warning');
if (!content.trim() || content.length < 5) return showMessage('内容不能为空', 'warning');
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) return showMessage(result.msg || '发布失败', 'error');
showMessage('时间轴发布成功', 'success');
$.pjax({url: '/member/timeline', container: '#pjax-container'});
} catch (error) {
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) return showMessage('请选择照片', 'warning');
const tagsContainer = document.getElementById('photoTags');
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);
const tags = Array.from(tagsContainer?.querySelectorAll('input[type="checkbox"]:checked') || [])
.map((x) => x.value);
for (const tag of tags) {
formData.append('tags', tag);
}
try {
showProgress('正在上传照片...');
const result = await this.uploadWithProgress(action, formData, (percent) => updateProgress(percent, '正在上传照片...'));
if (!result.success) return showMessage(result.msg || '上传失败', 'error');
showMessage('照片上传成功', 'success');
$.pjax({url: '/member/photos', container: '#pjax-container'});
} catch (error) {
showMessage('上传失败: ' + error.message, 'error');
} finally {
hideProgress();
}
}
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 {
showProgress('正在上传图片...');
const result = await this.uploadWithProgress(uploadUrl, formData, (percent) => updateProgress(percent, '正在上传图片...'));
hideProgress();
callback(result.url || '');
} catch (error) {
hideProgress();
showMessage('图片上传失败: ' + error.message, 'error');
console.error('图片上传失败:', error);
callback('');
}
}
}
$(document).ready(() => {
try {
window.memberCenter = new MemberCenter();
} catch (error) {
console.error('个人中心初始化失败:', error);
}
});⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
总体说明
- 这是一个用于“个人中心 / 后台管理”页面的前端类组件实现(MemberCenter),用来初始化页面、绑定交互事件、处理文章/碎碎念/时间轴/照片的发布与上传、图片预览与查看、个人信息与头像更新、删除确认等常见功能。
- 代码依赖外部模块和库:CONSTANTS(选择器、断点等常量)、ui.js(显示/隐藏 loading、进度、消息、主题切换等 UI 辅助函数)、MarkdownEditor、photos.js(照片查看器相关)、AudioRecorder、jQuery、pjax、showdown(markdown 转 HTML 可选)、Prism(代码高亮 可选)等。
主要结构与初始化
- MemberCenter 类在构造函数中初始化内部 state(如当前主题、待删除项)、markdown 编辑器和音频录制器引用,并调用 initialize() 完成页面初始化。
- initialize():
- ensureProgressOverlay():确保进度 overlay 存在(UI 相关)。
- 绑定各种事件(bindEvents)。
- 初始化 pjax(initPjax)以实现局部页面异步加载与导航。
- 使用 initTheme() 设置当前主题,并初始化 markdown 编辑器、照片查看器,若页面存在录音按钮则初始化音频录制器。
PJAX(局部异步加载)
- initPjax() 设置 $.pjax 默认超时时间,并为一些导航/分页链接注册 PJAX 容器,避免整页刷新。
- 监听 pjax 事件(pjax:send/pjax:complete/pjax:end),用于显示/隐藏页面加载遮罩、滚动到顶部、重新初始化编辑器/查看器、更新页面标题、在移动端自动关闭侧栏等。
Markdown 编辑器
- initMarkdown() 查找编辑器元素(articleContent、mumbleContent、timelineContent 中的一个),以及对应的隐藏 Markdown 值元素 txt_
。 - 若存在则创建 MarkdownEditor 实例,传入:元素 id、当前 markdown 内容、主题、上传地址、以及自定义 fileUpload 回调(使用本类的 fileUpload 方法)等。
照片查看与上传
- initPhotoViewer():
- 销毁已存在查看器,处理 photoPreview 的加载占位(imageLoadingPlaceholder),对已存在图片做加载成功/失败处理,最后初始化照片查看器(initPhotoViewer)。
- bindPhotoUpload():
- 处理 file input 的 change:读取文件并设置预览、显示文件名、大小,初始化查看器。
- 支持移除预览(removeFileBtn)、查看预览(viewPhotoBtn -> openPhotoSwipe)。
- 实现拖放上传区域(dropZone)的拖拽事件,阻止默认行为并将拖入文件赋给 file input 并触发 change。
- formatFileSize 用于显示文件大小(来自 ui.js)。
音频录制
- initAudioRecorder():如果未创建则用 AudioRecorder 初始化并绑定消息/loading 回调,然后调用 audioRecorder.init()。
事件绑定(bindEvents 和若干子绑定)
- bindNavigation(): 点击菜单项时设置 active 状态。
- bindSearch(): 为文章/碎碎念/时间轴/照片提供搜索、清空和回车触发搜索逻辑,利用 PJAX 请求更新列表容器。
- bindPublish(): 绑定保存资料、登出、头像上传按钮与文件变化,绑定带 data-action 的确认发布按钮,使用 withButtonLoading 给按钮加 loading 状态并执行对应动作(按钮 data-action 对应 MemberCenter 的方法名)。
- bindTagInput(): 支持输入补充标签并添加到标签选择区(按回车或点击添加)。
- bindModal(): 删除确认模态框的打开/关闭逻辑与点击遮罩关闭。
- bindViewContent(): 查看内容模态框(渲染 markdown/text 到模态并显示),并支持代码高亮(Prism)和图片样式限制。
- bindKeyboard(): 全局 Esc 键用于关闭删除模态框。
- 还绑定了移动端菜单切换与窗口 resize 行为(在宽度超过断点时自动关闭移动菜单)。
标签管理
- addTagFromInput(inputId, containerId):从输入框取值并调用 addTagToSelector。
- addTagToSelector(containerId, value):检查是否已存在同名标签(不区分大小写),若存在则选中,否则动态创建一个带 checkbox 的标签项并选中。
发布/保存/退出等数据操作
- saveProfile(): 收集昵称、性别、签名,POST 到 /Account/UpdateInfo,显示 loading,并根据返回提示状态。
- logout(): 弹窗确认后调用 /Account/LogOut(POST),根据响应做重定向或提示。
- handleAvatarUpload(event):检查文件是否存在、是否图片、大小不超过 5MB,预览头像,并调用 uploadImage 上传。
- uploadImage(file):将头像文件 formData POST 到 /Account/UpdateAvatar,显示 loading 并处理返回。
- deleteItem(type, id) / hideDeleteModal() / confirmDelete():
- deleteItem 保存要删除的项到 state 并显示删除模态。
- confirmDelete 目前为演示性实现:showLoading 后用 setTimeout 模拟删除成功、隐藏 loading、关闭模态并通过 $.pjax.reload 刷新当前列表容器(实际应调用后端删除接口)。
- getCurrentContainerId(): 根据当前 pathname 返回对应的 pj ax 容器 id(articles/mumbles/timeline/photos),用于 reload。
发布各类内容(文章/碎碎念/时间轴/照片)
- publishArticle():
- 收集表单字段:submitAddress(目标 action url)、id、title、tags(checkbox 选中值)、introduction、markdown/html(优先从 markdownEditor 获取)。
- 做数据校验(标题/标签/简介/内容等),组装 FormData,POST 到 action,处理返回并用 PJAX 导回 articles 列表。
- publishMumble()、publishTimeline() 类似,收集各自字段,校验后 POST。
- publishPhoto():
- 收集照片文件、描述、标签,使用 uploadWithProgress 上传(用于显示进度条),处理返回并 PJAX 返回照片列表,最后隐藏进度。
上传与进度
- uploadWithProgress(url, formData, onProgress):基于 XMLHttpRequest 实现上传进度回调(xhr.upload.onprogress),返回 Promise,完成后解析 JSON 或抛错。
- fileUpload(file, callback, uploadUrl):用于 Markdown 编辑器或其他需要上传图片的场景,构建 formData 并调用 uploadWithProgress,回调上传后的图片 URL 或空字符串,并显示/隐藏进度与错误提示。
显示内容模态
- showContent(content, type):可显示 markdown(如果有 showdown 则转换)、纯文本(转义后换行替换为
)或直接 HTML;在注入内容后对图片设定 max-width,显示模态,若存在 Prism 则尝试高亮代码块。
其它细节与错误处理
- 大量使用 showLoading/hideLoading/showProgress/hideProgress/showMessage/updateProgress 等 ui 工具函数来管理 loading、进度与消息提示。
- 对网络请求使用 try/catch,捕获错误并以 showMessage 或控制台输出进行提示。
- 部分功能(如 confirmDelete)是本地模拟,需要与后端实际删除接口对接才能完成真实删除。
- 依赖全局常量 CONSTANTS.SELECTORS 与 CONSTANTS.BREAKPOINTS(例如侧栏、菜单切换选择器、tablet 断点等)。
总结
- 这是一个典型的前端控制器,整合了页面初始化、局部刷新(pjax)、编辑器/查看器/录音等组件的初始化与交互绑定,封装了文件上传(含进度)、表单提交、模态交互和常用 UI 提示。若要在项目中使用,需要确保相关外部依赖(jQuery、pjax、MarkdownEditor、photos.js、AudioRecorder、ui.js、showdown、Prism 等)已引入,并将模拟或占位的逻辑(例如删除)替换为真实后端 API 调用。
评论加载中...