Ver a proveniência

'update'

main
郑州 há 3 anos
ascendente
cometimento
5603eaeec5
15 ficheiros alterados com 398 adições e 70 eliminações
  1. +1
    -0
      electron/config.js
  2. +31
    -6
      electron/main.js
  3. +25
    -8
      electron/socket.js
  4. +1
    -0
      src/app.ts
  5. +71
    -36
      src/components/FileStatus/FileStatus.tsx
  6. +24
    -5
      src/components/SyncModal/SyncModal.tsx
  7. +41
    -2
      src/pages/sync/index.tsx
  8. +35
    -5
      src/pages/sync/sync.less
  9. +40
    -1
      src/services/API.d.ts
  10. +28
    -0
      src/services/API.helper.ts
  11. +56
    -2
      src/services/socket.ts
  12. +20
    -4
      src/services/system.ts
  13. +2
    -0
      src/services/user.ts
  14. +6
    -1
      src/utils/request.ts
  15. +17
    -0
      src/utils/tool.ts

+ 1
- 0
electron/config.js Ver ficheiro

@@ -1,4 +1,5 @@
module.exports = {
remoteUrl: 'http://139.198.180.242:9003/',
// remoteUrl: 'https://www.locking.cn/',
gatewayPort: 7888,
};

+ 31
- 6
electron/main.js Ver ficheiro

@@ -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);
},
);

+ 25
- 8
electron/socket.js Ver ficheiro

@@ -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);
}
});
};

+ 1
- 0
src/app.ts Ver ficheiro

@@ -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: '服务器成功返回请求的数据。',


+ 71
- 36
src/components/FileStatus/FileStatus.tsx Ver ficheiro

@@ -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 (
<div className={classNames(styles.fileStatus, className)} style={style}>
<div className={styles.left}>
<FileIcon className={styles.icon} extension="folder" />
<FileIcon className={styles.icon} extension={data.extension} />
<div className={styles.content}>
<ATooltip placement="top" title="123">
<div className={styles.fileName}>文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称</div>
<ATooltip placement="top" title={data.archName}>
<div className={styles.fileName}>{data.archName}</div>
</ATooltip>
<div className={styles.subContent}>
<ATooltip placement="bottom" title="456">
<div className={styles.filePath}>文件路径文件路径文件路径文件路径文件路径文件路径</div>
<ATooltip placement="bottom" title={filePath}>
<div className={styles.filePath}>{filePath}</div>
</ATooltip>
<div className={styles.modifyInfo}>XX创建/XX同步</div>
<div className={styles.modifyInfo}>{relationModifier}</div>
</div>
</div>
</div>
<div className={styles.mid}>
<LoadDesc {...restProps} />
{
restProps.loadingState === 'complete'
? <Time time="2020-01-01 18:24:56" />
: null
}
<LoadDesc
type={data.taskType}
loadingState={data.taskSyncStatus}
progress={data.taskSyncProgress}
{...restProps}
/>
{data.taskSyncStatus === TaskStatus.FINISH ? (
<Time time={'2020-01-01 18:24:56'} />
) : null}
</div>
<div className={styles.right}>
{/* 查看1: 已下载 文件打开文件夹 */}
@@ -47,11 +67,11 @@ export default function FileStatus(props: FileStatusProps & LoadDescProps) {
{/* 重新下载: 下载失败时出现 */}
{/* 重新上传: 上传失败时出现 */}
{/* <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>
{/* <Button type="link" className={styles.button} size="small">暂停</Button> */}
{/* <Button type="link" className={styles.button} size="small">取消</Button> */}
</div>
</div>
)
);
}

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 ['成功', <CheckCircleFilled className={classNames(styles.stateIcon, styles.success)} />]; }
if (result === 'fail') { return ['失败', <CloseCircleFilled className={classNames(styles.stateIcon, styles.error)} />]; }
if (loadingState === TaskStatus.FINISH) {
return [
'成功!',
<CheckCircleFilled
className={classNames(styles.stateIcon, styles.success)}
/>,
];
}
if (loadingState === TaskStatus.FAILED) {
return [
'失败!',
<CloseCircleFilled
className={classNames(styles.stateIcon, styles.error)}
/>,
];
}
if (loadingState === TaskStatus.WAITING) {
return [`等待${keywords}`];
}
return ['', null];
}, [result]);
}, [result, keywords]);
return (
<span className={styles.loadDesc}>
{
loadingState === 'loading'
? <Progress percent={progress} format={() => `${keywords}中...`} />
: (
<>
{icon}
{keywords}
{resultText}!
</>
)
}
{loadingState === TaskStatus.SYNCING ? (
<Progress percent={progress} format={() => `${keywords}中...`} />
) : (
<>
{icon}
{keywords}
{resultText}
</>
)}
</span>
)
}
);
}

+ 24
- 5
src/components/SyncModal/SyncModal.tsx Ver ficheiro

@@ -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<string[]>([]);
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 (
<Modal
title="同步文件夹至工作空间"
@@ -59,6 +72,11 @@ export default function SyncModal() {
okText="确定"
width={540}
onCancel={() => setModalVisible(false)}
okButtonProps={{
disabled: selectedKeys.length === 0,
loading: btnLoading,
}}
onOk={onOk}
>
<div className={styles.title}>
选择「与我有关的文件夹」,并同步到「我的电脑/工作空间」中
@@ -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}


+ 41
- 2
src/pages/sync/index.tsx Ver ficheiro

@@ -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>(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 (
<div className={styles.sync}>
<div className={styles.left}>
@@ -43,9 +77,14 @@ export default function SyncView() {
))}
</div>
<div className={styles.right}>
同步任务界面
<SyncModal />
<div className={styles.date}>
<div>2021年6月14日</div>
</div>
{curretList.map((data) => (
<FileStatus key={data.taskId} className={styles.item} data={data} />
))}
</div>
<SyncModal />
</div>
);
}

+ 35
- 5
src/pages/sync/sync.less Ver ficheiro

@@ -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;
}
}
}
}
}

+ 40
- 1
src/services/API.d.ts Ver ficheiro

@@ -1,3 +1,5 @@
import { TaskStatus, TaskType } from './API.helper';

declare namespace API {
export interface ResponseData<T> {
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
}
}

+ 28
- 0
src/services/API.helper.ts Ver ficheiro

@@ -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',
}

+ 56
- 2
src/services/socket.ts Ver ficheiro

@@ -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;
}

+ 20
- 4
src/services/system.ts Ver ficheiro

@@ -33,10 +33,26 @@ const system = {
ipcRenderer.invoke('storage:remove', { key });
}),
},

// 发往网关的请求
login: safeCall((userId: string, userPhone: string) => {
return fetchLocalApi<null>('login', { userId, userPhone });
/* 发往网关的请求 */
// 登陆
login: safeCall(async (userId: string, userPhone: string) => {
const res = await fetchLocalApi<null>(
'login',
{ userId, userPhone },
{ silent: true },
);
ipcRenderer.invoke('socket:send-message', `login:${userPhone}`);
return res;
}),
// 退出登陆
logout: safeCall(async () => {
return await fetchLocalApi<null>('logout');
}),
// 发送下载同步文件夹的指令
syncProjects: safeCall((projectIds: string[]) => {
return fetchLocalApi<null>('syncFolderToWorkSpace', {
ids: projectIds.join(','),
});
}),
};



+ 2
- 0
src/services/user.ts Ver ficheiro

@@ -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();


+ 6
- 1
src/utils/request.ts Ver ficheiro

@@ -26,10 +26,15 @@ export async function fetchApi<T = any>(
return res;
}

interface IOption {
silent?: boolean;
method?: 'GET' | 'POST';
}

export async function fetchLocalApi<T = any>(
path: string,
params = {},
options = { silent: false, method: 'GET' },
options: IOption = { silent: false, method: 'GET' },
) {
if (!gatewayPort) {
return errorReponse('gateway infomation failed');


+ 17
- 0
src/utils/tool.ts Ver ficheiro

@@ -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<Observer> = [];
// 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));
}
}

Carregando…
Cancelar
Guardar