feat: 优化签名热力图与金额展示

This commit is contained in:
hectorzhao
2026-08-12 11:41:47 +08:00
parent e64b5e23fe
commit 0cd353450a
33 changed files with 344 additions and 121 deletions
+35
View File
@@ -0,0 +1,35 @@
import { useEffect, useRef } from 'react';
type Particle = { x: number; y: number; vx: number; vy: number; radius: number; alpha: number };
export function ClientLoginCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas?.getContext('2d');
if (!canvas || !context) return undefined;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let particles: Particle[] = []; let frame = 0; let width = 0; let height = 0;
const resize = () => {
const rect = canvas.getBoundingClientRect(); const ratio = Math.min(window.devicePixelRatio || 1, 2);
width = rect.width; height = rect.height; canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio); context.setTransform(ratio, 0, 0, ratio, 0, 0);
const count = Math.max(18, Math.min(42, Math.round(width / 34)));
particles = Array.from({ length: count }, (_, index) => ({ x: (index * 83.7) % Math.max(width, 1), y: (index * 47.3) % Math.max(height, 1), vx: 0.08 + (index % 4) * 0.025, vy: (index % 3 - 1) * 0.035, radius: 1.2 + (index % 3) * 0.45, alpha: 0.18 + (index % 5) * 0.045 }));
};
const draw = () => {
context.clearRect(0, 0, width, height);
for (const particle of particles) {
if (!reduceMotion.matches && !document.hidden) { particle.x = (particle.x + particle.vx + width) % width; particle.y = (particle.y + particle.vy + height) % height; }
context.beginPath(); context.fillStyle = `rgba(59, 130, 246, ${particle.alpha})`; context.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); context.fill();
}
for (let left = 0; left < particles.length; left += 1) for (let right = left + 1; right < particles.length; right += 1) {
const dx = particles[left].x - particles[right].x; const dy = particles[left].y - particles[right].y; const distance = Math.hypot(dx, dy);
if (distance <= 105) { context.beginPath(); context.strokeStyle = `rgba(59, 130, 246, ${0.09 * (1 - distance / 105)})`; context.moveTo(particles[left].x, particles[left].y); context.lineTo(particles[right].x, particles[right].y); context.stroke(); }
}
frame = window.requestAnimationFrame(draw);
};
const observer = new ResizeObserver(resize); observer.observe(canvas); resize(); draw();
return () => { observer.disconnect(); window.cancelAnimationFrame(frame); };
}, []);
return <canvas aria-hidden="true" className="client-login-canvas" ref={canvasRef} />;
}
+5 -4
View File
@@ -5,6 +5,7 @@ import { createUuid } from '@/utils/randomId';
import { Button } from './Button';
import { Input } from './Input';
import { Modal } from './Modal';
import { MoneyText } from './MoneyText';
import { Select } from './Select';
import { Textarea } from './Textarea';
@@ -130,7 +131,7 @@ export function ManualRechargeDialog({ initialTargetId, lockTarget = false, onCl
<div className="manual-recharge-result" role="status">
<strong>{result.amountCents > 0 ? '充值已入账' : '余额冲正已入账'}</strong>
<span>{result.orderNo}</span>
<span>¥{formatCents(result.balanceAfterCents)}</span>
<span><MoneyText>¥{formatCents(result.balanceAfterCents)}</MoneyText></span>
<span>{result.operationId}{result.replayed ? '(幂等重放)' : ''}</span>
</div>
) : review ? (
@@ -139,9 +140,9 @@ export function ManualRechargeDialog({ initialTargetId, lockTarget = false, onCl
<dl>
<div><dt></dt><dd><strong>{review.tenant.name}</strong><span>{review.tenant.code} · {review.tenant.id}</span></dd></div>
<div><dt></dt><dd>{review.direction === 'topup' ? '余额充值' : '余额冲正'}</dd></div>
<div><dt></dt><dd>¥{formatCents(review.balanceCents)}</dd></div>
<div><dt></dt><dd className={review.amountCents > 0 ? 'is-positive' : 'is-negative'}>{review.amountCents > 0 ? '+' : '-'}¥{formatCents(Math.abs(review.amountCents))}</dd></div>
<div><dt></dt><dd><strong>¥{formatCents(review.balanceAfterCents)}</strong></dd></div>
<div><dt></dt><dd><MoneyText>¥{formatCents(review.balanceCents)}</MoneyText></dd></div>
<div><dt></dt><dd className={review.amountCents > 0 ? 'is-positive' : 'is-negative'}><MoneyText>{review.amountCents > 0 ? '+' : '-'}¥{formatCents(Math.abs(review.amountCents))}</MoneyText></dd></div>
<div><dt></dt><dd><strong><MoneyText>¥{formatCents(review.balanceAfterCents)}</MoneyText></strong></dd></div>
</dl>
{remark.trim() ? <p className="manual-recharge-review__remark"><strong></strong>{remark.trim()}</p> : null}
</div>
+8
View File
@@ -0,0 +1,8 @@
import { Children, type ReactNode } from 'react';
export function MoneyText({ children, className }: { children: ReactNode; className?: string }) {
const text = Children.toArray(children).map((value) => typeof value === 'string' || typeof value === 'number' ? String(value) : '').join('');
const match = text.match(/^(.*?)(\.\d+)(\s*[^\d]*)$/);
if (!match) return <span className={className}>{text}</span>;
return <span className={className}>{match[1]}<span className="money-text__fraction">{match[2]}</span>{match[3]}</span>;
}
+4 -3
View File
@@ -4,6 +4,7 @@ import { formatCents } from '@/utils/currency';
import { formatDateTime } from '@/utils/dateTime';
import { Button } from './Button';
import { Modal } from './Modal';
import { MoneyText } from './MoneyText';
type RechargeReceiptDialogProps = {
open: boolean;
@@ -57,7 +58,7 @@ export function RechargeReceiptDialog({
<strong>
{isCorrection ? '' : '+'}
<small>¥</small>
{formatReceiptAmount(record.amountCents)}
<MoneyText>{formatReceiptAmount(record.amountCents)}</MoneyText>
</strong>
</section>
@@ -78,11 +79,11 @@ export function RechargeReceiptDialog({
</div>
<div>
<dt></dt>
<dd>{balanceBefore === null ? '-' : `¥${formatCents(balanceBefore)}`}</dd>
<dd>{balanceBefore === null ? '-' : <MoneyText>¥{formatCents(balanceBefore)}</MoneyText>}</dd>
</div>
<div>
<dt></dt>
<dd>{balanceAfter === null || balanceAfter === undefined ? '-' : `¥${formatCents(balanceAfter)}`}</dd>
<dd>{balanceAfter === null || balanceAfter === undefined ? '-' : <MoneyText>¥{formatCents(balanceAfter)}</MoneyText>}</dd>
</div>
<div>
<dt></dt>
+2
View File
@@ -14,6 +14,8 @@ export type { ManualRechargeTarget } from './ManualRechargeDialog';
export { RechargeReceiptDialog } from './RechargeReceiptDialog';
export { Input } from './Input';
export { Modal } from './Modal';
export { MoneyText } from './MoneyText';
export { ClientLoginCanvas } from './ClientLoginCanvas';
export { InlineTextPreview, Pagination, QueryPanel } from './PagePrimitives';
export { Select } from './Select';
export { Table } from './Table';