- Add CompanyExpireWarning component to layout header for expiring subscription alerts - Add CompanyExpire wrapper to guard rules and data-source routes - Refactor CompanyExpireGuard to support optional fullscreen mode via prop - Add responsive Popover support to VoiceCheck for mobile breakpoints - Hide rule CRUD action buttons when company validity is expired - Update profile page to display expiry status alongside validity date - Update i18n strings for clearer expiry messaging Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
190 lines
5.0 KiB
TypeScript
190 lines
5.0 KiB
TypeScript
import { ChromeOutlined, SoundOutlined } from '@ant-design/icons';
|
||
import { useBreakpoint } from '@ant-design/pro-components';
|
||
import { getLocale, Icon } from '@umijs/max';
|
||
import {
|
||
Alert,
|
||
Divider,
|
||
Flex,
|
||
Image,
|
||
Modal,
|
||
Popover,
|
||
Space,
|
||
Tabs,
|
||
Typography,
|
||
} from 'antd';
|
||
import { useCallback, useEffect, useState } from 'react';
|
||
import useRichI18n from '@/hooks/useRichI18n';
|
||
import { checkAudioPermission } from '@/utils';
|
||
import bus from '@/utils/eventBus';
|
||
import { $t } from '@/utils/i18n';
|
||
|
||
const { Paragraph } = Typography;
|
||
const VoiceCheck: React.FC = () => {
|
||
const { richFormat } = useRichI18n();
|
||
const screens = useBreakpoint();
|
||
const [visible, setVisible] = useState(false);
|
||
const [hasAudioPermission, setHasAudioPermission] = useState(true);
|
||
|
||
const checkAudio = useCallback((showPopup?: boolean) => {
|
||
checkAudioPermission().then((hasPermission) => {
|
||
setHasAudioPermission(hasPermission);
|
||
});
|
||
showPopup && setVisible(true);
|
||
}, []);
|
||
|
||
const openPopup = useCallback(() => setVisible(true), []);
|
||
|
||
useEffect(() => {
|
||
checkAudio();
|
||
|
||
bus.on('check-voice-permission', checkAudio);
|
||
bus.on('open-voice-popup', openPopup);
|
||
// document.addEventListener('click', () => checkAudio());
|
||
return () => {
|
||
bus.off('check-voice-permission', checkAudio);
|
||
bus.off('open-voice-popup', openPopup);
|
||
// document.removeEventListener('click', () => checkAudio());
|
||
};
|
||
}, []);
|
||
|
||
const browserGuides = [
|
||
{
|
||
key: 'chrome',
|
||
label: (
|
||
<span>
|
||
<ChromeOutlined /> Chrome
|
||
</span>
|
||
),
|
||
children: (
|
||
<div>
|
||
<Paragraph>1. {richFormat('pages.voiceCheck.chrome.tips')}</Paragraph>
|
||
<Paragraph>
|
||
2. {richFormat('pages.voiceCheck.chrome.tips2')}
|
||
</Paragraph>
|
||
<Paragraph>
|
||
3. {richFormat('pages.voiceCheck.chrome.tips3')}
|
||
</Paragraph>
|
||
<Paragraph>
|
||
4. {richFormat('pages.voiceCheck.chrome.tips4')}
|
||
</Paragraph>
|
||
<Paragraph>
|
||
5. {richFormat('pages.voiceCheck.chrome.tips5')}
|
||
</Paragraph>
|
||
<Image
|
||
width="100%"
|
||
src={require(
|
||
`@/assets/imgs/voice_permission_tips_${getLocale()}.png`,
|
||
)}
|
||
/>
|
||
</div>
|
||
),
|
||
},
|
||
];
|
||
const tipsNode = (
|
||
<>
|
||
<Alert
|
||
message={$t('pages.voiceCheck.alertMessage')}
|
||
type="warning"
|
||
showIcon
|
||
/>
|
||
</>
|
||
);
|
||
return (
|
||
<>
|
||
<Flex align="center" onClick={() => setVisible(true)}>
|
||
{!hasAudioPermission &&
|
||
(['xs', 'sm'].includes(screens || '') ? (
|
||
<Popover
|
||
content={tipsNode}
|
||
styles={{
|
||
body: { padding: 0 },
|
||
}}
|
||
zIndex={999}
|
||
defaultOpen={true}
|
||
>
|
||
<Icon
|
||
icon="local:silence"
|
||
height="20px"
|
||
fill="#f59e0b"
|
||
className="flex align-center"
|
||
/>
|
||
</Popover>
|
||
) : (
|
||
<>
|
||
{tipsNode}
|
||
<Icon
|
||
icon="local:silence"
|
||
height="20px"
|
||
fill="#f59e0b"
|
||
className="flex align-center"
|
||
/>
|
||
</>
|
||
))}
|
||
{/* {hasAudioPermission ? (
|
||
<Icon
|
||
icon="local:voice"
|
||
height="20px"
|
||
fill="#34d399"
|
||
className="flex align-center"
|
||
/>
|
||
) : (
|
||
<Icon
|
||
icon="local:silence"
|
||
height="20px"
|
||
fill="#f59e0b"
|
||
className="flex align-center"
|
||
/>
|
||
)} */}
|
||
</Flex>
|
||
|
||
<Modal
|
||
title={
|
||
<Space>
|
||
<SoundOutlined /> {$t('pages.voiceCheck.title')}
|
||
</Space>
|
||
}
|
||
open={visible}
|
||
onCancel={() => setVisible(false)}
|
||
width={800}
|
||
footer={null}
|
||
centered
|
||
styles={{
|
||
body: {
|
||
maxHeight: '85vh',
|
||
overflowY: 'auto',
|
||
overflowX: 'hidden',
|
||
marginRight: -20,
|
||
paddingRight: 8,
|
||
},
|
||
}}
|
||
>
|
||
<Space direction="vertical" size="middle">
|
||
<Alert
|
||
message={$t('pages.voiceCheck.alertMessage2')}
|
||
description={$t('pages.voiceCheck.alertMessage3')}
|
||
type="warning"
|
||
banner
|
||
/>
|
||
|
||
{/* <div>
|
||
<Space style={{ marginBottom: 10 }} align="center">
|
||
<InfoCircleOutlined />
|
||
<Text strong>{$t('pages.voiceCheck.tips')}:</Text>
|
||
</Space>
|
||
<div>{$t('pages.voiceCheck.tips2')}</div>
|
||
</div> */}
|
||
|
||
<Divider style={{ margin: 0 }}>
|
||
{$t('pages.voiceCheck.tips3')}
|
||
</Divider>
|
||
|
||
<Tabs defaultActiveKey="chrome" items={browserGuides} size="small" />
|
||
|
||
<Alert type="info" message={$t('pages.voiceCheck.tips4')} banner />
|
||
</Space>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
export default VoiceCheck;
|