Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useRef, useState, type CSSProperties } from 'react';
|
||||
|
||||
const RING_R = 16;
|
||||
const CIRC = 2 * Math.PI * RING_R;
|
||||
|
||||
function formatYuan(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
type BalanceRingProps = {
|
||||
balanceCents: number;
|
||||
totalCreditCents?: number;
|
||||
onRecharge: (force?: boolean) => void;
|
||||
};
|
||||
|
||||
export function BalanceRing({ balanceCents, totalCreditCents, onRecharge }: BalanceRingProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [popoverStyle, setPopoverStyle] = useState<CSSProperties>({});
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const popoverWidth = 220;
|
||||
|
||||
const total = Math.max(totalCreditCents ?? balanceCents, balanceCents, 0);
|
||||
const spent = Math.max(0, total - balanceCents);
|
||||
const pct = total > 0 ? Math.round((balanceCents / total) * 100) : 0;
|
||||
const spentLen = total > 0 ? (spent / total) * CIRC : 0;
|
||||
const remainLen = total > 0 ? (balanceCents / total) * CIRC : 0;
|
||||
const spentPct = total > 0 ? (spent / total) * 100 : 0;
|
||||
const remainPct = total > 0 ? (balanceCents / total) * 100 : 0;
|
||||
|
||||
const low = balanceCents > 0 && balanceCents <= 100;
|
||||
const empty = balanceCents <= 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (wrapRef.current && !wrapRef.current.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClick);
|
||||
return () => document.removeEventListener('click', handleClick);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const updatePosition = () => {
|
||||
const anchor = wrapRef.current;
|
||||
if (!anchor) return;
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const width = Math.min(popoverWidth, window.innerWidth - 24);
|
||||
let left = rect.left + rect.width / 2 - width / 2;
|
||||
left = Math.max(12, Math.min(left, window.innerWidth - width - 12));
|
||||
setPopoverStyle({
|
||||
position: 'fixed',
|
||||
top: rect.bottom + 10,
|
||||
left,
|
||||
width,
|
||||
});
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
window.addEventListener('resize', updatePosition);
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const centerLabel = total <= 0 ? '—' : empty ? '0%' : `${pct}%`;
|
||||
const ariaLabel = total > 0 ? `账户额度,剩余 ${pct}%` : '账户额度';
|
||||
|
||||
return (
|
||||
<div className={`balance-popover-wrap${open ? ' open' : ''}`} ref={wrapRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`balance-ring-btn${low ? ' low' : ''}${empty ? ' empty' : ''}`}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={open}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setOpen((value) => !value);
|
||||
}}
|
||||
>
|
||||
<svg className="balance-ring-svg" viewBox="0 0 40 40" aria-hidden="true">
|
||||
<circle className="balance-ring-track" cx="20" cy="20" r={RING_R} />
|
||||
<circle
|
||||
className="balance-ring-spent"
|
||||
cx="20"
|
||||
cy="20"
|
||||
r={RING_R}
|
||||
strokeDasharray={`${spentLen} ${CIRC}`}
|
||||
strokeDashoffset={0}
|
||||
/>
|
||||
<circle
|
||||
className="balance-ring-remain"
|
||||
cx="20"
|
||||
cy="20"
|
||||
r={RING_R}
|
||||
strokeDasharray={`${remainLen} ${CIRC}`}
|
||||
strokeDashoffset={-spentLen}
|
||||
/>
|
||||
</svg>
|
||||
<span className="balance-ring-center">{centerLabel}</span>
|
||||
</button>
|
||||
|
||||
<div className="balance-popover" role="dialog" aria-label="账户额度" style={popoverStyle}>
|
||||
<h4>账户额度</h4>
|
||||
<div className="balance-popover-row">
|
||||
<span className="balance-popover-label">
|
||||
<span className="balance-dot balance-dot-remain" aria-hidden="true" />
|
||||
剩余
|
||||
</span>
|
||||
<strong>{formatYuan(balanceCents)}</strong>
|
||||
</div>
|
||||
<div className="balance-popover-row">
|
||||
<span className="balance-popover-label">
|
||||
<span className="balance-dot balance-dot-spent" aria-hidden="true" />
|
||||
已消耗
|
||||
</span>
|
||||
<strong>{formatYuan(spent)}</strong>
|
||||
</div>
|
||||
<div className="balance-popover-row">
|
||||
<span className="balance-popover-label balance-popover-label-muted">累计额度</span>
|
||||
<span>{formatYuan(total)}</span>
|
||||
</div>
|
||||
<div className="balance-popover-bar" aria-hidden="true">
|
||||
<div className="balance-popover-bar-spent" style={{ width: `${spentPct}%` }} />
|
||||
<div className="balance-popover-bar-remain" style={{ width: `${remainPct}%` }} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="balance-popover-cta"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onRecharge(empty);
|
||||
}}
|
||||
>
|
||||
去充值
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user