feat(layout): refine footer visibility logic to target CardPro component pages

- Replace blanket console route footer hiding with specific page targeting
- Only hide footer on pages that use CardPro component:
  * /console/channel (channels management)
  * /console/log (usage logs)
  * /console/redemption (redemption codes)
  * /console/user (user management)
  * /console/token (token management)
  * /console/midjourney (midjourney logs)
  * /console/task (task logs)
  * /console/models (model management)
  * /pricing (pricing page)
- Footer now displays on other console pages (dashboard, settings, topup, etc.)
- Improves UI consistency by showing footer where CardPro's internal pagination isn't used

This change ensures footer is only hidden when CardPro component provides its own
pagination/footer functionality, while preserving footer visibility on other pages
that benefit from the global footer navigation.
This commit is contained in:
t0ng7u
2025-09-27 18:47:53 +08:00
committed by CaIon
parent f473d20a09
commit df19a8de5d
20 changed files with 240 additions and 143 deletions

View File

@@ -6,4 +6,4 @@
} }
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }

View File

@@ -135,7 +135,9 @@ const TwoFactorAuthModal = ({
autoFocus autoFocus
/> />
<Typography.Text type='tertiary' size='small' className='mt-2 block'> <Typography.Text type='tertiary' size='small' className='mt-2 block'>
{t('支持6位TOTP验证码或8位备用码可到`个人设置-安全设置-两步验证设置`配置或查看。')} {t(
'支持6位TOTP验证码或8位备用码可到`个人设置-安全设置-两步验证设置`配置或查看。',
)}
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>

View File

@@ -48,9 +48,19 @@ const PageLayout = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const location = useLocation(); const location = useLocation();
const shouldHideFooter = const cardProPages = [
location.pathname.startsWith('/console') || '/console/channel',
location.pathname === '/pricing'; '/console/log',
'/console/redemption',
'/console/user',
'/console/token',
'/console/midjourney',
'/console/task',
'/console/models',
'/pricing',
];
const shouldHideFooter = cardProPages.includes(location.pathname);
const shouldInnerPadding = const shouldInnerPadding =
location.pathname.includes('/console') && location.pathname.includes('/console') &&

View File

@@ -46,7 +46,6 @@ import { useTranslation } from 'react-i18next';
const SystemSetting = () => { const SystemSetting = () => {
const { t } = useTranslation(); const { t } = useTranslation();
let [inputs, setInputs] = useState({ let [inputs, setInputs] = useState({
PasswordLoginEnabled: '', PasswordLoginEnabled: '',
PasswordRegisterEnabled: '', PasswordRegisterEnabled: '',
EmailVerificationEnabled: '', EmailVerificationEnabled: '',
@@ -212,7 +211,9 @@ const SystemSetting = () => {
setInputs(newInputs); setInputs(newInputs);
setOriginInputs(newInputs); setOriginInputs(newInputs);
// 同步模式布尔到本地状态 // 同步模式布尔到本地状态
if (typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined') { if (
typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined'
) {
setDomainFilterMode(!!newInputs['fetch_setting.domain_filter_mode']); setDomainFilterMode(!!newInputs['fetch_setting.domain_filter_mode']);
} }
if (typeof newInputs['fetch_setting.ip_filter_mode'] !== 'undefined') { if (typeof newInputs['fetch_setting.ip_filter_mode'] !== 'undefined') {
@@ -749,14 +750,17 @@ const SystemSetting = () => {
noLabel noLabel
extraText={t('SSRF防护开关详细说明')} extraText={t('SSRF防护开关详细说明')}
onChange={(e) => onChange={(e) =>
handleCheckboxChange('fetch_setting.enable_ssrf_protection', e) handleCheckboxChange(
'fetch_setting.enable_ssrf_protection',
e,
)
} }
> >
{t('启用SSRF防护推荐开启以保护服务器安全')} {t('启用SSRF防护推荐开启以保护服务器安全')}
</Form.Checkbox> </Form.Checkbox>
</Col> </Col>
</Row> </Row>
<Row <Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
@@ -767,14 +771,19 @@ const SystemSetting = () => {
noLabel noLabel
extraText={t('私有IP访问详细说明')} extraText={t('私有IP访问详细说明')}
onChange={(e) => onChange={(e) =>
handleCheckboxChange('fetch_setting.allow_private_ip', e) handleCheckboxChange(
'fetch_setting.allow_private_ip',
e,
)
} }
> >
{t('允许访问私有IP地址127.0.0.1、192.168.x.x等内网地址')} {t(
'允许访问私有IP地址127.0.0.1、192.168.x.x等内网地址',
)}
</Form.Checkbox> </Form.Checkbox>
</Col> </Col>
</Row> </Row>
<Row <Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
@@ -785,7 +794,10 @@ const SystemSetting = () => {
noLabel noLabel
extraText={t('域名IP过滤详细说明')} extraText={t('域名IP过滤详细说明')}
onChange={(e) => onChange={(e) =>
handleCheckboxChange('fetch_setting.apply_ip_filter_for_domain', e) handleCheckboxChange(
'fetch_setting.apply_ip_filter_for_domain',
e,
)
} }
style={{ marginBottom: 8 }} style={{ marginBottom: 8 }}
> >
@@ -794,17 +806,23 @@ const SystemSetting = () => {
<Text strong> <Text strong>
{t(domainFilterMode ? '域名白名单' : '域名黑名单')} {t(domainFilterMode ? '域名白名单' : '域名黑名单')}
</Text> </Text>
<Text type="secondary" style={{ display: 'block', marginBottom: 8 }}> <Text
{t('支持通配符格式example.com, *.api.example.com')} type='secondary'
style={{ display: 'block', marginBottom: 8 }}
>
{t(
'支持通配符格式example.com, *.api.example.com',
)}
</Text> </Text>
<Radio.Group <Radio.Group
type='button' type='button'
value={domainFilterMode ? 'whitelist' : 'blacklist'} value={domainFilterMode ? 'whitelist' : 'blacklist'}
onChange={(val) => { onChange={(val) => {
const selected = val && val.target ? val.target.value : val; const selected =
val && val.target ? val.target.value : val;
const isWhitelist = selected === 'whitelist'; const isWhitelist = selected === 'whitelist';
setDomainFilterMode(isWhitelist); setDomainFilterMode(isWhitelist);
setInputs(prev => ({ setInputs((prev) => ({
...prev, ...prev,
'fetch_setting.domain_filter_mode': isWhitelist, 'fetch_setting.domain_filter_mode': isWhitelist,
})); }));
@@ -819,9 +837,9 @@ const SystemSetting = () => {
onChange={(value) => { onChange={(value) => {
setDomainList(value); setDomainList(value);
// 触发Form的onChange事件 // 触发Form的onChange事件
setInputs(prev => ({ setInputs((prev) => ({
...prev, ...prev,
'fetch_setting.domain_list': value 'fetch_setting.domain_list': value,
})); }));
}} }}
placeholder={t('输入域名后回车example.com')} placeholder={t('输入域名后回车example.com')}
@@ -838,17 +856,21 @@ const SystemSetting = () => {
<Text strong> <Text strong>
{t(ipFilterMode ? 'IP白名单' : 'IP黑名单')} {t(ipFilterMode ? 'IP白名单' : 'IP黑名单')}
</Text> </Text>
<Text type="secondary" style={{ display: 'block', marginBottom: 8 }}> <Text
type='secondary'
style={{ display: 'block', marginBottom: 8 }}
>
{t('支持CIDR格式8.8.8.8, 192.168.1.0/24')} {t('支持CIDR格式8.8.8.8, 192.168.1.0/24')}
</Text> </Text>
<Radio.Group <Radio.Group
type='button' type='button'
value={ipFilterMode ? 'whitelist' : 'blacklist'} value={ipFilterMode ? 'whitelist' : 'blacklist'}
onChange={(val) => { onChange={(val) => {
const selected = val && val.target ? val.target.value : val; const selected =
val && val.target ? val.target.value : val;
const isWhitelist = selected === 'whitelist'; const isWhitelist = selected === 'whitelist';
setIpFilterMode(isWhitelist); setIpFilterMode(isWhitelist);
setInputs(prev => ({ setInputs((prev) => ({
...prev, ...prev,
'fetch_setting.ip_filter_mode': isWhitelist, 'fetch_setting.ip_filter_mode': isWhitelist,
})); }));
@@ -863,9 +885,9 @@ const SystemSetting = () => {
onChange={(value) => { onChange={(value) => {
setIpList(value); setIpList(value);
// 触发Form的onChange事件 // 触发Form的onChange事件
setInputs(prev => ({ setInputs((prev) => ({
...prev, ...prev,
'fetch_setting.ip_list': value 'fetch_setting.ip_list': value,
})); }));
}} }}
placeholder={t('输入IP地址后回车8.8.8.8')} placeholder={t('输入IP地址后回车8.8.8.8')}
@@ -880,7 +902,10 @@ const SystemSetting = () => {
> >
<Col xs={24} sm={24} md={24} lg={24} xl={24}> <Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Text strong>{t('允许的端口')}</Text> <Text strong>{t('允许的端口')}</Text>
<Text type="secondary" style={{ display: 'block', marginBottom: 8 }}> <Text
type='secondary'
style={{ display: 'block', marginBottom: 8 }}
>
{t('支持单个端口和端口范围80, 443, 8000-8999')} {t('支持单个端口和端口范围80, 443, 8000-8999')}
</Text> </Text>
<TagInput <TagInput
@@ -888,15 +913,18 @@ const SystemSetting = () => {
onChange={(value) => { onChange={(value) => {
setAllowedPorts(value); setAllowedPorts(value);
// 触发Form的onChange事件 // 触发Form的onChange事件
setInputs(prev => ({ setInputs((prev) => ({
...prev, ...prev,
'fetch_setting.allowed_ports': value 'fetch_setting.allowed_ports': value,
})); }));
}} }}
placeholder={t('输入端口后回车80 或 8000-8999')} placeholder={t('输入端口后回车80 或 8000-8999')}
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
<Text type="secondary" style={{ display: 'block', marginBottom: 8 }}> <Text
type='secondary'
style={{ display: 'block', marginBottom: 8 }}
>
{t('端口配置详细说明')} {t('端口配置详细说明')}
</Text> </Text>
</Col> </Col>

View File

@@ -91,22 +91,7 @@ const REGION_EXAMPLE = {
// 支持并且已适配通过接口获取模型列表的渠道类型 // 支持并且已适配通过接口获取模型列表的渠道类型
const MODEL_FETCHABLE_TYPES = new Set([ const MODEL_FETCHABLE_TYPES = new Set([
1, 1, 4, 14, 34, 17, 26, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48,
4,
14,
34,
17,
26,
24,
47,
25,
20,
23,
31,
35,
40,
42,
48,
43, 43,
]); ]);
@@ -279,8 +264,8 @@ const EditChannelModal = (props) => {
const scrollToSection = (sectionKey) => { const scrollToSection = (sectionKey) => {
const sectionElement = formSectionRefs.current[sectionKey]; const sectionElement = formSectionRefs.current[sectionKey];
if (sectionElement) { if (sectionElement) {
sectionElement.scrollIntoView({ sectionElement.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start', block: 'start',
inline: 'nearest' inline: 'nearest'
}); });
@@ -301,7 +286,7 @@ const EditChannelModal = (props) => {
} else { } else {
newIndex = currentSectionIndex < availableSections.length - 1 ? currentSectionIndex + 1 : 0; newIndex = currentSectionIndex < availableSections.length - 1 ? currentSectionIndex + 1 : 0;
} }
setCurrentSectionIndex(newIndex); setCurrentSectionIndex(newIndex);
scrollToSection(availableSections[newIndex]); scrollToSection(availableSections[newIndex]);
}; };
@@ -1340,7 +1325,7 @@ const EditChannelModal = (props) => {
type='tertiary' type='tertiary'
icon={<IconChevronUp />} icon={<IconChevronUp />}
onClick={() => navigateToSection('up')} onClick={() => navigateToSection('up')}
style={{ style={{
borderRadius: '50%', borderRadius: '50%',
width: '32px', width: '32px',
height: '32px', height: '32px',
@@ -1356,7 +1341,7 @@ const EditChannelModal = (props) => {
type='tertiary' type='tertiary'
icon={<IconChevronDown />} icon={<IconChevronDown />}
onClick={() => navigateToSection('down')} onClick={() => navigateToSection('down')}
style={{ style={{
borderRadius: '50%', borderRadius: '50%',
width: '32px', width: '32px',
height: '32px', height: '32px',
@@ -1398,8 +1383,8 @@ const EditChannelModal = (props) => {
> >
{() => ( {() => (
<Spin spinning={loading}> <Spin spinning={loading}>
<div <div
className='p-2' className='p-2'
ref={formContainerRef} ref={formContainerRef}
> >
<div ref={el => formSectionRefs.current.basicInfo = el}> <div ref={el => formSectionRefs.current.basicInfo = el}>

View File

@@ -56,10 +56,10 @@ const MjLogsFilters = ({
showClear showClear
pure pure
size='small' size='small'
presets={DATE_RANGE_PRESETS.map(preset => ({ presets={DATE_RANGE_PRESETS.map((preset) => ({
text: t(preset.text), text: t(preset.text),
start: preset.start(), start: preset.start(),
end: preset.end() end: preset.end(),
}))} }))}
/> />
</div> </div>

View File

@@ -36,8 +36,9 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { import {
TASK_ACTION_FIRST_TAIL_GENERATE, TASK_ACTION_FIRST_TAIL_GENERATE,
TASK_ACTION_GENERATE, TASK_ACTION_REFERENCE_GENERATE, TASK_ACTION_GENERATE,
TASK_ACTION_TEXT_GENERATE TASK_ACTION_REFERENCE_GENERATE,
TASK_ACTION_TEXT_GENERATE,
} from '../../../constants/common.constant'; } from '../../../constants/common.constant';
import { CHANNEL_OPTIONS } from '../../../constants/channel.constants'; import { CHANNEL_OPTIONS } from '../../../constants/channel.constants';

View File

@@ -56,10 +56,10 @@ const TaskLogsFilters = ({
showClear showClear
pure pure
size='small' size='small'
presets={DATE_RANGE_PRESETS.map(preset => ({ presets={DATE_RANGE_PRESETS.map((preset) => ({
text: t(preset.text), text: t(preset.text),
start: preset.start(), start: preset.start(),
end: preset.end() end: preset.end(),
}))} }))}
/> />
</div> </div>

View File

@@ -57,10 +57,10 @@ const LogsFilters = ({
showClear showClear
pure pure
size='small' size='small'
presets={DATE_RANGE_PRESETS.map(preset => ({ presets={DATE_RANGE_PRESETS.map((preset) => ({
text: t(preset.text), text: t(preset.text),
start: preset.start(), start: preset.start(),
end: preset.end() end: preset.end(),
}))} }))}
/> />
</div> </div>

View File

@@ -30,7 +30,8 @@ import {
Space, Space,
Row, Row,
Col, Col,
Spin, Tooltip Spin,
Tooltip,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react'; import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react';
@@ -266,7 +267,8 @@ const RechargeCard = ({
{payMethods && payMethods.length > 0 ? ( {payMethods && payMethods.length > 0 ? (
<Space wrap> <Space wrap>
{payMethods.map((payMethod) => { {payMethods.map((payMethod) => {
const minTopupVal = Number(payMethod.min_topup) || 0; const minTopupVal =
Number(payMethod.min_topup) || 0;
const isStripe = payMethod.type === 'stripe'; const isStripe = payMethod.type === 'stripe';
const disabled = const disabled =
(!enableOnlineTopUp && !isStripe) || (!enableOnlineTopUp && !isStripe) ||
@@ -280,7 +282,9 @@ const RechargeCard = ({
type='tertiary' type='tertiary'
onClick={() => preTopUp(payMethod.type)} onClick={() => preTopUp(payMethod.type)}
disabled={disabled} disabled={disabled}
loading={paymentLoading && payWay === payMethod.type} loading={
paymentLoading && payWay === payMethod.type
}
icon={ icon={
payMethod.type === 'alipay' ? ( payMethod.type === 'alipay' ? (
<SiAlipay size={18} color='#1677FF' /> <SiAlipay size={18} color='#1677FF' />
@@ -291,7 +295,10 @@ const RechargeCard = ({
) : ( ) : (
<CreditCard <CreditCard
size={18} size={18}
color={payMethod.color || 'var(--semi-color-text-2)'} color={
payMethod.color ||
'var(--semi-color-text-2)'
}
/> />
) )
} }
@@ -301,12 +308,22 @@ const RechargeCard = ({
</Button> </Button>
); );
return disabled && minTopupVal > Number(topUpCount || 0) ? ( return disabled &&
<Tooltip content={t('此支付方式最低充值金额为') + ' ' + minTopupVal} key={payMethod.type}> minTopupVal > Number(topUpCount || 0) ? (
<Tooltip
content={
t('此支付方式最低充值金额为') +
' ' +
minTopupVal
}
key={payMethod.type}
>
{buttonEl} {buttonEl}
</Tooltip> </Tooltip>
) : ( ) : (
<React.Fragment key={payMethod.type}>{buttonEl}</React.Fragment> <React.Fragment key={payMethod.type}>
{buttonEl}
</React.Fragment>
); );
})} })}
</Space> </Space>
@@ -324,23 +341,27 @@ const RechargeCard = ({
<Form.Slot label={t('选择充值额度')}> <Form.Slot label={t('选择充值额度')}>
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'> <div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2'>
{presetAmounts.map((preset, index) => { {presetAmounts.map((preset, index) => {
const discount = preset.discount || topupInfo?.discount?.[preset.value] || 1.0; const discount =
preset.discount ||
topupInfo?.discount?.[preset.value] ||
1.0;
const originalPrice = preset.value * priceRatio; const originalPrice = preset.value * priceRatio;
const discountedPrice = originalPrice * discount; const discountedPrice = originalPrice * discount;
const hasDiscount = discount < 1.0; const hasDiscount = discount < 1.0;
const actualPay = discountedPrice; const actualPay = discountedPrice;
const save = originalPrice - discountedPrice; const save = originalPrice - discountedPrice;
return ( return (
<Card <Card
key={index} key={index}
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
border: selectedPreset === preset.value border:
? '2px solid var(--semi-color-primary)' selectedPreset === preset.value
: '1px solid var(--semi-color-border)', ? '2px solid var(--semi-color-primary)'
: '1px solid var(--semi-color-border)',
height: '100%', height: '100%',
width: '100%' width: '100%',
}} }}
bodyStyle={{ padding: '12px' }} bodyStyle={{ padding: '12px' }}
onClick={() => { onClick={() => {
@@ -352,24 +373,35 @@ const RechargeCard = ({
}} }}
> >
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<Typography.Title heading={6} style={{ margin: '0 0 8px 0' }}> <Typography.Title
heading={6}
style={{ margin: '0 0 8px 0' }}
>
<Coins size={18} /> <Coins size={18} />
{formatLargeNumber(preset.value)} {formatLargeNumber(preset.value)}
{hasDiscount && ( {hasDiscount && (
<Tag style={{ marginLeft: 4 }} color="green"> <Tag style={{ marginLeft: 4 }} color='green'>
{t('折').includes('off') ? {t('折').includes('off')
((1 - parseFloat(discount)) * 100).toFixed(1) : ? (
(discount * 10).toFixed(1)}{t('折')} (1 - parseFloat(discount)) *
</Tag> 100
).toFixed(1)
: (discount * 10).toFixed(1)}
{t('折')}
</Tag>
)} )}
</Typography.Title> </Typography.Title>
<div style={{ <div
color: 'var(--semi-color-text-2)', style={{
fontSize: '12px', color: 'var(--semi-color-text-2)',
margin: '4px 0' fontSize: '12px',
}}> margin: '4px 0',
}}
>
{t('实付')} {actualPay.toFixed(2)} {t('实付')} {actualPay.toFixed(2)}
{hasDiscount ? `${t('节省')} ${save.toFixed(2)}` : `${t('节省')} 0.00`} {hasDiscount
? `${t('节省')} ${save.toFixed(2)}`
: `${t('节省')} 0.00`}
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -80,11 +80,11 @@ const TopUp = () => {
// 预设充值额度选项 // 预设充值额度选项
const [presetAmounts, setPresetAmounts] = useState([]); const [presetAmounts, setPresetAmounts] = useState([]);
const [selectedPreset, setSelectedPreset] = useState(null); const [selectedPreset, setSelectedPreset] = useState(null);
// 充值配置信息 // 充值配置信息
const [topupInfo, setTopupInfo] = useState({ const [topupInfo, setTopupInfo] = useState({
amount_options: [], amount_options: [],
discount: {} discount: {},
}); });
const topUp = async () => { const topUp = async () => {
@@ -262,9 +262,9 @@ const TopUp = () => {
if (success) { if (success) {
setTopupInfo({ setTopupInfo({
amount_options: data.amount_options || [], amount_options: data.amount_options || [],
discount: data.discount || {} discount: data.discount || {},
}); });
// 处理支付方式 // 处理支付方式
let payMethods = data.pay_methods || []; let payMethods = data.pay_methods || [];
try { try {
@@ -280,10 +280,15 @@ const TopUp = () => {
payMethods = payMethods.map((method) => { payMethods = payMethods.map((method) => {
// 规范化最小充值数 // 规范化最小充值数
const normalizedMinTopup = Number(method.min_topup); const normalizedMinTopup = Number(method.min_topup);
method.min_topup = Number.isFinite(normalizedMinTopup) ? normalizedMinTopup : 0; method.min_topup = Number.isFinite(normalizedMinTopup)
? normalizedMinTopup
: 0;
// Stripe 的最小充值从后端字段回填 // Stripe 的最小充值从后端字段回填
if (method.type === 'stripe' && (!method.min_topup || method.min_topup <= 0)) { if (
method.type === 'stripe' &&
(!method.min_topup || method.min_topup <= 0)
) {
const stripeMin = Number(data.stripe_min_topup); const stripeMin = Number(data.stripe_min_topup);
if (Number.isFinite(stripeMin)) { if (Number.isFinite(stripeMin)) {
method.min_topup = stripeMin; method.min_topup = stripeMin;
@@ -313,7 +318,11 @@ const TopUp = () => {
setPayMethods(payMethods); setPayMethods(payMethods);
const enableStripeTopUp = data.enable_stripe_topup || false; const enableStripeTopUp = data.enable_stripe_topup || false;
const enableOnlineTopUp = data.enable_online_topup || false; const enableOnlineTopUp = data.enable_online_topup || false;
const minTopUpValue = enableOnlineTopUp? data.min_topup : enableStripeTopUp? data.stripe_min_topup : 1; const minTopUpValue = enableOnlineTopUp
? data.min_topup
: enableStripeTopUp
? data.stripe_min_topup
: 1;
setEnableOnlineTopUp(enableOnlineTopUp); setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp); setEnableStripeTopUp(enableStripeTopUp);
setMinTopUp(minTopUpValue); setMinTopUp(minTopUpValue);
@@ -330,12 +339,12 @@ const TopUp = () => {
console.log('解析支付方式失败:', e); console.log('解析支付方式失败:', e);
setPayMethods([]); setPayMethods([]);
} }
// 如果有自定义充值数量选项,使用它们替换默认的预设选项 // 如果有自定义充值数量选项,使用它们替换默认的预设选项
if (data.amount_options && data.amount_options.length > 0) { if (data.amount_options && data.amount_options.length > 0) {
const customPresets = data.amount_options.map(amount => ({ const customPresets = data.amount_options.map((amount) => ({
value: amount, value: amount,
discount: data.discount[amount] || 1.0 discount: data.discount[amount] || 1.0,
})); }));
setPresetAmounts(customPresets); setPresetAmounts(customPresets);
} }
@@ -483,7 +492,7 @@ const TopUp = () => {
const selectPresetAmount = (preset) => { const selectPresetAmount = (preset) => {
setTopUpCount(preset.value); setTopUpCount(preset.value);
setSelectedPreset(preset.value); setSelectedPreset(preset.value);
// 计算实际支付金额,考虑折扣 // 计算实际支付金额,考虑折扣
const discount = preset.discount || topupInfo.discount[preset.value] || 1.0; const discount = preset.discount || topupInfo.discount[preset.value] || 1.0;
const discountedAmount = preset.value * priceRatio * discount; const discountedAmount = preset.value * priceRatio * discount;

View File

@@ -40,9 +40,10 @@ const PaymentConfirmModal = ({
amountNumber, amountNumber,
discountRate, discountRate,
}) => { }) => {
const hasDiscount = discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0; const hasDiscount =
const originalAmount = hasDiscount ? (amountNumber / discountRate) : 0; discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0;
const discountAmount = hasDiscount ? (originalAmount - amountNumber) : 0; const originalAmount = hasDiscount ? amountNumber / discountRate : 0;
const discountAmount = hasDiscount ? originalAmount - amountNumber : 0;
return ( return (
<Modal <Modal
title={ title={

View File

@@ -24,26 +24,26 @@ export const DATE_RANGE_PRESETS = [
{ {
text: '今天', text: '今天',
start: () => dayjs().startOf('day').toDate(), start: () => dayjs().startOf('day').toDate(),
end: () => dayjs().endOf('day').toDate() end: () => dayjs().endOf('day').toDate(),
}, },
{ {
text: '近 7 天', text: '近 7 天',
start: () => dayjs().subtract(6, 'day').startOf('day').toDate(), start: () => dayjs().subtract(6, 'day').startOf('day').toDate(),
end: () => dayjs().endOf('day').toDate() end: () => dayjs().endOf('day').toDate(),
}, },
{ {
text: '本周', text: '本周',
start: () => dayjs().startOf('week').toDate(), start: () => dayjs().startOf('week').toDate(),
end: () => dayjs().endOf('week').toDate() end: () => dayjs().endOf('week').toDate(),
}, },
{ {
text: '近 30 天', text: '近 30 天',
start: () => dayjs().subtract(29, 'day').startOf('day').toDate(), start: () => dayjs().subtract(29, 'day').startOf('day').toDate(),
end: () => dayjs().endOf('day').toDate() end: () => dayjs().endOf('day').toDate(),
}, },
{ {
text: '本月', text: '本月',
start: () => dayjs().startOf('month').toDate(), start: () => dayjs().startOf('month').toDate(),
end: () => dayjs().endOf('month').toDate() end: () => dayjs().endOf('month').toDate(),
}, },
]; ];

View File

@@ -131,13 +131,11 @@ export const buildApiPayload = (
seed: 'seed', seed: 'seed',
}; };
Object.entries(parameterMappings).forEach(([key, param]) => { Object.entries(parameterMappings).forEach(([key, param]) => {
const enabled = parameterEnabled[key]; const enabled = parameterEnabled[key];
const value = inputs[param]; const value = inputs[param];
const hasValue = value !== undefined && value !== null; const hasValue = value !== undefined && value !== null;
if (enabled && hasValue) { if (enabled && hasValue) {
payload[param] = value; payload[param] = value;
} }

View File

@@ -1074,7 +1074,7 @@ export function renderModelPrice(
(completionTokens / 1000000) * completionRatioPrice * groupRatio + (completionTokens / 1000000) * completionRatioPrice * groupRatio +
(webSearchCallCount / 1000) * webSearchPrice * groupRatio + (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
(imageGenerationCallPrice * groupRatio); imageGenerationCallPrice * groupRatio;
return ( return (
<> <>

View File

@@ -183,7 +183,10 @@ export const useSidebar = () => {
sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
return () => { return () => {
sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); sidebarEventTarget.removeEventListener(
SIDEBAR_REFRESH_EVENT,
handleRefresh,
);
}; };
}, [adminConfig]); }, [adminConfig]);

View File

@@ -130,19 +130,20 @@ export default function GeneralSettings(props) {
showClear showClear
/> />
</Col> </Col>
{inputs.QuotaPerUnit !== '500000' && inputs.QuotaPerUnit !== 500000 && ( {inputs.QuotaPerUnit !== '500000' &&
<Col xs={24} sm={12} md={8} lg={8} xl={8}> inputs.QuotaPerUnit !== 500000 && (
<Form.Input <Col xs={24} sm={12} md={8} lg={8} xl={8}>
field={'QuotaPerUnit'} <Form.Input
label={t('单位美元额度')} field={'QuotaPerUnit'}
initValue={''} label={t('单位美元额度')}
placeholder={t('一单位货币能兑换的额度')} initValue={''}
onChange={handleFieldChange('QuotaPerUnit')} placeholder={t('一单位货币能兑换的额度')}
showClear onChange={handleFieldChange('QuotaPerUnit')}
onClick={() => setShowQuotaWarning(true)} showClear
/> onClick={() => setShowQuotaWarning(true)}
</Col> />
)} </Col>
)}
<Col xs={24} sm={12} md={8} lg={8} xl={8}> <Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input <Form.Input
field={'USDExchangeRate'} field={'USDExchangeRate'}

View File

@@ -128,7 +128,8 @@ export default function SettingsMonitoring(props) {
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
...inputs, ...inputs,
'monitor_setting.auto_test_channel_minutes': parseInt(value), 'monitor_setting.auto_test_channel_minutes':
parseInt(value),
}) })
} }
/> />

View File

@@ -118,14 +118,20 @@ export default function SettingsPaymentGateway(props) {
} }
} }
if (originInputs['AmountOptions'] !== inputs.AmountOptions && inputs.AmountOptions.trim() !== '') { if (
originInputs['AmountOptions'] !== inputs.AmountOptions &&
inputs.AmountOptions.trim() !== ''
) {
if (!verifyJSON(inputs.AmountOptions)) { if (!verifyJSON(inputs.AmountOptions)) {
showError(t('自定义充值数量选项不是合法的 JSON 数组')); showError(t('自定义充值数量选项不是合法的 JSON 数组'));
return; return;
} }
} }
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount && inputs.AmountDiscount.trim() !== '') { if (
originInputs['AmountDiscount'] !== inputs.AmountDiscount &&
inputs.AmountDiscount.trim() !== ''
) {
if (!verifyJSON(inputs.AmountDiscount)) { if (!verifyJSON(inputs.AmountDiscount)) {
showError(t('充值金额折扣配置不是合法的 JSON 对象')); showError(t('充值金额折扣配置不是合法的 JSON 对象'));
return; return;
@@ -163,10 +169,16 @@ export default function SettingsPaymentGateway(props) {
options.push({ key: 'PayMethods', value: inputs.PayMethods }); options.push({ key: 'PayMethods', value: inputs.PayMethods });
} }
if (originInputs['AmountOptions'] !== inputs.AmountOptions) { if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions }); options.push({
key: 'payment_setting.amount_options',
value: inputs.AmountOptions,
});
} }
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) { if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) {
options.push({ key: 'payment_setting.amount_discount', value: inputs.AmountDiscount }); options.push({
key: 'payment_setting.amount_discount',
value: inputs.AmountDiscount,
});
} }
// 发送请求 // 发送请求
@@ -273,7 +285,7 @@ export default function SettingsPaymentGateway(props) {
placeholder={t('为一个 JSON 文本')} placeholder={t('为一个 JSON 文本')}
autosize autosize
/> />
<Row <Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
@@ -282,13 +294,17 @@ export default function SettingsPaymentGateway(props) {
<Form.TextArea <Form.TextArea
field='AmountOptions' field='AmountOptions'
label={t('自定义充值数量选项')} label={t('自定义充值数量选项')}
placeholder={t('为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]')} placeholder={t(
'为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]',
)}
autosize autosize
extraText={t('设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]')} extraText={t(
'设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]',
)}
/> />
</Col> </Col>
</Row> </Row>
<Row <Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
@@ -297,13 +313,17 @@ export default function SettingsPaymentGateway(props) {
<Form.TextArea <Form.TextArea
field='AmountDiscount' field='AmountDiscount'
label={t('充值金额折扣配置')} label={t('充值金额折扣配置')}
placeholder={t('为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}')} placeholder={t(
'为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
autosize autosize
extraText={t('设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}')} extraText={t(
'设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
/> />
</Col> </Col>
</Row> </Row>
<Button onClick={submitPayAddress}>{t('更新支付设置')}</Button> <Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
</Form.Section> </Form.Section>
</Form> </Form>

View File

@@ -226,8 +226,12 @@ export default function ModelRatioSettings(props) {
<Col xs={24} sm={16}> <Col xs={24} sm={16}>
<Form.TextArea <Form.TextArea
label={t('图片输入倍率(仅部分模型支持该计费)')} label={t('图片输入倍率(仅部分模型支持该计费)')}
extraText={t('图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费')} extraText={t(
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-image-1": 2}')} '图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费',
)}
placeholder={t(
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-image-1": 2}',
)}
field={'ImageRatio'} field={'ImageRatio'}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
@@ -238,9 +242,7 @@ export default function ModelRatioSettings(props) {
message: '不是合法的 JSON 字符串', message: '不是合法的 JSON 字符串',
}, },
]} ]}
onChange={(value) => onChange={(value) => setInputs({ ...inputs, ImageRatio: value })}
setInputs({ ...inputs, ImageRatio: value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -249,7 +251,9 @@ export default function ModelRatioSettings(props) {
<Form.TextArea <Form.TextArea
label={t('音频倍率(仅部分模型支持该计费)')} label={t('音频倍率(仅部分模型支持该计费)')}
extraText={t('音频输入相关的倍率设置,键为模型名称,值为倍率')} extraText={t('音频输入相关的倍率设置,键为模型名称,值为倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-audio-preview": 16}')} placeholder={t(
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-audio-preview": 16}',
)}
field={'AudioRatio'} field={'AudioRatio'}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
@@ -260,9 +264,7 @@ export default function ModelRatioSettings(props) {
message: '不是合法的 JSON 字符串', message: '不是合法的 JSON 字符串',
}, },
]} ]}
onChange={(value) => onChange={(value) => setInputs({ ...inputs, AudioRatio: value })}
setInputs({ ...inputs, AudioRatio: value })
}
/> />
</Col> </Col>
</Row> </Row>
@@ -270,8 +272,12 @@ export default function ModelRatioSettings(props) {
<Col xs={24} sm={16}> <Col xs={24} sm={16}>
<Form.TextArea <Form.TextArea
label={t('音频补全倍率(仅部分模型支持该计费)')} label={t('音频补全倍率(仅部分模型支持该计费)')}
extraText={t('音频输出补全相关的倍率设置,键为模型名称,值为倍率')} extraText={t(
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-realtime": 2}')} '音频输出补全相关的倍率设置,键为模型名称,值为倍率',
)}
placeholder={t(
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-realtime": 2}',
)}
field={'AudioCompletionRatio'} field={'AudioCompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'