✨ 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:
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 剪贴板图片提取的公用件,供「新建工单」与 profile 值编辑器共享。
|
||||
*
|
||||
* 刻意拆成「同步探测」+「异步读取」两步:粘贴的默认行为(把二进制当乱码文本插入)
|
||||
* 必须在事件处理里同步 `preventDefault` 才拦得住,而读文件是异步的。所以调用方先用
|
||||
* `getPastedImageFiles` 同步拿到图片文件、立刻 preventDefault,再 await `readClipboardImages`。
|
||||
*/
|
||||
|
||||
export interface PastedImage {
|
||||
/** e.g. "image/png" */
|
||||
mediaType: string
|
||||
/** 裸 base64(不含 `data:...;base64,` 前缀) */
|
||||
base64: string
|
||||
/** `data:...;base64,...` 形式,供 `<img src>` 直接预览 */
|
||||
previewDataUrl: string
|
||||
}
|
||||
|
||||
/** 同步从粘贴事件里挑出所有 image/* 文件;无图返回空数组(调用方据此决定是否 preventDefault)。 */
|
||||
export function getPastedImageFiles(e: React.ClipboardEvent): File[] {
|
||||
const items = e.clipboardData?.items
|
||||
if (!items)
|
||||
return []
|
||||
const files: File[] = []
|
||||
for (const item of items) {
|
||||
if (item.kind === 'file' && item.type.startsWith('image/')) {
|
||||
const f = item.getAsFile()
|
||||
if (f)
|
||||
files.push(f)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
/** 把单个图片文件读成 base64 + data URL;读失败回 null。 */
|
||||
export function readClipboardImage(file: File): Promise<PastedImage | null> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const result = reader.result
|
||||
if (typeof result !== 'string') {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
// result 形如 "data:image/png;base64,iVBORw0K..."
|
||||
const commaIdx = result.indexOf(',')
|
||||
const header = commaIdx > 0 ? result.slice(0, commaIdx) : ''
|
||||
const base64 = commaIdx > 0 ? result.slice(commaIdx + 1) : ''
|
||||
const mediaMatch = header.match(/^data:([^;]+);base64$/)
|
||||
const mediaType = mediaMatch ? mediaMatch[1] : file.type || 'image/png'
|
||||
resolve({ mediaType, base64, previewDataUrl: result })
|
||||
}
|
||||
reader.onerror = () => resolve(null)
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量读取,丢弃读失败的项。 */
|
||||
export function readClipboardImages(files: File[]): Promise<PastedImage[]> {
|
||||
return Promise.all(files.map(readClipboardImage)).then(
|
||||
arr => arr.filter((p): p is PastedImage => p !== null),
|
||||
)
|
||||
}
|
||||
@@ -118,6 +118,9 @@ export type ExtensionToWebview
|
||||
| { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
||||
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
||||
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
||||
// profile 值编辑器粘贴的图片:存盘/读盘的回执,requestId 关联并发请求。
|
||||
| { type: 'profile-asset/saved', requestId: string, assetPath?: string, error?: string }
|
||||
| { type: 'profile-asset/loaded', requestId: string, dataUrl?: string, error?: string }
|
||||
|
||||
export type WebviewToExtension
|
||||
= | { type: 'issues/refresh' }
|
||||
@@ -192,3 +195,6 @@ export type WebviewToExtension
|
||||
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
||||
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
||||
| { type: 'pr-review/set', issueNumber: number, confirmed: Record<string, string> }
|
||||
// profile 值编辑器:图片存盘(base64→磁盘)与读盘(磁盘→dataUrl)请求。
|
||||
| { type: 'profile-asset/save', requestId: string, base64: string, mediaType: string }
|
||||
| { type: 'profile-asset/load', requestId: string, assetPath: string }
|
||||
|
||||
Reference in New Issue
Block a user