Merge branch 'origin/main' into nightly
Resolve 4 conflicts:
- relay/compatible_handler.go: accept main's refactor (postConsumeQuota -> service.PostTextConsumeQuota)
- service/quota.go: accept main's PostClaudeConsumeQuota deletion, keep nightly's tiered billing in PostWssConsumeQuota and PostAudioConsumeQuota
- web/src/i18n/locales/{en,zh-CN}.json: merge both sets of translation keys
Post-merge integration:
- Add tiered billing (TryTieredSettle, InjectTieredBillingInfo) to PostTextConsumeQuota
- Update tool pricing calls to use nightly's generic GetToolPriceForModel/GetToolPrice API
This commit is contained in:
@@ -56,7 +56,7 @@ const OAuth2Callback = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message === 'bind') {
|
||||
if (data?.action === 'bind') {
|
||||
showSuccess(t('绑定成功!'));
|
||||
navigate('/console/personal');
|
||||
} else {
|
||||
|
||||
@@ -221,23 +221,27 @@ const FooterBar = () => {
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{footer ? (
|
||||
<div className='relative'>
|
||||
<div
|
||||
className='custom-footer'
|
||||
dangerouslySetInnerHTML={{ __html: footer }}
|
||||
></div>
|
||||
<div className='absolute bottom-2 right-4 text-xs !text-semi-color-text-2 opacity-70'>
|
||||
<span>{t('设计与开发由')} </span>
|
||||
<a
|
||||
href='https://github.com/QuantumNous/new-api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='!text-semi-color-primary font-medium'
|
||||
>
|
||||
New API
|
||||
</a>
|
||||
<footer className='relative h-auto py-4 px-6 md:px-24 w-full flex items-center justify-center overflow-hidden'>
|
||||
<div className='flex flex-col md:flex-row items-center justify-between w-full max-w-[1110px] gap-4'>
|
||||
<div
|
||||
className='custom-footer na-cb6feafeb3990c78 text-sm !text-semi-color-text-1'
|
||||
dangerouslySetInnerHTML={{ __html: footer }}
|
||||
></div>
|
||||
<div className='text-sm flex-shrink-0'>
|
||||
<span className='!text-semi-color-text-1'>
|
||||
{t('设计与开发由')}{' '}
|
||||
</span>
|
||||
<a
|
||||
href='https://github.com/QuantumNous/new-api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='!text-semi-color-primary font-medium'
|
||||
>
|
||||
New API
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
) : (
|
||||
customFooter
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralP
|
||||
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
|
||||
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
|
||||
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
|
||||
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
|
||||
import { API, showError, toBoolean } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -66,7 +67,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析TopupGroupRatio出错:', error);
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -78,7 +78,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析AmountOptions出错:', error);
|
||||
newInputs['AmountOptions'] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -90,7 +89,6 @@ const PaymentSetting = () => {
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('解析AmountDiscount出错:', error);
|
||||
newInputs['AmountDiscount'] = item.value;
|
||||
}
|
||||
break;
|
||||
@@ -146,6 +144,9 @@ const PaymentSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayWaffo options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -306,9 +306,9 @@ const PersonalSetting = () => {
|
||||
|
||||
const bindWeChat = async () => {
|
||||
if (inputs.wechat_verification_code === '') return;
|
||||
const res = await API.get(
|
||||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
|
||||
);
|
||||
const res = await API.post('/api/oauth/wechat/bind', {
|
||||
code: inputs.wechat_verification_code,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('微信账户绑定成功!'));
|
||||
@@ -378,9 +378,10 @@ const PersonalSetting = () => {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
|
||||
);
|
||||
const res = await API.post('/api/oauth/email/bind', {
|
||||
email: inputs.email,
|
||||
code: inputs.email_verification_code,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('邮箱账户绑定成功!'));
|
||||
|
||||
@@ -108,7 +108,7 @@ const SystemSetting = () => {
|
||||
'fetch_setting.domain_list': [],
|
||||
'fetch_setting.ip_list': [],
|
||||
'fetch_setting.allowed_ports': [],
|
||||
'fetch_setting.apply_ip_filter_for_domain': false,
|
||||
'fetch_setting.apply_ip_filter_for_domain': true,
|
||||
});
|
||||
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
@@ -847,7 +847,7 @@ const SystemSetting = () => {
|
||||
}
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
{t('对域名启用 IP 过滤(实验性)')}
|
||||
{t('对域名启用 IP 过滤(推荐开启)')}
|
||||
</Form.Checkbox>
|
||||
<Text strong>
|
||||
{t(domainFilterMode ? '域名白名单' : '域名黑名单')}
|
||||
|
||||
@@ -538,19 +538,24 @@ export const getChannelsColumns = ({
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
t('剩余额度') +
|
||||
': ' +
|
||||
renderQuotaWithAmount(record.balance) +
|
||||
t(',点击更新')
|
||||
record.type === 57
|
||||
? t('查看 Codex 帐号信息与用量')
|
||||
: t('剩余额度') +
|
||||
': ' +
|
||||
renderQuotaWithAmount(record.balance) +
|
||||
t(',点击更新')
|
||||
}
|
||||
>
|
||||
<Tag
|
||||
color='white'
|
||||
type='ghost'
|
||||
color={record.type === 57 ? 'light-blue' : 'white'}
|
||||
type={record.type === 57 ? 'light' : 'ghost'}
|
||||
shape='circle'
|
||||
className={record.type === 57 ? 'cursor-pointer' : ''}
|
||||
onClick={() => updateChannelBalance(record)}
|
||||
>
|
||||
{renderQuotaWithAmount(record.balance)}
|
||||
{record.type === 57
|
||||
? t('帐号信息')
|
||||
: renderQuotaWithAmount(record.balance)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
@@ -22,9 +22,11 @@ import {
|
||||
Modal,
|
||||
Button,
|
||||
Progress,
|
||||
Tag,
|
||||
Typography,
|
||||
Spin,
|
||||
Tag,
|
||||
Descriptions,
|
||||
Collapse,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError } from '../../../../helpers';
|
||||
|
||||
@@ -43,6 +45,68 @@ const pickStrokeColor = (percent) => {
|
||||
return '#3b82f6';
|
||||
};
|
||||
|
||||
const normalizePlanType = (value) => {
|
||||
if (value == null) return '';
|
||||
return String(value).trim().toLowerCase();
|
||||
};
|
||||
|
||||
const getWindowDurationSeconds = (windowData) => {
|
||||
const value = Number(windowData?.limit_window_seconds);
|
||||
if (!Number.isFinite(value) || value <= 0) return null;
|
||||
return value;
|
||||
};
|
||||
|
||||
const classifyWindowByDuration = (windowData) => {
|
||||
const seconds = getWindowDurationSeconds(windowData);
|
||||
if (seconds == null) return null;
|
||||
return seconds >= 24 * 60 * 60 ? 'weekly' : 'fiveHour';
|
||||
};
|
||||
|
||||
const resolveRateLimitWindows = (data) => {
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
const primary = rateLimit?.primary_window ?? null;
|
||||
const secondary = rateLimit?.secondary_window ?? null;
|
||||
const windows = [primary, secondary].filter(Boolean);
|
||||
const planType = normalizePlanType(data?.plan_type ?? rateLimit?.plan_type);
|
||||
|
||||
let fiveHourWindow = null;
|
||||
let weeklyWindow = null;
|
||||
|
||||
for (const windowData of windows) {
|
||||
const bucket = classifyWindowByDuration(windowData);
|
||||
if (bucket === 'fiveHour' && !fiveHourWindow) {
|
||||
fiveHourWindow = windowData;
|
||||
continue;
|
||||
}
|
||||
if (bucket === 'weekly' && !weeklyWindow) {
|
||||
weeklyWindow = windowData;
|
||||
}
|
||||
}
|
||||
|
||||
if (planType === 'free') {
|
||||
if (!weeklyWindow) {
|
||||
weeklyWindow = primary ?? secondary ?? null;
|
||||
}
|
||||
return { fiveHourWindow: null, weeklyWindow };
|
||||
}
|
||||
|
||||
if (!fiveHourWindow && !weeklyWindow) {
|
||||
return {
|
||||
fiveHourWindow: primary ?? null,
|
||||
weeklyWindow: secondary ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!fiveHourWindow) {
|
||||
fiveHourWindow = windows.find((windowData) => windowData !== weeklyWindow) ?? null;
|
||||
}
|
||||
if (!weeklyWindow) {
|
||||
weeklyWindow = windows.find((windowData) => windowData !== fiveHourWindow) ?? null;
|
||||
}
|
||||
|
||||
return { fiveHourWindow, weeklyWindow };
|
||||
};
|
||||
|
||||
const formatDurationSeconds = (seconds, t) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const s = Number(seconds);
|
||||
@@ -66,8 +130,93 @@ const formatUnixSeconds = (unixSeconds) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getDisplayText = (value) => {
|
||||
if (value == null) return '';
|
||||
return String(value).trim();
|
||||
};
|
||||
|
||||
const formatAccountTypeLabel = (value, t) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const normalized = normalizePlanType(value);
|
||||
switch (normalized) {
|
||||
case 'free':
|
||||
return 'Free';
|
||||
case 'plus':
|
||||
return 'Plus';
|
||||
case 'pro':
|
||||
return 'Pro';
|
||||
case 'team':
|
||||
return 'Team';
|
||||
case 'enterprise':
|
||||
return 'Enterprise';
|
||||
default:
|
||||
return getDisplayText(value) || tt('未识别');
|
||||
}
|
||||
};
|
||||
|
||||
const getAccountTypeTagColor = (value) => {
|
||||
const normalized = normalizePlanType(value);
|
||||
switch (normalized) {
|
||||
case 'enterprise':
|
||||
return 'green';
|
||||
case 'team':
|
||||
return 'cyan';
|
||||
case 'pro':
|
||||
return 'blue';
|
||||
case 'plus':
|
||||
return 'violet';
|
||||
case 'free':
|
||||
return 'amber';
|
||||
default:
|
||||
return 'grey';
|
||||
}
|
||||
};
|
||||
|
||||
const resolveUsageStatusTag = (t, rateLimit) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
if (!rateLimit || Object.keys(rateLimit).length === 0) {
|
||||
return <Tag color='grey'>{tt('待确认')}</Tag>;
|
||||
}
|
||||
if (rateLimit?.allowed && !rateLimit?.limit_reached) {
|
||||
return <Tag color='green'>{tt('可用')}</Tag>;
|
||||
}
|
||||
return <Tag color='red'>{tt('受限')}</Tag>;
|
||||
};
|
||||
|
||||
const AccountInfoValue = ({ t, value, onCopy, monospace = false }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const text = getDisplayText(value);
|
||||
const hasValue = text !== '';
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 items-start justify-between gap-2'>
|
||||
<div
|
||||
className={`min-w-0 flex-1 break-all text-xs leading-5 text-semi-color-text-1 ${
|
||||
monospace ? 'font-mono' : ''
|
||||
}`}
|
||||
>
|
||||
{hasValue ? text : '-'}
|
||||
</div>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
theme='borderless'
|
||||
className='shrink-0 px-1 text-xs'
|
||||
disabled={!hasValue}
|
||||
onClick={() => onCopy?.(text)}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const hasWindowData =
|
||||
!!windowData &&
|
||||
typeof windowData === 'object' &&
|
||||
Object.keys(windowData).length > 0;
|
||||
const percent = clampPercent(windowData?.used_percent ?? 0);
|
||||
const resetAt = windowData?.reset_at;
|
||||
const resetAfterSeconds = windowData?.reset_after_seconds;
|
||||
@@ -83,26 +232,30 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='mt-2'>
|
||||
<Progress
|
||||
percent={percent}
|
||||
stroke={pickStrokeColor(percent)}
|
||||
showInfo={true}
|
||||
/>
|
||||
</div>
|
||||
{hasWindowData ? (
|
||||
<div className='mt-2'>
|
||||
<Progress
|
||||
percent={percent}
|
||||
stroke={pickStrokeColor(percent)}
|
||||
showInfo={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-3 text-sm text-semi-color-text-2'>-</div>
|
||||
)}
|
||||
|
||||
<div className='mt-1 flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
|
||||
<div>
|
||||
{tt('已使用:')}
|
||||
{percent}%
|
||||
{hasWindowData ? `${percent}%` : '-'}
|
||||
</div>
|
||||
<div>
|
||||
{tt('距离重置:')}
|
||||
{formatDurationSeconds(resetAfterSeconds, tt)}
|
||||
{hasWindowData ? formatDurationSeconds(resetAfterSeconds, tt) : '-'}
|
||||
</div>
|
||||
<div>
|
||||
{tt('窗口:')}
|
||||
{formatDurationSeconds(limitWindowSeconds, tt)}
|
||||
{hasWindowData ? formatDurationSeconds(limitWindowSeconds, tt) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,84 +264,139 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
|
||||
const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const [showRawJson, setShowRawJson] = useState(false);
|
||||
const data = payload?.data ?? null;
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
|
||||
const primary = rateLimit?.primary_window ?? null;
|
||||
const secondary = rateLimit?.secondary_window ?? null;
|
||||
|
||||
const allowed = !!rateLimit?.allowed;
|
||||
const limitReached = !!rateLimit?.limit_reached;
|
||||
const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data);
|
||||
const upstreamStatus = payload?.upstream_status;
|
||||
|
||||
const statusTag =
|
||||
allowed && !limitReached ? (
|
||||
<Tag color='green'>{tt('可用')}</Tag>
|
||||
) : (
|
||||
<Tag color='red'>{tt('受限')}</Tag>
|
||||
);
|
||||
const accountType = data?.plan_type ?? rateLimit?.plan_type;
|
||||
const accountTypeLabel = formatAccountTypeLabel(accountType, tt);
|
||||
const accountTypeTagColor = getAccountTypeTagColor(accountType);
|
||||
const statusTag = resolveUsageStatusTag(tt, rateLimit);
|
||||
const userId = data?.user_id;
|
||||
const email = data?.email;
|
||||
const accountId = data?.account_id;
|
||||
const errorMessage =
|
||||
payload?.success === false ? getDisplayText(payload?.message) || tt('获取用量失败') : '';
|
||||
|
||||
const rawText =
|
||||
typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
{statusTag}
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
theme='borderless'
|
||||
onClick={onRefresh}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
{errorMessage && (
|
||||
<div className='rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-3'>
|
||||
<div className='flex flex-wrap items-start justify-between gap-2'>
|
||||
<div className='min-w-0'>
|
||||
<div className='text-xs font-medium text-semi-color-text-2'>
|
||||
{tt('Codex 帐号')}
|
||||
</div>
|
||||
<div className='mt-2 flex flex-wrap items-center gap-2'>
|
||||
<Tag
|
||||
color={accountTypeTagColor}
|
||||
type='light'
|
||||
shape='circle'
|
||||
size='large'
|
||||
className='font-semibold'
|
||||
>
|
||||
{accountTypeLabel}
|
||||
</Tag>
|
||||
{statusTag}
|
||||
<Tag color='grey' type='light' shape='circle'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<Button size='small' type='tertiary' theme='outline' onClick={onRefresh}>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 rounded-lg bg-semi-color-fill-0 px-3 py-2'>
|
||||
<Descriptions>
|
||||
<Descriptions.Item itemKey='User ID'>
|
||||
<AccountInfoValue
|
||||
t={tt}
|
||||
value={userId}
|
||||
onCopy={onCopy}
|
||||
monospace={true}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey={tt('邮箱')}>
|
||||
<AccountInfoValue t={tt} value={email} onCopy={onCopy} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='Account ID'>
|
||||
<AccountInfoValue
|
||||
t={tt}
|
||||
value={accountId}
|
||||
onCopy={onCopy}
|
||||
monospace={true}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 text-xs text-semi-color-text-2'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Text>
|
||||
<div>
|
||||
<div className='mb-2'>
|
||||
<div className='text-sm font-semibold text-semi-color-text-0'>
|
||||
{tt('额度窗口')}
|
||||
</div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('用于观察当前帐号在 Codex 上游的限额使用情况')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
windowData={fiveHourWindow}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
windowData={weeklyWindow}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between gap-2'>
|
||||
<div className='text-sm font-medium'>{tt('原始 JSON')}</div>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => onCopy?.(rawText)}
|
||||
disabled={!rawText}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||
{rawText}
|
||||
</pre>
|
||||
</div>
|
||||
<Collapse
|
||||
activeKey={showRawJson ? ['raw-json'] : []}
|
||||
onChange={(activeKey) => {
|
||||
const keys = Array.isArray(activeKey) ? activeKey : [activeKey];
|
||||
setShowRawJson(keys.includes('raw-json'));
|
||||
}}
|
||||
>
|
||||
<Collapse.Panel header={tt('原始 JSON')} itemKey='raw-json'>
|
||||
<div className='mb-2 flex justify-end'>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => onCopy?.(rawText)}
|
||||
disabled={!rawText}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className='max-h-[50vh] overflow-y-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||
{rawText}
|
||||
</pre>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -283,7 +491,7 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
|
||||
Modal.info({
|
||||
title: tt('Codex 用量'),
|
||||
title: tt('Codex 帐号与用量'),
|
||||
centered: true,
|
||||
width: 900,
|
||||
style: { maxWidth: '95vw' },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -116,6 +116,8 @@ const renderTokenKey = (
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
t,
|
||||
) => {
|
||||
const revealed = !!showKeys[record.id];
|
||||
const loading = !!loadingTokenKeys[record.id];
|
||||
@@ -145,18 +147,35 @@ const renderTokenKey = (
|
||||
await toggleTokenVisibility(record);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
theme='borderless'
|
||||
size='small'
|
||||
type='tertiary'
|
||||
icon={<IconCopy />}
|
||||
loading={loading}
|
||||
aria-label='copy token key'
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await copyTokenKey(record);
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
clickToHide
|
||||
menu={[
|
||||
{
|
||||
node: 'item',
|
||||
name: t('复制密钥'),
|
||||
onClick: () => copyTokenKey(record),
|
||||
},
|
||||
{
|
||||
node: 'item',
|
||||
name: t('复制连接信息'),
|
||||
onClick: () => copyTokenConnectionString(record),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
theme='borderless'
|
||||
size='small'
|
||||
type='tertiary'
|
||||
icon={<IconCopy />}
|
||||
loading={loading}
|
||||
aria-label='copy token key'
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -444,6 +463,7 @@ export const getTokensColumns = ({
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -484,6 +504,8 @@ export const getTokensColumns = ({
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
t,
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@ const TokensTable = (tokensData) => {
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -60,6 +61,7 @@ const TokensTable = (tokensData) => {
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -73,6 +75,7 @@ const TokensTable = (tokensData) => {
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ParamOverrideEntry = ({ count, onOpen, t }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
style={{ fontVariantNumeric: 'tabular-nums' }}
|
||||
>
|
||||
{t('{{count}} 项操作', { count })}
|
||||
</Text>
|
||||
<Text
|
||||
link
|
||||
size='small'
|
||||
style={{ fontWeight: 600 }}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{t('查看详情')}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ParamOverrideEntry);
|
||||
@@ -25,6 +25,7 @@ import LogsFilters from './UsageLogsFilters';
|
||||
import ColumnSelectorModal from './modals/ColumnSelectorModal';
|
||||
import UserInfoModal from './modals/UserInfoModal';
|
||||
import ChannelAffinityUsageCacheModal from './modals/ChannelAffinityUsageCacheModal';
|
||||
import ParamOverrideModal from './modals/ParamOverrideModal';
|
||||
import { useLogsData } from '../../../hooks/usage-logs/useUsageLogsData';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import { createCardProPagination } from '../../../helpers/utils';
|
||||
@@ -39,6 +40,7 @@ const LogsPage = () => {
|
||||
<ColumnSelectorModal {...logsData} />
|
||||
<UserInfoModal {...logsData} />
|
||||
<ChannelAffinityUsageCacheModal {...logsData} />
|
||||
<ParamOverrideModal {...logsData} />
|
||||
|
||||
{/* Main Content */}
|
||||
<CardPro
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Empty,
|
||||
Divider,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconCopy } from '@douyinfe/semi-icons';
|
||||
import { copy, showError, showSuccess } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const parseAuditLine = (line) => {
|
||||
if (typeof line !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const firstSpaceIndex = line.indexOf(' ');
|
||||
if (firstSpaceIndex <= 0) {
|
||||
return { action: line, content: line };
|
||||
}
|
||||
return {
|
||||
action: line.slice(0, firstSpaceIndex),
|
||||
content: line.slice(firstSpaceIndex + 1),
|
||||
};
|
||||
};
|
||||
|
||||
const getActionLabel = (action, t) => {
|
||||
switch ((action || '').toLowerCase()) {
|
||||
case 'set':
|
||||
return t('设置');
|
||||
case 'delete':
|
||||
return t('删除');
|
||||
case 'copy':
|
||||
return t('复制');
|
||||
case 'move':
|
||||
return t('移动');
|
||||
case 'append':
|
||||
return t('追加');
|
||||
case 'prepend':
|
||||
return t('前置');
|
||||
case 'trim_prefix':
|
||||
return t('去前缀');
|
||||
case 'trim_suffix':
|
||||
return t('去后缀');
|
||||
case 'ensure_prefix':
|
||||
return t('保前缀');
|
||||
case 'ensure_suffix':
|
||||
return t('保后缀');
|
||||
case 'trim_space':
|
||||
return t('去空格');
|
||||
case 'to_lower':
|
||||
return t('转小写');
|
||||
case 'to_upper':
|
||||
return t('转大写');
|
||||
case 'replace':
|
||||
return t('替换');
|
||||
case 'regex_replace':
|
||||
return t('正则替换');
|
||||
case 'set_header':
|
||||
return t('设请求头');
|
||||
case 'delete_header':
|
||||
return t('删请求头');
|
||||
case 'copy_header':
|
||||
return t('复制请求头');
|
||||
case 'move_header':
|
||||
return t('移动请求头');
|
||||
case 'pass_headers':
|
||||
return t('透传请求头');
|
||||
case 'sync_fields':
|
||||
return t('同步字段');
|
||||
case 'return_error':
|
||||
return t('返回错误');
|
||||
default:
|
||||
return action;
|
||||
}
|
||||
};
|
||||
|
||||
const ParamOverrideModal = ({
|
||||
showParamOverrideModal,
|
||||
setShowParamOverrideModal,
|
||||
paramOverrideTarget,
|
||||
t,
|
||||
}) => {
|
||||
const lines = Array.isArray(paramOverrideTarget?.lines)
|
||||
? paramOverrideTarget.lines
|
||||
: [];
|
||||
|
||||
const parsedLines = useMemo(() => {
|
||||
return lines.map(parseAuditLine);
|
||||
}, [lines]);
|
||||
|
||||
const copyAll = async () => {
|
||||
const content = lines.join('\n');
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
if (await copy(content)) {
|
||||
showSuccess(t('参数覆盖已复制'));
|
||||
return;
|
||||
}
|
||||
showError(t('无法复制到剪贴板,请手动复制'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('参数覆盖详情')}
|
||||
visible={showParamOverrideModal}
|
||||
onCancel={() => setShowParamOverrideModal(false)}
|
||||
footer={null}
|
||||
centered
|
||||
closable
|
||||
maskClosable
|
||||
width={640}
|
||||
>
|
||||
<div style={{ padding: '8px 20px 20px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
gap: 12,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text style={{ fontWeight: 600 }}>
|
||||
{t('{{count}} 项操作', { count: lines.length })}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
fontSize: 12,
|
||||
color: 'var(--semi-color-text-2)',
|
||||
}}
|
||||
>
|
||||
{paramOverrideTarget?.modelName ? (
|
||||
<Text type='tertiary' size='small'>
|
||||
{paramOverrideTarget.modelName}
|
||||
</Text>
|
||||
) : null}
|
||||
{paramOverrideTarget?.requestId ? (
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('Request ID')}: {paramOverrideTarget.requestId}
|
||||
</Text>
|
||||
) : null}
|
||||
{paramOverrideTarget?.requestPath ? (
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('请求路径')}: {paramOverrideTarget.requestPath}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={<IconCopy />}
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
size='small'
|
||||
onClick={copyAll}
|
||||
disabled={lines.length === 0}
|
||||
>
|
||||
{t('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider margin='12px' />
|
||||
|
||||
{lines.length === 0 ? (
|
||||
<Empty
|
||||
description={t('暂无参数覆盖记录')}
|
||||
style={{ padding: '24px 0 8px' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
maxHeight: '56vh',
|
||||
overflowY: 'auto',
|
||||
paddingRight: 2,
|
||||
}}
|
||||
>
|
||||
{parsedLines.map((item, index) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${item.action}-${index}`}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
background: 'var(--semi-color-fill-0)',
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
minWidth: 74,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
lineHeight: '20px',
|
||||
padding: '0 8px',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(var(--semi-blue-5), 0.12)',
|
||||
color: 'var(--semi-color-primary)',
|
||||
}}
|
||||
>
|
||||
{getActionLabel(item.action, t)}
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
}}
|
||||
>
|
||||
{item.content}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParamOverrideModal;
|
||||
@@ -87,6 +87,9 @@ const RechargeCard = ({
|
||||
statusLoading,
|
||||
topupInfo,
|
||||
onOpenHistory,
|
||||
enableWaffoTopUp,
|
||||
waffoTopUp,
|
||||
waffoPayMethods,
|
||||
subscriptionLoading = false,
|
||||
subscriptionPlans = [],
|
||||
billingPreference,
|
||||
@@ -224,19 +227,19 @@ const RechargeCard = ({
|
||||
<div className='py-8 flex justify-center'>
|
||||
<Spin size='large' />
|
||||
</div>
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? (
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
|
||||
<Form
|
||||
getFormApi={(api) => (onlineFormApiRef.current = api)}
|
||||
initValues={{ topUpCount: topUpCount }}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
|
||||
<Row gutter={12}>
|
||||
<Col xs={24} sm={24} md={24} lg={10} xl={10}>
|
||||
<Form.InputNumber
|
||||
field='topUpCount'
|
||||
label={t('充值数量')}
|
||||
disabled={!enableOnlineTopUp && !enableStripeTopUp}
|
||||
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWaffoTopUp}
|
||||
placeholder={
|
||||
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
|
||||
}
|
||||
@@ -288,11 +291,11 @@ const RechargeCard = ({
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
{payMethods && payMethods.filter(m => m.type !== 'waffo').length > 0 && (
|
||||
<Col xs={24} sm={24} md={24} lg={14} xl={14}>
|
||||
<Form.Slot label={t('选择支付方式')}>
|
||||
{payMethods && payMethods.length > 0 ? (
|
||||
<Space wrap>
|
||||
{payMethods.map((payMethod) => {
|
||||
{payMethods.filter(m => m.type !== 'waffo').map((payMethod) => {
|
||||
const minTopupVal = Number(payMethod.min_topup) || 0;
|
||||
const isStripe = payMethod.type === 'stripe';
|
||||
const disabled =
|
||||
@@ -352,17 +355,13 @@ const RechargeCard = ({
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
) : (
|
||||
<div className='text-gray-500 text-sm p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300'>
|
||||
{t('暂无可用的支付方式,请联系管理员配置')}
|
||||
</div>
|
||||
)}
|
||||
</Form.Slot>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
{(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
|
||||
<Form.Slot
|
||||
label={
|
||||
<div className='flex items-center gap-2'>
|
||||
@@ -483,6 +482,46 @@ const RechargeCard = ({
|
||||
</Form.Slot>
|
||||
)}
|
||||
|
||||
{/* Waffo 充值区域 */}
|
||||
{enableWaffoTopUp &&
|
||||
waffoPayMethods &&
|
||||
waffoPayMethods.length > 0 && (
|
||||
<Form.Slot label={t('Waffo 充值')}>
|
||||
<Space wrap>
|
||||
{waffoPayMethods.map((method, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
onClick={() => waffoTopUp(index)}
|
||||
loading={paymentLoading}
|
||||
icon={
|
||||
method.icon ? (
|
||||
<img
|
||||
src={method.icon}
|
||||
alt={method.name}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CreditCard
|
||||
size={18}
|
||||
color='var(--semi-color-text-2)'
|
||||
/>
|
||||
)
|
||||
}
|
||||
className='!rounded-lg !px-4 !py-2'
|
||||
>
|
||||
{method.name}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Form.Slot>
|
||||
)}
|
||||
|
||||
{/* Creem 充值区域 */}
|
||||
{enableCreemTopUp && creemProducts.length > 0 && (
|
||||
<Form.Slot label={t('Creem 充值')}>
|
||||
|
||||
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useContext, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal';
|
||||
|
||||
const TopUp = () => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState] = useContext(StatusContext);
|
||||
|
||||
@@ -69,6 +71,11 @@ const TopUp = () => {
|
||||
const [creemOpen, setCreemOpen] = useState(false);
|
||||
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
|
||||
|
||||
// Waffo 相关状态
|
||||
const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
|
||||
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
|
||||
const [waffoMinTopUp, setWaffoMinTopUp] = useState(1);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [payWay, setPayWay] = useState('');
|
||||
@@ -256,7 +263,6 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setOpen(false);
|
||||
@@ -302,7 +308,6 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setCreemOpen(false);
|
||||
@@ -310,6 +315,37 @@ const TopUp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const waffoTopUp = async (payMethodIndex) => {
|
||||
try {
|
||||
if (topUpCount < waffoMinTopUp) {
|
||||
showError(t('充值数量不能小于') + waffoMinTopUp);
|
||||
return;
|
||||
}
|
||||
setPaymentLoading(true);
|
||||
const requestBody = {
|
||||
amount: parseInt(topUpCount),
|
||||
};
|
||||
if (payMethodIndex != null) {
|
||||
requestBody.pay_method_index = payMethodIndex;
|
||||
}
|
||||
const res = await API.post('/api/user/waffo/pay', requestBody);
|
||||
if (res !== undefined) {
|
||||
const { message, data } = res.data;
|
||||
if (message === 'success' && data?.payment_url) {
|
||||
window.open(data.payment_url, '_blank');
|
||||
} else {
|
||||
showError(data || t('支付请求失败'));
|
||||
}
|
||||
} else {
|
||||
showError(res);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setPaymentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processCreemCallback = (data) => {
|
||||
// 与 Stripe 保持一致的实现方式
|
||||
window.open(data.checkout_url, '_blank');
|
||||
@@ -449,17 +485,21 @@ const TopUp = () => {
|
||||
? data.min_topup
|
||||
: enableStripeTopUp
|
||||
? data.stripe_min_topup
|
||||
: 1;
|
||||
: data.enable_waffo_topup
|
||||
? data.waffo_min_topup
|
||||
: 1;
|
||||
setEnableOnlineTopUp(enableOnlineTopUp);
|
||||
setEnableStripeTopUp(enableStripeTopUp);
|
||||
setEnableCreemTopUp(enableCreemTopUp);
|
||||
const enableWaffoTopUp = data.enable_waffo_topup || false;
|
||||
setEnableWaffoTopUp(enableWaffoTopUp);
|
||||
setWaffoPayMethods(data.waffo_pay_methods || []);
|
||||
setWaffoMinTopUp(data.waffo_min_topup || 1);
|
||||
setMinTopUp(minTopUpValue);
|
||||
setTopUpCount(minTopUpValue);
|
||||
|
||||
// 设置 Creem 产品
|
||||
try {
|
||||
console.log(' data is ?', data);
|
||||
console.log(' creem products is ?', data.creem_products);
|
||||
const products = JSON.parse(data.creem_products || '[]');
|
||||
setCreemProducts(products);
|
||||
} catch (e) {
|
||||
@@ -474,7 +514,6 @@ const TopUp = () => {
|
||||
// 初始化显示实付金额
|
||||
getAmount(minTopUpValue);
|
||||
} catch (e) {
|
||||
console.log('解析支付方式失败:', e);
|
||||
setPayMethods([]);
|
||||
}
|
||||
|
||||
@@ -487,10 +526,10 @@ const TopUp = () => {
|
||||
setPresetAmounts(customPresets);
|
||||
}
|
||||
} else {
|
||||
console.error('获取充值配置失败:', data);
|
||||
showError(data || t('获取充值配置失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取充值配置异常:', error);
|
||||
showError(t('获取充值配置异常'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -531,6 +570,15 @@ const TopUp = () => {
|
||||
showSuccess(t('邀请链接已复制到剪切板'));
|
||||
};
|
||||
|
||||
// URL 参数自动打开账单弹窗(支付回跳时触发)
|
||||
useEffect(() => {
|
||||
if (searchParams.get('show_history') === 'true') {
|
||||
setOpenHistory(true);
|
||||
searchParams.delete('show_history');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 始终获取最新用户数据,确保余额等统计信息准确
|
||||
getUserQuota().then();
|
||||
@@ -587,7 +635,7 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// amount fetch failed silently
|
||||
}
|
||||
setAmountLoading(false);
|
||||
};
|
||||
@@ -613,7 +661,7 @@ const TopUp = () => {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// amount fetch failed silently
|
||||
} finally {
|
||||
setAmountLoading(false);
|
||||
}
|
||||
@@ -740,6 +788,9 @@ const TopUp = () => {
|
||||
enableCreemTopUp={enableCreemTopUp}
|
||||
creemProducts={creemProducts}
|
||||
creemPreTopUp={creemPreTopUp}
|
||||
enableWaffoTopUp={enableWaffoTopUp}
|
||||
waffoTopUp={waffoTopUp}
|
||||
waffoPayMethods={waffoPayMethods}
|
||||
presetAmounts={presetAmounts}
|
||||
selectedPreset={selectedPreset}
|
||||
selectPresetAmount={selectPresetAmount}
|
||||
|
||||
@@ -37,13 +37,13 @@ import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { API, timestamp2string } from '../../../helpers';
|
||||
import { isAdmin } from '../../../helpers/utils';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 状态映射配置
|
||||
const STATUS_CONFIG = {
|
||||
success: { type: 'success', key: '成功' },
|
||||
pending: { type: 'warning', key: '待支付' },
|
||||
failed: { type: 'danger', key: '失败' },
|
||||
expired: { type: 'danger', key: '已过期' },
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ const STATUS_CONFIG = {
|
||||
const PAYMENT_METHOD_MAP = {
|
||||
stripe: 'Stripe',
|
||||
creem: 'Creem',
|
||||
waffo: 'Waffo',
|
||||
alipay: '支付宝',
|
||||
wxpay: '微信',
|
||||
};
|
||||
@@ -62,7 +63,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const loadTopups = async (currentPage, currentPageSize) => {
|
||||
@@ -82,7 +82,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
Toast.error({ content: message || t('加载失败') });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load topups error:', error);
|
||||
Toast.error({ content: t('加载账单失败') });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -214,17 +213,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => {
|
||||
if (record.status !== 'pending') return null;
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
const actions = [];
|
||||
if (record.status === 'pending') {
|
||||
actions.push(
|
||||
<Button
|
||||
key="complete"
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return actions.length > 0 ? <>{actions}</> : null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user