update style
This commit is contained in:
parent
bc81aeec1d
commit
039197c5da
@ -5,7 +5,7 @@ import { cookies } from "next/headers";
|
||||
|
||||
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 jwt = (await cookies()).get('jwt')?.value
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
27
app/icon.svg
Normal file
27
app/icon.svg
Normal 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 |
@ -15,8 +15,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "LiDAR",
|
||||
description: "LiDAR for Radar",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@ -33,12 +33,12 @@ export default function Page() {
|
||||
<div className="flex flex-row h-full">
|
||||
<AppSidebar />
|
||||
<WSProvider>
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<StatusBar />
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex-1 relative min-h-0">
|
||||
<MapComponent />
|
||||
<div className="absolute top-0 left-0 right-0 z-10">
|
||||
<StatusBar />
|
||||
</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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -2,10 +2,12 @@
|
||||
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useWS } from "./ws-context"
|
||||
import { useMap } from "./map-context"
|
||||
|
||||
export default function StatusBar() {
|
||||
|
||||
const { wsStatus } = useWS()
|
||||
const { currentDatetime } = useMap()
|
||||
|
||||
// 根据WebSocket状态返回对应颜色的圆点
|
||||
const getStatusDot = () => {
|
||||
@ -21,7 +23,15 @@ export default function StatusBar() {
|
||||
}
|
||||
|
||||
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">
|
||||
{getStatusDot()}
|
||||
<Label className="ml-2 text-xs font-bold text-muted-foreground">
|
||||
|
||||
1546
app/timeline.tsx
1546
app/timeline.tsx
File diff suppressed because it is too large
Load Diff
43
app/tl.tsx
43
app/tl.tsx
@ -1,9 +1,13 @@
|
||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||
import vsSource from './glsl/timeline/vert.glsl';
|
||||
import fsSource from './glsl/timeline/frag.glsl';
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { cn } from "@/lib/utils";
|
||||
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 { parse } from "date-fns"
|
||||
|
||||
@ -80,6 +84,8 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
visibleRange: [0, 0] as [number, number],
|
||||
currentLevel: null as any
|
||||
});
|
||||
const [open, setOpen] = useState(false)
|
||||
const [time, setDateTime] = useState(new Date())
|
||||
|
||||
const { data } = useWS()
|
||||
|
||||
@ -252,7 +258,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
|
||||
return (
|
||||
<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
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
@ -264,7 +270,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant={isPlaying ? "default" : "secondary"}
|
||||
size="icon"
|
||||
className="size-5"
|
||||
onClick={() => {
|
||||
@ -319,7 +325,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant={lock ? "default" : "secondary"}
|
||||
size="icon"
|
||||
className="size-5"
|
||||
onClick={() => setLock(!lock)}
|
||||
@ -340,6 +346,31 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
<RefreshCwIcon size={10} />
|
||||
</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 className={cn("relative", "w-full h-full bg-gray-800/20")}>
|
||||
|
||||
@ -44,7 +44,7 @@ export function MapComponent({
|
||||
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)
|
||||
|
||||
// 使用ref来避免频繁的状态更新
|
||||
|
||||
133
lib/timeline.ts
133
lib/timeline.ts
@ -583,6 +583,9 @@ class RealTimeTimeline {
|
||||
private config: Required<TimelineConfig>;
|
||||
private animationFrameId: 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) {
|
||||
this.canvas = canvas;
|
||||
@ -690,17 +693,19 @@ class RealTimeTimeline {
|
||||
|
||||
// 鼠标移动
|
||||
this.canvas.addEventListener('mousemove', (e) => {
|
||||
if (e.button === 0) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// 更新鼠标位置并显示指示线
|
||||
this.mousePosition = { x, y };
|
||||
this.showMouseIndicator = true;
|
||||
|
||||
// 处理拖拽逻辑
|
||||
this.interaction.handleMouseMove(x, y, this.viewport);
|
||||
|
||||
if (this.interaction.getIsDragging()) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用节流渲染以优化性能
|
||||
this.requestRender();
|
||||
});
|
||||
|
||||
// 鼠标释放
|
||||
@ -717,6 +722,8 @@ class RealTimeTimeline {
|
||||
this.canvas.addEventListener('mouseleave', () => {
|
||||
this.interaction.endDrag();
|
||||
this.canvas.style.cursor = 'crosshair';
|
||||
this.showMouseIndicator = false;
|
||||
this.requestRender(); // 重新渲染以隐藏鼠标指示线
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('mouseup', (e) => {
|
||||
@ -779,6 +786,17 @@ class RealTimeTimeline {
|
||||
}, 1000); // 每秒更新一次
|
||||
}
|
||||
|
||||
/** 请求渲染(使用 requestAnimationFrame 节流) */
|
||||
private requestRender(): void {
|
||||
if (!this.pendingRender) {
|
||||
this.pendingRender = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.render();
|
||||
this.pendingRender = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 渲染时间轴 */
|
||||
render(): void {
|
||||
const width = this.viewport.getWidth();
|
||||
@ -810,6 +828,9 @@ class RealTimeTimeline {
|
||||
|
||||
// 绘制时间标记
|
||||
this.drawTimeMarks();
|
||||
|
||||
// 绘制鼠标位置指示线
|
||||
this.drawMouseIndicator();
|
||||
}
|
||||
|
||||
private changeTime(date: Date): void {
|
||||
@ -920,31 +941,59 @@ class RealTimeTimeline {
|
||||
|
||||
if (x >= 0 && x <= this.viewport.getWidth()) {
|
||||
const height = this.viewport.getHeight();
|
||||
const currentTimeColor = this.config.colors.currentTime ?? '#ff4444';
|
||||
|
||||
// 绘制时间线
|
||||
this.ctx.strokeStyle = this.config.colors.currentTime ?? '#ff4444';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.setLineDash([5, 5]);
|
||||
this.ctx.save();
|
||||
|
||||
// 绘制毛玻璃背景条带
|
||||
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.moveTo(x, 0);
|
||||
this.ctx.lineTo(x, height);
|
||||
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');
|
||||
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 labelWidth = metrics.width + 10;
|
||||
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.fillStyle = '#ffffff';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
// this.ctx.fillStyle = '#ffffff';
|
||||
// this.ctx.textAlign = 'center';
|
||||
// this.ctx.textBaseline = 'middle';
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 获取当前视口信息 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user