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:
@@ -6,19 +6,22 @@
|
|||||||
// - drawer → antd Drawer(侧滑抽屉)
|
// - drawer → antd Drawer(侧滑抽屉)
|
||||||
// - floating → FloatingPanel(非模态浮动面板,可拖拽,不阻断交互)
|
// - floating → FloatingPanel(非模态浮动面板,可拖拽,不阻断交互)
|
||||||
//
|
//
|
||||||
// 支持多面板共存(floating 类型可同时打开多个)
|
// 全局互斥:同一时刻最多打开一个页面组件。
|
||||||
|
// 已打开时不响应 OPEN_PAGE 事件(保留面板内互操作能力:拖拽、图片拖放等)。
|
||||||
//
|
//
|
||||||
// PAGE_MAP 集中注册所有可调度页面:
|
// PAGE_MAP 集中注册所有可调度页面:
|
||||||
// 有真实组件 → { component: XxxPage }
|
// 有真实组件 → { component: XxxPage, label: '...', width?: number }
|
||||||
// 占位页面 → 只写 label,自动渲染 PlaceholderPage
|
// 占位页面 → 只写 label,自动渲染 "页面开发中"
|
||||||
|
// 懒加载页面 → component: React.lazy(() => import('...'))
|
||||||
// 新增页面只需在这里加一行,无需创建独立文件
|
// 新增页面只需在这里加一行,无需创建独立文件
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef, type ComponentType } from 'react';
|
import { type ComponentType, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Modal, Drawer } from 'antd';
|
import { Drawer, Modal } from 'antd';
|
||||||
import { on, off, EVENTS } from '@/utils/event-bus';
|
import { EVENTS, off, on } from '@/utils/event-bus';
|
||||||
import { FloatingPanel } from '@/components/ui/FloatingPanel';
|
import { FloatingPanel } from '@/components/ui/FloatingPanel';
|
||||||
import { WechatWorkPage } from '@/pages/wechat-work/WechatWorkPage';
|
import { WechatWorkPage } from '@/pages/headers/WechatWorkPage';
|
||||||
|
import { Test2 } from '@/pages/headers/Test2';
|
||||||
|
|
||||||
// ---------- 类型 ----------
|
// ---------- 类型 ----------
|
||||||
|
|
||||||
@@ -30,20 +33,24 @@ interface PageEventPayload {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActivePanel {
|
/** 当前活跃面板(id 用于 key / 关闭定位) */
|
||||||
|
interface ActivePanel extends PageEventPayload {
|
||||||
id: string;
|
id: string;
|
||||||
target: string;
|
|
||||||
label: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageConfig {
|
interface PageConfig {
|
||||||
/** 页面组件(未提供则使用占位组件) */
|
/** 页面组件(未提供则使用自动生成的占位组件)。
|
||||||
|
* 支持 React.lazy(() => import('...')) 懒加载。 */
|
||||||
component?: ComponentType;
|
component?: ComponentType;
|
||||||
/** 页面标签(显示在容器标题栏) */
|
/** 页面标签(显示在容器标题栏) */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** 容器宽度(px)。也支持函数动态计算:() => window.innerWidth * 0.6 */
|
||||||
|
width?: number | (() => number);
|
||||||
|
/** 容器高度(px),仅 FloatingPanel 生效。也支持函数动态计算 */
|
||||||
|
height?: number | (() => number);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 占位组件 ----------
|
// ---------- 占位组件(模块级,引用稳定,避免渲染中创建组件) ----------
|
||||||
|
|
||||||
function PlaceholderPage({ label }: { label: string }) {
|
function PlaceholderPage({ label }: { label: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -53,22 +60,24 @@ function PlaceholderPage({ label }: { label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建一个包装了 label 的稳定占位组件(模块级,避免渲染时重建) */
|
/** 为每个占位页预创建稳定组件引用 */
|
||||||
function makePlaceholder(label: string): ComponentType {
|
function createPlaceholder(label: string): ComponentType {
|
||||||
const Placeholder = () => <PlaceholderPage label={label} />;
|
const C = () => <PlaceholderPage label={label} />;
|
||||||
return Placeholder;
|
C.displayName = `Placeholder_${label}`;
|
||||||
|
return C;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- target → 页面组件映射 ----------
|
// ---------- target → 页面组件映射 ----------
|
||||||
//
|
|
||||||
// 新增页面只需在此加一行 ——
|
|
||||||
// 有自定义组件:{ component: XxxPage, label: '页面标题' }
|
|
||||||
// 占位页面: { label: '页面标题' } ← 自动渲染 "页面标题 — 页面开发中"
|
|
||||||
|
|
||||||
const PAGE_MAP: Record<string, PageConfig> = {
|
const PAGE_MAP: Record<string, PageConfig> = {
|
||||||
'/wechat-work': { component: WechatWorkPage, label: '企业微信' },
|
'/wechat-work': { component: WechatWorkPage, label: '企业微信' },
|
||||||
'/compliance-assets': { label: '合规素材库' },
|
'/compliance-assets': { label: '合规素材库' },
|
||||||
'/account': { label: '查帐户' },
|
'/account': {
|
||||||
|
component: Test2,
|
||||||
|
label: '账户',
|
||||||
|
width: () => Math.min(window.innerWidth * 0.6, 720),
|
||||||
|
height: () => Math.min(window.innerHeight * 0.6, 680),
|
||||||
|
},
|
||||||
'/vip': { label: '开会员/激活' },
|
'/vip': { label: '开会员/激活' },
|
||||||
'/recharge': { label: '充值积分' },
|
'/recharge': { label: '充值积分' },
|
||||||
'/billing': { label: '消费记录' },
|
'/billing': { label: '消费记录' },
|
||||||
@@ -77,21 +86,33 @@ const PAGE_MAP: Record<string, PageConfig> = {
|
|||||||
'/quota': { label: '查配额' },
|
'/quota': { label: '查配额' },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 解析页面组件:有自定义组件用自定义,否则用占位 */
|
// 预计算所有占位组件(模块加载时执行一次,引用永久稳定)
|
||||||
|
const RESOLVED_PAGE_MAP = new Map<string, ComponentType>();
|
||||||
|
for (const [key, config] of Object.entries(PAGE_MAP)) {
|
||||||
|
RESOLVED_PAGE_MAP.set(key, config.component || createPlaceholder(config.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析页面组件(纯查表,不创建新组件) */
|
||||||
function resolvePage(target: string): ComponentType | null {
|
function resolvePage(target: string): ComponentType | null {
|
||||||
const config = PAGE_MAP[target];
|
return RESOLVED_PAGE_MAP.get(target) || null;
|
||||||
if (!config) return null;
|
}
|
||||||
return config.component || makePlaceholder(config.label);
|
|
||||||
|
/** 解析容器宽度(支持静态数值或动态函数) */
|
||||||
|
function resolveWidth(target: string): number | undefined {
|
||||||
|
const w = PAGE_MAP[target]?.width;
|
||||||
|
return typeof w === 'function' ? w() : w;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析容器高度(支持静态数值或动态函数) */
|
||||||
|
function resolveHeight(target: string): number | undefined {
|
||||||
|
const h = PAGE_MAP[target]?.height;
|
||||||
|
return typeof h === 'function' ? h() : h;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 组件 ----------
|
// ---------- 组件 ----------
|
||||||
|
|
||||||
export function PageDispatcher() {
|
export function PageDispatcher() {
|
||||||
// 单实例(modal / drawer)—— 同一时间只有一个
|
const [activePanel, setActivePanel] = useState<ActivePanel | null>(null);
|
||||||
const [singleton, setSingleton] = useState<PageEventPayload | null>(null);
|
|
||||||
|
|
||||||
// 浮动面板列表(支持多个同时打开)
|
|
||||||
const [floatingPanels, setFloatingPanels] = useState<ActivePanel[]>([]);
|
|
||||||
const nextIdRef = useRef(0);
|
const nextIdRef = useRef(0);
|
||||||
|
|
||||||
// 监听 OPEN_PAGE 事件
|
// 监听 OPEN_PAGE 事件
|
||||||
@@ -104,80 +125,82 @@ export function PageDispatcher() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modalType === 'floating') {
|
// 全局互斥:已有面板打开时,禁止打开新面板
|
||||||
// 浮动面板:追加到列表
|
// 不关闭已有面板,保留其内部互操作能力(拖拽、图片拖放等)
|
||||||
const id = `fp-${nextIdRef.current++}`;
|
setActivePanel((prev) => {
|
||||||
setFloatingPanels((prev) => [...prev, { id, target, label }]);
|
if (prev) return prev;
|
||||||
} else {
|
return { id: `pnl-${nextIdRef.current++}`, target, modalType, label };
|
||||||
// modal / drawer:单实例,覆盖之前的
|
});
|
||||||
setSingleton({ target, modalType, label });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
on(EVENTS.OPEN_PAGE, handler);
|
on(EVENTS.OPEN_PAGE, handler);
|
||||||
return () => off(EVENTS.OPEN_PAGE, handler);
|
return () => off(EVENTS.OPEN_PAGE, handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 关闭单实例
|
// 关闭面板
|
||||||
const closeSingleton = useCallback(() => {
|
const closePanel = useCallback(() => {
|
||||||
setSingleton(null);
|
setActivePanel(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 关闭指定浮动面板
|
// 页面组件(useMemo 稳定引用)
|
||||||
const closeFloating = useCallback((id: string) => {
|
const PanelPage = useMemo<ComponentType | null>(
|
||||||
setFloatingPanels((prev) => prev.filter((p) => p.id !== id));
|
() => (activePanel ? resolvePage(activePanel.target) : null),
|
||||||
}, []);
|
[activePanel],
|
||||||
|
);
|
||||||
|
|
||||||
// 单实例对应的页面组件
|
const panelWidth = activePanel ? resolveWidth(activePanel.target) : undefined;
|
||||||
const SingletonPage = singleton ? resolvePage(singleton.target) : null;
|
const panelHeight = activePanel ? resolveHeight(activePanel.target) : undefined;
|
||||||
|
|
||||||
|
// 容器默认尺寸(动态计算,视口缩放自动适配)
|
||||||
|
const defaultModalWidth = Math.min(window.innerWidth * 0.55, 720);
|
||||||
|
const defaultDrawerWidth = Math.min(window.innerWidth * 0.38, 520);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ======== Modal ======== */}
|
{/* ======== Modal ======== */}
|
||||||
{singleton?.modalType === 'modal' && SingletonPage && (
|
{activePanel?.modalType === 'modal' && PanelPage && (
|
||||||
<Modal
|
<Modal
|
||||||
open
|
open
|
||||||
centered
|
centered
|
||||||
title={singleton.label}
|
title={activePanel.label}
|
||||||
onCancel={closeSingleton}
|
onCancel={closePanel}
|
||||||
footer={null}
|
footer={null}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
mask={{ closable: true }}
|
mask={{ closable: true }}
|
||||||
width={640}
|
width={panelWidth ?? defaultModalWidth}
|
||||||
>
|
>
|
||||||
<SingletonPage />
|
{/* eslint-disable-next-line react-hooks/static-components */}
|
||||||
|
<PanelPage />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======== Drawer ======== */}
|
{/* ======== Drawer ======== */}
|
||||||
{singleton?.modalType === 'drawer' && SingletonPage && (
|
{activePanel?.modalType === 'drawer' && PanelPage && (
|
||||||
<Drawer
|
<Drawer
|
||||||
open
|
open
|
||||||
title={singleton.label}
|
title={activePanel.label}
|
||||||
onClose={closeSingleton}
|
onClose={closePanel}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
size={480}
|
size={panelWidth ?? defaultDrawerWidth}
|
||||||
>
|
>
|
||||||
<SingletonPage />
|
{/* eslint-disable-next-line react-hooks/static-components */}
|
||||||
|
<PanelPage />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======== Floating Panels ======== */}
|
{/* ======== Floating Panel ======== */}
|
||||||
{floatingPanels.map((panel) => {
|
{activePanel?.modalType === 'floating' && PanelPage && (
|
||||||
const PageContent = resolvePage(panel.target);
|
|
||||||
if (!PageContent) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FloatingPanel
|
<FloatingPanel
|
||||||
key={panel.id}
|
key={activePanel.id}
|
||||||
open
|
open
|
||||||
title={panel.label}
|
title={activePanel.label}
|
||||||
onClose={() => closeFloating(panel.id)}
|
onClose={closePanel}
|
||||||
|
width={panelWidth ?? defaultDrawerWidth}
|
||||||
|
height={panelHeight}
|
||||||
>
|
>
|
||||||
<PageContent />
|
<PanelPage />
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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