feat: support custom panel title

This commit is contained in:
copilot-swe-agent[bot]
2026-07-09 17:39:16 +00:00
committed by GitHub
parent 29ce610f6f
commit 4dc3865d96
9 changed files with 147 additions and 37 deletions
+18
View File
@@ -442,6 +442,24 @@ export default (app: Router) => {
},
);
route.put(
'/config/panel-title',
celebrate({
body: Joi.object({
panelTitle: Joi.string().max(100).allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updatePanelTitle(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/global-ssh-key',
celebrate({
+1
View File
@@ -32,6 +32,7 @@ export enum AuthDataType {
export interface SystemConfigInfo {
lang?: string;
panelTitle?: string;
logRemoveFrequency?: number;
cronConcurrency?: number;
dependenceProxy?: string;
+32 -14
View File
@@ -49,7 +49,7 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) { }
) {}
public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig });
@@ -276,7 +276,7 @@ export default class SystemService {
);
const text = await body.text();
lastVersionContent = parseContentVersion(text);
} catch (error) { }
} catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
@@ -399,16 +399,23 @@ export default class SystemService {
}
}
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {
public async run(
{ command, logPath }: { command: string; logPath?: string },
callback: TaskCallbacks,
) {
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
const logPathPrefix = logPath ? `real_log_path=${logPath}` : ''
this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, {
command,
id: command.replace(/ /g, '-'),
runOrigin: 'system',
});
const logPathPrefix = logPath ? `real_log_path=${logPath}` : '';
this.scheduleService.runTask(
`${logPathPrefix} real_time=true ${command}`,
callback,
{
command,
id: command.replace(/ /g, '-'),
runOrigin: 'system',
},
);
}
public async stop({ command, pid }: { command: string; pid: number }) {
@@ -441,7 +448,8 @@ export default class SystemService {
}
const dataPaths = dataDirs.map((dir) => `data/${dir}`);
await promiseExec(
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile
`cd ${config.dataPath} && cd ../ && tar -zcvf ${
config.dataTgzFile
} ${dataPaths.join(' ')}`,
);
res.download(config.dataTgzFile);
@@ -548,24 +556,34 @@ export default class SystemService {
return { code: 200, data: { lang } };
}
public async updatePanelTitle(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
const panelTitle = info.panelTitle?.trim() || '';
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, panelTitle },
});
return { code: 200, data: { panelTitle } };
}
public async updateGlobalSshKey(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
// Apply the global SSH key
const SshKeyService = require('./sshKey').default;
const Container = require('typedi').Container;
const sshKeyService = Container.get(SshKeyService);
if (info.globalSshKey) {
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
} else {
await sshKeyService.removeGlobalSSHKey('global');
}
return { code: 200, data: result };
}
@@ -576,7 +594,7 @@ export default class SystemService {
try {
const finalPath = path.join(config.dependenceCachePath, type);
await fs.promises.rm(finalPath, { recursive: true });
} catch (error) { }
} catch (error) {}
return { code: 200 };
}
}
+33 -13
View File
@@ -22,9 +22,11 @@ import WebSocketManager from '../utils/websocket';
export interface SharedContext {
headerStyle: React.CSSProperties;
isPhone: boolean;
siteTitle: string;
theme: 'vs' | 'vs-dark';
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadSystemConfig: () => void;
reloadTheme: () => void;
systemInfo: TSystemInfo;
}
@@ -45,6 +47,7 @@ export default function () {
const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const [siteTitle, setSiteTitle] = useState(config.siteName);
const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const {
@@ -124,20 +127,18 @@ export default function () {
getUser(needLoading);
};
useEffect(() => {
if (systemInfo && systemInfo.isInitialized && !user) {
getUser();
}
}, [location.pathname]);
useEffect(() => {
getHealthStatus();
}, []);
useEffect(() => {
const reloadSystemConfig = () => {
request
.get(`${config.apiPrefix}system/config`)
.then(({ data }: any) => {
const panelTitle = data?.info?.panelTitle?.trim();
if (panelTitle) {
setSiteTitle(panelTitle);
localStorage.setItem('qinglong_panel_title', panelTitle);
} else {
setSiteTitle(intl.get('青龙'));
localStorage.removeItem('qinglong_panel_title');
}
if (!data?.info?.lang) {
const browserLang =
localStorage.getItem('lang') ||
@@ -149,6 +150,20 @@ export default function () {
}
})
.catch(() => {});
};
useEffect(() => {
if (systemInfo && systemInfo.isInitialized && !user) {
getUser();
}
}, [location.pathname]);
useEffect(() => {
getHealthStatus();
}, []);
useEffect(() => {
reloadSystemConfig();
}, []);
useEffect(() => {
@@ -229,7 +244,10 @@ export default function () {
theme,
user,
reloadUser,
reloadSystemConfig,
reloadTheme,
siteTitle,
systemInfo,
}}
/>
);
@@ -263,7 +281,7 @@ export default function () {
<>
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
<div className="title">
<span className="title">{intl.get('青龙')}</span>
<span className="title">{siteTitle}</span>
<span
onClick={(e) => {
e.stopPropagation();
@@ -310,7 +328,7 @@ export default function () {
const title =
(config.documentTitleMap as any)[location.pathname] ||
intl.get('未找到');
return `${title} - ${intl.get('青龙')}`;
return `${title} - ${siteTitle}`;
}}
onCollapse={setCollapsed}
collapsed={collapsed}
@@ -372,7 +390,9 @@ export default function () {
theme,
user,
reloadUser,
reloadSystemConfig,
reloadTheme,
siteTitle,
systemInfo,
}}
/>
+3
View File
@@ -581,7 +581,10 @@
"链接": "Link",
"错误": "Error",
"错误日志": "Error Log",
"留空使用默认值“青龙”": "Leave blank to use the default title \"Qinglong\"",
"队列中": "In Queue",
"自定义面板的站点标题,留空使用默认值“青龙”": "Customize the panel site title. Leave blank to use the default title \"Qinglong\"",
"面板标题": "Panel Title",
"青龙": "Qinglong",
"项": "items",
"顺序": "Order",
+3
View File
@@ -581,7 +581,10 @@
"链接": "链接",
"错误": "错误",
"错误日志": "错误日志",
"留空使用默认值“青龙”": "留空使用默认值“青龙”",
"队列中": "队列中",
"自定义面板的站点标题,留空使用默认值“青龙”": "自定义面板的站点标题,留空使用默认值“青龙”",
"面板标题": "面板标题",
"青龙": "青龙",
"项": "项",
"顺序": "顺序",
+7 -2
View File
@@ -46,10 +46,11 @@ const Setting = () => {
user,
theme,
reloadUser,
reloadSystemConfig,
reloadTheme,
systemInfo,
} = useOutletContext<SharedContext>();
console.log('user',user)
console.log('user', user);
const columns = [
{
title: intl.get('名称'),
@@ -353,7 +354,11 @@ const Setting = () => {
key: 'other',
label: intl.get('其他设置'),
children: (
<Other reloadTheme={reloadTheme} systemInfo={systemInfo} />
<Other
reloadSystemConfig={reloadSystemConfig}
reloadTheme={reloadTheme}
systemInfo={systemInfo}
/>
),
},
{
+45 -4
View File
@@ -27,6 +27,7 @@ import { disableBody } from '@/utils';
import { TIMEZONES } from '@/utils/const';
const dataMap = {
'panel-title': 'panelTitle',
'log-remove-frequency': 'logRemoveFrequency',
'cron-concurrency': 'cronConcurrency',
timezone: 'timezone',
@@ -48,10 +49,15 @@ const exportModules = [
const Other = ({
systemInfo,
reloadSystemConfig,
reloadTheme,
}: Pick<SharedContext, 'reloadTheme' | 'systemInfo'>) => {
}: Pick<
SharedContext,
'reloadSystemConfig' | 'reloadTheme' | 'systemInfo'
>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [systemConfig, setSystemConfig] = useState<{
panelTitle?: string | null;
logRemoveFrequency?: number | null;
cronConcurrency?: number | null;
timezone?: string | null;
@@ -120,6 +126,9 @@ const Other = ({
.then(({ code, data }) => {
if (code === 200) {
message.success(intl.get('更新成功'));
if (path === 'panel-title') {
reloadSystemConfig();
}
}
})
.catch((error: any) => {
@@ -236,6 +245,35 @@ const Other = ({
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
label={intl.get('面板标题')}
name="panelTitle"
tooltip={intl.get('自定义面板的站点标题,留空使用默认值“青龙”')}
>
<Input.Group compact>
<Input
style={{ width: 180 }}
maxLength={100}
value={systemConfig?.panelTitle || ''}
placeholder={intl.get('留空使用默认值“青龙”')}
onChange={(e) => {
setSystemConfig({
...systemConfig,
panelTitle: e.target.value,
});
}}
/>
<Button
type="primary"
onClick={() => {
updateSystemConfig('panel-title');
}}
style={{ width: 84 }}
>
{intl.get('确认')}
</Button>
</Input.Group>
</Form.Item>
<Form.Item
label={intl.get('日志删除频率')}
name="frequency"
@@ -314,8 +352,8 @@ const Other = ({
</Button>
</Input.Group>
</Form.Item>
<Form.Item
label={intl.get('全局SSH私钥')}
<Form.Item
label={intl.get('全局SSH私钥')}
name="globalSshKey"
tooltip={intl.get('用于访问所有私有仓库的全局SSH私钥')}
>
@@ -326,7 +364,10 @@ const Other = ({
autoSize={{ minRows: 3, maxRows: 8 }}
placeholder={intl.get('请输入完整的SSH私钥内容')}
onChange={(e) => {
setSystemConfig({ ...systemConfig, globalSshKey: e.target.value });
setSystemConfig({
...systemConfig,
globalSshKey: e.target.value,
});
}}
/>
</Input.Group>
+5 -4
View File
@@ -1,8 +1,11 @@
import intl from 'react-intl-universal';
const baseUrl = window.__ENV__QlBaseUrl || '/';
const defaultSiteName = intl.get('青龙');
const siteName =
localStorage.getItem('qinglong_panel_title')?.trim() || defaultSiteName;
export default {
siteName: intl.get('青龙'),
siteName,
baseUrl,
apiPrefix: `${baseUrl}api/`,
authKey: 'token',
@@ -404,9 +407,7 @@ export default {
},
{
label: 'larkSecret',
tip: intl.get(
'飞书群组机器人加签密钥,安全设置中开启签名校验后获得',
),
tip: intl.get('飞书群组机器人加签密钥,安全设置中开启签名校验后获得'),
},
],
email: [