@@ -265,8 +265,9 @@ | |||
window.ipcRenderer.invoke('re-sync-file', contextMsg); | |||
return; | |||
} | |||
if (false) { // 下载 | |||
if (contextMsg.notifyType === 1) { // 下载 | |||
window.ipcRenderer.invoke('download-file', contextMsg); | |||
return; | |||
} | |||
}); | |||
@@ -283,22 +284,14 @@ | |||
contextMsg = message; | |||
resetDomStatus(); | |||
let runningMethod = noop; | |||
// todo | |||
switch (message.taskSyncStatus) { | |||
case 'success': | |||
runningMethod = renderTaskSuccess; | |||
break; | |||
case 'error': | |||
runningMethod = renderTaskError; | |||
break; | |||
case 'incomming': | |||
runningMethod = renderTaskError; | |||
break; | |||
case 'conflict': | |||
runningMethod = renderTaskError; | |||
break; | |||
default: | |||
break; | |||
if(message.notifyType === 1) { | |||
runningMethod = renderTaskIncoming; | |||
} else if(message.notifyType === 2 || message.notifyType === 3) { | |||
runningMethod = renderConflictMsg; | |||
} else if(message.taskSyncStatus === 'TASK_SYNC_STATUS_FINISH') { | |||
runningMethod = renderTaskSuccess; | |||
} else if(message.taskSyncStatus === 'TASK_SYNC_STATUS_FAIL') { | |||
runningMethod = renderTaskError; | |||
} | |||
runningMethod(message); | |||
} | |||
@@ -337,7 +330,7 @@ | |||
useView(syncIncoming); | |||
syncMsgFileName.innerText = fileFullName(message); | |||
syncMsgDate.innerText = message.taskCreateDateStr; | |||
syncIncoming.innerText = '某某人同步了「某某文件」'; | |||
syncIncoming.innerText = message.notifyMessage; | |||
} | |||
function renderConflictMsg(message) { | |||
useView(conflictMsgView); | |||
@@ -34,7 +34,17 @@ const errorHandler = (error: ResponseError) => { | |||
if (response && response.status) { | |||
const errorText = codeMessage[response.status] || response.statusText; | |||
const { status, url } = response; | |||
if (response.status === 401) { | |||
notification.error({ | |||
message: '登录超时', | |||
description: '登录状态超时,请重新登录', | |||
}); | |||
logout(false); | |||
return { | |||
code: response.status, | |||
message: errorText, | |||
}; | |||
} | |||
notification.error({ | |||
message: `请求错误 ${status}: ${url}`, | |||
description: errorText, | |||
@@ -45,7 +55,7 @@ const errorHandler = (error: ResponseError) => { | |||
}; | |||
} | |||
if (data && typeof data === 'object' && 'error' in data) return data; | |||
// todo 报错都不走error, 改为构建成data; | |||
// 报错都不走error, 改为构建成data; | |||
if (!response) { | |||
notification.error({ | |||
description: '您的网络发生异常,无法连接服务器', | |||
@@ -6,8 +6,8 @@ import FileIcon from '../FileIcon'; | |||
import ATooltip from '../Tooltip'; | |||
import styles from './FileStatus.less'; | |||
import { CloseCircleFilled, CheckCircleFilled } from '@ant-design/icons'; | |||
import { Progress, Button } from 'antd'; | |||
import { TaskStatus, TaskType } from '@/services/API.helper'; | |||
import { Progress, Button, ButtonProps } from 'antd'; | |||
import { NotificationType, TaskStatus, TaskType } from '@/services/API.helper'; | |||
import { DATA } from '@/services/API'; | |||
import { identity } from 'lodash'; | |||
import { useCallback } from 'react'; | |||
@@ -39,6 +39,10 @@ export default function FileStatus(props: FileStatusProps) { | |||
fetchLocalApi('restartTask', { taskIds: data.taskId }); | |||
}, [data.taskId]); | |||
const downloadFile = useCallback(() => { | |||
fetchLocalApi('downloadFileFromMsg', { fileObject: data.originData }); | |||
}, [data.taskId]); | |||
const checkFile = useCallback(() => { | |||
system.openFilePosition(data); | |||
}, [data]); | |||
@@ -60,43 +64,35 @@ export default function FileStatus(props: FileStatusProps) { | |||
</div> | |||
</div> | |||
<div className={styles.mid}> | |||
<LoadDesc | |||
type={data.taskType} | |||
loadingState={data.taskSyncStatus} | |||
progress={data.taskSyncProgress} | |||
{...restProps} | |||
/> | |||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | |||
{data.notifyType ? ( | |||
<span>{data.notifyMessage}</span> | |||
) : ( | |||
<LoadDesc | |||
type={data.taskType} | |||
loadingState={data.taskSyncStatus} | |||
progress={data.taskSyncProgress} | |||
{...restProps} | |||
/> | |||
)} | |||
{data.taskSyncStatus === TaskStatus.FINISH || data.notifyType ? ( | |||
<Time time={data.taskCreateDate} /> | |||
) : null} | |||
</div> | |||
<div className={styles.right}> | |||
{/* 查看1: 已下载 文件打开文件夹 */} | |||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | |||
<Button | |||
type="link" | |||
className={styles.button} | |||
size="small" | |||
onClick={checkFile} | |||
> | |||
查看 | |||
</Button> | |||
<ActionButton onClick={checkFile}>查看</ActionButton> | |||
) : null} | |||
{/* 查看2: 未下载 且 已删除 文件跳转到web端 */} | |||
{/* 下载: 未下载 且 未删除 文件 */} | |||
{/* 重新下载: 下载失败时出现 */} | |||
{/* 重新上传: 上传失败时出现 */} | |||
{data.taskSyncStatus === TaskStatus.FAILED ? ( | |||
<Button | |||
type="link" | |||
className={styles.button} | |||
size="small" | |||
onClick={redoTask} | |||
> | |||
<ActionButton onClick={redoTask}> | |||
{data.taskType === TaskType.DOWNLOAD ? '重新下载' : '重新上传'} | |||
</Button> | |||
</ActionButton> | |||
) : null} | |||
{data.notifyType && data.notifyType === NotificationType.SYNC ? ( | |||
<ActionButton onClick={downloadFile}>下载</ActionButton> | |||
) : null} | |||
{/* <Button type="link" className={styles.button} size="small">重新上传</Button> */} | |||
{/* <Button type="link" className={styles.button} size="small">暂停</Button> */} | |||
{/* <Button type="link" className={styles.button} size="small">取消</Button> */} | |||
</div> | |||
@@ -104,9 +100,32 @@ export default function FileStatus(props: FileStatusProps) { | |||
); | |||
} | |||
const Time = memo((props: { time: Dayjs }) => ( | |||
<span className={styles.time}>{props.time.format('HH:mm:ss')}</span> | |||
)); | |||
function ActionButton({ onClick, children }: ButtonProps) { | |||
return ( | |||
<Button | |||
type="link" | |||
className={styles.button} | |||
size="small" | |||
onClick={onClick} | |||
> | |||
{children} | |||
</Button> | |||
); | |||
} | |||
interface TimeProps { | |||
time: Dayjs; | |||
} | |||
const Time = memo(({ time }: TimeProps) => { | |||
const now = dayjs(); | |||
const isToday = time.isSame(now, 'day'); | |||
const isSameYear = time.isSame(now, 'year'); | |||
const formatStr = useMemo(() => { | |||
if (isToday) return 'HH:mm:ss'; | |||
if (isSameYear) return 'MM/DD HH:mm:ss'; | |||
return 'YYYY/MM/DD HH:mm:ss'; | |||
}, [isToday, isSameYear]); | |||
return <span className={styles.time}>{time.format(formatStr)}</span>; | |||
}); | |||
interface LoadDescProps { | |||
type: TaskType; | |||
@@ -1,9 +1,14 @@ | |||
import React from 'react' | |||
import FileStatus from '@/components/FileStatus'; | |||
import { useNotificationMessages } from '@/utils/hooks'; | |||
import styles from './messages.less'; | |||
export default function MessagesView() { | |||
const list = useNotificationMessages(); | |||
return ( | |||
<div> | |||
消息通知界面 | |||
<div className={styles.messages}> | |||
{list.map((data) => ( | |||
<FileStatus key={data.taskId} className={styles.item} data={data} /> | |||
))} | |||
</div> | |||
) | |||
} | |||
); | |||
} |
@@ -0,0 +1,8 @@ | |||
.messages { | |||
height: 100%; | |||
padding: 0 12px; | |||
.item { | |||
margin-top: 12px; | |||
margin-bottom: 8px; | |||
} | |||
} |
@@ -6,11 +6,11 @@ import syncSuccessImg from './assets/sync.success.png'; | |||
import syncErrorImg from './assets/sync.error.png'; | |||
import classNames from 'classnames'; | |||
import SyncModal from '@/components/SyncModal'; | |||
import { useSocketMessages } from '@/services/socket'; | |||
import FileStatus from '@/components/FileStatus'; | |||
import { useMemo } from 'react'; | |||
import { TaskStatus } from '@/services/API.helper'; | |||
import { DATA } from '@/services/API'; | |||
import { useSocketMessages } from '@/utils/hooks'; | |||
enum SyncType { | |||
Syncing = 'syncing', | |||
@@ -89,9 +89,9 @@ export default function SyncView() { | |||
))} | |||
</div> | |||
<div className={styles.right}> | |||
<div className={styles.date}> | |||
{/* <div className={styles.date}> // 由于现在消息列表没有清除功能,优先考虑使用虚拟滚动以保证页面性能 | |||
<div>2021年6月14日</div> | |||
</div> | |||
</div> */} | |||
{curretList.map((data) => ( | |||
<FileStatus key={data.taskId} className={styles.item} data={data} /> | |||
))} | |||
@@ -54,7 +54,7 @@ | |||
flex: 1; | |||
overflow: auto; | |||
padding: 0 12px; | |||
.item ~ .item { | |||
.item { | |||
margin-top: 12px; | |||
margin-bottom: 8px; | |||
} | |||
@@ -1,5 +1,5 @@ | |||
import { Dayjs } from 'dayjs'; | |||
import { TaskStatus, TaskType } from './API.helper'; | |||
import { NotificationType, TaskStatus, TaskType } from './API.helper'; | |||
declare namespace API { | |||
export interface ResponseData<T> { | |||
@@ -81,5 +81,15 @@ declare namespace DATA { | |||
absolutePath: string; // 绝对路径 | |||
version: number; // 1, | |||
workStatus: number; // 1 | |||
/* 消息通知型数据补完 */ | |||
originData: any; // 用于消息通知 | |||
/** | |||
* 通知类型 | |||
*/ | |||
notifyType: NotificationType; | |||
/** | |||
* 通知消息内容 | |||
*/ | |||
notifyMessage: string; | |||
} | |||
} |
@@ -26,3 +26,8 @@ export enum TaskType { | |||
// 上传 | |||
UPLOAD = 'UPLOAD', | |||
} | |||
export enum NotificationType { | |||
SYNC = 1, | |||
UPLOAD_CONFLICT = 2, | |||
DOWNLOAD_CONFLICT = 3, | |||
} |
@@ -1,56 +1,9 @@ | |||
import { firstCharToLowerCase, Subject } from '@/utils/tool'; | |||
import dayjs from 'dayjs'; | |||
import { useEffect, useState } from 'react'; | |||
import { DATA } from './API'; | |||
import { isClient } from './system'; | |||
import { fileMng, msgNotifySubject } from '@/utils/hooks'; | |||
import { firstCharToLowerCase } from '@/utils/tool'; | |||
const gatewayPort = window.systemConfig?.gatewayPort || 7888; | |||
const skUrl = `ws://127.0.0.1:${gatewayPort}/websocket/subscriptionTaskSync`; | |||
class FileMessageManager extends Subject { | |||
public list: DATA.SocketFileMsg[] = []; | |||
// 主键 taskId | |||
public fileMap = new Map(); | |||
addFileMsg(message: DATA.SocketFileMsg) { | |||
this.fileMap.set(message.taskId, message); | |||
this.list = [message].concat(this.list); | |||
this.notifyObservers(this.list); | |||
} | |||
updateFileMsg(message: DATA.SocketFileMsg) { | |||
this.fileMap.set(message.taskId, message); | |||
this.list = this.list.map((msg) => | |||
msg.taskId === message.taskId ? message : msg, | |||
); | |||
this.notifyObservers(this.list); | |||
} | |||
receiveMessage(messageStr: string) { | |||
try { | |||
const message = firstCharToLowerCase( | |||
JSON.parse(messageStr), | |||
) as DATA.SocketFileMsg; | |||
// 数据处理 | |||
message.taskCreateDate = dayjs((message.taskCreateUnixTime || 0) * 1000); | |||
// 给消息窗口使用 | |||
message.taskCreateDateStr = | |||
message.taskCreateDate.format('YY/MM/DD HH:mm:ss'); | |||
const existMsg = this.fileMap.get(message.taskId); | |||
if (existMsg) { | |||
this.updateFileMsg(message); | |||
} else { | |||
this.addFileMsg(message); | |||
} | |||
// todo: 临时测试 | |||
if (isClient) { | |||
window.ipcRenderer.invoke('notify', message); | |||
} | |||
} catch (e) { | |||
console.error('message 解析失败:', e, messageStr); | |||
} | |||
} | |||
} | |||
const fileMng = new FileMessageManager(); | |||
const taskSyncSkUrl = `ws://127.0.0.1:${gatewayPort}/websocket/subscriptionTaskSync`; | |||
const msgNotifySkUrl = `ws://127.0.0.1:${gatewayPort}/websocket/subscriptionMessageNotify`; | |||
enum SocketState { | |||
CONNECTING = 0, // 连接尚未建立 | |||
@@ -60,8 +13,9 @@ enum SocketState { | |||
} | |||
let taskSyncSocket: WebSocket; | |||
let msgNotifySocket: WebSocket; | |||
function initTaskSyncSocket() { | |||
taskSyncSocket = new WebSocket(skUrl); | |||
taskSyncSocket = new WebSocket(taskSyncSkUrl); | |||
taskSyncSocket.onopen = () => { | |||
console.log('taskSyncSocket connection'); | |||
}; | |||
@@ -76,6 +30,28 @@ function initTaskSyncSocket() { | |||
setTimeout(() => initTaskSyncSocket(), 5000); | |||
}; | |||
} | |||
function initMessageNotifySocket() { | |||
msgNotifySocket = new WebSocket(msgNotifySkUrl); | |||
msgNotifySocket.onopen = () => { | |||
console.log('taskSyncSocket connection'); | |||
}; | |||
msgNotifySocket.onmessage = ({ data }) => { | |||
// fileMng.receiveMessage(data); | |||
try { | |||
const originNotifyMessage = firstCharToLowerCase(JSON.parse(data)) as any; | |||
msgNotifySubject.notifyObservers(originNotifyMessage); | |||
} catch (e) { | |||
console.error('socket notify message 解析失败:', e, data); | |||
} | |||
}; | |||
msgNotifySocket.onerror = (...args) => { | |||
console.log('taskSyncSocket error:', args); | |||
}; | |||
msgNotifySocket.onclose = (...args) => { | |||
console.log('taskSyncSocket close:', args); | |||
setTimeout(() => initTaskSyncSocket(), 5000); | |||
}; | |||
} | |||
export function sendSocketMessage(message: string) { | |||
if (taskSyncSocket && taskSyncSocket.readyState === SocketState.OPEN) { | |||
taskSyncSocket.send(message); | |||
@@ -83,17 +59,7 @@ export function sendSocketMessage(message: string) { | |||
} | |||
export function initialWebsocket() { | |||
initTaskSyncSocket(); | |||
initMessageNotifySocket(); | |||
} | |||
export function useSocketMessages() { | |||
const [list, setList] = useState(fileMng.list); | |||
// console.log(list); | |||
useEffect(() => { | |||
fileMng.addObserver(setList); | |||
return () => { | |||
fileMng.removeObserver(setList); | |||
}; | |||
}, []); | |||
return list; | |||
} | |||
// export function use |
@@ -66,8 +66,10 @@ export async function login(account: string, password: string) { | |||
return res; | |||
} | |||
export function logout() { | |||
storage.clear(); | |||
export function logout(clearStorage = true) { | |||
if (clearStorage) { | |||
storage.clear(); | |||
} | |||
system.logout(); | |||
// window.location.href = '/login'; | |||
history.replace('/login'); | |||
@@ -0,0 +1,160 @@ | |||
import { useCallback, useEffect, useState } from 'react'; | |||
import { | |||
firstCharToLowerCase, | |||
firstCharToUpperCase, | |||
Subject, | |||
} from '@/utils/tool'; | |||
import { API, DATA } from '@/services/API'; | |||
import dayjs from 'dayjs'; | |||
import { isClient } from '@/services/system'; | |||
import { throttle } from 'lodash'; | |||
import { fetchApi } from './request'; | |||
import storage from './storage'; | |||
// // let workspacePath = ''; | |||
// // 暂时不用 待定 | |||
// export function useWorkspacePath() { | |||
// const ctx = useState(''); | |||
// // const setPath = useCallback((value: string) => { | |||
// // setThePath(value); | |||
// // workspacePath = value; | |||
// // }, [setThePath]); | |||
// useEffect(() => { | |||
// // 获取当前使用的路径; 这里的接口需要做一层防抖 | |||
// }, []); | |||
// return ctx; | |||
// } | |||
class FileMessageManager extends Subject { | |||
public list: DATA.SocketFileMsg[] = []; | |||
// 主键 taskId | |||
public fileMap = new Map(); | |||
addFileMsg(message: DATA.SocketFileMsg) { | |||
this.fileMap.set(message.taskId, message); | |||
this.list = [message].concat(this.list); | |||
this.notifyObservers(this.list); | |||
} | |||
updateFileMsg(message: DATA.SocketFileMsg) { | |||
this.fileMap.set(message.taskId, message); | |||
this.list = this.list.map((msg) => | |||
msg.taskId === message.taskId ? message : msg, | |||
); | |||
this.notifyObservers(this.list); | |||
} | |||
receiveMessage(messageStr: string) { | |||
try { | |||
const message = firstCharToLowerCase( | |||
JSON.parse(messageStr), | |||
) as DATA.SocketFileMsg; | |||
// 数据处理 | |||
message.taskCreateDate = dayjs((message.taskCreateUnixTime || 0) * 1000); | |||
// 给消息窗口使用 | |||
message.taskCreateDateStr = | |||
message.taskCreateDate.format('YY/MM/DD HH:mm:ss'); | |||
const existMsg = this.fileMap.get(message.taskId); | |||
if (existMsg) { | |||
this.updateFileMsg(message); | |||
} else { | |||
this.addFileMsg(message); | |||
} | |||
} catch (e) { | |||
console.error('message 解析失败:', e, messageStr); | |||
} | |||
} | |||
} | |||
export const fileMng = new FileMessageManager(); | |||
export const msgNotifySubject = new Subject(); | |||
msgNotifySubject.addObserver((upperNotifyMessage) => { | |||
if (!isClient) { | |||
return; | |||
} | |||
const message = transformNotifyMessage(upperNotifyMessage); | |||
if (!message) { | |||
return; | |||
} | |||
window.ipcRenderer.invoke('notify', message); | |||
}); | |||
function transformNotifyMessage(notifyMessage: any) { | |||
try { | |||
if (!notifyMessage.parameter) return null; | |||
const fileMsg = firstCharToLowerCase( | |||
JSON.parse(notifyMessage.parameter), | |||
) as DATA.SocketFileMsg; | |||
fileMsg.originData = notifyMessage.parameter; | |||
fileMsg.notifyType = notifyMessage.type; | |||
fileMsg.notifyMessage = notifyMessage.body; | |||
// 数据处理 | |||
fileMsg.taskCreateDate = dayjs((notifyMessage.unixTime || 0) * 1000); | |||
// 给消息窗口使用 | |||
fileMsg.taskCreateDateStr = | |||
fileMsg.taskCreateDate.format('YY/MM/DD HH:mm:ss'); | |||
return fileMsg; | |||
} catch (e) { | |||
console.error('transformNotifyMessage 解析失败:', e, notifyMessage); | |||
return null; | |||
} | |||
} | |||
export function useSocketMessages() { | |||
const [list, setList] = useState(fileMng.list); | |||
useEffect(() => { | |||
fileMng.addObserver(setList); | |||
return () => { | |||
fileMng.removeObserver(setList); | |||
}; | |||
}, []); | |||
return list; | |||
} | |||
export function useNotificationMessages() { | |||
const [list, setList] = useState<Array<DATA.SocketFileMsg>>([]); | |||
const fetchMessages = useCallback( | |||
throttle(async () => { | |||
const accountId = storage.get('accountId'); | |||
const [headList, hisList] = await Promise.all< | |||
API.ResponseData<any[]>, | |||
API.ResponseData<any[]> | |||
>([ | |||
fetchApi('lockingmsg/queryLockingMsgListByFilter', { | |||
userId: accountId, | |||
status: 1, | |||
}), | |||
fetchApi('lockingmsg/queryLockingMsgListByFilter', { | |||
userId: accountId, | |||
status: 2, | |||
}), | |||
]); | |||
const finalList = headList.data | |||
?.concat(hisList.data || []) | |||
.reduce((arr, notifyMessage) => { | |||
const message = transformNotifyMessage(notifyMessage); | |||
if (!message) return arr; | |||
arr.push(message); | |||
return arr; | |||
}, []) as DATA.SocketFileMsg[]; | |||
setList(finalList); | |||
}, 500), | |||
[], | |||
); | |||
console.log(list); | |||
useEffect(() => { | |||
msgNotifySubject.addObserver(fetchMessages); | |||
return () => { | |||
msgNotifySubject.removeObserver(fetchMessages); | |||
}; | |||
}, []); | |||
useEffect(() => { | |||
fetchMessages(); | |||
}, []); | |||
return list; | |||
} |