From d46035708e89f75a09e903e3f1ba4ca2365355f4 Mon Sep 17 00:00:00 2001 From: Muse Date: Thu, 22 Oct 2026 13:30:57 +0000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E7=AC=AC22=E6=9D=A1=20=E6=94=B9?= =?UTF-8?q?=E5=90=8D=20L-UI=20+=20=E7=AC=AC14/21=E6=9D=A1=20+=20=E7=AC=AC1?= =?UTF-8?q?8/19/20=E6=9D=A1=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第22条 L-UI:登录页品牌名、侧栏 brand-text(收起态 L)、抽屉品牌、 usePageTitle 默认标题(HTML 标题)、13 个语言文件里用户可见串中的 3X-UI 字样(更新说明×2、TG 版本行)一并改为 L-UI;仓库/二进制/ 服务名/路径/内部包名不动 - 第18条:formatPanelVersion 对 V/A 系显示版本原样返回(新增单测); PanelUpdateModal 与首页动作条改用 display* 字段判更新与显示 - 第14条:首页运行时间卡片删除 Xray 半边与分隔线,只留 OS - 第21条:节点表列宽 操作190→150/已启用80→64/客户端180→150/ 延迟100→84/心跳120→96、地址列 330;agent 行 IPv4·IPv6 单行 (浅点分隔、沿用可见性 class),传统行不变 - 第20条:agent 行云朵按钮与移动端菜单改用「更新 Agent」文案; NodesPage 对 agent 行用无渠道选择的简单确认(传统行不变); schemas 增 agentUpdateState/Error/Target - 第19条:节点编辑页(agent)拉 /panel/api/agent/install/:id 展示 完整安装命令(master 可编辑、含 token 时完整命令+复制按钮); 无 token 时展示无 token 命令+agent.json 路径提示,不报错不空白 - i18n 新增 4 键(updateAgent/updateAgentConfirmContent/ agentInstallCommand/agentTokenNotStored)补齐全部 13 个语言文件 (zh-CN 中文、其余英文),tsc 干净、panel-version 测试全过、 i18n 失败仍为既有 forwarding 键集问题(第10条)零新增 --- frontend/src/hooks/usePageTitle.ts | 2 +- frontend/src/layouts/AppSidebar.tsx | 4 +- frontend/src/lib/panel-version.ts | 3 + frontend/src/pages/index/IndexPage.tsx | 13 ++- frontend/src/pages/index/PanelUpdateModal.tsx | 27 ++++-- frontend/src/pages/index/SystemStrip.tsx | 17 ++-- frontend/src/pages/login/LoginPage.tsx | 2 +- frontend/src/pages/nodes/NodeFormModal.tsx | 85 ++++++++++++++++++- frontend/src/pages/nodes/NodeList.tsx | 49 +++++++---- frontend/src/pages/nodes/NodesPage.tsx | 12 +++ frontend/src/schemas/node.ts | 4 + frontend/src/test/panel-version.test.ts | 10 ++- internal/web/translation/ar-EG.json | 10 ++- internal/web/translation/en-US.json | 10 ++- internal/web/translation/es-ES.json | 8 +- internal/web/translation/fa-IR.json | 8 +- internal/web/translation/id-ID.json | 10 ++- internal/web/translation/ja-JP.json | 8 +- internal/web/translation/pt-BR.json | 10 ++- internal/web/translation/ru-RU.json | 8 +- internal/web/translation/tr-TR.json | 10 ++- internal/web/translation/uk-UA.json | 10 ++- internal/web/translation/vi-VN.json | 8 +- internal/web/translation/zh-CN.json | 8 +- internal/web/translation/zh-TW.json | 8 +- 25 files changed, 270 insertions(+), 74 deletions(-) diff --git a/frontend/src/hooks/usePageTitle.ts b/frontend/src/hooks/usePageTitle.ts index 48011a0..b14e7c0 100644 --- a/frontend/src/hooks/usePageTitle.ts +++ b/frontend/src/hooks/usePageTitle.ts @@ -20,7 +20,7 @@ export function usePageTitle() { useEffect(() => { const key = TITLE_KEYS[pathname]; - const title = key ? t(key) : '3X-UI'; + const title = key ? t(key) : 'L-UI'; const host = window.location.hostname; document.title = host ? `${host} - ${title}` : title; }, [pathname, t]); diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index 0861a4a..5d57094 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -356,7 +356,7 @@ export default function AppSidebar() { >
- {railCollapsed ? '3X' : '3X-UI'} + {railCollapsed ? 'L' : 'L-UI'}
{!railCollapsed && (
@@ -420,7 +420,7 @@ export default function AppSidebar() { >
- 3X-UI + L-UI
diff --git a/frontend/src/lib/panel-version.ts b/frontend/src/lib/panel-version.ts index 598a1e9..cd9faf5 100644 --- a/frontend/src/lib/panel-version.ts +++ b/frontend/src/lib/panel-version.ts @@ -22,6 +22,9 @@ function parseVersionParts(version: string): [number, number, number] | null { export function formatPanelVersion(version: string | undefined | null): string { const v = (version || '').trim(); if (!v) return ''; + // This fork's display versions (master "V1.0.0", agent "A1.0.0") are + // shown exactly as written — never re-prefixed or re-cased. + if (/^[VA]\d+\.\d+\.\d+$/.test(v)) return v; const normalized = v.replace(/^v/i, ''); return /^\d/.test(normalized) ? `v${normalized}` : v; } diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx index 4f7c58d..f6d9e9c 100644 --- a/frontend/src/pages/index/IndexPage.tsx +++ b/frontend/src/pages/index/IndexPage.tsx @@ -172,8 +172,17 @@ export default function IndexPage() { setBackupOpen(true)} onOpenPanelUpdate={() => setPanelUpdateOpen(true)} diff --git a/frontend/src/pages/index/PanelUpdateModal.tsx b/frontend/src/pages/index/PanelUpdateModal.tsx index c785cc9..27f23dd 100644 --- a/frontend/src/pages/index/PanelUpdateModal.tsx +++ b/frontend/src/pages/index/PanelUpdateModal.tsx @@ -17,6 +17,11 @@ export interface PanelUpdateInfo { currentCommit?: string; latestCommit?: string; updateAvailable: boolean; + // V-series display versions (items 18/20); the stable channel's update + // decision and labels use these when present. + displayCurrentVersion?: string; + displayLatestVersion?: string; + displayUpdateAvailable?: boolean; } interface BusyEvent { @@ -47,6 +52,13 @@ export default function PanelUpdateModal({ const isDev = info.channel === 'dev'; + // On the stable channel the V-series display comparison decides (item + // 20); the dev channel keeps its commit-based decision. + const masterUpdateAvailable = isDev + ? info.updateAvailable + : (info.displayUpdateAvailable ?? info.updateAvailable); + const displayLatest = info.displayLatestVersion || info.latestVersion; + async function pollUpdateStatus(expectedRunId: string): Promise { await PromiseUtil.sleep(5000); const deadline = Date.now() + 90_000; @@ -85,13 +97,14 @@ export default function PanelUpdateModal({ title: t('pages.index.panelUpdateDialog'), content: t('pages.index.panelUpdateDialogDesc').replace( '#version#', - info.latestVersion || '', + isDev ? info.latestVersion || '' : displayLatest || '', ), okText: t('confirm'), cancelText: t('cancel'), onOk: async () => { const baseTip = t('pages.index.dontRefresh'); - const tip = info.latestVersion ? `${baseTip} (${info.latestVersion})` : baseTip; + const tipVersion = isDev ? info.latestVersion : displayLatest; + const tip = tipVersion ? `${baseTip} (${tipVersion})` : baseTip; onClose(); onBusy({ busy: true, tip }); const result = await HttpUtil.post<{ runId: string }>('/panel/api/server/updatePanel'); @@ -128,7 +141,7 @@ export default function PanelUpdateModal({ <> {contextHolder} - {info.updateAvailable && ( + {masterUpdateAvailable && ( )}
- {info.updateAvailable ? ( + {masterUpdateAvailable ? (
{isDev ? t('pages.index.latestCommit') : t('pages.index.latestPanelVersion')} - {(isDev ? info.latestCommit : info.latestVersion) || '-'} + + {(isDev ? info.latestCommit : formatPanelVersion(displayLatest)) || '-'} +
) : (
@@ -184,7 +199,7 @@ export default function PanelUpdateModal({
-
-
-
Xray
-
- {TimeFormatter.formatSecond(status.appStats.uptime)} -
-
- -
-
OS
-
{TimeFormatter.formatSecond(status.uptime)}
-
+ {/* The lite master runs no local xray core, so the uptime card + shows the OS uptime only (the Xray half always read 0s). */} +
+
OS
+
{TimeFormatter.formatSecond(status.uptime)}
diff --git a/frontend/src/pages/login/LoginPage.tsx b/frontend/src/pages/login/LoginPage.tsx index 59f8360..4183da3 100644 --- a/frontend/src/pages/login/LoginPage.tsx +++ b/frontend/src/pages/login/LoginPage.tsx @@ -179,7 +179,7 @@ export default function LoginPage() { ) : (
- 3X-UI + L-UI

diff --git a/frontend/src/pages/nodes/NodeFormModal.tsx b/frontend/src/pages/nodes/NodeFormModal.tsx index 5aefab0..ab583a8 100644 --- a/frontend/src/pages/nodes/NodeFormModal.tsx +++ b/frontend/src/pages/nodes/NodeFormModal.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, @@ -16,7 +16,7 @@ import { import { FormProvider, useForm, useWatch } from 'react-hook-form'; import type { NodeRecord } from '@/api/queries/useNodesQuery'; import type { RemoteInboundOption } from '@/api/queries/useNodeMutations'; -import type { Msg } from '@/utils'; +import { HttpUtil, type Msg } from '@/utils'; import { NodeFormSchema, type NodeFormValues, type ProbeResult } from '@/schemas/node'; import { FormField, rhfZodValidate } from '@/components/form/rhf'; import { useOutboundTagGroups } from '@/api/queries/useOutboundTags'; @@ -81,6 +81,17 @@ export default function NodeFormModal({ const [agentToken, setAgentToken] = useState(null); const [agentNodeName, setAgentNodeName] = useState(''); const [agentMasterUrl, setAgentMasterUrl] = useState(''); + // Edit mode (item 19): the stored install material for an existing agent + // node, fetched from the master; null until loaded / for non-agent nodes. + // Keyed by node id so a stale result from a previously edited node is + // never shown (visibility is derived during render below). + const [editInstall, setEditInstall] = useState<{ + nodeId: number; + name: string; + token: string; + hasToken: boolean; + } | null>(null); + const [editMasterUrl, setEditMasterUrl] = useState(''); const scheme = useWatch({ control: methods.control, name: 'scheme' }) ?? 'https'; const tlsVerifyMode = useWatch({ control: methods.control, name: 'tlsVerifyMode' }) ?? 'verify'; const inboundSyncMode = useWatch({ control: methods.control, name: 'inboundSyncMode' }) ?? 'all'; @@ -136,6 +147,39 @@ export default function NodeFormModal({ setTestResult(null); } + // Load the stored install material when editing an agent node so the + // full install command can be re-displayed (item 19). Nodes created + // before tokens were stored answer hasToken=false until the agent's + // next authenticated request backfills the token. + const editNodeId = mode === 'edit' && isAgent ? (node?.id ?? 0) : 0; + useEffect(() => { + if (!open || !editNodeId) return; + let cancelled = false; + HttpUtil.get<{ name: string; token: string; hasToken: boolean }>( + `/panel/api/agent/install/${editNodeId}`, + ).then((msg) => { + if (!cancelled && msg?.success && msg.obj) { + setEditInstall({ nodeId: editNodeId, ...msg.obj }); + setEditMasterUrl(currentPanelMasterUrl()); + } + }); + return () => { + cancelled = true; + }; + }, [open, editNodeId]); + + const shownInstall = editInstall && editInstall.nodeId === editNodeId ? editInstall : null; + + const editInstallCmd = useMemo(() => { + if (!shownInstall || !editMasterUrl) return ''; + const base = + `bash <(curl -fL https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/install-agent.sh)` + + ` --master ${editMasterUrl}`; + return shownInstall.hasToken + ? `${base} --token ${shownInstall.token} --name ${shownInstall.name}` + : `${base} --name ${shownInstall.name}`; + }, [shownInstall, editMasterUrl]); + const title = useMemo( () => (mode === 'edit' ? t('pages.nodes.editNode') : t('pages.nodes.addNode')), [mode, t], @@ -391,6 +435,43 @@ export default function NodeFormModal({ )} + {mode === 'edit' && isAgent && shownInstall && ( +
+
{t('pages.nodes.agentInstallCommand')}
+

{t('pages.nodes.agentMasterUrlHint')}

+ setEditMasterUrl(e.target.value.trim())} + style={{ marginBottom: 12, fontFamily: 'monospace' }} + /> + {!shownInstall.hasToken && ( + + )} + + +
+ )} + {!isAgent && ( record.transitive ? ( {isUpdateEligible(record) && ( - +