Skip to content

AE插件UI交互指南

概述

本文档详细说明Eagle2Ae AE插件的用户界面交互流程、操作指南和最佳实践,包括多面板支持架构、HTTP轮询通信机制、图层检测系统、弹窗交互机制以及Demo模式功能,帮助开发者理解用户操作逻辑和界面响应机制。

在多面板支持架构下,每个面板实例拥有独立的UI状态和交互逻辑,确保多面板环境下的用户体验一致性。

1. 插件启动和初始化流程

1.1 启动序列

mermaid
sequenceDiagram
    participant User as 用户
    participant UI as 插件界面
    participant Main as 主程序
    participant Eagle as Eagle插件
    
    User->>UI: 打开插件面板
    UI->>Main: 初始化应用
    Main->>UI: 显示加载状态
    Main->>Eagle: HTTP轮询连接
    Eagle-->>Main: 连接响应
    Main->>UI: 更新连接状态
    UI->>User: 显示就绪界面

1.2 多面板支持架构

Eagle2Ae 扩展支持多面板架构,允许同时打开多个独立的面板实例。每个面板实例都有独立的状态管理、配置存储和通信通道,确保各面板之间的操作互不干扰。

面板识别机制

javascript
// 面板识别机制
class AEExtension {
    constructor() {
        // 识别当前面板 ID
        this.panelId = this.getPanelId(); // 'panel1', 'panel2', 或 'panel3'
        
        // 面板独立设置管理器
        this.settingsManager = new SettingsManager(this.panelId);
        
        // 面板独立状态检测器
        this.projectStatusChecker = new ProjectStatusChecker(this.csInterface, this.log.bind(this), this.panelId);
        
        // 面板独立文件处理器
        this.fileHandler = new FileHandler(this.settingsManager, this.csInterface, this.log.bind(this), this.panelId);
    }
}

1.2 初始化检查项

  1. CEP环境初始化

    • 初始化CSInterface接口
    • 设置CEP调试模式
    • 加载扩展配置
  2. Eagle连接检测

    • 自动发现Eagle端口(默认8080)
    • 建立HTTP通信连接
    • 获取Eagle基本信息和状态
  3. AE项目信息获取

    • 通过ExtendScript读取当前项目状态
    • 获取活动合成信息
    • 更新项目信息显示
  4. 拖拽系统初始化

    • 设置全局拖拽事件监听器
    • 初始化拖拽视觉反馈系统
    • 配置文件类型检测机制

2. 连接状态管理

2.1 连接状态指示器

状态颜色显示文本用户操作
未连接灰色"未连接"点击测试连接
连接中黄色"连接中..."等待连接完成
已连接绿色"已连接 (XXms)"正常使用功能
连接失败红色"连接失败"检查Eagle状态

2.2 连接测试流程

javascript
// 连接测试逻辑
async function testConnection() {
    // 1. 更新UI状态为"连接中"
    updateConnectionStatus('connecting');
    
    try {
        // 2. 发送HTTP轮询ping请求
        const startTime = Date.now();
        const response = await sendPingRequest();
        const pingTime = Date.now() - startTime;
        
        // 3. 更新为已连接状态
        updateConnectionStatus('connected', pingTime);
        
        // 4. 刷新Eagle信息
        await refreshEagleInfo();
        
    } catch (error) {
        // 5. 显示连接失败
        updateConnectionStatus('failed', error.message);
    }
}

2.3 HTTP轮询机制

Eagle2Ae 扩展使用HTTP轮询机制与Eagle插件进行通信,每个面板实例都有独立的轮询通道:

javascript
// HTTP轮询客户端
class PollingClient {
    constructor(eagleUrl, clientId, panelId) {
        this.eagleUrl = eagleUrl;
        this.clientId = clientId; // 面板唯一标识
        this.panelId = panelId;   // 面板ID (panel1, panel2, panel3)
        this.pollingInterval = 1000; // 1秒轮询间隔
        this.isPolling = false;
    }
    
    // 启动轮询
    startPolling() {
        this.isPolling = true;
        this.poll();
    }
    
    // 轮询消息
    async poll() {
        if (!this.isPolling) return;
        
        try {
            // 携带客户端ID和面板ID进行轮询
            const response = await fetch(
                `${this.eagleUrl}/messages?clientId=${this.clientId}&panelId=${this.panelId}`
            );
            
            if (response.ok) {
                const messages = await response.json();
                this.handleMessages(messages);
            }
        } catch (error) {
            console.error('轮询错误:', error);
        } finally {
            // 继续下一次轮询
            setTimeout(() => this.poll(), this.pollingInterval);
        }
    }
    
    // 处理接收到的消息
    handleMessages(messages) {
        messages.forEach(message => {
            switch (message.type) {
                case 'IMPORT_REQUEST':
                    this.handleImportRequest(message);
                    break;
                case 'STATUS_UPDATE':
                    this.handleStatusUpdate(message);
                    break;
                // 其他消息类型...
            }
        });
    }
}

3. 导入模式交互流程

3.1 导入模式选择

直接导入模式

  • 用户操作: 点击"直接导入"按钮
  • UI响应: 按钮高亮,其他模式取消选中
  • 后台逻辑: 设置导入模式为direct
  • 适用场景: 文件已在合适位置,无需移动

项目旁复制模式

  • 用户操作: 点击"项目旁复制"按钮
  • UI响应: 按钮高亮,可能弹出文件夹名称设置
  • 配置选项: 文件夹名称(默认:Eagle_Assets)
  • 后台逻辑: 计算项目文件旁边的目标路径

指定文件夹模式

  • 用户操作: 点击"指定文件夹"按钮
  • UI响应: 按钮高亮,弹出文件夹选择对话框
  • 配置要求: 必须设置有效的目标文件夹路径
  • 路径验证: 检查路径存在性和写入权限

4. 导入行为配置

核心文档: 关于此功能的完整技术实现、数据流和代码示例,请参阅 “导入行为”设置功能逻辑

4.1 行为选项说明

此设置决定了素材在导入项目后,是否以及如何被添加到当前合成的时间轴中。

选项功能描述使用场景
不导入合成no_import仅将素材导入到项目面板,不执行任何时间轴操作。批量导入大量素材,以便稍后手动整理和使用。
当前时间current_time将素材作为一个新图层,添加到当前合成中,其入点与时间轴的当前时间指示器对齐。在视频的特定时间点精确插入视觉元素。
时间轴开始timeline_start将素材作为一个新图层,添加到当前合成中,其入点设置为 0适用于添加背景、水印或作为项目起始的基础图层。

4.2 行为选择逻辑

用户的选择会实时更新 SettingsManager 中的两个关键设置项:addToComposition (布尔值) 和 timelineOptions.placement (字符串)。

  • 选择 “不导入合成” 会将 addToComposition 设置为 false
  • 选择 “当前时间”“时间轴开始” 会将 addToComposition 设置为 true,并将 timelineOptions.placement 设置为对应的值 (current_timetimeline_start)。

4.3 设置传递与执行流程

时间轴设置从UI传递到ExtendScript的完整流程如下:

  1. UI设置更新: 用户在UI上选择后,main.js 中的监听器立即调用 SettingsManagerupdateField 方法,将 addToCompositiontimelineOptions.placement 的值实时保存到 localStorage
  2. 参数构建: 当导入开始时,FileHandler.jsSettingsManager 获取包含这两个设置的完整 settings 对象。
  3. 参数传递: FileHandler.js 将整个 settings 对象序列化为JSON,并作为参数传递给 hostscript.jsx 中的 importFilesWithSettings 函数。
  4. ExtendScript处理: 在 hostscript.jsx 中:
    • 首先检查 settings.addToComposition 的值。如果为 false,则导入流程在将素材添加到项目面板后即告结束。
    • 如果为 true,脚本会继续将素材添加到当前合成,然后读取 settings.timelineOptions.placement 的值,并据此将新图层的 startTime 设置为 comp.time0

5. 文件拖拽交互系统

5.1 拖拽系统初始化

javascript
// 拖拽系统设置 (setupDragAndDrop方法)
setupDragAndDrop() {
    try {
        // 防止默认拖拽行为
        document.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.stopPropagation();
            // 添加视觉反馈
            document.body.classList.add('drag-over');
        });

        document.addEventListener('dragenter', (e) => {
            e.preventDefault();
            e.stopPropagation();
        });

        document.addEventListener('dragleave', (e) => {
            // 只有当拖拽完全离开窗口时才移除样式
            if (e.clientX === 0 && e.clientY === 0) {
                document.body.classList.remove('drag-over');
            }
        });

        // 处理文件拖拽
        document.addEventListener('drop', this.handleFileDrop.bind(this));
    } catch (error) {
        this.log(`设置拖拽监听失败: ${error.message}`, 'error');
    }
}

5.2 文件拖拽处理流程

5.2.1 主要拖拽处理方法

javascript
// 处理文件拖拽 (handleFileDrop方法)
async handleFileDrop(event) {
    event.preventDefault();
    event.stopPropagation();

    // 移除视觉反馈
    document.body.classList.remove('drag-over');

    try {
        const files = Array.from(event.dataTransfer.files);
        const items = Array.from(event.dataTransfer.items);
        
        if (files.length === 0 && items.length === 0) {
            this.showDropMessage('未检测到文件', 'warning');
            return;
        }

        // 检查是否包含文件夹
        const hasDirectories = items.some(item => 
            item.webkitGetAsEntry && item.webkitGetAsEntry()?.isDirectory
        );
        
        if (hasDirectories) {
            // 处理文件夹拖拽(可能包含序列帧)
            await this.handleDirectoryDrop(items, files);
        } else {
            // 处理普通文件拖拽
            await this.handleFilesDrop(files, event.dataTransfer);
        }
    } catch (error) {
        this.log(`处理拖拽失败: ${error.message}`, 'error');
        this.showDropMessage('拖拽处理失败', 'error');
    }
}

5.2.2 Eagle拖拽识别机制

javascript
// Eagle拖拽识别 (isEagleDrag方法)
isEagleDrag(dataTransfer, files) {
    try {
        // 方法1:检查文件路径特征
        const hasEaglePath = files.some(file => {
            const path = file.path || file.webkitRelativePath || '';
            const pathLower = path.toLowerCase();
            return pathLower.includes('eagle') ||
                   pathLower.includes('.eaglepack') ||
                   pathLower.includes('library.library') ||
                   (pathLower.includes('images') && pathLower.includes('library'));
        });

        // 方法2:检查自定义数据类型
        const hasEagleData = dataTransfer.types.some(type => {
            const typeLower = type.toLowerCase();
            return typeLower.includes('eagle') ||
                   typeLower.includes('x-eagle') ||
                   typeLower.includes('application/x-eagle');
        });

        // 方法3:检查拖拽来源信息
        const plainText = dataTransfer.getData('text/plain') || '';
        const plainTextLower = plainText.toLowerCase();
        const hasEagleMetadata = plainTextLower.includes('eagle') ||
                               plainTextLower.includes('.eaglepack') ||
                               plainTextLower.includes('library.library');

        return hasEaglePath || hasEagleData || hasEagleMetadata;
    } catch (error) {
        this.log(`Eagle拖拽检测失败: ${error.message}`, 'error');
        return false;
    }
}

5.3 拖拽视觉反馈系统

5.3.1 CSS样式实现

css
/* 拖拽悬停时的整体效果 */
body.drag-over {
    transition: all 0.3s ease;
}

/* 拖拽时的背景蒙版 */
body.drag-over::before {
    content: '';
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(4px);
    z-index: 999;
    pointer-events: none;
    animation: fadeIn 0.3s ease;
}

/* 拖拽时的边框效果 */
body.drag-over::after {
    content: '';
    position: fixed;
    top: 8px; left: 8px; right: 8px; bottom: 8px;
    border: 2px dashed #3498db;
    border-radius: 12px;
    z-index: 1000;
    pointer-events: none;
    animation: dragPulse 1.5s ease-in-out infinite alternate;
}

/* 拖拽时的中央提示 */
.drag-overlay {
    position: fixed;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    z-index: 1002;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
}

body.drag-over .drag-overlay {
    opacity: 1;
}

5.3.2 视觉反馈组件

  1. 背景蒙版: 半透明黑色背景,带模糊效果
  2. 边框动画: 蓝色虚线边框,脉冲动画效果
  3. 中央提示: 显示"+"图标和"拖拽文件到此处"文字
  4. 状态消息: 右上角滑入式消息提示

5.4 文件夹和序列帧处理

5.4.1 文件夹拖拽处理

javascript
// 处理文件夹拖拽 (handleDirectoryDrop方法)
async handleDirectoryDrop(items, files) {
    this.log('检测到文件夹拖拽,开始处理...', 'info');
    
    const allFiles = [];
    
    // 递归读取文件夹内容
    for (const item of items) {
        const entry = item.webkitGetAsEntry();
        if (entry) {
            const entryFiles = await this.readDirectoryEntry(entry);
            allFiles.push(...entryFiles);
        }
    }
    
    // 添加直接拖拽的文件
    allFiles.push(...files);
    
    if (allFiles.length === 0) {
        this.showDropMessage('文件夹中没有找到可导入的文件', 'warning');
        return;
    }
    
    // 分析文件类型和序列帧
    const analysis = this.analyzeDroppedFiles(allFiles);
    
    // 显示导入选项对话框
    this.showFileImportDialog(allFiles, analysis);
}

5.4.2 文件分析和分类

javascript
// 分析拖拽的文件 (analyzeDroppedFiles方法)
analyzeDroppedFiles(files) {
    const analysis = {
        total: files.length,
        categories: {
            image: [], video: [], audio: [],
            design: [], project: [], unknown: []
        },
        sequences: [],
        folders: new Set()
    };
    
    // 按文件夹分组
    const folderGroups = {};
    
    files.forEach(file => {
        const category = this.getFileCategory(file);
        analysis.categories[category].push(file);
        
        // 提取文件夹路径
        const path = file.fullPath || file.relativePath || file.webkitRelativePath || '';
        const folderPath = path.substring(0, path.lastIndexOf('/'));
        
        if (folderPath) {
            analysis.folders.add(folderPath);
            if (!folderGroups[folderPath]) {
                folderGroups[folderPath] = [];
            }
            folderGroups[folderPath].push(file);
        }
    });
    
    // 检测序列帧
    for (const [folderPath, folderFiles] of Object.entries(folderGroups)) {
        const sequence = this.detectImageSequence(folderFiles);
        if (sequence) {
            analysis.sequences.push(sequence);
        }
    }
    
    return analysis;
}

5.5 导入确认弹窗系统

5.5.1 弹窗触发条件

弹窗会在以下情况下自动触发:

  1. 文件夹拖拽: 检测到文件夹结构时
  2. 序列帧检测: 发现图像序列时
  3. 混合文件类型: 包含多种文件类型时
  4. 大量文件: 文件数量超过阈值时
  5. 非Eagle拖拽: 普通文件拖拽需要确认时
  6. 合成检查: 当用户设置为添加到合成但当前无活动合成时

5.5.2 合成检查机制

在拖拽导入过程中,系统会检查用户的合成设置和当前AE项目状态:

javascript
// 合成检查逻辑
function checkCompositionStatus(settings) {
    // 检查用户是否设置了添加到合成
    if (settings.addToComposition) {
        // 获取当前活动合成
        const activeComp = app.project.activeItem;
        
        // 如果没有活动合成或活动项不是合成
        if (!activeComp || !(activeComp instanceof CompItem)) {
            return {
                needsConfirmation: true,
                reason: 'no_active_composition',
                message: '当前没有活动的合成,是否继续导入?'
            };
        }
    }
    
    return {
        needsConfirmation: false
    };
}

5. 文件拖拽交互系统

插件的拖拽导入功能是一个核心交互,它支持两种主要的拖拽来源:从Eagle客户端拖拽从操作系统直接拖拽文件/文件夹。两者共享一套处理流水线,但入口和预处理步骤有所不同。

5.1 统一事件入口: handleFileDrop

所有拖拽操作的起点是 main.js 中的 setupDragAndDrop() 方法,它为整个插件面板注册了 drop 事件监听器,并统一由 handleFileDrop() 函数处理。

handleFileDrop() 的核心职责是作为拖拽来源的路由器

javascript
// 真实实现: AEExtension.handleFileDrop() in main.js
dropZone.addEventListener('drop', (e) => {
    e.preventDefault();
    e.stopPropagation();
    dropZone.classList.remove('drag-over');

    // 关键:检查拖拽事件是否来自Eagle
    if (this.isEagleDrag(e.dataTransfer)) {
        // 路径A: Eagle客户端拖拽
        this.handleEagleDragImport(e.dataTransfer);
    } else {
        // 路径B: 操作系统拖拽
        // 注意:此处的逻辑在真实代码中更为复杂,涉及文件夹读取
        // 此处为简化示意,实际会调用 handleDirectoryDrop 等
        const files = Array.from(e.dataTransfer.files);
        if (files.length > 0) {
            // 构造一个标准消息体,并进入通用处理流程
            const message = { files: files, isDragImport: true, source: 'os_drag' };
            this.handleImportFiles(message);
        }
    }
});

5.2 拖拽源识别: isEagleDrag

handleFileDrop 函数依赖 isEagleDrag 的返回值来区分来源。文档中旧的复杂实现已被废弃,当前代码中的实现非常简洁高效:

javascript
// 真实实现: AEExtension.isEagleDrag() in main.js
isEagleDrag(dataTransfer) {
    if (!dataTransfer) return false;

    const types = dataTransfer.types;
    if (types.includes('text/uri-list')) {
        const url = dataTransfer.getData('text/uri-list');
        // 核心检查:只验证拖拽数据是否为 eagle:// 协议
        if (url.startsWith('eagle://')) {
            return true;
        }
    }
    return false;
}

5.3 路径A: Eagle客户端拖拽流程

此路径用于处理从Eagle客户端拖拽素材的场景,流程相对直接:

  1. 触发: isEagleDrag 返回 true
  2. 执行: handleEagleDragImport() 函数被调用。
  3. 解析: 该函数从 dataTransfer 中获取 eagle:// 协议的URL,并调用 parseEagleUrl() 将URL中的数据解析成一个包含文件完整信息(路径、元数据等)的数组。
  4. 汇合: 将解析出的文件数组打包成一个标准消息对象,然后直接调用 handleImportFiles(),进入通用的导入处理流水线。

5.4 路径B: 操作系统拖拽流程

此路径用于处理用户从桌面或文件管理器直接拖拽文件或文件夹的场景,流程更为复杂:

  1. 触发: isEagleDrag 返回 false
  2. 执行: handleDirectoryDrop() (或类似的) 函数被调用。
  3. 文件读取: 使用 item.webkitGetAsEntry() API 遍历所有拖入的项目,如果是文件夹,则递归读取其中所有的文件。
  4. 分析与检测: 所有文件被收集后,传递给 analyzeDroppedFiles() 函数。此函数会:
    • 对文件进行分类(图像、视频等)。
    • 调用 detectImageSequence() 对图像文件进行序列帧检测
  5. 用户确认: 分析结果会通过 showFileImportDialog() 以一个自定义的HTML对话框呈现给用户,让用户选择导入方式(例如“作为序列导入”或“作为独立文件导入”)。
  6. 汇合: 根据用户的选择,将相应的文件列表打包成一个标准消息对象,然后调用 handleImportFiles(),进入通用的导入处理流水线。

5.5 流程汇合与委托: handleImportFiles

handleImportFiles 是所有拖拽导入方式的最终汇合点。它的核心职责是:

  1. 执行导入前的预检,例如检查当前AE中是否有活动的合成。
  2. SettingsManager 中获取最新的导入设置。
  3. 将文件列表和设置参数,统一委托给 FileHandler.js 服务模块进行下一步的重度操作。

5.6 核心处理器: FileHandler.js

FileHandler 是实际执行文件操作的“工人”。在收到 handleImportFiles 的任务后,它会:

  1. 模式路由: 调用 processFilesByMode(),根据用户的导入模式(direct, project_adjacent, custom_folder)选择不同的处理路径。

  2. 文件操作: 如果导入模式需要,FileHandler 会负责将源文件复制到目标位置(例如项目文件旁的 Eagle_Assets 文件夹)。

  3. 调用JSX: 当所有文件在物理磁盘上准备就绪后,FileHandler 会调用 importFilesToAE(),将最终有效的文件路径列表和相关设置打包成一个大型JSON对象,通过 evalScript 传递给 hostscript.jsx 中的 importFilesWithSettings 函数,完成在AE项目中的最终导入。 "/g, '\"'); const escapedMessage = message.replace(/"/g, '\"').replace(/\n/g, '\n');

    // 构建ExtendScript调用 const confirmScript = showPanelConfirmDialog("${escapedTitle}", "${escapedMessage}");

    return new Promise((resolve) => { csInterface.evalScript(confirmScript, (result) => { // 解析结果:0表示确认,1表示取消 const confirmed = parseInt(result) === 0; resolve(confirmed); }); }); }

// ExtendScript端实现 (dialog-warning.jsx) function showPanelConfirmDialog(title, message, button1Text, button2Text) { try { // 创建Panel样式对话框 var dialog = new Window("dialog", title || "确认"); dialog.orientation = "column"; dialog.alignChildren = "fill"; dialog.spacing = 10; dialog.margins = 16;

    // 添加消息文本
    var messageGroup = dialog.add("group");
    messageGroup.orientation = "column";
    messageGroup.alignChildren = "left";
    
    var messageText = messageGroup.add("statictext", undefined, message || "请确认操作", {multiline: true});
    messageText.preferredSize.width = 350;
    
    // 添加按钮组
    var buttonGroup = dialog.add("group");
    buttonGroup.alignment = "center";
    buttonGroup.spacing = 10;
    
    var confirmBtn = buttonGroup.add("button", undefined, button1Text || "继续导入");
    var cancelBtn = buttonGroup.add("button", undefined, button2Text || "取消");
    
    // 设置按钮事件
    confirmBtn.onClick = function() {
        dialog.close(0); // 返回0表示确认
    };
    
    cancelBtn.onClick = function() {
        dialog.close(1); // 返回1表示取消
    };
    
    // 设置默认按钮和键盘快捷键
    confirmBtn.active = true;
    dialog.defaultElement = confirmBtn;
    dialog.cancelElement = cancelBtn;
    
    // 显示对话框并返回结果
    return dialog.show();
    
} catch (error) {
    // 如果Panel创建失败,降级到原生confirm
    return confirm(message || "请确认操作") ? 0 : 1;
}

}


---

## 6. CEP环境兼容性

### 6.1 CEP环境特点

After Effects的扩展基于Adobe Common Extensibility Platform(CEP),使用内嵌的WebKit引擎渲染HTML界面。与标准Web浏览器相比,CEP环境有以下特点:

1. **WebKit版本限制**:
   - CEP使用的WebKit版本相对较旧
   - 对现代CSS特性支持有限
   - 需要使用 `-webkit-` 前缀

2. **CSS Flexbox支持**:
   - 标准flexbox属性可能不工作
   - 需要添加 `-webkit-` 前缀
   - 某些flexbox特性不支持

3. **样式继承机制**:
   - 样式继承行为可能与标准浏览器不同
   - 需要显式设置样式优先级
   - 使用 `!important` 确保样式生效

### 6.2 CEP兼容性修复策略

#### 6.2.1 Flexbox布局修复

```css
/* 标准Web环境 */
.horizontal-container {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
}

/* CEP兼容性修复 */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
    .horizontal-container {
        display: -webkit-flex !important;
        -webkit-flex-direction: row !important;
        -webkit-flex-wrap: nowrap !important;
        -webkit-align-items: center !important;
        -webkit-justify-content: center !important;
    }
}

6.2.2 水平布局组件修复

css
/* 导入模式选项组 */
.import-mode-options-horizontal {
    display: -webkit-flex;
    -webkit-flex-direction: row;
    -webkit-align-items: stretch;
    gap: 8px;
}

/* 导入行为选项组 */
.import-behavior-options-horizontal {
    display: -webkit-flex;
    -webkit-flex-direction: row;
    gap: 8px;
}

/* 复选框组 */
.checkbox-group-horizontal {
    display: -webkit-flex;
    -webkit-flex-direction: row;
    -webkit-flex-wrap: nowrap;
    -webkit-align-items: flex-start;
    gap: 8px;
}

6.2.3 选项组件修复

css
/* 选项组件 */
.import-mode-option,
.import-behavior-option,
.checkbox-option {
    display: -webkit-flex;
    -webkit-align-items: center;
    -webkit-justify-content: center;
    gap: 6px;
    cursor: pointer;
    padding: 6px 8px;
    border-radius: 4px;
    transition: all 0.2s ease;
}

6.3 CEP兼容性最佳实践

6.3.1 使用媒体查询隔离CEP修复

css
/* 只在CEP环境中应用修复 */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
    /* CEP特定的样式修复 */
    .horizontal-container {
        display: -webkit-flex !important;
    }
}

6.3.2 添加必要的CSS前缀

css
/* 为所有flexbox属性添加前缀 */
.flex-container {
    display: flex;
    display: -webkit-flex;
    
    flex-direction: row;
    -webkit-flex-direction: row;
    
    align-items: center;
    -webkit-align-items: center;
    
    justify-content: center;
    -webkit-justify-content: center;
}

6.3.3 使用box-sizing控制布局

css
/* 确保box-sizing一致性 */
.cep-compatible-container {
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
}

6.3.4 测试CEP兼容性

javascript
// 检测是否在CEP环境中运行
function isCEPEnvironment() {
    return typeof window.__adobe_cep__ !== 'undefined';
}

// 根据环境应用不同的样式
if (isCEPEnvironment()) {
    document.body.classList.add('cep-environment');
}

6.4 CEP兼容性检查清单

  • [ ] 所有flexbox容器都添加了 -webkit- 前缀
  • [ ] 使用媒体查询隔离CEP修复
  • [ ] 测试水平布局在CEP中是否正常
  • [ ] 验证复选框和选项组件的样式
  • [ ] 检查滚动条样式是否正确显示
  • [ ] 测试在不同AE版本中的兼容性
  • [ ] 验证响应式布局在CEP中的效果

7. UI优化和视觉效果

7.1 独显模式优化

7.1.1 独显模式特点

独显模式是指扩展面板占据整个AE窗口空间的模式,在这种模式下:

  1. 空间最大化

    • 移除所有不必要的边距和间距
    • 最大化内容显示区域
    • 提供最紧凑的布局
  2. 背景一致性

    • 使用深灰色背景 #2a2a2a
    • 保持与整体界面的一致性
    • 避免透明背景导致的显示异常
  3. 响应式设计

    • 根据屏幕尺寸自动调整布局
    • 在极小屏幕下优化显示
    • 保持可用性和美观性

7.1.2 独显模式CSS优化

css
/* 独显模式容器 */
.content.fullscreen-mode {
    padding: 0;
    margin: 0;
    height: 100vh;
    box-sizing: border-box;
    width: 100vw;
    gap: 0;
}

/* 导入模式区域 */
.content.fullscreen-mode #import-mode-section {
    display: flex !important;
    flex-direction: column;
    justify-content: space-between;
    gap: 0;  /* 消除垂直间距 */
    flex: 1;
    min-height: 0;
    background: #2a2a2a;  /* 深灰色背景 */
    border: none;
    padding: 0;
    height: 100%;
    width: 100%;
}

/* 按钮组容器 */
.content.fullscreen-mode #import-mode-section .import-mode-selection,
.content.fullscreen-mode #import-mode-section .import-behavior,
.content.fullscreen-mode #import-mode-section .layer-operations {
    width: 100%;
    height: 100%;
    min-height: 24px;
    justify-content: space-between;
}

/* 按钮样式 */
.content.fullscreen-mode #import-mode-section .mode-button,
.content.fullscreen-mode #import-mode-section .import-behavior-button,
.content.fullscreen-mode #import-mode-section .layer-operation-button {
    flex: 1;
    height: 100%;
    min-height: 24px;
    font-size: clamp(10px, 1.5vh, 14px);
    padding: 2px 4px;  /* 最小内边距 */
}

7.1.3 响应式优化

css
/* 极小屏幕优化 */
@media (max-height: 500px) {
    .content.fullscreen-mode #import-mode-section {
        gap: 0;
        padding: 0;
    }

    .content.fullscreen-mode #import-mode-section .import-mode-selection,
    .content.fullscreen-mode #import-mode-section .import-behavior,
    .content.fullscreen-mode #import-mode-section .layer-operations {
        min-height: 20px;
        justify-content: flex-start;
    }

    .content.fullscreen-mode #import-mode-section .mode-buttons,
    .content.fullscreen-mode #import-mode-section .import-behavior-buttons,
    .content.fullscreen-mode #import-mode-section .layer-operation-buttons {
        gap: 2px;
        min-height: 20px;
    }

    .content.fullscreen-mode #import-mode-section .mode-button,
    .content.fullscreen-mode #import-mode-section .import-behavior-button,
    .content.fullscreen-mode #import-mode-section .layer-operation-button {
        min-height: 20px;
        font-size: 10px;
        padding: 2px 4px;
    }
}

/* 更小屏幕优化 */
@media (max-height: 350px) {
    .content.fullscreen-mode #import-mode-section {
        gap: 0;
        padding: 0;
    }

    .content.fullscreen-mode #import-mode-section .mode-buttons,
    .content.fullscreen-mode #import-mode-section .import-behavior-buttons,
    .content.fullscreen-mode #import-mode-section .layer-operation-buttons {
        gap: 1px;
    }

    .content.fullscreen-mode #import-mode-section .mode-button,
    .content.fullscreen-mode #import-mode-section .import-behavior-button,
    .content.fullscreen-mode #import-mode-section .layer-operation-button {
        min-height: 18px;
        font-size: 9px;
        padding: 1px 3px;
    }
}

7.2 拟态滚动条样式

7.2.1 拟态滚动条设计理念

拟态滚动条(Neumorphic Scrollbar)是一种现代UI设计风格,具有以下特点:

  1. 视觉细腻

    • 6px宽度的精细滚动条
    • 渐变背景和阴影效果
    • 微妙的视觉反馈
  2. 交互友好

    • 悬停时颜色变亮
    • 激活时颜色变暗
    • 平滑的过渡动画
  3. 主题适配

    • 支持深色和浅色主题
    • 与界面风格协调
    • 增强整体美感

7.2.2 深色主题滚动条

css
/* 深色主题拟态滚动条 */
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

::-webkit-scrollbar-track {
    background: rgba(42, 42, 42, 0.8);
    border-radius: 3px;
}

::-webkit-scrollbar-thumb {
    background: linear-gradient(180deg, #4a4a4a 0%, #3a3a3a 100%);
    border-radius: 3px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.1),
                0 1px 2px rgba(0, 0, 0, 0.3);
}

::-webkit-scrollbar-thumb:hover {
    background: linear-gradient(180deg, #5a5a5a 0%, #4a4a4a 100%);
    border-color: rgba(255, 255, 255, 0.2);
}

::-webkit-scrollbar-thumb:active {
    background: linear-gradient(180deg, #3a3a3a 0%, #2a2a2a 100%);
}

::-webkit-scrollbar-corner {
    background: transparent;
}

7.2.3 浅色主题滚动条

css
/* 浅色主题拟态滚动条 */
html.theme-light ::-webkit-scrollbar-track {
    background: rgba(255, 255, 255, 0.8);
    border-radius: 3px;
}

html.theme-light ::-webkit-scrollbar-thumb {
    background: linear-gradient(180deg, #e0e0e0 0%, #d0d0d0 100%);
    border-radius: 3px;
    border: 1px solid rgba(0, 0, 0, 0.1);
    box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.8),
                0 1px 2px rgba(0, 0, 0, 0.1);
}

html.theme-light ::-webkit-scrollbar-thumb:hover {
    background: linear-gradient(180deg, #d0d0d0 0%, #c0c0c0 100%);
    border-color: rgba(0, 0, 0, 0.2);
}

html.theme-light ::-webkit-scrollbar-thumb:active {
    background: linear-gradient(180deg, #c0c0c0 0%, #b0b0b0 100%);
}

7.3 UI性能优化

7.3.1 减少重绘和重排

css
/* 使用transform代替position变化 */
.animated-element {
    transform: translateX(0);
    transition: transform 0.3s ease;
}

.animated-element:hover {
    transform: translateX(10px);
}

/* 使用opacity代替display切换 */
.fade-element {
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.fade-element.visible {
    opacity: 1;
    pointer-events: auto;
}

7.3.2 使用GPU加速

css
/* 启用GPU加速 */
.gpu-accelerated {
    transform: translateZ(0);
    will-change: transform;
}

/* 复杂动画使用GPU加速 */
.animated-container {
    transform: translateZ(0);
    backface-visibility: hidden;
    perspective: 1000px;
}

7.3.3 优化渲染性能

css
/* 减少阴影计算 */
.optimized-shadow {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* 使用单层阴影代替多层 */
.complex-shadow {
    box-shadow: 
        0 1px 3px rgba(0, 0, 0, 0.12),
        0 1px 2px rgba(0, 0, 0, 0.24);
}

8. 测试和验证

8.1 跨环境测试

8.1.1 Web环境测试

javascript
// 测试工具函数
function testWebEnvironment() {
    const tests = {
        'Flexbox布局': testFlexboxLayout(),
        '滚动条样式': testScrollbarStyles(),
        '响应式布局': testResponsiveLayout(),
        '动画效果': testAnimations()
    };
    
    return tests;
}

function testFlexboxLayout() {
    const container = document.querySelector('.horizontal-container');
    const computedStyle = window.getComputedStyle(container);
    
    return {
        display: computedStyle.display,
        flexDirection: computedStyle.flexDirection,
        alignItems: computedStyle.alignItems,
        justifyContent: computedStyle.justifyContent
    };
}

8.1.2 CEP环境测试

javascript
// 测试CEP环境兼容性
function testCEPCompatibility() {
    if (typeof window.__adobe_cep__ === 'undefined') {
        return { error: 'Not in CEP environment' };
    }
    
    const tests = {
        'Flexbox布局': testCEPFlexbox(),
        '水平排列': testHorizontalLayout(),
        '复选框样式': testCheckboxStyles(),
        '按钮布局': testButtonLayout()
    };
    
    return tests;
}

function testCEPFlexbox() {
    const container = document.querySelector('.import-mode-options-horizontal');
    const computedStyle = window.getComputedStyle(container);
    
    return {
        display: computedStyle.display,
        webkitDisplay: computedStyle.webkitDisplay,
        flexDirection: computedStyle.flexDirection,
        webkitFlexDirection: computedStyle.webkitFlexDirection
    };
}

8.2 视觉回归测试

8.2.1 截图对比

javascript
// 视觉回归测试工具
class VisualRegressionTester {
    constructor() {
        this.baselineImages = new Map();
        this.currentImages = new Map();
    }
    
    async captureScreenshot(element, testName) {
        try {
            // 使用html2canvas截图
            const canvas = await html2canvas(element);
            this.currentImages.set(testName, canvas.toDataURL());
            return canvas.toDataURL();
        } catch (error) {
            console.error('截图失败:', error);
            return null;
        }
    }
    
    async compareImages(testName) {
        const baseline = this.baselineImages.get(testName);
        const current = this.currentImages.get(testName);
        
        if (!baseline || !current) {
            return { error: 'Missing baseline or current image' };
        }
        
        // 使用pixelmatch库对比图片
        const baselineImg = new Image();
        const currentImg = new Image();
        
        baselineImg.src = baseline;
        currentImg.src = current;
        
        await Promise.all([
            new Promise(resolve => baselineImg.onload = resolve),
            new Promise(resolve => currentImg.onload = resolve)
        ]);
        
        const diff = pixelmatch(
            baselineImg.data, 
            currentImg.data, 
            null, 
            baselineImg.width, 
            baselineImg.height,
            { threshold: 0.1 }
        );
        
        return {
            differentPixels: diff,
            totalPixels: baselineImg.width * baselineImg.height,
            percentage: (diff / (baselineImg.width * baselineImg.height)) * 100
        };
    }
}

8.2.2 自动化测试

javascript
// 自动化UI测试套件
class UITestSuite {
    constructor() {
        this.tests = [];
        this.results = [];
    }
    
    addTest(name, testFunction) {
        this.tests.push({ name, testFunction });
    }
    
    async runTests() {
        for (const test of this.tests) {
            try {
                const result = await test.testFunction();
                this.results.push({
                    name: test.name,
                    status: 'passed',
                    result
                });
            } catch (error) {
                this.results.push({
                    name: test.name,
                    status: 'failed',
                    error: error.message
                });
            }
        }
        
        return this.results;
    }
    
    generateReport() {
        const passed = this.results.filter(r => r.status === 'passed').length;
        const failed = this.results.filter(r => r.status === 'failed').length;
        
        return {
            total: this.results.length,
            passed,
            failed,
            successRate: (passed / this.results.length) * 100,
            details: this.results
        };
    }
}

9. 最佳实践总结

9.1 开发最佳实践

  1. 渐进增强

    • 从标准Web环境开始开发
    • 逐步添加CEP兼容性修复
    • 使用媒体查询隔离环境特定代码
  2. 测试驱动

    • 在多个环境中测试UI
    • 使用自动化测试工具
    • 定期进行视觉回归测试
  3. 性能优先

    • 优化CSS性能
    • 减少重绘和重排
    • 使用GPU加速
  4. 可维护性

    • 保持代码清晰
    • 添加详细注释
    • 使用有意义的类名

9.2 用户体验最佳实践

  1. 一致性

    • 保持界面风格一致
    • 统一交互模式
    • 标准化视觉反馈
  2. 响应性

    • 优化加载性能
    • 提供即时反馈
    • 平滑的动画效果
  3. 可访问性

    • 支持键盘导航
    • 提供文本替代
    • 确保足够的对比度
  4. 适应性

    • 支持不同屏幕尺寸
    • 适应不同主题
    • 兼容不同浏览器

9.3 CEP环境特定最佳实践

  1. 兼容性优先

    • 使用 -webkit- 前缀
    • 避免使用实验性特性
    • 提供降级方案
  2. 性能优化

    • 减少DOM操作
    • 优化事件处理
    • 使用虚拟滚动
  3. 调试友好

    • 添加详细日志
    • 提供调试工具
    • 支持远程调试
  4. 文档完善

    • 记录已知问题
    • 提供解决方案
    • 更新最佳实践

Released under the MIT License.