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:
John
2026-06-15 15:04:43 -07:00
commit 2e14873f2d
272 changed files with 64133 additions and 0 deletions
+145
View File
@@ -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>
);
}