tukuaiai 1 месяц назад
Родитель
Сommit
b63fc41586

+ 74 - 0
libs/external/html-tools-main/README.md

@@ -0,0 +1,74 @@
+# HTML Tools
+
+**HTML 工具集:集成多种独立 HTML 工具,涵盖单一或多功能设计,致力于一键高效完成任务。**
+
+## 项目简介
+
+HTML Tools 是一个集合各种 HTML 工具的项目,旨在帮助开发者快速、高效地完成 HTML 页面相关的任务。无论是单一功能的小工具,还是集成多种功能的工具,这个项目都能够满足您的需求。
+
+---
+
+## 功能特色
+
+- **多种工具集成**:提供从简单到复杂 HTML 操作的工具。
+- **高效便捷**:一键式操作,快速完成目标任务。
+- **模块化设计**:工具独立,便于扩展和维护。
+- **开源免费**:完全开源,欢迎贡献。
+
+---
+
+## 安装与使用
+
+### 1. 克隆仓库
+```bash
+git clone https://github.com/fud114514/html-tools.git
+cd html-tools
+```
+
+### 2. 打开工具
+- 将工具直接部署在浏览器中运行,或者在本地 HTML 文件中打开。
+- 无需安装额外依赖。
+
+### 3. 贡献代码
+如果您有兴趣为项目贡献代码,请按照以下步骤进行:
+1. Fork 本仓库。
+2. 创建自己的分支:`git checkout -b feature/your-feature`
+3. 提交更改:`git commit -m 'Add some feature'`
+4. 推送分支到 GitHub:`git push origin feature/your-feature`
+5. 提交 Pull Request。
+
+---
+
+## 文件结构
+
+```plaintext
+html-tools/
+├── index.html       # 主页面
+├── tools/           # 各种独立工具文件
+├── assets/          # 静态资源 (如图片、样式表)
+└── README.md        # 项目文档
+```
+
+---
+
+## 参与贡献
+
+欢迎大家为 HTML Tools 项目贡献自己的力量!无论是修复问题、添加新功能或改进文档,您的每一个贡献都对我们非常重要。
+
+### 提交 Issue
+如果您在使用过程中遇到问题,可以通过 [Issue 页面](https://github.com/fud114514/html-tools/issues) 提交问题。
+
+### 提交 Pull Request
+详细步骤请参考上文的“贡献代码”部分。
+
+---
+
+## 开发者
+
+该项目由 [fud114514](https://github.com/fud114514) 开发和维护。
+
+---
+
+## 许可证
+
+本项目采用 [MIT 许可证](https://opensource.org/licenses/MIT) 开源,详情请参阅 [LICENSE 文件](https://github.com/fud114514/html-tools/blob/main/LICENSE)。

+ 424 - 0
libs/external/html-tools-main/clean_epub_css.html

@@ -0,0 +1,424 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>EPUB CSS 清理工具 (Web版)</title>
+    <style>
+        body {
+            font-family: sans-serif;
+            line-height: 1.6;
+            padding: 20px;
+            max-width: 600px;
+            margin: 20px auto;
+            background-color: #f4f4f4;
+            border: 1px solid #ccc;
+            border-radius: 8px;
+        }
+        h1 {
+            text-align: center;
+            color: #333;
+        }
+        #drop-area {
+            border: 2px dashed #ccc;
+            border-radius: 5px;
+            padding: 30px;
+            text-align: center;
+            background-color: #fff;
+            margin-bottom: 20px;
+            cursor: pointer;
+        }
+        #drop-area.highlight {
+            border-color: dodgerblue;
+        }
+        #drop-area p {
+            margin: 0;
+            color: #555;
+        }
+        #fileInput {
+            display: none; /* Hide default input, use drop area or label */
+        }
+        label.file-label {
+             display: inline-block;
+             padding: 10px 15px;
+             background-color: dodgerblue;
+             color: white;
+             border: none;
+             border-radius: 4px;
+             cursor: pointer;
+             margin-top: 10px;
+             transition: background-color 0.2s;
+        }
+         label.file-label:hover {
+             background-color: #007ae5;
+         }
+
+        #status {
+            margin-top: 15px;
+            padding: 10px;
+            background-color: #eee;
+            border-radius: 4px;
+            min-height: 40px; /* Ensure space for messages */
+            text-align: center;
+            font-weight: bold;
+        }
+        .status-details {
+             font-size: 0.9em;
+             color: #555;
+             margin-top: 5px;
+        }
+        .warning {
+            color: #a00;
+            font-weight: bold;
+            text-align: center;
+            margin-top: 20px;
+            padding: 10px;
+            background-color: #fdd;
+            border: 1px solid #fbb;
+            border-radius: 4px;
+        }
+        progress {
+            width: 100%;
+            margin-top: 10px;
+            display: none; /* Hidden by default */
+        }
+    </style>
+    <!-- Include JSZip library -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
+    <!-- Include FileSaver.js library -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
+</head>
+<body>
+
+    <h1>EPUB CSS 清理工具 (Web版)</h1>
+
+    <p>将单个 EPUB 文件或包含多个 EPUB 的 ZIP 文件拖拽到下方区域,或点击按钮选择文件,即可移除指定的 CSS 样式。</p>
+
+    <div id="drop-area">
+        <!-- Accept both .epub and .zip -->
+        <input type="file" id="fileInput" accept=".epub,.zip">
+        <p>将 EPUB 或 ZIP 文件拖拽到这里</p>
+        <label for="fileInput" class="file-label">或者选择文件</label>
+    </div>
+
+    <progress id="progressBar" value="0" max="100"></progress>
+    <div id="status">请选择或拖拽一个 EPUB 或 ZIP 文件。</div>
+    <div id="statusDetails" class="status-details"></div>
+
+    <div class="warning">
+        <strong>重要提示:</strong> 此工具在你的浏览器中处理文件,不会上传到服务器。处理后的文件需要你手动下载。建议在处理前备份原始文件!处理 ZIP 文件时,会生成一个包含所有处理后(或原始)EPUB 的新 ZIP 文件。
+    </div>
+
+    <script>
+        // --- Check if libraries loaded ---
+        if (typeof JSZip === 'undefined' || typeof saveAs === 'undefined') {
+            const errorMsg = '错误:无法加载必需的库 (JSZip 或 FileSaver)。请检查您的网络连接,并确保没有浏览器插件(如广告拦截器)阻止从 cdnjs.cloudflare.com 加载脚本。然后请刷新页面重试。';
+            document.getElementById('status').textContent = errorMsg;
+            document.getElementById('status').style.color = '#a00';
+            document.getElementById('drop-area').style.display = 'none';
+            document.querySelector('.warning').textContent = errorMsg;
+        } else {
+            // --- Global Variables & Constants ---
+            const dropArea = document.getElementById('drop-area');
+            const fileInput = document.getElementById('fileInput');
+            const statusDiv = document.getElementById('status');
+            const statusDetailsDiv = document.getElementById('statusDetails');
+            const progressBar = document.getElementById('progressBar');
+            const CSS_REMOVE_PATTERN = /((text-indent|line-height|font-size|height|font-family|color)\s*:\s*[^;]*;|display\s*:\s*block\s*;)/ig;
+
+            // --- Event Handlers ---
+            setupDragDropHandlers();
+            fileInput.addEventListener('change', handleFileSelect, false);
+
+            function setupDragDropHandlers() {
+                ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
+                    dropArea.addEventListener(eventName, preventDefaults, false);
+                    document.body.addEventListener(eventName, preventDefaults, false);
+                });
+                ['dragenter', 'dragover'].forEach(eventName => {
+                    dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'), false);
+                });
+                ['dragleave', 'drop'].forEach(eventName => {
+                    dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'), false);
+                });
+                dropArea.addEventListener('drop', handleDrop, false);
+            }
+
+            function preventDefaults(e) {
+                e.preventDefault();
+                e.stopPropagation();
+            }
+
+            function handleDrop(e) {
+                handleFiles(e.dataTransfer.files);
+            }
+
+            function handleFileSelect(e) {
+                 handleFiles(e.target.files);
+            }
+
+            // --- Main File Handling Logic ---
+            function handleFiles(files) {
+                if (files.length === 0) return;
+                const file = files[0]; // Process only the first file
+
+                // Check file type
+                const isEpub = file.name.toLowerCase().endsWith('.epub');
+                const isZip = file.name.toLowerCase().endsWith('.zip');
+
+                if (!isEpub && !isZip) {
+                    updateStatus('错误:请选择一个 .epub 或 .zip 文件。', true);
+                    resetInput();
+                    return;
+                }
+
+                updateStatus(`正在读取文件: ${file.name}...`);
+                clearStatusDetails();
+                progressBar.style.display = 'block';
+                progressBar.value = 0;
+
+                const reader = new FileReader();
+                reader.onload = async (e) => {
+                    const fileContent = e.target.result; // ArrayBuffer
+                    try {
+                        if (isEpub) {
+                            updateStatus(`正在处理 EPUB 文件: ${file.name}...`);
+                            const result = await processEpub(fileContent, file.name);
+                            if (result.blob) {
+                                saveAs(result.blob, result.filename);
+                                updateStatus(`处理完成!已生成 ${result.filename}。`);
+                            } else if (result.modified === false) {
+                                updateStatus('信息:文件无需修改。');
+                            }
+                            resetInput();
+                        } else if (isZip) {
+                            updateStatus(`正在处理 ZIP 压缩包: ${file.name}...`);
+                            await processZipArchive(fileContent, file.name);
+                        }
+                    } catch (error) {
+                        console.error("处理失败:", error);
+                        updateStatus(`处理失败:${error.message}`, true);
+                        resetInput();
+                    }
+                };
+                reader.onprogress = updateReadProgress;
+                reader.onerror = handleReadError;
+                reader.readAsArrayBuffer(file);
+            }
+
+             function updateReadProgress(e) {
+                 if (e.lengthComputable) {
+                     progressBar.value = Math.round((e.loaded / e.total) * 10); // Reading is ~10%
+                 }
+             }
+
+             function handleReadError() {
+                 updateStatus('错误:读取文件时出错。', true);
+                 resetInput();
+             }
+
+            // --- ZIP Archive Processing ---
+            async function processZipArchive(zipFileContent, zipFilename) {
+                let outputZip = new JSZip();
+                let processedCount = 0;
+                let failedCount = 0;
+                let foundCount = 0;
+                let modifiedCount = 0;
+                let filePromises = [];
+
+                updateStatus(`正在打开 ZIP 压缩包...`);
+                progressBar.value = 15;
+                const inputZip = await JSZip.loadAsync(zipFileContent);
+                const totalFilesInZip = Object.keys(inputZip.files).length;
+
+                updateStatus(`在 ZIP 中查找 EPUB 文件...`);
+
+                inputZip.forEach((relativePath, zipEntry) => {
+                    if (!zipEntry.dir && relativePath.toLowerCase().endsWith('.epub')) {
+                        foundCount++;
+                        updateStatusDetails(`找到 EPUB: ${relativePath}`);
+
+                        // Add a promise to process this EPUB entry
+                        const promise = zipEntry.async('arraybuffer')
+                            .then(async (innerEpubContent) => {
+                                try {
+                                    // Process the inner EPUB
+                                    const result = await processEpub(innerEpubContent, relativePath);
+                                    if (result.blob) {
+                                        // Add modified EPUB to output ZIP
+                                        outputZip.file(result.filename, result.blob);
+                                        modifiedCount++;
+                                        console.log(`Added cleaned ${result.filename} to output zip.`);
+                                    } else {
+                                        // Add original EPUB back if not modified
+                                        outputZip.file(relativePath, innerEpubContent);
+                                        console.log(`Added original ${relativePath} (no changes needed) to output zip.`);
+                                    }
+                                    processedCount++;
+                                } catch (epubError) {
+                                    // Add original EPUB back if processing failed
+                                    console.error(`Error processing inner EPUB ${relativePath}:`, epubError);
+                                    updateStatusDetails(`处理 ${relativePath} 时出错,将保留原文件。`);
+                                    outputZip.file(relativePath, innerEpubContent); // Add original back
+                                    failedCount++;
+                                }
+                            })
+                            .catch(extractError => {
+                                // Handle error extracting the inner EPUB content
+                                console.error(`Error extracting inner EPUB ${relativePath} from zip:`, extractError);
+                                updateStatusDetails(`无法从 ZIP 中提取 ${relativePath},已跳过。`);
+                                failedCount++;
+                            })
+                            .finally(() => {
+                                // Update progress based on entries processed (success or fail)
+                                const progress = 15 + Math.round(((processedCount + failedCount) / foundCount) * 60); // Processing EPUBs is ~60%
+                                progressBar.value = Math.min(progress, 75); // Cap at 75 before final packing
+                            });
+                        filePromises.push(promise);
+                    }
+                    // We are currently NOT adding non-EPUB files from the source ZIP to the output ZIP.
+                    // To do so, handle the 'else' case here and add zipEntry data to outputZip.
+                });
+
+                // Wait for all inner EPUB processing promises to complete
+                await Promise.all(filePromises);
+
+                 progressBar.value = 80; // Done processing files inside zip
+
+                if (foundCount === 0) {
+                    updateStatus('错误:在 ZIP 文件中未找到任何 EPUB 文件。', true);
+                    resetInput();
+                    return;
+                }
+
+                updateStatus(`处理了 ${foundCount} 个 EPUB 文件。正在生成输出 ZIP...`);
+                statusDetailsDiv.innerHTML += `<br>成功: ${processedCount}, 修改: ${modifiedCount}, 失败/跳过: ${failedCount}`;
+                progressBar.value = 90;
+
+                const outputZipBlob = await outputZip.generateAsync({
+                    type: "blob",
+                    compression: "DEFLATE",
+                    platform: "browser"
+                 }, (metadata) => {
+                    progressBar.value = 90 + Math.round(metadata.percent * 0.1);
+                 });
+
+                progressBar.value = 100;
+                const baseName = zipFilename.substring(0, zipFilename.lastIndexOf('.')) || zipFilename;
+                const outputZipFilename = `${baseName}_cleaned.zip`;
+                saveAs(outputZipBlob, outputZipFilename);
+
+                updateStatus(`处理完成!已生成包含处理后 EPUB 的 ${outputZipFilename}。`);
+                resetInput();
+            }
+
+
+            // --- Single EPUB Processing (Modified to return result) ---
+            async function processEpub(fileContent, originalFilename) {
+                 // This function now returns an object:
+                 // { blob: Blob, filename: string } if modified
+                 // { modified: false } if no modification needed
+                 // Throws error if processing fails critically
+
+                console.log(`Processing EPUB: ${originalFilename}`);
+                // Note: Progress bar updates inside this function are less meaningful now,
+                // as the overall progress is handled by the caller (handleFiles or processZipArchive).
+                // We could pass a progress callback if detailed inner progress is needed.
+
+                const zip = await JSZip.loadAsync(fileContent);
+                const newZip = new JSZip();
+                let cssFilesFound = false;
+                let modified = false;
+                let filePromises = [];
+
+                zip.forEach((relativePath, zipEntry) => {
+                    if (zipEntry.dir) { newZip.folder(relativePath); return; }
+
+                    const options = {
+                        date: zipEntry.date,
+                        unixPermissions: zipEntry.unixPermissions,
+                        dosPermissions: zipEntry.dosPermissions,
+                        comment: zipEntry.comment,
+                        dir: zipEntry.dir,
+                        compression: zipEntry.options?.compression === "STORE" ? "STORE" : "DEFLATE"
+                    };
+
+                    if (relativePath.toLowerCase() === 'mimetype') {
+                        const promise = zipEntry.async('uint8array').then(data => {
+                            newZip.file(relativePath, data, { ...options, compression: "STORE" });
+                        });
+                         filePromises.push(promise);
+                    } else if (relativePath.toLowerCase().endsWith('.css')) {
+                        cssFilesFound = true;
+                        const promise = zipEntry.async('string').then(originalCss => {
+                            const cleanedCss = originalCss.replace(CSS_REMOVE_PATTERN, '');
+                            let contentToAdd = originalCss;
+                            if (cleanedCss !== originalCss) {
+                                modified = true;
+                                contentToAdd = cleanedCss;
+                                console.log(`  Cleaned CSS: ${relativePath}`);
+                            }
+                            newZip.file(relativePath, contentToAdd, options);
+                        }).catch(err => {
+                             console.warn(`  Could not process CSS ${relativePath} as text, keeping original. Error: ${err}`);
+                             return zipEntry.async('uint8array').then(originalData => {
+                                 newZip.file(relativePath, originalData, options);
+                             });
+                         });
+                         filePromises.push(promise);
+                    } else {
+                        const promise = zipEntry.async('uint8array').then(data => {
+                            newZip.file(relativePath, data, options);
+                        });
+                        filePromises.push(promise);
+                    }
+                });
+
+                await Promise.all(filePromises);
+
+                if (!modified) {
+                    console.log(`  EPUB ${originalFilename} - No modifications needed.`);
+                    return { modified: false }; // Indicate no changes were made
+                }
+
+                console.log(`  Repacking modified EPUB: ${originalFilename}`);
+                const newEpubBlob = await newZip.generateAsync({
+                    type: "blob",
+                    mimeType: "application/epub+zip",
+                    compression: "DEFLATE",
+                    platform: "browser"
+                });
+
+                const baseName = originalFilename.substring(0, originalFilename.lastIndexOf('.')) || originalFilename;
+                const newFilename = `${baseName}_cleaned.epub`;
+
+                return { blob: newEpubBlob, filename: newFilename }; // Return data for the caller
+            } // end of processEpub function
+
+            // --- UI Update Functions ---
+            function updateStatus(message, isError = false) {
+                statusDiv.textContent = message;
+                statusDiv.style.color = isError ? '#a00' : '#000';
+                if (isError) {
+                   progressBar.style.display = 'none';
+                   clearStatusDetails();
+                }
+            }
+             function updateStatusDetails(message) {
+                 statusDetailsDiv.innerHTML += message + "<br>";
+             }
+             function clearStatusDetails() {
+                  statusDetailsDiv.innerHTML = "";
+             }
+
+            function resetInput() {
+                 fileInput.value = '';
+                 // Hide progress bar after a delay
+                 setTimeout(() => { progressBar.style.display = 'none'; progressBar.value = 0; }, 5000);
+            }
+        } // End of the 'else' block for library check
+
+    </script>
+
+</body>
+</html>

+ 303 - 0
libs/external/html-tools-main/markdown-bianjiqi.html

@@ -0,0 +1,303 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Markdown Editor</title>
+    <style>
+        :root {
+            --border-color: #eee;
+            --bg-color: #fafafa;
+            --text-color: #333;
+            --btn-border: #ddd;
+            --code-bg: #f5f5f5;
+            --scrollbar-color: #ccc;
+        }
+        
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            height: 100vh;
+            display: flex;
+            flex-direction: column;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+            overflow: hidden;
+        }
+
+        .header {
+            padding: 8px 20px;
+            background: var(--bg-color);
+            border-bottom: 1px solid var(--border-color);
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            font-size: 14px;
+            color: var(--text-color);
+        }
+
+        .toolbar {
+            display: flex;
+            gap: 8px;
+        }
+
+        .btn {
+            border: 1px solid var(--btn-border);
+            background: white;
+            padding: 4px 12px;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 14px;
+            color: var(--text-color);
+            min-width: 80px;
+            text-align: center;
+            transition: background-color 0.2s ease;
+        }
+
+        .btn:hover {
+            background: var(--code-bg);
+        }
+
+        .content {
+            display: flex;
+            flex: 1;
+            overflow: hidden;
+        }
+        
+        #preview, #editor {
+            width: 50%;
+            height: 100%;
+            padding: 20px;
+            line-height: 1.6;
+        }
+        
+        #preview {
+            color: var(--text-color);
+            border-right: 1px solid var(--border-color);
+            overflow-y: auto;
+        }
+        
+        #editor {
+            font-family: monospace;
+            font-size: 14px;
+            border: none;
+            outline: none;
+            resize: none;
+            overflow-y: auto;
+            scrollbar-width: thin;
+            scrollbar-color: var(--scrollbar-color) transparent;
+        }
+
+        #editor::-webkit-scrollbar {
+            width: 8px;
+        }
+
+        #editor::-webkit-scrollbar-thumb {
+            background-color: var(--scrollbar-color);
+            border-radius: 4px;
+        }
+
+        #editor::-webkit-scrollbar-track {
+            background-color: transparent;
+        }
+
+        .preview-content {
+            height: auto;
+            min-height: 100%;
+            padding-right: 8px;
+        }
+
+        #preview h1, #preview h2, #preview h3 {
+            margin: 16px 0;
+            font-weight: 500;
+        }
+
+        #preview h1:first-child {
+            margin-top: 0;
+        }
+
+        #preview p {
+            margin: 12px 0;
+        }
+
+        #preview ul, #preview ol {
+            padding-left: 24px;
+            margin: 12px 0;
+        }
+
+        #preview code {
+            background-color: var(--code-bg);
+            padding: 2px 4px;
+            border-radius: 3px;
+            font-size: 0.9em;
+        }
+
+        #preview pre code {
+            display: block;
+            padding: 12px;
+            overflow-x: auto;
+            line-height: 1.45;
+        }
+
+        .toast {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            background: rgba(0, 0, 0, 0.8);
+            color: white;
+            padding: 8px 16px;
+            border-radius: 4px;
+            font-size: 14px;
+            display: none;
+            z-index: 1000;
+            animation: fadeIn 0.3s ease;
+        }
+
+        @keyframes fadeIn {
+            from { opacity: 0; transform: translateY(-10px); }
+            to { opacity: 1; transform: translateY(0); }
+        }
+    </style>
+</head>
+<body>
+    <div class="header">
+        <div class="toolbar">
+            <button class="btn" id="copyBtn">一键复制</button>
+            <button class="btn" id="exportBtn">导出文本</button>
+        </div>
+    </div>
+    <div class="content">
+        <div id="preview">
+            <div class="preview-content"></div>
+        </div>
+        <textarea id="editor" placeholder="在这里输入 Markdown 文本..."></textarea>
+    </div>
+    <div id="toast" class="toast"></div>
+
+    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+    <script>
+        document.addEventListener('DOMContentLoaded', function() {
+            const editor = document.getElementById('editor');
+            const preview = document.getElementById('preview');
+            const previewContent = preview.querySelector('.preview-content');
+            const toast = document.getElementById('toast');
+            const copyBtn = document.getElementById('copyBtn');
+            const exportBtn = document.getElementById('exportBtn');
+
+            // Initialize marked with safe options
+            marked.setOptions({
+                breaks: true,
+                gfm: true,
+                headerIds: true,
+                sanitize: false,
+                highlight: function(code, lang) {
+                    return code;
+                }
+            });
+
+            function showToast(message, duration = 2000) {
+                toast.textContent = message;
+                toast.style.display = 'block';
+                setTimeout(() => {
+                    toast.style.display = 'none';
+                }, duration);
+            }
+
+            function updatePreview() {
+                try {
+                    const markdownText = editor.value;
+                    const htmlContent = marked.parse(markdownText);
+                    previewContent.innerHTML = htmlContent;
+                } catch (error) {
+                    console.error('Markdown parsing error:', error);
+                    previewContent.innerHTML = '<p style="color: red;">Error parsing markdown</p>';
+                }
+            }
+
+            function debounce(func, wait) {
+                let timeout;
+                return function(...args) {
+                    clearTimeout(timeout);
+                    timeout = setTimeout(() => func.apply(this, args), wait);
+                };
+            }
+
+            const debouncedUpdate = debounce(updatePreview, 150);
+
+            async function copyText() {
+                try {
+                    await navigator.clipboard.writeText(editor.value);
+                    showToast('已复制到剪贴板');
+                } catch (err) {
+                    console.error('Copy failed:', err);
+                    showToast('复制失败');
+                }
+            }
+
+            function exportTxt() {
+                try {
+                    const text = editor.value;
+                    const blob = new Blob([text], { type: 'text/plain' });
+                    const url = URL.createObjectURL(blob);
+                    const a = document.createElement('a');
+                    const timestamp = new Date().toISOString().slice(0,19).replace(/[-:]/g, '');
+                    a.href = url;
+                    a.download = `markdown_${timestamp}.txt`;
+                    document.body.appendChild(a);
+                    a.click();
+                    document.body.removeChild(a);
+                    URL.revokeObjectURL(url);
+                    showToast('已导出为文本文件');
+                } catch (err) {
+                    console.error('Export failed:', err);
+                    showToast('导出失败');
+                }
+            }
+
+            // Event Listeners
+            editor.addEventListener('input', debouncedUpdate);
+            copyBtn.addEventListener('click', copyText);
+            exportBtn.addEventListener('click', exportTxt);
+
+            // Sync scroll
+            editor.addEventListener('scroll', () => {
+                const percentage = editor.scrollTop / (editor.scrollHeight - editor.clientHeight);
+                preview.scrollTop = percentage * (preview.scrollHeight - preview.clientHeight);
+            });
+
+            // Initial content
+            const initialText = `# Markdown 编辑器
+
+## 欢迎使用!
+
+这是一个简单的 Markdown 编辑器。
+
+### 主要功能:
+
+1. 实时预览
+2. 一键复制内容
+3. 导出文本文件
+4. 支持常用 Markdown 语法
+
+### 快速开始:
+
+- 在右侧输入 Markdown 文本
+- 在左侧查看实时预览
+- 使用顶部按钮进行操作
+
+\`\`\`markdown
+# 示例代码
+**粗体** 和 *斜体*
+- 列表项
+- 另一个列表项
+\`\`\``;
+
+            editor.value = initialText;
+            updatePreview();
+        });
+    </script>
+</body>
+</html>

+ 289 - 0
libs/external/html-tools-main/task card generator.html

@@ -0,0 +1,289 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>任务卡片生成器</title>
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            justify-content: flex-start; /* 从顶部开始排列 */
+            align-items: center;
+            min-height: 100vh;
+            background: #121212; /* 深色背景,让卡片在预览时效果更一致 */
+            margin: 0;
+            font-family: 'Arial', 'Helvetica Neue', Helvetica, sans-serif; /* 更现代的字体栈 */
+            padding: 30px 20px; /* 给body一些padding */
+            box-sizing: border-box;
+        }
+        .export-container {
+            background: #282828; /* 图片中外部容器的深灰色 */
+            padding: 20px;      /* 容器内边距 */
+            border-radius: 8px;
+            display: inline-block;
+            box-shadow: 0 8px 20px rgba(0,0,0,0.3); /* 给容器一些阴影 */
+        }
+        .card {
+            width: 370px; /* 调整宽度以适应两列布局和间距 */
+            background: #000000; /* 卡片纯黑背景 */
+            padding: 25px;
+            box-sizing: border-box;
+            color: #ffffff;
+            border-radius: 6px; /* 给卡片本身也加一点圆角 */
+        }
+        .card-header {
+            border-bottom: 1px solid #444444; /* 更细、颜色更深的分割线 */
+            padding-bottom: 15px;
+            margin-bottom: 20px;
+        }
+        .task-id {
+            font-size: 14px;
+            color: #888888; /* 灰色ID文字 */
+            margin-bottom: 8px; /* ID和标题间距 */
+            min-height: 1.2em; /* 确保空的时候也有高度,placeholder可以显示 */
+        }
+        .task-title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #ffffff;
+            word-wrap: break-word;
+            min-height: 1.2em;
+        }
+        .task-description {
+            font-size: 15px;
+            color: #e0e0e0;
+            line-height: 1.6;
+            word-wrap: break-word;
+            margin-top: 15px;
+            margin-bottom: 25px;
+            min-height: 45px; /* 描述区域最小高度 */
+            padding: 5px 2px; /* 轻微内边距 */
+        }
+        .task-details {
+            display: grid;
+            grid-template-columns: 1fr 1fr; /* 固定两列 */
+            gap: 15px;
+            font-size: 14px;
+            margin-top: 20px;
+        }
+        .detail-item {
+            background-color: #1c1c1c; /* 详情项的背景色,比卡片黑底亮一点 */
+            padding: 12px;
+            border-radius: 6px;
+        }
+        /* 如果最后一个元素是奇数个中的最后一个(例如第3个),则让它占据整行 */
+        .detail-item:last-child:nth-child(odd) { 
+             grid-column: 1 / -1; /* 跨越所有列 */
+        }
+        .detail-item label {
+            font-weight: normal;
+            color: #aaaaaa; /* 标签文字颜色 */
+            display: block;
+            margin-bottom: 8px;
+            font-size: 13px;
+        }
+        .detail-item .value.editable,
+        .detail-item input[type="date"].value {
+            color: #ffffff;
+            background-color: #000000; /* 值区域纯黑背景 */
+            border: 1px solid #333333; /* 值区域的细边框 */
+            width: 100%;
+            padding: 8px 10px; /* 值区域内边距 */
+            min-height: 1.5em; /* 确保有高度 */
+            border-radius: 4px;
+            font-size: 15px;
+            line-height: 1.4;
+            box-sizing: border-box; /* padding和border不增加额外宽度 */
+        }
+
+        .detail-item input[type="date"].value {
+            -webkit-appearance: none;
+            -moz-appearance: none;
+            appearance: none;
+            position: relative; 
+        }
+        .detail-item input[type="date"].value::-webkit-calendar-picker-indicator {
+            filter: invert(0.8) brightness(0.8); 
+            cursor: pointer;
+            opacity: 0.7;
+        }
+        .detail-item .value.editable {
+            position: relative; 
+        }
+        .detail-item .value.editable:empty::before {
+            content: attr(data-placeholder);
+            color: #555555; 
+            position: absolute;
+            left: 10px; 
+            top: 50%;
+            transform: translateY(-50%);
+            pointer-events: none; 
+            width: calc(100% - 20px); 
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+        .editable {
+            position: relative; 
+            min-height: 1.2em; 
+        }
+        .editable:empty:not(:focus)::before { 
+            content: attr(data-placeholder);
+            color: #666666; 
+            position: absolute;
+            left: 0; 
+            top: 0;  
+            pointer-events: none; 
+            width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+        .task-id.editable:empty:not(:focus)::before,
+        .task-title.editable:empty:not(:focus)::before,
+        .task-description.editable:empty:not(:focus)::before {
+            left: 2px; 
+            top: 2px;
+        }
+        .editable:focus {
+            outline: 1px dashed #777777;
+            background-color: #0a0a0a; 
+        }
+        [contenteditable] {
+            -webkit-user-select: text;
+            user-select: text;
+        }
+        [contenteditable]:focus {
+            background-color: #0a0a0a !important;
+        }
+        ::selection {
+            background-color: #444444;
+            color: #ffffff;
+        }
+        .button-container {
+            margin-top: 25px;
+            text-align: center;
+        }
+        .export-btn {
+            padding: 12px 25px;
+            background: #1c1c1c;
+            color: #ffffff;
+            border: 1px solid #444444;
+            cursor: pointer;
+            font-size: 16px;
+            border-radius: 6px;
+            transition: background-color 0.2s ease, border-color 0.2s ease;
+        }
+        .export-btn:hover {
+            background: #282828;
+            border-color: #555555;
+        }
+    </style>
+</head>
+<body>
+    <div class="export-container" id="export-container">
+        <div id="taskCard" class="card">
+            <div class="card-header">
+                <div class="task-id editable" contenteditable="true" data-placeholder="任务ID (可选)"></div>
+                <div class="task-title editable" contenteditable="true" data-placeholder="点击输入任务标题"></div>
+            </div>
+
+            <div class="task-description editable" contenteditable="true" data-placeholder="点击输入任务详细描述..."></div>
+
+            <div class="task-details">
+                <div class="detail-item">
+                    <label for="assignee-value">负责人:</label>
+                    <div id="assignee-value" class="value editable" contenteditable="true" data-placeholder="未分配"></div>
+                </div>
+                <!-- 标签现在是第二个 -->
+                <div class="detail-item">
+                    <label for="tags-value">标签:</label>
+                    <div id="tags-value" class="value editable" contenteditable="true" data-placeholder="例如: 项目A, Bug"></div>
+                </div>
+                <!-- 截止日期现在是第三个,会自动占据整行 -->
+                <div class="detail-item"> 
+                    <label for="dueDate-value">截止日期:</label>
+                    <input type="date" id="dueDate-value" class="value">
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="button-container">
+        <button class="export-btn" onclick="exportToPng()">导出为PNG</button>
+    </div>
+
+    <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
+    <script>
+        function sanitizeFilename(name) {
+            return name.replace(/[^a-z0-9_\-.\u4e00-\u9fa5\s]/gi, '').replace(/\s+/g, '_');
+        }
+
+        function exportToPng() {
+            if (window.getSelection) {
+                if (window.getSelection().empty) { window.getSelection().empty(); }
+                else if (window.getSelection().removeAllRanges) { window.getSelection().removeAllRanges(); }
+            } else if (document.selection) { document.selection.empty(); }
+            
+            const container = document.getElementById('export-container');
+            // const card = document.getElementById('taskCard'); // card variable not strictly needed here
+            const originalDueDate = document.getElementById('dueDate-value').value;
+
+            html2canvas(container, {
+                backgroundColor: null, 
+                scale: 2,
+                useCORS: true,
+                logging: false, // Changed to false to reduce console noise unless debugging
+                onclone: (documentClone) => {
+                    // const clonedCard = documentClone.getElementById('taskCard'); // clonedCard not strictly needed
+                    
+                    const dueDateInputClone = documentClone.getElementById('dueDate-value');
+                    if (dueDateInputClone) {
+                        dueDateInputClone.value = originalDueDate;
+                        if (!originalDueDate) {
+                            dueDateInputClone.style.color = '#555555'; 
+                        } else {
+                             dueDateInputClone.style.color = '#ffffff';
+                        }
+                    }
+
+                    const editables = documentClone.querySelectorAll('.editable');
+                    editables.forEach(el => {
+                        // Placeholder handling is mostly done by CSS (:empty::before)
+                        // If specific adjustments needed for clone, add here.
+                        // For example, ensuring text color if content exists:
+                        // if (el.textContent.trim() !== '' && el.classList.contains('value')) {
+                        //     el.style.color = '#ffffff';
+                        // }
+                    });
+                }
+            }).then(canvas => {
+                const link = document.createElement('a');
+                const taskTitleElement = document.getElementById('task-title');
+                let filename = '任务卡片.png';
+                if (taskTitleElement && taskTitleElement.textContent.trim() !== '') {
+                    const sanitizedTitle = sanitizeFilename(taskTitleElement.textContent.trim());
+                    if (sanitizedTitle) { // Ensure title isn't just invalid chars
+                        filename = sanitizedTitle + '.png';
+                    }
+                }
+                link.download = filename;
+                link.href = canvas.toDataURL('image/png');
+                link.click();
+            }).catch(err => {
+                console.error("导出PNG失败:", err);
+                alert("导出PNG失败,请查看控制台获取更多信息。");
+            });
+        }
+
+        document.querySelectorAll('.editable').forEach(element => {
+            element.addEventListener('focus', function() {
+                // CSS handles placeholder via :not(:focus)::before
+            });
+            element.addEventListener('blur', function() {
+                // CSS handles placeholder
+            });
+        });
+    </script>
+</body>
+</html>

+ 320 - 0
libs/external/html-tools-main/xhs graphic production - 1.0.html

@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Markdown 编辑与预览 (带同步滚动和滑块)</title>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"></script>
+    <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            min-height: 100vh;
+            background: #f0f0f0;
+            margin: 0;
+            font-family: 'Arial', sans-serif;
+            padding-top: 20px;
+            overflow: hidden; /* Prevent body scrollbars if editor is full height */
+        }
+        .editor-container {
+            display: flex;
+            width: 90%; /* Use percentage for better responsiveness */
+            max-width: 1200px; /* Max width for large screens */
+            height: calc(100vh - 100px); /* Adjust height to fill more of the viewport */
+            background: #333333;
+            border-radius: 8px;
+            overflow: hidden;
+            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
+            position: relative; /* For resizer positioning if needed */
+        }
+        .markdown-editor, .preview-pane {
+            padding: 20px;
+            box-sizing: border-box;
+            overflow-y: auto;
+            height: 100%; /* Fill the container height */
+        }
+        .markdown-editor {
+            /* flex: 1; Will be set by JS */
+            background: #222222;
+            color: #e0e0e0;
+            border: none;
+            resize: none;
+            font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
+            font-size: 14px;
+            line-height: 1.6;
+            outline: none;
+        }
+        .resizer {
+            flex: 0 0 10px; /* Don't grow, don't shrink, base width 10px */
+            background: #555555;
+            cursor: col-resize;
+            height: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            user-select: none; /* Prevent text selection during drag */
+        }
+        .resizer::before { /* Optional: visual indicator for dragging */
+            content: '⋮';
+            color: #aaa;
+            font-size: 18px;
+            line-height: 0;
+        }
+        .preview-pane {
+            /* flex: 1; Will be set by JS */
+            background: #000000;
+            color: #ffffff;
+            /* border-left: 2px solid #666666; /* Replaced by resizer */
+            word-wrap: break-word;
+        }
+
+        /* Styling for rendered Markdown elements in preview-pane */
+        .preview-pane h1, .preview-pane h2, .preview-pane h3, .preview-pane h4, .preview-pane h5, .preview-pane h6 {
+            color: #ffffff;
+            border-bottom: 1px solid #444444;
+            padding-bottom: 5px;
+            margin-top: 1em;
+            margin-bottom: 0.5em;
+        }
+        .preview-pane h1 { font-size: 24px; font-weight: bold; border-bottom: 2px solid #666666; padding-bottom: 10px; margin-bottom: 15px;}
+        .preview-pane p {
+            font-size: 16px;
+            line-height: 1.5;
+            margin-bottom: 1em;
+        }
+        .preview-pane ul, .preview-pane ol {
+            margin-left: 20px;
+            margin-bottom: 1em;
+        }
+        .preview-pane li {
+            margin-bottom: 0.5em;
+        }
+        .preview-pane blockquote {
+            border-left: 4px solid #555555;
+            padding-left: 10px;
+            color: #aaaaaa;
+            margin-left: 0;
+            margin-right: 0;
+            margin-bottom: 1em;
+        }
+        .preview-pane pre {
+            background-color: #1e1e1e;
+            padding: 10px;
+            border-radius: 4px;
+            overflow-x: auto;
+            font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
+        }
+        .preview-pane code {
+            font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
+            background-color: #282c34;
+            padding: 2px 4px;
+            border-radius: 3px;
+            font-size: 0.9em;
+        }
+        .preview-pane pre code {
+            background-color: transparent;
+            padding: 0;
+            border-radius: 0;
+        }
+        .preview-pane table {
+            border-collapse: collapse;
+            width: 100%;
+            margin-bottom: 1em;
+        }
+        .preview-pane th, .preview-pane td {
+            border: 1px solid #666666;
+            padding: 8px;
+            text-align: left;
+        }
+        .preview-pane th {
+            background-color: #1a1a1a;
+        }
+        .preview-pane img {
+            max-width: 100%;
+            height: auto;
+            border-radius: 4px;
+        }
+
+        .button-container {
+            margin-top: 20px;
+            text-align: center;
+        }
+        .export-btn {
+            padding: 10px 20px;
+            background: #000000;
+            color: #ffffff;
+            border: 2px solid #666666;
+            cursor: pointer;
+            font-size: 16px;
+        }
+        .export-btn:hover {
+            background: #333333;
+        }
+    </style>
+</head>
+<body>
+    <div class="editor-container">
+        <textarea id="markdown-input" class="markdown-editor" placeholder="在此输入 Markdown... 例如:\n# 标题\n\n- 列表项1\n- 列表项2\n\n**加粗文本**"></textarea>
+        <div id="resizer" class="resizer"></div>
+        <div id="preview-output" class="preview-pane"></div>
+    </div>
+    <div class="button-container">
+        <button class="export-btn" onclick="exportToPng()">导出为PNG</button>
+    </div>
+
+    <script>
+        const markdownInput = document.getElementById('markdown-input');
+        const previewOutput = document.getElementById('preview-output');
+        const resizer = document.getElementById('resizer');
+        const editorContainer = document.querySelector('.editor-container');
+
+        // Initial flex distribution
+        markdownInput.style.flex = '1';
+        previewOutput.style.flex = '1';
+        
+        const converter = new showdown.Converter({
+            tables: true,
+            strikethrough: true,
+            tasklists: true,
+            ghCompatibleHeaderId: true,
+            simpleLineBreaks: true
+        });
+        converter.setFlavor('github');
+
+        function renderMarkdown() {
+            const markdownText = markdownInput.value;
+            const html = converter.makeHtml(markdownText);
+            previewOutput.innerHTML = html;
+        }
+
+        markdownInput.addEventListener('input', renderMarkdown);
+        renderMarkdown(); // Initial render
+
+        // --- Resizer Logic ---
+        let isResizing = false;
+        let startX, startEditorWidth, startPreviewWidth;
+
+        resizer.addEventListener('mousedown', (e) => {
+            e.preventDefault(); // Prevent text selection, etc.
+            isResizing = true;
+            startX = e.clientX;
+            startEditorWidth = markdownInput.offsetWidth;
+            startPreviewWidth = previewOutput.offsetWidth;
+
+            document.addEventListener('mousemove', handleMouseMove);
+            document.addEventListener('mouseup', stopResize);
+        });
+
+        function handleMouseMove(e) {
+            if (!isResizing) return;
+
+            const dx = e.clientX - startX;
+            let newEditorWidth = startEditorWidth + dx;
+            let newPreviewWidth = startPreviewWidth - dx;
+
+            const minWidth = 50; // Minimum width for panes
+
+            if (newEditorWidth < minWidth) {
+                newEditorWidth = minWidth;
+                newPreviewWidth = startEditorWidth + startPreviewWidth - minWidth;
+            } else if (newPreviewWidth < minWidth) {
+                newPreviewWidth = minWidth;
+                newEditorWidth = startEditorWidth + startPreviewWidth - minWidth;
+            }
+            
+            const totalWidth = editorContainer.offsetWidth - resizer.offsetWidth;
+            markdownInput.style.flex = `0 0 ${newEditorWidth}px`;
+            previewOutput.style.flex = `0 0 ${newPreviewWidth}px`;
+        }
+
+        function stopResize() {
+            isResizing = false;
+            document.removeEventListener('mousemove', handleMouseMove);
+            document.removeEventListener('mouseup', stopResize);
+        }
+
+        // --- Synchronized Scrolling ---
+        let scrollTimeout;
+        let isSyncingEditor = false;
+        let isSyncingPreview = false;
+
+        markdownInput.addEventListener('scroll', () => {
+            if (isSyncingEditor) {
+                isSyncingEditor = false; // Reset for next manual scroll
+                return;
+            }
+            if (isResizing) return; // Don't sync scroll while resizing
+
+            isSyncingPreview = true; // Prevent preview scroll from triggering editor scroll
+            
+            const scrollPercentage = markdownInput.scrollTop / (markdownInput.scrollHeight - markdownInput.clientHeight);
+            if (isNaN(scrollPercentage)) return; // Avoid NaN if scrollHeight equals clientHeight
+
+            previewOutput.scrollTop = scrollPercentage * (previewOutput.scrollHeight - previewOutput.clientHeight);
+            
+            // Clear any existing timeout to avoid race conditions
+            clearTimeout(scrollTimeout);
+            scrollTimeout = setTimeout(() => {
+                isSyncingPreview = false;
+            }, 100); // Adjust delay as needed
+        });
+
+        previewOutput.addEventListener('scroll', () => {
+            if (isSyncingPreview) {
+                isSyncingPreview = false; // Reset for next manual scroll
+                return;
+            }
+            if (isResizing) return; // Don't sync scroll while resizing
+
+            isSyncingEditor = true; // Prevent editor scroll from triggering preview scroll
+
+            const scrollPercentage = previewOutput.scrollTop / (previewOutput.scrollHeight - previewOutput.clientHeight);
+            if (isNaN(scrollPercentage)) return;
+
+            markdownInput.scrollTop = scrollPercentage * (markdownInput.scrollHeight - markdownInput.clientHeight);
+            
+            clearTimeout(scrollTimeout);
+            scrollTimeout = setTimeout(() => {
+                isSyncingEditor = false;
+            }, 100); // Adjust delay as needed
+        });
+
+
+        function exportToPng() {
+            const previewPane = document.getElementById('preview-output');
+            const originalHeight = previewPane.style.height;
+            const originalOverflowY = previewPane.style.overflowY;
+
+            previewPane.style.height = previewPane.scrollHeight + 'px';
+            previewPane.style.overflowY = 'visible';
+            previewPane.style.backgroundColor = '#000000';
+
+            html2canvas(previewPane, {
+                backgroundColor: '#000000',
+                scale: 2,
+                useCORS: true,
+                logging: false,
+                onclone: (documentClone) => {
+                    const clonedPreviewPane = documentClone.getElementById('preview-output');
+                    if (clonedPreviewPane) {
+                        clonedPreviewPane.style.backgroundColor = '#000000';
+                        clonedPreviewPane.style.color = '#ffffff';
+                        clonedPreviewPane.style.height = clonedPreviewPane.scrollHeight + 'px';
+                        clonedPreviewPane.style.overflowY = 'visible';
+                    }
+                }
+            }).then(canvas => {
+                const link = document.createElement('a');
+                link.download = 'markdown_card.png';
+                link.href = canvas.toDataURL('image/png');
+                link.click();
+            }).finally(() => {
+                previewPane.style.height = originalHeight;
+                previewPane.style.overflowY = originalOverflowY;
+            });
+        }
+    </script>
+</body>
+</html>

+ 146 - 0
libs/external/html-tools-main/xhs graphic production.html

@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>小红书内容卡片</title>
+    <style>
+        body {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            background: #f0f0f0;
+            margin: 0;
+            font-family: 'Arial', sans-serif;
+        }
+        .export-container {
+            background: #333333;
+            padding: 10px;
+            border-radius: 8px;
+        }
+        .card {
+            width: 320px;
+            background: #000000;
+            border: 2px solid #666666; /* 改为灰色边框 */
+            box-shadow: 4px 4px 0 #666666; /* 改为灰色阴影 */
+            padding: 20px;
+            box-sizing: border-box;
+        }
+        .title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #ffffff;
+            border-bottom: 2px solid #666666; /* 改为灰色边框 */
+            padding-bottom: 10px;
+            margin-bottom: 15px;
+            word-wrap: break-word;
+            background-color: #000000;
+        }
+        .content {
+            font-size: 16px;
+            color: #ffffff;
+            line-height: 1.5;
+            word-wrap: break-word;
+            background-color: #000000;
+        }
+        .editable {
+            position: relative;
+            min-height: 1em;
+        }
+        .editable:empty:before {
+            content: attr(data-placeholder);
+            color: #666666;
+            position: absolute;
+            pointer-events: none;
+        }
+        .editable:focus {
+            outline: none;
+            background-color: #000000;
+        }
+        [contenteditable] {
+            -webkit-user-select: text;
+            user-select: text;
+            background-color: #000000 !important;
+        }
+        [contenteditable]:focus {
+            background-color: #000000 !important;
+        }
+        ::selection {
+            background-color: #333333;
+            color: #ffffff;
+        }
+        .button-container {
+            margin-top: 20px;
+            text-align: center;
+        }
+        .export-btn {
+            padding: 10px 20px;
+            background: #000000;
+            color: #ffffff;
+            border: 2px solid #666666; /* 添加灰色边框保持一致性 */
+            cursor: pointer;
+            font-size: 16px;
+        }
+        .export-btn:hover {
+            background: #333333;
+        }
+    </style>
+</head>
+<body>
+    <div class="export-container" id="export-container">
+        <div id="card" class="card">
+            <div class="title editable" contenteditable="true" data-placeholder="点击输入标题"></div>
+            <div class="content editable" contenteditable="true" data-placeholder="点击输入内容"></div>
+        </div>
+    </div>
+    <div class="button-container">
+        <button class="export-btn" onclick="exportToPng()">导出为PNG</button>
+    </div>
+
+    <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
+    <script>
+        function exportToPng() {
+            // 清除选中状态
+            window.getSelection().removeAllRanges();
+            
+            const container = document.getElementById('export-container');
+            html2canvas(container, {
+                backgroundColor: '#333333',
+                scale: 2,
+                useCORS: true,
+                logging: false,
+                removeContainer: true,
+                foreignObjectRendering: false,
+                onclone: (document) => {
+                    const editables = document.querySelectorAll('.editable');
+                    editables.forEach(el => {
+                        // 确保克隆的元素保持相同的样式
+                        el.style.backgroundColor = '#000000';
+                        if (el.classList.contains('title')) {
+                            el.style.borderBottom = '2px solid #666666';
+                        }
+                    });
+                    // 确保卡片样式在克隆中保持一致
+                    const card = document.querySelector('.card');
+                    card.style.border = '2px solid #666666';
+                    card.style.boxShadow = '4px 4px 0 #666666';
+                }
+            }).then(canvas => {
+                const link = document.createElement('a');
+                link.download = 'xiaohongshu_card.png';
+                link.href = canvas.toDataURL('image/png');
+                link.click();
+            });
+        }
+
+        // 监听编辑事件,保持样式一致性
+        document.querySelectorAll('.editable').forEach(element => {
+            element.addEventListener('input', function() {
+                this.style.backgroundColor = '#000000';
+            });
+        });
+    </script>
+</body>
+</html>