fix: synchronize runtime locales and harden online updates

Translate UI, scheduler, validation, preload and maintenance messages dynamically, and propagate locale changes between workers. Harden update checks, downloads and terminal states, verify Debian pnpm, and ignore generated test artifacts.
This commit is contained in:
whyour
2026-10-17 07:07:09 +08:00
parent 8305825ffd
commit ef348b445b
72 changed files with 1733 additions and 553 deletions
+16 -5
View File
@@ -48,7 +48,8 @@ export default function () {
const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const [siteTitle, setSiteTitle] = useState(
() => localStorage.getItem('qinglong_panel_title')?.trim() || intl.get('青龙'),
() =>
localStorage.getItem('qinglong_panel_title')?.trim() || intl.get('青龙'),
);
const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
@@ -221,15 +222,25 @@ export default function () {
useEffect(() => {
window.onload = () => {
const timing = performance.timing;
console.log(`白屏时间: ${timing.responseStart - timing.navigationStart}`);
console.log(
`请求完毕至DOM加载: ${timing.domInteractive - timing.responseEnd}`,
intl.get('白屏时间', {
duration: timing.responseStart - timing.navigationStart,
}),
);
console.log(
`解释dom树耗时: ${timing.domComplete - timing.domInteractive}`,
intl.get('请求完毕至DOM加载', {
duration: timing.domInteractive - timing.responseEnd,
}),
);
console.log(
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
intl.get('解释DOM树耗时', {
duration: timing.domComplete - timing.domInteractive,
}),
);
console.log(
intl.get('从开始至load总耗时', {
duration: timing.loadEventEnd - timing.navigationStart,
}),
);
};
}, []);
+23 -1
View File
@@ -1,4 +1,19 @@
{
"选中条数": "Selected {count} items",
"登录失败次数": "Failed {count} time(s)",
"间隔定时规则": "Every {value} {unit}",
"SOCK5代理,例如 IP:PORT": "SOCKS5 proxy, e.g. IP:PORT",
"HTTP/SOCK5代理,例如 http://127.0.0.1:1080": "HTTP/SOCKS5 proxy, e.g. http://127.0.0.1:1080",
"确认操作对象": "Confirm {operation} {object}",
"确认批量操作": "Confirm {operation} selected {object}?",
"检查更新失败,请检查网络后重试": "Failed to check for updates. Check your network and try again.",
"确认操作": "Confirm {operation}",
"分页总数": "{start}-{end} of {total} items",
"总条数": "{total} items",
"内网IP": "Private IP",
"容器更新修复命令": "1. Run on the host: docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR <container_name>",
"WxPusher(极简推送SPT-推荐)": "WxPusher (Simple Push SPT, recommended)",
"新版本可用,当前版本": "Version {version} is available. You are using {current}.",
"1. 宿主机执行 docker run --rm -v\n /var/run/docker.sock:/var/run/docker.sock\n containrrr/watchtower -cR <容器名>": "1. Execute 'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR <container_name>' on the host machine",
"2. 容器内执行 ql check、ql update": "2. Execute 'ql check' and 'ql update' inside the container",
"24小时未运行": "Idle 24h+",
@@ -709,5 +724,12 @@
"脚本通知": "Script notification",
"系统通知": "System notification",
"Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python supports package names, Git URLs, and existing requirements.txt or pyproject.toml files under the scripts or repository directory.",
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "Package name, Git URL, requirements.txt or pyproject.toml path"
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "Package name, Git URL, requirements.txt or pyproject.toml path",
"控制台诗句": "A rain hat and cloak, a song among the hills. The moon, the breeze and I spend our idle days together.",
"运行环境信息": "QingLong runtime:\n\nSystem: {system}\nBrowser: {browser}\nEngine: {engine}",
"WebSocket错误": "WebSocket error:",
"白屏时间": "Time to first response: {duration} ms",
"请求完毕至DOM加载": "Response end to DOM interactive: {duration} ms",
"解释DOM树耗时": "DOM completion time: {duration} ms",
"从开始至load总耗时": "Total load time: {duration} ms"
}
+23 -1
View File
@@ -1,4 +1,19 @@
{
"选中条数": "已选择 {count} 项",
"登录失败次数": "失败{count}次",
"间隔定时规则": "每{value}{unit}",
"SOCK5代理,例如 IP:PORT": "SOCK5代理,例如 IP:PORT",
"HTTP/SOCK5代理,例如 http://127.0.0.1:1080": "HTTP/SOCK5代理,例如 http://127.0.0.1:1080",
"确认操作对象": "确认{operation}{object}",
"确认批量操作": "确认{operation}选中的{object}吗",
"检查更新失败,请检查网络后重试": "检查更新失败,请检查网络后重试",
"确认操作": "确认{operation}",
"分页总数": "第 {start}-{end} 条/总共 {total} 条",
"总条数": "共 {total} 个",
"内网IP": "内网IP",
"容器更新修复命令": "1. 宿主机执行 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR <容器名>",
"WxPusher(极简推送SPT-推荐)": "WxPusher(极简推送SPT-推荐)",
"新版本可用,当前版本": "新版本 {version} 可用,你使用的版本为 {current}。",
"1. 宿主机执行 docker run --rm -v\n /var/run/docker.sock:/var/run/docker.sock\n containrrr/watchtower -cR <容器名>": "1. 宿主机执行 docker run --rm -v\n /var/run/docker.sock:/var/run/docker.sock\n containrrr/watchtower -cR <容器名>",
"2. 容器内执行 ql check、ql update": "2. 容器内执行 ql check、ql update",
"24小时未运行": "24小时未运行",
@@ -709,5 +724,12 @@
"脚本通知": "脚本通知",
"系统通知": "系统通知",
"Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径",
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "支持包名、GitHub链接、requirements.txt或pyproject.toml路径"
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "支持包名、GitHub链接、requirements.txt或pyproject.toml路径",
"控制台诗句": "忘形雨笠烟蓑,知心牧唱樵歌。明月清风共我,闲人三个,从他今古消磨。",
"运行环境信息": "青龙运行环境:\n\n系统:{system}\n浏览器:{browser}\n内核:{engine}",
"WebSocket错误": "WebSocket错误:",
"白屏时间": "白屏时间: {duration} ms",
"请求完毕至DOM加载": "请求完毕至DOM加载: {duration} ms",
"解释DOM树耗时": "解释 DOM 树耗时: {duration} ms",
"从开始至load总耗时": "从开始至 load 总耗时: {duration} ms"
}
+4 -10
View File
@@ -364,13 +364,10 @@ const CronDetailModal = ({
const enabledOrDisabledCron = () => {
Modal.confirm({
title: `确认${currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
}`,
title: intl.get('确认操作', { operation: currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用') }),
content: (
<>
{intl.get('确认')}
{currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')}
{intl.get('定时任务')}{' '}
{intl.get('确认操作对象', { operation: currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用'), object: intl.get('定时任务') })}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
@@ -398,13 +395,10 @@ const CronDetailModal = ({
const pinOrUnPinCron = () => {
Modal.confirm({
title: `确认${currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
}`,
title: intl.get('确认操作', { operation: currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶') }),
content: (
<>
{intl.get('确认')}
{currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}
{intl.get('定时任务')}{' '}
{intl.get('确认操作对象', { operation: currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'), object: intl.get('定时任务') })}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
+16 -18
View File
@@ -544,13 +544,13 @@ const Crontab = () => {
const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
}`,
title: intl.get('确认操作', {
operation:
record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用'),
}),
content: (
<>
{intl.get('确认')}
{record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')}
{intl.get('定时任务')}{' '}
{intl.get('确认操作对象', { operation: record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用'), object: intl.get('定时任务') })}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
@@ -584,13 +584,13 @@ const Crontab = () => {
const pinOrUnPinCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
}`,
title: intl.get('确认操作', {
operation:
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'),
}),
content: (
<>
{intl.get('确认')}
{record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}
{intl.get('定时任务')}{' '}
{intl.get('确认操作对象', { operation: record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'), object: intl.get('定时任务') })}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
@@ -745,12 +745,12 @@ const Crontab = () => {
const operateCrons = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
title: intl.get('确认操作', {
operation: intl.get(OperationName[operationStatus]),
}),
content: (
<>
{intl.get('确认')}
{OperationName[operationStatus]}
{intl.get('选中的定时任务吗')}
{intl.get('确认批量操作', { operation: intl.get(OperationName[operationStatus]), object: intl.get('定时任务') })}
</>
),
onOk() {
@@ -1035,9 +1035,7 @@ const Crontab = () => {
{intl.get('批量修改标签')}
</Button>
<span style={{ marginLeft: 8 }}>
{intl.get('已选择')}
<a>{selectedRowIds?.length}</a>
{intl.get('项')}
{intl.get('选中条数', { count: selectedRowIds.length })}
</span>
</div>
)}
@@ -1051,7 +1049,7 @@ const Crontab = () => {
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
intl.get('分页总数', { start: range[0], end: range[1], total }),
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
+2 -2
View File
@@ -167,7 +167,7 @@ const Dashboard = () => {
const runtimePagination = {
pageSize: 5,
showSizeChanger: false,
showTotal: (total: number) => `共 ${total} 个`,
showTotal: (total: number) => intl.get('总条数', { total }),
};
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
@@ -348,7 +348,7 @@ const Dashboard = () => {
data={{
target: Number(system.memUsagePercent),
total: 100,
name: '内存使用率',
name: intl.get('内存使用'),
}}
style={{
textY: '70%',
+1 -3
View File
@@ -536,9 +536,7 @@ const Dependence = () => {
{intl.get('批量强制删除')}
</Button>
<span style={{ marginLeft: 8 }}>
{intl.get('已选择')}
<a>{selectedRowIds?.length}</a>
{intl.get('项')}
{intl.get('选中条数', { count: selectedRowIds.length })}
</span>
</div>
)}
+18 -22
View File
@@ -279,16 +279,13 @@ const Env = () => {
const enabledOrDisabledEnv = (record: any, index: number) => {
Modal.confirm({
title: `确认${
record.status === Status.已禁用 ? intl.get('启用') : intl.get('禁用')
}`,
title: intl.get('确认操作', {
operation:
record.status === Status.已禁用 ? intl.get('启用') : intl.get('禁用'),
}),
content: (
<>
{intl.get('确认')}
{record.status === Status.已禁用
? intl.get('启用')
: intl.get('禁用')}
Env{' '}
{intl.get('确认操作对象', { operation: record.status === Status.已禁用 ? intl.get('启用') : intl.get('禁用'), object: 'Env' })}{' '}
<Paragraph
style={{ wordBreak: 'break-all', display: 'inline' }}
ellipsis={{ rows: 6, expandable: true }}
@@ -343,14 +340,13 @@ const Env = () => {
const pinOrUnpinEnv = (record: any, index: number) => {
Modal.confirm({
title: `确认${
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
}`,
title: intl.get('确认操作', {
operation:
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'),
}),
content: (
<>
{intl.get('确认')}
{record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}
Env{' '}
{intl.get('确认操作对象', { operation: record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'), object: 'Env' })}{' '}
<Paragraph
style={{ wordBreak: 'break-all', display: 'inline' }}
ellipsis={{ rows: 6, expandable: true }}
@@ -543,12 +539,12 @@ const Env = () => {
const operateEnvs = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
title: intl.get('确认操作', {
operation: intl.get(OperationName[operationStatus]),
}),
content: (
<>
{intl.get('确认')}
{OperationName[operationStatus]}
{intl.get('选中的变量吗')}
{intl.get('确认批量操作', { operation: intl.get(OperationName[operationStatus]), object: intl.get('环境变量') })}
</>
),
onOk() {
@@ -603,7 +599,9 @@ const Env = () => {
);
if (code === 200) {
message.success(`${intl.get('成功上传')}${data.length}${intl.get('个环境变量')}`);
message.success(
`${intl.get('成功上传')}${data.length}${intl.get('个环境变量')}`,
);
getEnvs();
}
setImportLoading(false);
@@ -708,9 +706,7 @@ const Env = () => {
{intl.get('批量取消置顶')}
</Button>
<span style={{ marginLeft: 8 }}>
{intl.get('已选择')}
<a>{selectedRowIds?.length}</a>
{intl.get('项')}
{intl.get('选中条数', { count: selectedRowIds.length })}
</span>
</div>
)}
+2 -8
View File
@@ -79,16 +79,10 @@ const Error = () => {
description={
<Typography.Text type="danger">
<div>{intl.get('请先按如下方式修复:')}</div>
<div>
1. 宿主机执行 docker run --rm -v
/var/run/docker.sock:/var/run/docker.sock
containrrr/watchtower -cR &lt;容器名&gt;
</div>
<div>{intl.get('容器更新修复命令')}</div>
<div>{intl.get('2. 容器内执行 ql check、ql update')}</div>
<div>
{intl.get(
'3. 如果无法解决,容器内执行 ql log,拷贝执行结果',
)}
{intl.get('3. 如果无法解决,容器内执行 ql log,拷贝执行结果')}
<Typography.Link href="https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml">
{intl.get('提交 issue')}
</Typography.Link>
+1 -1
View File
@@ -129,7 +129,7 @@ const Initialization = () => {
>
<Input.TextArea
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={`请输入${x.label}`}
placeholder={`${intl.get('请输入')} ${x.label}`}
/>
</Form.Item>
))}
+2 -2
View File
@@ -91,7 +91,7 @@ const Login = () => {
</div>
<div>
{intl.get('上次登录地点:')}
{lastaddr || '-'}
{lastaddr === '内网IP' ? intl.get('内网IP') : lastaddr || '-'}
</div>
<div>
{intl.get('上次登录IP:')}
@@ -103,7 +103,7 @@ const Login = () => {
</div>
<div>
{intl.get('上次登录状态:')}
{retries > 0 ? `失败${retries}次` : intl.get('成功')}
{retries > 0 ? intl.get('登录失败次数', { count: retries }) : intl.get('成功')}
</div>
</>
),
+75 -63
View File
@@ -1,24 +1,25 @@
import { disableBody } from "@/utils";
import config from "@/utils/config";
import { request } from "@/utils/http";
import WebSocketManager from "@/utils/websocket";
import Ansi from "ansi-to-react";
import { Button, Modal, Statistic, message } from "antd";
import { useCallback, useEffect, useRef, useState } from "react";
import intl from "react-intl-universal";
import { disableBody } from '@/utils';
import config from '@/utils/config';
import { request } from '@/utils/http';
import WebSocketManager from '@/utils/websocket';
import Ansi from 'ansi-to-react';
import { Button, Modal, Statistic, message } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
import intl from 'react-intl-universal';
const { Countdown } = Statistic;
const CheckUpdate = ({ systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState("");
const [value, setValue] = useState('');
const modalRef = useRef<any>();
const lastStatusRef = useRef<string | undefined>();
const [updateStatus, setUpdateStatus] = useState<string>();
const checkUpgrade = () => {
if (updateLoading) return;
if (updateLoading || modalRef.current) return;
setUpdateLoading(true);
message.loading(intl.get("检查更新中..."), 0);
message.loading(intl.get('检查更新中...'), 0);
request
.put(`${config.apiPrefix}system/update-check`)
.then(({ code, data }) => {
@@ -33,7 +34,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
})
.catch((error: any) => {
message.destroy();
console.log(error);
message.error(intl.get('检查更新失败,请检查网络后重试'));
})
.finally(() => {
setUpdateLoading(false);
@@ -43,25 +44,19 @@ const CheckUpdate = ({ systemInfo }: any) => {
const showForceUpdateModal = (data: any) => {
Modal.confirm({
width: 500,
title: intl.get("更新"),
title: intl.get('更新'),
content: (
<>
<div>{intl.get("已经是最新版了!")}</div>
<div>{intl.get('已经是最新版了!')}</div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{intl.get("青龙")} {data.lastVersion}{" "}
{intl.get("是目前检测到的最新可用版本了。")}
{intl.get('青龙')} {data.lastVersion}{' '}
{intl.get('是目前检测到的最新可用版本了。')}
</div>
</>
),
okText: intl.get("重新下载"),
okText: intl.get('重新下载'),
onOk() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
.then((_data: any) => { })
.catch((error: any) => {
console.log(error);
});
startUpdate();
},
});
};
@@ -72,10 +67,12 @@ const CheckUpdate = ({ systemInfo }: any) => {
width: 500,
title: (
<>
<div>{intl.get("更新可用")}</div>
<div>{intl.get('更新可用')}</div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{intl.get("新版本")} {lastVersion}{" "}
{intl.get("可用,你使用的版本为")} {systemInfo.version}。
{intl.get('新版本可用,当前版本', {
version: lastVersion,
current: systemInfo.version,
})}
</div>
</>
),
@@ -84,52 +81,60 @@ const CheckUpdate = ({ systemInfo }: any) => {
<Ansi>{lastLog}</Ansi>
</pre>
),
okText: intl.get("下载更新"),
cancelText: intl.get("以后再说"),
okText: intl.get('下载更新'),
cancelText: intl.get('以后再说'),
onOk() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
.then((_data: any) => { })
.catch((error: any) => {
console.log(error);
});
startUpdate();
},
});
};
const showUpdatingModal = () => {
setValue("");
setValue('');
lastStatusRef.current = 'running';
setUpdateStatus('running');
modalRef.current = Modal.info({
width: 600,
maskClosable: false,
closable: false,
keyboard: false,
okButtonProps: { disabled: true },
title: intl.get("下载更新中..."),
title: intl.get('下载更新中...'),
centered: true,
content: (
<pre>
<Ansi>{value}</Ansi>
</pre>
),
afterClose: () => {
modalRef.current = undefined;
},
content: <pre />,
});
};
const startUpdate = async () => {
showUpdatingModal();
try {
const response = await request.put(`${config.apiPrefix}system/update`);
if (response.code !== 200) throw new Error(response.message);
} catch {
lastStatusRef.current = 'failed';
setUpdateStatus('failed');
message.error(intl.get('更新失败,请检查网络及日志或稍后再试'));
}
};
const reloadSystem = (type?: string) => {
request
.put(`${config.apiPrefix}update/${type}`)
.then((_data: any) => {
.then(({ code }: any) => {
if (code !== 200) return;
message.success({
content: (
<span>
{intl.get("系统将在")}
{intl.get('系统将在')}
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
{intl.get("秒后自动刷新")}
{intl.get('秒后自动刷新')}
</span>
),
duration: 30,
@@ -148,15 +153,15 @@ const CheckUpdate = ({ systemInfo }: any) => {
Modal.confirm({
width: 600,
maskClosable: false,
title: intl.get("确认重启"),
title: intl.get('确认重启'),
centered: true,
content: intl.get("系统安装包下载成功,确认重启"),
okText: intl.get("重启"),
content: intl.get('系统安装包下载成功,确认重启'),
okText: intl.get('重启'),
onOk() {
reloadSystem("system");
reloadSystem('system');
},
onCancel() {
modalRef.current.update({
modalRef.current?.update({
maskClosable: true,
closable: true,
okButtonProps: { disabled: false },
@@ -166,8 +171,8 @@ const CheckUpdate = ({ systemInfo }: any) => {
};
useEffect(() => {
if (!value) return;
const updateFailed = lastStatusRef.current === 'failed';
if (!modalRef.current) return;
const updateFailed = updateStatus === 'failed';
modalRef.current.update({
maskClosable: updateFailed,
@@ -182,27 +187,32 @@ const CheckUpdate = ({ systemInfo }: any) => {
</>
),
});
}, [value]);
}, [value, updateStatus]);
const handleMessage = useCallback((payload: any) => {
if (!modalRef.current || lastStatusRef.current !== 'running') return;
const { message: _message, status } = payload;
if (status === 'failed') {
lastStatusRef.current = 'failed';
message.error(intl.get("更新失败,请检查网络及日志或稍后再试"));
setUpdateStatus('failed');
message.error(intl.get('更新失败,请检查网络及日志或稍后再试'));
}
if (status === 'success') {
lastStatusRef.current = 'success';
setUpdateStatus('success');
setTimeout(() => {
showReloadModal();
if (modalRef.current && lastStatusRef.current === 'success') {
showReloadModal();
}
}, 1000);
}
setTimeout(() => {
document
.querySelector("#log-identifier")
?.scrollIntoView({ behavior: "smooth" });
.querySelector('#log-identifier')
?.scrollIntoView({ behavior: 'smooth' });
}, 600);
if (_message) {
@@ -212,24 +222,26 @@ const CheckUpdate = ({ systemInfo }: any) => {
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe("updateSystemVersion", handleMessage);
ws.subscribe('updateSystemVersion', handleMessage);
return () => {
ws.unsubscribe("updateSystemVersion", handleMessage);
ws.unsubscribe('updateSystemVersion', handleMessage);
modalRef.current?.destroy();
modalRef.current = undefined;
};
}, []);
return (
<>
<Button type="primary" onClick={checkUpgrade}>
{intl.get("检查更新")}
{intl.get('检查更新')}
</Button>
<Button
type="primary"
onClick={() => reloadSystem("reload")}
onClick={() => reloadSystem('reload')}
style={{ marginLeft: 8 }}
>
{intl.get("重新启动")}
{intl.get('重新启动')}
</Button>
</>
);
+5
View File
@@ -33,6 +33,11 @@
gap: 40px;
}
.notify-log-table .ant-table-thead > tr > th,
.login-log-table .ant-table-thead > tr > th {
white-space: nowrap;
}
.ql-container-wrapper.ql-setting-container {
.ant-tabs-tabpane > div {
padding-left: 2px;
+6 -4
View File
@@ -47,7 +47,7 @@ const LoginLog = ({ data, height }: { data: Array<any>; height: number }) => {
const columns = [
{
title: intl.get('序号'),
width: 50,
width: 90,
render: (text: string, record: any, index: number) => index + 1,
},
{
@@ -63,6 +63,7 @@ const LoginLog = ({ data, height }: { data: Array<any>; height: number }) => {
dataIndex: 'address',
width: 120,
key: 'address',
render: (text: string) => (text === '内网IP' ? intl.get('内网IP') : text),
},
{
title: intl.get('登录IP'),
@@ -74,13 +75,13 @@ const LoginLog = ({ data, height }: { data: Array<any>; height: number }) => {
title: intl.get('登录设备'),
dataIndex: 'platform',
key: 'platform',
width: 80,
width: 140,
},
{
title: intl.get('登录状态'),
dataIndex: 'status',
key: 'status',
width: 80,
width: 140,
render: (text: string, record: any) => (
<Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>
{intl.get(LoginStatus[record.status])}
@@ -116,12 +117,13 @@ const LoginLog = ({ data, height }: { data: Array<any>; height: number }) => {
return (
<>
<Table
className="login-log-table"
columns={columns}
pagination={false}
dataSource={data}
rowKey={(record) => `${record.ip}-${record.timestamp}`}
size="middle"
scroll={{ x: 1000, y: height }}
scroll={{ x: 1120, y: height }}
/>
</>
);
+5 -3
View File
@@ -2,6 +2,7 @@ import intl from 'react-intl-universal';
import React from 'react';
import { Table, Tag } from 'antd';
import dayjs from 'dayjs';
import './index.less';
interface NotifyLogItem {
id?: number;
@@ -40,7 +41,7 @@ const columns = [
},
{
title: intl.get('序号'),
width: 50,
width: 90,
render: (text: string, record: any, index: number) => {
return index + 1;
},
@@ -79,7 +80,7 @@ const columns = [
title: intl.get('发送状态'),
dataIndex: 'status',
key: 'status',
width: 90,
width: 140,
render: (text: string, record: NotifyLogItem) => {
const statusKey = record.status ?? 1;
return (
@@ -101,12 +102,13 @@ const NotifyLog = ({
return (
<>
<Table
className="notify-log-table"
columns={columns}
pagination={false}
dataSource={data}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: height }}
scroll={{ x: 1240, y: height }}
/>
</>
);
+8 -9
View File
@@ -103,7 +103,7 @@ const Subscription = () => {
key: 'type',
width: 130,
render: (text: string, record: any) => {
return (SubscriptionType as any)[record.type];
return intl.get((SubscriptionType as any)[record.type]);
},
},
{
@@ -121,7 +121,7 @@ const Subscription = () => {
render: (text: string, record: any) => {
if (record.schedule_type === 'interval') {
const { type, value } = record.interval_schedule;
return `每${value}${(IntervalSchedule as any)[type]}`;
return intl.get('间隔定时规则', { value, unit: intl.get((IntervalSchedule as any)[type]) });
}
return record.schedule;
},
@@ -369,14 +369,13 @@ const Subscription = () => {
const enabledOrDisabledSubscription = (record: any, index: number) => {
Modal.confirm({
title: `确认${
record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用')
}`,
title: intl.get('确认操作', {
operation:
record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用'),
}),
content: (
<>
{intl.get('确认')}
{record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用')}
{intl.get('定时订阅')}{' '}
{intl.get('确认操作对象', { operation: record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用'), object: intl.get('定时订阅') })}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
@@ -569,7 +568,7 @@ const Subscription = () => {
simple: isPhone,
defaultPageSize: 20,
showTotal: (total: number, range: number[]) =>
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
intl.get('分页总数', { start: range[0], end: range[1], total }),
pageSizeOptions: [20, 100, 500, 1000] as any,
}}
dataSource={value}
+4 -4
View File
@@ -150,10 +150,10 @@ const SubscriptionModal = ({
precision={0}
min={1}
value={intervalNumber}
style={{ width: 'calc(100% - 58px)' }}
style={{ width: 'calc(100% - 100px)' }}
onChange={numberChange}
/>
<Select value={intervalType} onChange={intervalTypeChange}>
<Select value={intervalType} onChange={intervalTypeChange} style={{ width: 100 }}>
<Option value="days">{intl.get('天')}</Option>
<Option value="hours">{intl.get('时')}</Option>
<Option value="minutes">{intl.get('分')}</Option>
@@ -527,8 +527,8 @@ const SubscriptionModal = ({
onPaste={onNamePaste}
placeholder={
type === 'private-repo'
? 'SOCK5代理,例如 IP:PORT'
: 'HTTP/SOCK5代理,例如 http://127.0.0.1:1080'
? intl.get('SOCK5代理,例如 IP:PORT')
: intl.get('HTTP/SOCK5代理,例如 http://127.0.0.1:1080')
}
/>
</Form.Item>
+2 -4
View File
@@ -80,7 +80,7 @@ export default {
{ value: 'pushPlus', label: 'PushPlus' },
{ value: 'wePlusBot', label: intl.get('微加机器人') },
{ value: 'wxPusherBot', label: 'wxPusher' },
{ value: 'wxPusherSpt', label: 'WxPusher(极简推送SPT-推荐)' },
{ value: 'wxPusherSpt', label: intl.get('WxPusher(极简推送SPT-推荐)') },
{ value: 'openiLink', label: 'OpeniLink' },
{ value: 'wpush', label: 'WPUSH' },
{ value: 'chat', label: intl.get('群晖chat') },
@@ -431,9 +431,7 @@ export default {
},
{
label: 'larkSecret',
tip: intl.get(
'飞书群组机器人加签密钥,安全设置中开启签名校验后获得',
),
tip: intl.get('飞书群组机器人加签密钥,安全设置中开启签名校验后获得'),
},
],
email: [
+10 -6
View File
@@ -155,9 +155,9 @@ export default function browserType() {
shell === 'none'
? {}
: {
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
);
console.log(
@@ -165,11 +165,15 @@ export default function browserType() {
'color: blue;font-size: 14px;',
);
console.log(
'%c忘形雨笠烟蓑,知心牧唱樵歌。明月清风共我,闲人三个,从他今古消磨。\n',
`%c${intl.get('控制台诗句')}\n`,
'color: yellow;font-size: 18px;',
);
console.log(
`%c青龙运行环境:\n\n系统:${result.system}/${result.systemVs}\n浏览器:${result.supporter}/${result.supporterVs}\n内核:${result.engine}/${result.engineVs}`,
`%c${intl.get('运行环境信息', {
system: `${result.system}/${result.systemVs}`,
browser: `${result.supporter}/${result.supporterVs}`,
engine: `${result.engine}/${result.engineVs}`,
})}`,
'color: green;font-size: 14px;font-weight: bold;',
);
@@ -337,7 +341,7 @@ export function parseCrontab(schedule: string): Date | null {
if (time) {
return time.next().toDate();
}
} catch (error) { }
} catch (error) {}
return null;
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { loader } from '@monaco-editor/react';
import config from './config';
import intl from 'react-intl-universal';
export function init(version: string) {
// monaco 编辑器配置cdn和locale
@@ -9,7 +10,7 @@ export function init(version: string) {
},
'vs/nls': {
availableLanguages: {
'*': 'zh-cn',
'*': intl.getInitOptions().currentLocale === 'en' ? 'i-default' : 'zh-cn',
},
},
});
+15 -5
View File
@@ -1,11 +1,13 @@
import SockJS from 'sockjs-client';
import intl from 'react-intl-universal';
import { SockMessageType } from './type';
class WebSocketManager {
private static instance: WebSocketManager | null = null;
private url: string;
private socket: WebSocket | null = null;
private subscriptions: Map<SockMessageType, Set<(p: any) => void>> = new Map();
private subscriptions: Map<SockMessageType, Set<(p: any) => void>> =
new Map();
private options: {
maxReconnectAttempts: number;
reconnectInterval: number;
@@ -15,7 +17,10 @@ class WebSocketManager {
private heartbeatTimeout: NodeJS.Timeout | null = null;
private state: 'closed' | 'connecting' | 'open' = 'closed';
constructor(url: string, options: Partial<typeof WebSocketManager.prototype.options> = {}) {
constructor(
url: string,
options: Partial<typeof WebSocketManager.prototype.options> = {},
) {
this.url = url;
this.options = {
maxReconnectAttempts: options.maxReconnectAttempts || 5,
@@ -26,7 +31,10 @@ class WebSocketManager {
this.init();
}
public static getInstance(url: string = '', options?: Partial<typeof WebSocketManager.prototype.options>): WebSocketManager {
public static getInstance(
url: string = '',
options?: Partial<typeof WebSocketManager.prototype.options>,
): WebSocketManager {
if (!WebSocketManager.instance) {
WebSocketManager.instance = new WebSocketManager(url, options);
}
@@ -47,7 +55,9 @@ class WebSocketManager {
this.socket = null;
this.reconnectAttempts++;
await new Promise((resolve) => setTimeout(resolve, this.options.reconnectInterval));
await new Promise((resolve) =>
setTimeout(resolve, this.options.reconnectInterval),
);
}
} catch (error) {
this.handleError(error);
@@ -138,7 +148,7 @@ class WebSocketManager {
}
private handleError(error: any) {
console.error('WebSocket错误:', error);
console.error(intl.get('WebSocket错误'), error);
this.emit('error', error);
}