✨ 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 parent
35085f72f7
commit
cace658ac1
9 files changed
+549
-82
No files matched your search
@@ -10,8 +10,9 @@
|
||||
*/
|
||||
|
||||
import type { ProfileRow, ProfilesData } from '../lib/messages'
|
||||
import { ExternalLink, Folder, Plus } from 'lucide-react'
|
||||
import { ExternalLink, Folder, Image as ImageIcon, Plus } from 'lucide-react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ProfileCellEditor } from './ProfileCellEditor'
|
||||
|
||||
interface ProfileGridProps {
|
||||
data: ProfilesData
|
||||
@@ -33,6 +34,11 @@ type EditTarget
|
||||
const URL_RE = /^(?:https?:\/\/|git@)/i
|
||||
const WIN_PATH_RE = /^[A-Z]:[\\/]/i
|
||||
|
||||
/** 值里是否含粘贴图片的引用,用来给折叠单元格加 🖼 徽标。 */
|
||||
function hasImageRef(value: string): boolean {
|
||||
return /!\[[^\]]*\]\(\.spx\/profile-assets\//.test(value)
|
||||
}
|
||||
|
||||
function classifyValue(value: string): 'url' | 'path' | null {
|
||||
const v = value.trim()
|
||||
if (!v)
|
||||
@@ -51,6 +57,8 @@ export function ProfileGrid({ data, onSave, onOpen }: ProfileGridProps) {
|
||||
const [editing, setEditing] = useState<EditTarget | null>(null)
|
||||
const [draft, setDraft] = useState<string>('')
|
||||
const [menu, setMenu] = useState<MenuState | null>(null)
|
||||
// 值单元格改用富编辑弹窗(多行 + 图片),key/列头仍走内联 input。
|
||||
const [cellEdit, setCellEdit] = useState<{ row: number, profile: string } | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -322,6 +330,7 @@ export function ProfileGrid({ data, onSave, onOpen }: ProfileGridProps) {
|
||||
setDraft={setDraft}
|
||||
openKeyMenu={openKeyMenu}
|
||||
onOpen={onOpen}
|
||||
onEditCell={profile => setCellEdit({ row: rIdx, profile })}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -366,6 +375,20 @@ export function ProfileGrid({ data, onSave, onOpen }: ProfileGridProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 值单元格富编辑弹窗:双击值格打开,多行 + 图片粘贴 */}
|
||||
{cellEdit && (
|
||||
<ProfileCellEditor
|
||||
keyLabel={rows[cellEdit.row]?.key ?? ''}
|
||||
profileLabel={cellEdit.profile}
|
||||
initialValue={rows[cellEdit.row]?.values[cellEdit.profile] ?? ''}
|
||||
onSave={(val) => {
|
||||
commitEdit({ kind: 'cell', row: cellEdit.row, profile: cellEdit.profile }, val)
|
||||
setCellEdit(null)
|
||||
}}
|
||||
onCancel={() => setCellEdit(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -385,6 +408,7 @@ interface RowFragmentProps {
|
||||
setDraft: (v: string) => void
|
||||
openKeyMenu: (e: React.MouseEvent, row: number) => void
|
||||
onOpen: (value: string) => void
|
||||
onEditCell: (profile: string) => void
|
||||
}
|
||||
|
||||
function RowFragment({
|
||||
@@ -402,6 +426,7 @@ function RowFragment({
|
||||
setDraft,
|
||||
openKeyMenu,
|
||||
onOpen,
|
||||
onEditCell,
|
||||
}: RowFragmentProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -438,57 +463,43 @@ function RowFragment({
|
||||
</div>
|
||||
{profiles.map((profile, col) => {
|
||||
const value = row.values[profile] ?? ''
|
||||
const isEditing = editing && editing.kind === 'cell' && editing.row === rIdx && editing.profile === profile
|
||||
const kind = classifyValue(value)
|
||||
// 折叠态只显示首行;多行/含图的值不再当 URL/path 处理(不显示打开图标)。
|
||||
const singleLine = !value.includes('\n')
|
||||
const firstLine = singleLine ? value : value.split('\n', 1)[0]
|
||||
const kind = singleLine ? classifyValue(value) : null
|
||||
const withImage = hasImageRef(value)
|
||||
return (
|
||||
<div
|
||||
key={`r${rIdx}-c${col}`}
|
||||
className={`${cellBase} ${editableHover} group px-2`}
|
||||
onDoubleClick={() => beginEdit({ kind: 'cell', row: rIdx, profile })}
|
||||
title="双击编辑"
|
||||
onDoubleClick={() => onEditCell(profile)}
|
||||
title="双击编辑(多行 + 图片)"
|
||||
>
|
||||
{isEditing
|
||||
? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={draft}
|
||||
onChange={e => setDraft(e.target.value)}
|
||||
onBlur={finishEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
finishEdit()
|
||||
}
|
||||
else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
cancelEdit()
|
||||
}
|
||||
}}
|
||||
className="w-full bg-transparent px-0 py-0 text-xs text-[var(--vscode-input-foreground)] outline-none focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="flex w-full min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{value || <span className="opacity-40">(空)</span>}
|
||||
</span>
|
||||
{kind && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpen(value)
|
||||
}}
|
||||
title={kind === 'url' ? '在浏览器打开' : '在系统中打开'}
|
||||
aria-label={kind === 'url' ? '在浏览器打开' : '在系统中打开'}
|
||||
className="grid size-5 shrink-0 place-items-center rounded opacity-0 transition-opacity hover:bg-[var(--vscode-toolbar-hoverBackground,var(--vscode-list-hoverBackground))] hover:opacity-100 group-hover:opacity-70"
|
||||
>
|
||||
{kind === 'url' ? <ExternalLink className="size-3.5" /> : <Folder className="size-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{firstLine || <span className="opacity-40">(空)</span>}
|
||||
</span>
|
||||
{withImage && (
|
||||
<ImageIcon
|
||||
className="size-3.5 shrink-0 text-[var(--vscode-descriptionForeground)]"
|
||||
aria-label="含图片"
|
||||
/>
|
||||
)}
|
||||
{kind && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpen(value)
|
||||
}}
|
||||
title={kind === 'url' ? '在浏览器打开' : '在系统中打开'}
|
||||
aria-label={kind === 'url' ? '在浏览器打开' : '在系统中打开'}
|
||||
className="grid size-5 shrink-0 place-items-center rounded opacity-0 transition-opacity hover:bg-[var(--vscode-toolbar-hoverBackground,var(--vscode-list-hoverBackground))] hover:opacity-100 group-hover:opacity-70"
|
||||
>
|
||||
{kind === 'url' ? <ExternalLink className="size-3.5" /> : <Folder className="size-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in new issue
Block a user