ly0303521

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

@@ -61,6 +61,7 @@ class GalleryImage(BaseModel): @@ -61,6 +61,7 @@ class GalleryImage(BaseModel):
61 likes: int = 0 61 likes: int = 0
62 is_mock: bool = Field(default=False, alias="isMock") 62 is_mock: bool = Field(default=False, alias="isMock")
63 negative_prompt: Optional[str] = None 63 negative_prompt: Optional[str] = None
  64 + liked_by: List[str] = Field(default_factory=list, alias="likedBy")
64 65
65 66
66 ImageGenerationResponse.model_rebuild() 67 ImageGenerationResponse.model_rebuild()
@@ -129,6 +130,31 @@ class GalleryStore: @@ -129,6 +130,31 @@ class GalleryStore:
129 self._write(data) 130 self._write(data)
130 return payload 131 return payload
131 132
  133 + def toggle_like(self, image_id: str, user_id: str) -> Optional[dict]:
  134 + with self.lock:
  135 + data = self._read()
  136 + images = data.get("images", [])
  137 + target_image = next((img for img in images if img.get("id") == image_id), None)
  138 +
  139 + if not target_image:
  140 + return None
  141 +
  142 + liked_by = target_image.get("likedBy", [])
  143 + # Handle legacy data where likedBy might be missing
  144 + if not isinstance(liked_by, list):
  145 + liked_by = []
  146 +
  147 + if user_id in liked_by:
  148 + liked_by.remove(user_id)
  149 + target_image["likes"] = max(0, target_image.get("likes", 0) - 1)
  150 + else:
  151 + liked_by.append(user_id)
  152 + target_image["likes"] = target_image.get("likes", 0) + 1
  153 +
  154 + target_image["likedBy"] = liked_by
  155 + self._write(data)
  156 + return target_image
  157 +
132 158
133 gallery_store = GalleryStore(GALLERY_DATA_PATH, GALLERY_MAX_ITEMS) 159 gallery_store = GalleryStore(GALLERY_DATA_PATH, GALLERY_MAX_ITEMS)
134 160
@@ -160,6 +186,18 @@ async def health() -> dict: @@ -160,6 +186,18 @@ async def health() -> dict:
160 return {"status": "ok"} 186 return {"status": "ok"}
161 187
162 188
  189 +@app.post("/likes/{image_id}")
  190 +async def toggle_like(
  191 + image_id: str,
  192 + user_id: str = Query(..., alias="userId")
  193 +) -> dict:
  194 + """Toggle like status for an image by a user."""
  195 + updated_image = gallery_store.toggle_like(image_id, user_id)
  196 + if not updated_image:
  197 + raise HTTPException(status_code=404, detail="Image not found")
  198 + return updated_image
  199 +
  200 +
163 @app.get("/gallery") 201 @app.get("/gallery")
164 async def gallery( 202 async def gallery(
165 limit: int = Query(200, ge=1, le=1000), 203 limit: int = Query(200, ge=1, le=1000),
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react';
2 import { ImageItem, ImageGenerationParams, UserProfile } from './types'; 2 import { ImageItem, ImageGenerationParams, UserProfile } from './types';
3 import { SHOWCASE_IMAGES, ADMIN_ID } from './constants'; 3 import { SHOWCASE_IMAGES, ADMIN_ID } from './constants';
4 import { generateImage } from './services/imageService'; 4 import { generateImage } from './services/imageService';
5 -import { fetchGallery } from './services/galleryService'; 5 +import { fetchGallery, toggleLike } from './services/galleryService';
6 import MasonryGrid from './components/MasonryGrid'; 6 import MasonryGrid from './components/MasonryGrid';
7 import InputBar from './components/InputBar'; 7 import InputBar from './components/InputBar';
8 import HistoryBar from './components/HistoryBar'; 8 import HistoryBar from './components/HistoryBar';
@@ -83,18 +83,17 @@ const App: React.FC = () => { @@ -83,18 +83,17 @@ const App: React.FC = () => {
83 try { 83 try {
84 const remoteImages = await fetchGallery(); 84 const remoteImages = await fetchGallery();
85 setImages(prev => { 85 setImages(prev => {
86 - const localState = new Map(  
87 - prev.map(img => [  
88 - img.id,  
89 - { likes: img.likes, isLikedByCurrentUser: !!img.isLikedByCurrentUser },  
90 - ])  
91 - );  
92 -  
93 - const normalized = remoteImages.map(img => ({  
94 - ...img,  
95 - likes: localState.get(img.id)?.likes ?? img.likes ?? 0,  
96 - isLikedByCurrentUser: localState.get(img.id)?.isLikedByCurrentUser ?? false,  
97 - })); 86 + // Map server images, calculating isLikedByCurrentUser
  87 + const normalized = remoteImages.map(img => {
  88 + const isLiked = img.likedBy && currentUser
  89 + ? img.likedBy.includes(currentUser.employeeId)
  90 + : false;
  91 + return {
  92 + ...img,
  93 + likes: img.likes, // Trust server
  94 + isLikedByCurrentUser: isLiked,
  95 + };
  96 + });
98 97
99 if (normalized.length >= MIN_GALLERY_ITEMS) { 98 if (normalized.length >= MIN_GALLERY_ITEMS) {
100 return normalized; 99 return normalized;
@@ -110,17 +109,9 @@ const App: React.FC = () => { @@ -110,17 +109,9 @@ const App: React.FC = () => {
110 }); 109 });
111 } catch (err) { 110 } catch (err) {
112 console.error("Failed to sync gallery", err); 111 console.error("Failed to sync gallery", err);
113 - setImages(prev => {  
114 - if (prev.length >= MIN_GALLERY_ITEMS) return prev;  
115 - const existingIds = new Set(prev.map(img => img.id));  
116 - const filler = SHOWCASE_IMAGES.filter(img => !existingIds.has(img.id)).slice(  
117 - 0,  
118 - Math.max(0, MIN_GALLERY_ITEMS - prev.length)  
119 - );  
120 - return [...prev, ...filler];  
121 - }); 112 + // Keep previous state if sync fails
122 } 113 }
123 - }, []); 114 + }, [currentUser]);
124 115
125 useEffect(() => { 116 useEffect(() => {
126 syncGallery(); 117 syncGallery();
@@ -170,11 +161,14 @@ const App: React.FC = () => { @@ -170,11 +161,14 @@ const App: React.FC = () => {
170 } 161 }
171 }; 162 };
172 163
173 - const handleLike = (image: ImageItem) => { 164 + const handleLike = async (image: ImageItem) => {
174 if (!currentUser) { 165 if (!currentUser) {
175 setIsAuthModalOpen(true); 166 setIsAuthModalOpen(true);
176 return; 167 return;
177 } 168 }
  169 +
  170 + // Optimistic update
  171 + const previousImages = [...images];
178 setImages(prev => prev.map(img => { 172 setImages(prev => prev.map(img => {
179 if (img.id === image.id) { 173 if (img.id === image.id) {
180 const isLiked = !!img.isLikedByCurrentUser; 174 const isLiked = !!img.isLikedByCurrentUser;
@@ -186,6 +180,14 @@ const App: React.FC = () => { @@ -186,6 +180,14 @@ const App: React.FC = () => {
186 } 180 }
187 return img; 181 return img;
188 })); 182 }));
  183 +
  184 + try {
  185 + await toggleLike(image.id, currentUser.employeeId);
  186 + } catch (e) {
  187 + console.error("Like failed", e);
  188 + setImages(previousImages); // Revert
  189 + alert("操作失败");
  190 + }
189 }; 191 };
190 192
191 const handleGenerateSimilar = (params: ImageGenerationParams) => { 193 const handleGenerateSimilar = (params: ImageGenerationParams) => {
@@ -20,3 +20,19 @@ export const fetchGallery = async (authorId?: string): Promise<ImageItem[]> => { @@ -20,3 +20,19 @@ export const fetchGallery = async (authorId?: string): Promise<ImageItem[]> => {
20 const data: GalleryResponse = await response.json(); 20 const data: GalleryResponse = await response.json();
21 return data.images ?? []; 21 return data.images ?? [];
22 }; 22 };
  23 +
  24 +export const toggleLike = async (imageId: string, userId: string): Promise<ImageItem> => {
  25 + if (API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL) {
  26 + throw new Error("Cannot like images in direct mode");
  27 + }
  28 + const response = await fetch(`${API_BASE_URL}/likes/${imageId}?userId=${userId}`, {
  29 + method: 'POST',
  30 + });
  31 +
  32 + if (!response.ok) {
  33 + const errorText = await response.text();
  34 + throw new Error(`Like failed (${response.status}): ${errorText}`);
  35 + }
  36 +
  37 + return await response.json();
  38 +};
@@ -17,6 +17,7 @@ export interface ImageItem extends ImageGenerationParams { @@ -17,6 +17,7 @@ export interface ImageItem extends ImageGenerationParams {
17 // New fields for community features 17 // New fields for community features
18 authorId?: string; // The 8-digit employee ID 18 authorId?: string; // The 8-digit employee ID
19 likes: number; 19 likes: number;
  20 + likedBy?: string[]; // List of user IDs who liked this image
20 isLikedByCurrentUser?: boolean; // UI state 21 isLikedByCurrentUser?: boolean; // UI state
21 22
22 isMock?: boolean; // True if it's a static showcase image 23 isMock?: boolean; // True if it's a static showcase image