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
+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>