feat: 使用 FloatButton 上传 PageSpy 日志

2.0/email-builder
Jimmy Liow 12 months ago
parent 7c3572bd3f
commit e5a48f9ac2

@ -2,7 +2,15 @@ import ErrorBoundary from '@/components/ErrorBoundary'
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { useThemeContext } from '@/stores/ThemeContext' import { useThemeContext } from '@/stores/ThemeContext'
import { App as AntApp, ConfigProvider, Empty, theme } from 'antd' import {
App as AntApp,
ConfigProvider,
Empty,
message,
FloatButton,
theme,
} from 'antd'
import { BugOutlined } from '@ant-design/icons'
import zhLocale from 'antd/locale/zh_CN' import zhLocale from 'antd/locale/zh_CN'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -15,44 +23,56 @@ import '@/assets/App.css'
import 'react-chat-elements/dist/main.css' import 'react-chat-elements/dist/main.css'
function AuthApp() { function AuthApp() {
const navigate = useNavigate() const navigate = useNavigate()
const [messageApi, contextHolder] = message.useMessage()
const { colorPrimary, borderRadius } = useThemeContext() const { colorPrimary, borderRadius } = useThemeContext()
const loginUser = useAuthStore(state => state.loginUser) const loginUser = useAuthStore((state) => state.loginUser)
const href = useHref() const href = useHref()
const [connectWebsocket, fetchInitialData, disconnectWebsocket ] = useConversationStore((state) => [ const [connectWebsocket, fetchInitialData, disconnectWebsocket] =
state.connectWebsocket, useConversationStore((state) => [
state.fetchInitialData, state.connectWebsocket,
state.disconnectWebsocket, state.fetchInitialData,
]); state.disconnectWebsocket,
])
useEffect(() => { useEffect(() => {
if (!("Notification" in window)) { if (!('Notification' in window)) {
// alert("This browser does not support desktop notification"); // alert("This browser does not support desktop notification");
} else { } else {
Notification.requestPermission(); Notification.requestPermission()
} }
if (loginUser.userId > 0) { if (loginUser.userId > 0) {
appendRequestHeader('X-User-Id', loginUser.userId); appendRequestHeader('X-User-Id', loginUser.userId)
loadPageSpy(loginUser.username); loadPageSpy(loginUser.username)
connectWebsocket(loginUser.userId); connectWebsocket(loginUser.userId)
fetchInitialData(loginUser.userId); fetchInitialData(loginUser.userId)
} }
return () => { return () => {
disconnectWebsocket(); disconnectWebsocket()
}; }
}, []) }, [])
const uploadLog = () => {
if (window.$pageSpy) {
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload')
messageApi.info('Success')
} else {
messageApi.error('Failure')
}
}
// /p... // /p...
const needToLogin = (loginUser.userId === -1) && (href.indexOf('/p/') === -1) const needToLogin = loginUser.userId === -1 && href.indexOf('/p/') === -1
useEffect(() => { useEffect(() => {
if (needToLogin) { if (needToLogin) {
navigate('/p/dingding/login?origin_url=' + href) navigate('/p/dingding/login?origin_url=' + href)
} }
}, [href]) }, [href])
return ( return (
<ConfigProvider <ConfigProvider
@ -66,20 +86,40 @@ function AuthApp() {
algorithm: theme.defaultAlgorithm, algorithm: theme.defaultAlgorithm,
}} }}
locale={zhLocale} locale={zhLocale}
renderEmpty={() => <Empty description={false} />}> renderEmpty={() => <Empty description={false} />}
>
<AntApp> <AntApp>
<ErrorBoundary> <ErrorBoundary>
<FloatButton.Group
shape="square"
style={{
insetInlineEnd: 94,
}}
>
<FloatButton icon={<BugOutlined />} onClick={() => uploadLog()} />
<FloatButton.BackTop />
</FloatButton.Group>
{contextHolder}
{needToLogin ? <>login...</> : <Outlet />} {needToLogin ? <>login...</> : <Outlet />}
<dialog id='about-dialog' className='border-0'> <dialog id="about-dialog" className="border-0">
<img className='logo' src={AppLogo} alt='logo' /> <img className="logo" src={AppLogo} alt="logo" />
<section className='about'> <section className="about">
<h1>销售平台</h1> <h1>销售平台</h1>
<h2>Sales CRM</h2> <h2>Sales CRM</h2>
<p>Haina travel global sales CRM system</p> <p>Haina travel global sales CRM system</p>
</section> </section>
<form className='actions flex gap-1' method='dialog'> <form className="actions flex gap-1" method="dialog">
<button value='cancel' className='px-4 py-2 rounded-full border-0'>Close</button> <button
<button value='install' id='install-button' className='px-4 py-2 rounded-full border-0 border-transparent bg-indigo-500 text-white'> value="cancel"
className="px-4 py-2 rounded-full border-0"
>
Close
</button>
<button
value="install"
id="install-button"
className="px-4 py-2 rounded-full border-0 border-transparent bg-indigo-500 text-white"
>
Install app Install app
</button> </button>
</form> </form>
@ -87,7 +127,7 @@ function AuthApp() {
</ErrorBoundary> </ErrorBoundary>
</AntApp> </AntApp>
</ConfigProvider> </ConfigProvider>
); )
} }
export default AuthApp export default AuthApp

@ -2,7 +2,18 @@ import useAuthStore from '@/stores/AuthStore'
import useConversationStore from '@/stores/ConversationStore' import useConversationStore from '@/stores/ConversationStore'
import { useThemeContext } from '@/stores/ThemeContext' import { useThemeContext } from '@/stores/ThemeContext'
import { DownOutlined } from '@ant-design/icons' import { DownOutlined } from '@ant-design/icons'
import { Avatar, Col, Dropdown, Layout, Menu, Row, Space, Typography, theme, Badge } from 'antd' import {
Avatar,
Col,
Dropdown,
Layout,
Menu,
Row,
Space,
Typography,
theme,
Badge,
} from 'antd'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link, NavLink, Outlet, useHref } from 'react-router-dom' import { Link, NavLink, Outlet, useHref } from 'react-router-dom'
@ -10,9 +21,8 @@ import { Link, NavLink, Outlet, useHref } from 'react-router-dom'
import '@/assets/App.css' import '@/assets/App.css'
import AppLogo from '@/assets/highlights_travel_300_300.png' import AppLogo from '@/assets/highlights_travel_300_300.png'
import 'react-chat-elements/dist/main.css' import 'react-chat-elements/dist/main.css'
import ReloadPrompt from './ReloadPrompt'; import ReloadPrompt from './ReloadPrompt'
import ClearCache from './ClearCache'; import ClearCache from './ClearCache'
import PageSpy from './PageSpy';
import { BUILD_VERSION, BUILD_DATE } from '@/config' import { BUILD_VERSION, BUILD_DATE } from '@/config'
@ -20,9 +30,8 @@ const { Header, Footer, Content } = Layout
const { Title } = Typography const { Title } = Typography
function DesktopApp() { function DesktopApp() {
const { colorPrimary } = useThemeContext() const { colorPrimary } = useThemeContext()
const loginUser = useAuthStore(state => state.loginUser) const loginUser = useAuthStore((state) => state.loginUser)
const href = useHref() const href = useHref()
@ -31,7 +40,7 @@ function DesktopApp() {
let defaultPath = '/order/follow' let defaultPath = '/order/follow'
if (href !== '/') { if (href !== '/') {
const splitPath = href.split('/'); const splitPath = href.split('/')
if (splitPath.length > 2) { if (splitPath.length > 2) {
defaultPath = '/' + splitPath[1] + '/' + splitPath[2] defaultPath = '/' + splitPath[1] + '/' + splitPath[2]
} }
@ -44,46 +53,60 @@ function DesktopApp() {
/** /**
* 标签页标题闪烁 * 标签页标题闪烁
*/ */
const [isTitleVisible, setIsTitleVisible] = useState(true); const [isTitleVisible, setIsTitleVisible] = useState(true)
useEffect(() => { useEffect(() => {
let interval; let interval
if (totalNotify > 0) { if (totalNotify > 0) {
if ('setAppBadge' in navigator) { if ('setAppBadge' in navigator) {
navigator.setAppBadge(totalNotify).catch((error) => {}); navigator.setAppBadge(totalNotify).catch((error) => {})
} }
interval = setInterval(() => { interval = setInterval(() => {
document.title = isTitleVisible ? `🔔🔥💬【${totalNotify}条新消息】` : '______________'; document.title = isTitleVisible
setIsTitleVisible(!isTitleVisible); ? `🔔🔥💬【${totalNotify}条新消息】`
}, 500); : '______________'
setIsTitleVisible(!isTitleVisible)
}, 500)
} else { } else {
document.title = '销售平台'; document.title = '销售平台'
if ('clearAppBadge' in navigator) { if ('clearAppBadge' in navigator) {
navigator.clearAppBadge().catch((error) => {}); navigator.clearAppBadge().catch((error) => {})
} }
} }
return () => clearInterval(interval); return () => clearInterval(interval)
}, [totalNotify, isTitleVisible]); }, [totalNotify, isTitleVisible])
return ( return (
<Layout> <Layout>
<Header className='header' style={{ position: 'sticky', top: 0, zIndex: 2, width: '100%', background: 'white' }}> <Header
<Row gutter={{ md: 24 }} align='middle'> className="header"
<Col flex='220px'> style={{
<NavLink to='/'> position: 'sticky',
<img src={AppLogo} className='logo' alt='App logo' /> top: 0,
zIndex: 2,
width: '100%',
background: 'white',
}}
>
<Row gutter={{ md: 24 }} align="middle">
<Col flex="220px">
<NavLink to="/">
<img src={AppLogo} className="logo" alt="App logo" />
</NavLink> </NavLink>
<Title level={3}>销售平台</Title> <Title level={3}>销售平台</Title>
</Col> </Col>
<Col span={10}> <Col span={10}>
<Menu <Menu
mode='horizontal' mode="horizontal"
selectedKeys={[defaultPath]} selectedKeys={[defaultPath]}
items={[ items={[
{ key: '/order/follow', label: <Link to='/order/follow'>订单跟踪</Link> }, {
key: '/order/follow',
label: <Link to="/order/follow">订单跟踪</Link>,
},
{ {
key: '/order/chat', key: '/order/chat',
label: ( label: (
<Link to='/order/chat'> <Link to="/order/chat">
在线聊天 在线聊天
<Badge <Badge
count={totalNotify > 0 ? totalNotify : undefined} count={totalNotify > 0 ? totalNotify : undefined}
@ -94,18 +117,32 @@ function DesktopApp() {
</Link> </Link>
), ),
}, },
{ key: '/callcenter/call', label: <Link to='/callcenter/call'>语音通话</Link> }, {
{ key: '/chat/history', label: <Link to='/chat/history'>聊天记录</Link> }, key: '/callcenter/call',
label: <Link to="/callcenter/call">语音通话</Link>,
},
{
key: '/chat/history',
label: <Link to="/chat/history">聊天记录</Link>,
},
]} ]}
/> />
</Col> </Col>
<Col flex='auto' style={{ color: 'white', marginBottom: '0', display: 'flex', justifyContent: 'end' }}> <Col
flex="auto"
style={{
color: 'white',
marginBottom: '0',
display: 'flex',
justifyContent: 'end',
}}
>
<ReloadPrompt /> <ReloadPrompt />
<Dropdown <Dropdown
menu={{ menu={{
items: [ items: [
{ {
label: <Link to='/account/profile'>个人资料</Link>, label: <Link to="/account/profile">个人资料</Link>,
key: '1', key: '1',
}, },
{ type: 'divider' }, { type: 'divider' },
@ -114,15 +151,21 @@ function DesktopApp() {
{ label: <ClearCache />, key: 'clearcache' }, { label: <ClearCache />, key: 'clearcache' },
{ type: 'divider' }, { type: 'divider' },
{ {
label: <Link to='/p/dingding/logout'>退出</Link>, label: <Link to="/p/dingding/logout">退出</Link>,
key: '3', key: '3',
}, },
], ],
}} }}
trigger={['click']}> trigger={['click']}
<a onClick={(e) => e.preventDefault()} style={{ color: colorPrimary }}> >
<a
onClick={(e) => e.preventDefault()}
style={{ color: colorPrimary }}
>
<Space> <Space>
<Avatar src={loginUser.avatarUrl}>{loginUser?.username?.substring(1)}</Avatar> <Avatar src={loginUser.avatarUrl}>
{loginUser?.username?.substring(1)}
</Avatar>
{loginUser.username} {loginUser.username}
<DownOutlined /> <DownOutlined />
</Space> </Space>
@ -138,13 +181,16 @@ function DesktopApp() {
margin: 0, margin: 0,
minHeight: 280, minHeight: 280,
background: colorBgContainer, background: colorBgContainer,
}}> }}
>
<Outlet /> <Outlet />
</Content> </Content>
</Layout> </Layout>
<Footer>桂林海纳国际旅行社有限公司 Version: {BUILD_VERSION}({BUILD_DATE}){' '}<PageSpy /></Footer> <Footer>
桂林海纳国际旅行社有限公司 Version: {BUILD_VERSION}({BUILD_DATE})
</Footer>
</Layout> </Layout>
); )
} }
export default DesktopApp export default DesktopApp

@ -1,59 +1,74 @@
import { useEffect } from 'react'; import { useEffect } from 'react'
import '@/assets/App.css'; import '@/assets/App.css'
import AppLogo from '@/assets/highlights_travel_300_300.png'; import AppLogo from '@/assets/highlights_travel_300_300.png'
import { useThemeContext } from '@/stores/ThemeContext'; import { useThemeContext } from '@/stores/ThemeContext'
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore'
import { Layout, Typography, theme, Space, Avatar, Dropdown, Flex } from 'antd'; import { Layout, Typography, theme, Space, Avatar, Dropdown, Flex } from 'antd'
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons'
import { NavLink, Outlet, Link } from 'react-router-dom'; import { NavLink, Outlet, Link } from 'react-router-dom'
import ReloadPrompt from './ReloadPrompt'; import ReloadPrompt from './ReloadPrompt'
import ClearCache from './ClearCache'; import ClearCache from './ClearCache'
import PageSpy from './PageSpy';
import { BUILD_VERSION } from '@/config'; import { BUILD_VERSION } from '@/config'
const { Header, Footer, Content } = Layout; const { Header, Footer, Content } = Layout
const { Title } = Typography; const { Title } = Typography
function MobileApp() { function MobileApp() {
const { colorPrimary, borderRadius } = useThemeContext(); const { colorPrimary, borderRadius } = useThemeContext()
const loginUser = useAuthStore((state) => state.loginUser); const loginUser = useAuthStore((state) => state.loginUser)
const { const {
token: { colorBgContainer }, token: { colorBgContainer },
} = theme.useToken(); } = theme.useToken()
useEffect(() => { useEffect(() => {
const handleLoad = () => { const handleLoad = () => {
const isPWAInstalled = window.matchMedia('(display-mode: window-controls-overlay)').matches || window.matchMedia('(display-mode: standalone)').matches; const isPWAInstalled =
const isStandalone = navigator.standalone || window.navigator.standalone; window.matchMedia('(display-mode: window-controls-overlay)').matches ||
window.matchMedia('(display-mode: standalone)').matches
const isStandalone = navigator.standalone || window.navigator.standalone
if (isPWAInstalled || isStandalone) { if (isPWAInstalled || isStandalone) {
document.getElementById('install-button').disabled = true; document.getElementById('install-button').disabled = true
} else { } else {
document.getElementById('about-dialog').showModal(); document.getElementById('about-dialog').showModal()
window.addEventListener('beforeinstallprompt', (e) => { window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); e.preventDefault()
document.getElementById('about-dialog').addEventListener('close', () => { document
if (document.getElementById('about-dialog').returnValue === 'install') { .getElementById('about-dialog')
e.prompt(); .addEventListener('close', () => {
} if (
}); document.getElementById('about-dialog').returnValue ===
}); 'install'
) {
e.prompt()
}
})
})
} }
}; }
window.addEventListener('load', handleLoad); window.addEventListener('load', handleLoad)
return () => window.removeEventListener('load', handleLoad); return () => window.removeEventListener('load', handleLoad)
}, []); }, [])
return ( return (
<Layout> <Layout>
<Header className='header px-2' style={{ position: 'sticky', top: 0, zIndex: 1, width: '100%', background: 'white' }}> <Header
className="header px-2"
style={{
position: 'sticky',
top: 0,
zIndex: 1,
width: '100%',
background: 'white',
}}
>
<Flex justify={'space-between'}> <Flex justify={'space-between'}>
<NavLink to='/'> <NavLink to="/">
<img src={AppLogo} className='logo' alt='App logo' /> <img src={AppLogo} className="logo" alt="App logo" />
{!('Notification' in window) && <span>🔕</span>} {!('Notification' in window) && <span>🔕</span>}
</NavLink> </NavLink>
<ReloadPrompt /> <ReloadPrompt />
@ -64,17 +79,21 @@ function MobileApp() {
{ type: 'divider' }, { type: 'divider' },
{ label: <ClearCache />, key: 'clearcache' }, { label: <ClearCache />, key: 'clearcache' },
{ type: 'divider' }, { type: 'divider' },
{ label: <Link to='/p/dingding/logout'>退出</Link>, key: '3' }, { label: <Link to="/p/dingding/logout">退出</Link>, key: '3' },
{ type: 'divider' }, { type: 'divider' },
{ label: <>v{BUILD_VERSION}</>, key: 'BUILD_VERSION' }, { label: <>v{BUILD_VERSION}</>, key: 'BUILD_VERSION' },
{ type: 'divider' },
{ label: <PageSpy />, key: 'pagespy' },
], ],
}} }}
trigger={['click']}> trigger={['click']}
<a onClick={(e) => e.preventDefault()} style={{ color: colorPrimary }}> >
<a
onClick={(e) => e.preventDefault()}
style={{ color: colorPrimary }}
>
<Space> <Space>
<Avatar src={loginUser.avatarUrl}>{loginUser?.username?.substring(1)}</Avatar> <Avatar src={loginUser.avatarUrl}>
{loginUser?.username?.substring(1)}
</Avatar>
{loginUser.username} {loginUser.username}
<DownOutlined /> <DownOutlined />
</Space> </Space>
@ -93,13 +112,14 @@ function MobileApp() {
margin: 0, margin: 0,
minHeight: 200, minHeight: 200,
background: colorBgContainer, background: colorBgContainer,
}}> }}
>
<Outlet /> <Outlet />
</Content> </Content>
</Layout> </Layout>
{/* <Footer>桂林海纳国际旅行社有限公司</Footer> */} {/* <Footer>桂林海纳国际旅行社有限公司</Footer> */}
</Layout> </Layout>
); )
} }
export default MobileApp; export default MobileApp

@ -1,17 +0,0 @@
const PageSpyLog = () => {
return (
<>
{window.$pageSpy && (
<a
className='text-primary'
onClick={() => {
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
}}>
上传Debug日志 ({window.$pageSpy.address.substring(0, 4)})
</a>
)}
</>
);
};
export default PageSpyLog;
Loading…
Cancel
Save