Showing
5 changed files
with
80 additions
and
5 deletions
| @@ -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 | +}; |
-
Please register or login to post a comment