import { CONSTANTS } from './constants.js';
export function initTheme() {
try {
const savedTheme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
return setTheme(savedTheme);
} catch (error) {
console.error('初始化主题失败:', error);
return setTheme('light');
}
}
export function setTheme(theme) {
const finalTheme = ['light', 'dark'].includes(theme) ? theme : 'light';
$('html').attr('data-theme', finalTheme);
try {
localStorage.setItem('theme', finalTheme);
} catch (e) {
console.warn('无法保存主题偏好:', e);
}
return finalTheme;
}
export function ensureProgressOverlay() {
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>
`);
}
}
export function showLoading(message = '加载中...') {
const loadingText = $('.loading-spinner .loading-text');
if (loadingText.length > 0) {
loadingText.text(message);
}
$(CONSTANTS.SELECTORS.loadingOverlay).addClass('active');
}
export function hideLoading() {
$(CONSTANTS.SELECTORS.loadingOverlay).removeClass('active');
}
export function showProgress(message = '上传中...') {
ensureProgressOverlay();
const $overlay = $(CONSTANTS.SELECTORS.progressOverlay);
$overlay.find('#progressText').text(message);
$overlay.find('#progressBar').css('width', '0%');
$overlay.find('#progressPercent').text('0%');
$overlay.css('display', 'flex').addClass('active');
}
export function updateProgress(percent, message) {
const $overlay = $(CONSTANTS.SELECTORS.progressOverlay);
if (message) $overlay.find('#progressText').text(message);
$overlay.find('#progressBar').css('width', `${percent}%`);
$overlay.find('#progressPercent').text(`${percent}%`);
}
export function hideProgress() {
const $overlay = $(CONSTANTS.SELECTORS.progressOverlay);
$overlay.removeClass('active').fadeOut(200);
}
export function showMessage(message, type = 'info') {
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 = $(`
<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
});
let container = $(CONSTANTS.SELECTORS.toastContainer);
if (container.length === 0) {
container = $('<div id="toast-container"></div>');
$('body').append(container);
}
container.append(alert);
setTimeout(() => alert.addClass('toast-show'), 10);
setTimeout(() => alert.find('.toast-progress-bar').addClass('toast-progress-active'), 100);
alert.find('.toast-close').on('click', () => hideMessage(messageId));
setTimeout(() => hideMessage(messageId), CONSTANTS.DELAYS.TOAST_AUTO_HIDE);
}
export function hideMessage(messageId) {
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);
}
export async function withButtonLoading(btn, asyncAction) {
const $btn = $(btn);
const originalHtml = $btn.html();
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);
}
}
export function 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);
}
}
export function isMobile() {
return $(window).width() <= CONSTANTS.BREAKPOINTS.MOBILE;
}
export function toggleMobileMenu() {
$(CONSTANTS.SELECTORS.memberSidebar).toggleClass('active');
$(CONSTANTS.SELECTORS.sidebarOverlay).toggleClass('active');
}
export function closeMobileMenu() {
$(CONSTANTS.SELECTORS.memberSidebar).removeClass('active');
$(CONSTANTS.SELECTORS.sidebarOverlay).removeClass('active');
}
export 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]}`;
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面是对你贴出代码的逐项说明与整体功能概述(中文)——包括每个导出函数的用途、参数、返回值、副作用以及需要的外部依赖/约束。
前提与依赖
- 代码基于 jQuery($)操作 DOM。
- 依赖一个外部常量模块:import { CONSTANTS } from './constants.js'。该常量对象里预计包含 SELECTORS(各种元素选择器)、MESSAGE_TYPES(toast 类型配置)、DELAYS(各类延时)、BREAKPOINTS(断点,如 MOBILE)等。
- 使用 localStorage、window.matchMedia、FontAwesome 图标类、以及若干 CSS 变量(如 --text-color、--primary-color 等)和若干 CSS 类(例如 active、toast-show、toast-hide、toast-progress-active、btn-loading)。
- 某些选择器/ID(例如 progressOverlay、toast-container、#progressBar 等)在代码中被引用或创建,样式/动画需在外部 CSS 中实现。
总体功能 这份模块主要负责一些 UI 通用工具,包括:
- 主题初始化/设置(light/dark)
- 全局加载/上传进度覆盖层(loading/progress overlay)
- 全局 toast 消息通知
- 按钮异步操作的 loading 状态封装
- 页面标题更新、移动端侧边栏控制、文件大小格式化与是否为移动设备的判断
逐个函数说明
- initTheme()
- 作用:初始化页面主题并应用。
- 行为:
- 尝试从 localStorage 读取 key 'theme';
- 若无保存值,则根据 prefers-color-scheme 媒体查询选择 'dark' 或 'light';
- 调用 setTheme(savedTheme) 并返回其结果。
- 异常处理:若 try 内抛错(例如 localStorage 不可用),会记录错误并默认调用 setTheme('light')。
- setTheme(theme)
- 参数:theme(期望 'light' 或 'dark')
- 作用:设置最终主题到 HTML(data-theme 属性),并尝试保存到 localStorage。
- 行为:
- 校验输入,仅接受 'light'、'dark',否则降级为 'light';
- 使用 $('html').attr('data-theme', finalTheme) 让 CSS 根据 data-theme 生效;
- 尝试 localStorage.setItem('theme', finalTheme);失败时记录警告。
- 返回:最终使用的主题字符串(finalTheme)。
- ensureProgressOverlay()
- 作用:确保页面中存在用于上传/进度显示的覆盖层元素;如果不存在则 append 一段 HTML 到 body。
- 行为:检测 $(CONSTANTS.SELECTORS.progressOverlay) 是否存在(length === 0),不存在则添加一个 id="progressOverlay" 的 .loading-overlay 元素,内含文字 (#progressText)、进度条 (#progressBar)、进度百分比 (#progressPercent) 等。
- 注意:插入的 HTML 包含行内样式和 CSS 变量引用(var(--text-color, --primary-color)),需要外部 CSS 支持显示/定位/隐藏等。
- showLoading(message = '加载中...')
- 作用:显示一个通用的“加载中”覆盖层(不是上传进度)。
- 行为:
- 尝试找到 .loading-spinner .loading-text 元素并设置文本(如果存在);
- 将 CONSTANTS.SELECTORS.loadingOverlay 对应的元素加上 class 'active'。
- 注意:在模块中 ensureProgressOverlay 创建的 overlay 内并没有 .loading-text 类(而是 #progressText),所以 showLoading 需要配合页面已有的 loading overlay HTML 或者确保选择器与 HTML 一致,否则不会设置文本。
- hideLoading()
- 作用:移除 loadingOverlay 的 active 类,从而隐藏加载层。
- 行为:$(CONSTANTS.SELECTORS.loadingOverlay).removeClass('active');
- showProgress(message = '上传中...')
- 作用:显示进度覆盖层并重置进度到 0%。
- 行为:
- 调用 ensureProgressOverlay() 确保 overlay 存在;
- 选中 overlay(CONSTANTS.SELECTORS.progressOverlay),设置 #progressText、重置 #progressBar 宽度为 0%、#progressPercent 为 0%;
- 以 css display:flex 显示 overlay,并 addClass('active')。
- updateProgress(percent, message)
- 参数:percent(数字 0-100),message(可选)
- 作用:更新进度条和文字显示。
- 行为:
- 若提供 message 则更新 #progressText;
- 设置 #progressBar 的 width 为
${percent}%并更新 #progressPercent 文本为${percent}%。
- 注意:没有对 percent 做边界检查(建议在调用方确保 0-100)。
- hideProgress()
- 作用:隐藏进度覆盖层。
- 行为:选择 overlay,removeClass('active') 并调用 jQuery.fadeOut(200) 做淡出(200ms)。
- showMessage(message, type = 'info')
- 参数:message(字符串),type(字符串,默认 'info')
- 作用:创建并展示一个 toast 通知。
- 行为:
- 从 CONSTANTS.MESSAGE_TYPES 根据 type(会调用 type.toUpperCase())获取配置(图标、颜色等),回退为 INFO;
- 生成一个唯一 messageId;
- 构建 alert DOM(带图标、文本、关闭按钮、进度条)并设置若干 CSS 变量 (--toast-color 等) 来传递颜色样式;
- 如果容器 (CONSTANTS.SELECTORS.toastContainer) 不存在,则创建并 append 到 body;
- 将 alert append 到容器,稍后添加类 toast-show 和 toast-progress-active 来触发 CSS 动画;
- 绑定关闭按钮点击事件:调用 hideMessage(messageId);
- 设置超时:在 CONSTANTS.DELAYS.TOAST_AUTO_HIDE 后自动隐藏(调用 hideMessage)。
- 依赖:需要外部 CSS 实现 .toast-show、.toast-hide、.toast-progress-active、.toast-progress-bar 动画等。
- hideMessage(messageId)
- 参数:messageId(字符串)
- 作用:隐藏并移除指定的 toast。
- 行为:
- 找到对应 alert,若不存在则返回;
- 给 alert 加类 toast-hide,然后在 CONSTANTS.DELAYS.TOAST_HIDE_ANIMATION 毫秒后 remove 元素;
- 如果容器没有子元素,移除容器本身。
- withButtonLoading(btn, asyncAction)
- 参数:btn(DOM 元素或选择器),asyncAction(返回 Promise 的异步函数)
- 作用:在按钮执行异步操作期间显示 loading 状态(禁用按钮并显示 spinner)。
- 行为:
- 保存按钮原始 innerHTML;
- 将按钮加类 btn-loading,禁用按钮(prop disabled),并把内容替换为带 spinner 的 HTML(使用 originalHtml 做后缀);
- await asyncAction();
- finally 恢复按钮:启用、移除 btn-loading 类、恢复原始 HTML。
- 返回:没有明确返回值,但 asyncAction 抛错会向上抛出(因为没有捕获),外层可 await withButtonLoading(...)。
- updatePageTitle()
- 作用:根据当前侧边栏/导航中被选中的项更新 document.title。
- 行为:
- 查找 $('nav.member-nav .nav-item.active a'),取其文本(trim)作为 title;
- 若未找到或为空,使用 '个人中心' 作为默认;
- 设置 document.title =
${title} - 个人中心; - catch 块会在失败时 console.error。
- isMobile()
- 返回布尔:$(window).width() <= CONSTANTS.BREAKPOINTS.MOBILE。
- 作用:判断当前视口是否为移动端(基于断点宽度)。
- toggleMobileMenu() / closeMobileMenu()
- toggleMobileMenu:切换侧边栏与遮罩的 active 类($(CONSTANTS.SELECTORS.memberSidebar).toggleClass('active'),同理 overlay)。
- closeMobileMenu:移除 active 类,确保侧边栏/遮罩关闭。
- 依赖:CONSTANTS.SELECTORS.memberSidebar、CONSTANTS.SELECTORS.sidebarOverlay。
- formatFileSize(bytes)
- 参数:bytes(数字)
- 返回:格式化的人类可读文件大小字符串(例如 "2.34 MB")。
- 行为:
- 如果 bytes === 0,返回 '0 B';
- 使用 1024 为基数,计算合适的单位并保留两位小数(toFixed(2)),返回字符串。
- 注意:对非常大的字节数(超过 TB)没有额外单位,但数组包含到 TB。
可能的注意事项 / 小问题提示
- showLoading 尝试更新的选择器是 .loading-spinner .loading-text,但 ensureProgressOverlay 插入的是 #progressText,所以两者不一致:如果你想复用同一个 overlay,需要统一选择器或在页面中另外保持一个 loadingOverlay HTML 结构。
- updateProgress 没有对 percent 做边界限制或类型检查(NaN、负值或 >100 的值可能导致 UI 异常)。
- withButtonLoading 会直接把按钮原始 HTML 包括图标文本当作后缀插入 spinner HTML,若原始 HTML 包含事件或组件状态外部依赖,可能需要更谨慎处理。
- showMessage 依赖 CSS 动画时序(例如先用 setTimeout 触发添加类),如果动画/延时常量配置不合理可能导致闪烁或进度条不工作。
- 该模块大量使用 jQuery 全局选择器和直接操作 DOM,适合传统 jQuery 项目;在 React/Vue 等框架里需要适配/封装以避免直接操作虚拟 DOM。
简短总结
- 这是一个前端 UI 工具模块,提供主题管理(light/dark)、全局 loading 和进度覆盖层、toast 通知、按钮 loading 封装、页面标题更新、移动端菜单控制和文件大小格式化等常用功能。实际外观和动画依赖于 CONSTANTS 提供的选择器/配置与外部 CSS/图标资源。
评论加载中...