@@ -31,5 +31,13 @@ export default defineConfig({ | |||||
changeOrigin: true, | changeOrigin: true, | ||||
secure: false, | secure: false, | ||||
}, | }, | ||||
'/gateway': { | |||||
target: 'http://127.0.0.1:7888', | |||||
changeOrigin: true, | |||||
secure: false, | |||||
pathRewrite: { | |||||
'^/gateway': '', | |||||
}, | |||||
}, | |||||
}, | }, | ||||
}); | }); |
@@ -1,18 +1,19 @@ | |||||
const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron'); | |||||
const { | |||||
app, | |||||
BrowserWindow, | |||||
dialog, | |||||
ipcMain, | |||||
shell, | |||||
Tray, | |||||
Menu, | |||||
screen, | |||||
} = require('electron'); | |||||
const path = require('path'); | const path = require('path'); | ||||
const url = require('url'); | const url = require('url'); | ||||
const { initialStorageEvents, storage } = require('./storage'); | const { initialStorageEvents, storage } = require('./storage'); | ||||
const { initialWebsocketEvents } = require('./socket'); | |||||
const { Subject } = require('./tool'); | |||||
let mainWindow; | let mainWindow; | ||||
const socketSubjects = { | |||||
onMessage: new Subject(), | |||||
onError: new Subject(), | |||||
onClose: new Subject(), | |||||
}; | |||||
function createWindow() { | function createWindow() { | ||||
//创建窗口 | //创建窗口 | ||||
mainWindow = new BrowserWindow({ | mainWindow = new BrowserWindow({ | ||||
@@ -43,70 +44,91 @@ function createWindow() { | |||||
// 加载html文件 | // 加载html文件 | ||||
// 这里的路径是umi输出的html路径,如果没有修改过,路径和下面是一样的 | // 这里的路径是umi输出的html路径,如果没有修改过,路径和下面是一样的 | ||||
mainWindow.webContents.openDevTools(); | mainWindow.webContents.openDevTools(); | ||||
// mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); | |||||
mainWindow.loadURL( | |||||
url.format({ | |||||
pathname: path.join(__dirname, '../dist/index.html'), | |||||
protocol: 'file:', | |||||
slashes: true, | |||||
}), | |||||
); | |||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); | |||||
// mainWindow.loadURL( | |||||
// url.format({ | |||||
// pathname: path.join(__dirname, '../dist/index.html'), | |||||
// protocol: 'file:', | |||||
// slashes: true, | |||||
// }), | |||||
// ); | |||||
} | } | ||||
const onMessageReceive = (message) => { | |||||
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', () => { | mainWindow.webContents.on('did-finish-load', () => { | ||||
// 加载初始localStorage数据 | // 加载初始localStorage数据 | ||||
mainWindow.webContents.send('initialStorageData', storage.getAllItem()); | mainWindow.webContents.send('initialStorageData', storage.getAllItem()); | ||||
socketSubjects.onMessage.add(onMessageReceive); | |||||
socketSubjects.onError.add(onSocketError); | |||||
socketSubjects.onClose.add(onSocketClose); | |||||
}); | }); | ||||
mainWindow.on('closed', () => { | mainWindow.on('closed', () => { | ||||
mainWindow = null; | mainWindow = null; | ||||
socketSubjects.onMessage.remove(onMessageReceive); | |||||
socketSubjects.onError.remove(onSocketError); | |||||
socketSubjects.onClose.remove(onSocketClose); | |||||
}); | |||||
// 创建系统通知区菜单 | |||||
tray = new Tray(path.join(__dirname, 'logo.ico')); | |||||
const contextMenu = Menu.buildFromTemplate([ | |||||
{ | |||||
label: '最大化', | |||||
click: () => { | |||||
mainWindow.maximize(); | |||||
}, | |||||
}, | |||||
{ | |||||
label: '最小化', | |||||
click: () => { | |||||
mainWindow.minimize(); | |||||
}, | |||||
}, | |||||
{ | |||||
label: '还原', | |||||
click: () => { | |||||
mainWindow.restore(); | |||||
}, | |||||
}, | |||||
{ | |||||
label: '退出', | |||||
click: () => { | |||||
mainWindow.destroy(); | |||||
notifyWindow.destroy(); | |||||
}, | |||||
}, //我们需要在这里有一个真正的退出(这里直接强制退出) | |||||
]); | |||||
tray.setToolTip('LOCKING盒子'); | |||||
tray.setContextMenu(contextMenu); | |||||
tray.on('click', () => { | |||||
//我们这里模拟桌面程序点击通知区图标实现打开关闭应用的功能 | |||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); | |||||
mainWindow.isVisible() | |||||
? mainWindow.setSkipTaskbar(false) | |||||
: mainWindow.setSkipTaskbar(true); | |||||
}); | }); | ||||
} | } | ||||
// 监听必要的自定义事件 | |||||
ipcMain.handle('manipulate-window', (event, { action }) => { | |||||
// console.log('manipulate-window', event, action); | |||||
const iWindow = BrowserWindow.fromId(event.frameId); | |||||
if (!iWindow) { | |||||
return; | |||||
} | |||||
if (action === 'close') { | |||||
// 关闭窗口 | |||||
iWindow.close(); | |||||
} | |||||
if (action === 'zoom') { | |||||
// 最大化/恢复窗口 | |||||
if (iWindow.isMaximized()) { | |||||
iWindow.unmaximize(); | |||||
} else { | |||||
iWindow.maximize(); | |||||
} | |||||
} | |||||
if (action === 'minimize') { | |||||
// 最小化窗口 | |||||
iWindow.minimize(); | |||||
// 窗口操作事件 | |||||
ipcMain.handle('window:close', (event) => { | |||||
const iWindow = BrowserWindow.fromId(event.sender.id); | |||||
iWindow.hide(); | |||||
if (iWindow === mainWindow) { | |||||
iWindow.setSkipTaskbar(true); | |||||
} | } | ||||
event.preventDefault(); | |||||
// BrowserWindow.fromId(event.sender.id)?.close(); | |||||
}); | |||||
ipcMain.handle('window:zoom', (event) => { | |||||
const iWindow = BrowserWindow.fromId(event.sender.id); | |||||
if (!iWindow) return; | |||||
iWindow.isMaximized() ? iWindow.unmaximize() : iWindow.maximize(); | |||||
}); | |||||
ipcMain.handle('window:minimize', (event) => { | |||||
BrowserWindow.fromId(event.sender.id)?.minimize(); | |||||
}); | |||||
ipcMain.handle('window:hide', (event) => { | |||||
// console.log(event.sender.id, event.senderFrame, event); | |||||
BrowserWindow.fromId(event.sender.id)?.hide(); | |||||
}); | }); | ||||
// 选择文件夹 | // 选择文件夹 | ||||
ipcMain.handle('project-choose-folders', async (event, args) => { | |||||
ipcMain.handle('select-folder', async (event, args) => { | |||||
const res = await dialog.showOpenDialog({ | const res = await dialog.showOpenDialog({ | ||||
properties: ['multiSelections', 'openDirectory'], | |||||
properties: ['openDirectory'], | |||||
}); | }); | ||||
return res; | return res; | ||||
}); | }); | ||||
@@ -114,34 +136,80 @@ ipcMain.handle('project-choose-folders', async (event, args) => { | |||||
ipcMain.handle('open-browser', (event, url) => { | ipcMain.handle('open-browser', (event, url) => { | ||||
shell.openExternal(url); | shell.openExternal(url); | ||||
}); | }); | ||||
// 打开文件所在位置 | |||||
ipcMain.handle('open-file-position', (event, message) => { | |||||
shell.showItemInFolder(message.absolutePath); | |||||
}); | |||||
// 主窗口给消息窗口notifyWindow发送消息 | |||||
ipcMain.handle('notify', (event, messageObj) => { | |||||
// if (!mainWindow.isVisible()) { // 在主窗口不可见时才给消息窗口返送消息 | |||||
notifyWindow.show(); | |||||
notifyWindow.webContents.send('on-notify', messageObj); | |||||
// } | |||||
}); | |||||
// 消息窗口给主窗口发送重新同步文件的命令 | |||||
ipcMain.handle('re-sync-file', (event, messageObj) => { | |||||
mainWindow.webContents.send('request-resync-file', messageObj); | |||||
}); | |||||
ipcMain.handle('download-file', (event, messageObj) => { | |||||
mainWindow.webContents.send('request-download-file', messageObj); | |||||
}); | |||||
// 初始化electron-store相关API | // 初始化electron-store相关API | ||||
initialStorageEvents(ipcMain); | initialStorageEvents(ipcMain); | ||||
app.on('ready', () => { | app.on('ready', () => { | ||||
const { width: windowWidth, height: windowHeight } = | |||||
screen.getPrimaryDisplay().workAreaSize; | |||||
createWindow(); | createWindow(); | ||||
createNotifycationWindow(windowWidth, windowHeight); | |||||
app.on('activate', function () { | app.on('activate', function () { | ||||
// On macOS it's common to re-create a window in the app when the | // On macOS it's common to re-create a window in the app when the | ||||
// dock icon is clicked and there are no other windows open. | // dock icon is clicked and there are no other windows open. | ||||
if (BrowserWindow.getAllWindows().length === 0) createWindow(); | if (BrowserWindow.getAllWindows().length === 0) createWindow(); | ||||
}); | }); | ||||
}); | }); | ||||
app.on('second-instance', (event, commandLine, workingDirectory) => { | |||||
// 当运行第二个实例时,将会聚焦到mainWindow这个窗口 | |||||
if (mainWindow) { | |||||
if (mainWindow.isMinimized()) mainWindow.restore(); | |||||
mainWindow.focus(); | |||||
mainWindow.show(); | |||||
} | |||||
}); | |||||
app.on('window-all-closed', () => { | app.on('window-all-closed', () => { | ||||
if (process.platform !== 'darwin') { | if (process.platform !== 'darwin') { | ||||
app.quit(); | app.quit(); | ||||
} | } | ||||
}); | }); | ||||
initialWebsocketEvents( | |||||
ipcMain, | |||||
function onMessage(message) { | |||||
socketSubjects.onMessage.notify(message); | |||||
}, | |||||
function onError(...args) { | |||||
socketSubjects.onError.notify(...args); | |||||
}, | |||||
function onClose(...args) { | |||||
socketSubjects.onClose.notify(...args); | |||||
}, | |||||
); | |||||
let notifyWindow; | |||||
function createNotifycationWindow(windowWidth, windowHeight) { | |||||
if (notifyWindow) return notifyWindow; | |||||
//创建窗口 | |||||
notifyWindow = new BrowserWindow({ | |||||
width: 504, | |||||
height: 219, // 184, | |||||
x: windowWidth - 480 - 20, | |||||
y: windowHeight - 219 + 10, | |||||
webPreferences: { | |||||
preload: path.join(__dirname, 'preload.js'), | |||||
webSecurity: false, | |||||
nodeIntegration: true, | |||||
}, | |||||
frame: false, | |||||
resizable: false, | |||||
transparent: true, | |||||
alwaysOnTop: true, | |||||
}); | |||||
// 这里的路径是umi输出的html路径,如果没有修改过,路径和下面是一样的 | |||||
notifyWindow.webContents.openDevTools(); | |||||
notifyWindow.loadFile(path.join(__dirname, 'notifycation.html')); | |||||
notifyWindow.setSkipTaskbar(true); | |||||
notifyWindow.hide(); | |||||
// return notifyWindow; | |||||
} |
@@ -0,0 +1,350 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||||
<title>Document</title> | |||||
<style> | |||||
* { | |||||
margin: 0; | |||||
padding: 0; | |||||
} | |||||
#board { | |||||
position: relative; | |||||
width: 504px; | |||||
height: 219px; | |||||
padding: 9px 12px 15px; | |||||
background: url(./bg.png) no-repeat; | |||||
box-sizing: border-box; | |||||
} | |||||
.inner { | |||||
position: relative; | |||||
height: 100%; | |||||
width: 100%; | |||||
display: flex; | |||||
flex-direction: column; | |||||
} | |||||
#close-icon { | |||||
position: absolute; | |||||
width: 24px; | |||||
height: 24px; | |||||
top: 24px; | |||||
right: 32px; | |||||
cursor: pointer; | |||||
z-index: 1; | |||||
} | |||||
.close-icon:before, | |||||
.close-icon:after { | |||||
content: ''; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
right: 0; | |||||
bottom: 0; | |||||
margin: auto; | |||||
width: 16px; | |||||
height: 2px; | |||||
background-color: #fff; | |||||
} | |||||
.close-icon:before { | |||||
transform: rotate(45deg); | |||||
} | |||||
.close-icon:after { | |||||
transform: rotate(-45deg); | |||||
} | |||||
h3 { | |||||
flex: none; | |||||
height: 52px; | |||||
line-height: 52px; | |||||
color: #fff; | |||||
font-size: 20px; | |||||
padding-left: 20px; | |||||
} | |||||
#syncMsg { | |||||
display: flex; | |||||
flex-direction: column; | |||||
justify-content: space-evenly; | |||||
flex: 1; | |||||
} | |||||
.line { | |||||
display: flex; | |||||
font-size: 14px; | |||||
line-height: 22px; | |||||
flex-direction: row; | |||||
justify-content: space-between; | |||||
padding: 0 20px; | |||||
} | |||||
.primary-btn { | |||||
font-size: 16px; | |||||
color: #7850FF; | |||||
cursor: pointer; | |||||
} | |||||
.err-icon { | |||||
position: relative; | |||||
border-radius: 50%; | |||||
display: inline-block; | |||||
vertical-align: top; | |||||
height: 24px; | |||||
width: 24px; | |||||
background-color: #FF3F17; | |||||
} | |||||
.succ-icon { | |||||
position: relative; | |||||
border-radius: 50%; | |||||
display: inline-block; | |||||
vertical-align: top; | |||||
height: 24px; | |||||
width: 24px; | |||||
background: #51DCB6; | |||||
} | |||||
.succ-icon:before, | |||||
.succ-icon:after { | |||||
content: ''; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
right: 0; | |||||
bottom: 0; | |||||
margin: auto; | |||||
width: 16px; | |||||
height: 2px; | |||||
background-color: #fff; | |||||
} | |||||
.succ-icon:before { | |||||
width: 7px; | |||||
transform: translate(-3px, 2px) rotate(41deg); | |||||
} | |||||
.succ-icon:after { | |||||
width: 10px; | |||||
transform: translateX(2px) rotate(-60deg); | |||||
} | |||||
.label { | |||||
margin-left: 8px; | |||||
} | |||||
#conflictMsg { | |||||
display: flex; | |||||
flex-direction: row; | |||||
/* justify-content: space-evenly; */ | |||||
flex: 1; | |||||
padding: 0 20px; | |||||
align-items: center; | |||||
} | |||||
#conflictMsg .right { | |||||
flex: none; | |||||
margin-left: 72px; | |||||
} | |||||
#conflictMsg .left { | |||||
flex: 1; | |||||
font-size: 20px; | |||||
color: rgba(0, 0, 0, 0.85); | |||||
} | |||||
.hide { | |||||
display: none !important; | |||||
} | |||||
#syncMsg.success .err-icon { | |||||
display: none; | |||||
} | |||||
#syncMsg.success .label:after { | |||||
content: '成功!'; | |||||
} | |||||
#syncMsg.success .primary-btn:after { | |||||
content: '查看'; | |||||
} | |||||
#syncMsg.error .succ-icon { | |||||
display: none; | |||||
} | |||||
#syncMsg.error .label:after { | |||||
content: '失败'; | |||||
} | |||||
#syncMsg.error .primary-btn:after { | |||||
content: '重新同步'; | |||||
} | |||||
#syncMsg.incomming #syncIncoming { | |||||
font-size: 20px; | |||||
color: rgba(0, 0, 0, 0.85); | |||||
} | |||||
#syncMsg.incomming .primary-btn:after { | |||||
content: '下载'; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div id="board"> | |||||
<div id="close-icon" class="close-icon"></div> | |||||
<div class="inner"> | |||||
<h3>消息通知</h3> | |||||
<div id="syncMsg"> | |||||
<div class="line"> | |||||
<span id="syncMsgFileName">文件名称</span> | |||||
<span id="syncMsgDate">21/08/20 16:34:21</span> | |||||
</div> | |||||
<div class="line"> | |||||
<div id="syncResult"> | |||||
<span class="err-icon close-icon"></span> | |||||
<span class="succ-icon"></span> | |||||
<span class="label">同步</span> | |||||
</div> | |||||
<div id="syncIncoming"> | |||||
</div> | |||||
<span id="syncMsgBtn" class="primary-btn"></span> | |||||
</div> | |||||
</div> | |||||
<div id="conflictMsg"> | |||||
<div class="left" id="conflictMsgText"> | |||||
该文件存在版本冲突,请先下载该文件最新版本,修改后再上传! | |||||
</div> | |||||
<div class="right"> | |||||
<span id="conflictMsgBtn" class="primary-btn">下载</span> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<script type="text/javascript"> | |||||
let contextMsg; | |||||
const noop = () => { }; | |||||
const domId = document.getElementById.bind(document); | |||||
const closeBtn = domId('close-icon'); | |||||
const syncMsgView = domId('syncMsg'); | |||||
const syncMsgBtn = domId('syncMsgBtn'); | |||||
const syncMsgFileName = domId('syncMsgFileName'); | |||||
const syncMsgDate = domId('syncMsgDate'); | |||||
const syncResult = domId('syncResult'); | |||||
const syncIncoming = domId('syncIncoming'); | |||||
const conflictMsgView = domId('conflictMsg'); | |||||
const conflictMsgBtn = domId('conflictMsgBtn'); | |||||
const conflictMsgText = domId('conflictMsgText'); | |||||
syncMsgView.classList.add('hide'); | |||||
conflictMsgView.classList.add('hide'); | |||||
syncResult.classList.add('hide'); | |||||
syncIncoming.classList.add('hide'); | |||||
closeBtn.addEventListener('click', function () { window.ipcRenderer.invoke('window:hide'); }); | |||||
syncMsgBtn.addEventListener('click', function () { | |||||
if (contextMsg.taskSyncStatus === 'TASK_SYNC_STATUS_FINISH') { // 查看 | |||||
window.ipcRenderer.invoke('open-file-position', contextMsg); | |||||
return; | |||||
} | |||||
if (contextMsg.taskSyncStatus === 'TASK_SYNC_STATUS_FAIL') { // 重新同步 | |||||
window.ipcRenderer.invoke('re-sync-file', contextMsg); | |||||
return; | |||||
} | |||||
if (false) { // 下载 | |||||
window.ipcRenderer.invoke('download-file', contextMsg); | |||||
} | |||||
}); | |||||
conflictMsgBtn.addEventListener('click', function () { | |||||
window.ipcRenderer.invoke('download-file', contextMsg); | |||||
}); | |||||
window.addIpcRendererListener('on-notify', function (_, message) { // message: DATA.SocketFileMsg | |||||
console.log('comming message: ', message); | |||||
render(message); | |||||
}); | |||||
function render(message) { | |||||
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; | |||||
} | |||||
runningMethod(message); | |||||
} | |||||
function useView(dom) { dom.classList.remove('hide'); }; | |||||
function fileFullName(message) { | |||||
const extension = message.extension; | |||||
return `${message.archName}${extension ? '.' + extension : ''}`; | |||||
} | |||||
function resetDomStatus() { | |||||
syncMsgView.classList.add('hide'); | |||||
syncMsgView.classList.remove('success'); | |||||
syncMsgView.classList.remove('error'); | |||||
syncMsgView.classList.remove('incomming'); | |||||
syncResult.classList.add('hide'); | |||||
syncIncoming.classList.add('hide'); | |||||
conflictMsgView.classList.add('hide'); | |||||
} | |||||
function renderTaskSuccess(message) { | |||||
useView(syncMsgView); | |||||
syncMsgView.classList.add('success'); | |||||
useView(syncResult); | |||||
syncMsgFileName.innerText = fileFullName(message); | |||||
syncMsgDate.innerText = message.taskCreateDateStr; | |||||
} | |||||
function renderTaskError(message) { | |||||
useView(syncMsgView); | |||||
syncMsgView.classList.add('error'); | |||||
syncResult.classList.remove('hide'); | |||||
syncMsgFileName.innerText = fileFullName(message); | |||||
syncMsgDate.innerText = message.taskCreateDateStr; | |||||
} | |||||
function renderTaskIncoming(message) { | |||||
useView(syncMsgView); | |||||
syncMsgView.classList.add('incomming'); | |||||
useView(syncIncoming); | |||||
syncMsgFileName.innerText = fileFullName(message); | |||||
syncMsgDate.innerText = message.taskCreateDateStr; | |||||
syncIncoming.innerText = '某某人同步了「某某文件」'; | |||||
} | |||||
function renderConflictMsg(message) { | |||||
useView(conflictMsgView); | |||||
conflictMsgText.innerText = `「${fileFullName(message)}」存在版本冲突,请先下载该文件最新版本,修改后再上传!`; | |||||
} | |||||
</script> | |||||
</body> | |||||
</html> |
@@ -1,3 +1,5 @@ | |||||
// DEPRECATED | |||||
// 现在socket链接改在页面发起 | |||||
const io = require('ws'); | const io = require('ws'); | ||||
const config = require('./config'); | const config = require('./config'); | ||||
@@ -11,7 +13,7 @@ function initialWebsocket(onMessage, onError, onClose) { | |||||
// socket.send('1'); | // socket.send('1'); | ||||
}); | }); | ||||
socket.on('message', (message) => { | socket.on('message', (message) => { | ||||
console.log('socket message', message); | |||||
// console.log('socket message', message); | |||||
onMessage(message); | onMessage(message); | ||||
}); | }); | ||||
socket.on('error', (...args) => { | socket.on('error', (...args) => { | ||||
@@ -5,7 +5,7 @@ import { firstCharToLowerCase, handleRequest } from './utils/tool'; | |||||
import { isObject } from 'lodash'; | import { isObject } from 'lodash'; | ||||
import { logout, queryCurrent } from './services/user'; | import { logout, queryCurrent } from './services/user'; | ||||
import storage from './utils/storage'; | import storage from './utils/storage'; | ||||
import './services/socket'; | |||||
import { initialWebsocket } from './services/socket'; | |||||
const codeMessage = { | const codeMessage = { | ||||
200: '服务器成功返回请求的数据。', | 200: '服务器成功返回请求的数据。', | ||||
@@ -134,3 +134,6 @@ export async function getInitialState(): Promise<{ | |||||
fetchUserInfo, | fetchUserInfo, | ||||
}; | }; | ||||
} | } | ||||
// 链接socket | |||||
initialWebsocket(); |
@@ -1,10 +1,42 @@ | |||||
import React from 'react'; | import React from 'react'; | ||||
import { Modal, ModalProps, Input, Button } from 'antd'; | |||||
import { Modal, ModalProps, Input, Button, message } from 'antd'; | |||||
import styles from './FolderSetModal.less'; | import styles from './FolderSetModal.less'; | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import { useCallback } from 'react'; | |||||
import system from '@/services/system'; | |||||
import { useState } from 'react'; | |||||
import { useEffect } from 'react'; | |||||
export default function FolderSetModal(props: ModalProps) { | export default function FolderSetModal(props: ModalProps) { | ||||
const { className, ...restProps } = props; | const { className, ...restProps } = props; | ||||
const [folderPath, setFolderPath] = useState(''); | |||||
const [loading, setLoading] = useState(false); | |||||
const onSetFolderPos = useCallback(async () => { | |||||
const nextPath = await system.chooseFolder(); | |||||
console.log(nextPath); | |||||
if (nextPath) { | |||||
setFolderPath(nextPath); | |||||
} | |||||
}, []); | |||||
useEffect(() => { | |||||
// todo: 查询当前使用的路径 | |||||
}, []); | |||||
const updateFolderPath = useCallback(() => { | |||||
if (!folderPath) { | |||||
message.error('请选择文件夹'); | |||||
return; | |||||
} | |||||
setLoading(true); | |||||
// todo: 调接口 | |||||
setLoading(false); | |||||
if (restProps.onCancel) { | |||||
restProps.onCancel(); | |||||
} | |||||
}, [folderPath]); | |||||
return ( | return ( | ||||
<Modal | <Modal | ||||
title="同步设置" | title="同步设置" | ||||
@@ -15,10 +47,23 @@ export default function FolderSetModal(props: ModalProps) { | |||||
maskClosable={false} | maskClosable={false} | ||||
> | > | ||||
<span className={styles.label}>「工作空间」本地盘位置</span> | <span className={styles.label}>「工作空间」本地盘位置</span> | ||||
<Input size="small" suffix={<Button type="link">选择</Button>} /> | |||||
<Input | |||||
value={folderPath} | |||||
size="small" | |||||
suffix={ | |||||
<Button type="link" onClick={onSetFolderPos}> | |||||
选择 | |||||
</Button> | |||||
} | |||||
/> | |||||
<div className={styles.btnGroup}> | <div className={styles.btnGroup}> | ||||
<Button className={styles.btn} type="primary"> | |||||
<Button | |||||
loading={loading} | |||||
className={styles.btn} | |||||
type="primary" | |||||
onClick={updateFolderPath} | |||||
> | |||||
更新资料 | 更新资料 | ||||
</Button> | </Button> | ||||
</div> | </div> | ||||
@@ -2,7 +2,7 @@ | |||||
display: flex; | display: flex; | ||||
align-items: center; | align-items: center; | ||||
flex-direction: row; | flex-direction: row; | ||||
background: #FFFFFF; | |||||
background: #ffffff; | |||||
box-shadow: 0px 1px 2px 0px rgba(102, 110, 115, 0.3); | box-shadow: 0px 1px 2px 0px rgba(102, 110, 115, 0.3); | ||||
border-radius: 4px; | border-radius: 4px; | ||||
height: 76px; | height: 76px; | ||||
@@ -27,7 +27,11 @@ | |||||
} | } | ||||
.left { | .left { | ||||
height: 100%; | height: 100%; | ||||
.icon { width:36px; margin-right: 12px;} | |||||
.icon { | |||||
width: 36px; | |||||
margin-right: 12px; | |||||
height: 100%; | |||||
} | |||||
.content { | .content { | ||||
display: inline-flex; | display: inline-flex; | ||||
width: calc(100% - 36px - 12px); | width: calc(100% - 36px - 12px); | ||||
@@ -79,8 +83,12 @@ | |||||
vertical-align: sub; | vertical-align: sub; | ||||
margin-right: 8px; | margin-right: 8px; | ||||
font-size: 16px; | font-size: 16px; | ||||
&.success { color: #51DCB6; } | |||||
&.error {color: #D6243A; } | |||||
&.success { | |||||
color: #51dcb6; | |||||
} | |||||
&.error { | |||||
color: #d6243a; | |||||
} | |||||
} | } | ||||
} | } | ||||
.time { | .time { | ||||
@@ -91,11 +99,8 @@ | |||||
transform: scale(0.83); | transform: scale(0.83); | ||||
} | } | ||||
.textOverflow { | .textOverflow { | ||||
white-space: nowrap; | white-space: nowrap; | ||||
text-overflow: ellipsis; | text-overflow: ellipsis; | ||||
overflow: hidden; | overflow: hidden; | ||||
} | } | ||||
@@ -1,5 +1,5 @@ | |||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import dayjs from 'dayjs'; | |||||
import dayjs, { Dayjs } from 'dayjs'; | |||||
import React, { CSSProperties, useMemo } from 'react'; | import React, { CSSProperties, useMemo } from 'react'; | ||||
import { memo } from 'react'; | import { memo } from 'react'; | ||||
import FileIcon from '../FileIcon'; | import FileIcon from '../FileIcon'; | ||||
@@ -19,8 +19,6 @@ interface FileStatusProps { | |||||
export default function FileStatus(props: FileStatusProps) { | export default function FileStatus(props: FileStatusProps) { | ||||
const { className, style, data, ...restProps } = props; | const { className, style, data, ...restProps } = props; | ||||
console.log(data); | |||||
const filePath = useMemo(() => { | const filePath = useMemo(() => { | ||||
return [data.projName, data.nodeName, data.relativePath] | return [data.projName, data.nodeName, data.relativePath] | ||||
.filter(identity) | .filter(identity) | ||||
@@ -57,11 +55,16 @@ export default function FileStatus(props: FileStatusProps) { | |||||
{...restProps} | {...restProps} | ||||
/> | /> | ||||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | {data.taskSyncStatus === TaskStatus.FINISH ? ( | ||||
<Time time={'2020-01-01 18:24:56'} /> | |||||
<Time time={data.taskCreateDate} /> | |||||
) : null} | ) : null} | ||||
</div> | </div> | ||||
<div className={styles.right}> | <div className={styles.right}> | ||||
{/* 查看1: 已下载 文件打开文件夹 */} | {/* 查看1: 已下载 文件打开文件夹 */} | ||||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | |||||
<Button type="link" className={styles.button} size="small"> | |||||
查看 | |||||
</Button> | |||||
) : null} | |||||
{/* 查看2: 未下载 且 已删除 文件跳转到web端 */} | {/* 查看2: 未下载 且 已删除 文件跳转到web端 */} | ||||
{/* 下载: 未下载 且 未删除 文件 */} | {/* 下载: 未下载 且 未删除 文件 */} | ||||
{/* 重新下载: 下载失败时出现 */} | {/* 重新下载: 下载失败时出现 */} | ||||
@@ -74,8 +77,8 @@ export default function FileStatus(props: FileStatusProps) { | |||||
); | ); | ||||
} | } | ||||
const Time = memo((props: { time: string }) => ( | |||||
<span className={styles.time}>{dayjs(props.time).format('HH:mm:ss')}</span> | |||||
const Time = memo((props: { time: Dayjs }) => ( | |||||
<span className={styles.time}>{props.time.format('HH:mm:ss')}</span> | |||||
)); | )); | ||||
interface LoadDescProps { | interface LoadDescProps { | ||||
@@ -13,9 +13,9 @@ import { TaskStatus } from '@/services/API.helper'; | |||||
import { DATA } from '@/services/API'; | import { DATA } from '@/services/API'; | ||||
enum SyncType { | enum SyncType { | ||||
Syncing, | |||||
SyncSuccess, | |||||
SyncError, | |||||
Syncing = 'syncing', | |||||
SyncSuccess = 'syncSuccess', | |||||
SyncError = 'syncError', | |||||
} | } | ||||
const syncTypeList = [ | const syncTypeList = [ | ||||
@@ -49,6 +49,14 @@ export default function SyncView() { | |||||
); | ); | ||||
}, [originList]); | }, [originList]); | ||||
const mapSyncTypeToCount = useMemo(() => { | |||||
return { | |||||
[SyncType.Syncing]: syncingList.length, | |||||
[SyncType.SyncSuccess]: succList.length, | |||||
[SyncType.SyncError]: failList.length, | |||||
}; | |||||
}, [syncingList, succList, failList]); | |||||
const curretList = useMemo(() => { | const curretList = useMemo(() => { | ||||
if (type === SyncType.SyncSuccess) return succList; | if (type === SyncType.SyncSuccess) return succList; | ||||
if (type === SyncType.SyncError) return failList; | if (type === SyncType.SyncError) return failList; | ||||
@@ -72,7 +80,11 @@ export default function SyncView() { | |||||
> | > | ||||
<img className={styles.icon} src={icon} alt="" /> | <img className={styles.icon} src={icon} alt="" /> | ||||
<span className={styles.label}>{label}</span> | <span className={styles.label}>{label}</span> | ||||
<span className={styles.count}>99+</span> | |||||
<span className={styles.count}> | |||||
{mapSyncTypeToCount[iType] > 99 | |||||
? '99+' | |||||
: mapSyncTypeToCount[iType] || ''} | |||||
</span> | |||||
</div> | </div> | ||||
))} | ))} | ||||
</div> | </div> | ||||
@@ -1,3 +1,4 @@ | |||||
import { Dayjs } from 'dayjs'; | |||||
import { TaskStatus, TaskType } from './API.helper'; | import { TaskStatus, TaskType } from './API.helper'; | ||||
declare namespace API { | declare namespace API { | ||||
@@ -74,6 +75,10 @@ declare namespace DATA { | |||||
taskSyncProgress: 100; | taskSyncProgress: 100; | ||||
taskSyncStatus: TaskStatus; // "TASK_SYNC_STATUS_FINISH", | taskSyncStatus: TaskStatus; // "TASK_SYNC_STATUS_FINISH", | ||||
taskType: TaskType; // "DOWNLOAD", | taskType: TaskType; // "DOWNLOAD", | ||||
taskCreateUnixTime: number; // 秒级时间 | |||||
taskCreateDate: Dayjs; // 前端根据taskCreateUnixTime生成的时间对象 | |||||
taskCreateDateStr: string; // 前端根据taskCreateUnixTime生成的时间字符串 供给消息窗口使用, 格式: YY/MM/DD HH:mm:ss | |||||
absolutePath: string; // 绝对路径 | |||||
version: number; // 1, | version: number; // 1, | ||||
workStatus: number; // 1 | workStatus: number; // 1 | ||||
} | } | ||||
@@ -1,8 +1,11 @@ | |||||
import { firstCharToLowerCase, Subject } from '@/utils/tool'; | import { firstCharToLowerCase, Subject } from '@/utils/tool'; | ||||
import dayjs from 'dayjs'; | |||||
import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
import { DATA } from './API'; | import { DATA } from './API'; | ||||
import { isClient } from './system'; | |||||
export const isClient = !!window.ipcRenderer; // process.env.IS_CLIENT; | |||||
const gatewayPort = window.systemConfig?.gatewayPort || 7888; | |||||
const skUrl = `ws://127.0.0.1:${gatewayPort}/websocket/subscriptionTaskSync`; | |||||
class FileMessageManager extends Subject { | class FileMessageManager extends Subject { | ||||
public list: DATA.SocketFileMsg[] = []; | public list: DATA.SocketFileMsg[] = []; | ||||
@@ -10,6 +13,7 @@ class FileMessageManager extends Subject { | |||||
public fileMap = new Map(); | public fileMap = new Map(); | ||||
addFileMsg(message: DATA.SocketFileMsg) { | addFileMsg(message: DATA.SocketFileMsg) { | ||||
this.fileMap.set(message.taskId, message); | |||||
this.list = [message].concat(this.list); | this.list = [message].concat(this.list); | ||||
this.notifyObservers(this.list); | this.notifyObservers(this.list); | ||||
} | } | ||||
@@ -25,12 +29,21 @@ class FileMessageManager extends Subject { | |||||
const message = firstCharToLowerCase( | const message = firstCharToLowerCase( | ||||
JSON.parse(messageStr), | JSON.parse(messageStr), | ||||
) as DATA.SocketFileMsg; | ) 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); | const existMsg = this.fileMap.get(message.taskId); | ||||
if (existMsg) { | if (existMsg) { | ||||
this.updateFileMsg(message); | this.updateFileMsg(message); | ||||
} else { | } else { | ||||
this.addFileMsg(message); | this.addFileMsg(message); | ||||
} | } | ||||
// todo: 临时测试 | |||||
if (isClient) { | |||||
window.ipcRenderer.invoke('notify', message); | |||||
} | |||||
} catch (e) { | } catch (e) { | ||||
console.error('message 解析失败:', e, messageStr); | console.error('message 解析失败:', e, messageStr); | ||||
} | } | ||||
@@ -39,21 +52,42 @@ class FileMessageManager extends Subject { | |||||
const fileMng = new FileMessageManager(); | 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); | |||||
}); | |||||
enum SocketState { | |||||
CONNECTING = 0, // 连接尚未建立 | |||||
OPEN, // WebSocket的链接已经建立 | |||||
CLOSING, // 连接正在关闭 | |||||
CLOSED, // 连接已经关闭或不可用 | |||||
} | |||||
let taskSyncSocket: WebSocket; | |||||
function initTaskSyncSocket() { | |||||
taskSyncSocket = new WebSocket(skUrl); | |||||
taskSyncSocket.onopen = () => { | |||||
console.log('taskSyncSocket connection'); | |||||
}; | |||||
taskSyncSocket.onmessage = ({ data }) => { | |||||
fileMng.receiveMessage(data); | |||||
}; | |||||
taskSyncSocket.onerror = (...args) => { | |||||
console.log('taskSyncSocket error:', args); | |||||
}; | |||||
taskSyncSocket.onclose = (...args) => { | |||||
console.log('taskSyncSocket close:', args); | |||||
setTimeout(() => initTaskSyncSocket(), 5000); | |||||
}; | |||||
} | |||||
export function sendSocketMessage(message: string) { | |||||
if (taskSyncSocket && taskSyncSocket.readyState === SocketState.OPEN) { | |||||
taskSyncSocket.send(message); | |||||
} | |||||
} | |||||
export function initialWebsocket() { | |||||
initTaskSyncSocket(); | |||||
} | } | ||||
export function useSocketMessages() { | export function useSocketMessages() { | ||||
const [list, setList] = useState(fileMng.list); | const [list, setList] = useState(fileMng.list); | ||||
// console.log(list); | |||||
useEffect(() => { | useEffect(() => { | ||||
fileMng.addObserver(setList); | fileMng.addObserver(setList); | ||||
return () => { | return () => { | ||||
@@ -1,4 +1,6 @@ | |||||
import { fetchLocalApi } from '@/utils/request'; | import { fetchLocalApi } from '@/utils/request'; | ||||
import { DATA } from './API'; | |||||
import { sendSocketMessage } from './socket'; | |||||
export const isClient = !!window.ipcRenderer; // process.env.IS_CLIENT; | export const isClient = !!window.ipcRenderer; // process.env.IS_CLIENT; | ||||
@@ -9,13 +11,13 @@ const ipcRenderer = window.ipcRenderer; | |||||
const system = { | const system = { | ||||
closeWindow() { | closeWindow() { | ||||
ipcRenderer.invoke('manipulate-window', { action: 'close' }); | |||||
ipcRenderer.invoke('window:close'); | |||||
}, | }, | ||||
zoomWindow() { | zoomWindow() { | ||||
ipcRenderer.invoke('manipulate-window', { action: 'zoom' }); | |||||
ipcRenderer.invoke('window:zoom'); | |||||
}, | }, | ||||
minimizeWindow() { | minimizeWindow() { | ||||
ipcRenderer.invoke('manipulate-window', { action: 'minimize' }); | |||||
ipcRenderer.invoke('window:minimize'); | |||||
}, | }, | ||||
openUrl(url: string) { | openUrl(url: string) { | ||||
if (!isClient) { | if (!isClient) { | ||||
@@ -24,6 +26,13 @@ const system = { | |||||
} | } | ||||
ipcRenderer.invoke('open-browser', url); | ipcRenderer.invoke('open-browser', url); | ||||
}, | }, | ||||
// 选择文件 | |||||
chooseFolder: safeCall(async () => { | |||||
const res = await ipcRenderer.invoke('select-folder'); | |||||
const { canceled, filePaths } = res; | |||||
if (canceled) return null; | |||||
return filePaths[0] as string; | |||||
}), | |||||
storage: { | storage: { | ||||
set: safeCall((key: string, value: any) => { | set: safeCall((key: string, value: any) => { | ||||
@@ -35,25 +44,36 @@ const system = { | |||||
}, | }, | ||||
/* 发往网关的请求 */ | /* 发往网关的请求 */ | ||||
// 登陆 | // 登陆 | ||||
login: safeCall(async (userId: string, userPhone: string) => { | |||||
login: async (userId: string, userPhone: string) => { | |||||
const res = await fetchLocalApi<null>( | const res = await fetchLocalApi<null>( | ||||
'login', | 'login', | ||||
{ userId, userPhone }, | { userId, userPhone }, | ||||
{ silent: true }, | { silent: true }, | ||||
); | ); | ||||
ipcRenderer.invoke('socket:send-message', `login:${userPhone}`); | |||||
sendSocketMessage(`login:${userPhone}`); | |||||
return res; | return res; | ||||
}), | |||||
}, | |||||
// 退出登陆 | // 退出登陆 | ||||
logout: safeCall(async () => { | |||||
logout: async () => { | |||||
return await fetchLocalApi<null>('logout'); | return await fetchLocalApi<null>('logout'); | ||||
}), | |||||
}, | |||||
// 发送下载同步文件夹的指令 | // 发送下载同步文件夹的指令 | ||||
syncProjects: safeCall((projectIds: string[]) => { | |||||
syncProjects: (projectIds: string[]) => { | |||||
return fetchLocalApi<null>('syncFolderToWorkSpace', { | return fetchLocalApi<null>('syncFolderToWorkSpace', { | ||||
ids: projectIds.join(','), | ids: projectIds.join(','), | ||||
}); | }); | ||||
}), | |||||
}, | |||||
}; | }; | ||||
if (isClient) { | |||||
window.addIpcRendererListener( | |||||
'request-resync-file', | |||||
(e, msg: DATA.SocketFileMsg) => {}, | |||||
); | |||||
window.addIpcRendererListener( | |||||
'request-download-file', | |||||
(e, msg: DATA.SocketFileMsg) => {}, | |||||
); | |||||
} | |||||
export default system; | export default system; |
@@ -34,11 +34,9 @@ export async function login(account: string, password: string) { | |||||
return errorReponse('该账号没有访问权限'); | return errorReponse('该账号没有访问权限'); | ||||
} | } | ||||
if (isClient) { | |||||
const systemLoginRes = await system.login(res.data.id, res.data.phone); | |||||
if (!isReqSuccess(systemLoginRes)) { | |||||
return errorReponse('本地网管通讯失败'); | |||||
} | |||||
const systemLoginRes = await system.login(res.data.id, res.data.phone); | |||||
if (!isReqSuccess(systemLoginRes)) { | |||||
return errorReponse('本地网管通讯失败'); | |||||
} | } | ||||
// const companyInfoRes = await fetchApi('company/queryFrontDeskCompanyById', { id: userData.companyId }, { silent: true }); | // const companyInfoRes = await fetchApi('company/queryFrontDeskCompanyById', { id: userData.companyId }, { silent: true }); | ||||
@@ -1,10 +1,11 @@ | |||||
import { isClient } from '@/services/system'; | |||||
import { message } from 'antd'; | import { message } from 'antd'; | ||||
import { request } from 'umi'; | import { request } from 'umi'; | ||||
import { parseRequest } from './request.config'; | import { parseRequest } from './request.config'; | ||||
import { errorReponse, firstCharToLowerCase, handleRequest } from './tool'; | import { errorReponse, firstCharToLowerCase, handleRequest } from './tool'; | ||||
const remoteUrl = window.systemConfig?.remoteUrl || ''; | const remoteUrl = window.systemConfig?.remoteUrl || ''; | ||||
const gatewayPort = window.systemConfig?.gatewayPort || 0; | |||||
const gatewayPort = window.systemConfig?.gatewayPort || 7888; | |||||
export async function fetchApi<T = any>( | export async function fetchApi<T = any>( | ||||
path: string, | path: string, | ||||
@@ -41,7 +42,7 @@ export async function fetchLocalApi<T = any>( | |||||
} | } | ||||
const { silent, method = 'GET', ...restOptions } = options; | const { silent, method = 'GET', ...restOptions } = options; | ||||
const res = await request<API.ResponseData<T>>( | const res = await request<API.ResponseData<T>>( | ||||
`http://127.0.0.1:${gatewayPort}/api/${path}`, | |||||
`${isClient ? `http://127.0.0.1:${gatewayPort}` : '/gateway'}/api/${path}`, | |||||
{ method, [method === 'GET' ? 'params' : 'data']: params, ...restOptions }, | { method, [method === 'GET' ? 'params' : 'data']: params, ...restOptions }, | ||||
); | ); | ||||
if (!silent) { | if (!silent) { | ||||