components.js 9.58 KB
/**
 * UI 组件库
 */

const Components = {
    /**
     * 格式化金额
     */
    formatAmount(value) {
        if (value === null || value === undefined) return '-';
        return ${Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
    },

    /**
     * 格式化数字
     */
    formatNumber(value, decimals = 2) {
        if (value === null || value === undefined) return '-';
        return Number(value).toLocaleString('zh-CN', { 
            minimumFractionDigits: decimals, 
            maximumFractionDigits: decimals 
        });
    },

    /**
     * 格式化库销比
     */
    formatRatio(value) {
        if (value === null || value === undefined || value === 999) return '-';
        if (value === 0) return '0.00';
        return Number(value).toFixed(2);
    },

    /**
     * 获取配件标签
     * 呆滞:有库存,滚动90天没有销量
     * 低频:无库存,滚动90天有出库,月均销量<1
     * 缺货:无库存,滚动90天有出库,月均销量≥1
     */
    getPartTag(validStorage, avgSales) {
        const storage = Number(validStorage) || 0;
        const sales = Number(avgSales) || 0;
        
        if (storage > 0 && sales === 0) {
            return { type: 'stagnant', text: '呆滞', class: 'tag-stagnant' };
        }
        if (storage <= 0 && sales > 0 && sales < 1) {
            return { type: 'low-freq', text: '低频', class: 'tag-low-freq' };
        }
        if (storage <= 0 && sales >= 1) {
            return { type: 'shortage', text: '缺货', class: 'tag-shortage' };
        }
        return null;
    },

    /**
     * 渲染配件标签HTML
     */
    renderPartTag(validStorage, avgSales) {
        const tag = this.getPartTag(validStorage, avgSales);
        if (!tag) return '';
        return `<span class="part-tag ${tag.class}">${tag.text}</span>`;
    },



    /**
     * 格式化时长
     */
    formatDuration(seconds) {
        if (!seconds) return '-';
        seconds = Math.floor(seconds);
        if (seconds < 60) return `${seconds}秒`;
        const minutes = Math.floor(seconds / 60);
        const secs = seconds % 60;
        return `${minutes}${secs}秒`;
    },

    /**
     * 获取状态徽章 HTML
     */
    getStatusBadge(status, statusText) {
        const statusMap = {
            0: { class: 'badge-info', icon: 'loader-2', text: statusText || '运行中' },
            1: { class: 'badge-success', icon: 'check-circle', text: statusText || '成功' },
            2: { class: 'badge-danger', icon: 'x-circle', text: statusText || '失败' },
        };
        const config = statusMap[status] || statusMap[0];
        return `
            <span class="badge ${config.class}">
                <span class="badge-dot"></span>
                ${config.text}
            </span>
        `;
    },

    /**
     * 获取优先级徽章 HTML
     */
    getPriorityBadge(priority) {
        const priorityMap = {
            1: { class: 'priority-high', text: '高' },
            2: { class: 'priority-medium', text: '中' },
            3: { class: 'priority-low', text: '低' },
        };
        const config = priorityMap[priority] || priorityMap[2];
        return `<span class="priority-badge ${config.class}">${config.text}</span>`;
    },

    /**
     * 获取库销比指示器 HTML
     */
    getRatioIndicator(current, base) {
        if (current === null || current === undefined || current === 999) {
            return '<span class="text-muted">-</span>';
        }
        
        const percentage = Math.min((current / (base || 1.5)) * 100, 100);
        let className = 'ratio-normal';
        if (current < 0.5) className = 'ratio-low';
        else if (current > 2) className = 'ratio-high';
        
        return `
            <div class="ratio-indicator ${className}">
                <span>${this.formatRatio(current)}</span>
                <div class="ratio-bar">
                    <div class="ratio-bar-fill" style="width: ${percentage}%"></div>
                </div>
            </div>
        `;
    },

    /**
     * 渲染分页控件
     */
    renderPagination(current, total, pageSize, onChange) {
        const totalPages = Math.ceil(total / pageSize);
        const start = (current - 1) * pageSize + 1;
        const end = Math.min(current * pageSize, total);

        return `
            <div class="pagination">
                <div class="pagination-info">
                    显示 ${start}-${end} 条,共 ${total} 
                </div>
                <div class="pagination-controls">
                    <button class="pagination-btn" ${current <= 1 ? 'disabled' : ''} data-page="${current - 1}">
                        <i data-lucide="chevron-left"></i>
                    </button>
                    ${this.getPaginationNumbers(current, totalPages)}
                    <button class="pagination-btn" ${current >= totalPages ? 'disabled' : ''} data-page="${current + 1}">
                        <i data-lucide="chevron-right"></i>
                    </button>
                </div>
            </div>
        `;
    },

    /**
     * 获取分页数字
     */
    getPaginationNumbers(current, total) {
        const pages = [];
        const maxVisible = 5;
        
        let start = Math.max(1, current - Math.floor(maxVisible / 2));
        let end = Math.min(total, start + maxVisible - 1);
        start = Math.max(1, end - maxVisible + 1);
        
        for (let i = start; i <= end; i++) {
            pages.push(`
                <button class="pagination-btn ${i === current ? 'active' : ''}" data-page="${i}">
                    ${i}
                </button>
            `);
        }
        
        return pages.join('');
    },

    /**
     * 渲染统计卡片
     */
    renderStatCard(icon, label, value, iconClass = 'primary', change = null) {
        let changeHtml = '';
        if (change !== null) {
            const changeClass = change >= 0 ? 'positive' : 'negative';
            const changeIcon = change >= 0 ? 'trending-up' : 'trending-down';
            changeHtml = `
                <div class="stat-change ${changeClass}">
                    <i data-lucide="${changeIcon}"></i>
                    ${Math.abs(change)}%
                </div>
            `;
        }

        return `
            <div class="stat-card">
                <div class="stat-icon ${iconClass}">
                    <i data-lucide="${icon}"></i>
                </div>
                <div class="stat-content">
                    <div class="stat-label">${label}</div>
                    <div class="stat-value">${value}</div>
                    ${changeHtml}
                </div>
            </div>
        `;
    },

    /**
     * 渲染空状态
     */
    renderEmptyState(icon = 'inbox', title = '暂无数据', description = '') {
        return `
            <div class="empty-state">
                <i data-lucide="${icon}"></i>
                <div class="empty-state-title">${title}</div>
                ${description ? `<div class="empty-state-description">${description}</div>` : ''}
            </div>
        `;
    },

    /**
     * 渲染信息列表项
     */
    renderInfoItem(label, value) {
        return `
            <div class="info-item">
                <span class="info-label">${label}</span>
                <span class="info-value">${value || '-'}</span>
            </div>
        `;
    },

    /**
     * 渲染 Markdown 内容
     */
    renderMarkdown(content) {
        if (!content) return '';
        if (typeof marked !== 'undefined') {
            let html = marked.parse(content);
            html = html.replace(/<pre>\s*<code[^>]*>[\s\\n]*<\/code>\s*<\/pre>/gi, '');
            html = html.replace(/<pre>[\s\\n]*<\/pre>/gi, '');
            html = html.replace(/<pre>\s*<code[^>]*>\n<\/code>\s*<\/pre>/gi, '');
            return `<div class="markdown-content">${html}</div>`;
        }
        return `<div class="markdown-content"><pre>${content}</pre></div>`;
    },


    /**
     * 显示 Toast 通知
     */
    showToast(message, type = 'info') {
        const container = document.getElementById('toast-container');
        const icons = {
            success: 'check-circle',
            error: 'x-circle',
            warning: 'alert-triangle',
            info: 'info',
        };

        const toast = document.createElement('div');
        toast.className = `toast ${type}`;
        toast.innerHTML = `
            <i data-lucide="${icons[type]}" class="toast-icon"></i>
            <span class="toast-message">${message}</span>
        `;

        container.appendChild(toast);
        lucide.createIcons({ icons: { [icons[type]]: lucide.icons[icons[type]] }, attrs: {} });

        setTimeout(() => {
            toast.style.animation = 'slideIn 0.25s ease reverse';
            setTimeout(() => toast.remove(), 250);
        }, 3000);
    },

    /**
     * 显示加载遮罩
     */
    showLoading() {
        document.getElementById('loading-overlay').classList.add('active');
    },

    /**
     * 隐藏加载遮罩
     */
    hideLoading() {
        document.getElementById('loading-overlay').classList.remove('active');
    },

    /**
     * 显示模态框
     */
    showModal(title, content) {
        document.getElementById('modal-title').textContent = title;
        document.getElementById('modal-body').innerHTML = content;
        document.getElementById('modal-overlay').classList.add('active');
        lucide.createIcons();
    },

    /**
     * 关闭模态框
     */
    closeModal() {
        document.getElementById('modal-overlay').classList.remove('active');
    },
};

// 导出到全局
window.Components = Components;