update style

This commit is contained in:
Tsuki 2025-08-14 00:29:23 +08:00
parent bc81aeec1d
commit 039197c5da
13 changed files with 203 additions and 1678 deletions

View File

@ -5,7 +5,7 @@ import { cookies } from "next/headers";
export const revalidate = 60; // ISR: 60秒后重新验证 export const revalidate = 60; // ISR: 60秒后重新验证
export default async function Page({ params }: { params: { slug: string } }) { export default async function Page({ params }: any) {
const { slug } = await params const { slug } = await params
const jwt = (await cookies()).get('jwt')?.value const jwt = (await cookies()).get('jwt')?.value

View File

@ -1,48 +0,0 @@
'use client';
import { cn } from '@/lib/utils';
import { motion, Transition } from 'framer-motion';
type BorderTrailProps = {
className?: string;
size?: number;
transition?: Transition;
delay?: number;
onAnimationComplete?: () => void;
style?: React.CSSProperties;
};
export function BorderTrail({
className,
size = 60,
transition,
delay,
onAnimationComplete,
style,
}: BorderTrailProps) {
const BASE_TRANSITION = {
repeat: Infinity,
duration: 5,
ease: 'linear',
};
return (
<div className='pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]'>
<motion.div
className={cn('absolute aspect-square bg-zinc-500', className)}
style={{
width: size,
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
...style,
}}
animate={{
offsetDistance: ['0%', '100%'],
}}
transition={{
...(transition ?? BASE_TRANSITION),
delay: delay,
}}
onAnimationComplete={onAnimationComplete}
/>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

27
app/icon.svg Normal file
View File

@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- 背景圆盘 -->
<circle cx="32" cy="32" r="30" fill="#0a1f33" stroke="#00ffcc" stroke-width="2" />
<!-- 雷达同心圆 -->
<circle cx="32" cy="32" r="10" fill="none" stroke="#00ffcc" stroke-width="1" opacity="0.5"/>
<circle cx="32" cy="32" r="20" fill="none" stroke="#00ffcc" stroke-width="1" opacity="0.5"/>
<!-- 十字刻度线 -->
<line x1="32" y1="2" x2="32" y2="62" stroke="#00ffcc" stroke-width="1" opacity="0.3"/>
<line x1="2" y1="32" x2="62" y2="32" stroke="#00ffcc" stroke-width="1" opacity="0.3"/>
<!-- 雷达扫描扇形 -->
<path d="M32 32 L32 2 A30 30 0 0 1 56.57 14.43 Z"
fill="url(#scanGradient)" opacity="0.6" />
<!-- 扫描渐变定义 -->
<defs>
<linearGradient id="scanGradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#00ffcc" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#00ffcc" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- 中心点 -->
<circle cx="32" cy="32" r="2" fill="#00ffcc"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -15,8 +15,8 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "LiDAR",
description: "Generated by create next app", description: "LiDAR for Radar",
}; };
export default function RootLayout({ export default function RootLayout({

View File

@ -33,12 +33,12 @@ export default function Page() {
<div className="flex flex-row h-full"> <div className="flex flex-row h-full">
<AppSidebar /> <AppSidebar />
<WSProvider> <WSProvider>
<div className="flex-1 flex flex-col min-h-0"> <div className="flex-1 relative min-h-0">
<StatusBar /> <MapComponent />
<div className="flex-1 min-h-0"> <div className="absolute top-0 left-0 right-0 z-10">
<MapComponent /> <StatusBar />
</div> </div>
<div className="flex-shrink-0"> <div className="absolute bottom-0 left-0 right-0 z-10 bg-black/20 backdrop-blur-xl m-3 border border-white/10 rounded-xl shadow-2xl overflow-hidden">
<Timeline /> <Timeline />
</div> </div>
</div> </div>

View File

@ -1,48 +0,0 @@
'use client';
import { cn } from '@/lib/utils';
import { motion, Transition } from 'framer-motion';
type BorderTrailProps = {
className?: string;
size?: number;
transition?: Transition;
delay?: number;
onAnimationComplete?: () => void;
style?: React.CSSProperties;
};
export function BorderTrail({
className,
size = 60,
transition,
delay,
onAnimationComplete,
style,
}: BorderTrailProps) {
const BASE_TRANSITION = {
repeat: Infinity,
duration: 5,
ease: 'linear',
};
return (
<div className='pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]'>
<motion.div
className={cn('absolute aspect-square bg-zinc-500', className)}
style={{
width: size,
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
...style,
}}
animate={{
offsetDistance: ['0%', '100%'],
}}
transition={{
...(transition ?? BASE_TRANSITION),
delay: delay,
}}
onAnimationComplete={onAnimationComplete}
/>
</div>
);
}

View File

@ -2,10 +2,12 @@
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { useWS } from "./ws-context" import { useWS } from "./ws-context"
import { useMap } from "./map-context"
export default function StatusBar() { export default function StatusBar() {
const { wsStatus } = useWS() const { wsStatus } = useWS()
const { currentDatetime } = useMap()
// 根据WebSocket状态返回对应颜色的圆点 // 根据WebSocket状态返回对应颜色的圆点
const getStatusDot = () => { const getStatusDot = () => {
@ -21,7 +23,15 @@ export default function StatusBar() {
} }
return ( return (
<div className="h-8 flex items-center justify-end px-4"> <div className="h-8 flex items-center justify-between px-4">
<div className="flex items-center">
<Label className="text-xs font-bold text-muted-foreground">
Data Time:
</Label>
<Label className="ml-2 text-xs font-bold text-muted-foreground">
{currentDatetime?.toLocaleString()}
</Label>
</div>
<div className="flex items-center"> <div className="flex items-center">
{getStatusDot()} {getStatusDot()}
<Label className="ml-2 text-xs font-bold text-muted-foreground"> <Label className="ml-2 text-xs font-bold text-muted-foreground">

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,13 @@
import React, { useRef, useEffect, useState, useCallback } from "react"; import React, { useRef, useEffect, useState, useCallback } from "react";
import vsSource from './glsl/timeline/vert.glsl'; import { Calendar } from "@/components/ui/calendar"
import fsSource from './glsl/timeline/frag.glsl'; import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, RefreshCwIcon, UnlockIcon } from "lucide-react"; import { CalendarIcon, ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, RefreshCwIcon, UnlockIcon } from "lucide-react";
import { formatInTimeZone } from "date-fns-tz"; import { formatInTimeZone } from "date-fns-tz";
import { parse } from "date-fns" import { parse } from "date-fns"
@ -80,6 +84,8 @@ export const Timeline: React.FC<Props> = React.memo(({
visibleRange: [0, 0] as [number, number], visibleRange: [0, 0] as [number, number],
currentLevel: null as any currentLevel: null as any
}); });
const [open, setOpen] = useState(false)
const [time, setDateTime] = useState(new Date())
const { data } = useWS() const { data } = useWS()
@ -252,7 +258,7 @@ export const Timeline: React.FC<Props> = React.memo(({
return ( return (
<div className={cn(props.className, "w-full h-10 flex flex-row")}> <div className={cn(props.className, "w-full h-10 flex flex-row")}>
<div className="h-full flex flex-row items-center px-3 gap-2 bg-black/60" style={{ boxShadow: '8px 0 24px rgba(0, 0, 0, 0.15), 4px 0 12px rgba(0, 0, 0, 0.1)' }}> <div className="h-full flex flex-row items-center px-3 gap-2 bg-black/30" >
<Button <Button
variant="secondary" variant="secondary"
size="icon" size="icon"
@ -264,7 +270,7 @@ export const Timeline: React.FC<Props> = React.memo(({
</Button> </Button>
<Button <Button
variant="secondary" variant={isPlaying ? "default" : "secondary"}
size="icon" size="icon"
className="size-5" className="size-5"
onClick={() => { onClick={() => {
@ -319,7 +325,7 @@ export const Timeline: React.FC<Props> = React.memo(({
</div> </div>
<Button <Button
variant="secondary" variant={lock ? "default" : "secondary"}
size="icon" size="icon"
className="size-5" className="size-5"
onClick={() => setLock(!lock)} onClick={() => setLock(!lock)}
@ -340,6 +346,31 @@ export const Timeline: React.FC<Props> = React.memo(({
<RefreshCwIcon size={10} /> <RefreshCwIcon size={10} />
</Button> </Button>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="secondary"
size="icon"
className="size-5"
title="设置时间"
>
<CalendarIcon size={10} />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="end">
<Calendar
mode="single"
captionLayout="dropdown"
onSelect={(date) => {
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
</div> </div>
<div className={cn("relative", "w-full h-full bg-gray-800/20")}> <div className={cn("relative", "w-full h-full bg-gray-800/20")}>

View File

@ -44,7 +44,7 @@ export function MapComponent({
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType) const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
// 拖动状态 // 拖动状态
const [colorbarPosition, setColorbarPosition] = useState({ x: 16, y: 16 }) // 从右边和下边的距离 const [colorbarPosition, setColorbarPosition] = useState({ x: 16, y: 36 }) // 从右边和下边的距离
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
// 使用ref来避免频繁的状态更新 // 使用ref来避免频繁的状态更新

View File

@ -583,6 +583,9 @@ class RealTimeTimeline {
private config: Required<TimelineConfig>; private config: Required<TimelineConfig>;
private animationFrameId: number | null = null; private animationFrameId: number | null = null;
private currentTimeUpdateInterval: number | null = null; private currentTimeUpdateInterval: number | null = null;
private mousePosition: { x: number; y: number } | null = null;
private showMouseIndicator: boolean = false;
private pendingRender: boolean = false;
constructor(canvas: HTMLCanvasElement, config?: TimelineConfig) { constructor(canvas: HTMLCanvasElement, config?: TimelineConfig) {
this.canvas = canvas; this.canvas = canvas;
@ -690,17 +693,19 @@ class RealTimeTimeline {
// 鼠标移动 // 鼠标移动
this.canvas.addEventListener('mousemove', (e) => { this.canvas.addEventListener('mousemove', (e) => {
if (e.button === 0) { const rect = this.canvas.getBoundingClientRect();
const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left;
const x = e.clientX - rect.left; const y = e.clientY - rect.top;
const y = e.clientY - rect.top;
this.interaction.handleMouseMove(x, y, this.viewport);
if (this.interaction.getIsDragging()) { // 更新鼠标位置并显示指示线
this.render(); this.mousePosition = { x, y };
} this.showMouseIndicator = true;
}
// 处理拖拽逻辑
this.interaction.handleMouseMove(x, y, this.viewport);
// 使用节流渲染以优化性能
this.requestRender();
}); });
// 鼠标释放 // 鼠标释放
@ -717,6 +722,8 @@ class RealTimeTimeline {
this.canvas.addEventListener('mouseleave', () => { this.canvas.addEventListener('mouseleave', () => {
this.interaction.endDrag(); this.interaction.endDrag();
this.canvas.style.cursor = 'crosshair'; this.canvas.style.cursor = 'crosshair';
this.showMouseIndicator = false;
this.requestRender(); // 重新渲染以隐藏鼠标指示线
}); });
this.canvas.addEventListener('mouseup', (e) => { this.canvas.addEventListener('mouseup', (e) => {
@ -779,6 +786,17 @@ class RealTimeTimeline {
}, 1000); // 每秒更新一次 }, 1000); // 每秒更新一次
} }
/** 请求渲染(使用 requestAnimationFrame 节流) */
private requestRender(): void {
if (!this.pendingRender) {
this.pendingRender = true;
requestAnimationFrame(() => {
this.render();
this.pendingRender = false;
});
}
}
/** 渲染时间轴 */ /** 渲染时间轴 */
render(): void { render(): void {
const width = this.viewport.getWidth(); const width = this.viewport.getWidth();
@ -810,6 +828,9 @@ class RealTimeTimeline {
// 绘制时间标记 // 绘制时间标记
this.drawTimeMarks(); this.drawTimeMarks();
// 绘制鼠标位置指示线
this.drawMouseIndicator();
} }
private changeTime(date: Date): void { private changeTime(date: Date): void {
@ -920,31 +941,59 @@ class RealTimeTimeline {
if (x >= 0 && x <= this.viewport.getWidth()) { if (x >= 0 && x <= this.viewport.getWidth()) {
const height = this.viewport.getHeight(); const height = this.viewport.getHeight();
const currentTimeColor = this.config.colors.currentTime ?? '#ff4444';
// 绘制时间线 this.ctx.save();
this.ctx.strokeStyle = this.config.colors.currentTime ?? '#ff4444';
this.ctx.lineWidth = 1; // 绘制毛玻璃背景条带
this.ctx.setLineDash([5, 5]); const backgroundWidth = 14;
const gradient = this.ctx.createLinearGradient(x - backgroundWidth / 2, 0, x + backgroundWidth / 2, 0);
// 使用当前时间颜色的红色调
gradient.addColorStop(0, `rgba(255, 68, 68, 0.02)`);
gradient.addColorStop(0.5, `rgba(255, 68, 68, 0.18)`);
gradient.addColorStop(1, `rgba(255, 68, 68, 0.02)`);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x - backgroundWidth / 2, 0, backgroundWidth, height);
// 绘制外层辉光效果
this.ctx.shadowColor = `rgba(255, 68, 68, 0.7)`;
this.ctx.shadowBlur = 10;
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
// 绘制主指示线(实线)
this.ctx.strokeStyle = `rgba(255, 68, 68, 0.95)`;
this.ctx.lineWidth = 2.5;
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.moveTo(x, 0); this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, height); this.ctx.lineTo(x, height);
this.ctx.stroke(); this.ctx.stroke();
this.ctx.setLineDash([]);
// 绘制时间标签背景 // 重置阴影,绘制内层亮线增强辉光效果
this.ctx.shadowBlur = 0;
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, height);
this.ctx.stroke();
this.ctx.restore();
// 绘制时间标签(可选,目前被注释掉)
const timeStr = TimeUtils.format(new Date(now), 'HH:mm:ss'); const timeStr = TimeUtils.format(new Date(now), 'HH:mm:ss');
this.ctx.font = `bold ${this.config.sizes.primaryFontSize ?? 13}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`; this.ctx.font = `bold ${this.config.sizes.primaryFontSize ?? 13}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
const metrics = this.ctx.measureText(timeStr); const metrics = this.ctx.measureText(timeStr);
const labelWidth = metrics.width + 10; const labelWidth = metrics.width + 10;
const labelHeight = 20; const labelHeight = 20;
this.ctx.fillStyle = this.config.colors.currentTime ?? '#ff4444'; // this.ctx.fillStyle = currentTimeColor;
// this.ctx.fillRect(x - labelWidth / 2, 0, labelWidth, labelHeight); // this.ctx.fillRect(x - labelWidth / 2, 0, labelWidth, labelHeight);
// 绘制时间文本 // this.ctx.fillStyle = '#ffffff';
this.ctx.fillStyle = '#ffffff'; // this.ctx.textAlign = 'center';
this.ctx.textAlign = 'center'; // this.ctx.textBaseline = 'middle';
this.ctx.textBaseline = 'middle';
// this.ctx.fillText(timeStr, x, labelHeight / 2); // this.ctx.fillText(timeStr, x, labelHeight / 2);
} }
} }
@ -995,6 +1044,56 @@ class RealTimeTimeline {
}); });
} }
/** 绘制鼠标位置指示线 */
private drawMouseIndicator(): void {
if (!this.showMouseIndicator || !this.mousePosition) {
return;
}
const height = this.viewport.getHeight();
const x = this.mousePosition.x;
// 确保指示线在画布范围内
if (x >= 0 && x <= this.viewport.getWidth()) {
this.ctx.save();
// 绘制毛玻璃背景条带
const backgroundWidth = 12;
const gradient = this.ctx.createLinearGradient(x - backgroundWidth / 2, 0, x + backgroundWidth / 2, 0);
gradient.addColorStop(0, 'rgba(135, 206, 235, 0.02)');
gradient.addColorStop(0.5, 'rgba(135, 206, 235, 0.15)');
gradient.addColorStop(1, 'rgba(135, 206, 235, 0.02)');
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x - backgroundWidth / 2, 0, backgroundWidth, height);
// 绘制外层辉光效果
this.ctx.shadowColor = 'rgba(135, 206, 235, 0.6)';
this.ctx.shadowBlur = 8;
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
// 绘制主指示线(实线)
this.ctx.strokeStyle = 'rgba(135, 206, 235, 0.9)';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, height);
this.ctx.stroke();
// 重置阴影,绘制内层亮线增强辉光效果
this.ctx.shadowBlur = 0;
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, height);
this.ctx.stroke();
this.ctx.restore();
}
}
/** 获取当前视口信息 */ /** 获取当前视口信息 */