亚伦影音工作室 发表于 2025-9-27 09:01

黑黑贴宝是你做音画的好帮手!

本帖最后由 亚伦影音工作室 于 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>

亚伦影音工作室 发表于 2025-9-27 09:08

帖宝属于单文件,没有任何第三方资源依赖,追求的是简洁易用。其定位为做HTML帖子、写HTML文章,希望能帮到喜欢制作音画作品、写HTML文章的朋友。

老谟深虑 发表于 2025-9-27 17:10

         谢谢老师,收藏了。

小辣椒 发表于 2025-9-27 22:16

这个还不会用{:4_170:}
页: [1]
查看完整版本: 黑黑贴宝是你做音画的好帮手!