PageDispatcher 重构: - singleton + floatingPanels[] 双状态 → 单一 activePanel 状态 - 全局互斥:已打开面板时禁止打开新面板(保留拖拽/图片拖放等互操作) - 占位组件预计算为模块级 Map,消除 ESLint static-components 误报 - PAGE_MAP 支持 width/height 配置,动态函数计算(视口比例自适应) - 容器默认尺寸改为动态:Modal=min(w*0.55,720) Drawer=min(w*0.38,520) - 修复 FloatingPanel 未接收 width/height 配置的问题 Test2 账户信息页: - AuthAPI.getUserInfo() → 三卡片布局(积分/套餐/今日统计) - 加载态 Skeleton / 错误态 Result+重试 - centToYuan 积分转换、formatDate 日期格式化 目录调整:wechat-work/ → headers/ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
211 lines
7.1 KiB
TypeScript
211 lines
7.1 KiB
TypeScript
// ============================================================
|
||
// Test2 — 账户信息展示
|
||
// 数据来源:AuthAPI.getUserInfo() → GET /api/v1/auth/me
|
||
// ============================================================
|
||
|
||
import { useEffect, useState, useCallback } from 'react';
|
||
import {
|
||
Card,
|
||
Descriptions,
|
||
Button,
|
||
Typography,
|
||
Skeleton,
|
||
Result,
|
||
Space,
|
||
Tag,
|
||
Divider,
|
||
theme as antTheme,
|
||
} from 'antd';
|
||
import {
|
||
UserOutlined,
|
||
WalletOutlined,
|
||
CrownOutlined,
|
||
BarChartOutlined,
|
||
RightOutlined,
|
||
} from '@ant-design/icons';
|
||
import { AuthAPI, type UserInfoBody } from '@/services/modules/auth';
|
||
|
||
const { Text, Title } = Typography;
|
||
|
||
/** 将分转为元,保留两位小数 */
|
||
function centToYuan(cent: number): string {
|
||
return (cent / 100).toFixed(2);
|
||
}
|
||
|
||
/** 格式化日期 */
|
||
function formatDate(date: Date | string | undefined): string {
|
||
if (!date) return '—';
|
||
const d = typeof date === 'string' ? new Date(date) : date;
|
||
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||
}
|
||
|
||
export function Test2() {
|
||
const { token } = antTheme.useToken();
|
||
const [userInfo, setUserInfo] = useState<UserInfoBody | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const info = await AuthAPI.getUserInfo();
|
||
setUserInfo(info);
|
||
} catch (err) {
|
||
setError((err as Error).message || '加载账户信息失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, [load]);
|
||
|
||
// ======== 加载态 ========
|
||
if (loading) {
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
<Skeleton active paragraph={{ rows: 6 }} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ======== 错误态 ========
|
||
if (error || !userInfo) {
|
||
return (
|
||
<Result
|
||
status="error"
|
||
title="加载失败"
|
||
subTitle={error || '未能获取账户信息'}
|
||
extra={
|
||
<Button type="primary" onClick={load}>
|
||
重试
|
||
</Button>
|
||
}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// ======== 数据展示 ========
|
||
const totalBalance = userInfo.balance_cent + userInfo.gift_balance_cent;
|
||
|
||
return (
|
||
<div style={{ padding: 24, maxWidth: 640 }}>
|
||
{/* ======== 标题:账户 ======== */}
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
||
<Space align="center" size={12}>
|
||
<UserOutlined style={{ fontSize: 22, color: token.colorPrimary }} />
|
||
<div>
|
||
<Title level={4} style={{ margin: 0 }}>
|
||
{userInfo.display_name || userInfo.username}
|
||
</Title>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
ID: {userInfo.id}
|
||
</Text>
|
||
</div>
|
||
{userInfo.role !== 'user' && (
|
||
<Tag color="gold" style={{ lineHeight: '18px', fontSize: 11 }}>
|
||
{userInfo.role === 'admin' ? '管理员' : '超级管理员'}
|
||
</Tag>
|
||
)}
|
||
</Space>
|
||
<Button type="primary" ghost size="small" icon={<WalletOutlined />}>
|
||
充值
|
||
</Button>
|
||
</div>
|
||
|
||
{/* ======== 积分栏 ======== */}
|
||
<Card
|
||
size="small"
|
||
styles={{
|
||
body: { padding: '16px 20px' },
|
||
}}
|
||
style={{ marginBottom: 16 }}
|
||
>
|
||
<Text type="secondary" style={{ fontSize: 12, marginBottom: 8, display: 'block' }}>
|
||
账户总积分
|
||
</Text>
|
||
<Title level={3} style={{ margin: '0 0 12px 0' }}>
|
||
{centToYuan(totalBalance)}
|
||
<Text type="secondary" style={{ fontSize: 14, marginLeft: 4 }}>积分</Text>
|
||
</Title>
|
||
<Space size={32}>
|
||
<div>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>主积分</Text>
|
||
<br />
|
||
<Text strong>{centToYuan(userInfo.balance_cent)}</Text>
|
||
</div>
|
||
<div>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>赠送积分</Text>
|
||
<br />
|
||
<Text strong>{centToYuan(userInfo.gift_balance_cent)}</Text>
|
||
</div>
|
||
{userInfo.discount_rate > 0 && userInfo.discount_rate < 1 && (
|
||
<div>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>折扣率</Text>
|
||
<br />
|
||
<Tag color="green">{Math.round(userInfo.discount_rate * 100)}%</Tag>
|
||
</div>
|
||
)}
|
||
</Space>
|
||
</Card>
|
||
|
||
{/* ======== 套餐信息 ======== */}
|
||
<Card
|
||
size="small"
|
||
styles={{
|
||
body: { padding: '16px 20px' },
|
||
}}
|
||
style={{ marginBottom: 16 }}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||
<Space size={8}>
|
||
<CrownOutlined style={{ color: token.colorWarning }} />
|
||
<Text strong>当前套餐</Text>
|
||
</Space>
|
||
<Button type="link" size="small" icon={<RightOutlined />} iconPlacement="end">
|
||
续费 / 升级
|
||
</Button>
|
||
</div>
|
||
<Descriptions column={1} size="small" colon={false}>
|
||
<Descriptions.Item label="套餐名称">
|
||
<Text strong>{userInfo.subscription_plan || '免费版'}</Text>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="到期时间">
|
||
<Text type={new Date(userInfo.subscription_expires_at) < new Date() ? 'danger' : undefined}>
|
||
{formatDate(userInfo.subscription_expires_at)}
|
||
</Text>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="席位上限">
|
||
{userInfo.seat_limit || '—'}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
</Card>
|
||
|
||
{/* ======== 今日统计 ======== */}
|
||
<Card
|
||
size="small"
|
||
styles={{
|
||
body: { padding: '16px 20px' },
|
||
}}
|
||
style={{ marginBottom: 16 }}
|
||
>
|
||
<Space size={8} style={{ marginBottom: 12 }}>
|
||
<BarChartOutlined style={{ color: token.colorSuccess }} />
|
||
<Text strong>今日使用</Text>
|
||
</Space>
|
||
<Text type="secondary" style={{ display: 'block', textAlign: 'center', padding: '12px 0', fontSize: 13 }}>
|
||
统计数据将在次日更新
|
||
</Text>
|
||
</Card>
|
||
|
||
{/* ======== 查看更多权益 ======== */}
|
||
<Divider style={{ margin: '8px 0' }} />
|
||
<Button type="link" block icon={<RightOutlined />} iconPlacement="end" style={{ color: token.colorPrimary }}>
|
||
查看更多权益
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|