mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
增加网站配置
This commit is contained in:
@@ -1033,7 +1033,7 @@ func tcpPingHost(ip string, port int, count int, timeoutMs int) (float64, float6
|
||||
totalTime += elapsed.Seconds() * 1000 // 转换为毫秒
|
||||
successCount++
|
||||
}
|
||||
|
||||
|
||||
// 如果不是最后一次,等待一下再进行下次测试
|
||||
if i < count-1 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
@@ -154,9 +154,20 @@ CREATE TABLE `user_tunnel` (
|
||||
`status` int(10) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- 转储表的索引
|
||||
--
|
||||
CREATE TABLE `vite_config` (
|
||||
`id` int(10) NOT NULL,
|
||||
`name` varchar(200) NOT NULL,
|
||||
`value` varchar(200) NOT NULL,
|
||||
`time` bigint(20) NOT NULL,
|
||||
UNIQUE KEY `unique_name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `vite_config` (`id`, `name`, `value`, `time`) VALUES
|
||||
(1, 'app_name', '哆啦A梦', 1753344708000);
|
||||
|
||||
ALTER TABLE `vite_config`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `unique_name` (`name`);
|
||||
|
||||
--
|
||||
-- 表的索引 `forward`
|
||||
@@ -202,37 +213,42 @@ ALTER TABLE `user_tunnel`
|
||||
-- 使用表AUTO_INCREMENT `forward`
|
||||
--
|
||||
ALTER TABLE `forward`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=164;
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `node`
|
||||
--
|
||||
ALTER TABLE `node`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=27;
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `speed_limit`
|
||||
--
|
||||
ALTER TABLE `speed_limit`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=70;
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `tunnel`
|
||||
--
|
||||
ALTER TABLE `tunnel`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=59;
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `user`
|
||||
--
|
||||
ALTER TABLE `user`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=38;
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `user_tunnel`
|
||||
--
|
||||
ALTER TABLE `user_tunnel`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=56;
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT;
|
||||
COMMIT;
|
||||
|
||||
|
||||
ALTER TABLE `vite_config`
|
||||
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
|
||||
@@ -754,6 +754,23 @@ UPDATE \`forward\`
|
||||
SET \`strategy\` = 'fifo'
|
||||
WHERE \`strategy\` IS NULL;
|
||||
|
||||
-- 创建 vite_config 表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS \`vite_config\` (
|
||||
\`id\` int(10) NOT NULL AUTO_INCREMENT,
|
||||
\`name\` varchar(200) NOT NULL,
|
||||
\`value\` varchar(200) NOT NULL,
|
||||
\`time\` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (\`id\`),
|
||||
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'
|
||||
);
|
||||
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addInterceptor(jwtInterceptor())
|
||||
.addPathPatterns("/api/**")
|
||||
.excludePathPatterns("/flow/**")
|
||||
.excludePathPatterns("/api/v1/config/get")
|
||||
.excludePathPatterns("/api/v1/user/login");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ public class BaseController {
|
||||
@Autowired
|
||||
ForwardService forwardService;
|
||||
|
||||
@Autowired
|
||||
ViteConfigService viteConfigService;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.admin.controller;
|
||||
|
||||
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.lang.R;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 网站配置控制器
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api/v1/config")
|
||||
public class ViteConfigController extends BaseController {
|
||||
|
||||
/**
|
||||
* 获取所有网站配置
|
||||
* 前端无需权限即可访问,用于获取网站基本信息
|
||||
*/
|
||||
@LogAnnotation
|
||||
@PostMapping("/list")
|
||||
public R getConfigs() {
|
||||
return viteConfigService.getConfigs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置名获取配置值
|
||||
* 前端无需权限即可访问,用于获取特定配置
|
||||
*/
|
||||
@LogAnnotation
|
||||
@PostMapping("/get")
|
||||
public R getConfigByName(@RequestBody Map<String, Object> params) {
|
||||
String name = params.get("name").toString();
|
||||
return viteConfigService.getConfigByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新网站配置
|
||||
* 需要管理员权限
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update")
|
||||
public R updateConfigs(@RequestBody Map<String, String> configMap) {
|
||||
return viteConfigService.updateConfigs(configMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个配置项
|
||||
* 需要管理员权限
|
||||
*/
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/update-single")
|
||||
public R updateConfig(@RequestBody Map<String, Object> params) {
|
||||
String name = params.get("name").toString();
|
||||
String value = params.get("value").toString();
|
||||
return viteConfigService.updateConfig(name, value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
*
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
public class ViteConfig {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String value;
|
||||
|
||||
private Long time;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.admin.mapper;
|
||||
|
||||
import com.admin.entity.ViteConfig;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
public interface ViteConfigMapper extends BaseMapper<ViteConfig> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.admin.service;
|
||||
|
||||
import com.admin.entity.ViteConfig;
|
||||
import com.admin.common.lang.R;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 网站配置服务类
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
public interface ViteConfigService extends IService<ViteConfig> {
|
||||
|
||||
/**
|
||||
* 获取所有网站配置
|
||||
* @return 配置Map,key为配置名,value为配置值
|
||||
*/
|
||||
R getConfigs();
|
||||
|
||||
/**
|
||||
* 根据配置名获取配置值
|
||||
* @param name 配置名
|
||||
* @return 配置值
|
||||
*/
|
||||
R getConfigByName(String name);
|
||||
|
||||
/**
|
||||
* 批量更新网站配置
|
||||
* @param configMap 配置Map,key为配置名,value为新的配置值
|
||||
* @return 更新结果
|
||||
*/
|
||||
R updateConfigs(Map<String, String> configMap);
|
||||
|
||||
/**
|
||||
* 更新单个配置项
|
||||
* @param name 配置名
|
||||
* @param value 配置值
|
||||
* @return 更新结果
|
||||
*/
|
||||
R updateConfig(String name, String value);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.admin.service.impl;
|
||||
|
||||
import com.admin.entity.ViteConfig;
|
||||
import com.admin.mapper.ViteConfigMapper;
|
||||
import com.admin.service.ViteConfigService;
|
||||
import com.admin.common.lang.R;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 网站配置服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-07-24
|
||||
*/
|
||||
@Service
|
||||
public class ViteConfigServiceImpl extends ServiceImpl<ViteConfigMapper, ViteConfig> implements ViteConfigService {
|
||||
|
||||
// ========== 常量定义 ==========
|
||||
|
||||
/** 成功响应消息 */
|
||||
private static final String SUCCESS_UPDATE_MSG = "配置更新成功";
|
||||
|
||||
/** 错误响应消息 */
|
||||
private static final String ERROR_UPDATE_MSG = "配置更新失败";
|
||||
private static final String ERROR_CONFIG_NOT_FOUND = "配置不存在";
|
||||
private static final String ERROR_CONFIG_NAME_REQUIRED = "配置名称不能为空";
|
||||
private static final String ERROR_CONFIG_VALUE_REQUIRED = "配置值不能为空";
|
||||
|
||||
// ========== 公共接口实现 ==========
|
||||
|
||||
/**
|
||||
* 获取所有网站配置
|
||||
*
|
||||
* @return 包含所有配置的Map
|
||||
*/
|
||||
@Override
|
||||
public R getConfigs() {
|
||||
List<ViteConfig> configList = this.list();
|
||||
Map<String, String> configMap = new HashMap<>();
|
||||
|
||||
for (ViteConfig config : configList) {
|
||||
configMap.put(config.getName(), config.getValue());
|
||||
}
|
||||
|
||||
return R.ok(configMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置名称获取配置值
|
||||
*
|
||||
* @param name 配置名称
|
||||
* @return 配置响应对象
|
||||
*/
|
||||
@Override
|
||||
public R getConfigByName(String name) {
|
||||
if (!StringUtils.hasText(name)) {
|
||||
return R.err(ERROR_CONFIG_NAME_REQUIRED);
|
||||
}
|
||||
|
||||
QueryWrapper<ViteConfig> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("name", name);
|
||||
ViteConfig config = this.getOne(queryWrapper);
|
||||
|
||||
if (config == null) {
|
||||
return R.err(ERROR_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
return R.ok(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新网站配置
|
||||
*
|
||||
* @param configMap 配置Map
|
||||
* @return 更新结果响应
|
||||
*/
|
||||
@Override
|
||||
public R updateConfigs(Map<String, String> configMap) {
|
||||
if (configMap == null || configMap.isEmpty()) {
|
||||
return R.err("配置数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
for (Map.Entry<String, String> entry : configMap.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
if (!StringUtils.hasText(name)) {
|
||||
continue; // 跳过无效的配置名
|
||||
}
|
||||
|
||||
updateOrCreateConfig(name, value);
|
||||
}
|
||||
return R.ok(SUCCESS_UPDATE_MSG);
|
||||
} catch (Exception e) {
|
||||
return R.err(ERROR_UPDATE_MSG + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个配置项
|
||||
*
|
||||
* @param name 配置名
|
||||
* @param value 配置值
|
||||
* @return 更新结果响应
|
||||
*/
|
||||
@Override
|
||||
public R updateConfig(String name, String value) {
|
||||
// 1. 验证必填字段
|
||||
if (!StringUtils.hasText(name)) {
|
||||
return R.err(ERROR_CONFIG_NAME_REQUIRED);
|
||||
}
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return R.err(ERROR_CONFIG_VALUE_REQUIRED);
|
||||
}
|
||||
|
||||
try {
|
||||
updateOrCreateConfig(name, value);
|
||||
return R.ok(SUCCESS_UPDATE_MSG);
|
||||
} catch (Exception e) {
|
||||
return R.err(ERROR_UPDATE_MSG + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
/**
|
||||
* 更新或创建配置项
|
||||
* 如果配置存在则更新,不存在则创建
|
||||
*/
|
||||
private void updateOrCreateConfig(String name, String value) {
|
||||
QueryWrapper<ViteConfig> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("name", name);
|
||||
ViteConfig existingConfig = this.getOne(queryWrapper);
|
||||
|
||||
if (existingConfig != null) {
|
||||
// 更新现有配置
|
||||
existingConfig.setValue(value);
|
||||
existingConfig.setTime(System.currentTimeMillis());
|
||||
this.updateById(existingConfig);
|
||||
} else {
|
||||
// 创建新配置
|
||||
ViteConfig newConfig = new ViteConfig();
|
||||
newConfig.setName(name);
|
||||
newConfig.setValue(value);
|
||||
newConfig.setTime(System.currentTimeMillis());
|
||||
this.save(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.admin.mapper.ViteConfigMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
html.dark body {
|
||||
background-color: #1f2937;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
||||
@@ -9,8 +9,10 @@ import TunnelPage from "@/pages/tunnel";
|
||||
import NodePage from "@/pages/node";
|
||||
import UserPage from "@/pages/user";
|
||||
import LimitPage from "@/pages/limit";
|
||||
import ConfigPage from "@/pages/config";
|
||||
|
||||
import { isLoggedIn } from "@/utils/auth";
|
||||
import { siteConfig } from "@/config/site";
|
||||
|
||||
// 简化的路由保护组件 - 使用浏览器重定向避免React循环
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -34,6 +36,7 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
// 登录页面路由组件 - 已登录则重定向到dashboard
|
||||
const LoginRoute = () => {
|
||||
const authenticated = isLoggedIn();
|
||||
@@ -57,6 +60,30 @@ const LoginRoute = () => {
|
||||
};
|
||||
|
||||
function App() {
|
||||
// 立即设置页面标题(使用已从缓存读取的配置)
|
||||
useEffect(() => {
|
||||
document.title = siteConfig.name;
|
||||
|
||||
// 异步检查是否有配置更新
|
||||
const checkTitleUpdate = async () => {
|
||||
try {
|
||||
// 引入必要的函数
|
||||
const { getCachedConfig } = await import('@/config/site');
|
||||
const cachedAppName = await getCachedConfig('app_name');
|
||||
if (cachedAppName && cachedAppName !== document.title) {
|
||||
document.title = cachedAppName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('检查标题更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟检查,避免阻塞初始渲染
|
||||
const timer = setTimeout(checkTitleUpdate, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginRoute />} />
|
||||
@@ -116,6 +143,14 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/config"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ConfigPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,4 +70,10 @@ export const updateSpeedLimit = (data: any) => Network.post("/speed-limit/update
|
||||
export const deleteSpeedLimit = (id: number) => Network.post("/speed-limit/delete", { id });
|
||||
|
||||
// 修改密码接口
|
||||
export const updatePassword = (data: any) => Network.post("/user/updatePassword", data);
|
||||
export const updatePassword = (data: any) => Network.post("/user/updatePassword", data);
|
||||
|
||||
// 网站配置相关接口
|
||||
export const getConfigs = () => Network.post("/config/list");
|
||||
export const getConfigByName = (name: string) => Network.post("/config/get", { name });
|
||||
export const updateConfigs = (configMap: Record<string, string>) => Network.post("/config/update", configMap);
|
||||
export const updateConfig = (name: string, value: string) => Network.post("/config/update-single", { name, value });
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "@heroui/link";
|
||||
import {
|
||||
Navbar as HeroUINavbar,
|
||||
@@ -7,19 +8,62 @@ import {
|
||||
|
||||
import { ThemeSwitch } from "@/components/theme-switch";
|
||||
import { Logo } from "@/components/icons";
|
||||
import { siteConfig, getCachedConfig } from "@/config/site";
|
||||
|
||||
export const Navbar = () => {
|
||||
// 初始状态使用siteConfig中已经从缓存读取的值,避免闪烁
|
||||
const [appName, setAppName] = useState(siteConfig.name);
|
||||
|
||||
useEffect(() => {
|
||||
// 异步检查是否有更新的配置
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
const cachedAppName = await getCachedConfig('app_name');
|
||||
if (cachedAppName && cachedAppName !== appName) {
|
||||
setAppName(cachedAppName);
|
||||
// 同步更新siteConfig
|
||||
siteConfig.name = cachedAppName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('检查配置更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟执行,避免阻塞初始渲染
|
||||
const timer = setTimeout(checkForUpdates, 100);
|
||||
|
||||
// 监听配置更新事件
|
||||
const handleConfigUpdate = async () => {
|
||||
try {
|
||||
const cachedAppName = await getCachedConfig('app_name');
|
||||
if (cachedAppName) {
|
||||
setAppName(cachedAppName);
|
||||
siteConfig.name = cachedAppName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('更新配置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('configUpdated', handleConfigUpdate);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener('configUpdated', handleConfigUpdate);
|
||||
};
|
||||
}, [appName]);
|
||||
|
||||
return (
|
||||
<HeroUINavbar maxWidth="xl" position="sticky">
|
||||
<HeroUINavbar maxWidth="xl" position="sticky" height="60px" className="shrink-0">
|
||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||
<NavbarBrand className="gap-3 max-w-fit">
|
||||
<NavbarBrand className="gap-2 max-w-fit">
|
||||
<Link
|
||||
className="flex justify-start items-center gap-1"
|
||||
className="flex justify-start items-center gap-2 max-w-[200px] sm:max-w-none"
|
||||
color="foreground"
|
||||
href="/"
|
||||
>
|
||||
<Logo />
|
||||
<p className="font-bold text-inherit">哆啦A梦</p>
|
||||
<Logo size={24} />
|
||||
<p className="font-bold text-inherit truncate">{appName}</p>
|
||||
</Link>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
@@ -1,15 +1,153 @@
|
||||
import { getConfigByName, getConfigs } from '@/api';
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
|
||||
export const siteConfig = {
|
||||
name: "哆啦A梦",
|
||||
description: "哆啦A梦",
|
||||
navItems: [],
|
||||
navMenuItems: [],
|
||||
links: {
|
||||
github: "https://github.com/go-gost/gost",
|
||||
twitter: "",
|
||||
docs: "",
|
||||
discord: "",
|
||||
sponsor: "",
|
||||
},
|
||||
// 缓存相关常量
|
||||
const CACHE_PREFIX = 'vite_config_';
|
||||
|
||||
// 缓存策略:
|
||||
// - 长期有效,不设置过期时间
|
||||
// - 只在以下情况清除:1. 更新配置时 2. 退出登录时(safeLogout会清除localStorage)
|
||||
|
||||
// 立即从缓存获取配置(同步操作,避免闪烁)
|
||||
const getInitialConfig = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
name: "哆啦A梦",
|
||||
version: "1.0.9",
|
||||
};
|
||||
}
|
||||
|
||||
const cachedAppName = localStorage.getItem(CACHE_PREFIX + 'app_name');
|
||||
if (cachedAppName) {
|
||||
return {
|
||||
name: cachedAppName,
|
||||
version: "1.0.9",
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: "哆啦A梦",
|
||||
version: "1.0.9",
|
||||
};
|
||||
};
|
||||
|
||||
export const siteConfig = getInitialConfig();
|
||||
|
||||
// 缓存工具函数
|
||||
export const configCache = {
|
||||
// 获取缓存的配置
|
||||
get: (key: string): string | null => {
|
||||
const cacheKey = CACHE_PREFIX + key;
|
||||
return localStorage.getItem(cacheKey);
|
||||
},
|
||||
|
||||
// 设置缓存的配置
|
||||
set: (key: string, value: string): void => {
|
||||
const cacheKey = CACHE_PREFIX + key;
|
||||
localStorage.setItem(cacheKey, value);
|
||||
},
|
||||
|
||||
// 删除指定配置的缓存
|
||||
remove: (key: string): void => {
|
||||
const cacheKey = CACHE_PREFIX + key;
|
||||
localStorage.removeItem(cacheKey);
|
||||
},
|
||||
|
||||
// 清空所有配置缓存
|
||||
clear: (): void => {
|
||||
// 获取所有localStorage的key
|
||||
const keys = Object.keys(localStorage);
|
||||
keys.forEach(key => {
|
||||
if (key.startsWith(CACHE_PREFIX)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个配置(优先从缓存)
|
||||
export const getCachedConfig = async (key: string): Promise<string | null> => {
|
||||
const cachedValue = configCache.get(key);
|
||||
if (cachedValue !== null) {
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
const response = await getConfigByName(key);
|
||||
if (response.code === 0 && response.data?.value) {
|
||||
const value = response.data.value;
|
||||
configCache.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取所有配置(优先从缓存)
|
||||
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
||||
// 尝试从缓存获取所有配置
|
||||
const configKeys = ['app_name', 'site_title', 'site_description', 'contact_email', 'footer_text', 'logo_url'];
|
||||
const cachedConfigs: Record<string, string> = {};
|
||||
let hasCachedData = false;
|
||||
|
||||
configKeys.forEach(key => {
|
||||
const cachedValue = configCache.get(key);
|
||||
if (cachedValue !== null) {
|
||||
cachedConfigs[key] = cachedValue;
|
||||
hasCachedData = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 从API获取最新配置
|
||||
try {
|
||||
const response = await getConfigs();
|
||||
if (response.code === 0 && response.data) {
|
||||
const configs = response.data;
|
||||
// 将所有配置存入缓存
|
||||
Object.entries(configs).forEach(([key, value]) => {
|
||||
configCache.set(key, value as string);
|
||||
});
|
||||
return configs;
|
||||
}
|
||||
} catch (error) {
|
||||
// API失败时返回缓存的数据
|
||||
if (hasCachedData) {
|
||||
return cachedConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
// 动态更新网站配置
|
||||
export const updateSiteConfig = async () => {
|
||||
const appName = await getCachedConfig('app_name');
|
||||
if (appName && appName !== siteConfig.name) {
|
||||
siteConfig.name = appName;
|
||||
// 更新页面标题
|
||||
document.title = appName;
|
||||
}
|
||||
};
|
||||
|
||||
// 清除配置缓存的工具函数
|
||||
// 缓存清除时机:
|
||||
// 1. 配置更新时:调用此函数清除所有缓存
|
||||
// 2. 退出登录时:safeLogout()中的localStorage.clear()会自动清除
|
||||
export const clearConfigCache = (keys?: string[]) => {
|
||||
if (keys && keys.length > 0) {
|
||||
// 删除指定的配置缓存
|
||||
keys.forEach(key => configCache.remove(key));
|
||||
} else {
|
||||
// 清空所有配置缓存
|
||||
configCache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// 在页面加载时异步更新配置(如果有更新的话)
|
||||
if (typeof window !== 'undefined') {
|
||||
// 延迟执行,避免阻塞初始渲染
|
||||
setTimeout(() => {
|
||||
updateSiteConfig();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Logo } from '@/components/icons';
|
||||
import { ThemeSwitch } from '@/components/theme-switch';
|
||||
import { updatePassword } from '@/api';
|
||||
import { safeLogout } from '@/utils/logout';
|
||||
import { siteConfig } from '@/config/site';
|
||||
|
||||
interface MenuItem {
|
||||
path: string;
|
||||
@@ -105,6 +106,16 @@ export default function AdminLayout({
|
||||
</svg>
|
||||
),
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
label: '网站配置',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
),
|
||||
adminOnly: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -245,7 +256,7 @@ export default function AdminLayout({
|
||||
<aside className={`
|
||||
${isMobile ? 'fixed' : 'relative'}
|
||||
${isMobile && !mobileMenuVisible ? '-translate-x-full' : 'translate-x-0'}
|
||||
${isMobile ? 'w-64' : 'w-64'}
|
||||
${isMobile ? 'w-64' : 'w-72'}
|
||||
bg-white dark:bg-black
|
||||
shadow-lg
|
||||
border-r border-gray-200 dark:border-gray-600
|
||||
@@ -255,12 +266,12 @@ export default function AdminLayout({
|
||||
h-full
|
||||
`}>
|
||||
{/* Logo 区域 */}
|
||||
<div className="px-4 py-3 h-14 flex items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<Logo size={28} />
|
||||
<div>
|
||||
<h1 className="text-base font-bold text-foreground">哆啦A梦</h1>
|
||||
<p className="text-xs text-default-500">v1.0.8</p>
|
||||
<div className="px-3 py-3 h-14 flex items-center">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Logo size={24} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-sm font-bold text-foreground overflow-hidden whitespace-nowrap">{siteConfig.name}</h1>
|
||||
<p className="text-xs text-default-500">v{siteConfig.version}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,9 @@ export default function DefaultLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex flex-col h-screen">
|
||||
<div className="relative flex flex-col min-h-screen bg-white dark:bg-black">
|
||||
<Navbar />
|
||||
<main className="container mx-auto max-w-7xl px-6 flex-grow pt-16">
|
||||
<main className="container mx-auto max-w-7xl px-4 sm:px-6 flex-grow pt-4 sm:pt-16">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="w-full flex items-center justify-center py-3">
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from "@heroui/button";
|
||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||
import { Input } from "@heroui/input";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Divider } from "@heroui/divider";
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateConfigs } from '@/api';
|
||||
import { SettingsIcon } from '@/components/icons';
|
||||
import AdminLayout from "@/layouts/admin";
|
||||
import { isAdmin } from '@/utils/auth';
|
||||
import { getCachedConfigs, clearConfigCache, updateSiteConfig } from '@/config/site';
|
||||
|
||||
// 简单的保存图标组件
|
||||
const SaveIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
||||
<polyline points="17,21 17,13 7,13 7,21" />
|
||||
<polyline points="7,3 7,8 15,8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface ConfigItem {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 网站配置项定义
|
||||
const CONFIG_ITEMS: ConfigItem[] = [
|
||||
{
|
||||
key: 'app_name',
|
||||
label: '应用名称',
|
||||
placeholder: '请输入应用名称',
|
||||
description: '在浏览器标签页和导航栏显示的应用名称'
|
||||
}
|
||||
];
|
||||
|
||||
// 初始化时从缓存读取配置,避免闪烁
|
||||
const getInitialConfigs = (): Record<string, string> => {
|
||||
if (typeof window === 'undefined') return {};
|
||||
|
||||
const configKeys = ['app_name', 'site_title', 'site_description', 'contact_email', 'footer_text', 'logo_url'];
|
||||
const initialConfigs: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
configKeys.forEach(key => {
|
||||
const cachedValue = localStorage.getItem('vite_config_' + key);
|
||||
if (cachedValue) {
|
||||
initialConfigs[key] = cachedValue;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return initialConfigs;
|
||||
};
|
||||
|
||||
export default function ConfigPage() {
|
||||
const initialConfigs = getInitialConfigs();
|
||||
const [configs, setConfigs] = useState<Record<string, string>>(initialConfigs);
|
||||
const [loading, setLoading] = useState(Object.keys(initialConfigs).length === 0); // 如果有缓存数据,不显示loading
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [originalConfigs, setOriginalConfigs] = useState<Record<string, string>>(initialConfigs);
|
||||
|
||||
// 权限检查
|
||||
useEffect(() => {
|
||||
if (!isAdmin()) {
|
||||
toast.error('权限不足,只有管理员可以访问此页面');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载配置数据(优先从缓存)
|
||||
const loadConfigs = async (currentConfigs?: Record<string, string>) => {
|
||||
const configsToCompare = currentConfigs || configs;
|
||||
const hasInitialData = Object.keys(configsToCompare).length > 0;
|
||||
|
||||
// 如果已有缓存数据,不显示loading,静默更新
|
||||
if (!hasInitialData) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const configData = await getCachedConfigs();
|
||||
|
||||
// 只有在数据有变化时才更新
|
||||
const hasDataChanged = JSON.stringify(configData) !== JSON.stringify(configsToCompare);
|
||||
if (hasDataChanged) {
|
||||
setConfigs(configData);
|
||||
setOriginalConfigs({ ...configData });
|
||||
setHasChanges(false);
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
// 只有在没有缓存数据时才显示错误
|
||||
if (!hasInitialData) {
|
||||
toast.error('加载配置出错,请重试');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 延迟加载,避免阻塞初始渲染
|
||||
const timer = setTimeout(() => {
|
||||
loadConfigs(initialConfigs);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []); // 只在组件挂载时执行一次
|
||||
|
||||
// 处理配置项变更
|
||||
const handleConfigChange = (key: string, value: string) => {
|
||||
const newConfigs = { ...configs, [key]: value };
|
||||
setConfigs(newConfigs);
|
||||
|
||||
// 检查是否有变更
|
||||
const hasChangesNow = Object.keys(newConfigs).some(
|
||||
k => newConfigs[k] !== originalConfigs[k]
|
||||
) || Object.keys(originalConfigs).some(
|
||||
k => originalConfigs[k] !== newConfigs[k]
|
||||
);
|
||||
setHasChanges(hasChangesNow);
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await updateConfigs(configs);
|
||||
if (response.code === 0) {
|
||||
toast.success('配置保存成功');
|
||||
|
||||
// 清除所有配置缓存,强制下次重新获取
|
||||
clearConfigCache();
|
||||
|
||||
// 获取变更的配置项
|
||||
const changedKeys = Object.keys(configs).filter(
|
||||
key => configs[key] !== originalConfigs[key]
|
||||
);
|
||||
|
||||
setOriginalConfigs({ ...configs });
|
||||
setHasChanges(false);
|
||||
|
||||
// 如果应用名称发生变化,立即更新网站配置
|
||||
if (changedKeys.includes('app_name')) {
|
||||
await updateSiteConfig();
|
||||
}
|
||||
|
||||
// 触发配置更新事件,通知其他组件
|
||||
window.dispatchEvent(new CustomEvent('configUpdated', {
|
||||
detail: { changedKeys }
|
||||
}));
|
||||
} else {
|
||||
toast.error('保存配置失败: ' + response.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('保存配置出错,请重试');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const handleReset = () => {
|
||||
setConfigs({ ...originalConfigs });
|
||||
setHasChanges(false);
|
||||
toast.success('已重置为保存前的状态');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Spinner size="lg" label="加载配置中..." />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<SettingsIcon className="w-8 h-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">网站配置</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
管理网站的基本信息和显示设置
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-md">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">基本设置</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
配置网站的基本信息,这些设置会影响网站的显示效果
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
color="default"
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={saving}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="primary"
|
||||
startContent={<SaveIcon className="w-4 h-4" />}
|
||||
onClick={handleSave}
|
||||
isLoading={saving}
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<Divider />
|
||||
|
||||
<CardBody className="space-y-6 pt-6">
|
||||
{CONFIG_ITEMS.map((item, index) => (
|
||||
<div key={item.key} className="space-y-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{item.label}
|
||||
</label>
|
||||
{item.description && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
value={configs[item.key] || ''}
|
||||
onChange={(e) => 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 && (
|
||||
<Divider className="mt-4" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 操作提示 */}
|
||||
{hasChanges && (
|
||||
<Card className="mt-4 bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-800">
|
||||
<CardBody className="py-3">
|
||||
<div className="flex items-center gap-2 text-warning-700 dark:text-warning-300">
|
||||
<div className="w-2 h-2 bg-warning-500 rounded-full animate-pulse" />
|
||||
<span className="text-sm">
|
||||
检测到配置变更,请记得保存您的修改
|
||||
</span>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
@@ -108,8 +108,8 @@ export default function IndexPage() {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10 min-h-[calc(100vh-200px)]">
|
||||
<div className="w-full max-w-md">
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 min-h-[calc(100vh-120px)] sm:min-h-[calc(100vh-200px)]">
|
||||
<div className="w-full max-w-md px-4 sm:px-0">
|
||||
<Card className="w-full">
|
||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||
<h1 className={title({ size: "sm" })}>登陆</h1>
|
||||
|
||||
@@ -698,14 +698,24 @@ export default function UserPage() {
|
||||
label="流量限制(GB)"
|
||||
type="number"
|
||||
value={userForm.flow.toString()}
|
||||
onChange={(e) => setUserForm(prev => ({ ...prev, flow: Number(e.target.value) || 0 }))}
|
||||
onChange={(e) => {
|
||||
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
|
||||
setUserForm(prev => ({ ...prev, flow: value }));
|
||||
}}
|
||||
min="1"
|
||||
max="99999"
|
||||
isRequired
|
||||
/>
|
||||
<Input
|
||||
label="转发数量"
|
||||
type="number"
|
||||
value={userForm.num.toString()}
|
||||
onChange={(e) => setUserForm(prev => ({ ...prev, num: Number(e.target.value) || 0 }))}
|
||||
onChange={(e) => {
|
||||
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
|
||||
setUserForm(prev => ({ ...prev, num: value }));
|
||||
}}
|
||||
min="1"
|
||||
max="99999"
|
||||
isRequired
|
||||
/>
|
||||
<Select
|
||||
@@ -720,11 +730,11 @@ export default function UserPage() {
|
||||
<SelectItem key="0" textValue="不重置">
|
||||
不重置
|
||||
</SelectItem>
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
|
||||
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
|
||||
每月{day}号(0点重置)
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
|
||||
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
|
||||
每月{day}号(0点重置)
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
</Select>
|
||||
<DatePicker
|
||||
@@ -823,14 +833,24 @@ export default function UserPage() {
|
||||
label="流量限制(GB)"
|
||||
type="number"
|
||||
value={tunnelForm.flow.toString()}
|
||||
onChange={(e) => setTunnelForm(prev => ({ ...prev, flow: Number(e.target.value) || 0 }))}
|
||||
onChange={(e) => {
|
||||
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
|
||||
setTunnelForm(prev => ({ ...prev, flow: value }));
|
||||
}}
|
||||
min="1"
|
||||
max="99999"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="转发数量"
|
||||
type="number"
|
||||
value={tunnelForm.num.toString()}
|
||||
onChange={(e) => setTunnelForm(prev => ({ ...prev, num: Number(e.target.value) || 0 }))}
|
||||
onChange={(e) => {
|
||||
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
|
||||
setTunnelForm(prev => ({ ...prev, num: value }));
|
||||
}}
|
||||
min="1"
|
||||
max="99999"
|
||||
/>
|
||||
|
||||
<Select
|
||||
@@ -845,11 +865,11 @@ export default function UserPage() {
|
||||
<SelectItem key="0" textValue="不重置">
|
||||
不重置
|
||||
</SelectItem>
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
|
||||
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
|
||||
每月{day}号(0点重置)
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
|
||||
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
|
||||
每月{day}号(0点重置)
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
</Select>
|
||||
|
||||
@@ -986,14 +1006,24 @@ export default function UserPage() {
|
||||
label="流量限制(GB)"
|
||||
type="number"
|
||||
value={editTunnelForm.flow.toString()}
|
||||
onChange={(e) => setEditTunnelForm(prev => prev ? { ...prev, flow: Number(e.target.value) || 0 } : null)}
|
||||
onChange={(e) => {
|
||||
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
|
||||
setEditTunnelForm(prev => prev ? { ...prev, flow: value } : null);
|
||||
}}
|
||||
min="1"
|
||||
max="99999"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="转发数量"
|
||||
type="number"
|
||||
value={editTunnelForm.num.toString()}
|
||||
onChange={(e) => setEditTunnelForm(prev => prev ? { ...prev, num: Number(e.target.value) || 0 } : null)}
|
||||
onChange={(e) => {
|
||||
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
|
||||
setEditTunnelForm(prev => prev ? { ...prev, num: value } : null);
|
||||
}}
|
||||
min="1"
|
||||
max="99999"
|
||||
/>
|
||||
|
||||
<Select
|
||||
@@ -1026,11 +1056,11 @@ export default function UserPage() {
|
||||
<SelectItem key="0" textValue="不重置">
|
||||
不重置
|
||||
</SelectItem>
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
|
||||
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
|
||||
每月{day}号(0点重置)
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
|
||||
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
|
||||
每月{day}号(0点重置)
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
</Select>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user