重写复现方法

This commit is contained in:
2025-09-05 15:10:25 +08:00
parent a4ef2d7770
commit 89ed87e362
70 changed files with 14 additions and 8797 deletions

View File

@@ -1,25 +0,0 @@
package com.mini.capi.config;
import com.mini.capi.biz.domain.ApiUser;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpSession;
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpSession session = request.getSession();
ApiUser apiUser = (ApiUser) session.getAttribute("userInfo");
if (apiUser == null) {
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
return true;
}
}

View File

@@ -1,69 +0,0 @@
package com.mini.capi.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("forward:/index.html");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource requested = super.getResource(resourcePath, location);
// 文件存在就返回,不存在返回 index.html
return requested != null ? requested : new ClassPathResource("/static/index.html");
}
});
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns(
// ① 排除 Vue 静态资源(匹配 static 目录下所有 js/css/img 等)
"/js/**",
"/css/**",
"/img/**",
"/favicon.ico",
"/index.html", // 排除首页Vue 入口)
"/assets/**", // 若 Vue 打包后有 assets 目录,需排除
"/resource/**", // 若有其他静态资源目录,需排除
"/cApi/index/**",
"/login",
"/Sys/login/**", // 你的登录接口(原 /Sys/login/** 缺少 /cApi 前缀,修复)
"/Sys/jobs/**", // 原路径补充 /cApi 前缀
"/Sys/hosts/**",
"/Sys/dbs/**",
"/cApi/swagger-ui/**",
"/cApi/v3/api-docs/**"
);
}
}

View File

@@ -1,11 +1,10 @@
package com.mini.capi.model;
import com.mini.capi.biz.domain.SyncTablesView;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@Data
public class ApiResult<T> implements Serializable {

View File

@@ -15,12 +15,6 @@ public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public Object handleError(HttpServletRequest request) {
String uri = request.getRequestURI();
// 1. 前端路由:统一转发到 index.html
if (uri.startsWith("/cApi/index/")) {
return "forward:/index.html";
}
// 2. 其他 404返回 JSON
HttpStatus status = getStatus(request);
return ResponseEntity

View File

@@ -0,0 +1,13 @@
package com.mini.capi.sys.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class RootController {
@GetMapping("/")
public String redirectToSwagger() {
return "redirect:/swagger-ui.html";
}
}

View File

@@ -1,22 +0,0 @@
package com.mini.capi.webssh.config;
import com.mini.capi.webssh.websocket.SSHWebSocketHandler;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private SSHWebSocketHandler sshWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(sshWebSocketHandler, "/ssh")
.setAllowedOriginPatterns("*"); // 生产环境中应该限制域名
}
}

View File

@@ -1,253 +0,0 @@
package com.mini.capi.webssh.controller;
import com.mini.capi.biz.domain.SshServers;
import com.mini.capi.biz.service.SshServersService;
import com.mini.capi.webssh.service.FileTransferService;
import jakarta.annotation.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/files")
public class FileTransferController {
@Resource
private FileTransferService fileTransferService;
@Resource
private SshServersService serverService;
/**
* 上传文件到服务器
*/
@PostMapping("/upload/{serverId}")
public ResponseEntity<Map<String, Object>> uploadFile(
@PathVariable Long serverId,
@RequestParam("file") MultipartFile file,
@RequestParam("remotePath") String remotePath) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "服务器不存在"));
}
if (file.isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "文件不能为空"));
}
fileTransferService.uploadFile(serverOpt.get(), file, remotePath);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "文件上传成功",
"filename", file.getOriginalFilename(),
"size", file.getSize()
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "上传失败: " + e.getMessage()));
}
}
/**
* 批量上传文件
*/
@PostMapping("/upload-batch/{serverId}")
public ResponseEntity<Map<String, Object>> uploadFiles(
@PathVariable Long serverId,
@RequestParam("files") MultipartFile[] files,
@RequestParam("remotePath") String remotePath) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "服务器不存在"));
}
if (files == null || files.length == 0) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "请选择要上传的文件"));
}
fileTransferService.uploadFiles(serverOpt.get(), files, remotePath);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "批量上传成功",
"count", files.length
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "批量上传失败: " + e.getMessage()));
}
}
/**
* 从服务器下载文件
*/
@GetMapping("/download/{serverId}")
public ResponseEntity<byte[]> downloadFile(
@PathVariable Long serverId,
@RequestParam("remoteFilePath") String remoteFilePath) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest().build();
}
byte[] fileContent = fileTransferService.downloadFile(serverOpt.get(), remoteFilePath);
// 从路径中提取文件名
String filename = remoteFilePath.substring(remoteFilePath.lastIndexOf('/') + 1);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(fileContent);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
/**
* 列出远程目录内容
*/
@GetMapping("/list/{serverId}")
public ResponseEntity<Map<String, Object>> listDirectory(
@PathVariable Long serverId,
@RequestParam("remotePath") String remotePath) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "服务器不存在"));
}
List<FileTransferService.FileInfo> files =
fileTransferService.listDirectory(serverOpt.get(), remotePath);
return ResponseEntity.ok(Map.of(
"success", true,
"files", files,
"path", remotePath
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "获取目录列表失败: " + e.getMessage()));
}
}
/**
* 创建远程目录
*/
@PostMapping("/mkdir/{serverId}")
public ResponseEntity<Map<String, Object>> createDirectory(
@PathVariable Long serverId,
@RequestBody Map<String, String> request) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "服务器不存在"));
}
String remotePath = request.get("remotePath");
if (remotePath == null || remotePath.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "目录路径不能为空"));
}
fileTransferService.createRemoteDirectory(serverOpt.get(), remotePath);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "目录创建成功",
"path", remotePath
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "创建目录失败: " + e.getMessage()));
}
}
/**
* 删除远程文件或目录
*/
@DeleteMapping("/delete/{serverId}")
public ResponseEntity<Map<String, Object>> deleteFile(
@PathVariable Long serverId,
@RequestParam("remotePath") String remotePath,
@RequestParam(value = "isDirectory", defaultValue = "false") boolean isDirectory) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "服务器不存在"));
}
fileTransferService.deleteRemoteFile(serverOpt.get(), remotePath, isDirectory);
return ResponseEntity.ok(Map.of(
"success", true,
"message", (isDirectory ? "目录" : "文件") + "删除成功",
"path", remotePath
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "删除失败: " + e.getMessage()));
}
}
/**
* 重命名远程文件
*/
@PostMapping("/rename/{serverId}")
public ResponseEntity<Map<String, Object>> renameFile(
@PathVariable Long serverId,
@RequestBody Map<String, String> request) {
try {
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
if (!serverOpt.isPresent()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "服务器不存在"));
}
String oldPath = request.get("oldPath");
String newPath = request.get("newPath");
if (oldPath == null || newPath == null || oldPath.trim().isEmpty() || newPath.trim().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "路径不能为空"));
}
fileTransferService.renameRemoteFile(serverOpt.get(), oldPath, newPath);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "重命名成功",
"oldPath", oldPath,
"newPath", newPath
));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", "重命名失败: " + e.getMessage()));
}
}
}

View File

@@ -1,127 +0,0 @@
package com.mini.capi.webssh.controller;
import com.mini.capi.biz.domain.SshServers;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.mini.capi.biz.service.SshServersService;
import jakarta.annotation.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/servers")
public class ServerController {
@Resource
private SshServersService serverService;
/**
* 获取服务器列表
*/
@GetMapping
public ResponseEntity<List<SshServers>> getServers() {
List<SshServers> servers = serverService.list();
return ResponseEntity.ok(servers);
}
/**
* 获取单个服务器配置
*/
@GetMapping("/{id}")
public ResponseEntity<SshServers> getServer(@PathVariable Long id) {
try {
Optional<SshServers> server = serverService.getOptById(id);
if (server.isPresent()) {
return ResponseEntity.ok(server.get());
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
/**
* 添加服务器
*/
@PostMapping
public ResponseEntity<Map<String, Object>> addServer(@RequestBody SshServers server) {
try {
// 验证必要参数
if (server.getHost() == null || server.getHost().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "服务器地址不能为空"));
}
if (server.getUsername() == null || server.getUsername().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "用户名不能为空"));
}
if (server.getPassword() == null || server.getPassword().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "密码不能为空"));
}
// 设置默认值
if (server.getPort() == null) {
server.setPort(22);
}
if (server.getName() == null || server.getName().trim().isEmpty()) {
server.setName(server.getUsername() + "@" + server.getHost());
}
serverService.save(server);
return ResponseEntity.ok(Map.of("success", true));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
}
}
/**
* 删除服务器
*/
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> deleteServer(@PathVariable Long id) {
try {
serverService.removeById(id);
return ResponseEntity.ok(Map.of("success", true));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
}
}
/**
* 测试服务器连接
*/
@PostMapping("/test")
public ResponseEntity<Map<String, Object>> testConnection(@RequestBody SshServers server) {
try {
// 验证必要参数
if (server.getHost() == null || server.getHost().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "服务器地址不能为空"));
}
if (server.getUsername() == null || server.getUsername().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "用户名不能为空"));
}
if (server.getPassword() == null || server.getPassword().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "密码不能为空"));
}
// 设置默认端口
int port = server.getPort() != null ? server.getPort() : 22;
// 简单的连接测试
JSch jsch = new JSch();
Session session = jsch.getSession(server.getUsername(), server.getHost(), port);
session.setPassword(server.getPassword());
session.setConfig("StrictHostKeyChecking", "no");
session.connect(5000); // 5秒超时
session.disconnect();
return ResponseEntity.ok(Map.of("success", true, "message", "连接测试成功"));
} catch (Exception e) {
return ResponseEntity.ok(Map.of("success", false, "message", "连接测试失败: " + e.getMessage()));
}
}
}

View File

@@ -1,301 +0,0 @@
package com.mini.capi.webssh.service;
import com.jcraft.jsch.*;
import com.mini.capi.biz.domain.SshServers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
@Service
@Slf4j
public class FileTransferService {
/**
* 上传文件到远程服务器
*/
public void uploadFile(SshServers server, MultipartFile file, String remotePath) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
// 确保远程目录存在
createRemoteDirectory(sftpChannel, remotePath);
// 上传文件
String remoteFilePath = remotePath + "/" + file.getOriginalFilename();
try (InputStream inputStream = file.getInputStream()) {
sftpChannel.put(inputStream, remoteFilePath);
}
log.info("文件上传成功: {} -> {}", file.getOriginalFilename(), remoteFilePath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 从远程服务器下载文件
*/
public byte[] downloadFile(SshServers server, String remoteFilePath) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
InputStream inputStream = sftpChannel.get(remoteFilePath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
log.info("文件下载成功: {}", remoteFilePath);
return outputStream.toByteArray();
}
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 列出远程目录内容
*/
@SuppressWarnings("unchecked")
public List<FileInfo> listDirectory(SshServers server, String remotePath) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
List<FileInfo> files = new ArrayList<>();
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
Vector<ChannelSftp.LsEntry> entries = sftpChannel.ls(remotePath);
for (ChannelSftp.LsEntry entry : entries) {
String filename = entry.getFilename();
if (!filename.equals(".") && !filename.equals("..")) {
SftpATTRS attrs = entry.getAttrs();
files.add(new FileInfo(
filename,
attrs.isDir(),
attrs.getSize(),
attrs.getMTime() * 1000L, // Convert to milliseconds
getPermissionString(attrs.getPermissions())
));
}
}
log.info("目录列表获取成功: {}, 文件数: {}", remotePath, files.size());
return files;
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 创建远程目录
*/
public void createRemoteDirectory(SshServers server, String remotePath) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
createRemoteDirectory(sftpChannel, remotePath);
log.info("远程目录创建成功: {}", remotePath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 删除远程文件或目录
*/
public void deleteRemoteFile(SshServers server, String remotePath, boolean isDirectory) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
if (isDirectory) {
sftpChannel.rmdir(remotePath);
} else {
sftpChannel.rm(remotePath);
}
log.info("远程文件删除成功: {}", remotePath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 重命名远程文件
*/
public void renameRemoteFile(SshServers server, String oldPath, String newPath) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
sftpChannel.rename(oldPath, newPath);
log.info("文件重命名成功: {} -> {}", oldPath, newPath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 批量上传文件
*/
public void uploadFiles(SshServers server, MultipartFile[] files, String remotePath) throws Exception {
Session session = null;
ChannelSftp sftpChannel = null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
// 确保远程目录存在
createRemoteDirectory(sftpChannel, remotePath);
for (MultipartFile file : files) {
if (!file.isEmpty()) {
String remoteFilePath = remotePath + "/" + file.getOriginalFilename();
try (InputStream inputStream = file.getInputStream()) {
sftpChannel.put(inputStream, remoteFilePath);
log.info("文件上传成功: {}", file.getOriginalFilename());
}
}
}
log.info("批量上传完成,共上传 {} 个文件", files.length);
} finally {
closeConnections(sftpChannel, session);
}
}
// 私有辅助方法
private Session createSession(SshServers server) throws JSchException {
JSch jsch = new JSch();
Session session = jsch.getSession(server.getUsername(), server.getHost(), server.getPort());
session.setPassword(server.getPassword());
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
config.put("PreferredAuthentications", "password");
session.setConfig(config);
session.connect(10000); // 10秒超时
return session;
}
private void createRemoteDirectory(ChannelSftp sftpChannel, String remotePath) {
try {
String[] pathParts = remotePath.split("/");
String currentPath = "";
for (String part : pathParts) {
if (!part.isEmpty()) {
currentPath += "/" + part;
try {
sftpChannel.mkdir(currentPath);
} catch (SftpException e) {
log.error(e.getMessage(),e);
}
}
}
} catch (Exception e) {
log.warn("创建远程目录失败: {}", e.getMessage());
}
}
private void closeConnections(ChannelSftp sftpChannel, Session session) {
if (sftpChannel != null && sftpChannel.isConnected()) {
sftpChannel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
private String getPermissionString(int permissions) {
StringBuilder sb = new StringBuilder();
// Owner permissions
sb.append((permissions & 0400) != 0 ? 'r' : '-');
sb.append((permissions & 0200) != 0 ? 'w' : '-');
sb.append((permissions & 0100) != 0 ? 'x' : '-');
// Group permissions
sb.append((permissions & 0040) != 0 ? 'r' : '-');
sb.append((permissions & 0020) != 0 ? 'w' : '-');
sb.append((permissions & 0010) != 0 ? 'x' : '-');
// Others permissions
sb.append((permissions & 0004) != 0 ? 'r' : '-');
sb.append((permissions & 0002) != 0 ? 'w' : '-');
sb.append((permissions & 0001) != 0 ? 'x' : '-');
return sb.toString();
}
// 文件信息内部类
public static class FileInfo {
private String name;
private boolean isDirectory;
private long size;
private long lastModified;
private String permissions;
public FileInfo(String name, boolean isDirectory, long size, long lastModified, String permissions) {
this.name = name;
this.isDirectory = isDirectory;
this.size = size;
this.lastModified = lastModified;
this.permissions = permissions;
}
// Getters
public String getName() { return name; }
public boolean isDirectory() { return isDirectory; }
public long getSize() { return size; }
public long getLastModified() { return lastModified; }
public String getPermissions() { return permissions; }
}
}

View File

@@ -1,96 +0,0 @@
package com.mini.capi.webssh.service;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class SSHConnectionManager {
private final Map<String, Session> connections = new ConcurrentHashMap<>();
private final Map<String, ChannelShell> channels = new ConcurrentHashMap<>();
/**
* 建立SSH连接
*/
public String createConnection(String host, int port, String username, String password) {
try {
JSch jsch = new JSch();
Session session = jsch.getSession(username, host, port);
// 配置连接参数
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
config.put("PreferredAuthentications", "password");
session.setConfig(config);
session.setPassword(password);
// 建立连接
session.connect(30000); // 30秒超时
// 创建Shell通道
ChannelShell channel = (ChannelShell) session.openChannel("shell");
channel.setPty(true);
channel.setPtyType("xterm", 80, 24, 640, 480);
// 生成连接ID
String connectionId = UUID.randomUUID().toString();
// 保存连接和通道
connections.put(connectionId, session);
channels.put(connectionId, channel);
log.info("SSH连接建立成功: {}@{}:{}", username, host, port);
return connectionId;
} catch (JSchException e) {
log.error("SSH连接失败: {}", e.getMessage());
throw new RuntimeException("SSH连接失败: " + e.getMessage());
}
}
/**
* 获取SSH通道
*/
public ChannelShell getChannel(String connectionId) {
return channels.get(connectionId);
}
/**
* 获取SSH会话
*/
public Session getSession(String connectionId) {
return connections.get(connectionId);
}
/**
* 关闭SSH连接
*/
public void closeConnection(String connectionId) {
ChannelShell channel = channels.remove(connectionId);
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
Session session = connections.remove(connectionId);
if (session != null && session.isConnected()) {
session.disconnect();
}
log.info("SSH连接已关闭: {}", connectionId);
}
/**
* 检查连接状态
*/
public boolean isConnected(String connectionId) {
Session session = connections.get(connectionId);
return session != null && session.isConnected();
}
}

View File

@@ -1,263 +0,0 @@
package com.mini.capi.webssh.websocket;
import com.mini.capi.webssh.service.SSHConnectionManager;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSchException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class SSHWebSocketHandler extends TextWebSocketHandler {
@Resource
private SSHConnectionManager connectionManager;
private final Map<WebSocketSession, String> sessionConnections = new ConcurrentHashMap<>();
private final Map<WebSocketSession, String> sessionUsers = new ConcurrentHashMap<>();
// 为每个WebSocket会话添加同步锁
private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("WebSocket连接建立: {}", session.getId());
// 为每个会话创建同步锁
sessionLocks.put(session, new Object());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
try {
String payload = message.getPayload();
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(payload);
String type = jsonNode.get("type").asText();
switch (type) {
case "connect":
handleConnect(session, jsonNode);
break;
case "command":
handleCommand(session, jsonNode);
break;
case "resize":
handleResize(session, jsonNode);
break;
case "disconnect":
handleDisconnect(session);
break;
default:
log.warn("未知的消息类型: {}", type);
}
} catch (Exception e) {
log.error("处理WebSocket消息失败", e);
sendError(session, "处理消息失败: " + e.getMessage());
}
}
/**
* 处理SSH连接请求
*/
private void handleConnect(WebSocketSession session, JsonNode jsonNode) {
try {
String host = jsonNode.get("host").asText();
int port = jsonNode.get("port").asInt(22);
String username = jsonNode.get("username").asText();
String password = jsonNode.get("password").asText();
boolean enableCollaboration = jsonNode.has("enableCollaboration") &&
jsonNode.get("enableCollaboration").asBoolean();
// 存储用户信息
sessionUsers.put(session, username);
// 建立SSH连接
String connectionId = connectionManager.createConnection(host, port, username, password);
sessionConnections.put(session, connectionId);
// 启动SSH通道
ChannelShell channel = connectionManager.getChannel(connectionId);
startSSHChannel(session, channel);
// 发送连接成功消息
Map<String, Object> response = new HashMap<>();
response.put("type", "connected");
response.put("message", "SSH连接建立成功");
sendMessage(session, response);
} catch (Exception e) {
log.error("建立SSH连接失败", e);
sendError(session, "连接失败: " + e.getMessage());
}
}
/**
* 处理命令执行请求
*/
private void handleCommand(WebSocketSession session, JsonNode jsonNode) {
String connectionId = sessionConnections.get(session);
if (connectionId == null) {
sendError(session, "SSH连接未建立");
return;
}
String command = jsonNode.get("command").asText();
ChannelShell channel = connectionManager.getChannel(connectionId);
String username = sessionUsers.get(session);
if (channel != null && channel.isConnected()) {
try {
// 发送命令到SSH通道
OutputStream out = channel.getOutputStream();
out.write(command.getBytes());
out.flush();
} catch (IOException e) {
log.error("发送SSH命令失败", e);
sendError(session, "命令执行失败");
}
}
}
/**
* 启动SSH通道并处理输出
*/
private void startSSHChannel(WebSocketSession session, ChannelShell channel) {
try {
// 连接通道
channel.connect();
// 处理SSH输出
InputStream in = channel.getInputStream();
// 在单独的线程中读取SSH输出
new Thread(() -> {
byte[] buffer = new byte[4096];
try {
while (channel.isConnected() && session.isOpen()) {
if (in.available() > 0) {
int len = in.read(buffer);
if (len > 0) {
String output = new String(buffer, 0, len, "UTF-8");
// 发送给当前会话
sendMessage(session, Map.of(
"type", "output",
"data", output
));
}
} else {
// 没有数据时短暂休眠避免CPU占用过高
Thread.sleep(10);
}
}
} catch (IOException | InterruptedException e) {
log.warn("SSH输出读取中断: {}", e.getMessage());
}
}, "SSH-Output-Reader-" + session.getId()).start();
} catch (JSchException | IOException e) {
log.error("启动SSH通道失败", e);
sendError(session, "通道启动失败: " + e.getMessage());
}
}
/**
* 处理终端大小调整
*/
private void handleResize(WebSocketSession session, JsonNode jsonNode) {
String connectionId = sessionConnections.get(session);
if (connectionId != null) {
ChannelShell channel = connectionManager.getChannel(connectionId);
if (channel != null) {
try {
int cols = jsonNode.get("cols").asInt();
int rows = jsonNode.get("rows").asInt();
channel.setPtySize(cols, rows, cols * 8, rows * 16);
} catch (Exception e) {
log.warn("调整终端大小失败", e);
}
}
}
}
/**
* 处理断开连接
*/
private void handleDisconnect(WebSocketSession session) {
String connectionId = sessionConnections.remove(session);
String username = sessionUsers.remove(session);
if (connectionId != null) {
connectionManager.closeConnection(connectionId);
}
// 清理锁资源
sessionLocks.remove(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
handleDisconnect(session);
log.info("WebSocket连接关闭: {}", session.getId());
}
/**
* 发送消息到WebSocket客户端线程安全
*/
private void sendMessage(WebSocketSession session, Object message) {
Object lock = sessionLocks.get(session);
if (lock == null) return;
synchronized (lock) {
try {
if (session.isOpen()) {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(message);
session.sendMessage(new TextMessage(json));
}
} catch (Exception e) {
log.error("发送WebSocket消息失败", e);
}
}
}
/**
* 发送错误消息
*/
private void sendError(WebSocketSession session, String error) {
sendMessage(session, Map.of(
"type", "error",
"message", error
));
}
/**
* 从会话中获取用户信息
*/
private String getUserFromSession(WebSocketSession session) {
// 简化实现实际应用中可以从session中获取认证用户信息
return "anonymous";
}
/**
* 从会话中获取主机信息
*/
private String getHostFromSession(WebSocketSession session) {
// 简化实现,实际应用中可以保存连接信息
return "unknown";
}
}

View File

@@ -20,15 +20,3 @@ logging.level.root=INFO
logging.level.com.example.webssh=DEBUG
logging.file.name=logs/webssh.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# ===============================
# Custom WebSSH
# ===============================
webssh.ssh.connection-timeout=30000
webssh.ssh.session-timeout=1800000
webssh.ssh.max-connections-per-user=10
webssh.file.upload-max-size=100MB
webssh.file.temp-dir=/ogsapp/temp/webssh-uploads
webssh.collaboration.enabled=true
webssh.collaboration.max-participants=10
webssh.collaboration.session-timeout=3600000
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration

View File

@@ -1 +0,0 @@
@charset "UTF-8";.login-wrapper[data-v-c077d3e4]{height:100vh;display:flex;align-items:center;justify-content:flex-end;padding-right:200px;background:linear-gradient(120deg,rgba(79,172,254,.7) 0%,rgba(0,242,254,.7) 50%,rgba(122,90,248,.7) 100%),url(/cApi/assets/backImg.5cfc718b.jpg) center center no-repeat;background-blend-mode:overlay;background-size:cover}.login-text[data-v-c077d3e4]{text-align:center;color:#fff;width:800px;padding:32px 48px;border-radius:8px}.login-card[data-v-c077d3e4]{width:520px;padding:32px 48px;border-radius:8px;box-shadow:0 8px 24px #0000001f}.login-header[data-v-c077d3e4]{text-align:center;margin-bottom:32px}.login-header .logo[data-v-c077d3e4]{width:64px;height:64px;margin-bottom:12px}.login-header h1[data-v-c077d3e4]{font-size:24px;font-weight:600;margin:0}.login-header p[data-v-c077d3e4]{font-size:14px;color:#8c8c8c;margin:8px 0 0}

View File

@@ -1 +0,0 @@
import{d as S,_ as E,a as h,u as D,r as f,b as k,o as w,c as y,e as u,w as o,f as s,g as c,h as _,U as x,L as b,i as L,m}from"./index.04365c99.js";import{a as N}from"./auth.545ce83f.js";var O="/cApi/assets/logo.03d6d6da.png";const U=S("auth",{state:()=>({token:localStorage.getItem("token")||null,userInfo:localStorage.getItem("userInfo")?JSON.parse(localStorage.getItem("userInfo")):null}),actions:{setAuthInfo(i,r){this.token=i,this.userInfo=r,localStorage.setItem("token",i),localStorage.setItem("userInfo",JSON.stringify(r))},clearAuthInfo(){this.token=null,this.userInfo=null,localStorage.removeItem("token"),localStorage.removeItem("userInfo")}}});const J={class:"login-wrapper"},R=h({__name:"Login",setup(i){const r=D(),g=f(),n=f(!1),t=k({account:"",password:""}),F={account:[{required:!0,message:"\u8BF7\u8F93\u5165\u7528\u6237\u540D"}],password:[{required:!0,message:"\u8BF7\u8F93\u5165\u5BC6\u7801"}]},B=async()=>{if(n.value)return;n.value=!0;const p=U();try{const e=await N(t);if(e.code===200&&e.data){const l=e.data.token,a=JSON.stringify(e.data);localStorage.setItem("token",l),localStorage.setItem("userInfo",a),p.setAuthInfo(l,a),m.success(e.msg||"\u767B\u5F55\u6210\u529F"),setTimeout(()=>{r.replace("/index/console")},800);return}m.error(e.msg||"\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8D26\u53F7\u5BC6\u7801")}catch(e){console.error("\u767B\u5F55\u8BF7\u6C42\u5F02\u5E38:",e),m.error("\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u91CD\u8BD5")}finally{n.value=!1}};return(p,e)=>{const l=s("a-input"),a=s("a-form-item"),v=s("a-input-password"),I=s("a-button"),A=s("a-form"),C=s("a-card");return w(),y("div",J,[u(C,{class:"login-card",bordered:!1},{default:o(()=>[e[3]||(e[3]=c("div",{class:"login-header"},[c("img",{class:"logo",src:O,alt:"logo"}),c("h1",null,"cApi\u7BA1\u7406\u7CFB\u7EDF"),c("p",null,"\u5B89\u5168\u3001\u9AD8\u6548\u7684\u4E00\u7AD9\u5F0F\u7BA1\u7406\u89E3\u51B3\u65B9\u6848\uFF0C\u4E3A\u60A8\u7684\u4E1A\u52A1\u4FDD\u9A7E\u62A4\u822A")],-1)),u(A,{ref_key:"formRef",ref:g,model:t,rules:F,size:"large",onFinish:B},{default:o(()=>[u(a,{name:"account"},{default:o(()=>[u(l,{value:t.account,"onUpdate:value":e[0]||(e[0]=d=>t.account=d),placeholder:"\u8BF7\u8F93\u5165\u7528\u6237\u540D","allow-clear":""},{prefix:o(()=>[u(_(x))]),_:1},8,["value"])]),_:1}),u(a,{name:"password"},{default:o(()=>[u(v,{value:t.password,"onUpdate:value":e[1]||(e[1]=d=>t.password=d),placeholder:"\u8BF7\u8F93\u5165\u5BC6\u7801","allow-clear":""},{prefix:o(()=>[u(_(b))]),_:1},8,["value"])]),_:1}),u(a,null,{default:o(()=>[u(I,{type:"primary","html-type":"submit",loading:n.value,block:""},{default:o(()=>[...e[2]||(e[2]=[L(" \u767B\u5F55 ",-1)])]),_:1},8,["loading"])]),_:1})]),_:1},8,["model"])]),_:1})])}}});var T=E(R,[["__scopeId","data-v-c077d3e4"]]);export{T as default};

View File

@@ -1 +0,0 @@
import{_ as r}from"./index.04365c99.js";const e={};function j(_,c){return" jjjjjjjjj "}var t=r(e,[["render",j]]);export{t as default};

View File

@@ -1 +0,0 @@
import{_ as r}from"./index.04365c99.js";const e={};function t(_,c){return" \u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876\u9876 "}var o=r(e,[["render",t]]);export{o as default};

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -1 +0,0 @@
.icon-gallery[data-v-64d10c62]{width:100%;box-sizing:border-box}.search-input[data-v-64d10c62]{max-width:400px}.icons-grid[data-v-64d10c62]{width:100%;box-sizing:border-box}.icon-item[data-v-64d10c62]:hover{border-color:#1890ff;box-shadow:0 2px 8px #00000014}.icon-name[data-v-64d10c62]{font-size:12px;color:#595959;text-align:center;word-break:break-all;max-width:100%}.copy-buttons[data-v-64d10c62]{display:flex;gap:8px;margin-top:8px;width:100%;justify-content:center}.copy-btn[data-v-64d10c62]{padding:2px 8px!important;font-size:12px!important}.empty-state[data-v-64d10c62]{padding:40px 0;text-align:center}.copy-message[data-v-64d10c62]{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:1000}

View File

@@ -1 +0,0 @@
import{_ as U,a as W,I as q,r as a,n as u,o as s,c as l,j as g,t as p,g as w,F as G,y as H,A as m,e as r,f as y,z as J,v as K,w as S,h as F,D as T,i as z,k as B}from"./index.04365c99.js";const Q=["onMouseenter"],X={class:"icon-name"},Y={key:0,class:"copy-buttons"},Z={key:1,class:"empty-state"},ee=W({__name:"icon",props:{showSearch:{type:Boolean,default:!0},searchSize:{type:String,default:"middle"},columns:{type:Number,default:6},iconSize:{type:Number,default:24},itemPadding:{type:String,default:"16px"},messageDuration:{type:Number,default:2e3}},setup(c){const n=c,N=["createFromIconfontCN","getTwoToneColor","setTwoToneColor","default","CopyOutlined"],_=Object.entries(q).filter(([e])=>!N.includes(e)).map(([e,t])=>({name:e,component:t})),i=a(""),C=u(()=>i.value?_.filter(({name:e})=>e.toLowerCase().includes(i.value.toLowerCase())):_),f=a(""),v=a(!1),h=a(""),D=a("success");function M(e){f.value=e}function $(){f.value=""}function E(e){const t=`<${e} />`;x(t).then(()=>{d(`\u5DF2\u590D\u5236: ${t}`,"success")}).catch(()=>{d("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236","error")})}function I(e){x(e).then(()=>{d(`\u5DF2\u590D\u5236: ${e}`,"success")}).catch(()=>{d("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236","error")})}async function x(e){try{return await navigator.clipboard.writeText(e),!0}catch(t){return console.error("\u65E0\u6CD5\u590D\u5236\u6587\u672C: ",t),!1}}function d(e,t){h.value=e,D.value=t,v.value=!0,setTimeout(()=>{v.value=!1},n.messageDuration)}const L=u(()=>({padding:"16px",backgroundColor:"#f5f5f5",borderRadius:"4px"})),A=u(()=>({display:"grid",gridTemplateColumns:`repeat(${n.columns}, 1fr)`,gap:"12px",marginTop:n.showSearch?"16px":0})),V=u(()=>({display:"flex",flexDirection:"column",alignItems:"center",backgroundColor:"#fff",border:"1px solid #e8e8e8",borderRadius:"6px",padding:n.itemPadding,cursor:"pointer",transition:"all 0.2s"})),O=u(()=>({fontSize:`${n.iconSize}px`,color:"#262626",marginBottom:"8px"}));return(e,t)=>{const j=y("a-input"),b=y("a-button"),P=y("a-empty"),R=y("a-message");return s(),l("div",{class:"icon-gallery",style:m(L.value)},[c.showSearch?(s(),g(j,{key:0,value:i.value,"onUpdate:value":t[0]||(t[0]=o=>i.value=o),placeholder:"\u641C\u7D22\u56FE\u6807\u540D\u79F0...","allow-clear":"",class:"search-input",size:c.searchSize},null,8,["value","size"])):p("",!0),w("div",{class:"icons-grid",style:m(A.value)},[(s(!0),l(G,null,H(C.value,o=>(s(),l("div",{key:o.name,class:"icon-item",style:m(V.value),onMouseenter:k=>M(o.name),onMouseleave:$},[(s(),g(J(o.component),{class:"icon",style:m(O.value)},null,8,["style"])),w("span",X,K(o.name),1),f.value===o.name?(s(),l("div",Y,[r(b,{type:"primary",size:"small",onClick:B(k=>E(o.name),["stop"]),class:"copy-btn"},{default:S(()=>[r(F(T),{class:"mr-1"}),t[1]||(t[1]=z("\u6807\u7B7E ",-1))]),_:1},8,["onClick"]),r(b,{type:"default",size:"small",onClick:B(k=>I(o.name),["stop"]),class:"copy-btn"},{default:S(()=>[r(F(T),{class:"mr-1"}),t[2]||(t[2]=z("\u540D\u79F0 ",-1))]),_:1},8,["onClick"])])):p("",!0)],44,Q))),128))],4),C.value.length===0?(s(),l("div",Z,[r(P,{description:"\u6CA1\u6709\u627E\u5230\u5339\u914D\u7684\u56FE\u6807"})])):p("",!0),v.value?(s(),g(R,{key:2,content:h.value,type:D.value,duration:c.messageDuration,class:"copy-message"},null,8,["content","type","duration"])):p("",!0)],4)}}});var oe=U(ee,[["__scopeId","data-v-64d10c62"]]);export{oe as default};

View File

@@ -1 +0,0 @@
import{_ as r}from"./index.04365c99.js";const e={};function _(c,n){return" app "}var a=r(e,[["render",_]]);export{a as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.empty-page-container[data-v-2de931e9]{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:8px;box-shadow:0 1px 2px #0000000d}.empty-content[data-v-2de931e9]{width:100%;max-width:600px;padding:48px 24px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:20px}.empty-icon[data-v-2de931e9]{width:80px;height:80px;border-radius:50%;background-color:#e8f4ff;display:flex;align-items:center;justify-content:center;color:#1890ff}.empty-icon[data-v-2de931e9] svg{width:40px;height:40px}.empty-title[data-v-2de931e9]{font-size:18px;font-weight:500;color:#333;margin:0}.empty-desc[data-v-2de931e9]{font-size:14px;color:#666;line-height:1.6;margin:0}.empty-actions[data-v-2de931e9]{display:flex;gap:12px;margin-top:8px}.empty-meta[data-v-2de931e9]{margin-top:16px;display:flex;flex-wrap:wrap;justify-content:center;gap:16px;font-size:12px;color:#999}@media (max-width: 768px){.empty-content[data-v-2de931e9]{padding:32px 16px;gap:16px}.empty-icon[data-v-2de931e9]{width:64px;height:64px}.empty-icon[data-v-2de931e9] svg{width:32px;height:32px}.empty-title[data-v-2de931e9]{font-size:16px}.empty-meta[data-v-2de931e9]{flex-direction:column;gap:8px}}

View File

@@ -1,4 +0,0 @@
import{_ as B,a as E,r as d,o as p,c as r,g as e,e as t,h as a,E as A,i as s,w as F,f as m,P as c,Q as C}from"./index.04365c99.js";const _={class:"empty-page-container"},D={class:"empty-content"},v={class:"empty-icon"},y={class:"empty-actions"},f=E({__name:"index",setup(x){const n=d("\u7A7A\u767D\u6A21\u677F\u9875\u9762"),l=()=>{console.log(`[${n.value}] \u89E6\u53D1\u300C\u521B\u5EFA\u5185\u5BB9\u300D\u64CD\u4F5C`),alert("\u53EF\u5728\u6B64\u5904\u5B9E\u73B0\u300C\u521B\u5EFA\u5185\u5BB9\u300D\u7684\u4E1A\u52A1\u903B\u8F91")},i=()=>{console.log(`[${n.value}] \u67E5\u770B\u4F7F\u7528\u6307\u5357`),alert(`\u4F7F\u7528\u6307\u5357\uFF1A
1. \u6B64\u9875\u9762\u5DF2\u9002\u914D\u7236\u5BB9\u5668\u81EA\u9002\u5E94\u5C3A\u5BF8
2. \u53EF\u5728 empty-content \u5185\u6DFB\u52A0\u4E1A\u52A1\u7EC4\u4EF6
3. \u6837\u5F0F\u53EF\u53C2\u8003\u539F\u9879\u76EE\u89C4\u8303\u8C03\u6574`)};return(g,u)=>{const o=m("a-button");return p(),r("div",_,[e("div",D,[e("div",v,[t(a(A))]),u[2]||(u[2]=e("h2",{class:"empty-title"},"\u8FD9\u662F\u4E00\u4E2A\u7A7A\u767D\u9875\u9762",-1)),u[3]||(u[3]=e("p",{class:"empty-desc"},[s(" \u4F60\u53EF\u4EE5\u5728\u8FD9\u91CC\u6269\u5C55\u529F\u80FD\uFF0C\u6BD4\u5982\u6DFB\u52A0\u6570\u636E\u5C55\u793A\u3001\u8868\u5355\u63D0\u4EA4\u3001\u56FE\u8868\u5206\u6790\u7B49\u5185\u5BB9"),e("br"),s(" \u9875\u9762\u5DF2\u9002\u914D\u7236\u5BB9\u5668\u5C3A\u5BF8\uFF0C\u652F\u6301\u9AD8\u5EA6/\u5BBD\u5EA6\u81EA\u9002\u5E94 ")],-1)),e("div",y,[t(o,{type:"primary",onClick:l},{default:F(()=>[t(a(c),{class:"mr-2"}),u[0]||(u[0]=s(" \u521B\u5EFA\u5185\u5BB9 ",-1))]),_:1}),t(o,{onClick:i,style:{"margin-left":"12px"}},{default:F(()=>[t(a(C),{class:"mr-2"}),u[1]||(u[1]=s(" \u67E5\u770B\u4F7F\u7528\u6307\u5357 ",-1))]),_:1})]),u[4]||(u[4]=e("div",{class:"empty-meta"},[e("span",{class:"meta-item"},"\u9875\u9762\u8DEF\u5F84\uFF1A@/views/EmptyTemplatePage.vue"),e("span",{class:"meta-item"},"\u9002\u914D\u573A\u666F\uFF1A\u5217\u8868\u9875\u3001\u8BE6\u60C5\u9875\u3001\u8868\u5355\u9875\u7B49\u521D\u59CB\u6A21\u677F")],-1))])])}}});var N=B(f,[["__scopeId","data-v-2de931e9"]]);export{N as default};

View File

@@ -1 +0,0 @@
html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type="range"]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/cApi/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cApi系统管理</title>
<script type="module" crossorigin src="/cApi/assets/index.04365c99.js"></script>
<link rel="stylesheet" href="/cApi/assets/index.fc5e98cf.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB