MD编辑器更新

UI更新
This commit is contained in:
2026-09-23 14:07:19 +08:00
parent 429e8a7bb7
commit ec21ba0876
379 changed files with 15504 additions and 606 deletions
+82 -596
View File
@@ -3,188 +3,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>笔记管理</title>
<style>
:root {
--brand: #C8956E;
--brand-light: #FFF6EC;
--text: #2B2B2B;
--text-dim: #8A8078;
--border: #EAE3DC;
--bg: #FAF8F5;
--danger: #D9534F;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px; line-height: 1.6;
}
button {
font-family: inherit; font-size: 14px; cursor: pointer;
border: 1px solid var(--border); background: #fff; color: var(--text);
padding: 7px 14px; border-radius: 8px; transition: .15s;
}
button:hover { border-color: var(--brand); color: var(--brand); }
button.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
button.primary:hover { opacity: .88; color: #fff; }
button.danger:hover { border-color: var(--danger); color: var(--danger); }
input, textarea {
font-family: inherit; font-size: 14px; color: var(--text);
border: 1px solid var(--border); border-radius: 8px;
padding: 8px 12px; background: #fff; outline: none; width: 100%;
}
input:focus, textarea:focus { border-color: var(--brand); }
/* ---------- 登录 ---------- */
#login-view {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
}
.login-box {
width: 340px; background: #fff; padding: 36px 32px;
border-radius: 16px; border: 1px solid var(--border);
box-shadow: 0 8px 32px rgba(0,0,0,.05);
}
.login-box h1 { margin: 0 0 4px; font-size: 20px; }
.login-box p.sub { margin: 0 0 24px; color: var(--text-dim); font-size: 13px; }
.login-box .field { margin-bottom: 14px; }
.login-box .field label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--text-dim); }
.login-box button { width: 100%; margin-top: 8px; padding: 10px; }
/* ---------- 主界面 ---------- */
header {
background: #fff; border-bottom: 1px solid var(--border);
padding: 14px 24px; display: flex; align-items: center; gap: 16px;
position: sticky; top: 0; z-index: 10;
}
header h1 { margin: 0; font-size: 17px; font-weight: 600; white-space: nowrap; }
header .search { flex: 1; max-width: 380px; }
header .spacer { flex: 1; }
header .who { color: var(--text-dim); font-size: 13px; white-space: nowrap; }
.layout { display: flex; gap: 24px; padding: 24px; align-items: flex-start; max-width: 1200px; margin: 0 auto; }
aside.tags {
width: 180px; flex-shrink: 0; background: #fff; border: 1px solid var(--border);
border-radius: 12px; padding: 16px; position: sticky; top: 84px;
}
aside.tags h3 { margin: 0 0 12px; font-size: 13px; color: var(--text-dim); font-weight: 500; }
.tag-chip {
display: block; width: 100%; text-align: left; border: none; background: transparent;
padding: 6px 10px; border-radius: 6px; margin-bottom: 2px; color: var(--text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tag-chip:hover { background: var(--brand-light); color: var(--brand); }
.tag-chip.active { background: var(--brand); color: #fff; }
.tag-chip.active:hover { background: var(--brand); color: #fff; }
main { flex: 1; min-width: 0; }
#dropzone {
border: 2px dashed var(--border); border-radius: 12px; padding: 20px;
text-align: center; color: var(--text-dim); margin-bottom: 20px;
background: #fff; transition: .15s;
}
#dropzone.over { border-color: var(--brand); background: var(--brand-light); color: var(--brand); }
.note-card {
background: #fff; border: 1px solid var(--border); border-radius: 12px;
padding: 16px 18px; margin-bottom: 12px; transition: .15s;
}
.note-card:hover { border-color: var(--brand); box-shadow: 0 4px 16px rgba(0,0,0,.04); }
.note-card .head { display: flex; align-items: baseline; gap: 8px; }
.note-card h3 {
margin: 0; font-size: 15px; font-weight: 600; flex: 1;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.note-card .pin { color: var(--brand); font-size: 13px; }
.note-card .summary {
color: var(--text-dim); font-size: 13px; margin: 6px 0 10px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.note-card .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.note-card .tag {
background: var(--brand-light); color: var(--brand);
padding: 2px 8px; border-radius: 4px; font-size: 12px;
}
.note-card .time { color: var(--text-dim); font-size: 12px; margin-left: auto; }
.note-card .actions { display: flex; gap: 6px; margin-left: 12px; }
.note-card .actions button { padding: 3px 10px; font-size: 12px; }
.empty { text-align: center; color: var(--text-dim); padding: 60px 0; }
#loadmore { width: 100%; margin-top: 4px; padding: 10px; }
/* ---------- 编辑器 ---------- */
#editor {
position: fixed; inset: 0; background: rgba(0,0,0,.4);
display: none; align-items: center; justify-content: center; z-index: 100; padding: 24px;
}
#editor.open { display: flex; }
.editor-box {
background: #fff; border-radius: 16px; width: 100%; max-width: 1000px;
height: 100%; max-height: 760px; display: flex; flex-direction: column; overflow: hidden;
}
.editor-head {
padding: 14px 20px; border-bottom: 1px solid var(--border);
display: flex; gap: 12px; align-items: center;
}
.editor-head input.title { flex: 1; font-size: 15px; font-weight: 600; }
.editor-head input.tags { width: 220px; }
.editor-body { flex: 1; display: flex; min-height: 0; }
.editor-body textarea {
flex: 1; border: none; border-radius: 0; resize: none;
padding: 18px 20px; font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: 13px; line-height: 1.7;
}
.editor-body textarea:focus { border: none; }
.editor-body .preview {
flex: 1; overflow-y: auto; padding: 18px 20px; border-left: 1px solid var(--border); background: #FCFBF9;
}
.editor-foot {
padding: 12px 20px; border-top: 1px solid var(--border);
display: flex; gap: 10px; align-items: center;
}
.editor-foot .hint { color: var(--text-dim); font-size: 12px; margin-right: auto; }
/* Markdown 预览样式 */
.preview h1, .preview h2, .preview h3 { margin: 18px 0 10px; line-height: 1.3; }
.preview h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.preview h2 { font-size: 18px; }
.preview h3 { font-size: 15px; }
.preview p { margin: 10px 0; }
.preview code {
background: #F2EEE9; padding: 2px 5px; border-radius: 4px;
font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
}
.preview pre {
background: #2B2B2B; color: #F0EDEA; padding: 14px; border-radius: 8px; overflow-x: auto;
}
.preview pre code { background: none; color: inherit; padding: 0; }
.preview blockquote {
border-left: 3px solid var(--brand); margin: 10px 0; padding: 2px 14px; color: var(--text-dim);
}
.preview table { border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 13px; }
.preview th, .preview td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.preview th { background: var(--brand-light); }
.preview img { max-width: 100%; border-radius: 6px; }
.preview a { color: var(--brand); }
#toast {
position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%);
background: #2B2B2B; color: #fff; padding: 10px 20px; border-radius: 8px;
font-size: 13px; opacity: 0; pointer-events: none; transition: .25s; z-index: 999;
}
#toast.show { opacity: 1; }
#toast.err { background: var(--danger); }
</style>
<title>强宝小助手</title>
<link rel="stylesheet" href="/admin/vendor/vditor/dist/index.css">
<link rel="stylesheet" href="/admin/admin.css">
</head>
<body>
<!-- ==================== 登录 ==================== -->
<div id="login-view">
<div class="login-box">
<h1>笔记管理</h1>
<p class="sub">使用手机端同一个账号登录</p>
<h1>强宝小助手</h1>
<p class="sub">强宝管理小助手</p>
<div class="field">
<label>用户名</label>
<input id="login-username" autocomplete="username" placeholder="用户名">
@@ -197,31 +26,73 @@
</div>
</div>
<!-- ==================== 主界面 ==================== -->
<!-- ==================== 主界面 ====================
三层结构:
顶栏 —— 系统级,只放品牌和用户操作(跨模块的东西才配放这儿)
模块菜单 —— 左侧,以后加 Agent 等模块直接往这里塞
模块面板 —— 当前模块自己的工具栏、侧栏和内容
-->
<div id="app-view" style="display:none">
<header>
<h1>笔记管理</h1>
<div class="search"><input id="search" placeholder="搜索标题和正文…"></div>
<header class="topbar">
<div class="brand"><span class="brand-icon">🐻</span>强宝小助手</div>
<div class="spacer"></div>
<span class="who" id="who"></span>
<button id="new-btn" class="primary">+ 新建</button>
<button id="logout-btn">退出</button>
</header>
<div class="layout">
<aside class="tags">
<h3>标签</h3>
<div id="tag-list"></div>
</aside>
<div class="console">
<main>
<div id="dropzone">
把 <b>.md</b> 文件拖到这里批量导入,或 <a href="#" id="pick-files">点击选择文件</a>
<input type="file" id="file-input" accept=".md,.markdown,.txt" multiple style="display:none">
<nav class="module-nav">
<button class="module-item active" data-module="notes">
<span class="mi-icon">📝</span>
<span class="mi-text">笔记管理</span>
</button>
<!-- 以后的模块(Agent、数据看板…)往这里加 -->
</nav>
<section class="module-panel">
<div class="toolbar">
<input id="search" placeholder="搜索标题和正文…">
<select id="scope" title="搜索范围">
<option value="current" selected>当前目录</option>
<option value="all">全部笔记</option>
</select>
<select id="sort" title="排序方式">
<option value="updated">最近更新</option>
<option value="created">创建时间</option>
<option value="title">标题</option>
</select>
<div class="spacer"></div>
<button id="new-folder-btn">+ 新建目录</button>
<button id="new-btn" class="primary">+ 新建笔记</button>
</div>
<div id="note-list"></div>
<button id="loadmore" style="display:none">加载更多</button>
</main>
<div class="module-body">
<aside class="sidebar">
<div id="folder-tree"></div>
<div class="side-divider"></div>
<div class="side-title">标签</div>
<div id="tag-list"></div>
</aside>
<main class="content">
<div class="breadcrumb" id="breadcrumb"></div>
<div id="dropzone">
把 <b>.md</b> 文件拖到这里批量导入<span id="dropzone-hint"></span>,或 <a href="#" id="pick-files">点击选择文件</a>
<input type="file" id="file-input" accept=".md,.markdown,.txt" multiple style="display:none">
</div>
<div id="note-list"></div>
<button id="loadmore" style="display:none">加载更多</button>
</main>
</div>
</section>
</div>
</div>
@@ -233,418 +104,33 @@
<input class="tags" id="ed-tags" placeholder="标签,逗号分隔">
</div>
<div class="editor-body">
<textarea id="ed-content" placeholder="在这里写 Markdown…"></textarea>
<div class="preview" id="ed-preview"></div>
<div id="vditor"></div>
</div>
<div class="editor-foot">
<span class="hint" id="ed-hint"></span>
<button id="ed-upload">插入图片</button>
<input type="file" id="ed-image" accept="image/*" style="display:none">
<button id="ed-cancel">取消</button>
<button class="primary" id="ed-save">保存</button>
<button class="primary" id="ed-save">保存 (Ctrl+S)</button>
</div>
</div>
</div>
<!-- ==================== 目录名输入 / 目录选择弹窗 ==================== -->
<div id="dialog">
<div class="dialog-box">
<h3 id="dialog-title">新建目录</h3>
<input id="dialog-input" placeholder="目录名">
<select id="dialog-select" style="display:none"></select>
<div class="dialog-foot">
<button id="dialog-cancel">取消</button>
<button class="primary" id="dialog-ok">确定</button>
</div>
</div>
</div>
<div id="toast"></div>
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<script>
(function () {
'use strict';
var API = '/api';
var TOKEN_KEY = 'aq_note_admin_token';
var USER_KEY = 'aq_note_admin_user';
var state = {
token: localStorage.getItem(TOKEN_KEY) || '',
username: localStorage.getItem(USER_KEY) || '',
page: 1,
size: 20,
keyword: '',
tag: '',
total: 0,
editingId: null
};
// ---------------------------------------------------------------- 工具
function $(id) { return document.getElementById(id); }
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function toast(msg, isError) {
var el = $('toast');
el.textContent = msg;
el.className = (isError ? 'err ' : '') + 'show';
clearTimeout(el._t);
el._t = setTimeout(function () { el.className = ''; }, 2400);
}
function formatTime(s) {
if (!s) return '';
return String(s).replace('T', ' ').substring(0, 16);
}
/** 统一的请求封装,带 token 和 401 处理 */
function request(path, options) {
options = options || {};
var headers = options.headers || {};
if (state.token) headers['Authorization'] = 'Bearer ' + state.token;
if (options.body && !(options.body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
}
return fetch(API + path, {
method: options.method || 'GET',
headers: headers,
body: options.body
}).then(function (res) {
if (res.status === 401 || res.status === 403) {
doLogout();
throw new Error('登录已过期,请重新登录');
}
var ct = res.headers.get('content-type') || '';
var parsed = ct.indexOf('application/json') >= 0 ? res.json() : res.text();
return parsed.then(function (data) {
if (!res.ok) {
var msg = (data && (data.message || data.error)) || ('请求失败 ' + res.status);
throw new Error(msg);
}
return data;
});
});
}
// ---------------------------------------------------------------- 登录
function showApp() {
$('login-view').style.display = 'none';
$('app-view').style.display = '';
$('who').textContent = state.username ? ('当前:' + state.username) : '';
state.page = 1;
state.keyword = '';
state.tag = '';
$('search').value = '';
loadTags();
loadNotes(true);
}
function doLogout() {
state.token = '';
state.username = '';
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
$('app-view').style.display = 'none';
$('login-view').style.display = 'flex';
$('note-list').innerHTML = '';
$('tag-list').innerHTML = '';
}
function doLogin() {
var username = $('login-username').value.trim();
var password = $('login-password').value;
if (!username || !password) { toast('请填写用户名和密码', true); return; }
fetch(API + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username, password: password })
}).then(function (res) {
return res.json().then(function (data) {
if (!res.ok || !data.token) {
throw new Error(data.message || data.error || '用户名或密码错误');
}
return data;
});
}).then(function (data) {
state.token = data.token;
state.username = data.username || username;
localStorage.setItem(TOKEN_KEY, state.token);
localStorage.setItem(USER_KEY, state.username);
$('login-password').value = '';
showApp();
}).catch(function (e) {
toast(e.message, true);
});
}
// ---------------------------------------------------------------- 列表
function loadNotes(reset) {
if (reset) { state.page = 1; }
var qs = '?page=' + state.page + '&size=' + state.size;
if (state.keyword) qs += '&keyword=' + encodeURIComponent(state.keyword);
if (state.tag) qs += '&tag=' + encodeURIComponent(state.tag);
request('/notes' + qs).then(function (data) {
state.total = data.total;
renderNotes(data.list, reset);
$('loadmore').style.display = data.hasMore ? 'block' : 'none';
if (data.list.length) state.page++;
}).catch(function (e) { toast(e.message, true); });
}
function renderNotes(list, reset) {
var box = $('note-list');
if (reset) {
if (!list.length) {
box.innerHTML = '<div class="empty">' +
(state.keyword || state.tag ? '没有匹配的笔记' : '还没有笔记,拖个 .md 文件进来试试') +
'</div>';
return;
}
box.innerHTML = '';
}
list.forEach(function (n) { box.appendChild(noteCard(n)); });
}
function noteCard(n) {
var el = document.createElement('div');
el.className = 'note-card';
var tags = (n.tagList || []).map(function (t) {
return '<span class="tag">' + esc(t) + '</span>';
}).join('');
el.innerHTML =
'<div class="head">' +
'<h3>' + esc(n.title) + '</h3>' +
(n.isPinned ? '<span class="pin">📌</span>' : '') +
'</div>' +
'<div class="summary">' + esc(n.summary || '(空笔记)') + '</div>' +
'<div class="meta">' + tags +
'<span class="time">' + esc(formatTime(n.updateTime)) + '</span>' +
'<span class="actions">' +
'<button data-act="pin">' + (n.isPinned ? '取消置顶' : '置顶') + '</button>' +
'<button data-act="edit">编辑</button>' +
'<button class="danger" data-act="del">删除</button>' +
'</span>' +
'</div>';
el.querySelector('[data-act="edit"]').onclick = function () { openEditor(n.id); };
el.querySelector('[data-act="del"]').onclick = function () { removeNote(n); };
el.querySelector('[data-act="pin"]').onclick = function () { togglePin(n); };
return el;
}
function removeNote(n) {
if (!confirm('确定删除「' + n.title + '」?')) return;
request('/notes/' + n.id, { method: 'DELETE' })
.then(function () { toast('已删除'); loadTags(); loadNotes(true); })
.catch(function (e) { toast(e.message, true); });
}
function togglePin(n) {
// 后端 update 是部分更新语义:只发 isPinned,正文不会被覆盖
request('/notes/' + n.id, {
method: 'PUT',
body: JSON.stringify({ isPinned: n.isPinned ? 0 : 1 })
}).then(function () { loadNotes(true); })
.catch(function (e) { toast(e.message, true); });
}
function loadTags() {
request('/notes/tags').then(function (tags) {
var box = $('tag-list');
box.innerHTML = '';
var all = document.createElement('button');
all.className = 'tag-chip' + (state.tag === '' ? ' active' : '');
all.textContent = '全部';
all.onclick = function () { state.tag = ''; loadTags(); loadNotes(true); };
box.appendChild(all);
tags.forEach(function (t) {
var b = document.createElement('button');
b.className = 'tag-chip' + (state.tag === t ? ' active' : '');
b.textContent = t;
b.title = t;
b.onclick = function () { state.tag = t; loadTags(); loadNotes(true); };
box.appendChild(b);
});
}).catch(function () { /* 标签加载失败不影响主流程 */ });
}
// ---------------------------------------------------------------- 编辑器
function renderPreview() {
var md = $('ed-content').value;
var box = $('ed-preview');
if (typeof marked === 'undefined') {
// CDN 不可用时的降级:原样显示,至少不白屏
box.innerHTML = '<pre style="background:none;color:inherit;padding:0">' + esc(md) + '</pre>';
return;
}
var html = marked.parse(md);
box.innerHTML = (typeof DOMPurify !== 'undefined') ? DOMPurify.sanitize(html) : html;
}
function openEditor(id) {
state.editingId = id || null;
$('ed-hint').textContent = id ? '编辑已有笔记' : '新建笔记';
if (!id) {
$('ed-title').value = '';
$('ed-tags').value = '';
$('ed-content').value = '';
renderPreview();
$('editor').classList.add('open');
$('ed-title').focus();
return;
}
request('/notes/' + id).then(function (n) {
$('ed-title').value = n.title || '';
$('ed-tags').value = (n.tagList || []).join(',');
$('ed-content').value = n.content || '';
renderPreview();
$('editor').classList.add('open');
}).catch(function (e) { toast(e.message, true); });
}
function closeEditor() {
$('editor').classList.remove('open');
state.editingId = null;
}
function saveNote() {
var payload = {
title: $('ed-title').value,
content: $('ed-content').value,
tags: $('ed-tags').value,
isPinned: 0
};
var req = state.editingId
? request('/notes/' + state.editingId, { method: 'PUT', body: JSON.stringify(payload) })
: request('/notes', { method: 'POST', body: JSON.stringify(payload) });
req.then(function () {
toast('已保存');
closeEditor();
loadTags();
loadNotes(true);
}).catch(function (e) { toast(e.message, true); });
}
// ---------------------------------------------------------------- 批量导入
function importFiles(files) {
var list = Array.prototype.slice.call(files).filter(function (f) {
return /\.(md|markdown|txt)$/i.test(f.name);
});
if (!list.length) { toast('没有找到 .md 文件', true); return; }
Promise.all(list.map(function (f) {
return new Promise(function (resolve) {
var reader = new FileReader();
reader.onload = function () {
var text = String(reader.result || '');
var firstLine = '';
text.split(/\r?\n/).some(function (line) {
var l = line.replace(/^\s*#{1,6}\s*/, '').trim();
if (l) { firstLine = l; return true; }
return false;
});
resolve({
title: firstLine,
content: text,
sourcePath: f.name,
tags: ''
});
};
reader.readAsText(f, 'utf-8');
});
})).then(function (notes) {
return request('/notes/import', {
method: 'POST',
body: JSON.stringify({ notes: notes })
});
}).then(function (res) {
toast('成功导入 ' + (res.imported || list.length) + ' 篇笔记');
loadTags();
loadNotes(true);
}).catch(function (e) {
toast('导入失败:' + e.message, true);
});
}
// ---------------------------------------------------------------- 图片上传
function uploadImage(file) {
var fd = new FormData();
fd.append('file', file);
request('/notes/attachments', { method: 'POST', body: fd })
.then(function (res) {
var ta = $('ed-content');
var snippet = '![](' + res.url + ')';
var start = ta.selectionStart || 0;
ta.value = ta.value.slice(0, start) + snippet + ta.value.slice(ta.selectionEnd || start);
ta.selectionStart = ta.selectionEnd = start + snippet.length;
ta.focus();
renderPreview();
toast('图片已插入');
})
.catch(function (e) { toast('上传失败:' + e.message, true); });
}
// ---------------------------------------------------------------- 事件绑定
$('login-btn').onclick = doLogin;
$('login-password').onkeydown = function (e) { if (e.key === 'Enter') doLogin(); };
$('login-username').onkeydown = function (e) { if (e.key === 'Enter') $('login-password').focus(); };
$('logout-btn').onclick = doLogout;
$('new-btn').onclick = function () { openEditor(null); };
var searchTimer = null;
$('search').oninput = function () {
clearTimeout(searchTimer);
var v = this.value.trim();
searchTimer = setTimeout(function () { state.keyword = v; loadNotes(true); }, 350);
};
$('loadmore').onclick = function () { loadNotes(false); };
$('pick-files').onclick = function (e) { e.preventDefault(); $('file-input').click(); };
$('file-input').onchange = function () { importFiles(this.files); this.value = ''; };
var dz = $('dropzone');
['dragenter', 'dragover'].forEach(function (ev) {
dz.addEventListener(ev, function (e) { e.preventDefault(); dz.classList.add('over'); });
});
['dragleave', 'drop'].forEach(function (ev) {
dz.addEventListener(ev, function (e) { e.preventDefault(); dz.classList.remove('over'); });
});
dz.addEventListener('drop', function (e) {
if (e.dataTransfer && e.dataTransfer.files) importFiles(e.dataTransfer.files);
});
$('ed-content').oninput = renderPreview;
$('ed-cancel').onclick = closeEditor;
$('ed-save').onclick = saveNote;
$('ed-upload').onclick = function () { $('ed-image').click(); };
$('ed-image').onchange = function () {
if (this.files && this.files[0]) uploadImage(this.files[0]);
this.value = '';
};
$('editor').onclick = function (e) { if (e.target === this) closeEditor(); };
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && $('editor').classList.contains('open')) closeEditor();
if ((e.ctrlKey || e.metaKey) && e.key === 's' && $('editor').classList.contains('open')) {
e.preventDefault(); saveNote();
}
});
// ---------------------------------------------------------------- 启动
if (state.token) {
// 用一次轻量请求验证本地 token 还有效
request('/notes?page=1&size=1')
.then(showApp)
.catch(function () { doLogout(); });
} else {
doLogout();
}
})();
</script>
<!-- Vditor:本地托管,不依赖任何外部 CDN -->
<script src="/admin/vendor/vditor/dist/index.min.js"></script>
<script src="/admin/admin.js"></script>
</body>
</html>