diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 136943cb..a914413f 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -708,3 +708,14 @@ export async function deleteOAuthAccount(params: DeleteOAuthAccountParams): Prom export async function getAllOAuthAccountList(): Promise { return api.post(`/upload-material/oauth_account_list`); } + + +// 首页素材案例头 +export async function getHomeCaseHeader(): Promise { + return api.get(`/home-materials/categories`); +} + +// 首页素材按钮资源 +export async function getHomeCaseButton(id: string,limit:number=8): Promise { + return api.get(`/home-materials?category_id=${id}&limit_per_category=${limit}&include_empty_categories=false&response_mode=grouped&page=1&page_size=20`); +} diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index 42834800..31e76605 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -334,6 +334,7 @@ const AppLayout: React.FC = () => { limitValue: string; limitUnit: string; } | null>(null); + const [isMobile, setIsMobile] = useState(false); const PENDING_ORDER_KEY = 'pending_payment_order'; @@ -387,6 +388,13 @@ const AppLayout: React.FC = () => { }, []); + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= 767); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + const handleContactMouseDown = (e: React.MouseEvent) => { if (e.button === 0) { setIsDragging(true); @@ -876,7 +884,7 @@ const AppLayout: React.FC = () => { padding: '24px 32px 32px', border: '1px solid rgba(0, 0, 0, 0.06)', }}> - + {!isMobile && } @@ -896,7 +904,7 @@ const AppLayout: React.FC = () => {
- + {isMobile && }
{ setEngineOptions, setEnginesele, setInputValue, + currentMedia, + setCurrentMedia, } = useAppStore(); const [uploading, setUploading] = useState(false); const [loading, setLoading] = useState(false); - const [currentMedia, setCurrentMedia] = useState<{ name: string; type: 'image' | 'video'; url: string }[]>([]); + // @ 提及相关状态 + const [mentionVisible, setMentionVisible] = useState(false); + const mentionInputRef = useRef(null); + const [mentionPosition, setMentionPosition] = useState({ top: 0, left: 0 }); + + // 数字转中文数字(一、二、三、四) + const numberToChinese = (num: number): string => { + const map = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; + if (num <= 10) return map[num]; + if (num < 20) return '十' + map[num % 10]; + if (num < 100) { + const tens = Math.floor(num / 10); + const ones = num % 10; + return map[tens] + '十' + (ones ? map[ones] : ''); + } + return String(num); + }; + + // 根据媒体列表生成标签(图片一、图片二、视频一、视频二...) + const generateMediaLabels = (media: { type: 'image' | 'video' }[]) => { + let imgCount = 0; + let vidCount = 0; + return media.map((m) => { + if (m.type === 'image') { + imgCount++; + return `图片${numberToChinese(imgCount)}`; + } else { + vidCount++; + return `视频${numberToChinese(vidCount)}`; + } + }); + }; const [previewVisible, setPreviewVisible] = useState(false); const [previewUrl, setPreviewUrl] = useState(''); @@ -396,15 +429,7 @@ const AIChatPage: React.FC = () => { }) .catch(() => { }); - getCreditRatios() - .then((data: any) => { - // console.log('积分', data); - - setCreditRatios(data.video || []); - setCimage(data.image || []); - }) - .catch((error) => { - }); + calculateCredits().then((data: any) => { // console.log('积分计算', data); // 保存积分计算数据 @@ -416,36 +441,7 @@ const AIChatPage: React.FC = () => { setGen_list(mess_list) setTotalnumber(total) }) - getParameters() - .then((data) => { - let supportedSizes = (data as any).items?.[0]?.supportedSizes || {}; - let supportedResolutions = []; - let twokwidth = []; - let fourkwidth = []; - - for (let key in supportedSizes["2K"]) { - supportedResolutions.push(key); - twokwidth.push(supportedSizes["2K"][key]); - } - - const newRatioOptions = supportedResolutions.map((res: any, index: number) => ({ - value: res, - label: String(index), - })); - setRatioOptions(newRatioOptions); - - for (let key in supportedSizes["4K"]) { - fourkwidth.push(supportedSizes["4K"][key]); - } - - const newWidthandHeight = [twokwidth, fourkwidth]; - setWidthandHeight(newWidthandHeight); - setResolutionOptions([ - { value: '2K', label: '高清 2K' }, - { value: '4K', label: '超清 4K' }, - ]); - }) - .catch(() => { }); + }, []); @@ -799,11 +795,15 @@ const AIChatPage: React.FC = () => { const uploadFn = isImage ? uploadImage : uploadVideo; const res = await uploadFn(file); // 添加到统一的媒体列表 - setCurrentMedia((prev) => [...prev, { + const mediaType: 'image' | 'video' = isImage ? 'image' : 'video'; + const newList = [...currentMedia, { name: file.name, - type: isImage ? 'image' : 'video', + type: mediaType, url: res.url, - }]); + label: '', + }]; + const labels = generateMediaLabels(newList); + setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] }))); message.success(`${isImage ? '图片' : '视频'}上传成功`); } catch (error) { message.error('上传失败'); @@ -817,7 +817,9 @@ const AIChatPage: React.FC = () => { const handleRemoveMedia = (index: number) => { - setCurrentMedia((prev) => prev.filter((_, i) => i !== index)); + const newList = currentMedia.filter((_, i) => i !== index); + const labels = generateMediaLabels(newList); + setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] }))); }; @@ -828,6 +830,63 @@ const AIChatPage: React.FC = () => { } }; + // 检测光标前的 @ 符号 + const checkMention = (textarea: HTMLTextAreaElement, value: string) => { + const cursorPos = textarea.selectionStart; + const textBeforeCursor = value.slice(0, cursorPos); + const atMatch = textBeforeCursor.match(/@([^@\s]*)$/); + + if (atMatch && currentMedia.length > 0) { + setMentionVisible(true); + return true; + } + setMentionVisible(false); + return false; + }; + + // 输入框变化处理 + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + const textarea = e.target; + checkMention(textarea, value); + }; + + // 键盘事件处理(ESC 关闭提及、Tab/Enter 选择) + const handleInputKeyDown = (e: React.KeyboardEvent) => { + if (mentionVisible) { + if (e.key === 'Escape') { + setMentionVisible(false); + } + } + }; + + // 插入 @ 提及 + const insertMention = (label: string) => { + const textarea = mentionInputRef.current?.resizableTextArea?.textArea; + if (!textarea) return; + + const cursorPos = textarea.selectionStart; + const textBefore = inputValue.slice(0, cursorPos); + const textAfter = inputValue.slice(cursorPos); + + const atIndex = textBefore.lastIndexOf('@'); + if (atIndex === -1) { + setMentionVisible(false); + return; + } + + const newValue = textBefore.slice(0, atIndex) + `@${label} ` + textAfter; + setInputValue(newValue); + setMentionVisible(false); + + setTimeout(() => { + const pos = atIndex + label.length + 2; + textarea.focus(); + textarea.setSelectionRange(pos, pos); + }, 0); + }; + const handleClosePreview = () => { @@ -1444,7 +1503,7 @@ const AIChatPage: React.FC = () => { {currentMedia.length > 0 && (
{currentMedia.map((media, idx) => ( -
+
{media.type === 'image' ? ( { style={{ width: '100%', height: 80, objectFit: 'cover', borderRadius: 8 }} /> )} + + {media.label} + {/* 删除已上传媒体按钮 */}