重写复现方法
This commit is contained in:
@@ -14,12 +14,12 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
HttpSession session = request.getSession();
|
||||
ApiUser apiUser = (ApiUser) session.getAttribute("Authorization");
|
||||
if (apiUser == null) {
|
||||
response.sendRedirect(request.getContextPath() + "/login");
|
||||
return false;
|
||||
}
|
||||
// HttpSession session = request.getSession();
|
||||
// ApiUser apiUser = (ApiUser) session.getAttribute("Authorization");
|
||||
// if (apiUser == null) {
|
||||
// response.sendRedirect(request.getContextPath() + "/login");
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.mini.capi.config;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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;
|
||||
|
||||
@Configuration
|
||||
@@ -11,6 +13,20 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
// @Override
|
||||
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// registry.addResourceHandler("/cApi/**")
|
||||
// .addResourceLocations("classpath:/static/")
|
||||
// .setCachePeriod(0);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// @Override
|
||||
// public void addViewControllers(ViewControllerRegistry registry) {
|
||||
// registry.addViewController("/cApi/**")
|
||||
// .setViewName("forward:/cApi/index.html");
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@@ -9,33 +8,33 @@ public class loginController {
|
||||
|
||||
@GetMapping("/login")
|
||||
public String loginPage() {
|
||||
return "index";
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出登录:清空 session 并返回到退出成功页面
|
||||
*/
|
||||
@GetMapping("/userLogout")
|
||||
public String logout(HttpSession session) {
|
||||
session.invalidate();
|
||||
return "index";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 主页
|
||||
*/
|
||||
@GetMapping("/welcome")
|
||||
public String welcomePage() {
|
||||
return "views/demo";
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统首页-控制台
|
||||
*/
|
||||
@GetMapping("/home")
|
||||
public String homePage() {
|
||||
return "views/home";
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * 退出登录:清空 session 并返回到退出成功页面
|
||||
// */
|
||||
// @GetMapping("/userLogout")
|
||||
// public String logout(HttpSession session) {
|
||||
// session.invalidate();
|
||||
// return "index";
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 主页
|
||||
// */
|
||||
// @GetMapping("/welcome")
|
||||
// public String welcomePage() {
|
||||
// return "views/demo";
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 系统首页-控制台
|
||||
// */
|
||||
// @GetMapping("/home")
|
||||
// public String homePage() {
|
||||
// return "views/home";
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.mini.capi.sys.pageController;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.mini.capi.biz.domain.ApiMenus;
|
||||
import com.mini.capi.biz.domain.ApiModule;
|
||||
@@ -34,12 +35,35 @@ public class loginPageController {
|
||||
|
||||
|
||||
@Data
|
||||
public static class LoginRequest implements Serializable {
|
||||
private String username;
|
||||
public static class LoginParams implements Serializable {
|
||||
private String account;
|
||||
private String password;
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
public static class ApiUserDTO implements Serializable {
|
||||
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 登录名称
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户名称
|
||||
*/
|
||||
private String uname;
|
||||
|
||||
// 构造方法(从实体类转换)
|
||||
public ApiUserDTO(ApiUser apiUser) {
|
||||
this.userId = apiUser.getUserId();
|
||||
this.username = apiUser.getApiUser();
|
||||
this.uname = apiUser.getUname();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码校验(生产环境需替换为加密比对)
|
||||
*/
|
||||
@@ -52,23 +76,24 @@ public class loginPageController {
|
||||
* 用户登录
|
||||
*/
|
||||
@PostMapping("/userLogin")
|
||||
public Result login(@RequestBody LoginRequest user, HttpSession session) {
|
||||
public Result login(@RequestBody LoginParams user, HttpSession session) {
|
||||
try {
|
||||
QueryWrapper<ApiUser> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("api_user", user.getUsername())
|
||||
queryWrapper.eq("api_user", user.getAccount())
|
||||
.eq("ustatus", 1);
|
||||
ApiUser apiUser = userService.getOne(queryWrapper);
|
||||
if (apiUser == null) {
|
||||
return Result.error("账户不存在");
|
||||
return Result.error(101, "账户不存在");
|
||||
}
|
||||
if (!verifyPassword(user.getPassword(), apiUser.getApiPswd())) {
|
||||
// 可记录登录失败日志,用于后续风控
|
||||
return Result.error("账户或密码错误");
|
||||
return Result.error(102, "账户或密码错误");
|
||||
}
|
||||
session.setAttribute("Authorization", apiUser);
|
||||
return Result.success("登录成功");
|
||||
session.setAttribute("token", apiUser);
|
||||
ApiUserDTO userDTO = new ApiUserDTO(apiUser);
|
||||
return Result.success("登录成功", userDTO);
|
||||
} catch (Exception e) {
|
||||
return Result.error("登录失败,请稍后重试");
|
||||
return Result.error(103, "登录失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,13 @@ server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javasc
|
||||
server.tomcat.max-connections=200
|
||||
server.tomcat.threads.max=100
|
||||
server.tomcat.threads.min-spare=10
|
||||
spring.datasource.url=jdbc:mysql://192.168.31.189:33069/work?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
|
||||
spring.datasource.url=jdbc:mysql://crontab.club:33069/work?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
|
||||
spring.datasource.username=dream
|
||||
spring.datasource.password=info_dream
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
|
||||
spring.web.resources.static-locations=classpath:/static/
|
||||
|
||||
# ===============================
|
||||
# Logging
|
||||
# ===============================
|
||||
|
||||
1
src/main/resources/static/assets/Dashboard.cd76d04c.js
Normal file
1
src/main/resources/static/assets/Dashboard.cd76d04c.js
Normal file
@@ -0,0 +1 @@
|
||||
const n={};n.render=function(n,e){return null};export{n as default};
|
||||
1
src/main/resources/static/assets/Login.008b81ee.css
Normal file
1
src/main/resources/static/assets/Login.008b81ee.css
Normal file
@@ -0,0 +1 @@
|
||||
@charset "UTF-8";.login-wrapper[data-v-c93922d0]{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-c93922d0]{text-align:center;color:#fff;width:800px;padding:32px 48px;border-radius:8px}.login-card[data-v-c93922d0]{width:520px;padding:32px 48px;border-radius:8px;box-shadow:0 8px 24px #0000001f}.login-header[data-v-c93922d0]{text-align:center;margin-bottom:32px}.login-header .logo[data-v-c93922d0]{width:64px;height:64px;margin-bottom:12px}.login-header h1[data-v-c93922d0]{font-size:24px;font-weight:600;margin:0}.login-header p[data-v-c93922d0]{font-size:14px;color:#8c8c8c;margin:8px 0 0}
|
||||
1
src/main/resources/static/assets/Login.ab12ce3d.js
Normal file
1
src/main/resources/static/assets/Login.ab12ce3d.js
Normal file
File diff suppressed because one or more lines are too long
BIN
src/main/resources/static/assets/backImg.5cfc718b.jpg
Normal file
BIN
src/main/resources/static/assets/backImg.5cfc718b.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
@@ -1,324 +0,0 @@
|
||||
/* 全局样式重置:增加box-sizing强制继承,避免尺寸计算偏差 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
/* 样式变量:统一管理,减少硬编码 */
|
||||
:root {
|
||||
--primary-color: #3498db;
|
||||
--primary-dark: #2980b9;
|
||||
--primary-light: #e3f2fd;
|
||||
--bg-gradient-1: #2c3e50;
|
||||
--bg-gradient-2: #3498db;
|
||||
--white: #fff;
|
||||
--gray-light: #f8f9fa;
|
||||
--gray-border: #ddd;
|
||||
--gray-hover: #f1f1f1;
|
||||
--text-gray: #555;
|
||||
--text-light-gray: #888;
|
||||
--danger-color: #e74c3c;
|
||||
--shadow-normal: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
--shadow-modal: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
--shadow-input: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||
--transition-base: all 0.25s ease;
|
||||
--login-box-width: 420px; /* 固定登录框宽度,避免变形 */
|
||||
--input-height: 48px; /* 固定输入框高度,统一视觉 */
|
||||
}
|
||||
|
||||
/* 页面主体:固定布局,确保登录框不被挤压 */
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-1), var(--bg-gradient-2));
|
||||
padding: 0 80px;
|
||||
overflow: hidden; /* 禁止页面滚动 */
|
||||
}
|
||||
|
||||
/* -------------------------- 左侧品牌区域:固定尺寸和位置 -------------------------- */
|
||||
.brand-side {
|
||||
width: 55%;
|
||||
max-width: 700px;
|
||||
color: var(--white);
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
margin-right: 60px;
|
||||
}
|
||||
|
||||
.brand-title-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.brand-side h1 {
|
||||
font-size: 3.2rem;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 3px 12px rgba(0, 0, 0, 0.25);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.brand-divider {
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, var(--white), transparent);
|
||||
margin: 0 auto 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.brand-side p {
|
||||
font-size: 1.15rem;
|
||||
opacity: 0.95;
|
||||
line-height: 1.7;
|
||||
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.brand-icon i {
|
||||
font-size: 2.8rem;
|
||||
opacity: 0.95;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.brand-icon i:hover {
|
||||
transform: translateY(-6px) scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* -------------------------- 右侧登录容器:固定尺寸,彻底解决变形 -------------------------- */
|
||||
.login-container {
|
||||
width: var(--login-box-width); /* 固定宽度,不随内容拉伸 */
|
||||
background: var(--white);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-normal);
|
||||
overflow: hidden;
|
||||
transition: var(--transition-base);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-container:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
padding: 25px;
|
||||
background: var(--gray-light);
|
||||
border-bottom: 1px solid var(--gray-border);
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
font-size: 1.7rem;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 表单区域:固定内边距,避免内容溢出 */
|
||||
.login-form {
|
||||
padding: 35px 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 表单组:固定间距,不随内容变化 */
|
||||
.form-group {
|
||||
margin-bottom: 28px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.required-mark {
|
||||
color: var(--danger-color);
|
||||
margin-left: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
color: var(--text-gray);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* -------------------------- 输入框组:固定高度+定位,彻底解决变形 -------------------------- */
|
||||
.input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--input-height); /* 固定容器高度,与输入框一致 */
|
||||
}
|
||||
|
||||
/* 输入框:固定高度+内边距,不拉伸 */
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
height: 100%; /* 继承容器高度,避免自行拉伸 */
|
||||
padding: 0 18px 0 45px; /* 左侧留足图标空间,右侧留足按钮空间 */
|
||||
border: 1px solid var(--gray-border);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: var(--transition-base);
|
||||
background-color: var(--white);
|
||||
resize: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* 输入框聚焦:仅改变边框和阴影,不改变尺寸 */
|
||||
.form-group input:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-input);
|
||||
background-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.form-group input:not(:placeholder-shown) {
|
||||
border-color: #b3d9f2;
|
||||
}
|
||||
|
||||
/* 输入框图标:固定左侧定位,不随输入框变化 */
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 16px; /* 固定左侧距离,不偏移 */
|
||||
color: var(--text-light-gray);
|
||||
font-size: 1.1rem;
|
||||
z-index: 1;
|
||||
width: 20px; /* 固定图标宽度,避免抖动 */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group input:not(:placeholder-shown) + .input-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 密码切换按钮:固定最右侧,不偏移 */
|
||||
.toggle-pwd {
|
||||
position: absolute;
|
||||
right: 12px; /* 固定右侧距离 */
|
||||
top: 50%;
|
||||
transform: translateY(-50%); /* 垂直居中 */
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-light-gray);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
transition: var(--transition-base);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-pwd:hover {
|
||||
background-color: var(--gray-hover);
|
||||
color: var(--text-gray);
|
||||
}
|
||||
|
||||
/* 登录按钮:固定尺寸,不拉伸 */
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
height: 52px; /* 固定按钮高度 */
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 3px 8px rgba(52, 152, 219, 0.2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 12px rgba(41, 128, 185, 0.25);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 5px rgba(41, 128, 185, 0.2);
|
||||
}
|
||||
|
||||
/* -------------------------- 弹窗:固定层级,避免被遮挡 -------------------------- */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999; /* 最高层级,不被任何元素遮挡 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--white);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
width: 320px;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-modal);
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-content .icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--danger-color);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-gray);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-content button {
|
||||
padding: 10px 25px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.modal-content button:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.modal-content button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
1
src/main/resources/static/assets/index.6a28b391.css
Normal file
1
src/main/resources/static/assets/index.6a28b391.css
Normal file
@@ -0,0 +1 @@
|
||||
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}
|
||||
1
src/main/resources/static/assets/index.8dd9ceba.js
Normal file
1
src/main/resources/static/assets/index.8dd9ceba.js
Normal file
@@ -0,0 +1 @@
|
||||
import{r as e,o as r,c as t,a as o,b as n,d as s,A as i}from"./vendor.a8167e4a.js";!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver((e=>{for(const t of e)if("childList"===t.type)for(const e of t.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)})).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),"use-credentials"===e.crossorigin?r.credentials="include":"anonymous"===e.crossorigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();const c={};c.render=function(o,n){const s=e("router-view");return r(),t(s)};const a={},d=function(e,r){return r&&0!==r.length?Promise.all(r.map((e=>{if((e=`/cApi/${e}`)in a)return;a[e]=!0;const r=e.endsWith(".css"),t=r?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${e}"]${t}`))return;const o=document.createElement("link");return o.rel=r?"stylesheet":"modulepreload",r||(o.as="script",o.crossOrigin=""),o.href=e,document.head.appendChild(o),r?new Promise(((e,r)=>{o.addEventListener("load",e),o.addEventListener("error",r)})):void 0}))).then((()=>e())):e()},l=[{path:"/",redirect:"/login"},{path:"/login",name:"Login",component:()=>d((()=>import("./Login.ab12ce3d.js")),["assets/Login.ab12ce3d.js","assets/Login.008b81ee.css","assets/vendor.a8167e4a.js"])},{path:"/dashboard",name:"Dashboard",component:()=>d((()=>import("./Dashboard.cd76d04c.js")),[]),meta:{requiresAuth:!0}}],u=o({history:n("/cApi/"),routes:l});s(c).use(i).use(u).mount("#app");
|
||||
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* 登录逻辑处理:阻止表单默认提交,校验用户名/密码,调用登录接口
|
||||
* @param {Event} e - 表单提交事件
|
||||
* @returns {boolean} - 返回false阻止默认提交
|
||||
*/
|
||||
function handleLogin(e) {
|
||||
// 1. 阻止表单默认提交行为(避免页面刷新)
|
||||
e.preventDefault();
|
||||
|
||||
// 2. 获取并清理用户名、密码(去除前后空格)
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
// 3. 前端简单校验(空值判断)
|
||||
if (!username) {
|
||||
showModal('请输入用户名');
|
||||
return false;
|
||||
}
|
||||
if (!password) {
|
||||
showModal('请输入密码');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 调用后端登录接口(POST请求)
|
||||
fetch('Sys/login/userLogin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json' // 声明请求体为JSON格式
|
||||
},
|
||||
body: JSON.stringify({username, password}) // 转换为JSON字符串
|
||||
})
|
||||
.then(response => {
|
||||
// 4.1 校验HTTP响应状态(非200-299范围视为错误)
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败,状态码:${response.status}`);
|
||||
}
|
||||
// 4.2 解析响应体(假设后端返回JSON格式)
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// 5. 处理后端返回结果(假设接口返回 { code: 200, msg: '成功' } 结构)
|
||||
if (data.code === 200) {
|
||||
// 登录成功:跳转到demo页面(可根据实际需求修改跳转路径)
|
||||
window.location.href = 'welcome';
|
||||
} else {
|
||||
// 业务错误:显示后端返回的错误信息,无信息时显示默认提示
|
||||
showModal(data.msg || '登录失败,请检查账号密码是否正确');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 6. 捕获请求异常(网络错误、接口500等)
|
||||
showModal(`登录异常:${error.message}`);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误弹窗
|
||||
* @param {string} msg - 弹窗提示内容
|
||||
*/
|
||||
function showModal(msg) {
|
||||
const errorMsgElem = document.getElementById('errorMsg');
|
||||
const modalElem = document.getElementById('errorModal');
|
||||
|
||||
// 设置弹窗文本内容
|
||||
errorMsgElem.innerText = msg;
|
||||
// 显示弹窗(通过flex布局实现居中)
|
||||
modalElem.style.display = 'flex';
|
||||
// 延迟添加show类(触发过渡动画,避免动画不生效)
|
||||
setTimeout(() => {
|
||||
modalElem.classList.add('show');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭错误弹窗
|
||||
*/
|
||||
function closeModal() {
|
||||
const modalElem = document.getElementById('errorModal');
|
||||
|
||||
// 移除show类(触发淡出动画)
|
||||
modalElem.classList.remove('show');
|
||||
// 动画结束后隐藏弹窗(300ms与CSS过渡时间保持一致)
|
||||
setTimeout(() => {
|
||||
modalElem.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空指定输入框内容
|
||||
* @param {string} inputId - 输入框的ID属性值
|
||||
*/
|
||||
function clearInput(inputId) {
|
||||
const inputElem = document.getElementById(inputId);
|
||||
if (inputElem) {
|
||||
// 清空输入框内容
|
||||
inputElem.value = '';
|
||||
// 触发input事件(更新清空按钮的显示状态)
|
||||
inputElem.dispatchEvent(new Event('input'));
|
||||
// 聚焦到当前输入框(提升用户体验)
|
||||
inputElem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换密码输入框的可见性(明文/密文切换)
|
||||
*/
|
||||
function togglePasswordVisibility() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const toggleBtn = passwordInput.parentElement.querySelector('.toggle-pwd i');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
// 切换为明文显示
|
||||
passwordInput.type = 'text';
|
||||
// 更换图标为"眼睛"(表示当前是明文)
|
||||
toggleBtn.classList.remove('bi-eye-slash');
|
||||
toggleBtn.classList.add('bi-eye');
|
||||
} else {
|
||||
// 切换为密文显示
|
||||
passwordInput.type = 'password';
|
||||
// 更换图标为"眼睛斜杠"(表示当前是密文)
|
||||
toggleBtn.classList.remove('bi-eye');
|
||||
toggleBtn.classList.add('bi-eye-slash');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框内容变化时的动态反馈:控制清空按钮的显示/隐藏
|
||||
* @param {HTMLInputElement} inputElem - 输入框元素
|
||||
*/
|
||||
function handleInputChange(inputElem) {
|
||||
const clearBtn = inputElem.parentElement.querySelector('.clear-btn');
|
||||
if (clearBtn) {
|
||||
// 输入框有内容时显示清空按钮,无内容时隐藏
|
||||
clearBtn.hidden = !inputElem.value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后:绑定全局交互事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. 获取用户名和密码输入框元素
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
// 2. 绑定输入框内容变化事件(控制清空按钮显示)
|
||||
if (usernameInput) {
|
||||
usernameInput.addEventListener('input', () => handleInputChange(usernameInput));
|
||||
}
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener('input', () => handleInputChange(passwordInput));
|
||||
}
|
||||
|
||||
// 3. 点击弹窗外部关闭弹窗
|
||||
const modalElem = document.getElementById('errorModal');
|
||||
modalElem.addEventListener('click', (e) => {
|
||||
// 仅当点击弹窗背景(而非内容区)时关闭
|
||||
if (e.target === modalElem) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 按ESC键关闭弹窗
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// 仅当弹窗显示时生效
|
||||
if (e.key === 'Escape' && modalElem.style.display === 'flex') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 页面初始化时,默认聚焦到用户名输入框(提升用户体验)
|
||||
if (usernameInput) {
|
||||
usernameInput.focus();
|
||||
}
|
||||
});
|
||||
@@ -1,980 +0,0 @@
|
||||
/**
|
||||
* Web SSH 多会话客户端
|
||||
* 支持多个SSH连接和会话管理
|
||||
*/
|
||||
|
||||
class MultiSessionWebSSHClient {
|
||||
constructor() {
|
||||
this.sessions = new Map(); // 存储所有会话
|
||||
this.activeSessionId = null; // 当前激活的会话ID
|
||||
this.nextSessionId = 1; // 下一个会话ID
|
||||
this.savedServers = []; // 缓存保存的服务器列表
|
||||
|
||||
this.initializeUI();
|
||||
this.loadSavedServers();
|
||||
}
|
||||
|
||||
initializeUI() {
|
||||
// 初始化时隐藏终端容器
|
||||
document.getElementById('terminalContainer').classList.add('hidden');
|
||||
}
|
||||
|
||||
// ========== 会话管理 ==========
|
||||
createSession(host, port, username, password) {
|
||||
const sessionId = `session_${this.nextSessionId++}`;
|
||||
const serverName = `${username}@${host}:${port}`;
|
||||
|
||||
const session = {
|
||||
id: sessionId,
|
||||
host: host,
|
||||
port: port,
|
||||
username: username,
|
||||
password: password,
|
||||
name: serverName,
|
||||
terminal: null,
|
||||
websocket: null,
|
||||
fitAddon: null,
|
||||
connected: false,
|
||||
serverId: null
|
||||
};
|
||||
|
||||
// 创建终端实例
|
||||
session.terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#ffffff',
|
||||
selection: '#ffffff40'
|
||||
},
|
||||
rows: 25,
|
||||
cols: 100
|
||||
});
|
||||
|
||||
session.fitAddon = new FitAddon.FitAddon();
|
||||
session.terminal.loadAddon(session.fitAddon);
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
this.createTabForSession(session);
|
||||
this.createTerminalForSession(session);
|
||||
this.switchToSession(sessionId);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
createTabForSession(session) {
|
||||
const tabsContainer = document.getElementById('terminalTabs');
|
||||
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'terminal-tab';
|
||||
tab.id = `tab_${session.id}`;
|
||||
tab.onclick = () => this.switchToSession(session.id);
|
||||
|
||||
tab.innerHTML = `
|
||||
<div class="tab-status disconnected"></div>
|
||||
<div class="tab-title" title="${session.name}">${session.name}</div>
|
||||
<div class="tab-actions">
|
||||
<button class="tab-btn" onclick="event.stopPropagation(); sshClient.duplicateSession('${session.id}')" title="复制会话">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="tab-btn" onclick="event.stopPropagation(); sshClient.closeSession('${session.id}')" title="关闭会话">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tabsContainer.appendChild(tab);
|
||||
}
|
||||
|
||||
createTerminalForSession(session) {
|
||||
const contentContainer = document.getElementById('terminalContent');
|
||||
|
||||
const sessionDiv = document.createElement('div');
|
||||
sessionDiv.className = 'terminal-session';
|
||||
sessionDiv.id = `session_${session.id}`;
|
||||
|
||||
sessionDiv.innerHTML = `
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-info">
|
||||
<span class="connection-status" id="status_${session.id}">
|
||||
🔴 未连接
|
||||
</span>
|
||||
</div>
|
||||
<div class="terminal-actions">
|
||||
<button class="terminal-btn" onclick="switchPage('files')">
|
||||
<i class="fas fa-folder"></i> 文件管理
|
||||
</button>
|
||||
<button class="terminal-btn" onclick="sshClient.disconnectSession('${session.id}')">
|
||||
<i class="fas fa-times"></i> 断开连接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-wrapper">
|
||||
<div id="terminal_${session.id}"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
contentContainer.appendChild(sessionDiv);
|
||||
|
||||
// 初始化终端
|
||||
session.terminal.open(document.getElementById(`terminal_${session.id}`));
|
||||
session.fitAddon.fit();
|
||||
}
|
||||
|
||||
switchToSession(sessionId) {
|
||||
// 更新标签状态
|
||||
document.querySelectorAll('.terminal-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`tab_${sessionId}`).classList.add('active');
|
||||
|
||||
// 更新内容显示
|
||||
document.querySelectorAll('.terminal-session').forEach(session => {
|
||||
session.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`session_${sessionId}`).classList.add('active');
|
||||
|
||||
this.activeSessionId = sessionId;
|
||||
|
||||
// 调整终端大小
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session && session.fitAddon) {
|
||||
setTimeout(() => session.fitAddon.fit(), 100);
|
||||
}
|
||||
|
||||
// 显示终端容器
|
||||
document.getElementById('terminalContainer').classList.remove('hidden');
|
||||
|
||||
this.updateStatusBar();
|
||||
}
|
||||
|
||||
updateStatusBar() {
|
||||
const session = this.sessions.get(this.activeSessionId);
|
||||
if (session && session.terminal) {
|
||||
const size = session.terminal.buffer.active;
|
||||
document.getElementById('terminalStats').textContent =
|
||||
`行: ${size.baseY + size.cursorY + 1}, 列: ${size.cursorX + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
closeSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (this.sessions.size === 1) {
|
||||
// 如果是最后一个会话,隐藏终端容器
|
||||
document.getElementById('terminalContainer').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
if (session.websocket) {
|
||||
session.websocket.close();
|
||||
}
|
||||
|
||||
// 清理DOM元素
|
||||
const tab = document.getElementById(`tab_${sessionId}`);
|
||||
const sessionDiv = document.getElementById(`session_${sessionId}`);
|
||||
if (tab) tab.remove();
|
||||
if (sessionDiv) sessionDiv.remove();
|
||||
|
||||
// 从sessions中删除
|
||||
this.sessions.delete(sessionId);
|
||||
|
||||
// 如果关闭的是当前激活会话,切换到其他会话
|
||||
if (sessionId === this.activeSessionId) {
|
||||
const remainingSessions = Array.from(this.sessions.keys());
|
||||
if (remainingSessions.length > 0) {
|
||||
this.switchToSession(remainingSessions[0]);
|
||||
} else {
|
||||
this.activeSessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.showAlert('会话已关闭', 'info');
|
||||
}
|
||||
|
||||
duplicateSession(sessionId) {
|
||||
const originalSession = this.sessions.get(sessionId);
|
||||
if (!originalSession) return;
|
||||
|
||||
// 创建新会话,使用相同的连接参数
|
||||
const newSession = this.createSession(
|
||||
originalSession.host,
|
||||
originalSession.port,
|
||||
originalSession.username,
|
||||
originalSession.password
|
||||
);
|
||||
|
||||
// 自动连接
|
||||
this.connectSession(newSession.id);
|
||||
|
||||
this.showAlert('会话已复制', 'success');
|
||||
}
|
||||
|
||||
// ========== SSH连接管理 ==========
|
||||
async connectSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.connected) {
|
||||
this.showAlert('会话已连接', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 建立WebSocket连接
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/cApi/ssh`;
|
||||
session.websocket = new WebSocket(wsUrl);
|
||||
|
||||
session.websocket.onopen = () => {
|
||||
console.log(`Session ${sessionId} WebSocket连接建立`);
|
||||
this.updateSessionStatus(sessionId, '正在连接SSH...');
|
||||
|
||||
// 发送SSH连接请求
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
host: session.host,
|
||||
port: parseInt(session.port),
|
||||
username: session.username,
|
||||
password: session.password
|
||||
}));
|
||||
};
|
||||
|
||||
session.websocket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleSessionMessage(sessionId, message);
|
||||
};
|
||||
|
||||
session.websocket.onerror = (error) => {
|
||||
console.error(`Session ${sessionId} WebSocket错误:`, error);
|
||||
this.showAlert('WebSocket连接错误', 'danger');
|
||||
session.terminal.writeln('\\r\\n❌ WebSocket连接错误');
|
||||
};
|
||||
|
||||
session.websocket.onclose = () => {
|
||||
console.log(`Session ${sessionId} WebSocket连接关闭`);
|
||||
this.handleSessionDisconnection(sessionId);
|
||||
};
|
||||
|
||||
// 处理终端输入
|
||||
session.terminal.onData((data) => {
|
||||
if (session.connected && session.websocket.readyState === WebSocket.OPEN) {
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'command',
|
||||
command: data
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 处理终端大小变化
|
||||
session.terminal.onResize((size) => {
|
||||
if (session.connected && session.websocket.readyState === WebSocket.OPEN) {
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: size.cols,
|
||||
rows: size.rows
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
this.showAlert('连接失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionMessage(sessionId, message) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
switch (message.type) {
|
||||
case 'connected':
|
||||
session.connected = true;
|
||||
this.updateTabStatus(sessionId, true);
|
||||
this.updateSessionStatus(sessionId, '已连接');
|
||||
|
||||
// 查找服务器ID
|
||||
session.serverId = this.findServerIdByConnection(
|
||||
session.host,
|
||||
session.port,
|
||||
session.username
|
||||
);
|
||||
|
||||
session.terminal.clear();
|
||||
session.terminal.writeln('🎉 SSH连接建立成功!');
|
||||
session.terminal.writeln(`连接到: ${session.name}`);
|
||||
session.terminal.writeln('');
|
||||
|
||||
this.showAlert(`会话 "${session.name}" 连接成功`, 'success');
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
session.terminal.write(message.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
session.terminal.writeln(`\\r\\n❌ 错误: ${message.message}`);
|
||||
this.showAlert(`会话连接失败: ${message.message}`, 'danger');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionDisconnection(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
session.connected = false;
|
||||
session.serverId = null;
|
||||
this.updateTabStatus(sessionId, false);
|
||||
this.updateSessionStatus(sessionId, '已断开连接');
|
||||
|
||||
if (session.terminal) {
|
||||
session.terminal.writeln('\\r\\n🔌 连接已关闭');
|
||||
}
|
||||
|
||||
this.showAlert(`会话 "${session.name}" 已断开连接`, 'warning');
|
||||
}
|
||||
|
||||
disconnectSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.websocket) {
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'disconnect'
|
||||
}));
|
||||
session.websocket.close();
|
||||
}
|
||||
|
||||
this.handleSessionDisconnection(sessionId);
|
||||
}
|
||||
|
||||
updateTabStatus(sessionId, connected) {
|
||||
const tab = document.getElementById(`tab_${sessionId}`);
|
||||
if (!tab) return;
|
||||
|
||||
const statusDot = tab.querySelector('.tab-status');
|
||||
if (connected) {
|
||||
statusDot.classList.remove('disconnected');
|
||||
statusDot.classList.add('connected');
|
||||
} else {
|
||||
statusDot.classList.remove('connected');
|
||||
statusDot.classList.add('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
updateSessionStatus(sessionId, message) {
|
||||
const statusElement = document.getElementById(`status_${sessionId}`);
|
||||
if (statusElement) {
|
||||
statusElement.innerHTML = message.includes('已连接') ?
|
||||
`🟢 ${message}` :
|
||||
`🔴 ${message}`;
|
||||
}
|
||||
|
||||
// 更新状态栏
|
||||
if (sessionId === this.activeSessionId) {
|
||||
document.getElementById('statusBar').textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 服务器配置管理 ==========
|
||||
async loadSavedServers() {
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers');
|
||||
const servers = await response.json();
|
||||
this.savedServers = servers;
|
||||
|
||||
const select = document.getElementById('savedServers');
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
|
||||
select.innerHTML = '<option value="">选择已保存的服务器...</option>';
|
||||
fileServerSelect.innerHTML = '<option value="">选择服务器...</option>';
|
||||
|
||||
servers.forEach(server => {
|
||||
const option = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
select.add(option);
|
||||
|
||||
const fileOption = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
fileServerSelect.add(fileOption);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
findServerIdByConnection(host, port, username) {
|
||||
const matchedServer = this.savedServers.find(server =>
|
||||
server.host === host &&
|
||||
server.port === parseInt(port) &&
|
||||
server.username === username
|
||||
);
|
||||
return matchedServer ? matchedServer.id : null;
|
||||
}
|
||||
|
||||
async loadServerConfig() {
|
||||
const serverId = document.getElementById('savedServers').value;
|
||||
if (!serverId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/servers/${serverId}`);
|
||||
const server = await response.json();
|
||||
|
||||
document.getElementById('host').value = server.host;
|
||||
document.getElementById('port').value = server.port;
|
||||
document.getElementById('username').value = server.username;
|
||||
document.getElementById('serverName').value = server.name;
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器配置失败:', error);
|
||||
this.showAlert('加载服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async saveServerConfig() {
|
||||
const serverData = {
|
||||
name: document.getElementById('serverName').value ||
|
||||
`${document.getElementById('username').value}@${document.getElementById('host').value}`,
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('服务器配置已保存', 'success');
|
||||
this.loadSavedServers();
|
||||
} else {
|
||||
this.showAlert('保存失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存服务器配置失败:', error);
|
||||
this.showAlert('保存服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const originalText = testBtn.innerHTML;
|
||||
testBtn.innerHTML = '<div class="loading"></div> 测试中...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
const serverData = {
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('连接测试成功', 'success');
|
||||
} else {
|
||||
this.showAlert('连接测试失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error);
|
||||
this.showAlert('连接测试失败', 'danger');
|
||||
} finally {
|
||||
testBtn.innerHTML = originalText;
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 文件管理相关 ==========
|
||||
getCurrentServerId() {
|
||||
if (this.activeSessionId) {
|
||||
const session = this.sessions.get(this.activeSessionId);
|
||||
return session ? session.serverId : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 文件管理功能
|
||||
currentFileServerId = null;
|
||||
currentPath = '/';
|
||||
|
||||
async switchFileServer() {
|
||||
this.currentFileServerId = document.getElementById('fileServerSelect').value;
|
||||
if (this.currentFileServerId) {
|
||||
this.currentPath = '/';
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshFiles() {
|
||||
if (!this.currentFileServerId) {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/list/${this.currentFileServerId}?remotePath=${encodeURIComponent(this.currentPath)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.displayFiles(result.files);
|
||||
} else {
|
||||
this.showAlert('获取文件列表失败: ' + result.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${result.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
this.showAlert('获取文件列表失败: ' + error.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
displayFiles(files) {
|
||||
const container = document.getElementById('fileGrid');
|
||||
container.innerHTML = '';
|
||||
|
||||
files.forEach(file => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.onclick = () => this.handleFileClick(file);
|
||||
|
||||
const icon = file.directory ? 'fas fa-folder' : 'fas fa-file';
|
||||
const size = file.directory ? '-' : this.formatFileSize(file.size);
|
||||
const date = new Date(file.lastModified).toLocaleString('zh-CN');
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<i class="${icon} file-icon"></i>
|
||||
<span class="file-name">${file.name}</span>
|
||||
<span class="file-size">${size}</span>
|
||||
<span class="file-date">${date}</span>
|
||||
<div class="file-actions">
|
||||
${!file.directory ? `
|
||||
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); downloadFile('${file.name}')">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); deleteFile('${file.name}', ${file.directory})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
|
||||
async handleFileClick(file) {
|
||||
if (file.directory) {
|
||||
this.currentPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + file.name :
|
||||
this.currentPath + '/' + file.name;
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
}
|
||||
|
||||
async navigateUp() {
|
||||
if (this.currentPath === '/') return;
|
||||
|
||||
const pathParts = this.currentPath.split('/').filter(p => p);
|
||||
pathParts.pop();
|
||||
this.currentPath = '/' + pathParts.join('/');
|
||||
if (this.currentPath !== '/' && !this.currentPath.endsWith('/')) {
|
||||
this.currentPath += '/';
|
||||
}
|
||||
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
console.log('uploadFiles called');
|
||||
|
||||
if (!this.currentFileServerId) {
|
||||
this.showAlert('请先选择一个服务器', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Server selected:', this.currentFileServerId);
|
||||
|
||||
const files = document.getElementById('uploadFiles').files;
|
||||
const uploadPath = document.getElementById('uploadPath').value;
|
||||
|
||||
if (files.length === 0) {
|
||||
this.showAlert('请选择要上传的文件', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (let file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
formData.append('remotePath', uploadPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/upload-batch/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert(`成功上传 ${result.count} 个文件`, 'success');
|
||||
closeModal('uploadModal');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('上传失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
this.showAlert('上传文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(filename) {
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/download/${this.currentFileServerId}?remoteFilePath=${encodeURIComponent(filePath)}`);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
this.showAlert('文件下载成功', 'success');
|
||||
} else {
|
||||
this.showAlert('文件下载失败', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
this.showAlert('下载文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(filename, isDirectory) {
|
||||
if (!confirm(`确定要删除${isDirectory ? '目录' : '文件'} "${filename}" 吗?`)) return;
|
||||
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/delete/${this.currentFileServerId}?remotePath=${encodeURIComponent(filePath)}&isDirectory=${isDirectory}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('删除成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('删除失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.showAlert('删除失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async createFolder() {
|
||||
const folderName = prompt('请输入文件夹名称:');
|
||||
if (!folderName) return;
|
||||
|
||||
const folderPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + folderName :
|
||||
this.currentPath + '/' + folderName;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/mkdir/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ remotePath: folderPath })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('文件夹创建成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('创建失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文件夹失败:', error);
|
||||
this.showAlert('创建文件夹失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UI工具方法 ==========
|
||||
showAlert(message, type) {
|
||||
const container = document.getElementById('alertContainer');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 全局函数 ==========
|
||||
let sshClient = null;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
sshClient = new MultiSessionWebSSHClient();
|
||||
});
|
||||
|
||||
// 页面切换
|
||||
function switchPage(pageName) {
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
const onclick = item.getAttribute('onclick');
|
||||
if (onclick && onclick.includes(`switchPage('${pageName}')`)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.page-content').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`page-${pageName}`).classList.add('active');
|
||||
|
||||
if (pageName === 'files') {
|
||||
sshClient.loadSavedServers().then(() => {
|
||||
// 如果当前有激活的会话,自动选择对应的服务器
|
||||
const currentServerId = sshClient.getCurrentServerId();
|
||||
if (currentServerId) {
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
fileServerSelect.value = currentServerId;
|
||||
|
||||
// 设置文件管理的当前服务器ID并加载文件
|
||||
sshClient.currentFileServerId = currentServerId;
|
||||
sshClient.currentPath = '/';
|
||||
document.getElementById('currentPath').value = sshClient.currentPath;
|
||||
sshClient.refreshFiles(); // 自动加载文件列表
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏折叠
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const title = document.getElementById('sidebarTitle');
|
||||
const navTexts = document.querySelectorAll('.nav-text');
|
||||
|
||||
sidebar.classList.toggle('collapsed');
|
||||
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
title.style.display = 'none';
|
||||
navTexts.forEach(text => text.style.display = 'none');
|
||||
} else {
|
||||
title.style.display = 'inline';
|
||||
navTexts.forEach(text => text.style.display = 'inline');
|
||||
}
|
||||
}
|
||||
|
||||
// SSH连接相关
|
||||
function connectSSH() {
|
||||
const host = document.getElementById('host').value.trim();
|
||||
const port = document.getElementById('port').value.trim();
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
if (!host || !username || !password) {
|
||||
sshClient.showAlert('请填写完整的连接信息', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新会话并连接
|
||||
const session = sshClient.createSession(host, port || 22, username, password);
|
||||
sshClient.connectSession(session.id);
|
||||
|
||||
// 保存服务器配置(如果需要)
|
||||
if (document.getElementById('saveServer').checked) {
|
||||
sshClient.saveServerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectSSH() {
|
||||
if (sshClient.activeSessionId) {
|
||||
sshClient.disconnectSession(sshClient.activeSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
sshClient.testConnection();
|
||||
}
|
||||
|
||||
function loadSavedServers() {
|
||||
sshClient.loadSavedServers();
|
||||
}
|
||||
|
||||
function loadServerConfig() {
|
||||
sshClient.loadServerConfig();
|
||||
}
|
||||
|
||||
// 文件管理相关
|
||||
function switchFileServer() {
|
||||
sshClient.switchFileServer();
|
||||
}
|
||||
|
||||
function refreshFiles() {
|
||||
sshClient.refreshFiles();
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
sshClient.navigateUp();
|
||||
}
|
||||
|
||||
function showUploadModal() {
|
||||
document.getElementById('uploadModal').classList.add('active');
|
||||
document.getElementById('uploadPath').value = sshClient.currentPath || '/';
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
console.log('handleUpload called');
|
||||
try {
|
||||
sshClient.uploadFiles();
|
||||
} catch (error) {
|
||||
console.error('Error in handleUpload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFiles(event) {
|
||||
sshClient.uploadFiles(event);
|
||||
}
|
||||
|
||||
function downloadFile(filename) {
|
||||
sshClient.downloadFile(filename);
|
||||
}
|
||||
|
||||
function deleteFile(filename, isDirectory) {
|
||||
sshClient.deleteFile(filename, isDirectory);
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
sshClient.createFolder();
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
// 点击弹窗背景关闭弹窗
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Escape 关闭弹窗
|
||||
if (event.key === 'Escape') {
|
||||
document.querySelectorAll('.modal.active').forEach(modal => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Ctrl+Enter 快速连接
|
||||
if (event.ctrlKey && event.key === 'Enter' && sshClient.sessions.size === 0) {
|
||||
connectSSH();
|
||||
}
|
||||
|
||||
// Ctrl+T 新建会话 (when connected)
|
||||
if (event.ctrlKey && event.key === 't' && sshClient.activeSessionId) {
|
||||
const currentSession = sshClient.sessions.get(sshClient.activeSessionId);
|
||||
if (currentSession) {
|
||||
sshClient.duplicateSession(sshClient.activeSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+W 关闭当前会话
|
||||
if (event.ctrlKey && event.key === 'w' && sshClient.activeSessionId) {
|
||||
sshClient.closeSession(sshClient.activeSessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时断开所有连接
|
||||
window.addEventListener('beforeunload', function(event) {
|
||||
if (sshClient && sshClient.sessions.size > 0) {
|
||||
sshClient.sessions.forEach((session, sessionId) => {
|
||||
if (session.websocket) {
|
||||
session.websocket.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,800 +0,0 @@
|
||||
/**
|
||||
* Web SSH 简化版客户端
|
||||
* 支持SSH连接和文件管理功能
|
||||
*/
|
||||
|
||||
class SimpleWebSSHClient {
|
||||
constructor() {
|
||||
this.terminal = null;
|
||||
this.websocket = null;
|
||||
this.fitAddon = null;
|
||||
this.connected = false;
|
||||
this.currentServer = null;
|
||||
this.currentServerId = null; // 添加当前服务器ID
|
||||
this.currentFileServerId = null;
|
||||
this.currentPath = '/';
|
||||
this.savedServers = []; // 缓存保存的服务器列表
|
||||
|
||||
this.initializeTerminal();
|
||||
this.loadSavedServers();
|
||||
}
|
||||
|
||||
// ========== 终端初始化 ==========
|
||||
initializeTerminal() {
|
||||
this.terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#ffffff',
|
||||
selection: '#ffffff40'
|
||||
},
|
||||
rows: 30,
|
||||
cols: 120
|
||||
});
|
||||
|
||||
this.fitAddon = new FitAddon.FitAddon();
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
|
||||
this.terminal.open(document.getElementById('terminal'));
|
||||
this.fitAddon.fit();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.fitAddon) {
|
||||
setTimeout(() => this.fitAddon.fit(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新终端统计信息
|
||||
this.terminal.onResize((size) => {
|
||||
document.getElementById('terminalStats').textContent =
|
||||
`行: ${size.rows}, 列: ${size.cols}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ========== SSH连接管理 ==========
|
||||
async connect(host, port, username, password) {
|
||||
if (this.connected) {
|
||||
this.showAlert('已有连接存在,请先断开', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentServer = {
|
||||
host, port, username,
|
||||
name: `${username}@${host}:${port}`
|
||||
};
|
||||
|
||||
try {
|
||||
// 建立WebSocket连接
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ssh`;
|
||||
this.websocket = new WebSocket(wsUrl);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
console.log('WebSocket连接建立');
|
||||
this.updateStatus('正在连接SSH...');
|
||||
|
||||
// 发送SSH连接请求
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
host: host,
|
||||
port: parseInt(port),
|
||||
username: username,
|
||||
password: password
|
||||
}));
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleWebSocketMessage(message);
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
this.showAlert('WebSocket连接错误', 'danger');
|
||||
this.terminal.writeln('\r\n❌ WebSocket连接错误');
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
console.log('WebSocket连接关闭');
|
||||
this.handleDisconnection();
|
||||
};
|
||||
|
||||
// 处理终端输入
|
||||
this.terminal.onData((data) => {
|
||||
if (this.connected && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'command',
|
||||
command: data
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 处理终端大小变化
|
||||
this.terminal.onResize((size) => {
|
||||
if (this.connected && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: size.cols,
|
||||
rows: size.rows
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
this.showAlert('连接失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
handleWebSocketMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'connected':
|
||||
this.connected = true;
|
||||
this.updateConnectionStatus(true);
|
||||
|
||||
// 查找并设置当前服务器ID
|
||||
this.currentServerId = this.findServerIdByConnection(
|
||||
this.currentServer.host,
|
||||
this.currentServer.port,
|
||||
this.currentServer.username
|
||||
);
|
||||
|
||||
this.terminal.clear();
|
||||
this.terminal.writeln('🎉 SSH连接建立成功!');
|
||||
this.terminal.writeln(`连接到: ${this.currentServer.name}`);
|
||||
this.terminal.writeln('');
|
||||
this.showAlert('SSH连接成功', 'success');
|
||||
this.updateStatus('已连接');
|
||||
|
||||
// 显示终端容器
|
||||
document.getElementById('terminalContainer').classList.remove('hidden');
|
||||
this.fitAddon.fit();
|
||||
|
||||
// 保存服务器配置(如果需要)
|
||||
if (document.getElementById('saveServer').checked) {
|
||||
this.saveServerConfig();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
this.terminal.write(message.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.terminal.writeln(`\r\n❌ 错误: ${message.message}`);
|
||||
this.showAlert(`连接失败: ${message.message}`, 'danger');
|
||||
this.updateStatus('连接失败');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.websocket) {
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'disconnect'
|
||||
}));
|
||||
this.websocket.close();
|
||||
}
|
||||
|
||||
this.handleDisconnection();
|
||||
}
|
||||
|
||||
handleDisconnection() {
|
||||
this.connected = false;
|
||||
this.currentServer = null;
|
||||
this.currentServerId = null; // 清除当前服务器ID
|
||||
this.updateConnectionStatus(false);
|
||||
this.updateStatus('已断开连接');
|
||||
|
||||
if (this.terminal) {
|
||||
this.terminal.writeln('\r\n🔌 连接已关闭');
|
||||
}
|
||||
|
||||
document.getElementById('terminalContainer').classList.add('hidden');
|
||||
this.showAlert('SSH连接已断开', 'danger');
|
||||
}
|
||||
|
||||
// ========== 服务器配置管理 ==========
|
||||
async loadSavedServers() {
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers');
|
||||
const servers = await response.json();
|
||||
|
||||
// 缓存服务器列表
|
||||
this.savedServers = servers;
|
||||
|
||||
const select = document.getElementById('savedServers');
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
|
||||
// 清空现有选项
|
||||
select.innerHTML = '<option value="">选择已保存的服务器...</option>';
|
||||
fileServerSelect.innerHTML = '<option value="">选择服务器...</option>';
|
||||
|
||||
servers.forEach(server => {
|
||||
const option = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
select.add(option);
|
||||
|
||||
const fileOption = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
fileServerSelect.add(fileOption);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接信息查找服务器ID
|
||||
findServerIdByConnection(host, port, username) {
|
||||
const matchedServer = this.savedServers.find(server =>
|
||||
server.host === host &&
|
||||
server.port === parseInt(port) &&
|
||||
server.username === username
|
||||
);
|
||||
return matchedServer ? matchedServer.id : null;
|
||||
}
|
||||
|
||||
async loadServerConfig() {
|
||||
const serverId = document.getElementById('savedServers').value;
|
||||
if (!serverId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/servers/${serverId}`);
|
||||
const server = await response.json();
|
||||
|
||||
document.getElementById('host').value = server.host;
|
||||
document.getElementById('port').value = server.port;
|
||||
document.getElementById('username').value = server.username;
|
||||
document.getElementById('serverName').value = server.name;
|
||||
// 不填充密码,出于安全考虑
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器配置失败:', error);
|
||||
this.showAlert('加载服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async saveServerConfig() {
|
||||
const serverData = {
|
||||
name: document.getElementById('serverName').value ||
|
||||
`${document.getElementById('username').value}@${document.getElementById('host').value}`,
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('服务器配置已保存', 'success');
|
||||
this.loadSavedServers(); // 重新加载列表
|
||||
} else {
|
||||
this.showAlert('保存失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存服务器配置失败:', error);
|
||||
this.showAlert('保存服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const originalText = testBtn.innerHTML;
|
||||
testBtn.innerHTML = '<div class="loading"></div> 测试中...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
const serverData = {
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('连接测试成功', 'success');
|
||||
} else {
|
||||
this.showAlert('连接测试失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error);
|
||||
this.showAlert('连接测试失败', 'danger');
|
||||
} finally {
|
||||
testBtn.innerHTML = originalText;
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动选择当前连接的服务器并切换到文件管理
|
||||
async switchToFileManagerWithCurrentServer() {
|
||||
if (this.currentServerId) {
|
||||
// 设置文件服务器选择框
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
fileServerSelect.value = this.currentServerId;
|
||||
|
||||
// 切换文件服务器
|
||||
this.currentFileServerId = this.currentServerId;
|
||||
this.currentPath = '/';
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 文件管理 ==========
|
||||
async switchFileServer() {
|
||||
this.currentFileServerId = document.getElementById('fileServerSelect').value;
|
||||
if (this.currentFileServerId) {
|
||||
this.currentPath = '/';
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshFiles() {
|
||||
if (!this.currentFileServerId) {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/list/${this.currentFileServerId}?remotePath=${encodeURIComponent(this.currentPath)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.displayFiles(result.files);
|
||||
} else {
|
||||
this.showAlert('获取文件列表失败: ' + result.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${result.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
this.showAlert('获取文件列表失败: ' + error.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
displayFiles(files) {
|
||||
const container = document.getElementById('fileGrid');
|
||||
container.innerHTML = '';
|
||||
|
||||
files.forEach(file => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.onclick = () => this.handleFileClick(file);
|
||||
|
||||
const icon = file.directory ? 'fas fa-folder' : 'fas fa-file';
|
||||
const size = file.directory ? '-' : this.formatFileSize(file.size);
|
||||
const date = new Date(file.lastModified).toLocaleString('zh-CN');
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<i class="${icon} file-icon"></i>
|
||||
<span class="file-name">${file.name}</span>
|
||||
<span class="file-size">${size}</span>
|
||||
<span class="file-date">${date}</span>
|
||||
<div class="file-actions">
|
||||
${!file.directory ? `
|
||||
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); downloadFile('${file.name}')">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); deleteFile('${file.name}', ${file.directory})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
|
||||
async handleFileClick(file) {
|
||||
if (file.directory) {
|
||||
this.currentPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + file.name :
|
||||
this.currentPath + '/' + file.name;
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
}
|
||||
|
||||
async navigateUp() {
|
||||
if (this.currentPath === '/') return;
|
||||
|
||||
const pathParts = this.currentPath.split('/').filter(p => p);
|
||||
pathParts.pop();
|
||||
this.currentPath = '/' + pathParts.join('/');
|
||||
if (this.currentPath !== '/' && !this.currentPath.endsWith('/')) {
|
||||
this.currentPath += '/';
|
||||
}
|
||||
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
console.log('uploadFiles called');
|
||||
|
||||
// 检查是否已选择服务器
|
||||
if (!this.currentFileServerId) {
|
||||
this.showAlert('请先选择一个服务器', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Server selected:', this.currentFileServerId);
|
||||
|
||||
const files = document.getElementById('uploadFiles').files;
|
||||
const uploadPath = document.getElementById('uploadPath').value;
|
||||
|
||||
if (files.length === 0) {
|
||||
this.showAlert('请选择要上传的文件', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (let file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
formData.append('remotePath', uploadPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/upload-batch/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert(`成功上传 ${result.count} 个文件`, 'success');
|
||||
this.closeModal('uploadModal');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('上传失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
this.showAlert('上传文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(filename) {
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/download/${this.currentFileServerId}?remoteFilePath=${encodeURIComponent(filePath)}`);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
this.showAlert('文件下载成功', 'success');
|
||||
} else {
|
||||
this.showAlert('文件下载失败', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
this.showAlert('下载文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(filename, isDirectory) {
|
||||
if (!confirm(`确定要删除${isDirectory ? '目录' : '文件'} "${filename}" 吗?`)) return;
|
||||
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/delete/${this.currentFileServerId}?remotePath=${encodeURIComponent(filePath)}&isDirectory=${isDirectory}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('删除成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('删除失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.showAlert('删除失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async createFolder() {
|
||||
const folderName = prompt('请输入文件夹名称:');
|
||||
if (!folderName) return;
|
||||
|
||||
const folderPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + folderName :
|
||||
this.currentPath + '/' + folderName;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/mkdir/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ remotePath: folderPath })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('文件夹创建成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('创建失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文件夹失败:', error);
|
||||
this.showAlert('创建文件夹失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UI工具方法 ==========
|
||||
updateConnectionStatus(connected) {
|
||||
const statusElement = document.getElementById('connectionStatus');
|
||||
const connectBtn = document.querySelector('button[onclick="connectSSH()"]');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
|
||||
if (connected) {
|
||||
statusElement.innerHTML = `🟢 已连接 - ${this.currentServer.name}`;
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
} else {
|
||||
statusElement.innerHTML = '🔴 未连接';
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(message) {
|
||||
document.getElementById('statusBar').textContent = message;
|
||||
}
|
||||
|
||||
showAlert(message, type) {
|
||||
const container = document.getElementById('alertContainer');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// 5秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 全局函数 ==========
|
||||
let sshClient = null;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
sshClient = new SimpleWebSSHClient();
|
||||
});
|
||||
|
||||
// 页面切换
|
||||
function switchPage(pageName) {
|
||||
// 更新导航状态
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 找到对应的导航项并设为激活状态
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
const onclick = item.getAttribute('onclick');
|
||||
if (onclick && onclick.includes(`switchPage('${pageName}')`)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 切换页面内容
|
||||
document.querySelectorAll('.page-content').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`page-${pageName}`).classList.add('active');
|
||||
|
||||
// 根据页面执行特定操作
|
||||
if (pageName === 'files') {
|
||||
sshClient.loadSavedServers().then(() => {
|
||||
// 如果当前有连接的服务器,自动选择它
|
||||
sshClient.switchToFileManagerWithCurrentServer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏折叠
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const title = document.getElementById('sidebarTitle');
|
||||
const navTexts = document.querySelectorAll('.nav-text');
|
||||
|
||||
sidebar.classList.toggle('collapsed');
|
||||
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
title.style.display = 'none';
|
||||
navTexts.forEach(text => text.style.display = 'none');
|
||||
} else {
|
||||
title.style.display = 'inline';
|
||||
navTexts.forEach(text => text.style.display = 'inline');
|
||||
}
|
||||
}
|
||||
|
||||
// SSH连接相关
|
||||
function connectSSH() {
|
||||
const host = document.getElementById('host').value.trim();
|
||||
const port = document.getElementById('port').value.trim();
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
if (!host || !username || !password) {
|
||||
sshClient.showAlert('请填写完整的连接信息', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
sshClient.connect(host, port || 22, username, password);
|
||||
}
|
||||
|
||||
function disconnectSSH() {
|
||||
sshClient.disconnect();
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
sshClient.testConnection();
|
||||
}
|
||||
|
||||
function loadSavedServers() {
|
||||
sshClient.loadSavedServers();
|
||||
}
|
||||
|
||||
function loadServerConfig() {
|
||||
sshClient.loadServerConfig();
|
||||
}
|
||||
|
||||
// 文件管理相关
|
||||
function switchFileServer() {
|
||||
sshClient.switchFileServer();
|
||||
}
|
||||
|
||||
function refreshFiles() {
|
||||
sshClient.refreshFiles();
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
sshClient.navigateUp();
|
||||
}
|
||||
|
||||
function showUploadModal() {
|
||||
document.getElementById('uploadModal').classList.add('active');
|
||||
document.getElementById('uploadPath').value = sshClient.currentPath || '/';
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
console.log('handleUpload called');
|
||||
try {
|
||||
sshClient.uploadFiles();
|
||||
} catch (error) {
|
||||
console.error('Error in handleUpload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFiles(event) {
|
||||
sshClient.uploadFiles(event);
|
||||
}
|
||||
|
||||
function downloadFile(filename) {
|
||||
sshClient.downloadFile(filename);
|
||||
}
|
||||
|
||||
function deleteFile(filename, isDirectory) {
|
||||
sshClient.deleteFile(filename, isDirectory);
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
sshClient.createFolder();
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
// 点击弹窗背景关闭弹窗
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Escape 关闭弹窗
|
||||
if (event.key === 'Escape') {
|
||||
document.querySelectorAll('.modal.active').forEach(modal => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Ctrl+Enter 快速连接
|
||||
if (event.ctrlKey && event.key === 'Enter' && !sshClient.connected) {
|
||||
connectSSH();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时断开连接
|
||||
window.addEventListener('beforeunload', function(event) {
|
||||
if (sshClient && sshClient.connected) {
|
||||
sshClient.disconnect();
|
||||
}
|
||||
});
|
||||
BIN
src/main/resources/static/assets/logo.03d6d6da.png
Normal file
BIN
src/main/resources/static/assets/logo.03d6d6da.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
28
src/main/resources/static/assets/vendor.a8167e4a.js
Normal file
28
src/main/resources/static/assets/vendor.a8167e4a.js
Normal file
File diff suppressed because one or more lines are too long
BIN
src/main/resources/static/favicon.ico
Normal file
BIN
src/main/resources/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
16
src/main/resources/static/index.html
Normal file
16
src/main/resources/static/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!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>Vite App</title>
|
||||
<script type="module" crossorigin src="/cApi/assets/index.8dd9ceba.js"></script>
|
||||
<link rel="modulepreload" href="/cApi/assets/vendor.a8167e4a.js">
|
||||
<link rel="stylesheet" href="/cApi/assets/index.6a28b391.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,77 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" th:href="@{/assets/css/login-style.css}">
|
||||
<title>cApi登录</title>
|
||||
<script>
|
||||
/* 如果当前窗口不是顶层窗口,就让顶层窗口跳转到登录页 */
|
||||
if (window.top !== window) {
|
||||
window.top.location.href = location.href;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 左侧品牌区域 -->
|
||||
<div class="brand-side">
|
||||
<div class="brand-icon">
|
||||
<i class="bi bi-shield-lock" style="color: #ecf0f1;"></i>
|
||||
<i class="bi bi-key" style="color: #2ecc71;"></i>
|
||||
<i class="bi bi-server" style="color: #1abc9c;"></i>
|
||||
</div>
|
||||
<div class="brand-title-group">
|
||||
<h1>cApi管理系统</h1>
|
||||
<div class="brand-divider"></div>
|
||||
<p>安全、高效的一站式管理解决方案,为您的业务保驾护航</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧登录容器:固定宽度,避免变形 -->
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h2>系统登录</h2>
|
||||
</div>
|
||||
<form class="login-form" onsubmit="return handleLogin(event)">
|
||||
<!-- 用户名输入组:结构扁平化,避免嵌套冲突 -->
|
||||
<div class="form-group">
|
||||
<label for="username">
|
||||
账户
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<!-- 输入框容器:仅包裹输入框+功能按钮,固定定位规则 -->
|
||||
<div class="input-group">
|
||||
<input type="text" id="username" placeholder="请输入用户名" required>
|
||||
<i class="bi bi-person input-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入组:同用户名结构,保持一致性 -->
|
||||
<div class="form-group">
|
||||
<label for="password">
|
||||
密码
|
||||
<span class="required-mark">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" id="password" placeholder="请输入密码" required>
|
||||
<i class="bi bi-lock input-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-login">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示弹窗:固定层级,避免被遮挡 -->
|
||||
<div class="modal" id="errorModal">
|
||||
<div class="modal-content">
|
||||
<i class="bi bi-exclamation-circle icon"></i>
|
||||
<p id="errorMsg"></p>
|
||||
<button onclick="closeModal()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/assets/js/login-script.js}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,573 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>表结构信息</title>
|
||||
<style>
|
||||
/* ===== 全局 & 布局(沿用原样式,略) ===== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 10px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
min-height: calc(100vh - 10px);
|
||||
}
|
||||
|
||||
.left-column {
|
||||
flex: 0 0 520px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.time-info p {
|
||||
margin-bottom: 4px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 12px;
|
||||
color: #6c757d;
|
||||
font-size: .85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* ===== 标签栏 ===== */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
padding: 0 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 24px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #5f6368;
|
||||
position: relative;
|
||||
transition: .2s;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(60, 64, 67, .08);
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #e8f0fe;
|
||||
color: #1967d2;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: #1967d2;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
/* ===== 内容区 ===== */
|
||||
.tab-content {
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 16px;
|
||||
height: calc(100vh - 120px);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
background: #1967d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #1557b0;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== 字段表格:行边框、无列边框 ===== */
|
||||
.table-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.field-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.field-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.field-table th, .field-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.field-table thead tr:first-child th {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.field-table th {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-table tr:nth-child(even) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.field-table tr:hover {
|
||||
background: #e8f5e9;
|
||||
}
|
||||
|
||||
.col-name {
|
||||
display: inline-block;
|
||||
font-family: "JetBrains Mono", SFMono-Regular, Consolas, monospace;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #0c4a6e; /* 深蓝字 */
|
||||
padding: 3px 8px 4px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, #e0f2fe 0%, #bae6fd 100%); /* 浅蓝渐变 */
|
||||
box-shadow: 0 1px 2px rgba(14, 165, 233, .15);
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
|
||||
.col-name:hover {
|
||||
box-shadow: 0 2px 6px rgba(14, 165, 233, .25);
|
||||
}
|
||||
|
||||
|
||||
/* ===== 代码块 ===== */
|
||||
.code-block {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
color: #202124;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== 对话框 ===== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, .1);
|
||||
width: 350px;
|
||||
max-width: 90%;
|
||||
padding: 24px;
|
||||
transform: translateY(-20px);
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #5f6368;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 24px;
|
||||
color: #3c4043;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 8px 16px;
|
||||
background: #1967d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
background: #1557b0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 左侧 -->
|
||||
<div class="left-column" th:each="t : ${data}">
|
||||
<div class="card table-info">
|
||||
<div class="card-header">表基础信息</div>
|
||||
<div class="detail">数据库: <span th:text="${t.dbName}"></span></div>
|
||||
<div class="detail">表名称: <strong th:text="${t.tableName}"></strong></div>
|
||||
<div class="detail">表描述: <span th:text="${t.tableDesc}"></span></div>
|
||||
<div class="detail">存储量: <span th:text="${t.dataLength}"></span></div>
|
||||
</div>
|
||||
<div class="card time-info">
|
||||
<div>创建时间: <span th:text="${t.createTime}"></span></div>
|
||||
<div>更新时间: <span th:text="${t.updateTime}"></span></div>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<div class="stat-item"><i class="fas fa-key" style="color:#165dff;"></i>
|
||||
<p>主键:<span th:text="${pkCnt}">0</span> 个</p></div>
|
||||
<div class="stat-item"><i class="fas fa-sitemap" style="color:#00864e;"></i>
|
||||
<p>索引:<span th:text="${idxCnt}">0</span> 个</p></div>
|
||||
<div class="stat-item"><i class="fas fa-list" style="color:#4096ff;"></i>
|
||||
<p>字段:<span th:text="${colCnt}">0</span> 个</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 -->
|
||||
<div class="right-column">
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="field">字段信息</div>
|
||||
<div class="tab" data-tab="select">生成SELECT</div>
|
||||
<div class="tab" data-tab="ddl">生成DDL</div>
|
||||
</div>
|
||||
|
||||
<!-- 字段信息 -->
|
||||
<div class="tab-content active" id="field-content">
|
||||
<div class="content-header">
|
||||
<div class="content-title">字段信息</div>
|
||||
<button class="action-btn" id="export-field">导出 ↓</button>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<div class="table-container">
|
||||
<table class="field-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th>
|
||||
<th>字段名称</th>
|
||||
<th>字段类型</th>
|
||||
<th>描述</th>
|
||||
<th>键类型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody th:each="c : ${columns}">
|
||||
<tr>
|
||||
<td th:text="${c.sort}"></td>
|
||||
<td><span class="col-name" th:text="${c.colName}">f_user_name</span></td>
|
||||
<td th:text="${c.colType}"></td>
|
||||
<td th:text="${c.colDesc}"></td>
|
||||
<td th:text="${c.keyType}"></td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SELECT -->
|
||||
<div class="tab-content" id="select-content">
|
||||
<div class="content-header">
|
||||
<div class="content-title">SELECT语句</div>
|
||||
<button class="action-btn" id="copy-select">复制SQL</button>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<pre class="code-block" th:text="${selectSql}"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DDL -->
|
||||
<div class="tab-content" id="ddl-content">
|
||||
<div class="content-header">
|
||||
<div class="content-title">DDL语句</div>
|
||||
<button class="action-btn" id="copy-ddl">复制DDL</button>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<pre class="code-block" th:text="${ddlSql}"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话框 -->
|
||||
<div class="modal-overlay" id="modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="modal-title">提示</div>
|
||||
<button class="modal-close" id="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-message">这是一条提示信息</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-btn" id="modal-confirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* 对话框 */
|
||||
const modal = document.getElementById('modal');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const modalMessage = document.getElementById('modal-message');
|
||||
const modalClose = document.getElementById('modal-close');
|
||||
const modalConfirm = document.getElementById('modal-confirm');
|
||||
|
||||
function showModal(title, message) {
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.textContent = message;
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
modalClose.addEventListener('click', hideModal);
|
||||
modalConfirm.addEventListener('click', hideModal);
|
||||
modal.addEventListener('click', e => {
|
||||
if (e.target === modal) hideModal();
|
||||
});
|
||||
|
||||
/* 标签切换 */
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById(`${tab.dataset.tab}-content`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
/* 复制/导出 */
|
||||
/* 优化后的复制功能:支持HTTPS/HTTP、兼容旧浏览器、增强错误处理 */
|
||||
// 通用复制函数:接收代码块容器ID,返回复制结果
|
||||
async function copyToClipboard(codeBlockContainerId) {
|
||||
try {
|
||||
// 1. 安全获取代码块元素(避免选择器失效)
|
||||
const container = document.getElementById(codeBlockContainerId);
|
||||
if (!container) {
|
||||
throw new Error("复制失败:未找到代码块容器,请检查页面结构");
|
||||
}
|
||||
const codeBlock = container.querySelector(".code-block");
|
||||
if (!codeBlock) {
|
||||
throw new Error("复制失败:代码块元素不存在");
|
||||
}
|
||||
|
||||
// 2. 获取纯净文本(排除HTML标签,处理换行格式)
|
||||
const codeText = codeBlock.textContent.trim();
|
||||
if (!codeText) {
|
||||
throw new Error("复制失败:代码块内容为空");
|
||||
}
|
||||
|
||||
// 3. 优先使用现代剪贴板API(HTTPS环境),降级使用execCommand(HTTP/旧浏览器)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// 现代浏览器:HTTPS环境下使用clipboard API
|
||||
await navigator.clipboard.writeText(codeText);
|
||||
} else {
|
||||
// 降级方案:创建临时文本框,用execCommand复制(兼容HTTP/旧浏览器)
|
||||
const tempTextarea = document.createElement("textarea");
|
||||
// 隐藏临时文本框(避免影响页面布局)
|
||||
tempTextarea.style.position = "fixed";
|
||||
tempTextarea.style.top = "-999px";
|
||||
tempTextarea.style.left = "-999px";
|
||||
tempTextarea.value = codeText;
|
||||
document.body.appendChild(tempTextarea);
|
||||
|
||||
// 选中文本并复制
|
||||
tempTextarea.select();
|
||||
const copySuccess = document.execCommand("copy");
|
||||
document.body.removeChild(tempTextarea); // 复制后删除临时元素
|
||||
|
||||
if (!copySuccess) {
|
||||
throw new Error("复制失败:浏览器不支持传统复制方法,请升级浏览器或切换HTTPS环境");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 复制成功提示
|
||||
showModal("复制成功", "代码已成功复制到剪贴板,可直接粘贴使用");
|
||||
} catch (err) {
|
||||
// 5. 区分错误类型,给出明确提示(方便生产环境排查)
|
||||
let errorMsg = "复制失败:";
|
||||
if (err.message.includes("HTTPS")) {
|
||||
errorMsg += "当前为HTTP环境,建议切换到HTTPS以使用更稳定的复制功能";
|
||||
} else if (err.message.includes("不存在") || err.message.includes("容器")) {
|
||||
errorMsg += "页面元素异常,请刷新页面重试";
|
||||
} else if (err.message.includes("空")) {
|
||||
errorMsg += "代码块无内容,无需复制";
|
||||
} else {
|
||||
errorMsg += err.message || "未知错误,请检查浏览器权限";
|
||||
}
|
||||
showModal("复制失败", errorMsg);
|
||||
console.error("复制功能错误详情:", err); // 生产环境可上报日志
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定SELECT复制按钮事件(传入SELECT代码块容器ID)
|
||||
document.getElementById("copy-select").addEventListener("click", () => {
|
||||
copyToClipboard("select-content");
|
||||
});
|
||||
|
||||
// 绑定DDL复制按钮事件(传入DDL代码块容器ID)
|
||||
document.getElementById("copy-ddl").addEventListener("click", () => {
|
||||
copyToClipboard("ddl-content");
|
||||
});
|
||||
|
||||
// 导出功能提示优化(原功能待实现,增强用户体验)
|
||||
document.getElementById("export-field").addEventListener("click", () => {
|
||||
showModal("提示", "导出功能暂未实现,您可先复制字段信息表格内容");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,197 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>数据库表管理</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
secondary: '#64748B',
|
||||
accent: '#10B981',
|
||||
neutral: '#F8FAFC',
|
||||
'neutral-dark': '#1E293B',
|
||||
},
|
||||
fontFamily: {inter: ['Inter', 'system-ui', 'sans-serif']},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
/* 弹窗尺寸配置 */
|
||||
#detailDialog {
|
||||
width: 65vw;
|
||||
height: 80vh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
/* 表格卡片hover交互效果(补充体验优化) */
|
||||
.card-hover {
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 font-inter text-neutral-dark">
|
||||
|
||||
<!-- 顶部搜索栏 -->
|
||||
<header class="fixed top-0 inset-x-0 z-30 bg-white shadow-md">
|
||||
<div class="max-w-7xl mx-auto px-[30px] py-3 flex items-center gap-4">
|
||||
<!-- 数据库下拉框 -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<select id="dbSelect"
|
||||
class="appearance-none bg-gray-50 border border-gray-300 text-gray-700 py-2 px-4 pr-8 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50">
|
||||
<option value="">请选择数据库</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<i class="fa fa-chevron-down text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表名关键字输入框 -->
|
||||
<div class="relative flex-1 min-w-[200px]">
|
||||
<input id="searchInput" type="text" placeholder="请输入表关键字..."
|
||||
class="w-full bg-gray-50 border border-gray-300 text-gray-700 py-2 px-4 pr-10 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50"/>
|
||||
<button id="searchBtn" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-primary">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button id="queryBtn" class="bg-primary hover:bg-primary/90 text-white py-2 px-6 rounded-lg flex-shrink-0">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容:两侧间距改为15px -->
|
||||
<!-- 主内容 -->
|
||||
<main class="pt-20 pb-8">
|
||||
<div class="w-full px-[15px]">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="bg-white rounded-xl shadow-md px-4 py-2 flex items-center justify-end space-x-1">
|
||||
<span class="text-gray-500 text-sm">共</span>
|
||||
<span class="text-xl font-bold text-primary" id="totalCount">0</span>
|
||||
<span class="text-gray-500 text-sm">个对象</span>
|
||||
</div>
|
||||
|
||||
<!-- 表列表 -->
|
||||
<div id="tableList" class="space-y-6 mt-[5px]"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 表详情弹窗 -->
|
||||
<dialog id="detailDialog" class="rounded-xl shadow-2xl bg-white p-0 backdrop:bg-black/30">
|
||||
<iframe id="detailFrame" class="w-full h-full rounded-xl" frameborder="0"></iframe>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
/*****************************************************************
|
||||
* 工具函数
|
||||
*****************************************************************/
|
||||
const $ = sel => document.querySelector(sel);
|
||||
const $$ = sel => document.querySelectorAll(sel);
|
||||
|
||||
/* Toast 提示组件 */
|
||||
function showToast(msg) {
|
||||
const t = document.createElement('div');
|
||||
t.className = 'fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg z-50';
|
||||
t.textContent = msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 3000);
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* 1. 加载数据库下拉框数据
|
||||
*****************************************************************/
|
||||
fetch('getDbList')
|
||||
.then(r => r.json())
|
||||
.then(list => {
|
||||
const frag = document.createDocumentFragment();
|
||||
list.forEach(item => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = item.dbId || item;
|
||||
opt.textContent = item.dbName || item;
|
||||
frag.appendChild(opt);
|
||||
});
|
||||
$('#dbSelect').appendChild(frag);
|
||||
})
|
||||
.catch(() => showToast('获取数据库列表失败'));
|
||||
|
||||
/*****************************************************************
|
||||
* 2. 加载/渲染表格卡片列表
|
||||
*****************************************************************/
|
||||
function loadTableList() {
|
||||
const dbId = $('#dbSelect').value;
|
||||
const targetTable = $('#searchInput').value.trim();
|
||||
if (!dbId) {
|
||||
showToast('请先选择数据库');
|
||||
return;
|
||||
}
|
||||
fetch(`getTableList?dbId=${encodeURIComponent(dbId)}&targetTable=${encodeURIComponent(targetTable)}`)
|
||||
.then(r => r.json())
|
||||
.then(list => {
|
||||
$('#totalCount').textContent = list.length;
|
||||
const box = $('#tableList');
|
||||
box.innerHTML = '';
|
||||
list.forEach(item => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-white rounded-xl shadow-md overflow-hidden card-hover';
|
||||
card.innerHTML = `
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-blue-100 p-3 rounded-lg"><i class="fa fa-table text-primary text-xl"></i></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 cursor-pointer hover:text-primary" onclick="openDetailDialog('${item.taskId}')">
|
||||
${item.targetTable}
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm mt-1">表说明: ${item.taskName || '暂无'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-2 text-sm">
|
||||
<div><p class="text-gray-500">源端数据库</p><p class="font-medium">${item.dbType}</p></div>
|
||||
<div><p class="text-gray-500">创建时间</p><p class="font-medium">${item.createTime || '-'}</p></div>
|
||||
<div><p class="text-gray-500">更新时间</p><p class="font-medium">${item.updateTime || '-'}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 text-gray-600"><span class="font-medium">存储量:</span> ${item.successRows || '-'}条</div>
|
||||
</div>`;
|
||||
box.appendChild(card);
|
||||
});
|
||||
})
|
||||
.catch(() => showToast('获取表列表失败'));
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* 3. 事件绑定
|
||||
*****************************************************************/
|
||||
$('#queryBtn').addEventListener('click', loadTableList);
|
||||
$('#searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') loadTableList();
|
||||
});
|
||||
$('#searchBtn').addEventListener('click', loadTableList);
|
||||
|
||||
/*****************************************************************
|
||||
* 4. 表详情弹窗控制
|
||||
*****************************************************************/
|
||||
function openDetailDialog(taskId) {
|
||||
const dialog = $('#detailDialog');
|
||||
const frame = $('#detailFrame');
|
||||
frame.src = `getTableDetail?taskId=${encodeURIComponent(taskId)}`;
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
// 点击弹窗遮罩关闭弹窗
|
||||
$('#detailDialog').addEventListener('click', e => {
|
||||
if (e.target === $('#detailDialog')) $('#detailDialog').close();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,704 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>智能化管理系统</title>
|
||||
<!-- 引入Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 引入Font Awesome图标库 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
|
||||
<!-- 配置Tailwind自定义颜色和字体 -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#165DFF', // 深蓝色 - 主色调
|
||||
'primary-light': '#E8F0FE', // 淡蓝色 - 辅助色
|
||||
secondary: '#0FC6C2', // 青色 - 次要强调色
|
||||
neutral: '#F5F7FA', // 浅灰 - 背景色
|
||||
dark: '#1D2129', // 深灰 - 文本色
|
||||
light: '#F2F3F5', // 更浅的灰 - 悬停背景
|
||||
'tab-border': '#DADCE0', // 标签边框色
|
||||
'tab-separator': '#E8EAED' // 标签分隔线色
|
||||
},
|
||||
fontSize: {
|
||||
'sm': '0.875rem',
|
||||
'base': '0.9375rem',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply transition-all duration-300 hover:shadow-lg hover:-translate-y-1;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@apply flex items-center gap-2 px-4 py-2.5 rounded-lg cursor-pointer transition-all duration-200 text-sm;
|
||||
}
|
||||
|
||||
.menu-item-active {
|
||||
@apply bg-primary/10 text-primary font-medium;
|
||||
}
|
||||
|
||||
.menu-item-inactive {
|
||||
@apply hover:bg-light;
|
||||
}
|
||||
|
||||
.modal-enter {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.modal-exit {
|
||||
animation: fadeOut 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.google-tab {
|
||||
@apply relative px-3.5 py-1.5 cursor-pointer flex items-center space-x-1 text-xs transition-all duration-200 rounded-t-md border-y border-l border-transparent;
|
||||
min-width: 80px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 1px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.google-tab-active {
|
||||
@apply bg-primary text-white border-primary font-medium z-10;
|
||||
box-shadow: 0 -2px 0 0 rgba(255,255,255,0.3) inset;
|
||||
}
|
||||
|
||||
.google-tab-inactive {
|
||||
@apply bg-primary-light text-primary/80 hover:bg-primary-light/80 border-transparent;
|
||||
}
|
||||
|
||||
.google-tab-inactive:hover .close-tab {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.close-tab {
|
||||
@apply w-4 h-4 flex items-center justify-center rounded-full hover:bg-white/20 text-xs opacity-0 transition-opacity duration-200;
|
||||
}
|
||||
|
||||
.google-tab-active .close-tab {
|
||||
@apply opacity-100 text-white;
|
||||
}
|
||||
|
||||
.google-tab-active .close-tab:hover {
|
||||
@apply bg-white/20;
|
||||
}
|
||||
|
||||
/* 标签右键菜单样式 */
|
||||
.tab-contextmenu {
|
||||
@apply absolute bg-white rounded-lg shadow-lg py-1 z-50 border border-gray-200 w-48 hidden text-sm;
|
||||
}
|
||||
|
||||
.tab-contextmenu-item {
|
||||
@apply flex items-center gap-2 px-4 py-1.5 hover:bg-light transition-colors duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
/* 提示对话框样式 */
|
||||
.info-toast {
|
||||
@apply fixed bg-white shadow-lg rounded-lg p-4 border border-gray-200 z-50 flex items-center gap-3 transform transition-all duration-300 opacity-0 translate-y-[-20px];
|
||||
min-width: 280px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.info-toast i {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
/* 用户菜单样式 */
|
||||
.user-menu {
|
||||
@apply absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg z-50 border border-gray-100 overflow-hidden;
|
||||
}
|
||||
|
||||
.user-menu-actions {
|
||||
@apply py-2;
|
||||
}
|
||||
|
||||
.user-menu-action-item {
|
||||
@apply flex items-center gap-3 px-5 py-2.5 text-sm hover:bg-light transition-colors duration-200 cursor-pointer w-full text-left;
|
||||
}
|
||||
|
||||
/* 核心:彻底隐藏滚动条(跨浏览器兼容) */
|
||||
.scrollbar-hide {
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
/* IE/Edge */
|
||||
-ms-overflow-style: none;
|
||||
/* 禁止横向/纵向滚动 */
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
/* Chrome/Safari/Opera */
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/* 主内容区容器:精确设置四周边距 */
|
||||
.main-content-wrapper {
|
||||
@apply w-full h-[calc(100%-25px)] mt-4 mb-2.5 px-2.5; /* 上15px 下10px 左右10px(精确匹配) */
|
||||
}
|
||||
|
||||
.main-content-container {
|
||||
@apply w-full h-full overflow-hidden bg-white rounded-lg shadow-sm border border-gray-100; /* 容器样式 */
|
||||
}
|
||||
|
||||
/* 全局容器样式 */
|
||||
.app-container {
|
||||
@apply h-screen flex flex-col overflow-hidden;
|
||||
}
|
||||
|
||||
/* 对话框菜单按钮样式 */
|
||||
.modal-menu-btn {
|
||||
@apply flex items-center gap-2 px-4 py-3 border border-gray-100 rounded-lg hover:border-primary hover:bg-primary/5 transition-colors cursor-pointer w-full justify-start;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px) scale(0.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; transform: translateY(0) scale(1); }
|
||||
to { opacity: 0; transform: translateY(-10px) scale(0.98); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<!-- 全局容器:限制高度为屏幕高度,禁止滚动 -->
|
||||
<body class="bg-neutral font-sans text-dark text-sm overflow-hidden">
|
||||
<div class="app-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="bg-primary-light shadow-sm sticky top-0 z-30 h-14 border-b border-gray-200">
|
||||
<!-- 其余内容保持不变 -->
|
||||
<div class="flex items-center h-full px-6">
|
||||
<!-- 系统 Logo + 名称 -->
|
||||
<div class="flex items-center space-x-2.5 flex-shrink-0">
|
||||
<div class="w-9 h-9 rounded-lg bg-primary flex items-center justify-center">
|
||||
<i class="fa fa-cubes text-white text-base"></i>
|
||||
</div>
|
||||
<h1 class="text-base font-bold whitespace-nowrap">智能管理平台</h1>
|
||||
</div>
|
||||
|
||||
<!-- 模块导航 -->
|
||||
<nav class="flex-1 flex justify-center items-center space-x-1" id="moduleContainer">
|
||||
<div class="menu-item menu-item-inactive" id="loadingModules">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<span>加载模块中...</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="relative flex-shrink-0" id="adminContainer">
|
||||
<div class="menu-item menu-item-inactive flex items-center cursor-pointer" id="adminUser">
|
||||
<i class="fa fa-user-circle"></i>
|
||||
<span class="ml-2">加载用户中...</span>
|
||||
<i class="fa fa-angle-down ml-1"></i>
|
||||
</div>
|
||||
<!-- 用户下拉菜单 -->
|
||||
<div class="user-menu hidden" id="adminMenu">
|
||||
<div class="user-menu-actions">
|
||||
<div class="user-menu-action-item" id="logoutBtn">
|
||||
<i class="fa fa-sign-out text-gray-500 w-4 text-center"></i>
|
||||
<span>退出系统</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 标签栏(无滚动条) -->
|
||||
<div class="bg-white border-b border-tab-border relative z-20 h-9">
|
||||
<div class="scrollbar-hide h-full w-full">
|
||||
<div class="flex items-center space-x-0.5 px-3 min-w-max h-full" id="tabContainer">
|
||||
<!-- 默认控制台标签 -->
|
||||
<div class="google-tab google-tab-active" data-tab-id="console">
|
||||
<i class="fa fa-home text-xs mr-1"></i>
|
||||
<span>控制台</span>
|
||||
<i class="fa fa-times close-tab ml-1" onclick="event.stopPropagation();"></i>
|
||||
</div>
|
||||
<!-- 新建标签按钮 -->
|
||||
<button id="newTabBtn" class="w-8 h-8 flex items-center justify-center text-gray-500 hover:bg-gray-200 rounded-md transition-colors mx-0.5">
|
||||
<i class="fa fa-plus text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签右键菜单 -->
|
||||
<div class="tab-contextmenu" id="tabContextMenu">
|
||||
<div class="tab-contextmenu-item" id="closeOtherTabs">
|
||||
<i class="fa fa-window-close-o text-gray-500 text-xs"></i>
|
||||
<span>关闭其他标签</span>
|
||||
</div>
|
||||
<div class="tab-contextmenu-item" id="closeAllTabs">
|
||||
<i class="fa fa-times-circle text-gray-500 text-xs"></i>
|
||||
<span>关闭全部标签</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区(精确设置四周边距) -->
|
||||
<main class="flex-1 bg-neutral overflow-hidden p-0">
|
||||
<!-- 外层容器,实现精确的10px左右间距和其他边距 -->
|
||||
<div class="main-content-wrapper">
|
||||
<div class="main-content-container">
|
||||
<iframe id="contentFrame" src="home" class="w-full border-0" style="overflow: hidden;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 信息提示框 -->
|
||||
<div id="infoToast" class="info-toast">
|
||||
<i class="fa fa-info-circle text-primary text-lg"></i>
|
||||
<p>请从顶部模块菜单选择要打开的功能</p>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗背景遮罩 -->
|
||||
<div id="modalOverlay" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden opacity-0 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- 动态菜单弹窗 -->
|
||||
<div id="dynamicModal" class="fixed bg-white rounded-xl shadow-xl z-50 hidden" style="width: 70%; left: 15%; top: 10%;">
|
||||
<div class="p-5 border-b flex justify-between items-center">
|
||||
<h3 class="text-base font-semibold flex items-center" id="modalTitle">
|
||||
<i class="modal-icon fa text-primary mr-2 text-sm"></i>
|
||||
<span id="modalModuleName"></span>
|
||||
</h3>
|
||||
<button class="modal-close text-gray-400 hover:text-gray-600">
|
||||
<i class="fa fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5" id="menuContainer">
|
||||
<div class="flex justify-center items-center h-40">
|
||||
<i class="fa fa-spinner fa-spin text-xl text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 退出确认弹窗 -->
|
||||
<div id="logoutModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-[60] hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl shadow-xl w-96 p-5">
|
||||
<h3 class="text-base font-semibold mb-4 flex items-center">
|
||||
<i class="fa fa-exclamation-triangle text-yellow-500 mr-2 text-sm"></i>
|
||||
确认退出
|
||||
</h3>
|
||||
<p class="text-gray-600 mb-5 text-sm">您确定要退出系统吗?</p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button id="cancelLogout" class="px-3.5 py-1.5 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors text-sm">
|
||||
取消
|
||||
</button>
|
||||
<button id="confirmLogout" class="px-3.5 py-1.5 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm">
|
||||
确认退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 模拟API请求函数
|
||||
const api = {
|
||||
getModules: () => fetch('Sys/login/modules').then(res => res.json()),
|
||||
getMenusByModuleCode: (moduleCode) => fetch(`Sys/login/menus?moduleCode=${moduleCode}`).then(res => res.json()),
|
||||
getCurrentUser: () => fetch('Sys/login/me').then(res => res.json())
|
||||
};
|
||||
|
||||
// 标签管理核心数据
|
||||
let tabs = [{ id: 'console', name: '控制台', url: 'home', closable: false, icon: 'fa-home' }];
|
||||
let activeTabId = 'console';
|
||||
let currentRightClickTabId = 'console';
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 元素获取
|
||||
const moduleContainer = document.getElementById('moduleContainer');
|
||||
const adminUser = document.getElementById('adminUser');
|
||||
const adminMenu = document.getElementById('adminMenu');
|
||||
const menuContainer = document.getElementById('menuContainer');
|
||||
const modalModuleName = document.getElementById('modalModuleName');
|
||||
const modalIcon = document.querySelector('.modal-icon');
|
||||
const dynamicModal = document.getElementById('dynamicModal');
|
||||
const modalOverlay = document.getElementById('modalOverlay');
|
||||
const modalCloseButtons = document.querySelectorAll('.modal-close');
|
||||
const tabContainer = document.getElementById('tabContainer');
|
||||
const contentFrame = document.getElementById('contentFrame');
|
||||
const logoutModal = document.getElementById('logoutModal');
|
||||
const cancelLogout = document.getElementById('cancelLogout');
|
||||
const confirmLogout = document.getElementById('confirmLogout');
|
||||
const newTabBtn = document.getElementById('newTabBtn');
|
||||
const infoToast = document.getElementById('infoToast');
|
||||
const tabContextMenu = document.getElementById('tabContextMenu');
|
||||
const closeOtherTabsBtn = document.getElementById('closeOtherTabs');
|
||||
const closeAllTabsBtn = document.getElementById('closeAllTabs');
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
|
||||
// 初始化功能
|
||||
loadModules();
|
||||
loadCurrentUser();
|
||||
initTabContextMenu();
|
||||
bindModalEvents();
|
||||
bindLogoutEvents();
|
||||
initContentHeight(); // 初始化iframe高度(考虑所有边距)
|
||||
window.addEventListener('resize', initContentHeight); // 窗口 resize 时重新计算高度
|
||||
|
||||
// 新建标签按钮事件
|
||||
newTabBtn.addEventListener('click', () => showInfoToast('请从顶部模块菜单选择要打开的功能'));
|
||||
|
||||
// 用户菜单切换
|
||||
adminUser.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
adminMenu.classList.toggle('hidden');
|
||||
});
|
||||
// 点击页面其他区域关闭用户菜单
|
||||
document.addEventListener('click', () => adminMenu.classList.add('hidden'));
|
||||
|
||||
/**
|
||||
* 初始化主内容区高度(精确计算所有边距影响)
|
||||
*/
|
||||
function initContentHeight() {
|
||||
// 强制设置body高度为窗口高度
|
||||
document.body.style.height = `${window.innerHeight}px`;
|
||||
|
||||
const headerHeight = document.querySelector('header').offsetHeight; // 顶部导航栏高度
|
||||
const tabBarHeight = document.querySelector('.border-tab-border').offsetHeight; // 标签栏高度
|
||||
const topSpacing = 16; // 与标签栏的顶部间距(px)
|
||||
const bottomSpacing = 10; // 与底部的间距(px)
|
||||
// 主内容区高度 = 窗口高度 - 顶部导航栏高度 - 标签栏高度 - 顶部间距 - 底部间距
|
||||
const contentHeight = window.innerHeight - headerHeight - tabBarHeight - topSpacing - bottomSpacing;
|
||||
|
||||
// 强制设置iframe高度,禁止内部滚动
|
||||
contentFrame.style.height = `${contentHeight}px`;
|
||||
contentFrame.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示信息提示框
|
||||
*/
|
||||
function showInfoToast(message, duration = 2000) {
|
||||
infoToast.querySelector('p').textContent = message;
|
||||
infoToast.style.left = '50%';
|
||||
infoToast.style.top = '20%';
|
||||
infoToast.style.transform = 'translate(-50%, 0)';
|
||||
infoToast.classList.remove('opacity-0', 'translate-y-[-20px]');
|
||||
infoToast.classList.add('opacity-100', 'translate-y-0');
|
||||
|
||||
setTimeout(() => {
|
||||
infoToast.classList.remove('opacity-100', 'translate-y-0');
|
||||
infoToast.classList.add('opacity-0', 'translate-y-[-20px]');
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化标签右键菜单功能
|
||||
*/
|
||||
function initTabContextMenu() {
|
||||
tabContainer.addEventListener('contextmenu', function (e) {
|
||||
const targetTab = e.target.closest('[data-tab-id]');
|
||||
if (targetTab) {
|
||||
e.preventDefault();
|
||||
currentRightClickTabId = targetTab.getAttribute('data-tab-id');
|
||||
const menuWidth = tabContextMenu.offsetWidth;
|
||||
const menuHeight = tabContextMenu.offsetHeight;
|
||||
tabContextMenu.style.left = `${Math.min(e.clientX, window.innerWidth - menuWidth)}px`;
|
||||
tabContextMenu.style.top = `${Math.min(e.clientY, window.innerHeight - menuHeight)}px`;
|
||||
tabContextMenu.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => tabContextMenu.classList.add('hidden'));
|
||||
tabContextMenu.addEventListener('click', (e) => e.stopPropagation());
|
||||
|
||||
closeOtherTabsBtn.addEventListener('click', function () {
|
||||
tabs = tabs.filter(tab => tab.id === currentRightClickTabId || tab.id === 'console');
|
||||
if (!tabs.some(tab => tab.id === activeTabId)) {
|
||||
activeTabId = 'console';
|
||||
contentFrame.src = 'home';
|
||||
}
|
||||
renderTabs();
|
||||
tabContextMenu.classList.add('hidden');
|
||||
});
|
||||
|
||||
closeAllTabsBtn.addEventListener('click', function () {
|
||||
tabs = [{ id: 'console', name: '控制台', url: 'home', closable: false, icon: 'fa-home' }];
|
||||
activeTabId = 'console';
|
||||
contentFrame.src = 'home';
|
||||
renderTabs();
|
||||
tabContextMenu.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载顶部模块导航
|
||||
*/
|
||||
async function loadModules() {
|
||||
try {
|
||||
const modules = await api.getModules();
|
||||
moduleContainer.innerHTML = '';
|
||||
modules.forEach(module => {
|
||||
const moduleElement = document.createElement('div');
|
||||
moduleElement.className = 'menu-item menu-item-inactive';
|
||||
moduleElement.setAttribute('data-module-code', module.moduleCode);
|
||||
moduleElement.setAttribute('data-module-name', module.moduleName);
|
||||
moduleElement.setAttribute('data-module-icon', module.cicon);
|
||||
moduleElement.innerHTML = `
|
||||
<i class="fa ${module.cicon}"></i>
|
||||
<span>${module.moduleName}</span>
|
||||
`;
|
||||
moduleElement.addEventListener('click', function () {
|
||||
const code = this.getAttribute('data-module-code');
|
||||
const name = this.getAttribute('data-module-name');
|
||||
const icon = this.getAttribute('data-module-icon');
|
||||
showModuleMenus(code, name, icon, this);
|
||||
});
|
||||
moduleContainer.appendChild(moduleElement);
|
||||
});
|
||||
} catch (error) {
|
||||
moduleContainer.innerHTML = `
|
||||
<div class="menu-item menu-item-inactive text-red-500">
|
||||
<i class="fa fa-exclamation-circle"></i>
|
||||
<span>模块加载失败</span>
|
||||
</div>
|
||||
`;
|
||||
console.error('加载模块失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载当前用户信息
|
||||
*/
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const user = await api.getCurrentUser();
|
||||
const usernameElement = adminUser.querySelector('span');
|
||||
usernameElement.textContent = user.uname;
|
||||
|
||||
// 绑定退出按钮事件
|
||||
logoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
adminMenu.classList.add('hidden');
|
||||
logoutModal.classList.remove('hidden');
|
||||
});
|
||||
} catch (error) {
|
||||
const usernameElement = adminUser.querySelector('span');
|
||||
usernameElement.textContent = '用户信息加载失败';
|
||||
console.error('加载用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模块菜单弹窗
|
||||
*/
|
||||
async function showModuleMenus(moduleCode, moduleName, moduleIcon, menuItemElement) {
|
||||
try {
|
||||
modalModuleName.textContent = `${moduleName}菜单`;
|
||||
modalIcon.className = `modal-icon fa ${moduleIcon} text-primary mr-2 text-sm`;
|
||||
|
||||
menuContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center h-40">
|
||||
<i class="fa fa-spinner fa-spin text-xl text-gray-400"></i>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const menus = await api.getMenusByModuleCode(moduleCode);
|
||||
|
||||
if (menus.length > 0) {
|
||||
// 菜单布局:图标在前、名称在后,使用网格布局排列
|
||||
let menuHtml = '<div class="grid grid-cols-3 gap-4">';
|
||||
menus.forEach(menu => {
|
||||
menuHtml += `
|
||||
<button class="modal-menu-btn"
|
||||
onclick="openTab('${menu.menuCode}', '${menu.menuName}', '${menu.chref}', '${menu.cicon}')">
|
||||
<i class="fa ${menu.cicon} text-primary text-base"></i>
|
||||
<span class="text-sm">${menu.menuName}</span>
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
menuHtml += '</div>';
|
||||
menuContainer.innerHTML = menuHtml;
|
||||
} else {
|
||||
menuContainer.innerHTML = `
|
||||
<div class="flex flex-col justify-center items-center h-40 text-gray-500">
|
||||
<i class="fa fa-folder-open-o text-xl mb-2"></i>
|
||||
<p class="text-sm">该模块暂无菜单</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.classList.remove('menu-item-active');
|
||||
item.classList.add('menu-item-inactive');
|
||||
});
|
||||
menuItemElement.classList.remove('menu-item-inactive');
|
||||
menuItemElement.classList.add('menu-item-active');
|
||||
|
||||
dynamicModal.classList.remove('hidden');
|
||||
dynamicModal.classList.add('modal-enter');
|
||||
modalOverlay.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
modalOverlay.style.opacity = '1';
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} catch (error) {
|
||||
menuContainer.innerHTML = `
|
||||
<div class="flex flex-col justify-center items-center h-40 text-red-500">
|
||||
<i class="fa fa-exclamation-circle text-xl mb-2"></i>
|
||||
<p class="text-sm">菜单加载失败</p>
|
||||
</div>
|
||||
`;
|
||||
console.error('加载菜单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开新标签页
|
||||
*/
|
||||
function openTab(id, name, url, icon) {
|
||||
const existingTab = tabs.find(tab => tab.id === id);
|
||||
if (existingTab) {
|
||||
switchTab(id);
|
||||
} else {
|
||||
tabs.push({ id, name, url, closable: true, icon });
|
||||
renderTabs();
|
||||
switchTab(id);
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换标签页
|
||||
*/
|
||||
function switchTab(id) {
|
||||
activeTabId = id;
|
||||
const activeTab = tabs.find(tab => tab.id === id);
|
||||
if (activeTab) {
|
||||
contentFrame.src = activeTab.url;
|
||||
renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭指定标签页
|
||||
*/
|
||||
function closeTab(id) {
|
||||
if (id === 'console') return;
|
||||
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === id);
|
||||
if (tabIndex !== -1) {
|
||||
let newActiveTabId = activeTabId;
|
||||
if (activeTabId === id) {
|
||||
newActiveTabId = tabIndex > 0 ? tabs[tabIndex - 1].id : 'console';
|
||||
}
|
||||
|
||||
tabs.splice(tabIndex, 1);
|
||||
activeTabId = newActiveTabId;
|
||||
|
||||
const activeTab = tabs.find(tab => tab.id === activeTabId);
|
||||
if (activeTab) {
|
||||
contentFrame.src = activeTab.url;
|
||||
}
|
||||
|
||||
renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新渲染所有标签
|
||||
*/
|
||||
function renderTabs() {
|
||||
const tabContainer = document.getElementById('tabContainer');
|
||||
const newTabBtn = document.getElementById('newTabBtn');
|
||||
tabContainer.innerHTML = '';
|
||||
|
||||
tabs.forEach(tab => {
|
||||
const tabElement = document.createElement('div');
|
||||
tabElement.className = `google-tab ${tab.id === activeTabId ? 'google-tab-active' : 'google-tab-inactive'}`;
|
||||
tabElement.setAttribute('data-tab-id', tab.id);
|
||||
|
||||
tabElement.innerHTML = `
|
||||
<i class="fa ${tab.icon} text-xs mr-1"></i>
|
||||
<span>${tab.name}</span>
|
||||
<i class="fa fa-times close-tab ml-1" onclick="event.stopPropagation(); closeTab('${tab.id}')"></i>
|
||||
`;
|
||||
|
||||
tabElement.addEventListener('click', () => switchTab(tab.id));
|
||||
tabContainer.appendChild(tabElement);
|
||||
});
|
||||
|
||||
tabContainer.appendChild(newTabBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭模态框
|
||||
*/
|
||||
function closeModal() {
|
||||
const dynamicModal = document.getElementById('dynamicModal');
|
||||
const modalOverlay = document.getElementById('modalOverlay');
|
||||
|
||||
dynamicModal.classList.add('hidden');
|
||||
dynamicModal.classList.remove('modal-enter');
|
||||
modalOverlay.classList.add('hidden');
|
||||
modalOverlay.style.opacity = '0';
|
||||
document.body.style.overflow = 'hidden'; // 保持全局无滚动
|
||||
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.classList.remove('menu-item-active');
|
||||
item.classList.add('menu-item-inactive');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定弹窗关闭事件
|
||||
*/
|
||||
function bindModalEvents() {
|
||||
modalCloseButtons.forEach(button => {
|
||||
button.addEventListener('click', closeModal);
|
||||
});
|
||||
|
||||
modalOverlay.addEventListener('click', closeModal);
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
logoutModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定退出相关事件
|
||||
*/
|
||||
function bindLogoutEvents() {
|
||||
cancelLogout.addEventListener('click', function () {
|
||||
logoutModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
confirmLogout.addEventListener('click', function () {
|
||||
window.location.href = 'userLogout';
|
||||
});
|
||||
}
|
||||
|
||||
// 全局函数暴露
|
||||
window.openTab = openTab;
|
||||
window.closeTab = closeTab;
|
||||
window.showInfoToast = showInfoToast;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,312 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Font Awesome 4.7.0 所有图标</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#2563eb',
|
||||
secondary: '#64748b',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.icon-card {
|
||||
@apply bg-white rounded-lg shadow-md p-4 transition-all duration-300 hover:shadow-lg hover:scale-[1.02] border border-gray-100;
|
||||
}
|
||||
.icon-display {
|
||||
@apply text-3xl mb-2 text-primary;
|
||||
}
|
||||
.icon-code {
|
||||
@apply text-sm bg-gray-100 p-2 rounded font-mono text-secondary break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 font-sans">
|
||||
<header class="bg-primary text-white py-8 px-4 shadow-lg">
|
||||
<div class="container mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-2">Font Awesome 4.7.0 图标库</h1>
|
||||
<p class="text-blue-100">完整展示所有图标及其代码</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto py-8 px-4">
|
||||
<div class="mb-8">
|
||||
<div class="relative">
|
||||
<input type="text" id="search" placeholder="搜索图标..."
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
|
||||
<i class="fa fa-search absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<button class="category-btn px-3 py-1 bg-primary text-white rounded-full text-sm" data-category="all">全部</button>
|
||||
<button class="category-btn px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm" data-category="web-application">Web应用</button>
|
||||
<button class="category-btn px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm" data-category="text-editor">文本编辑</button>
|
||||
<button class="category-btn px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm" data-category="directional">方向</button>
|
||||
<button class="category-btn px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm" data-category="video-player">视频播放</button>
|
||||
<button class="category-btn px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm" data-category="brand">品牌</button>
|
||||
<button class="category-btn px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm" data-category="medical">医疗</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="icons-container" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
<!-- 图标将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-gray-800 text-white py-6 px-4 mt-12">
|
||||
<div class="container mx-auto text-center">
|
||||
<p>Font Awesome 4.7.0 图标展示 | 使用 <a href="https://fontawesome.com/v4/" class="text-blue-300 hover:underline">Font Awesome</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Font Awesome 4.7.0 所有图标数据(按类别分组)
|
||||
const fontAwesomeIcons = {
|
||||
"web-application": [
|
||||
"fa-adjust", "fa-anchor", "fa-archive", "fa-area-chart", "fa-arrows", "fa-arrows-alt",
|
||||
"fa-arrows-h", "fa-arrows-v", "fa-asterisk", "fa-at", "fa-ban", "fa-bar-chart",
|
||||
"fa-bar-chart-o", "fa-bars", "fa-bath", "fa-bathtub", "fa-battery-0", "fa-battery-1",
|
||||
"fa-battery-2", "fa-battery-3", "fa-battery-4", "fa-battery-empty", "fa-battery-full",
|
||||
"fa-battery-half", "fa-battery-quarter", "fa-battery-three-quarters", "fa-bed",
|
||||
"fa-beer", "fa-bell", "fa-bell-o", "fa-bell-slash", "fa-bell-slash-o", "fa-bicycle",
|
||||
"fa-binoculars", "fa-birthday-cake", "fa-bolt", "fa-bomb", "fa-book", "fa-bookmark",
|
||||
"fa-bookmark-o", "fa-briefcase", "fa-bullhorn", "fa-bullseye", "fa-calendar",
|
||||
"fa-calendar-check-o", "fa-calendar-o", "fa-calendar-plus-o", "fa-calendar-times-o",
|
||||
"fa-camera", "fa-camera-retro", "fa-car", "fa-caret-down", "fa-caret-left",
|
||||
"fa-caret-right", "fa-caret-up", "fa-cart-arrow-down", "fa-cart-plus", "fa-cc",
|
||||
"fa-certificate", "fa-check", "fa-check-circle", "fa-check-circle-o", "fa-check-square",
|
||||
"fa-check-square-o", "fa-child", "fa-circle", "fa-circle-o", "fa-circle-o-notch",
|
||||
"fa-circle-thin", "fa-clipboard", "fa-clock-o", "fa-clone", "fa-cloud", "fa-cloud-download",
|
||||
"fa-cloud-upload", "fa-code", "fa-code-fork", "fa-coffee", "fa-cog", "fa-cogs", "fa-columns",
|
||||
"fa-comment", "fa-comment-o", "fa-comments", "fa-comments-o", "fa-compass", "fa-credit-card",
|
||||
"fa-cutlery", "fa-dashboard", "fa-database", "fa-diamond", "fa-download", "fa-edit",
|
||||
"fa-ellipsis-h", "fa-ellipsis-v", "fa-envelope", "fa-envelope-o", "fa-envelope-open",
|
||||
"fa-envelope-open-o", "fa-envelope-square", "fa-eraser", "fa-exclamation", "fa-exclamation-circle",
|
||||
"fa-exclamation-triangle", "fa-external-link", "fa-external-link-square", "fa-eye", "fa-eye-slash",
|
||||
"fa-fax", "fa-file", "fa-file-archive-o", "fa-file-audio-o", "fa-file-code-o", "fa-file-excel-o",
|
||||
"fa-file-image-o", "fa-file-pdf-o", "fa-file-powerpoint-o", "fa-file-text", "fa-file-text-o",
|
||||
"fa-file-video-o", "fa-file-word-o", "fa-film", "fa-filter", "fa-fire", "fa-fire-extinguisher",
|
||||
"fa-flag", "fa-flag-checkered", "fa-flag-o", "fa-flask", "fa-folder", "fa-folder-o",
|
||||
"fa-folder-open", "fa-folder-open-o", "fa-frown-o", "fa-gamepad", "fa-gavel", "fa-gear",
|
||||
"fa-gears", "fa-gift", "fa-glass", "fa-globe", "fa-graduation-cap", "fa-hand-lizard-o",
|
||||
"fa-hand-paper-o", "fa-hand-peace-o", "fa-hand-pointer-o", "fa-hand-rock-o", "fa-hand-scissors-o",
|
||||
"fa-hand-spock-o", "fa-hand-stop-o", "fa-hdd-o", "fa-headphones", "fa-heart", "fa-heart-o",
|
||||
"fa-history", "fa-home", "fa-horoscope", "fa-hospital-o", "fa-hourglass", "fa-hourglass-1",
|
||||
"fa-hourglass-2", "fa-hourglass-3", "fa-hourglass-end", "fa-hourglass-half", "fa-hourglass-o",
|
||||
"fa-hourglass-start", "fa-i-cursor", "fa-inbox", "fa-indent", "fa-info", "fa-info-circle",
|
||||
"fa-institution", "fa-key", "fa-keyboard-o", "fa-language", "fa-laptop", "fa-legal",
|
||||
"fa-leaf", "fa-leanpub", "fa-legal", "fa-leaf", "fa-leanpub", "fa-level-down", "fa-level-up",
|
||||
"fa-life-ring", "fa-lightbulb-o", "fa-line-chart", "fa-link", "fa-list", "fa-list-alt",
|
||||
"fa-list-ol", "fa-list-ul", "fa-lock", "fa-magic", "fa-magnet", "fa-mail-forward",
|
||||
"fa-mail-reply", "fa-mail-reply-all", "fa-male", "fa-map", "fa-map-marker", "fa-map-o",
|
||||
"fa-map-pin", "fa-map-signs", "fa-meh-o", "fa-microchip", "fa-microphone", "fa-microphone-slash",
|
||||
"fa-minus", "fa-minus-circle", "fa-minus-square", "fa-minus-square-o", "fa-mobile",
|
||||
"fa-mobile-phone", "fa-money", "fa-moon-o", "fa-motorcycle", "fa-mouse-pointer", "fa-music",
|
||||
"fa-navicon", "fa-newspaper-o", "fa-ellipsis-h", "fa-ellipsis-v", "fa-envelope",
|
||||
"fa-envelope-o", "fa-envelope-open", "fa-envelope-open-o", "fa-envelope-square", "fa-eraser",
|
||||
"fa-exclamation", "fa-exclamation-circle", "fa-exclamation-triangle", "fa-external-link",
|
||||
"fa-external-link-square", "fa-eye", "fa-eye-slash", "fa-fax", "fa-file",
|
||||
"fa-file-archive-o", "fa-file-audio-o", "fa-file-code-o", "fa-file-excel-o",
|
||||
"fa-file-image-o", "fa-file-pdf-o", "fa-file-powerpoint-o", "fa-file-text",
|
||||
"fa-file-text-o", "fa-file-video-o", "fa-file-word-o", "fa-film", "fa-filter",
|
||||
"fa-fire", "fa-fire-extinguisher", "fa-flag", "fa-flag-checkered", "fa-flag-o",
|
||||
"fa-flask", "fa-folder", "fa-folder-o", "fa-folder-open", "fa-folder-open-o",
|
||||
"fa-frown-o", "fa-gamepad", "fa-gavel", "fa-gear", "fa-gears", "fa-gift",
|
||||
"fa-glass", "fa-globe", "fa-graduation-cap", "fa-hand-lizard-o", "fa-hand-paper-o",
|
||||
"fa-hand-peace-o", "fa-hand-pointer-o", "fa-hand-rock-o", "fa-hand-scissors-o",
|
||||
"fa-hand-spock-o", "fa-hand-stop-o", "fa-hdd-o", "fa-headphones", "fa-heart",
|
||||
"fa-heart-o", "fa-history", "fa-home", "fa-horoscope", "fa-hospital-o", "fa-hourglass",
|
||||
"fa-hourglass-1", "fa-hourglass-2", "fa-hourglass-3", "fa-hourglass-end",
|
||||
"fa-hourglass-half", "fa-hourglass-o", "fa-hourglass-start", "fa-i-cursor",
|
||||
"fa-inbox", "fa-indent", "fa-info", "fa-info-circle", "fa-institution", "fa-key",
|
||||
"fa-keyboard-o", "fa-language", "fa-laptop", "fa-legal", "fa-leaf", "fa-leanpub",
|
||||
"fa-legal", "fa-leaf", "fa-leanpub", "fa-level-down", "fa-level-up", "fa-life-ring",
|
||||
"fa-lightbulb-o", "fa-line-chart", "fa-link", "fa-list", "fa-list-alt", "fa-list-ol",
|
||||
"fa-list-ul", "fa-lock", "fa-magic", "fa-magnet", "fa-mail-forward", "fa-mail-reply",
|
||||
"fa-mail-reply-all", "fa-male", "fa-map", "fa-map-marker", "fa-map-o", "fa-map-pin",
|
||||
"fa-map-signs", "fa-meh-o", "fa-microchip", "fa-microphone", "fa-microphone-slash",
|
||||
"fa-minus", "fa-minus-circle", "fa-minus-square", "fa-minus-square-o", "fa-mobile",
|
||||
"fa-mobile-phone", "fa-money", "fa-moon-o", "fa-motorcycle", "fa-mouse-pointer",
|
||||
"fa-music", "fa-navicon"
|
||||
],
|
||||
"text-editor": [
|
||||
"fa-align-center", "fa-align-justify", "fa-align-left", "fa-align-right",
|
||||
"fa-bold", "fa-italic", "fa-underline", "fa-strikethrough", "fa-subscript",
|
||||
"fa-superscript", "fa-eraser", "fa-font", "fa-header", "fa-indent", "fa-outdent",
|
||||
"fa-list", "fa-list-ol", "fa-list-ul", "fa-paperclip", "fa-paragraph", "fa-pencil",
|
||||
"fa-pencil-square", "fa-pencil-square-o", "fa-repeat", "fa-rotate-left", "fa-rotate-right",
|
||||
"fa-save", "fa-strikethrough", "fa-undo"
|
||||
],
|
||||
"directional": [
|
||||
"fa-angle-double-down", "fa-angle-double-left", "fa-angle-double-right",
|
||||
"fa-angle-double-up", "fa-angle-down", "fa-angle-left", "fa-angle-right",
|
||||
"fa-angle-up", "fa-arrow-circle-down", "fa-arrow-circle-left", "fa-arrow-circle-o-down",
|
||||
"fa-arrow-circle-o-left", "fa-arrow-circle-o-right", "fa-arrow-circle-o-up",
|
||||
"fa-arrow-circle-right", "fa-arrow-circle-up", "fa-arrow-down", "fa-arrow-left",
|
||||
"fa-arrow-right", "fa-arrow-up", "fa-arrows", "fa-arrows-alt", "fa-arrows-h",
|
||||
"fa-arrows-v", "fa-backward", "fa-chevron-circle-down", "fa-chevron-circle-left",
|
||||
"fa-chevron-circle-right", "fa-chevron-circle-up", "fa-chevron-down", "fa-chevron-left",
|
||||
"fa-chevron-right", "fa-chevron-up", "fa-hand-o-down", "fa-hand-o-left", "fa-hand-o-right",
|
||||
"fa-hand-o-up", "fa-level-down", "fa-level-up", "fa-long-arrow-down", "fa-long-arrow-left",
|
||||
"fa-long-arrow-right", "fa-long-arrow-up", "fa-refresh", "fa-repeat", "fa-rotate-left",
|
||||
"fa-rotate-right", "fa-step-backward", "fa-step-forward"
|
||||
],
|
||||
"video-player": [
|
||||
"fa-backward", "fa-pause", "fa-pause-circle", "fa-pause-circle-o", "fa-play",
|
||||
"fa-play-circle", "fa-play-circle-o", "fa-step-backward", "fa-step-forward",
|
||||
"fa-stop", "fa-stop-circle", "fa-stop-circle-o", "fa-forward", "fa-eject",
|
||||
"fa-volume-down", "fa-volume-off", "fa-volume-up", "fa-music", "fa-film"
|
||||
],
|
||||
"brand": [
|
||||
"fa-500px", "fa-amazon", "fa-android", "fa-angellist", "fa-apple", "fa-archive",
|
||||
"fa-area-chart", "fa-atlassian", "fa-automobile", "fa-bank", "fa-bandcamp",
|
||||
"fa-btc", "fa-buffer", "fa-buysellads", "fa-cc-amex", "fa-cc-discover",
|
||||
"fa-cc-mastercard", "fa-cc-paypal", "fa-cc-stripe", "fa-cc-visa", "fa-chrome",
|
||||
"fa-cloudflare", "fa-codepen", "fa-connectdevelop", "fa-contao", "fa-css3",
|
||||
"fa-dashcube", "fa-delicious", "fa-deviantart", "fa-digg", "fa-dribbble",
|
||||
"fa-dropbox", "fa-drupal", "fa-edge", "fa-empire", "fa-envira", "fa-etsy",
|
||||
"fa-eur", "fa-facebook", "fa-facebook-f", "fa-facebook-official", "fa-facebook-square",
|
||||
"fa-firefox", "fa-flickr", "fa-font-awesome", "fa-forumbee", "fa-foursquare",
|
||||
"fa-github", "fa-github-alt", "fa-github-square", "fa-gittip", "fa-glide",
|
||||
"fa-glide-g", "fa-google", "fa-google-plus", "fa-google-plus-circle",
|
||||
"fa-google-plus-official", "fa-google-plus-square", "fa-google-wallet", "fa-gratipay",
|
||||
"fa-grav", "fa-hacker-news", "fa-hacker-news-square", "fa-hipchat", "fa-hooli",
|
||||
"fa-html5", "fa-instagram", "fa-internet-explorer", "fa-ioxhost", "fa-joomla",
|
||||
"fa-jpy", "fa-js", "fa-js-square", "fa-jsfiddle", "fa-leanpub", "fa-linkedin",
|
||||
"fa-linkedin-square", "fa-linux", "fa-maxcdn", "fa-medium", "fa-meetup", "fa-microsoft",
|
||||
"fa-minus-square", "fa-minus-square-o", "fa-mixcloud", "fa-modx", "fa-monero",
|
||||
"fa-usd", "fa-reddit", "fa-reddit-alien", "fa-reddit-square", "fa-renren",
|
||||
"fa-rmb", "fa-rocketchat", "fa-rotate-right", "fa-rss", "fa-rss-square",
|
||||
"fa-rub", "fa-ruble", "fa-safari", "fa-steam", "fa-steam-square", "fa-stumbleupon",
|
||||
"fa-stumbleupon-circle", "fa-superpowers", "fa-telegram", "fa-television", "fa-tencent-weibo",
|
||||
"fa-thumb-tack", "fa-try", "fa-tumblr", "fa-tumblr-square", "fa-twitch", "fa-twitter",
|
||||
"fa-twitter-square", "fa-usd", "fa-ubuntu", "fa-umbrella", "fa-underline", "fa-unsplash",
|
||||
"fa-upload", "fa-usb", "fa-user-md", "fa-venus", "fa-venus-double", "fa-vimeo",
|
||||
"fa-vimeo-square", "fa-vine", "fa-vk", "fa-vuejs", "fa-wechat", "fa-weibo",
|
||||
"fa-weixin", "fa-whatsapp", "fa-whatsapp-square", "fa-wikipedia-w", "fa-window-close",
|
||||
"fa-window-close-o", "fa-window-maximize", "fa-window-minimize", "fa-window-restore",
|
||||
"fa-windows", "fa-wordpress", "fa-wpbeginner", "fa-wpforms", "fa-wrench", "fa-xing",
|
||||
"fa-xing-square", "fa-y-combinator", "fa-yahoo", "fa-yelp", "fa-yen", "fa-youtube",
|
||||
"fa-youtube-play", "fa-youtube-square"
|
||||
],
|
||||
"medical": [
|
||||
"fa-ambulance", "fa-heartbeat", "fa-medkit", "fa-stethoscope", "fa-user-md",
|
||||
"fa-medkit", "fa-heartbeat", "fa-plus-square", "fa-minus-square", "fa-thermometer",
|
||||
"fa-thermometer-0", "fa-thermometer-1", "fa-thermometer-2", "fa-thermometer-3",
|
||||
"fa-thermometer-4", "fa-thermometer-empty", "fa-thermometer-full", "fa-thermometer-half",
|
||||
"fa-thermometer-quarter", "fa-thermometer-three-quarters", "fa-medkit"
|
||||
]
|
||||
};
|
||||
|
||||
// 生成所有图标HTML
|
||||
function renderIcons(icons, category) {
|
||||
const container = document.getElementById('icons-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
icons.forEach(icon => {
|
||||
const iconCard = document.createElement('div');
|
||||
iconCard.className = 'icon-card';
|
||||
iconCard.setAttribute('data-category', category);
|
||||
iconCard.setAttribute('data-name', icon);
|
||||
|
||||
iconCard.innerHTML = `
|
||||
<i class="fa ${icon} icon-display"></i>
|
||||
<p class="font-medium mb-2">${icon.replace('fa-', '')}</p>
|
||||
<div class="icon-code"><i class="fa ${icon}"></i></div>
|
||||
`;
|
||||
|
||||
container.appendChild(iconCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始渲染所有图标
|
||||
function renderAllIcons() {
|
||||
const allIcons = [];
|
||||
for (const category in fontAwesomeIcons) {
|
||||
fontAwesomeIcons[category].forEach(icon => {
|
||||
allIcons.push({icon, category});
|
||||
});
|
||||
}
|
||||
|
||||
const container = document.getElementById('icons-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
allIcons.forEach(item => {
|
||||
const iconCard = document.createElement('div');
|
||||
iconCard.className = 'icon-card';
|
||||
iconCard.setAttribute('data-category', item.category);
|
||||
iconCard.setAttribute('data-name', item.icon);
|
||||
|
||||
iconCard.innerHTML = `
|
||||
<i class="fa ${item.icon} icon-display"></i>
|
||||
<p class="font-medium mb-2">${item.icon.replace('fa-', '')}</p>
|
||||
<div class="icon-code"><i class="fa ${item.icon}"></i></div>
|
||||
`;
|
||||
|
||||
container.appendChild(iconCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
document.getElementById('search').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const iconCards = document.querySelectorAll('.icon-card');
|
||||
|
||||
iconCards.forEach(card => {
|
||||
const iconName = card.getAttribute('data-name');
|
||||
if (iconName.includes(searchTerm)) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 分类筛选
|
||||
document.querySelectorAll('.category-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// 更新按钮样式
|
||||
document.querySelectorAll('.category-btn').forEach(b => {
|
||||
b.classList.remove('bg-primary', 'text-white');
|
||||
b.classList.add('bg-gray-200', 'hover:bg-gray-300');
|
||||
});
|
||||
this.classList.remove('bg-gray-200', 'hover:bg-gray-300');
|
||||
this.classList.add('bg-primary', 'text-white');
|
||||
|
||||
const category = this.getAttribute('data-category');
|
||||
const iconCards = document.querySelectorAll('.icon-card');
|
||||
|
||||
if (category === 'all') {
|
||||
iconCards.forEach(card => {
|
||||
card.style.display = 'block';
|
||||
});
|
||||
} else {
|
||||
iconCards.forEach(card => {
|
||||
if (card.getAttribute('data-category') === category) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载完成后渲染所有图标
|
||||
window.addEventListener('DOMContentLoaded', renderAllIcons);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user