ai创作未使用接口清除,添加@选取

This commit is contained in:
sjy
2026-07-01 10:41:19 +08:00
parent c6df04a895
commit 291b240805
5 changed files with 200 additions and 53 deletions
+11
View File
@@ -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
+165 -50
View File
@@ -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"
+9 -1
View File
@@ -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 () => {
+5
View File
@@ -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: [],
}),
}));