From 1ed4bd9e0dce25f988c1ab99b3638851812b9a35 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 29 Aug 2026 14:33:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(voices):=20=E9=85=8D=E9=9F=B3=E5=BA=93?= =?UTF-8?q?=E9=85=8D=E9=9F=B3=E7=B4=A0=E6=9D=90tab=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MaterialVoiceTab 卡片 hover 显示 danger 删除按钮(stopPropagation 防误触) - 新增 useMaterialDelete hook:Modal.confirm 二次确认 → deleteAsset - 删除成功 toast 提示并 invalidate voice-materials / assets-voice 列表,失败 toast 报错 - voices.css 补 .vmat-card-actions 样式(对齐 voice-materials 页),.vmat-card 加 position:relative - 预置音色、我的克隆 tab 不受影响 --- apps/web/src/pages/voices/VoiceLibrary.tsx | 8 ++++ .../voices/components/MaterialVoiceTab.tsx | 18 ++++++- .../pages/voices/hooks/useMaterialDelete.ts | 48 +++++++++++++++++++ apps/web/src/pages/voices/voices.css | 42 ++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/pages/voices/hooks/useMaterialDelete.ts 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..e6dc6e603 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,20 @@ 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..dd251e258 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,47 @@ 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; +} + +.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); -- 2.54.0 From 798c01ad5be5968858bac34976da17b8af13e1a0 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 29 Aug 2026 14:54:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(voices):=20=E4=BF=AE=E5=A4=8D=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E5=88=A0=E9=99=A4=E6=8C=89=E9=92=AE=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E4=B8=8D=E5=8F=AF=E8=A7=81=20+=20=E8=A1=A5=20aria-lab?= =?UTF-8?q?el?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @media (hover:none) 触摸屏设备常驻显示 .vmat-card-actions(移动端无 hover 伪类,按钮此前完全无法点击) - 删除按钮补 aria-label="删除配音素材",屏幕阅读器可朗读用途 --- .../src/pages/voices/components/MaterialVoiceTab.tsx | 1 + apps/web/src/pages/voices/voices.css | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx b/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx index e6dc6e603..187636b52 100644 --- a/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx +++ b/apps/web/src/pages/voices/components/MaterialVoiceTab.tsx @@ -55,6 +55,7 @@ export const MaterialVoiceTab: React.FC = ({ onDelete(asset) }} title="删除" + aria-label="删除配音素材" > diff --git a/apps/web/src/pages/voices/voices.css b/apps/web/src/pages/voices/voices.css index dd251e258..853b68263 100644 --- a/apps/web/src/pages/voices/voices.css +++ b/apps/web/src/pages/voices/voices.css @@ -1001,6 +1001,18 @@ 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; -- 2.54.0