Rebuild UI for master-slave file distribution
This commit is contained in:
84
Makefile
Normal file
84
Makefile
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
APP_NAME := sftp-manager
|
||||||
|
PORT := 48081
|
||||||
|
BASE_PATH := /sftp-manager
|
||||||
|
APP_URL := http://localhost:$(PORT)$(BASE_PATH)
|
||||||
|
|
||||||
|
MVN := mvn
|
||||||
|
DOCKER_COMPOSE := docker compose
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
.PHONY: help check-mvn check-docker install build test start stop restart status logs deploy bootstrap clean jar-start jar-stop jar-restart
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " make bootstrap - One command install + deploy + start (Docker)"
|
||||||
|
@echo " make deploy - Build app and start with Docker Compose"
|
||||||
|
@echo " make install - Maven install (skip tests)"
|
||||||
|
@echo " make build - Maven package (skip tests)"
|
||||||
|
@echo " make test - Run all tests"
|
||||||
|
@echo " make start - Start service by Docker Compose"
|
||||||
|
@echo " make stop - Stop service by Docker Compose"
|
||||||
|
@echo " make restart - Restart Docker service"
|
||||||
|
@echo " make status - Show Docker service status"
|
||||||
|
@echo " make logs - Tail service logs"
|
||||||
|
@echo " make jar-start - Start packaged jar using deploy.sh"
|
||||||
|
@echo " make jar-stop - Stop packaged jar using deploy.sh"
|
||||||
|
@echo " make jar-restart - Restart packaged jar using deploy.sh"
|
||||||
|
|
||||||
|
check-mvn:
|
||||||
|
@command -v $(MVN) >/dev/null 2>&1 || { echo "Error: mvn not found"; exit 1; }
|
||||||
|
|
||||||
|
check-docker:
|
||||||
|
@command -v docker >/dev/null 2>&1 || { echo "Error: docker not found"; exit 1; }
|
||||||
|
@$(DOCKER_COMPOSE) version >/dev/null 2>&1 || { echo "Error: docker compose not available"; exit 1; }
|
||||||
|
|
||||||
|
install: check-mvn
|
||||||
|
@echo ">>> Installing project artifacts (skip tests)..."
|
||||||
|
@$(MVN) clean install -DskipTests
|
||||||
|
|
||||||
|
build: check-mvn
|
||||||
|
@echo ">>> Packaging application (skip tests)..."
|
||||||
|
@$(MVN) clean package -DskipTests
|
||||||
|
|
||||||
|
test: check-mvn
|
||||||
|
@echo ">>> Running tests..."
|
||||||
|
@$(MVN) clean test
|
||||||
|
|
||||||
|
start: check-docker
|
||||||
|
@echo ">>> Starting $(APP_NAME) with Docker Compose..."
|
||||||
|
@$(DOCKER_COMPOSE) up -d --build
|
||||||
|
@echo ">>> Started. Visit: $(APP_URL)"
|
||||||
|
|
||||||
|
stop: check-docker
|
||||||
|
@echo ">>> Stopping $(APP_NAME)..."
|
||||||
|
@$(DOCKER_COMPOSE) down
|
||||||
|
|
||||||
|
restart: stop start
|
||||||
|
|
||||||
|
status: check-docker
|
||||||
|
@$(DOCKER_COMPOSE) ps
|
||||||
|
|
||||||
|
logs: check-docker
|
||||||
|
@$(DOCKER_COMPOSE) logs -f --tail=200 $(APP_NAME)
|
||||||
|
|
||||||
|
deploy: build start
|
||||||
|
|
||||||
|
bootstrap: install start
|
||||||
|
|
||||||
|
clean: check-mvn
|
||||||
|
@$(MVN) clean
|
||||||
|
|
||||||
|
jar-start: build
|
||||||
|
@chmod +x ./deploy.sh
|
||||||
|
@./deploy.sh start
|
||||||
|
|
||||||
|
jar-stop:
|
||||||
|
@chmod +x ./deploy.sh
|
||||||
|
@./deploy.sh stop
|
||||||
|
|
||||||
|
jar-restart: build
|
||||||
|
@chmod +x ./deploy.sh
|
||||||
|
@./deploy.sh restart
|
||||||
@@ -8,8 +8,8 @@ services:
|
|||||||
- "48081:48081"
|
- "48081:48081"
|
||||||
volumes:
|
volumes:
|
||||||
# 默认映射到用户家目录(Linux/Mac 为 $HOME,未设置时为当前目录)
|
# 默认映射到用户家目录(Linux/Mac 为 $HOME,未设置时为当前目录)
|
||||||
- ${HOME:-.}/sftp-manager/data:/app/data
|
- ./docker/data:/app/data
|
||||||
- ${HOME:-.}/sftp-manager/logs:/app/logs
|
- ./docker/logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- SPRING_PROFILES_ACTIVE=prod
|
- SPRING_PROFILES_ACTIVE=prod
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,116 +4,153 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SFTP文件管理器</title>
|
<title>SFTP文件管理器</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-shell">
|
||||||
<!-- 顶部导航栏 -->
|
<header class="topbar" role="banner">
|
||||||
<nav class="navbar navbar-dark bg-dark">
|
<div class="topbar-left">
|
||||||
<div class="container-fluid">
|
<h1 class="brand-title">SFTP Manager</h1>
|
||||||
<span class="navbar-brand mb-0 h1">SFTP文件管理器</span>
|
<p class="brand-subtitle">主从文件分发工作台</p>
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary btn-sm" onclick="showConnectionDialog()">连接管理</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
<div class="topbar-actions">
|
||||||
|
<button type="button" class="btn btn-light btn-sm" onclick="showConnectionDialog()">连接管理</button>
|
||||||
<!-- 页面头部信息 -->
|
<button type="button" class="btn btn-outline-light btn-sm" onclick="refreshPanels()">刷新全部</button>
|
||||||
<header class="app-header">
|
|
||||||
<div class="app-header-main">
|
|
||||||
<h1 class="app-title">文件工作台</h1>
|
|
||||||
<p class="app-subtitle">双面板本地 / SFTP 文件管理器,支持拖拽、键盘快捷键与批量操作。</p>
|
|
||||||
</div>
|
|
||||||
<div class="app-header-tags">
|
|
||||||
<span class="header-tag">双面板布局</span>
|
|
||||||
<span class="header-tag">SFTP / 本地</span>
|
|
||||||
<span class="header-tag">拖拽上传</span>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 工具栏 -->
|
<main class="workspace" role="main">
|
||||||
<div class="toolbar bg-light border-bottom">
|
<section class="summary-strip" aria-label="传输策略说明">
|
||||||
<div class="btn-group" role="group">
|
<div class="summary-item">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="uploadFiles()">上传</button>
|
<span class="summary-k">主端</span>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="downloadFiles()">下载</button>
|
<span class="summary-v" id="master-summary">左侧:本机 / 单台远端</span>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="transferToRight()">传输到右侧</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="transferToLeft()">传输到左侧</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFiles()">删除</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="showRenameDialog()">重命名</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="showMkdirDialog()">新建文件夹</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-show-hidden" onclick="toggleShowHidden()" title="切换是否显示隐藏文件">显示隐藏文件</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshPanels()">刷新</button>
|
|
||||||
</div>
|
|
||||||
<!-- 文件上传输入框(隐藏) -->
|
|
||||||
<input type="file" id="file-input" multiple style="display:none">
|
|
||||||
<!-- 传输进度(上传/下载/跨面板传输) -->
|
|
||||||
<div class="transfer-progress" id="transfer-progress" style="display:none;">
|
|
||||||
<span class="transfer-progress-label" id="transfer-progress-label"></span>
|
|
||||||
<div class="progress transfer-progress-bar" style="height: 20px; margin-left: 10px; width: 220px;">
|
|
||||||
<div class="progress-bar" id="transfer-progress-bar" role="progressbar" style="width: 0%">0%</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="summary-item">
|
||||||
</div>
|
<span class="summary-k">从端</span>
|
||||||
|
<span class="summary-v" id="slave-summary">右侧:单台或多台远端</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item summary-item-wide">
|
||||||
|
<span class="summary-k">策略</span>
|
||||||
|
<span class="summary-v">主向从发送(支持单发/多发),右侧可同时选择多目标进行分发</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 双面板区域 -->
|
<section class="ops-toolbar" aria-label="操作区">
|
||||||
<div class="panels-container">
|
<div class="ops-group">
|
||||||
<!-- 左面板 -->
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="uploadFiles()">上传到主端</button>
|
||||||
<div class="panel" id="left-panel">
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="downloadFiles()">从主端下载</button>
|
||||||
<div class="panel-header">
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteFiles()">删除</button>
|
||||||
<select class="form-select form-select-sm panel-mode" id="left-mode" onchange="onModeChange('left')">
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="showRenameDialog()">重命名</button>
|
||||||
<option value="local">本地文件</option>
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="showMkdirDialog()">新建文件夹</button>
|
||||||
<option value="sftp">SFTP服务器</option>
|
</div>
|
||||||
</select>
|
<div class="ops-group ops-group-right">
|
||||||
<div class="connection-status" id="left-status" style="display:none;">
|
<button type="button" class="btn btn-primary btn-sm" id="btn-transfer-master" onclick="transferMasterToSlaves()">主 -> 从发送</button>
|
||||||
<span class="status-dot" data-status="disconnected"></span>
|
<button type="button" class="btn btn-outline-warning btn-sm" id="btn-retry-failed" onclick="retryFailedDispatch()" style="display:none;">重试失败目标</button>
|
||||||
<span class="status-text">未连接</span>
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn-show-hidden" onclick="toggleShowHidden()" title="切换隐藏文件">显示隐藏文件</button>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="file-input" multiple style="display:none">
|
||||||
|
<div class="transfer-progress" id="transfer-progress" style="display:none;" aria-live="polite">
|
||||||
|
<span class="transfer-progress-label" id="transfer-progress-label"></span>
|
||||||
|
<div class="progress transfer-progress-bar" style="height: 20px; width: 260px;">
|
||||||
|
<div class="progress-bar" id="transfer-progress-bar" role="progressbar" style="width: 0%">0%</div>
|
||||||
</div>
|
</div>
|
||||||
<select class="form-select form-select-sm connection-select" id="left-connection" style="display:none;" onchange="onConnectionChange('left')">
|
|
||||||
<option value="">选择连接</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="path-bar">
|
</section>
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="goUp('left')">↑</button>
|
|
||||||
<input type="text" class="form-control form-control-sm path-input" id="left-path" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="file-list" id="left-file-list">
|
|
||||||
<div class="text-center text-muted p-3">加载中...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右面板 -->
|
<section class="panes-grid" aria-label="主从面板">
|
||||||
<div class="panel" id="right-panel">
|
<article class="pane panel pane-master" id="left-panel" aria-label="主端面板">
|
||||||
<div class="panel-header">
|
<header class="pane-head">
|
||||||
<select class="form-select form-select-sm panel-mode" id="right-mode" onchange="onModeChange('right')">
|
<div class="pane-head-title">
|
||||||
<option value="local">本地文件</option>
|
<span class="pane-badge pane-badge-master">主端</span>
|
||||||
<option value="sftp">SFTP服务器</option>
|
<span class="pane-head-text">发送源</span>
|
||||||
</select>
|
</div>
|
||||||
<div class="connection-status" id="right-status" style="display:none;">
|
<div class="pane-head-controls">
|
||||||
<span class="status-dot" data-status="disconnected"></span>
|
<label class="visually-hidden" for="left-mode">主端模式</label>
|
||||||
<span class="status-text">未连接</span>
|
<select class="form-select form-select-sm panel-mode" id="left-mode" onchange="onModeChange('left')">
|
||||||
|
<option value="local">本地文件</option>
|
||||||
|
<option value="sftp">SFTP服务器</option>
|
||||||
|
</select>
|
||||||
|
<div class="connection-status" id="left-status" style="display:none;">
|
||||||
|
<span class="status-dot" data-status="disconnected"></span>
|
||||||
|
<span class="status-text">未连接</span>
|
||||||
|
</div>
|
||||||
|
<label class="visually-hidden" for="left-connection">主端连接</label>
|
||||||
|
<select class="form-select form-select-sm connection-select" id="left-connection" style="display:none;" onchange="onConnectionChange('left')">
|
||||||
|
<option value="">选择连接</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="path-bar">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" onclick="goUp('left')" aria-label="主端返回上级">↑</button>
|
||||||
|
<input type="text" class="form-control form-control-sm path-input" id="left-path" readonly>
|
||||||
</div>
|
</div>
|
||||||
<select class="form-select form-select-sm connection-select" id="right-connection" style="display:none;" onchange="onConnectionChange('right')">
|
<div class="file-list" id="left-file-list">
|
||||||
<option value="">选择连接</option>
|
<div class="text-center text-muted p-3">加载中...</div>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
<div class="path-bar">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="goUp('right')">↑</button>
|
|
||||||
<input type="text" class="form-control form-control-sm path-input" id="right-path" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="file-list" id="right-file-list">
|
|
||||||
<div class="text-center text-muted p-3">加载中...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 状态栏 -->
|
<article class="pane panel pane-slave" id="right-panel" aria-label="从端面板">
|
||||||
<div class="status-bar bg-light border-top">
|
<header class="pane-head">
|
||||||
|
<div class="pane-head-title">
|
||||||
|
<span class="pane-badge pane-badge-slave">从端</span>
|
||||||
|
<span class="pane-head-text">接收目标(单台或多台)</span>
|
||||||
|
</div>
|
||||||
|
<div class="pane-head-controls">
|
||||||
|
<label class="visually-hidden" for="right-mode">从端模式</label>
|
||||||
|
<select class="form-select form-select-sm panel-mode" id="right-mode" onchange="onModeChange('right')">
|
||||||
|
<option value="local">本地文件</option>
|
||||||
|
<option value="sftp">SFTP服务器</option>
|
||||||
|
</select>
|
||||||
|
<div class="connection-status" id="right-status" style="display:none;">
|
||||||
|
<span class="status-dot" data-status="disconnected"></span>
|
||||||
|
<span class="status-text">未连接</span>
|
||||||
|
</div>
|
||||||
|
<label class="visually-hidden" for="right-connection">从端连接</label>
|
||||||
|
<select class="form-select form-select-sm connection-select" id="right-connection" style="display:none;" onchange="onConnectionChange('right')">
|
||||||
|
<option value="">选择连接</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="slave-targets" aria-label="从端目标选择">
|
||||||
|
<div class="slave-targets-head">
|
||||||
|
<span>分发目标</span>
|
||||||
|
<div class="slave-targets-actions">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-xs" onclick="selectAllSlaveTargets(true)">全选</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-xs" onclick="selectAllSlaveTargets(false)">清空</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="slave-target-search"
|
||||||
|
class="form-control form-control-sm slave-target-search"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索从端目标(名称 / 主机 / 用户)"
|
||||||
|
oninput="filterSlaveTargets(this.value)">
|
||||||
|
<div id="slave-target-list" class="slave-target-list">
|
||||||
|
<div class="text-muted">暂无可用目标,请先在连接管理中连接服务器。</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="path-bar">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" onclick="goUp('right')" aria-label="从端返回上级">↑</button>
|
||||||
|
<input type="text" class="form-control form-control-sm path-input" id="right-path" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="file-list" id="right-file-list">
|
||||||
|
<div class="text-center text-muted p-3">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="status-bar">
|
||||||
<span id="status-text" aria-live="polite">就绪</span>
|
<span id="status-text" aria-live="polite">就绪</span>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 连接管理对话框 -->
|
|
||||||
<div class="modal fade" id="connectionModal" tabindex="-1">
|
<div class="modal fade" id="connectionModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -125,9 +162,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button class="btn btn-primary" onclick="showAddConnectionDialog()">添加连接</button>
|
<button class="btn btn-primary" onclick="showAddConnectionDialog()">添加连接</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="connection-list" id="connection-list">
|
<div class="connection-list" id="connection-list"></div>
|
||||||
<!-- 连接列表动态生成 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||||
@@ -136,7 +171,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加连接对话框 -->
|
|
||||||
<div class="modal fade" id="addConnectionModal" tabindex="-1">
|
<div class="modal fade" id="addConnectionModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ let activePanelId = 'left';
|
|||||||
// 是否展示隐藏文件(默认不展示)
|
// 是否展示隐藏文件(默认不展示)
|
||||||
let showHiddenFiles = false;
|
let showHiddenFiles = false;
|
||||||
|
|
||||||
|
// 从端目标会话(支持多选)
|
||||||
|
let selectedSlaveSessions = new Set(['local']);
|
||||||
|
|
||||||
|
// 从端目标最近一次分发状态
|
||||||
|
let slaveDeliveryStatus = {};
|
||||||
|
|
||||||
|
// 最近一次分发上下文(用于失败重试)
|
||||||
|
let lastDispatchContext = null;
|
||||||
|
let lastFailedSlaveSessions = [];
|
||||||
|
let dispatchRunning = false;
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initApp();
|
initApp();
|
||||||
@@ -42,6 +53,10 @@ function initApp() {
|
|||||||
// 加载活跃连接
|
// 加载活跃连接
|
||||||
loadActiveConnections();
|
loadActiveConnections();
|
||||||
|
|
||||||
|
// 初始化从端多目标
|
||||||
|
renderSlaveTargetList();
|
||||||
|
refreshTopologySummary();
|
||||||
|
|
||||||
// 绑定键盘事件
|
// 绑定键盘事件
|
||||||
bindKeyboardEvents();
|
bindKeyboardEvents();
|
||||||
|
|
||||||
@@ -54,6 +69,22 @@ function initApp() {
|
|||||||
// 面板点击切换活动面板
|
// 面板点击切换活动面板
|
||||||
$('#left-panel').on('click', function() { setActivePanel('left'); });
|
$('#left-panel').on('click', function() { setActivePanel('left'); });
|
||||||
$('#right-panel').on('click', function() { setActivePanel('right'); });
|
$('#right-panel').on('click', function() { setActivePanel('right'); });
|
||||||
|
|
||||||
|
// 从端多目标勾选
|
||||||
|
$(document).on('change', '.slave-target-check', function() {
|
||||||
|
const sessionId = $(this).data('session-id');
|
||||||
|
if (!sessionId) return;
|
||||||
|
if (this.checked) {
|
||||||
|
selectedSlaveSessions.add(sessionId);
|
||||||
|
} else {
|
||||||
|
selectedSlaveSessions.delete(sessionId);
|
||||||
|
}
|
||||||
|
if (selectedSlaveSessions.size === 0) {
|
||||||
|
selectedSlaveSessions.add(panelState.right.sessionId || 'local');
|
||||||
|
renderSlaveTargetList();
|
||||||
|
}
|
||||||
|
refreshTopologySummary();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前活动面板并应用高亮样式
|
// 设置当前活动面板并应用高亮样式
|
||||||
@@ -61,6 +92,195 @@ function setActivePanel(panelId) {
|
|||||||
activePanelId = panelId;
|
activePanelId = panelId;
|
||||||
$('#left-panel, #right-panel').removeClass('active-panel');
|
$('#left-panel, #right-panel').removeClass('active-panel');
|
||||||
$('#' + panelId + '-panel').addClass('active-panel');
|
$('#' + panelId + '-panel').addClass('active-panel');
|
||||||
|
refreshTopologySummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionDisplayName(sessionId) {
|
||||||
|
if (!sessionId || sessionId === 'local') {
|
||||||
|
return '本机(local)';
|
||||||
|
}
|
||||||
|
const conn = activeConnections[sessionId];
|
||||||
|
if (!conn) {
|
||||||
|
return '会话 ' + sessionId.substring(0, 8);
|
||||||
|
}
|
||||||
|
const base = conn.name || conn.host || '远端';
|
||||||
|
return base + ':' + (conn.port || 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStatusTime(ts) {
|
||||||
|
if (!ts) return '';
|
||||||
|
const d = new Date(ts);
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
return hh + ':' + mm + ':' + ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDeliveryStatus(sessionId, state, message) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
slaveDeliveryStatus[sessionId] = {
|
||||||
|
state: state,
|
||||||
|
message: message || '',
|
||||||
|
time: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeliveryStatusText(meta) {
|
||||||
|
if (!meta) return '未分发';
|
||||||
|
const timeText = formatStatusTime(meta.time);
|
||||||
|
if (meta.state === 'success') {
|
||||||
|
return '成功 ' + timeText;
|
||||||
|
}
|
||||||
|
if (meta.state === 'failed') {
|
||||||
|
return '失败 ' + timeText;
|
||||||
|
}
|
||||||
|
if (meta.state === 'running') {
|
||||||
|
return '发送中...';
|
||||||
|
}
|
||||||
|
return '未分发';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRetryFailedButton() {
|
||||||
|
const retryBtn = $('#btn-retry-failed');
|
||||||
|
if (retryBtn.length === 0) return;
|
||||||
|
if (lastFailedSlaveSessions.length > 0 && !dispatchRunning) {
|
||||||
|
retryBtn.show();
|
||||||
|
retryBtn.text('重试失败目标(' + lastFailedSlaveSessions.length + ')');
|
||||||
|
retryBtn.prop('disabled', false);
|
||||||
|
} else {
|
||||||
|
retryBtn.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlaveTargetSessions() {
|
||||||
|
const targets = [];
|
||||||
|
selectedSlaveSessions.forEach(function(sessionId) {
|
||||||
|
if (sessionId) {
|
||||||
|
targets.push(sessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (targets.length === 0) {
|
||||||
|
const fallback = panelState.right.mode === 'sftp'
|
||||||
|
? (panelState.right.sessionId && panelState.right.sessionId !== 'local' ? panelState.right.sessionId : null)
|
||||||
|
: 'local';
|
||||||
|
if (fallback) {
|
||||||
|
targets.push(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSlaveTargetList() {
|
||||||
|
const box = $('#slave-target-list');
|
||||||
|
if (box.length === 0) return;
|
||||||
|
|
||||||
|
const sessions = [];
|
||||||
|
if (panelState.right.mode === 'local') {
|
||||||
|
sessions.push({
|
||||||
|
sessionId: 'local',
|
||||||
|
label: '本机(local)',
|
||||||
|
info: '127.0.0.1'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(activeConnections || {}).forEach(function(sessionId) {
|
||||||
|
const conn = activeConnections[sessionId];
|
||||||
|
if (!conn) return;
|
||||||
|
sessions.push({
|
||||||
|
sessionId: sessionId,
|
||||||
|
label: conn.name || conn.host || ('会话 ' + sessionId.substring(0, 8)),
|
||||||
|
info: (conn.username || 'user') + '@' + (conn.host || 'host') + ':' + (conn.port || 22)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const validSet = new Set(sessions.map(function(item) { return item.sessionId; }));
|
||||||
|
const normalizedSelected = new Set();
|
||||||
|
selectedSlaveSessions.forEach(function(sessionId) {
|
||||||
|
if (validSet.has(sessionId)) {
|
||||||
|
normalizedSelected.add(sessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (normalizedSelected.size === 0 && sessions.length > 0) {
|
||||||
|
normalizedSelected.add(sessions[0].sessionId);
|
||||||
|
}
|
||||||
|
selectedSlaveSessions = normalizedSelected;
|
||||||
|
|
||||||
|
if (sessions.length === 0) {
|
||||||
|
box.html('<div class="text-muted">当前为从端SFTP模式,但暂无可用连接,请先在连接管理中连接服务器。</div>');
|
||||||
|
refreshTopologySummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
sessions.forEach(function(item) {
|
||||||
|
const checked = selectedSlaveSessions.has(item.sessionId) ? 'checked' : '';
|
||||||
|
const searchable = (item.label + ' ' + item.info).toLowerCase();
|
||||||
|
const statusMeta = slaveDeliveryStatus[item.sessionId];
|
||||||
|
const statusClass = statusMeta ? (' slave-target-status-' + statusMeta.state) : '';
|
||||||
|
const statusTitle = statusMeta && statusMeta.message ? $('<div>').text(statusMeta.message).html() : '';
|
||||||
|
const statusText = getDeliveryStatusText(statusMeta);
|
||||||
|
html += '<label class="slave-target-item" data-search-key="' + $('<div>').text(searchable).html() + '" title="' + $('<div>').text(item.info).html() + '">' +
|
||||||
|
'<input class="form-check-input slave-target-check" type="checkbox" data-session-id="' + item.sessionId + '" ' + checked + '>' +
|
||||||
|
'<span class="slave-target-main">' + $('<div>').text(item.label).html() + '</span>' +
|
||||||
|
'<span class="slave-target-sub">' + $('<div>').text(item.info).html() + '</span>' +
|
||||||
|
'<span class="slave-target-status' + statusClass + '" title="' + statusTitle + '">' + $('<div>').text(statusText).html() + '</span>' +
|
||||||
|
'</label>';
|
||||||
|
});
|
||||||
|
|
||||||
|
box.html(html || '<div class="text-muted">暂无目标。</div>');
|
||||||
|
filterSlaveTargets($('#slave-target-search').val() || '');
|
||||||
|
refreshTopologySummary();
|
||||||
|
updateRetryFailedButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSlaveTargets(keyword) {
|
||||||
|
const value = (keyword || '').trim().toLowerCase();
|
||||||
|
const list = $('#slave-target-list .slave-target-item');
|
||||||
|
if (list.length === 0) return;
|
||||||
|
|
||||||
|
list.each(function() {
|
||||||
|
const key = ($(this).attr('data-search-key') || '').toLowerCase();
|
||||||
|
const hit = value === '' || key.indexOf(value) !== -1;
|
||||||
|
$(this).toggleClass('is-hidden', !hit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTopologySummary() {
|
||||||
|
const masterEl = $('#master-summary');
|
||||||
|
const slaveEl = $('#slave-summary');
|
||||||
|
if (masterEl.length === 0 || slaveEl.length === 0) return;
|
||||||
|
|
||||||
|
masterEl.text('左侧主端:' + getSessionDisplayName(panelState.left.sessionId));
|
||||||
|
|
||||||
|
const slaves = getSlaveTargetSessions().map(getSessionDisplayName);
|
||||||
|
if (slaves.length === 0) {
|
||||||
|
slaveEl.text('右侧从端目标(0):请先连接从端目标');
|
||||||
|
} else {
|
||||||
|
slaveEl.text('右侧从端目标(' + slaves.length + '):' + slaves.join(' / '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllSlaveTargets(checked) {
|
||||||
|
const mode = panelState.right.mode;
|
||||||
|
if (checked) {
|
||||||
|
selectedSlaveSessions.clear();
|
||||||
|
if (mode === 'local') {
|
||||||
|
selectedSlaveSessions.add('local');
|
||||||
|
} else {
|
||||||
|
Object.keys(activeConnections || {}).forEach(function(sessionId) {
|
||||||
|
selectedSlaveSessions.add(sessionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedSlaveSessions.clear();
|
||||||
|
if (mode === 'local') {
|
||||||
|
selectedSlaveSessions.add('local');
|
||||||
|
} else if (panelState.right.sessionId && panelState.right.sessionId !== 'local') {
|
||||||
|
selectedSlaveSessions.add(panelState.right.sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderSlaveTargetList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化面板路径
|
// 初始化面板路径
|
||||||
@@ -418,6 +638,9 @@ function handleFileDrop(data, targetPanelId) {
|
|||||||
// 更新路径输入框
|
// 更新路径输入框
|
||||||
function updatePathInput(panelId, path) {
|
function updatePathInput(panelId, path) {
|
||||||
$(`#${panelId}-path`).val(path || '');
|
$(`#${panelId}-path`).val(path || '');
|
||||||
|
if (panelId === 'right') {
|
||||||
|
refreshTopologySummary();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新状态栏
|
// 更新状态栏
|
||||||
@@ -456,6 +679,17 @@ function onModeChange(panelId) {
|
|||||||
updateStatus('请选择SFTP连接');
|
updateStatus('请选择SFTP连接');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panelId === 'right') {
|
||||||
|
selectedSlaveSessions.clear();
|
||||||
|
if (panelState.right.mode === 'local') {
|
||||||
|
selectedSlaveSessions.add('local');
|
||||||
|
} else if (panelState.right.sessionId && panelState.right.sessionId !== 'local') {
|
||||||
|
selectedSlaveSessions.add(panelState.right.sessionId);
|
||||||
|
}
|
||||||
|
renderSlaveTargetList();
|
||||||
|
}
|
||||||
|
refreshTopologySummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接切换
|
// 连接切换
|
||||||
@@ -469,6 +703,17 @@ function onConnectionChange(panelId) {
|
|||||||
$(`#${panelId}-file-list`).html('<div class="text-center text-muted p-3">请选择SFTP连接</div>');
|
$(`#${panelId}-file-list`).html('<div class="text-center text-muted p-3">请选择SFTP连接</div>');
|
||||||
showConnectionStatus(panelId, null);
|
showConnectionStatus(panelId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panelId === 'right') {
|
||||||
|
selectedSlaveSessions.clear();
|
||||||
|
if (panelState.right.mode === 'local') {
|
||||||
|
selectedSlaveSessions.add('local');
|
||||||
|
} else if (panelState.right.sessionId && panelState.right.sessionId !== 'local') {
|
||||||
|
selectedSlaveSessions.add(panelState.right.sessionId);
|
||||||
|
}
|
||||||
|
renderSlaveTargetList();
|
||||||
|
}
|
||||||
|
refreshTopologySummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新连接状态显示(状态:connected / disconnected / connecting)
|
// 更新连接状态显示(状态:connected / disconnected / connecting)
|
||||||
@@ -514,6 +759,7 @@ function loadActiveConnections() {
|
|||||||
activeConnections = response.data;
|
activeConnections = response.data;
|
||||||
updateConnectionSelect('left');
|
updateConnectionSelect('left');
|
||||||
updateConnectionSelect('right');
|
updateConnectionSelect('right');
|
||||||
|
renderSlaveTargetList();
|
||||||
['left', 'right'].forEach(function(panelId) {
|
['left', 'right'].forEach(function(panelId) {
|
||||||
if (panelState[panelId].mode === 'sftp') {
|
if (panelState[panelId].mode === 'sftp') {
|
||||||
showConnectionStatus(panelId, panelState[panelId].sessionId);
|
showConnectionStatus(panelId, panelState[panelId].sessionId);
|
||||||
@@ -570,6 +816,7 @@ function refreshPanels() {
|
|||||||
activeConnections = response.data;
|
activeConnections = response.data;
|
||||||
updateConnectionSelect('left');
|
updateConnectionSelect('left');
|
||||||
updateConnectionSelect('right');
|
updateConnectionSelect('right');
|
||||||
|
renderSlaveTargetList();
|
||||||
['left', 'right'].forEach(function(panelId) {
|
['left', 'right'].forEach(function(panelId) {
|
||||||
if (panelState[panelId].mode === 'sftp') {
|
if (panelState[panelId].mode === 'sftp') {
|
||||||
showConnectionStatus(panelId, panelState[panelId].sessionId);
|
showConnectionStatus(panelId, panelState[panelId].sessionId);
|
||||||
@@ -737,6 +984,7 @@ function deleteConnection(id) {
|
|||||||
loadActiveConnections();
|
loadActiveConnections();
|
||||||
updateConnectionSelect('left');
|
updateConnectionSelect('left');
|
||||||
updateConnectionSelect('right');
|
updateConnectionSelect('right');
|
||||||
|
renderSlaveTargetList();
|
||||||
} else {
|
} else {
|
||||||
alert('删除失败: ' + (response.message || '未知错误'));
|
alert('删除失败: ' + (response.message || '未知错误'));
|
||||||
}
|
}
|
||||||
@@ -761,6 +1009,8 @@ function disconnectFromSftp(sessionId) {
|
|||||||
updatePanelsAfterDisconnect(sessionId);
|
updatePanelsAfterDisconnect(sessionId);
|
||||||
updateConnectionSelect('left');
|
updateConnectionSelect('left');
|
||||||
updateConnectionSelect('right');
|
updateConnectionSelect('right');
|
||||||
|
selectedSlaveSessions.delete(sessionId);
|
||||||
|
renderSlaveTargetList();
|
||||||
['left', 'right'].forEach(function(panelId) {
|
['left', 'right'].forEach(function(panelId) {
|
||||||
showConnectionStatus(panelId, panelState[panelId].sessionId);
|
showConnectionStatus(panelId, panelState[panelId].sessionId);
|
||||||
});
|
});
|
||||||
@@ -789,6 +1039,8 @@ function updatePanelsAfterDisconnect(sessionId) {
|
|||||||
initPanelPath(panelId);
|
initPanelPath(panelId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
renderSlaveTargetList();
|
||||||
|
refreshTopologySummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接成功后只更新当前活动面板为新连接,另一侧保持原选择,便于左右选不同 SFTP
|
// 连接成功后只更新当前活动面板为新连接,另一侧保持原选择,便于左右选不同 SFTP
|
||||||
@@ -870,6 +1122,10 @@ function connectToServer(connId) {
|
|||||||
activeConnections[sessionId] = conn;
|
activeConnections[sessionId] = conn;
|
||||||
updateConnectionSelect('left');
|
updateConnectionSelect('left');
|
||||||
updateConnectionSelect('right');
|
updateConnectionSelect('right');
|
||||||
|
if (panelState.right.mode === 'sftp' && panelState.right.sessionId === sessionId) {
|
||||||
|
selectedSlaveSessions.add(sessionId);
|
||||||
|
}
|
||||||
|
renderSlaveTargetList();
|
||||||
updatePanelStateWithConnection(sessionId, conn);
|
updatePanelStateWithConnection(sessionId, conn);
|
||||||
loadConnectionList();
|
loadConnectionList();
|
||||||
if (typeof bootstrap !== 'undefined') {
|
if (typeof bootstrap !== 'undefined') {
|
||||||
@@ -1198,6 +1454,213 @@ function transferToLeft() {
|
|||||||
doTransfer('right', 'left');
|
doTransfer('right', 'left');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主端 -> 多从端分发
|
||||||
|
function transferMasterToSlaves() {
|
||||||
|
const transferBtn = $('#btn-transfer-master');
|
||||||
|
const sourceSessionId = panelState.left.sessionId;
|
||||||
|
const sourceSelected = panelState.left.selectedFiles || [];
|
||||||
|
if (sourceSelected.length === 0) {
|
||||||
|
alert('请先在左侧主端选择要发送的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourcePaths = sourceSelected
|
||||||
|
.filter(function(item) { return item && item.path; })
|
||||||
|
.map(function(item) { return item.path; });
|
||||||
|
|
||||||
|
if (sourcePaths.length === 0) {
|
||||||
|
alert('未找到有效源路径');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = panelState.right.currentPath;
|
||||||
|
if (!targetPath) {
|
||||||
|
alert('请先在右侧选择目标目录路径');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSessions = getSlaveTargetSessions();
|
||||||
|
if (targetSessions.length === 0) {
|
||||||
|
alert('请至少选择一个从端目标');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceSessionId === 'local' && targetSessions.length === 1 && targetSessions[0] === 'local') {
|
||||||
|
alert('主端与从端均为本机,无需执行分发');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDirectory = sourceSelected.some(function(item) { return item && item.isDirectory; });
|
||||||
|
const recursive = hasDirectory;
|
||||||
|
|
||||||
|
runDispatchToTargets({
|
||||||
|
sourceSessionId: sourceSessionId,
|
||||||
|
sourcePaths: sourcePaths,
|
||||||
|
targetPath: targetPath,
|
||||||
|
recursive: recursive,
|
||||||
|
targetSessions: targetSessions,
|
||||||
|
transferBtn: transferBtn,
|
||||||
|
saveAsLastContext: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDispatchToTargets(options) {
|
||||||
|
const transferBtn = options.transferBtn || $('#btn-transfer-master');
|
||||||
|
const sourceSessionId = options.sourceSessionId;
|
||||||
|
const sourcePaths = options.sourcePaths || [];
|
||||||
|
const targetPath = options.targetPath;
|
||||||
|
const recursive = !!options.recursive;
|
||||||
|
const targetSessions = options.targetSessions || [];
|
||||||
|
const saveAsLastContext = options.saveAsLastContext === true;
|
||||||
|
|
||||||
|
if (dispatchRunning) {
|
||||||
|
alert('已有分发任务在执行,请稍后再试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (targetSessions.length === 0) {
|
||||||
|
alert('没有可分发的目标');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchRunning = true;
|
||||||
|
updateRetryFailedButton();
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let success = 0;
|
||||||
|
let fail = 0;
|
||||||
|
let failMessages = [];
|
||||||
|
const failedSessions = [];
|
||||||
|
const MAX_CONCURRENCY = 2;
|
||||||
|
let running = 0;
|
||||||
|
let finished = 0;
|
||||||
|
|
||||||
|
showTransferProgress(true, '开始主从分发...');
|
||||||
|
updateTransferProgress(0, '分发 0/' + targetSessions.length, true);
|
||||||
|
transferBtn.prop('disabled', true).text('分发中...');
|
||||||
|
|
||||||
|
if (saveAsLastContext) {
|
||||||
|
lastDispatchContext = {
|
||||||
|
sourceSessionId: sourceSessionId,
|
||||||
|
sourcePaths: sourcePaths.slice(),
|
||||||
|
targetPath: targetPath,
|
||||||
|
recursive: recursive
|
||||||
|
};
|
||||||
|
lastFailedSlaveSessions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
dispatchRunning = false;
|
||||||
|
showTransferProgress(false);
|
||||||
|
transferBtn.prop('disabled', false).text('主 -> 从发送');
|
||||||
|
const msg = '分发完成:成功 ' + success + ' 台,失败 ' + fail + ' 台';
|
||||||
|
updateStatus(msg);
|
||||||
|
|
||||||
|
lastFailedSlaveSessions = failedSessions.slice();
|
||||||
|
updateRetryFailedButton();
|
||||||
|
renderSlaveTargetList();
|
||||||
|
|
||||||
|
if (fail > 0) {
|
||||||
|
alert(msg + '\n\n失败详情:\n' + failMessages.join('\n'));
|
||||||
|
}
|
||||||
|
loadFiles('right');
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule() {
|
||||||
|
if (finished >= targetSessions.length) {
|
||||||
|
if (running === 0) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (running < MAX_CONCURRENCY && index < targetSessions.length) {
|
||||||
|
const targetSessionId = targetSessions[index++];
|
||||||
|
running++;
|
||||||
|
|
||||||
|
markDeliveryStatus(targetSessionId, 'running', '分发中');
|
||||||
|
renderSlaveTargetList();
|
||||||
|
|
||||||
|
const label = '分发中 (' + (finished + 1) + '/' + targetSessions.length + ') -> ' + getSessionDisplayName(targetSessionId);
|
||||||
|
updateTransferProgress(0, label, true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: API_BASE + 'api/files/transfer',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
sourceSessionId: sourceSessionId,
|
||||||
|
sourcePaths: sourcePaths,
|
||||||
|
targetSessionId: targetSessionId,
|
||||||
|
targetPath: targetPath,
|
||||||
|
recursive: recursive
|
||||||
|
}),
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
success++;
|
||||||
|
markDeliveryStatus(targetSessionId, 'success', '分发成功');
|
||||||
|
} else {
|
||||||
|
fail++;
|
||||||
|
failedSessions.push(targetSessionId);
|
||||||
|
const msg = response.message || '未知错误';
|
||||||
|
failMessages.push(getSessionDisplayName(targetSessionId) + ' - ' + msg);
|
||||||
|
markDeliveryStatus(targetSessionId, 'failed', msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
|
||||||
|
fail++;
|
||||||
|
failedSessions.push(targetSessionId);
|
||||||
|
failMessages.push(getSessionDisplayName(targetSessionId) + ' - ' + errMsg);
|
||||||
|
markDeliveryStatus(targetSessionId, 'failed', errMsg);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
running--;
|
||||||
|
finished++;
|
||||||
|
renderSlaveTargetList();
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryFailedDispatch() {
|
||||||
|
if (dispatchRunning) {
|
||||||
|
alert('当前有任务进行中,请稍后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!lastDispatchContext || lastFailedSlaveSessions.length === 0) {
|
||||||
|
alert('当前没有可重试的失败目标');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = lastFailedSlaveSessions.filter(function(sessionId) {
|
||||||
|
if (sessionId === 'local') {
|
||||||
|
return panelState.right.mode === 'local';
|
||||||
|
}
|
||||||
|
return !!activeConnections[sessionId];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
alert('失败目标已失效(可能已断开),请重新选择目标后分发');
|
||||||
|
lastFailedSlaveSessions = [];
|
||||||
|
updateRetryFailedButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runDispatchToTargets({
|
||||||
|
sourceSessionId: lastDispatchContext.sourceSessionId,
|
||||||
|
sourcePaths: lastDispatchContext.sourcePaths,
|
||||||
|
targetPath: lastDispatchContext.targetPath,
|
||||||
|
recursive: lastDispatchContext.recursive,
|
||||||
|
targetSessions: targets,
|
||||||
|
transferBtn: $('#btn-transfer-master'),
|
||||||
|
saveAsLastContext: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 文件删除功能(模块06)==========
|
// ========== 文件删除功能(模块06)==========
|
||||||
|
|
||||||
// 工具栏删除:删除当前活动面板中选中的文件
|
// 工具栏删除:删除当前活动面板中选中的文件
|
||||||
|
|||||||
Reference in New Issue
Block a user