✨ 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:
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { getPastedImageFiles, readClipboardImages } from '../lib/imagePaste'
|
||||
|
||||
export interface PastedImage {
|
||||
mediaType: string
|
||||
@@ -37,28 +38,6 @@ interface Props {
|
||||
defaultProfileName?: string
|
||||
}
|
||||
|
||||
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 shape: "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 NewIssueModal({ open, onCancel, onSubmit, profiles, defaultProfileName }: Props) {
|
||||
const [value, setValue] = useState('')
|
||||
const [images, setImages] = useState<PastedImageWithId[]>([])
|
||||
@@ -100,22 +79,11 @@ export function NewIssueModal({ open, onCancel, onSubmit, profiles, defaultProfi
|
||||
}, [open, onCancel, profiles, defaultProfileName])
|
||||
|
||||
const handlePaste = useCallback(async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
const files = getPastedImageFiles(e)
|
||||
if (files.length > 0) {
|
||||
// Intercept so the file binary doesn't end up pasted as garbled text.
|
||||
e.preventDefault()
|
||||
const parsed = await Promise.all(files.map(readClipboardImage))
|
||||
const ok = parsed.filter((p): p is PastedImage => p !== null)
|
||||
const ok = await readClipboardImages(files)
|
||||
if (ok.length === 0)
|
||||
return
|
||||
// Assign sequential numbers; mirror pastedTexts and insert `[Image #N]`
|
||||
|
||||
Reference in New Issue
Block a user