257 lines
13 KiB
TypeScript
257 lines
13 KiB
TypeScript
'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>
|
||
);
|
||
}
|