WP Code Notes 是一款专为开发者打造的轻量级、高性能代码片段管理插件。它不仅能让您在 WordPress 后台快速记录和整理技术笔记,更通过深度优化的交互设计,实现了“指尖级”的代码采集与复用体验。
核心特性
1. 极速响应式交互
- 行号直选复用:独创行号拖拽选中逻辑,松开鼠标自动复制。无需按下 Ctrl+C,让代码复用快人一步。
- 智能排版:内置
js-beautify引擎,一键对 HTML、CSS、JS、PHP 进行专业级格式化美化。 - 沉浸式全屏:支持全屏编辑模式,提供纯净的沉浸式编码环境。
2. 完美移动端适配
- 双菜单入口:除了左侧侧边栏,特设顶部管理栏快捷入口。
- 手机端优化:针对移动端屏幕深度定制,即使在手机后台也能随时查看、搜索和记录灵感。
3. 极致视觉体验
- Monokai 主题:原生搭载经典的 Monokai 护眼配色,长时间编写不疲劳。
- 自适应网格:智能 4 列布局(可调),笔记卡片错落有致,信息密度高且清晰。
- 自动语感识别:基于内容自动切换高亮模式(PHP/JS/HTML/CSS)。
4. 强大的生态扩展 (Browser Extension Ready)
- REST API 支持:内置标准化 API 接口,可无缝对接 Chrome/Edge 浏览器插件。
- 无感鉴权:采用经过优化的 Cookie 验证机制,无需配置复杂的 Nonce,只要登录后台即可实现全平台同步。
快捷键支持
| 按键 | 动作 |
| Enter | 在快速录入框内直接保存笔记并刷新 |
| Ctrl + Enter | 在录入框内换行,不触发保存 |
| 鼠标拖拽行号 | 快速选中多行代码,松开即自动复制 |
技术规格
- 核心引擎:CodeMirror 5.x
- 数据库:WP 原生存储表,查询性能极致优化
- 兼容性:支持 WordPress 5.0+ 及所有现代浏览器
- 资源占用:采用 Intersection Observer 懒加载技术,即使有上千条笔记,首屏加载依然秒开
如何使用
- 安装:上传插件至
wp-content/plugins并启用。 - 记录:在顶部或左侧点击“笔记”,利用顶部的输入框快速录入。
- 整理:通过搜索功能快速定位代码片段。
- 复用:点击行号或工具栏复制图标,瞬间完成代码采集。
<?php
/*
Plugin Name: wp-code-notes
Description: 稳定版 v5.7.2:完美快捷键逻辑(Enter提交/Ctrl+Enter换行),保留所有核心增强功能。
Version: 5.7.2
Author: Gemini Thought Partner
License: GPL2
*/
if (!defined('ABSPATH')) exit;
// 1. 数据库初始化
register_activation_hook(__FILE__, function() {
global $wpdb;
$t = $wpdb->prefix . 'code_notes';
$wpdb->query("CREATE TABLE IF NOT EXISTS $t (id int NOT NULL AUTO_INCREMENT, content longtext, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id)) {$wpdb->get_charset_collate()};");
});
// 2. 菜单设置 (左侧菜单 + 顶部快捷入口)
add_action('admin_menu', function() {
add_menu_page('笔记', '笔记', 'manage_options', 'wp-code-notes', 'wck_render_v572', 'dashicons-media-code', 3);
});
// 顶部管理栏快捷入口(手机端优化版)
add_action('admin_bar_menu', function($wp_admin_bar) {
if (!current_user_can('manage_options')) return;
$wp_admin_bar->add_node([
'id' => 'wp-code-notes-bar',
'title' => '<span class="ab-icon dashicons dashicons-media-code"></span><span class="ab-label">笔记</span>',
'href' => admin_url('admin.php?page=wp-code-notes'),
'meta' => [ 'class' => 'wck-force-show' ]
]);
}, 90);
// 3. 静态资源加载
add_action('admin_enqueue_scripts', function($hook) {
if ($hook != 'toplevel_page_wp-code-notes') return;
$base_url = plugin_dir_url(__FILE__) . 'codemirror/';
wp_enqueue_style('cm-core-css', $base_url . 'lib/codemirror.css');
wp_enqueue_style('cm-monokai', $base_url . 'theme/monokai.css');
wp_enqueue_script('cm-core-js', $base_url . 'lib/codemirror.js', [], null, true);
$modes = ['xml/xml.js', 'css/css.js', 'javascript/javascript.js', 'clike/clike.js', 'php/php.js'];
foreach($modes as $index => $path) {
wp_enqueue_script('cm-mode-' . $index, $base_url . 'mode/' . $path, ['cm-core-js'], null, true);
}
wp_enqueue_script('js-beautify', $base_url . 'beautify.min.js', [], null, true);
wp_enqueue_script('css-beautify', $base_url . 'beautify-css.min.js', [], null, true);
wp_enqueue_script('html-beautify', $base_url . 'beautify-html.min.js', [], null, true);
wp_add_inline_style('cm-core-css', '
.wck-master { max-width: 98%; margin: 20px 15px 0 0; }
.wck-nav { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; width: 100%; }
.nav-section { display: flex; align-items: stretch; flex: 1; min-width: 350px; }
.nav-section.input-area { flex: 1.5; }
.wck-ctrl, .wck-btn {
height: 36px !important; min-height: 36px !important;
box-sizing: border-box !important; border: 1px solid #ccd0d4 !important;
margin: 0 !important; display: inline-flex; align-items: center; vertical-align: top;
}
input[type="text"].wck-ctrl, textarea.wck-ctrl { background: #fff; flex: 1; padding: 0 12px !important; border-radius: 4px 0 0 4px !important; font-size: 14px; }
textarea.wck-ctrl { line-height: 24px; padding-top: 5px !important; resize: none; overflow: hidden; }
.wck-btn { width: 40px !important; justify-content: center; cursor: pointer; background: #fff; color: #2271b1; border-left: none !important; border-radius: 0 !important; }
.wck-btn:hover { background: #f0f0f1; border-color: #a7aaad; }
.btn-end { border-radius: 0 4px 4px 0 !important; }
.btn-plus { background: #2271b1 !important; color: #fff !important; border-color: #2271b1 !important; }
.note-grid { display: grid !important; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important; gap: 15px !important; width: 100%; }
.note-card { background: #272822; border: 1px solid #dcdcde; border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; position: relative; height: 300px; }
.note-card.collapsed { height: 50px !important; }
.card-body { flex: 1; overflow: hidden; position: relative; background: #272822; }
.editor-wrap { opacity: 0; transition: opacity 0.3s; height: 100%; }
.editor-wrap.loaded { opacity: 1; }
.CodeMirror { height: 100% !important; font-size: 13px; background: transparent !important; }
.cm-s-monokai .CodeMirror-selected { background: #49483e !important; }
.cm-s-monokai .CodeMirror-focused .CodeMirror-selected { background: #555555 !important; }
.card-footer {
position: absolute; bottom: 0; left: 0; right: 0; z-index: 100; background: rgba(30,30,30,0.95);
border-top: 1px solid #444; padding: 0 12px; height: 42px; display: flex; justify-content: space-between; align-items: center;
transform: translateY(100%); transition: transform 0.2s;
}
.note-card:not(.collapsed):hover .card-footer { transform: translateY(0); }
.toolbar-group { display: flex; border: 1px solid #555; border-radius: 4px; overflow: hidden; }
.toolbar-group span { padding: 5px 10px; color: #eee; cursor: pointer; border-right: 1px solid #555; display: flex; align-items: center; }
.toolbar-group span:hover { background: #2271b1; color: #fff; }
.lang-label { color:#888; font-size:10px; border:1px solid #444; padding:0 4px; border-radius:3px; }
.tablenav-pages { text-align: center; margin: 30px 0; }
.tablenav-pages a, .tablenav-pages span { display: inline-block; padding: 6px 12px; background: #fff; border: 1px solid #ccd0d4; margin: 0 2px; text-decoration: none; border-radius: 3px; color: #2271b1; }
.tablenav-pages .current { background: #2271b1; color: #fff; border-color: #2271b1; }
body.wck-off { overflow: hidden !important; }
.wck-full { display: none; position: fixed; z-index: 999999; left: 0; top: 0; width: 100vw; height: 100vh; background: #272822; flex-direction: column; }
.full-header { display: flex; justify-content: space-between; align-items: center; background: #1a1a1a; padding: 0 15px; height: 50px; border-bottom: 1px solid #333; }
.wck-full .CodeMirror { height: calc(100vh - 50px) !important; }
#wck-toast { visibility: hidden; background: rgba(0,0,0,0.9); color: #fff; padding: 8px 16px; position: fixed; z-index: 1000000; left: 50%; top: 20px; transform: translateX(-50%); border-radius: 4px; opacity: 0; transition: .3s; }
#wck-toast.show { visibility: visible; opacity: 1; }
');
});
add_action("wp_ajax_wck_save", function() { global $wpdb; $wpdb->update($wpdb->prefix.'code_notes', ['content'=>wp_unslash($_POST['content'])], ['id'=>(int)$_POST['id']]); wp_send_json_success(); });
add_action("wp_ajax_wck_create", function() { global $wpdb; if(!empty(trim($_POST['content']))) $wpdb->insert($wpdb->prefix.'code_notes', ['content'=>wp_unslash($_POST['content'])]); wp_send_json_success(); });
add_action("wp_ajax_wck_delete", function() { global $wpdb; $wpdb->delete($wpdb->prefix.'code_notes', ['id'=>(int)$_POST['id']]); wp_send_json_success(); });
function wck_render_v572() {
global $wpdb; $t = $wpdb->prefix.'code_notes';
$s = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
$p = max(1, (int)($_GET['paged']??1)); $size = 20; $off = ($p-1)*$size;
$where = $s ? $wpdb->prepare("WHERE content LIKE %s", '%'.$wpdb->esc_like($s).'%') : '';
$total = $wpdb->get_var("SELECT COUNT(*) FROM $t $where");
$notes = $wpdb->get_results($wpdb->prepare("SELECT * FROM $t $where ORDER BY updated_at DESC LIMIT %d OFFSET %d", $size, $off));
?>
<div id="wck-toast">已同步</div>
<div class="wck-master">
<div class="wck-nav">
<form method="get" class="nav-section">
<input type="hidden" name="page" value="wp-code-notes">
<input type="text" name="s" class="wck-ctrl" placeholder="搜索关键词..." value="<?=esc_attr($s)?>">
<button type="submit" class="wck-btn"><span class="dashicons dashicons-search"></span></button>
<div class="wck-btn btn-end" onclick="toggleAll(false)"><span class="dashicons dashicons-editor-expand"></span></div>
</form>
<div class="nav-section input-area">
<textarea id="wck-a" class="wck-ctrl" placeholder="快速录入... (Enter 保存, Ctrl+Enter 换行)"></textarea>
<div class="wck-btn btn-plus" onclick="doAct('create')"><span class="dashicons dashicons-plus-alt2"></span></div>
<div class="wck-btn btn-end" onclick="toggleAll(true)"><span class="dashicons dashicons-editor-contract"></span></div>
</div>
</div>
<div class="note-grid">
<?php foreach ($notes as $n): ?>
<div class="note-card" data-id="<?=$n->id?>">
<div class="card-body">
<div class="editor-wrap"><textarea class="editor"><?=esc_textarea($n->content)?></textarea></div>
</div>
<div class="card-footer">
<div class="toolbar-group">
<span class="dashicons dashicons-editor-code" onclick="smartFormat(eds[<?=$n->id?>])"></span>
<span class="dashicons dashicons-undo" onclick="eds[<?=$n->id?>].undo()"></span>
<span class="dashicons dashicons-redo" onclick="eds[<?=$n->id?>].redo()"></span>
<span class="dashicons dashicons-clipboard" onclick="smartPaste(eds[<?=$n->id?>])"></span>
<span class="dashicons dashicons-admin-page" onclick="smartCopy(eds[<?=$n->id?>])"></span>
<span class="dashicons dashicons-editor-expand" onclick="openF(<?=$n->id?>)"></span>
<span class="dashicons dashicons-trash" onclick="doAct('delete',<?=$n->id?>)"></span>
</div>
<div class="lang-label">AUTO</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="tablenav-pages"><?= paginate_links(['total'=>ceil($total/$size), 'current'=>$p, 'base'=>add_query_arg('paged', '%#%')]) ?></div>
</div>
<div id="full-modal" class="wck-full">
<div class="full-header">
<div class="toolbar-group">
<span class="dashicons dashicons-editor-code" onclick="smartFormat(fCM)"></span>
<span class="dashicons dashicons-undo" onclick="fCM.undo()"></span>
<span class="dashicons dashicons-redo" onclick="fCM.redo()"></span>
<span class="dashicons dashicons-clipboard" onclick="smartPaste(fCM)"></span>
<span class="dashicons dashicons-admin-page" onclick="smartCopy(fCM)"></span>
<span class="dashicons dashicons-editor-contract" onclick="closeF()"></span>
</div>
<button class="button button-primary" onclick="saveF()">保存并返回</button>
</div>
<div style="flex:1;"><textarea id="full-area"></textarea></div>
</div>
<script>
const eds={}; let fCM, curId, dragStart=null, activeCM=null;
const toast=m=>{const t=document.getElementById('wck-toast');t.innerText=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1500)};
const smartCopy = cm => {
const text = cm.getSelection() || cm.getValue();
if(!text) return;
navigator.clipboard.writeText(text).then(() => toast('已复制'));
};
const smartPaste = cm => { cm.focus(); navigator.clipboard.readText().then(t => { if(t) { cm.replaceSelection(t); cm.focus(); } }); };
function smartFormat(cm) {
if (typeof js_beautify === 'undefined') return toast('加载中...');
const oldVal = cm.getValue(), mode = cm.getOption("mode"), opt = { indent_size: 2 };
let newVal = "";
try {
if (mode === "xml" || mode === "htmlmixed") newVal = html_beautify(oldVal, opt);
else if (mode === "css") newVal = css_beautify(oldVal, opt);
else newVal = js_beautify(oldVal, opt);
if (newVal && newVal !== oldVal) { cm.setValue(newVal); toast('已格式化'); }
} catch (e) { toast('失败'); }
}
function detectMode(text) {
if (text.match(/<\?php|<\?|namespace\s+\w+/i)) return {name: "php", label: "PHP"};
if (text.match(/^\s*[{.#].*\{|body\s*\{/m)) return {name: "css", label: "CSS"};
if (text.match(/<[a-z][\s\S]*>/i)) return {name: "xml", label: "HTML"};
return {name: "javascript", label: "JS/TEXT"};
}
function applySelectionLogic(cm) {
cm.on("gutterClick", (ins, line) => { ins.focus(); dragStart = line; activeCM = ins; ins.setSelection({line: line, ch: 0}, {line: line, ch: ins.getLine(line).length}); });
cm.on("mousedown", (ins, e) => {
if (e.target.closest(".CodeMirror-linenumber")) {
ins.focus(); const line = ins.lineAtHeight(e.clientY, "window");
dragStart = line; activeCM = ins; ins.setSelection({line: line, ch: 0}, {line: line, ch: ins.getLine(line).length});
}
});
}
window.addEventListener("mousemove", (e) => {
if (dragStart !== null && activeCM) {
const curLine = activeCM.lineAtHeight(e.clientY, "window");
activeCM.setSelection({line: Math.min(dragStart, curLine), ch: 0}, {line: Math.max(dragStart, curLine), ch: activeCM.getLine(Math.max(dragStart, curLine)).length});
}
});
window.addEventListener("mouseup", () => {
if (dragStart !== null && activeCM) {
const s = activeCM.getSelection();
if (s && s.length > 0) smartCopy(activeCM);
dragStart = null; activeCM = null;
}
});
function initCM(card) {
const id = card.dataset.id; if (eds[id]) return;
const ta = card.querySelector('.editor'), wrap = card.querySelector('.editor-wrap');
const cm = CodeMirror.fromTextArea(ta, { mode: "javascript", theme: "monokai", lineNumbers: true, lineWrapping: true, inputStyle: "contenteditable" });
wrap.classList.add('loaded');
applySelectionLogic(cm);
const up = () => { const i = detectMode(cm.getValue()); cm.setOption("mode", i.name); card.querySelector('.lang-label').innerText = i.label; };
up();
cm.on('change', () => { up(); clearTimeout(ta.t); ta.t = setTimeout(() => {
const d = new FormData(); d.append('action', 'wck_save'); d.append('id', id); d.append('content', cm.getValue());
fetch(ajaxurl, {method:'POST', body:d}).then(() => toast('已同步'));
}, 1500); });
eds[id] = cm;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { if (entry.isIntersecting) { initCM(entry.target.closest('.note-card')); observer.unobserve(entry.target); } });
}, { rootMargin: '150px' });
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.note-card').forEach(card => observer.observe(card.querySelector('.card-body')));
// --- 核心快捷键修复逻辑 ---
const quickInput = document.getElementById('wck-a');
quickInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
if (e.ctrlKey || e.metaKey || e.shiftKey) {
// Ctrl+Enter 或 Shift+Enter: 手动插入换行符并阻止默认提交
const start = this.selectionStart;
const end = this.selectionEnd;
this.value = this.value.substring(0, start) + "\n" + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 1;
e.preventDefault();
} else {
// 纯 Enter: 执行创建
e.preventDefault();
doAct('create');
}
}
});
});
function toggleAll(c) {
document.querySelectorAll('.note-card').forEach(card => {
c ? card.classList.add('collapsed') : card.classList.remove('collapsed');
const id = card.dataset.id;
if (!c && !eds[id]) initCM(card); else if (!c) setTimeout(()=>eds[id].refresh(), 100);
});
}
function openF(id){
curId = id; document.body.classList.add('wck-off');
document.getElementById('full-modal').style.display = 'flex';
if(!fCM) { fCM = CodeMirror.fromTextArea(document.getElementById('full-area'), { mode: "javascript", theme: "monokai", lineNumbers: true, lineWrapping: true, inputStyle: "contenteditable" }); applySelectionLogic(fCM); }
fCM.setValue(eds[id].getValue()); setTimeout(() => fCM.refresh(), 50);
}
function closeF(){ document.getElementById('full-modal').style.display = 'none'; document.body.classList.remove('wck-off'); }
function saveF(){
const v = fCM.getValue(); const d = new FormData(); d.append('action', 'wck_save'); d.append('id', curId); d.append('content', v);
fetch(ajaxurl, {method:'POST', body:d}).then(() => { if(eds[curId]) eds[curId].setValue(v); closeF(); toast('已保存'); });
}
function doAct(a, id){
if(a==='delete' && !confirm('确认删除?')) return;
const d = new FormData(); d.append('action', 'wck_'+a);
if(id) d.append('id', id);
if(a==='create') d.append('content', document.getElementById('wck-a').value);
fetch(ajaxurl, {method:'POST', body:d}).then(() => location.reload());
}
</script>
<?php
}
add_action('admin_head', function() {
?>
<style>
/* 电脑端优化:防止图标贴在一起 */
#wp-admin-bar-wp-code-notes-bar .ab-icon { margin-right: 4px !important; }
@media screen and (max-width: 782px) {
/* 1. 强制在手机端后台显示该节点 */
#wpadminbar li#wp-admin-bar-wp-code-notes-bar {
display: block !important;
width: 52px !important;
position: relative !important;
}
/* 2. 核心修复:把 <a> 标签撑满并置顶,防止点击被 JS 拦截用于展开子菜单 */
#wpadminbar #wp-admin-bar-wp-code-notes-bar > .ab-item {
display: block !important;
width: 100% !important;
height: 46px !important;
line-height: 46px !important;
padding: 0 !important;
text-align: center !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
z-index: 99999 !important; /* 确保它在最上层 */
}
/* 3. 修正图标在手机端的位置 */
#wpadminbar #wp-admin-bar-wp-code-notes-bar .ab-icon {
display: inline-block !important;
margin: 0 !important;
padding: 0 !important;
width: 20px !important;
height: 20px !important;
vertical-align: middle !important;
}
/* 4. 彻底隐藏手机端文字,只留图标点击 */
#wpadminbar #wp-admin-bar-wp-code-notes-bar .ab-label {
display: none !important;
}
}
</style>
<?php
});
// 6. 注册 REST API 路由 - 浏览器插件接口优化版
add_action('rest_api_init', function () {
$auth_check = function() {
// 核心权限:必须是管理员权限
if (current_user_can('manage_options')) return true;
return new WP_Error('rest_forbidden', '权限不足,请确保已登录后台', ['status' => 401]);
};
// 获取/搜索列表
register_rest_route('wck/v1', '/notes', [
'methods' => 'GET',
'callback' => function($data) {
global $wpdb;
$t = $wpdb->prefix . 'code_notes';
$s = $data->get_param('s');
if ($s) {
$sql = $wpdb->prepare("SELECT id, content FROM $t WHERE content LIKE %s ORDER BY updated_at DESC LIMIT 50", '%' . $wpdb->esc_like($s) . '%');
} else {
$sql = "SELECT id, content FROM $t ORDER BY updated_at DESC LIMIT 50";
}
return rest_ensure_response($wpdb->get_results($sql));
},
'permission_callback' => $auth_check
]);
// 新增/编辑/删除
register_rest_route('wck/v1', '/add', [
'methods' => 'POST',
'callback' => function($request) {
global $wpdb;
$t = $wpdb->prefix . 'code_notes';
$p = $request->get_json_params();
$id = isset($p['id']) ? (int)$p['id'] : null;
// 关键:使用 wp_unslash 去除多余的反斜杠,保持代码原样
$content = isset($p['content']) ? wp_unslash($p['content']) : '';
// 删除逻辑
if (isset($p['delete']) && $id) {
$wpdb->delete($t, ['id' => $id]);
return ['success' => true, 'action' => 'delete'];
}
if ($id) {
// 更新
$wpdb->update($t, ['content' => $content], ['id' => $id]);
$action = 'update';
} else if (!empty(trim($content))) {
// 新增
$wpdb->insert($t, ['content' => $content]);
$id = $wpdb->insert_id;
$action = 'create';
}
return ['success' => true, 'id' => $id, 'action' => $action];
},
'permission_callback' => $auth_check
]);
});
// 核心身份验证逻辑保持不变(非常棒的方案,解决了 Nonce 跨域问题)
add_filter('rest_authentication_errors', function($result) {
if (!empty($result)) return $result;
if (is_user_logged_in() && current_user_can('manage_options')) {
return true;
}
return $result;
});