mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): add bounded panel bootstrap
This commit is contained in:
+64
-8
@@ -18,6 +18,13 @@ import defaultProps from './defaultProps';
|
||||
import './index.less';
|
||||
import { init } from '../utils/init';
|
||||
import WebSocketManager from '../utils/websocket';
|
||||
import {
|
||||
clearQingLong3Credential,
|
||||
discoverQingLong3,
|
||||
isQingLong3PanelSession,
|
||||
qingLong3Capabilities,
|
||||
qingLong3Credential,
|
||||
} from '../utils/qinglong3';
|
||||
|
||||
export interface SharedContext {
|
||||
headerStyle: React.CSSProperties;
|
||||
@@ -38,6 +45,12 @@ interface TSystemInfo {
|
||||
version: string;
|
||||
changeLog: string;
|
||||
changeLogLink: string;
|
||||
ql3?: {
|
||||
schemaVersion: 1;
|
||||
mode: 'local';
|
||||
profile: 'edge' | 'standalone';
|
||||
capabilitiesPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function () {
|
||||
@@ -48,7 +61,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);
|
||||
@@ -61,6 +75,12 @@ export default function () {
|
||||
} = DarkReader || {};
|
||||
|
||||
const logout = () => {
|
||||
if (isQingLong3PanelSession()) {
|
||||
clearQingLong3Credential();
|
||||
setUser({});
|
||||
history.push('/login');
|
||||
return;
|
||||
}
|
||||
request.post(`${config.apiPrefix}user/logout`).then(() => {
|
||||
localStorage.removeItem(config.authKey);
|
||||
history.push('/login');
|
||||
@@ -70,14 +90,27 @@ export default function () {
|
||||
const getSystemInfo = () => {
|
||||
request
|
||||
.get(`${config.apiPrefix}system`)
|
||||
.then(({ code, data }) => {
|
||||
.then(async ({ code, data }) => {
|
||||
if (code === 200) {
|
||||
let qingLong3 = null;
|
||||
if (data?.ql3?.capabilitiesPath === '/api/v3/capabilities') {
|
||||
qingLong3 = await discoverQingLong3(
|
||||
`${config.apiPrefix}v3/capabilities`,
|
||||
);
|
||||
}
|
||||
setSystemInfo(data);
|
||||
if (!data.isInitialized) {
|
||||
history.push('/initialization');
|
||||
} else {
|
||||
init(data.version);
|
||||
getUser();
|
||||
if (!qingLong3 || qingLong3Credential()) {
|
||||
getUser();
|
||||
} else {
|
||||
setLoading(false);
|
||||
if (!['/login', '/error'].includes(location.pathname)) {
|
||||
history.replace('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -94,7 +127,7 @@ export default function () {
|
||||
if (code === 200 && data.username) {
|
||||
setUser(data);
|
||||
if (location.pathname === '/') {
|
||||
history.push('/dashboard');
|
||||
history.push(data?.ql3?.panelHome || '/dashboard');
|
||||
}
|
||||
}
|
||||
needLoading && setLoading(false);
|
||||
@@ -165,8 +198,10 @@ export default function () {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!systemInfo) return;
|
||||
if (systemInfo.ql3 && !isQingLong3PanelSession()) return;
|
||||
reloadSystemConfig();
|
||||
}, []);
|
||||
}, [systemInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!['/login', '/initialization', '/error'].includes(location.pathname)) {
|
||||
@@ -210,7 +245,7 @@ export default function () {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !user.username) return;
|
||||
if (!user || !user.username || isQingLong3PanelSession()) return;
|
||||
const ws = WebSocketManager.getInstance(
|
||||
`${window.location.origin}${
|
||||
config.apiPrefix
|
||||
@@ -222,6 +257,16 @@ export default function () {
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isQingLong3PanelSession() &&
|
||||
user?.username &&
|
||||
!['/login', '/error', '/crontab'].includes(location.pathname)
|
||||
) {
|
||||
history.replace('/crontab');
|
||||
}
|
||||
}, [location.pathname, user]);
|
||||
|
||||
useEffect(() => {
|
||||
window.onload = () => {
|
||||
const timing = performance.timing;
|
||||
@@ -244,7 +289,7 @@ export default function () {
|
||||
|
||||
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
|
||||
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
|
||||
history.push('/dashboard');
|
||||
history.push(qingLong3Capabilities() ? '/crontab' : '/dashboard');
|
||||
}
|
||||
|
||||
if (systemInfo || location.pathname === '/error') {
|
||||
@@ -282,6 +327,17 @@ export default function () {
|
||||
},
|
||||
],
|
||||
};
|
||||
const layoutProps = isQingLong3PanelSession()
|
||||
? {
|
||||
...defaultProps,
|
||||
route: {
|
||||
...defaultProps.route,
|
||||
routes: defaultProps.route?.routes?.filter((item) =>
|
||||
['/login', '/error', '/crontab'].includes(item.path || ''),
|
||||
),
|
||||
},
|
||||
}
|
||||
: defaultProps;
|
||||
return loading ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
@@ -393,7 +449,7 @@ export default function () {
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{...defaultProps}
|
||||
{...layoutProps}
|
||||
>
|
||||
<Outlet
|
||||
context={{
|
||||
|
||||
+146
-81
@@ -4,6 +4,10 @@ import { getCommandScript, getCrontabsNextDate } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { request } from '@/utils/http';
|
||||
import {
|
||||
isQingLong3PanelSession,
|
||||
qingLong3Capabilities,
|
||||
} from '@/utils/qinglong3';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CheckOutlined,
|
||||
@@ -66,6 +70,9 @@ const SHOW_TAB_COUNT = 10;
|
||||
|
||||
const Crontab = () => {
|
||||
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||
const qingLong3 = qingLong3Capabilities();
|
||||
const qingLong3ReadOnly = qingLong3 !== null;
|
||||
const qingLong3Authenticated = isQingLong3PanelSession();
|
||||
const [allSubscriptions, setAllSubscriptions] = useState<any[]>([]);
|
||||
const columns: ColumnProps<ICrontab>[] = [
|
||||
{
|
||||
@@ -79,16 +86,21 @@ const Crontab = () => {
|
||||
style={{
|
||||
wordBreak: 'break-all',
|
||||
marginBottom: 0,
|
||||
color: '#1890ff',
|
||||
cursor: 'pointer',
|
||||
color: qingLong3ReadOnly ? undefined : '#1890ff',
|
||||
cursor: qingLong3ReadOnly ? 'default' : 'pointer',
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
onClick={() => {
|
||||
if (qingLong3ReadOnly) return;
|
||||
setDetailCron(record);
|
||||
setIsDetailModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<Link>{record.name || '-'}</Link>
|
||||
{qingLong3ReadOnly ? (
|
||||
record.name || '-'
|
||||
) : (
|
||||
<Link>{record.name || '-'}</Link>
|
||||
)}
|
||||
</Paragraph>
|
||||
),
|
||||
sorter: {
|
||||
@@ -109,13 +121,17 @@ const Crontab = () => {
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
>
|
||||
<a
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
{qingLong3ReadOnly ? (
|
||||
text
|
||||
) : (
|
||||
<a
|
||||
onClick={() => {
|
||||
goToScriptManager(record);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
)}
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
@@ -248,8 +264,8 @@ const Crontab = () => {
|
||||
>
|
||||
{record.last_execution_time
|
||||
? dayjs(record.last_execution_time * 1000).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
);
|
||||
@@ -354,6 +370,19 @@ const Crontab = () => {
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
const tableScrollHeight = useTableScrollHeight(tableRef);
|
||||
const [activeKey, setActiveKey] = useState('');
|
||||
const visibleColumns = qingLong3ReadOnly
|
||||
? columns
|
||||
.filter((column) =>
|
||||
['name', 'command', 'status', 'schedule'].includes(
|
||||
String(column.key || ''),
|
||||
),
|
||||
)
|
||||
.map((column) => ({
|
||||
...column,
|
||||
sorter: undefined,
|
||||
filters: undefined,
|
||||
}))
|
||||
: columns;
|
||||
|
||||
const goToScriptManager = (record: any) => {
|
||||
const result = getCommandScript(record.command);
|
||||
@@ -366,12 +395,20 @@ const Crontab = () => {
|
||||
};
|
||||
|
||||
const getCrons = async (silent?: boolean) => {
|
||||
if (qingLong3ReadOnly && !qingLong3Authenticated) {
|
||||
if (!silent) setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!silent) setLoading(true);
|
||||
const { page = 1, size = 20, sorter, filters = {} } = pageConf;
|
||||
let url = `${config.apiPrefix
|
||||
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||
filters,
|
||||
)}`;
|
||||
const effectiveSize = qingLong3ReadOnly
|
||||
? Math.min(size, qingLong3?.limits.cronPageSize || 20)
|
||||
: size;
|
||||
const effectiveSearchText = qingLong3ReadOnly ? '' : searchText;
|
||||
const serializedFilters = qingLong3ReadOnly
|
||||
? encodeURIComponent('{}')
|
||||
: JSON.stringify(filters);
|
||||
let url = `${config.apiPrefix}crons?searchValue=${effectiveSearchText}&page=${page}&size=${effectiveSize}&filters=${serializedFilters}`;
|
||||
if (sorter && sorter.column && sorter.order) {
|
||||
url += `&sorter=${JSON.stringify({
|
||||
field: sorter.column.key,
|
||||
@@ -390,6 +427,11 @@ const Crontab = () => {
|
||||
.then(async ({ code, data: _data }) => {
|
||||
if (code === 200) {
|
||||
const { data, total } = _data;
|
||||
if (qingLong3ReadOnly) {
|
||||
setValue(data);
|
||||
setTotal(total);
|
||||
return;
|
||||
}
|
||||
const subscriptions = await request.get(
|
||||
`${config.apiPrefix}subscriptions?ids=${JSON.stringify([
|
||||
...new Set(data.map((x) => x.sub_id).filter(Boolean)),
|
||||
@@ -544,8 +586,9 @@ const Crontab = () => {
|
||||
|
||||
const enabledOrDisabledCron = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: `确认${record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
||||
}`,
|
||||
title: `确认${
|
||||
record.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
||||
}`,
|
||||
content: (
|
||||
<>
|
||||
{intl.get('确认')}
|
||||
@@ -560,7 +603,8 @@ const Crontab = () => {
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isDisabled === 1 ? 'enable' : 'disable'
|
||||
}`,
|
||||
[record.id],
|
||||
)
|
||||
@@ -584,8 +628,9 @@ const Crontab = () => {
|
||||
|
||||
const pinOrUnPinCron = (record: any, index: number) => {
|
||||
Modal.confirm({
|
||||
title: `确认${record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
||||
}`,
|
||||
title: `确认${
|
||||
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
||||
}`,
|
||||
content: (
|
||||
<>
|
||||
{intl.get('确认')}
|
||||
@@ -600,7 +645,8 @@ const Crontab = () => {
|
||||
onOk() {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
`${config.apiPrefix}crons/${
|
||||
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||
}`,
|
||||
[record.id],
|
||||
)
|
||||
@@ -777,8 +823,8 @@ const Crontab = () => {
|
||||
setPageConf({
|
||||
page: current as number,
|
||||
size: pageSize as number,
|
||||
sorter,
|
||||
filters,
|
||||
sorter: qingLong3ReadOnly ? {} : sorter,
|
||||
filters: qingLong3ReadOnly ? {} : filters,
|
||||
});
|
||||
localStorage.setItem('pageSize', String(pageSize));
|
||||
};
|
||||
@@ -824,10 +870,20 @@ const Crontab = () => {
|
||||
setAllSubscriptions(data || []);
|
||||
}
|
||||
})
|
||||
.catch(() => { });
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (qingLong3ReadOnly) {
|
||||
setPageConf({
|
||||
page: 1,
|
||||
size: Math.min(20, qingLong3?.limits.cronPageSize || 20),
|
||||
sorter: {},
|
||||
filters: {},
|
||||
});
|
||||
setActiveKey('all');
|
||||
return;
|
||||
}
|
||||
getCronViews();
|
||||
getAllSubscriptions();
|
||||
}, []);
|
||||
@@ -931,56 +987,63 @@ const Crontab = () => {
|
||||
<PageContainer
|
||||
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
|
||||
title={intl.get('定时任务')}
|
||||
extra={[
|
||||
<Search
|
||||
placeholder={intl.get('请输入名称或者关键词')}
|
||||
style={{ width: 'auto' }}
|
||||
enterButton
|
||||
allowClear
|
||||
loading={loading}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||
{intl.get('创建任务')}
|
||||
</Button>,
|
||||
]}
|
||||
extra={
|
||||
qingLong3ReadOnly
|
||||
? [<Tag key="ql3-read-only">QingLong 3.0 · 有界只读</Tag>]
|
||||
: [
|
||||
<Search
|
||||
key="search"
|
||||
placeholder={intl.get('请输入名称或者关键词')}
|
||||
style={{ width: 'auto' }}
|
||||
enterButton
|
||||
allowClear
|
||||
loading={loading}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onSearch={onSearch}
|
||||
/>,
|
||||
<Button key="create" type="primary" onClick={() => addCron()}>
|
||||
{intl.get('创建任务')}
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
header={{
|
||||
style: headerStyle,
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
defaultActiveKey="all"
|
||||
size="small"
|
||||
activeKey={activeKey}
|
||||
tabPosition="top"
|
||||
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
|
||||
tabBarExtraContent={
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={{ minWidth: 200 }}
|
||||
>
|
||||
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
|
||||
<Space>
|
||||
{intl.get('更多')}
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
}
|
||||
onTabClick={tabClick}
|
||||
items={[
|
||||
...[...enabledCronViews].slice(0, SHOW_TAB_COUNT).map((x) => ({
|
||||
key: x.id,
|
||||
label: x.name,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
{!qingLong3ReadOnly && (
|
||||
<Tabs
|
||||
defaultActiveKey="all"
|
||||
size="small"
|
||||
activeKey={activeKey}
|
||||
tabPosition="top"
|
||||
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
|
||||
tabBarExtraContent={
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={{ minWidth: 200 }}
|
||||
>
|
||||
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
|
||||
<Space>
|
||||
{intl.get('更多')}
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
}
|
||||
onTabClick={tabClick}
|
||||
items={[
|
||||
...[...enabledCronViews].slice(0, SHOW_TAB_COUNT).map((x) => ({
|
||||
key: x.id,
|
||||
label: x.name,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div ref={tableRef}>
|
||||
{selectedRowIds.length > 0 && (
|
||||
{!qingLong3ReadOnly && selectedRowIds.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -1042,7 +1105,7 @@ const Crontab = () => {
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
columns={columns}
|
||||
columns={visibleColumns}
|
||||
sortDirections={['descend', 'ascend']}
|
||||
pagination={{
|
||||
current: pageConf.page,
|
||||
@@ -1052,22 +1115,24 @@ const Crontab = () => {
|
||||
total,
|
||||
showTotal: (total: number, range: number[]) =>
|
||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
|
||||
(a, b) => a - b,
|
||||
),
|
||||
pageSizeOptions: qingLong3ReadOnly
|
||||
? [10, 20, 50, qingLong3?.limits.cronPageSize || 64]
|
||||
: [10, 20, 50, 100, 200, 500, total || 10000].sort(
|
||||
(a, b) => a - b,
|
||||
),
|
||||
}}
|
||||
dataSource={value}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
scroll={{ x: 1200, y: tableScrollHeight }}
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
rowSelection={qingLong3ReadOnly ? undefined : rowSelection}
|
||||
rowClassName={getRowClassName}
|
||||
onChange={onPageChange}
|
||||
components={isPhone || pageConf.size < 50 ? undefined : vt}
|
||||
/>
|
||||
</div>
|
||||
{isLogModalVisible && (
|
||||
{!qingLong3ReadOnly && isLogModalVisible && (
|
||||
<CronLogModal
|
||||
handleCancel={() => {
|
||||
getCronDetail(logCron);
|
||||
@@ -1076,10 +1141,10 @@ const Crontab = () => {
|
||||
cron={logCron}
|
||||
/>
|
||||
)}
|
||||
{isModalVisible && (
|
||||
{!qingLong3ReadOnly && isModalVisible && (
|
||||
<CronModal handleCancel={handleCancel} cron={editedCron} />
|
||||
)}
|
||||
{isLabelModalVisible && (
|
||||
{!qingLong3ReadOnly && isLabelModalVisible && (
|
||||
<CronLabelModal
|
||||
handleCancel={(needUpdate?: boolean) => {
|
||||
setIsLabelModalVisible(false);
|
||||
@@ -1090,7 +1155,7 @@ const Crontab = () => {
|
||||
ids={selectedRowIds}
|
||||
/>
|
||||
)}
|
||||
{isDetailModalVisible && (
|
||||
{!qingLong3ReadOnly && isDetailModalVisible && (
|
||||
<CronDetailModal
|
||||
handleCancel={(needUpdate?: boolean) => {
|
||||
setIsDetailModalVisible(false);
|
||||
@@ -1103,7 +1168,7 @@ const Crontab = () => {
|
||||
isPhone={isPhone}
|
||||
/>
|
||||
)}
|
||||
{isCreateViewModalVisible && (
|
||||
{!qingLong3ReadOnly && isCreateViewModalVisible && (
|
||||
<ViewCreateModal
|
||||
handleCancel={(data) => {
|
||||
setIsCreateViewModalVisible(false);
|
||||
@@ -1111,7 +1176,7 @@ const Crontab = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isViewManageModalVisible && (
|
||||
{!qingLong3ReadOnly && isViewManageModalVisible && (
|
||||
<ViewManageModal
|
||||
cronViews={cronViews}
|
||||
handleCancel={() => {
|
||||
|
||||
+84
-19
@@ -17,24 +17,59 @@ import { useTheme } from '@/utils/hooks';
|
||||
import { MobileOutlined } from '@ant-design/icons';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
clearQingLong3Credential,
|
||||
qingLong3Capabilities,
|
||||
qingLong3Credential,
|
||||
setQingLong3Credential,
|
||||
} from '@/utils/qinglong3';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { Countdown } = Statistic;
|
||||
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
|
||||
|
||||
const Login = () => {
|
||||
const { reloadUser } = useOutletContext<SharedContext>();
|
||||
const { reloadSystemConfig, reloadUser } = useOutletContext<SharedContext>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [waitTime, setWaitTime] = useState<any>();
|
||||
const { theme } = useTheme();
|
||||
const [twoFactor, setTwoFactor] = useState(false);
|
||||
const [verifying, setVerifying] = useState(false);
|
||||
const [loginInfo, setLoginInfo] = useState<any>();
|
||||
const qingLong3 = qingLong3Capabilities();
|
||||
|
||||
const handleOk = (values: any) => {
|
||||
setLoading(true);
|
||||
setTwoFactor(false);
|
||||
setWaitTime(null);
|
||||
if (qingLong3) {
|
||||
const token = String(values.credential || '').trim();
|
||||
if (!setQingLong3Credential(token)) {
|
||||
message.error('API Credential 格式无效');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
request
|
||||
.get(`${config.apiPrefix}user`)
|
||||
.then(({ code, data }: any) => {
|
||||
if (code !== 200 || !data?.username) {
|
||||
throw new TypeError('QL3 identity is unavailable');
|
||||
}
|
||||
notification.success({
|
||||
message: 'QingLong 3.0 已连接',
|
||||
description: `${data.username} · ${qingLong3.deployment.profile}`,
|
||||
});
|
||||
reloadSystemConfig();
|
||||
reloadUser(true);
|
||||
history.push('/crontab');
|
||||
})
|
||||
.catch(() => {
|
||||
clearQingLong3Credential();
|
||||
message.error('API Credential 验证失败');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
return;
|
||||
}
|
||||
request
|
||||
.post(`${config.apiPrefix}user/login`, {
|
||||
username: values.username,
|
||||
@@ -134,11 +169,13 @@ const Login = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const isAuth = localStorage.getItem(config.authKey);
|
||||
const isAuth = qingLong3
|
||||
? qingLong3Credential()
|
||||
: localStorage.getItem(config.authKey);
|
||||
if (isAuth) {
|
||||
history.push('/dashboard');
|
||||
history.push(qingLong3 ? '/crontab' : '/dashboard');
|
||||
}
|
||||
}, []);
|
||||
}, [qingLong3]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -150,7 +187,11 @@ const Login = () => {
|
||||
src="https://qn.whyour.cn/logo.png"
|
||||
/>
|
||||
<span className={styles.title}>
|
||||
{twoFactor ? intl.get('两步验证') : config.siteName}
|
||||
{twoFactor
|
||||
? intl.get('两步验证')
|
||||
: qingLong3
|
||||
? 'QingLong 3.0'
|
||||
: config.siteName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,20 +227,44 @@ const Login = () => {
|
||||
</Form>
|
||||
) : (
|
||||
<Form layout="vertical" onFinish={handleOk}>
|
||||
<FormItem name="username" label={intl.get('用户名')} hasFeedback>
|
||||
<Input
|
||||
placeholder={`${intl.get('用户名')}${
|
||||
isDemoEnv ? ': admin' : ''
|
||||
}`}
|
||||
autoFocus
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="password" label={intl.get('密码')} hasFeedback>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={`${intl.get('密码')}${isDemoEnv ? ': 123' : ''}`}
|
||||
/>
|
||||
</FormItem>
|
||||
{qingLong3 ? (
|
||||
<FormItem
|
||||
name="credential"
|
||||
label="API Credential"
|
||||
hasFeedback
|
||||
rules={[{ required: true, message: '请输入 API Credential' }]}
|
||||
extra="凭据只保存在当前页面内存;刷新或关闭页面后需要重新输入。"
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="ql3c_…"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
</FormItem>
|
||||
) : (
|
||||
<>
|
||||
<FormItem
|
||||
name="username"
|
||||
label={intl.get('用户名')}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder={`${intl.get('用户名')}${
|
||||
isDemoEnv ? ': admin' : ''
|
||||
}`}
|
||||
autoFocus
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="password" label={intl.get('密码')} hasFeedback>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={`${intl.get('密码')}${
|
||||
isDemoEnv ? ': 123' : ''
|
||||
}`}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
<Row>
|
||||
{waitTime ? (
|
||||
<Button type="primary" style={{ width: '100%' }} disabled>
|
||||
|
||||
+4
-1
@@ -9,6 +9,7 @@ import axios, {
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
import { clearQingLong3Credential, qingLong3Credential } from './qinglong3';
|
||||
|
||||
export interface IResponseData {
|
||||
code?: number;
|
||||
@@ -49,6 +50,7 @@ const errorHandler = function (
|
||||
} else if (responseStatus === 401) {
|
||||
if (history.location.pathname !== '/login') {
|
||||
message.error(intl.get('登录已过期,请重新登录'));
|
||||
clearQingLong3Credential();
|
||||
localStorage.removeItem(config.authKey);
|
||||
history.push('/login');
|
||||
}
|
||||
@@ -93,7 +95,7 @@ const apiWhiteList = [
|
||||
];
|
||||
|
||||
_request.interceptors.request.use((_config) => {
|
||||
const token = localStorage.getItem(config.authKey);
|
||||
const token = qingLong3Credential() || localStorage.getItem(config.authKey);
|
||||
if (token && !apiWhiteList.includes(_config.url!)) {
|
||||
_config.headers.Authorization = `Bearer ${token}`;
|
||||
return _config;
|
||||
@@ -107,6 +109,7 @@ _request.interceptors.response.use(async (response) => {
|
||||
history.push('/error');
|
||||
} else if (responseStatus === 401) {
|
||||
if (history.location.pathname !== '/login') {
|
||||
clearQingLong3Credential();
|
||||
localStorage.removeItem(config.authKey);
|
||||
history.push('/login');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
export interface QingLong3Capabilities {
|
||||
readonly schemaVersion: 1;
|
||||
readonly product: 'qinglong3';
|
||||
readonly version: string;
|
||||
readonly deployment: Readonly<{
|
||||
mode: 'local';
|
||||
profile: 'edge' | 'standalone';
|
||||
}>;
|
||||
readonly authentication: Readonly<{
|
||||
kind: 'api_credential';
|
||||
transport: 'bearer';
|
||||
persistence: 'memory_only';
|
||||
loginEndpoint: null;
|
||||
}>;
|
||||
readonly panel: Readonly<{
|
||||
bootstrap: true;
|
||||
cronList: 'bounded_read_only';
|
||||
legacyMutations: false;
|
||||
legacyLogin: false;
|
||||
subscriptions: false;
|
||||
scripts: false;
|
||||
environmentVariables: false;
|
||||
webSocket: false;
|
||||
}>;
|
||||
readonly limits: Readonly<{
|
||||
cronRows: number;
|
||||
cronPageSize: number;
|
||||
logChunkBytes: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const CREDENTIAL_PATTERN =
|
||||
/^ql3c_[A-Za-z0-9][A-Za-z0-9._:-]{0,63}_[A-Za-z0-9_-]{43}$/;
|
||||
|
||||
let capabilities: Readonly<QingLong3Capabilities> | null = null;
|
||||
let credential: string | null = null;
|
||||
|
||||
function validCapabilities(value: any): value is QingLong3Capabilities {
|
||||
const profile = value?.deployment?.profile;
|
||||
return Boolean(
|
||||
value?.schemaVersion === 1 &&
|
||||
value?.product === 'qinglong3' &&
|
||||
typeof value?.version === 'string' &&
|
||||
value.version.startsWith('3.') &&
|
||||
value?.deployment?.mode === 'local' &&
|
||||
(profile === 'edge' || profile === 'standalone') &&
|
||||
value?.authentication?.kind === 'api_credential' &&
|
||||
value?.authentication?.transport === 'bearer' &&
|
||||
value?.authentication?.persistence === 'memory_only' &&
|
||||
value?.authentication?.loginEndpoint === null &&
|
||||
value?.panel?.bootstrap === true &&
|
||||
value?.panel?.cronList === 'bounded_read_only' &&
|
||||
value?.panel?.legacyMutations === false &&
|
||||
value?.panel?.legacyLogin === false &&
|
||||
value?.panel?.subscriptions === false &&
|
||||
value?.panel?.scripts === false &&
|
||||
value?.panel?.environmentVariables === false &&
|
||||
value?.panel?.webSocket === false &&
|
||||
Number.isSafeInteger(value?.limits?.cronRows) &&
|
||||
value.limits.cronRows >= 1 &&
|
||||
value.limits.cronRows <= 256 &&
|
||||
Number.isSafeInteger(value?.limits?.cronPageSize) &&
|
||||
value.limits.cronPageSize >= 1 &&
|
||||
value.limits.cronPageSize <= 64 &&
|
||||
Number.isSafeInteger(value?.limits?.logChunkBytes) &&
|
||||
value.limits.logChunkBytes >= 1 &&
|
||||
value.limits.logChunkBytes <= 32 * 1024,
|
||||
);
|
||||
}
|
||||
|
||||
export async function discoverQingLong3(
|
||||
endpoint: string,
|
||||
): Promise<Readonly<QingLong3Capabilities> | null> {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
credentials: 'omit',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
headers: { accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const value = await response.json();
|
||||
if (!validCapabilities(value?.capabilities)) return null;
|
||||
capabilities = Object.freeze(value.capabilities);
|
||||
return capabilities;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function qingLong3Capabilities(): Readonly<QingLong3Capabilities> | null {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
export function setQingLong3Credential(value: string): boolean {
|
||||
if (!CREDENTIAL_PATTERN.test(value)) return false;
|
||||
credential = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function qingLong3Credential(): string | null {
|
||||
return credential;
|
||||
}
|
||||
|
||||
export function clearQingLong3Credential(): void {
|
||||
credential = null;
|
||||
}
|
||||
|
||||
export function isQingLong3PanelSession(): boolean {
|
||||
return capabilities !== null && credential !== null;
|
||||
}
|
||||
Reference in New Issue
Block a user