500 lines
19 KiB
TypeScript
500 lines
19 KiB
TypeScript
"use client"
|
||
|
||
import type React from "react"
|
||
import { useState, useRef, useCallback, useEffect } from "react"
|
||
import { defaultStars, type EsportsStar } from "../_lib/config-types"
|
||
import { getStreamerAvatarUrl, getAvatarPlaceholderUrl } from "@/lib/streamer-avatar"
|
||
|
||
// 游戏类型颜色
|
||
const gameColors: Record<string, string> = {
|
||
王者荣耀: "#f59e0b",
|
||
英雄联盟: "#3b82f6",
|
||
魔兽世界: "#10b981",
|
||
}
|
||
|
||
const chinaProvinces = [
|
||
{
|
||
id: "xinjiang",
|
||
name: "新疆",
|
||
d: "M85,95 L110,85 L145,80 L180,85 L210,95 L235,115 L250,145 L255,180 L250,215 L235,250 L210,275 L175,290 L140,295 L105,285 L75,265 L55,235 L45,200 L45,160 L55,125 L70,105 Z",
|
||
},
|
||
{
|
||
id: "xizang",
|
||
name: "西藏",
|
||
d: "M55,265 L105,285 L145,295 L180,300 L210,315 L225,345 L220,380 L200,410 L165,430 L125,435 L85,420 L55,390 L35,350 L35,305 Z",
|
||
},
|
||
{
|
||
id: "qinghai",
|
||
name: "青海",
|
||
d: "M210,215 L250,205 L285,210 L315,225 L330,255 L325,290 L305,320 L270,335 L235,330 L205,310 L190,280 L195,245 Z",
|
||
},
|
||
{
|
||
id: "gansu",
|
||
name: "甘肃",
|
||
d: "M285,165 L330,150 L375,145 L410,155 L435,175 L445,205 L440,240 L420,270 L385,290 L345,295 L310,280 L290,250 L285,215 Z",
|
||
},
|
||
{
|
||
id: "neimenggu",
|
||
name: "内蒙古",
|
||
d: "M330,55 L400,45 L470,40 L540,45 L600,60 L645,85 L665,120 L660,155 L635,185 L590,200 L535,205 L475,200 L415,190 L365,175 L335,150 L320,115 L320,80 Z",
|
||
},
|
||
{
|
||
id: "heilongjiang",
|
||
name: "黑龙江",
|
||
d: "M665,35 L720,25 L765,35 L795,60 L805,100 L795,140 L770,170 L730,185 L690,180 L660,155 L655,115 L660,75 Z",
|
||
},
|
||
{
|
||
id: "jilin",
|
||
name: "吉林",
|
||
d: "M665,165 L715,155 L755,165 L780,190 L780,225 L760,255 L720,270 L680,265 L655,240 L650,200 Z",
|
||
},
|
||
{
|
||
id: "liaoning",
|
||
name: "辽宁",
|
||
d: "M620,215 L680,200 L720,210 L750,235 L755,270 L740,300 L700,320 L655,320 L620,300 L605,265 L605,235 Z",
|
||
},
|
||
{
|
||
id: "hebei",
|
||
name: "河北",
|
||
d: "M545,150 L590,140 L625,150 L645,180 L645,215 L630,250 L595,275 L555,280 L525,265 L515,230 L520,190 Z",
|
||
},
|
||
{ id: "beijing", name: "北京", d: "M575,175 L595,170 L605,185 L595,200 L575,200 L568,188 Z", isCity: true },
|
||
{ id: "tianjin", name: "天津", d: "M605,200 L620,195 L628,212 L618,228 L602,225 L598,212 Z", isCity: true },
|
||
{
|
||
id: "shanxi",
|
||
name: "山西",
|
||
d: "M500,190 L545,175 L565,210 L560,255 L540,290 L505,300 L480,280 L475,240 L480,205 Z",
|
||
},
|
||
{
|
||
id: "shandong",
|
||
name: "山东",
|
||
d: "M580,245 L630,230 L680,235 L720,255 L735,285 L720,320 L680,340 L630,345 L590,330 L570,295 Z",
|
||
},
|
||
{
|
||
id: "henan",
|
||
name: "河南",
|
||
d: "M500,280 L555,265 L590,280 L600,320 L580,360 L540,380 L500,375 L475,345 L475,310 Z",
|
||
},
|
||
{
|
||
id: "jiangsu",
|
||
name: "江苏",
|
||
d: "M620,310 L680,295 L720,305 L740,340 L725,380 L685,400 L645,395 L615,370 L610,335 Z",
|
||
},
|
||
{
|
||
id: "anhui",
|
||
name: "安徽",
|
||
d: "M575,340 L620,325 L650,345 L660,385 L640,425 L600,440 L560,430 L545,390 L555,360 Z",
|
||
},
|
||
{ id: "shanghai", name: "上海", d: "M705,355 L725,350 L732,372 L718,385 L700,378 L698,362 Z", isCity: true },
|
||
{ id: "zhejiang", name: "浙江", d: "M660,385 L710,370 L740,390 L745,435 L720,470 L680,480 L645,465 L640,425 Z" },
|
||
{ id: "fujian", name: "福建", d: "M645,465 L695,450 L735,465 L750,510 L730,555 L685,570 L645,555 L630,510 Z" },
|
||
{ id: "taiwan", name: "台湾", d: "M755,480 L780,470 L795,505 L785,550 L760,565 L745,540 L745,505 Z" },
|
||
{ id: "jiangxi", name: "江西", d: "M575,420 L640,405 L670,435 L675,485 L650,530 L600,545 L560,525 L550,475 Z" },
|
||
{ id: "hubei", name: "湖北", d: "M475,335 L540,320 L580,340 L590,385 L565,425 L520,440 L475,425 L460,385 Z" },
|
||
{ id: "hunan", name: "湖南", d: "M475,430 L535,415 L575,435 L585,485 L560,535 L515,555 L470,540 L455,490 Z" },
|
||
{
|
||
id: "guangdong",
|
||
name: "广东",
|
||
d: "M505,545 L575,530 L635,540 L680,560 L695,605 L665,645 L605,660 L545,650 L500,620 L485,580 Z",
|
||
},
|
||
{ id: "guangxi", name: "广西", d: "M395,545 L470,530 L520,545 L535,595 L510,640 L455,655 L400,640 L375,600 Z" },
|
||
{ id: "hainan", name: "海南", d: "M485,680 L530,670 L545,710 L520,745 L480,750 L460,720 Z" },
|
||
{
|
||
id: "sichuan",
|
||
name: "四川",
|
||
d: "M295,320 L365,300 L420,315 L455,355 L455,410 L430,455 L375,475 L320,460 L280,420 L275,365 Z",
|
||
},
|
||
{ id: "chongqing", name: "重庆", d: "M420,370 L470,355 L490,390 L480,435 L445,455 L410,445 L400,405 Z" },
|
||
{ id: "guizhou", name: "贵州", d: "M375,455 L445,440 L485,460 L495,510 L470,555 L420,570 L375,555 L360,505 Z" },
|
||
{
|
||
id: "yunnan",
|
||
name: "云南",
|
||
d: "M275,455 L345,440 L395,465 L410,525 L390,585 L340,620 L280,625 L235,590 L225,530 L240,480 Z",
|
||
},
|
||
{
|
||
id: "shaanxi",
|
||
name: "陕西",
|
||
d: "M420,210 L480,195 L510,230 L515,285 L495,335 L455,355 L415,345 L395,300 L400,250 Z",
|
||
},
|
||
{ id: "ningxia", name: "宁夏", d: "M400,195 L435,185 L450,215 L445,250 L425,275 L395,270 L385,235 Z" },
|
||
]
|
||
|
||
export default function ChinaMap() {
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
const [scale, setScale] = useState(1)
|
||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||
const [isDragging, setIsDragging] = useState(false)
|
||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||
const [hoveredProvince, setHoveredProvince] = useState<string | null>(null)
|
||
const [hoveredStar, setHoveredStar] = useState<EsportsStar | null>(null)
|
||
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 })
|
||
const [stars, setStars] = useState<EsportsStar[]>(defaultStars)
|
||
/** 头像加载失败的主播 id,改用首字占位避免裂图 */
|
||
const [failedAvatarIds, setFailedAvatarIds] = useState<Set<string>>(new Set())
|
||
|
||
useEffect(() => {
|
||
fetch("/api/screen/streamers")
|
||
.then((res) => res.json())
|
||
.then((body: { success?: boolean; data?: EsportsStar[] }) => {
|
||
if (body.success && Array.isArray(body.data) && body.data.length > 0) {
|
||
setStars(body.data)
|
||
}
|
||
})
|
||
.catch(() => {})
|
||
}, [])
|
||
|
||
// 将经纬度映射到SVG坐标系
|
||
const getStarPosition = useCallback((lng: number, lat: number) => {
|
||
const x = ((lng - 73) / (135 - 73)) * 750 + 30
|
||
const y = ((54 - lat) / (54 - 18)) * 680 + 25
|
||
return { x, y }
|
||
}, [])
|
||
|
||
// 鼠标滚轮缩放
|
||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||
e.preventDefault()
|
||
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||
setScale((prev) => Math.min(Math.max(prev * delta, 0.5), 4))
|
||
}, [])
|
||
|
||
// 拖拽事件
|
||
const handleMouseDown = useCallback(
|
||
(e: React.MouseEvent) => {
|
||
if (e.button === 0) {
|
||
setIsDragging(true)
|
||
setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y })
|
||
}
|
||
},
|
||
[position],
|
||
)
|
||
|
||
const handleMouseMove = useCallback(
|
||
(e: React.MouseEvent) => {
|
||
if (isDragging) {
|
||
setPosition({
|
||
x: e.clientX - dragStart.x,
|
||
y: e.clientY - dragStart.y,
|
||
})
|
||
}
|
||
setTooltipPos({ x: e.clientX, y: e.clientY })
|
||
},
|
||
[isDragging, dragStart],
|
||
)
|
||
|
||
const handleMouseUp = useCallback(() => {
|
||
setIsDragging(false)
|
||
}, [])
|
||
|
||
// 触摸事件
|
||
const handleTouchStart = useCallback(
|
||
(e: React.TouchEvent) => {
|
||
if (e.touches.length === 1) {
|
||
setIsDragging(true)
|
||
setDragStart({
|
||
x: e.touches[0].clientX - position.x,
|
||
y: e.touches[0].clientY - position.y,
|
||
})
|
||
}
|
||
},
|
||
[position],
|
||
)
|
||
|
||
const handleTouchMove = useCallback(
|
||
(e: React.TouchEvent) => {
|
||
if (isDragging && e.touches.length === 1) {
|
||
setPosition({
|
||
x: e.touches[0].clientX - dragStart.x,
|
||
y: e.touches[0].clientY - dragStart.y,
|
||
})
|
||
}
|
||
},
|
||
[isDragging, dragStart],
|
||
)
|
||
|
||
const handleTouchEnd = useCallback(() => {
|
||
setIsDragging(false)
|
||
}, [])
|
||
|
||
const resetView = useCallback(() => {
|
||
setScale(1)
|
||
setPosition({ x: 0, y: 0 })
|
||
}, [])
|
||
|
||
return (
|
||
<div className="w-full h-full rounded-lg overflow-hidden wz-screen-map-wrap relative">
|
||
{/* 地图标题和图例 */}
|
||
<div className="absolute top-4 left-4 z-20 bg-secondary/90 backdrop-blur-sm rounded-lg p-4 border border-border/50">
|
||
<h3 className="text-foreground font-semibold text-sm mb-3 flex items-center">
|
||
<span className="w-2 h-2 bg-cyan-400 rounded-full mr-2 animate-pulse"></span>
|
||
全国电竞明星分布
|
||
</h3>
|
||
<div className="space-y-2 text-xs">
|
||
<div className="flex items-center">
|
||
<div className="w-3 h-3 rounded-full bg-yellow-500 mr-2 shadow-lg shadow-yellow-500/50"></div>
|
||
<span className="text-muted-foreground">王者荣耀</span>
|
||
</div>
|
||
<div className="flex items-center">
|
||
<div className="w-3 h-3 rounded-full bg-blue-500 mr-2 shadow-lg shadow-blue-500/50"></div>
|
||
<span className="text-muted-foreground">英雄联盟</span>
|
||
</div>
|
||
<div className="flex items-center">
|
||
<div className="w-3 h-3 rounded-full bg-green-500 mr-2 shadow-lg shadow-green-500/50"></div>
|
||
<span className="text-muted-foreground">魔兽世界</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 缩放控制按钮 */}
|
||
<div className="absolute top-4 right-4 z-20 flex flex-col gap-2">
|
||
<button
|
||
onClick={() => setScale((prev) => Math.min(prev * 1.2, 4))}
|
||
className="w-8 h-8 bg-secondary/90 hover:bg-accent backdrop-blur-sm rounded-lg border border-border/50 text-foreground flex items-center justify-center transition-colors text-lg font-bold"
|
||
>
|
||
+
|
||
</button>
|
||
<button
|
||
onClick={() => setScale((prev) => Math.max(prev * 0.8, 0.5))}
|
||
className="w-8 h-8 bg-secondary/90 hover:bg-accent backdrop-blur-sm rounded-lg border border-border/50 text-foreground flex items-center justify-center transition-colors text-lg font-bold"
|
||
>
|
||
-
|
||
</button>
|
||
<button
|
||
onClick={resetView}
|
||
className="w-8 h-8 bg-secondary/90 hover:bg-accent backdrop-blur-sm rounded-lg border border-border/50 text-foreground flex items-center justify-center transition-colors text-sm"
|
||
>
|
||
↺
|
||
</button>
|
||
<div className="text-xs text-muted-foreground text-center mt-1 bg-secondary/70 rounded px-1">
|
||
{Math.round(scale * 100)}%
|
||
</div>
|
||
</div>
|
||
|
||
{/* SVG地图容器 */}
|
||
<div
|
||
ref={containerRef}
|
||
className="w-full h-full cursor-grab active:cursor-grabbing"
|
||
onWheel={handleWheel}
|
||
onMouseDown={handleMouseDown}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseLeave={handleMouseUp}
|
||
onTouchStart={handleTouchStart}
|
||
onTouchMove={handleTouchMove}
|
||
onTouchEnd={handleTouchEnd}
|
||
>
|
||
<svg
|
||
viewBox="0 0 850 800"
|
||
className="w-full h-full"
|
||
style={{
|
||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||
transformOrigin: "center center",
|
||
transition: isDragging ? "none" : "transform 0.1s ease-out",
|
||
}}
|
||
>
|
||
<defs>
|
||
{/* 发光滤镜 */}
|
||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||
<feMerge>
|
||
<feMergeNode in="coloredBlur" />
|
||
<feMergeNode in="SourceGraphic" />
|
||
</feMerge>
|
||
</filter>
|
||
{/* 省份高亮渐变 */}
|
||
<linearGradient id="provinceHover" x1="0%" y1="0%" x2="100%" y2="100%">
|
||
<stop offset="0%" stopColor="hsl(185 80% 50%)" stopOpacity="0.5" />
|
||
<stop offset="100%" stopColor="hsl(210 90% 55%)" stopOpacity="0.5" />
|
||
</linearGradient>
|
||
{/* 涟漪渐变 */}
|
||
<radialGradient id="ripple-yellow" cx="50%" cy="50%" r="50%">
|
||
<stop offset="0%" stopColor="#f59e0b" stopOpacity="0.6" />
|
||
<stop offset="100%" stopColor="#f59e0b" stopOpacity="0" />
|
||
</radialGradient>
|
||
<radialGradient id="ripple-blue" cx="50%" cy="50%" r="50%">
|
||
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.6" />
|
||
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
|
||
</radialGradient>
|
||
<radialGradient id="ripple-green" cx="50%" cy="50%" r="50%">
|
||
<stop offset="0%" stopColor="#10b981" stopOpacity="0.6" />
|
||
<stop offset="100%" stopColor="#10b981" stopOpacity="0" />
|
||
</radialGradient>
|
||
</defs>
|
||
|
||
{/* 中国地图省份 */}
|
||
<g className="provinces">
|
||
{chinaProvinces.map((province) => (
|
||
<path
|
||
key={province.id}
|
||
d={province.d}
|
||
fill={hoveredProvince === province.id ? "url(#provinceHover)" : "hsl(217.2 32.6% 14%)"}
|
||
stroke="hsl(217.2 32.6% 28%)"
|
||
strokeWidth={province.isCity ? "1" : "1.5"}
|
||
className="transition-all duration-200 cursor-pointer"
|
||
onMouseEnter={() => setHoveredProvince(province.id)}
|
||
onMouseLeave={() => setHoveredProvince(null)}
|
||
/>
|
||
))}
|
||
</g>
|
||
|
||
{/* 南海诸岛示意框 */}
|
||
<g className="south-sea">
|
||
<rect
|
||
x="720"
|
||
y="620"
|
||
width="110"
|
||
height="140"
|
||
fill="hsl(217.2 32.6% 10%)"
|
||
stroke="hsl(217.2 32.6% 28%)"
|
||
strokeWidth="1.5"
|
||
rx="4"
|
||
/>
|
||
<text x="775" y="645" fontSize="10" fill="hsl(215 20.2% 65.1%)" textAnchor="middle">
|
||
南海诸岛
|
||
</text>
|
||
<circle cx="745" cy="680" r="3" fill="hsl(217.2 32.6% 30%)" />
|
||
<circle cx="775" cy="705" r="3" fill="hsl(217.2 32.6% 30%)" />
|
||
<circle cx="800" cy="730" r="3" fill="hsl(217.2 32.6% 30%)" />
|
||
</g>
|
||
|
||
{/* 电竞明星标记(带头像)- 与后台主播管理数据互通 */}
|
||
{stars.map((star, index) => {
|
||
const pos = getStarPosition(star.location.lng, star.location.lat)
|
||
const color = gameColors[star.game] || "#06b6d4"
|
||
const rippleId =
|
||
star.game === "王者荣耀" ? "ripple-yellow" : star.game === "英雄联盟" ? "ripple-blue" : "ripple-green"
|
||
const avatarUrl =
|
||
failedAvatarIds.has(star.id)
|
||
? getAvatarPlaceholderUrl(star.name, 32)
|
||
: getStreamerAvatarUrl(star.name, star.avatar, 32)
|
||
|
||
return (
|
||
<g
|
||
key={star.id}
|
||
className="cursor-pointer"
|
||
onMouseEnter={() => setHoveredStar(star)}
|
||
onMouseLeave={() => setHoveredStar(null)}
|
||
>
|
||
{/* 涟漪动画 */}
|
||
<circle
|
||
cx={pos.x}
|
||
cy={pos.y}
|
||
r="28"
|
||
fill={`url(#${rippleId})`}
|
||
className="animate-ping"
|
||
style={{
|
||
animationDuration: "2.5s",
|
||
animationDelay: `${index * 0.2}s`,
|
||
transformOrigin: `${pos.x}px ${pos.y}px`,
|
||
}}
|
||
/>
|
||
{/* 外圈发光边框 */}
|
||
<circle
|
||
cx={pos.x}
|
||
cy={pos.y}
|
||
r="20"
|
||
fill="none"
|
||
stroke={color}
|
||
strokeWidth="3"
|
||
filter="url(#glow)"
|
||
className="transition-all duration-200"
|
||
style={{
|
||
transform: hoveredStar?.id === star.id ? "scale(1.3)" : "scale(1)",
|
||
transformOrigin: `${pos.x}px ${pos.y}px`,
|
||
}}
|
||
/>
|
||
{/* 头像背景 */}
|
||
<circle cx={pos.x} cy={pos.y} r="18" fill="hsl(217.2 32.6% 12%)" />
|
||
{/* 头像图片 */}
|
||
<clipPath id={`avatar-clip-${star.id}`}>
|
||
<circle cx={pos.x} cy={pos.y} r="16" />
|
||
</clipPath>
|
||
<image
|
||
href={avatarUrl}
|
||
x={pos.x - 16}
|
||
y={pos.y - 16}
|
||
width="32"
|
||
height="32"
|
||
clipPath={`url(#avatar-clip-${star.id})`}
|
||
preserveAspectRatio="xMidYMid slice"
|
||
onError={() => setFailedAvatarIds((prev) => new Set(prev).add(star.id))}
|
||
/>
|
||
{/* 游戏类型小标识 */}
|
||
<circle
|
||
cx={pos.x + 14}
|
||
cy={pos.y + 14}
|
||
r="6"
|
||
fill={color}
|
||
stroke="hsl(217.2 32.6% 10%)"
|
||
strokeWidth="2"
|
||
/>
|
||
</g>
|
||
)
|
||
})}
|
||
</svg>
|
||
</div>
|
||
|
||
{/* 悬停省份名称 */}
|
||
{hoveredProvince && (
|
||
<div className="absolute bottom-4 left-4 z-20 bg-secondary/90 backdrop-blur-sm rounded-lg px-3 py-2 border border-border/50">
|
||
<span className="text-foreground text-sm font-medium">
|
||
{chinaProvinces.find((p) => p.id === hoveredProvince)?.name}
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 悬停明星信息卡片 */}
|
||
{hoveredStar && (
|
||
<div
|
||
className="fixed z-50 bg-secondary/95 backdrop-blur-sm rounded-lg p-4 border border-border/50 shadow-xl pointer-events-none"
|
||
style={{
|
||
left: Math.min(tooltipPos.x + 15, typeof window !== "undefined" ? window.innerWidth - 280 : 500),
|
||
top: tooltipPos.y - 10,
|
||
maxWidth: "260px",
|
||
}}
|
||
>
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<img
|
||
src={
|
||
failedAvatarIds.has(hoveredStar.id)
|
||
? getAvatarPlaceholderUrl(hoveredStar.name, 48)
|
||
: getStreamerAvatarUrl(hoveredStar.name, hoveredStar.avatar, 48)
|
||
}
|
||
alt={hoveredStar.name}
|
||
className="w-12 h-12 rounded-full border-2 object-cover"
|
||
style={{ borderColor: gameColors[hoveredStar.game] }}
|
||
onError={(e) => {
|
||
setFailedAvatarIds((prev) => new Set(prev).add(hoveredStar.id))
|
||
const t = e.target as HTMLImageElement
|
||
if (t) t.src = getAvatarPlaceholderUrl(hoveredStar.name, 48)
|
||
}}
|
||
/>
|
||
<div>
|
||
<div className="font-bold text-foreground">{hoveredStar.name}</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
{hoveredStar.location.province} · {hoveredStar.location.city}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="text-xs mb-3 px-2 py-1 rounded-full inline-block"
|
||
style={{ backgroundColor: `${gameColors[hoveredStar.game]}20`, color: gameColors[hoveredStar.game] }}
|
||
>
|
||
{hoveredStar.game}
|
||
</div>
|
||
<div className="border-t border-border pt-2 grid grid-cols-2 gap-2">
|
||
<div className="text-green-400 text-xs">日收入: ¥{hoveredStar.dailyRevenue.toLocaleString()}</div>
|
||
<div className="text-blue-400 text-xs">粉丝: {(hoveredStar.fans / 1000).toFixed(1)}K</div>
|
||
<div className="text-white/70 text-xs">转化率: {hoveredStar.conversionRate}%</div>
|
||
<div className="text-yellow-400 text-xs">直播平台: 抖音</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作提示 */}
|
||
<div className="absolute bottom-4 right-4 z-20 bg-secondary/70 backdrop-blur-sm rounded-lg px-3 py-2 border border-border/30">
|
||
<div className="text-xs text-muted-foreground">滚轮缩放 · 拖拽移动</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|