Browse Source

优化样式

feature/260608
TianYun 1 month ago
parent
commit
ae17b99e49
  1. 26
      src/components/nodes/AudioInputNode.tsx
  2. 17
      src/components/nodes/GenerateAudioNode.tsx
  3. 67
      src/components/nodes/ImageInputNode.tsx
  4. 2
      src/components/nodes/SmartAudioNode.tsx
  5. 2
      src/components/nodes/SmartImageNode.tsx
  6. 2
      src/components/nodes/SmartVideoNode.tsx
  7. 9
      src/components/nodes/VideoInputNode.tsx

26
src/components/nodes/AudioInputNode.tsx

@ -181,6 +181,7 @@ export function AudioInputNodeView({
}, [isUploading]);
const title = getAudioNodeTitle(id, nodeData, t("node.audioInput"));
const mediaActionVisibility = selected ? "opacity-100" : "opacity-0 group-hover:opacity-100";
return (
<BaseNode
@ -295,24 +296,34 @@ export function AudioInputNodeView({
</span>
</div>
{/* Download button */}
<button
onClick={(e) => {
e.stopPropagation();
handleOpenFileDialog();
}}
aria-label={t("imageInput.replace")}
title={t("imageInput.replace")}
className={`absolute top-2 right-[4.5rem] flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-black/80 focus:opacity-100 [&_svg]:h-3.5 [&_svg]:w-3.5 ${mediaActionVisibility}`}
>
<UploadIcon />
</button>
<button
onClick={() => downloadMedia(nodeData.audioFile!, "audio")}
aria-label={t("audioInput.download")}
className="absolute top-1 right-7 w-5 h-5 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-white transition-opacity flex items-center justify-center"
title={t("audioInput.download")}
className={`absolute top-2 right-10 flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-black/80 focus:opacity-100 ${mediaActionVisibility}`}
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
{/* Remove button */}
<button
onClick={handleRemove}
aria-label={t("audioInput.remove")}
className="absolute top-1 right-1 w-5 h-5 bg-black/60 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
title={t("audioInput.remove")}
className={`absolute top-2 right-2 flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-red-600/80 focus:opacity-100 focus:ring-1 focus:ring-red-400 ${mediaActionVisibility}`}
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
@ -334,7 +345,8 @@ export function AudioInputNodeView({
className="flex h-full min-h-28 w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-neutral-700 bg-neutral-900/40 transition-colors hover:border-neutral-500 hover:bg-neutral-900/60"
>
<svg className="w-8 h-8 text-neutral-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M5 14v-2a7 7 0 0 1 14 0v2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M5 14a2 2 0 0 1 2-2h1v6H7a2 2 0 0 1-2-2v-2zM19 14a2 2 0 0 0-2-2h-1v6h1a2 2 0 0 0 2-2v-2z" />
</svg>
<span className="text-xs text-neutral-500 mt-2">
{nodeData.isOptional ? t("common.optional") : t("audioInput.dropAudio")}

17
src/components/nodes/GenerateAudioNode.tsx

@ -1,7 +1,6 @@
"use client";
import React, { useCallback, useState, useEffect, useMemo } from "react";
import { CustomerServiceOutlined } from "@ant-design/icons";
import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react";
import { BaseNode } from "./BaseNode";
import { ProviderBadge } from "./ProviderBadge";
@ -493,13 +492,6 @@ export function GenerateAudioNodeView({
</InlineParameterPanel>
) : undefined}
>
<div className="nodrag pointer-events-none absolute -top-10 left-0 z-[10001] flex items-center gap-2">
<span className="flex h-7 w-12 items-center justify-center rounded-full bg-violet-600 text-white shadow-lg shadow-violet-500/20">
<CustomerServiceOutlined className="text-base" />
</span>
<span className="text-sm font-medium text-neutral-100 drop-shadow-sm">{displayTitle}</span>
</div>
<div className="relative h-full w-full min-h-0">
{nodeData.outputAudio ? (
<div className="relative group flex h-full flex-col gap-2 p-3">
@ -618,8 +610,8 @@ export function GenerateAudioNodeView({
</button>
</div>
) : (
<div className="flex h-full w-full items-center justify-center bg-neutral-900/20 p-8">
<div className="flex h-full min-h-28 w-full flex-col items-center justify-center rounded-lg bg-neutral-900/40 text-neutral-500">
<div className="flex h-full w-full items-center justify-center p-8 bg-neutral-900/40">
<div className="flex h-full min-h-28 w-full flex-col items-center justify-center rounded-lg text-neutral-500">
{nodeData.status === "loading" ? (
<>
<div className="animate-spin w-4 h-4 border-2 border-violet-500 border-t-transparent rounded-full" />
@ -630,7 +622,10 @@ export function GenerateAudioNodeView({
{displayError || t("node.generationFailed")}
</span>
) : (
<CustomerServiceOutlined className="text-4xl text-neutral-500" />
<svg className="w-12 h-12 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 14v-2a7 7 0 0 1 14 0v2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M5 14a2 2 0 0 1 2-2h1v6H7a2 2 0 0 1-2-2v-2zM19 14a2 2 0 0 0-2-2h-1v6h1a2 2 0 0 0 2-2v-2z" />
</svg>
)}
</div>
</div>

67
src/components/nodes/ImageInputNode.tsx

@ -300,6 +300,7 @@ export function ImageInputNodeView({ id, data, selected }: ImageInputNodeViewPro
const title = getImageNodeTitle(id, nodeData, t("node.image"));
const hasImage = Boolean(displayImage);
const mediaActionVisibility = selected ? "opacity-100" : "opacity-0 group-hover:opacity-100";
return (
<BaseNode
@ -459,28 +460,6 @@ export function ImageInputNodeView({ id, data, selected }: ImageInputNodeViewPro
)}
{selected && (
<>
<button
onClick={(e) => {
e.stopPropagation();
handleOpenFileDialog();
}}
aria-label={t("imageInput.replace")}
className="absolute right-3 top-3 flex h-12 items-center gap-2 rounded-xl bg-black/35 px-4 text-lg font-medium text-white shadow-lg backdrop-blur-sm transition-colors hover:bg-black/55"
>
<ImageToolbarIcon kind="upload" />
<span>{t("imageInput.replace")}</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
setIsAssetPickerOpen(true);
}}
aria-label={t("imageInput.fromAssets")}
className="absolute right-3 top-[4.25rem] flex h-12 items-center gap-2 rounded-xl bg-black/35 px-4 text-lg font-medium text-white shadow-lg backdrop-blur-sm transition-colors hover:bg-black/55"
>
<ImageToolbarIcon kind="assets" />
<span>{t("imageInput.fromAssets")}</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
@ -493,6 +472,50 @@ export function ImageInputNodeView({ id, data, selected }: ImageInputNodeViewPro
</button>
</>
)}
<button
onClick={(e) => {
e.stopPropagation();
setIsAssetPickerOpen(true);
}}
aria-label={t("imageInput.fromAssets")}
title={t("imageInput.fromAssets")}
className={`absolute top-2 right-[6.75rem] flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-black/80 focus:opacity-100 [&_svg]:h-3.5 [&_svg]:w-3.5 ${mediaActionVisibility}`}
>
<ImageToolbarIcon kind="assets" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleOpenFileDialog();
}}
aria-label={t("imageInput.replace")}
title={t("imageInput.replace")}
className={`absolute top-2 right-[4.5rem] flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-black/80 focus:opacity-100 [&_svg]:h-3.5 [&_svg]:w-3.5 ${mediaActionVisibility}`}
>
<ImageToolbarIcon kind="upload" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
downloadMedia(nodeData.image!, "image");
}}
aria-label={t("imageInput.download")}
title={t("imageInput.download")}
className={`absolute top-2 right-10 flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-black/80 focus:opacity-100 [&_svg]:h-3.5 [&_svg]:w-3.5 ${mediaActionVisibility}`}
>
<ImageToolbarIcon kind="download" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleRemove();
}}
aria-label={t("imageInput.remove")}
title={t("imageInput.remove")}
className={`absolute top-2 right-2 flex h-6 w-6 items-center justify-center rounded bg-black/60 text-xs text-white transition-all hover:bg-red-600/80 focus:opacity-100 focus:ring-1 focus:ring-red-400 [&_svg]:h-3.5 [&_svg]:w-3.5 ${mediaActionVisibility}`}
>
<ImageToolbarIcon kind="delete" />
</button>
</div>
) : (
<div className="flex h-full w-full items-center justify-center bg-neutral-900/20 p-8">

2
src/components/nodes/SmartAudioNode.tsx

@ -103,7 +103,7 @@ function SmartAudioUploadToolbar({
/>
{selected && (
<div className="nodrag nopan absolute left-1/2 -top-[86px] z-[10002] flex -translate-x-1/2 items-center gap-2">
<div className="nodrag nopan absolute left-1/2 -top-[50px] z-[10002] flex -translate-x-1/2 items-center gap-2">
<button
type="button"
onClick={(event) => {

2
src/components/nodes/SmartImageNode.tsx

@ -203,7 +203,7 @@ function SmartImageUploadToolbar({
/>
{selected && (
<div className="nodrag nopan absolute left-1/2 -top-[86px] z-[10002] flex -translate-x-1/2 items-center gap-2">
<div className="nodrag nopan absolute left-1/2 -top-[50px] z-[10002] flex -translate-x-1/2 items-center gap-2">
<button
type="button"
onClick={(event) => {

2
src/components/nodes/SmartVideoNode.tsx

@ -248,7 +248,7 @@ function SmartVideoUploadToolbar({
/>
{selected && (
<div className="nodrag nopan absolute left-1/2 -top-[86px] z-[10002] flex -translate-x-1/2 items-center gap-2">
<div className="nodrag nopan absolute left-1/2 -top-[50px] z-[10002] flex -translate-x-1/2 items-center gap-2">
<button
type="button"
onClick={(event) => {

9
src/components/nodes/VideoInputNode.tsx

@ -309,6 +309,7 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro
const title = getVideoNodeTitle(id, nodeData, t("node.videoInput"));
const hasVideoAsset = Boolean(nodeData.video || nodeData.previewVideoPoster);
const mediaActionVisibility = selected ? "opacity-100" : "opacity-0 group-hover:opacity-100";
return (
<BaseNode
@ -402,7 +403,7 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro
handleOpenFileDialog();
}}
aria-label={t("videoInput.upload")}
className="absolute top-2 right-[4.5rem] w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus:opacity-100 transition-all flex items-center justify-center"
className={`absolute top-2 right-[4.5rem] w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs focus:opacity-100 transition-all flex items-center justify-center ${mediaActionVisibility}`}
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v12m0-12 4.5 4.5M12 3 7.5 7.5M3 15v3a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-3" />
@ -414,7 +415,7 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro
setIsAssetPickerOpen(true);
}}
aria-label={t("imageInput.fromAssets")}
className="absolute top-2 right-[6.75rem] w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus:opacity-100 transition-all flex items-center justify-center"
className={`absolute top-2 right-[6.75rem] w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs focus:opacity-100 transition-all flex items-center justify-center ${mediaActionVisibility}`}
>
<AssetIcon className="w-3.5 h-3.5" />
</button>
@ -424,7 +425,7 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro
downloadMedia(nodeData.video!, "video");
}}
aria-label={t("videoInput.download")}
className="absolute top-2 right-10 w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus:opacity-100 transition-all flex items-center justify-center"
className={`absolute top-2 right-10 w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs focus:opacity-100 transition-all flex items-center justify-center ${mediaActionVisibility}`}
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
@ -436,7 +437,7 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro
handleRemove();
}}
aria-label={t("videoInput.remove")}
className="absolute top-2 right-2 w-6 h-6 bg-black/60 hover:bg-red-600/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus:opacity-100 focus:ring-1 focus:ring-red-400 transition-all flex items-center justify-center"
className={`absolute top-2 right-2 w-6 h-6 bg-black/60 hover:bg-red-600/80 text-white rounded text-xs focus:opacity-100 focus:ring-1 focus:ring-red-400 transition-all flex items-center justify-center ${mediaActionVisibility}`}
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />

Loading…
Cancel
Save