Files
xk-admin/apps/web-antd/src/components/public-account-lock/LockWarningCanvas.vue
李琦 879efb8f29 feat: 管理端公账支付与确认、锁店遮罩及表单提交修复
新增公账账单/到账确认页与全局锁店组件;系统配置补公账参数面板;
统一弹窗 onConfirm 校验提交,并完善订单详情与门店相关交互。
2026-08-21 12:51:42 +08:00

307 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
/**
* 公账逾期弹窗背景:警戒条、脉冲环、余烬粒子
* 跟随亮/暗主题,弹窗关闭即停 RAF避免后台空转
*/
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
const props = defineProps<{
active?: boolean;
/** 后台配置blue / yellow / red */
color?: 'blue' | 'yellow' | 'red';
/** 上传/等待页只保留上下警戒条,避免三角和表单叠在一起 */
tapesOnly?: boolean;
}>();
const containerRef = ref<HTMLDivElement | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
interface Ember {
x: number;
y: number;
vx: number;
vy: number;
life: number;
maxLife: number;
size: number;
}
let animationId = 0;
let resizeObserver: ResizeObserver | null = null;
let themeObserver: MutationObserver | null = null;
let running = false;
function isDarkTheme() {
return (
document.documentElement.classList.contains('dark') ||
document.documentElement.getAttribute('data-theme') === 'dark'
);
}
function startLoop() {
if (running || !containerRef.value || !canvasRef.value) return;
const canvas = canvasRef.value;
const ctx = canvas.getContext('2d');
const container = containerRef.value;
if (!ctx) return;
running = true;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let cssW = 0;
let cssH = 0;
let last = 0;
let elapsed = 0;
let embers: Ember[] = [];
const MAX_EMBERS = 22;
function updateSize() {
const rect = container.getBoundingClientRect();
cssW = rect.width || 760;
cssH = rect.height || 460;
const pw = Math.round(cssW * dpr);
const ph = Math.round(cssH * dpr);
if (canvas.width !== pw || canvas.height !== ph) {
canvas.width = pw;
canvas.height = ph;
}
}
function theme() {
const dark = isDarkTheme();
const color = props.color === 'blue' || props.color === 'red' ? props.color : 'yellow';
if (color === 'blue') {
return {
dark,
warn: dark ? { r: 96, g: 165, b: 250 } : { r: 37, g: 99, b: 235 },
tape: dark ? { r: 59, g: 130, b: 246 } : { r: 37, g: 99, b: 235 },
ink: dark ? { r: 15, g: 23, b: 42 } : { r: 15, g: 23, b: 42 },
bg: dark ? 'rgba(10, 18, 32, 0.55)' : 'rgba(239, 246, 255, 0.42)',
};
}
if (color === 'red') {
return {
dark,
warn: dark ? { r: 248, g: 113, b: 113 } : { r: 220, g: 38, b: 38 },
tape: dark ? { r: 239, g: 68, b: 68 } : { r: 220, g: 38, b: 38 },
ink: dark ? { r: 23, g: 15, b: 15 } : { r: 28, g: 25, b: 23 },
bg: dark ? 'rgba(32, 10, 10, 0.55)' : 'rgba(254, 242, 242, 0.42)',
};
}
return {
dark,
warn: dark ? { r: 251, g: 191, b: 36 } : { r: 217, g: 119, b: 6 },
tape: dark ? { r: 245, g: 158, b: 11 } : { r: 217, g: 119, b: 6 },
ink: dark ? { r: 15, g: 23, b: 42 } : { r: 28, g: 25, b: 23 },
bg: dark ? 'rgba(23, 18, 10, 0.55)' : 'rgba(255, 251, 235, 0.35)',
};
}
function spawnEmber(): Ember {
return {
x: Math.random() * cssW,
y: cssH * (0.35 + Math.random() * 0.5),
vx: (Math.random() - 0.5) * 18,
vy: -12 - Math.random() * 22,
life: 1,
maxLife: 1.6 + Math.random() * 2.2,
size: 1.2 + Math.random() * 2.4,
};
}
function drawTape(
c: CanvasRenderingContext2D,
y: number,
h: number,
offset: number,
t: ReturnType<typeof theme>,
) {
c.save();
c.beginPath();
c.rect(0, y, cssW, h);
c.clip();
c.fillStyle = `rgb(${t.tape.r},${t.tape.g},${t.tape.b})`;
c.fillRect(0, y, cssW, h);
c.fillStyle = `rgba(${t.ink.r},${t.ink.g},${t.ink.b},0.82)`;
const stripe = 16;
const shift = offset % (stripe * 2);
for (let x = -h - stripe * 2 + shift; x < cssW + h; x += stripe * 2) {
c.beginPath();
c.moveTo(x, y);
c.lineTo(x + stripe, y);
c.lineTo(x + stripe + h, y + h);
c.lineTo(x + h, y + h);
c.closePath();
c.fill();
}
c.restore();
}
function drawTriangle(
c: CanvasRenderingContext2D,
cx: number,
cy: number,
size: number,
pulse: number,
t: ReturnType<typeof theme>,
) {
const s = size * pulse;
c.save();
c.shadowColor = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.55)`;
c.shadowBlur = 22;
c.beginPath();
c.moveTo(cx, cy - s);
c.lineTo(cx + s * 0.92, cy + s * 0.78);
c.lineTo(cx - s * 0.92, cy + s * 0.78);
c.closePath();
c.fillStyle = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.16)`;
c.fill();
c.lineWidth = 3;
c.strokeStyle = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.95)`;
c.stroke();
c.shadowBlur = 0;
c.fillStyle = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.95)`;
c.beginPath();
c.roundRect(cx - 3, cy - s * 0.28, 6, s * 0.52, 2);
c.fill();
c.beginPath();
c.arc(cx, cy + s * 0.42, 4, 0, Math.PI * 2);
c.fill();
c.restore();
}
function draw(ts: number) {
if (!running) return;
updateSize();
if (cssW <= 0 || cssH <= 0) {
animationId = requestAnimationFrame(draw);
return;
}
if (!last) last = ts;
let dt = (ts - last) / 1000;
if (dt <= 0) dt = 0.016;
if (dt > 0.2) dt = 0.2;
last = ts;
elapsed += dt;
const t = theme();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(dpr, dpr);
ctx.fillStyle = t.bg;
ctx.fillRect(0, 0, cssW, cssH);
if (!props.tapesOnly) {
// 三角/光环随面板高度缩放,避免矮屏占满上半区把正文挤乱
const tapeReserve = Math.round(Math.min(18, Math.max(10, cssH * 0.028)));
const cy = Math.round(
Math.min(Math.max(48, tapeReserve + 28 + cssH * 0.04), cssH * 0.16),
);
const triSize = Math.round(Math.min(36, Math.max(16, cssH * 0.045)));
const ringSpan = Math.min(150, Math.max(60, cssH * 0.18));
const cx = cssW / 2;
const glow = ctx.createRadialGradient(cx, cy, 8, cx, cy, Math.max(cssW, cssH) * 0.55);
glow.addColorStop(0, `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.22)`);
glow.addColorStop(0.45, `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.06)`);
glow.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, cssW, cssH);
for (let i = 0; i < 3; i++) {
const phase = (elapsed * 0.55 + i / 3) % 1;
const radius = triSize * 0.85 + phase * ringSpan;
const alpha = (1 - phase) * 0.35;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},${alpha})`;
ctx.lineWidth = Math.max(1.5, triSize / 28);
ctx.stroke();
}
const pulse = 1 + Math.sin(elapsed * 3.2) * 0.06;
drawTriangle(ctx, cx, cy, triSize, pulse, t);
while (embers.length < MAX_EMBERS) embers.push(spawnEmber());
for (let i = embers.length - 1; i >= 0; i--) {
const p = embers[i]!;
p.life -= dt / p.maxLife;
p.x += p.vx * dt;
p.y += p.vy * dt;
if (p.life <= 0) {
embers.splice(i, 1);
continue;
}
ctx.save();
ctx.globalAlpha = p.life * 0.75;
ctx.shadowColor = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},0.8)`;
ctx.shadowBlur = 8;
ctx.fillStyle = `rgba(${t.warn.r},${t.warn.g},${t.warn.b},${p.life})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * Math.min(1, cssH / 520), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
const tapeH = Math.round(Math.min(18, Math.max(12, cssH * 0.028)));
const shift = elapsed * 36;
drawTape(ctx, 0, tapeH, shift, t);
drawTape(ctx, cssH - tapeH, tapeH, -shift, t);
ctx.restore();
animationId = requestAnimationFrame(draw);
}
updateSize();
animationId = requestAnimationFrame(draw);
if (window.ResizeObserver) {
resizeObserver = new ResizeObserver(() => updateSize());
resizeObserver.observe(container);
}
themeObserver = new MutationObserver(() => undefined);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'data-theme'],
});
}
function stopLoop() {
running = false;
cancelAnimationFrame(animationId);
animationId = 0;
resizeObserver?.disconnect();
resizeObserver = null;
themeObserver?.disconnect();
themeObserver = null;
}
onMounted(async () => {
await nextTick();
if (props.active !== false) startLoop();
});
watch(
() => props.active,
(active) => {
if (active === false) stopLoop();
else nextTick(() => startLoop());
},
);
onUnmounted(() => {
stopLoop();
});
</script>
<template>
<div ref="containerRef" class="lock-canvas-wrap">
<canvas ref="canvasRef" class="lock-canvas" />
</div>
</template>
<style scoped>
.lock-canvas-wrap,
.lock-canvas {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
}
.lock-canvas {
display: block;
pointer-events: none;
}
</style>