diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index 92db212..074e222 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -50,6 +50,7 @@ import { H5TemplateDrawer } from "./H5TemplateDrawer"; import { PcPhotoStoryModal } from "./PcPhotoStoryModal"; import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer"; import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector"; +import { PortraitAnimeSelector, PortraitAnimeValue } from "./PortraitAnimeSelector"; import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"; import { getClientUserData } from "@/api/common"; @@ -151,7 +152,6 @@ export function ChatInputBox({ noData }: { noData: boolean }) { // 共享状态 - 需要在不同渲染函数间共享 const [script, setScript] = useState(""); // 用户输入的脚本内容 const router = useRouter(); - const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态 // 各种操作的加载状态 const [isCreating, setIsCreating] = useState(false); // 主视频创建按钮的加载状态 @@ -167,6 +167,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { language: string; videoDuration: string; expansion_mode: boolean; + pcode: PortraitAnimeValue; aspect_ratio: AspectRatioValue; }; @@ -176,6 +177,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { language: "english", videoDuration: "unlimited", expansion_mode: true, + pcode: "portrait", aspect_ratio: isMobile ? "VIDEO_ASPECT_RATIO_PORTRAIT" : "VIDEO_ASPECT_RATIO_LANDSCAPE", }); @@ -187,6 +189,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { const parsed = JSON.parse(savedConfig); setConfigOptions({ mode: parsed.mode || "auto", + pcode: parsed.pcode || "portrait", resolution: parsed.resolution || "720p", language: parsed.language || "english", videoDuration: parsed.videoDuration || "unlimited", @@ -273,6 +276,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { user_id: String(User.id), user_data: getClientUserData(), script: script, + pcode: configOptions.pcode, mode: configOptions.mode, resolution: configOptions.resolution, language: configOptions.language, @@ -423,7 +427,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { {/* 第二行:功能按钮和Action按钮 - 同一行 */} -
+
{/* 左侧功能按钮区域 */}
{/* 获取创意按钮 @@ -605,6 +609,15 @@ export function ChatInputBox({ noData }: { noData: boolean }) { placement="top" className={`${isMobile ? '!px-1' : ''}`} /> + {/* 分隔线(移动端隐藏,避免拥挤) */} +
+ + onConfigChange('pcode', v)} + placement="top" + className={`${isMobile ? '!px-1' : ''}`} + />
{/* 右侧Action按钮 */} diff --git a/components/ChatInputBox/PortraitAnimeSelector.tsx b/components/ChatInputBox/PortraitAnimeSelector.tsx new file mode 100644 index 0000000..b163f20 --- /dev/null +++ b/components/ChatInputBox/PortraitAnimeSelector.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from 'react'; +import { Dropdown } from 'antd'; +import { ChevronUp } from 'lucide-react'; +import { fetchSettingByCode } from '@/api/serversetting'; + +export type PortraitAnimeValue = 'portrait' | string; + +type DropdownPlacement = + | 'top' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; + +export interface PortraitAnimeSelectorProps { + value: PortraitAnimeValue; + onChange: (value: PortraitAnimeValue) => void; + className?: string; + disabled?: boolean; + placement?: DropdownPlacement; +} + +/** + * Portrait/Anime selector with a split button and dropdown for Anime subtype. + * @param {PortraitAnimeValue} value - Current value: 'portrait' | '2d' | '3d'. + * @param {(value: PortraitAnimeValue) => void} onChange - Change handler. + * @param {string} [className] - Optional wrapper class. + * @param {boolean} [disabled] - Disable interaction when true. + * @param {DropdownPlacement} [placement] - Dropdown placement. + * @returns {JSX.Element} - Selector component. + */ +export function PortraitAnimeSelector({ + value, + onChange, + className, + disabled = false, + placement = 'top', +}: PortraitAnimeSelectorProps) { + const [lastAnimeChoice, setLastAnimeChoice] = useState('STANDARD_V1_734684_116483'); + const [animeOptions, setAnimeOptions] = useState>([]); + + useEffect(() => { + if (value && value !== 'portrait') { + setLastAnimeChoice(value); + } + }, [value]); + + useEffect(() => { + let mounted = true; + (async () => { + const list = await fetchSettingByCode>('comic_config', []); + if (!mounted) return; + if (Array.isArray(list) && list.length > 0) { + setAnimeOptions(list); + // initialize last choice to first entry if current is still default + setLastAnimeChoice((prev) => (prev === 'STANDARD_V1_734684_116483' ? list[0].pcode : prev)); + } else { + setAnimeOptions([ + { name: 'Korean Comics Long', pcode: 'STANDARD_V1_734684_116483' }, + ]); + } + })(); + return () => { mounted = false; }; + }, []); + + const isPortrait = value === 'portrait'; + const currentAnime = useMemo(() => (value && value !== 'portrait' ? value : lastAnimeChoice), [value, lastAnimeChoice]); + const pcodeToName = useMemo(() => { + const map: Record = {}; + animeOptions.forEach((o) => { map[o.pcode] = o.name; }); + return map; + }, [animeOptions]); + + const animeMenuItems = (animeOptions.length > 0 ? animeOptions : []).map((opt) => ({ + key: opt.pcode, + label: ( +
+ {opt.name} +
+ ), + })); + + return ( +
+ {/* Portrait button */} + + + {/* Anime split button */} + onChange(key as PortraitAnimeValue), + selectable: true, + selectedKeys: isPortrait ? [] : [String(currentAnime)], + }} + trigger={["hover", "click"]} + placement={placement} + disabled={disabled} + > +
+ + +
+
+
+ ); +} + +export default PortraitAnimeSelector; + +