<template>
	<div class="page-login">
		<!-- 星河+海洋 Canvas 背景 -->
		<canvas ref="canvasRef" class="bg-canvas"></canvas>

		<!-- 登录卡片 -->
		<div class="login-card">
			<div class="logo">
				<div class="icon">
					<img src="/logo.png" alt="Logo" />
				</div>
				<span>{{ app.info.name }}</span>
			</div>

			<p class="desc">{{ $t('GPU 智能监控与防护平台') }}</p>

			<div class="form">
				<el-form label-position="top" class="form" :disabled="saving">
					<el-form-item :label="$t('用户名')">
						<el-input
							v-model="form.username"
							:placeholder="$t('请输入用户名')"
							maxlength="20"
						/>
					</el-form-item>

					<el-form-item :label="$t('密码')">
						<el-input
							v-model="form.password"
							type="password"
							:placeholder="$t('请输入密码')"
							maxlength="20"
							show-password
							autocomplete="new-password"
						/>
					</el-form-item>

					<el-form-item :label="$t('验证码')">
						<el-input
							v-model="form.verifyCode"
							:placeholder="$t('验证码')"
							maxlength="4"
							@keyup.enter="toLogin"
						>
							<template #suffix>
								<pic-captcha
									:ref="setRefs('picCaptcha')"
									v-model="form.captchaId"
									@change="
										() => {
											form.verifyCode = '';
										}
									"
								/>
							</template>
						</el-input>
					</el-form-item>

					<div class="op">
						<el-button type="primary" :loading="saving" @click="toLogin">
							{{ $t('登录') }}
						</el-button>
					</div>
				</el-form>
			</div>
		</div>

		<span class="copyright">Copyright © GPU Guard v1.0</span>
	</div>
</template>

<script lang="ts" setup>
defineOptions({
	name: 'login'
});

import { reactive, ref, onMounted, onBeforeUnmount } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useCool } from '/@/cool';
import { useBase } from '/$/base';
import { storage } from '/@/cool/utils';
import { useI18n } from 'vue-i18n';
import PicCaptcha from './components/pic-captcha.vue';

const { refs, setRefs, router, service } = useCool();
const { user, app } = useBase();
const { t } = useI18n();

const saving = ref(false);
const canvasRef = ref<HTMLCanvasElement>();
let animId = 0;

const form = reactive({
	username: storage.get('username') || '',
	password: '',
	captchaId: '',
	verifyCode: ''
});

if (import.meta.env.MODE == 'demo') {
	form.username = 'admin';
	form.password = '123456';
}

// ========== 星河+海洋动画（性能优化版） ==========
interface Star {
	x: number;
	y: number;
	r: number;
	a: number;
	da: number;
	cr: number; cg: number; cb: number; // 内联颜色分量，避免数组访问
}

interface ShootingStar {
	x: number;
	y: number;
	len: number;
	speed: number;
	cosA: number; sinA: number;
	life: number;
	maxLife: number;
}

// 彗星：缓慢移动、带弧形长尾巴和朦胧彗发
interface Comet {
	x: number;
	y: number;
	speed: number;
	cosA: number; sinA: number;
	tailLen: number;
	headR: number;
	curve: number;       // 尾巴弯曲方向 (正/负)
	life: number;
	maxLife: number;
	cr: number; cg: number; cb: number;
	// 尾部粒子
	particles: { rx: number; ry: number; size: number; drift: number; phase: number }[];
}

// 陨石：大而快、带火焰拖尾和碎片
interface Meteorite {
	x: number;
	y: number;
	speed: number;
	cosA: number; sinA: number;
	size: number;
	tailLen: number;
	life: number;
	maxLife: number;
	sparks: { ox: number; oy: number; vx: number; vy: number; life: number }[];
}

interface Nebula {
	x: number; y: number; rx: number; ry: number;
	rot: number; r: number; g: number; b: number; a: number;
	scaleY: number; // 预计算 ry/rx
}

interface DustLane {
	x: number; y: number; rx: number; ry: number;
	rot: number; a: number; a03: number; // 预计算 a*0.3
	scaleY: number;
}

// 银河带密度（初始化时用，不在帧循环中调用）
function milkyWayDensity(px: number, py: number, w: number, h: number): number {
	const startX = 0, startY = h * 0.08;
	const endX = w, endY = h * 0.48;
	const dx = endX - startX, dy = endY - startY;
	const len2 = dx * dx + dy * dy;
	const t = Math.max(0, Math.min(1, ((px - startX) * dx + (py - startY) * dy) / len2));
	const projX = startX + t * dx, projY = startY + t * dy;
	const dist2 = (px - projX) ** 2 + (py - projY) ** 2;
	const bandWidth = h * 0.18;
	if (dist2 > bandWidth * bandWidth) return 0;
	const sigma = bandWidth * 0.4;
	return Math.exp(-dist2 / (2 * sigma * sigma));
}

function initCanvas() {
	const canvas = canvasRef.value;
	if (!canvas) return;
	const ctx = canvas.getContext('2d', { alpha: false })!;
	let w = (canvas.width = window.innerWidth);
	let h = (canvas.height = window.innerHeight);

	// 监听 resize（节流），避免每帧重设尺寸
	let resizePending = false;
	const onResize = () => {
		if (resizePending) return;
		resizePending = true;
		requestAnimationFrame(() => {
			w = canvas.width = window.innerWidth;
			h = canvas.height = window.innerHeight;
			// 重建尺寸相关的缓存渐变
			rebuildGradients();
			resizePending = false;
		});
	};
	window.addEventListener('resize', onResize);

	// 星星颜色库
	const starColors = [
		[200, 210, 255], [170, 190, 255], [255, 240, 220],
		[255, 220, 180], [255, 200, 160], [220, 225, 255], [255, 255, 255],
	];

	// 背景稀疏星
	const bgStars: Star[] = [];
	for (let i = 0; i < 160; i++) {
		const c = starColors[Math.floor(Math.random() * starColors.length)];
		bgStars.push({
			x: Math.random() * w, y: Math.random() * h * 0.7,
			r: Math.random() * 1.4 + 0.3,
			a: Math.random() * 0.6 + 0.2, da: (Math.random() - 0.5) * 0.008,
			cr: c[0], cg: c[1], cb: c[2]
		});
	}
	// 按大小分组：亮星（有星芒）和暗星（无星芒）
	const brightBgStars = bgStars.filter(s => s.r > 1.2);
	const dimBgStars = bgStars.filter(s => s.r <= 1.2);

	// 银河带密集星
	const milkyStars: Star[] = [];
	let placed = 0;
	while (placed < 800) {
		const px = Math.random() * w;
		const py = Math.random() * h * 0.7;
		const density = milkyWayDensity(px, py, w, h);
		if (Math.random() < density) {
			const c = starColors[Math.floor(Math.random() * starColors.length)];
			const sizeBoost = density > 0.7 ? 0.5 : 0;
			milkyStars.push({
				x: px, y: py,
				r: Math.random() * (1.0 + sizeBoost) + 0.2,
				a: Math.random() * 0.5 + density * 0.5,
				da: (Math.random() - 0.5) * 0.006,
				cr: c[0], cg: c[1], cb: c[2]
			});
			placed++;
		}
	}

	// 星云节点
	const nebulae: Nebula[] = [];
	const nebulaColors = [
		[80, 100, 180], [100, 80, 160], [60, 90, 150],
		[90, 70, 140], [70, 110, 170], [110, 90, 160],
	];
	for (let i = 0; i < 30; i++) {
		const t = Math.random();
		const c = nebulaColors[Math.floor(Math.random() * nebulaColors.length)];
		const rx = Math.random() * 120 + 60;
		const ry = Math.random() * 40 + 20;
		nebulae.push({
			x: t * w, y: h * 0.08 + t * h * 0.4 + (Math.random() - 0.5) * h * 0.12,
			rx, ry, rot: -0.38 + (Math.random() - 0.5) * 0.3,
			r: c[0], g: c[1], b: c[2],
			a: Math.random() * 0.04 + 0.02,
			scaleY: ry / rx
		});
	}

	// 暗尘埃带
	const dustLanes: DustLane[] = [];
	for (let i = 0; i < 10; i++) {
		const t = 0.2 + Math.random() * 0.6;
		const rx = Math.random() * 80 + 40;
		const ry = Math.random() * 15 + 5;
		const a = Math.random() * 0.25 + 0.1;
		dustLanes.push({
			x: t * w, y: h * 0.08 + t * h * 0.4 + (Math.random() - 0.5) * h * 0.06,
			rx, ry, rot: -0.38 + (Math.random() - 0.5) * 0.4,
			a, a03: a * 0.3, scaleY: ry / rx
		});
	}

	// 流星
	const shootingStars: ShootingStar[] = [];
	function spawnShootingStar() {
		const angle = (Math.PI / 6) + Math.random() * 0.3;
		shootingStars.push({
			x: Math.random() * w * 0.8, y: Math.random() * h * 0.25,
			len: Math.random() * 80 + 60, speed: Math.random() * 6 + 8,
			cosA: Math.cos(angle), sinA: Math.sin(angle),
			life: 0, maxLife: Math.random() * 40 + 30
		});
	}

	// 彗星 — 缓慢划过天空，柔光弥散尾巴
	const comets: Comet[] = [];
	function spawnComet() {
		const angle = (Math.PI / 7) + Math.random() * 0.25;
		const colors = [
			[140, 200, 255], [180, 220, 255], [160, 210, 230], [170, 200, 240],
		];
		const c = colors[Math.floor(Math.random() * colors.length)];
		// 沿尾巴方向散布粒子
		const particles: Comet['particles'] = [];
		for (let p = 0; p < 40; p++) {
			particles.push({
				rx: Math.random() * 0.95 + 0.05,  // 沿尾巴0~1位置
				ry: (Math.random() - 0.5) * 2,     // 垂直偏移 -1~1
				size: Math.random() * 1.5 + 0.3,
				drift: (Math.random() - 0.5) * 0.3,
				phase: Math.random() * Math.PI * 2,
			});
		}
		comets.push({
			x: -80 + Math.random() * w * 0.2,
			y: Math.random() * h * 0.12 + h * 0.02,
			speed: 2.7 + Math.random() * 2,
			cosA: Math.cos(angle), sinA: Math.sin(angle),
			tailLen: 50 + Math.random() * 50,
			headR: 2.5 + Math.random() * 1.5,
			curve: (Math.random() > 0.5 ? 1 : -1) * (0.08 + Math.random() * 0.12),
			life: 0,
			maxLife: 150 + Math.random() * 120,
			cr: c[0], cg: c[1], cb: c[2],
			particles
		});
	}

	// 陨石 — 快速坠落，带火焰尾巴和飞散碎片
	const meteorites: Meteorite[] = [];
	function spawnMeteorite() {
		const angle = (Math.PI / 4) + Math.random() * 0.4;
		meteorites.push({
			x: Math.random() * w * 0.7 + w * 0.1,
			y: -20,
			speed: 4 + Math.random() * 5,
			cosA: Math.cos(angle), sinA: Math.sin(angle),
			size: 3 + Math.random() * 3,
			tailLen: 50 + Math.random() * 60,
			life: 0,
			maxLife: 60 + Math.random() * 50,
			sparks: []
		});
	}

	// 预构建静态渐变（尺寸相关，resize 时重建）
	let skyGrad: CanvasGradient;
	let reflectGrad: CanvasGradient;
	function rebuildGradients() {
		skyGrad = ctx.createLinearGradient(0, 0, 0, h);
		skyGrad.addColorStop(0, '#060810');
		skyGrad.addColorStop(0.25, '#0a0f1c');
		skyGrad.addColorStop(0.5, '#0e1528');
		skyGrad.addColorStop(0.68, '#121a34');
		skyGrad.addColorStop(0.78, '#151f40');
		skyGrad.addColorStop(1, '#0a0f1c');
	}
	rebuildGradients();

	// ===== 北斗七星 =====
	// 顺序：摇光→开阳→玉衡→天权→天玑→天璇→天枢（一条连续链，不闭合）
	// 缩小一倍，放在登录框左侧区域，寓意指明方向
	const dipperRelative = [
		{ x: 0.04, y: 0.26, name: '摇光' },
		{ x: 0.10, y: 0.21, name: '开阳' },
		{ x: 0.15, y: 0.19, name: '玉衡' },
		{ x: 0.21, y: 0.21, name: '天权' },
		{ x: 0.24, y: 0.28, name: '天玑' },
		{ x: 0.32, y: 0.24, name: '天璇' },
		{ x: 0.33, y: 0.16, name: '天枢' },
	];
	// 连线：一条链 0→1→2→3→4→5→6，不闭合
	const dipperLines: [number, number][] = [
		[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6],
	];
	// 实际像素坐标（初始化和resize时更新）
	let dipperStars = dipperRelative.map(p => ({ x: p.x * w, y: p.y * h, name: p.name }));
	const origRebuild = rebuildGradients;
	rebuildGradients = () => {
		origRebuild();
		dipperStars = dipperRelative.map(p => ({ x: p.x * w, y: p.y * h, name: p.name }));
	};

	// 预计算 RGBA 颜色字符串缓存（alpha 0~255 级别的查找表）
	// 减少每帧拼接大量 `rgba(...)` 字符串
	function rgba(r: number, g: number, b: number, a: number): string {
		return `rgba(${r},${g},${b},${a.toFixed(3)})`;
	}

	// 网格参数
	const gridRows = 18; // 从24减到18
	const gridCols = 20; // 从28减到20
	const gridSegments = 16; // 纵线段数从24减到16

	let tick = 0;

	function draw() {
		// 先调度下一帧，确保即使本帧出错动画也不会停
		animId = requestAnimationFrame(draw);
		tick++;

		// 不要每帧重设 canvas 尺寸！这是卡顿的主因
		// 只用 clearRect 清屏
		ctx.clearRect(0, 0, w, h);

		// 天空渐变
		ctx.fillStyle = skyGrad;
		ctx.fillRect(0, 0, w, h);

		// 银河主光带
		ctx.save();
		const bandCx = w * 0.5, bandCy = h * 0.28;
		ctx.translate(bandCx, bandCy);
		ctx.rotate(-0.4);
		const coreGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, w * 0.55);
		coreGrad.addColorStop(0, 'rgba(140,150,210,0.10)');
		coreGrad.addColorStop(0.15, 'rgba(120,130,200,0.07)');
		coreGrad.addColorStop(0.4, 'rgba(90,100,170,0.03)');
		coreGrad.addColorStop(1, 'transparent');
		ctx.fillStyle = coreGrad;
		ctx.fillRect(-w * 0.7, -h * 0.13, w * 1.4, h * 0.26);
		const outerGrad = ctx.createRadialGradient(0, 0, w * 0.1, 0, 0, w * 0.7);
		outerGrad.addColorStop(0, 'rgba(100,110,180,0.04)');
		outerGrad.addColorStop(0.5, 'rgba(80,90,160,0.015)');
		outerGrad.addColorStop(1, 'transparent');
		ctx.fillStyle = outerGrad;
		ctx.fillRect(-w * 0.8, -h * 0.22, w * 1.6, h * 0.44);
		ctx.restore();

		// 星云雾气 — 合并批量绘制
		const tickSlow = tick * 0.008;
		for (const nb of nebulae) {
			ctx.save();
			ctx.translate(nb.x, nb.y);
			ctx.rotate(nb.rot);
			ctx.scale(1, nb.scaleY);
			const ng = ctx.createRadialGradient(0, 0, 0, 0, 0, nb.rx);
			const pulse = nb.a + Math.sin(tickSlow + nb.x * 0.01) * 0.008;
			ng.addColorStop(0, rgba(nb.r, nb.g, nb.b, pulse));
			ng.addColorStop(0.5, rgba(nb.r, nb.g, nb.b, pulse * 0.4));
			ng.addColorStop(1, 'transparent');
			ctx.fillStyle = ng;
			ctx.fillRect(-nb.rx, -nb.rx, nb.rx * 2, nb.rx * 2);
			ctx.restore();
		}

		// 暗尘埃带
		for (const dl of dustLanes) {
			ctx.save();
			ctx.translate(dl.x, dl.y);
			ctx.rotate(dl.rot);
			ctx.scale(1, dl.scaleY);
			const dg = ctx.createRadialGradient(0, 0, 0, 0, 0, dl.rx);
			dg.addColorStop(0, rgba(6, 8, 16, dl.a));
			dg.addColorStop(0.6, rgba(6, 8, 16, dl.a03));
			dg.addColorStop(1, 'transparent');
			ctx.fillStyle = dg;
			ctx.fillRect(-dl.rx, -dl.rx, dl.rx * 2, dl.rx * 2);
			ctx.restore();
		}

		// 银河带密集星 — 批量绘制同 alpha 区间的星减少 fillStyle 切换
		for (const s of milkyStars) {
			s.a += s.da;
			if (s.a > 0.95 || s.a < 0.15) s.da = -s.da;
			ctx.fillStyle = rgba(s.cr, s.cg, s.cb, s.a);
			ctx.fillRect(s.x - s.r, s.y - s.r, s.r * 2, s.r * 2);
			// 用 fillRect 替代 arc — 小圆点视觉差异极小但性能大幅提升
		}

		// 背景暗星（无星芒，用 fillRect）
		for (const s of dimBgStars) {
			s.a += s.da;
			if (s.a > 0.85 || s.a < 0.1) s.da = -s.da;
			ctx.fillStyle = rgba(s.cr, s.cg, s.cb, s.a);
			ctx.fillRect(s.x - s.r, s.y - s.r, s.r * 2, s.r * 2);
		}

		// 背景亮星（有十字星芒）
		for (const s of brightBgStars) {
			s.a += s.da;
			if (s.a > 0.85 || s.a < 0.1) s.da = -s.da;
			ctx.fillStyle = rgba(s.cr, s.cg, s.cb, s.a);
			ctx.beginPath();
			ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
			ctx.fill();
			ctx.strokeStyle = rgba(s.cr, s.cg, s.cb, s.a * 0.2);
			ctx.lineWidth = 0.5;
			ctx.beginPath();
			ctx.moveTo(s.x - s.r * 3, s.y);
			ctx.lineTo(s.x + s.r * 3, s.y);
			ctx.moveTo(s.x, s.y - s.r * 3);
			ctx.lineTo(s.x, s.y + s.r * 3);
			ctx.stroke();
		}

		// ===== 北斗七星 =====
		// 连线动画周期：每 900 帧(~15秒) 一个循环
		// 0~250: 逐段画线 + 逐个显示星名
		// 250~550: 全部保持（呼吸）
		// 550~900: 连线和星名一起渐隐
		const dipperCycle = 900;
		const dipperPhase = tick % dipperCycle;
		const totalSegments = dipperLines.length;

		// 连线/名字的总透明度
		let lineAlpha = 0;
		let nameAlpha = 0;
		let drawProgress = 1; // 逐段绘制进度 0~1

		if (dipperPhase < 250) {
			drawProgress = dipperPhase / 250;
			lineAlpha = 0.7;
			nameAlpha = 0.85;
		} else if (dipperPhase < 550) {
			lineAlpha = 0.6 + Math.sin((dipperPhase - 250) * 0.015) * 0.1;
			nameAlpha = 0.75 + Math.sin((dipperPhase - 250) * 0.015) * 0.1;
		} else {
			const fadeOut = 1 - (dipperPhase - 550) / 350;
			lineAlpha = 0.7 * fadeOut;
			nameAlpha = 0.85 * fadeOut;
		}

		// 绘制七颗星（始终显示，最亮的星）
		for (let si = 0; si < dipperStars.length; si++) {
			const ds = dipperStars[si];
			const twinkle = 0.8 + Math.sin(tick * 0.015 + si * 1.7) * 0.15;
			const starR = 1.5 + Math.sin(tick * 0.01 + si * 2.3) * 0.2;

			// 外层辉光
			const glowR = starR * 5;
			const gg = ctx.createRadialGradient(ds.x, ds.y, 0, ds.x, ds.y, glowR);
			gg.addColorStop(0, rgba(180, 200, 255, twinkle * 0.12));
			gg.addColorStop(0.3, rgba(160, 185, 255, twinkle * 0.04));
			gg.addColorStop(1, 'transparent');
			ctx.fillStyle = gg;
			ctx.beginPath();
			ctx.arc(ds.x, ds.y, glowR, 0, Math.PI * 2);
			ctx.fill();

			// 星体
			ctx.beginPath();
			ctx.arc(ds.x, ds.y, starR, 0, Math.PI * 2);
			ctx.fillStyle = rgba(220, 230, 255, twinkle);
			ctx.fill();

			// 十字星芒
			const spikeLen = starR * 4;
			ctx.strokeStyle = rgba(200, 215, 255, twinkle * 0.25);
			ctx.lineWidth = 0.5;
			ctx.beginPath();
			ctx.moveTo(ds.x - spikeLen, ds.y);
			ctx.lineTo(ds.x + spikeLen, ds.y);
			ctx.moveTo(ds.x, ds.y - spikeLen);
			ctx.lineTo(ds.x, ds.y + spikeLen);
			ctx.stroke();
			// 45度副星芒
			const spike2 = spikeLen * 0.4;
			ctx.strokeStyle = rgba(200, 215, 255, twinkle * 0.12);
			ctx.beginPath();
			ctx.moveTo(ds.x - spike2 * 0.707, ds.y - spike2 * 0.707);
			ctx.lineTo(ds.x + spike2 * 0.707, ds.y + spike2 * 0.707);
			ctx.moveTo(ds.x + spike2 * 0.707, ds.y - spike2 * 0.707);
			ctx.lineTo(ds.x - spike2 * 0.707, ds.y + spike2 * 0.707);
			ctx.stroke();
		}

		// 连线 + 星名
		if (lineAlpha > 0.01) {
			// 连线
			ctx.lineCap = 'round';
			const drawSegCount = dipperPhase < 250
				? Math.ceil(drawProgress * totalSegments)
				: totalSegments;

			for (let li = 0; li < drawSegCount && li < dipperLines.length; li++) {
				const [a, b] = dipperLines[li];
				const sa = dipperStars[a];
				const sb = dipperStars[b];

				// 逐段绘制：最后一段可能只画一部分
				let segProgress = 1;
				if (dipperPhase < 250) {
					const segStart = li / totalSegments;
					const segEnd = (li + 1) / totalSegments;
					segProgress = Math.min(1, Math.max(0, (drawProgress - segStart) / (segEnd - segStart)));
				}

				const ex = sa.x + (sb.x - sa.x) * segProgress;
				const ey = sa.y + (sb.y - sa.y) * segProgress;

				// 外层发光线
				ctx.beginPath();
				ctx.moveTo(sa.x, sa.y);
				ctx.lineTo(ex, ey);
				ctx.strokeStyle = rgba(80, 140, 240, lineAlpha * 0.10);
				ctx.lineWidth = 3;
				ctx.stroke();

				// 中层
				ctx.beginPath();
				ctx.moveTo(sa.x, sa.y);
				ctx.lineTo(ex, ey);
				ctx.strokeStyle = rgba(140, 185, 255, lineAlpha * 0.35);
				ctx.lineWidth = 1;
				ctx.stroke();

				// 芯线
				ctx.beginPath();
				ctx.moveTo(sa.x, sa.y);
				ctx.lineTo(ex, ey);
				ctx.strokeStyle = rgba(200, 220, 255, lineAlpha * 0.5);
				ctx.lineWidth = 0.4;
				ctx.stroke();
			}
			ctx.lineCap = 'butt';

			// 星名标注（逐个出现，跟随连线进度）
			if (nameAlpha > 0.02) {
				ctx.font = '9px "Microsoft YaHei", "PingFang SC", sans-serif';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'bottom';

				for (let si = 0; si < dipperStars.length; si++) {
					const ds = dipperStars[si];

					// 计算这颗星的名字何时出现
					// 斗勺4颗(index 0-3)跟随前4段线，斗柄3颗(index 4-6)跟随后3段线
					let starShowProgress: number;
					if (si <= 3) {
						// 斗勺星：第si段线开始画时出现
						starShowProgress = si / totalSegments;
					} else {
						// 斗柄星：第(si)段线画到终点时出现(因为斗柄是3-4,4-5,5-6)
						starShowProgress = si / totalSegments;
					}

					let thisNameAlpha = nameAlpha;
					if (dipperPhase < 250) {
						// 逐个出现：当drawProgress超过这颗星的阈值时渐入
						const appear = Math.min(1, Math.max(0, (drawProgress - starShowProgress) * totalSegments));
						thisNameAlpha = nameAlpha * appear;
					}

					if (thisNameAlpha < 0.02) continue;

					// 名字位置：星星正上方
					const nameY = ds.y - 10;

					// 文字发光（先画一层模糊底色）
					ctx.fillStyle = rgba(100, 160, 255, thisNameAlpha * 0.3);
					ctx.fillText(ds.name, ds.x, nameY - 1);
					ctx.fillText(ds.name, ds.x + 1, nameY);
					ctx.fillText(ds.name, ds.x - 1, nameY);

					// 文字本体
					ctx.fillStyle = rgba(200, 220, 255, thisNameAlpha);
					ctx.fillText(ds.name, ds.x, nameY);
				}
			}
		}

		// 流星
		if (tick % 120 === 0 && Math.random() > 0.3) spawnShootingStar();
		for (let i = shootingStars.length - 1; i >= 0; i--) {
			const ss = shootingStars[i];
			ss.life++;
			ss.x += ss.cosA * ss.speed;
			ss.y += ss.sinA * ss.speed;
			const fade = 1 - ss.life / ss.maxLife;
			const tailX = ss.x - ss.cosA * ss.len;
			const tailY = ss.y - ss.sinA * ss.len;
			const grad = ctx.createLinearGradient(tailX, tailY, ss.x, ss.y);
			grad.addColorStop(0, 'transparent');
			grad.addColorStop(1, rgba(220, 230, 255, fade * 0.9));
			ctx.strokeStyle = grad;
			ctx.lineWidth = 1.5;
			ctx.beginPath();
			ctx.moveTo(tailX, tailY);
			ctx.lineTo(ss.x, ss.y);
			ctx.stroke();
			if (ss.life >= ss.maxLife) shootingStars.splice(i, 1);
		}

		// ===== 彗星 — 柔光弥散，多层叠加 =====
		if (tick % 600 === 0 && Math.random() > 0.4 && comets.length < 2) spawnComet();
		for (let i = comets.length - 1; i >= 0; i--) {
			const cm = comets[i];
			cm.life++;
			if (cm.life >= cm.maxLife) { comets.splice(i, 1); continue; }
			cm.x += cm.cosA * cm.speed;
			cm.y += cm.sinA * cm.speed;

			const fadeIn = Math.min(1, cm.life / 80);
			const fadeOut = Math.max(0, 1 - (cm.life - cm.maxLife + 80) / 80);
			const fade = Math.min(fadeIn, fadeOut);
			if (fade < 0.01) continue;

			// 计算尾巴上任意 t(0~1) 处的坐标（带弯曲）
			// perpX/perpY: 垂直于运动方向的单位向量
			const perpX = -cm.sinA;
			const perpY = cm.cosA;
			function tailPos(t: number): [number, number] {
				// 弯曲量随t²增长（越远越弯）
				const bend = cm.curve * cm.tailLen * t * t;
				return [
					cm.x - cm.cosA * cm.tailLen * t + perpX * bend,
					cm.y - cm.sinA * cm.tailLen * t + perpY * bend
				];
			}

			// 多层柔光尾巴（由粗到细，由淡到亮，叠加产生弥散效果）
			ctx.lineCap = 'round';
			const layers = [
				{ width: 16, alpha: 0.02 },  // 最外层：极淡极宽
				{ width: 10, alpha: 0.04 },
				{ width: 6, alpha: 0.08 },
				{ width: 3, alpha: 0.14 },
				{ width: 1.5, alpha: 0.25 },  // 核心：亮而细
			];
			for (const layer of layers) {
				ctx.beginPath();
				const [sx, sy] = tailPos(0);
				ctx.moveTo(sx, sy);
				// 用二次贝塞尔曲线绘制平滑尾巴
				const steps = 6;
				for (let s = 1; s <= steps; s++) {
					const t = s / steps;
					const tMid = (s - 0.5) / steps;
					const [cpx, cpy] = tailPos(tMid);
					const [ex, ey] = tailPos(t);
					ctx.quadraticCurveTo(cpx, cpy, ex, ey);
				}
				// 渐变：头部亮 → 尾部透明
				const [endX, endY] = tailPos(1);
				const grad = ctx.createLinearGradient(sx, sy, endX, endY);
				grad.addColorStop(0, rgba(cm.cr, cm.cg, cm.cb, fade * layer.alpha));
				grad.addColorStop(0.6, rgba(cm.cr, cm.cg, cm.cb, fade * layer.alpha * 0.3));
				grad.addColorStop(1, 'transparent');
				ctx.strokeStyle = grad;
				ctx.lineWidth = layer.width * fade;
				ctx.stroke();
			}
			ctx.lineCap = 'butt';

			// 尾部粒子（沿尾巴散布，闪烁漂移）
			for (const p of cm.particles) {
				const [bx, by] = tailPos(p.rx);
				// 粒子在垂直方向漂移
				const spread = (8 + p.rx * 18) * fade; // 越远散布越宽
				const px = bx + perpX * p.ry * spread + Math.sin(cm.life * 0.03 + p.phase) * p.drift * 5;
				const py = by + perpY * p.ry * spread + Math.cos(cm.life * 0.025 + p.phase) * p.drift * 5;
				const pAlpha = fade * (1 - p.rx) * (0.3 + Math.sin(cm.life * 0.05 + p.phase) * 0.2);
				if (pAlpha < 0.03) continue;
				ctx.fillStyle = rgba(cm.cr, cm.cg, cm.cb, pAlpha);
				ctx.fillRect(px - p.size, py - p.size, p.size * 2, p.size * 2);
			}

			// 彗发（头部多层辉光）
			const comaLayers = [35, 18, 8];
			const comaAlphas = [0.03, 0.08, 0.18];
			for (let ci = 0; ci < 3; ci++) {
				const r = Math.max(1, comaLayers[ci] * fade);
				const cg = ctx.createRadialGradient(cm.x, cm.y, 0, cm.x, cm.y, r);
				cg.addColorStop(0, rgba(cm.cr, cm.cg, cm.cb, fade * comaAlphas[ci]));
				cg.addColorStop(1, 'transparent');
				ctx.fillStyle = cg;
				ctx.beginPath();
				ctx.arc(cm.x, cm.y, r, 0, Math.PI * 2);
				ctx.fill();
			}

			// 彗核（小而亮的白点）
			const hr = Math.max(0.3, cm.headR * fade);
			ctx.beginPath();
			ctx.arc(cm.x, cm.y, hr, 0, Math.PI * 2);
			ctx.fillStyle = rgba(255, 255, 255, fade * 0.85);
			ctx.fill();
		}

		// ===== 陨石 — 火焰坠落 =====
		if (tick % 300 === 0 && Math.random() > 0.5 && meteorites.length < 2) spawnMeteorite();
		for (let i = meteorites.length - 1; i >= 0; i--) {
			const mt = meteorites[i];
			mt.life++;
			if (mt.life >= mt.maxLife) { meteorites.splice(i, 1); continue; }
			mt.x += mt.cosA * mt.speed;
			mt.y += mt.sinA * mt.speed;
			const fade = Math.max(0.01, 1 - mt.life / mt.maxLife);

			// 抛出火花碎片
			if (mt.life % 3 === 0 && fade > 0.2) {
				mt.sparks.push({
					ox: 0, oy: 0,
					vx: (Math.random() - 0.5) * 3 - mt.cosA * 1.5,
					vy: (Math.random() - 0.5) * 3 - mt.sinA * 1.5,
					life: 15 + Math.random() * 15
				});
			}

			// 绘制碎片
			for (let si = mt.sparks.length - 1; si >= 0; si--) {
				const sp = mt.sparks[si];
				sp.ox += sp.vx;
				sp.oy += sp.vy;
				sp.vy += 0.08;
				sp.life--;
				if (sp.life <= 0) { mt.sparks.splice(si, 1); continue; }
				const sf = sp.life / 25;
				ctx.fillStyle = rgba(255, (120 + sf * 120) | 0, (40 * sf) | 0, sf * fade * 0.7);
				const sr = 0.5 + sf * 1.2;
				ctx.fillRect(mt.x + sp.ox - sr, mt.y + sp.oy - sr, sr * 2, sr * 2);
			}

			// 多层火焰尾（柔光叠加）
			ctx.lineCap = 'round';
			const fireLayers = [
				{ w: mt.size * 5, len: 1.0, r: 200, g: 60, b: 20, a: 0.06 },   // 最外：暗红宽辉
				{ w: mt.size * 3, len: 0.9, r: 255, g: 140, b: 40, a: 0.12 },   // 橙
				{ w: mt.size * 2, len: 0.8, r: 255, g: 200, b: 80, a: 0.2 },    // 亮黄
				{ w: mt.size * 1, len: 0.6, r: 255, g: 250, b: 220, a: 0.35 },  // 白芯
			];
			for (const fl of fireLayers) {
				const layerW = fl.w * fade;
				if (layerW < 0.3) continue;
				const endX = mt.x - mt.cosA * mt.tailLen * fl.len;
				const endY = mt.y - mt.sinA * mt.tailLen * fl.len;
				const grad = ctx.createLinearGradient(endX, endY, mt.x, mt.y);
				grad.addColorStop(0, 'transparent');
				grad.addColorStop(0.4, rgba(fl.r, fl.g, fl.b, fl.a * fade * 0.3));
				grad.addColorStop(1, rgba(fl.r, fl.g, fl.b, fl.a * fade));
				ctx.beginPath();
				ctx.moveTo(endX, endY);
				ctx.lineTo(mt.x, mt.y);
				ctx.strokeStyle = grad;
				ctx.lineWidth = layerW;
				ctx.stroke();
			}
			ctx.lineCap = 'butt';

			// 头部辉光
			const glowR = Math.max(1, mt.size * 3.5 * fade);
			const gg = ctx.createRadialGradient(mt.x, mt.y, 0, mt.x, mt.y, glowR);
			gg.addColorStop(0, rgba(255, 240, 200, fade * 0.7));
			gg.addColorStop(0.3, rgba(255, 180, 60, fade * 0.3));
			gg.addColorStop(0.7, rgba(255, 100, 30, fade * 0.08));
			gg.addColorStop(1, 'transparent');
			ctx.fillStyle = gg;
			ctx.beginPath();
			ctx.arc(mt.x, mt.y, glowR, 0, Math.PI * 2);
			ctx.fill();

			// 核心亮点
			const cr = Math.max(0.3, mt.size * 0.5 * fade);
			ctx.beginPath();
			ctx.arc(mt.x, mt.y, cr, 0, Math.PI * 2);
			ctx.fillStyle = rgba(255, 255, 240, fade * 0.9);
			ctx.fill();
		}

		// 海面基线
		const seaY = h * 0.68;
		const time = tick * 0.012;

		// 预计算波浪三角函数的时间分量（避免在内层循环重复计算）
		const tA = time * 0.6, tB = time * 0.9 + 1.3, tC = time * 1.4 + 3.7, tD = time * 0.3;

		// 通用波浪函数（使用预计算的时间分量）
		function waveAt(x: number, baseY: number, depth: number): number {
			return baseY
				+ Math.sin(x * 0.005 + tA) * (18 - depth * 2)
				+ Math.sin(x * 0.011 + tB) * (10 - depth)
				+ Math.sin(x * 0.023 + tC) * (5 - depth * 0.5)
				+ Math.sin(x * 0.003 + tD) * (12 - depth);
		}

		// 海面底色渐变
		reflectGrad = ctx.createLinearGradient(0, seaY - 20, 0, h);
		reflectGrad.addColorStop(0, 'rgba(8,14,30,0.0)');
		reflectGrad.addColorStop(0.06, 'rgba(6,12,28,0.4)');
		reflectGrad.addColorStop(0.3, 'rgba(4,8,20,0.75)');
		reflectGrad.addColorStop(1, 'rgba(2,4,12,0.95)');
		ctx.fillStyle = reflectGrad;
		ctx.fillRect(0, seaY - 25, w, h - seaY + 25);

		// 波浪填充层（步长从3增到5，减少lineTo调用）
		const waveLayerData = [
			[0, 0, 15, 40, 110, 0.35],
			[18, 1, 12, 35, 95, 0.3],
			[38, 2, 8, 28, 75, 0.25],
			[60, 3, 5, 20, 55, 0.2],
		];
		for (const [offset, depth, cr, cg, cb, ca] of waveLayerData) {
			ctx.beginPath();
			ctx.moveTo(0, h);
			for (let x = 0; x <= w; x += 5) {
				ctx.lineTo(x, waveAt(x, seaY + offset, depth));
			}
			ctx.lineTo(w, h);
			ctx.closePath();
			ctx.fillStyle = rgba(cr, cg, cb, ca);
			ctx.fill();
		}

		// 波峰高光（步长从2增到4）
		for (let li = 0; li < 3; li++) {
			const baseOffset = li * 22;
			ctx.beginPath();
			let started = false;
			for (let x = 0; x <= w; x += 4) {
				const y = waveAt(x, seaY + baseOffset, li);
				const yNext = waveAt(x + 6, seaY + baseOffset, li);
				if (yNext > y) {
					if (!started) { ctx.moveTo(x, y); started = true; }
					else ctx.lineTo(x, y);
				} else {
					started = false;
				}
			}
			ctx.strokeStyle = rgba(120, 180, 255, 0.12 - li * 0.03);
			ctx.lineWidth = 1.2 - li * 0.3;
			ctx.stroke();
		}

		// ===== 一叶扁舟 — 在数字海洋中毅力前行 =====
		// 小舟缓缓从左向右移动，到达右边后回到左边（正弦平滑）
		const boatCycleLen = w + 200; // 移动总距离
		const boatSpeed = 0.3;
		const boatRawX = (tick * boatSpeed) % boatCycleLen - 100; // -100 ~ w+100
		const boatX = boatRawX;
		const boatWaveY = waveAt(boatX, seaY, 0); // 跟随第一层海浪
		// 小舟随波浪倾斜：取前后两点算斜率
		const boatWaveYL = waveAt(boatX - 8, seaY, 0);
		const boatWaveYR = waveAt(boatX + 8, seaY, 0);
		const boatTilt = Math.atan2(boatWaveYR - boatWaveYL, 16); // 倾斜角

		// 只在屏幕可见范围内绘制
		if (boatX > -60 && boatX < w + 60) {
			ctx.save();
			ctx.translate(boatX, boatWaveY - 3); // 浮在水面上一点
			ctx.rotate(boatTilt);

			const boatScale = 1.8; // 小舟缩放
			ctx.scale(boatScale, boatScale);

			// 船身（弯月形，深色剪影）
			ctx.beginPath();
			ctx.moveTo(-22, 0);
			ctx.quadraticCurveTo(-18, -6, 0, -5);
			ctx.quadraticCurveTo(18, -6, 24, -2);
			ctx.quadraticCurveTo(20, 4, 0, 5);
			ctx.quadraticCurveTo(-18, 4, -22, 0);
			ctx.closePath();
			ctx.fillStyle = rgba(15, 25, 50, 0.85);
			ctx.fill();
			// 船身边缘高光
			ctx.strokeStyle = rgba(80, 140, 220, 0.25);
			ctx.lineWidth = 0.8;
			ctx.stroke();

			// 桅杆
			ctx.beginPath();
			ctx.moveTo(2, -5);
			ctx.lineTo(2, -32);
			ctx.strokeStyle = rgba(40, 60, 100, 0.8);
			ctx.lineWidth = 1.2;
			ctx.stroke();

			// 帆（三角帆，微微鼓起）
			ctx.beginPath();
			ctx.moveTo(2, -30);
			ctx.quadraticCurveTo(18 + Math.sin(tick * 0.02) * 2, -18, 2, -7);
			ctx.lineTo(2, -30);
			ctx.closePath();
			// 帆的渐变（上亮下暗，半透明）
			const sailGrad = ctx.createLinearGradient(2, -30, 2, -7);
			sailGrad.addColorStop(0, rgba(140, 170, 220, 0.35));
			sailGrad.addColorStop(0.5, rgba(100, 140, 200, 0.25));
			sailGrad.addColorStop(1, rgba(60, 100, 170, 0.15));
			ctx.fillStyle = sailGrad;
			ctx.fill();
			ctx.strokeStyle = rgba(100, 150, 220, 0.2);
			ctx.lineWidth = 0.5;
			ctx.stroke();

			// 船上人影（极简剪影）
			ctx.beginPath();
			// 身体
			ctx.moveTo(-2, -5);
			ctx.lineTo(-2, -12);
			// 头
			ctx.arc(-2, -14, 2, 0, Math.PI * 2);
			ctx.fillStyle = rgba(20, 35, 60, 0.75);
			ctx.fill();
			// 身体线
			ctx.beginPath();
			ctx.moveTo(-2, -5);
			ctx.lineTo(-2, -12);
			ctx.strokeStyle = rgba(20, 35, 60, 0.7);
			ctx.lineWidth = 1.5;
			ctx.stroke();

			ctx.restore();

			// 船在水面的倒影（淡淡的）
			ctx.save();
			ctx.translate(boatX, boatWaveY + 4);
			ctx.rotate(-boatTilt * 0.3);
			ctx.scale(boatScale * 0.9, -boatScale * 0.4); // 压扁、翻转
			ctx.globalAlpha = 0.12;
			// 简化倒影：只画船身轮廓
			ctx.beginPath();
			ctx.moveTo(-22, 0);
			ctx.quadraticCurveTo(-18, -6, 0, -5);
			ctx.quadraticCurveTo(18, -6, 24, -2);
			ctx.quadraticCurveTo(20, 4, 0, 5);
			ctx.quadraticCurveTo(-18, 4, -22, 0);
			ctx.closePath();
			ctx.fillStyle = rgba(80, 140, 220, 0.5);
			ctx.fill();
			ctx.globalAlpha = 1;
			ctx.restore();

			// 船头划过的微小水花
			const splashAlpha = 0.08 + Math.sin(tick * 0.1) * 0.03;
			for (let sp = 0; sp < 3; sp++) {
				const spx = boatX + (14 * boatScale) + sp * 4 + Math.sin(tick * 0.15 + sp) * 2;
				const spy = boatWaveY + Math.sin(tick * 0.12 + sp * 2) * 2;
				ctx.fillStyle = rgba(150, 200, 255, splashAlpha * (1 - sp * 0.3));
				ctx.fillRect(spx - 1, spy - 0.5, 2, 1);
			}
		}

		// ===== 赛博朋克透视网格（纯正弦驱动，无跳变） =====
		const gridAlpha = 0.28 + Math.sin(time * 0.3) * 0.05;
		const vanishY = seaY - 5;
		const vanishX = w * 0.5;
		const gridBottom = h + 20;

		// 网格的波浪时间参数预计算
		const gTime06 = time * 0.6;
		const gTime10 = time * 1.0;

		// 横线 — 不再用 gridFlow 偏移行位置，改用波浪本身的 time 驱动流动感
		const halfCols = gridCols / 2;
		for (let i = 0; i <= gridRows; i++) {
			const rowT = i / gridRows;
			const t2 = rowT * rowT;
			const lineAlpha = gridAlpha * (0.05 + rowT * 0.95);
			const glow = rowT > 0.5 ? (rowT - 0.5) * 0.5 : 0;
			const basePersp = vanishY + (gridBottom - vanishY) * t2;
			const wAmp = rowT * 20;
			const phaseI = i * 0.4;
			const phaseI2 = i * 0.7;

			ctx.beginPath();
			for (let x = 0; x <= w; x += 10) {
				const wy = basePersp
					+ Math.sin(x * 0.005 + gTime06 + phaseI) * wAmp
					+ Math.sin(x * 0.013 + gTime10 + phaseI2) * wAmp * 0.4;
				if (x === 0) ctx.moveTo(x, wy);
				else ctx.lineTo(x, wy);
			}
			ctx.strokeStyle = rgba(0, 155 + (glow * 200) | 0, 215 + (glow * 40) | 0, lineAlpha);
			ctx.lineWidth = 0.4 + rowT * 1.0;
			ctx.stroke();
		}

		// 纵线
		for (let j = -halfCols; j <= halfCols; j++) {
			const ratio = j / halfCols;
			const topX = vanishX + ratio * 25;
			const bottomX = vanishX + ratio * (w * 0.72);
			const lineAlpha = gridAlpha * (1 - Math.abs(ratio) * 0.45) * 0.6;
			const phaseJ = j * 0.3;
			const phaseJ2 = j * 0.5;

			ctx.beginPath();
			for (let s = 0; s <= gridSegments; s++) {
				const st = s / gridSegments;
				const st2 = st * st;
				const sx = topX + (bottomX - topX) * st2;
				const sy = vanishY + (gridBottom - vanishY) * st2
					+ Math.sin(sx * 0.005 + gTime06 + phaseJ) * st * 20
					+ Math.sin(sx * 0.013 + gTime10 + phaseJ2) * st * 8;
				if (s === 0) ctx.moveTo(sx, sy);
				else ctx.lineTo(sx, sy);
			}
			ctx.strokeStyle = rgba(0, 165, 225, lineAlpha);
			ctx.lineWidth = 0.3 + (1 - Math.abs(ratio)) * 0.5;
			ctx.stroke();
		}

		// 网格交叉点亮点
		for (let i = 3; i <= gridRows; i += 4) {
			const rowT = i / gridRows;
			const t2 = rowT * rowT;
			const basePersp = vanishY + (gridBottom - vanishY) * t2;
			const wAmp = rowT * 20;
			const phaseI = i * 0.4;
			const phaseI2 = i * 0.7;

			for (let j = -halfCols; j <= halfCols; j += 4) {
				const ratio = j / halfCols;
				const nodeX = vanishX + ratio * (w * 0.72) * t2;
				const nodeY = basePersp
					+ Math.sin(nodeX * 0.005 + gTime06 + phaseI) * wAmp
					+ Math.sin(nodeX * 0.013 + gTime10 + phaseI2) * wAmp * 0.4;
				if (nodeY < vanishY || nodeY > h) continue;

				// 用正弦缓慢闪烁，不同节点频率略有差异，避免同步感
				const nodeAlpha = (0.25 + Math.sin(time * 0.7 + i * 3.7 + j * 2.3) * 0.25) * rowT;
				if (nodeAlpha < 0.12) continue;

				ctx.fillStyle = rgba(0, 220, 255, nodeAlpha);
				ctx.fillRect(nodeX - 1.5, nodeY - 1.5, 3, 3);

				if (nodeAlpha > 0.3) {
					ctx.fillStyle = rgba(0, 180, 240, nodeAlpha * 0.12);
					const r = 3 + rowT * 4;
					ctx.fillRect(nodeX - r, nodeY - r, r * 2, r * 2);
				}
			}
		}

		// ===== 数据流光线（正弦驱动，无跳变） =====
		// 每条光线用 sin 做位置，cos 做透明度 — 自然往返，永不跳变
		for (let i = 0; i < 8; i++) {
			// 每条光线不同速度和相位偏移，避免同步
			const speed = 0.15 + i * 0.04;
			const phaseOff = i * 1.9;
			// sin → -1~1 映射到 0~1 做位置
			const sinVal = Math.sin(time * speed + phaseOff);
			const progress = sinVal * 0.5 + 0.5; // 0~1 平滑往返
			// cos 做透明度（当 sin 在两端时 cos 接近0，光线自然淡出）
			const cosVal = Math.cos(time * speed + phaseOff);
			const trailAlpha = Math.abs(cosVal) * 0.35;
			if (trailAlpha < 0.03) continue; // 太淡就跳过

			const rangeStart = w * (0.05 + (i % 5) * 0.18);
			const rangeLen = w * (0.2 + (i % 3) * 0.06);
			const trailX = rangeStart + rangeLen * progress;
			const trailLen = 35 + i * 7;
			const ty = seaY + 20 + (h - seaY - 20) * ((i % 3) * 0.25 + 0.15);
			const waveY = Math.sin(trailX * 0.01 + time) * 3;
			const y = ty + waveY;

			const trailGrad = ctx.createLinearGradient(trailX - trailLen, 0, trailX, 0);
			trailGrad.addColorStop(0, 'transparent');
			trailGrad.addColorStop(0.7, rgba(0, 200, 255, trailAlpha * 0.5));
			trailGrad.addColorStop(1, rgba(0, 240, 255, trailAlpha));
			ctx.strokeStyle = trailGrad;
			ctx.lineWidth = 1.2;
			ctx.beginPath();
			ctx.moveTo(trailX - trailLen, y);
			ctx.lineTo(trailX, y);
			ctx.stroke();

			ctx.fillStyle = rgba(150, 255, 255, trailAlpha);
			ctx.fillRect(trailX - 2, y - 2, 4, 4);
		}

		// 地平线发光边缘
		const edgeGrad = ctx.createLinearGradient(0, seaY - 8, 0, seaY + 15);
		edgeGrad.addColorStop(0, 'transparent');
		edgeGrad.addColorStop(0.4, rgba(0, 140, 200, 0.08 + Math.sin(time * 0.5) * 0.03));
		edgeGrad.addColorStop(0.6, rgba(0, 180, 240, 0.12 + Math.sin(time * 0.7) * 0.04));
		edgeGrad.addColorStop(1, 'transparent');
		ctx.fillStyle = edgeGrad;
		ctx.fillRect(0, seaY - 8, w, 23);
	}

	draw();

	// 清理函数存储到外部以便 onBeforeUnmount 调用
	(canvas as any).__cleanup = () => {
		window.removeEventListener('resize', onResize);
	};
}

onMounted(() => {
	initCanvas();
});

onBeforeUnmount(() => {
	cancelAnimationFrame(animId);
	const canvas = canvasRef.value;
	if (canvas && (canvas as any).__cleanup) {
		(canvas as any).__cleanup();
	}
});

// ========== 登录逻辑 ==========
async function toLogin() {
	if (!form.username) {
		return ElMessage.error(t('用户名不能为空'));
	}
	if (!form.password) {
		return ElMessage.error(t('密码不能为空'));
	}
	if (!form.verifyCode) {
		return ElMessage.error(t('图片验证码不能为空'));
	}

	saving.value = true;
	try {
		await service.base.open.login(form).then(user.setToken);
		await Promise.all(app.events.hasToken.map((e) => e()));
		storage.set('username', form.username);
		router.push('/');
	} catch (err) {
		refs.picCaptcha.refresh();
		ElMessageBox.alert((err as Error).message, {
			title: t('提示'),
			type: 'error'
		});
	}
	saving.value = false;
}
</script>

<style lang="scss" scoped>
.page-login {
	display: flex;
	justify-content: center;
	align-items: center;
	height: 100%;
	width: 100%;
	position: relative;
	overflow: hidden;
	background-color: #0a0e1a;
}

.bg-canvas {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	pointer-events: none;
}

.login-card {
	position: relative;
	z-index: 10;
	display: flex;
	flex-direction: column;
	align-items: center;
	padding: 48px 44px 36px;
	border-radius: 20px;
	background: rgba(15, 20, 40, 0.55);
	backdrop-filter: blur(24px) saturate(1.4);
	-webkit-backdrop-filter: blur(24px) saturate(1.4);
	border: 1px solid rgba(100, 140, 220, 0.15);
	box-shadow:
		0 8px 40px rgba(0, 0, 0, 0.4),
		0 0 80px rgba(60, 100, 200, 0.06),
		inset 0 1px 0 rgba(255, 255, 255, 0.05);

	.logo {
		display: flex;
		align-items: center;
		margin-bottom: 16px;
		user-select: none;

		.icon {
			border-radius: 10px;
			padding: 6px;
			margin-right: 12px;
			background: linear-gradient(135deg, rgba(60, 100, 200, 0.4), rgba(100, 60, 180, 0.3));
			box-shadow: 0 2px 12px rgba(60, 100, 200, 0.3);

			img {
				height: 36px;
				display: block;
			}
		}

		span {
			font-size: 30px;
			font-weight: bold;
			line-height: 1;
			letter-spacing: 2px;
			color: #e8ecf4;
			text-shadow: 0 0 20px rgba(100, 150, 255, 0.3);
		}
	}

	.desc {
		font-size: 14px;
		letter-spacing: 1px;
		margin-bottom: 36px;
		user-select: none;
		color: rgba(180, 195, 230, 0.7);
	}

	.form {
		width: 300px;

		:deep(.el-form) {
			.el-form-item {
				margin-bottom: 20px;
			}

			.el-form-item__label {
				color: rgba(180, 195, 230, 0.75);
				padding-left: 5px;
				user-select: none;
			}

			.el-input {
				box-sizing: border-box;
				font-size: 15px;
				border: 0;
				border-radius: 10px;
				background-color: rgba(255, 255, 255, 0.06);
				padding: 0 5px;
				position: relative;
				transition: background-color 0.3s;

				&:hover,
				&:focus-within {
					background-color: rgba(255, 255, 255, 0.1);
				}

				&__wrapper {
					box-shadow: none;
					background-color: transparent;
				}

				&__inner {
					height: 45px;
					color: #dce2f0;
				}
			}
		}

		:deep(.pic-captcha) {
			position: absolute;
			right: -5px;
			top: 0;
		}
	}

	.op {
		display: flex;
		justify-content: center;
		margin-top: 36px;

		:deep(.el-button) {
			height: 45px;
			width: 100%;
			font-size: 16px;
			border-radius: 10px;
			letter-spacing: 2px;
			background: linear-gradient(135deg, #3a6bd5, #5a3dcf);
			border: none;
			box-shadow: 0 4px 20px rgba(60, 100, 220, 0.35);
			transition: all 0.3s;

			&:hover {
				box-shadow: 0 6px 28px rgba(60, 100, 220, 0.5);
				transform: translateY(-1px);
			}
		}
	}
}

.copyright {
	position: absolute;
	bottom: 15px;
	left: 0;
	text-align: center;
	width: 100%;
	color: rgba(150, 165, 200, 0.5);
	font-size: 13px;
	user-select: none;
	z-index: 10;
}

@media screen and (max-width: 768px) {
	.login-card {
		padding: 36px 28px 28px;
		margin: 0 16px;
		border-radius: 16px;

		.logo span {
			font-size: 24px;
		}

		.form {
			width: 260px;
		}
	}
}
</style>
