feat: 自由创作任务素材组编辑优化
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
// processing 素材每 8s 轮询火山刷新状态(与基础资产送审轮询同节奏)。
|
// processing 素材每 8s 轮询火山刷新状态(与基础资产送审轮询同节奏)。
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { ChevronLeft, FolderPlus, Pencil, Trash2, Upload, Users, X } from "lucide-react";
|
import { Check, ChevronLeft, FolderPlus, Pencil, Trash2, Upload, Users, X } from "lucide-react";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import type { FreeAssetGroup, FreeAssetItem, FreeVideoRef } from "../../types";
|
import type { FreeAssetGroup, FreeAssetItem, FreeVideoRef } from "../../types";
|
||||||
import { useBodyScrollLock } from "../overlays";
|
import { useBodyScrollLock } from "../overlays";
|
||||||
@@ -25,9 +25,20 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
const [assets, setAssets] = useState<FreeAssetItem[]>([]);
|
const [assets, setAssets] = useState<FreeAssetItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [creatingGroup, setCreatingGroup] = useState(false);
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [quickUploading, setQuickUploading] = useState(false);
|
const [quickUploading, setQuickUploading] = useState(false);
|
||||||
|
const [editingAssetId, setEditingAssetId] = useState<string | null>(null);
|
||||||
|
const [editingName, setEditingName] = useState("");
|
||||||
|
const [renamingAssetId, setRenamingAssetId] = useState<string | null>(null);
|
||||||
|
const [confirmDeleteAssetId, setConfirmDeleteAssetId] = useState<string | null>(null);
|
||||||
|
const [deletingAssetId, setDeletingAssetId] = useState<string | null>(null);
|
||||||
|
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
||||||
|
const [editingGroupName, setEditingGroupName] = useState("");
|
||||||
|
const [renamingGroupId, setRenamingGroupId] = useState<string | null>(null);
|
||||||
|
const [confirmDeleteGroupId, setConfirmDeleteGroupId] = useState<string | null>(null);
|
||||||
|
const [deletingGroupId, setDeletingGroupId] = useState<string | null>(null);
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
const quickFileRef = useRef<HTMLInputElement>(null);
|
const quickFileRef = useRef<HTMLInputElement>(null);
|
||||||
useBodyScrollLock(open);
|
useBodyScrollLock(open);
|
||||||
@@ -61,6 +72,12 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
if (!open) return;
|
if (!open) return;
|
||||||
setActiveGroup(null);
|
setActiveGroup(null);
|
||||||
setAssets([]);
|
setAssets([]);
|
||||||
|
setEditingAssetId(null);
|
||||||
|
setEditingName("");
|
||||||
|
setConfirmDeleteAssetId(null);
|
||||||
|
setEditingGroupId(null);
|
||||||
|
setEditingGroupName("");
|
||||||
|
setConfirmDeleteGroupId(null);
|
||||||
void loadGroups();
|
void loadGroups();
|
||||||
}, [open, loadGroups]);
|
}, [open, loadGroups]);
|
||||||
|
|
||||||
@@ -83,7 +100,8 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
|
|
||||||
const createGroup = async () => {
|
const createGroup = async () => {
|
||||||
const name = newName.trim();
|
const name = newName.trim();
|
||||||
if (!name) return;
|
if (!name || creatingGroup) return;
|
||||||
|
setCreatingGroup(true);
|
||||||
try {
|
try {
|
||||||
const data = await api.createFreeAssetGroup({ name });
|
const data = await api.createFreeAssetGroup({ name });
|
||||||
setGroups((prev) => [data.group, ...prev]);
|
setGroups((prev) => [data.group, ...prev]);
|
||||||
@@ -92,6 +110,8 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
notify("success", `素材组「${name}」已创建`);
|
notify("success", `素材组「${name}」已创建`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify("error", error instanceof Error ? error.message : "创建失败");
|
notify("error", error instanceof Error ? error.message : "创建失败");
|
||||||
|
} finally {
|
||||||
|
setCreatingGroup(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,48 +148,100 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startRenameAsset = (item: FreeAssetItem) => {
|
||||||
|
setConfirmDeleteAssetId(null);
|
||||||
|
setConfirmDeleteGroupId(null);
|
||||||
|
setEditingAssetId(item.id);
|
||||||
|
setEditingName(item.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelRenameAsset = () => {
|
||||||
|
setEditingAssetId(null);
|
||||||
|
setEditingName("");
|
||||||
|
};
|
||||||
|
|
||||||
const renameAsset = async (item: FreeAssetItem) => {
|
const renameAsset = async (item: FreeAssetItem) => {
|
||||||
const name = window.prompt("素材名称(用于 @ 引用)", item.name);
|
const name = editingName.trim();
|
||||||
if (!name || name.trim() === item.name) return;
|
if (!name) {
|
||||||
|
notify("info", "素材名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name === item.name) {
|
||||||
|
cancelRenameAsset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRenamingAssetId(item.id);
|
||||||
try {
|
try {
|
||||||
const data = await api.renameFreeAsset(item.id, name.trim());
|
const data = await api.renameFreeAsset(item.id, name);
|
||||||
setAssets((prev) => prev.map((a) => (a.id === item.id ? data.asset : a)));
|
setAssets((prev) => prev.map((a) => (a.id === item.id ? data.asset : a)));
|
||||||
|
cancelRenameAsset();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify("error", error instanceof Error ? error.message : "重命名失败");
|
notify("error", error instanceof Error ? error.message : "重命名失败");
|
||||||
|
} finally {
|
||||||
|
setRenamingAssetId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeAsset = async (item: FreeAssetItem) => {
|
const removeAsset = async (item: FreeAssetItem) => {
|
||||||
if (!window.confirm(`删除素材「${item.name}」?`)) return;
|
setDeletingAssetId(item.id);
|
||||||
try {
|
try {
|
||||||
await api.deleteFreeAsset(item.id);
|
await api.deleteFreeAsset(item.id);
|
||||||
setAssets((prev) => prev.filter((a) => a.id !== item.id));
|
setAssets((prev) => prev.filter((a) => a.id !== item.id));
|
||||||
|
setConfirmDeleteAssetId(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify("error", error instanceof Error ? error.message : "删除失败");
|
notify("error", error instanceof Error ? error.message : "删除失败");
|
||||||
|
} finally {
|
||||||
|
setDeletingAssetId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startRenameGroup = (group: FreeAssetGroup) => {
|
||||||
|
setConfirmDeleteAssetId(null);
|
||||||
|
setConfirmDeleteGroupId(null);
|
||||||
|
setEditingGroupId(group.id);
|
||||||
|
setEditingGroupName(group.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelRenameGroup = () => {
|
||||||
|
setEditingGroupId(null);
|
||||||
|
setEditingGroupName("");
|
||||||
|
};
|
||||||
|
|
||||||
const renameGroup = async (group: FreeAssetGroup) => {
|
const renameGroup = async (group: FreeAssetGroup) => {
|
||||||
const name = window.prompt("素材组名称", group.name);
|
const name = editingGroupName.trim();
|
||||||
if (!name || name.trim() === group.name) return;
|
if (!name) {
|
||||||
|
notify("info", "素材组名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name === group.name) {
|
||||||
|
cancelRenameGroup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRenamingGroupId(group.id);
|
||||||
try {
|
try {
|
||||||
const data = await api.updateFreeAssetGroup(group.id, { name: name.trim() });
|
const data = await api.updateFreeAssetGroup(group.id, { name });
|
||||||
setGroups((prev) => prev.map((g) => (g.id === group.id ? data.group : g)));
|
setGroups((prev) => prev.map((g) => (g.id === group.id ? data.group : g)));
|
||||||
if (activeGroup?.id === group.id) setActiveGroup(data.group);
|
if (activeGroup?.id === group.id) setActiveGroup(data.group);
|
||||||
|
cancelRenameGroup();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify("error", error instanceof Error ? error.message : "重命名失败");
|
notify("error", error instanceof Error ? error.message : "重命名失败");
|
||||||
|
} finally {
|
||||||
|
setRenamingGroupId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeGroup = async (group: FreeAssetGroup) => {
|
const removeGroup = async (group: FreeAssetGroup) => {
|
||||||
if (!window.confirm(`删除素材组「${group.name}」及其全部素材?此操作不可恢复。`)) return;
|
setDeletingGroupId(group.id);
|
||||||
try {
|
try {
|
||||||
await api.deleteFreeAssetGroup(group.id);
|
await api.deleteFreeAssetGroup(group.id);
|
||||||
setGroups((prev) => prev.filter((g) => g.id !== group.id));
|
setGroups((prev) => prev.filter((g) => g.id !== group.id));
|
||||||
if (activeGroup?.id === group.id) { setActiveGroup(null); setAssets([]); }
|
if (activeGroup?.id === group.id) { setActiveGroup(null); setAssets([]); }
|
||||||
|
setConfirmDeleteGroupId(null);
|
||||||
notify("success", "素材组已删除");
|
notify("success", "素材组已删除");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify("error", error instanceof Error ? error.message : "删除失败");
|
notify("error", error instanceof Error ? error.message : "删除失败");
|
||||||
|
} finally {
|
||||||
|
setDeletingGroupId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,7 +278,7 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
</div>
|
</div>
|
||||||
<button className="x modal-x" type="button" onClick={onClose} aria-label="关闭"><X size={14} /></button>
|
<button className="x modal-x" type="button" onClick={onClose} aria-label="关闭"><X size={14} /></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-b fc-lib-body">
|
<div className="modal-b fc-lib-body" onClick={() => { setConfirmDeleteAssetId(null); setConfirmDeleteGroupId(null); }}>
|
||||||
{!activeGroup ? (
|
{!activeGroup ? (
|
||||||
<>
|
<>
|
||||||
<div className="fc-lib-toolbar">
|
<div className="fc-lib-toolbar">
|
||||||
@@ -228,11 +300,17 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
autoFocus
|
autoFocus
|
||||||
placeholder="素材组名称(如:碧碧)"
|
placeholder="素材组名称(如:碧碧)"
|
||||||
value={newName}
|
value={newName}
|
||||||
|
disabled={creatingGroup}
|
||||||
onChange={(event) => setNewName(event.target.value)}
|
onChange={(event) => setNewName(event.target.value)}
|
||||||
onKeyDown={(event) => { if (event.key === "Enter") void createGroup(); if (event.key === "Escape") setCreating(false); }}
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" && !creatingGroup) void createGroup();
|
||||||
|
if (event.key === "Escape" && !creatingGroup) setCreating(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button type="button" className="btn btn-sm btn-primary" onClick={() => void createGroup()}>创建</button>
|
<button type="button" className="btn btn-sm btn-primary" disabled={creatingGroup || !newName.trim()} onClick={() => void createGroup()}>
|
||||||
<button type="button" className="btn btn-sm btn-ghost" onClick={() => setCreating(false)}>取消</button>
|
{creatingGroup ? "创建中…" : "创建"}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-sm btn-ghost" disabled={creatingGroup} onClick={() => setCreating(false)}>取消</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -252,19 +330,71 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="fc-lib-groups">
|
<div className="fc-lib-groups">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => {
|
||||||
<div key={group.id} className="fc-lib-group" role="button" tabIndex={0} onClick={() => void loadGroupDetail(group)} onKeyDown={(event) => { if (event.key === "Enter") void loadGroupDetail(group); }}>
|
const isEditing = editingGroupId === group.id;
|
||||||
<div className="fc-lib-thumb">
|
const isDeleting = confirmDeleteGroupId === group.id;
|
||||||
{group.thumbnail_url ? <img src={group.thumbnail_url} alt={group.name} /> : <Users size={20} />}
|
return (
|
||||||
|
<div
|
||||||
|
key={group.id}
|
||||||
|
className="fc-lib-group"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => { if (!editingGroupId) void loadGroupDetail(group); }}
|
||||||
|
onKeyDown={(event) => { if (!editingGroupId && event.key === "Enter") void loadGroupDetail(group); }}
|
||||||
|
>
|
||||||
|
<div className="fc-lib-thumb">
|
||||||
|
{group.thumbnail_url ? <img src={group.thumbnail_url} alt={group.name} /> : <Users size={20} />}
|
||||||
|
</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="fc-lib-rename-row" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<input
|
||||||
|
className="input fc-lib-name-input"
|
||||||
|
autoFocus
|
||||||
|
value={editingGroupName}
|
||||||
|
disabled={renamingGroupId === group.id}
|
||||||
|
onChange={(event) => setEditingGroupName(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.key === "Enter") void renameGroup(group);
|
||||||
|
if (event.key === "Escape") cancelRenameGroup();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="fc-lib-rename-btn ok"
|
||||||
|
title="保存"
|
||||||
|
disabled={renamingGroupId === group.id || !editingGroupName.trim()}
|
||||||
|
onClick={() => void renameGroup(group)}
|
||||||
|
>
|
||||||
|
<Check size={12} />
|
||||||
|
</button>
|
||||||
|
<button type="button" className="fc-lib-rename-btn" title="取消" onClick={cancelRenameGroup}>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="fc-lib-name" title={group.name}>{group.name}</div>
|
||||||
|
)}
|
||||||
|
<div className="fc-lib-count mono">// {group.asset_count} 个素材</div>
|
||||||
|
<div className="fc-lib-ops">
|
||||||
|
<button type="button" title="重命名" onClick={(event) => { event.stopPropagation(); startRenameGroup(group); }}><Pencil size={12} /></button>
|
||||||
|
<button type="button" title="删除" onClick={(event) => { event.stopPropagation(); setEditingGroupId(null); setConfirmDeleteGroupId(group.id); }}><Trash2 size={12} /></button>
|
||||||
|
</div>
|
||||||
|
{isDeleting ? (
|
||||||
|
<div className="fc-lib-delete-pop" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div className="fc-lib-delete-title">删除素材组?</div>
|
||||||
|
<div className="fc-lib-delete-note mono">// 组内素材也会删除</div>
|
||||||
|
<div className="fc-lib-delete-actions">
|
||||||
|
<button type="button" className="btn btn-sm btn-ghost" onClick={() => setConfirmDeleteGroupId(null)}>取消</button>
|
||||||
|
<button type="button" className="btn btn-sm fc-lib-delete-danger" disabled={deletingGroupId === group.id} onClick={() => void removeGroup(group)}>
|
||||||
|
{deletingGroupId === group.id ? "删除中…" : "删除"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="fc-lib-name">{group.name}</div>
|
);
|
||||||
<div className="fc-lib-count mono">// {group.asset_count} 个素材</div>
|
})}
|
||||||
<div className="fc-lib-ops">
|
|
||||||
<button type="button" title="重命名" onClick={(event) => { event.stopPropagation(); void renameGroup(group); }}><Pencil size={12} /></button>
|
|
||||||
<button type="button" title="删除" onClick={(event) => { event.stopPropagation(); void removeGroup(group); }}><Trash2 size={12} /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -297,19 +427,68 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
|
|||||||
<div className="fc-lib-assets">
|
<div className="fc-lib-assets">
|
||||||
{assets.map((item) => {
|
{assets.map((item) => {
|
||||||
const pill = STATUS_PILL[item.status];
|
const pill = STATUS_PILL[item.status];
|
||||||
|
const isEditing = editingAssetId === item.id;
|
||||||
|
const isDeleting = confirmDeleteAssetId === item.id;
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className={`fc-lib-asset${item.status === "active" ? " pickable" : ""}`} role="button" tabIndex={0} onClick={() => pick(item)} onKeyDown={(event) => { if (event.key === "Enter") pick(item); }}>
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`fc-lib-asset${item.status === "active" ? " pickable" : ""}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => { if (!editingAssetId) pick(item); }}
|
||||||
|
onKeyDown={(event) => { if (!editingAssetId && event.key === "Enter") pick(item); }}
|
||||||
|
>
|
||||||
<div className="fc-lib-thumb">
|
<div className="fc-lib-thumb">
|
||||||
{item.thumb_url ? <img src={item.thumb_url} alt={item.name} /> : <span className="mono">{item.type === "audio" ? "♪" : item.type.toUpperCase()}</span>}
|
{item.thumb_url ? <img src={item.thumb_url} alt={item.name} /> : <span className="mono">{item.type === "audio" ? "♪" : item.type.toUpperCase()}</span>}
|
||||||
{item.duration ? <span className="fc-ref-dur mono">{item.duration}s</span> : null}
|
{item.duration ? <span className="fc-ref-dur mono">{item.duration}s</span> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="fc-lib-name" title={item.name}>{item.name}</div>
|
{isEditing ? (
|
||||||
<span className={`pill pill-l3 ${pill.cls}`}><span className="dot" />{pill.label}</span>
|
<div className="fc-lib-rename-row" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<input
|
||||||
|
className="input fc-lib-name-input"
|
||||||
|
autoFocus
|
||||||
|
value={editingName}
|
||||||
|
disabled={renamingAssetId === item.id}
|
||||||
|
onChange={(event) => setEditingName(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.key === "Enter") void renameAsset(item);
|
||||||
|
if (event.key === "Escape") cancelRenameAsset();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="fc-lib-rename-btn ok"
|
||||||
|
title="保存"
|
||||||
|
disabled={renamingAssetId === item.id || !editingName.trim()}
|
||||||
|
onClick={() => void renameAsset(item)}
|
||||||
|
>
|
||||||
|
<Check size={12} />
|
||||||
|
</button>
|
||||||
|
<button type="button" className="fc-lib-rename-btn" title="取消" onClick={cancelRenameAsset}>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="fc-lib-name" title={item.name}>{item.name}</div>
|
||||||
|
)}
|
||||||
|
<span className={`pill pill-l3 fc-lib-status ${pill.cls}`}><span className="dot" />{pill.label}</span>
|
||||||
{item.status === "failed" && item.error_message && <div className="fc-lib-err mono" title={item.error_message}>// {item.error_message}</div>}
|
{item.status === "failed" && item.error_message && <div className="fc-lib-err mono" title={item.error_message}>// {item.error_message}</div>}
|
||||||
<div className="fc-lib-ops">
|
<div className="fc-lib-ops">
|
||||||
<button type="button" title="重命名" onClick={(event) => { event.stopPropagation(); void renameAsset(item); }}><Pencil size={12} /></button>
|
<button type="button" title="重命名" onClick={(event) => { event.stopPropagation(); startRenameAsset(item); }}><Pencil size={12} /></button>
|
||||||
<button type="button" title="删除" onClick={(event) => { event.stopPropagation(); void removeAsset(item); }}><Trash2 size={12} /></button>
|
<button type="button" title="删除" onClick={(event) => { event.stopPropagation(); setEditingAssetId(null); setConfirmDeleteAssetId(item.id); }}><Trash2 size={12} /></button>
|
||||||
</div>
|
</div>
|
||||||
|
{isDeleting ? (
|
||||||
|
<div className="fc-lib-delete-pop" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div className="fc-lib-delete-title">删除素材?</div>
|
||||||
|
<div className="fc-lib-delete-actions">
|
||||||
|
<button type="button" className="btn btn-sm btn-ghost" onClick={() => setConfirmDeleteAssetId(null)}>取消</button>
|
||||||
|
<button type="button" className="btn btn-sm fc-lib-delete-danger" disabled={deletingAssetId === item.id} onClick={() => void removeAsset(item)}>
|
||||||
|
{deletingAssetId === item.id ? "删除中…" : "删除"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -555,8 +555,61 @@
|
|||||||
}
|
}
|
||||||
.fc-lib-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
.fc-lib-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
.fc-lib-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.fc-lib-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.fc-lib-rename-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 24px 24px;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.fc-lib-name-input {
|
||||||
|
height: 28px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.fc-lib-rename-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--border-faint);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--black-alpha-56);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.fc-lib-rename-btn:hover { color: var(--accent-black); border-color: var(--black-alpha-24); background: var(--black-alpha-4); }
|
||||||
|
.fc-lib-rename-btn.ok { color: var(--accent-forest); border-color: var(--forest-bd); background: var(--forest-bg); }
|
||||||
|
.fc-lib-rename-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||||
.fc-lib-count { font-size: 10.5px; letter-spacing: 0.04em; color: var(--black-alpha-48); }
|
.fc-lib-count { font-size: 10.5px; letter-spacing: 0.04em; color: var(--black-alpha-48); }
|
||||||
.fc-lib-err { font-size: 10.5px; color: var(--accent-crimson); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.fc-lib-err { font-size: 10.5px; color: var(--accent-crimson); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.fc-lib-status {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.fc-lib-status.pill-ok {
|
||||||
|
background: var(--forest-bg);
|
||||||
|
color: var(--accent-forest);
|
||||||
|
border-color: var(--forest-bd);
|
||||||
|
}
|
||||||
|
.fc-lib-status.pill-info {
|
||||||
|
background: var(--heat-12);
|
||||||
|
color: var(--heat);
|
||||||
|
border-color: var(--heat-20);
|
||||||
|
}
|
||||||
|
.fc-lib-status.pill-err {
|
||||||
|
background: var(--crimson-bg);
|
||||||
|
color: var(--accent-crimson);
|
||||||
|
border-color: var(--crimson-bd);
|
||||||
|
}
|
||||||
|
.fc-lib-status .dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
.fc-lib-ops { position: absolute; top: 14px; right: 14px; display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; }
|
.fc-lib-ops { position: absolute; top: 14px; right: 14px; display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; }
|
||||||
.fc-lib-group:hover .fc-lib-ops, .fc-lib-asset:hover .fc-lib-ops { opacity: 1; }
|
.fc-lib-group:hover .fc-lib-ops, .fc-lib-asset:hover .fc-lib-ops { opacity: 1; }
|
||||||
.fc-lib-ops button {
|
.fc-lib-ops button {
|
||||||
@@ -572,6 +625,58 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.fc-lib-ops button:hover { background: var(--accent-black); }
|
.fc-lib-ops button:hover { background: var(--accent-black); }
|
||||||
|
.fc-lib-delete-pop {
|
||||||
|
position: absolute;
|
||||||
|
top: 42px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
min-width: 148px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-muted);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: 0 0 0 1px var(--black-alpha-7), var(--shadow-floating);
|
||||||
|
}
|
||||||
|
.fc-lib-delete-pop::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: 16px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-left: 1px solid var(--border-muted);
|
||||||
|
border-top: 1px solid var(--border-muted);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.fc-lib-delete-title {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-black);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.fc-lib-delete-note {
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--black-alpha-48);
|
||||||
|
margin: -2px 0 8px;
|
||||||
|
}
|
||||||
|
.fc-lib-delete-actions { display: flex; justify-content: flex-end; gap: 6px; }
|
||||||
|
.fc-lib-delete-actions .btn {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.fc-lib-delete-danger {
|
||||||
|
color: var(--accent-crimson);
|
||||||
|
border-color: var(--crimson-bd);
|
||||||
|
background: var(--crimson-bg);
|
||||||
|
}
|
||||||
|
.fc-lib-delete-danger:hover {
|
||||||
|
color: var(--accent-crimson);
|
||||||
|
border-color: var(--crimson-bd);
|
||||||
|
background: var(--crimson-bg);
|
||||||
|
}
|
||||||
|
|
||||||
/* 移动端:输入条参数换行、任务流单列 */
|
/* 移动端:输入条参数换行、任务流单列 */
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user