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 (
+ {
+ const selectedKey = Array.from(keys)[0] as string;
+ if (selectedKey) {
+ handleConfigChange(item.key, selectedKey);
+ }
+ }}
+ placeholder="请选择验证码类型"
+ variant="bordered"
+ size="md"
+ classNames={{
+ trigger: isChanged
+ ? "border-warning-300 data-[hover=true]:border-warning-400"
+ : ""
+ }}
+ >
+ {item.options?.map((option) => (
+
+ {option.label}
+
+ )) || []}
+
+ );
+
+ default:
+ return null;
+ }
};
if (loading) {
@@ -215,17 +355,7 @@ export default function ConfigPage() {
- {hasChanges && (
-
- 重置
-
- )}
+
}
@@ -242,36 +372,39 @@ export default function ConfigPage() {
- {CONFIG_ITEMS.map((item, index) => (
-
-
-
- {item.label}
-
- {item.description && (
-
- {item.description}
-
+ {CONFIG_ITEMS.map((item, index) => {
+ // 检查配置项是否应该显示
+ if (!shouldShowItem(item)) {
+ return null;
+ }
+
+ // 计算是否是最后一个显示的项目(用于决定是否显示分隔线)
+ const remainingItems = CONFIG_ITEMS.slice(index + 1).filter(shouldShowItem);
+ const isLastItem = remainingItems.length === 0;
+
+ return (
+
+
+
+ {item.label}
+
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+ {/* 渲染配置项 */}
+ {renderConfigItem(item)}
+
+ {/* 分隔线 */}
+ {!isLastItem && (
+
)}
-
handleConfigChange(item.key, e.target.value)}
- placeholder={item.placeholder}
- variant="bordered"
- size="md"
- classNames={{
- input: "text-sm",
- inputWrapper: hasChanges && configs[item.key] !== originalConfigs[item.key]
- ? "border-warning-300 data-[hover=true]:border-warning-400"
- : ""
- }}
- />
- {index < CONFIG_ITEMS.length - 1 && (
-
- )}
-
- ))}
+ );
+ })}
diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx
index 5d617e4..e31fac6 100644
--- a/vite-frontend/src/pages/dashboard.tsx
+++ b/vite-frontend/src/pages/dashboard.tsx
@@ -3,6 +3,7 @@ import { Button } from "@heroui/button";
import { Modal, ModalContent, ModalHeader, ModalBody } from "@heroui/modal";
import { useState, useEffect } from "react";
import toast from 'react-hot-toast';
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import AdminLayout from "@/layouts/admin";
import { getUserPackageInfo } from "@/api";
@@ -48,11 +49,20 @@ interface AddressItem {
copying: boolean;
}
+interface StatisticsFlow {
+ id: number;
+ userId: number;
+ flow: number;
+ totalFlow: number;
+ time: string;
+}
+
export default function DashboardPage() {
const [loading, setLoading] = useState(true);
const [userInfo, setUserInfo] = useState
({} as UserInfo);
const [userTunnels, setUserTunnels] = useState([]);
const [forwardList, setForwardList] = useState([]);
+ const [statisticsFlows, setStatisticsFlows] = useState([]);
const [addressModalOpen, setAddressModalOpen] = useState(false);
const [addressModalTitle, setAddressModalTitle] = useState('');
@@ -154,6 +164,7 @@ export default function DashboardPage() {
setUserInfo({} as UserInfo);
setUserTunnels([]);
setForwardList([]);
+ setStatisticsFlows([]);
loadPackageData();
localStorage.setItem('e', '/dashboard');
@@ -168,6 +179,7 @@ export default function DashboardPage() {
setUserInfo(data.userInfo || {});
setUserTunnels(data.tunnelPermissions || []);
setForwardList(data.forwards || []);
+ setStatisticsFlows(data.statisticsFlows || []);
// 检查有效期并显示通知
checkExpirationNotifications(data.userInfo, data.tunnelPermissions || []);
@@ -207,6 +219,33 @@ export default function DashboardPage() {
return value.toString();
};
+ // 处理24小时流量统计数据
+ const processFlowChartData = () => {
+ // 生成最近24小时的时间数组(从当前小时往前推24小时)
+ const now = new Date();
+ const hours: string[] = [];
+ for (let i = 23; i >= 0; i--) {
+ const time = new Date(now.getTime() - i * 60 * 60 * 1000);
+ const hourString = time.getHours().toString().padStart(2, '0') + ':00';
+ hours.push(hourString);
+ }
+
+ // 创建数据映射
+ const flowMap = new Map();
+ statisticsFlows.forEach(item => {
+ flowMap.set(item.time, item.flow || 0);
+ });
+
+ // 生成图表数据,没有数据的小时显示为0
+ return hours.map(hour => ({
+ time: hour,
+ flow: flowMap.get(hour) || 0,
+ // 格式化显示用的流量值
+ formattedFlow: formatFlow(flowMap.get(hour) || 0)
+ }));
+ };
+
+
const getExpStatus = (expTime?: string) => {
if (!expTime) return {
color: 'text-green-600 dark:text-green-400',
@@ -636,6 +675,82 @@ export default function DashboardPage() {
+ {/* 24小时流量统计图表 */}
+
+
+
+
+
+ {statisticsFlows.length === 0 ? (
+
+ ) : (
+
+
+ {/* 流量趋势图 */}
+
+
+
+
+
+ {
+ if (value === 0) return '0';
+ if (value < 1024) return `${value}B`;
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}K`;
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)}M`;
+ return `${(value / (1024 * 1024 * 1024)).toFixed(1)}G`;
+ }}
+ />
+ {
+ if (active && payload && payload.length) {
+ return (
+
+
{`时间: ${label}`}
+
+ {`流量: ${formatFlow(payload[0]?.value as number || 0)}`}
+
+
+ );
+ }
+ return null;
+ }}
+ />
+
+
+
+
+
+ )}
+
+
+
{/* 隧道权限 */}
diff --git a/vite-frontend/src/pages/index.tsx b/vite-frontend/src/pages/index.tsx
index 349ced4..ecebafa 100644
--- a/vite-frontend/src/pages/index.tsx
+++ b/vite-frontend/src/pages/index.tsx
@@ -1,27 +1,63 @@
import { Button } from "@heroui/button";
import { Input } from "@heroui/input";
import { Card, CardBody, CardHeader } from "@heroui/card";
-import { useState } from "react";
+import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import toast from 'react-hot-toast';
import { title } from "@/components/primitives";
import DefaultLayout from "@/layouts/default";
-import { login, LoginData } from "@/api";
+import { login, LoginData, checkCaptcha } from "@/api";
+import "@/utils/tac.css";
+import "@/utils/tac.min.js";
+
interface LoginForm {
username: string;
password: string;
+ captchaId: string;
+}
+
+
+
+interface CaptchaConfig {
+ requestCaptchaDataUrl: string;
+ validCaptchaUrl: string;
+ bindEl: string;
+ validSuccess: (res: any, captcha: any, tac: any) => void;
+ validFail?: (res: any, captcha: any, tac: any) => void;
+ btnCloseFun?: (event: any, tac: any) => void;
+ btnRefreshFun?: (event: any, tac: any) => void;
+}
+
+interface CaptchaStyle {
+ logoUrl?: string | null;
+ moveTrackMaskBgColor?: string;
+ moveTrackMaskBorderColor?: string;
}
export default function IndexPage() {
const [form, setForm] = useState({
username: "",
- password: ""
+ password: "",
+ captchaId: "",
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState>({});
+ const [showCaptcha, setShowCaptcha] = useState(false);
const navigate = useNavigate();
+ const tacInstanceRef = useRef(null);
+ const captchaContainerRef = useRef(null);
+
+ // 清理验证码实例
+ useEffect(() => {
+ return () => {
+ if (tacInstanceRef.current) {
+ tacInstanceRef.current.destroyWindow();
+ tacInstanceRef.current = null;
+ }
+ };
+ }, []);
// 验证表单
const validateForm = (): boolean => {
@@ -37,6 +73,7 @@ export default function IndexPage() {
newErrors.password = '密码长度至少6位';
}
+
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@@ -50,21 +87,81 @@ export default function IndexPage() {
}
};
- const handleLogin = async () => {
- if (!validateForm()) return;
+ // 初始化验证码
+ const initCaptcha = async () => {
+ if (!window.TAC || !captchaContainerRef.current) {
+ return;
+ }
+
+ try {
+ // 清理之前的验证码实例
+ if (tacInstanceRef.current) {
+ tacInstanceRef.current.destroyWindow();
+ tacInstanceRef.current = null;
+ }
+
+ // 获取API基础URL
+ const baseURL = import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1` : '/api/v1';
+
+ const config: CaptchaConfig = {
+ requestCaptchaDataUrl: `${baseURL}/captcha/generate`,
+ validCaptchaUrl: `${baseURL}/captcha/verify`,
+ bindEl: "#captcha-container",
+ validSuccess: (res: any, _: any, tac: any) => {
+
+
+ form.captchaId = res.data.validToken
+
+ setShowCaptcha(false);
+ tac.destroyWindow();
+ performLogin();
+ },
+ validFail: (_: any, _captcha: any, tac: any) => {
+ toast.error("验证码验证失败,请重试");
+ tac.reloadCaptcha();
+ },
+ btnCloseFun: (_event: any, tac: any) => {
+ setShowCaptcha(false);
+ tac.destroyWindow();
+ setLoading(false);
+ },
+ btnRefreshFun: (_event: any, tac: any) => {
+ tac.reloadCaptcha();
+ }
+ };
+
+ const style: CaptchaStyle = {
+ logoUrl: null, // 去除默认logo
+ moveTrackMaskBgColor: "#0298f8",
+ moveTrackMaskBorderColor: "#0298f8"
+ };
+
+ tacInstanceRef.current = new window.TAC(config, style);
+ tacInstanceRef.current.init();
+
+ } catch (error) {
+ console.error('初始化验证码失败:', error);
+ toast.error('验证码初始化失败,请刷新页面重试');
+ setShowCaptcha(false);
+ setLoading(false);
+ }
+ };
+
+ // 执行登录请求
+ const performLogin = async () => {
- setLoading(true);
try {
const loginData: LoginData = {
username: form.username.trim(),
- password: form.password
+ password: form.password,
+ captchaId: form.captchaId,
};
const response = await login(loginData);
if (response.code !== 0) {
- toast.error(response.msg || "登陆失败");
+ toast.error(response.msg || "登录失败");
return;
}
@@ -73,33 +170,65 @@ export default function IndexPage() {
localStorage.setItem('token', response.data.token);
localStorage.setItem("role_id", response.data.role_id.toString());
localStorage.setItem("name", response.data.name);
- // 设置admin字段 - role_id为0的是管理员
localStorage.setItem("admin", (response.data.role_id === 0).toString());
toast.success('检测到默认密码,即将跳转到修改密码页面');
- // 直接跳转到修改密码页面
navigate("/change-password");
return;
}
- // 保存登陆信息
+ // 保存登录信息
localStorage.setItem('token', response.data.token);
localStorage.setItem("role_id", response.data.role_id.toString());
localStorage.setItem("name", response.data.name);
- // 设置admin字段 - role_id为0的是管理员
localStorage.setItem("admin", (response.data.role_id === 0).toString());
- // 登陆成功
- toast.success('登陆成功');
+ // 登录成功
+ toast.success('登录成功');
navigate("/dashboard");
} catch (error) {
- console.error('登陆错误:', error);
+ console.error('登录错误:', error);
toast.error("网络错误,请稍后重试");
} finally {
setLoading(false);
}
};
+ const handleLogin = async () => {
+ if (!validateForm()) return;
+
+ setLoading(true);
+
+ try {
+ // 先检查是否需要验证码
+ const checkResponse = await checkCaptcha();
+
+ if (checkResponse.code !== 0) {
+ toast.error("检查验证码状态失败,请重试");
+ setLoading(false);
+ return;
+ }
+
+ // 根据返回值决定是否显示验证码
+ if (checkResponse.data === 0) {
+ // 不需要验证码,直接登录
+ await performLogin();
+ } else {
+ // 需要验证码,显示验证码弹层
+ setShowCaptcha(true);
+ // 延时初始化验证码,确保DOM已渲染
+ setTimeout(() => {
+ initCaptcha();
+ }, 100);
+ }
+ } catch (error) {
+ console.error('检查验证码状态错误:', error);
+ toast.error("网络错误,请稍后重试");
+ setLoading(false);
+ }
+ };
+
+
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !loading) {
handleLogin();
@@ -139,8 +268,8 @@ export default function IndexPage() {
variant="bordered"
isDisabled={loading}
isInvalid={!!errors.password}
- errorMessage={errors.password}
/>
+
- {loading ? "登陆中..." : "登陆"}
+ {loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"}
+
+ {/* 验证码弹层 */}
+ {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')),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')),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 ')),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
\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