ly0303521

添加视频生成限制次数,通过集赞给予视频生成次数奖励

@@ -3,6 +3,7 @@ import json @@ -3,6 +3,7 @@ import json
3 import os 3 import os
4 import secrets 4 import secrets
5 import time 5 import time
  6 +import fcntl
6 from pathlib import Path 7 from pathlib import Path
7 from threading import Lock, RLock 8 from threading import Lock, RLock
8 from typing import List, Literal, Optional 9 from typing import List, Literal, Optional
@@ -19,8 +20,60 @@ Z_IMAGE_BASE_URL = os.getenv("Z_IMAGE_BASE_URL", "http://106.120.52.146:39009"). @@ -19,8 +20,60 @@ Z_IMAGE_BASE_URL = os.getenv("Z_IMAGE_BASE_URL", "http://106.120.52.146:39009").
19 REQUEST_TIMEOUT_SECONDS = float(os.getenv("REQUEST_TIMEOUT_SECONDS", "120")) 20 REQUEST_TIMEOUT_SECONDS = float(os.getenv("REQUEST_TIMEOUT_SECONDS", "120"))
20 GALLERY_IMAGES_PATH = Path(os.getenv("GALLERY_IMAGES_PATH", Path(__file__).with_name("gallery_images.json"))) 21 GALLERY_IMAGES_PATH = Path(os.getenv("GALLERY_IMAGES_PATH", Path(__file__).with_name("gallery_images.json")))
21 GALLERY_VIDEOS_PATH = Path(os.getenv("GALLERY_VIDEOS_PATH", Path(__file__).with_name("gallery_videos.json"))) 22 GALLERY_VIDEOS_PATH = Path(os.getenv("GALLERY_VIDEOS_PATH", Path(__file__).with_name("gallery_videos.json")))
  23 +USAGE_PATH = Path(os.getenv("USAGE_PATH", Path(__file__).with_name("usage.json")))
22 GALLERY_MAX_ITEMS = int(os.getenv("GALLERY_MAX_ITEMS", "500")) 24 GALLERY_MAX_ITEMS = int(os.getenv("GALLERY_MAX_ITEMS", "500"))
23 WHITELIST_PATH = Path(os.getenv("WHITELIST_PATH", Path(__file__).with_name("whitelist.txt"))) 25 WHITELIST_PATH = Path(os.getenv("WHITELIST_PATH", Path(__file__).with_name("whitelist.txt")))
  26 +ADMIN_ID = "86427531"
  27 +
  28 +# --- Usage Store ---
  29 +class UsageStore:
  30 + def __init__(self, path: Path):
  31 + self.path = path
  32 + if not self.path.exists():
  33 + self._write({})
  34 +
  35 + def _read(self) -> dict:
  36 + try:
  37 + if not self.path.exists(): return {}
  38 + with self.path.open("r", encoding="utf-8") as f:
  39 + return json.load(f)
  40 + except (FileNotFoundError, json.JSONDecodeError):
  41 + return {}
  42 +
  43 + def _write(self, data: dict):
  44 + try:
  45 + payload = json.dumps(data, ensure_ascii=False, indent=2)
  46 + temp_path = self.path.with_suffix(".tmp_proxy")
  47 + with temp_path.open("w", encoding="utf-8") as f:
  48 + f.write(payload)
  49 + temp_path.replace(self.path)
  50 + except Exception as e:
  51 + logger.error(f"Failed to write usage: {e}")
  52 +
  53 + def get_usage(self, user_id: str) -> dict:
  54 + data = self._read()
  55 + import datetime
  56 + today = datetime.date.today().isoformat()
  57 + user_data = data.get(user_id, {"daily_used": 0, "bonus_count": 0, "last_reset": today})
  58 +
  59 + if user_data.get("last_reset") != today:
  60 + user_data["daily_used"] = 0
  61 + user_data["last_reset"] = today
  62 + return user_data
  63 +
  64 + def update_bonus(self, user_id: str, delta: int):
  65 + data = self._read()
  66 + import datetime
  67 + today = datetime.date.today().isoformat()
  68 + user_data = data.get(user_id, {"daily_used": 0, "bonus_count": 0, "last_reset": today})
  69 +
  70 + if user_data.get("last_reset") != today:
  71 + user_data["daily_used"] = 0
  72 + user_data["last_reset"] = today
  73 +
  74 + user_data["bonus_count"] = max(0, user_data.get("bonus_count", 0) + delta)
  75 + data[user_id] = user_data
  76 + self._write(data)
24 77
25 # --- Pydantic Models --- 78 # --- Pydantic Models ---
26 # Define dependent models first to avoid forward reference issues. 79 # Define dependent models first to avoid forward reference issues.
@@ -163,17 +216,20 @@ class JsonStore: @@ -163,17 +216,20 @@ class JsonStore:
163 image_store = JsonStore(GALLERY_IMAGES_PATH, item_key="images", max_items=GALLERY_MAX_ITEMS) 216 image_store = JsonStore(GALLERY_IMAGES_PATH, item_key="images", max_items=GALLERY_MAX_ITEMS)
164 video_store = JsonStore(GALLERY_VIDEOS_PATH, item_key="videos", max_items=GALLERY_MAX_ITEMS) 217 video_store = JsonStore(GALLERY_VIDEOS_PATH, item_key="videos", max_items=GALLERY_MAX_ITEMS)
165 whitelist_store = WhitelistStore(WHITELIST_PATH) 218 whitelist_store = WhitelistStore(WHITELIST_PATH)
  219 +usage_store = UsageStore(USAGE_PATH)
166 220
167 # --- App Setup --- 221 # --- App Setup ---
168 app = FastAPI(title="Z-Image Proxy", version="1.0.0") 222 app = FastAPI(title="Z-Image Proxy", version="1.0.0")
169 app.add_middleware( 223 app.add_middleware(
170 CORSMiddleware, 224 CORSMiddleware,
171 - allow_origins=["http://106.120.52.146:37001"], # Explicitly allow the frontend origin 225 + allow_origins=["*"],
172 allow_credentials=True, 226 allow_credentials=True,
173 allow_methods=["*"], 227 allow_methods=["*"],
174 allow_headers=["*"], 228 allow_headers=["*"],
  229 + expose_headers=["*"]
175 ) 230 )
176 @app.on_event("startup") 231 @app.on_event("startup")
  232 +
177 async def startup(): app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(REQUEST_TIMEOUT_SECONDS, connect=5.0)) 233 async def startup(): app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(REQUEST_TIMEOUT_SECONDS, connect=5.0))
178 @app.on_event("shutdown") 234 @app.on_event("shutdown")
179 async def shutdown(): await app.state.http.aclose() 235 async def shutdown(): await app.state.http.aclose()
@@ -188,12 +244,52 @@ async def login(user_id: str = Query(..., alias="userId")): @@ -188,12 +244,52 @@ async def login(user_id: str = Query(..., alias="userId")):
188 244
189 @app.post("/likes/{item_id}") 245 @app.post("/likes/{item_id}")
190 async def toggle_like(item_id: str, user_id: str = Query(..., alias="userId")): 246 async def toggle_like(item_id: str, user_id: str = Query(..., alias="userId")):
  247 + is_liked_before = False
  248 + items = image_store.list_items()
  249 + target_item = next((i for i in items if i.get("id") == item_id), None)
  250 + if target_item:
  251 + is_liked_before = user_id in target_item.get("likedBy", [])
  252 +
191 updated_item = image_store.toggle_like(item_id, user_id) 253 updated_item = image_store.toggle_like(item_id, user_id)
  254 + if updated_item:
  255 + is_liked_after = user_id in updated_item.get("likedBy", [])
  256 + if is_liked_after and not is_liked_before:
  257 + usage_store.update_bonus(user_id, 1)
  258 + elif not is_liked_after and is_liked_before:
  259 + usage_store.update_bonus(user_id, -1)
  260 + return updated_item
  261 +
  262 + updated_item = video_store.toggle_like(item_id, user_id)
192 if updated_item: return updated_item 263 if updated_item: return updated_item
  264 + raise HTTPException(status_code=404, detail="Item not found")
  265 +
193 updated_item = video_store.toggle_like(item_id, user_id) 266 updated_item = video_store.toggle_like(item_id, user_id)
194 if updated_item: return updated_item 267 if updated_item: return updated_item
195 raise HTTPException(status_code=404, detail="Item not found") 268 raise HTTPException(status_code=404, detail="Item not found")
196 269
  270 +@app.get("/usage/{user_id}")
  271 +async def get_user_usage(user_id: str):
  272 + try:
  273 + usage = usage_store.get_usage(user_id)
  274 + is_admin = user_id == ADMIN_ID
  275 + remaining = (2 - usage["daily_used"]) + usage["bonus_count"] if not is_admin else 999999
  276 + return {
  277 + "daily_used": usage["daily_used"],
  278 + "bonus_count": usage["bonus_count"],
  279 + "base_limit": 2,
  280 + "remaining": max(0, remaining),
  281 + "is_admin": is_admin
  282 + }
  283 + except Exception as e:
  284 + logger.error(f"Error getting usage for {user_id}: {e}")
  285 + return {
  286 + "daily_used": 0,
  287 + "bonus_count": 0,
  288 + "base_limit": 2,
  289 + "remaining": 2,
  290 + "is_admin": user_id == ADMIN_ID
  291 + }
  292 +
197 @app.get("/gallery/images") 293 @app.get("/gallery/images")
198 async def gallery_images(limit: int = Query(200, ge=1, le=1000), author_id: Optional[str] = Query(None, alias="authorId")): 294 async def gallery_images(limit: int = Query(200, ge=1, le=1000), author_id: Optional[str] = Query(None, alias="authorId")):
199 items = image_store.list_items() 295 items = image_store.list_items()
  1 +{
  2 + "10773758": {
  3 + "daily_used": 4,
  4 + "bonus_count": 7,
  5 + "last_reset": "2026-01-20"
  6 + },
  7 + "11110000": {
  8 + "daily_used": 2,
  9 + "bonus_count": 0,
  10 + "last_reset": "2026-01-20"
  11 + }
  12 +}
1 import React, { useState, useEffect, useMemo, useCallback } from 'react'; 1 import React, { useState, useEffect, useMemo, useCallback } from 'react';
2 -import { ImageItem, ImageGenerationParams, UserProfile, VideoStatus } from './types'; 2 +import { ImageItem, ImageGenerationParams, UserProfile, VideoStatus, UserUsage } from './types';
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, deleteVideo } from './services/galleryService'; 6 +import { fetchGallery, fetchVideoGallery, saveVideo, toggleLike, deleteVideo, fetchUsage } 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';
@@ -29,8 +29,10 @@ const App: React.FC = () => { @@ -29,8 +29,10 @@ const App: React.FC = () => {
29 const [images, setImages] = useState<ImageItem[]>(SHOWCASE_IMAGES); 29 const [images, setImages] = useState<ImageItem[]>(SHOWCASE_IMAGES);
30 const [videos, setVideos] = useState<ImageItem[]>([]); 30 const [videos, setVideos] = useState<ImageItem[]>([]);
31 const [isGenerating, setIsGenerating] = useState(false); 31 const [isGenerating, setIsGenerating] = useState(false);
  32 + const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
32 const [videoStatus, setVideoStatus] = useState<VideoStatus | null>(null); 33 const [videoStatus, setVideoStatus] = useState<VideoStatus | null>(null);
33 const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null); 34 const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
  35 + const [userUsage, setUserUsage] = useState<UserUsage | null>(null);
34 const [error, setError] = useState<string | null>(null); 36 const [error, setError] = useState<string | null>(null);
35 const [incomingParams, setIncomingParams] = useState<ImageGenerationParams | null>(null); 37 const [incomingParams, setIncomingParams] = useState<ImageGenerationParams | null>(null);
36 const [isAdminModalOpen, setIsAdminModalOpen] = useState(false); 38 const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
@@ -38,7 +40,6 @@ const App: React.FC = () => { @@ -38,7 +40,6 @@ const App: React.FC = () => {
38 const [editingImage, setEditingImage] = useState<ImageItem | null>(null); 40 const [editingImage, setEditingImage] = useState<ImageItem | null>(null);
39 41
40 const isAdmin = currentUser?.employeeId === ADMIN_ID; 42 const isAdmin = currentUser?.employeeId === ADMIN_ID;
41 - const isGeneratingVideo = videoStatus !== null;  
42 43
43 const sortedImages = useMemo(() => [...images].sort((a, b) => (b.likes || 0) - (a.likes || 0)), [images]); 44 const sortedImages = useMemo(() => [...images].sort((a, b) => (b.likes || 0) - (a.likes || 0)), [images]);
44 const sortedVideos = useMemo(() => [...videos].sort((a, b) => b.createdAt - a.createdAt), [videos]); 45 const sortedVideos = useMemo(() => [...videos].sort((a, b) => b.createdAt - a.createdAt), [videos]);
@@ -81,15 +82,25 @@ const App: React.FC = () => { @@ -81,15 +82,25 @@ const App: React.FC = () => {
81 } catch (err) { console.error("Failed to sync video gallery", err); } 82 } catch (err) { console.error("Failed to sync video gallery", err); }
82 }, [currentUser]); 83 }, [currentUser]);
83 84
  85 + const syncUsage = useCallback(async () => {
  86 + if (!currentUser) return;
  87 + try {
  88 + const usage = await fetchUsage(currentUser.employeeId);
  89 + setUserUsage(usage);
  90 + } catch (err) { console.error("Failed to sync usage", err); }
  91 + }, [currentUser]);
  92 +
84 useEffect(() => { 93 useEffect(() => {
85 syncImageGallery(); 94 syncImageGallery();
86 syncVideoGallery(); 95 syncVideoGallery();
  96 + syncUsage();
87 const interval = setInterval(() => { 97 const interval = setInterval(() => {
88 syncImageGallery(); 98 syncImageGallery();
89 syncVideoGallery(); 99 syncVideoGallery();
  100 + syncUsage();
90 }, 30000); 101 }, 30000);
91 return () => clearInterval(interval); 102 return () => clearInterval(interval);
92 - }, [syncImageGallery, syncVideoGallery]); 103 + }, [syncImageGallery, syncVideoGallery, syncUsage]);
93 104
94 105
95 // --- Handlers --- 106 // --- Handlers ---
@@ -110,7 +121,14 @@ const App: React.FC = () => { @@ -110,7 +121,14 @@ const App: React.FC = () => {
110 121
111 const handleGenerateVideo = async (params: ImageGenerationParams, imageFile: File) => { 122 const handleGenerateVideo = async (params: ImageGenerationParams, imageFile: File) => {
112 if (!currentUser) { setIsAuthModalOpen(true); return; } 123 if (!currentUser) { setIsAuthModalOpen(true); return; }
  124 +
  125 + if (!isAdmin && userUsage && userUsage.remaining <= 0) {
  126 + alert("今日剩余次数不足,请前往灵感图库点赞获取生成次数");
  127 + return;
  128 + }
  129 +
113 setError(null); 130 setError(null);
  131 + setIsGeneratingVideo(true);
114 setVideoStatus({ status: 'submitting', message: '提交中...', task_id: 'temp' }); 132 setVideoStatus({ status: 'submitting', message: '提交中...', task_id: 'temp' });
115 133
116 try { 134 try {
@@ -137,12 +155,17 @@ const App: React.FC = () => { @@ -137,12 +155,17 @@ const App: React.FC = () => {
137 155
138 const savedVideo = await saveVideo(newVideoData); 156 const savedVideo = await saveVideo(newVideoData);
139 setVideos(prev => [savedVideo, ...prev]); 157 setVideos(prev => [savedVideo, ...prev]);
  158 + syncUsage();
140 159
141 - setTimeout(() => setVideoStatus(null), 3000); 160 + setTimeout(() => {
  161 + setVideoStatus(null);
  162 + setIsGeneratingVideo(false);
  163 + }, 3000);
142 } catch (err: any) { 164 } catch (err: any) {
143 console.error(err); 165 console.error(err);
144 setError("视频生成失败。请确保视频生成服务正常运行。"); 166 setError("视频生成失败。请确保视频生成服务正常运行。");
145 setVideoStatus(null); 167 setVideoStatus(null);
  168 + setIsGeneratingVideo(false);
146 } 169 }
147 }; 170 };
148 171
@@ -185,6 +208,7 @@ const App: React.FC = () => { @@ -185,6 +208,7 @@ const App: React.FC = () => {
185 208
186 try { 209 try {
187 await toggleLike(item.id, currentUser.employeeId); 210 await toggleLike(item.id, currentUser.employeeId);
  211 + if (!isVideo) syncUsage();
188 } catch (e) { 212 } catch (e) {
189 console.error("Like failed", e); 213 console.error("Like failed", e);
190 if (isVideo) syncVideoGallery(); else syncImageGallery(); 214 if (isVideo) syncVideoGallery(); else syncImageGallery();
@@ -246,8 +270,15 @@ const App: React.FC = () => { @@ -246,8 +270,15 @@ const App: React.FC = () => {
246 <span className={`text-xs uppercase tracking-wider px-2 py-0.5 rounded font-bold ${isAdmin ? 'text-purple-600 bg-purple-50' : 'text-gray-400 bg-gray-50'}`}> 270 <span className={`text-xs uppercase tracking-wider px-2 py-0.5 rounded font-bold ${isAdmin ? 'text-purple-600 bg-purple-50' : 'text-gray-400 bg-gray-50'}`}>
247 {isAdmin ? 'Administrator' : '设计师'} 271 {isAdmin ? 'Administrator' : '设计师'}
248 </span> 272 </span>
  273 + <div className="flex items-center gap-2">
  274 + {userUsage && !isAdmin && (
  275 + <span className="text-[10px] bg-purple-100 text-purple-600 px-1.5 py-0.5 rounded font-bold">
  276 + 视频剩余: {userUsage.remaining}
  277 + </span>
  278 + )}
249 <span className="font-mono font-bold text-gray-800">{currentUser.employeeId}</span> 279 <span className="font-mono font-bold text-gray-800">{currentUser.employeeId}</span>
250 </div> 280 </div>
  281 + </div>
251 )} 282 )}
252 283
253 {isAdmin && ( 284 {isAdmin && (
@@ -283,7 +314,7 @@ const App: React.FC = () => { @@ -283,7 +314,7 @@ const App: React.FC = () => {
283 </main> 314 </main>
284 315
285 <HistoryBar images={galleryMode === GalleryMode.Image ? userHistory : userVideoHistory} onSelect={setSelectedImage} /> 316 <HistoryBar images={galleryMode === GalleryMode.Image ? userHistory : userVideoHistory} onSelect={setSelectedImage} />
286 - <InputBar onGenerate={handleGenerate} isGenerating={isGenerating || isGeneratingVideo} incomingParams={incomingParams} isVideoMode={galleryMode === GalleryMode.Video} videoStatus={videoStatus} /> 317 + <InputBar onGenerate={handleGenerate} isGenerating={isGenerating || isGeneratingVideo} incomingParams={incomingParams} isVideoMode={galleryMode === GalleryMode.Video} videoStatus={videoStatus} userUsage={userUsage} />
287 {selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar} onDelete={handleDeleteVideo} currentUser={currentUser} />} 318 {selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar} onDelete={handleDeleteVideo} currentUser={currentUser} />}
288 <AdminModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} onSave={handleSaveImage} onDelete={handleDeleteImage} initialData={editingImage} /> 319 <AdminModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} onSave={handleSaveImage} onDelete={handleDeleteImage} initialData={editingImage} />
289 <WhitelistModal isOpen={isWhitelistModalOpen} onClose={() => setIsWhitelistModalOpen(false)} /> 320 <WhitelistModal isOpen={isWhitelistModalOpen} onClose={() => setIsWhitelistModalOpen(false)} />
1 import React, { useState, useEffect, KeyboardEvent, useRef } from 'react'; 1 import React, { useState, useEffect, KeyboardEvent, useRef } from 'react';
2 import { Loader2, ArrowUp, Sliders, Dices, X, RefreshCw, Image as ImageIcon, Hourglass } from 'lucide-react'; 2 import { Loader2, ArrowUp, Sliders, Dices, X, RefreshCw, Image as ImageIcon, Hourglass } from 'lucide-react';
3 -import { ImageGenerationParams, VideoStatus } from '../types'; 3 +import { ImageGenerationParams, VideoStatus, UserUsage } from '../types';
4 4
5 interface InputBarProps { 5 interface InputBarProps {
6 onGenerate: (params: ImageGenerationParams, imageFile?: File) => void; 6 onGenerate: (params: ImageGenerationParams, imageFile?: File) => void;
7 isGenerating: boolean; 7 isGenerating: boolean;
8 incomingParams?: ImageGenerationParams | null; 8 incomingParams?: ImageGenerationParams | null;
9 isVideoMode: boolean; 9 isVideoMode: boolean;
10 - videoStatus?: VideoStatus | null; // New prop 10 + videoStatus?: VideoStatus | null;
  11 + userUsage?: UserUsage | null;
11 } 12 }
12 13
13 const ASPECT_RATIOS = [ 14 const ASPECT_RATIOS = [
@@ -19,7 +20,7 @@ const ASPECT_RATIOS = [ @@ -19,7 +20,7 @@ const ASPECT_RATIOS = [
19 { label: 'Custom', w: 0, h: 0 }, 20 { label: 'Custom', w: 0, h: 0 },
20 ]; 21 ];
21 22
22 -const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingParams, isVideoMode, videoStatus }) => { 23 +const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingParams, isVideoMode, videoStatus, userUsage }) => {
23 const [prompt, setPrompt] = useState(''); 24 const [prompt, setPrompt] = useState('');
24 const [showSettings, setShowSettings] = useState(false); 25 const [showSettings, setShowSettings] = useState(false);
25 const [isSubmittingLocal, setIsSubmittingLocal] = useState(false); // New state for local submission status 26 const [isSubmittingLocal, setIsSubmittingLocal] = useState(false); // New state for local submission status
@@ -252,7 +253,11 @@ const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingP @@ -252,7 +253,11 @@ const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingP
252 <div className="pointer-events-auto w-full max-w-2xl transition-all duration-300 ease-in-out transform hover:scale-[1.01]"> 253 <div className="pointer-events-auto w-full max-w-2xl transition-all duration-300 ease-in-out transform hover:scale-[1.01]">
253 {videoStatus && ( // Display video status above the input bar 254 {videoStatus && ( // Display video status above the input bar
254 <div className="absolute -top-16 left-1/2 -translate-x-1/2 bg-white/90 dark:bg-gray-800/90 backdrop-blur-md px-4 py-2 rounded-xl shadow-lg flex items-center gap-2 z-10 animate-fade-in-up"> 255 <div className="absolute -top-16 left-1/2 -translate-x-1/2 bg-white/90 dark:bg-gray-800/90 backdrop-blur-md px-4 py-2 rounded-xl shadow-lg flex items-center gap-2 z-10 animate-fade-in-up">
  256 + {videoStatus.status === 'complete' ? (
  257 + <div className="text-green-500 font-bold">✓</div>
  258 + ) : (
255 <Hourglass size={20} className="text-purple-500 animate-spin" /> 259 <Hourglass size={20} className="text-purple-500 animate-spin" />
  260 + )}
256 <span className="text-gray-800 dark:text-white font-medium text-sm"> 261 <span className="text-gray-800 dark:text-white font-medium text-sm">
257 {videoStatus.status === 'submitting' && '提交请求中...'} 262 {videoStatus.status === 'submitting' && '提交请求中...'}
258 {videoStatus.status === 'queued' && `排队中... (第 ${videoStatus.queue_position || '?'} 位)`} 263 {videoStatus.status === 'queued' && `排队中... (第 ${videoStatus.queue_position || '?'} 位)`}
@@ -262,6 +267,12 @@ const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingP @@ -262,6 +267,12 @@ const InputBar: React.FC<InputBarProps> = ({ onGenerate, isGenerating, incomingP
262 </div> 267 </div>
263 )} 268 )}
264 <div className="relative group"> 269 <div className="relative group">
  270 + {isVideoMode && userUsage && !userUsage.is_admin && (
  271 + <div className="absolute -top-8 right-6 bg-purple-600/90 backdrop-blur-md text-white text-[10px] px-2 py-1 rounded-lg font-bold shadow-lg animate-fade-in flex items-center gap-1 border border-white/20">
  272 + <RefreshCw size={10} className="animate-spin-slow" />
  273 + 今日剩余次数: {userUsage.remaining}
  274 + </div>
  275 + )}
265 <div className="absolute inset-0 bg-white/80 dark:bg-black/80 backdrop-blur-2xl rounded-full shadow-2xl border border-white/20 dark:border-white/10" /> 276 <div className="absolute inset-0 bg-white/80 dark:bg-black/80 backdrop-blur-2xl rounded-full shadow-2xl border border-white/20 dark:border-white/10" />
266 277
267 <div className="relative flex items-center p-2 pr-2"> 278 <div className="relative flex items-center p-2 pr-2">
@@ -11,13 +11,12 @@ export const VIDEO_OSS_BASE_URL = "http://106.120.52.146:39997"; @@ -11,13 +11,12 @@ export const VIDEO_OSS_BASE_URL = "http://106.120.52.146:39997";
11 const ENV_PROXY_URL = import.meta.env?.VITE_API_BASE_URL?.trim(); 11 const ENV_PROXY_URL = import.meta.env?.VITE_API_BASE_URL?.trim();
12 const DEFAULT_PROXY_URL = ENV_PROXY_URL && ENV_PROXY_URL.length > 0 12 const DEFAULT_PROXY_URL = ENV_PROXY_URL && ENV_PROXY_URL.length > 0
13 ? ENV_PROXY_URL 13 ? ENV_PROXY_URL
14 - : "http://localhost:9009"; 14 + : "http://106.120.52.146:37000";
15 15
16 export const API_BASE_URL = DEFAULT_PROXY_URL; 16 export const API_BASE_URL = DEFAULT_PROXY_URL;
17 17
18 -export const API_ENDPOINTS = API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL  
19 - ? [Z_IMAGE_DIRECT_BASE_URL]  
20 - : [API_BASE_URL, Z_IMAGE_DIRECT_BASE_URL]; 18 +export const API_ENDPOINTS = [API_BASE_URL, Z_IMAGE_DIRECT_BASE_URL];
  19 +
21 20
22 // This is the specific Administrator ID requested 21 // This is the specific Administrator ID requested
23 export const ADMIN_ID = '86427531'; 22 export const ADMIN_ID = '86427531';
1 import { API_BASE_URL, Z_IMAGE_DIRECT_BASE_URL } from '../constants'; 1 import { API_BASE_URL, Z_IMAGE_DIRECT_BASE_URL } from '../constants';
2 -import { GalleryResponse, ImageItem } from '../types'; 2 +import { GalleryResponse, ImageItem, UserUsage } from '../types';
  3 +
  4 +export const fetchUsage = async (userId: string): Promise<UserUsage> => {
  5 + const response = await fetch(`${API_BASE_URL}/usage/${userId}`);
  6 + if (!response.ok) {
  7 + throw new Error('Failed to fetch usage');
  8 + }
  9 + return await response.json();
  10 +};
3 11
4 export const fetchGallery = async (authorId?: string): Promise<ImageItem[]> => { 12 export const fetchGallery = async (authorId?: string): Promise<ImageItem[]> => {
5 if (API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL) { 13 if (API_BASE_URL === Z_IMAGE_DIRECT_BASE_URL) {
@@ -50,3 +50,12 @@ export interface VideoStatus { @@ -50,3 +50,12 @@ export interface VideoStatus {
50 processing_time?: number; 50 processing_time?: number;
51 video_filename?: string; // The final filename of the video 51 video_filename?: string; // The final filename of the video
52 } 52 }
  53 +
  54 +export interface UserUsage {
  55 + daily_used: number;
  56 + bonus_count: number;
  57 + base_limit: number;
  58 + total_limit: number;
  59 + remaining: number;
  60 + is_admin: boolean;
  61 +}