From af6ea966fd60750076a9a781fa362af226abe6a6 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Thu, 7 May 2026 02:44:06 +0800 Subject: [PATCH] feat(web): add asset gallery with waterfall layout, preview and delete Co-Authored-By: Claude Opus 4.7 --- .../src/components/assets/AssetGallery.tsx | 96 ++++++++++++++++++- .../src/components/assets/AssetPreview.tsx | 35 +++++++ web/client/src/hooks/useAssets.ts | 19 ++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 web/client/src/components/assets/AssetPreview.tsx create mode 100644 web/client/src/hooks/useAssets.ts diff --git a/web/client/src/components/assets/AssetGallery.tsx b/web/client/src/components/assets/AssetGallery.tsx index fb0dee6..07227a9 100644 --- a/web/client/src/components/assets/AssetGallery.tsx +++ b/web/client/src/components/assets/AssetGallery.tsx @@ -1,3 +1,97 @@ +import { useState, useEffect } from 'react'; +import { Trash2, RefreshCw } from 'lucide-react'; +import { useAssets } from '@/hooks/useAssets'; +import { AssetPreview } from './AssetPreview'; +import { Button } from '@/components/ui/button'; +import type { Asset } from '@/types'; + export function AssetGallery() { - return
资产画廊
; + const [accountFilter, setAccountFilter] = useState(''); + const [typeFilter, setTypeFilter] = useState(''); + const [accounts, setAccounts] = useState<{ id: string; name: string }[]>([]); + const { assets, loading, remove } = useAssets({ + accountId: accountFilter || undefined, + type: typeFilter || undefined, + }); + const [previewAsset, setPreviewAsset] = useState(null); + + useEffect(() => { + fetch('/api/accounts').then((r) => r.json()).then(setAccounts).catch(() => {}); + }, []); + + const handleScan = async () => { + await fetch('/api/assets/scan', { method: 'POST' }); + window.location.reload(); + }; + + return ( +
+
+ + + +
+ +
+ {loading ? ( +

加载中...

+ ) : assets.length === 0 ? ( +

暂无资产,点击"扫描"导入

+ ) : ( +
+ {assets.map((asset) => ( +
setPreviewAsset(asset)} + > + {asset.type === 'image' ? ( + + ) : ( +
+ ))} +
+ )} +
+ + {previewAsset && setPreviewAsset(null)} />} +
+ ); } diff --git a/web/client/src/components/assets/AssetPreview.tsx b/web/client/src/components/assets/AssetPreview.tsx new file mode 100644 index 0000000..30ff18b --- /dev/null +++ b/web/client/src/components/assets/AssetPreview.tsx @@ -0,0 +1,35 @@ +import { X } from 'lucide-react'; +import type { Asset } from '@/types'; + +export function AssetPreview({ asset, onClose }: { asset: Asset; onClose: () => void }) { + return ( +
+ + +
e.stopPropagation()}> + {asset.type === 'image' ? ( + + ) : ( +
+
+ ); +} diff --git a/web/client/src/hooks/useAssets.ts b/web/client/src/hooks/useAssets.ts new file mode 100644 index 0000000..971f5cc --- /dev/null +++ b/web/client/src/hooks/useAssets.ts @@ -0,0 +1,19 @@ +import { useState, useEffect, useCallback } from 'react'; +import { api } from '@/lib/api'; +import type { Asset } from '@/types'; + +export function useAssets(params?: { accountId?: string; type?: string }) { + const [assets, setAssets] = useState([]); + const [loading, setLoading] = useState(true); + + const refresh = useCallback(() => { + setLoading(true); + api.listAssets(params).then(setAssets).finally(() => setLoading(false)); + }, [params?.accountId, params?.type]); + + useEffect(() => { refresh(); }, [refresh]); + + const remove = (id: string) => api.deleteAsset(id).then(refresh); + + return { assets, loading, refresh, remove }; +}