fix: resolve camera settings click and subscription API errors

- Camera settings: Use UUID `id` instead of optional `deviceId` for camera selection
- Camera settings: Only fetch history when deviceId exists
- Camera settings: Fix location display for JSONB location object
- Subscription: Safely handle empty JSON response when user has no subscription

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
grabbit 2025-12-21 03:25:03 +08:00
parent 95353ae2d7
commit 1c272140d9
2 changed files with 33 additions and 24 deletions

View File

@ -46,14 +46,15 @@ export default function CameraSettingsPage() {
try { try {
setLoading(true); setLoading(true);
// 获取相机详细信息 // 使用 id 查找相机
const camera = cameras.find(c => c.deviceId === cameraId); const camera = cameras.find(c => c.id === cameraId);
if (camera) { if (camera) {
setCameraDetails(camera); setCameraDetails(camera);
setSelectedCamera(cameraId); setSelectedCamera(cameraId);
// 获取历史数据 // 只有当 deviceId 存在时才获取历史数据
const historyData = await cameraService.getCameraHistory(cameraId); if (camera.deviceId) {
const historyData = await cameraService.getCameraHistory(camera.deviceId);
const formattedHistory: CameraHistoryRecord[] = historyData.map(item => ({ const formattedHistory: CameraHistoryRecord[] = historyData.map(item => ({
date: new Date(item.time).toLocaleString(), date: new Date(item.time).toLocaleString(),
status: 'active' as const, // API doesn't provide status in history, defaulting to active status: 'active' as const, // API doesn't provide status in history, defaulting to active
@ -63,8 +64,10 @@ export default function CameraSettingsPage() {
exposureCount: undefined, exposureCount: undefined,
uptime: undefined uptime: undefined
})); }));
setCameraHistory(formattedHistory); setCameraHistory(formattedHistory);
} else {
setCameraHistory([]);
}
} }
} catch (error) { } catch (error) {
console.error('获取相机详细信息失败:', error); console.error('获取相机详细信息失败:', error);
@ -160,7 +163,7 @@ export default function CameraSettingsPage() {
<div <div
key={camera.id} key={camera.id}
className="bg-white dark:bg-gray-800 rounded-lg p-4 sm:p-5 border border-gray-200 dark:border-gray-700 cursor-pointer hover:shadow-lg transition-all duration-200 hover:scale-105 min-w-[280px] w-full" className="bg-white dark:bg-gray-800 rounded-lg p-4 sm:p-5 border border-gray-200 dark:border-gray-700 cursor-pointer hover:shadow-lg transition-all duration-200 hover:scale-105 min-w-[280px] w-full"
onClick={() => handleSelectCamera(camera.deviceId)} onClick={() => handleSelectCamera(camera.id)}
> >
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center min-w-0 flex-1"> <div className="flex items-center min-w-0 flex-1">
@ -175,7 +178,7 @@ export default function CameraSettingsPage() {
<div className="space-y-2.5"> <div className="space-y-2.5">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-500 dark:text-gray-400 flex-shrink-0">:</span> <span className="text-sm text-gray-500 dark:text-gray-400 flex-shrink-0">:</span>
<span className="text-sm font-medium text-gray-900 dark:text-white truncate ml-2">{camera.location}</span> <span className="text-sm font-medium text-gray-900 dark:text-white truncate ml-2">{camera.location?.site_name || camera.legacyLocation || '未知'}</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-gray-500 dark:text-gray-400 flex-shrink-0">:</span> <span className="text-sm text-gray-500 dark:text-gray-400 flex-shrink-0">:</span>
@ -233,7 +236,7 @@ export default function CameraSettingsPage() {
<Monitor className="text-blue-500 mr-3" size={32} /> <Monitor className="text-blue-500 mr-3" size={32} />
<div> <div>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">{cameraDetails.name}</h3> <h3 className="text-2xl font-bold text-gray-900 dark:text-white">{cameraDetails.name}</h3>
<p className="text-gray-500 dark:text-gray-400">{cameraDetails.location}</p> <p className="text-gray-500 dark:text-gray-400">{cameraDetails.location?.site_name || cameraDetails.legacyLocation || '未知'}</p>
</div> </div>
</div> </div>
<span className={`px-3 py-2 rounded-full text-sm font-medium ${getStatusColor(cameraDetails.status)}`}> <span className={`px-3 py-2 rounded-full text-sm font-medium ${getStatusColor(cameraDetails.status)}`}>

View File

@ -32,7 +32,7 @@ export interface CheckoutSessionResponse {
// New interfaces for the enhanced subscription API // New interfaces for the enhanced subscription API
export interface SubscriptionPlan { export interface SubscriptionPlan {
id: number; id: string;
planId: string; planId: string;
name: string; name: string;
description?: string; description?: string;
@ -49,9 +49,9 @@ export interface SubscriptionPlan {
} }
export interface UserSubscription { export interface UserSubscription {
id: number; id: string;
userProfileId: string; userProfileId: string;
subscriptionPlanId: number; subscriptionPlanId: string;
subscriptionPlan: SubscriptionPlan; subscriptionPlan: SubscriptionPlan;
stripeSubscriptionId?: string; stripeSubscriptionId?: string;
status: 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete'; status: 'active' | 'canceled' | 'past_due' | 'trialing' | 'incomplete';
@ -213,7 +213,13 @@ export const subscriptionApi = {
throw new Error(`Failed to fetch user subscription: ${response.statusText}`) throw new Error(`Failed to fetch user subscription: ${response.statusText}`)
} }
return response.json() // Safely handle empty response body
const text = await response.text()
if (!text || text.trim() === '' || text === 'null') {
return null
}
return JSON.parse(text)
} catch (error) { } catch (error) {
console.error('Error fetching user subscription:', error) console.error('Error fetching user subscription:', error)
return null return null