黑黑贴宝是你做音画的好帮手!
本帖最后由 亚伦影音工作室 于 2025-9-27 10:00 编辑 <br /><br /><style>.blackhole {margin: 10px-300px ;
position: relative;
width: 1200px;
height: 720px;
overflow: hidden;
}
:root { --menuBg: rgba(0,0,0,.65); --menuColor: rgb(250,255,255); --editorBG: rgba(245,245,220,.5); editorColor: rgb(0,0,0); caret-color: red;}
body { background: white; display: grid; place-items: center; }
body::selection { background: #000; color: yellow; }
iframe { position: relative; width: 100%; height: 100%; border: none; outline: none; box-sizing: border-box; margin: 0; }
input { margin: 0 4px; padding: 4px 6px; font-size: 16px; }
#articleTitle { width: 16em; padding: 4px 6px ; text-align: right; color: white; background: transparent; border: none; cursor: pointer; }
#editor { flex-grow: 1; padding: 8px; background: var(--editorBG); color: var(--editorColor); border: none; outline: none; resize: none; word-break: break-word; font: normal 18px/24px Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; tab-size: 4; overflow-x: hidden; overflow-y: auto; }
#listContainer { position: absolute; display: none; background: white; width: 100%; height: 100%; box-shadow: 4px 4px 18px rgba(0,0,0,.5); }
#articleList { position: absolute; padding: 10px 20px; width: 97%; height: 91%; overflow: hidden auto; column-count: 3; column-gap: 8px; column-fill: balance; }
#showBox { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: white; display: none; padding: 0; overflow: hidden; z-index: 100; margin: 0; }
#showBox::after { position: absolute; content: '关闭预览'; bottom: 10px; left: calc(50% - 40px); padding: 0 4px; width: 80px; height: 30px; line-height: 30px; color: darkred; text-align: center; border: 1px solid #efe; border-radius: 6px; background: #eee; font-size: 14px; box-shadow: 2px 2px 6px rgba(0,0,0,.25); cursor: pointer; }
#importFile { display: none; }
.containe{ display: flex; flex-direction: column; margin: 0 auto; padding: 0; width: clamp(600px,98vw,1200px); min-height: 98vh; overflow: hidden auto; box-shadow: 4px 4px 18px rgba(0,0,0,.8); border-radius: 6px; background:url(https://pic1.imgdb.cn/item/683ae68458cb8da5c81eb3bd.jpg) no-repeat center / cover;}
.menu { margin: 0; padding: 10px; background: var(--menuBg); color: var(--menuColor); display: flex; justify-content: flex-start; align-items: center; gap: 10px; }
.btn { padding: 4px 8px; border-radius: 4px; user-select: none; cursor: pointer; }
.btn:hover { background: rgba(255,255,255,.25); outline: 1px solid rgba(255,255,255,.9); }
.grow { flex-grow: 1; }
.articleItem { position: relative; padding: 8px 16px; margin: 0 0 5px 10px; cursor: pointer; border-radius: 4px; }
.articleItem:hover { color: red; }
.articleItem::before { position: absolute; content: '❌'; left: -20px; width: 20px; background: none; display: none; }
.articleItem:hover::before { display: block; }
.invisibled { display: none; }
</style>
<div class="blackhole">
<div id="showBox"></div>
<div id="listContainer">
<div class="menu">
<span id="btnImport" class="btn" title="导入数据">导入</span>
<span id="btnExport" class="btn" title="导出数据">导出</span>
<label for="searchInput">检索</label>
<input type="text" id="searchInput" placeholder="关键词" />
<span id="artNum">0</span>
<input type="file" id="importFile" accept=".json" />
<span class="grow"></span>
<span id="btnShut" class="btn" title="关闭列表">❌</span>
</div>
<div id="articleList">Data List</div>
</div>
<div class="containe">
<div class="menu">
<span id="btnNewArticle" class="btn" title="创建新文章">新建</span>
<span id="btnOpenArticle" class="btn" title="打开以往文章">打开</span>
<span id="btnSaveArticle" class="btn" title="保存当前文章">保存</span>
<span id="btnPrev" class="btn" title="预览当前文章">预览</span>
<span class="grow"></span>
<input type="text" id="articleTitle" value="给音画署名" title="编辑标题" />
</div>
<textarea id="editor" placeholder="输入代码..." autofocus></textarea>
<div class="menu">
<span id="lineMsg" class="btn">行 1 / 1 列 0</span>
<span class="grow"></span>
<span id="btnDelArticle" class="btn" title="保存当前文章">删除</span>
</div>
</div>
</div>
<script>
const editor = document.getElementById('editor');
const btnPrev = document.getElementById('btnPrev');
const listContainer = document.getElementById('listContainer');
const showBox = document.getElementById('showBox');
const btnShut = document.getElementById('btnShut');
const btnNewArticle = document.getElementById('btnNewArticle');
const btnOpenArticle = document.getElementById('btnOpenArticle');
const btnDelArticle = document.getElementById('btnDelArticle');
const btnSaveArticle = document.getElementById('btnSaveArticle');
const btnImport = document.getElementById('btnImport');
const btnExport = document.getElementById('btnExport');
const articleTitle = document.getElementById('articleTitle');
const articleList = document.getElementById('articleList');
const importFile = document.getElementById('importFile');
const artNum = document.getElementById('artNum');
//数据库
const DB_NAME = 'WritingAppDB';
const DB_VERSION = 1;
const STORE_NAME = 'articles';
// 全局变量
let db = null;
let currentArticleId = null;
//初始化数据库
const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error('数据库打开失败:', event.target.error);
reject('数据库打开失败');
};
request.onsuccess = (event) => {
db = event.target.result;
//加载上次编辑的文章
const lastArticleId = localStorage.getItem('lastEditedArticleId');
if (lastArticleId) {
getArticleById(parseInt(lastArticleId)).then(article => {
if (article) {
loadArticle(article.id);
}
resolve(db);
}).catch(() => resolve(db));
} else {
resolve(db);
}
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('title', 'title', { unique: false });
}
};
});
}
//获取所有文章
const getAllArticles = () => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = (event) => {
const articles = event.target.result.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt);
});
resolve(articles);
};
request.onerror = (event) => {
console.error('获取文章列表失败:', event.target.error);
reject('获取文章列表失败');
};
});
};
//根据ID获取文章
const getArticleById = (id) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
console.error('获取文章失败:', event.target.error);
reject('获取文章失败');
};
});
};
//文章列表
const renderArticleList = () => {
getAllArticles().then(articles => {
articleList.innerHTML = '';
if (articles.length === 0) {
articleList.innerHTML = '<p>文章仓库空空如也</p>';
return;
}
const fragment = new DocumentFragment();
articles.forEach((article, idx) => {
const articleElement = document.createElement('div');
articleElement.className = 'articleItem';
const timeStr = article.updatedAt;
articleElement.textContent = `${idx + 1}. ${article.title}(${timeStr})`;
articleElement.dataset.id = article.id;
articleElement.addEventListener('click', (e) => {
e.offsetX > 0
? (loadArticle(article.id), shutArticles())
: deleteArticleByID(article.id, article.title);
});
articleElement.addEventListener('mousemove', (e) => {
articleElement.title = e.offsetX > 0 ? '打开' : '删除';
});
fragment.appendChild(articleElement);
});
articleList.appendChild(fragment);
artNum.textContent = `${articles.length} 篇`;
});
};
//加载文章到编辑器
const loadArticle = (id) => {
getArticleById(id).then(article => {
if (!article) return;
currentArticleId = article.id;
articleTitle.value = article.title;
editor.value = article.content;
editor.focus();
document.title = '帖宝: ' + article.title;
calcLines();
});
};
//删除文章
const deleteArticle = (id) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => {
document.title = '帖宝: 未命名';
currentArticleId = null; //清空当前文章id
resolve();
};
request.onerror = (event) => {
console.error('删除文章失败:', event.target.error);
reject('删除文章失败');
};
});
};
//删除当前文章
const deleteCurrentArticle = () => {
if (!currentArticleId) {
alert('没有选择要删除的文章');
return;
}
const title = articleTitle.value || '未命名';
if (confirm(`确定要删除『${title}』吗?`)) {
deleteArticle(currentArticleId).then(() => {
createNewArticle();
renderArticleList();
});
}
};
//根据id删除文章
const deleteArticleByID = (id, title) => {
if (confirm(`确定要删除『${title}』吗?`)) {
deleteArticle(id).then(() => {
if (id === currentArticleId) createNewArticle();
renderArticleList();
});
}
};
//更新文章
const updateArticle = (article) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(article);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('更新文章失败:', event.target.error);
reject('更新文章失败');
};
});
};
//添加文章到数据库
const addArticle = (article) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.add(article);
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
console.error('添加文章失败:', event.target.error);
reject('添加文章失败');
};
});
};
//保存当前文章
const saveCurrentArticle = () => {
const title = articleTitle.value.trim() || '未命名';
const content = editor.value.trim();
const article = {
title: title,
content: content,
updatedAt: new Date().toLocaleString()
};
if (currentArticleId) {
article.id = currentArticleId;
updateArticle(article).then(() => {
renderArticleList();
});
} else {
addArticle(article).then(id => {
currentArticleId = id;
renderArticleList();
});
}
articleTitle.value = article.title;
document.title = '帖宝: ' + article.title;
editor.focus();
};
//新建文章
const createNewArticle = () => {
if (currentArticleId && editor.value.trim() !== '') saveCurrentArticle();
currentArticleId = null;
editor.value = '';
articleTitle.value = '未命名';
calcLines();
};
//导出数据
const exportAllArticles = () => {
getAllArticles().then(articles => {
if (articles.length === 0) {
alert('没有文章可导出');
return;
}
const dataStr = JSON.stringify(articles, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileName = `帖宝_${new Date().toISOString().slice(0, 10)}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileName);
linkElement.click();
});
};
//处理文章导入
const handleFileImport = (event) => {
const file = event.target.files;
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const articles = JSON.parse(e.target.result);
if (Array.isArray(articles) && articles.length > 0) {
importArticles(articles);
} else {
alert('导入的文件不包含有效文章数据');
}
} catch (error) {
alert('解析JSON文件失败: ' + error.message);
}
};
reader.readAsText(file);
event.target.value = '';
};
//导入文章到数据库
const importArticles = (articles) => {
const clearFirst = confirm('导入前是否清空现有文章?选择"取消"将保留现有文章并添加新文章。');
const transaction = db.transaction(, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
if (clearFirst) {
const clearRequest = store.clear();
clearRequest.onsuccess = () => {
addArticlesInBatch(store, articles);
};
} else {
addArticlesInBatch(store, articles);
}
};
//批量添加文章
const addArticlesInBatch = (store, articles) => {
let addedCount = 0;
let skippedCount = 0;
articles.forEach(article => {
delete article.id;
const request = store.add(article);
request.onsuccess = () => {
addedCount++;
};
request.onerror = () => {
skippedCount++;
};
});
store.transaction.oncomplete = () => {
renderArticleList();
alert(`导入完成!\n成功导入: ${addedCount}篇\n跳过: ${skippedCount}篇`);
};
};
//tab和回车输入
const insetIndent = (ele,e) => {
e.preventDefault();
let indent = '';
if (e.keyCode === 13) {
const end = ele.selectionEnd;
const tstr = ele.value.substring(0, end);
const strAr = tstr.split('\n');
const target = strAr;
indent = '\n' + target.match(/^\s*/) || '';
} else if (e.keyCode === 9) {
indent = '\t';
}
ele.setRangeText(indent);
ele.selectionStart += indent.length;
};
//行列信息
const getLineMsg = (textarea) => {
const text = textarea.value;
const total = text.split('\n').length;
const cursorPos = textarea.selectionStart;
const lines = text.substr(0, cursorPos).split('\n');
const line = lines.length;
const column = lines.length;
return {total: total, line: line, column: column};
};
//预览
const preView = (htmlCode, targetBox) => {
if (!htmlCode) return;
const iframe = document.createElement('iframe');
htmlCode = htmlCode + '\n<style>body {margin: 0; }</style>\n';
iframe.srcdoc = htmlCode;
targetBox.appendChild(iframe);
targetBox.style.display = 'block';
targetBox.onclick = () => {
targetBox.innerHTML = '';
targetBox.style.display = 'none';
editor.focus();
}
};
//搜索文章
const searchArticles = () => {
const articleItems = document.querySelectorAll('.articleItem');
const keyword = searchInput.value;
let total = 0;
articleItems.forEach(item => item.classList.remove('invisibled'));
for (let j = 0; j < articleItems.length; j++) {
if (articleItems.textContent.indexOf(keyword) === -1) {
articleItems.classList.add('invisibled');
} else {
total ++;
}
}
artNum.textContent = `${total} 篇`;
};
//打开文章列表
const showArticles = () => {
if (currentArticleId && editor.value.trim()!== '') saveCurrentArticle();
listContainer.style.display = 'block';
}
//关闭文章列表
const shutArticles = () => {
listContainer.style.display = 'none';
searchInput.value = '';
renderArticleList();
editor.focus();
};
//显示行列信息
const calcLines = () => {
const res = getLineMsg(editor);
lineMsg.textContent = `行 ${res.line} / ${res.total} 列 ${res.column}`;
};
//保存最后操作文档id
const saveCurrentState = () => {
localStorage.setItem('lastEditedArticleId', currentArticleId);
}
// 事件监听
const setupEventListeners = () => {
btnNewArticle.addEventListener('click', createNewArticle);
btnOpenArticle.addEventListener('click', showArticles);
btnSaveArticle.addEventListener('click', saveCurrentArticle);
btnDelArticle.addEventListener('click', deleteCurrentArticle);
btnExport.addEventListener('click', exportAllArticles);
btnImport.addEventListener('click', () => importFile.click());
importFile.addEventListener('change', handleFileImport);
btnShut.addEventListener('click', shutArticles);
btnPrev.addEventListener('click', () => {
preView(editor.value, showBox);
saveCurrentArticle();
});
editor.addEventListener('keydown', (e) => {
if (e.keyCode === 9 || e.keyCode === 13) {
insetIndent(editor,e);
}
});
editor.addEventListener('keyup', calcLines);
editor.addEventListener('click', calcLines);
searchInput.addEventListener('input', searchArticles);
articleTitle.addEventListener('mouseenter', () => {
articleTitle.focus();
articleTitle.select();
});
window.addEventListener('beforeunload', saveCurrentState);
};
//初始化
document.addEventListener('DOMContentLoaded', () => {
initDB().then(() => {
renderArticleList();
setupEventListeners();
});
});
</script>
帖宝属于单文件,没有任何第三方资源依赖,追求的是简洁易用。其定位为做HTML帖子、写HTML文章,希望能帮到喜欢制作音画作品、写HTML文章的朋友。 谢谢老师,收藏了。
这个还不会用{:4_170:}
页:
[1]