Skip to content

Commit 45a2810

Browse files
authored
Add configurable browser tab title for multi-instance QingLong deployments (#3041)
1 parent da6dac8 commit 45a2810

10 files changed

Lines changed: 161 additions & 25 deletions

File tree

back/api/system.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,24 @@ export default (app: Router) => {
442442
},
443443
);
444444

445+
route.put(
446+
'/config/panel-title',
447+
celebrate({
448+
body: Joi.object({
449+
panelTitle: Joi.string().max(100).allow('').allow(null),
450+
}),
451+
}),
452+
async (req: Request, res: Response, next: NextFunction) => {
453+
try {
454+
const systemService = Container.get(SystemService);
455+
const result = await systemService.updatePanelTitle(req.body);
456+
res.send(result);
457+
} catch (e) {
458+
return next(e);
459+
}
460+
},
461+
);
462+
445463
route.put(
446464
'/config/global-ssh-key',
447465
celebrate({

back/data/system.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export enum AuthDataType {
3232

3333
export interface SystemConfigInfo {
3434
lang?: string;
35+
panelTitle?: string;
3536
logRemoveFrequency?: number;
3637
cronConcurrency?: number;
3738
dependenceProxy?: string;

back/services/system.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,24 +548,34 @@ export default class SystemService {
548548
return { code: 200, data: { lang } };
549549
}
550550

551+
public async updatePanelTitle(info: SystemModelInfo) {
552+
const oDoc = await this.getSystemConfig();
553+
const panelTitle = info.panelTitle?.trim() || '';
554+
await this.updateAuthDb({
555+
...oDoc,
556+
info: { ...oDoc.info, panelTitle },
557+
});
558+
return { code: 200, data: { panelTitle } };
559+
}
560+
551561
public async updateGlobalSshKey(info: SystemModelInfo) {
552562
const oDoc = await this.getSystemConfig();
553563
const result = await this.updateAuthDb({
554564
...oDoc,
555565
info: { ...oDoc.info, ...info },
556566
});
557-
567+
558568
// Apply the global SSH key
559569
const SshKeyService = require('./sshKey').default;
560570
const Container = require('typedi').Container;
561571
const sshKeyService = Container.get(SshKeyService);
562-
572+
563573
if (info.globalSshKey) {
564574
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
565575
} else {
566576
await sshKeyService.removeGlobalSSHKey('global');
567577
}
568-
578+
569579
return { code: 200, data: result };
570580
}
571581

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@whyour/qinglong",
33
"packageManager": "pnpm@8.3.1",
4-
"version": "2.20.2-3",
4+
"version": "2.21.0-12",
55
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
66
"repository": {
77
"type": "git",
@@ -21,9 +21,7 @@
2121
"panel": "npm run build:back && node static/build/app.js",
2222
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
2323
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
24-
"postinstall": "max setup 2>/dev/null || true",
25-
"test": "umi-test",
26-
"test:coverage": "umi-test --coverage"
24+
"postinstall": "max setup 2>/dev/null || true"
2725
},
2826
"gitHooks": {
2927
"pre-commit": "lint-staged"

src/layouts/index.tsx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import WebSocketManager from '../utils/websocket';
2222
export interface SharedContext {
2323
headerStyle: React.CSSProperties;
2424
isPhone: boolean;
25+
siteTitle: string;
2526
theme: 'vs' | 'vs-dark';
2627
user: any;
2728
reloadUser: (needLoading?: boolean) => void;
29+
reloadSystemConfig: () => void;
2830
reloadTheme: () => void;
2931
systemInfo: TSystemInfo;
3032
}
@@ -45,6 +47,9 @@ export default function () {
4547
const [user, setUser] = useState<any>({});
4648
const [loading, setLoading] = useState<boolean>(true);
4749
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
50+
const [siteTitle, setSiteTitle] = useState(
51+
() => localStorage.getItem('qinglong_panel_title')?.trim() || intl.get('青龙'),
52+
);
4853
const [collapsed, setCollapsed] = useState(false);
4954
const [initLoading, setInitLoading] = useState<boolean>(true);
5055
const {
@@ -124,20 +129,18 @@ export default function () {
124129
getUser(needLoading);
125130
};
126131

127-
useEffect(() => {
128-
if (systemInfo && systemInfo.isInitialized && !user) {
129-
getUser();
130-
}
131-
}, [location.pathname]);
132-
133-
useEffect(() => {
134-
getHealthStatus();
135-
}, []);
136-
137-
useEffect(() => {
132+
const reloadSystemConfig = () => {
138133
request
139134
.get(`${config.apiPrefix}system/config`)
140135
.then(({ data }: any) => {
136+
const panelTitle = data?.info?.panelTitle?.trim();
137+
if (panelTitle) {
138+
setSiteTitle(panelTitle);
139+
localStorage.setItem('qinglong_panel_title', panelTitle);
140+
} else {
141+
setSiteTitle(intl.get('青龙'));
142+
localStorage.removeItem('qinglong_panel_title');
143+
}
141144
if (!data?.info?.lang) {
142145
const browserLang =
143146
localStorage.getItem('lang') ||
@@ -149,8 +152,31 @@ export default function () {
149152
}
150153
})
151154
.catch(() => {});
155+
};
156+
157+
useEffect(() => {
158+
if (systemInfo && systemInfo.isInitialized && !user) {
159+
getUser();
160+
}
161+
}, [location.pathname]);
162+
163+
useEffect(() => {
164+
getHealthStatus();
165+
}, []);
166+
167+
useEffect(() => {
168+
reloadSystemConfig();
152169
}, []);
153170

171+
useEffect(() => {
172+
if (!['/login', '/initialization', '/error'].includes(location.pathname)) {
173+
return;
174+
}
175+
const title =
176+
(config.documentTitleMap as any)[location.pathname] || intl.get('未找到');
177+
document.title = `${title} - ${siteTitle}`;
178+
}, [location.pathname, siteTitle]);
179+
154180
useEffect(() => {
155181
if (theme === 'vs-dark') {
156182
document.body.setAttribute('data-dark', 'true');
@@ -229,7 +255,10 @@ export default function () {
229255
theme,
230256
user,
231257
reloadUser,
258+
reloadSystemConfig,
232259
reloadTheme,
260+
siteTitle,
261+
systemInfo,
233262
}}
234263
/>
235264
);
@@ -310,7 +339,7 @@ export default function () {
310339
const title =
311340
(config.documentTitleMap as any)[location.pathname] ||
312341
intl.get('未找到');
313-
return `${title} - ${intl.get('青龙')}`;
342+
return `${title} - ${siteTitle}`;
314343
}}
315344
onCollapse={setCollapsed}
316345
collapsed={collapsed}
@@ -372,7 +401,9 @@ export default function () {
372401
theme,
373402
user,
374403
reloadUser,
404+
reloadSystemConfig,
375405
reloadTheme,
406+
siteTitle,
376407
systemInfo,
377408
}}
378409
/>

src/locales/en-US.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,10 @@
581581
"链接": "Link",
582582
"错误": "Error",
583583
"错误日志": "Error Log",
584+
"留空使用默认值“青龙”": "Leave blank to use the default title \"Qinglong\"",
584585
"队列中": "In Queue",
586+
"自定义面板的站点标题,留空使用默认值“青龙”": "Customize the panel site title. Leave blank to use the default title \"Qinglong\"",
587+
"面板标题": "Panel Title",
585588
"青龙": "Qinglong",
586589
"项": "items",
587590
"顺序": "Order",

src/locales/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,10 @@
581581
"链接": "链接",
582582
"错误": "错误",
583583
"错误日志": "错误日志",
584+
"留空使用默认值“青龙”": "留空使用默认值“青龙”",
584585
"队列中": "队列中",
586+
"自定义面板的站点标题,留空使用默认值“青龙”": "自定义面板的站点标题,留空使用默认值“青龙”",
587+
"面板标题": "面板标题",
585588
"青龙": "青龙",
586589
"项": "",
587590
"顺序": "顺序",

src/pages/setting/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const Setting = () => {
4646
user,
4747
theme,
4848
reloadUser,
49+
reloadSystemConfig,
4950
reloadTheme,
5051
systemInfo,
5152
} = useOutletContext<SharedContext>();
@@ -353,7 +354,11 @@ const Setting = () => {
353354
key: 'other',
354355
label: intl.get('其他设置'),
355356
children: (
356-
<Other reloadTheme={reloadTheme} systemInfo={systemInfo} />
357+
<Other
358+
reloadSystemConfig={reloadSystemConfig}
359+
reloadTheme={reloadTheme}
360+
systemInfo={systemInfo}
361+
/>
357362
),
358363
},
359364
{

src/pages/setting/other.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { disableBody } from '@/utils';
2727
import { TIMEZONES } from '@/utils/const';
2828

2929
const dataMap = {
30+
'panel-title': 'panelTitle',
3031
'log-remove-frequency': 'logRemoveFrequency',
3132
'cron-concurrency': 'cronConcurrency',
3233
timezone: 'timezone',
@@ -48,10 +49,15 @@ const exportModules = [
4849

4950
const Other = ({
5051
systemInfo,
52+
reloadSystemConfig,
5153
reloadTheme,
52-
}: Pick<SharedContext, 'reloadTheme' | 'systemInfo'>) => {
54+
}: Pick<
55+
SharedContext,
56+
'reloadSystemConfig' | 'reloadTheme' | 'systemInfo'
57+
>) => {
5358
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
5459
const [systemConfig, setSystemConfig] = useState<{
60+
panelTitle?: string | null;
5561
logRemoveFrequency?: number | null;
5662
cronConcurrency?: number | null;
5763
timezone?: string | null;
@@ -120,6 +126,9 @@ const Other = ({
120126
.then(({ code, data }) => {
121127
if (code === 200) {
122128
message.success(intl.get('更新成功'));
129+
if (path === 'panel-title') {
130+
reloadSystemConfig();
131+
}
123132
}
124133
})
125134
.catch((error: any) => {
@@ -236,6 +245,35 @@ const Other = ({
236245
</Radio.Button>
237246
</Radio.Group>
238247
</Form.Item>
248+
<Form.Item
249+
label={intl.get('面板标题')}
250+
name="panelTitle"
251+
tooltip={intl.get('自定义面板的站点标题,留空使用默认值“青龙”')}
252+
>
253+
<Input.Group compact>
254+
<Input
255+
style={{ width: 180 }}
256+
maxLength={100}
257+
value={systemConfig?.panelTitle || ''}
258+
placeholder={intl.get('留空使用默认值“青龙”')}
259+
onChange={(e) => {
260+
setSystemConfig({
261+
...systemConfig,
262+
panelTitle: e.target.value,
263+
});
264+
}}
265+
/>
266+
<Button
267+
type="primary"
268+
onClick={() => {
269+
updateSystemConfig('panel-title');
270+
}}
271+
style={{ width: 84 }}
272+
>
273+
{intl.get('确认')}
274+
</Button>
275+
</Input.Group>
276+
</Form.Item>
239277
<Form.Item
240278
label={intl.get('日志删除频率')}
241279
name="frequency"

version.yaml

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
1-
version: 2.20.2
2-
changeLogLink: https://t.me/jiao_long/434
3-
publishTime: 2026-03-01 1800
1+
version: 2.21.0
2+
changeLogLink: https://t.me/jiao_long/436
3+
publishTime: 2026-06-18 2300
44
changeLog: |
5-
1. 修复 path 安全漏洞(重要)
5+
1. shell 增加国际化支持
6+
2. 接口提示信息国际化,更新国际化文案
7+
3. 统一 Alpine/Debian 分支,QL_SCHEDULER 参数化调度
8+
4. grpc 服务增加证书校验
9+
5. 定时任务增加 work_dir 设置
10+
6. 增加任务统计功能
11+
7. 新增 OpeniLink 通知渠道
12+
8. 支持自定义接收邮箱地址
13+
9. 增加运行实例功能
14+
10. 增加 sudo 命令判断
15+
11. 延迟增加运行时间提示
16+
12. 开机运行任务支持同时开始运行
17+
13. 增加 localhost 检测
18+
14. 增加环境变量标签功能
19+
15. 修复路径穿越安全漏洞
20+
16. 修复配置文件路径可能越权
21+
17. 修复 work_dir 验证和目录判断
22+
18. 修复多个 API 问题(getCronById、定时任务参数、任务退出码等)
23+
19. 修复 ESM 依赖查询路径
24+
20. 修复 CodeMirror 页面切换多实例崩溃
25+
21. 修复 IPv6 网络连接问题
26+
22. 修复内部服务 IP 地址
27+
23. 修复 gRPC extra_schedules 为空时序列化报错
28+
24. 修复 HITOKOTO 参数布尔/字符串类型处理
29+
25. 修复 Server酱 返回错误时 undefined 异常
30+
26. 修复环境变量 position 数据类型异常
31+
27. 修复 Docker 健康检查 QlPort 环境变量读取
32+
28. 修复任务统计日志
33+
29. 升级 multer 解决 CVE 漏洞
34+
30. 升级 nodemailer
635

0 commit comments

Comments
 (0)