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:
9
src/pages/headers/Test1.tsx
Normal file
9
src/pages/headers/Test1.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Component} from "react";
|
||||
|
||||
export class Test1 extends Component {
|
||||
render() {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
}
|
||||
210
src/pages/headers/Test2.tsx
Normal file
210
src/pages/headers/Test2.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
src/pages/headers/Test3.tsx
Normal file
5
src/pages/headers/Test3.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export function Test3() {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user