🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)
- Ran: bun run eslint:fix && bun run lint:fix - Inserted AGPL license header via eslint-plugin-header - Enforced no-multiple-empty-lines and other lint rules - Formatted code using Prettier v3 (@so1ve/prettier-config) - No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
@@ -17,7 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useIsMobile } from '../../hooks/common/useIsMobile';
|
||||
import {
|
||||
Modal,
|
||||
@@ -31,220 +36,241 @@ import {
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
|
||||
|
||||
const ChannelSelectorModal = forwardRef(({
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
allChannels,
|
||||
selectedChannelIds,
|
||||
setSelectedChannelIds,
|
||||
channelEndpoints,
|
||||
updateChannelEndpoint,
|
||||
t,
|
||||
}, ref) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetPagination: () => {
|
||||
setCurrentPage(1);
|
||||
setSearchText('');
|
||||
const ChannelSelectorModal = forwardRef(
|
||||
(
|
||||
{
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
allChannels,
|
||||
selectedChannelIds,
|
||||
setSelectedChannelIds,
|
||||
channelEndpoints,
|
||||
updateChannelEndpoint,
|
||||
t,
|
||||
},
|
||||
}));
|
||||
ref,
|
||||
) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (!allChannels) return;
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
|
||||
const searchLower = searchText.trim().toLowerCase();
|
||||
const matched = searchLower
|
||||
? allChannels.filter((item) => {
|
||||
const name = (item.label || '').toLowerCase();
|
||||
const baseUrl = (item._originalData?.base_url || '').toLowerCase();
|
||||
return name.includes(searchLower) || baseUrl.includes(searchLower);
|
||||
})
|
||||
: allChannels;
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetPagination: () => {
|
||||
setCurrentPage(1);
|
||||
setSearchText('');
|
||||
},
|
||||
}));
|
||||
|
||||
setFilteredData(matched);
|
||||
}, [allChannels, searchText]);
|
||||
useEffect(() => {
|
||||
if (!allChannels) return;
|
||||
|
||||
const total = filteredData.length;
|
||||
const searchLower = searchText.trim().toLowerCase();
|
||||
const matched = searchLower
|
||||
? allChannels.filter((item) => {
|
||||
const name = (item.label || '').toLowerCase();
|
||||
const baseUrl = (item._originalData?.base_url || '').toLowerCase();
|
||||
return name.includes(searchLower) || baseUrl.includes(searchLower);
|
||||
})
|
||||
: allChannels;
|
||||
|
||||
const paginatedData = filteredData.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize,
|
||||
);
|
||||
setFilteredData(matched);
|
||||
}, [allChannels, searchText]);
|
||||
|
||||
const updateEndpoint = (channelId, endpoint) => {
|
||||
if (typeof updateChannelEndpoint === 'function') {
|
||||
updateChannelEndpoint(channelId, endpoint);
|
||||
}
|
||||
};
|
||||
const total = filteredData.length;
|
||||
|
||||
const renderEndpointCell = (text, record) => {
|
||||
const channelId = record.key || record.value;
|
||||
const currentEndpoint = channelEndpoints[channelId] || '';
|
||||
const paginatedData = filteredData.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize,
|
||||
);
|
||||
|
||||
const getEndpointType = (ep) => {
|
||||
if (ep === '/api/ratio_config') return 'ratio_config';
|
||||
if (ep === '/api/pricing') return 'pricing';
|
||||
return 'custom';
|
||||
};
|
||||
|
||||
const currentType = getEndpointType(currentEndpoint);
|
||||
|
||||
const handleTypeChange = (val) => {
|
||||
if (val === 'ratio_config') {
|
||||
updateEndpoint(channelId, '/api/ratio_config');
|
||||
} else if (val === 'pricing') {
|
||||
updateEndpoint(channelId, '/api/pricing');
|
||||
} else {
|
||||
if (currentType !== 'custom') {
|
||||
updateEndpoint(channelId, '');
|
||||
}
|
||||
const updateEndpoint = (channelId, endpoint) => {
|
||||
if (typeof updateChannelEndpoint === 'function') {
|
||||
updateChannelEndpoint(channelId, endpoint);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Select
|
||||
size="small"
|
||||
value={currentType}
|
||||
onChange={handleTypeChange}
|
||||
style={{ width: 120 }}
|
||||
optionList={[
|
||||
{ label: 'ratio_config', value: 'ratio_config' },
|
||||
{ label: 'pricing', value: 'pricing' },
|
||||
{ label: 'custom', value: 'custom' },
|
||||
]}
|
||||
/>
|
||||
{currentType === 'custom' && (
|
||||
<Input
|
||||
size="small"
|
||||
value={currentEndpoint}
|
||||
onChange={(val) => updateEndpoint(channelId, val)}
|
||||
placeholder="/your/endpoint"
|
||||
style={{ width: 160, fontSize: 12 }}
|
||||
const renderEndpointCell = (text, record) => {
|
||||
const channelId = record.key || record.value;
|
||||
const currentEndpoint = channelEndpoints[channelId] || '';
|
||||
|
||||
const getEndpointType = (ep) => {
|
||||
if (ep === '/api/ratio_config') return 'ratio_config';
|
||||
if (ep === '/api/pricing') return 'pricing';
|
||||
return 'custom';
|
||||
};
|
||||
|
||||
const currentType = getEndpointType(currentEndpoint);
|
||||
|
||||
const handleTypeChange = (val) => {
|
||||
if (val === 'ratio_config') {
|
||||
updateEndpoint(channelId, '/api/ratio_config');
|
||||
} else if (val === 'pricing') {
|
||||
updateEndpoint(channelId, '/api/pricing');
|
||||
} else {
|
||||
if (currentType !== 'custom') {
|
||||
updateEndpoint(channelId, '');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Select
|
||||
size='small'
|
||||
value={currentType}
|
||||
onChange={handleTypeChange}
|
||||
style={{ width: 120 }}
|
||||
optionList={[
|
||||
{ label: 'ratio_config', value: 'ratio_config' },
|
||||
{ label: 'pricing', value: 'pricing' },
|
||||
{ label: 'custom', value: 'custom' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{currentType === 'custom' && (
|
||||
<Input
|
||||
size='small'
|
||||
value={currentEndpoint}
|
||||
onChange={(val) => updateEndpoint(channelId, val)}
|
||||
placeholder='/your/endpoint'
|
||||
style={{ width: 160, fontSize: 12 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatusCell = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag
|
||||
color='green'
|
||||
shape='circle'
|
||||
prefixIcon={<CheckCircle size={14} />}
|
||||
>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag
|
||||
color='yellow'
|
||||
shape='circle'
|
||||
prefixIcon={<AlertCircle size={14} />}
|
||||
>
|
||||
{t('自动禁用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag
|
||||
color='grey'
|
||||
shape='circle'
|
||||
prefixIcon={<HelpCircle size={14} />}
|
||||
>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderNameCell = (text) => (
|
||||
<Highlight sourceString={text} searchWords={[searchText]} />
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatusCell = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
||||
{t('自动禁用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
const renderBaseUrlCell = (text) => (
|
||||
<Highlight sourceString={text} searchWords={[searchText]} />
|
||||
);
|
||||
|
||||
const renderNameCell = (text) => (
|
||||
<Highlight sourceString={text} searchWords={[searchText]} />
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
title: t('名称'),
|
||||
dataIndex: 'label',
|
||||
render: renderNameCell,
|
||||
},
|
||||
{
|
||||
title: t('源地址'),
|
||||
dataIndex: '_originalData.base_url',
|
||||
render: (_, record) =>
|
||||
renderBaseUrlCell(record._originalData?.base_url || ''),
|
||||
},
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: '_originalData.status',
|
||||
render: (_, record) =>
|
||||
renderStatusCell(record._originalData?.status || 0),
|
||||
},
|
||||
{
|
||||
title: t('同步接口'),
|
||||
dataIndex: 'endpoint',
|
||||
fixed: 'right',
|
||||
render: renderEndpointCell,
|
||||
},
|
||||
];
|
||||
|
||||
const renderBaseUrlCell = (text) => (
|
||||
<Highlight sourceString={text} searchWords={[searchText]} />
|
||||
);
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedChannelIds,
|
||||
onChange: (keys) => setSelectedChannelIds(keys),
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('名称'),
|
||||
dataIndex: 'label',
|
||||
render: renderNameCell,
|
||||
},
|
||||
{
|
||||
title: t('源地址'),
|
||||
dataIndex: '_originalData.base_url',
|
||||
render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
|
||||
},
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: '_originalData.status',
|
||||
render: (_, record) => renderStatusCell(record._originalData?.status || 0),
|
||||
},
|
||||
{
|
||||
title: t('同步接口'),
|
||||
dataIndex: 'endpoint',
|
||||
fixed: 'right',
|
||||
render: renderEndpointCell,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
title={
|
||||
<span className='text-lg font-semibold'>{t('选择同步渠道')}</span>
|
||||
}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
keepDOM
|
||||
lazyRender={false}
|
||||
>
|
||||
<Space vertical style={{ width: '100%' }}>
|
||||
<Input
|
||||
prefix={<IconSearch size={14} />}
|
||||
placeholder={t('搜索渠道名称或地址')}
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
showClear
|
||||
/>
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedChannelIds,
|
||||
onChange: (keys) => setSelectedChannelIds(keys),
|
||||
};
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={paginatedData}
|
||||
rowKey='key'
|
||||
rowSelection={rowSelection}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
},
|
||||
onShowSizeChange: (curr, size) => {
|
||||
setCurrentPage(1);
|
||||
setPageSize(size);
|
||||
},
|
||||
}}
|
||||
size='small'
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
keepDOM
|
||||
lazyRender={false}
|
||||
>
|
||||
<Space vertical style={{ width: '100%' }}>
|
||||
<Input
|
||||
prefix={<IconSearch size={14} />}
|
||||
placeholder={t('搜索渠道名称或地址')}
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={paginatedData}
|
||||
rowKey="key"
|
||||
rowSelection={rowSelection}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
},
|
||||
onShowSizeChange: (curr, size) => {
|
||||
setCurrentPage(1);
|
||||
setPageSize(size);
|
||||
},
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ChannelSelectorModal;
|
||||
export default ChannelSelectorModal;
|
||||
|
||||
@@ -79,4 +79,4 @@ const ChatsSetting = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatsSetting;
|
||||
export default ChatsSetting;
|
||||
|
||||
@@ -62,8 +62,7 @@ const DashboardSetting = () => {
|
||||
if (item.key in inputs) {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
if (item.key.endsWith('Enabled') &&
|
||||
(item.key === 'DataExportEnabled')) {
|
||||
if (item.key.endsWith('Enabled') && item.key === 'DataExportEnabled') {
|
||||
newInputs[item.key] = toBoolean(item.value);
|
||||
}
|
||||
});
|
||||
@@ -91,8 +90,14 @@ const DashboardSetting = () => {
|
||||
|
||||
// 用于迁移检测的旧键,下个版本会删除
|
||||
const hasLegacyData = useMemo(() => {
|
||||
const legacyKeys = ['ApiInfo', 'Announcements', 'FAQ', 'UptimeKumaUrl', 'UptimeKumaSlug'];
|
||||
return legacyKeys.some(k => inputs[k]);
|
||||
const legacyKeys = [
|
||||
'ApiInfo',
|
||||
'Announcements',
|
||||
'FAQ',
|
||||
'UptimeKumaUrl',
|
||||
'UptimeKumaSlug',
|
||||
];
|
||||
return legacyKeys.some((k) => inputs[k]);
|
||||
}, [inputs]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -121,17 +126,18 @@ const DashboardSetting = () => {
|
||||
<Spin spinning={loading} size='large'>
|
||||
{/* 用于迁移检测的旧键模态框,下个版本会删除 */}
|
||||
<Modal
|
||||
title="配置迁移确认"
|
||||
title='配置迁移确认'
|
||||
visible={showMigrateModal}
|
||||
onOk={handleMigrate}
|
||||
onCancel={() => setShowMigrateModal(false)}
|
||||
confirmLoading={loading}
|
||||
okText="确认迁移"
|
||||
cancelText="取消"
|
||||
okText='确认迁移'
|
||||
cancelText='取消'
|
||||
>
|
||||
<p>检测到旧版本的配置数据,是否要迁移到新的配置格式?</p>
|
||||
<p style={{ color: '#f57c00', marginTop: '10px' }}>
|
||||
<strong>注意:</strong>迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
|
||||
<strong>注意:</strong>
|
||||
迁移过程中会自动处理数据格式转换,迁移完成后旧配置将被清除,请在迁移前在数据库中备份好旧配置。
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
@@ -164,4 +170,4 @@ const DashboardSetting = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSetting;
|
||||
export default DashboardSetting;
|
||||
|
||||
@@ -81,4 +81,4 @@ const DrawingSetting = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawingSetting;
|
||||
export default DrawingSetting;
|
||||
|
||||
@@ -56,7 +56,11 @@ const PaymentSetting = () => {
|
||||
switch (item.key) {
|
||||
case 'TopupGroupRatio':
|
||||
try {
|
||||
newInputs[item.key] = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
newInputs[item.key] = JSON.stringify(
|
||||
JSON.parse(item.value),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析TopupGroupRatio出错:', error);
|
||||
newInputs[item.key] = item.value;
|
||||
@@ -116,4 +120,4 @@ const PaymentSetting = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentSetting;
|
||||
export default PaymentSetting;
|
||||
|
||||
@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess
|
||||
} from '../../helpers';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -271,7 +265,11 @@ const PersonalSetting = () => {
|
||||
const handleNotificationSettingChange = (type, value) => {
|
||||
setNotificationSettings((prev) => ({
|
||||
...prev,
|
||||
[type]: value.target ? value.target.value !== undefined ? value.target.value : value.target.checked : value, // handle checkbox properly
|
||||
[type]: value.target
|
||||
? value.target.value !== undefined
|
||||
? value.target.value
|
||||
: value.target.checked
|
||||
: value, // handle checkbox properly
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -302,14 +300,14 @@ const PersonalSetting = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-[60px]">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-7xl mx-auto px-2">
|
||||
<div className='mt-[60px]'>
|
||||
<div className='flex justify-center'>
|
||||
<div className='w-full max-w-7xl mx-auto px-2'>
|
||||
{/* 顶部用户信息区域 */}
|
||||
<UserInfoHeader t={t} userState={userState} />
|
||||
|
||||
{/* 账户管理和其他设置 */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6">
|
||||
<div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
|
||||
{/* 左侧:账户管理设置 */}
|
||||
<AccountManagement
|
||||
t={t}
|
||||
|
||||
@@ -103,34 +103,19 @@ const RatioSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<Tabs type='card'>
|
||||
<Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
|
||||
<ModelRatioSettings
|
||||
options={inputs}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
<ModelRatioSettings options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('分组倍率设置')} itemKey='group'>
|
||||
<GroupRatioSettings
|
||||
options={inputs}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
<GroupRatioSettings options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey='visual'>
|
||||
<ModelSettingsVisualEditor
|
||||
options={inputs}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('未设置倍率模型')} itemKey='unset_models'>
|
||||
<ModelRatioNotSetEditor
|
||||
options={inputs}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
<ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
|
||||
<UpstreamRatioSync
|
||||
options={inputs}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
<UpstreamRatioSync options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
@@ -138,4 +123,4 @@ const RatioSetting = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RatioSetting;
|
||||
export default RatioSetting;
|
||||
|
||||
@@ -473,7 +473,10 @@ const SystemSetting = () => {
|
||||
value: inputs.LinuxDOClientSecret,
|
||||
});
|
||||
}
|
||||
if (originInputs['LinuxDOMinimumTrustLevel'] !== inputs.LinuxDOMinimumTrustLevel) {
|
||||
if (
|
||||
originInputs['LinuxDOMinimumTrustLevel'] !==
|
||||
inputs.LinuxDOMinimumTrustLevel
|
||||
) {
|
||||
options.push({
|
||||
key: 'LinuxDOMinimumTrustLevel',
|
||||
value: inputs.LinuxDOMinimumTrustLevel,
|
||||
@@ -530,11 +533,15 @@ const SystemSetting = () => {
|
||||
field='ServerAddress'
|
||||
label={t('服务器地址')}
|
||||
placeholder='https://yourdomain.com'
|
||||
extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
|
||||
extraText={t(
|
||||
'该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
|
||||
<Button onClick={submitServerAddress}>
|
||||
{t('更新服务器地址')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
@@ -755,7 +762,10 @@ const SystemSetting = () => {
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input field='SMTPServer' label={t('SMTP 服务器地址')} />
|
||||
<Form.Input
|
||||
field='SMTPServer'
|
||||
label={t('SMTP 服务器地址')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input field='SMTPPort' label={t('SMTP 端口')} />
|
||||
@@ -769,7 +779,10 @@ const SystemSetting = () => {
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input field='SMTPFrom' label={t('SMTP 发送者邮箱')} />
|
||||
<Form.Input
|
||||
field='SMTPFrom'
|
||||
label={t('SMTP 发送者邮箱')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
@@ -797,7 +810,9 @@ const SystemSetting = () => {
|
||||
<Card>
|
||||
<Form.Section text={t('配置 OIDC')}>
|
||||
<Text>
|
||||
{t('用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP')}
|
||||
{t(
|
||||
'用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP',
|
||||
)}
|
||||
</Text>
|
||||
<Banner
|
||||
type='info'
|
||||
@@ -805,7 +820,9 @@ const SystemSetting = () => {
|
||||
style={{ marginBottom: 20, marginTop: 16 }}
|
||||
/>
|
||||
<Text>
|
||||
{t('若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置')}
|
||||
{t(
|
||||
'若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置',
|
||||
)}
|
||||
</Text>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
@@ -862,7 +879,9 @@ const SystemSetting = () => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={submitOIDCSettings}>{t('保存 OIDC 设置')}</Button>
|
||||
<Button onClick={submitOIDCSettings}>
|
||||
{t('保存 OIDC 设置')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
@@ -1033,7 +1052,9 @@ const SystemSetting = () => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={submitTurnstile}>{t('保存 Turnstile 设置')}</Button>
|
||||
<Button onClick={submitTurnstile}>
|
||||
{t('保存 Turnstile 设置')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
@@ -1048,7 +1069,11 @@ const SystemSetting = () => {
|
||||
okText={t('确认')}
|
||||
cancelText={t('取消')}
|
||||
>
|
||||
<p>{t('您确定要取消密码登录功能吗?这可能会影响用户的登录方式。')}</p>
|
||||
<p>
|
||||
{t(
|
||||
'您确定要取消密码登录功能吗?这可能会影响用户的登录方式。',
|
||||
)}
|
||||
</p>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Avatar,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Popover
|
||||
Popover,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconMail,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
IconGithubLogo,
|
||||
IconKey,
|
||||
IconLock,
|
||||
IconDelete
|
||||
IconDelete,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
||||
import { UserPlus, ShieldCheck } from 'lucide-react';
|
||||
@@ -43,7 +43,7 @@ import TelegramLoginButton from 'react-telegram-login';
|
||||
import {
|
||||
onGitHubOAuthClicked,
|
||||
onOIDCClicked,
|
||||
onLinuxDOOAuthClicked
|
||||
onLinuxDOOAuthClicked,
|
||||
} from '../../../../helpers';
|
||||
import TwoFASetting from '../components/TwoFASetting';
|
||||
|
||||
@@ -57,77 +57,89 @@ const AccountManagement = ({
|
||||
generateAccessToken,
|
||||
handleSystemTokenClick,
|
||||
setShowChangePasswordModal,
|
||||
setShowAccountDeleteModal
|
||||
setShowAccountDeleteModal,
|
||||
}) => {
|
||||
const renderAccountInfo = (accountId, label) => {
|
||||
if (!accountId || accountId === '') {
|
||||
return <span className="text-gray-500">{t('未绑定')}</span>;
|
||||
return <span className='text-gray-500'>{t('未绑定')}</span>;
|
||||
}
|
||||
|
||||
const popContent = (
|
||||
<div className="text-xs p-2">
|
||||
<div className='text-xs p-2'>
|
||||
<Typography.Paragraph copyable={{ content: accountId }}>
|
||||
{accountId}
|
||||
</Typography.Paragraph>
|
||||
{label ? (
|
||||
<div className="mt-1 text-[11px] text-gray-500">{label}</div>
|
||||
<div className='mt-1 text-[11px] text-gray-500'>{label}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} position="top" trigger="hover">
|
||||
<span className="block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer">
|
||||
<Popover content={popContent} position='top' trigger='hover'>
|
||||
<span className='block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer'>
|
||||
{accountId}
|
||||
</span>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Card className="!rounded-2xl">
|
||||
<Card className='!rounded-2xl'>
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="teal" className="mr-3 shadow-md">
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='teal' className='mr-3 shadow-md'>
|
||||
<UserPlus size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">{t('账户管理')}</Typography.Text>
|
||||
<div className="text-xs text-gray-600">{t('账户绑定、安全设置和身份验证')}</div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('账户管理')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('账户绑定、安全设置和身份验证')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs type="card" defaultActiveKey="binding">
|
||||
<Tabs type='card' defaultActiveKey='binding'>
|
||||
{/* 账户绑定 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<UserPlus size={16} className="mr-2" />
|
||||
<div className='flex items-center'>
|
||||
<UserPlus size={16} className='mr-2' />
|
||||
{t('账户绑定')}
|
||||
</div>
|
||||
}
|
||||
itemKey="binding"
|
||||
itemKey='binding'
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className='py-4'>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
|
||||
{/* 邮箱绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center flex-1 min-w-0'>
|
||||
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||
<IconMail
|
||||
size='default'
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('邮箱')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{renderAccountInfo(userState.user?.email, t('邮箱地址'))}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-gray-900'>
|
||||
{t('邮箱')}
|
||||
</div>
|
||||
<div className='text-sm text-gray-500 truncate'>
|
||||
{renderAccountInfo(
|
||||
userState.user?.email,
|
||||
t('邮箱地址'),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className='flex-shrink-0'>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
type='primary'
|
||||
theme='outline'
|
||||
size='small'
|
||||
onClick={() => setShowEmailBindModal(true)}
|
||||
>
|
||||
{userState.user && userState.user.email !== ''
|
||||
@@ -139,26 +151,31 @@ const AccountManagement = ({
|
||||
</Card>
|
||||
|
||||
{/* 微信绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center flex-1 min-w-0'>
|
||||
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||
<SiWechat
|
||||
size={20}
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('微信')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-gray-900'>
|
||||
{t('微信')}
|
||||
</div>
|
||||
<div className='text-sm text-gray-500 truncate'>
|
||||
{userState.user && userState.user.wechat_id !== ''
|
||||
? t('已绑定')
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className='flex-shrink-0'>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
type='primary'
|
||||
theme='outline'
|
||||
size='small'
|
||||
disabled={!status.wechat_login}
|
||||
onClick={() => setShowWeChatBindModal(true)}
|
||||
>
|
||||
@@ -173,25 +190,35 @@ const AccountManagement = ({
|
||||
</Card>
|
||||
|
||||
{/* GitHub绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center flex-1 min-w-0'>
|
||||
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||
<IconGithubLogo
|
||||
size='default'
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('GitHub')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{renderAccountInfo(userState.user?.github_id, t('GitHub ID'))}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-gray-900'>
|
||||
{t('GitHub')}
|
||||
</div>
|
||||
<div className='text-sm text-gray-500 truncate'>
|
||||
{renderAccountInfo(
|
||||
userState.user?.github_id,
|
||||
t('GitHub ID'),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className='flex-shrink-0'>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
|
||||
type='primary'
|
||||
theme='outline'
|
||||
size='small'
|
||||
onClick={() =>
|
||||
onGitHubOAuthClicked(status.github_client_id)
|
||||
}
|
||||
disabled={
|
||||
(userState.user && userState.user.github_id !== '') ||
|
||||
!status.github_oauth
|
||||
@@ -204,28 +231,38 @@ const AccountManagement = ({
|
||||
</Card>
|
||||
|
||||
{/* OIDC绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center flex-1 min-w-0'>
|
||||
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||
<IconShield
|
||||
size='default'
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('OIDC')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{renderAccountInfo(userState.user?.oidc_id, t('OIDC ID'))}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-gray-900'>
|
||||
{t('OIDC')}
|
||||
</div>
|
||||
<div className='text-sm text-gray-500 truncate'>
|
||||
{renderAccountInfo(
|
||||
userState.user?.oidc_id,
|
||||
t('OIDC ID'),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className='flex-shrink-0'>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => onOIDCClicked(
|
||||
status.oidc_authorization_endpoint,
|
||||
status.oidc_client_id,
|
||||
)}
|
||||
type='primary'
|
||||
theme='outline'
|
||||
size='small'
|
||||
onClick={() =>
|
||||
onOIDCClicked(
|
||||
status.oidc_authorization_endpoint,
|
||||
status.oidc_client_id,
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
(userState.user && userState.user.oidc_id !== '') ||
|
||||
!status.oidc_enabled
|
||||
@@ -238,27 +275,35 @@ const AccountManagement = ({
|
||||
</Card>
|
||||
|
||||
{/* Telegram绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center flex-1 min-w-0'>
|
||||
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||
<SiTelegram
|
||||
size={20}
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('Telegram')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{renderAccountInfo(userState.user?.telegram_id, t('Telegram ID'))}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-gray-900'>
|
||||
{t('Telegram')}
|
||||
</div>
|
||||
<div className='text-sm text-gray-500 truncate'>
|
||||
{renderAccountInfo(
|
||||
userState.user?.telegram_id,
|
||||
t('Telegram ID'),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className='flex-shrink-0'>
|
||||
{status.telegram_oauth ? (
|
||||
userState.user.telegram_id !== '' ? (
|
||||
<Button disabled={true} size="small">
|
||||
<Button disabled={true} size='small'>
|
||||
{t('已绑定')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="scale-75">
|
||||
<div className='scale-75'>
|
||||
<TelegramLoginButton
|
||||
dataAuthUrl='/api/oauth/telegram/bind'
|
||||
botName={status.telegram_bot_name}
|
||||
@@ -266,7 +311,7 @@ const AccountManagement = ({
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Button disabled={true} size="small">
|
||||
<Button disabled={true} size='small'>
|
||||
{t('未启用')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -275,25 +320,35 @@ const AccountManagement = ({
|
||||
</Card>
|
||||
|
||||
{/* LinuxDO绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center flex-1 min-w-0'>
|
||||
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||
<SiLinux
|
||||
size={20}
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{renderAccountInfo(userState.user?.linux_do_id, t('LinuxDO ID'))}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-gray-900'>
|
||||
{t('LinuxDO')}
|
||||
</div>
|
||||
<div className='text-sm text-gray-500 truncate'>
|
||||
{renderAccountInfo(
|
||||
userState.user?.linux_do_id,
|
||||
t('LinuxDO ID'),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className='flex-shrink-0'>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
|
||||
type='primary'
|
||||
theme='outline'
|
||||
size='small'
|
||||
onClick={() =>
|
||||
onLinuxDOOAuthClicked(status.linuxdo_client_id)
|
||||
}
|
||||
disabled={
|
||||
(userState.user && userState.user.linux_do_id !== '') ||
|
||||
!status.linuxdo_oauth
|
||||
@@ -311,37 +366,37 @@ const AccountManagement = ({
|
||||
{/* 安全设置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<ShieldCheck size={16} className="mr-2" />
|
||||
<div className='flex items-center'>
|
||||
<ShieldCheck size={16} className='mr-2' />
|
||||
{t('安全设置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="security"
|
||||
itemKey='security'
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className="space-y-6">
|
||||
<div className='py-4'>
|
||||
<div className='space-y-6'>
|
||||
<Space vertical className='w-full'>
|
||||
{/* 系统访问令牌 */}
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconKey size="large" className="text-slate-600" />
|
||||
<Card className='!rounded-xl w-full'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
|
||||
<IconKey size='large' className='text-slate-600' />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
<div className='flex-1'>
|
||||
<Typography.Title heading={6} className='mb-1'>
|
||||
{t('系统访问令牌')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t('用于API调用的身份验证令牌,请妥善保管')}
|
||||
</Typography.Text>
|
||||
{systemToken && (
|
||||
<div className="mt-3">
|
||||
<div className='mt-3'>
|
||||
<Input
|
||||
readonly
|
||||
value={systemToken}
|
||||
onClick={handleSystemTokenClick}
|
||||
size="large"
|
||||
size='large'
|
||||
prefix={<IconKey />}
|
||||
/>
|
||||
</div>
|
||||
@@ -349,10 +404,10 @@ const AccountManagement = ({
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
onClick={generateAccessToken}
|
||||
className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||
className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
|
||||
icon={<IconKey />}
|
||||
>
|
||||
{systemToken ? t('重新生成') : t('生成令牌')}
|
||||
@@ -361,26 +416,26 @@ const AccountManagement = ({
|
||||
</Card>
|
||||
|
||||
{/* 密码管理 */}
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconLock size="large" className="text-slate-600" />
|
||||
<Card className='!rounded-xl w-full'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
|
||||
<IconLock size='large' className='text-slate-600' />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
<Typography.Title heading={6} className='mb-1'>
|
||||
{t('密码管理')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t('定期更改密码可以提高账户安全性')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
onClick={() => setShowChangePasswordModal(true)}
|
||||
className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||
className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
|
||||
icon={<IconLock />}
|
||||
>
|
||||
{t('修改密码')}
|
||||
@@ -392,26 +447,29 @@ const AccountManagement = ({
|
||||
<TwoFASetting t={t} />
|
||||
|
||||
{/* 危险区域 */}
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconDelete size="large" className="text-slate-600" />
|
||||
<Card className='!rounded-xl w-full'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<div className='w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0'>
|
||||
<IconDelete size='large' className='text-slate-600' />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1 text-slate-700">
|
||||
<Typography.Title
|
||||
heading={6}
|
||||
className='mb-1 text-slate-700'
|
||||
>
|
||||
{t('删除账户')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t('此操作不可逆,所有数据将被永久删除')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="danger"
|
||||
theme="solid"
|
||||
type='danger'
|
||||
theme='solid'
|
||||
onClick={() => setShowAccountDeleteModal(true)}
|
||||
className="w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
|
||||
className='w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600'
|
||||
icon={<IconDelete />}
|
||||
>
|
||||
{t('删除账户')}
|
||||
|
||||
@@ -27,13 +27,13 @@ import {
|
||||
Tabs,
|
||||
TabPane,
|
||||
Typography,
|
||||
Avatar
|
||||
Avatar,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp
|
||||
} from '@douyinfe/semi-icons';
|
||||
IllustrationNoContent,
|
||||
IllustrationNoContentDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { renderModelTag, getModelCategories } from '../../../../helpers';
|
||||
|
||||
@@ -52,38 +52,48 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
}, [isModelsExpanded]);
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<div className='py-4'>
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="green" className="mr-3 shadow-md">
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='green' className='mr-3 shadow-md'>
|
||||
<Settings size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">{t('可用模型')}</Typography.Text>
|
||||
<div className="text-xs text-gray-600">{t('查看当前可用的所有模型')}</div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('可用模型')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('查看当前可用的所有模型')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可用模型部分 */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className='bg-gray-50 dark:bg-gray-800 rounded-xl'>
|
||||
{modelsLoading ? (
|
||||
// 骨架屏加载状态 - 模拟实际加载后的布局
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
{/* 模拟分类标签 */}
|
||||
<div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
|
||||
<div className="flex overflow-x-auto py-2 gap-2">
|
||||
<div
|
||||
className='mb-4'
|
||||
style={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
>
|
||||
<div className='flex overflow-x-auto py-2 gap-2'>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<Skeleton.Button key={`cat-${index}`} style={{
|
||||
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
||||
height: 36,
|
||||
borderRadius: 8
|
||||
}} />
|
||||
<Skeleton.Button
|
||||
key={`cat-${index}`}
|
||||
style={{
|
||||
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
||||
height: 36,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模拟模型标签列表 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{Array.from({ length: 20 }).map((_, index) => (
|
||||
<Skeleton.Button
|
||||
key={`model-${index}`}
|
||||
@@ -91,17 +101,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
width: 100 + Math.random() * 100,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
margin: '4px'
|
||||
margin: '4px',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : models.length === 0 ? (
|
||||
<div className="py-8">
|
||||
<div className='py-8'>
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
||||
image={
|
||||
<IllustrationNoContent style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoContentDark
|
||||
style={{ width: 150, height: 150 }}
|
||||
/>
|
||||
}
|
||||
description={t('没有可用模型')}
|
||||
style={{ padding: '24px 0' }}
|
||||
/>
|
||||
@@ -109,59 +125,81 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
) : (
|
||||
<>
|
||||
{/* 模型分类标签页 */}
|
||||
<div className="mb-4">
|
||||
<div className='mb-4'>
|
||||
<Tabs
|
||||
type="card"
|
||||
type='card'
|
||||
activeKey={activeModelCategory}
|
||||
onChange={key => setActiveModelCategory(key)}
|
||||
className="mt-2"
|
||||
onChange={(key) => setActiveModelCategory(key)}
|
||||
className='mt-2'
|
||||
collapsible
|
||||
>
|
||||
{Object.entries(getModelCategories(t)).map(([key, category]) => {
|
||||
// 计算该分类下的模型数量
|
||||
const modelCount = key === 'all'
|
||||
? models.length
|
||||
: models.filter(model => category.filter({ model_name: model })).length;
|
||||
{Object.entries(getModelCategories(t)).map(
|
||||
([key, category]) => {
|
||||
// 计算该分类下的模型数量
|
||||
const modelCount =
|
||||
key === 'all'
|
||||
? models.length
|
||||
: models.filter((model) =>
|
||||
category.filter({ model_name: model }),
|
||||
).length;
|
||||
|
||||
if (modelCount === 0 && key !== 'all') return null;
|
||||
if (modelCount === 0 && key !== 'all') return null;
|
||||
|
||||
return (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className="flex items-center gap-2">
|
||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
||||
{category.label}
|
||||
<Tag
|
||||
color={activeModelCategory === key ? 'red' : 'grey'}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className='flex items-center gap-2'>
|
||||
{category.icon && (
|
||||
<span className='w-4 h-4'>{category.icon}</span>
|
||||
)}
|
||||
{category.label}
|
||||
<Tag
|
||||
color={
|
||||
activeModelCategory === key ? 'red' : 'grey'
|
||||
}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-700 rounded-lg p-3">
|
||||
<div className='bg-white dark:bg-gray-700 rounded-lg p-3'>
|
||||
{(() => {
|
||||
// 根据当前选中的分类过滤模型
|
||||
const categories = getModelCategories(t);
|
||||
const filteredModels = activeModelCategory === 'all'
|
||||
? models
|
||||
: models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
|
||||
const filteredModels =
|
||||
activeModelCategory === 'all'
|
||||
? models
|
||||
: models.filter((model) =>
|
||||
categories[activeModelCategory].filter({
|
||||
model_name: model,
|
||||
}),
|
||||
);
|
||||
|
||||
// 如果过滤后没有模型,显示空状态
|
||||
if (filteredModels.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
|
||||
image={
|
||||
<IllustrationNoContent
|
||||
style={{ width: 120, height: 120 }}
|
||||
/>
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoContentDark
|
||||
style={{ width: 120, height: 120 }}
|
||||
/>
|
||||
}
|
||||
description={t('该分类下没有可用模型')}
|
||||
style={{ padding: '16px 0' }}
|
||||
/>
|
||||
@@ -171,13 +209,13 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
|
||||
return (
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) => (
|
||||
{filteredModels.map((model) =>
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
}),
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
} else {
|
||||
@@ -185,17 +223,17 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
<>
|
||||
<Collapsible isOpen={isModelsExpanded}>
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) => (
|
||||
{filteredModels.map((model) =>
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
}),
|
||||
)}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
className='cursor-pointer !rounded-lg'
|
||||
onClick={() => setIsModelsExpanded(false)}
|
||||
icon={<IconChevronUp />}
|
||||
>
|
||||
@@ -207,21 +245,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
<Space wrap>
|
||||
{filteredModels
|
||||
.slice(0, MODELS_DISPLAY_COUNT)
|
||||
.map((model) => (
|
||||
.map((model) =>
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
}),
|
||||
)}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
className='cursor-pointer !rounded-lg'
|
||||
onClick={() => setIsModelsExpanded(true)}
|
||||
icon={<IconChevronDown />}
|
||||
>
|
||||
{t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
||||
{t('更多')}{' '}
|
||||
{filteredModels.length - MODELS_DISPLAY_COUNT}{' '}
|
||||
{t('个模型')}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@@ -27,14 +27,9 @@ import {
|
||||
Radio,
|
||||
Toast,
|
||||
Tabs,
|
||||
TabPane
|
||||
TabPane,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconMail,
|
||||
IconKey,
|
||||
IconBell,
|
||||
IconLink
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
|
||||
import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
|
||||
import { renderQuotaWithPrompt } from '../../../../helpers';
|
||||
import CodeViewer from '../../../playground/CodeViewer';
|
||||
@@ -43,7 +38,7 @@ const NotificationSettings = ({
|
||||
t,
|
||||
notificationSettings,
|
||||
handleNotificationSettingChange,
|
||||
saveNotificationSettings
|
||||
saveNotificationSettings,
|
||||
}) => {
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
@@ -62,7 +57,8 @@ const NotificationSettings = ({
|
||||
// 表单提交
|
||||
const handleSubmit = () => {
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.validate()
|
||||
formApiRef.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
saveNotificationSettings();
|
||||
})
|
||||
@@ -77,26 +73,27 @@ const NotificationSettings = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl shadow-sm border-0"
|
||||
className='!rounded-2xl shadow-sm border-0'
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<div className='flex justify-end'>
|
||||
<Button type='primary' onClick={handleSubmit}>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="blue" className="mr-3 shadow-md">
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
|
||||
<Bell size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">{t('其他设置')}</Typography.Text>
|
||||
<div className="text-xs text-gray-600">{t('通知、价格和隐私相关设置')}</div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('其他设置')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('通知、价格和隐私相关设置')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,18 +103,18 @@ const NotificationSettings = ({
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Tabs type="card" defaultActiveKey="notification">
|
||||
<Tabs type='card' defaultActiveKey='notification'>
|
||||
{/* 通知配置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<Bell size={16} className="mr-2" />
|
||||
<div className='flex items-center'>
|
||||
<Bell size={16} className='mr-2' />
|
||||
{t('通知配置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="notification"
|
||||
itemKey='notification'
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className='py-4'>
|
||||
<Form.RadioGroup
|
||||
field='warningType'
|
||||
label={t('通知方式')}
|
||||
@@ -125,15 +122,18 @@ const NotificationSettings = ({
|
||||
onChange={(value) => handleFormChange('warningType', value)}
|
||||
rules={[{ required: true, message: t('请选择通知方式') }]}
|
||||
>
|
||||
<Radio value="email">{t('邮件通知')}</Radio>
|
||||
<Radio value="webhook">{t('Webhook通知')}</Radio>
|
||||
<Radio value='email'>{t('邮件通知')}</Radio>
|
||||
<Radio value='webhook'>{t('Webhook通知')}</Radio>
|
||||
</Form.RadioGroup>
|
||||
|
||||
<Form.AutoComplete
|
||||
field='warningThreshold'
|
||||
label={
|
||||
<span>
|
||||
{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
|
||||
{t('额度预警阈值')}{' '}
|
||||
{renderQuotaWithPrompt(
|
||||
notificationSettings.warningThreshold,
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
placeholder={t('请输入预警额度')}
|
||||
@@ -145,7 +145,9 @@ const NotificationSettings = ({
|
||||
]}
|
||||
onChange={(val) => handleFormChange('warningThreshold', val)}
|
||||
prefix={<IconBell />}
|
||||
extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
|
||||
extraText={t(
|
||||
'当剩余额度低于此数值时,系统将通过选择的方式发送通知',
|
||||
)}
|
||||
style={{ width: '100%', maxWidth: '300px' }}
|
||||
rules={[
|
||||
{ required: true, message: t('请输入预警阈值') },
|
||||
@@ -156,8 +158,8 @@ const NotificationSettings = ({
|
||||
return Promise.reject(t('预警阈值必须为正数'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -167,9 +169,13 @@ const NotificationSettings = ({
|
||||
field='notificationEmail'
|
||||
label={t('通知邮箱')}
|
||||
placeholder={t('留空则使用账号绑定的邮箱')}
|
||||
onChange={(val) => handleFormChange('notificationEmail', val)}
|
||||
onChange={(val) =>
|
||||
handleFormChange('notificationEmail', val)
|
||||
}
|
||||
prefix={<IconMail />}
|
||||
extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
|
||||
extraText={t(
|
||||
'设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
|
||||
)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
@@ -180,20 +186,25 @@ const NotificationSettings = ({
|
||||
<Form.Input
|
||||
field='webhookUrl'
|
||||
label={t('Webhook地址')}
|
||||
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
|
||||
placeholder={t(
|
||||
'请输入Webhook地址,例如: https://example.com/webhook',
|
||||
)}
|
||||
onChange={(val) => handleFormChange('webhookUrl', val)}
|
||||
prefix={<IconLink />}
|
||||
extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
|
||||
extraText={t(
|
||||
'只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
|
||||
)}
|
||||
showClear
|
||||
rules={[
|
||||
{
|
||||
required: notificationSettings.warningType === 'webhook',
|
||||
message: t('请输入Webhook地址')
|
||||
required:
|
||||
notificationSettings.warningType === 'webhook',
|
||||
message: t('请输入Webhook地址'),
|
||||
},
|
||||
{
|
||||
pattern: /^https:\/\/.+/,
|
||||
message: t('Webhook地址必须以https://开头')
|
||||
}
|
||||
message: t('Webhook地址必须以https://开头'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -203,7 +214,9 @@ const NotificationSettings = ({
|
||||
placeholder={t('请输入密钥')}
|
||||
onChange={(val) => handleFormChange('webhookSecret', val)}
|
||||
prefix={<IconKey />}
|
||||
extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
|
||||
extraText={t(
|
||||
'密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
|
||||
)}
|
||||
showClear
|
||||
/>
|
||||
|
||||
@@ -212,22 +225,36 @@ const NotificationSettings = ({
|
||||
<div style={{ height: '200px', marginBottom: '12px' }}>
|
||||
<CodeViewer
|
||||
content={{
|
||||
"type": "quota_exceed",
|
||||
"title": "额度预警通知",
|
||||
"content": "您的额度即将用尽,当前剩余额度为 {{value}}",
|
||||
"values": ["$0.99"],
|
||||
"timestamp": 1739950503
|
||||
type: 'quota_exceed',
|
||||
title: '额度预警通知',
|
||||
content:
|
||||
'您的额度即将用尽,当前剩余额度为 {{value}}',
|
||||
values: ['$0.99'],
|
||||
timestamp: 1739950503,
|
||||
}}
|
||||
title="webhook"
|
||||
language="json"
|
||||
title='webhook'
|
||||
language='json'
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 leading-relaxed">
|
||||
<div><strong>type:</strong> {t('通知类型 (quota_exceed: 额度预警)')} </div>
|
||||
<div><strong>title:</strong> {t('通知标题')}</div>
|
||||
<div><strong>content:</strong> {t('通知内容,支持 {{value}} 变量占位符')}</div>
|
||||
<div><strong>values:</strong> {t('按顺序替换content中的变量占位符')}</div>
|
||||
<div><strong>timestamp:</strong> {t('Unix时间戳')}</div>
|
||||
<div className='text-xs text-gray-500 leading-relaxed'>
|
||||
<div>
|
||||
<strong>type:</strong>{' '}
|
||||
{t('通知类型 (quota_exceed: 额度预警)')}{' '}
|
||||
</div>
|
||||
<div>
|
||||
<strong>title:</strong> {t('通知标题')}
|
||||
</div>
|
||||
<div>
|
||||
<strong>content:</strong>{' '}
|
||||
{t('通知内容,支持 {{value}} 变量占位符')}
|
||||
</div>
|
||||
<div>
|
||||
<strong>values:</strong>{' '}
|
||||
{t('按顺序替换content中的变量占位符')}
|
||||
</div>
|
||||
<div>
|
||||
<strong>timestamp:</strong> {t('Unix时间戳')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Slot>
|
||||
@@ -239,21 +266,25 @@ const NotificationSettings = ({
|
||||
{/* 价格设置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<DollarSign size={16} className="mr-2" />
|
||||
<div className='flex items-center'>
|
||||
<DollarSign size={16} className='mr-2' />
|
||||
{t('价格设置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="pricing"
|
||||
itemKey='pricing'
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className='py-4'>
|
||||
<Form.Switch
|
||||
field='acceptUnsetModelRatioModel'
|
||||
label={t('接受未设置价格模型')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
|
||||
extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
|
||||
onChange={(value) =>
|
||||
handleFormChange('acceptUnsetModelRatioModel', value)
|
||||
}
|
||||
extraText={t(
|
||||
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
@@ -261,21 +292,23 @@ const NotificationSettings = ({
|
||||
{/* 隐私设置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<ShieldCheck size={16} className="mr-2" />
|
||||
<div className='flex items-center'>
|
||||
<ShieldCheck size={16} className='mr-2' />
|
||||
{t('隐私设置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="privacy"
|
||||
itemKey='privacy'
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className='py-4'>
|
||||
<Form.Switch
|
||||
field='recordIpLog'
|
||||
label={t('记录请求与错误日志IP')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) => handleFormChange('recordIpLog', value)}
|
||||
extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
|
||||
extraText={t(
|
||||
'开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
@@ -17,12 +17,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { API, showError, showSuccess, showWarning } from '../../../../helpers';
|
||||
import { Banner, Button, Card, Checkbox, Divider, Input, Modal, Tag, Typography, Steps, Space, Badge } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Input,
|
||||
Modal,
|
||||
Tag,
|
||||
Typography,
|
||||
Steps,
|
||||
Space,
|
||||
Badge,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconShield,
|
||||
IconAlertTriangle,
|
||||
IconRefresh,
|
||||
IconCopy
|
||||
IconCopy,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
@@ -35,7 +48,7 @@ const TwoFASetting = ({ t }) => {
|
||||
const [status, setStatus] = useState({
|
||||
enabled: false,
|
||||
locked: false,
|
||||
backup_codes_remaining: 0
|
||||
backup_codes_remaining: 0,
|
||||
});
|
||||
|
||||
// 模态框状态
|
||||
@@ -96,7 +109,7 @@ const TwoFASetting = ({ t }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/enable', {
|
||||
code: verificationCode
|
||||
code: verificationCode,
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('两步验证启用成功!'));
|
||||
@@ -130,7 +143,7 @@ const TwoFASetting = ({ t }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/disable', {
|
||||
code: verificationCode
|
||||
code: verificationCode,
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('两步验证已禁用'));
|
||||
@@ -158,7 +171,7 @@ const TwoFASetting = ({ t }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/backup_codes', {
|
||||
code: verificationCode
|
||||
code: verificationCode,
|
||||
});
|
||||
if (res.data.success) {
|
||||
setBackupCodes(res.data.data.backup_codes);
|
||||
@@ -177,11 +190,14 @@ const TwoFASetting = ({ t }) => {
|
||||
|
||||
// 通用复制函数
|
||||
const copyTextToClipboard = (text, successMessage = t('已复制到剪贴板')) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showSuccess(successMessage);
|
||||
}).catch(() => {
|
||||
showError(t('复制失败,请手动复制'));
|
||||
});
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
showSuccess(successMessage);
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('复制失败,请手动复制'));
|
||||
});
|
||||
};
|
||||
|
||||
const copyBackupCodes = () => {
|
||||
@@ -192,28 +208,25 @@ const TwoFASetting = ({ t }) => {
|
||||
// 备用码展示组件
|
||||
const BackupCodesDisplay = ({ codes, title, onCopy }) => {
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-xl"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text strong className="text-slate-700 dark:text-slate-200">
|
||||
<Card className='!rounded-xl' style={{ width: '100%' }}>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Text strong className='text-slate-700 dark:text-slate-200'>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-2'>
|
||||
{codes.map((code, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
||||
<div key={index} className='rounded-lg p-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Text
|
||||
code
|
||||
className='text-sm font-mono text-slate-700 dark:text-slate-200'
|
||||
>
|
||||
{code}
|
||||
</Text>
|
||||
<Text type="quaternary" className="text-xs">
|
||||
<Text type='quaternary' className='text-xs'>
|
||||
#{(index + 1).toString().padStart(2, '0')}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -223,11 +236,11 @@ const TwoFASetting = ({ t }) => {
|
||||
|
||||
<Divider margin={12} />
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
icon={<IconCopy />}
|
||||
onClick={onCopy}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full"
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full'
|
||||
>
|
||||
{t('复制所有代码')}
|
||||
</Button>
|
||||
@@ -243,24 +256,24 @@ const TwoFASetting = ({ t }) => {
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
>
|
||||
{t('上一步')}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < 2 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
onClick={() => setCurrentStep(currentStep + 1)}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||
>
|
||||
{t('下一步')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
if (!verificationCode) {
|
||||
@@ -269,7 +282,7 @@ const TwoFASetting = ({ t }) => {
|
||||
}
|
||||
handleEnable2FA();
|
||||
}}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||
>
|
||||
{t('完成设置并启用两步验证')}
|
||||
</Button>
|
||||
@@ -288,17 +301,17 @@ const TwoFASetting = ({ t }) => {
|
||||
setVerificationCode('');
|
||||
setConfirmDisable(false);
|
||||
}}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
theme="solid"
|
||||
type='danger'
|
||||
theme='solid'
|
||||
loading={loading}
|
||||
disabled={!confirmDisable || !verificationCode}
|
||||
onClick={handleDisable2FA}
|
||||
className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
|
||||
className='!rounded-lg !bg-slate-500 hover:!bg-slate-600'
|
||||
>
|
||||
{t('确认禁用')}
|
||||
</Button>
|
||||
@@ -311,14 +324,14 @@ const TwoFASetting = ({ t }) => {
|
||||
if (backupCodes.length > 0) {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
onClick={() => {
|
||||
setBackupModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setBackupCodes([]);
|
||||
}}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||
>
|
||||
{t('完成')}
|
||||
</Button>
|
||||
@@ -333,17 +346,17 @@ const TwoFASetting = ({ t }) => {
|
||||
setVerificationCode('');
|
||||
setBackupCodes([]);
|
||||
}}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
loading={loading}
|
||||
disabled={!verificationCode}
|
||||
onClick={handleRegenerateBackupCodes}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||
>
|
||||
{t('生成新的备用码')}
|
||||
</Button>
|
||||
@@ -353,67 +366,82 @@ const TwoFASetting = ({ t }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconShield size="large" className="text-slate-600 dark:text-slate-300" />
|
||||
<Card className='!rounded-xl w-full'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<div className='w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4 flex-shrink-0'>
|
||||
<IconShield
|
||||
size='large'
|
||||
className='text-slate-600 dark:text-slate-300'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Typography.Title heading={6} className="mb-0">
|
||||
<div className='flex-1'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
<Typography.Title heading={6} className='mb-0'>
|
||||
{t('两步验证设置')}
|
||||
</Typography.Title>
|
||||
{status.enabled ? (
|
||||
<Tag color="green" shape="circle" size="small">{t('已启用')}</Tag>
|
||||
<Tag color='green' shape='circle' size='small'>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="red" shape="circle" size="small">{t('未启用')}</Tag>
|
||||
<Tag color='red' shape='circle' size='small'>
|
||||
{t('未启用')}
|
||||
</Tag>
|
||||
)}
|
||||
{status.locked && (
|
||||
<Tag color="orange" shape="circle" size="small">{t('账户已锁定')}</Tag>
|
||||
<Tag color='orange' shape='circle' size='small'>
|
||||
{t('账户已锁定')}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
{t('两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。')}
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t(
|
||||
'两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。',
|
||||
)}
|
||||
</Typography.Text>
|
||||
{status.enabled && (
|
||||
<div className="mt-2">
|
||||
<Text size="small" type="secondary">{t('剩余备用码:')}{status.backup_codes_remaining || 0}{t('个')}</Text>
|
||||
<div className='mt-2'>
|
||||
<Text size='small' type='secondary'>
|
||||
{t('剩余备用码:')}
|
||||
{status.backup_codes_remaining || 0}
|
||||
{t('个')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2 w-full sm:w-auto">
|
||||
<div className='flex flex-col space-y-2 w-full sm:w-auto'>
|
||||
{!status.enabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size="default"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
size='default'
|
||||
onClick={handleSetup2FA}
|
||||
loading={loading}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
|
||||
icon={<IconShield />}
|
||||
>
|
||||
{t('启用验证')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className='flex flex-col space-y-2'>
|
||||
<Button
|
||||
type="danger"
|
||||
theme="solid"
|
||||
size="default"
|
||||
type='danger'
|
||||
theme='solid'
|
||||
size='default'
|
||||
onClick={() => setDisableModalVisible(true)}
|
||||
className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
|
||||
className='!rounded-lg !bg-slate-500 hover:!bg-slate-600'
|
||||
icon={<IconAlertTriangle />}
|
||||
>
|
||||
{t('禁用两步验证')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size="default"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
size='default'
|
||||
onClick={() => setBackupModalVisible(true)}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
icon={<IconRefresh />}
|
||||
>
|
||||
{t('重新生成备用码')}
|
||||
@@ -427,8 +455,8 @@ const TwoFASetting = ({ t }) => {
|
||||
{/* 2FA设置模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconShield className="mr-2 text-slate-600" />
|
||||
<div className='flex items-center'>
|
||||
<IconShield className='mr-2 text-slate-600' />
|
||||
{t('设置两步验证')}
|
||||
</div>
|
||||
}
|
||||
@@ -444,36 +472,50 @@ const TwoFASetting = ({ t }) => {
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
{setupData && (
|
||||
<div className="space-y-6">
|
||||
<div className='space-y-6'>
|
||||
{/* 步骤进度 */}
|
||||
<Steps type="basic" size="small" current={currentStep}>
|
||||
<Steps.Step title={t('扫描二维码')} description={t('使用认证器应用扫描二维码')} />
|
||||
<Steps.Step title={t('保存备用码')} description={t('保存备用码以备不时之需')} />
|
||||
<Steps.Step title={t('验证设置')} description={t('输入验证码完成设置')} />
|
||||
<Steps type='basic' size='small' current={currentStep}>
|
||||
<Steps.Step
|
||||
title={t('扫描二维码')}
|
||||
description={t('使用认证器应用扫描二维码')}
|
||||
/>
|
||||
<Steps.Step
|
||||
title={t('保存备用码')}
|
||||
description={t('保存备用码以备不时之需')}
|
||||
/>
|
||||
<Steps.Step
|
||||
title={t('验证设置')}
|
||||
description={t('输入验证码完成设置')}
|
||||
/>
|
||||
</Steps>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
<div className="rounded-xl">
|
||||
<div className='rounded-xl'>
|
||||
{currentStep === 0 && (
|
||||
<div>
|
||||
<Paragraph className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{t('使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:')}
|
||||
<Paragraph className='text-gray-600 dark:text-gray-300 mb-4'>
|
||||
{t(
|
||||
'使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:',
|
||||
)}
|
||||
</Paragraph>
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<div className='flex justify-center mb-4'>
|
||||
<div className='bg-white p-4 rounded-lg shadow-sm'>
|
||||
<QRCodeSVG value={setupData.qr_code_data} size={180} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
|
||||
<Text className="text-blue-800 dark:text-blue-200 text-sm">
|
||||
{t('或手动输入密钥:')}<Text code copyable className="ml-2">{setupData.secret}</Text>
|
||||
<div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-3'>
|
||||
<Text className='text-blue-800 dark:text-blue-200 text-sm'>
|
||||
{t('或手动输入密钥:')}
|
||||
<Text code copyable className='ml-2'>
|
||||
{setupData.secret}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
{/* 备用码展示 */}
|
||||
<BackupCodesDisplay
|
||||
codes={setupData.backup_codes}
|
||||
@@ -491,9 +533,9 @@ const TwoFASetting = ({ t }) => {
|
||||
placeholder={t('输入认证器应用显示的6位数字验证码')}
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
size="large"
|
||||
size='large'
|
||||
maxLength={6}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -504,8 +546,8 @@ const TwoFASetting = ({ t }) => {
|
||||
{/* 禁用2FA模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconAlertTriangle className="mr-2 text-red-500" />
|
||||
<div className='flex items-center'>
|
||||
<IconAlertTriangle className='mr-2 text-red-500' />
|
||||
{t('禁用两步验证')}
|
||||
</div>
|
||||
}
|
||||
@@ -519,36 +561,41 @@ const TwoFASetting = ({ t }) => {
|
||||
width={550}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className='space-y-6'>
|
||||
{/* 警告提示 */}
|
||||
<div className="rounded-xl">
|
||||
<div className='rounded-xl'>
|
||||
<Banner
|
||||
type="warning"
|
||||
description={t('警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!')}
|
||||
className="!rounded-lg"
|
||||
type='warning'
|
||||
description={t(
|
||||
'警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!',
|
||||
)}
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
|
||||
<Text
|
||||
strong
|
||||
className='block mb-2 text-slate-700 dark:text-slate-200'
|
||||
>
|
||||
{t('禁用后的影响:')}
|
||||
</Text>
|
||||
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<ul className='space-y-2 text-sm text-slate-600 dark:text-slate-300'>
|
||||
<li className='flex items-start gap-2'>
|
||||
<Badge dot type='warning' />
|
||||
{t('降低您账户的安全性')}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<li className='flex items-start gap-2'>
|
||||
<Badge dot type='warning' />
|
||||
{t('需要重新完整设置才能再次启用')}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<li className='flex items-start gap-2'>
|
||||
<Badge dot type='danger' />
|
||||
{t('永久删除您的两步验证设置')}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<li className='flex items-start gap-2'>
|
||||
<Badge dot type='danger' />
|
||||
{t('永久删除所有备用码(包括未使用的)')}
|
||||
</li>
|
||||
@@ -557,17 +604,20 @@ const TwoFASetting = ({ t }) => {
|
||||
|
||||
<Divider margin={16} />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
|
||||
<Text
|
||||
strong
|
||||
className='block mb-2 text-slate-700 dark:text-slate-200'
|
||||
>
|
||||
{t('验证身份')}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder={t('请输入认证器验证码或备用码')}
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -575,9 +625,11 @@ const TwoFASetting = ({ t }) => {
|
||||
<Checkbox
|
||||
checked={confirmDisable}
|
||||
onChange={(e) => setConfirmDisable(e.target.checked)}
|
||||
className="text-sm"
|
||||
className='text-sm'
|
||||
>
|
||||
{t('我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销')}
|
||||
{t(
|
||||
'我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销',
|
||||
)}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
@@ -588,8 +640,8 @@ const TwoFASetting = ({ t }) => {
|
||||
{/* 重新生成备用码模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconRefresh className="mr-2 text-slate-600" />
|
||||
<div className='flex items-center'>
|
||||
<IconRefresh className='mr-2 text-slate-600' />
|
||||
{t('重新生成备用码')}
|
||||
</div>
|
||||
}
|
||||
@@ -603,30 +655,35 @@ const TwoFASetting = ({ t }) => {
|
||||
width={500}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className='space-y-6'>
|
||||
{backupCodes.length === 0 ? (
|
||||
<>
|
||||
{/* 警告提示 */}
|
||||
<div className="rounded-xl">
|
||||
<div className='rounded-xl'>
|
||||
<Banner
|
||||
type="warning"
|
||||
description={t('重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。')}
|
||||
className="!rounded-lg"
|
||||
type='warning'
|
||||
description={t(
|
||||
'重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。',
|
||||
)}
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 验证区域 */}
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
|
||||
<Text
|
||||
strong
|
||||
className='block mb-2 text-slate-700 dark:text-slate-200'
|
||||
>
|
||||
{t('验证身份')}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder={t('请输入认证器验证码')}
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -635,13 +692,16 @@ const TwoFASetting = ({ t }) => {
|
||||
<>
|
||||
{/* 成功提示 */}
|
||||
<Space vertical style={{ width: '100%' }}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<Badge dot type='success' />
|
||||
<Text strong className="text-lg text-slate-700 dark:text-slate-200">
|
||||
<Text
|
||||
strong
|
||||
className='text-lg text-slate-700 dark:text-slate-200'
|
||||
>
|
||||
{t('新的备用码已生成')}
|
||||
</Text>
|
||||
</div>
|
||||
<Text className="text-slate-500 dark:text-slate-400 text-sm">
|
||||
<Text className='text-slate-500 dark:text-slate-400 text-sm'>
|
||||
{t('旧的备用码已失效,请保存新的备用码')}
|
||||
</Text>
|
||||
|
||||
@@ -660,4 +720,4 @@ const TwoFASetting = ({ t }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoFASetting;
|
||||
export default TwoFASetting;
|
||||
|
||||
@@ -18,12 +18,23 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar, Card, Tag, Divider, Typography, Badge } from '@douyinfe/semi-ui';
|
||||
import { isRoot, isAdmin, renderQuota, stringToColor } from '../../../../helpers';
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
Tag,
|
||||
Divider,
|
||||
Typography,
|
||||
Badge,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
isRoot,
|
||||
isAdmin,
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
} from '../../../../helpers';
|
||||
import { Coins, BarChart2, Users } from 'lucide-react';
|
||||
|
||||
const UserInfoHeader = ({ t, userState }) => {
|
||||
|
||||
const getUsername = () => {
|
||||
if (userState.user) {
|
||||
return userState.user.username;
|
||||
@@ -42,31 +53,33 @@ const UserInfoHeader = ({ t, userState }) => {
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden"
|
||||
className='!rounded-2xl overflow-hidden'
|
||||
cover={
|
||||
<div
|
||||
className="relative h-32"
|
||||
className='relative h-32'
|
||||
style={{
|
||||
'--palette-primary-darkerChannel': '0 75 80',
|
||||
backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
{/* 用户信息内容 */}
|
||||
<div className="relative z-10 h-full flex flex-col justify-end p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0">
|
||||
<Avatar
|
||||
size='large'
|
||||
color={stringToColor(getUsername())}
|
||||
>
|
||||
<div className='relative z-10 h-full flex flex-col justify-end p-6'>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-stretch gap-3 sm:gap-4 flex-1 min-w-0'>
|
||||
<Avatar size='large' color={stringToColor(getUsername())}>
|
||||
{getAvatarText()}
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-between">
|
||||
<div className="text-3xl font-bold truncate" style={{ color: 'white' }}>{getUsername()}</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className='flex-1 min-w-0 flex flex-col justify-between'>
|
||||
<div
|
||||
className='text-3xl font-bold truncate'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
{getUsername()}
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
{isRoot() ? (
|
||||
<Tag
|
||||
size='large'
|
||||
@@ -92,11 +105,7 @@ const UserInfoHeader = ({ t, userState }) => {
|
||||
{t('普通用户')}
|
||||
</Tag>
|
||||
)}
|
||||
<Tag
|
||||
size='large'
|
||||
shape='circle'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
<Tag size='large' shape='circle' style={{ color: 'white' }}>
|
||||
ID: {userState?.user?.id}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -108,34 +117,50 @@ const UserInfoHeader = ({ t, userState }) => {
|
||||
}
|
||||
>
|
||||
{/* 当前余额和桌面版统计信息 */}
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className='flex items-start justify-between gap-6'>
|
||||
{/* 当前余额显示 */}
|
||||
<Badge count={t('当前余额')} position='rightTop' type='danger'>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide">
|
||||
<div className='text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide'>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</div>
|
||||
</Badge>
|
||||
|
||||
{/* 桌面版统计信息(Semi UI 卡片) */}
|
||||
<div className="hidden lg:block flex-shrink-0">
|
||||
<Card size="small" className="!rounded-xl" bodyStyle={{ padding: '12px 16px' }}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='hidden lg:block flex-shrink-0'>
|
||||
<Card
|
||||
size='small'
|
||||
className='!rounded-xl'
|
||||
bodyStyle={{ padding: '12px 16px' }}
|
||||
>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Coins size={16} />
|
||||
<Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
|
||||
<Typography.Text size="small" type="tertiary" strong>{renderQuota(userState?.user?.used_quota)}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary'>
|
||||
{t('历史消耗')}
|
||||
</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary' strong>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Divider layout='vertical' />
|
||||
<div className='flex items-center gap-2'>
|
||||
<BarChart2 size={16} />
|
||||
<Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
|
||||
<Typography.Text size="small" type="tertiary" strong>{userState.user?.request_count || 0}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary'>
|
||||
{t('请求次数')}
|
||||
</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary' strong>
|
||||
{userState.user?.request_count || 0}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Divider layout='vertical' />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Users size={16} />
|
||||
<Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
|
||||
<Typography.Text size="small" type="tertiary" strong>{userState?.user?.group || t('默认')}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary'>
|
||||
{t('用户分组')}
|
||||
</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary' strong>
|
||||
{userState?.user?.group || t('默认')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -143,31 +168,47 @@ const UserInfoHeader = ({ t, userState }) => {
|
||||
</div>
|
||||
|
||||
{/* 移动端和中等屏幕统计信息卡片 */}
|
||||
<div className="lg:hidden mt-2">
|
||||
<Card size="small" className="!rounded-xl" bodyStyle={{ padding: '12px 16px' }} >
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='lg:hidden mt-2'>
|
||||
<Card
|
||||
size='small'
|
||||
className='!rounded-xl'
|
||||
bodyStyle={{ padding: '12px 16px' }}
|
||||
>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Coins size={16} />
|
||||
<Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary'>
|
||||
{t('历史消耗')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text size="small" type="tertiary" strong>{renderQuota(userState?.user?.used_quota)}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary' strong>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Divider margin='8px' />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<BarChart2 size={16} />
|
||||
<Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary'>
|
||||
{t('请求次数')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text size="small" type="tertiary" strong>{userState.user?.request_count || 0}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary' strong>
|
||||
{userState.user?.request_count || 0}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Divider margin='8px' />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Users size={16} />
|
||||
<Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary'>
|
||||
{t('用户分组')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text size="small" type="tertiary" strong>{userState?.user?.group || t('默认')}</Typography.Text>
|
||||
<Typography.Text size='small' type='tertiary' strong>
|
||||
{userState?.user?.group || t('默认')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -176,4 +217,4 @@ const UserInfoHeader = ({ t, userState }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default UserInfoHeader;
|
||||
export default UserInfoHeader;
|
||||
|
||||
@@ -32,13 +32,13 @@ const AccountDeleteModal = ({
|
||||
userState,
|
||||
turnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
setTurnstileToken
|
||||
setTurnstileToken,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconDelete className="mr-2 text-red-500" />
|
||||
<div className='flex items-center'>
|
||||
<IconDelete className='mr-2 text-red-500' />
|
||||
{t('删除账户确认')}
|
||||
</div>
|
||||
}
|
||||
@@ -47,35 +47,37 @@ const AccountDeleteModal = ({
|
||||
onOk={deleteAccount}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
className="modern-modal"
|
||||
className='modern-modal'
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className='space-y-4 py-4'>
|
||||
<Banner
|
||||
type='danger'
|
||||
description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
|
||||
closeIcon={null}
|
||||
className="!rounded-lg"
|
||||
className='!rounded-lg'
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2 text-red-600">
|
||||
<Typography.Text strong className='block mb-2 text-red-600'>
|
||||
{t('请输入您的用户名以确认删除')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })}
|
||||
placeholder={t('输入你的账户名{{username}}以确认删除', {
|
||||
username: ` ${userState?.user?.username} `,
|
||||
})}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={(value) =>
|
||||
handleInputChange('self_account_deletion_confirmation', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
prefix={<IconUser />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center">
|
||||
<div className='flex justify-center'>
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
|
||||
@@ -31,13 +31,13 @@ const ChangePasswordModal = ({
|
||||
changePassword,
|
||||
turnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
setTurnstileToken
|
||||
setTurnstileToken,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconLock className="mr-2 text-orange-500" />
|
||||
<div className='flex items-center'>
|
||||
<IconLock className='mr-2 text-orange-500' />
|
||||
{t('修改密码')}
|
||||
</div>
|
||||
}
|
||||
@@ -46,43 +46,45 @@ const ChangePasswordModal = ({
|
||||
onOk={changePassword}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
className="modern-modal"
|
||||
className='modern-modal'
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className='space-y-4 py-4'>
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text>
|
||||
<Typography.Text strong className='block mb-2'>
|
||||
{t('原密码')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
name='original_password'
|
||||
placeholder={t('请输入原密码')}
|
||||
type='password'
|
||||
value={inputs.original_password}
|
||||
onChange={(value) =>
|
||||
handleInputChange('original_password', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
onChange={(value) => handleInputChange('original_password', value)}
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
prefix={<IconLock />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">{t('新密码')}</Typography.Text>
|
||||
<Typography.Text strong className='block mb-2'>
|
||||
{t('新密码')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
name='set_new_password'
|
||||
placeholder={t('请输入新密码')}
|
||||
type='password'
|
||||
value={inputs.set_new_password}
|
||||
onChange={(value) =>
|
||||
handleInputChange('set_new_password', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
onChange={(value) => handleInputChange('set_new_password', value)}
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
prefix={<IconLock />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">{t('确认新密码')}</Typography.Text>
|
||||
<Typography.Text strong className='block mb-2'>
|
||||
{t('确认新密码')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
name='set_new_password_confirmation'
|
||||
placeholder={t('请再次输入新密码')}
|
||||
@@ -91,14 +93,14 @@ const ChangePasswordModal = ({
|
||||
onChange={(value) =>
|
||||
handleInputChange('set_new_password_confirmation', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
prefix={<IconLock />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center">
|
||||
<div className='flex justify-center'>
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
|
||||
@@ -35,13 +35,13 @@ const EmailBindModal = ({
|
||||
countdown,
|
||||
turnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
setTurnstileToken
|
||||
setTurnstileToken,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconMail className="mr-2 text-blue-500" />
|
||||
<div className='flex items-center'>
|
||||
<IconMail className='mr-2 text-blue-500' />
|
||||
{t('绑定邮箱地址')}
|
||||
</div>
|
||||
}
|
||||
@@ -51,28 +51,30 @@ const EmailBindModal = ({
|
||||
size={'small'}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
className="modern-modal"
|
||||
className='modern-modal'
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex gap-3">
|
||||
<div className='space-y-4 py-4'>
|
||||
<div className='flex gap-3'>
|
||||
<Input
|
||||
placeholder={t('输入邮箱地址')}
|
||||
onChange={(value) => handleInputChange('email', value)}
|
||||
name='email'
|
||||
type='email'
|
||||
size="large"
|
||||
className="!rounded-lg flex-1"
|
||||
size='large'
|
||||
className='!rounded-lg flex-1'
|
||||
prefix={<IconMail />}
|
||||
/>
|
||||
<Button
|
||||
onClick={sendVerificationCode}
|
||||
disabled={disableButton || loading}
|
||||
className="!rounded-lg"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
className='!rounded-lg'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
size='large'
|
||||
>
|
||||
{disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
|
||||
{disableButton
|
||||
? `${t('重新发送')} (${countdown})`
|
||||
: t('获取验证码')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -83,13 +85,13 @@ const EmailBindModal = ({
|
||||
onChange={(value) =>
|
||||
handleInputChange('email_verification_code', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
prefix={<IconKey />}
|
||||
/>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center">
|
||||
<div className='flex justify-center'>
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
|
||||
@@ -29,13 +29,13 @@ const WeChatBindModal = ({
|
||||
inputs,
|
||||
handleInputChange,
|
||||
bindWeChat,
|
||||
status
|
||||
status,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<SiWechat className="mr-2 text-green-500" size={20} />
|
||||
<div className='flex items-center'>
|
||||
<SiWechat className='mr-2 text-green-500' size={20} />
|
||||
{t('绑定微信账户')}
|
||||
</div>
|
||||
}
|
||||
@@ -44,30 +44,30 @@ const WeChatBindModal = ({
|
||||
footer={null}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
className="modern-modal"
|
||||
className='modern-modal'
|
||||
>
|
||||
<div className="space-y-4 py-4 text-center">
|
||||
<Image src={status.wechat_qrcode} className="mx-auto" />
|
||||
<div className="text-gray-600">
|
||||
<p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
|
||||
<div className='space-y-4 py-4 text-center'>
|
||||
<Image src={status.wechat_qrcode} className='mx-auto' />
|
||||
<div className='text-gray-600'>
|
||||
<p>
|
||||
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('验证码')}
|
||||
name='wechat_verification_code'
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={(v) =>
|
||||
handleInputChange('wechat_verification_code', v)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
onChange={(v) => handleInputChange('wechat_verification_code', v)}
|
||||
size='large'
|
||||
className='!rounded-lg'
|
||||
prefix={<IconKey />}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
type='primary'
|
||||
theme='solid'
|
||||
size='large'
|
||||
onClick={bindWeChat}
|
||||
className="!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700"
|
||||
className='!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700'
|
||||
icon={<SiWechat size={16} />}
|
||||
>
|
||||
{t('绑定')}
|
||||
|
||||
Reference in New Issue
Block a user