@@ -31,5 +31,13 @@ export default defineConfig({ | |||
changeOrigin: true, | |||
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 url = require('url'); | |||
const { initialStorageEvents, storage } = require('./storage'); | |||
const { initialWebsocketEvents } = require('./socket'); | |||
const { Subject } = require('./tool'); | |||
let mainWindow; | |||
const socketSubjects = { | |||
onMessage: new Subject(), | |||
onError: new Subject(), | |||
onClose: new Subject(), | |||
}; | |||
function createWindow() { | |||
//创建窗口 | |||
mainWindow = new BrowserWindow({ | |||
@@ -43,70 +44,91 @@ function createWindow() { | |||
// 加载html文件 | |||
// 这里的路径是umi输出的html路径,如果没有修改过,路径和下面是一样的 | |||
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', () => { | |||
// 加载初始localStorage数据 | |||
mainWindow.webContents.send('initialStorageData', storage.getAllItem()); | |||
socketSubjects.onMessage.add(onMessageReceive); | |||
socketSubjects.onError.add(onSocketError); | |||
socketSubjects.onClose.add(onSocketClose); | |||
}); | |||
mainWindow.on('closed', () => { | |||
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({ | |||
properties: ['multiSelections', 'openDirectory'], | |||
properties: ['openDirectory'], | |||
}); | |||
return res; | |||
}); | |||
@@ -114,34 +136,80 @@ ipcMain.handle('project-choose-folders', async (event, args) => { | |||
ipcMain.handle('open-browser', (event, 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 | |||
initialStorageEvents(ipcMain); | |||
app.on('ready', () => { | |||
const { width: windowWidth, height: windowHeight } = | |||
screen.getPrimaryDisplay().workAreaSize; | |||
createWindow(); | |||
createNotifycationWindow(windowWidth, windowHeight); | |||
app.on('activate', function () { | |||
// 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. | |||
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', () => { | |||
if (process.platform !== 'darwin') { | |||
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 config = require('./config'); | |||
@@ -11,7 +13,7 @@ function initialWebsocket(onMessage, onError, onClose) { | |||
// socket.send('1'); | |||
}); | |||
socket.on('message', (message) => { | |||
console.log('socket message', message); | |||
// console.log('socket message', message); | |||
onMessage(message); | |||
}); | |||
socket.on('error', (...args) => { | |||
@@ -5,7 +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'; | |||
import { initialWebsocket } from './services/socket'; | |||
const codeMessage = { | |||
200: '服务器成功返回请求的数据。', | |||
@@ -134,3 +134,6 @@ export async function getInitialState(): Promise<{ | |||
fetchUserInfo, | |||
}; | |||
} | |||
// 链接socket | |||
initialWebsocket(); |
@@ -1,10 +1,42 @@ | |||
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 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) { | |||
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 ( | |||
<Modal | |||
title="同步设置" | |||
@@ -15,10 +47,23 @@ export default function FolderSetModal(props: ModalProps) { | |||
maskClosable={false} | |||
> | |||
<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}> | |||
<Button className={styles.btn} type="primary"> | |||
<Button | |||
loading={loading} | |||
className={styles.btn} | |||
type="primary" | |||
onClick={updateFolderPath} | |||
> | |||
更新资料 | |||
</Button> | |||
</div> | |||
@@ -2,7 +2,7 @@ | |||
display: flex; | |||
align-items: center; | |||
flex-direction: row; | |||
background: #FFFFFF; | |||
background: #ffffff; | |||
box-shadow: 0px 1px 2px 0px rgba(102, 110, 115, 0.3); | |||
border-radius: 4px; | |||
height: 76px; | |||
@@ -27,7 +27,11 @@ | |||
} | |||
.left { | |||
height: 100%; | |||
.icon { width:36px; margin-right: 12px;} | |||
.icon { | |||
width: 36px; | |||
margin-right: 12px; | |||
height: 100%; | |||
} | |||
.content { | |||
display: inline-flex; | |||
width: calc(100% - 36px - 12px); | |||
@@ -79,8 +83,12 @@ | |||
vertical-align: sub; | |||
margin-right: 8px; | |||
font-size: 16px; | |||
&.success { color: #51DCB6; } | |||
&.error {color: #D6243A; } | |||
&.success { | |||
color: #51dcb6; | |||
} | |||
&.error { | |||
color: #d6243a; | |||
} | |||
} | |||
} | |||
.time { | |||
@@ -91,11 +99,8 @@ | |||
transform: scale(0.83); | |||
} | |||
.textOverflow { | |||
white-space: nowrap; | |||
text-overflow: ellipsis; | |||
overflow: hidden; | |||
} | |||
@@ -1,5 +1,5 @@ | |||
import classNames from 'classnames'; | |||
import dayjs from 'dayjs'; | |||
import dayjs, { Dayjs } from 'dayjs'; | |||
import React, { CSSProperties, useMemo } from 'react'; | |||
import { memo } from 'react'; | |||
import FileIcon from '../FileIcon'; | |||
@@ -19,8 +19,6 @@ interface FileStatusProps { | |||
export default function FileStatus(props: FileStatusProps) { | |||
const { className, style, data, ...restProps } = props; | |||
console.log(data); | |||
const filePath = useMemo(() => { | |||
return [data.projName, data.nodeName, data.relativePath] | |||
.filter(identity) | |||
@@ -57,11 +55,16 @@ export default function FileStatus(props: FileStatusProps) { | |||
{...restProps} | |||
/> | |||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | |||
<Time time={'2020-01-01 18:24:56'} /> | |||
<Time time={data.taskCreateDate} /> | |||
) : null} | |||
</div> | |||
<div className={styles.right}> | |||
{/* 查看1: 已下载 文件打开文件夹 */} | |||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | |||
<Button type="link" className={styles.button} size="small"> | |||
查看 | |||
</Button> | |||
) : null} | |||
{/* 查看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 { | |||
@@ -13,9 +13,9 @@ import { TaskStatus } from '@/services/API.helper'; | |||
import { DATA } from '@/services/API'; | |||
enum SyncType { | |||
Syncing, | |||
SyncSuccess, | |||
SyncError, | |||
Syncing = 'syncing', | |||
SyncSuccess = 'syncSuccess', | |||
SyncError = 'syncError', | |||
} | |||
const syncTypeList = [ | |||
@@ -49,6 +49,14 @@ export default function SyncView() { | |||
); | |||
}, [originList]); | |||
const mapSyncTypeToCount = useMemo(() => { | |||
return { | |||
[SyncType.Syncing]: syncingList.length, | |||
[SyncType.SyncSuccess]: succList.length, | |||
[SyncType.SyncError]: failList.length, | |||
}; | |||
}, [syncingList, succList, failList]); | |||
const curretList = useMemo(() => { | |||
if (type === SyncType.SyncSuccess) return succList; | |||
if (type === SyncType.SyncError) return failList; | |||
@@ -72,7 +80,11 @@ export default function SyncView() { | |||
> | |||
<img className={styles.icon} src={icon} alt="" /> | |||
<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> | |||
@@ -1,3 +1,4 @@ | |||
import { Dayjs } from 'dayjs'; | |||
import { TaskStatus, TaskType } from './API.helper'; | |||
declare namespace API { | |||
@@ -74,6 +75,10 @@ declare namespace DATA { | |||
taskSyncProgress: 100; | |||
taskSyncStatus: TaskStatus; // "TASK_SYNC_STATUS_FINISH", | |||
taskType: TaskType; // "DOWNLOAD", | |||
taskCreateUnixTime: number; // 秒级时间 | |||
taskCreateDate: Dayjs; // 前端根据taskCreateUnixTime生成的时间对象 | |||
taskCreateDateStr: string; // 前端根据taskCreateUnixTime生成的时间字符串 供给消息窗口使用, 格式: YY/MM/DD HH:mm:ss | |||
absolutePath: string; // 绝对路径 | |||
version: number; // 1, | |||
workStatus: number; // 1 | |||
} | |||
@@ -1,8 +1,11 @@ | |||
import { firstCharToLowerCase, Subject } from '@/utils/tool'; | |||
import dayjs from 'dayjs'; | |||
import { useEffect, useState } from 'react'; | |||
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 { | |||
public list: DATA.SocketFileMsg[] = []; | |||
@@ -10,6 +13,7 @@ class FileMessageManager extends Subject { | |||
public fileMap = new Map(); | |||
addFileMsg(message: DATA.SocketFileMsg) { | |||
this.fileMap.set(message.taskId, message); | |||
this.list = [message].concat(this.list); | |||
this.notifyObservers(this.list); | |||
} | |||
@@ -25,12 +29,21 @@ class FileMessageManager extends Subject { | |||
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); | |||
} | |||
@@ -39,21 +52,42 @@ class FileMessageManager extends Subject { | |||
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() { | |||
const [list, setList] = useState(fileMng.list); | |||
// console.log(list); | |||
useEffect(() => { | |||
fileMng.addObserver(setList); | |||
return () => { | |||
@@ -1,4 +1,6 @@ | |||
import { fetchLocalApi } from '@/utils/request'; | |||
import { DATA } from './API'; | |||
import { sendSocketMessage } from './socket'; | |||
export const isClient = !!window.ipcRenderer; // process.env.IS_CLIENT; | |||
@@ -9,13 +11,13 @@ const ipcRenderer = window.ipcRenderer; | |||
const system = { | |||
closeWindow() { | |||
ipcRenderer.invoke('manipulate-window', { action: 'close' }); | |||
ipcRenderer.invoke('window:close'); | |||
}, | |||
zoomWindow() { | |||
ipcRenderer.invoke('manipulate-window', { action: 'zoom' }); | |||
ipcRenderer.invoke('window:zoom'); | |||
}, | |||
minimizeWindow() { | |||
ipcRenderer.invoke('manipulate-window', { action: 'minimize' }); | |||
ipcRenderer.invoke('window:minimize'); | |||
}, | |||
openUrl(url: string) { | |||
if (!isClient) { | |||
@@ -24,6 +26,13 @@ const system = { | |||
} | |||
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: { | |||
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>( | |||
'login', | |||
{ userId, userPhone }, | |||
{ silent: true }, | |||
); | |||
ipcRenderer.invoke('socket:send-message', `login:${userPhone}`); | |||
sendSocketMessage(`login:${userPhone}`); | |||
return res; | |||
}), | |||
}, | |||
// 退出登陆 | |||
logout: safeCall(async () => { | |||
logout: async () => { | |||
return await fetchLocalApi<null>('logout'); | |||
}), | |||
}, | |||
// 发送下载同步文件夹的指令 | |||
syncProjects: safeCall((projectIds: string[]) => { | |||
syncProjects: (projectIds: string[]) => { | |||
return fetchLocalApi<null>('syncFolderToWorkSpace', { | |||
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; |
@@ -34,11 +34,9 @@ export async function login(account: string, password: string) { | |||
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 }); | |||
@@ -1,10 +1,11 @@ | |||
import { isClient } from '@/services/system'; | |||
import { message } from 'antd'; | |||
import { request } from 'umi'; | |||
import { parseRequest } from './request.config'; | |||
import { errorReponse, firstCharToLowerCase, handleRequest } from './tool'; | |||
const remoteUrl = window.systemConfig?.remoteUrl || ''; | |||
const gatewayPort = window.systemConfig?.gatewayPort || 0; | |||
const gatewayPort = window.systemConfig?.gatewayPort || 7888; | |||
export async function fetchApi<T = any>( | |||
path: string, | |||
@@ -41,7 +42,7 @@ export async function fetchLocalApi<T = any>( | |||
} | |||
const { silent, method = 'GET', ...restOptions } = options; | |||
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 }, | |||
); | |||
if (!silent) { | |||