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 状态封装
  • 页面标题更新、移动端侧边栏控制、文件大小格式化与是否为移动设备的判断

逐个函数说明

  1. initTheme()
  • 作用:初始化页面主题并应用。
  • 行为:
    • 尝试从 localStorage 读取 key 'theme';
    • 若无保存值,则根据 prefers-color-scheme 媒体查询选择 'dark' 或 'light';
    • 调用 setTheme(savedTheme) 并返回其结果。
  • 异常处理:若 try 内抛错(例如 localStorage 不可用),会记录错误并默认调用 setTheme('light')。
  1. 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)。
  1. ensureProgressOverlay()
  • 作用:确保页面中存在用于上传/进度显示的覆盖层元素;如果不存在则 append 一段 HTML 到 body。
  • 行为:检测 $(CONSTANTS.SELECTORS.progressOverlay) 是否存在(length === 0),不存在则添加一个 id="progressOverlay" 的 .loading-overlay 元素,内含文字 (#progressText)、进度条 (#progressBar)、进度百分比 (#progressPercent) 等。
  • 注意:插入的 HTML 包含行内样式和 CSS 变量引用(var(--text-color, --primary-color)),需要外部 CSS 支持显示/定位/隐藏等。
  1. showLoading(message = '加载中...')
  • 作用:显示一个通用的“加载中”覆盖层(不是上传进度)。
  • 行为:
    • 尝试找到 .loading-spinner .loading-text 元素并设置文本(如果存在);
    • 将 CONSTANTS.SELECTORS.loadingOverlay 对应的元素加上 class 'active'。
  • 注意:在模块中 ensureProgressOverlay 创建的 overlay 内并没有 .loading-text 类(而是 #progressText),所以 showLoading 需要配合页面已有的 loading overlay HTML 或者确保选择器与 HTML 一致,否则不会设置文本。
  1. hideLoading()
  • 作用:移除 loadingOverlay 的 active 类,从而隐藏加载层。
  • 行为:$(CONSTANTS.SELECTORS.loadingOverlay).removeClass('active');
  1. showProgress(message = '上传中...')
  • 作用:显示进度覆盖层并重置进度到 0%。
  • 行为:
    • 调用 ensureProgressOverlay() 确保 overlay 存在;
    • 选中 overlay(CONSTANTS.SELECTORS.progressOverlay),设置 #progressText、重置 #progressBar 宽度为 0%、#progressPercent 为 0%;
    • 以 css display:flex 显示 overlay,并 addClass('active')。
  1. updateProgress(percent, message)
  • 参数:percent(数字 0-100),message(可选)
  • 作用:更新进度条和文字显示。
  • 行为:
    • 若提供 message 则更新 #progressText;
    • 设置 #progressBar 的 width 为 ${percent}% 并更新 #progressPercent 文本为 ${percent}%
  • 注意:没有对 percent 做边界检查(建议在调用方确保 0-100)。
  1. hideProgress()
  • 作用:隐藏进度覆盖层。
  • 行为:选择 overlay,removeClass('active') 并调用 jQuery.fadeOut(200) 做淡出(200ms)。
  1. 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 动画等。
  1. hideMessage(messageId)
  • 参数:messageId(字符串)
  • 作用:隐藏并移除指定的 toast。
  • 行为:
    • 找到对应 alert,若不存在则返回;
    • 给 alert 加类 toast-hide,然后在 CONSTANTS.DELAYS.TOAST_HIDE_ANIMATION 毫秒后 remove 元素;
    • 如果容器没有子元素,移除容器本身。
  1. 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(...)。
  1. updatePageTitle()
  • 作用:根据当前侧边栏/导航中被选中的项更新 document.title。
  • 行为:
    • 查找 $('nav.member-nav .nav-item.active a'),取其文本(trim)作为 title;
    • 若未找到或为空,使用 '个人中心' 作为默认;
    • 设置 document.title = ${title} - 个人中心
    • catch 块会在失败时 console.error。
  1. isMobile()
  • 返回布尔:$(window).width() <= CONSTANTS.BREAKPOINTS.MOBILE。
  • 作用:判断当前视口是否为移动端(基于断点宽度)。
  1. toggleMobileMenu() / closeMobileMenu()
  • toggleMobileMenu:切换侧边栏与遮罩的 active 类($(CONSTANTS.SELECTORS.memberSidebar).toggleClass('active'),同理 overlay)。
  • closeMobileMenu:移除 active 类,确保侧边栏/遮罩关闭。
  • 依赖:CONSTANTS.SELECTORS.memberSidebar、CONSTANTS.SELECTORS.sidebarOverlay。
  1. 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/图标资源。
评论加载中...