feat: 企业微信改为 Modal 展示二维码,修复 PageDispatcher hooks 错误
将"企业微信"导航项从 external(跳转浏览器)改为 page(Modal 展示),
二维码图片数据来源于 Banner API(/api/v1/creatives,category=promotion)。
- 新增 WechatWorkPage:调用 fetchBanners,筛选 category==='promotion',
展示 image_url 二维码 + 扫码提示 + 刷新按钮,加载/空态/错误三态覆盖
- nav-config:wechat-work 从 action:'external' 改为 action:'page',
modalType:'modal',target:'/wechat-work'
- PageDispatcher:注册 /wechat-work 映射,Modal 新增 centered 居中
- 修复 PageDispatcher 致命 bug:PAGE_MAP[...]() 函数调用改为
JSX <SingletonPage />,避免子组件 hooks 误跑在父组件链上导致崩溃
- PAGE_MAP 类型从 Record<string, () => ReactNode> 修正为
Record<string, ComponentType>
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
// 支持多面板共存(floating 类型可同时打开多个)
|
// 支持多面板共存(floating 类型可同时打开多个)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
|
import {useState, useEffect, useCallback, useRef, type ComponentType} from 'react';
|
||||||
import {Modal, Drawer} from 'antd';
|
import {Modal, Drawer} from 'antd';
|
||||||
import {on, off, EVENTS} from '@/utils/event-bus';
|
import {on, off, EVENTS} from '@/utils/event-bus';
|
||||||
import {FloatingPanel} from '@/components/common/FloatingPanel';
|
import {FloatingPanel} from '@/components/common/FloatingPanel';
|
||||||
@@ -21,6 +21,7 @@ import { BillingPage } from '@/pages/billing/BillingPage';
|
|||||||
import {OrdersPage} from '@/pages/orders/OrdersPage';
|
import {OrdersPage} from '@/pages/orders/OrdersPage';
|
||||||
import {ProjectsPage} from '@/pages/projects/ProjectsPage';
|
import {ProjectsPage} from '@/pages/projects/ProjectsPage';
|
||||||
import {QuotaPage} from '@/pages/quota/QuotaPage';
|
import {QuotaPage} from '@/pages/quota/QuotaPage';
|
||||||
|
import {WechatWorkPage} from '@/pages/wechat-work/WechatWorkPage';
|
||||||
|
|
||||||
// ---------- 类型 ----------
|
// ---------- 类型 ----------
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ interface ActivePanel {
|
|||||||
|
|
||||||
// ---------- target → 页面组件映射 ----------
|
// ---------- target → 页面组件映射 ----------
|
||||||
|
|
||||||
const PAGE_MAP: Record<string, () => ReactNode> = {
|
const PAGE_MAP: Record<string, ComponentType> = {
|
||||||
'/compliance-assets': ComplianceAssetsPage,
|
'/compliance-assets': ComplianceAssetsPage,
|
||||||
'/account': AccountPage,
|
'/account': AccountPage,
|
||||||
'/vip': VipPage,
|
'/vip': VipPage,
|
||||||
@@ -49,6 +50,7 @@ const PAGE_MAP: Record<string, () => ReactNode> = {
|
|||||||
'/orders': OrdersPage,
|
'/orders': OrdersPage,
|
||||||
'/projects': ProjectsPage,
|
'/projects': ProjectsPage,
|
||||||
'/quota': QuotaPage,
|
'/quota': QuotaPage,
|
||||||
|
'/wechat-work': WechatWorkPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 组件 ----------
|
// ---------- 组件 ----------
|
||||||
@@ -95,36 +97,39 @@ export function PageDispatcher() {
|
|||||||
setFloatingPanels((prev) => prev.filter((p) => p.id !== id));
|
setFloatingPanels((prev) => prev.filter((p) => p.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 渲染单实例内容
|
// 单实例对应的页面组件
|
||||||
const singletonContent = singleton && PAGE_MAP[singleton.target] ? PAGE_MAP[singleton.target]() : null;
|
const SingletonPage = singleton ? PAGE_MAP[singleton.target] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ======== Modal ======== */}
|
{/* ======== Modal ======== */}
|
||||||
{singleton?.modalType === 'modal' && (
|
{singleton?.modalType === 'modal' && SingletonPage && (
|
||||||
<Modal
|
<Modal
|
||||||
open
|
open
|
||||||
|
centered
|
||||||
title={singleton.label}
|
title={singleton.label}
|
||||||
onCancel={closeSingleton}
|
onCancel={closeSingleton}
|
||||||
footer={null}
|
footer={null}
|
||||||
destroyOnClose
|
destroyOnHidden={true}
|
||||||
maskClosable
|
mask={{
|
||||||
|
closable: true,
|
||||||
|
}}
|
||||||
width={640}
|
width={640}
|
||||||
>
|
>
|
||||||
{singletonContent}
|
<SingletonPage />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======== Drawer ======== */}
|
{/* ======== Drawer ======== */}
|
||||||
{singleton?.modalType === 'drawer' && (
|
{singleton?.modalType === 'drawer' && SingletonPage && (
|
||||||
<Drawer
|
<Drawer
|
||||||
open
|
open
|
||||||
title={singleton.label}
|
title={singleton.label}
|
||||||
onClose={closeSingleton}
|
onClose={closeSingleton}
|
||||||
destroyOnClose
|
destroyOnHidden={true}
|
||||||
width={480}
|
size={480}
|
||||||
>
|
>
|
||||||
{singletonContent}
|
<SingletonPage />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -97,8 +97,9 @@ const personalRight: NavItemConfig[] = [
|
|||||||
key: 'wechat-work',
|
key: 'wechat-work',
|
||||||
label: '企业微信',
|
label: '企业微信',
|
||||||
iconSrc: wechatWorkSvg,
|
iconSrc: wechatWorkSvg,
|
||||||
action: 'external',
|
action: 'page',
|
||||||
target: 'https://work.weixin.qq.com/',
|
target: '/wechat-work',
|
||||||
|
modalType: 'modal',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
98
src/pages/wechat-work/WechatWorkPage.tsx
Normal file
98
src/pages/wechat-work/WechatWorkPage.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// ============================================================
|
||||||
|
// WechatWorkPage — 企业微信二维码 Modal 内容
|
||||||
|
//
|
||||||
|
// 点击导航栏"企业微信"后以 Modal 形式展示二维码。
|
||||||
|
// 数据来源:GET /api/v1/creatives(Banner API)
|
||||||
|
//
|
||||||
|
// 识别逻辑:Banner 中 category === 'promotion' 的条目,
|
||||||
|
// 其 image_url 为企业微信二维码图片
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { Spin, Empty } from 'antd';
|
||||||
|
import { WechatOutlined } from '@ant-design/icons';
|
||||||
|
import { fetchBanners, type Banner } from '@/services/modules';
|
||||||
|
|
||||||
|
export function WechatWorkPage() {
|
||||||
|
const [banner, setBanner] = useState<Banner | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const list = await fetchBanners();
|
||||||
|
// category === 'promotion' → 企业微信二维码
|
||||||
|
const promotion = list.find(
|
||||||
|
(item) => item.is_active && item.category === 'promotion',
|
||||||
|
);
|
||||||
|
setBanner(promotion ?? null);
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message || '加载失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
// 加载态
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 48 }}>
|
||||||
|
<Spin description="加载中..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无数据 / 错误
|
||||||
|
if (error || !banner || !banner.image_url) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 48 }}>
|
||||||
|
<Empty
|
||||||
|
image={<WechatOutlined style={{ fontSize: 48, color: '#9CA3AF' }} />}
|
||||||
|
description={error || '暂未获取到企业微信二维码,请联系管理员'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, padding: 8 }}>
|
||||||
|
{/* 二维码图片 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 240,
|
||||||
|
height: 240,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: '#fff',
|
||||||
|
border: '1px solid #E5E7EB',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={banner.image_url}
|
||||||
|
alt="企业微信二维码"
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题(如果 Banner 有标题则显示) */}
|
||||||
|
{banner.title && (
|
||||||
|
<p style={{ margin: 0, fontSize: 14, fontWeight: 500, color: 'inherit' }}>
|
||||||
|
{banner.title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 提示文字 */}
|
||||||
|
<p style={{ margin: 0, fontSize: 13, color: 'inherit', opacity: 0.5 }}>
|
||||||
|
请使用微信扫描二维码添加企业微信
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user