项目生成模块选择器闪动修改
This commit is contained in:
+41
-41
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-C9TClkR9.js"></script>
|
<script type="module" crossorigin src="/assets/index-iwDaDgKC.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const isPending = (item: GenerationTaskResourceItem): boolean => {
|
|||||||
return !status || ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status);
|
return !status || ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_DURATION_SECONDS = 180;
|
const MAX_DURATION_SECONDS = 300;
|
||||||
|
|
||||||
interface ProgressItemProps {
|
interface ProgressItemProps {
|
||||||
item: GenerationTaskResourceItem;
|
item: GenerationTaskResourceItem;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
import React, { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||||
import { copyToClipboard } from "../utils/clipboard";
|
import { copyToClipboard } from "../utils/clipboard";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
@@ -62,6 +62,322 @@ import { formatDate } from "../utils/formatDate";
|
|||||||
import UploadSelector from "../components/UploadSelector";
|
import UploadSelector from "../components/UploadSelector";
|
||||||
import { generateUUID } from "../utils/uuid";
|
import { generateUUID } from "../utils/uuid";
|
||||||
|
|
||||||
|
interface PortalDropdownProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
options: string[];
|
||||||
|
suffix?: string;
|
||||||
|
expanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
onSelect: (val: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortalDropdown: React.FC<PortalDropdownProps> = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
suffix,
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
onSelect,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const expandedRef = useRef(expanded);
|
||||||
|
const [pos, setPos] = useState<{
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
expandedRef.current = expanded;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded && triggerRef.current) {
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
setPos({
|
||||||
|
bottom: window.innerHeight - rect.top + 4,
|
||||||
|
left: rect.left,
|
||||||
|
width: Math.max(rect.width, 100),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPos(null);
|
||||||
|
}
|
||||||
|
}, [expanded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expanded) return;
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (triggerRef.current?.contains(e.target as Node)) return;
|
||||||
|
if (menuRef.current?.contains(e.target as Node)) return;
|
||||||
|
if (expandedRef.current) onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handler);
|
||||||
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
|
}, [expanded, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={onToggle}
|
||||||
|
translate="no"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
padding: "5px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
transition: "background 0.2s, border-color 0.2s",
|
||||||
|
background: expanded ? "rgba(99,102,241,0.08)" : "#f8f9fc",
|
||||||
|
border: expanded ? "1px solid #6366f1" : "1px solid #e2e8f0",
|
||||||
|
minWidth: 70,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#6366f1",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
<CaretDownOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: 9,
|
||||||
|
color: "#94a3b8",
|
||||||
|
transition: "transform 0.2s",
|
||||||
|
transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{pos &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: pos.bottom,
|
||||||
|
left: pos.left,
|
||||||
|
minWidth: pos.width,
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowY: "auto",
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 6,
|
||||||
|
zIndex: 99999,
|
||||||
|
boxShadow: "0 8px 30px rgba(0,0,0,0.12)",
|
||||||
|
border: "1px solid #e2e8f0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: "#94a3b8",
|
||||||
|
display: "block",
|
||||||
|
padding: "2px 6px 4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography.Text>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
{options.map((opt, oi) => {
|
||||||
|
const isSelected = value === opt;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={oi}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(opt);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: "6px 10px",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.15s",
|
||||||
|
background: isSelected
|
||||||
|
? "linear-gradient(135deg, #6366f1, #8b5cf6)"
|
||||||
|
: "transparent",
|
||||||
|
color: isSelected ? "#fff" : "#1a1a2e",
|
||||||
|
fontWeight: isSelected ? 600 : 400,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!isSelected)
|
||||||
|
e.currentTarget.style.background = "#f1f5f9";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isSelected)
|
||||||
|
e.currentTarget.style.background = "transparent";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PortalOptionGroupProps {
|
||||||
|
group: OptionGroup;
|
||||||
|
groupName: string;
|
||||||
|
selected: string | undefined;
|
||||||
|
expanded: boolean;
|
||||||
|
onToggle: (groupName: string) => void;
|
||||||
|
onSelect: (opt: string, groupName: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortalOptionGroup: React.FC<PortalOptionGroupProps> = ({ group, groupName, selected, expanded, onToggle, onSelect, onClose }) => {
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const expandedRef = useRef(expanded);
|
||||||
|
const [pos, setPos] = useState<{ bottom: number; left: number } | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expandedRef.current = expanded;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded && triggerRef.current) {
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
setPos({ bottom: window.innerHeight - rect.top + 4, left: rect.left });
|
||||||
|
} else {
|
||||||
|
setPos(null);
|
||||||
|
}
|
||||||
|
}, [expanded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expanded) return;
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (triggerRef.current?.contains(e.target as Node)) return;
|
||||||
|
if (menuRef.current?.contains(e.target as Node)) return;
|
||||||
|
if (expandedRef.current) onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handler);
|
||||||
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
|
}, [expanded, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={() => onToggle(groupName)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
padding: "5px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
cursor: "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
background: selected ? "rgba(99,102,241,0.08)" : "#f8f9fc",
|
||||||
|
border: expanded ? "1px solid #6366f1" : "1px solid #e2e8f0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: selected ? "#6366f1" : "#64748b",
|
||||||
|
fontWeight: selected ? 500 : 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected || group.name}
|
||||||
|
</Typography.Text>
|
||||||
|
<CaretDownOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: 9,
|
||||||
|
color: "#94a3b8",
|
||||||
|
transition: "transform 0.2s",
|
||||||
|
transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{pos &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: pos.bottom,
|
||||||
|
left: pos.left,
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 8,
|
||||||
|
minWidth: 160,
|
||||||
|
zIndex: 99999,
|
||||||
|
boxShadow: "0 8px 30px rgba(0,0,0,0.12)",
|
||||||
|
border: "1px solid #e2e8f0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: "#94a3b8",
|
||||||
|
display: "block",
|
||||||
|
padding: "2px 6px 4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</Typography.Text>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
{group.options.map((opt, oi) => {
|
||||||
|
const isSelected = selected === opt;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={oi}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(opt, groupName);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: "6px 10px",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.15s",
|
||||||
|
background: isSelected
|
||||||
|
? "linear-gradient(135deg, #6366f1, #8b5cf6)"
|
||||||
|
: "transparent",
|
||||||
|
color: isSelected ? "#fff" : "#1a1a2e",
|
||||||
|
fontWeight: isSelected ? 600 : 400,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!isSelected)
|
||||||
|
e.currentTarget.style.background = "#f1f5f9";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isSelected)
|
||||||
|
e.currentTarget.style.background = "transparent";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// const calcVideoCredits = (duration: number, resolution: Resolution): number => {
|
// const calcVideoCredits = (duration: number, resolution: Resolution): number => {
|
||||||
// const configs: Record<
|
// const configs: Record<
|
||||||
// string,
|
// string,
|
||||||
@@ -340,6 +656,40 @@ const GeneratePage: React.FC = () => {
|
|||||||
durations: [5, 8, 10, 12, 15],
|
durations: [5, 8, 10, 12, 15],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const durationOptions = useMemo(() => engineOptions.durations.map((d) => `${d}`), [engineOptions.durations]);
|
||||||
|
|
||||||
|
const handleDurationToggle = useCallback(() => {
|
||||||
|
setExpandedEngine(prev => prev === "duration" ? null : "duration");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDurationClose = useCallback(() => {
|
||||||
|
setExpandedEngine(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGroupToggle = useCallback((groupName: string) => {
|
||||||
|
setExpandedGroup(prev => prev === groupName ? null : groupName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGroupClose = useCallback(() => {
|
||||||
|
setExpandedGroup(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDurationSelect = useCallback((v: string) => {
|
||||||
|
setVideoDuration(parseInt(v));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOptionGroupSelect = useCallback((opt: string, groupName: string) => {
|
||||||
|
setSelectedOptions((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
if (next[groupName] === opt) {
|
||||||
|
delete next[groupName];
|
||||||
|
} else {
|
||||||
|
next[groupName] = opt;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Track last text credits for display
|
// Track last text credits for display
|
||||||
const [lastTextCredits, setLastTextCredits] = useState(0);
|
const [lastTextCredits, setLastTextCredits] = useState(0);
|
||||||
const [lastTextTokens, setLastTextTokens] = useState(0);
|
const [lastTextTokens, setLastTextTokens] = useState(0);
|
||||||
@@ -883,9 +1233,9 @@ const GeneratePage: React.FC = () => {
|
|||||||
const project = projects.find((p) => p.id === projectId);
|
const project = projects.find((p) => p.id === projectId);
|
||||||
const projectRecords = recordItems.filter((r) => r.projectId === projectId);
|
const projectRecords = recordItems.filter((r) => r.projectId === projectId);
|
||||||
const projectName = project?.name ?? "项目";
|
const projectName = project?.name ?? "项目";
|
||||||
const currentOptionGroups = project
|
const currentOptionGroups = useMemo(() => {
|
||||||
? industryOptionGroups[project.industry] || []
|
return project ? industryOptionGroups[project.industry] || [] : [];
|
||||||
: [];
|
}, [project, industryOptionGroups]);
|
||||||
const userCredits = user?.credits ?? 0;
|
const userCredits = user?.credits ?? 0;
|
||||||
|
|
||||||
// 根据图片分辨率从 cimage 获取积分
|
// 根据图片分辨率从 cimage 获取积分
|
||||||
@@ -1320,318 +1670,6 @@ const GeneratePage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
failed: { color: "error", text: "失败", icon: <CloseCircleOutlined /> },
|
failed: { color: "error", text: "失败", icon: <CloseCircleOutlined /> },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Portal-based dropdown that renders menu at document.body level (above trigger)
|
|
||||||
const PortalDropdown: React.FC<{
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
options: string[];
|
|
||||||
suffix?: string;
|
|
||||||
expanded: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
onSelect: (val: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}> = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
suffix,
|
|
||||||
expanded,
|
|
||||||
onToggle,
|
|
||||||
onSelect,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const triggerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
const expandedRef = useRef(expanded);
|
|
||||||
const [pos, setPos] = useState<{
|
|
||||||
bottom: number;
|
|
||||||
left: number;
|
|
||||||
width: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
expandedRef.current = expanded;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (expanded && triggerRef.current) {
|
|
||||||
const rect = triggerRef.current.getBoundingClientRect();
|
|
||||||
setPos({
|
|
||||||
bottom: window.innerHeight - rect.top + 4,
|
|
||||||
left: rect.left,
|
|
||||||
width: Math.max(rect.width, 100),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setPos(null);
|
|
||||||
}
|
|
||||||
}, [expanded]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!expanded) return;
|
|
||||||
const handler = (e: MouseEvent) => {
|
|
||||||
if (triggerRef.current?.contains(e.target as Node)) return;
|
|
||||||
if (menuRef.current?.contains(e.target as Node)) return;
|
|
||||||
if (expandedRef.current) onClose();
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handler);
|
|
||||||
return () => document.removeEventListener("mousedown", handler);
|
|
||||||
}, [expanded, onClose]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={triggerRef}
|
|
||||||
onClick={onToggle}
|
|
||||||
translate="no"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 6,
|
|
||||||
padding: "5px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
cursor: "pointer",
|
|
||||||
userSelect: "none",
|
|
||||||
transition: "background 0.2s, border-color 0.2s",
|
|
||||||
background: expanded ? "rgba(99,102,241,0.08)" : "#f8f9fc",
|
|
||||||
border: expanded ? "1px solid #6366f1" : "1px solid #e2e8f0",
|
|
||||||
minWidth: 70,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#6366f1",
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: "18px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
{suffix}
|
|
||||||
</span>
|
|
||||||
<CaretDownOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: 9,
|
|
||||||
color: "#94a3b8",
|
|
||||||
transition: "transform 0.2s",
|
|
||||||
transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{pos &&
|
|
||||||
createPortal(
|
|
||||||
<div
|
|
||||||
ref={menuRef}
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
bottom: pos.bottom,
|
|
||||||
left: pos.left,
|
|
||||||
minWidth: pos.width,
|
|
||||||
background: "#fff",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 6,
|
|
||||||
zIndex: 99999,
|
|
||||||
boxShadow: "0 8px 30px rgba(0,0,0,0.12)",
|
|
||||||
border: "1px solid #e2e8f0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Text
|
|
||||||
style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: "#94a3b8",
|
|
||||||
display: "block",
|
|
||||||
padding: "2px 6px 4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Typography.Text>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
||||||
{options.map((opt, oi) => {
|
|
||||||
const isSelected = value === opt;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={oi}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(opt);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
padding: "6px 10px",
|
|
||||||
borderRadius: 8,
|
|
||||||
fontSize: 13,
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.15s",
|
|
||||||
background: isSelected
|
|
||||||
? "linear-gradient(135deg, #6366f1, #8b5cf6)"
|
|
||||||
: "transparent",
|
|
||||||
color: isSelected ? "#fff" : "#1a1a2e",
|
|
||||||
fontWeight: isSelected ? 600 : 400,
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (!isSelected)
|
|
||||||
e.currentTarget.style.background = "#f1f5f9";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (!isSelected)
|
|
||||||
e.currentTarget.style.background = "transparent";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{opt}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body,
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Portal-based option group dropdown for industry options (above trigger)
|
|
||||||
const PortalOptionGroup: React.FC<{
|
|
||||||
group: OptionGroup;
|
|
||||||
selected: string | undefined;
|
|
||||||
expanded: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
onSelect: (opt: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}> = ({ group, selected, expanded, onToggle, onSelect, onClose }) => {
|
|
||||||
const triggerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
const expandedRef = useRef(expanded);
|
|
||||||
const [pos, setPos] = useState<{ bottom: number; left: number } | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
expandedRef.current = expanded;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (expanded && triggerRef.current) {
|
|
||||||
const rect = triggerRef.current.getBoundingClientRect();
|
|
||||||
setPos({ bottom: window.innerHeight - rect.top + 4, left: rect.left });
|
|
||||||
} else {
|
|
||||||
setPos(null);
|
|
||||||
}
|
|
||||||
}, [expanded]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!expanded) return;
|
|
||||||
const handler = (e: MouseEvent) => {
|
|
||||||
if (triggerRef.current?.contains(e.target as Node)) return;
|
|
||||||
if (menuRef.current?.contains(e.target as Node)) return;
|
|
||||||
if (expandedRef.current) onClose();
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handler);
|
|
||||||
return () => document.removeEventListener("mousedown", handler);
|
|
||||||
}, [expanded, onClose]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={triggerRef}
|
|
||||||
onClick={onToggle}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 6,
|
|
||||||
padding: "5px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
cursor: "pointer",
|
|
||||||
userSelect: "none",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
background: selected ? "rgba(99,102,241,0.08)" : "#f8f9fc",
|
|
||||||
border: expanded ? "1px solid #6366f1" : "1px solid #e2e8f0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Text
|
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: selected ? "#6366f1" : "#64748b",
|
|
||||||
fontWeight: selected ? 600 : 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selected || group.name}
|
|
||||||
</Typography.Text>
|
|
||||||
<CaretDownOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: 9,
|
|
||||||
color: selected ? "#6366f1" : "#94a3b8",
|
|
||||||
transition: "transform 0.2s",
|
|
||||||
transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{pos &&
|
|
||||||
createPortal(
|
|
||||||
<div
|
|
||||||
ref={menuRef}
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
bottom: pos.bottom,
|
|
||||||
left: pos.left,
|
|
||||||
background: "#fff",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 8,
|
|
||||||
minWidth: 160,
|
|
||||||
zIndex: 99999,
|
|
||||||
boxShadow: "0 8px 30px rgba(0,0,0,0.12)",
|
|
||||||
border: "1px solid #e2e8f0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Text
|
|
||||||
style={{
|
|
||||||
fontSize: 10,
|
|
||||||
color: "#94a3b8",
|
|
||||||
display: "block",
|
|
||||||
padding: "2px 6px 4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</Typography.Text>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
||||||
{group.options.map((opt, oi) => {
|
|
||||||
const isSelected = selected === opt;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={oi}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(opt);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
padding: "6px 10px",
|
|
||||||
borderRadius: 8,
|
|
||||||
fontSize: 13,
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.15s",
|
|
||||||
background: isSelected
|
|
||||||
? "linear-gradient(135deg, #6366f1, #8b5cf6)"
|
|
||||||
: "transparent",
|
|
||||||
color: isSelected ? "#fff" : "#1a1a2e",
|
|
||||||
fontWeight: isSelected ? 600 : 400,
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (!isSelected)
|
|
||||||
e.currentTarget.style.background = "#f1f5f9";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (!isSelected)
|
|
||||||
e.currentTarget.style.background = "transparent";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{opt}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body,
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -1971,6 +2009,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
|
display: "block",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -2127,7 +2166,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
rows={3}
|
rows={3}
|
||||||
placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
bordered={false}
|
variant="borderless"
|
||||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||||
style={{
|
style={{
|
||||||
padding: "6px 2px",
|
padding: "6px 2px",
|
||||||
@@ -2273,16 +2312,12 @@ const GeneratePage: React.FC = () => {
|
|||||||
<PortalDropdown
|
<PortalDropdown
|
||||||
label="时长"
|
label="时长"
|
||||||
value={`${videoDuration}`}
|
value={`${videoDuration}`}
|
||||||
options={engineOptions.durations.map((d) => `${d}`)}
|
options={durationOptions}
|
||||||
suffix="秒"
|
suffix="秒"
|
||||||
expanded={expandedEngine === "duration"}
|
expanded={expandedEngine === "duration"}
|
||||||
onToggle={() =>
|
onToggle={handleDurationToggle}
|
||||||
setExpandedEngine(
|
onSelect={handleDurationSelect}
|
||||||
expandedEngine === "duration" ? null : "duration",
|
onClose={handleDurationClose}
|
||||||
)
|
|
||||||
}
|
|
||||||
onSelect={(v) => setVideoDuration(parseInt(v))}
|
|
||||||
onClose={() => setExpandedEngine(null)}
|
|
||||||
/>
|
/>
|
||||||
{currentOptionGroups.map((group, gi) => {
|
{currentOptionGroups.map((group, gi) => {
|
||||||
const isExpanded = expandedGroup === group.name;
|
const isExpanded = expandedGroup === group.name;
|
||||||
@@ -2291,23 +2326,12 @@ const GeneratePage: React.FC = () => {
|
|||||||
<PortalOptionGroup
|
<PortalOptionGroup
|
||||||
key={`og-${gi}`}
|
key={`og-${gi}`}
|
||||||
group={group}
|
group={group}
|
||||||
|
groupName={group.name}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
onToggle={() =>
|
onToggle={handleGroupToggle}
|
||||||
setExpandedGroup(isExpanded ? null : group.name)
|
onSelect={handleOptionGroupSelect}
|
||||||
}
|
onClose={handleGroupClose}
|
||||||
onSelect={(opt) => {
|
|
||||||
setSelectedOptions((prev) => {
|
|
||||||
const next = { ...prev };
|
|
||||||
if (next[group.name] === opt) {
|
|
||||||
delete next[group.name];
|
|
||||||
} else {
|
|
||||||
next[group.name] = opt;
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onClose={() => setExpandedGroup(null)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user