重写复现方法

This commit is contained in:
2025-09-01 18:18:44 +08:00
parent 7c11c1519b
commit 5d3d515b8e
41 changed files with 4249 additions and 5952 deletions

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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";
// }
}

View File

@@ -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, "登录失败,请稍后重试");
}
}

View File

@@ -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
# ===============================

View File

@@ -0,0 +1 @@
const n={};n.render=function(n,e){return null};export{n as default};

View 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}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -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);
}

View 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}

View 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");

View File

@@ -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();
}
});

View File

@@ -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();
}
});
}
});

View File

@@ -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();
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View File

@@ -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>

View File

@@ -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">&times;</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. 优先使用现代剪贴板APIHTTPS环境降级使用execCommandHTTP/旧浏览器)
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -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">&lt;i class="fa ${icon}"&gt;&lt;/i&gt;</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">&lt;i class="fa ${item.icon}"&gt;&lt;/i&gt;</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