项目生成模块选择器闪动修改
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
const MAX_DURATION_SECONDS = 180;
|
||||
const MAX_DURATION_SECONDS = 300;
|
||||
|
||||
interface ProgressItemProps {
|
||||
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 { createPortal } from "react-dom";
|
||||
import {
|
||||
@@ -62,6 +62,322 @@ import { formatDate } from "../utils/formatDate";
|
||||
import UploadSelector from "../components/UploadSelector";
|
||||
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 configs: Record<
|
||||
// string,
|
||||
@@ -340,6 +656,40 @@ const GeneratePage: React.FC = () => {
|
||||
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
|
||||
const [lastTextCredits, setLastTextCredits] = useState(0);
|
||||
const [lastTextTokens, setLastTextTokens] = useState(0);
|
||||
@@ -883,9 +1233,9 @@ const GeneratePage: React.FC = () => {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
const projectRecords = recordItems.filter((r) => r.projectId === projectId);
|
||||
const projectName = project?.name ?? "项目";
|
||||
const currentOptionGroups = project
|
||||
? industryOptionGroups[project.industry] || []
|
||||
: [];
|
||||
const currentOptionGroups = useMemo(() => {
|
||||
return project ? industryOptionGroups[project.industry] || [] : [];
|
||||
}, [project, industryOptionGroups]);
|
||||
const userCredits = user?.credits ?? 0;
|
||||
|
||||
// 根据图片分辨率从 cimage 获取积分
|
||||
@@ -1320,318 +1670,6 @@ const GeneratePage: React.FC = () => {
|
||||
},
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
@@ -1971,6 +2009,7 @@ const GeneratePage: React.FC = () => {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -2127,7 +2166,7 @@ const GeneratePage: React.FC = () => {
|
||||
rows={3}
|
||||
placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
||||
maxLength={500}
|
||||
bordered={false}
|
||||
variant="borderless"
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
style={{
|
||||
padding: "6px 2px",
|
||||
@@ -2273,16 +2312,12 @@ const GeneratePage: React.FC = () => {
|
||||
<PortalDropdown
|
||||
label="时长"
|
||||
value={`${videoDuration}`}
|
||||
options={engineOptions.durations.map((d) => `${d}`)}
|
||||
options={durationOptions}
|
||||
suffix="秒"
|
||||
expanded={expandedEngine === "duration"}
|
||||
onToggle={() =>
|
||||
setExpandedEngine(
|
||||
expandedEngine === "duration" ? null : "duration",
|
||||
)
|
||||
}
|
||||
onSelect={(v) => setVideoDuration(parseInt(v))}
|
||||
onClose={() => setExpandedEngine(null)}
|
||||
onToggle={handleDurationToggle}
|
||||
onSelect={handleDurationSelect}
|
||||
onClose={handleDurationClose}
|
||||
/>
|
||||
{currentOptionGroups.map((group, gi) => {
|
||||
const isExpanded = expandedGroup === group.name;
|
||||
@@ -2291,23 +2326,12 @@ const GeneratePage: React.FC = () => {
|
||||
<PortalOptionGroup
|
||||
key={`og-${gi}`}
|
||||
group={group}
|
||||
groupName={group.name}
|
||||
selected={selected}
|
||||
expanded={isExpanded}
|
||||
onToggle={() =>
|
||||
setExpandedGroup(isExpanded ? null : group.name)
|
||||
}
|
||||
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)}
|
||||
onToggle={handleGroupToggle}
|
||||
onSelect={handleOptionGroupSelect}
|
||||
onClose={handleGroupClose}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user