ai创作未使用接口清除,添加@选取
This commit is contained in:
@@ -708,3 +708,14 @@ export async function deleteOAuthAccount(params: DeleteOAuthAccountParams): Prom
|
||||
export async function getAllOAuthAccountList(): Promise<any> {
|
||||
return api.post(`/upload-material/oauth_account_list`);
|
||||
}
|
||||
|
||||
|
||||
// 首页素材案例头
|
||||
export async function getHomeCaseHeader(): Promise<any> {
|
||||
return api.get(`/home-materials/categories`);
|
||||
}
|
||||
|
||||
// 首页素材按钮资源
|
||||
export async function getHomeCaseButton(id: string,limit:number=8): Promise<any> {
|
||||
return api.get(`/home-materials?category_id=${id}&limit_per_category=${limit}&include_empty_categories=false&response_mode=grouped&page=1&page_size=20`);
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
}}>
|
||||
<Outlet />
|
||||
{!isMobile && <Outlet />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -896,7 +904,7 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mobile-content">
|
||||
<Outlet />
|
||||
{isMobile && <Outlet />}
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
|
||||
@@ -139,13 +139,46 @@ const AIChatPage: React.FC = () => {
|
||||
setEngineOptions,
|
||||
setEnginesele,
|
||||
setInputValue,
|
||||
currentMedia,
|
||||
setCurrentMedia,
|
||||
} = useAppStore();
|
||||
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const [currentMedia, setCurrentMedia] = useState<{ name: string; type: 'image' | 'video'; url: string }[]>([]);
|
||||
// @ 提及相关状态
|
||||
const [mentionVisible, setMentionVisible] = useState(false);
|
||||
const mentionInputRef = useRef<any>(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<boolean>(false);
|
||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
||||
@@ -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<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
const textarea = e.target;
|
||||
checkMention(textarea, value);
|
||||
};
|
||||
|
||||
// 键盘事件处理(ESC 关闭提及、Tab/Enter 选择)
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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 && (
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12, overflowX: 'auto' }}>
|
||||
{currentMedia.map((media, idx) => (
|
||||
<div key={`media-${idx}`} style={{ position: 'relative', width: media.type === 'video' ? 120 : 80, flexShrink: 0 }}>
|
||||
<div key={`media-${idx}`} style={{ position: 'relative', width: media.type === 'video' ? 120 : 80, flexShrink: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
|
||||
{media.type === 'image' ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.url}`}
|
||||
@@ -1457,6 +1516,9 @@ const AIChatPage: React.FC = () => {
|
||||
style={{ width: '100%', height: 80, objectFit: 'cover', borderRadius: 8 }}
|
||||
/>
|
||||
)}
|
||||
<span style={{ fontSize: 11, color: '#64748b', fontWeight: 500 }}>
|
||||
{media.label}
|
||||
</span>
|
||||
{/* 删除已上传媒体按钮 */}
|
||||
<button
|
||||
onClick={() => handleRemoveMedia(idx)}
|
||||
@@ -1495,6 +1557,7 @@ const AIChatPage: React.FC = () => {
|
||||
border: '1px solid rgba(99, 102, 241, 0.08)',
|
||||
boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)',
|
||||
transition: 'all 0.2s ease',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* 上传按钮 */}
|
||||
<Upload
|
||||
@@ -1539,8 +1602,10 @@ const AIChatPage: React.FC = () => {
|
||||
|
||||
{/* 文本输入框 */}
|
||||
<TextArea
|
||||
ref={mentionInputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="上传最多4张参考图,输入提示词描述您想生成的画面..."
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
@@ -1554,14 +1619,64 @@ const AIChatPage: React.FC = () => {
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
color: '#1e293b',
|
||||
// resize: 'none',
|
||||
|
||||
// boxShadow: 'none',
|
||||
// padding: '8px 12px',
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{/* @ 提及下拉列表 */}
|
||||
{mentionVisible && currentMedia.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: 70,
|
||||
marginBottom: 8,
|
||||
zIndex: 1000,
|
||||
background: '#fff',
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)',
|
||||
border: '1px solid #e2e8f0',
|
||||
padding: 6,
|
||||
minWidth: 160,
|
||||
maxHeight: 260,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{currentMedia.map((media, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => insertMention(media.label)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 6,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
fontSize: 13,
|
||||
color: '#334155',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.08)';
|
||||
e.currentTarget.style.color = '#6366f1';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = '#334155';
|
||||
}}
|
||||
>
|
||||
{media.type === 'image' ? (
|
||||
<PictureOutlined style={{ fontSize: 14, color: '#6366f1' }} />
|
||||
) : (
|
||||
<VideoCameraOutlined style={{ fontSize: 14, color: '#8b5cf6' }} />
|
||||
)}
|
||||
<span>{media.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 发送按钮 */}
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getmedit } from '../api';
|
||||
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
|
||||
|
||||
import hot from '../assets/homebtn1.png';
|
||||
import mashup from '../assets/homebtn2.png';
|
||||
@@ -39,6 +39,14 @@ const HomePage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('project');
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [mockVideos, setMockVideos] = useState<any[]>([]);
|
||||
useEffect(() => {
|
||||
getHomeCaseHeader().then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
getHomeCaseButton('0019f17d00fdbece98e').then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAll = async () => {
|
||||
|
||||
@@ -32,6 +32,7 @@ interface AppState {
|
||||
};
|
||||
enginesele: any;
|
||||
inputValue: string;
|
||||
currentMedia: { name: string; type: 'image' | 'video'; url: string; label: string }[];
|
||||
|
||||
fetchProjects: () => Promise<void>;
|
||||
createProject: (name: string, industry: Industry) => Promise<Project>;
|
||||
@@ -58,6 +59,7 @@ interface AppState {
|
||||
setVideoResolution: (resolution: string) => void;
|
||||
setEnginesele: (enginesele: any) => void;
|
||||
setInputValue: (inputValue: string) => void;
|
||||
setCurrentMedia: (currentMedia: { name: string; type: 'image' | 'video'; url: string; label: string }[]) => void;
|
||||
resetGenerationConfig: () => void;
|
||||
}
|
||||
|
||||
@@ -83,6 +85,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
},
|
||||
enginesele: [],
|
||||
inputValue: '',
|
||||
currentMedia: [],
|
||||
|
||||
fetchProjects: async () => {
|
||||
set({ loading: true });
|
||||
@@ -172,6 +175,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
setVideoResolution: (resolution) => set({ videoResolution: resolution }),
|
||||
setEnginesele: (enginesele) => set({ enginesele }),
|
||||
setInputValue: (inputValue) => set({ inputValue }),
|
||||
setCurrentMedia: (currentMedia) => set({ currentMedia }),
|
||||
resetGenerationConfig: () => set({
|
||||
mediaType: 'image',
|
||||
countType: '请选择',
|
||||
@@ -189,5 +193,6 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
},
|
||||
enginesele: [],
|
||||
inputValue: '',
|
||||
currentMedia: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user