mirror of
https://github.com/kmvan/x-prober.git
synced 2026-08-12 19:31:34 +08:00
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"APP_VERSION": "10.0.0",
|
||||
"APP_VERSION": "10.0.1",
|
||||
"APP_NAME": "X Prober",
|
||||
"APP_URL": "https://github.com/kmvan/x-prober",
|
||||
"AUTHOR_URL": "https://inn-studio.com/prober",
|
||||
|
||||
+1068
-491
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,14 @@
|
||||
[](screenshots/preview.webp)
|
||||
|
||||
- [简体中文](README-zh_CN.md)
|
||||
- [繁体體中文(中國臺灣)](README-zh_TW.md)
|
||||
- [繁体體中文(中國香港)](README-zh_HK.md)
|
||||
- [繁體中文(中國臺灣)](README-zh_TW.md)
|
||||
- [繁體中文(中國香港)](README-zh_HK.md)
|
||||
- [日本語](README-jp.md)
|
||||
|
||||
# 😎 X Prober, Notch Prober
|
||||
|
||||
> A PHP environment probe program that not only displays server information intuitively.
|
||||
> A PHP environment probe program that not only displays server information
|
||||
> intuitively.
|
||||
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
@@ -23,7 +24,9 @@
|
||||
|
||||
## Download & Usage
|
||||
|
||||
- Click [INN Download Node](https://api.inn-studio.com/download?id=xprober) or [GitHub Node](https://github.com/kmvan/x-prober/raw/master/dist/prober.php) to download the probe file
|
||||
- Click [INN Download Node](https://api.inn-studio.com/download?id=xprober) or
|
||||
[GitHub Node](https://github.com/kmvan/x-prober/raw/master/dist/prober.php) to
|
||||
download the probe file
|
||||
- You'll get a single file. Rename it to `x.php` and upload to your server
|
||||
- Access via browser: `your-domain/x.php`
|
||||
|
||||
@@ -44,7 +47,8 @@
|
||||
2. Fetch your repository
|
||||
3. Install npm modules: `$ npx pnpm i`
|
||||
4. Install composer: `$ composer install && composer dumpautoload -o`
|
||||
5. Generate multilingual files: `$ npm run lang` to rebuild `./languages/lang.pot` template
|
||||
5. Generate multilingual files: `$ npm run lang` to rebuild
|
||||
`./languages/lang.pot` template
|
||||
6. Start PHP backend: `$ npx pnpm dev:php`
|
||||
7. Start Vite frontend: `$ npx pnpm dev`
|
||||
8. Access: `http://localhost:5173/`
|
||||
@@ -73,15 +77,19 @@
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/x-prober#backer)]
|
||||
Thank you to all our backers! 🙏
|
||||
[[Become a backer](https://opencollective.com/x-prober#backer)]
|
||||
|
||||
<a href="https://opencollective.com/x-prober#backers" target="_blank"><img src="https://opencollective.com/x-prober/backers.svg?width=890"></a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/x-prober#sponsor)]
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a
|
||||
link to your website.
|
||||
[[Become a sponsor](https://opencollective.com/x-prober#sponsor)]
|
||||
|
||||
- Thanks to [VPSPlayer.com](https://vpsplayer.com/aff.php?aff=50) - Jan 16, 2021 - ¥199
|
||||
- Thanks to [VPSPlayer.com](https://vpsplayer.com/aff.php?aff=50) - Jan 16,
|
||||
2021 - ¥199
|
||||
- Thanks to 1529\*\*\*576 - Apr 4, 2019 – ¥150
|
||||
- Thanks to [Vultr.com](https://www.vultr.com/?ref=7256513) - Mar 13, 2019 - $50
|
||||
|
||||
|
||||
@@ -125,11 +125,11 @@ final class Compiler
|
||||
*/
|
||||
private function getCodeViaFilePath(string $filePath): string
|
||||
{
|
||||
echo "Packing `{$filePath}`... ";
|
||||
// echo "Packing `{$filePath}`... ";
|
||||
|
||||
$code = file_get_contents($filePath);
|
||||
if (false === $code) {
|
||||
echo "FAILED\n";
|
||||
echo "[FAILED] {$filePath}\n";
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -140,7 +140,7 @@ final class Compiler
|
||||
// 💡 Opt: Safely remove leading <?php tag via regex to avoid bugs caused by magic number 5
|
||||
$code = preg_replace('/^<\\?php\\s*/i', '', $code);
|
||||
|
||||
echo "OK\n";
|
||||
// echo "OK\n";
|
||||
|
||||
return $code . "\n";
|
||||
}
|
||||
|
||||
@@ -34,12 +34,12 @@ final class ScriptGeneration
|
||||
public function generate(): void
|
||||
{
|
||||
// 验证源脚本文件是否存在 / Validate if the source script file exists
|
||||
if ( ! is_file($this->scriptFilePath)) {
|
||||
if ( !is_file($this->scriptFilePath)) {
|
||||
throw new InvalidArgumentException("[ScriptGeneration] Source file not found: {$this->scriptFilePath}");
|
||||
}
|
||||
|
||||
// 验证目标模板文件是否存在 / Validate if the destination file exists
|
||||
if ( ! is_file($this->distFilePath)) {
|
||||
if ( !is_file($this->distFilePath)) {
|
||||
throw new InvalidArgumentException("[ScriptGeneration] Destination file not found: {$this->distFilePath}");
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ final class ScriptGeneration
|
||||
|
||||
$this->setScript($scriptContent);
|
||||
|
||||
echo "[ScriptGeneration] Script content written successfully.\n";
|
||||
echo "[ScriptGeneration] Script content has been written successfully.\n";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,7 @@ final class ScriptGeneration
|
||||
|
||||
// 检查是否存在占位符(可选优化:防止无效替换)
|
||||
// Check if the placeholder exists (Optional optimization: prevent redundant writes)
|
||||
if ( ! str_contains($distContent, '{{X_SCRIPT}}')) {
|
||||
if ( !str_contains($distContent, '{{X_SCRIPT}}')) {
|
||||
throw new RuntimeException('[ScriptGeneration] Placeholder {{X_SCRIPT}} not found in destination file.');
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ final class StyleGeneration
|
||||
{
|
||||
// 校验源样式文件是否存在
|
||||
// Validate if the source style file exists.
|
||||
if ( ! is_file($this->styleFilePath)) {
|
||||
if ( !is_file($this->styleFilePath)) {
|
||||
throw new RuntimeException("[StyleGeneration] File not found: {$this->styleFilePath}");
|
||||
}
|
||||
|
||||
// 校验目标文件是否存在
|
||||
// Validate if the target dist file exists.
|
||||
if ( ! is_file($this->distFilePath)) {
|
||||
if ( !is_file($this->distFilePath)) {
|
||||
throw new RuntimeException("[StyleGeneration] File not found: {$this->distFilePath}");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ final class StyleGeneration
|
||||
// 执行替换并写入文件
|
||||
// Perform replacement and write to file.
|
||||
if ($this->setStyle($styleContent)) {
|
||||
echo "[StyleGeneration] Script content wrote successfully.\n";
|
||||
echo "[StyleGeneration] Script content has been written successfully.\n";
|
||||
} else {
|
||||
throw new RuntimeException('[StyleGeneration] Error: Cannot write script content to dist.');
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"APP_VERSION": "10.0.0",
|
||||
"APP_VERSION": "10.0.1",
|
||||
"APP_NAME": "X Prober",
|
||||
"APP_URL": "https://github.com/kmvan/x-prober",
|
||||
"AUTHOR_URL": "https://inn-studio.com/prober",
|
||||
|
||||
Vendored
+6
-6
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ namespace InnStudio\Prober\Components\Config;
|
||||
class ConfigApi
|
||||
{
|
||||
public static $config = array (
|
||||
'APP_VERSION' => '10.0.0',
|
||||
'APP_VERSION' => '10.0.1',
|
||||
'APP_NAME' => 'X Prober',
|
||||
'APP_URL' => 'https://github.com/kmvan/x-prober',
|
||||
'AUTHOR_URL' => 'https://inn-studio.com/prober',
|
||||
|
||||
@@ -25,14 +25,8 @@
|
||||
grid-template-areas:
|
||||
"x-meter-name x-meter-percent"
|
||||
"x-meter-name x-meter-overview"
|
||||
"x-meter-core x-meter-core ";
|
||||
"x-meter-core x-meter-core";
|
||||
gap: var(--x-gutter-sm);
|
||||
// border-radius: var(--x-radius);
|
||||
// background: var(--x-meter-bar-bg);
|
||||
// padding: var(--x-gutter-sm);
|
||||
// &:hover {
|
||||
// background: var(--x-meter-bar-bg-hover);
|
||||
// }
|
||||
}
|
||||
.percent {
|
||||
grid-area: x-meter-percent;
|
||||
@@ -42,7 +36,6 @@
|
||||
border-radius: var(--x-radius);
|
||||
background-color: var(--x-meter-percent-bg);
|
||||
padding: 0 var(--x-gutter);
|
||||
// font-weight: bold;
|
||||
}
|
||||
.name {
|
||||
all: unset;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, type RefObject, useCallback, useRef } from "react";
|
||||
import { type FC, type RefObject, useCallback, useEffect, useRef } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { Button } from "@/Components/Button/components/index.tsx";
|
||||
import { ButtonStatus } from "@/Components/Button/components/types.ts";
|
||||
@@ -18,11 +18,27 @@ const Results: FC = () => {
|
||||
useShallow((s) => s.serverToBrowserPingItems),
|
||||
);
|
||||
const count = serverToBrowserPingItems.length;
|
||||
|
||||
// 安全计算,不使用 ... 操作符,防止大数据量时爆栈
|
||||
let min = count ? serverToBrowserPingItems[0].time : 0;
|
||||
let max = count ? serverToBrowserPingItems[0].time : 0;
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = serverToBrowserPingItems[i].time;
|
||||
sum += t;
|
||||
if (t < min) {
|
||||
min = t;
|
||||
}
|
||||
if (t > max) {
|
||||
max = t;
|
||||
}
|
||||
}
|
||||
|
||||
const avg = count ? (sum / count).toFixed(2) : "0.00";
|
||||
const items = serverToBrowserPingItems.map(({ time }) => time);
|
||||
const avg = count ? (items.reduce((a, b) => a + b, 0) / count).toFixed(2) : 0;
|
||||
const max = count ? Math.max(...items) : 0;
|
||||
const min = count ? Math.min(...items) : 0;
|
||||
const mdev = calculateMdev(items).toFixed(2);
|
||||
|
||||
return (
|
||||
<div className={styles.result}>
|
||||
{template(
|
||||
@@ -34,6 +50,7 @@ const Results: FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResultContainer: FC<{
|
||||
refContainer: RefObject<HTMLUListElement | null>;
|
||||
}> = ({ refContainer }) => {
|
||||
@@ -41,6 +58,14 @@ const ResultContainer: FC<{
|
||||
useShallow((s) => s.serverToBrowserPingItems),
|
||||
);
|
||||
const hasServerToBrowserPingItems = Boolean(serverToBrowserPingItems.length);
|
||||
|
||||
// 优化点:每次列表长度改变时,自动将容器滚动到最下方
|
||||
useEffect(() => {
|
||||
if (refContainer.current) {
|
||||
refContainer.current.scrollTop = refContainer.current.scrollHeight;
|
||||
}
|
||||
}, [serverToBrowserPingItems.length, refContainer]);
|
||||
|
||||
return (
|
||||
<ModuleGroup label={gettext("Results")}>
|
||||
<div className={styles.resultContainer}>
|
||||
@@ -57,6 +82,7 @@ const ResultContainer: FC<{
|
||||
</ModuleGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const PingServerToBrowser: FC = () => {
|
||||
const {
|
||||
isPing,
|
||||
@@ -71,52 +97,77 @@ export const PingServerToBrowser: FC = () => {
|
||||
setIsPingServerToBrowser: s.setIsPingServerToBrowser,
|
||||
})),
|
||||
);
|
||||
|
||||
const refItemContainer = useRef<HTMLUListElement | null>(null);
|
||||
const refPingTimer = useRef<number>(0);
|
||||
const refPingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const ServerTimeMultiplier = 1000;
|
||||
const TimeoutTimerMs = 1000;
|
||||
const ScrollTimerMs = 100;
|
||||
|
||||
// 核心单次 Ping 请求
|
||||
const ping = useCallback(async (): Promise<void> => {
|
||||
const start = Date.now();
|
||||
const { data, status } = await serverFetch<ServerToBrowserPingItemProps>(
|
||||
"ping",
|
||||
);
|
||||
if (data?.time && status === OK) {
|
||||
const { id, time } = data;
|
||||
const end = Date.now();
|
||||
const serverTime = time * ServerTimeMultiplier;
|
||||
addServerToBrowserPingItem({
|
||||
id,
|
||||
time: Math.floor(end - start - serverTime),
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!refItemContainer.current) {
|
||||
return;
|
||||
}
|
||||
const st = refItemContainer.current.scrollTop;
|
||||
const sh = refItemContainer.current.scrollHeight;
|
||||
if (st < sh) {
|
||||
refItemContainer.current.scrollTop = sh;
|
||||
}
|
||||
}, ScrollTimerMs);
|
||||
try {
|
||||
const { data, status } = await serverFetch<ServerToBrowserPingItemProps>(
|
||||
"ping",
|
||||
);
|
||||
if (data?.time && status === OK) {
|
||||
const { id, time } = data;
|
||||
const end = Date.now();
|
||||
const serverTime = time * ServerTimeMultiplier;
|
||||
|
||||
// 防御本地与服务端时钟不同步导致的负数问题
|
||||
const calculatedTime = Math.max(
|
||||
0,
|
||||
Math.floor(end - start - serverTime),
|
||||
);
|
||||
|
||||
addServerToBrowserPingItem({
|
||||
id,
|
||||
time: calculatedTime,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ping failed:", error);
|
||||
}
|
||||
}, [addServerToBrowserPingItem]);
|
||||
const pingLoop = useCallback(async (): Promise<void> => {
|
||||
await ping();
|
||||
refPingTimer.current = window.setTimeout(async () => {
|
||||
await pingLoop();
|
||||
}, TimeoutTimerMs);
|
||||
}, [ping]);
|
||||
const handlePing = useCallback(async () => {
|
||||
|
||||
// 利用 useEffect 集中管理循环逻辑与定时器的销毁
|
||||
useEffect(() => {
|
||||
if (!isPingServerToBrowser) {
|
||||
if (refPingTimer.current) {
|
||||
clearTimeout(refPingTimer.current);
|
||||
refPingTimer.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const runLoop = async () => {
|
||||
await ping();
|
||||
// 只有在依然处于激活状态时才继续下一个定时
|
||||
if (isPingServerToBrowser) {
|
||||
refPingTimer.current = setTimeout(runLoop, TimeoutTimerMs);
|
||||
}
|
||||
};
|
||||
|
||||
runLoop();
|
||||
|
||||
// 组件卸载或状态变为不 Ping 时,严格执行垃圾清理
|
||||
return () => {
|
||||
if (refPingTimer.current) {
|
||||
clearTimeout(refPingTimer.current);
|
||||
}
|
||||
};
|
||||
}, [isPingServerToBrowser, ping]);
|
||||
|
||||
const handlePing = useCallback(() => {
|
||||
if (isPing || isPingServerToBrowser) {
|
||||
setIsPingServerToBrowser(false);
|
||||
clearTimeout(refPingTimer.current);
|
||||
return;
|
||||
}
|
||||
setIsPingServerToBrowser(true);
|
||||
await pingLoop();
|
||||
}, [isPing, isPingServerToBrowser, pingLoop, setIsPingServerToBrowser]);
|
||||
// const count = serverToBrowserPingItems.length;
|
||||
}, [isPing, isPingServerToBrowser, setIsPingServerToBrowser]);
|
||||
|
||||
return (
|
||||
<UiSingleColContainer>
|
||||
<ModuleGroup label={gettext("Server ⇄ Browser")}>
|
||||
|
||||
@@ -11,19 +11,6 @@ export default defineConfig(({ mode }) => {
|
||||
manifest: true,
|
||||
outDir: "../dist",
|
||||
target: "esnext",
|
||||
// rollupOptions: {
|
||||
// external: ['react', 'react-dom'],
|
||||
// output: {
|
||||
// globals: {
|
||||
// react: 'React',
|
||||
// 'react-dom': 'ReactDOM',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
// rollupOptions: {
|
||||
// input: new URL('./src/main.tsx', import.meta.url).pathname,
|
||||
// },
|
||||
},
|
||||
css: {
|
||||
modules: {
|
||||
|
||||
+1
-33
@@ -6,61 +6,29 @@ import { defineConfig } from "vite";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const tmpDir = resolve(__dirname, ".tmp");
|
||||
|
||||
if (!existsSync(tmpDir)) {
|
||||
mkdirSync(tmpDir);
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
cssFileName: "app", // 如果想让生成的 css 叫 app.css,保留此项
|
||||
cssFileName: "app",
|
||||
entry: resolve(__dirname, "src/main.tsx"),
|
||||
fileName: () => "app.js",
|
||||
formats: ["umd"],
|
||||
name: "app",
|
||||
},
|
||||
outDir: tmpDir,
|
||||
sourcemap: "hidden",
|
||||
target: "es2024",
|
||||
// 如果之后需要强制把所有 asset/css 改名,可以取消下方注释
|
||||
// rollupOptions: {
|
||||
// output: {
|
||||
// assetFileNames: (assetInfo) => {
|
||||
// if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
||||
// return 'app.css';
|
||||
// }
|
||||
// return '[name].[ext]';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
|
||||
// 1. 核心 CSS 优化(适配 Vite 8.1 + 解决 lightningcss 的 :global 报错)
|
||||
css: {
|
||||
lightningcss: {
|
||||
// 👈 开启这个,lightningcss 就能完美识别并处理 :global 语法了
|
||||
cssModules: true,
|
||||
},
|
||||
minify: true, // Vite 8.x 推荐直接在这里开启 CSS 压缩
|
||||
transformer: "lightningcss",
|
||||
},
|
||||
|
||||
define: {
|
||||
__DEV__: "false",
|
||||
DEBUG: "false",
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
"process.env.WEBPACK_ENV": JSON.stringify("production"),
|
||||
},
|
||||
|
||||
esbuild: {
|
||||
legalComments: "none",
|
||||
},
|
||||
mode: "production",
|
||||
|
||||
plugins: [react()],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src/"),
|
||||
|
||||
Reference in New Issue
Block a user