From 5603eaeec5aae4a41f929faebdfdbf3b3751dc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=B7=9E?= Date: Mon, 12 Jul 2021 18:51:14 +0800 Subject: [PATCH] 'update' --- electron/config.js | 1 + electron/main.js | 37 ++++++-- electron/socket.js | 33 +++++-- src/app.ts | 1 + src/components/FileStatus/FileStatus.tsx | 107 +++++++++++++++-------- src/components/SyncModal/SyncModal.tsx | 29 ++++-- src/pages/sync/index.tsx | 43 ++++++++- src/pages/sync/sync.less | 40 +++++++-- src/services/API.d.ts | 41 ++++++++- src/services/API.helper.ts | 28 ++++++ src/services/socket.ts | 58 +++++++++++- src/services/system.ts | 24 ++++- src/services/user.ts | 2 + src/utils/request.ts | 7 +- src/utils/tool.ts | 17 ++++ 15 files changed, 398 insertions(+), 70 deletions(-) create mode 100644 src/services/API.helper.ts diff --git a/electron/config.js b/electron/config.js index fe4b1c0..329a3cf 100644 --- a/electron/config.js +++ b/electron/config.js @@ -1,4 +1,5 @@ module.exports = { remoteUrl: 'http://139.198.180.242:9003/', + // remoteUrl: 'https://www.locking.cn/', gatewayPort: 7888, }; diff --git a/electron/main.js b/electron/main.js index 94b9210..176d11b 100644 --- a/electron/main.js +++ b/electron/main.js @@ -7,7 +7,11 @@ const { Subject } = require('./tool'); let mainWindow; -const socketMsgSubject = new Subject(); +const socketSubjects = { + onMessage: new Subject(), + onError: new Subject(), + onClose: new Subject(), +}; function createWindow() { //创建窗口 @@ -52,14 +56,26 @@ function createWindow() { mainWindow.webContents.send('socket:on-message', message); }; + const onSocketError = (...args) => { + mainWindow.webContents.send('socket:on-error', ...args); + }; + + const onSocketClose = (...args) => { + mainWindow.webContents.send('socket:on-close', ...args); + }; + mainWindow.webContents.on('did-finish-load', () => { // 加载初始localStorage数据 mainWindow.webContents.send('initialStorageData', storage.getAllItem()); - socketMsgSubject.add(onMessageReceive); + socketSubjects.onMessage.add(onMessageReceive); + socketSubjects.onError.add(onSocketError); + socketSubjects.onClose.add(onSocketClose); }); mainWindow.on('closed', () => { mainWindow = null; - socketMsgSubject.remove(onMessageReceive); + socketSubjects.onMessage.remove(onMessageReceive); + socketSubjects.onError.remove(onSocketError); + socketSubjects.onClose.remove(onSocketClose); }); } @@ -117,6 +133,15 @@ app.on('window-all-closed', () => { } }); -initialWebsocketEvents(function onMessage(message) { - socketMsgSubject.notify(message); -}); +initialWebsocketEvents( + ipcMain, + function onMessage(message) { + socketSubjects.onMessage.notify(message); + }, + function onError(...args) { + socketSubjects.onError.notify(...args); + }, + function onClose(...args) { + socketSubjects.onClose.notify(...args); + }, +); diff --git a/electron/socket.js b/electron/socket.js index 8cc0c2e..9e2d05c 100644 --- a/electron/socket.js +++ b/electron/socket.js @@ -1,26 +1,43 @@ const io = require('ws'); const config = require('./config'); -function initialWebsocket(onMessage, onError) { - const socket = new io( - `ws://127.0.0.1:${config.gatewayPort}/websocket/subscriptionTaskSync`, - ); - +const skUrl = `ws://127.0.0.1:${config.gatewayPort}/websocket/subscriptionTaskSync`; +let socket; +function initialWebsocket(onMessage, onError, onClose) { + socket = new io(skUrl); socket.on('open', () => { // socket.emit("hello", "world"); console.log('socket connection'); - socket.on('message', onMessage); + // socket.send('1'); + }); + socket.on('message', (message) => { + console.log('socket message', message); + onMessage(message); }); socket.on('error', (...args) => { console.log('socket error:', args); onError && onError(...args); }); + socket.on('close', (...args) => { + console.log('socket close:', args); + onClose && onClose(...args); + setTimeout(() => initialWebsocket(onMessage, onError, onClose), 5000); + }); } module.exports.initialWebsocketEvents = function initialWebsocketEvents( + ipcMain, onMessage, onError, + onClose, ) { - initialWebsocket(onMessage, onError); - // ipcMain.handle('socket:on', ) + initialWebsocket(onMessage, onError, onClose); + ipcMain.handle('socket:send-message', (event, message) => { + if (socket && socket.readyState === io.OPEN) { + console.log('socket send message to gateway:', message); + socket.send(message); + } else { + console.log('socket status not ready:', socket.readyState); + } + }); }; diff --git a/src/app.ts b/src/app.ts index 975e5a1..40afb08 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import { firstCharToLowerCase, handleRequest } from './utils/tool'; import { isObject } from 'lodash'; import { logout, queryCurrent } from './services/user'; import storage from './utils/storage'; +import './services/socket'; const codeMessage = { 200: '服务器成功返回请求的数据。', diff --git a/src/components/FileStatus/FileStatus.tsx b/src/components/FileStatus/FileStatus.tsx index 952bbda..5d21855 100644 --- a/src/components/FileStatus/FileStatus.tsx +++ b/src/components/FileStatus/FileStatus.tsx @@ -7,38 +7,58 @@ 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 { DATA } from '@/services/API'; +import { identity } from 'lodash'; interface FileStatusProps { className?: string; style?: CSSProperties; + data: DATA.SocketFileMsg; } +export default function FileStatus(props: FileStatusProps) { + const { className, style, data, ...restProps } = props; + console.log(data); -export default function FileStatus(props: FileStatusProps & LoadDescProps) { - const { className, style, ...restProps } = props; + const filePath = useMemo(() => { + return [data.projName, data.nodeName, data.relativePath] + .filter(identity) + .join(' / '); + }, [data]); + const relationModifier = useMemo(() => { + return data.modifyName + ? `${data.modifyName}同步` + : data.createUserName + ? `${data.createUserName}同步` + : ''; + }, [data]); return (
- +
- -
文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称
+ +
{data.archName}
- -
文件路径文件路径文件路径文件路径文件路径文件路径
+ +
{filePath}
-
XX创建/XX同步
+
{relationModifier}
- - { - restProps.loadingState === 'complete' - ?
{/* 查看1: 已下载 文件打开文件夹 */} @@ -47,11 +67,11 @@ export default function FileStatus(props: FileStatusProps & LoadDescProps) { {/* 重新下载: 下载失败时出现 */} {/* 重新上传: 上传失败时出现 */} {/* */} - - + {/* */} + {/* */}
- ) + ); } const Time = memo((props: { time: string }) => ( @@ -59,33 +79,48 @@ const Time = memo((props: { time: string }) => ( )); interface LoadDescProps { - type: 'upload' | 'download'; - loadingState: 'loading' | 'complete'; + type: TaskType; + loadingState: TaskStatus; result?: 'success' | 'fail'; progress?: number; } function LoadDesc(props: LoadDescProps) { const { result, type, loadingState, progress } = props; - const keywords = type === 'upload' ? '上传' : '下载'; + const keywords = type === 'UPLOAD' ? '上传' : '下载'; const [resultText, icon] = useMemo(() => { - if (result === 'success') { return ['成功', ]; } - if (result === 'fail') { return ['失败', ]; } + if (loadingState === TaskStatus.FINISH) { + return [ + '成功!', + , + ]; + } + if (loadingState === TaskStatus.FAILED) { + return [ + '失败!', + , + ]; + } + if (loadingState === TaskStatus.WAITING) { + return [`等待${keywords}`]; + } return ['', null]; - }, [result]); + }, [result, keywords]); return ( - { - loadingState === 'loading' - ? `${keywords}中...`} /> - : ( - <> - {icon} - {keywords} - {resultText}! - - ) - } + {loadingState === TaskStatus.SYNCING ? ( + `${keywords}中...`} /> + ) : ( + <> + {icon} + {keywords} + {resultText} + + )} - ) -} \ No newline at end of file + ); +} diff --git a/src/components/SyncModal/SyncModal.tsx b/src/components/SyncModal/SyncModal.tsx index 18185de..706ef8e 100644 --- a/src/components/SyncModal/SyncModal.tsx +++ b/src/components/SyncModal/SyncModal.tsx @@ -5,7 +5,9 @@ import { useState } from 'react'; import styles from './SyncModal.less'; import ATooltip from '../Tooltip'; import { fetchApi } from '@/utils/request'; -import { firstCharToLowerCase } from '@/utils/tool'; +import { firstCharToLowerCase, handleRequest } from '@/utils/tool'; +import { useCallback } from 'react'; +import system from '@/services/system'; const columns = [ { @@ -43,13 +45,24 @@ interface DataType { export default function SyncModal() { const { initialState: { currentUser } = {} } = useModel('@@initialState'); + const [btnLoading, setBtnLoading] = useState(false); const [modalVisible, setModalVisible] = useState(true); + const [selectedKeys, setSelectedKeys] = useState([]); const { loading, data } = useRequest(async () => { return await fetchApi('project/queryProjectListByUserId', { userId: currentUser!.id, }); }); + const onOk = useCallback(async () => { + setBtnLoading(true); + const res = await system.syncProjects(selectedKeys); + setBtnLoading(false); + handleRequest(res!).success(() => { + setModalVisible(false); + }); + }, [selectedKeys]); + return ( setModalVisible(false)} + okButtonProps={{ + disabled: selectedKeys.length === 0, + loading: btnLoading, + }} + onOk={onOk} >
选择「与我有关的文件夹」,并同步到「我的电脑/工作空间」中 @@ -78,11 +96,12 @@ export default function SyncModal() { 'selectedRows: ', selectedRows, ); + setSelectedKeys(selectedRowKeys as string[]); }, - getCheckboxProps: (record: DataType) => ({ - disabled: record.name === 'Disabled User', // Column configuration not to be checked - name: record.name, - }), + // getCheckboxProps: (record: DataType) => ({ + // disabled: record.name === 'Disabled User', // Column configuration not to be checked + // name: record.name, + // }), }} pagination={false} columns={columns} diff --git a/src/pages/sync/index.tsx b/src/pages/sync/index.tsx index 6b6b9c5..543becb 100644 --- a/src/pages/sync/index.tsx +++ b/src/pages/sync/index.tsx @@ -6,6 +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'; enum SyncType { Syncing, @@ -20,7 +25,36 @@ const syncTypeList = [ ]; export default function SyncView() { + const originList = useSocketMessages(); const [type, setType] = useState(SyncType.Syncing); + + const [syncingList, succList, failList] = useMemo(() => { + return originList.reduce( + (arr, item) => { + if (item.taskSyncStatus === TaskStatus.FINISH) { + arr[1].push(item); + return arr; + } else if (item.taskSyncStatus === TaskStatus.FAILED) { + arr[2].push(item); + return arr; + } + arr[0].push(item); + return arr; + }, + [[], [], []] as [ + DATA.SocketFileMsg[], + DATA.SocketFileMsg[], + DATA.SocketFileMsg[], + ], + ); + }, [originList]); + + const curretList = useMemo(() => { + if (type === SyncType.SyncSuccess) return succList; + if (type === SyncType.SyncError) return failList; + return syncingList; + }, [type, succList, failList, syncingList]); + return (
@@ -43,9 +77,14 @@ export default function SyncView() { ))}
- 同步任务界面 - +
+
2021年6月14日
+
+ {curretList.map((data) => ( + + ))}
+
); } diff --git a/src/pages/sync/sync.less b/src/pages/sync/sync.less index 1f45335..d78023d 100644 --- a/src/pages/sync/sync.less +++ b/src/pages/sync/sync.less @@ -13,7 +13,10 @@ padding-left: 12px; line-height: 32px; margin-bottom: 0; - span { display: block; transform: scale(0.83); } + span { + display: block; + transform: scale(0.83); + } } .tab { position: relative; @@ -30,13 +33,40 @@ border-right-color: @primary-color; background: rgba(120, 80, 255, 0.04); } - .icon { flex: none; } - .label { flex: 0 0 48px; margin: 0 8px; font-size: 12px; color: rgba(#000, 0.85); } - .count { flex: 1; font-size: 12px; color: #8E909F; transform: scale(0.83); } + .icon { + flex: none; + } + .label { + flex: 0 0 48px; + margin: 0 8px; + font-size: 12px; + color: rgba(#000, 0.85); + } + .count { + flex: 1; + font-size: 12px; + color: #8e909f; + transform: scale(0.83); + } } } .right { flex: 1; overflow: auto; + padding: 0 12px; + .item ~ .item { + margin-top: 12px; + margin-bottom: 8px; + } + .date { + height: 36px; + line-height: 36px; + font-size: 12px; + color: rgba(0, 0, 0, 0.8); + > div { + transform: scale(0.83); + transform-origin: left; + } + } } -} \ No newline at end of file +} diff --git a/src/services/API.d.ts b/src/services/API.d.ts index 340b490..7c8e268 100644 --- a/src/services/API.d.ts +++ b/src/services/API.d.ts @@ -1,3 +1,5 @@ +import { TaskStatus, TaskType } from './API.helper'; + declare namespace API { export interface ResponseData { code: number | string; @@ -8,7 +10,8 @@ declare namespace API { */ success: boolean; /** - * 用于标明业务请求内容是否成功, 等价于code === 0 + * 用于标明业务请求内容是否成功, + * 等价于code === 0 */ requestIsSuccess: boolean; } @@ -38,4 +41,40 @@ declare namespace DATA { */ phone: string; } + + export interface SocketFileMsg { + archName: string; + commonStatus: number; // 1, + createTime: string; // "2021-04-16T19:33:36+08:00", + createUserId: string; // "367294106252087297", + createUserName: string; // "", + deleted: number; // 0, + extension: string; // "pdf", + fileSize: number; // 9385910, + folderId: string; // "389513225458573314", + folderLevelId: string; // "389513225454379009_342050628186824706", + id: string; // "389513618179645440", + ipfsCid: string; // "QmPYXHLuXoTF7A1BxsgTRVByojMXyWjjrrZS5MQeTX1mNq", + isShowRecycle: number; // 0, + milestone: number; // 1, + modifyName: string; // "", + modifyTime: string; // "2021-04-16T19:36:28+08:00", + modifyUserId: string; // "0", + nodeName: string; // "产品分析", + projId: string; // "389513225454379008", + projName: string; // "2021416-1", + relativePath: string; // "", + showUrl: string; // "", + status: number; // 2, + syncFileSize: number; // 9385910, + syncIpfsCid: string; // "QmPYXHLuXoTF7A1BxsgTRVByojMXyWjjrrZS5MQeTX1mNq", + syncVersion: number; // 1, + taskId: string; // 420964845295652864, + taskIsModify: boolean; // false, + taskSyncProgress: 100; + taskSyncStatus: TaskStatus; // "TASK_SYNC_STATUS_FINISH", + taskType: TaskType; // "DOWNLOAD", + version: number; // 1, + workStatus: number; // 1 + } } diff --git a/src/services/API.helper.ts b/src/services/API.helper.ts new file mode 100644 index 0000000..a1fd665 --- /dev/null +++ b/src/services/API.helper.ts @@ -0,0 +1,28 @@ +export enum TaskStatus { + /** + * 等待同步 + */ + WAITING = 'TASK_SYNC_STATUS_WAIT', + /** + * 同步中 + */ + SYNCING = 'TASK_SYNC_STATUS_ING', + /** + * 同步暂停 + */ + PAUSED = 'TASK_SYNC_STATUS_PAUSE', + /** + * 同步成功 + */ + FINISH = 'TASK_SYNC_STATUS_FINISH', + /** + * 同步失败 + */ + FAILED = 'TASK_SYNC_STATUS_FAIL', +} +export enum TaskType { + // 下载 + DOWNLOAD = 'DOWNLOAD', + // 上传 + UPLOAD = 'UPLOAD', +} diff --git a/src/services/socket.ts b/src/services/socket.ts index fe6f22a..2facf9f 100644 --- a/src/services/socket.ts +++ b/src/services/socket.ts @@ -1,11 +1,65 @@ +import { firstCharToLowerCase, Subject } from '@/utils/tool'; +import { useEffect, useState } from 'react'; +import { DATA } from './API'; + export const isClient = !!window.ipcRenderer; // process.env.IS_CLIENT; -const safeCall = (f: (...args: any[]) => any) => (isClient ? f : () => {}); +class FileMessageManager extends Subject { + public list: DATA.SocketFileMsg[] = []; + // 主键 taskId + public fileMap = new Map(); + + addFileMsg(message: DATA.SocketFileMsg) { + 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; + const existMsg = this.fileMap.get(message.taskId); + if (existMsg) { + this.updateFileMsg(message); + } else { + this.addFileMsg(message); + } + } catch (e) { + console.error('message 解析失败:', e, messageStr); + } + } +} -const ipcRenderer = window.ipcRenderer; +const fileMng = new FileMessageManager(); if (isClient) { window.addIpcRendererListener('socket:on-message', (_, message) => { console.log('receive electron socket message:', message); + fileMng.receiveMessage(message); + }); + window.addIpcRendererListener('socket:on-error', (_, ...args) => { + console.log('receive electron socket error:', args); }); + window.addIpcRendererListener('socket:on-close', (_, ...args) => { + console.log('receive electron socket close:', args); + }); +} + +export function useSocketMessages() { + const [list, setList] = useState(fileMng.list); + useEffect(() => { + fileMng.addObserver(setList); + return () => { + fileMng.removeObserver(setList); + }; + }, []); + + return list; } diff --git a/src/services/system.ts b/src/services/system.ts index 727950c..b28712c 100644 --- a/src/services/system.ts +++ b/src/services/system.ts @@ -33,10 +33,26 @@ const system = { ipcRenderer.invoke('storage:remove', { key }); }), }, - - // 发往网关的请求 - login: safeCall((userId: string, userPhone: string) => { - return fetchLocalApi('login', { userId, userPhone }); + /* 发往网关的请求 */ + // 登陆 + login: safeCall(async (userId: string, userPhone: string) => { + const res = await fetchLocalApi( + 'login', + { userId, userPhone }, + { silent: true }, + ); + ipcRenderer.invoke('socket:send-message', `login:${userPhone}`); + return res; + }), + // 退出登陆 + logout: safeCall(async () => { + return await fetchLocalApi('logout'); + }), + // 发送下载同步文件夹的指令 + syncProjects: safeCall((projectIds: string[]) => { + return fetchLocalApi('syncFolderToWorkSpace', { + ids: projectIds.join(','), + }); }), }; diff --git a/src/services/user.ts b/src/services/user.ts index 2e68f73..d216027 100644 --- a/src/services/user.ts +++ b/src/services/user.ts @@ -4,6 +4,7 @@ import storage from '@/utils/storage'; import { errorReponse, firstCharToLowerCase, isReqSuccess } from '@/utils/tool'; import { propertyOf } from 'lodash'; import system, { isClient } from './system'; +import { DATA } from './API'; export async function queryCurrent() { const accountId = storage.get('accountId'); @@ -69,6 +70,7 @@ export async function login(account: string, password: string) { export function logout() { storage.clear(); + system.logout(); // window.location.href = '/login'; history.replace('/login'); // window.location.reload(); diff --git a/src/utils/request.ts b/src/utils/request.ts index 12bf03a..5143dba 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -26,10 +26,15 @@ export async function fetchApi( return res; } +interface IOption { + silent?: boolean; + method?: 'GET' | 'POST'; +} + export async function fetchLocalApi( path: string, params = {}, - options = { silent: false, method: 'GET' }, + options: IOption = { silent: false, method: 'GET' }, ) { if (!gatewayPort) { return errorReponse('gateway infomation failed'); diff --git a/src/utils/tool.ts b/src/utils/tool.ts index 2a5e493..b0728ab 100644 --- a/src/utils/tool.ts +++ b/src/utils/tool.ts @@ -77,3 +77,20 @@ export const isValidPhone = (phone: string) => export const passwordReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\S]{8,16}$/; export const isValidPassword = (maybePassword: string) => new RegExp(passwordReg).test(maybePassword); + +type Observer = (...args: any[]) => any; +export class Subject { + public observers: Array = []; + // constructor() { + // this.observers = []; + // } + addObserver(observer: Observer) { + this.observers.push(observer); + } + removeObserver(observer: Observer) { + this.observers = this.observers.filter((iO) => iO !== observer); + } + notifyObservers(...args: unknown[]) { + this.observers.forEach((f) => f(...args)); + } +}