ly0303521

添加视频的删除功能

... ... @@ -24,4 +24,5 @@ dist-ssr
*.sw?
*.pyc
backend/whitelist.txt
backend/gallery_data.json
backend/gallery_images.json
backend/gallery_videos.json
... ...
... ... @@ -148,6 +148,18 @@ class JsonStore:
self._write(data)
return target_item
def delete_item(self, item_id: str) -> bool:
with self.lock:
data = self._read()
items = data.get(self.item_key, [])
initial_len = len(items)
items = [i for i in items if i.get("id") != item_id]
if len(items) < initial_len:
data[self.item_key] = items
self._write(data)
return True
return False
image_store = JsonStore(GALLERY_IMAGES_PATH, item_key="images", max_items=GALLERY_MAX_ITEMS)
video_store = JsonStore(GALLERY_VIDEOS_PATH, item_key="videos", max_items=GALLERY_MAX_ITEMS)
whitelist_store = WhitelistStore(WHITELIST_PATH)
... ... @@ -201,6 +213,23 @@ async def add_video(video: GalleryVideo):
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to store video metadata: {exc}")
@app.delete("/gallery/videos/{item_id}")
async def delete_video(item_id: str, user_id: str = Query(..., alias="userId")):
items = video_store.list_items()
target_item = next((i for i in items if i.get("id") == item_id), None)
if not target_item:
raise HTTPException(status_code=404, detail="Video not found")
ADMIN_ID = "86427531"
if user_id != target_item.get("authorId") and user_id != ADMIN_ID:
raise HTTPException(status_code=403, detail="Not authorized to delete this video")
if video_store.delete_item(item_id):
return {"status": "ok", "id": item_id}
raise HTTPException(status_code=500, detail="Failed to delete video")
@app.post("/generate", response_model=ImageGenerationResponse)
async def generate_image(payload: ImageGenerationPayload):
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
import { SHOWCASE_IMAGES, ADMIN_ID, VIDEO_OSS_BASE_URL } from './constants';
import { generateImage } from './services/imageService';
import { submitVideoJob, pollVideoStatus } from './services/videoService';
import { fetchGallery, fetchVideoGallery, saveVideo, toggleLike } from './services/galleryService';
import { fetchGallery, fetchVideoGallery, saveVideo, toggleLike, deleteVideo } from './services/galleryService';
import MasonryGrid from './components/MasonryGrid';
import InputBar from './components/InputBar';
import HistoryBar from './components/HistoryBar';
... ... @@ -192,6 +192,21 @@ const App: React.FC = () => {
}
};
const handleDeleteVideo = async (video: ImageItem) => {
if (!currentUser) { setIsAuthModalOpen(true); return; }
if (!confirm("确定要删除这个视频吗?")) return;
try {
await deleteVideo(video.id, currentUser.employeeId);
setVideos(prev => prev.filter(v => v.id !== video.id));
setSelectedImage(null); // Close modal
} catch (e) {
console.error("Delete failed", e);
alert("删除失败");
}
};
const handleGenerateSimilar = (params: ImageGenerationParams) => {
setIncomingParams(params);
const banner = document.getElementById('similar-feedback');
... ... @@ -269,7 +284,7 @@ const App: React.FC = () => {
<HistoryBar images={galleryMode === GalleryMode.Image ? userHistory : userVideoHistory} onSelect={setSelectedImage} />
<InputBar onGenerate={handleGenerate} isGenerating={isGenerating || isGeneratingVideo} incomingParams={incomingParams} isVideoMode={galleryMode === GalleryMode.Video} videoStatus={videoStatus} />
{selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar}/>}
{selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar} onDelete={handleDeleteVideo} currentUser={currentUser} />}
<AdminModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} onSave={handleSaveImage} onDelete={handleDeleteImage} initialData={editingImage} />
<WhitelistModal isOpen={isWhitelistModalOpen} onClose={() => setIsWhitelistModalOpen(false)} />
</div>
... ...
import React from 'react';
import { ImageItem, ImageGenerationParams } from '../types';
import { X, Copy, Download, Edit3, Heart, Zap } from 'lucide-react';
import { X, Copy, Download, Edit3, Heart, Zap, Trash2 } from 'lucide-react';
interface DetailModalProps {
image: ImageItem | null;
onClose: () => void;
onEdit?: (image: ImageItem) => void;
onGenerateSimilar?: (params: ImageGenerationParams) => void;
onDelete?: (image: ImageItem) => void;
currentUser?: { employeeId: string; hasAccess: boolean } | null;
}
const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGenerateSimilar }) => {
import { ADMIN_ID } from '../constants';
const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGenerateSimilar, onDelete, currentUser }) => {
if (!image) return null;
const isVideo = image.id.startsWith('vid-');
const isAdmin = currentUser?.employeeId === ADMIN_ID;
const isAuthor = currentUser?.employeeId === image.authorId;
const canDelete = onDelete && (isAdmin || isAuthor);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
... ... @@ -52,6 +59,15 @@ const DetailModal: React.FC<DetailModalProps> = ({ image, onClose, onEdit, onGen
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">参数详情</h2>
<div className="flex gap-2">
{canDelete && (
<button
onClick={() => onDelete && onDelete(image)}
className="p-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-full transition-colors"
title="删除"
>
<Trash2 size={18} />
</button>
)}
{onEdit && (
<button
onClick={() => onEdit(image)}
... ...
... ... @@ -78,4 +78,18 @@ export const toggleLike = async (itemId: string, userId: string): Promise<ImageI
}
return await response.json();
};
export const deleteVideo = async (itemId: string, userId: string): Promise<void> => {
if (API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL) {
throw new Error("Cannot delete videos in direct mode");
}
const response = await fetch(`${API_BASE_URL}/gallery/videos/${itemId}?userId=${userId}`, {
method: 'DELETE',
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Delete failed (${response.status}): ${errorText}`);
}
};
\ No newline at end of file
... ...