Files
mqtt_power/charging_web_app/src/app/(authenticated)/charging-status/page.tsx

257 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import LoadingSpinner from '@/components/LoadingSpinner';
import { api, BaseResponse } from '@/utils/axios';
import { FiZap, FiClock, FiAlertCircle, FiCheckCircle, FiPower, FiArrowLeft, FiInfo } from 'react-icons/fi';
// 与 dashboard/page.tsx 类似的活动会话接口可能需要根据后端VO调整
interface ActiveChargingSession {
id: number;
spotUidSnapshot: string | null;
robotUidSnapshot?: string | null;
chargeStartTime: string | null; // ISO String Date
status: string;
// 根据需要可以从后端 ChargingSessionVO 添加更多字段
// e.g., energyConsumedKwh, cost (though these might be final values)
}
const POLLING_INTERVAL = 5000; // 5 seconds for polling
// 定义终止状态常量,用于组件内多处使用
const TERMINAL_STATUSES = ['COMPLETED', 'PAID', 'CANCELLED_BY_USER', 'CANCELLED_BY_SYSTEM', 'ERROR', 'ROBOT_TASK_TIMEOUT'];
export default function ChargingStatusPage() {
const { isAuthenticated, isLoading: authLoading } = useAuth();
const router = useRouter();
const [session, setSession] = useState<ActiveChargingSession | null | undefined>(undefined);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isStopping, setIsStopping] = useState(false);
const [elapsedTime, setElapsedTime] = useState("00:00:00");
const fetchActiveSession = useCallback(async () => {
if (!isAuthenticated) return;
try {
const response = await api.get<BaseResponse<ActiveChargingSession | null>>('/session/my/active');
console.log('Active charging session response:', response.data);
if (response.data && response.data.code === 0) {
const activeSessionData = response.data.data;
setSession(activeSessionData);
setError(null);
if (activeSessionData) {
console.log('Current session status:', activeSessionData.status);
if (TERMINAL_STATUSES.includes(activeSessionData.status)) {
setTimeout(() => router.push('/dashboard'), 3000);
}
} else {
console.log('No active charging session found');
}
} else {
setError(response.data?.message || '获取充电状态失败');
}
} catch (err: any) {
console.error("Error fetching active session:", err);
setError(err.response?.data?.message || err.message || '网络错误,无法获取充电状态');
} finally {
setIsLoading(false);
}
}, [isAuthenticated, router]);
useEffect(() => {
if (!authLoading && !isAuthenticated) {
router.replace('/login');
return;
}
if (isAuthenticated) {
setIsLoading(true);
fetchActiveSession();
}
}, [authLoading, isAuthenticated, router, fetchActiveSession]);
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isAuthenticated && session && !isLoading) {
if (session && !TERMINAL_STATUSES.includes(session.status)) {
intervalId = setInterval(fetchActiveSession, POLLING_INTERVAL);
}
}
return () => clearInterval(intervalId);
}, [isAuthenticated, session, isLoading, fetchActiveSession]);
const calculateDuration = useCallback((startTime: string | null): string => {
if (!startTime) return '00:00:00';
const start = new Date(startTime).getTime();
const now = Date.now();
if (now < start) return '00:00:00';
let diff = Math.floor((now - start) / 1000);
const hours = String(Math.floor(diff / 3600)).padStart(2, '0');
diff %= 3600;
const minutes = String(Math.floor(diff / 60)).padStart(2, '0');
const seconds = String(diff % 60).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}, []);
useEffect(() => {
let timerId: NodeJS.Timeout;
if (session && session.chargeStartTime && (session.status === 'CHARGING_STARTED' || session.status === 'CHARGING_IN_PROGRESS')) {
setElapsedTime(calculateDuration(session.chargeStartTime)); // Initial calculation
timerId = setInterval(() => {
setElapsedTime(calculateDuration(session.chargeStartTime));
}, 1000);
} else if (session && session.chargeStartTime) {
setElapsedTime(calculateDuration(session.chargeStartTime)); // Calculate final duration once if session ended
} else {
setElapsedTime('00:00:00');
}
return () => clearInterval(timerId);
}, [session, calculateDuration]);
const handleStopCharging = async () => {
if (!session || !session.id) {
setError("无法停止充电:无效的会话信息。");
return;
}
setIsStopping(true);
setError(null);
try {
const response = await api.post<BaseResponse<any>>(`/session/stop`, { sessionId: session.id });
if (response.data && response.data.code === 0) {
// Rely on polling to update the session state
// Optionally, trigger an immediate refresh:
fetchActiveSession();
} else {
setError(response.data?.message || "停止充电请求失败");
}
} catch (err: any) {
console.error("Error stopping charging session:", err);
setError(err.response?.data?.message || err.message || "停止充电时发生错误");
}
setIsStopping(false);
};
if (authLoading || (isLoading && session === undefined)) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <div className="p-4 text-center"></div>;
}
return (
<div className="min-h-screen bg-gray-100 p-4 md:p-8 flex flex-col items-center">
<div className="w-full max-w-2xl">
<header className="mb-8 flex items-center">
<Link href="/dashboard" className="text-indigo-600 hover:text-indigo-700 mr-4">
<FiArrowLeft size={24} />
</Link>
<h1 className="text-3xl font-bold text-gray-800"></h1>
</header>
{error && (
<div className="mb-6 p-4 bg-red-100 text-red-700 border border-red-300 rounded-md shadow-sm" role="alert">
<div className="flex items-center">
<FiAlertCircle className="mr-2" />
<p>{error}</p>
</div>
</div>
)}
{session === null && !error && (
<div className="bg-white p-8 rounded-xl shadow-xl text-center">
<FiInfo size={48} className="mx-auto text-blue-500 mb-4" />
<h2 className="text-2xl font-semibold text-gray-700 mb-2"></h2>
<p className="text-gray-600 mb-6"></p>
<Link href="/request-charging" className="inline-block bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
</Link>
</div>
)}
{session && (
<div className="bg-white p-6 md:p-8 rounded-xl shadow-xl">
<div className="mb-6 border-b pb-4 border-gray-200">
<h2 className="text-2xl font-semibold text-gray-700 mb-1">ID: {session.id}</h2>
<p className={`text-lg font-medium ${session.status === 'CHARGING_STARTED' || session.status === 'CHARGING_IN_PROGRESS' ? 'text-green-500' : 'text-gray-600'}`}>
: {session.status}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-lg font-medium text-gray-800">{session.spotUidSnapshot || 'N/A'}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-lg font-medium text-gray-800">{session.robotUidSnapshot || 'N/A'}</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-lg font-medium text-gray-800">
{session.chargeStartTime ? new Date(session.chargeStartTime).toLocaleString() : '等待开始'}
</p>
</div>
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-indigo-600 tabular-nums">
<FiClock className="inline mr-2 mb-1" />{elapsedTime}
</p>
</div>
</div>
<div className="mb-8">
<p className="text-sm text-gray-500 mb-1"> ()</p>
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
<div className={`bg-green-500 h-4 ${session.status === 'CHARGING_STARTED' || session.status === 'CHARGING_IN_PROGRESS' ? 'animate-pulse-fast' : ''}`} style={{ width: '100%' }}></div>
</div>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
{/* 根据会话状态显示不同的内容 */}
{(session.status === 'CHARGING_STARTED' || session.status === 'CHARGING_IN_PROGRESS' || session.status === 'ROBOT_ARRIVED' || session.status === 'ROBOT_ASSIGNED') ? (
// 显示停止充电按钮
<button
onClick={handleStopCharging}
disabled={isStopping}
className="w-full bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center"
>
{isStopping ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
...
</>
) : (
<><FiPower className="mr-2" /> </>
)}
</button>
) : (
// 会话状态不适合显示停止按钮,显示原因
<div className="mb-2 p-3 bg-yellow-50 border border-yellow-200 rounded-md text-sm text-yellow-700">
<strong>{session.status}</strong>
{TERMINAL_STATUSES.includes(session.status) && " 会话已结束或处于终止状态。"}
</div>
)}
{(session.status === 'COMPLETED' || session.status === 'PAID' || session.status === 'PAYMENT_PENDING') && (
<div className="mt-6 text-center p-4 bg-green-50 border-green-200 border rounded-md">
<FiCheckCircle size={28} className="text-green-500 mx-auto mb-2" />
<p className="text-green-700 font-semibold"></p>
<p className="text-sm text-gray-600">使</p>
<Link href="/my-sessions" className="mt-3 inline-block text-indigo-600 hover:text-indigo-800 font-medium">
</Link>
</div>
)}
</div>
)}
</div>
</div>
);
}