diff --git a/docker-compose-v4.yml b/docker-compose-v4.yml index 4257c32..d791b8d 100644 --- a/docker-compose-v4.yml +++ b/docker-compose-v4.yml @@ -36,7 +36,6 @@ services: DB_PASSWORD: ${DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} LOG_DIR: /app/logs - SERVER_ADDR: ${SERVER_HOST} JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" ports: - "${BACKEND_PORT}:6365" diff --git a/docker-compose-v6.yml b/docker-compose-v6.yml index 5e338e3..6aa8203 100644 --- a/docker-compose-v6.yml +++ b/docker-compose-v6.yml @@ -36,7 +36,6 @@ services: DB_PASSWORD: ${DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} LOG_DIR: /app/logs - SERVER_ADDR: ${SERVER_HOST} JAVA_OPTS: "-Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" ports: - "${BACKEND_PORT}:6365" diff --git a/gost.sql b/gost.sql index 493d0dd..0a6944d 100644 --- a/gost.sql +++ b/gost.sql @@ -21,6 +21,21 @@ SET time_zone = "+00:00"; -- 数据库: `gost` -- +-- -------------------------------------------------------- + +-- +-- 表的结构 `statistics_flow` +-- + +CREATE TABLE `statistics_flow` ( + `id` bigint(10) NOT NULL, + `user_id` int(10) NOT NULL, + `flow` bigint(20) NOT NULL, + `total_flow` bigint(20) NOT NULL, + `time` varchar(100) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + -- -------------------------------------------------------- -- @@ -181,6 +196,13 @@ INSERT INTO `vite_config` (`id`, `name`, `value`, `time`) VALUES -- 转储表的索引 -- + +-- +-- 表的索引 `statistics_flow` +-- +ALTER TABLE `statistics_flow` + ADD PRIMARY KEY (`id`); + -- -- 表的索引 `forward` -- @@ -228,6 +250,12 @@ ALTER TABLE `vite_config` -- 在导出的表使用AUTO_INCREMENT -- +-- +-- 使用表AUTO_INCREMENT `statistics_flow` +-- +ALTER TABLE `statistics_flow` + MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; + -- -- 使用表AUTO_INCREMENT `forward` -- diff --git a/install.sh b/install.sh index 6335d90..8b93f94 100755 --- a/install.sh +++ b/install.sh @@ -16,6 +16,15 @@ show_menu() { echo "===============================================" } +# 删除脚本自身 +delete_self() { + echo "" + echo "🗑️ 操作已完成,正在清理脚本文件..." + SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")" + sleep 1 + rm -f "$SCRIPT_PATH" && echo "✅ 脚本文件已删除" || echo "❌ 删除脚本文件失败" +} + # 检查并安装 tcpkill check_and_install_tcpkill() { # 检查 tcpkill 是否已安装 @@ -304,6 +313,7 @@ main() { # 如果提供了命令行参数,直接执行安装 if [[ -n "$SERVER_ADDR" && -n "$SECRET" ]]; then install_gost + delete_self exit 0 fi @@ -315,18 +325,22 @@ main() { case $choice in 1) install_gost - break + delete_self + exit 0 ;; 2) update_gost - break + delete_self + exit 0 ;; 3) uninstall_gost - break + delete_self + exit 0 ;; 4) echo "👋 退出脚本" + delete_self exit 0 ;; *) diff --git a/panel_install.sh b/panel_install.sh index e4f63d7..6d23820 100755 --- a/panel_install.sh +++ b/panel_install.sh @@ -5,24 +5,7 @@ set -e export LANG=en_US.UTF-8 export LC_ALL=C -# 显示重要更新说明 -show_update_notice() { - echo "===============================================" - echo "🚨🚨🚨 重要更新说明 🚨🚨🚨" - echo "===============================================" - echo "面板版本大于等于1.0.6可以直接更新,无需查看" - echo "⚠️ 此次更新包含重要的数据库结构变更!" - echo "⚠️ 使用前务必前往群组查看更新说明!" - echo "⚠️ 否则可能导致数据灰飞烟灭!" - echo "⚠️ 有任何疑问可在群组询问,不要盲目操作!" - echo "===============================================" - echo "📱 请先前往群组查看详细更新说明" - echo "📱 确认了解更新内容后再进行操作" - echo "===============================================" - echo "" - echo "按回车键继续..." - read -r -} + # 全局下载地址配置 DOCKER_COMPOSEV4_URL="https://raw.githubusercontent.com/bqlpfy/forward-panel/refs/heads/main/docker-compose-v4.yml" @@ -156,7 +139,7 @@ show_menu() { echo "1. 安装面板" echo "2. 更新面板" echo "3. 卸载面板" - echo "4. 导出数据库备份" + echo "4. 导出备份" echo "5. 退出" echo "===============================================" } @@ -165,23 +148,22 @@ generate_random() { LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null || realpath "$0" 2>/dev/null || echo "$0")" + sleep 1 + rm -f "$SCRIPT_PATH" && echo "✅ 脚本文件已删除" || echo "❌ 删除脚本文件失败" +} + # 获取用户输入的配置参数 get_config_params() { echo "🔧 请输入配置参数:" - echo "📡 节点端服务器和面板通信的地址,需要能正常访问(IPv6不需要加[])" - echo "不要套CDN,不支持HTTPS,直接输入面板服务器的ip" - echo "用于节点和面板通讯的地址,节点和面板通讯内容有加密!!!!!" - while true; do - read -p "当前面板服务器地址: " SERVER_HOST - if [ -n "$SERVER_HOST" ]; then - break - else - echo "面板服务器地址不能为空,请输入。" - fi - done + read -p "前端端口(默认 6366): " FRONTEND_PORT FRONTEND_PORT=${FRONTEND_PORT:-6366} @@ -193,7 +175,6 @@ get_config_params() { DB_USER=$(generate_random) DB_PASSWORD=$(generate_random) JWT_SECRET=$(generate_random) - SERVER_HOST_PORT="${SERVER_HOST}:${BACKEND_PORT}" } # 安装功能 @@ -227,7 +208,6 @@ DB_NAME=$DB_NAME DB_USER=$DB_USER DB_PASSWORD=$DB_PASSWORD JWT_SECRET=$JWT_SECRET -SERVER_HOST=$SERVER_HOST_PORT FRONTEND_PORT=$FRONTEND_PORT BACKEND_PORT=$BACKEND_PORT EOF @@ -236,10 +216,7 @@ EOF $DOCKER_CMD up -d echo "🎉 部署完成" - echo "🌐 访问地址:" - echo "前端地址: http://$SERVER_HOST:$FRONTEND_PORT" - echo "后端地址: http://$SERVER_HOST:$BACKEND_PORT" - echo "" + echo "🌐 访问地址: http://服务器IP:$FRONTEND_PORT" echo "📖 部署完成后请阅读下使用文档,求求了啊,不要上去就是一顿操作" echo "📚 文档地址: https://tes.cc/guide.html" echo "💡 默认管理员账号: admin_user / admin_user" @@ -825,12 +802,17 @@ CREATE TABLE IF NOT EXISTS \`vite_config\` ( UNIQUE KEY \`unique_name\` (\`name\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 插入默认应用名称配置(如果不存在) -INSERT INTO \`vite_config\` (\`name\`, \`value\`, \`time\`) -SELECT 'app_name', '哆啦A梦', 1753344708000 -WHERE NOT EXISTS ( - SELECT 1 FROM \`vite_config\` WHERE \`name\` = 'app_name' -); +-- 创建 statistics_flow 表(如果不存在) +CREATE TABLE IF NOT EXISTS \`statistics_flow\` ( + \`id\` bigint(20) NOT NULL AUTO_INCREMENT, + \`user_id\` int(10) NOT NULL, + \`flow\` bigint(20) NOT NULL, + \`total_flow\` bigint(20) NOT NULL, + \`time\` varchar(100) NOT NULL, + PRIMARY KEY (\`id\`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + EOF @@ -1002,9 +984,7 @@ uninstall_panel() { # 主逻辑 main() { - # 显示更新说明 - show_update_notice - + # 显示交互式菜单 while true; do show_menu @@ -1013,22 +993,27 @@ main() { case $choice in 1) install_panel - break + delete_self + exit 0 ;; 2) update_panel - break + delete_self + exit 0 ;; 3) uninstall_panel - break + delete_self + exit 0 ;; 4) export_migration_sql - break + delete_self + exit 0 ;; 5) echo "👋 退出脚本" + delete_self exit 0 ;; *) diff --git a/springboot-backend/pom.xml b/springboot-backend/pom.xml index dfc8aa8..132ecb0 100644 --- a/springboot-backend/pom.xml +++ b/springboot-backend/pom.xml @@ -120,6 +120,13 @@ spring-boot-starter-test test + + + + cloud.tianai.captcha + tianai-captcha-springboot-starter + 1.5.2 + diff --git a/springboot-backend/src/main/java/com/admin/common/dto/CaptchaVerifyDto.java b/springboot-backend/src/main/java/com/admin/common/dto/CaptchaVerifyDto.java new file mode 100644 index 0000000..0b6c8db --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/dto/CaptchaVerifyDto.java @@ -0,0 +1,18 @@ +package com.admin.common.dto; + +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 验证码验证请求DTO + */ +@Data +public class CaptchaVerifyDto { + + private String id; + + private ImageCaptchaTrack data; + +} diff --git a/springboot-backend/src/main/java/com/admin/common/dto/LoginDto.java b/springboot-backend/src/main/java/com/admin/common/dto/LoginDto.java index 8356798..ac587fa 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/LoginDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/LoginDto.java @@ -14,4 +14,7 @@ public class LoginDto { @NotBlank(message = "密码不能为空") private String password; + private String captchaId; + + } diff --git a/springboot-backend/src/main/java/com/admin/common/dto/UserPackageDto.java b/springboot-backend/src/main/java/com/admin/common/dto/UserPackageDto.java index 5ef43cf..bf31bcb 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/UserPackageDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/UserPackageDto.java @@ -1,5 +1,6 @@ package com.admin.common.dto; +import com.admin.entity.StatisticsFlow; import lombok.Data; import java.util.List; @@ -24,6 +25,11 @@ public class UserPackageDto { * 用户转发列表 */ private List forwards; + + /** + * 用户转发列表 + */ + private List statisticsFlows; /** * 用户基本信息 diff --git a/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java b/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java index c4717a8..2e109ab 100644 --- a/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java +++ b/springboot-backend/src/main/java/com/admin/common/task/ResetFlowAsync.java @@ -48,7 +48,7 @@ public class ResetFlowAsync { * - 不会影响DelayQueueManager的到期任务对status等其他字段的更新 * - 避免了并发修改导致的数据覆盖问题 */ - @Scheduled(cron = "0 0 0 * * ?") + @Scheduled(cron = "5 0 0 * * ?") public void reset_flow(){ log.info("开始执行流量重置任务"); diff --git a/springboot-backend/src/main/java/com/admin/common/task/StatisticsFlowAsync.java b/springboot-backend/src/main/java/com/admin/common/task/StatisticsFlowAsync.java new file mode 100644 index 0000000..76037f8 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/common/task/StatisticsFlowAsync.java @@ -0,0 +1,75 @@ +package com.admin.common.task; + + +import com.admin.entity.StatisticsFlow; +import com.admin.entity.User; +import com.admin.service.StatisticsFlowService; +import com.admin.service.UserService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Configuration +@EnableScheduling +public class StatisticsFlowAsync { + + @Resource + UserService userService; + + @Resource + StatisticsFlowService statisticsFlowService; + + @Scheduled(cron = "0 0 * * * ?") + public void statistics_flow() { + LocalDateTime currentHour = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0); + String hourString = currentHour.format(DateTimeFormatter.ofPattern("HH:mm")); + + List list = userService.list(); + List statisticsFlowList = new ArrayList<>(); + + for (User user : list) { + long currentFlow = user.getInFlow() + user.getOutFlow(); + + // 从数据库获取上一次记录 + StatisticsFlow lastFlowRecord = statisticsFlowService.getOne( + new LambdaQueryWrapper() + .eq(StatisticsFlow::getUserId, user.getId()) + .orderByDesc(StatisticsFlow::getId) + .last("LIMIT 1") + ); + + long currentTotalFlow = currentFlow; + long incrementFlow = currentTotalFlow; + + if (lastFlowRecord != null) { + long lastTotalFlow = lastFlowRecord.getTotalFlow(); + incrementFlow = currentTotalFlow - lastTotalFlow; + + if (incrementFlow < 0) { + incrementFlow = currentTotalFlow; + } + } + + StatisticsFlow statisticsFlow = new StatisticsFlow(); + statisticsFlow.setUserId(user.getId()); + statisticsFlow.setFlow(incrementFlow); + statisticsFlow.setTotalFlow(currentTotalFlow); + statisticsFlow.setTime(hourString); + + statisticsFlowList.add(statisticsFlow); + } + + statisticsFlowService.saveBatch(statisticsFlowList); + } + +} diff --git a/springboot-backend/src/main/java/com/admin/config/CaptchaResourceConfiguration.java b/springboot-backend/src/main/java/com/admin/config/CaptchaResourceConfiguration.java new file mode 100644 index 0000000..7dc9235 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/config/CaptchaResourceConfiguration.java @@ -0,0 +1,37 @@ +package com.admin.config; + +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.resource.FontCache; +import cloud.tianai.captcha.resource.ResourceStore; +import cloud.tianai.captcha.resource.common.model.dto.Resource; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +@Component +@RequiredArgsConstructor +public class CaptchaResourceConfiguration { + + private final ResourceStore resourceStore; + + + private static final String[] OPTIONS = { + "SLIDER", "WORD_IMAGE_CLICK", "ROTATE", "CONCAT" + }; + + @PostConstruct + public void init() { + + // 添加自定义背景图片 + for (String option : OPTIONS) { + for (int i = 1; i <= 26; i++) { + resourceStore.addResource(option, new Resource("classpath", "bgimages/"+i + ".jpg", "default")); + } + } + + + //添加自定义字体 + resourceStore.addResource(FontCache.FONT_TYPE,new Resource("classpath", "fonts/SIMSUN.TTC", "default")); + } +} \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/config/WebMvcConfig.java b/springboot-backend/src/main/java/com/admin/config/WebMvcConfig.java index 5e7dfab..b72324d 100644 --- a/springboot-backend/src/main/java/com/admin/config/WebMvcConfig.java +++ b/springboot-backend/src/main/java/com/admin/config/WebMvcConfig.java @@ -59,6 +59,7 @@ public class WebMvcConfig implements WebMvcConfigurer { .excludePathPatterns("/flow/**") .excludePathPatterns("/api/v1/open_api/**") .excludePathPatterns("/api/v1/config/get") - .excludePathPatterns("/api/v1/user/login"); + .excludePathPatterns("/api/v1/user/login") + .excludePathPatterns("/api/v1/captcha/**"); } } diff --git a/springboot-backend/src/main/java/com/admin/controller/CaptchaController.java b/springboot-backend/src/main/java/com/admin/controller/CaptchaController.java new file mode 100644 index 0000000..26f25ca --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/controller/CaptchaController.java @@ -0,0 +1,77 @@ +package com.admin.controller; + +import cloud.tianai.captcha.application.ImageCaptchaApplication; +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.common.response.ApiResponse; +import cloud.tianai.captcha.application.vo.ImageCaptchaVO; +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; +import cloud.tianai.captcha.validator.common.model.dto.MatchParam; +import com.admin.common.dto.CaptchaVerifyDto; +import com.admin.common.lang.R; +import com.admin.entity.ViteConfig; +import com.admin.service.ViteConfigService; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import cloud.tianai.captcha.application.vo.CaptchaResponse; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 验证码控制器 + */ +@Slf4j +@RestController +@CrossOrigin +@RequestMapping("/api/v1/captcha") +public class CaptchaController extends BaseController { + + @Resource + ImageCaptchaApplication application; + + private static final String[] OPTIONS = { + "SLIDER", "WORD_IMAGE_CLICK", "ROTATE", "CONCAT" + }; + + @PostMapping("/check") + public R check() { + ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "captcha_enabled")); + if (viteConfig == null) return R.ok(0); + if (!Objects.equals(viteConfig.getValue(), "true")) return R.ok(0); + return R.ok(1); + } + + + @PostMapping("/generate") + public CaptchaResponse genCaptcha() { + ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "captcha_type")); + String captchaType; + if (viteConfig == null || Objects.equals(viteConfig.getValue(), "RANDOM")) { + captchaType = getRandomOption(); + }else { + captchaType = viteConfig.getValue(); + } + return application.generateCaptcha(captchaType); + } + + + @PostMapping("/verify") + public ApiResponse verify(@Valid @RequestBody CaptchaVerifyDto verifyDto) { + ApiResponse response = application.matching(verifyDto.getId(), verifyDto.getData()); + if (response.isSuccess()) { + return ApiResponse.ofSuccess(Collections.singletonMap("validToken", verifyDto.getId())); + } + return response; + } + + public static String getRandomOption() { + int index = ThreadLocalRandom.current().nextInt(OPTIONS.length); + return OPTIONS[index]; + } +} diff --git a/springboot-backend/src/main/java/com/admin/entity/StatisticsFlow.java b/springboot-backend/src/main/java/com/admin/entity/StatisticsFlow.java new file mode 100644 index 0000000..b5063c9 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/entity/StatisticsFlow.java @@ -0,0 +1,32 @@ +package com.admin.entity; + +import com.admin.entity.BaseEntity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + *

+ * + *

+ * + * @author QAQ + * @since 2025-08-14 + */ +@Data +public class StatisticsFlow { + + private static final long serialVersionUID = 1L; + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private Long userId; + + private Long flow; + + private Long totalFlow; + + private String time; + + +} diff --git a/springboot-backend/src/main/java/com/admin/mapper/StatisticsFlowMapper.java b/springboot-backend/src/main/java/com/admin/mapper/StatisticsFlowMapper.java new file mode 100644 index 0000000..f63d7a3 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/mapper/StatisticsFlowMapper.java @@ -0,0 +1,16 @@ +package com.admin.mapper; + +import com.admin.entity.StatisticsFlow; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author QAQ + * @since 2025-08-14 + */ +public interface StatisticsFlowMapper extends BaseMapper { + +} diff --git a/springboot-backend/src/main/java/com/admin/service/StatisticsFlowService.java b/springboot-backend/src/main/java/com/admin/service/StatisticsFlowService.java new file mode 100644 index 0000000..cf3a5b6 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/StatisticsFlowService.java @@ -0,0 +1,16 @@ +package com.admin.service; + +import com.admin.entity.StatisticsFlow; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author QAQ + * @since 2025-08-14 + */ +public interface StatisticsFlowService extends IService { + +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java index 8d8010d..5beba63 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/NodeServiceImpl.java @@ -7,10 +7,12 @@ import com.admin.common.dto.NodeUpdateDto; import com.admin.common.lang.R; import com.admin.entity.Node; import com.admin.entity.Tunnel; +import com.admin.entity.ViteConfig; import com.admin.mapper.NodeMapper; import com.admin.mapper.TunnelMapper; import com.admin.service.NodeService; import com.admin.service.TunnelService; +import com.admin.service.ViteConfigService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -69,8 +71,9 @@ public class NodeServiceImpl extends ServiceImpl implements No @Lazy private TunnelService tunnelService; - @Value("${server-addr}") - private String serverAddr; + @Resource + ViteConfigService viteConfigService; + // ========== 公共接口实现 ========== @@ -321,9 +324,7 @@ public class NodeServiceImpl extends ServiceImpl implements No } // 2. 构建安装命令 - String installCommand = buildInstallCommand(node); - - return R.ok(installCommand); + return buildInstallCommand(node); } /** @@ -332,7 +333,10 @@ public class NodeServiceImpl extends ServiceImpl implements No * @param node 节点对象 * @return 格式化的安装命令 */ - private String buildInstallCommand(Node node) { + private R buildInstallCommand(Node node) { + ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "ip")); + if (viteConfig == null) return R.err("请先前往网站配置中设置ip"); + StringBuilder command = new StringBuilder(); // 第一部分:下载安装脚本 @@ -340,14 +344,14 @@ public class NodeServiceImpl extends ServiceImpl implements No .append(" -o ./install.sh && chmod +x ./install.sh && "); // 处理服务器地址,如果是IPv6需要添加方括号 - String processedServerAddr = processServerAddress(serverAddr); + String processedServerAddr = processServerAddress(viteConfig.getValue()); // 第二部分:执行安装脚本(去掉-u参数) command.append("./install.sh") .append(" -a ").append(processedServerAddr) // 服务器地址 .append(" -s ").append(node.getSecret()); // 节点密钥 - return command.toString(); + return R.ok(command.toString()); } /** diff --git a/springboot-backend/src/main/java/com/admin/service/impl/StatisticsFlowServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/StatisticsFlowServiceImpl.java new file mode 100644 index 0000000..c290185 --- /dev/null +++ b/springboot-backend/src/main/java/com/admin/service/impl/StatisticsFlowServiceImpl.java @@ -0,0 +1,20 @@ +package com.admin.service.impl; + +import com.admin.entity.StatisticsFlow; +import com.admin.mapper.StatisticsFlowMapper; +import com.admin.service.StatisticsFlowService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author QAQ + * @since 2025-08-14 + */ +@Service +public class StatisticsFlowServiceImpl extends ServiceImpl implements StatisticsFlowService { + +} diff --git a/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java index ec9da1a..e49f5dd 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/UserServiceImpl.java @@ -1,5 +1,7 @@ package com.admin.service.impl; +import cloud.tianai.captcha.application.ImageCaptchaApplication; +import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import com.admin.common.dto.*; @@ -7,28 +9,24 @@ import com.admin.common.lang.R; import com.admin.common.utils.GostUtil; import com.admin.common.utils.JwtUtil; import com.admin.common.utils.Md5Util; -import com.admin.entity.Forward; -import com.admin.entity.Node; -import com.admin.entity.Tunnel; -import com.admin.entity.User; -import com.admin.entity.UserTunnel; +import com.admin.entity.*; import com.admin.mapper.ForwardMapper; import com.admin.mapper.UserMapper; import com.admin.mapper.UserTunnelMapper; -import com.admin.service.NodeService; -import com.admin.service.TunnelService; -import com.admin.service.UserService; -import com.admin.service.UserTunnelService; +import com.admin.service.*; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** *

@@ -77,8 +75,7 @@ public class UserServiceImpl extends ServiceImpl implements Us private static final String ERROR_GET_PACKAGE_INFO_FAILED = "获取套餐信息失败"; private static final String ERROR_CURRENT_PASSWORD_WRONG = "当前密码错误"; private static final String ERROR_PASSWORD_NOT_MATCH = "新密码和确认密码不匹配"; - private static final String SUCCESS_PASSWORD_UPDATE = "密码修改成功"; - + /** 默认账号密码 */ private static final String DEFAULT_USERNAME = "admin_user"; private static final String DEFAULT_PASSWORD = "admin_user"; @@ -112,28 +109,48 @@ public class UserServiceImpl extends ServiceImpl implements Us @Resource UserTunnelService userTunnelService; + @Resource + ViteConfigService viteConfigService; + + @Resource + StatisticsFlowService statisticsFlowService; + + @Resource + private ImageCaptchaApplication application; + // ========== 公共接口实现 ========== /** * 用户登录 - * 验证用户名密码,检查账户状态,生成JWT令牌 + * 验证验证码、用户名密码,检查账户状态,生成JWT令牌 * * @param loginDto 登录数据传输对象 * @return 登录结果响应,包含令牌和用户信息 */ @Override public R login(LoginDto loginDto) { - // 1. 验证用户凭据 + + // 1. 验证验证码 + ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper().eq("name", "captcha_enabled")); + if (viteConfig != null && Objects.equals(viteConfig.getValue(), "true")) { + if (StringUtils.isBlank(loginDto.getCaptchaId())) return R.err("验证码校验失败"); + boolean valid = ((SecondaryVerificationApplication) application).secondaryVerification(loginDto.getCaptchaId()); + if (!valid) return R.err("验证码校验失败"); + } + + + + // 2. 验证用户凭据 LoginValidationResult validationResult = validateUserCredentials(loginDto); if (validationResult.isHasError()) { return R.err(validationResult.getErrorMessage()); } - // 2. 生成令牌并返回用户信息 + // 3. 生成令牌并返回用户信息 User user = validationResult.getUser(); String token = JwtUtil.generateToken(user); - // 3. 检查是否使用默认账号密码 + // 4. 检查是否使用默认账号密码 boolean requirePasswordChange = isDefaultCredentials(loginDto.getUsername(), loginDto.getPassword()); return R.ok(MapUtil.builder() @@ -238,6 +255,7 @@ public class UserServiceImpl extends ServiceImpl implements Us try { // 2. 级联删除用户相关数据 deleteUserRelatedData(id); + statisticsFlowService.remove(new QueryWrapper().eq("user_id", id)); // 3. 删除用户 boolean result = this.removeById(id); return result ? R.ok(SUCCESS_DELETE_MSG) : R.err(ERROR_DELETE_FAILED); @@ -345,6 +363,7 @@ public class UserServiceImpl extends ServiceImpl implements Us // ========== 私有辅助方法 ========== + /** * 验证用户登录凭据 * @@ -655,12 +674,16 @@ public class UserServiceImpl extends ServiceImpl implements Us // 3. 获取转发详情 List forwards = userMapper.getUserForwardDetails(user.getId().intValue()); + + // 4. 查询最近24小时流量信息,没有的补0 + List statisticsFlows = getLast24HoursFlowStatistics(user.getId()); - // 4. 构造返回结果 + // 5. 构造返回结果 UserPackageDto packageDto = new UserPackageDto(); packageDto.setUserInfo(userInfo); packageDto.setTunnelPermissions(tunnelPermissions); packageDto.setForwards(forwards); + packageDto.setStatisticsFlows(statisticsFlows); return packageDto; } @@ -697,6 +720,104 @@ public class UserServiceImpl extends ServiceImpl implements Us return userMapper.getUserTunnelDetails(userId.intValue()); } + /** + * 获取用户最近24小时的流量统计数据,没有数据的时间点补0 + * + * @param userId 用户ID + * @return 最近24小时流量统计列表 + */ + private List getLast24HoursFlowStatistics(Long userId) { + try { + // 按ID倒序查最近24条记录(ID越大越新,时间就是23:00, 22:00, 21:00...这样倒序) + List recentFlows = statisticsFlowService.list( + new QueryWrapper() + .eq("user_id", userId) + .orderByDesc("id") + .last("LIMIT 24") + ); + + List result = new ArrayList<>(recentFlows); + + // 如果查出来的记录不足24条,需要补0和对应的时间 + if (result.size() < 24) { + // 获取最早记录的时间,继续往前推 + int startHour = getCurrentHour(); + if (!result.isEmpty()) { + // 从最后一条记录的时间继续往前推 + String lastTime = result.get(result.size() - 1).getTime(); + startHour = parseHour(lastTime) - 1; + } + + // 补0到24条 + while (result.size() < 24) { + if (startHour < 0) startHour = 23; // 跨天处理 + + StatisticsFlow emptyFlow = new StatisticsFlow(); + emptyFlow.setUserId(userId); + emptyFlow.setFlow(0L); + emptyFlow.setTotalFlow(0L); + emptyFlow.setTime(String.format("%02d:00", startHour)); + result.add(emptyFlow); + + startHour--; + } + } + + log.debug("用户 {} 获取到 {} 条实际记录,补齐为 {} 条24小时记录", userId, recentFlows.size(), result.size()); + return result; + + } catch (Exception e) { + log.error("获取用户 {} 最近24小时流量统计失败", userId, e); + // 返回24条全0数据,时间从当前小时往前推 + return generateEmpty24HourData(userId); + } + } + + /** + * 获取当前小时(0-23) + */ + private int getCurrentHour() { + return java.time.LocalDateTime.now().getHour(); + } + + /** + * 解析时间字符串获取小时数 + */ + private int parseHour(String timeStr) { + try { + if (timeStr != null && timeStr.contains(":")) { + return Integer.parseInt(timeStr.split(":")[0]); + } + } catch (Exception e) { + // 解析失败,返回当前小时 + } + return getCurrentHour(); + } + + /** + * 生成24小时全0数据(异常情况使用) + */ + private List generateEmpty24HourData(Long userId) { + List result = new ArrayList<>(); + int currentHour = getCurrentHour(); + + for (int i = 0; i < 24; i++) { + StatisticsFlow emptyFlow = new StatisticsFlow(); + emptyFlow.setUserId(userId); + emptyFlow.setFlow(0L); + emptyFlow.setTotalFlow(0L); + emptyFlow.setTime(String.format("%02d:00", currentHour)); + result.add(emptyFlow); + + currentHour--; + if (currentHour < 0) currentHour = 23; + } + + return result; + } + + + diff --git a/springboot-backend/src/main/resources/application.yml b/springboot-backend/src/main/resources/application.yml index a9545ed..477f180 100644 --- a/springboot-backend/src/main/resources/application.yml +++ b/springboot-backend/src/main/resources/application.yml @@ -40,4 +40,10 @@ mybatis-plus: jwt-secret: ${JWT_SECRET} log-dir: ${LOG_DIR} -server-addr: ${SERVER_ADDR} + +# 验证码配置 +captcha: + init-default-resource: true + secondary: + enabled: true + expire: 120000 diff --git a/springboot-backend/src/main/resources/bgimages/1.jpg b/springboot-backend/src/main/resources/bgimages/1.jpg new file mode 100644 index 0000000..f96ca1f Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/1.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/10.jpg b/springboot-backend/src/main/resources/bgimages/10.jpg new file mode 100644 index 0000000..73f62d9 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/10.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/11.jpg b/springboot-backend/src/main/resources/bgimages/11.jpg new file mode 100644 index 0000000..40b8c69 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/11.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/12.jpg b/springboot-backend/src/main/resources/bgimages/12.jpg new file mode 100644 index 0000000..54f1454 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/12.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/13.jpg b/springboot-backend/src/main/resources/bgimages/13.jpg new file mode 100644 index 0000000..405f5f7 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/13.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/14.jpg b/springboot-backend/src/main/resources/bgimages/14.jpg new file mode 100644 index 0000000..52ca868 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/14.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/15.jpg b/springboot-backend/src/main/resources/bgimages/15.jpg new file mode 100644 index 0000000..b36d89f Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/15.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/16.jpg b/springboot-backend/src/main/resources/bgimages/16.jpg new file mode 100644 index 0000000..be2aa6b Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/16.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/17.jpg b/springboot-backend/src/main/resources/bgimages/17.jpg new file mode 100644 index 0000000..dafdcb4 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/17.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/18.jpg b/springboot-backend/src/main/resources/bgimages/18.jpg new file mode 100644 index 0000000..efdd342 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/18.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/19.jpg b/springboot-backend/src/main/resources/bgimages/19.jpg new file mode 100644 index 0000000..80f1358 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/19.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/2.jpg b/springboot-backend/src/main/resources/bgimages/2.jpg new file mode 100644 index 0000000..a5bac6f Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/2.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/20.jpg b/springboot-backend/src/main/resources/bgimages/20.jpg new file mode 100644 index 0000000..931f4cc Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/20.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/21.jpg b/springboot-backend/src/main/resources/bgimages/21.jpg new file mode 100644 index 0000000..0963e68 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/21.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/22.jpg b/springboot-backend/src/main/resources/bgimages/22.jpg new file mode 100644 index 0000000..d7bda2c Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/22.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/23.jpg b/springboot-backend/src/main/resources/bgimages/23.jpg new file mode 100644 index 0000000..287e6d3 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/23.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/24.jpg b/springboot-backend/src/main/resources/bgimages/24.jpg new file mode 100644 index 0000000..4adf515 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/24.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/25.jpg b/springboot-backend/src/main/resources/bgimages/25.jpg new file mode 100644 index 0000000..8aee7b0 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/25.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/26.jpg b/springboot-backend/src/main/resources/bgimages/26.jpg new file mode 100644 index 0000000..2b630f6 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/26.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/3.jpg b/springboot-backend/src/main/resources/bgimages/3.jpg new file mode 100644 index 0000000..350c729 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/3.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/4.jpg b/springboot-backend/src/main/resources/bgimages/4.jpg new file mode 100644 index 0000000..c7fe08b Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/4.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/5.jpg b/springboot-backend/src/main/resources/bgimages/5.jpg new file mode 100644 index 0000000..98ad3d2 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/5.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/6.jpg b/springboot-backend/src/main/resources/bgimages/6.jpg new file mode 100644 index 0000000..1d0cc0b Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/6.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/7.jpg b/springboot-backend/src/main/resources/bgimages/7.jpg new file mode 100644 index 0000000..26ead24 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/7.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/8.jpg b/springboot-backend/src/main/resources/bgimages/8.jpg new file mode 100644 index 0000000..f2591df Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/8.jpg differ diff --git a/springboot-backend/src/main/resources/bgimages/9.jpg b/springboot-backend/src/main/resources/bgimages/9.jpg new file mode 100644 index 0000000..9cc4dc1 Binary files /dev/null and b/springboot-backend/src/main/resources/bgimages/9.jpg differ diff --git a/springboot-backend/src/main/resources/fonts/SIMSUN.TTC b/springboot-backend/src/main/resources/fonts/SIMSUN.TTC new file mode 100644 index 0000000..40e9693 Binary files /dev/null and b/springboot-backend/src/main/resources/fonts/SIMSUN.TTC differ diff --git a/springboot-backend/src/main/resources/mapper/StatisticsFlowMapper.xml b/springboot-backend/src/main/resources/mapper/StatisticsFlowMapper.xml new file mode 100644 index 0000000..73b986c --- /dev/null +++ b/springboot-backend/src/main/resources/mapper/StatisticsFlowMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index 1698ef3..76b0145 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -4,6 +4,7 @@ import Network from './network'; export interface LoginData { username: string; password: string; + captchaId: string; } export interface LoginResponse { @@ -81,4 +82,10 @@ export const resetUserFlow = (data: { id: number; type: number }) => Network.pos export const getConfigs = () => Network.post("/config/list"); export const getConfigByName = (name: string) => Network.post("/config/get", { name }); export const updateConfigs = (configMap: Record) => Network.post("/config/update", configMap); -export const updateConfig = (name: string, value: string) => Network.post("/config/update-single", { name, value }); \ No newline at end of file +export const updateConfig = (name: string, value: string) => Network.post("/config/update-single", { name, value }); + + +// 验证码相关接口 +export const checkCaptcha = () => Network.post("/captcha/check"); +export const generateCaptcha = () => Network.post(`/captcha/generate`); +export const verifyCaptcha = (data: { captchaId: string; trackData: string }) => Network.post("/captcha/verify", data); \ No newline at end of file diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index c1057a7..f354012 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig; // 缓存相关常量 const CACHE_PREFIX = 'vite_config_'; -const VERSION = "1.2.6"; +const VERSION = "1.2.8"; const getInitialConfig = () => { diff --git a/vite-frontend/src/images/icon.png b/vite-frontend/src/images/icon.png new file mode 100644 index 0000000..586a123 Binary files /dev/null and b/vite-frontend/src/images/icon.png differ diff --git a/vite-frontend/src/images/loading.gif b/vite-frontend/src/images/loading.gif new file mode 100644 index 0000000..f191b28 Binary files /dev/null and b/vite-frontend/src/images/loading.gif differ diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx index 84ea13f..ec1b782 100644 --- a/vite-frontend/src/pages/config.tsx +++ b/vite-frontend/src/pages/config.tsx @@ -4,6 +4,8 @@ import { Card, CardBody, CardHeader } from "@heroui/card"; import { Input } from "@heroui/input"; import { Spinner } from "@heroui/spinner"; import { Divider } from "@heroui/divider"; +import { Switch } from "@heroui/switch"; +import { Select, SelectItem } from "@heroui/select"; import toast from 'react-hot-toast'; import { updateConfigs } from '@/api'; import { SettingsIcon } from '@/components/icons'; @@ -31,17 +33,70 @@ const SaveIcon = ({ className }: { className?: string }) => ( interface ConfigItem { key: string; label: string; - placeholder: string; + placeholder?: string; description?: string; + type: 'input' | 'switch' | 'select'; + options?: { label: string; value: string; description?: string }[]; + dependsOn?: string; // 依赖的配置项key + dependsValue?: string; // 依赖的配置项值 } // 网站配置项定义 const CONFIG_ITEMS: ConfigItem[] = [ + { + key: 'ip', + label: '面板后端地址', + placeholder: '请输入面板后端IP:PORT', + description: '格式“ip:port”,用于对接节点时使用,ip是你安装面板服务器的公网ip,端口是安装脚本内输入的后端端口。不要套CDN,不支持https,通讯数据有加密', + type: 'input' + }, { key: 'app_name', label: '应用名称', placeholder: '请输入应用名称', - description: '在浏览器标签页和导航栏显示的应用名称' + description: '在浏览器标签页和导航栏显示的应用名称', + type: 'input' + }, + { + key: 'captcha_enabled', + label: '启用验证码', + description: '开启后,用户登录时需要完成验证码验证', + type: 'switch' + }, + { + key: 'captcha_type', + label: '验证码类型', + description: '选择验证码的显示类型,不同类型有不同的安全级别', + type: 'select', + dependsOn: 'captcha_enabled', + dependsValue: 'true', + options: [ + { + label: '随机类型', + value: 'RANDOM', + description: '系统随机选择验证码类型' + }, + { + label: '滑块验证码', + value: 'SLIDER', + description: '拖动滑块完成拼图验证' + }, + { + label: '文字点选验证码', + value: 'WORD_IMAGE_CLICK', + description: '按顺序点击指定文字' + }, + { + label: '旋转验证码', + value: 'ROTATE', + description: '旋转图片到正确角度' + }, + { + label: '拼图验证码', + value: 'CONCAT', + description: '拖动滑块完成图片拼接' + } + ] } ]; @@ -49,7 +104,7 @@ const CONFIG_ITEMS: ConfigItem[] = [ const getInitialConfigs = (): Record => { if (typeof window === 'undefined') return {}; - const configKeys = ['app_name']; + const configKeys = ['app_name', 'captcha_enabled', 'captcha_type']; const initialConfigs: Record = {}; try { @@ -124,7 +179,15 @@ export default function ConfigPage() { // 处理配置项变更 const handleConfigChange = (key: string, value: string) => { - const newConfigs = { ...configs, [key]: value }; + let newConfigs = { ...configs, [key]: value }; + + // 特殊处理:启用验证码时,如果验证码类型未设置,默认为随机 + if (key === 'captcha_enabled' && value === 'true') { + if (!newConfigs.captcha_type) { + newConfigs.captcha_type = 'RANDOM'; + } + } + setConfigs(newConfigs); // 检查是否有变更 @@ -174,11 +237,88 @@ export default function ConfigPage() { } }; - // 重置配置 - const handleReset = () => { - setConfigs({ ...originalConfigs }); - setHasChanges(false); - toast.success('已重置为保存前的状态'); + + + // 检查配置项是否应该显示(依赖检查) + const shouldShowItem = (item: ConfigItem): boolean => { + if (!item.dependsOn || !item.dependsValue) { + return true; + } + return configs[item.dependsOn] === item.dependsValue; + }; + + // 渲染不同类型的配置项 + const renderConfigItem = (item: ConfigItem) => { + const isChanged = hasChanges && configs[item.key] !== originalConfigs[item.key]; + + switch (item.type) { + case 'input': + return ( + handleConfigChange(item.key, e.target.value)} + placeholder={item.placeholder} + variant="bordered" + size="md" + classNames={{ + input: "text-sm", + inputWrapper: isChanged + ? "border-warning-300 data-[hover=true]:border-warning-400" + : "" + }} + /> + ); + + case 'switch': + return ( + handleConfigChange(item.key, checked ? 'true' : 'false')} + color="primary" + size="md" + classNames={{ + wrapper: isChanged ? "border-warning-300" : "" + }} + > + + {configs[item.key] === 'true' ? '已启用' : '已禁用'} + + + ); + + case 'select': + return ( + + ); + + default: + return null; + } }; if (loading) { @@ -215,17 +355,7 @@ export default function ConfigPage() {

- {hasChanges && ( - - )} +
+ + {/* 验证码弹层 */} + {showCaptcha && ( +
+ {/* 背景遮罩层 - 模糊效果 */} +
+ {/* 验证码容器 */} +
+
+
+
+ )} ); diff --git a/vite-frontend/src/styles/globals.css b/vite-frontend/src/styles/globals.css index 5b01bbd..c37fefb 100644 --- a/vite-frontend/src/styles/globals.css +++ b/vite-frontend/src/styles/globals.css @@ -30,4 +30,34 @@ html, body { } +/* 验证码弹层动画 */ +.captcha-modal-enter { + animation: captchaModalIn 0.3s ease-out forwards; +} + +@keyframes captchaModalIn { + from { + opacity: 0; + transform: scale(0.9) translateY(-20px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* 验证码弹层背景渐入 */ +.captcha-backdrop-enter { + animation: backdropFadeIn 0.3s ease-out forwards; +} + +@keyframes backdropFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + @config "../../tailwind.config.js" \ No newline at end of file diff --git a/vite-frontend/src/types/tac.d.ts b/vite-frontend/src/types/tac.d.ts new file mode 100644 index 0000000..c974788 --- /dev/null +++ b/vite-frontend/src/types/tac.d.ts @@ -0,0 +1,54 @@ +// TAC验证码SDK的TypeScript类型声明 + +declare global { + interface Window { + TAC: typeof TAC; + currentCaptcha?: any; + currentCaptchaRes?: any; + } +} + +interface CaptchaConfig { + // 生成接口 (必选项,必须配置) + requestCaptchaDataUrl: string; + // 验证接口 (必选项,必须配置) + validCaptchaUrl: string; + // 验证码绑定的div块 (必选项,必须配置) + bindEl: string; + // 验证成功回调函数(必选项,必须配置) + validSuccess: (res: any, captcha: any, tac: any) => void; + // 验证失败的回调函数(可选) + validFail?: (res: any, captcha: any, tac: any) => void; + // 请求头配置(可选) + requestHeaders?: Record; + // 关闭按钮回调(可选) + btnCloseFun?: (event: any, tac: any) => void; + // 刷新按钮回调(可选) + btnRefreshFun?: (event: any, tac: any) => void; + // 时间转时间戳(可选) + timeToTimestamp?: boolean; +} + +interface CaptchaStyle { + // 按钮样式URL(可选) + btnUrl?: string; + // 背景样式URL(可选) + bgUrl?: string; + // logo地址URL(可选) + logoUrl?: string | null; + // 滑动边框背景色(可选) + moveTrackMaskBgColor?: string; + // 滑动边框颜色(可选) + moveTrackMaskBorderColor?: string; +} + +declare class TAC { + constructor(config: CaptchaConfig, style?: CaptchaStyle); + + init(): TAC; + reloadCaptcha(): void; + destroyWindow(): void; + openCaptcha(): void; +} + +export {}; diff --git a/vite-frontend/src/utils/tac.css b/vite-frontend/src/utils/tac.css new file mode 100644 index 0000000..74d744f --- /dev/null +++ b/vite-frontend/src/utils/tac.css @@ -0,0 +1,6 @@ +#tianai-captcha-parent{box-shadow:0 0 11px 0 #999;width:318px;height:318px;overflow:hidden;position:relative;z-index:997;box-sizing:border-box;border-radius:5px;padding:8px}#tianai-captcha-parent #tianai-captcha-box{height:260px;width:100%;position:relative;overflow:hidden}#tianai-captcha-parent #tianai-captcha-box .loading{width:50px;height:50px;text-align:center;display:block;z-index:998;position:absolute;top:105px;left:126px;-moz-user-select:none;-webkit-user-select:none;user-select:none;background-image:url(../images/loading.gif);background-size:cover}#tianai-captcha-parent #tianai-captcha-box #tianai-captcha{transform-style:preserve-3d;will-change:transform;transition-duration:.45s;transform:translateX(-300px)}#tianai-captcha-parent #tianai-captcha-bg-img{background-color:#fff;background-position:top;background-size:cover;z-index:-1;width:100%;height:100%;top:0;left:0;position:absolute;border-radius:6px}#tianai-captcha-parent .slider-bottom{height:19px;width:100%}#tianai-captcha-parent .slider-bottom .close-btn{width:20px;height:20px;background-image:url(../images/icon.png);background-repeat:no-repeat;background-position:0 -14px;float:right;margin-right:2px;cursor:pointer}#tianai-captcha-parent .slider-bottom .refresh-btn{width:20px;height:20px;background-image:url(../images/icon.png);background-position:0 -167px;background-repeat:no-repeat;float:right;margin-right:10px;cursor:pointer}#tianai-captcha-parent .slider-bottom .logo{height:30px;float:left}#tianai-captcha-parent .slider-move-shadow{animation:myanimation 2s infinite;height:100%;width:5px;background-color:#fff;position:absolute;top:0;left:0;filter:opacity(0.5);box-shadow:1px 1px 1px #fff;border-radius:50%}#tianai-captcha-parent #tianai-captcha-slider-move-track-mask{border-width:1px;border-style:solid;border-color:#00f4ab;width:0;height:32px;background-color:#a9ffe5;opacity:.5;position:absolute;top:-1px;left:-1px;border-radius:5px} +#tianai-captcha{text-align:left;box-sizing:content-box;width:300px;height:260px;z-index:999}#tianai-captcha .slider-bottom .logo{height:30px}#tianai-captcha .slider-bottom{height:19px;width:100%}#tianai-captcha .content .tianai-captcha-tips{height:25px;width:100%;position:absolute;bottom:-25px;left:0;z-index:999;font-size:15px;line-height:25px;color:#fff;text-align:center;transition:bottom .3s ease-in-out}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-error{background-color:#ff5d39}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-success{background-color:#39c522}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-on{bottom:0}#tianai-captcha .content #tianai-captcha-loading{z-index:9999;background-color:#f5f5f5;text-align:center;height:100%;overflow:hidden;position:relative;display:flex;justify-content:center;align-items:center}#tianai-captcha .content #tianai-captcha-loading img{display:block;width:45px;height:45px}#tianai-captcha #tianai-captcha-slider-bg-canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}@keyframes myanimation{from{left:0}to{left:289px}} +#tianai-captcha.tianai-captcha-slider{z-index:999;position:absolute;left:0;top:0;user-select:none}#tianai-captcha.tianai-captcha-slider .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-slider .bg-img-div{width:100%;height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .bg-img-div img{height:100%;width:100%;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-img-div{height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .slider-img-div #tianai-captcha-slider-move-img{height:100%}#tianai-captcha.tianai-captcha-slider .slider-move{height:34px;width:100%;margin:11px 0;position:relative}#tianai-captcha.tianai-captcha-slider .slider-move-track{position:relative;height:32px;line-height:32px;text-align:center;background:#f5f5f5;color:#999;transition:0s;font-size:14px;box-sizing:content-box;border:1px solid #f5f5f5;border-radius:4px}#tianai-captcha.tianai-captcha-slider .refresh-btn,#tianai-captcha.tianai-captcha-slider .close-btn{display:inline-block}#tianai-captcha.tianai-captcha-slider .slider-move{line-height:38px;font-size:14px;text-align:center;white-space:nowrap;color:#88949d;-moz-user-select:none;-webkit-user-select:none;user-select:none;filter:opacity(0.8)}#tianai-captcha.tianai-captcha-slider .slider-move .slider-move-btn{transform:translate(0px, 0px);position:absolute;top:-6px;left:0;width:63px;height:45px;background-color:#fff;background-repeat:no-repeat;background-size:contain;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-tip{margin-bottom:5px;font-weight:bold;font-size:15px;line-height:normal;color:#000}#tianai-captcha.tianai-captcha-slider .slider-move-btn:hover{cursor:pointer} +#tianai-captcha.tianai-captcha-rotate .rotate-img-div{height:100%;text-align:center}#tianai-captcha.tianai-captcha-rotate .rotate-img-div img{height:100%;transform:rotate(0deg);display:inline-block} +#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-img-div{background-size:100% 180px;position:absolute;transform:translate(0px, 0px);z-index:1;width:100%}#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-bg-img{width:100%;height:100%;position:absolute;transform:translate(0px, 0px);background-size:100% 180px} +#tianai-captcha.tianai-captcha-word-click{position:relative;box-sizing:border-box}#tianai-captcha.tianai-captcha-word-click .click-tip{position:relative;height:40px;width:100%}#tianai-captcha.tianai-captcha-word-click .click-tip .tip-img{width:130px;position:absolute;right:15px}#tianai-captcha.tianai-captcha-word-click .click-tip #tianai-captcha-click-track-font{font-size:18px;display:inline-block;height:40px;line-height:40px;position:absolute}#tianai-captcha.tianai-captcha-word-click .slider-bottom{position:relative;top:6px}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask{width:100%;height:100%;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask .click-span{position:absolute;left:0;top:0;border-radius:50px;background-color:#409eff;width:20px;height:20px;text-align:center;line-height:20px;color:#fff;border:2px solid #fff;box-sizing:content-box} diff --git a/vite-frontend/src/utils/tac.min.js b/vite-frontend/src/utils/tac.min.js new file mode 100644 index 0000000..bc16872 --- /dev/null +++ b/vite-frontend/src/utils/tac.min.js @@ -0,0 +1 @@ +(()=>{"use strict";var t,e,a={783:(t,e,a)=>{var i=a(618),r=Object.create(null),n="undefined"==typeof document,c=Array.prototype.forEach;function s(){}function o(t,e){if(!e){if(!t.href)return;e=t.href.split("?")[0]}if(l(e)&&!1!==t.isLoaded&&e&&e.indexOf(".css")>-1){t.visited=!0;var a=t.cloneNode();a.isLoaded=!1,a.addEventListener("load",(function(){a.isLoaded||(a.isLoaded=!0,t.parentNode.removeChild(t))})),a.addEventListener("error",(function(){a.isLoaded||(a.isLoaded=!0,t.parentNode.removeChild(t))})),a.href="".concat(e,"?").concat(Date.now()),t.nextSibling?t.parentNode.insertBefore(a,t.nextSibling):t.parentNode.appendChild(a)}}function d(t){if(!t)return!1;var e=document.querySelectorAll("link"),a=!1;return c.call(e,(function(e){if(e.href){var r=function(t,e){var a;return t=i(t),e.some((function(i){t.indexOf(e)>-1&&(a=i)})),a}(e.href,t);l(r)&&!0!==e.visited&&r&&(o(e,r),a=!0)}})),a}function h(){var t=document.querySelectorAll("link");c.call(t,(function(t){!0!==t.visited&&o(t)}))}function l(t){return!!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(t)}t.exports=function(t,e){if(n)return s;var a,c,o,l=function(t){var e=r[t];if(!e){if(document.currentScript)e=document.currentScript.src;else{var a=document.getElementsByTagName("script"),n=a[a.length-1];n&&(e=n.src)}r[t]=e}return function(t){if(!e)return null;var a=e.split(/([^\\/]+)\.js$/),r=a&&a[1];return r&&t?t.split(",").map((function(t){var a=new RegExp("".concat(r,"\\.js$"),"g");return i(e.replace(a,"".concat(t.replace(/{fileName}/g,r),".css")))})):[e.replace(".js",".css")]}}(t);return a=function(){var t=d(l(e.filename));e.locals?h():t||h()},c=50,o=0,function(){var t=this,e=arguments;clearTimeout(o),o=setTimeout((function(){return a.apply(t,e)}),c)}}},618:t=>{t.exports=function(t){if(t=t.trim(),/^data:/i.test(t))return t;var e=-1!==t.indexOf("//")?t.split("//")[0]+"//":"",a=t.replace(new RegExp(e,"i"),"").split("/"),i=a[0].toLowerCase().replace(/\.$/,"");return a[0]="",e+i+a.reduce((function(t,e){switch(e){case"..":t.pop();break;case".":break;default:t.push(e)}return t}),[]).join("/")}},488:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},523:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},991:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},492:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},305:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},444:(t,e,a)=>{var i=a(783)(t.id,{locals:!1});t.hot.dispose(i),t.hot.accept(void 0,i)},600:(t,e,a)=>{a(488),a(523),a(444);function i(t){t.preventDefault&&t.preventDefault()}function r(t){A(t).each((t=>{t.addEventListener("touchmove",i,{passive:!1}),t.addEventListener("mousemove",i,{passive:!1})}))}function n(t){if(null!==t.pageX&&void 0!==t.pageX)return{x:Math.round(t.pageX),y:Math.round(t.pageY)};let e;return t.changedTouches?e=t.changedTouches:t.targetTouches?e=t.targetTouches:t.originalEvent&&t.originalEvent.targetTouches&&(e=t.originalEvent.targetTouches),null!==e[0].pageX&&void 0!==e[0].pageX?{x:Math.round(e[0].pageX),y:Math.round(e[0].pageY)}:{x:Math.round(e[0].clientX),y:Math.round(e[0].clientY)}}function c(t){const e=n(t);let a=e.x,i=e.y;currentCaptcha.currentCaptchaData.startX=a,currentCaptcha.currentCaptchaData.startY=i;const r=currentCaptcha.currentCaptchaData.startX,c=currentCaptcha.currentCaptchaData.startY,o=currentCaptcha.currentCaptchaData.startTime;currentCaptcha.currentCaptchaData.trackArr.push({x:r-a,y:c-i,type:"down",t:(new Date).getTime()-o.getTime()}),window.addEventListener("mousemove",s),window.addEventListener("mouseup",d),window.addEventListener("touchmove",s,!1),window.addEventListener("touchend",d,!1),window.currentCaptcha.doDown&&window.currentCaptcha.doDown(t,window.currentCaptcha)}function s(t){t.touches&&t.touches.length>0&&(t=t.touches[0]);const e=n(t);let a=e.x,i=e.y;const r=window.currentCaptcha.currentCaptchaData.startX,c=window.currentCaptcha.currentCaptchaData.startY,s=window.currentCaptcha.currentCaptchaData.startTime,o=window.currentCaptcha.currentCaptchaData.end,d=window.currentCaptcha.currentCaptchaData.bgImageWidth,h=window.currentCaptcha.currentCaptchaData.trackArr;let l=a-r,p=i-c;const u={x:a-r,y:i-c,type:"move",t:(new Date).getTime()-s.getTime()};h.push(u),l<0?l=0:l>o&&(l=o),window.currentCaptcha.currentCaptchaData.moveX=l,window.currentCaptcha.currentCaptchaData.movePercent=l/d,window.currentCaptcha.currentCaptchaData.moveY=p,window.currentCaptcha.doMove&&window.currentCaptcha.doMove(t,currentCaptcha)}function o(){window.removeEventListener("mousemove",s),window.removeEventListener("mouseup",d),window.removeEventListener("touchmove",s),window.removeEventListener("touchend",d)}function d(t){o();const e=n(t);currentCaptcha.currentCaptchaData.stopTime=new Date;let a=e.x,i=e.y;const r=currentCaptcha.currentCaptchaData.startX,c=currentCaptcha.currentCaptchaData.startY,s=currentCaptcha.currentCaptchaData.startTime,d=currentCaptcha.currentCaptchaData.trackArr,h={x:a-r,y:i-c,type:"up",t:(new Date).getTime()-s.getTime()};d.push(h),window.currentCaptcha.doUp&&window.currentCaptcha.doUp(t,window.currentCaptcha),window.currentCaptcha.endCallback(currentCaptcha.currentCaptchaData,currentCaptcha)}function h(t,e,a,i,r){const n={startTime:new Date,trackArr:[],movePercent:0,clickCount:0,bgImageWidth:Math.round(t),bgImageHeight:Math.round(e),templateImageWidth:Math.round(a),templateImageHeight:Math.round(i),end:r};return n}function l(t,e){A(t).find("#tianai-captcha-tips").removeClass("tianai-captcha-tips-on"),e&&setTimeout(e,.35)}function p(t,e,a,i){const r=A(t).find("#tianai-captcha-tips");r.text(e),1===a?(r.removeClass("tianai-captcha-tips-error"),r.addClass("tianai-captcha-tips-success")):(r.removeClass("tianai-captcha-tips-success"),r.addClass("tianai-captcha-tips-error")),r.addClass("tianai-captcha-tips-on"),setTimeout(i,1e3)}class u{showTips(t,e,a){p(this.el,t,e,a)}closeTips(t,e){l(this.el,t)}}function A(t,e){return new g(t,e)}class g{constructor(t,e){if(e&&"object"==typeof e&&void 0!==e.nodeType)return this.dom=e,void(this.domStr=t);if(t instanceof g)this.dom=t.dom,this.domStr=t.domStr;else if("string"==typeof t)this.dom=document.querySelector(t),this.domStr=t;else{if("object"!=typeof document||void 0===document.nodeType)throw new Error("不支持的类型");this.dom=t,this.domStr=t.nodeName}}each(t){this.getTarget().querySelectorAll("*").forEach(t)}removeClass(t){let e=this.getTarget();if(e.classList)e.classList.remove(t);else{const a=e.className,i=new RegExp("(?:^|\\s)"+t+"(?!\\S)","g");e.className=a.replace(i,"")}return this}addClass(t){const e=this.getTarget();if(e.classList)e.classList.add(t);else{let a=e.className;-1===a.indexOf(t)&&(e.className=a+" "+t)}return this}find(t){const e=this.getTarget().querySelector(t);return e?new g(t,e):null}children(t){const e=this.getTarget().childNodes;for(let a=0;a\n
\n 拖动滑块完成拼图\n
\n
\n
\n \n \n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n
\n')),this.el=this.boxEl.find("#tianai-captcha"),this.loadStyle(),this.el.find("#tianai-captcha-slider-move-btn").mousedown(c),this.el.find("#tianai-captcha-slider-move-btn").touchstart(c),window.currentCaptcha=this,this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}showTips(t,e,a){p(this.el,t,e,a)}closeTips(t){l(this.el,t)}destroy(){const t=this.boxEl.children("#tianai-captcha");t&&t.remove(),o()}doMove(){const t=this.currentCaptchaData.moveX;this.el.find("#tianai-captcha-slider-move-btn").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-img-div").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-move-track-mask").css("width",t+"px")}loadStyle(){let t="",e="#00f4ab",a="#a9ffe5";const i=this.styleConfig;i&&(t=i.btnUrl,a=i.moveTrackMaskBgColor,e=i.moveTrackMaskBorderColor),this.el.find(".slider-move .slider-move-btn").css("background-image","url("+t+")"),this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color",e),this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color",a)}loadCaptchaForData(t,e){const a=t.el.find("#tianai-captcha-slider-bg-img"),i=t.el.find("#tianai-captcha-slider-move-img");a.attr("src",e.captcha.backgroundImage),i.attr("src",e.captcha.templateImage),a.on("load",(()=>{t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height(),242),t.currentCaptchaData.currentCaptchaId=e.id}))}};a(305);const m=class extends u{constructor(t,e){super(),this.boxEl=A(t),this.styleConfig=e,this.type="ROTATE",this.currentCaptchaData={}}init(t,e,a){return this.destroy(),this.boxEl.append((this.styleConfig,'\n
\n
\n 拖动滑块完成拼图\n
\n
\n
\n \n \n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n')),this.el=this.boxEl.find("#tianai-captcha"),this.loadStyle(),this.el.find("#tianai-captcha-slider-move-btn").mousedown(c),this.el.find("#tianai-captcha-slider-move-btn").touchstart(c),window.currentCaptcha=this,this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}destroy(){const t=this.boxEl.children("#tianai-captcha");t&&t.remove(),o()}doMove(){const t=this.currentCaptchaData.moveX;this.el.find("#tianai-captcha-slider-move-btn").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-move-img").css("transform","rotate("+t/(this.currentCaptchaData.end/360)+"deg)"),this.el.find("#tianai-captcha-slider-move-track-mask").css("width",t+"px")}loadStyle(){let t="",e="#00f4ab",a="#a9ffe5";const i=this.styleConfig;i&&(t=i.btnUrl,a=i.moveTrackMaskBgColor,e=i.moveTrackMaskBorderColor),this.el.find(".slider-move .slider-move-btn").css("background-image","url("+t+")"),this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color",e),this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color",a)}loadCaptchaForData(t,e){const a=t.el.find("#tianai-captcha-slider-bg-img"),i=t.el.find("#tianai-captcha-slider-move-img");a.attr("src",e.captcha.backgroundImage),i.attr("src",e.captcha.templateImage),a.on("load",(()=>{t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height(),242),t.currentCaptchaData.currentCaptchaId=e.id}))}};a(991);const v=class extends u{constructor(t,e){super(),this.boxEl=A(t),this.styleConfig=e,this.type="CONCAT",this.currentCaptchaData={}}init(t,e,a){return this.destroy(),this.boxEl.append((this.styleConfig,'\n
\n
\n 拖动滑块完成拼图\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n ')),this.el=this.boxEl.find("#tianai-captcha"),this.loadStyle(),this.el.find("#tianai-captcha-slider-move-btn").mousedown(c),this.el.find("#tianai-captcha-slider-move-btn").touchstart(c),r(this.el),window.currentCaptcha=this,this.loadCaptchaForData(this,t),this.endCallback=e,a&&a(this),this}destroy(){o();const t=this.boxEl.children("#tianai-captcha");t&&t.remove()}doMove(){const t=this.currentCaptchaData.moveX;this.el.find("#tianai-captcha-slider-move-btn").css("transform","translate("+t+"px, 0px)"),this.el.find("#tianai-captcha-slider-concat-img-div").css("background-position-x",t+"px"),this.el.find("#tianai-captcha-slider-move-track-mask").css("width",t+"px")}loadStyle(){let t="",e="#00f4ab",a="#a9ffe5";const i=this.styleConfig;i&&(t=i.btnUrl,a=i.moveTrackMaskBgColor,e=i.moveTrackMaskBorderColor),this.el.find(".slider-move .slider-move-btn").css("background-image","url("+t+")"),this.el.find("#tianai-captcha-slider-move-track-mask").css("border-color",e),this.el.find("#tianai-captcha-slider-move-track-mask").css("background-color",a)}loadCaptchaForData(t,e){const a=t.el.find(".tianai-captcha-slider-concat-bg-img"),i=t.el.find("#tianai-captcha-slider-concat-img-div");a.css("background-image","url("+e.captcha.backgroundImage+")"),i.css("background-image","url("+e.captcha.backgroundImage+")"),i.css("background-position","0px 0px");var r=e.captcha.backgroundImageHeight,n=(r-e.captcha.data.randomY)/r*180;i.css("height",n+"px"),t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height(),242),t.currentCaptchaData.currentCaptchaId=e.id}};a(492);const C=class extends u{constructor(t,e){super(),this.boxEl=A(t),this.styleConfig=e,this.type="IMAGE_CLICK",this.currentCaptchaData={}}init(t,e,a){return this.destroy(),this.boxEl.append((this.styleConfig,'\n
\n
\n 请依次点击:\n \n
\n
\n
\n \n \n
\n
\n
\n
\n
\n')),this.el=this.boxEl.find("#tianai-captcha"),window.currentCaptcha=this,this.loadCaptchaForData(this,t),this.endCallback=e,this.el.find("#bg-img-click-mask").click((t=>{this.currentCaptchaData.clickCount++;const e=this.currentCaptchaData.trackArr,a=this.currentCaptchaData.startTime;1===this.currentCaptchaData.clickCount&&(window.addEventListener("mousemove",s),this.currentCaptchaData.startX=t.offsetX,this.currentCaptchaData.startY=t.offsetY),e.push({x:Math.round(t.offsetX),y:Math.round(t.offsetY),type:"click",t:(new Date).getTime()-a.getTime()});const i=t.offsetX-10,r=t.offsetY-10;this.el.find("#bg-img-click-mask").append(""+this.currentCaptchaData.clickCount+""),4===this.currentCaptchaData.clickCount&&(this.currentCaptchaData.stopTime=new Date,window.removeEventListener("mousemove",s),this.endCallback(this.currentCaptchaData,this))})),a&&a(this),this}destroy(){const t=this.boxEl.children("#tianai-captcha");t&&t.remove(),o()}loadCaptchaForData(t,e){const a=t.el.find("#tianai-captcha-slider-bg-img"),i=t.el.find("#tianai-captcha-tip-img");a.on("load",(()=>{t.currentCaptchaData=h(a.width(),a.height(),i.width(),i.height()),t.currentCaptchaData.currentCaptchaId=e.id})),a.attr("src",e.captcha.backgroundImage),i.attr("src",e.captcha.templateImage)}};const w=class extends C{constructor(t,e){super(t,e),this.type="WORD_IMAGE_CLICK"}};class b{constructor(t){if(!t.bindEl)throw new Error("[TAC] 必须配置 [bindEl]用于将验证码绑定到该元素上");if(!t.requestCaptchaDataUrl)throw new Error("[TAC] 必须配置 [requestCaptchaDataUrl]请求验证码接口");if(!t.validCaptchaUrl)throw new Error("[TAC] 必须配置 [validCaptchaUrl]验证验证码接口");this.bindEl=t.bindEl,this.domBindEl=A(t.bindEl),this.requestCaptchaDataUrl=t.requestCaptchaDataUrl,this.validCaptchaUrl=t.validCaptchaUrl,t.validSuccess&&(this.validSuccess=t.validSuccess),t.validFail&&(this.validFail=t.validFail),t.requestHeaders?this.requestHeaders=t.requestHeaders:this.requestHeaders={},t.btnCloseFun&&(this.btnCloseFun=t.btnCloseFun),t.btnRefreshFun&&(this.btnRefreshFun=t.btnRefreshFun),this.requestChain=[],this.timeToTimestamp=t.timeToTimestamp,this.insertRequestChain(0,{preRequest(t,e,a,i){if(this.timeToTimestamp&&e.data)for(let t in e.data)e.data[t]instanceof Date&&(e.data[t]=e.data[t].getTime());return!0}})}addRequestChain(t){this.requestChain.push(t)}insertRequestChain(t,e){this.requestChain.splice(t,0,e)}removeRequestChain(t){this.requestChain.splice(t,1)}requestCaptchaData(){const t={};t.headers=this.requestHeaders||{},t.data={},t.headers["Content-Type"]="application/json;charset=UTF-8",t.method="POST",t.url=this.requestCaptchaDataUrl,this._preRequest("requestCaptchaData",t);return this.doSendRequest(t).then((e=>(this._postRequest("requestCaptchaData",t,e),e)))}doSendRequest(t){if(t.headers)for(const e in t.headers)if(t.headers[e].indexOf("application/json")>-1){"string"!=typeof t.data&&(t.data=JSON.stringify(t.data));break}return(e=t,new Promise((function(t,a){var i=new XMLHttpRequest;if(i.open(e.method||"GET",e.url),e.headers)for(const t in e.headers)e.headers.hasOwnProperty(t)&&i.setRequestHeader(t,e.headers[t]);i.onreadystatechange=function(){if(i.readyState===XMLHttpRequest.DONE)if(i.status>=200&&i.status<=500){const e=i.getResponseHeader("Content-Type");e&&-1!==e.indexOf("application/json")?t(JSON.parse(i.responseText)):t(i.responseText)}else a(new Error("Request failed with status: "+i.status))},i.onerror=function(){a(new Error("Network Error"))},i.send(e.data)}))).then((t=>{try{return JSON.parse(t)}catch(e){return t}}));var e}_preRequest(t,e,a,i){for(let r=0;r(this._postRequest("validCaptcha",n,t,a,i),t))).then((t=>{if(200==t.code){const r=(e.stopTime-e.startTime)/1e3;a.showTips(`验证成功,耗时${r}秒`,1,(()=>this.validSuccess(t,a,i)))}else{let e="验证失败,请重新尝试!";t.code&&4001!=t.code&&(e="验证码被黑洞吸走了!"),a.showTips(e,0,(()=>this.validFail(t,a,i)))}})).catch((t=>{let e=a.styleConfig.i18n.tips_error;t.code&&200!=t.code&&(4001!=res.code&&(e=a.styleConfig.i18n.tips_4001),a.showTips(e,0,(()=>this.validFail(res,a,i))))}))}validSuccess(t,e,a){window.currentCaptchaRes=t,a.destroyWindow()}validFail(t,e,a){a.reloadCaptcha()}}window.TAC=class{constructor(t,e){this.config=function(t){return t instanceof b?t:new b(t)}(t),this.config.btnRefreshFun&&(this.btnRefreshFun=this.config.btnRefreshFun),this.config.btnCloseFun&&(this.btnCloseFun=this.config.btnCloseFun),this.style=function(t){return t||(t={}),t.btnUrl||(t.btnUrl="data:image/png;base64,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"),t.moveTrackMaskBgColor||t.moveTrackMaskBorderColor||(t.moveTrackMaskBgColor="#89d2ff",t.moveTrackMaskBorderColor="#0298f8"),t}(e)}init(){return this.destroyWindow(),this.config.domBindEl.append('\n
\n
\n
\n loading\n
\n \x3c!-- 底部 --\x3e\n
\n \n
\n
\n
\n
\n '),this.domTemplate=this.config.domBindEl.find("#tianai-captcha-parent"),r(this.domTemplate),this.loadStyle(),this.config.domBindEl.find("#tianai-captcha-slider-refresh-btn").click((t=>{this.btnRefreshFun(t,this)})),this.config.domBindEl.find("#tianai-captcha-slider-close-btn").click((t=>{this.btnCloseFun(t,this)})),this.reloadCaptcha(),this}btnRefreshFun(t,e){e.reloadCaptcha()}btnCloseFun(t,e){e.destroyWindow()}reloadCaptcha(){this.showLoading(),this.destroyCaptcha((()=>{this.createCaptcha()}))}showLoading(){this.config.domBindEl.find("#tianai-captcha-loading").css("display","block")}closeLoading(){this.config.domBindEl.find("#tianai-captcha-loading").css("display","none")}loadStyle(){const t=this.style.bgUrl,e=this.style.logoUrl;t&&this.config.domBindEl.find("#tianai-captcha-bg-img").css("background-image","url("+t+")"),e&&""!==e?this.config.domBindEl.find("#tianai-captcha-logo").attr("src",e):null===e&&this.config.domBindEl.find("#tianai-captcha-logo").css("display","none")}destroyWindow(){window.currentCaptcha=void 0,this.domTemplate&&this.domTemplate.remove()}openCaptcha(){setTimeout((()=>{window.currentCaptcha.el.css("transform","translateX(0)")}),10)}createCaptcha(){this.config.requestCaptchaData().then((t=>{this.closeLoading();const e=function(t,e){switch(t){case"SLIDER":return new f("#tianai-captcha-box",e);case"ROTATE":return new m("#tianai-captcha-box",e);case"CONCAT":return new v("#tianai-captcha-box",e);case"WORD_IMAGE_CLICK":return new w("#tianai-captcha-box",e);default:return null}}(t.captcha.type,this.style);if(null==e)throw new Error("[TAC] 未知的验证码类型["+t.captcha.type+"]");e.init(t,((t,e)=>{const a=e.currentCaptchaData,i={bgImageWidth:a.bgImageWidth,bgImageHeight:a.bgImageHeight,templateImageWidth:a.sliderImageWidth,templateImageHeight:a.sliderImageHeight,startTime:a.startTime,stopTime:a.stopTime,trackList:a.trackArr};"ROTATE_DEGREE"!==e.type&&"ROTATE"!==e.type||(i.bgImageWidth=e.currentCaptchaData.end);const r=e.currentCaptchaData.currentCaptchaId;e.currentCaptchaData=void 0,this.config.validCaptcha(r,i,e,this)})),this.openCaptcha()}))}destroyCaptcha(t){window.currentCaptcha?(window.currentCaptcha.el.css("transform","translateX(300px)"),setTimeout((()=>{window.currentCaptcha.destroy(),t&&t()}),500)):t()}},window.CaptchaConfig=b}},i={};function r(t){var e=i[t];if(void 0!==e){if(void 0!==e.error)throw e.error;return e.exports}var n=i[t]={id:t,exports:{}};try{var c={id:t,module:n,factory:a[t],require:r};r.i.forEach((function(t){t(c)})),n=c.module,c.factory.call(n.exports,n,n.exports,c.require)}catch(t){throw n.error=t,t}return n.exports}r.m=a,r.c=i,r.i=[],r.hu=t=>t+"."+r.h()+".hot-update.js",r.miniCssF=t=>{},r.hmrF=()=>"main."+r.h()+".hot-update.json",r.h=()=>"4017d115f7b426a57c5b",r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),t={},e="webpack-demo:",r.l=(a,i,n,c)=>{if(t[a])t[a].push(i);else{var s,o;if(void 0!==n)for(var d=document.getElementsByTagName("script"),h=0;h{s.onerror=s.onload=null,clearTimeout(u);var r=t[a];if(delete t[a],s.parentNode&&s.parentNode.removeChild(s),r&&r.forEach((t=>t(i))),e)return e(i)},u=setTimeout(p.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=p.bind(null,s.onerror),s.onload=p.bind(null,s.onload),o&&document.head.appendChild(s)}},(()=>{var t,e,a,i={},n=r.c,c=[],s=[],o="idle",d=0,h=[];function l(t){o=t;for(var e=[],a=0;a0)return l("abort").then((function(){throw r[0]}));var n=l("dispose");i.forEach((function(t){t.dispose&&t.dispose()}));var c,s=l("apply"),o=function(t){c||(c=t)},d=[];return i.forEach((function(t){if(t.apply){var e=t.apply(o);if(e)for(var a=0;a=0&&v._disposeHandlers.splice(e,1)},invalidate:function(){switch(this._selfInvalidated=!0,o){case"idle":e=[],Object.keys(r.hmrI).forEach((function(t){r.hmrI[t](g,e)})),l("ready");break;case"ready":Object.keys(r.hmrI).forEach((function(t){r.hmrI[t](g,e)}));break;case"prepare":case"check":case"dispose":case"apply":(a=a||[]).push(g)}},check:u,apply:A,status:function(t){if(!t)return o;s.push(t)},addStatusHandler:function(t){s.push(t)},removeStatusHandler:function(t){var e=s.indexOf(t);e>=0&&s.splice(e,1)},data:i[g]},t=void 0,v),C.parents=c,C.children=[],c=[],h.require=w})),r.hmrC={},r.hmrI={}})(),(()=>{var t;r.g.importScripts&&(t=r.g.location+"");var e=r.g.document;if(!t&&e&&(e.currentScript&&(t=e.currentScript.src),!t)){var a=e.getElementsByTagName("script");if(a.length)for(var i=a.length-1;i>-1&&!t;)t=a[i--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),r.p=t})(),(()=>{if("undefined"!=typeof document){var t=(t,e,a,i,r)=>{var n=document.createElement("link");n.rel="stylesheet",n.type="text/css";return n.onerror=n.onload=a=>{if(n.onerror=n.onload=null,"load"===a.type)i();else{var c=a&&("load"===a.type?"missing":a.type),s=a&&a.target&&a.target.href||e,o=new Error("Loading CSS chunk "+t+" failed.\n("+s+")");o.code="CSS_CHUNK_LOAD_FAILED",o.type=c,o.request=s,n.parentNode&&n.parentNode.removeChild(n),r(o)}},n.href=e,a?a.parentNode.insertBefore(n,a.nextSibling):document.head.appendChild(n),n},e=(t,e)=>{for(var a=document.getElementsByTagName("link"),i=0;i({dispose:()=>{for(var t=0;t{for(var t=0;t{h.push(n),c.forEach((n=>{var c=r.miniCssF(n),s=r.p+c,o=e(c,s);o&&d.push(new Promise(((e,r)=>{var c=t(n,s,o,(()=>{c.as="style",c.rel="preload",e()}),r);a.push(o),i.push(c)})))}))}}})(),(()=>{var t,e,a,i,n,c=r.hmrS_jsonp=r.hmrS_jsonp||{179:0},s={};function o(e,a){return t=a,new Promise(((t,a)=>{s[e]=t;var i=r.p+r.hu(e),n=new Error;r.l(i,(t=>{if(s[e]){s[e]=void 0;var i=t&&("load"===t.type?"missing":t.type),r=t&&t.target&&t.target.src;n.message="Loading hot update chunk "+e+" failed.\n("+i+": "+r+")",n.name="ChunkLoadError",n.type=i,n.request=r,a(n)}}))}))}function d(t){function s(t){for(var e=[t],a={},i=e.map((function(t){return{chain:[t],id:t}}));i.length>0;){var n=i.pop(),c=n.id,s=n.chain,d=r.c[c];if(d&&(!d.hot._selfAccepted||d.hot._selfInvalidated)){if(d.hot._selfDeclined)return{type:"self-declined",chain:s,moduleId:c};if(d.hot._main)return{type:"unaccepted",chain:s,moduleId:c};for(var h=0;h ")),A.type){case"self-declined":t.onDeclined&&t.onDeclined(A),t.ignoreDeclined||(f=new Error("Aborted because of self decline: "+A.moduleId+C));break;case"declined":t.onDeclined&&t.onDeclined(A),t.ignoreDeclined||(f=new Error("Aborted because of declined dependency: "+A.moduleId+" in "+A.parentId+C));break;case"unaccepted":t.onUnaccepted&&t.onUnaccepted(A),t.ignoreUnaccepted||(f=new Error("Aborted because "+u+" is not accepted"+C));break;case"accepted":t.onAccepted&&t.onAccepted(A),m=!0;break;case"disposed":t.onDisposed&&t.onDisposed(A),v=!0;break;default:throw new Error("Unexception type "+A.type)}if(f)return{error:f};if(m)for(u in l[u]=g,o(h,A.outdatedModules),A.outdatedDependencies)r.o(A.outdatedDependencies,u)&&(d[u]||(d[u]=[]),o(d[u],A.outdatedDependencies[u]));v&&(o(h,[A.moduleId]),l[u]=p)}a=void 0;for(var w,b=[],D=0;D0;){var n=a.pop(),s=r.c[n];if(s){var o={},l=s.hot._disposeHandlers;for(D=0;D=0&&p.parents.splice(t,1))}}}for(var u in d)if(r.o(d,u)&&(s=r.c[u]))for(w=d[u],D=0;D=0&&s.children.splice(t,1)},apply:function(e){for(var a in l)r.o(l,a)&&(r.m[a]=l[a]);for(var i=0;i{for(var o in i)r.o(i,o)&&(a[o]=i[o],t&&t.push(o));c&&n.push(c),s[e]&&(s[e](),s[e]=void 0)},r.hmrI.jsonp=function(t,e){a||(a={},n=[],i=[],e.push(d)),r.o(a,t)||(a[t]=r.m[t])},r.hmrC.jsonp=function(t,s,h,l,p,u){p.push(d),e={},i=s,a=h.reduce((function(t,e){return t[e]=!1,t}),{}),n=[],t.forEach((function(t){r.o(c,t)&&void 0!==c[t]?(l.push(o(t,u)),e[t]=!0):e[t]=!1})),r.f&&(r.f.jsonpHmr=function(t,a){e&&r.o(e,t)&&!e[t]&&(a.push(o(t)),e[t]=!0)})},r.hmrM=()=>{if("undefined"==typeof fetch)throw new Error("No browser support: need fetch API");return fetch(r.p+r.hmrF()).then((t=>{if(404!==t.status){if(!t.ok)throw new Error("Failed to fetch update manifest "+t.statusText);return t.json()}}))}})();r(600)})(); \ No newline at end of file