Merge pull request #244 from kmvan/dev

Dev
This commit is contained in:
Km.Van
2026-07-14 11:32:44 +08:00
committed by GitHub
13 changed files with 1194 additions and 610 deletions
+1 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+16 -8
View File
@@ -1,13 +1,14 @@
[![X Prober preview](screenshots/preview.webp)](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 on Open Collective](https://opencollective.com/x-prober/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/x-prober/sponsors/badge.svg)](#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
+3 -3
View File
@@ -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";
}
+4 -4
View File
@@ -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.');
}
+3 -3
View 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
View File
@@ -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",
+6 -6
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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")}>
-13
View File
@@ -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
View File
@@ -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/"),