feat(s3): 图片上传到 S3——设置/自动上传/右键单张/批量上传(docs/30)

Rust 新增 s3.rs:SigV4 签名 + PUT Object(reqwest blocking+rustls,
签名对照 AWS 官方示例向量逐字一致;同份代码对本地模拟 S3 端到端
实测通过)。命令 s3_upload_image / s3_test_connection(总数 30 对账)。
配置存 UiState.s3(本机偏好),设置弹窗含测试连接与自动上传开关。
未上传判定 = src 非 http/data/blob 的本地引用;上传成功后同 src 的
图片节点一次事务回换为 S3 地址,本地 .assets 文件保留。
入口:粘贴/插入后自动上传回换(扑空重试一次、失败必提示);右键
命中本地图片多一项「上传到 S3」;文件菜单 + 状态栏批量上传带进度
弹窗。验证:vitest 133+2、tsc、构建通过,设置弹窗截图核对。
This commit is contained in:
Muse
2026-11-15 08:38:47 +08:00
parent 03c6c15439
commit 967aada3ca
19 changed files with 1444 additions and 30 deletions

View File

@@ -0,0 +1,53 @@
// 编辑器图片节点操作(docs/30):收集 src、按旧 src 回换、按 DOM 元素反查 src。
import { describe, it, expect } from 'vitest'
import { createMarkdownEditor, getMarkdown, collectImageSrcs, replaceImageSrc, imageSrcAtElement } from '../src/editor/editor'
const DOC = [
'# 图',
'',
'![本地](doc.assets/a.png)',
'',
'![网络](https://cdn.example.com/b.png)',
'',
'![重复](doc.assets/a.png)',
'',
].join('\n')
async function makeEditor() {
const root = document.createElement('div')
document.body.appendChild(root)
const editor = createMarkdownEditor(root, DOC)
await editor.create()
return { root, editor }
}
describe('editor image ops', () => {
it('收集全部图片 src 并去重', async () => {
const { root, editor } = await makeEditor()
expect(collectImageSrcs(editor)).toEqual(['doc.assets/a.png', 'https://cdn.example.com/b.png'])
await editor.destroy()
root.remove()
})
it('按旧 src 回换全部同名图片节点(含重复引用)', async () => {
const { root, editor } = await makeEditor()
const n = replaceImageSrc(editor, 'doc.assets/a.png', 'https://s3.example.com/a.png')
expect(n).toBe(2)
const md = getMarkdown(editor)
expect(md).toContain('https://s3.example.com/a.png')
expect(md).not.toContain('doc.assets/a.png')
expect(collectImageSrcs(editor)).toEqual(['https://s3.example.com/a.png', 'https://cdn.example.com/b.png'])
await editor.destroy()
root.remove()
})
it('按 <img> 元素反查节点 src;非图片元素返回 null', async () => {
const { root, editor } = await makeEditor()
const img = root.querySelector('img')
expect(img).not.toBeNull()
expect(imageSrcAtElement(editor, img as HTMLElement)).toBe('doc.assets/a.png')
expect(imageSrcAtElement(editor, root)).toBeNull()
await editor.destroy()
root.remove()
})
})