在这里插入图片描述

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

本文对应模块:Web 层的 UI 组件库实现,包括 CSS 变量定义、通用组件实现(按钮、对话框、Toast、Loader、Card)、响应式设计,以及与原生 UI 组件的集成。

📌 概述

一个优秀的应用需要一个完整的 UI 组件库和统一的样式系统。UI 组件库提供了一套可复用的组件,包括按钮、对话框、提示框、加载指示器和卡片等。样式系统使用 CSS 变量来定义颜色、字体、间距等设计令牌,确保整个应用的视觉一致性。通过合理的组件化设计,可以提高开发效率、降低维护成本,并提供良好的用户体验。

🔗 UI 系统的设计流程

Web 层首先定义全局的 CSS 变量,包括颜色系统、排版系统、间距系统等设计令牌。然后基于这些变量实现各种 UI 组件,如按钮、对话框等。每个组件都是独立的、可复用的,可以通过参数来定制其外观和行为。当需要修改应用的整体风格时,只需要修改 CSS 变量,所有使用这些变量的组件都会自动更新。原生层可以通过 Cordova 插件与 Web 层的 UI 系统进行交互,例如显示原生对话框或通知。

🎯 CSS 变量定义

/* ui-components.css */

:root {
    /* ==================== 颜色系统 ==================== */
    
    /* 主色调 */
    --color-primary: #4a90e2;
    --color-primary-light: #7aabf0;
    --color-primary-dark: #2e5c8a;
    
    /* 次色调 */
    --color-secondary: #50c878;
    --color-secondary-light: #7dd99f;
    --color-secondary-dark: #2d7a4a;
    
    /* 警告和错误 */
    --color-warning: #f5a623;
    --color-danger: #e74c3c;
    --color-success: #50c878;
    --color-info: #4a90e2;
    
    /* 中立色 */
    --color-text-primary: #333333;
    --color-text-secondary: #666666;
    --color-text-tertiary: #999999;
    --color-text-disabled: #cccccc;
    
    --color-bg-primary: #ffffff;
    --color-bg-secondary: #f5f5f5;
    --color-bg-tertiary: #eeeeee;
    
    --color-border: #e0e0e0;
    --color-border-light: #f0f0f0;
    
    /* ==================== 排版系统 ==================== */
    
    /* 字体族 */
    --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    --font-family-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
    
    /* 字体大小 */
    --font-size-xs: 12px;
    --font-size-sm: 14px;
    --font-size-base: 16px;
    --font-size-lg: 18px;
    --font-size-xl: 20px;
    --font-size-2xl: 24px;
    --font-size-3xl: 28px;
    --font-size-4xl: 32px;
    
    /* 行高 */
    --line-height-tight: 1.2;
    --line-height-normal: 1.5;
    --line-height-relaxed: 1.8;
    
    /* 字体粗细 */
    --font-weight-light: 300;
    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --font-weight-bold: 700;
    
    /* ==================== 间距系统 ==================== */
    
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 24px;
    --spacing-xl: 32px;
    --spacing-2xl: 48px;
    
    /* ==================== 圆角系统 ==================== */
    
    --radius-xs: 2px;
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 16px;
    --radius-full: 9999px;
    
    /* ==================== 阴影系统 ==================== */
    
    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
    --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
    
    /* ==================== 过渡效果 ==================== */
    
    --transition-fast: 150ms ease-in-out;
    --transition-base: 250ms ease-in-out;
    --transition-slow: 350ms ease-in-out;
    
    /* ==================== 其他 ==================== */
    
    --z-index-dropdown: 1000;
    --z-index-sticky: 1020;
    --z-index-fixed: 1030;
    --z-index-modal-backdrop: 1040;
    --z-index-modal: 1050;
    --z-index-popover: 1060;
    --z-index-tooltip: 1070;
}

代码解释:

CSS 变量使用 -- 前缀定义,可以在整个应用中复用。:root 选择器定义全局变量,所有元素都可以访问这些变量。代码中定义了颜色系统(包括主色、次色、警告色、中立色等)、排版系统(字体族、字体大小、行高、字体粗细)、间距系统(用于 padding 和 margin)、圆角系统、阴影系统和过渡效果。还定义了 z-index 层级,用于控制元素的堆叠顺序。使用 var() 函数引用变量,例如 color: var(--color-primary);。这种方式便于维护和修改整个应用的样式,只需要修改变量值,所有使用这个变量的地方都会自动更新。

🧩 UI 组件库实现

1. 按钮组件

/**
 * UI 组件库
 * 提供常用的 UI 组件
 */

class UIComponents {
    /**
     * 创建按钮
     * @param {string} text - 按钮文本
     * @param {Function} onClick - 点击回调
     * @param {Object} options - 选项
     */
    static createButton(text, onClick, options = {}) {
        const button = document.createElement('button');
        button.className = 'ui-button';
        button.textContent = text;
        
        // 应用变体样式
        const variant = options.variant || 'primary';
        button.classList.add(`ui-button--${variant}`);
        
        // 应用大小样式
        const size = options.size || 'md';
        button.classList.add(`ui-button--${size}`);
        
        // 禁用状态
        if (options.disabled) {
            button.disabled = true;
            button.classList.add('ui-button--disabled');
        }
        
        // 绑定点击事件
        if (onClick) {
            button.addEventListener('click', onClick);
        }
        
        return button;
    }

    /**
     * 创建输入框
     * @param {Object} options - 选项
     */
    static createInput(options = {}) {
        const input = document.createElement('input');
        input.className = 'ui-input';
        input.type = options.type || 'text';
        input.placeholder = options.placeholder || '';
        input.value = options.value || '';
        
        if (options.disabled) {
            input.disabled = true;
        }
        
        return input;
    }

    /**
     * 创建对话框
     * @param {string} title - 对话框标题
     * @param {HTMLElement|string} content - 对话框内容
     * @param {Array} buttons - 按钮数组
     */
    static createDialog(title, content, buttons = []) {
        const dialog = document.createElement('div');
        dialog.className = 'ui-dialog';
        
        // 创建对话框背景
        const backdrop = document.createElement('div');
        backdrop.className = 'ui-dialog-backdrop';
        backdrop.addEventListener('click', () => dialog.hide());
        
        // 创建对话框容器
        const container = document.createElement('div');
        container.className = 'ui-dialog-container';
        
        // 创建标题
        const titleElement = document.createElement('div');
        titleElement.className = 'ui-dialog-title';
        titleElement.textContent = title;
        container.appendChild(titleElement);
        
        // 创建内容
        const contentElement = document.createElement('div');
        contentElement.className = 'ui-dialog-content';
        if (typeof content === 'string') {
            contentElement.textContent = content;
        } else {
            contentElement.appendChild(content);
        }
        container.appendChild(contentElement);
        
        // 创建按钮区域
        const buttonArea = document.createElement('div');
        buttonArea.className = 'ui-dialog-buttons';
        buttons.forEach(button => {
            buttonArea.appendChild(button);
        });
        container.appendChild(buttonArea);
        
        dialog.appendChild(backdrop);
        dialog.appendChild(container);
        
        // 添加显示和隐藏方法
        dialog.show = () => {
            dialog.style.display = 'flex';
            document.body.appendChild(dialog);
        };
        
        dialog.hide = () => {
            dialog.style.display = 'none';
            if (dialog.parentNode) {
                dialog.parentNode.removeChild(dialog);
            }
        };
        
        return dialog;
    }

    /**
     * 显示 Toast 提示
     * @param {string} message - 提示信息
     * @param {Object} options - 选项
     */
    static showToast(message, options = {}) {
        const toast = document.createElement('div');
        toast.className = 'ui-toast';
        toast.textContent = message;
        
        // 应用类型样式
        const type = options.type || 'info';
        toast.classList.add(`ui-toast--${type}`);
        
        document.body.appendChild(toast);
        
        // 动画显示
        setTimeout(() => {
            toast.classList.add('ui-toast--show');
        }, 10);
        
        // 自动隐藏
        const duration = options.duration || 3000;
        setTimeout(() => {
            toast.classList.remove('ui-toast--show');
            setTimeout(() => {
                if (toast.parentNode) {
                    toast.parentNode.removeChild(toast);
                }
            }, 300);
        }, duration);
    }

    /**
     * 创建加载指示器
     */
    static createLoader() {
        const loader = document.createElement('div');
        loader.className = 'ui-loader';
        loader.innerHTML = `
            <div class="ui-loader-spinner"></div>
            <p class="ui-loader-text">加载中...</p>
        `;
        return loader;
    }

    /**
     * 创建卡片
     * @param {Object} options - 选项
     */
    static createCard(options = {}) {
        const card = document.createElement('div');
        card.className = 'ui-card';
        
        if (options.title) {
            const title = document.createElement('div');
            title.className = 'ui-card-title';
            title.textContent = options.title;
            card.appendChild(title);
        }
        
        if (options.content) {
            const content = document.createElement('div');
            content.className = 'ui-card-content';
            if (typeof options.content === 'string') {
                content.textContent = options.content;
            } else {
                content.appendChild(options.content);
            }
            card.appendChild(content);
        }
        
        return card;
    }
}

代码解释:

UIComponents 类提供了一套静态方法来创建常用的 UI 组件。createButton() 方法创建按钮元素,支持不同的变体(primary、secondary)和大小(sm、md、lg)。通过 options 参数可以配置按钮的外观和行为,包括是否禁用。createInput() 方法创建输入框,支持不同的类型(text、password、email 等)和占位符。createDialog() 方法创建模态对话框,包含标题、内容和按钮。showToast() 方法显示临时提示信息,自动在指定时间后隐藏。createCard() 方法创建卡片组件,用于展示内容。createLoader() 方法创建加载指示器,显示加载中的状态。所有组件都返回 DOM 元素,可以直接添加到页面中,这样可以灵活地组合和使用这些组件。

2. 按钮样式

/* 按钮基础样式 */
.ui-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-family-base);
    font-weight: var(--font-weight-medium);
    border: none;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all var(--transition-base);
    outline: none;
}

/* 按钮大小变体 */
.ui-button--sm {
    padding: var(--spacing-xs) var(--spacing-sm);
    font-size: var(--font-size-sm);
    min-height: 28px;
}

.ui-button--md {
    padding: var(--spacing-sm) var(--spacing-md);
    font-size: var(--font-size-base);
    min-height: 36px;
}

.ui-button--lg {
    padding: var(--spacing-md) var(--spacing-lg);
    font-size: var(--font-size-lg);
    min-height: 44px;
}

/* 按钮颜色变体 */
.ui-button--primary {
    background-color: var(--color-primary);
    color: white;
    box-shadow: var(--shadow-sm);
}

.ui-button--primary:hover {
    background-color: var(--color-primary-dark);
    box-shadow: var(--shadow-md);
}

.ui-button--primary:active {
    transform: scale(0.98);
}

.ui-button--secondary {
    background-color: transparent;
    color: var(--color-primary);
    border: 1px solid var(--color-primary);
}

.ui-button--secondary:hover {
    background-color: var(--color-primary);
    color: white;
}

/* 禁用状态 */
.ui-button--disabled,
.ui-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

代码解释:

按钮样式使用 CSS 变量保持样式的一致性,所有颜色、字体、间距等都引用了全局定义的变量。.ui-button 定义了按钮的基础样式,使用 inline-flex 布局使内容居中。transition 属性使按钮交互更流畅,当鼠标悬停或点击时会有平滑的动画效果。:hover 和 :active 伪类提供视觉反馈,让用户知道按钮是可交互的。不同大小的按钮(sm、md、lg)通过不同的 padding 和 font-size 来区分。不同颜色的按钮(primary、secondary)通过不同的背景色和文字色来区分。禁用状态使用 opacity 降低透明度,cursor: not-allowed 改变鼠标指针,pointer-events: none 禁用点击事件。

3. 对话框样式

/* 对话框背景 */
.ui-dialog {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: var(--z-index-modal);
    align-items: center;
    justify-content: center;
}

.ui-dialog-backdrop {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: var(--z-index-modal-backdrop);
}

/* 对话框容器 */
.ui-dialog-container {
    position: relative;
    background-color: var(--color-bg-primary);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xl);
    max-width: 90%;
    width: 400px;
    max-height: 90vh;
    overflow-y: auto;
    z-index: var(--z-index-modal);
    animation: slideUp 0.3s ease-out;
}

@keyframes slideUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* 对话框标题 */
.ui-dialog-title {
    padding: var(--spacing-lg);
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-primary);
    border-bottom: 1px solid var(--color-border);
}

/* 对话框内容 */
.ui-dialog-content {
    padding: var(--spacing-lg);
    color: var(--color-text-secondary);
    line-height: var(--line-height-normal);
}

/* 对话框按钮区域 */
.ui-dialog-buttons {
    display: flex;
    gap: var(--spacing-md);
    padding: var(--spacing-lg);
    border-top: 1px solid var(--color-border);
    justify-content: flex-end;
}

.ui-dialog-buttons .ui-button {
    flex: 1;
}

代码解释:

对话框样式使用 position: fixed 使对话框固定在视口中,不会随着页面滚动而移动。.ui-dialog 是对话框的容器,设置为 display: none 默认隐藏,需要时通过 JavaScript 改为 display: flex 来显示。.ui-dialog-backdrop 是半透明的背景层,用于遮挡页面内容,点击背景可以关闭对话框。z-index 确保对话框显示在其他元素上方,modal-backdrop 的 z-index 比 modal 小,这样背景在对话框后面。animation 属性提供平滑的进入动画,使用 slideUp 关键帧从下方滑入。max-height 和 overflow-y: auto 处理内容过长的情况,使对话框内容可以滚动。.ui-dialog-buttons 使用 flex 布局排列按钮,justify-content: flex-end 使按钮右对齐。

4. Toast 样式

/* Toast 提示 */
.ui-toast {
    position: fixed;
    bottom: var(--spacing-lg);
    left: 50%;
    transform: translateX(-50%) translateY(100px);
    padding: var(--spacing-md) var(--spacing-lg);
    background-color: var(--color-text-primary);
    color: white;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    z-index: var(--z-index-tooltip);
    transition: transform var(--transition-base);
    max-width: 90%;
    word-break: break-word;
}

.ui-toast--show {
    transform: translateX(-50%) translateY(0);
}

/* Toast 类型变体 */
.ui-toast--success {
    background-color: var(--color-success);
}

.ui-toast--warning {
    background-color: var(--color-warning);
}

.ui-toast--danger {
    background-color: var(--color-danger);
}

.ui-toast--info {
    background-color: var(--color-info);
}

代码解释:

Toast 样式定义了一个临时提示框,使用 position: fixed 固定在屏幕底部。初始状态下,使用 transform: translateY(100px) 将 Toast 隐藏在屏幕下方。当添加 .ui-toast–show 类时,transform 改为 translateY(0),使 Toast 滑入屏幕。transition 属性使这个动画平滑进行。left: 50% 和 translateX(-50%) 使 Toast 水平居中。不同的 Toast 类型(success、warning、danger、info)使用不同的背景颜色,通过 CSS 变量引用全局定义的颜色。这样可以快速改变所有 Toast 的颜色。

🎨 响应式设计

/* 平板设备 */
@media (max-width: 768px) {
    :root {
        --font-size-base: 14px;
        --spacing-md: 12px;
    }
    
    .ui-dialog-container {
        width: 90%;
    }
}

/* 手机设备 */
@media (max-width: 480px) {
    :root {
        --font-size-base: 12px;
        --spacing-md: 8px;
    }
    
    .ui-dialog-container {
        width: 95%;
        max-height: 80vh;
    }
    
    .ui-dialog-buttons {
        flex-direction: column;
    }
}

代码解释:

响应式设计使用 @media 查询根据不同的屏幕尺寸调整样式。对于平板设备(最大宽度 768px),减小字体大小和间距,使内容更紧凑。对于手机设备(最大宽度 480px),进一步减小字体大小和间距,并调整对话框的宽度和高度。对话框按钮改为 flex-direction: column,使按钮竖排排列,这样在小屏幕上更容易点击。通过修改 CSS 变量的值,可以影响所有使用这些变量的元素,实现全局的响应式调整。这种方法比为每个元素单独编写媒体查询更高效。

🔌 原生 UI 组件与 Web 层的集成

HarmonyOS 原生层提供了丰富的 UI 组件,可以通过 Cordova 插件与 Web 层的 UI 系统进行集成。例如,可以使用原生对话框来替代 Web 层的对话框,以获得更好的性能和原生体验。

// ArkTS 代码示例 - 原生对话框组件
import { promptAction } from '@kit.ArkUI';
import { CordovaPlugin, CallbackContext } from '@magongshou/harmony-cordova/Index';
import { PluginResult, MessageStatus } from '@magongshou/harmony-cordova/Index';

export class UIPlugin extends CordovaPlugin {
    // 显示原生对话框
    async showNativeDialog(callbackContext: CallbackContext, args: string[]): Promise<void> {
        try {
            const dialogConfig = JSON.parse(args[0]);
            
            const result = await promptAction.showDialog({
                title: dialogConfig.title,
                message: dialogConfig.message,
                buttons: [
                    { text: '取消', color: '#666666' },
                    { text: '确定', color: '#0066cc' }
                ]
            });
            
            const pluginResult = PluginResult.createByString(
                MessageStatus.OK, 
                JSON.stringify({ buttonIndex: result.index })
            );
            callbackContext.sendPluginResult(pluginResult);
        } catch (error) {
            const pluginResult = PluginResult.createByString(
                MessageStatus.ERROR, 
                (error as Error).message
            );
            callbackContext.sendPluginResult(pluginResult);
        }
    }

    // 显示原生 Toast 提示
    async showNativeToast(callbackContext: CallbackContext, args: string[]): Promise<void> {
        try {
            const message = args[0];
            
            promptAction.showToast({
                message: message,
                duration: 2000
            });
            
            const pluginResult = PluginResult.createByString(MessageStatus.OK, 'Toast 已显示');
            callbackContext.sendPluginResult(pluginResult);
        } catch (error) {
            const pluginResult = PluginResult.createByString(
                MessageStatus.ERROR, 
                (error as Error).message
            );
            callbackContext.sendPluginResult(pluginResult);
        }
    }
}

原生代码解释:

UIPlugin 是一个 Cordova 插件,提供了原生 UI 组件的访问接口。showNativeDialog 方法使用 HarmonyOS 的 promptAction 模块显示原生对话框。dialogConfig 包含对话框的标题和消息内容。promptAction.showDialog() 返回一个 Promise,解析后得到用户点击的按钮索引。showNativeToast 方法显示原生 Toast 提示,message 是要显示的文本,duration 是显示时间(毫秒)。这两个方法都通过 PluginResult 将结果返回给 Web 层。

Web 层调用原生 UI 组件

Web 层可以调用原生插件来显示原生 UI 组件,以获得更好的性能和用户体验:

// JavaScript 代码 - 调用原生 UI 组件
function showNativeDialog(title, message) {
    return new Promise((resolve, reject) => {
        cordova.exec(
            function(result) {
                console.log('对话框结果:', result);
                resolve(result.buttonIndex === 1); // 返回用户是否点击了确定
            },
            function(error) {
                console.error('显示对话框失败:', error);
                reject(error);
            },
            'UIPlugin',
            'showNativeDialog',
            [JSON.stringify({ title, message })]
        );
    });
}

function showNativeToast(message) {
    return new Promise((resolve, reject) => {
        cordova.exec(
            function() {
                resolve();
            },
            function(error) {
                reject(error);
            },
            'UIPlugin',
            'showNativeToast',
            [message]
        );
    });
}

// 在应用中使用
async function deleteTask(taskId) {
    const confirmed = await showNativeDialog('删除任务', '确定要删除这个任务吗?');
    if (confirmed) {
        await taskManager.deleteTask(taskId);
        await showNativeToast('任务已删除');
    }
}

Web 层代码解释:

showNativeDialog 函数使用 cordova.exec() 调用原生插件的 showNativeDialog 方法。成功回调接收对话框的结果,包括用户点击的按钮索引。如果用户点击了确定按钮(索引为 1),函数返回 true。showNativeToast 函数显示原生 Toast 提示,不需要返回结果。deleteTask 函数展示了实际的使用场景:首先显示原生对话框询问用户是否确定删除。如果用户确认,则调用 taskManager.deleteTask() 删除任务,然后显示 Toast 提示用户任务已删除。这样可以提供更好的原生体验。

📝 总结

一个完整的 UI 组件库和样式系统是构建现代应用的基础。Web 层通过 CSS 变量和 JavaScript 组件提供了灵活的 UI 系统,而原生层通过 Cordova 插件提供了高性能的原生 UI 组件。通过合理的设计和集成,可以在两层之间取得平衡,既能保证开发效率,又能提供优秀的用户体验。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐