mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 05:51:26 +08:00
commit7c8bf8b591Author: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 17:59:38 2025 +0800 fix: add token usage to agent session message commitff8e5ddd27Author: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 17:25:54 2025 +0800 fix: close prompt stream when finish or error chunk received commit530e6516fdAuthor: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 17:19:53 2025 +0800 chore: code cleanup commitab21c0d56cAuthor: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 16:13:36 2025 +0800 feat(SessionItem): implement auto-rename feature for sessions and improve context menu handling - Added a new context menu option to automatically rename sessions based on topics. - Introduced useDeferredValue for managing target session state. - Updated imports to include necessary thunk actions and components. - Enhanced API service to handle optional assistant model in message summary fetching. - Exported renameAgentSessionIfNeeded function for better accessibility in the store. commit21ea8ccf37Merge:ab7b207d2816a92c60Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 15:29:09 2025 +0800 Merge branch 'main' of github.com:CherryHQ/cherry-studio into refactor/heroui-antd # Conflicts: # src/renderer/src/pages/home/Tabs/components/AddButton.tsx # src/renderer/src/pages/home/Tabs/components/SessionItem.tsx # src/renderer/src/pages/home/Tabs/components/Sessions.tsx # src/renderer/src/pages/home/Tabs/components/Topics.tsx # src/renderer/src/pages/paintings/NewApiPage.tsx commitab7b207d29Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 14:50:05 2025 +0800 refactor: streamline event listener management in useAppInit and update ToolPermissionRequestCard styling commit3834c5d402Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 14:21:25 2025 +0800 refactor: enhance API server state management and remove unused initialization in useAppInit commita64b94a41fAuthor: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 13:21:58 2025 +0800 refactor: update OpenAPI documentation paths to include subdirectories for better route coverage commit2e0ff28505Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 12:26:09 2025 +0800 refactor: center align columns in InstalledPluginsList and set AntTable size to small commit84bf94e2ffAuthor: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 12:06:09 2025 +0800 refactor: align create agent model selection with edit agent commit84f2281506Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 11:29:32 2025 +0800 refactor: integrate API server functionality into various components and enhance user notifications commit4e01210df4Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:56:38 2025 +0800 refactor: replace ContextMenu with Dropdown in AgentItem and SessionItem components for improved context menu handling commit9df38c7e83Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:27:30 2025 +0800 refactor: update AddButton styling to use CSS variable for border radius and remove unused settings hook commit251c269ab3Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:11:21 2025 +0800 refactor: remove unused error handling alerts from AssistantsTab component commit9b9640d8d1Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:07:26 2025 +0800 refactor: adjust margin styling for UnifiedAddButton component commitedd6b11aa7Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:04:01 2025 +0800 refactor: update AddButton styling based on topic position and clean up CSS for root element commit1c0de625d8Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 09:56:42 2025 +0800 fix: update assistant addition messages for multiple languages commit0ea4dd4e3aAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 21:01:24 2025 +0800 fix: init message api err commitf3bbd4ed44Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 20:42:49 2025 +0800 refactor: remove heroui commitd01609fc36Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 19:08:41 2025 +0800 refactor: migrate heroui/toast to antd message commitf4b14dfc10Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 18:51:29 2025 +0800 refactor: enhance Sessions component layout with styled Scrollbar and adjust UnifiedAddButton margins commit6ae5f69163Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 18:44:13 2025 +0800 refactor: update PluginSettings and ToolingSettings for improved layout and functionality commitfcb0020787Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 18:29:52 2025 +0800 wip commit02265f369eAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 17:26:39 2025 +0800 fix: error block related commit5e22d9d36fAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 17:14:25 2025 +0800 fix: note head nav related commit3f52b7766aAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 16:45:49 2025 +0800 chore: remove dead code commit484622f12bAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 16:43:12 2025 +0800 chore: remove dead code commit2bceb302e0Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 15:33:25 2025 +0800 fix: tool setting related commit5c455f25ebAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 13:59:33 2025 +0800 chore: remove dead code commitd1d1dbc046Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 13:51:41 2025 +0800 fix: tool permission card related commitbf4ec23ef7Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 12:22:53 2025 +0800 fix: remove button and modal renaming commit47db5baeb1Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 12:20:36 2025 +0800 fix: plugin setting related commit81fecce552Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:16:42 2025 +0800 refactor: enhance ChatNavbarContent structure by replacing Breadcrumbs with custom layout and adding separators commitfc64b6c611Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:10:48 2025 +0800 refactor: simplify MessageAgentTools component structure by removing unnecessary wrapper div commite0f383a050Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:08:32 2025 +0800 fix: update button classes in AddAssistantOrAgentPopup for improved cursor behavior commit720284262fAuthor: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:06:58 2025 +0800 refactor: update AgentModal to use TopView for improved modal management and enhance form structure commitb334a2c5beAuthor: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 11:40:47 2025 +0800 refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling commit468aebd632Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 10:56:40 2025 +0800 fix: plugins related wip commitbd4a979f62Author: dev <verc20.dev@proton.me> Date: Tue Nov 4 17:46:14 2025 +0800 fix: add button related commitb3316a4dc8Author: dev <verc20.dev@proton.me> Date: Tue Nov 4 17:18:31 2025 +0800 fix: agent tool result related components commit6ca7597a98Author: dev <verc20.dev@proton.me> Date: Tue Nov 4 11:12:01 2025 +0800 fix: lint commit7d0f0b38a6Author: kangfenmao <kangfenmao@qq.com> Date: Tue Nov 4 09:56:32 2025 +0800 wip commit96a607a410Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 20:23:25 2025 +0800 wip commit235ad16252Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 20:08:45 2025 +0800 wip commitf23fe1b9e9Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 19:15:01 2025 +0800 wip commit28fac543fcAuthor: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 18:39:39 2025 +0800 wip commit3cc7ee01e2Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 17:33:13 2025 +0800 wip commit37bdf9e508Author: kangfenmao <kangfenmao@qq.com> Date: Sat Nov 1 19:16:58 2025 +0800 wip commit1bf5104f97Author: kangfenmao <kangfenmao@qq.com> Date: Sat Nov 1 12:12:01 2025 +0800 wip
554 lines
18 KiB
TypeScript
554 lines
18 KiB
TypeScript
import { loggerService } from '@logger'
|
||
import { AppLogo } from '@renderer/config/env'
|
||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||
import type { WebSocketCandidatesResponse } from '@shared/config/types'
|
||
import { Alert, Button, Modal, Progress, Spin } from 'antd'
|
||
import { QRCodeSVG } from 'qrcode.react'
|
||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||
import { useTranslation } from 'react-i18next'
|
||
|
||
import { TopView } from '../TopView'
|
||
|
||
const logger = loggerService.withContext('ExportToPhoneLanPopup')
|
||
|
||
interface Props {
|
||
resolve: (data: any) => void
|
||
}
|
||
|
||
type ConnectionPhase = 'initializing' | 'waiting_qr_scan' | 'connecting' | 'connected' | 'disconnected' | 'error'
|
||
type TransferPhase = 'idle' | 'preparing' | 'sending' | 'completed' | 'error'
|
||
|
||
const LoadingQRCode: React.FC = () => {
|
||
const { t } = useTranslation()
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||
<Spin />
|
||
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
|
||
{t('settings.data.export_to_phone.lan.generating_qr')}
|
||
</span>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||
const { t } = useTranslation()
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||
<QRCodeSVG
|
||
marginSize={2}
|
||
value={qrCodeValue}
|
||
level="H"
|
||
size={200}
|
||
imageSettings={{
|
||
src: AppLogo,
|
||
width: 40,
|
||
height: 40,
|
||
excavate: true
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>
|
||
{t('settings.data.export_to_phone.lan.scan_qr')}
|
||
</span>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const ConnectingAnimation: React.FC = () => {
|
||
const { t } = useTranslation()
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||
<div
|
||
style={{
|
||
width: '160px',
|
||
height: '160px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
border: '2px dashed var(--color-status-warning)',
|
||
borderRadius: '12px',
|
||
backgroundColor: 'var(--color-status-warning)'
|
||
}}>
|
||
<Spin size="large" />
|
||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
|
||
{t('settings.data.export_to_phone.lan.status.connecting')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const ConnectedDisplay: React.FC = () => {
|
||
const { t } = useTranslation()
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||
<div
|
||
style={{
|
||
width: '160px',
|
||
height: '160px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
border: '2px dashed var(--color-status-success)',
|
||
borderRadius: '12px',
|
||
backgroundColor: 'var(--color-status-success)'
|
||
}}>
|
||
<span style={{ fontSize: '48px' }}>📱</span>
|
||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '8px' }}>
|
||
{t('settings.data.export_to_phone.lan.connected')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const ErrorQRCode: React.FC<{ error: string | null }> = ({ error }) => {
|
||
const { t } = useTranslation()
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
gap: '12px',
|
||
padding: '20px',
|
||
border: `1px solid var(--color-error)`,
|
||
borderRadius: '8px',
|
||
backgroundColor: 'var(--color-error)'
|
||
}}>
|
||
<span style={{ fontSize: '48px' }}>⚠️</span>
|
||
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>
|
||
{t('settings.data.export_to_phone.lan.connection_failed')}
|
||
</span>
|
||
{error && <span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>{error}</span>}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||
const [isOpen, setIsOpen] = useState(true)
|
||
const [connectionPhase, setConnectionPhase] = useState<ConnectionPhase>('initializing')
|
||
const [transferPhase, setTransferPhase] = useState<TransferPhase>('idle')
|
||
const [qrCodeValue, setQrCodeValue] = useState('')
|
||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||
const [sendProgress, setSendProgress] = useState(0)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
|
||
|
||
const { t } = useTranslation()
|
||
|
||
// 派生状态
|
||
const isConnected = connectionPhase === 'connected'
|
||
const canSend = isConnected && selectedFolderPath && transferPhase === 'idle'
|
||
const isSending = transferPhase === 'preparing' || transferPhase === 'sending'
|
||
|
||
// 状态文本映射
|
||
const connectionStatusText = useMemo(() => {
|
||
const statusMap = {
|
||
initializing: t('settings.data.export_to_phone.lan.status.initializing'),
|
||
waiting_qr_scan: t('settings.data.export_to_phone.lan.status.waiting_qr_scan'),
|
||
connecting: t('settings.data.export_to_phone.lan.status.connecting'),
|
||
connected: t('settings.data.export_to_phone.lan.status.connected'),
|
||
disconnected: t('settings.data.export_to_phone.lan.status.disconnected'),
|
||
error: t('settings.data.export_to_phone.lan.status.error')
|
||
}
|
||
return statusMap[connectionPhase]
|
||
}, [connectionPhase, t])
|
||
|
||
const transferStatusText = useMemo(() => {
|
||
const statusMap = {
|
||
idle: '',
|
||
preparing: t('settings.data.export_to_phone.lan.status.preparing'),
|
||
sending: t('settings.data.export_to_phone.lan.status.sending'),
|
||
completed: t('settings.data.export_to_phone.lan.status.completed'),
|
||
error: t('settings.data.export_to_phone.lan.status.error')
|
||
}
|
||
return statusMap[transferPhase]
|
||
}, [transferPhase, t])
|
||
|
||
// 状态样式映射
|
||
const connectionStatusStyles = useMemo(() => {
|
||
const styleMap = {
|
||
initializing: {
|
||
bg: 'var(--color-background-mute)',
|
||
border: 'var(--color-border-mute)'
|
||
},
|
||
waiting_qr_scan: {
|
||
bg: 'var(--color-primary-mute)',
|
||
border: 'var(--color-primary-soft)'
|
||
},
|
||
connecting: { bg: 'var(--color-status-warning)', border: 'var(--color-status-warning)' },
|
||
connected: {
|
||
bg: 'var(--color-status-success)',
|
||
border: 'var(--color-status-success)'
|
||
},
|
||
disconnected: { bg: 'var(--color-error)', border: 'var(--color-error)' },
|
||
error: { bg: 'var(--color-error)', border: 'var(--color-error)' }
|
||
}
|
||
return styleMap[connectionPhase]
|
||
}, [connectionPhase])
|
||
|
||
const initWebSocket = useCallback(async () => {
|
||
try {
|
||
setConnectionPhase('initializing')
|
||
await window.api.webSocket.start()
|
||
const { port, ip } = await window.api.webSocket.status()
|
||
|
||
if (ip && port) {
|
||
const candidatesData = await window.api.webSocket.getAllCandidates()
|
||
|
||
const optimizeConnectionInfo = () => {
|
||
const ipToNumber = (ip: string) => {
|
||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0)
|
||
}
|
||
|
||
const compressedData = [
|
||
'CSA',
|
||
ipToNumber(ip),
|
||
candidatesData.map((candidate: WebSocketCandidatesResponse) => ipToNumber(candidate.host)),
|
||
port, // 端口号
|
||
Date.now() % 86400000
|
||
]
|
||
|
||
return compressedData
|
||
}
|
||
|
||
const compressedData = optimizeConnectionInfo()
|
||
const qrCodeValue = JSON.stringify(compressedData)
|
||
setQrCodeValue(qrCodeValue)
|
||
setConnectionPhase('waiting_qr_scan')
|
||
} else {
|
||
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
|
||
setConnectionPhase('error')
|
||
}
|
||
} catch (error) {
|
||
setError(
|
||
`${t('settings.data.export_to_phone.lan.error.init_failed')}: ${error instanceof Error ? error.message : ''}`
|
||
)
|
||
setConnectionPhase('error')
|
||
logger.error('Failed to initialize WebSocket:', error as Error)
|
||
}
|
||
}, [t])
|
||
|
||
const handleClientConnected = useCallback((_event: any, data: { connected: boolean }) => {
|
||
logger.info(`Client connection status: ${data.connected ? 'connected' : 'disconnected'}`)
|
||
if (data.connected) {
|
||
setConnectionPhase('connected')
|
||
setError(null)
|
||
} else {
|
||
setConnectionPhase('disconnected')
|
||
}
|
||
}, [])
|
||
|
||
const handleMessageReceived = useCallback((_event: any, data: any) => {
|
||
logger.info(`Received message from mobile: ${JSON.stringify(data)}`)
|
||
}, [])
|
||
|
||
const handleSendProgress = useCallback(
|
||
(_event: any, data: { progress: number }) => {
|
||
const progress = data.progress
|
||
setSendProgress(progress)
|
||
|
||
if (transferPhase === 'preparing' && progress > 0) {
|
||
setTransferPhase('sending')
|
||
}
|
||
|
||
if (progress >= 100) {
|
||
setTransferPhase('completed')
|
||
// 启动 3 秒倒计时自动关闭
|
||
setAutoCloseCountdown(3)
|
||
}
|
||
},
|
||
[transferPhase]
|
||
)
|
||
|
||
const handleSelectZip = useCallback(async () => {
|
||
const result = await window.api.file.select()
|
||
if (result) {
|
||
setSelectedFolderPath(result[0].path)
|
||
}
|
||
}, [])
|
||
|
||
const handleSendZip = useCallback(async () => {
|
||
if (!selectedFolderPath) {
|
||
setError(t('settings.data.export_to_phone.lan.error.no_file'))
|
||
return
|
||
}
|
||
|
||
setTransferPhase('preparing')
|
||
setError(null)
|
||
setSendProgress(0)
|
||
|
||
try {
|
||
logger.info(`Starting file transfer: ${selectedFolderPath}`)
|
||
await window.api.webSocket.sendFile(selectedFolderPath)
|
||
} catch (error) {
|
||
setError(
|
||
`${t('settings.data.export_to_phone.lan.error.send_failed')}: ${error instanceof Error ? error.message : ''}`
|
||
)
|
||
setTransferPhase('error')
|
||
logger.error('Failed to send file:', error as Error)
|
||
}
|
||
}, [selectedFolderPath, t])
|
||
|
||
// 尝试关闭弹窗 - 如果正在传输则显示确认
|
||
const handleCancel = useCallback(() => {
|
||
if (isSending) {
|
||
window.modal.confirm({
|
||
title: t('settings.data.export_to_phone.lan.confirm_close_title'),
|
||
content: t('settings.data.export_to_phone.lan.confirm_close_message'),
|
||
centered: true,
|
||
okButtonProps: {
|
||
danger: true
|
||
},
|
||
okText: t('settings.data.export_to_phone.lan.force_close'),
|
||
onOk: () => setIsOpen(false)
|
||
})
|
||
} else {
|
||
setIsOpen(false)
|
||
}
|
||
}, [isSending, t])
|
||
|
||
// 清理并关闭
|
||
const handleClose = useCallback(async () => {
|
||
try {
|
||
// 主动断开 WebSocket 连接
|
||
if (isConnected || connectionPhase !== 'disconnected') {
|
||
logger.info('Closing popup, stopping WebSocket')
|
||
await window.api.webSocket.stop()
|
||
}
|
||
} catch (error) {
|
||
logger.error('Failed to stop WebSocket on close:', error as Error)
|
||
}
|
||
resolve({})
|
||
}, [resolve, isConnected, connectionPhase])
|
||
|
||
useEffect(() => {
|
||
initWebSocket()
|
||
|
||
const removeClientConnectedListener = window.electron.ipcRenderer.on(
|
||
'websocket-client-connected',
|
||
handleClientConnected
|
||
)
|
||
const removeMessageReceivedListener = window.electron.ipcRenderer.on(
|
||
'websocket-message-received',
|
||
handleMessageReceived
|
||
)
|
||
const removeSendProgressListener = window.electron.ipcRenderer.on('file-send-progress', handleSendProgress)
|
||
|
||
return () => {
|
||
removeClientConnectedListener()
|
||
removeMessageReceivedListener()
|
||
removeSendProgressListener()
|
||
window.api.webSocket.stop()
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [])
|
||
|
||
// 自动关闭倒计时
|
||
useEffect(() => {
|
||
if (autoCloseCountdown === null) return
|
||
|
||
if (autoCloseCountdown <= 0) {
|
||
logger.debug('Auto-closing popup after transfer completion')
|
||
setIsOpen(false)
|
||
return
|
||
}
|
||
|
||
const timer = setTimeout(() => {
|
||
setAutoCloseCountdown(autoCloseCountdown - 1)
|
||
}, 1000)
|
||
|
||
return () => clearTimeout(timer)
|
||
}, [autoCloseCountdown])
|
||
|
||
// 状态指示器组件
|
||
const StatusIndicator = useCallback(
|
||
() => (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '8px',
|
||
padding: '5px 12px',
|
||
width: '100%',
|
||
backgroundColor: connectionStatusStyles.bg,
|
||
border: `1px solid ${connectionStatusStyles.border}`,
|
||
marginBottom: 10
|
||
}}>
|
||
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
|
||
</div>
|
||
),
|
||
[connectionStatusStyles, connectionStatusText]
|
||
)
|
||
|
||
// 二维码显示组件 - 使用显式条件渲染以避免类型不匹配
|
||
const QRCodeDisplay = useCallback(() => {
|
||
switch (connectionPhase) {
|
||
case 'waiting_qr_scan':
|
||
case 'disconnected':
|
||
return <ScanQRCode qrCodeValue={qrCodeValue} />
|
||
case 'initializing':
|
||
return <LoadingQRCode />
|
||
case 'connecting':
|
||
return <ConnectingAnimation />
|
||
case 'connected':
|
||
return <ConnectedDisplay />
|
||
case 'error':
|
||
return <ErrorQRCode error={error} />
|
||
default:
|
||
return null
|
||
}
|
||
}, [connectionPhase, qrCodeValue, error])
|
||
|
||
// 传输进度组件
|
||
const TransferProgress = useCallback(() => {
|
||
if (!isSending && transferPhase !== 'completed') return null
|
||
|
||
return (
|
||
<div style={{ paddingTop: '20px' }}>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '8px',
|
||
padding: '12px',
|
||
border: `1px solid var(--color-border)`,
|
||
borderRadius: '8px',
|
||
backgroundColor: 'var(--color-background-mute)'
|
||
}}>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
fontSize: '14px',
|
||
fontWeight: '500'
|
||
}}>
|
||
<span style={{ color: 'var(--color-text)' }}>
|
||
{t('settings.data.export_to_phone.lan.transfer_progress')}
|
||
</span>
|
||
<span
|
||
style={{ color: transferPhase === 'completed' ? 'var(--color-status-success)' : 'var(--color-primary)' }}>
|
||
{transferPhase === 'completed' ? '✅ ' + t('common.completed') : `${Math.round(sendProgress)}%`}
|
||
</span>
|
||
</div>
|
||
|
||
<Progress
|
||
percent={Math.round(sendProgress)}
|
||
status={transferPhase === 'completed' ? 'success' : 'active'}
|
||
showInfo={false}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
}, [isSending, transferPhase, sendProgress, t])
|
||
|
||
const AutoCloseCountdown = useCallback(() => {
|
||
if (transferPhase !== 'completed' || autoCloseCountdown === null || autoCloseCountdown <= 0) return null
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
fontSize: '12px',
|
||
color: 'var(--color-text-2)',
|
||
textAlign: 'center',
|
||
paddingTop: '4px'
|
||
}}>
|
||
{t('settings.data.export_to_phone.lan.auto_close_tip', { seconds: autoCloseCountdown })}
|
||
</div>
|
||
)
|
||
}, [transferPhase, autoCloseCountdown, t])
|
||
|
||
// 错误显示组件
|
||
const ErrorDisplay = useCallback(() => {
|
||
if (!error || transferPhase !== 'error') return null
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
padding: '12px',
|
||
border: `1px solid var(--color-error)`,
|
||
borderRadius: '8px',
|
||
backgroundColor: 'var(--color-error)',
|
||
textAlign: 'center'
|
||
}}>
|
||
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>❌ {error}</span>
|
||
</div>
|
||
)
|
||
}, [error, transferPhase])
|
||
|
||
return (
|
||
<Modal
|
||
open={isOpen}
|
||
onCancel={handleCancel}
|
||
afterClose={handleClose}
|
||
title={t('settings.data.export_to_phone.lan.title')}
|
||
centered
|
||
closable={!isSending}
|
||
maskClosable={false}
|
||
keyboard={true}
|
||
footer={null}
|
||
styles={{ body: { paddingBottom: 10 } }}>
|
||
<SettingRow>
|
||
<StatusIndicator />
|
||
</SettingRow>
|
||
|
||
<Alert message={t('settings.data.export_to_phone.lan.content')} type="info" style={{ borderRadius: 0 }} />
|
||
|
||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px', marginBlock: 25 }}>
|
||
<QRCodeDisplay />
|
||
</SettingRow>
|
||
|
||
<SettingRow style={{ display: 'flex', alignItems: 'center', marginBlock: 10 }}>
|
||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||
<Button onClick={handleSelectZip} disabled={isSending}>
|
||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||
</Button>
|
||
<Button type="primary" onClick={handleSendZip} disabled={!canSend} loading={isSending}>
|
||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||
</Button>
|
||
</div>
|
||
</SettingRow>
|
||
|
||
<SettingHelpText
|
||
style={{
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
textAlign: 'center'
|
||
}}>
|
||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||
</SettingHelpText>
|
||
|
||
<TransferProgress />
|
||
<AutoCloseCountdown />
|
||
<ErrorDisplay />
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
const TopViewKey = 'ExportToPhoneLanPopup'
|
||
|
||
export default class ExportToPhoneLanPopup {
|
||
static topviewId = 0
|
||
static hide() {
|
||
TopView.hide(TopViewKey)
|
||
}
|
||
static show() {
|
||
return new Promise<any>((resolve) => {
|
||
TopView.show(
|
||
<PopupContainer
|
||
resolve={(v) => {
|
||
resolve(v)
|
||
TopView.hide(TopViewKey)
|
||
}}
|
||
/>,
|
||
TopViewKey
|
||
)
|
||
})
|
||
}
|
||
}
|