feat: 优化签名热力图与金额展示
This commit is contained in:
@@ -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,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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user