feat: 统一页面调度体系 — 全局互斥 + 动态尺寸 + 账户信息页

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>
This commit is contained in:
2026-06-09 18:43:50 +08:00
parent 0c7b33aac1
commit c18d9c8404
5 changed files with 320 additions and 73 deletions

View File

@@ -0,0 +1,9 @@
import {Component} from "react";
export class Test1 extends Component {
render() {
return (
<></>
);
}
}

210
src/pages/headers/Test2.tsx Normal file
View File

@@ -0,0 +1,210 @@
// ============================================================
// 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>
);
}

View File

@@ -0,0 +1,5 @@
export function Test3() {
return (
<></>
);
}