refactor: extensions - remove three standalone HTML tools (#202)

This commit is contained in:
tradecatlabs
2026-10-22 05:50:18 +08:00
committed by GitHub
parent 44503a69db
commit b0e3454bb8
7 changed files with 3 additions and 1040 deletions
-3
View File
@@ -3,9 +3,6 @@
.github/extensions/claude-official-skills/** linguist-vendored
.github/extensions/tmux/** linguist-vendored
.github/extensions/oh-my-tmux/** linguist-vendored
.github/extensions/clean_epub_css.html linguist-vendored
.github/extensions/markdown-bianjiqi.html linguist-vendored
".github/extensions/task card generator.html" linguist-vendored
# 原始捕获不规范化上游空白。
.github/context/research/**/raw/*.raw.* -whitespace
+1 -16
View File
@@ -436,10 +436,7 @@
".github/tools/prompts-library/README.md": ".github/extensions/prompts-library/README.md",
".github/tools/prompts-library/main.py": ".github/extensions/prompts-library/main.py",
".github/skills/auto-skill/README.md": ".github/skills/auto-skill/README.md",
".github/skills/auto-tmux/README.md": ".github/skills/auto-tmux/README.md",
".github/tools/external/html-tools-main/clean_epub_css.html": ".github/extensions/clean_epub_css.html",
".github/tools/external/html-tools-main/markdown-bianjiqi.html": ".github/extensions/markdown-bianjiqi.html",
".github/tools/external/html-tools-main/task card generator.html": ".github/extensions/task card generator.html"
".github/skills/auto-tmux/README.md": ".github/skills/auto-tmux/README.md"
},
"live_sources": {
".github/skills/auto-skill/README.md": {
@@ -526,18 +523,6 @@
"path": ".github/extensions/claude-official-skills/skills/skill-creator/scripts/init_skill.py",
"sha256": "cf12790f1d4958799281920d599f56aea38fb5c26e2812a61dd740bf3d4b6b99"
},
".github/tools/external/html-tools-main/clean_epub_css.html": {
"path": ".github/extensions/clean_epub_css.html",
"sha256": "1716a7ea5f4bf4837bc044c9c164e58b3dbb04d492f677b7766b506edd1c0d2a"
},
".github/tools/external/html-tools-main/markdown-bianjiqi.html": {
"path": ".github/extensions/markdown-bianjiqi.html",
"sha256": "e4c76015749bf42e898412ec7b8e1229c847575ef740bcf6900311d7212dc61e"
},
".github/tools/external/html-tools-main/task card generator.html": {
"path": ".github/extensions/task card generator.html",
"sha256": "0eba89d2f674bb0bc26690c4e406f99b7a2bcd40ecd838c1c19e1f970f7b5670"
},
".github/tools/external/tmux/COPYING": {
"path": ".github/extensions/tmux/COPYING",
"sha256": "c031bd37f464c534277814f6aa38686fa023d094261d57fd2545ad592bb53ccd"
+1 -4
View File
@@ -2,7 +2,7 @@
## 字多不看
这里保存现存程序、资料维护脚本与固定上游;当前13个一级对象在本域平铺,程序内部源码结构不拆散。
这里保存现存程序、资料维护脚本与固定上游;当前10个一级对象在本域平铺,程序内部源码结构不拆散。
这里的Extensions不是Pi原生扩展的同义词;不能把整个目录交给Pi的`-e`加载。
## 快速导航
@@ -14,9 +14,6 @@
| [Skill初始化入口](init_skill.py) | 引用上游初始化脚本;不是Pi Skill入口或自动安装器 |
| [tmux](tmux/README)、[oh-my-tmux](oh-my-tmux/README.md) | 原固定源码与配置仓库;不是自动安装或启用 |
| [提示词转换](prompts-library/README.md) | 原Excel、Markdown、JSONL转换程序 |
| [EPUB CSS清理](clean_epub_css.html) | 保留的HTML工具文件 |
| [Markdown编辑](markdown-bianjiqi.html) | 保留的HTML工具文件 |
| [任务卡生成](task%20card%20generator.html) | 保留的HTML工具文件 |
<details>
<summary><strong>完整细粒度目录(点击展开/收起)</strong></summary>
-424
View File
@@ -1,424 +0,0 @@
<!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
View File
@@ -1,303 +0,0 @@
<!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
View File
@@ -1,289 +0,0 @@
<!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>
+1 -1
View File
@@ -24,7 +24,7 @@
Context公开维护8个集合:`gongfa`、`faqi`、`getting-started`、`prompts`、`external-resources`、`ai-citation`、`metadata`、`assets`。研究档案只在已有本机副本保留,不进入GitHub当前文件树。
当前2个自有Skill和现有Extensions清单分别在其README,不另建工具注册层;16个官方引用入口已按用户要求移除。
用户点名的chat-vault、my-nvim、codex-config、MCPlayerTransfer、XHS-image-to-PDF-conversion、两个xhs图文HTML及html-tools-main说明已移除;当前入口和绑定清理,旧来源/初审/排除保留,历史链接不是安装授权。
用户点名的chat-vault、my-nvim、codex-config、MCPlayerTransfer、XHS-image-to-PDF-conversion、两个xhs图文HTML、html-tools-main说明及三份独立HTML工具均已从当前树移除;活动入口与位置绑定清理,原始快照/初审/排除保留,历史链接不是安装授权。
Lambda演算已按用户要求加入功法,品级天级上品(现行`tian-advanced`),总表及首页学习入口列在拼好码之前;冻结原文与既有评级不变,不新增解释器或执行器。
## 原始数据与来源