fix(*): 素材库与画布 - 修复批量删除刷新、选择布局及工具引用
This commit is contained in:
@@ -13,7 +13,7 @@ import { PaginationBar } from "@/components/layout/workspace-page";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ExternalAssetPickerReference } from "@/lib/plugins/plugin-types";
|
||||
import { flushAssetStorePersistence, useAssetStore, type Asset } from "@/stores/use-asset-store";
|
||||
import { deleteAssetWithRemoteSync, loadAssetLibraryPage, localSavedRemotePendingMessage, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
import { deleteAssetsWithRemoteSync, loadAssetLibraryPage, localSavedRemotePendingMessage, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
|
||||
export type AssetPickerMediaKind = "image" | "video" | "audio" | "text";
|
||||
|
||||
@@ -291,14 +291,13 @@ export function AssetLibraryPickerModal({
|
||||
if (!archivedSelectedIds.length) return;
|
||||
setWorking(true);
|
||||
try {
|
||||
for (const id of archivedSelectedIds) await deleteAssetWithRemoteSync(id);
|
||||
await deleteAssetsWithRemoteSync(archivedSelectedIds);
|
||||
setSelected(new Set());
|
||||
message.success(`已彻底删除 ${archivedSelectedIds.length} 个素材`);
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "删除失败");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
if (remoteEnabled) void remoteQuery.refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -307,7 +306,7 @@ export function AssetLibraryPickerModal({
|
||||
if (!toDelete.length) return;
|
||||
setWorking(true);
|
||||
try {
|
||||
for (const item of toDelete) await deleteAssetWithRemoteSync(item.id);
|
||||
await deleteAssetsWithRemoteSync(toDelete.map((item) => item.id));
|
||||
setSelected(new Set());
|
||||
message.success(`已删除${remoteEnabled ? "当前页" : "回收站"} ${toDelete.length} 个素材`);
|
||||
setCategory("all");
|
||||
@@ -315,7 +314,6 @@ export function AssetLibraryPickerModal({
|
||||
message.error(err instanceof Error ? err.message : "清空回收站失败");
|
||||
} finally {
|
||||
setWorking(false);
|
||||
if (remoteEnabled) void remoteQuery.refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -532,7 +530,7 @@ export function AssetLibraryPickerModal({
|
||||
<div className="asset-picker-actions">
|
||||
{isRecycleBin ? (
|
||||
<>
|
||||
<Popconfirm title={remoteEnabled ? "确认删除当前页回收站素材?" : "确认清空回收站?"} description="仅删除当前列表中的素材;仍被引用的素材由服务端拒绝删除。删除不可恢复。" onConfirm={handleEmptyRecycleBin} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
<Popconfirm title={remoteEnabled ? "确认删除当前页回收站素材?" : "确认清空回收站?"} description="仅删除当前列表中的素材;关联文件会直接释放,原画布或任务中的旧引用可能失效。删除不可恢复。" onConfirm={handleEmptyRecycleBin} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
<Button type="text" danger disabled={working || !archivedCount}>
|
||||
{remoteEnabled ? "删除当前页" : "清空回收站"}
|
||||
</Button>
|
||||
|
||||
@@ -90,6 +90,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const hasImageContent = node.type === CanvasNodeType.Image && Boolean(node.metadata?.content);
|
||||
const savedPrompt = node.metadata?.composerContent ?? node.metadata?.prompt ?? "";
|
||||
const [prompt, setPrompt] = useState(savedPrompt);
|
||||
const promptRef = useRef(savedPrompt);
|
||||
const [presetOpen, setPresetOpen] = useState(false);
|
||||
const [expandedPresetOpen, setExpandedPresetOpen] = useState(false);
|
||||
const [nineGridOpen, setNineGridOpen] = useState(false);
|
||||
@@ -213,6 +214,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const canAutoMention = autoMentionedPrompt !== prompt;
|
||||
|
||||
useEffect(() => {
|
||||
promptRef.current = normalizedSavedPrompt;
|
||||
setPrompt(normalizedSavedPrompt);
|
||||
if (normalizedSavedPrompt !== savedPrompt) onPromptChange(node.id, normalizedSavedPrompt);
|
||||
}, [node.id, normalizedSavedPrompt, onPromptChange, savedPrompt]);
|
||||
@@ -258,6 +260,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const skillReferences = useMemo(() => resolvedMentionReferences.filter((item) => item.kind === "skill"), [resolvedMentionReferences]);
|
||||
|
||||
const updatePrompt = (value: string) => {
|
||||
promptRef.current = value;
|
||||
setPrompt(value);
|
||||
onPromptChange(node.id, value);
|
||||
if (showPromptTemplates && /(^|\s)\/[\p{L}\p{N}_-]*$/u.test(value)) {
|
||||
@@ -266,6 +269,10 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
}
|
||||
};
|
||||
|
||||
const updatePromptFromCurrent = (updater: (currentPrompt: string) => string) => {
|
||||
updatePrompt(updater(promptRef.current));
|
||||
};
|
||||
|
||||
const applyPreset = (preset: CanvasPromptPreset) => {
|
||||
const withoutSlash = prompt.replace(/(^|\s)\/[\p{L}\p{N}_-]*$/u, "$1").trimEnd();
|
||||
updatePrompt(withoutSlash ? `${withoutSlash}\n${preset.prompt}` : preset.prompt);
|
||||
@@ -285,7 +292,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
updatePrompt(trimmedBase ? `${trimmedBase} ${insertText}` : insertText);
|
||||
};
|
||||
|
||||
const removeMotionToolMention = () => updatePrompt(removeToolMentions(prompt, "motion"));
|
||||
const removeMotionToolMention = () => updatePromptFromCurrent((currentPrompt) => removeToolMentions(currentPrompt, "motion"));
|
||||
|
||||
const submit = () => {
|
||||
const text = prompt.trim();
|
||||
@@ -330,11 +337,11 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
<div className="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1">
|
||||
{!simpleMode && (mode === "image" || mode === "video") ? <div className="canvas-node-tool-controls canvas-node-tool-controls-inline flex items-center gap-1" data-canvas-no-zoom data-canvas-wheel-scroll onPointerDown={e => e.stopPropagation()}>
|
||||
{mode === "image" ? <>
|
||||
<CanvasChooseImageStylePicker open={expanded ? expandedStyleToolOpen : styleToolOpen} onOpenChange={expanded ? setExpandedStyleToolOpen : setStyleToolOpen} activeToolId={activeStyleTool?.toolId} activeLabel={activeStyleTool?.label} onSelect={(id,label) => updatePrompt(applyToolMention(prompt,{id,label,type:"style"},"Palette"))} onClear={() => updatePrompt(removeToolMentions(prompt,"style"))} />
|
||||
<CanvasNineGridPicker open={expanded ? expandedNineGridOpen : nineGridOpen} onOpenChange={expanded ? setExpandedNineGridOpen : setNineGridOpen} icon={activeNineGridIcon} onSelect={(id,label,icon) => updatePrompt(applyToolMention(prompt,{id,label,type:"nine_grid"},icon))} />
|
||||
<CanvasChooseImageStylePicker open={expanded ? expandedStyleToolOpen : styleToolOpen} onOpenChange={expanded ? setExpandedStyleToolOpen : setStyleToolOpen} activeToolId={activeStyleTool?.toolId} activeLabel={activeStyleTool?.label} onSelect={(id,label) => updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"style"},"Palette"))} onClear={() => updatePromptFromCurrent((currentPrompt) => removeToolMentions(currentPrompt,"style"))} />
|
||||
<CanvasNineGridPicker open={expanded ? expandedNineGridOpen : nineGridOpen} onOpenChange={expanded ? setExpandedNineGridOpen : setNineGridOpen} icon={activeNineGridIcon} onSelect={(id,label,icon) => updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"nine_grid"},icon))} />
|
||||
</> : <>
|
||||
<CanvasChooseEffectPicker open={expanded ? expandedEffectToolOpen : effectToolOpen} onOpenChange={expanded ? setExpandedEffectToolOpen : setEffectToolOpen} activeToolId={activeEffectTool?.toolId} activeLabel={activeEffectTool?.label} onSelect={(id,label) => updatePrompt(applyToolMention(prompt,{id,label,type:"effect"},"Sparkles"))} onClear={() => updatePrompt(removeToolMentions(prompt,"effect"))} />
|
||||
<CanvasChooseMotionPicker open={expanded ? expandedMotionToolOpen : motionToolOpen} onOpenChange={expanded ? setExpandedMotionToolOpen : setMotionToolOpen} activeToolIds={activeMotionTools.map(t => t.toolId)} activeLabel={activeMotionTool?.label} onSelect={(id,label) => updatePrompt(applyToolMention(prompt,{id,label,type:"motion"},"Camera"))} onClear={removeMotionToolMention} />
|
||||
<CanvasChooseEffectPicker open={expanded ? expandedEffectToolOpen : effectToolOpen} onOpenChange={expanded ? setExpandedEffectToolOpen : setEffectToolOpen} activeToolId={activeEffectTool?.toolId} activeLabel={activeEffectTool?.label} onSelect={(id,label) => updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"effect"},"Sparkles"))} onClear={() => updatePromptFromCurrent((currentPrompt) => removeToolMentions(currentPrompt,"effect"))} />
|
||||
<CanvasChooseMotionPicker open={expanded ? expandedMotionToolOpen : motionToolOpen} onOpenChange={expanded ? setExpandedMotionToolOpen : setMotionToolOpen} activeToolIds={activeMotionTools.map(t => t.toolId)} activeLabel={activeMotionTool?.label} onSelect={(id,label) => updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"motion"},"Camera"))} onClear={removeMotionToolMention} />
|
||||
</>}
|
||||
</div> : null}
|
||||
{showPromptTemplates ? <CanvasPresetPicker mode={mode} skillReferences={skillReferences} open={expanded ? expandedPresetOpen : presetOpen} onOpenChange={expanded ? setExpandedPresetOpen : setPresetOpen} onSelect={applyPreset} dense appearance="quiet" /> : null}
|
||||
|
||||
@@ -52,11 +52,12 @@ const TOOL_REF_PATTERN = /@\[tool:(\w+):(\d+):([^:\]]+):([^\]]+)\]/g;
|
||||
/** 从文本中解析所有 @[tool:type:ID:label:icon] 令牌,返回去重的工具引用。 */
|
||||
export function parseToolMentionTokens(text: string): { type: string; toolId: number; label: string; icon: string }[] {
|
||||
const results: { type: string; toolId: number; label: string; icon: string }[] = [];
|
||||
const seen = new Set<number>();
|
||||
const seen = new Set<string>();
|
||||
for (const match of text.matchAll(TOOL_REF_PATTERN)) {
|
||||
const toolId = Number(match[2]);
|
||||
if (seen.has(toolId)) continue;
|
||||
seen.add(toolId);
|
||||
const identity = `${match[1]}:${toolId}`;
|
||||
if (seen.has(identity)) continue;
|
||||
seen.add(identity);
|
||||
let label = match[3];
|
||||
try { label = decodeURIComponent(label); } catch { /* Keep readable text for malformed imported labels. */ }
|
||||
results.push({ type: match[1], toolId, label, icon: match[4] });
|
||||
@@ -585,8 +586,8 @@ function skillResourceText(node: CanvasNodeData) {
|
||||
|
||||
export function buildToolMentionReference(toolId: number, label: string, type: string, icon: string): CanvasResourceReference {
|
||||
return {
|
||||
id: `tool:${toolId}`,
|
||||
nodeId: `tool:${toolId}`,
|
||||
id: `tool:${type}:${toolId}`,
|
||||
nodeId: `tool:${type}:${toolId}`,
|
||||
kind: "tool",
|
||||
label,
|
||||
title: label,
|
||||
|
||||
@@ -26,7 +26,7 @@ import { uploadMediaFile } from "@/services/file-storage";
|
||||
import { flushAssetStorePersistence, useAssetStore, type Asset, type AssetCategory, type AssetKind, type ImageAsset } from "@/stores/use-asset-store";
|
||||
import { exportAssets, readAssetPackage } from "./asset-transfer";
|
||||
import { AssetStorageUsage, assetStorageUsageQueryKey } from "./asset-storage-usage";
|
||||
import { deleteAssetWithRemoteSync, loadAssetLibraryPage, localSavedRemotePendingMessage, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
import { deleteAssetWithRemoteSync, deleteAssetsWithRemoteSync, loadAssetLibraryPage, localSavedRemotePendingMessage, saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { createAssetFolder, deleteAssetFolder, listAssetFolders, listRemoteAssetsPage, moveRemoteAssetsToFolder, updateAssetFolder, type AssetFolder } from "@/services/api/user-data";
|
||||
import { AssetBatchUploadModal } from "./asset-batch-upload-modal";
|
||||
@@ -487,9 +487,7 @@ export default function AssetsPage() {
|
||||
const count = trashAssets.length;
|
||||
if (!count) return;
|
||||
try {
|
||||
for (const asset of trashAssets) {
|
||||
await deleteAssetWithRemoteSync(asset.id);
|
||||
}
|
||||
await deleteAssetsWithRemoteSync(trashAssets.map((asset) => asset.id));
|
||||
setSelectedIds([]);
|
||||
message.success(`已彻底清空回收站 ${count} 个素材`);
|
||||
} catch (error) {
|
||||
@@ -516,7 +514,7 @@ export default function AssetsPage() {
|
||||
const confirmBatchDelete = async () => {
|
||||
if (!selectedAssets.length) return;
|
||||
try {
|
||||
for (const asset of selectedAssets) await deleteAssetWithRemoteSync(asset.id);
|
||||
await deleteAssetsWithRemoteSync(selectedAssets.map((asset) => asset.id));
|
||||
message.success(`已彻底删除 ${selectedAssets.length} 个素材`);
|
||||
setSelectedIds([]);
|
||||
setBatchDeleteOpen(false);
|
||||
@@ -982,7 +980,7 @@ export default function AssetsPage() {
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="取消"
|
||||
>
|
||||
确定彻底删除「{deletingAsset?.title}」吗?未被其他内容引用的服务器本地或对象存储文件也会同步删除,操作不可恢复。
|
||||
确定彻底删除「{deletingAsset?.title}」吗?未被其他素材复用的服务器文件会直接释放,原画布或任务中的旧引用可能失效,操作不可恢复。
|
||||
</Modal>
|
||||
<Modal
|
||||
className="library-modal library-confirm-modal"
|
||||
@@ -994,7 +992,7 @@ export default function AssetsPage() {
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="取消"
|
||||
>
|
||||
确定彻底删除已选择的 {selectedAssets.length} 个素材吗?未被复用的服务器文件会同步删除,操作不可恢复。
|
||||
确定彻底删除已选择的 {selectedAssets.length} 个素材吗?未被其他素材复用的服务器文件会直接释放,原画布或任务中的旧引用可能失效,操作不可恢复。
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -359,11 +359,12 @@ export function useCanvasRenderModel({
|
||||
const text = node.metadata?.composerContent ?? node.metadata?.prompt ?? "";
|
||||
const tokens = parseToolMentionTokens(text);
|
||||
if (!tokens.length) continue;
|
||||
const seen = new Set<number>();
|
||||
const seen = new Set<string>();
|
||||
const refs: ReturnType<typeof buildToolMentionReference>[] = [];
|
||||
for (const { type, toolId, label, icon } of tokens) {
|
||||
if (seen.has(toolId)) continue;
|
||||
seen.add(toolId);
|
||||
const identity = `${type}:${toolId}`;
|
||||
if (seen.has(identity)) continue;
|
||||
seen.add(identity);
|
||||
refs.push(buildToolMentionReference(toolId, label, type, icon));
|
||||
}
|
||||
if (refs.length) map.set(node.id, refs);
|
||||
|
||||
@@ -109,6 +109,10 @@ export function deleteRemoteAsset(id: string) {
|
||||
return http.delete<{ id: string }>(`/assets/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
export function deleteRemoteAssets(ids: string[]) {
|
||||
return http.post<{ ids: string[] }>("/assets/batch-delete", ids);
|
||||
}
|
||||
|
||||
export function listRemoteCanvasProjects() {
|
||||
return http.get<{ projects: RemoteUserDataSummary[] }>("/canvas-projects");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getMediaBlob } from "@/services/file-storage";
|
||||
import { getImageBlob } from "@/services/image-storage";
|
||||
import { deleteRemoteAsset, deleteRemoteCanvasProject, getRemoteAsset, getRemoteAssetsByIds, getRemoteCanvasProject, getRemoteUserDataSnapshot, listRemoteAssetsPage, restoreRemoteCanvasHistory, upsertRemoteAsset, upsertRemoteCanvasProject } from "@/services/api/user-data";
|
||||
import { deleteRemoteAssets, deleteRemoteCanvasProject, getRemoteAsset, getRemoteAssetsByIds, getRemoteCanvasProject, getRemoteUserDataSnapshot, listRemoteAssetsPage, restoreRemoteCanvasHistory, upsertRemoteAsset, upsertRemoteCanvasProject } from "@/services/api/user-data";
|
||||
import { ApiError } from "@/services/api/request";
|
||||
import { canvasContentHash, sameCanvasContent } from "@/lib/canvas/canvas-content";
|
||||
import { getActiveUserScope } from "@/lib/user-scope";
|
||||
@@ -438,19 +438,34 @@ export async function createCanvasProjectWithRemoteSync(title: string, projectId
|
||||
}
|
||||
|
||||
export async function deleteAssetWithRemoteSync(id: string) {
|
||||
return deleteAssetsWithRemoteSync([id]);
|
||||
}
|
||||
|
||||
export async function deleteAssetsWithRemoteSync(ids: string[]) {
|
||||
const epoch = sessionEpoch;
|
||||
const assetId = id.trim();
|
||||
if (!assetId) throw new Error("素材 ID 不能为空");
|
||||
if (!ids.length || ids.length > 1000) throw new Error("每次请选择 1–1000 个素材删除");
|
||||
const assetIds = [...new Set(ids.map((id) => id.trim()))];
|
||||
if (assetIds.some((id) => !id)) throw new Error("素材 ID 不能为空");
|
||||
await withRemoteUserDataSyncExclusive(async () => {
|
||||
if (epoch !== sessionEpoch) throw new Error("账号已切换,请重新选择要删除的素材");
|
||||
if (activeRemoteUserId) {
|
||||
requireRemoteUserDataBaseline();
|
||||
await deleteRemoteAsset(assetId);
|
||||
acknowledgedAssets.delete(assetId);
|
||||
await deleteRemoteAssets(assetIds);
|
||||
if (epoch !== sessionEpoch) throw new Error("账号已切换,请刷新原账号素材库确认删除结果");
|
||||
for (const id of assetIds) acknowledgedAssets.delete(id);
|
||||
}
|
||||
await useAssetStore.getState().removeAsset(assetId);
|
||||
await useAssetStore.getState().removeAssets(assetIds);
|
||||
await flushAssetStorePersistence();
|
||||
});
|
||||
if (epoch !== sessionEpoch) throw new Error("账号已切换,请刷新原账号素材库确认删除结果");
|
||||
// 列表查询也会进入同步队列,必须在退出删除队列后触发,不能在队列内等待刷新。
|
||||
// 刷新慢或失败不应阻塞确认弹窗关闭,也不能把已完成的删除报告为失败。
|
||||
void Promise.all([
|
||||
appQueryClient.invalidateQueries({ queryKey: ["asset-library"] }, { throwOnError: true }),
|
||||
appQueryClient.invalidateQueries({ queryKey: ["asset-picker"] }, { throwOnError: true }),
|
||||
]).catch((error) => {
|
||||
console.warn("素材删除后列表刷新失败", error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCanvasProjectsWithRemoteSync(ids: string[]) {
|
||||
|
||||
@@ -59,6 +59,7 @@ type AssetStore = {
|
||||
addGenerationAsset: (effectKey: string, asset: NewAsset, signal?: AbortSignal) => Promise<string>;
|
||||
updateAsset: (id: string, patch: Partial<Omit<Asset, "id" | "createdAt">>) => void;
|
||||
removeAsset: (id: string) => Promise<void>;
|
||||
removeAssets: (ids: string[]) => Promise<void>;
|
||||
replaceAssets: (assets: Asset[]) => void;
|
||||
cleanupImages: (extra?: unknown) => Promise<void>;
|
||||
};
|
||||
@@ -393,18 +394,23 @@ export const useAssetStore = create<AssetStore>()(
|
||||
set((state) => ({
|
||||
assets: state.assets.map((asset) => (asset.id === id ? parseAssetRecord({ ...asset, ...patch, updatedAt: new Date().toISOString() }) : asset)),
|
||||
})),
|
||||
removeAsset: async (id) => {
|
||||
removeAsset: async (id) => get().removeAssets([id]),
|
||||
removeAssets: async (ids) => {
|
||||
const removedIds = new Set(ids);
|
||||
let remainingAssets: Asset[] = [];
|
||||
let removedAsset: Asset | undefined;
|
||||
let hasLocalMedia = false;
|
||||
set((state) => {
|
||||
removedAsset = state.assets.find((asset) => asset.id === id);
|
||||
const assets = state.assets.filter((asset) => asset.id !== id);
|
||||
const assets = state.assets.filter((asset) => {
|
||||
if (!removedIds.has(asset.id)) return true;
|
||||
hasLocalMedia ||= !!collectImageStorageKeys(asset).size || !!collectMediaStorageKeys(asset).size;
|
||||
return false;
|
||||
});
|
||||
remainingAssets = assets;
|
||||
return { assets };
|
||||
});
|
||||
// 没有本地媒体定位时没有需要由该删除动作回收的 Blob;跳过全库扫描,
|
||||
// 避免纯文本/远程资源删除依赖浏览器 IndexedDB 驱动。
|
||||
if (!removedAsset || (!collectImageStorageKeys(removedAsset).size && !collectMediaStorageKeys(removedAsset).size)) return;
|
||||
if (!hasLocalMedia) return;
|
||||
await get().cleanupImages({ assets: remainingAssets });
|
||||
},
|
||||
replaceAssets: (assets) => set({ assets: assets.map(parseAssetRecord) }),
|
||||
|
||||
@@ -14049,9 +14049,10 @@ html:not(.dark) .creation-history-drawer-root {
|
||||
.assets-cover-media { width: 100%; height: 100%; object-fit: contain; background: color-mix(in srgb, var(--foreground) 5%, transparent); }
|
||||
.assets-cover-fallback { display: grid; width: 100%; height: 100%; place-items: center; background: color-mix(in srgb, var(--foreground) 4%, transparent); color: color-mix(in srgb, var(--foreground) 42%, transparent); }
|
||||
.assets-cover-vignette { display: none; }
|
||||
.assets-cover-badges { position: absolute; top: 8px; left: 8px; z-index: 2; display: flex; align-items: center; gap: 5px; pointer-events: none; }
|
||||
.assets-cover-badges { position: absolute; top: 8px; left: 38px; right: 44px; z-index: 2; display: flex; align-items: center; gap: 5px; overflow: hidden; white-space: nowrap; pointer-events: none; }
|
||||
.assets-cover-badge {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@@ -14088,11 +14089,11 @@ html:not(.dark) .creation-history-drawer-root {
|
||||
.assets-cover-play svg { position: relative; width: 17px; height: 17px; margin-left: 2px; filter: drop-shadow(0 1px 4px rgba(0, 0, 0, .45)); }
|
||||
.assets-cover-link:hover .assets-cover-play, .assets-cover-link:focus-visible .assets-cover-play { opacity: 1; }
|
||||
|
||||
/* 选中态:accent 勾选框(右下)+ accent 描边由 is-selected 提供 */
|
||||
/* 选择入口固定在左上角,徽章预留其空间;选中描边由 is-selected 提供。 */
|
||||
.assets-select-check {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
z-index: 3;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
@@ -14167,17 +14168,19 @@ html:not(.dark) .creation-history-drawer-root {
|
||||
.assets-cover-model > svg { width: 34px; height: 34px; stroke-width: 1.1; }
|
||||
.assets-cover-model > span { max-width: 76%; overflow: hidden; font-size: var(--fs-micro); text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* 批量操作条:浮现动画,黑白表面 */
|
||||
/* 批量操作条随列表正常排版,留出选中描边和 hover 位移的空间。 */
|
||||
.assets-batch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin: 2px 0 0;
|
||||
margin: 2px 0 var(--space-4);
|
||||
padding: 8px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 9%, transparent);
|
||||
border-radius: var(--r-lg);
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
background: color-mix(in srgb, var(--surface-card) 82%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: var(--elevation-hairline);
|
||||
animation: assets-batch-in 240ms var(--motion-ease-out) both;
|
||||
}
|
||||
@@ -14185,7 +14188,7 @@ html:not(.dark) .creation-history-drawer-root {
|
||||
@media (prefers-reduced-motion: reduce) { .assets-batch-bar { animation: none; } }
|
||||
.assets-batch-count { display: inline-flex; align-items: baseline; gap: 6px; margin-right: auto; color: color-mix(in srgb, var(--foreground) 56%, transparent); font-size: var(--fs-label); }
|
||||
.assets-batch-count strong { color: var(--foreground); font-size: var(--fs-body); font-weight: 620; font-variant-numeric: tabular-nums; }
|
||||
.assets-batch-actions { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.assets-batch-actions { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }
|
||||
.assets-batch-actions .ant-btn { border: 0; border-radius: var(--r-md); background: transparent; color: color-mix(in srgb, var(--foreground) 56%, transparent); box-shadow: none; }
|
||||
.assets-batch-actions .ant-btn:hover { background: color-mix(in srgb, var(--foreground) 7%, transparent); color: var(--foreground); }
|
||||
|
||||
@@ -14284,16 +14287,6 @@ html:not(.dark) .creation-history-drawer-root {
|
||||
/* 1. 欢迎卡与 banner 同列宽,边缘对齐 */
|
||||
.assets-empty-cards { width: min(720px, calc(100% - 24px)); }
|
||||
|
||||
/* 2. 批量操作条:sticky + 毛玻璃,滚动长列表时始终可见 */
|
||||
.assets-batch-bar {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
z-index: 20;
|
||||
background: color-mix(in srgb, var(--surface-card) 82%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* 素材密度由页面传入卡片最小宽度,网格布局统一在 workspace-product.css。 */
|
||||
.assets-library-page .library-create-title { margin-top: 12px; font-size: var(--fs-body); }
|
||||
.assets-library-page .library-create-meta { margin-top: 4px; font-size: var(--fs-label); }
|
||||
|
||||
Reference in New Issue
Block a user