ly0303521

修复每个用户只能看到自己的点赞

... ... @@ -61,6 +61,7 @@ class GalleryImage(BaseModel):
likes: int = 0
is_mock: bool = Field(default=False, alias="isMock")
negative_prompt: Optional[str] = None
liked_by: List[str] = Field(default_factory=list, alias="likedBy")
ImageGenerationResponse.model_rebuild()
... ... @@ -129,6 +130,31 @@ class GalleryStore:
self._write(data)
return payload
def toggle_like(self, image_id: str, user_id: str) -> Optional[dict]:
with self.lock:
data = self._read()
images = data.get("images", [])
target_image = next((img for img in images if img.get("id") == image_id), None)
if not target_image:
return None
liked_by = target_image.get("likedBy", [])
# Handle legacy data where likedBy might be missing
if not isinstance(liked_by, list):
liked_by = []
if user_id in liked_by:
liked_by.remove(user_id)
target_image["likes"] = max(0, target_image.get("likes", 0) - 1)
else:
liked_by.append(user_id)
target_image["likes"] = target_image.get("likes", 0) + 1
target_image["likedBy"] = liked_by
self._write(data)
return target_image
gallery_store = GalleryStore(GALLERY_DATA_PATH, GALLERY_MAX_ITEMS)
... ... @@ -160,6 +186,18 @@ async def health() -> dict:
return {"status": "ok"}
@app.post("/likes/{image_id}")
async def toggle_like(
image_id: str,
user_id: str = Query(..., alias="userId")
) -> dict:
"""Toggle like status for an image by a user."""
updated_image = gallery_store.toggle_like(image_id, user_id)
if not updated_image:
raise HTTPException(status_code=404, detail="Image not found")
return updated_image
@app.get("/gallery")
async def gallery(
limit: int = Query(200, ge=1, le=1000),
... ...
... ... @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { ImageItem, ImageGenerationParams, UserProfile } from './types';
import { SHOWCASE_IMAGES, ADMIN_ID } from './constants';
import { generateImage } from './services/imageService';
import { fetchGallery } from './services/galleryService';
import { fetchGallery, toggleLike } from './services/galleryService';
import MasonryGrid from './components/MasonryGrid';
import InputBar from './components/InputBar';
import HistoryBar from './components/HistoryBar';
... ... @@ -83,18 +83,17 @@ const App: React.FC = () => {
try {
const remoteImages = await fetchGallery();
setImages(prev => {
const localState = new Map(
prev.map(img => [
img.id,
{ likes: img.likes, isLikedByCurrentUser: !!img.isLikedByCurrentUser },
])
);
const normalized = remoteImages.map(img => ({
...img,
likes: localState.get(img.id)?.likes ?? img.likes ?? 0,
isLikedByCurrentUser: localState.get(img.id)?.isLikedByCurrentUser ?? false,
}));
// Map server images, calculating isLikedByCurrentUser
const normalized = remoteImages.map(img => {
const isLiked = img.likedBy && currentUser
? img.likedBy.includes(currentUser.employeeId)
: false;
return {
...img,
likes: img.likes, // Trust server
isLikedByCurrentUser: isLiked,
};
});
if (normalized.length >= MIN_GALLERY_ITEMS) {
return normalized;
... ... @@ -110,17 +109,9 @@ const App: React.FC = () => {
});
} catch (err) {
console.error("Failed to sync gallery", err);
setImages(prev => {
if (prev.length >= MIN_GALLERY_ITEMS) return prev;
const existingIds = new Set(prev.map(img => img.id));
const filler = SHOWCASE_IMAGES.filter(img => !existingIds.has(img.id)).slice(
0,
Math.max(0, MIN_GALLERY_ITEMS - prev.length)
);
return [...prev, ...filler];
});
// Keep previous state if sync fails
}
}, []);
}, [currentUser]);
useEffect(() => {
syncGallery();
... ... @@ -170,11 +161,14 @@ const App: React.FC = () => {
}
};
const handleLike = (image: ImageItem) => {
const handleLike = async (image: ImageItem) => {
if (!currentUser) {
setIsAuthModalOpen(true);
return;
}
// Optimistic update
const previousImages = [...images];
setImages(prev => prev.map(img => {
if (img.id === image.id) {
const isLiked = !!img.isLikedByCurrentUser;
... ... @@ -186,6 +180,14 @@ const App: React.FC = () => {
}
return img;
}));
try {
await toggleLike(image.id, currentUser.employeeId);
} catch (e) {
console.error("Like failed", e);
setImages(previousImages); // Revert
alert("操作失败");
}
};
const handleGenerateSimilar = (params: ImageGenerationParams) => {
... ...
... ... @@ -20,3 +20,19 @@ export const fetchGallery = async (authorId?: string): Promise<ImageItem[]> => {
const data: GalleryResponse = await response.json();
return data.images ?? [];
};
export const toggleLike = async (imageId: string, userId: string): Promise<ImageItem> => {
if (API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL) {
throw new Error("Cannot like images in direct mode");
}
const response = await fetch(`${API_BASE_URL}/likes/${imageId}?userId=${userId}`, {
method: 'POST',
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Like failed (${response.status}): ${errorText}`);
}
return await response.json();
};
... ...
... ... @@ -17,6 +17,7 @@ export interface ImageItem extends ImageGenerationParams {
// New fields for community features
authorId?: string; // The 8-digit employee ID
likes: number;
likedBy?: string[]; // List of user IDs who liked this image
isLikedByCurrentUser?: boolean; // UI state
isMock?: boolean; // True if it's a static showcase image
... ...