🎨 style: format all code with gofmt and lint:fix

Apply consistent code formatting across the entire codebase using
gofmt and lint:fix tools. This ensures adherence to Go community
standards and improves code readability and maintainability.

Changes include:
- Run gofmt on all .go files to standardize formatting
- Apply lint:fix to automatically resolve linting issues
- Fix code style inconsistencies and formatting violations

No functional changes were made in this commit.
This commit is contained in:
t0ng7u
2026-01-30 23:43:27 +08:00
parent 697cbbf752
commit ecf50b754a
68 changed files with 1587 additions and 1148 deletions

View File

@@ -109,7 +109,9 @@ const renderType = (type, record = {}, t) => {
<Tooltip
content={
<div className='max-w-xs'>
<div className='text-xs text-gray-600'>{t('来源于 IO.NET 部署')}</div>
<div className='text-xs text-gray-600'>
{t('来源于 IO.NET 部署')}
</div>
{ionetMeta?.deployment_id && (
<div className='text-xs text-gray-500 mt-1'>
{t('部署 ID')}: {ionetMeta.deployment_id}

View File

@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal, Button, Space, Typography, Input, Banner } from '@douyinfe/semi-ui';
import {
Modal,
Button,
Space,
Typography,
Input,
Banner,
} from '@douyinfe/semi-ui';
import { API, copy, showError, showSuccess } from '../../../../helpers';
const { Text } = Typography;
@@ -33,14 +40,21 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
const startOAuth = async () => {
setLoading(true);
try {
const res = await API.post('/api/channel/codex/oauth/start', {}, { skipErrorHandler: true });
const res = await API.post(
'/api/channel/codex/oauth/start',
{},
{ skipErrorHandler: true },
);
if (!res?.data?.success) {
console.error('Codex OAuth start failed:', res?.data?.message);
throw new Error(t('启动授权失败'));
}
const url = res?.data?.data?.authorize_url || '';
if (!url) {
console.error('Codex OAuth start response missing authorize_url:', res?.data);
console.error(
'Codex OAuth start response missing authorize_url:',
res?.data,
);
throw new Error(t('响应缺少授权链接'));
}
setAuthorizeUrl(url);
@@ -106,7 +120,12 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
<Button theme='borderless' onClick={onCancel} disabled={loading}>
{t('取消')}
</Button>
<Button theme='solid' type='primary' onClick={completeOAuth} loading={loading}>
<Button
theme='solid'
type='primary'
onClick={completeOAuth}
loading={loading}
>
{t('生成并填入')}
</Button>
</Space>
@@ -141,7 +160,9 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
/>
<Text type='tertiary' size='small'>
{t('说明:生成结果是可直接粘贴到渠道密钥里的 JSON包含 access_token / refresh_token / account_id。')}
{t(
'说明:生成结果是可直接粘贴到渠道密钥里的 JSON包含 access_token / refresh_token / account_id。',
)}
</Text>
</Space>
</Modal>

View File

@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Modal, Button, Progress, Tag, Typography, Spin } from '@douyinfe/semi-ui';
import {
Modal,
Button,
Progress,
Tag,
Typography,
Spin,
} from '@douyinfe/semi-ui';
import { API, showError } from '../../../../helpers';
const { Text } = Typography;
@@ -134,7 +141,12 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
</Text>
<div className='flex items-center gap-2'>
{statusTag}
<Button size='small' type='tertiary' theme='borderless' onClick={onRefresh}>
<Button
size='small'
type='tertiary'
theme='borderless'
onClick={onRefresh}
>
{tt('刷新')}
</Button>
</div>
@@ -243,7 +255,12 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
<div className='flex flex-col gap-3'>
<Text type='danger'>{tt('获取用量失败')}</Text>
<div className='flex justify-end'>
<Button size='small' type='primary' theme='outline' onClick={fetchUsage}>
<Button
size='small'
type='primary'
theme='outline'
onClick={fetchUsage}
>
{tt('刷新')}
</Button>
</div>

View File

@@ -2000,171 +2000,180 @@ const EditChannelModal = (props) => {
autoComplete='new-password'
onChange={(value) => handleInputChange('key', value)}
disabled={isIonetLocked}
extraText={
<div className='flex items-center gap-2 flex-wrap'>
{isEdit &&
isMultiKeyChannel &&
keyMode === 'append' && (
<Text type='warning' size='small'>
{t(
'追加模式:新密钥将添加到现有密钥列表的末尾',
)}
</Text>
extraText={
<div className='flex items-center gap-2 flex-wrap'>
{isEdit &&
isMultiKeyChannel &&
keyMode === 'append' && (
<Text type='warning' size='small'>
{t(
'追加模式:新密钥将添加到现有密钥列表的末尾',
)}
</Text>
)}
{isEdit && (
<Button
size='small'
type='primary'
theme='outline'
onClick={handleShow2FAModal}
>
{t('查看密钥')}
</Button>
)}
{isEdit && (
<Button
size='small'
type='primary'
theme='outline'
onClick={handleShow2FAModal}
>
{t('查看密钥')}
</Button>
)}
{batchExtra}
</div>
}
showClear
/>
)
) : (
<>
{inputs.type === 57 ? (
<>
<Form.TextArea
field='key'
label={
isEdit
? t('密钥(编辑模式下,保存的密钥不会显示)')
: t('密钥')
}
placeholder={t(
'请输入 JSON 格式的 OAuth 凭据,例如:\n{\n "access_token": "...",\n "account_id": "..." \n}',
)}
rules={
isEdit
? []
: [{ required: true, message: t('请输入密钥') }]
}
autoComplete='new-password'
onChange={(value) => handleInputChange('key', value)}
disabled={isIonetLocked}
extraText={
<div className='flex flex-col gap-2'>
<Text type='tertiary' size='small'>
{t(
'仅支持 JSON 对象,必须包含 access_token 与 account_id',
)}
</Text>
{batchExtra}
</div>
}
showClear
/>
)
) : (
<>
{inputs.type === 57 ? (
<>
<Form.TextArea
field='key'
label={
isEdit
? t('密钥(编辑模式下,保存的密钥不会显示)')
: t('密钥')
}
placeholder={t(
'请输入 JSON 格式的 OAuth 凭据,例如:\n{\n "access_token": "...",\n "account_id": "..." \n}',
)}
rules={
isEdit
? []
: [
{
required: true,
message: t('请输入密钥'),
},
]
}
autoComplete='new-password'
onChange={(value) =>
handleInputChange('key', value)
}
disabled={isIonetLocked}
extraText={
<div className='flex flex-col gap-2'>
<Text type='tertiary' size='small'>
{t(
'仅支持 JSON 对象,必须包含 access_token 与 account_id',
)}
</Text>
<Space wrap spacing='tight'>
<Space wrap spacing='tight'>
<Button
size='small'
type='primary'
theme='outline'
onClick={() =>
setCodexOAuthModalVisible(true)
}
disabled={isIonetLocked}
>
{t('Codex 授权')}
</Button>
{isEdit && (
<Button
size='small'
type='primary'
theme='outline'
onClick={handleRefreshCodexCredential}
loading={codexCredentialRefreshing}
disabled={isIonetLocked}
>
{t('刷新凭证')}
</Button>
)}
<Button
size='small'
type='primary'
theme='outline'
onClick={() => formatJsonField('key')}
disabled={isIonetLocked}
>
{t('格式化')}
</Button>
{isEdit && (
<Button
size='small'
type='primary'
theme='outline'
onClick={handleShow2FAModal}
disabled={isIonetLocked}
>
{t('查看密钥')}
</Button>
)}
{batchExtra}
</Space>
</div>
}
autosize
showClear
/>
<CodexOAuthModal
visible={codexOAuthModalVisible}
onCancel={() => setCodexOAuthModalVisible(false)}
onSuccess={handleCodexOAuthGenerated}
/>
</>
) : inputs.type === 41 &&
(inputs.vertex_key_type || 'json') === 'json' ? (
<>
{!batch && (
<div className='flex items-center justify-between mb-3'>
<Text className='text-sm font-medium'>
{t('密钥输入方式')}
</Text>
<Space>
<Button
size='small'
type='primary'
theme='outline'
onClick={() =>
setCodexOAuthModalVisible(true)
type={
!useManualInput ? 'primary' : 'tertiary'
}
disabled={isIonetLocked}
onClick={() => {
setUseManualInput(false);
// 切换到文件上传模式时清空手动输入的密钥
if (formApiRef.current) {
formApiRef.current.setValue('key', '');
}
handleInputChange('key', '');
}}
>
{t('Codex 授权')}
{t('文件上传')}
</Button>
{isEdit && (
<Button
size='small'
type='primary'
theme='outline'
onClick={handleRefreshCodexCredential}
loading={codexCredentialRefreshing}
disabled={isIonetLocked}
>
{t('刷新凭证')}
</Button>
)}
<Button
size='small'
type='primary'
theme='outline'
onClick={() => formatJsonField('key')}
disabled={isIonetLocked}
type={
useManualInput ? 'primary' : 'tertiary'
}
onClick={() => {
setUseManualInput(true);
// 切换到手动输入模式时清空文件上传相关状态
setVertexKeys([]);
setVertexFileList([]);
if (formApiRef.current) {
formApiRef.current.setValue(
'vertex_files',
[],
);
}
setInputs((prev) => ({
...prev,
vertex_files: [],
}));
}}
>
{t('格式化')}
{t('手动输入')}
</Button>
{isEdit && (
<Button
size='small'
type='primary'
theme='outline'
onClick={handleShow2FAModal}
disabled={isIonetLocked}
>
{t('查看密钥')}
</Button>
)}
{batchExtra}
</Space>
</div>
}
autosize
showClear
/>
<CodexOAuthModal
visible={codexOAuthModalVisible}
onCancel={() => setCodexOAuthModalVisible(false)}
onSuccess={handleCodexOAuthGenerated}
/>
</>
) : inputs.type === 41 &&
(inputs.vertex_key_type || 'json') === 'json' ? (
<>
{!batch && (
<div className='flex items-center justify-between mb-3'>
<Text className='text-sm font-medium'>
{t('密钥输入方式')}
</Text>
<Space>
<Button
size='small'
type={
!useManualInput ? 'primary' : 'tertiary'
}
onClick={() => {
setUseManualInput(false);
// 切换到文件上传模式时清空手动输入的密钥
if (formApiRef.current) {
formApiRef.current.setValue('key', '');
}
handleInputChange('key', '');
}}
>
{t('文件上传')}
</Button>
<Button
size='small'
type={useManualInput ? 'primary' : 'tertiary'}
onClick={() => {
setUseManualInput(true);
// 切换到手动输入模式时清空文件上传相关状态
setVertexKeys([]);
setVertexFileList([]);
if (formApiRef.current) {
formApiRef.current.setValue(
'vertex_files',
[],
);
}
setInputs((prev) => ({
...prev,
vertex_files: [],
}));
}}
>
{t('手动输入')}
</Button>
</Space>
</div>
)}
)}
{batch && (
<Banner

View File

@@ -533,7 +533,11 @@ const EditTagModal = (props) => {
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
{/* Header: Advanced Settings */}
<div className='flex items-center mb-2'>
<Avatar size='small' color='orange' className='mr-2 shadow-md'>
<Avatar
size='small'
color='orange'
className='mr-2 shadow-md'
>
<IconSetting size={16} />
</Avatar>
<div>
@@ -549,9 +553,7 @@ const EditTagModal = (props) => {
field='param_override'
label={t('参数覆盖')}
placeholder={
t(
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
) +
t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数') +
'\n' +
t('旧格式(直接覆盖):') +
'\n{\n "temperature": 0,\n "max_tokens": 1000\n}' +

View File

@@ -104,7 +104,9 @@ const ModelSelectModal = ({
}, [normalizedRedirectModels, normalizedSelectedSet]);
const filteredModels = models.filter((m) =>
String(m || '').toLowerCase().includes(keyword.toLowerCase()),
String(m || '')
.toLowerCase()
.includes(keyword.toLowerCase()),
);
// 分类模型:新获取的模型和已有模型

View File

@@ -30,7 +30,7 @@ const ConfirmationDialog = ({
type = 'danger',
deployment,
t,
loading = false
loading = false,
}) => {
const [confirmText, setConfirmText] = useState('');
@@ -66,17 +66,17 @@ const ConfirmationDialog = ({
okButtonProps={{
disabled: !isConfirmed,
type: type === 'danger' ? 'danger' : 'primary',
loading
loading,
}}
width={480}
>
<div className="space-y-4">
<Text type="danger" strong>
<div className='space-y-4'>
<Text type='danger' strong>
{t('此操作具有风险,请确认要继续执行')}
</Text>
<Text>
{t('请输入部署名称以完成二次确认')}
<Text code className="ml-1">
<Text code className='ml-1'>
{requiredText || t('未知部署')}
</Text>
</Text>
@@ -87,7 +87,7 @@ const ConfirmationDialog = ({
autoFocus
/>
{!isConfirmed && confirmText && (
<Text type="danger" size="small">
<Text type='danger' size='small'>
{t('部署名称不匹配,请检查后重新输入')}
</Text>
)}

View File

@@ -130,9 +130,7 @@ const ExtendDurationModal = ({
? details.locations
.map((location) =>
Number(
location?.id ??
location?.location_id ??
location?.locationId,
location?.id ?? location?.location_id ?? location?.locationId,
),
)
.filter((id) => Number.isInteger(id) && id > 0)
@@ -181,9 +179,7 @@ const ExtendDurationModal = ({
} else {
const message = response.data.message || '';
setPriceEstimation(null);
setPriceError(
t('价格计算失败') + (message ? `: ${message}` : ''),
);
setPriceError(t('价格计算失败') + (message ? `: ${message}` : ''));
}
} catch (error) {
if (costRequestIdRef.current !== requestId) {
@@ -192,9 +188,7 @@ const ExtendDurationModal = ({
const message = error?.response?.data?.message || error.message || '';
setPriceEstimation(null);
setPriceError(
t('价格计算失败') + (message ? `: ${message}` : ''),
);
setPriceError(t('价格计算失败') + (message ? `: ${message}` : ''));
} finally {
if (costRequestIdRef.current === requestId) {
setCostLoading(false);
@@ -269,11 +263,8 @@ const ExtendDurationModal = ({
const newTotalTime = `${currentRemainingTime} + ${durationHours}${t('小时')}`;
const priceData = priceEstimation || {};
const breakdown =
priceData.price_breakdown || priceData.PriceBreakdown || {};
const currencyLabel = (
priceData.currency || priceData.Currency || 'USDC'
)
const breakdown = priceData.price_breakdown || priceData.PriceBreakdown || {};
const currencyLabel = (priceData.currency || priceData.Currency || 'USDC')
.toString()
.toUpperCase();
@@ -316,7 +307,10 @@ const ExtendDurationModal = ({
confirmLoading={loading}
okButtonProps={{
disabled:
!deployment?.id || detailsLoading || !durationHours || durationHours < 1,
!deployment?.id ||
detailsLoading ||
!durationHours ||
durationHours < 1,
}}
width={600}
className='extend-duration-modal'
@@ -357,9 +351,7 @@ const ExtendDurationModal = ({
<p>
{t('延长容器时长将会产生额外费用,请确认您有足够的账户余额。')}
</p>
<p>
{t('延长操作一旦确认无法撤销,费用将立即扣除。')}
</p>
<p>{t('延长操作一旦确认无法撤销,费用将立即扣除。')}</p>
</div>
}
/>
@@ -370,7 +362,9 @@ const ExtendDurationModal = ({
onValueChange={(values) => {
if (values.duration_hours !== undefined) {
const numericValue = Number(values.duration_hours);
setDurationHours(Number.isFinite(numericValue) ? numericValue : 0);
setDurationHours(
Number.isFinite(numericValue) ? numericValue : 0,
);
}
}}
>

View File

@@ -81,7 +81,9 @@ const renderModels = (text, record, t) => {
</div>
))}
{items.length > 3 && (
<div className='text-gray-500'>...{t('共')} {items.length} {t('个模型')}</div>
<div className='text-gray-500'>
...{t('共')} {items.length} {t('个模型')}
</div>
)}
</div>
);

View File

@@ -27,14 +27,8 @@ import {
import { getSubscriptionsColumns } from './SubscriptionsColumnDefs';
const SubscriptionsTable = (subscriptionsData) => {
const {
plans,
loading,
compactMode,
openEdit,
disablePlan,
t,
} = subscriptionsData;
const { plans, loading, compactMode, openEdit, disablePlan, t } =
subscriptionsData;
const columns = useMemo(() => {
return getSubscriptionsColumns({
@@ -47,12 +41,12 @@ const SubscriptionsTable = (subscriptionsData) => {
const tableColumns = useMemo(() => {
return compactMode
? columns.map((col) => {
if (col.dataIndex === 'operate') {
const { fixed, ...rest } = col;
return rest;
}
return col;
})
if (col.dataIndex === 'operate') {
const { fixed, ...rest } = col;
return rest;
}
return col;
})
: columns;
}, [compactMode, columns]);

View File

@@ -169,7 +169,10 @@ const AddEditSubscriptionModal = ({
next.push({
model_name: modelName,
quota_type: modelMeta.quota_type,
amount_total: Number.isFinite(defaultAmount) && defaultAmount >= 0 ? defaultAmount : 0,
amount_total:
Number.isFinite(defaultAmount) && defaultAmount >= 0
? defaultAmount
: 0,
});
});
setItems(next);
@@ -216,7 +219,9 @@ const AddEditSubscriptionModal = ({
return;
}
const keySet = new Set(selectedRowKeys);
const next = (items || []).filter((it) => !keySet.has(`${it.model_name}-${it.quota_type}`));
const next = (items || []).filter(
(it) => !keySet.has(`${it.model_name}-${it.quota_type}`),
);
setItems(next);
setSelectedRowKeys([]);
showSuccess(t('已删除选中项'));
@@ -417,7 +422,9 @@ const AddEditSubscriptionModal = ({
field='title'
label={t('套餐标题')}
placeholder={t('例如:基础套餐')}
rules={[{ required: true, message: t('请输入套餐标题') }]}
rules={[
{ required: true, message: t('请输入套餐标题') },
]}
showClear
/>
</Col>
@@ -585,7 +592,9 @@ const AddEditSubscriptionModal = ({
<Boxes size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>{t('模型权益')}</Text>
<Text className='text-lg font-medium'>
{t('模型权益')}
</Text>
<div className='text-xs text-gray-600'>
{t('配置套餐可使用的模型及额度')}
</div>
@@ -646,7 +655,9 @@ const AddEditSubscriptionModal = ({
<Button
theme='light'
type='primary'
onClick={() => applyBulkAmountTotal({ scope: 'selected' })}
onClick={() =>
applyBulkAmountTotal({ scope: 'selected' })
}
>
{t('应用到选中')}
</Button>

View File

@@ -378,7 +378,12 @@ const EditTokenModal = (props) => {
/>
)}
</Col>
<Col span={24} style={{ display: values.group === 'auto' ? 'block' : 'none' }}>
<Col
span={24}
style={{
display: values.group === 'auto' ? 'block' : 'none',
}}
>
<Form.Switch
field='cross_group_retry'
label={t('跨分组重试')}
@@ -561,7 +566,9 @@ const EditTokenModal = (props) => {
placeholder={t('允许的IP一行一个不填写则不限制')}
autosize
rows={1}
extraText={t('请勿过度信任此功能IP可能被伪造请配合nginx和cdn等网关使用')}
extraText={t(
'请勿过度信任此功能IP可能被伪造请配合nginx和cdn等网关使用',
)}
showClear
style={{ width: '100%' }}
/>

View File

@@ -203,7 +203,7 @@ function renderModelName(record, copyText, t) {
if (!modelMapped) {
return renderModelTag(record.model_name, {
onClick: (event) => {
copyText(event, record.model_name).then((r) => { });
copyText(event, record.model_name).then((r) => {});
},
});
} else {
@@ -220,7 +220,7 @@ function renderModelName(record, copyText, t) {
</Typography.Text>
{renderModelTag(record.model_name, {
onClick: (event) => {
copyText(event, record.model_name).then((r) => { });
copyText(event, record.model_name).then((r) => {});
},
})}
</div>
@@ -231,7 +231,7 @@ function renderModelName(record, copyText, t) {
{renderModelTag(other.upstream_model_name, {
onClick: (event) => {
copyText(event, other.upstream_model_name).then(
(r) => { },
(r) => {},
);
},
})}
@@ -242,7 +242,7 @@ function renderModelName(record, copyText, t) {
>
{renderModelTag(record.model_name, {
onClick: (event) => {
copyText(event, record.model_name).then((r) => { });
copyText(event, record.model_name).then((r) => {});
},
suffixIcon: (
<Route
@@ -581,7 +581,11 @@ export const getLogsColumns = ({
}
>
<span>
<Tag className='channel-affinity-tag' color='cyan' shape='circle'>
<Tag
className='channel-affinity-tag'
color='cyan'
shape='circle'
>
<span className='channel-affinity-tag-content'>
<IconStarStroked style={{ fontSize: 13 }} />
{t('优选')}
@@ -653,45 +657,45 @@ export const getLogsColumns = ({
let content = other?.claude
? renderModelPriceSimple(
other.model_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
other.cache_tokens || 0,
other.cache_ratio || 1.0,
other.cache_creation_tokens || 0,
other.cache_creation_ratio || 1.0,
other.cache_creation_tokens_5m || 0,
other.cache_creation_ratio_5m ||
other.cache_creation_ratio ||
1.0,
other.cache_creation_tokens_1h || 0,
other.cache_creation_ratio_1h ||
other.cache_creation_ratio ||
1.0,
false,
1.0,
other?.is_system_prompt_overwritten,
'claude',
)
other.model_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
other.cache_tokens || 0,
other.cache_ratio || 1.0,
other.cache_creation_tokens || 0,
other.cache_creation_ratio || 1.0,
other.cache_creation_tokens_5m || 0,
other.cache_creation_ratio_5m ||
other.cache_creation_ratio ||
1.0,
other.cache_creation_tokens_1h || 0,
other.cache_creation_ratio_1h ||
other.cache_creation_ratio ||
1.0,
false,
1.0,
other?.is_system_prompt_overwritten,
'claude',
)
: renderModelPriceSimple(
other.model_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
other.cache_tokens || 0,
other.cache_ratio || 1.0,
0,
1.0,
0,
1.0,
0,
1.0,
false,
1.0,
other?.is_system_prompt_overwritten,
'openai',
);
other.model_ratio,
other.model_price,
other.group_ratio,
other?.user_group_ratio,
other.cache_tokens || 0,
other.cache_ratio || 1.0,
0,
1.0,
0,
1.0,
0,
1.0,
false,
1.0,
other?.is_system_prompt_overwritten,
'openai',
);
// Do not add billing source here; keep details clean.
const summary = [content, text ? `${t('详情')}${text}` : null]
.filter(Boolean)

View File

@@ -121,4 +121,3 @@ const BindSubscriptionModal = ({ visible, onCancel, user, t, onSuccess }) => {
};
export default BindSubscriptionModal;

View File

@@ -129,7 +129,9 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
if (!user?.id) return;
setLoading(true);
try {
const res = await API.get(`/api/subscription/admin/users/${user.id}/subscriptions`);
const res = await API.get(
`/api/subscription/admin/users/${user.id}/subscriptions`,
);
if (res.data?.success) {
const next = res.data.data || [];
setSubs(next);
@@ -167,9 +169,12 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
}
setCreating(true);
try {
const res = await API.post(`/api/subscription/admin/users/${user.id}/subscriptions`, {
plan_id: selectedPlanId,
});
const res = await API.post(
`/api/subscription/admin/users/${user.id}/subscriptions`,
{
plan_id: selectedPlanId,
},
);
if (res.data?.success) {
showSuccess(t('新增成功'));
setSelectedPlanId(null);
@@ -217,7 +222,9 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
okType: 'danger',
onOk: async () => {
try {
const res = await API.delete(`/api/subscription/admin/user_subscriptions/${subId}`);
const res = await API.delete(
`/api/subscription/admin/user_subscriptions/${subId}`,
);
if (res.data?.success) {
showSuccess(t('已删除'));
await loadUserSubscriptions();
@@ -247,7 +254,8 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
render: (_, record) => {
const sub = record?.subscription;
const planId = sub?.plan_id;
const title = planTitleMap.get(planId) || (planId ? `#${planId}` : '-');
const title =
planTitleMap.get(planId) || (planId ? `#${planId}` : '-');
return (
<div className='min-w-0'>
<div className='font-medium truncate'>{title}</div>
@@ -292,7 +300,10 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
const content = (
<div className='max-w-[320px] space-y-1'>
{items.map((it) => (
<div key={`${it.id}-${it.model_name}`} className='flex justify-between text-xs'>
<div
key={`${it.id}-${it.model_name}`}
className='flex justify-between text-xs'
>
<span className='truncate mr-2'>{it.model_name}</span>
<span className='text-gray-600'>
{it.amount_used}/{it.amount_total}
@@ -319,7 +330,8 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
render: (_, record) => {
const sub = record?.subscription;
const now = Date.now() / 1000;
const isExpired = (sub?.end_time || 0) > 0 && (sub?.end_time || 0) < now;
const isExpired =
(sub?.end_time || 0) > 0 && (sub?.end_time || 0) < now;
const isActive = sub?.status === 'active' && !isExpired;
const isCancelled = sub?.status === 'cancelled';
return (
@@ -412,7 +424,9 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
}}
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
image={
<IllustrationNoResult style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
@@ -428,4 +442,3 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
};
export default UserSubscriptionsModal;