feat(vscode): profile 值编辑器改为多行+图片粘贴(图片落 .spx/profile-assets)

双击 profile 值单元格改为打开富编辑弹窗:多行 textarea + Ctrl+V 粘贴截图,
⌘/Ctrl+Enter 或点遮罩保存、Esc 取消。值仍是单个字符串,图片存磁盘
(内容寻址 sha1 命名,去重),值里只留 markdown 引用,不撑大 profiles.json。
折叠单元格显示首行 + 含图时加 🖼 徽标。key/列头仍走原内联输入。

- 抽公用 lib/imagePaste:同步探测(preventDefault) + 异步读取,新建工单改用它、行为不变
- 新增扩展侧 profileAssets handler:存图(image/* 校验)/读图(挡路径穿越)
- 新增 useProfileAssets(requestId 关联并发) 与 ProfileCellEditor 弹窗
- 消息两侧同步 profile-asset/save|load|saved|loaded
This commit is contained in:
2026-07-03 16:55:40 +08:00
parent 35085f72f7
commit cace658ac1
9 changed files with 549 additions and 82 deletions
@@ -0,0 +1,68 @@
/**
* profile 值编辑器的图片存/读桥:把 `profile-asset/save|load` 的一问一答封装成
* Promise。用 requestId 关联并发请求——同时粘多张图时,回执按 id 各归各的。
*/
import { useCallback, useEffect, useRef } from 'react'
import { onMessage, postMessage } from '../lib/vscode'
interface Pending {
resolve: (value: string) => void
reject: (err: Error) => void
}
export interface UseProfileAssetsResult {
/** 存图,resolve 出工作区相对路径(写进 profile 值的引用)。 */
saveAsset: (base64: string, mediaType: string) => Promise<string>
/** 读图,resolve 出 data URL(渲染缩略图)。 */
loadAsset: (assetPath: string) => Promise<string>
}
export function useProfileAssets(): UseProfileAssetsResult {
const pending = useRef<Map<string, Pending>>(new Map())
useEffect(() => {
return onMessage((msg) => {
if (msg.type === 'profile-asset/saved') {
const p = pending.current.get(msg.requestId)
if (!p)
return
pending.current.delete(msg.requestId)
if (msg.error || !msg.assetPath)
p.reject(new Error(msg.error ?? '保存失败'))
else
p.resolve(msg.assetPath)
}
else if (msg.type === 'profile-asset/loaded') {
const p = pending.current.get(msg.requestId)
if (!p)
return
pending.current.delete(msg.requestId)
if (msg.error || !msg.dataUrl)
p.reject(new Error(msg.error ?? '读取失败'))
else
p.resolve(msg.dataUrl)
}
})
}, [])
const newId = (): string => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const saveAsset = useCallback((base64: string, mediaType: string): Promise<string> => {
const requestId = newId()
return new Promise<string>((resolve, reject) => {
pending.current.set(requestId, { resolve, reject })
postMessage({ type: 'profile-asset/save', requestId, base64, mediaType })
})
}, [])
const loadAsset = useCallback((assetPath: string): Promise<string> => {
const requestId = newId()
return new Promise<string>((resolve, reject) => {
pending.current.set(requestId, { resolve, reject })
postMessage({ type: 'profile-asset/load', requestId, assetPath })
})
}, [])
return { saveAsset, loadAsset }
}