feat(remote): 添加 CORS 中间件支持文件跨域访问
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Clock, FileText, CheckSquare, Settings, Search, Trash2, BookOpen, Monitor, Code, GitBranch } from 'lucide-react'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts'
|
||||
import { getDayTimeData, getMonthTimeData, getYearTimeData } from './api'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, BarChart, Bar } from 'recharts'
|
||||
import { getDayTimeData, getWeekTimeData, getMonthTimeData, getYearTimeData } from './api'
|
||||
import type { WeekTimeData } from './api'
|
||||
import { WEEK_DAYS } from '@shared/constants'
|
||||
import type { DayTimeData, MonthTimeData, YearTimeData, TabType, TabSummary, DaySummary } from '@shared/types'
|
||||
import type { DayTimeData, MonthTimeData, YearTimeData, TabType, TabSummary, DaySummary } from '@shared/types/time'
|
||||
import { formatDurationShort, getWeekStart } from '@shared/utils/date'
|
||||
|
||||
type ViewMode = 'day' | 'week' | 'month' | 'year'
|
||||
import { ViewMode } from './types'
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
return formatDurationShort(seconds * 1000)
|
||||
@@ -65,9 +65,9 @@ const getTabColor = (tabType: TabType): string => {
|
||||
case 'remote':
|
||||
return 'text-indigo-500'
|
||||
case 'remote-desktop':
|
||||
return 'text-indigo-500'
|
||||
return 'text-blue-600'
|
||||
case 'remote-git':
|
||||
return 'text-orange-500'
|
||||
return 'text-teal-500'
|
||||
case 'pydemos':
|
||||
return 'text-yellow-500'
|
||||
default:
|
||||
@@ -156,18 +156,42 @@ const getHourlyUsageData = (sessions: Array<{ startTime: string; endTime?: strin
|
||||
const endTime = session.endTime ? new Date(session.endTime) : new Date()
|
||||
const startHour = startTime.getHours()
|
||||
const endHour = endTime.getHours()
|
||||
const startMinute = startTime.getMinutes()
|
||||
const startSecond = startTime.getSeconds()
|
||||
const endMinute = endTime.getMinutes()
|
||||
const endSecond = endTime.getSeconds()
|
||||
|
||||
for (let h = startHour; h <= endHour && h < 24; h++) {
|
||||
if (h === startHour && h === endHour) {
|
||||
const duration = Math.min(session.duration, 3600)
|
||||
hourlyMap[h] += duration
|
||||
} else if (h === startHour) {
|
||||
const minutes = 60 - startTime.getMinutes()
|
||||
hourlyMap[h] += minutes * 60
|
||||
} else if (h === endHour) {
|
||||
hourlyMap[h] += endTime.getMinutes() * 60 + endTime.getSeconds()
|
||||
} else {
|
||||
hourlyMap[h] += 3600
|
||||
if (startHour === endHour) {
|
||||
const duration = session.duration
|
||||
hourlyMap[startHour] += duration
|
||||
} else if (startHour < endHour) {
|
||||
for (let h = startHour; h <= endHour && h < 24; h++) {
|
||||
if (h === startHour) {
|
||||
const remainingMinutes = 60 - startMinute
|
||||
const remainingSeconds = remainingMinutes * 60 - startSecond
|
||||
hourlyMap[h] += remainingSeconds
|
||||
} else if (h === endHour) {
|
||||
hourlyMap[h] += endMinute * 60 + endSecond
|
||||
} else {
|
||||
hourlyMap[h] += 3600
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let h = startHour; h < 24; h++) {
|
||||
if (h === startHour) {
|
||||
const remainingMinutes = 60 - startMinute
|
||||
const remainingSeconds = remainingMinutes * 60 - startSecond
|
||||
hourlyMap[h] += remainingSeconds
|
||||
} else {
|
||||
hourlyMap[h] += 3600
|
||||
}
|
||||
}
|
||||
for (let h = 0; h <= endHour; h++) {
|
||||
if (h === endHour) {
|
||||
hourlyMap[h] += endMinute * 60 + endSecond
|
||||
} else {
|
||||
hourlyMap[h] += 3600
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +209,7 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('day')
|
||||
const [currentDate, setCurrentDate] = useState(new Date())
|
||||
const [dayData, setDayData] = useState<DayTimeData | null>(null)
|
||||
const [weekData, setWeekData] = useState<WeekTimeData | null>(null)
|
||||
const [monthData, setMonthData] = useState<MonthTimeData | null>(null)
|
||||
const [yearData, setYearData] = useState<YearTimeData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -200,6 +225,11 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
const dateStr = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
|
||||
const data = await getDayTimeData(dateStr)
|
||||
setDayData(data)
|
||||
} else if (viewMode === 'week') {
|
||||
const weekStart = getWeekStart(currentDate)
|
||||
const startDate = `${weekStart.getFullYear()}-${(weekStart.getMonth() + 1).toString().padStart(2, '0')}-${weekStart.getDate().toString().padStart(2, '0')}`
|
||||
const data = await getWeekTimeData(startDate)
|
||||
setWeekData(data)
|
||||
} else if (viewMode === 'month') {
|
||||
const data = await getMonthTimeData(`${year}-${month.toString().padStart(2, '0')}`)
|
||||
setMonthData(data)
|
||||
@@ -222,6 +252,8 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
const newDate = new Date(currentDate)
|
||||
if (viewMode === 'day') {
|
||||
newDate.setDate(newDate.getDate() - 1)
|
||||
} else if (viewMode === 'week') {
|
||||
newDate.setDate(newDate.getDate() - 7)
|
||||
} else if (viewMode === 'month') {
|
||||
newDate.setMonth(newDate.getMonth() - 1)
|
||||
} else if (viewMode === 'year') {
|
||||
@@ -234,6 +266,8 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
const newDate = new Date(currentDate)
|
||||
if (viewMode === 'day') {
|
||||
newDate.setDate(newDate.getDate() + 1)
|
||||
} else if (viewMode === 'week') {
|
||||
newDate.setDate(newDate.getDate() + 7)
|
||||
} else if (viewMode === 'month') {
|
||||
newDate.setMonth(newDate.getMonth() + 1)
|
||||
} else if (viewMode === 'year') {
|
||||
@@ -249,6 +283,19 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
|
||||
if (viewMode === 'day') {
|
||||
return `${year}年${month}月${day}日`
|
||||
} else if (viewMode === 'week') {
|
||||
const weekStart = getWeekStart(currentDate)
|
||||
const weekEnd = new Date(weekStart)
|
||||
weekEnd.setDate(weekEnd.getDate() + 6)
|
||||
const startMonth = weekStart.getMonth() + 1
|
||||
const startDay = weekStart.getDate()
|
||||
const endMonth = weekEnd.getMonth() + 1
|
||||
const endDay = weekEnd.getDate()
|
||||
if (startMonth === endMonth) {
|
||||
return `${startMonth}月${startDay}日 - ${endDay}日`
|
||||
} else {
|
||||
return `${startMonth}月${startDay}日 - ${endMonth}月${endDay}日`
|
||||
}
|
||||
} else if (viewMode === 'month') {
|
||||
return `${year}年${month}月`
|
||||
} else if (viewMode === 'year') {
|
||||
@@ -377,6 +424,126 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const renderWeekView = () => {
|
||||
if (!weekData) return null
|
||||
|
||||
const weekStart = getWeekStart(currentDate)
|
||||
const chartData = weekData.days.map((dayData, index) => {
|
||||
const date = new Date(weekStart)
|
||||
date.setDate(date.getDate() + index)
|
||||
return {
|
||||
day: WEEK_DAYS[date.getDay()],
|
||||
date: `${date.getMonth() + 1}月${date.getDate()}日`,
|
||||
duration: dayData.totalDuration,
|
||||
durationStr: formatDuration(dayData.totalDuration)
|
||||
}
|
||||
})
|
||||
|
||||
const maxDuration = Math.max(...chartData.map(d => d.duration), 1)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">本周概览</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
{formatDuration(weekData.totalDuration)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">总时长</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
{weekData.activeDays}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">活跃天数</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
{formatDuration(weekData.averageDaily)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">日均时长</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{weekData.totalDuration > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">每日使用时长</h3>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorWeekBar" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.3} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" opacity={0.2} />
|
||||
<XAxis
|
||||
dataKey="day"
|
||||
tick={{ fontSize: 12, fill: '#9ca3af' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#374151', opacity: 0.3 }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: '#9ca3af' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#374151', opacity: 0.3 }}
|
||||
tickFormatter={(value) => formatDuration(value)}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1f2937',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: '#f3f4f6'
|
||||
}}
|
||||
labelStyle={{ color: '#9ca3af' }}
|
||||
formatter={(value: number) => [formatDuration(value), '使用时长']}
|
||||
labelFormatter={(_, payload) => payload?.[0]?.payload?.date || ''}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="duration"
|
||||
fill="url(#colorWeekBar)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">每日明细</h3>
|
||||
<div className="space-y-2">
|
||||
{chartData.map((data, index) => (
|
||||
<div key={index} className="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200 w-12">
|
||||
{data.day}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{data.date}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
{data.durationStr}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{weekData.totalDuration === 0 && (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
|
||||
暂无使用记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderMonthView = () => {
|
||||
if (!monthData) return null
|
||||
|
||||
@@ -514,7 +681,7 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
{(['day', 'month', 'year'] as ViewMode[]).map((mode) => (
|
||||
{(['day', 'week', 'month', 'year'] as ViewMode[]).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => setViewMode(mode)}
|
||||
@@ -523,7 +690,7 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{mode === 'day' ? '日' : mode === 'month' ? '月' : '年'}
|
||||
{mode === 'day' ? '日' : mode === 'week' ? '周' : mode === 'month' ? '月' : '年'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -553,6 +720,7 @@ export const TimeTrackingPage: React.FC = () => {
|
||||
) : (
|
||||
<div className="max-w-2xl mx-auto pb-8">
|
||||
{viewMode === 'day' && renderDayView()}
|
||||
{viewMode === 'week' && renderWeekView()}
|
||||
{viewMode === 'month' && renderMonthView()}
|
||||
{viewMode === 'year' && renderYearView()}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user