diff --git a/apps/web/src/pages/voices/VoiceLibrary.tsx b/apps/web/src/pages/voices/VoiceLibrary.tsx index 37c0b8c01..b091374a4 100755 --- a/apps/web/src/pages/voices/VoiceLibrary.tsx +++ b/apps/web/src/pages/voices/VoiceLibrary.tsx @@ -32,6 +32,7 @@ import { useAudioPlayer } from "./hooks/useAudioPlayer" import { useCloneOperations } from "./hooks/useCloneOperations" import { useTtsSynthesize } from "./hooks/useTtsSynthesize" import { useVoiceUpload } from "./hooks/useVoiceUpload" +import { useMaterialDelete } from "./hooks/useMaterialDelete" import "./voices.css" let toastIdSeq = 0 @@ -70,6 +71,12 @@ const VoiceLibrary: React.FC = () => { materialCount, } = useVoicesData() + // ── 配音素材删除 ────────────────────────────────────── + const { handleMaterialDelete } = useMaterialDelete({ + materials: materials as AssetItem[], + showToast, + }) + // ── 播放控制 ────────────────────────────────────────── const { playingId, @@ -234,6 +241,7 @@ const VoiceLibrary: React.FC = () => { loading={materialLoading} materials={materials as AssetItem[]} onOpenUpload={() => setUploadOpen(true)} + onDelete={handleMaterialDelete} /> )} diff --git a/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx b/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx index 74c1dc444..187636b52 100644 --- a/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx +++ b/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx @@ -2,7 +2,7 @@ * VoiceLibrary 配音素材 Tab 内容 */ import React from "react" -import { SoundOutlined, UploadOutlined, AudioOutlined } from "@ant-design/icons" +import { SoundOutlined, UploadOutlined, AudioOutlined, DeleteOutlined } from "@ant-design/icons" import { Button } from "@/components/ui" import type { AssetItem } from "@/api/assets" @@ -10,12 +10,14 @@ export interface MaterialVoiceTabProps { loading: boolean materials: AssetItem[] onOpenUpload: () => void + onDelete: (asset: AssetItem) => void } export const MaterialVoiceTab: React.FC = ({ loading, materials, onOpenUpload, + onDelete, }) => { return (
@@ -43,6 +45,21 @@ export const MaterialVoiceTab: React.FC = ({ const seconds = Math.floor(duration % 60) return (
+ {/* hover 操作区:删除 */} +
+ +
diff --git a/apps/web/src/pages/voices/hooks/useMaterialDelete.ts b/apps/web/src/pages/voices/hooks/useMaterialDelete.ts new file mode 100644 index 000000000..8a8f88bb6 --- /dev/null +++ b/apps/web/src/pages/voices/hooks/useMaterialDelete.ts @@ -0,0 +1,48 @@ +/** + * 配音库「配音素材」Tab 删除逻辑 + */ +import { useCallback } from "react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { Modal } from "@/components/ui" +import { deleteAsset, type AssetItem } from "@/api/assets" +import type { Toast } from "../components/VoiceToasts" + +interface UseMaterialDeleteOptions { + materials: AssetItem[] + showToast: (message: string, type: Toast["type"]) => void +} + +export function useMaterialDelete({ materials, showToast }: UseMaterialDeleteOptions) { + const queryClient = useQueryClient() + + const deleteMutation = useMutation({ + mutationFn: (assetId: string) => deleteAsset(assetId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["voice-materials"] }) + queryClient.invalidateQueries({ queryKey: ["assets", "voice"] }) + showToast("素材已删除", "success") + }, + onError: (err: unknown) => { + const msg = err instanceof Error ? err.message : "未知错误" + showToast(`删除失败:${msg}`, "error") + }, + }) + + const handleMaterialDelete = useCallback( + (asset: AssetItem) => { + const material = materials.find((m) => m.id === asset.id) + if (!material) return + Modal.confirm({ + title: "确认删除", + content: `确定删除配音素材「${material.name}」吗?删除后不可恢复。`, + okText: "删除", + okButtonProps: { danger: true }, + cancelText: "取消", + onOk: () => deleteMutation.mutate(material.id), + }) + }, + [materials, deleteMutation], + ) + + return { handleMaterialDelete, isDeleting: deleteMutation.isPending } +} diff --git a/apps/web/src/pages/voices/voices.css b/apps/web/src/pages/voices/voices.css index 74ae27897..853b68263 100644 --- a/apps/web/src/pages/voices/voices.css +++ b/apps/web/src/pages/voices/voices.css @@ -976,6 +976,7 @@ ================================================================ */ .vmat-card { + position: relative; background: var(--bg-primary); border-radius: var(--radius-lg); overflow: hidden; @@ -984,6 +985,59 @@ cursor: pointer; } +/* 卡片 hover 操作按钮 */ +.vmat-card-actions { + position: absolute; + top: 8px; + right: 8px; + z-index: 2; + display: flex; + gap: 4px; + opacity: 0; + transition: opacity var(--transition-fast); +} + +.vmat-card:hover .vmat-card-actions { + opacity: 1; +} + +/* 触摸屏设备无 hover:操作按钮常驻显示 */ +@media (hover: none) { + .vmat-card-actions { + opacity: 1; + } + + .vmat-card-action-btn { + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); + } +} + +.vmat-card-action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: var(--radius-sm); + background: var(--bg-hover); + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + transition: all var(--transition-fast); +} + +.vmat-card-action-btn:hover { + background: var(--primary-50); + color: var(--primary-600); +} + +.vmat-card-action-btn--danger:hover { + background: var(--error-50); + color: var(--error-600); +} + .vmat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);