Merge branch 'main' into cloudflare-dev

This commit is contained in:
hamster1963 2024-09-30 23:48:18 +08:00
commit 74dbba7881
17 changed files with 244 additions and 75 deletions

View File

@ -1,6 +1,6 @@
NezhaBaseUrl=http://1.1.1.1:8008 NezhaBaseUrl=http://124.XX.XX.XX:8008
NezhaAuth=nezha-token NezhaAuth=your-nezha-api-token
ServerDisablePrefetch=false DefaultLocale=zh
NEXT_PUBLIC_NezhaFetchInterval=2000 NEXT_PUBLIC_NezhaFetchInterval=5000
NEXT_PUBLIC_ShowFlag=true NEXT_PUBLIC_ShowFlag=true
NEXT_PUBLIC_DisableCartoon=false NEXT_PUBLIC_DisableCartoon=true

View File

@ -5,8 +5,40 @@ on:
tags: tags:
- "v*" - "v*"
env:
REGISTRY_IMAGE: hamster1963/nezha-dash
ALIYUN_REGISTRY_IMAGE: registry.cn-guangzhou.aliyuncs.com/hamster-home/nezha-dash
jobs: jobs:
changelog:
name: Generate Changelog
runs-on: ubuntu-latest
outputs:
release_body: ${{ steps.git-cliff.outputs.content }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
id: git-cliff
with:
config: git-cliff-config/cliff.toml
args: -vv --latest --strip 'footer'
env:
OUTPUT: CHANGES.md
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
body: ${{ steps.git-cliff.outputs.content }}
token: ${{ secrets.GITHUB_TOKEN }}
env:
GITHUB_REPOSITORY: ${{ github.repository }}
build-and-push: build-and-push:
name: Build and push Docker image
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: Production environment: Production
steps: steps:
@ -37,8 +69,8 @@ jobs:
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: | images: |
hamster1963/nezha-dash ${{ env.REGISTRY_IMAGE }}
registry.cn-guangzhou.aliyuncs.com/hamster-home/nezha-dash ${{ env.ALIYUN_REGISTRY_IMAGE }}
tags: | tags: |
type=raw,value=latest type=raw,value=latest
type=ref,event=tag type=ref,event=tag

View File

@ -13,10 +13,10 @@
#### 环境变量 #### 环境变量
| 变量名 | 含义 | 示例 | | 变量名 | 含义 | 示例 |
| ------------------------------ | -------------------- | -------------------------------- | | ------------------------------ | -------------------------------- | -------------------------------- |
| NezhaBaseUrl | nezha 面板地址 | http://120.x.x.x:8008 | | NezhaBaseUrl | nezha 面板地址 | http://120.x.x.x:8008 |
| NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi | | NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi |
| ServerDisablePrefetch | 是否禁用预加载 | **默认**false | | DefaultLocale | 面板默认显示语言(代码参考下表) | **默认**en |
| NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**2000 | | NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**2000 |
| NEXT_PUBLIC_ShowFlag | 是否显示旗帜 | **默认**false | | NEXT_PUBLIC_ShowFlag | 是否显示旗帜 | **默认**false |
| NEXT_PUBLIC_DisableCartoon | 是否禁用卡通人物 | **默认**false | | NEXT_PUBLIC_DisableCartoon | 是否禁用卡通人物 | **默认**false |

View File

@ -6,18 +6,29 @@ import { nezhaFetcher } from "../../../../lib/utils";
import useSWR from "swr"; import useSWR from "swr";
import getEnv from "../../../../lib/env-entry"; import getEnv from "../../../../lib/env-entry";
export default function ServerListClient() { export default function ServerListClient() {
const { data } = useSWR<ServerApi>("/api/server", nezhaFetcher, { const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
}); });
if (error)
return (
<div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">
Please check your environment variables and review the server console
logs for more details.
</p>
</div>
);
if (!data) return null; if (!data) return null;
const sortedServers = data.result.sort((a, b) => {
if (a.display_index && b.display_index) { const { result } = data;
return b.display_index - a.display_index;
} const sortedServers = result.sort((a, b) => {
if (a.display_index) return -1; const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0);
if (b.display_index) return 1; if (displayIndexDiff !== 0) return displayIndexDiff;
return a.id - b.id; return a.id - b.id;
}); });
return ( return (
<section className="grid grid-cols-1 gap-2 md:grid-cols-2"> <section className="grid grid-cols-1 gap-2 md:grid-cols-2">
{sortedServers.map((serverInfo) => ( {sortedServers.map((serverInfo) => (

View File

@ -1,9 +1,15 @@
import { unstable_setRequestLocale } from "next-intl/server";
import ServerList from "../../../components/ServerList"; import ServerList from "../../../components/ServerList";
import ServerOverview from "../../../components/ServerOverview"; import ServerOverview from "../../../components/ServerOverview";
export const runtime = 'edge'; export const runtime = 'edge';
export default function Home() { export default function Home({
params: { locale },
}: {
params: { locale: string };
}) {
unstable_setRequestLocale(locale);
return ( return (
<div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6"> <div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6">
<ServerOverview /> <ServerOverview />

View File

@ -1,6 +1,8 @@
// @auto-i18n-check. Please do not delete the line. // @auto-i18n-check. Please do not delete the line.
import "@/styles/globals.css"; import "@/styles/globals.css";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import React from "react"; import React from "react";
import { NextIntlClientProvider, useMessages } from "next-intl"; import { NextIntlClientProvider, useMessages } from "next-intl";
import { PublicEnvScript } from "next-runtime-env"; import { PublicEnvScript } from "next-runtime-env";
@ -10,6 +12,7 @@ import { ThemeProvider } from "next-themes";
import { Viewport } from "next"; import { Viewport } from "next";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { locales } from "@/i18n-metadata"; import { locales } from "@/i18n-metadata";
import { unstable_setRequestLocale } from "next-intl/server";
const fontSans = FontSans({ const fontSans = FontSans({
subsets: ["latin"], subsets: ["latin"],
@ -34,11 +37,12 @@ export const viewport: Viewport = {
userScalable: false, userScalable: false,
}; };
export function generateStaticParams() { export const dynamic = "force-static";
export async function generateStaticParams() {
return locales.map((locale) => ({ locale })); return locales.map((locale) => ({ locale }));
} }
export default function LocaleLayout({ export default function LocaleLayout({
children, children,
params: { locale }, params: { locale },
@ -46,6 +50,8 @@ export default function LocaleLayout({
children: React.ReactNode; children: React.ReactNode;
params: { locale: string }; params: { locale: string };
}) { }) {
unstable_setRequestLocale(locale);
const messages = useMessages(); const messages = useMessages();
return ( return (
<html lang={locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>

View File

@ -8,17 +8,18 @@ export const dynamic = "force-dynamic";
export const runtime = 'edge'; export const runtime = 'edge';
export async function GET(_: Request) { interface NezhaDataResponse {
try { error?: string;
const response = await GetNezhaData(); data?: ServerApi;
return NextResponse.json(response, { status: 200 });
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "fetch nezha data failed" },
{ status: 400 },
);
} }
export async function GET(_: Request) {
const response = (await GetNezhaData()) as NezhaDataResponse;
if (response.error) {
console.log(response.error);
return NextResponse.json({ error: response.error }, { status: 400 });
}
return NextResponse.json(response, { status: 200 });
} }
async function GetNezhaData() { async function GetNezhaData() {

BIN
bun.lockb

Binary file not shown.

View File

@ -9,8 +9,9 @@ import {
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { cn, formatNezhaInfo } from "@/lib/utils"; import { cn, formatNezhaInfo } from "@/lib/utils";
import ServerCardPopover from "./ServerCardPopover"; import ServerCardPopover from "./ServerCardPopover";
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { env } from "next-runtime-env"; import { env } from "next-runtime-env";
import ServerFlag from "./ServerFlag";
export default function ServerCard({ export default function ServerCard({
serverInfo, serverInfo,
@ -32,15 +33,7 @@ export default function ServerCard({
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<section className={"flex items-center justify-start gap-2 lg:w-28"}> <section className={"flex items-center justify-start gap-2 lg:w-28"}>
{showFlag ? ( {showFlag ? <ServerFlag country_code={country_code} /> : null}
country_code ? (
<span className="text-[12px] text-muted-foreground">
{getUnicodeFlagIcon(country_code)}
</span>
) : (
<span className="text-[12px] text-muted-foreground">🏁</span>
)
) : null}
<p <p
className={cn( className={cn(
"break-all font-bold tracking-tight", "break-all font-bold tracking-tight",
@ -58,8 +51,6 @@ export default function ServerCard({
</Popover> </Popover>
<section className={"grid grid-cols-5 items-center gap-3"}> <section className={"grid grid-cols-5 items-center gap-3"}>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
{" "}
{/* 设置固定宽度 */}
<p className="text-xs text-muted-foreground">{t("CPU")}</p> <p className="text-xs text-muted-foreground">{t("CPU")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}% {cpu.toFixed(2)}%
@ -67,8 +58,6 @@ export default function ServerCard({
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
{" "}
{/* 设置固定宽度 */}
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-xs text-muted-foreground">{t("Mem")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}% {mem.toFixed(2)}%
@ -76,8 +65,6 @@ export default function ServerCard({
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
{" "}
{/* 设置固定宽度 */}
<p className="text-xs text-muted-foreground">{t("STG")}</p> <p className="text-xs text-muted-foreground">{t("STG")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}% {stg.toFixed(2)}%
@ -85,8 +72,6 @@ export default function ServerCard({
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
{" "}
{/* 设置固定宽度 */}
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-xs text-muted-foreground">{t("Upload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{up.toFixed(2)} {up.toFixed(2)}
@ -94,8 +79,6 @@ export default function ServerCard({
</div> </div>
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
{" "}
{/* 设置固定宽度 */}
<p className="text-xs text-muted-foreground">{t("Download")}</p> <p className="text-xs text-muted-foreground">{t("Download")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{down.toFixed(2)} {down.toFixed(2)}
@ -113,15 +96,7 @@ export default function ServerCard({
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<section className={"flex items-center justify-start gap-2 lg:w-28"}> <section className={"flex items-center justify-start gap-2 lg:w-28"}>
{showFlag ? ( {showFlag ? <ServerFlag country_code={country_code} /> : null}
country_code ? (
<span className="text-[12px] text-muted-foreground">
{getUnicodeFlagIcon(country_code)}
</span>
) : (
<span className="text-[12px] text-muted-foreground">🏁</span>
)
) : null}
<p <p
className={cn( className={cn(
"break-all font-bold tracking-tight", "break-all font-bold tracking-tight",

36
components/ServerFlag.tsx Normal file
View File

@ -0,0 +1,36 @@
import { useEffect, useState } from "react";
import getUnicodeFlagIcon from "country-flag-icons/unicode";
export default function ServerFlag({ country_code }: { country_code: string }) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
useEffect(() => {
const checkEmojiSupport = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
if (!ctx) return;
ctx.fillStyle = "#000";
ctx.textBaseline = "top";
ctx.font = "32px Arial";
ctx.fillText(emojiFlag, 0, 0);
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
setSupportsEmojiFlags(support);
};
checkEmojiSupport();
}, []);
if (!country_code) return null;
return (
<span className="text-[12px] text-muted-foreground">
{!supportsEmojiFlags ? (
<span className={`fi fi-${country_code}`}></span>
) : (
getUnicodeFlagIcon(country_code)
)}
</span>
);
}

View File

@ -1,6 +1,6 @@
NezhaBaseUrl=http://0.0.0.0:8008 NezhaBaseUrl=http://124.XX.XX.XX:8008
NezhaAuth=5hAY3QX6Nl9B3UOQgB26KdsdS1dsdUdM NezhaAuth=your-nezha-api-token
ServerDisablePrefetch=false DefaultLocale=zh
NEXT_PUBLIC_NezhaFetchInterval=5000 NEXT_PUBLIC_NezhaFetchInterval=5000
NEXT_PUBLIC_ShowFlag=true NEXT_PUBLIC_ShowFlag=true
NEXT_PUBLIC_DisableCartoon=true NEXT_PUBLIC_DisableCartoon=true

View File

@ -0,0 +1,83 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# changelog header
header = """
# Changelog\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/
body = """
{% if version %}\
## Release {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
* {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) by [@{{ commit.author.name }}](https://github.com/{{ commit.author.name }})\
{% for footer in commit.footers -%}
, {{ footer.token }}{{ footer.separator }}{{ footer.value }}\
{% endfor %}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/hamster1963/nezha-dash" }, # replace repository URL
]
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))" },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->⛰️ Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^update", group = "<!-- 9 -->🔼 Updates" },
{ message = "^delete", group = "<!-- 10 -->🔞 Delete" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-20]*"
# regex for skipping tags
skip_tags = ""
# regex for ignoring tags
ignore_tags = "v.*-beta.*"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

View File

@ -1,5 +1,7 @@
// @auto-i18n-check. Please do not delete the line. // @auto-i18n-check. Please do not delete the line.
import getEnv from "./lib/env-entry";
export const localeItems = [ export const localeItems = [
{ code: "en", name: "English" }, { code: "en", name: "English" },
{ code: "ja", name: "日本語" }, { code: "ja", name: "日本語" },
@ -21,4 +23,4 @@ export const localeItems = [
]; ];
export const locales = localeItems.map((item) => item.code); export const locales = localeItems.map((item) => item.code);
export const defaultLocale = "en"; export const defaultLocale = getEnv("DefaultLocale") || "en";

View File

@ -10,8 +10,8 @@ export async function GetNezhaData() {
var nezhaBaseUrl = getEnv("NezhaBaseUrl"); var nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.error("NezhaBaseUrl is not set"); console.log("NezhaBaseUrl is not set");
throw new Error("NezhaBaseUrl is not set"); return { error: "NezhaBaseUrl is not set" };
} }
// Remove trailing slash // Remove trailing slash
@ -27,7 +27,12 @@ export async function GetNezhaData() {
revalidate: 0, revalidate: 0,
}, },
}); });
const nezhaData = (await response.json()).result as NezhaAPI[]; const resData = await response.json();
const nezhaData = resData.result as NezhaAPI[];
if (!nezhaData) {
console.log(resData);
return { error: "NezhaData fetch failed" };
}
const data: ServerApi = { const data: ServerApi = {
live_servers: 0, live_servers: 0,
offline_servers: 0, offline_servers: 0,

View File

@ -1,14 +1,14 @@
// @auto-i18n-check. Please do not delete the line. // @auto-i18n-check. Please do not delete the line.
import createMiddleware from "next-intl/middleware"; import createMiddleware from "next-intl/middleware";
import { locales } from "./i18n-metadata"; import { defaultLocale, locales } from "./i18n-metadata";
export default createMiddleware({ export default createMiddleware({
// A list of all locales that are supported // A list of all locales that are supported
locales: locales, locales: locales,
// Used when no locale matches // Used when no locale matches
defaultLocale: "en", defaultLocale: defaultLocale,
// 'always': This is the default, The home page will also be redirected to the default language, such as www.abc.com to www.abc.com/en // 'always': This is the default, The home page will also be redirected to the default language, such as www.abc.com to www.abc.com/en
// 'as-needed': The default page is not redirected. For example, if you open www.abc.com, it is still www.abc.com // 'as-needed': The default page is not redirected. For example, if you open www.abc.com, it is still www.abc.com

View File

@ -1,3 +1,9 @@
import withBundleAnalyzer from "@next/bundle-analyzer";
const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
import withPWAInit from "@ducanh2912/next-pwa"; import withPWAInit from "@ducanh2912/next-pwa";
@ -16,5 +22,10 @@ const withPWA = withPWAInit({
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
reactStrictMode: true, reactStrictMode: true,
logging: {
fetches: {
fullUrl: true,
},
},
}; };
export default withPWA(withNextIntl(nextConfig)); export default bundleAnalyzer(withPWA(withNextIntl(nextConfig)));

View File

@ -27,6 +27,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"country-flag-icons": "^1.5.13", "country-flag-icons": "^1.5.13",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"flag-icons": "^7.2.3",
"lucide-react": "^0.414.0", "lucide-react": "^0.414.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"next": "^14.2.13", "next": "^14.2.13",
@ -47,8 +48,8 @@
"eslint-plugin-turbo": "^2.1.2", "eslint-plugin-turbo": "^2.1.2",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"@next/bundle-analyzer": "^14.2.13", "@next/bundle-analyzer": "^14.2.13",
"@types/node": "^22.7.2", "@types/node": "^22.7.4",
"@types/react": "^18.3.9", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.11.1", "eslint": "^9.11.1",