ly0303521

添加视频的删除功能

@@ -24,4 +24,5 @@ dist-ssr @@ -24,4 +24,5 @@ dist-ssr
24 *.sw? 24 *.sw?
25 *.pyc 25 *.pyc
26 backend/whitelist.txt 26 backend/whitelist.txt
27 -backend/gallery_data.json 27 +backend/gallery_images.json
  28 +backend/gallery_videos.json
@@ -148,6 +148,18 @@ class JsonStore: @@ -148,6 +148,18 @@ class JsonStore:
148 self._write(data) 148 self._write(data)
149 return target_item 149 return target_item
150 150
  151 + def delete_item(self, item_id: str) -> bool:
  152 + with self.lock:
  153 + data = self._read()
  154 + items = data.get(self.item_key, [])
  155 + initial_len = len(items)
  156 + items = [i for i in items if i.get("id") != item_id]
  157 + if len(items) < initial_len:
  158 + data[self.item_key] = items
  159 + self._write(data)
  160 + return True
  161 + return False
  162 +
151 image_store = JsonStore(GALLERY_IMAGES_PATH, item_key="images", max_items=GALLERY_MAX_ITEMS) 163 image_store = JsonStore(GALLERY_IMAGES_PATH, item_key="images", max_items=GALLERY_MAX_ITEMS)
152 video_store = JsonStore(GALLERY_VIDEOS_PATH, item_key="videos", max_items=GALLERY_MAX_ITEMS) 164 video_store = JsonStore(GALLERY_VIDEOS_PATH, item_key="videos", max_items=GALLERY_MAX_ITEMS)
153 whitelist_store = WhitelistStore(WHITELIST_PATH) 165 whitelist_store = WhitelistStore(WHITELIST_PATH)
@@ -201,6 +213,23 @@ async def add_video(video: GalleryVideo): @@ -201,6 +213,23 @@ async def add_video(video: GalleryVideo):
201 except Exception as exc: 213 except Exception as exc:
202 raise HTTPException(status_code=500, detail=f"Failed to store video metadata: {exc}") 214 raise HTTPException(status_code=500, detail=f"Failed to store video metadata: {exc}")
203 215
  216 +@app.delete("/gallery/videos/{item_id}")
  217 +async def delete_video(item_id: str, user_id: str = Query(..., alias="userId")):
  218 + items = video_store.list_items()
  219 + target_item = next((i for i in items if i.get("id") == item_id), None)
  220 +
  221 + if not target_item:
  222 + raise HTTPException(status_code=404, detail="Video not found")
  223 +
  224 + ADMIN_ID = "86427531"
  225 +
  226 + if user_id != target_item.get("authorId") and user_id != ADMIN_ID:
  227 + raise HTTPException(status_code=403, detail="Not authorized to delete this video")
  228 +
  229 + if video_store.delete_item(item_id):
  230 + return {"status": "ok", "id": item_id}
  231 + raise HTTPException(status_code=500, detail="Failed to delete video")
  232 +
204 @app.post("/generate", response_model=ImageGenerationResponse) 233 @app.post("/generate", response_model=ImageGenerationResponse)
205 async def generate_image(payload: ImageGenerationPayload): 234 async def generate_image(payload: ImageGenerationPayload):
206 request_params_data = payload.model_dump(); body = {k: v for k, v in request_params_data.items() if v is not None and k != "author_id"} 235 request_params_data = payload.model_dump(); body = {k: v for k, v in request_params_data.items() if v is not None and k != "author_id"}
@@ -3,7 +3,7 @@ import { ImageItem, ImageGenerationParams, UserProfile, VideoStatus } from './ty @@ -3,7 +3,7 @@ import { ImageItem, ImageGenerationParams, UserProfile, VideoStatus } from './ty
3 import { SHOWCASE_IMAGES, ADMIN_ID, VIDEO_OSS_BASE_URL } from './constants'; 3 import { SHOWCASE_IMAGES, ADMIN_ID, VIDEO_OSS_BASE_URL } from './constants';
4 import { generateImage } from './services/imageService'; 4 import { generateImage } from './services/imageService';
5 import { submitVideoJob, pollVideoStatus } from './services/videoService'; 5 import { submitVideoJob, pollVideoStatus } from './services/videoService';
6 -import { fetchGallery, fetchVideoGallery, saveVideo, toggleLike } from './services/galleryService'; 6 +import { fetchGallery, fetchVideoGallery, saveVideo, toggleLike, deleteVideo } from './services/galleryService';
7 import MasonryGrid from './components/MasonryGrid'; 7 import MasonryGrid from './components/MasonryGrid';
8 import InputBar from './components/InputBar'; 8 import InputBar from './components/InputBar';
9 import HistoryBar from './components/HistoryBar'; 9 import HistoryBar from './components/HistoryBar';
@@ -192,6 +192,21 @@ const App: React.FC = () => { @@ -192,6 +192,21 @@ const App: React.FC = () => {
192 } 192 }
193 }; 193 };
194 194
  195 + const handleDeleteVideo = async (video: ImageItem) => {
  196 + if (!currentUser) { setIsAuthModalOpen(true); return; }
  197 +
  198 + if (!confirm("确定要删除这个视频吗?")) return;
  199 +
  200 + try {
  201 + await deleteVideo(video.id, currentUser.employeeId);
  202 + setVideos(prev => prev.filter(v => v.id !== video.id));
  203 + setSelectedImage(null); // Close modal
  204 + } catch (e) {
  205 + console.error("Delete failed", e);
  206 + alert("删除失败");
  207 + }
  208 + };
  209 +
195 const handleGenerateSimilar = (params: ImageGenerationParams) => { 210 const handleGenerateSimilar = (params: ImageGenerationParams) => {
196 setIncomingParams(params); 211 setIncomingParams(params);
197 const banner = document.getElementById('similar-feedback'); 212 const banner = document.getElementById('similar-feedback');
@@ -269,7 +284,7 @@ const App: React.FC = () => { @@ -269,7 +284,7 @@ const App: React.FC = () => {
269 284
270 <HistoryBar images={galleryMode === GalleryMode.Image ? userHistory : userVideoHistory} onSelect={setSelectedImage} /> 285 <HistoryBar images={galleryMode === GalleryMode.Image ? userHistory : userVideoHistory} onSelect={setSelectedImage} />
271 <InputBar onGenerate={handleGenerate} isGenerating={isGenerating || isGeneratingVideo} incomingParams={incomingParams} isVideoMode={galleryMode === GalleryMode.Video} videoStatus={videoStatus} /> 286 <InputBar onGenerate={handleGenerate} isGenerating={isGenerating || isGeneratingVideo} incomingParams={incomingParams} isVideoMode={galleryMode === GalleryMode.Video} videoStatus={videoStatus} />
272 - {selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar}/>} 287 + {selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar} onDelete={handleDeleteVideo} currentUser={currentUser} />}
273 <AdminModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} onSave={handleSaveImage} onDelete={handleDeleteImage} initialData={editingImage} /> 288 <AdminModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} onSave={handleSaveImage} onDelete={handleDeleteImage} initialData={editingImage} />
274 <WhitelistModal isOpen={isWhitelistModalOpen} onClose={() => setIsWhitelistModalOpen(false)} /> 289 <WhitelistModal isOpen={isWhitelistModalOpen} onClose={() => setIsWhitelistModalOpen(false)} />
275 </div> 290 </div>
1 import React from 'react'; 1 import React from 'react';
2 import { ImageItem, ImageGenerationParams } from '../types'; 2 import { ImageItem, ImageGenerationParams } from '../types';
3 -import { X, Copy, Download, Edit3, Heart, Zap } from 'lucide-react'; 3 +import { X, Copy, Download, Edit3, Heart, Zap, Trash2 } from 'lucide-react';
4 4
5 interface DetailModalProps { 5 interface DetailModalProps {
6 image: ImageItem | null; 6 image: ImageItem | null;
7 onClose: () => void; 7 onClose: () => void;
8 onEdit?: (image: ImageItem) => void; 8 onEdit?: (image: ImageItem) => void;
9 onGenerateSimilar?: (params: ImageGenerationParams) => void; 9 onGenerateSimilar?: (params: ImageGenerationParams) => void;
  10 + onDelete?: (image: ImageItem) => void;
  11 + currentUser?: { employeeId: string; hasAccess: boolean } | null;
10 } 12 }
11 13
12 -const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGenerateSimilar }) => { 14 +import { ADMIN_ID } from '../constants';
  15 +
  16 +const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGenerateSimilar, onDelete, currentUser }) => {
13 if (!image) return null; 17 if (!image) return null;
14 18
15 const isVideo = image.id.startsWith('vid-'); 19 const isVideo = image.id.startsWith('vid-');
  20 + const isAdmin = currentUser?.employeeId === ADMIN_ID;
  21 + const isAuthor = currentUser?.employeeId === image.authorId;
  22 + const canDelete = onDelete && (isAdmin || isAuthor);
16 23
17 const copyToClipboard = (text: string) => { 24 const copyToClipboard = (text: string) => {
18 navigator.clipboard.writeText(text); 25 navigator.clipboard.writeText(text);
@@ -52,6 +59,15 @@ const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGen @@ -52,6 +59,15 @@ const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGen
52 <div className="flex justify-between items-start mb-6"> 59 <div className="flex justify-between items-start mb-6">
53 <h2 className="text-2xl font-bold text-gray-800 dark:text-white">参数详情</h2> 60 <h2 className="text-2xl font-bold text-gray-800 dark:text-white">参数详情</h2>
54 <div className="flex gap-2"> 61 <div className="flex gap-2">
  62 + {canDelete && (
  63 + <button
  64 + onClick={() => onDelete && onDelete(image)}
  65 + className="p-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-full transition-colors"
  66 + title="删除"
  67 + >
  68 + <Trash2 size={18} />
  69 + </button>
  70 + )}
55 {onEdit && ( 71 {onEdit && (
56 <button 72 <button
57 onClick={() => onEdit(image)} 73 onClick={() => onEdit(image)}
@@ -79,3 +79,17 @@ export const toggleLike = async (itemId: string, userId: string): Promise<ImageI @@ -79,3 +79,17 @@ export const toggleLike = async (itemId: string, userId: string): Promise<ImageI
79 79
80 return await response.json(); 80 return await response.json();
81 }; 81 };
  82 +
  83 +export const deleteVideo = async (itemId: string, userId: string): Promise<void> => {
  84 + if (API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL) {
  85 + throw new Error("Cannot delete videos in direct mode");
  86 + }
  87 + const response = await fetch(`${API_BASE_URL}/gallery/videos/${itemId}?userId=${userId}`, {
  88 + method: 'DELETE',
  89 + });
  90 +
  91 + if (!response.ok) {
  92 + const errorText = await response.text();
  93 + throw new Error(`Delete failed (${response.status}): ${errorText}`);
  94 + }
  95 +};