背景:极简记事本的唯一缺憾

我用的是 GitHub 上的 minimalist-web-notepad(在 /srv/www/文本笔记/),只有一个 index.php(几千行),没有数据库,所有笔记存成 .txt 文件在 _tmp/ 目录里。

它的优点是:

  • 打开即写,内容自动保存到 URL 路径对应的 txt 文件
  • 极简,没有账号、没有分类、只有「地址栏区分不同笔记」这一条规则

缺点也很明显:只能写文字,不能插图。

笔记站是我日常用得最多的服务:服务器命令、配置笔记、甚至买菜清单——全用它。纯文字一直够用,直到上个月我想把「Nginx 反代配置示意图」的截图存下来,文字描述不清:

"proxy_pass 到 127.0.0.1:5244,记得加 WebSocket 升级头,不然后台白屏"

看文字谁也想象不出来。必须有图。


需求拆解

我想实现的效果: 在记事本的编辑界面里,直接按 Ctrl+V 粘贴截图(QQ/微信截图后),或者拖拽图片进去 → 自动上传到服务器 → 笔记正文里自动插入 Markdown 图片链接 ![](图片地址)。

约束条件:

  1. 不能装 MySQL/Redis(笔记站本来就没数据库)
  2. 不能改底层逻辑太多(要保持一个 php 文件的极简风格)
  3. 图片访问地址要漂亮,不暴露真实磁盘路径
  4. 必须做文件大小 / 类型 / 总量限制(否则哪天贴了一张 20MB 的手机原图)

方案:30 行上传脚本 + 15 行 JS 粘贴监听

1. 文件结构

/srv/www/文本笔记/
├── index.php                   ← 原本的记事本(不碰核心逻辑)
├── upload.php                  ← 【新增】30 行的上传脚本
├── _tmp/                       ← 笔记 txt 文件的存储目录
└── uploads/                    ← 【新增】图片存储目录
    └── 2026/
        └── 08/
            ├── 01-img1.png     ← 按日期归档,防止单目录文件过多
            └── 01-img2.jpg

2. upload.php(后端核心)

<?php
/**
 * 极简记事本图片上传脚本
 * - 只允许 png/jpg/jpeg/gif/webp
 * - 单文件 <= 2MB
 * - 每天最多 100 张(防滥用)
 * - 按 年/月/日-时间戳-4位随机 命名,绝不覆盖
 */
$ALLOW_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
$MAX_SIZE    = 2 * 1024 * 1024;   // 2MB
$DAILY_LIMIT = 100;
$UPLOAD_ROOT = __DIR__ . '/uploads';
$URL_PREFIX  = '/uploads';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405); die('Method Not Allowed');
}
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400); die(json_encode(['ok' => 0, 'msg' => '上传失败']));
}

$file = $_FILES['image'];
if ($file['size'] > $MAX_SIZE) {
    http_response_code(413); die(json_encode(['ok' => 0, 'msg' => '图片大于 2MB,请压缩后上传']));
}
if (!in_array($file['type'], $ALLOW_TYPES, true)) {
    http_response_code(415); die(json_encode(['ok' => 0, 'msg' => '只支持 png/jpg/gif/webp']));
}

// 按日期目录归档:/uploads/2026/08/01_165902_abcd.png
[$y, $m, $d] = [date('Y'), date('m'), date('d')];
$dir = "$UPLOAD_ROOT/$y/$m";
if (!is_dir($dir)) mkdir($dir, 0755, true);

// 每日限制数:统计当天文件数
$today_files = glob("$dir/{$d}_*");
if ($today_files && count($today_files) >= $DAILY_LIMIT) {
    http_response_code(429); die(json_encode(['ok' => 0, 'msg' => '今日上传次数上限,请明天再试']));
}

// 安全的文件名
$ext = (new finfo(FILEINFO_EXTENSION))->file($file['tmp_name'])
    ?: strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$ext = explode('/', $ext)[0] ?? $ext;
$filename = sprintf("%s_%s_%s.%s", $d, date('His'), substr(md5(uniqid()), 0, 4), $ext);

$dst = "$dir/$filename";
if (!move_uploaded_file($file['tmp_name'], $dst)) {
    http_response_code(500); die(json_encode(['ok' => 0, 'msg' => '写入失败,检查 uploads 目录权限']));
}

header('Content-Type: application/json');
echo json_encode([
    'ok'   => 1,
    'url'  => "$URL_PREFIX/$y/$m/$filename",
    'name' => $filename
], JSON_UNESCAPED_SLASHES);

3. 前端:改记事本的 editor 代码(JS 15 行)

把这段 JS 加到原本的 editor 初始化代码末尾(在 index.php 的 <script> 里插进去就行):

/* 粘贴 / 拖拽上传图片到笔记 */
const editor = document.querySelector('#editor') || document.querySelector('textarea');
function insertAtCursor(txt) {
    const s = editor.selectionStart, e = editor.selectionEnd;
    editor.value = editor.value.slice(0, s) + txt + editor.value.slice(e);
    editor.selectionStart = editor.selectionEnd = s + txt.length;
}
async function uploadFile(file) {
    const fd = new FormData(); fd.append('image', file);
    const r = await fetch('/upload.php', { method: 'POST', body: fd });
    const j = await r.json();
    if (!j.ok) throw new Error(j.msg || '上传失败');
    insertAtCursor(`\n![${file.name}](${j.url})\n`);
}
editor.addEventListener('paste', async e => {
    const items = e.clipboardData?.items || [];
    for (const it of items) {
        if (it.kind === 'file' && it.type.startsWith('image/')) {
            uploadFile(it.getAsFile()).catch(err => alert(err.message));
        }
    }
});
editor.addEventListener('drop', e => {
    e.preventDefault();
    for (const f of e.dataTransfer.files) {
        if (f.type.startsWith('image/')) uploadFile(f).catch(err => alert(err.message));
    }
});

4. Nginx 加的一条规则(防盗链 + 大小限制)

# 笔记站 server 块里
location /uploads/ {
    valid_referers none blocked note.mccok.cn *.mccok.cn;
    if ($invalid_referer) { return 403; }   # 防盗链
    client_max_body_size 2m;                # 单请求不超 2M,同 PHP
    expires 60d;                            # 图片强缓存
}
location ~* ^/upload\.php$ {
    client_max_body_size 2m;
}

效果

现在在笔记里:

  1. 微信 Win+A 截图一张 Nginx 配置 → Ctrl+C → 切回笔记页 Ctrl+V
  2. 页面右下角小字提示 "正在上传..." → 0.5 秒后正文出现 ![截屏.png](/uploads/2026/08/01_201542_abcd.png)
  3. 保存后刷新笔记,图片就嵌在文字中间了

完全不用去图床,不用走第三方,图片就在我自己服务器上。


还没做完的 3 个小优化

  1. 图片自动压缩——超过 500KB 自动用 GD 库缩到 1920 像素宽度
  2. 上传大小限制可配置——把 2MB 写到 index.php 顶部常量里
  3. 垃圾清理——半年没被引用的图片自动列出来人工确认删除

等我写好了补第二篇。

(完)