资讯动态

Tauri2透明窗口文件拖拽实现:从事件捕获到路径解析全流程

发布时间:2026/9/2 13:56:44 来源:尧图企业网站定制
简介本资源面向Tauri 2桌面应用开发者聚焦解决Web技术栈下文件拖拽无法获取真实系统路径的核心痛点。通过创新性引入透明辅助子窗口拦截系统级拖拽事件实现跨平台Windows/macOS/Linux文件完整路径精准解析在保留HTML5标准拖拽交互体验的同时突破框架限制。资源包共61个文件涵盖6个Vue前端组件含drop_app.vue、drop_dom.vue等、6个Rust后端模块main.rs、drop.rs等、4个JSON配置drop-window.json等、6个PNG图标及配套文档与构建脚本整体仅273KB结构清晰、开箱即用。已有136人学习下载提供完整可运行示例、多窗口协同逻辑说明、事件传播链路控制要点及平台差异化适配细节助开发者快速集成高兼容性文件拖放能力。1. 项目概述当透明窗口遇上文件拖拽最近在折腾一个基于Tauri2的桌面应用里面有个挺有意思的需求主窗口是常规的但需要弹出一个完全透明的子窗口悬浮在主界面上方用来接收用户从系统文件管理器直接拖拽进来的文件然后解析出这些文件的完整路径。听起来好像就是监听个drop事件那么简单真上手了才发现这里面的坑一个接一个。透明窗口本身不渲染任何DOM元素事件传递机制和常规窗口有微妙差异文件拖拽事件在透明区域上的捕获不同操作系统Windows、macOS、Linux的表现还不完全一致更别提从事件对象里安全、准确地提取出跨平台兼容的文件系统路径了。这可不是简单的event.dataTransfer.files就能搞定的事儿尤其是在Tauri2这个旨在替代Electron的轻量级框架下需要同时处理好前端Rust核心与后端Web视图之间的交互。这个技术点对于那些需要实现“拖拽即用”功能的桌面应用特别关键比如设计素材管理工具、代码编辑器快速导入、或者任何需要用户频繁从桌面或文件夹中拖入文件的场景。如果你也在用Tauri2并且被透明窗口、无边框窗口的文件拖拽问题困扰过那这篇从踩坑到填坑的实录应该能给你省下不少折腾的时间。2. 核心思路与架构设计拆解2.1 为什么选择Tauri2与透明子窗口方案首先得聊聊为什么是Tauri2。相比ElectronTauri2最大的优势是应用体积小和内存占用低因为它使用系统自带的WebView在Windows上是WebView2macOS是WKWebViewLinux上通常是WebKitGTK而不是捆绑一个完整的Chromium。这对于追求原生体验和性能的桌面应用来说是巨大的吸引力。我们选择透明子窗口通常是为了实现一些特殊的UI效果比如一个只在拖拽文件时才出现的、半透明或全透明的“接收区”这个区域可能完全自定义形状不影响主窗口的视觉布局。然而透明窗口在大多数操作系统中其事件处理层和渲染层是解耦的。窗口可以透明但接收鼠标和拖拽事件的“命中测试”区域需要精确定义。一个完全透明且没有定义任何可交互元素的区域系统可能会认为它“不可见”或“不可交互”从而导致拖拽事件直接穿透根本触发不了我们的监听器。这就是第一个要解决的核心问题如何让一个视觉上透明的窗口在逻辑上变成一个能够稳稳接住文件的“篮子”。2.2 整体事件流与数据传递设计整个技术方案的核心链路可以概括为系统级拖拽事件 - 透明窗口命中测试 - 前端WebView事件捕获 - 路径信息提取 - 跨进程通信前端至Rust核心 - Rust核心路径验证与处理。系统层当用户从文件管理器拖拽一个或多个文件进入我们的应用窗口范围时操作系统会首先进行命中测试判断鼠标位置下的窗口是否接受拖拽。窗口层我们需要确保透明窗口被设置为接受文件拖拽例如在Windows上需要特定的窗口样式标志。WebView层拖拽事件成功进入窗口后会被转发到内部的WebView。我们需要在前端可能是React、Vue、Svelte或纯HTML/JS监听dragover和drop事件。数据处理层在drop事件的处理函数中我们不能直接使用浏览器环境下的File对象或路径因为前端代码运行在沙盒中无法直接访问真实的文件系统路径。这里需要借助Tauri提供的API。Rust核心层前端通过Tauri的指令invoke调用后端的Rust函数将文件信息通常是URI列表或特殊格式的路径字符串传递过去。Rust端负责解析这些信息并将其转换为标准的、跨平台的std::path::PathBuf同时进行安全性检查如路径是否存在、是否在允许的目录内。这个链条中透明窗口的“事件接收能力”和“前端至Rust的路径解析”是两个最脆弱的环节。注意一个常见的误解是在透明窗口的CSS里设置background: transparent就够了。实际上你很可能还需要在创建窗口的Rust配置中显式启用文件拖放支持并可能需要对窗口的某些区域进行“点击穿透”或“事件拦截”的精细控制。3. 透明子窗口的关键配置与事件捕获3.1 创建可接收拖拽的透明窗口一切始于窗口的创建。在Tauri2的Rust端通常是src-tauri/src/main.rs或src-tauri/src/lib.rs我们需要使用tauri::WindowBuilder来配置子窗口。use tauri::{Manager, WindowBuilder}; fn create_transparent_drop_window(app: tauri::AppHandle) - tauri::Result() { let window WindowBuilder::new( app, file_drop_window, // 窗口唯一标识 tauri::WindowUrl::App(/drop-zone.html.into()) // 指向一个专门的HTML页面 ) .title(Drop Zone) .inner_size(400.0, 300.0) .decorations(false) // 无边框对于透明窗口通常是必须的 .transparent(true) // 启用窗口透明 .shadow(false) // 可选关闭阴影可能使透明效果更干净 .always_on_top(true) // 可选让窗口始终悬浮在最前 .visible(false) // 初始不可见需要时再显示 .build()?; // **关键配置**启用文件拖放支持并非所有平台默认开启 // 这行代码告诉操作系统这个窗口是一个有效的文件拖放目标。 #[cfg(any(target_os windows, target_os macos))] window.set_drop_zone_enabled(true)?; // 这是一个简化示例具体API名称可能随Tauri版本变化 Ok(()) }这里有几个要点decorations(false)和transparent(true)这对组合是创建视觉透明无边框窗口的典型方式。set_drop_zone_enabled(或类似API)这是最容易被忽略但至关重要的一步。仅仅窗口透明且无边框并不代表它自动具备了接收文件拖拽的能力。在某些平台尤其是Windows上你需要显式地调用一个方法来注册窗口为拖放目标。Tauri的API可能会封装这个方法具体名称需要查阅对应版本的文档。如果找不到可能需要深入tauricrate的Windowtrait来寻找关于drag_and_drop或drop_zone的配置项。专门的HTML页面为这个透明窗口单独创建一个HTML入口点如drop-zone.html是个好习惯。这个页面可以只包含必要的脚本和样式甚至只有一个div作为事件监听容器保持轻量。3.2 前端事件监听与基础防御在透明窗口对应的Web页面例如drop-zone.html中我们需要编写前端代码来捕获事件。首先确保你的容器元素覆盖整个窗口并且能够接收事件。!DOCTYPE html html head style html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: transparent !important; /* 确保背景透明 */ } #drop-overlay { width: 100%; height: 100%; /* 可以加一个极低透明度的背景或边框用于调试事件区域 */ /* background: rgba(0, 255, 0, 0.01); */ display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; } /style /head body div iddrop-overlay 拖拽文件到此区域 /div script srcyour-frontend-entry.js/script /body /html接着在JavaScript中监听关键事件const dropZone document.getElementById(drop-overlay); // 必须阻止 dragover 和 drop 事件的默认行为否则浏览器会尝试打开文件 dropZone.addEventListener(dragover, (event) { event.preventDefault(); event.stopPropagation(); // 可以在这里改变UI反馈比如改变边框颜色 dropZone.style.border 2px dashed #4CAF50; }); dropZone.addEventListener(dragleave, (event) { event.preventDefault(); event.stopPropagation(); // 恢复UI dropZone.style.border none; }); dropZone.addEventListener(drop, async (event) { event.preventDefault(); event.stopPropagation(); dropZone.style.border none; console.log(文件被拖拽进来了); // 核心处理拖拽的文件数据 if (event.dataTransfer event.dataTransfer.items) { const items event.dataTransfer.items; const filePaths []; // 遍历拖拽项 for (let i 0; i items.length; i) { const item items[i]; // 我们只关心文件类型 if (item.kind file) { // **关键点**对于本地文件系统拖拽entry 可能是 FileSystemFileEntry // 但更通用且直接的方式是使用 Tauri 提供的 API const file item.getAsFile(); // 注意在前端你无法直接获取到文件的真实完整路径C:\Users\...。 // file.path 在浏览器沙盒环境中通常是 undefined 或只有文件名。 console.log(前端获取到的文件对象:, file.name, file.size, file.type); // 我们需要将文件对象或信息传递给 Rust 侧来处理路径 filePaths.push(file); // 这里先存File对象实际传递需要转换 } } if (filePaths.length 0) { // 调用 Tauri 后端命令来处理这些文件 // 这里需要将文件信息传递过去而不是路径 await handleDroppedFiles(filePaths); } } }); async function handleDroppedFiles(files) { // 这里需要与 Rust 通信 // 方案A如果文件不大可以转为ArrayBuffer/Uint8Array传递不推荐大文件 // 方案B推荐传递文件的“引用”或URI让Rust去读取路径。这需要依赖Tauri的特殊API。 console.log(准备将文件信息发送到Rust后端处理); }到这一步前端已经能感知到文件拖拽进来了但尴尬的是我们拿不到那个至关重要的完整系统路径。file.path在Web沙盒环境下基本不可用。这就是为什么必须让Rust后端介入。4. 从事件到路径Rust后端的深度解析4.1 Tauri 指令与文件信息传递前端无法直接获取路径但操作系统在发起拖拽时是知道文件路径的。Tauri作为桥梁提供了让Rust后端访问这些信息的通道。不过Tauri本身并没有一个开箱即用的“on_drop”事件直接给你路径。我们需要用一点技巧。一种常见且相对稳定的方法是前端在drop事件中从event.dataTransfer里提取出DataTransferItem的webkitGetAsEntry或直接使用getAsFileSystemHandle较新API但这些API兼容性不一且得到的依然是浏览器沙盒内的对象。更实用的方案是利用Tauri的“拖放”事件监听这个监听发生在Rust层早于前端事件。Tauri的Window对象可以监听tauri::WindowEvent::FileDrop事件。这个事件在文件被拖放到窗口时由操作系统直接触发并携带了文件的路径列表。让我们修改Rust端的窗口创建和事件处理逻辑use tauri::{Manager, Window, WindowEvent}; use std::path::PathBuf; fn create_transparent_drop_window(app: tauri::AppHandle) - tauri::Result() { let window WindowBuilder::new( app, file_drop_window, tauri::WindowUrl::App(/drop-zone.html.into()) ) .transparent(true) .decorations(false) .build()?; // 监听窗口事件 let window_ window.clone(); // 克隆引用用于闭包 window.on_window_event(move |event| { match event { WindowEvent::FileDrop(file_drop_event) { match file_drop_event { tauri::FileDropEvent::Hovered { paths, .. } { // 文件在窗口上悬停 println!(文件悬停在窗口上: {:?}, paths); // 可以在这里通知前端更新UI比如高亮 let _ window_.emit(drop-hover, paths.clone()); } tauri::FileDropEvent::Dropped { paths, .. } { // **文件被放下这是我们最关心的时刻** println!(文件被放下路径: {:?}, paths); // 处理路径验证、解析、传递给业务逻辑 handle_dropped_file_paths(paths); // 通知前端处理完成可选 let _ window_.emit(drop-complete, paths); } tauri::FileDropEvent::Cancelled { println!(文件拖放取消); let _ window_.emit(drop-cancelled, ()); } _ {} } } _ {} } }); Ok(()) } fn handle_dropped_file_paths(paths: [PathBuf]) { for path in paths { if path.exists() path.is_file() { println!(处理文件: {}, path.display()); // 你的业务逻辑读取文件、记录到数据库、触发其他操作等 // 例如将路径发送给前端渲染列表如果需要 } else { eprintln!(路径无效或不是文件: {}, path.display()); } } }这才是获取文件系统路径的正确姿势WindowEvent::FileDrop::Dropped事件中的paths就是一个VecPathBuf包含了被拖拽文件在操作系统上的完整路径。这个事件是由系统原生拖放API触发的绕过了浏览器沙盒的限制。4.2 路径解析、验证与安全处理拿到PathBuf之后事情还没完。我们不能盲目信任这个路径必须进行验证和安全处理。存在性与类型检查如上例所示使用path.exists()和path.is_file()或is_dir()来确保路径有效且符合预期。规范化路径不同操作系统路径分隔符不同\vs/。使用std::fs::canonicalize可以获取绝对路径并解析符号链接但它要求路径必须存在。一个更安全的方式是使用dunce::canonicalize需要引入duncecrate它能更好地处理Windows上的UNC路径。安全边界检查如果你的应用只允许操作特定目录如用户文档目录、下载目录需要检查解析后的路径是否在允许的范围内防止用户拖拽系统关键文件造成意外。处理非法字符和超长路径特别是在Windows上路径长度限制和非法字符是需要考虑的问题。use std::path::{Path, PathBuf}; fn safe_process_path(raw_path: Path) - OptionPathBuf { // 1. 检查存在性 if !raw_path.exists() { eprintln!(路径不存在: {}, raw_path.display()); return None; } // 2. 尝试规范化路径解析...和符号链接 let canonical_path dunce::canonicalize(raw_path).ok()?; // 3. 安全检查示例限制在用户家目录下 #[cfg(target_os windows)] let user_profile std::env::var(USERPROFILE).ok()?; #[cfg(not(target_os windows))] let user_profile std::env::var(HOME).ok()?; let allowed_root PathBuf::from(user_profile); if !canonical_path.starts_with(allowed_root) { eprintln!(安全警告尝试访问非允许目录: {}, canonical_path.display()); return None; } // 4. 返回安全的、规范化的路径 Some(canonical_path) }4.3 前后端协同事件同步与状态管理现在我们有两条事件流Rust后端通过WindowEvent::FileDrop获取到真实路径。前端WebView通过drop事件感知到拖拽动作但拿不到路径。我们需要让它们协同工作。通常有两种模式模式一Rust主导Rust在收到FileDrop::Dropped事件后直接处理路径如读取文件内容、更新数据库然后通过window.emit向前端发送一个自定义事件附带处理结果如文件列表的元数据。前端只需要监听这个自定义事件来更新UI。这种模式逻辑清晰数据流单向。// Rust端处理完路径后 let _ window.emit(files-processed, serde_json::json!({ files: file_metadata_list, // 你处理后的文件信息数组 }));// 前端监听 import { listen } from tauri-apps/api/event; listen(files-processed, (event) { const files event.payload; // 更新UI显示拖拽成功的文件列表 updateFileList(files); });模式二前后端握手前端在drop事件中先改变UI状态如显示加载动画然后主动调用一个Tauri命令Command请求Rust端“获取刚才拖拽的文件”。Rust端需要维护一个临时状态存储最近一次FileDrop事件的路径当收到前端命令时返回。这种模式更符合前端驱动的思维但需要处理竞态快速连续拖拽。// Rust端存储状态 use std::sync::Mutex; struct AppState { last_dropped_files: MutexVecPathBuf, } // 在 FileDrop::Dropped 事件中 *state.last_dropped_files.lock().unwrap() paths.clone(); // 暴露一个命令给前端 #[tauri::command] fn get_last_dropped_files(state: tauri::StateAppState) - ResultVecString, String { let paths state.last_dropped_files.lock().unwrap(); Ok(paths.iter().map(|p| p.to_string_lossy().into_owned()).collect()) }// 前端 drop 事件最后 async function handleDroppedFiles() { showLoading(); try { const filePaths await invoke(get_last_dropped_files); // 使用 filePaths 更新UI updateUI(filePaths); } catch (error) { console.error(获取文件路径失败:, error); } finally { hideLoading(); } }我个人更推荐模式一因为它更直接避免了状态管理的复杂性且事件触发的顺序是确定的。5. 跨平台差异与疑难问题排查5.1 Windows、macOS与Linux上的不同表现Windows拖放注册如前所述可能需要显式调用set_drop_zone_enabled或类似API。如果遇到拖拽无效这是首要检查点。路径格式路径使用反斜杠\且可能包含驱动器号C:\。使用dunce::canonicalize能很好地处理。管理员权限如果应用以管理员权限运行而拖拽来自非管理员权限的资源管理器某些安全策略可能会阻止拖放操作。这是一个深坑通常需要调整应用清单或避免请求过高权限。macOS通常对透明窗口和拖放的支持较好。需要注意Info.plist中是否启用了相应的权限但纯文件拖放一般不需要。文件类型统一标识符如果你需要过滤特定类型的文件可能需要处理Uniform Type Identifiers。路径以/开头。Linux (GTK/Wayland)情况最复杂。不同的桌面环境GNOME, KDE和显示服务器X11, Wayland行为可能不同。在Wayland下拖放协议XDG Drag and Drop与X11不同需要确保Tauri底层使用的WebKitGTK或其它WebView组件支持良好。常见问题在透明无边框窗口上鼠标事件穿透可能导致拖拽事件不稳定。有时需要设置窗口的“模态”或“弹出”属性来确保它能捕获事件。5.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案拖拽文件到窗口上无任何反应光标不变1. 窗口未启用文件拖放支持。2. 透明区域被系统视为“不可交互”。3. (Linux) Wayland会话下的兼容性问题。1.检查Rust配置确认创建窗口时已调用启用拖放的方法如window.set_drag_and_drop(true)具体查Tauri API。2.前端调试给透明区域加一个极浅的背景色如rgba(0,0,0,0.01)看是否能被鼠标事件触发。确保dragover事件监听器已绑定到覆盖全屏的元素。3.Linux特定尝试在X11会话下运行。检查Tauri和WebKitGTK版本。前端drop事件能触发但event.dataTransfer.files为空或只有文件名浏览器沙盒限制无法通过前端API获取完整路径。这是正常现象不是bug。必须切换到使用Rust端的WindowEvent::FileDrop事件来获取路径。放弃从前端直接读取路径的想法。Rust端FileDrop事件未触发1. 事件监听器未正确注册或窗口引用丢失。2. 拖放的文件类型被系统或Tauri过滤。1. 确保window.on_window_event回调被正确设置并且窗口实例在回调闭包的生命周期内有效。2. 检查WindowBuilder是否有过滤文件类型的设置如allowed_drop_types。默认应接受所有文件。拖拽时窗口出现黑色或白色背景闪烁透明窗口渲染问题。1. 检查CSS确保html, body和顶层容器元素的background设置为transparent并且没有意外的背景色或图片。2. 尝试设置窗口的transparent: true的同时也设置decorations: false。3. 某些平台可能需要额外的窗口样式标志来优化透明渲染。拖拽操作导致应用崩溃1. Rust端路径处理出现panic如访问不存在的路径。2. 跨线程访问窗口对象不安全。1. 在handle_dropped_file_paths函数中加强错误处理使用Result和?操作符而不是直接unwrap。2. 确保所有对窗口emit的操作都考虑了错误并且事件处理逻辑不阻塞主线程。5.3 性能优化与体验打磨防抖与性能FileDrop::Hovered事件在拖拽文件经过窗口时会频繁触发。不要在这里执行重操作如大量IO或复杂计算。可以设置一个简单的防抖逻辑只用于更新UI提示。UI反馈在文件悬停时通过Rust端emit事件或前端直接修改CSS给用户明确的视觉反馈如改变边框颜色、显示半透明蒙层。这能极大提升体验。多文件处理处理大量文件如拖拽一个包含成千上万文件的文件夹时要在Rust端做好异步处理避免阻塞主线程导致UI卡顿。可以考虑流式处理或分批处理。窗口管理透明子窗口在不需要时应隐藏window.hide()而不是销毁再创建以提高响应速度。同时注意管理窗口的z-index通过always_on_top确保它不会被其他窗口遮挡。6. 完整示例与集成要点最后让我们串联一个精简但可工作的示例。假设我们使用Tauri2的默认项目结构并配合ViteReact作为前端。Rust端 (src-tauri/src/main.rs或src-tauri/src/lib.rs):#[tauri::command] fn process_dropped_files(paths: VecString) - ResultVecString, String { let mut results Vec::new(); for path_str in paths { let path PathBuf::from(path_str); if path.exists() { // 你的业务处理逻辑这里简单返回文件名 if let Some(file_name) path.file_name() { results.push(file_name.to_string_lossy().into_owned()); } } } Ok(results) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![process_dropped_files]) .setup(|app| { // 创建主窗口... let main_window tauri::WindowBuilder::new( app, main, tauri::WindowUrl::App(index.html.into()) ) .build()?; // 创建透明拖放窗口 let drop_window tauri::WindowBuilder::new( app, drop, tauri::WindowUrl::App(drop.html.into()) ) .transparent(true) .decorations(false) .always_on_top(true) .visible(false) // 默认隐藏 .build()?; // 监听拖放窗口的文件拖放事件 let drop_window_ drop_window.clone(); drop_window.on_window_event(move |event| { if let tauri::WindowEvent::FileDrop(tauri::FileDropEvent::Dropped { paths, .. }) event { println!(捕获到文件路径: {:?}, paths); // 将路径发送给前端drop.html页面 let _ drop_window_.emit(files-dropped, paths.clone()); // 或者如果你希望主窗口处理可以发给主窗口 // let _ main_window.emit(files-dropped-to-drop-zone, paths.clone()); } }); // 提供一个命令给前端用于显示/隐藏拖放窗口 #[tauri::command] fn toggle_drop_window(window: tauri::Window) { if window.is_visible().unwrap_or(false) { let _ window.hide(); } else { let _ window.show(); let _ window.set_focus(); } } // ... 注册这个命令 Ok(()) }) .run(tauri::generate_context!()) .expect(error while running tauri application); }前端-透明窗口页 (src/drop.html或public/drop.html):!DOCTYPE html html head style body { margin:0; padding:0; overflow:hidden; background:transparent; } #drop-area { width:100vw; height:100vh; border: 3px dashed transparent; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.5em; color: #eee; transition: border-color 0.3s; } #drop-area.dragover { border-color: #4CAF50; } /style /head body div iddrop-area释放文件以导入/div script typemodule import { listen } from tauri-apps/api/event; const dropArea document.getElementById(drop-area); // 监听来自Rust端的文件路径事件 listen(files-dropped, (event) { const paths event.payload; console.log(收到文件路径:, paths); // 在这里你可以将路径发送给主窗口或者直接调用Rust命令处理 // 例如调用一个处理命令 // const { invoke } window.__TAURI__.tauri; // invoke(process_dropped_files, { paths: paths }).then(...) // 简单示例显示第一个文件名 if(paths paths.length 0) { dropArea.textContent 已接收: ${paths.length} 个文件; // 处理完成后可以隐藏窗口或给出其他反馈 setTimeout(() { // 假设有一个命令可以隐藏这个窗口 // invoke(hide_drop_window); }, 1500); } }); // 可选前端也监听dragover/drop事件以提供即时UI反馈 dropArea.addEventListener(dragover, (e) { e.preventDefault(); dropArea.classList.add(dragover); }); [dragleave, dragend, drop].forEach(eventName { dropArea.addEventListener(eventName, (e) { e.preventDefault(); dropArea.classList.remove(dragover); }); }); /script /body /html集成要点窗口通信透明子窗口和主窗口可以通过Tauri的事件系统emit/listen通信。状态共享复杂的业务状态如当前处理的任务队列最好放在Rust端通过tauri::State管理。错误边界在所有路径操作和跨进程调用周围都要有良好的错误处理避免因单个文件问题导致整个拖拽功能失效。用户体验考虑拖拽时显示一个全局的、半透明的覆盖层而不是一个固定大小的窗口这样用户可以在屏幕任意位置释放文件。实现透明窗口的文件拖拽核心在于理解操作系统原生事件与WebView沙盒环境的边界。紧紧抓住WindowEvent::FileDrop这个Rust端的“尚方宝剑”就能稳稳地拿到文件路径。剩下的就是如何优雅地设计窗口、管理事件流和打磨用户体验了。这套方案在Windows 11、macOS Sonoma和Ubuntu 22.04 (X11) 上实测通过对于追求原生体验的Tauri2应用来说是个非常实用的高级功能。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价