🚀 refactor: Simplify subscription quota to total amount model

Remove per-model subscription items and switch to a single total quota per plan and user subscription. Update billing, reset, and logging flows to operate on total quota, and refactor admin/user UI to configure and display total quota consistently.
This commit is contained in:
t0ng7u
2026-02-01 00:35:08 +08:00
parent b92a4ee987
commit 6300c31d70
17 changed files with 270 additions and 999 deletions

View File

@@ -33,8 +33,6 @@ import { convertUSDToCurrency } from '../../../helpers/render';
const { Text } = Typography;
const quotaTypeLabel = (quotaType) => (quotaType === 1 ? '按次' : '按量');
function formatDuration(plan, t) {
if (!plan) return '';
const u = plan.duration_unit || 'month';
@@ -68,8 +66,6 @@ function formatResetPeriod(plan, t) {
const renderPlanTitle = (text, record, t) => {
const subtitle = record?.plan?.subtitle;
const plan = record?.plan;
const items = record?.items || [];
const popoverContent = (
<div style={{ width: 260 }}>
<Text strong>{text}</Text>
@@ -84,6 +80,8 @@ const renderPlanTitle = (text, record, t) => {
<Text strong style={{ color: 'var(--semi-color-success)' }}>
{convertUSDToCurrency(Number(plan?.price_amount || 0), 2)}
</Text>
<Text type='tertiary'>{t('总额度')}</Text>
<Text>{plan?.total_amount > 0 ? plan.total_amount : t('不限')}</Text>
<Text type='tertiary'>{t('购买上限')}</Text>
<Text>
{plan?.max_purchase_per_user > 0
@@ -94,10 +92,6 @@ const renderPlanTitle = (text, record, t) => {
<Text>{formatDuration(plan, t)}</Text>
<Text type='tertiary'>{t('重置')}</Text>
<Text>{formatResetPeriod(plan, t)}</Text>
<Text type='tertiary'>{t('模型')}</Text>
<Text>
{items.length} {t('个')}
</Text>
</div>
</div>
);
@@ -165,54 +159,12 @@ const renderEnabled = (text, record, t) => {
);
};
const renderModels = (text, record, t) => {
const items = record?.items || [];
if (items.length === 0) {
return <Text type='tertiary'></Text>;
}
const popoverContent = (
<div style={{ maxWidth: 320, maxHeight: 260, overflowY: 'auto' }}>
<Text strong>
{t('模型权益')} ({items.length})
</Text>
<Divider margin={8} />
<Space vertical align='start' spacing={6}>
{items.map((it, idx) => (
<div
key={idx}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
gap: 12,
}}
>
<Text ellipsis={{ showTooltip: true }} style={{ maxWidth: 180 }}>
{it.model_name}
</Text>
<Space spacing={8}>
<Tag
color={it.quota_type === 1 ? 'amber' : 'teal'}
shape='circle'
>
{quotaTypeLabel(it.quota_type)}
</Tag>
<Text type='secondary'>{it.amount_total}</Text>
</Space>
</div>
))}
</Space>
</div>
);
const renderTotalAmount = (text, record, t) => {
const total = Number(record?.plan?.total_amount || 0);
return (
<Popover content={popoverContent} position='leftTop' showArrow>
<Tag color='blue' shape='circle' style={{ cursor: 'pointer' }}>
{items.length} {t('个模型')}
</Tag>
</Popover>
<Text type={total > 0 ? 'secondary' : 'tertiary'}>
{total > 0 ? total : t('不限')}
</Text>
);
};
@@ -359,9 +311,9 @@ export const getSubscriptionsColumns = ({ t, openEdit, setPlanEnabled }) => {
render: (text, record) => renderPaymentConfig(text, record, t),
},
{
title: t('模型'),
title: t('总额度'),
width: 100,
render: (text, record) => renderModels(text, record, t),
render: (text, record) => renderTotalAmount(text, record, t),
},
{
title: t('操作'),

View File

@@ -41,7 +41,6 @@ const SubscriptionsPage = () => {
openCreate,
compactMode,
setCompactMode,
pricingModels,
t,
} = subscriptionsData;
@@ -52,7 +51,6 @@ const SubscriptionsPage = () => {
handleClose={closeEdit}
editingPlan={editingPlan}
placement={sheetPlacement}
pricingModels={pricingModels}
refresh={refresh}
t={t}
/>

View File

@@ -17,22 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useMemo, useState, useRef } from 'react';
import React, { useState, useRef } from 'react';
import {
Avatar,
Button,
Card,
Col,
Form,
Input,
InputNumber,
Row,
Select,
SideSheet,
Space,
Spin,
Switch,
Table,
Tag,
Typography,
} from '@douyinfe/semi-ui';
@@ -40,11 +36,15 @@ import {
IconCalendarClock,
IconClose,
IconCreditCard,
IconPlusCircle,
IconSave,
} from '@douyinfe/semi-icons';
import { Trash2, Clock, Boxes, RefreshCw } from 'lucide-react';
import { API, showError, showSuccess } from '../../../../helpers';
import { Clock, RefreshCw } from 'lucide-react';
import {
API,
showError,
showSuccess,
renderQuotaWithPrompt,
} from '../../../../helpers';
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
const { Text, Title } = Typography;
@@ -65,14 +65,11 @@ const resetPeriodOptions = [
{ value: 'custom', label: '自定义(秒)' },
];
const quotaTypeLabel = (quotaType) => (quotaType === 1 ? '按次' : '按量');
const AddEditSubscriptionModal = ({
visible,
handleClose,
editingPlan,
placement = 'left',
pricingModels = [],
refresh,
t,
}) => {
@@ -95,17 +92,11 @@ const AddEditSubscriptionModal = ({
enabled: true,
sort_order: 0,
max_purchase_per_user: 0,
total_amount: 0,
stripe_price_id: '',
creem_product_id: '',
});
const [items, setItems] = useState([]);
// Model benefits UX
const [pendingModels, setPendingModels] = useState([]);
const [defaultNewAmountTotal, setDefaultNewAmountTotal] = useState(0);
const [bulkAmountTotal, setBulkAmountTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const buildFormValues = () => {
const base = getInitValues();
if (editingPlan?.plan?.id === undefined) return base;
@@ -124,152 +115,17 @@ const AddEditSubscriptionModal = ({
enabled: p.enabled !== false,
sort_order: Number(p.sort_order || 0),
max_purchase_per_user: Number(p.max_purchase_per_user || 0),
total_amount: Number(p.total_amount || 0),
stripe_price_id: p.stripe_price_id || '',
creem_product_id: p.creem_product_id || '',
};
};
useEffect(() => {
// 1) always keep items in sync
if (visible && isEdit && editingPlan) {
setItems((editingPlan.items || []).map((it) => ({ ...it })));
} else if (visible && !isEdit) {
setItems([]);
}
}, [visible, editingPlan]);
const modelOptions = useMemo(() => {
return (pricingModels || []).map((m) => ({
label: `${m.model_name} (${quotaTypeLabel(m.quota_type)})`,
value: m.model_name,
quota_type: m.quota_type,
}));
}, [pricingModels]);
const addItem = (modelName) => {
const modelMeta = modelOptions.find((m) => m.value === modelName);
if (!modelMeta) return;
if (items.some((it) => it.model_name === modelName)) {
showError(t('该模型已添加'));
return;
}
setItems([
...items,
{
model_name: modelName,
quota_type: modelMeta.quota_type,
amount_total: 0,
},
]);
};
const addPendingModels = () => {
const selected = (pendingModels || []).filter(Boolean);
if (selected.length === 0) {
showError(t('请选择要添加的模型'));
return;
}
const existing = new Set((items || []).map((it) => it.model_name));
const toAdd = selected.filter((name) => !existing.has(name));
if (toAdd.length === 0) {
showError(t('所选模型已全部存在'));
return;
}
const defaultAmount = Number(defaultNewAmountTotal || 0);
const next = [...items];
toAdd.forEach((modelName) => {
const modelMeta = modelOptions.find((m) => m.value === modelName);
if (!modelMeta) return;
next.push({
model_name: modelName,
quota_type: modelMeta.quota_type,
amount_total:
Number.isFinite(defaultAmount) && defaultAmount >= 0
? defaultAmount
: 0,
});
});
setItems(next);
setPendingModels([]);
showSuccess(t('已添加'));
};
const applyBulkAmountTotal = ({ scope }) => {
const n = Number(bulkAmountTotal || 0);
if (!Number.isFinite(n) || n < 0) {
showError(t('请输入有效的数量'));
return;
}
if (!items || items.length === 0) {
showError(t('请先添加模型权益'));
return;
}
if (scope === 'selected') {
if (!selectedRowKeys || selectedRowKeys.length === 0) {
showError(t('请先勾选要批量设置的权益'));
return;
}
const keySet = new Set(selectedRowKeys);
setItems(
items.map((it) => {
const k = `${it.model_name}-${it.quota_type}`;
if (!keySet.has(k)) return it;
return { ...it, amount_total: n };
}),
);
showSuccess(t('已对选中项批量设置'));
return;
}
// scope === 'all'
setItems(items.map((it) => ({ ...it, amount_total: n })));
showSuccess(t('已对全部批量设置'));
};
const deleteSelectedItems = () => {
if (!selectedRowKeys || selectedRowKeys.length === 0) {
showError(t('请先勾选要删除的权益'));
return;
}
const keySet = new Set(selectedRowKeys);
const next = (items || []).filter(
(it) => !keySet.has(`${it.model_name}-${it.quota_type}`),
);
setItems(next);
setSelectedRowKeys([]);
showSuccess(t('已删除选中项'));
};
const updateItem = (idx, patch) => {
const next = [...items];
next[idx] = { ...next[idx], ...patch };
setItems(next);
};
const removeItem = (idx) => {
const next = [...items];
next.splice(idx, 1);
setItems(next);
};
const submit = async (values) => {
if (!values.title || values.title.trim() === '') {
showError(t('套餐标题不能为空'));
return;
}
const cleanedItems = items
.filter((it) => it.model_name && Number(it.amount_total) > 0)
.map((it) => ({
model_name: it.model_name,
quota_type: Number(it.quota_type || 0),
amount_total: Number(it.amount_total),
}));
if (cleanedItems.length === 0) {
showError(t('请至少配置一个模型权益(且数量>0'));
return;
}
setLoading(true);
try {
const payload = {
@@ -286,8 +142,8 @@ const AddEditSubscriptionModal = ({
: 0,
sort_order: Number(values.sort_order || 0),
max_purchase_per_user: Number(values.max_purchase_per_user || 0),
total_amount: Number(values.total_amount || 0),
},
items: cleanedItems,
};
if (editingPlan?.plan?.id) {
const res = await API.put(
@@ -318,48 +174,6 @@ const AddEditSubscriptionModal = ({
}
};
const itemColumns = [
{
title: t('模型'),
dataIndex: 'model_name',
render: (v, row) => (
<div className='text-sm'>
<div className='font-medium'>{v}</div>
<div className='text-xs text-gray-500'>
{t('计费')}: {quotaTypeLabel(row.quota_type)}
</div>
</div>
),
},
{
title: t('数量'),
dataIndex: 'amount_total',
width: 220,
render: (v, row, idx) => (
<InputNumber
value={Number(v || 0)}
min={0}
precision={0}
onChange={(val) => updateItem(idx, { amount_total: val })}
placeholder={row.quota_type === 1 ? t('次数') : t('额度')}
style={{ width: '100%' }}
/>
),
},
{
title: '',
width: 60,
render: (_, __, idx) => (
<Button
type='danger'
theme='borderless'
icon={<Trash2 size={14} />}
onClick={() => removeItem(idx)}
/>
),
},
];
return (
<>
<SideSheet
@@ -382,7 +196,7 @@ const AddEditSubscriptionModal = ({
}
bodyStyle={{ padding: '0' }}
visible={visible}
width={isMobile ? '100%' : 700}
width={isMobile ? '100%' : 600}
footer={
<div className='flex justify-end bg-white'>
<Space>
@@ -470,6 +284,27 @@ const AddEditSubscriptionModal = ({
/>
</Col>
<Col span={12}>
<Form.AutoComplete
field='total_amount'
label={t('总额度')}
type='number'
rules={[{ required: true, message: t('请输入总额度') }]}
extraText={`${t('0 表示不限')} · ${renderQuotaWithPrompt(
Number(values.total_amount || 0),
)}`}
data={[
{ value: 500000, label: '1' },
{ value: 5000000, label: '10' },
{ value: 25000000, label: '50' },
{ value: 50000000, label: '100' },
{ value: 250000000, label: '500' },
{ value: 500000000, label: '1000' },
]}
style={{ width: '100%' }}
/>
</Col>
<Col span={12}>
<Form.Input
field='currency'
@@ -665,123 +500,6 @@ const AddEditSubscriptionModal = ({
</Col>
</Row>
</Card>
{/* 模型权益 */}
<Card className='!rounded-2xl shadow-sm border-0'>
<div className='flex items-center justify-between mb-3 gap-3'>
<div className='flex items-center'>
<Avatar
size='small'
color='orange'
className='mr-2 shadow-md'
>
<Boxes size={16} />
</Avatar>
<div>
<Text className='text-lg font-medium'>
{t('模型权益')}
</Text>
<div className='text-xs text-gray-600'>
{t('配置套餐可使用的模型及额度')}
</div>
</div>
</div>
</div>
{/* 工具栏:最少步骤完成“添加 + 批量设置” */}
<div className='flex flex-col gap-2 mb-3'>
<div className='flex flex-col md:flex-row gap-2 md:items-center'>
<Select
placeholder={t('选择模型(可多选)')}
multiple
filter
value={pendingModels}
onChange={setPendingModels}
style={{ width: '100%', flex: 1 }}
>
{modelOptions.map((o) => (
<Select.Option key={o.value} value={o.value}>
{o.label}
</Select.Option>
))}
</Select>
<InputNumber
value={Number(defaultNewAmountTotal || 0)}
min={0}
precision={0}
onChange={(v) => setDefaultNewAmountTotal(v)}
style={{ width: isMobile ? '100%' : 180 }}
placeholder={t('默认数量')}
/>
<Button
theme='solid'
type='primary'
icon={<IconPlusCircle />}
onClick={addPendingModels}
>
{t('添加')}
</Button>
</div>
<div className='flex flex-col md:flex-row gap-2 md:items-center md:justify-between'>
<div className='flex items-center gap-2'>
<Tag color='white' shape='circle'>
{t('已选')} {selectedRowKeys?.length || 0}
</Tag>
<InputNumber
value={Number(bulkAmountTotal || 0)}
min={0}
precision={0}
onChange={(v) => setBulkAmountTotal(v)}
style={{ width: isMobile ? '100%' : 220 }}
placeholder={t('统一设置数量')}
/>
</div>
<div className='flex items-center gap-2 justify-end'>
<Button
theme='light'
type='primary'
onClick={() =>
applyBulkAmountTotal({ scope: 'selected' })
}
>
{t('应用到选中')}
</Button>
<Button
theme='light'
type='primary'
onClick={() => applyBulkAmountTotal({ scope: 'all' })}
>
{t('应用到全部')}
</Button>
<Button
theme='light'
type='danger'
icon={<Trash2 size={14} />}
onClick={deleteSelectedItems}
>
{t('删除选中')}
</Button>
</div>
</div>
</div>
<Table
columns={itemColumns}
dataSource={items}
pagination={false}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys || []),
}}
rowKey={(row) => `${row.model_name}-${row.quota_type}`}
empty={
<div className='py-6 text-center text-gray-500'>
{t('尚未添加任何模型')}
</div>
}
/>
</Card>
</div>
)}
</Form>

View File

@@ -22,7 +22,6 @@ import {
Button,
Empty,
Modal,
Popover,
Select,
SideSheet,
Space,
@@ -295,34 +294,17 @@ const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => {
},
},
{
title: t('权益'),
key: 'items',
width: 80,
title: t('总额度'),
key: 'total',
width: 120,
render: (_, record) => {
const items = record?.items || [];
if (items.length === 0) return <Text type='tertiary'>-</Text>;
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'
>
<span className='truncate mr-2'>{it.model_name}</span>
<span className='text-gray-600'>
{it.amount_used}/{it.amount_total}
{it.quota_type === 1 ? t('次') : ''}
</span>
</div>
))}
</div>
);
const sub = record?.subscription;
const total = Number(sub?.amount_total || 0);
const used = Number(sub?.amount_used || 0);
return (
<Popover content={content} position='top' showArrow>
<Tag color='white' shape='circle'>
{items.length} {t('项')}
</Tag>
</Popover>
<Text type={total > 0 ? 'secondary' : 'tertiary'}>
{total > 0 ? `${used}/${total}` : t('不限')}
</Text>
);
},
},

View File

@@ -33,7 +33,7 @@ import {
} from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../helpers';
import { getCurrencyConfig, stringToColor } from '../../helpers/render';
import { CalendarClock, Check, Crown, RefreshCw, Sparkles } from 'lucide-react';
import { Crown, RefreshCw, Sparkles } from 'lucide-react';
import SubscriptionPurchaseModal from './modals/SubscriptionPurchaseModal';
const { Text } = Typography;
@@ -245,16 +245,10 @@ const SubscriptionPlansCard = ({
// 计算单个订阅的使用进度
const getUsagePercent = (sub) => {
const items = sub?.items || [];
if (items.length === 0) return 0;
let totalUsed = 0;
let totalAmount = 0;
items.forEach((it) => {
totalUsed += Number(it.amount_used || 0);
totalAmount += Number(it.amount_total || 0);
});
if (totalAmount === 0) return 0;
return Math.round((totalUsed / totalAmount) * 100);
const total = Number(sub?.subscription?.amount_total || 0);
const used = Number(sub?.subscription?.amount_used || 0);
if (total <= 0) return 0;
return Math.round((used / total) * 100);
};
return (
@@ -374,7 +368,12 @@ const SubscriptionPlansCard = ({
{allSubscriptions.map((sub, subIndex) => {
const isLast = subIndex === allSubscriptions.length - 1;
const subscription = sub.subscription;
const items = sub.items || [];
const totalAmount = Number(subscription?.amount_total || 0);
const usedAmount = Number(subscription?.amount_used || 0);
const remainAmount =
totalAmount > 0
? Math.max(0, totalAmount - usedAmount)
: 0;
const remainDays = getRemainingDays(sub);
const usagePercent = getUsagePercent(sub);
const now = Date.now() / 1000;
@@ -418,34 +417,17 @@ const SubscriptionPlansCard = ({
(subscription?.end_time || 0) * 1000,
).toLocaleString()}
</div>
{/* 权益列表 */}
{items.length > 0 && (
<div className='flex flex-wrap gap-1'>
{items.slice(0, 4).map((it) => {
const used = Number(it.amount_used || 0);
const total = Number(it.amount_total || 0);
const remain = total - used;
const label = it.quota_type === 1 ? t('') : '';
return (
<Tag
key={`${it.id}-${it.model_name}`}
size='small'
color='white'
shape='circle'
>
{it.model_name}: {remain}
{label}
</Tag>
);
})}
{items.length > 4 && (
<Tag size='small' color='white' shape='circle'>
+{items.length - 4}
</Tag>
)}
</div>
)}
<div className='text-xs text-gray-500 mb-2'>
{t('总额度')}:{' '}
{totalAmount > 0
? `${usedAmount}/${totalAmount} · ${t('剩余')} ${remainAmount}`
: t('不限')}
{totalAmount > 0 && (
<span className='ml-2'>
{t('已用')} {usagePercent}%
</span>
)}
</div>
{!isLast && <Divider margin={12} />}
</div>
);
@@ -464,7 +446,7 @@ const SubscriptionPlansCard = ({
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'>
{plans.map((p, index) => {
const plan = p?.plan;
const planItems = p?.items || [];
const totalAmount = Number(plan?.total_amount || 0);
const { symbol, rate } = getCurrencyConfig();
const price = Number(plan?.price_amount || 0);
const displayPrice = (price * rate).toFixed(
@@ -474,10 +456,14 @@ const SubscriptionPlansCard = ({
const limit = Number(plan?.max_purchase_per_user || 0);
const limitLabel =
limit > 0 ? `${t('限购')} ${limit}` : t('不限购');
const totalLabel =
totalAmount > 0
? `${t('总额度')}: ${totalAmount}`
: `${t('总额度')}: ${t('不限')}`;
const planTags = [
`${t('有效期')}: ${formatDuration(plan, t)}`,
`${t('重置')}: ${formatResetPeriod(plan, t)}`,
`${t('权益')}: ${planItems.length} ${t('项')}`,
totalLabel,
limitLabel,
];
@@ -548,35 +534,6 @@ const SubscriptionPlansCard = ({
<Divider margin={12} />
{/* 权益列表 */}
<div className='space-y-2 mb-4'>
{planItems.slice(0, 5).map((it, idx) => (
<div key={idx} className='flex items-center text-sm'>
<Check
size={14}
className='text-green-500 mr-2 flex-shrink-0'
/>
<span className='truncate flex-1'>
{it.model_name}
</span>
<Tag size='small' color='white' shape='circle'>
{it.amount_total}
{it.quota_type === 1 ? t('次') : ''}
</Tag>
</div>
))}
{planItems.length > 5 && (
<div className='text-xs text-gray-400 text-center'>
+{planItems.length - 5} {t('项更多权益')}
</div>
)}
{planItems.length === 0 && (
<div className='text-xs text-gray-400 text-center py-2'>
{t('暂无权益配置')}
</div>
)}
</div>
{/* 购买按钮 */}
{(() => {
const count = getPlanPurchaseCount(p?.plan?.id);

View File

@@ -23,12 +23,11 @@ import {
Modal,
Typography,
Card,
Tag,
Button,
Select,
Divider,
} from '@douyinfe/semi-ui';
import { Crown, CalendarClock, Package, Check } from 'lucide-react';
import { Crown, CalendarClock, Package } from 'lucide-react';
import { SiStripe } from 'react-icons/si';
import { IconCreditCard } from '@douyinfe/semi-icons';
import { getCurrencyConfig } from '../../../helpers/render';
@@ -89,7 +88,7 @@ const SubscriptionPurchaseModal = ({
onPayEpay,
}) => {
const plan = selectedPlan?.plan;
const items = selectedPlan?.items || [];
const totalAmount = Number(plan?.total_amount || 0);
const { symbol, rate } = getCurrencyConfig();
const price = plan ? Number(plan.price_amount || 0) : 0;
const displayPrice = (price * rate).toFixed(price % 1 === 0 ? 0 : 2);
@@ -156,12 +155,12 @@ const SubscriptionPurchaseModal = ({
</div>
<div className='flex justify-between items-center'>
<Text strong className='text-slate-700 dark:text-slate-200'>
{t('包含权益')}
{t('总额度')}
</Text>
<div className='flex items-center'>
<Package size={14} className='mr-1 text-slate-500' />
<Text className='text-slate-900 dark:text-slate-100'>
{items.length} {t('')}
{totalAmount > 0 ? totalAmount : t('不限')}
</Text>
</div>
</div>
@@ -178,35 +177,6 @@ const SubscriptionPurchaseModal = ({
</div>
</Card>
{/* 权益列表 */}
{items.length > 0 && (
<div className='space-y-2'>
<Text size='small' type='tertiary'>
{t('权益明细')}
</Text>
<div className='flex flex-wrap gap-1'>
{items.slice(0, 6).map((it, idx) => (
<Tag
key={idx}
size='small'
color='white'
type='light'
shape='circle'
>
<Check size={10} className='mr-1' />
{it.model_name}: {it.amount_total}
{it.quota_type === 1 ? t('次') : ''}
</Tag>
))}
{items.length > 6 && (
<Tag size='small' color='white' type='light' shape='circle'>
+{items.length - 6}
</Tag>
)}
</div>
</div>
)}
{/* 支付方式 */}
{purchaseLimitReached && (
<Banner

View File

@@ -29,7 +29,6 @@ export const useSubscriptionsData = () => {
// State management
const [allPlans, setAllPlans] = useState([]);
const [loading, setLoading] = useState(true);
const [pricingModels, setPricingModels] = useState([]);
// Pagination (client-side for now)
const [activePage, setActivePage] = useState(1);
@@ -40,18 +39,6 @@ export const useSubscriptionsData = () => {
const [editingPlan, setEditingPlan] = useState(null);
const [sheetPlacement, setSheetPlacement] = useState('left'); // 'left' | 'right'
// Load pricing models for dropdown
const loadModels = async () => {
try {
const res = await API.get('/api/pricing');
if (res.data?.success) {
setPricingModels(res.data.data || []);
}
} catch (e) {
setPricingModels([]);
}
};
// Load subscription plans
const loadPlans = async () => {
setLoading(true);
@@ -133,7 +120,6 @@ export const useSubscriptionsData = () => {
// Initialize data on component mount
useEffect(() => {
loadModels();
loadPlans();
}, []);
@@ -148,7 +134,6 @@ export const useSubscriptionsData = () => {
plans,
planCount,
loading,
pricingModels,
// Modal state
showEdit,

View File

@@ -517,14 +517,11 @@ export const useLogsData = () => {
if (other?.billing_source === 'subscription') {
const planId = other?.subscription_plan_id;
const planTitle = other?.subscription_plan_title || '';
const itemId = other?.subscription_item_id;
const quotaType = other?.subscription_quota_type;
const unit = quotaType === 1 ? t('次') : t('额度');
const subscriptionId = other?.subscription_id;
const unit = t('额度');
const pre = other?.subscription_pre_consumed ?? 0;
const postDelta = other?.subscription_post_delta ?? 0;
const finalConsumed =
other?.subscription_consumed ??
(quotaType === 1 ? 1 : pre + postDelta);
const finalConsumed = other?.subscription_consumed ?? pre + postDelta;
const remain = other?.subscription_remain;
const total = other?.subscription_total;
// Use multiple Description items to avoid an overlong single line.
@@ -534,20 +531,15 @@ export const useLogsData = () => {
value: `#${planId} ${planTitle}`.trim(),
});
}
if (itemId) {
if (subscriptionId) {
expandDataLocal.push({
key: t('订阅权益'),
value:
quotaType === 1
? `${t('权益ID')} ${itemId} · ${t('按次')}1 ${t('次')}/${t('请求')}`
: `${t('权益ID')} ${itemId} · ${t('按量')}`,
key: t('订阅实例'),
value: `#${subscriptionId}`,
});
}
const settlementLines = [
`${t('预扣')}${pre} ${unit}`,
quotaType === 0
? `${t('结算差额')}${postDelta > 0 ? '+' : ''}${postDelta} ${unit}`
: null,
`${t('结算差额')}${postDelta > 0 ? '+' : ''}${postDelta} ${unit}`,
`${t('最终抵扣')}${finalConsumed} ${unit}`,
]
.filter(Boolean)