mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 02:57:27 +00:00
Merge branch 'sub' into feature/subscription
This commit is contained in:
@@ -207,21 +207,23 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) {
|
|||||||
err := model.DB.Transaction(func(tx *gorm.DB) error {
|
err := model.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
// update plan (allow zero values updates with map)
|
// update plan (allow zero values updates with map)
|
||||||
updateMap := map[string]interface{}{
|
updateMap := map[string]interface{}{
|
||||||
"title": req.Plan.Title,
|
"title": req.Plan.Title,
|
||||||
"subtitle": req.Plan.Subtitle,
|
"subtitle": req.Plan.Subtitle,
|
||||||
"price_amount": req.Plan.PriceAmount,
|
"price_amount": req.Plan.PriceAmount,
|
||||||
"currency": req.Plan.Currency,
|
"currency": req.Plan.Currency,
|
||||||
"duration_unit": req.Plan.DurationUnit,
|
"duration_unit": req.Plan.DurationUnit,
|
||||||
"duration_value": req.Plan.DurationValue,
|
"duration_value": req.Plan.DurationValue,
|
||||||
"custom_seconds": req.Plan.CustomSeconds,
|
"custom_seconds": req.Plan.CustomSeconds,
|
||||||
"enabled": req.Plan.Enabled,
|
"enabled": req.Plan.Enabled,
|
||||||
"sort_order": req.Plan.SortOrder,
|
"sort_order": req.Plan.SortOrder,
|
||||||
"stripe_price_id": req.Plan.StripePriceId,
|
"stripe_price_id": req.Plan.StripePriceId,
|
||||||
"creem_product_id": req.Plan.CreemProductId,
|
"creem_product_id": req.Plan.CreemProductId,
|
||||||
"max_purchase_per_user": req.Plan.MaxPurchasePerUser,
|
"max_purchase_per_user": req.Plan.MaxPurchasePerUser,
|
||||||
"total_amount": req.Plan.TotalAmount,
|
"total_amount": req.Plan.TotalAmount,
|
||||||
"upgrade_group": req.Plan.UpgradeGroup,
|
"upgrade_group": req.Plan.UpgradeGroup,
|
||||||
"updated_at": common.GetTimestamp(),
|
"quota_reset_period": req.Plan.QuotaResetPeriod,
|
||||||
|
"quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds,
|
||||||
|
"updated_at": common.GetTimestamp(),
|
||||||
}
|
}
|
||||||
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
|
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -54,7 +54,15 @@ func SubscriptionRequestCreemPay(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
user, _ := model.GetUserById(userId, false)
|
user, err := model.GetUserById(userId, false)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
common.ApiErrorMsg(c, "用户不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if plan.MaxPurchasePerUser > 0 {
|
if plan.MaxPurchasePerUser > 0 {
|
||||||
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
||||||
|
|||||||
@@ -61,8 +61,16 @@ func SubscriptionRequestEpay(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
callBackAddress := service.GetCallbackAddress()
|
callBackAddress := service.GetCallbackAddress()
|
||||||
returnUrl, _ := url.Parse(callBackAddress + "/api/subscription/epay/return")
|
returnUrl, err := url.Parse(callBackAddress + "/api/subscription/epay/return")
|
||||||
notifyUrl, _ := url.Parse(callBackAddress + "/api/subscription/epay/notify")
|
if err != nil {
|
||||||
|
common.ApiErrorMsg(c, "回调地址配置错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notifyUrl, err := url.Parse(callBackAddress + "/api/subscription/epay/notify")
|
||||||
|
if err != nil {
|
||||||
|
common.ApiErrorMsg(c, "回调地址配置错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
||||||
tradeNo = fmt.Sprintf("SUBUSR%dNO%s", userId, tradeNo)
|
tradeNo = fmt.Sprintf("SUBUSR%dNO%s", userId, tradeNo)
|
||||||
|
|||||||
@@ -51,7 +51,15 @@ func SubscriptionRequestStripePay(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
user, _ := model.GetUserById(userId, false)
|
user, err := model.GetUserById(userId, false)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
common.ApiErrorMsg(c, "用户不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if plan.MaxPurchasePerUser > 0 {
|
if plan.MaxPurchasePerUser > 0 {
|
||||||
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
||||||
@@ -110,7 +118,7 @@ func genStripeSubscriptionLink(referenceId string, customerId string, email stri
|
|||||||
Quantity: stripe.Int64(1),
|
Quantity: stripe.Int64(1),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if "" == customerId {
|
if "" == customerId {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ func GetDBTimestamp() int64 {
|
|||||||
var err error
|
var err error
|
||||||
switch {
|
switch {
|
||||||
case common.UsingPostgreSQL:
|
case common.UsingPostgreSQL:
|
||||||
err = DB.Raw("SELECT EXTRACT(EPOCH FROM NOW())").Scan(&ts).Error
|
err = DB.Raw("SELECT EXTRACT(EPOCH FROM NOW())::bigint").Scan(&ts).Error
|
||||||
case common.UsingSQLite:
|
case common.UsingSQLite:
|
||||||
err = DB.Raw("SELECT strftime('%s','now')").Scan(&ts).Error
|
err = DB.Raw("SELECT strftime('%s','now')").Scan(&ts).Error
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu
|
|||||||
if relayInfo.SubscriptionId == 0 {
|
if relayInfo.SubscriptionId == 0 {
|
||||||
return errors.New("subscription id is missing")
|
return errors.New("subscription id is missing")
|
||||||
}
|
}
|
||||||
delta := int64(quota) - relayInfo.SubscriptionPreConsumed
|
delta := int64(quota)
|
||||||
if delta != 0 {
|
if delta != 0 {
|
||||||
if err := model.PostConsumeUserSubscriptionDelta(relayInfo.SubscriptionId, delta); err != nil {
|
if err := model.PostConsumeUserSubscriptionDelta(relayInfo.SubscriptionId, delta); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -202,9 +202,10 @@ const renderResetPeriod = (text, record, t) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPaymentConfig = (text, record, t) => {
|
const renderPaymentConfig = (text, record, t, enableEpay) => {
|
||||||
const hasStripe = !!record?.plan?.stripe_price_id;
|
const hasStripe = !!record?.plan?.stripe_price_id;
|
||||||
const hasCreem = !!record?.plan?.creem_product_id;
|
const hasCreem = !!record?.plan?.creem_product_id;
|
||||||
|
const hasEpay = !!enableEpay;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
@@ -218,9 +219,11 @@ const renderPaymentConfig = (text, record, t) => {
|
|||||||
Creem
|
Creem
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<Tag color='light-green' shape='circle'>
|
{hasEpay && (
|
||||||
{t('易支付')}
|
<Tag color='light-green' shape='circle'>
|
||||||
</Tag>
|
{t('易支付')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -274,7 +277,12 @@ const renderOperations = (text, record, { openEdit, setPlanEnabled, t }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSubscriptionsColumns = ({ t, openEdit, setPlanEnabled }) => {
|
export const getSubscriptionsColumns = ({
|
||||||
|
t,
|
||||||
|
openEdit,
|
||||||
|
setPlanEnabled,
|
||||||
|
enableEpay,
|
||||||
|
}) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
@@ -324,7 +332,8 @@ export const getSubscriptionsColumns = ({ t, openEdit, setPlanEnabled }) => {
|
|||||||
{
|
{
|
||||||
title: t('支付渠道'),
|
title: t('支付渠道'),
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (text, record) => renderPaymentConfig(text, record, t),
|
render: (text, record) =>
|
||||||
|
renderPaymentConfig(text, record, t, enableEpay),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('总额度'),
|
title: t('总额度'),
|
||||||
|
|||||||
@@ -27,16 +27,24 @@ import {
|
|||||||
import { getSubscriptionsColumns } from './SubscriptionsColumnDefs';
|
import { getSubscriptionsColumns } from './SubscriptionsColumnDefs';
|
||||||
|
|
||||||
const SubscriptionsTable = (subscriptionsData) => {
|
const SubscriptionsTable = (subscriptionsData) => {
|
||||||
const { plans, loading, compactMode, openEdit, setPlanEnabled, t } =
|
const {
|
||||||
subscriptionsData;
|
plans,
|
||||||
|
loading,
|
||||||
|
compactMode,
|
||||||
|
openEdit,
|
||||||
|
setPlanEnabled,
|
||||||
|
t,
|
||||||
|
enableEpay,
|
||||||
|
} = subscriptionsData;
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return getSubscriptionsColumns({
|
return getSubscriptionsColumns({
|
||||||
t,
|
t,
|
||||||
openEdit,
|
openEdit,
|
||||||
setPlanEnabled,
|
setPlanEnabled,
|
||||||
|
enableEpay,
|
||||||
});
|
});
|
||||||
}, [t, openEdit, setPlanEnabled]);
|
}, [t, openEdit, setPlanEnabled, enableEpay]);
|
||||||
|
|
||||||
const tableColumns = useMemo(() => {
|
const tableColumns = useMemo(() => {
|
||||||
return compactMode
|
return compactMode
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Banner } from '@douyinfe/semi-ui';
|
import { Banner } from '@douyinfe/semi-ui';
|
||||||
import CardPro from '../../common/ui/CardPro';
|
import CardPro from '../../common/ui/CardPro';
|
||||||
import SubscriptionsTable from './SubscriptionsTable';
|
import SubscriptionsTable from './SubscriptionsTable';
|
||||||
@@ -27,10 +27,13 @@ import AddEditSubscriptionModal from './modals/AddEditSubscriptionModal';
|
|||||||
import { useSubscriptionsData } from '../../../hooks/subscriptions/useSubscriptionsData';
|
import { useSubscriptionsData } from '../../../hooks/subscriptions/useSubscriptionsData';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
import { createCardProPagination } from '../../../helpers/utils';
|
import { createCardProPagination } from '../../../helpers/utils';
|
||||||
|
import { StatusContext } from '../../../context/Status';
|
||||||
|
|
||||||
const SubscriptionsPage = () => {
|
const SubscriptionsPage = () => {
|
||||||
const subscriptionsData = useSubscriptionsData();
|
const subscriptionsData = useSubscriptionsData();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const [statusState] = useContext(StatusContext);
|
||||||
|
const enableEpay = !!statusState?.status?.enable_online_topup;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showEdit,
|
showEdit,
|
||||||
@@ -91,7 +94,7 @@ const SubscriptionsPage = () => {
|
|||||||
})}
|
})}
|
||||||
t={t}
|
t={t}
|
||||||
>
|
>
|
||||||
<SubscriptionsTable {...subscriptionsData} />
|
<SubscriptionsTable {...subscriptionsData} enableEpay={enableEpay} />
|
||||||
</CardPro>
|
</CardPro>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ const AddEditSubscriptionModal = ({
|
|||||||
field='custom_seconds'
|
field='custom_seconds'
|
||||||
label={t('自定义秒数')}
|
label={t('自定义秒数')}
|
||||||
required
|
required
|
||||||
min={0}
|
min={1}
|
||||||
precision={0}
|
precision={0}
|
||||||
rules={[{ required: true, message: t('请输入秒数') }]}
|
rules={[{ required: true, message: t('请输入秒数') }]}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
|||||||
@@ -35,45 +35,13 @@ import { API, showError, showSuccess, renderQuota } from '../../helpers';
|
|||||||
import { getCurrencyConfig } from '../../helpers/render';
|
import { getCurrencyConfig } from '../../helpers/render';
|
||||||
import { Crown, RefreshCw, Sparkles } from 'lucide-react';
|
import { Crown, RefreshCw, Sparkles } from 'lucide-react';
|
||||||
import SubscriptionPurchaseModal from './modals/SubscriptionPurchaseModal';
|
import SubscriptionPurchaseModal from './modals/SubscriptionPurchaseModal';
|
||||||
|
import {
|
||||||
|
formatSubscriptionDuration,
|
||||||
|
formatSubscriptionResetPeriod,
|
||||||
|
} from '../../helpers/subscriptionFormat';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// 格式化有效期显示
|
|
||||||
function formatDuration(plan, t) {
|
|
||||||
const unit = plan?.duration_unit || 'month';
|
|
||||||
const value = plan?.duration_value || 1;
|
|
||||||
const unitLabels = {
|
|
||||||
year: t('年'),
|
|
||||||
month: t('个月'),
|
|
||||||
day: t('天'),
|
|
||||||
hour: t('小时'),
|
|
||||||
custom: t('自定义'),
|
|
||||||
};
|
|
||||||
if (unit === 'custom') {
|
|
||||||
const seconds = plan?.custom_seconds || 0;
|
|
||||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
|
||||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
|
||||||
return `${seconds} ${t('秒')}`;
|
|
||||||
}
|
|
||||||
return `${value} ${unitLabels[unit] || unit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatResetPeriod(plan, t) {
|
|
||||||
const period = plan?.quota_reset_period || 'never';
|
|
||||||
if (period === 'never') return t('不重置');
|
|
||||||
if (period === 'daily') return t('每天');
|
|
||||||
if (period === 'weekly') return t('每周');
|
|
||||||
if (period === 'monthly') return t('每月');
|
|
||||||
if (period === 'custom') {
|
|
||||||
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
|
|
||||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
|
||||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
|
||||||
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
|
|
||||||
return `${seconds} ${t('秒')}`;
|
|
||||||
}
|
|
||||||
return t('不重置');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤易支付方式
|
// 过滤易支付方式
|
||||||
function getEpayMethods(payMethods = []) {
|
function getEpayMethods(payMethods = []) {
|
||||||
return (payMethods || []).filter(
|
return (payMethods || []).filter(
|
||||||
@@ -473,8 +441,9 @@ const SubscriptionPlansCard = ({
|
|||||||
const totalAmount = Number(plan?.total_amount || 0);
|
const totalAmount = Number(plan?.total_amount || 0);
|
||||||
const { symbol, rate } = getCurrencyConfig();
|
const { symbol, rate } = getCurrencyConfig();
|
||||||
const price = Number(plan?.price_amount || 0);
|
const price = Number(plan?.price_amount || 0);
|
||||||
const displayPrice = (price * rate).toFixed(
|
const convertedPrice = price * rate;
|
||||||
price % 1 === 0 ? 0 : 2,
|
const displayPrice = convertedPrice.toFixed(
|
||||||
|
Number.isInteger(convertedPrice) ? 0 : 2,
|
||||||
);
|
);
|
||||||
const isPopular = index === 0 && plans.length > 1;
|
const isPopular = index === 0 && plans.length > 1;
|
||||||
const limit = Number(plan?.max_purchase_per_user || 0);
|
const limit = Number(plan?.max_purchase_per_user || 0);
|
||||||
@@ -487,11 +456,13 @@ const SubscriptionPlansCard = ({
|
|||||||
? `${t('升级分组')}: ${plan.upgrade_group}`
|
? `${t('升级分组')}: ${plan.upgrade_group}`
|
||||||
: null;
|
: null;
|
||||||
const resetLabel =
|
const resetLabel =
|
||||||
formatResetPeriod(plan, t) === t('不重置')
|
formatSubscriptionResetPeriod(plan, t) === t('不重置')
|
||||||
? null
|
? null
|
||||||
: `${t('额度重置')}: ${formatResetPeriod(plan, t)}`;
|
: `${t('额度重置')}: ${formatSubscriptionResetPeriod(plan, t)}`;
|
||||||
const planBenefits = [
|
const planBenefits = [
|
||||||
{ label: `${t('有效期')}: ${formatDuration(plan, t)}` },
|
{
|
||||||
|
label: `${t('有效期')}: ${formatSubscriptionDuration(plan, t)}`,
|
||||||
|
},
|
||||||
resetLabel ? { label: resetLabel } : null,
|
resetLabel ? { label: resetLabel } : null,
|
||||||
totalAmount > 0
|
totalAmount > 0
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ const TopUp = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateBillingPreference = async (pref) => {
|
const updateBillingPreference = async (pref) => {
|
||||||
|
const previousPref = billingPreference;
|
||||||
setBillingPreference(pref);
|
setBillingPreference(pref);
|
||||||
try {
|
try {
|
||||||
const res = await API.put('/api/subscription/self/preference', {
|
const res = await API.put('/api/subscription/self/preference', {
|
||||||
@@ -363,11 +364,16 @@ const TopUp = () => {
|
|||||||
});
|
});
|
||||||
if (res.data?.success) {
|
if (res.data?.success) {
|
||||||
showSuccess(t('更新成功'));
|
showSuccess(t('更新成功'));
|
||||||
|
const normalizedPref =
|
||||||
|
res.data?.data?.billing_preference || pref || previousPref;
|
||||||
|
setBillingPreference(normalizedPref);
|
||||||
} else {
|
} else {
|
||||||
showError(res.data?.message || t('更新失败'));
|
showError(res.data?.message || t('更新失败'));
|
||||||
|
setBillingPreference(previousPref);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(t('请求失败'));
|
showError(t('请求失败'));
|
||||||
|
setBillingPreference(previousPref);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,45 +33,13 @@ import { SiStripe } from 'react-icons/si';
|
|||||||
import { IconCreditCard } from '@douyinfe/semi-icons';
|
import { IconCreditCard } from '@douyinfe/semi-icons';
|
||||||
import { renderQuota } from '../../../helpers';
|
import { renderQuota } from '../../../helpers';
|
||||||
import { getCurrencyConfig } from '../../../helpers/render';
|
import { getCurrencyConfig } from '../../../helpers/render';
|
||||||
|
import {
|
||||||
|
formatSubscriptionDuration,
|
||||||
|
formatSubscriptionResetPeriod,
|
||||||
|
} from '../../../helpers/subscriptionFormat';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// 格式化有效期显示
|
|
||||||
function formatDuration(plan, t) {
|
|
||||||
const unit = plan?.duration_unit || 'month';
|
|
||||||
const value = plan?.duration_value || 1;
|
|
||||||
const unitLabels = {
|
|
||||||
year: t('年'),
|
|
||||||
month: t('个月'),
|
|
||||||
day: t('天'),
|
|
||||||
hour: t('小时'),
|
|
||||||
custom: t('自定义'),
|
|
||||||
};
|
|
||||||
if (unit === 'custom') {
|
|
||||||
const seconds = plan?.custom_seconds || 0;
|
|
||||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
|
||||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
|
||||||
return `${seconds} ${t('秒')}`;
|
|
||||||
}
|
|
||||||
return `${value} ${unitLabels[unit] || unit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatResetPeriod(plan, t) {
|
|
||||||
const period = plan?.quota_reset_period || 'never';
|
|
||||||
if (period === 'never') return t('不重置');
|
|
||||||
if (period === 'daily') return t('每天');
|
|
||||||
if (period === 'weekly') return t('每周');
|
|
||||||
if (period === 'monthly') return t('每月');
|
|
||||||
if (period === 'custom') {
|
|
||||||
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
|
|
||||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
|
||||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
|
||||||
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
|
|
||||||
return `${seconds} ${t('秒')}`;
|
|
||||||
}
|
|
||||||
return t('不重置');
|
|
||||||
}
|
|
||||||
|
|
||||||
const SubscriptionPurchaseModal = ({
|
const SubscriptionPurchaseModal = ({
|
||||||
t,
|
t,
|
||||||
visible,
|
visible,
|
||||||
@@ -93,7 +61,10 @@ const SubscriptionPurchaseModal = ({
|
|||||||
const totalAmount = Number(plan?.total_amount || 0);
|
const totalAmount = Number(plan?.total_amount || 0);
|
||||||
const { symbol, rate } = getCurrencyConfig();
|
const { symbol, rate } = getCurrencyConfig();
|
||||||
const price = plan ? Number(plan.price_amount || 0) : 0;
|
const price = plan ? Number(plan.price_amount || 0) : 0;
|
||||||
const displayPrice = (price * rate).toFixed(price % 1 === 0 ? 0 : 2);
|
const convertedPrice = price * rate;
|
||||||
|
const displayPrice = convertedPrice.toFixed(
|
||||||
|
Number.isInteger(convertedPrice) ? 0 : 2,
|
||||||
|
);
|
||||||
// 只有当管理员开启支付网关 AND 套餐配置了对应的支付ID时才显示
|
// 只有当管理员开启支付网关 AND 套餐配置了对应的支付ID时才显示
|
||||||
const hasStripe = enableStripeTopUp && !!plan?.stripe_price_id;
|
const hasStripe = enableStripeTopUp && !!plan?.stripe_price_id;
|
||||||
const hasCreem = enableCreemTopUp && !!plan?.creem_product_id;
|
const hasCreem = enableCreemTopUp && !!plan?.creem_product_id;
|
||||||
@@ -142,17 +113,17 @@ const SubscriptionPurchaseModal = ({
|
|||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<CalendarClock size={14} className='mr-1 text-slate-500' />
|
<CalendarClock size={14} className='mr-1 text-slate-500' />
|
||||||
<Text className='text-slate-900 dark:text-slate-100'>
|
<Text className='text-slate-900 dark:text-slate-100'>
|
||||||
{formatDuration(plan, t)}
|
{formatSubscriptionDuration(plan, t)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{formatResetPeriod(plan, t) !== t('不重置') && (
|
{formatSubscriptionResetPeriod(plan, t) !== t('不重置') && (
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<Text strong className='text-slate-700 dark:text-slate-200'>
|
<Text strong className='text-slate-700 dark:text-slate-200'>
|
||||||
{t('重置周期')}:
|
{t('重置周期')}:
|
||||||
</Text>
|
</Text>
|
||||||
<Text className='text-slate-900 dark:text-slate-100'>
|
<Text className='text-slate-900 dark:text-slate-100'>
|
||||||
{formatResetPeriod(plan, t)}
|
{formatSubscriptionResetPeriod(plan, t)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
34
web/src/helpers/subscriptionFormat.js
Normal file
34
web/src/helpers/subscriptionFormat.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export function formatSubscriptionDuration(plan, t) {
|
||||||
|
const unit = plan?.duration_unit || 'month';
|
||||||
|
const value = plan?.duration_value || 1;
|
||||||
|
const unitLabels = {
|
||||||
|
year: t('年'),
|
||||||
|
month: t('个月'),
|
||||||
|
day: t('天'),
|
||||||
|
hour: t('小时'),
|
||||||
|
custom: t('自定义'),
|
||||||
|
};
|
||||||
|
if (unit === 'custom') {
|
||||||
|
const seconds = plan?.custom_seconds || 0;
|
||||||
|
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||||
|
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||||
|
return `${seconds} ${t('秒')}`;
|
||||||
|
}
|
||||||
|
return `${value} ${unitLabels[unit] || unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSubscriptionResetPeriod(plan, t) {
|
||||||
|
const period = plan?.quota_reset_period || 'never';
|
||||||
|
if (period === 'never') return t('不重置');
|
||||||
|
if (period === 'daily') return t('每天');
|
||||||
|
if (period === 'weekly') return t('每周');
|
||||||
|
if (period === 'monthly') return t('每月');
|
||||||
|
if (period === 'custom') {
|
||||||
|
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
|
||||||
|
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||||
|
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||||
|
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
|
||||||
|
return `${seconds} ${t('秒')}`;
|
||||||
|
}
|
||||||
|
return t('不重置');
|
||||||
|
}
|
||||||
@@ -2660,7 +2660,7 @@
|
|||||||
"推荐": "おすすめ",
|
"推荐": "おすすめ",
|
||||||
"已达到购买上限": "購入上限に達しました",
|
"已达到购买上限": "購入上限に達しました",
|
||||||
"已达上限": "上限に達しました",
|
"已达上限": "上限に達しました",
|
||||||
"立即订阅": "今すぐ購読",
|
"立即订阅": "今すぐサブスクリプション",
|
||||||
"暂无可购买套餐": "購入可能なプランがありません",
|
"暂无可购买套餐": "購入可能なプランがありません",
|
||||||
"该套餐未配置 Stripe": "このプランには Stripe が設定されていません",
|
"该套餐未配置 Stripe": "このプランには Stripe が設定されていません",
|
||||||
"已打开支付页面": "決済ページを開きました",
|
"已打开支付页面": "決済ページを開きました",
|
||||||
|
|||||||
@@ -2616,7 +2616,7 @@
|
|||||||
"格式化 JSON": "Форматировать JSON",
|
"格式化 JSON": "Форматировать JSON",
|
||||||
"关闭提示": "Закрыть уведомление",
|
"关闭提示": "Закрыть уведомление",
|
||||||
"说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "Примечание: тесты на этой странице используют нестриминговые запросы. Если канал поддерживает только стриминговые ответы, тест может завершиться неудачей. Ориентируйтесь на реальное использование.",
|
"说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "Примечание: тесты на этой странице используют нестриминговые запросы. Если канал поддерживает только стриминговые ответы, тест может завершиться неудачей. Ориентируйтесь на реальное использование.",
|
||||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление endpoint'ов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
|
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление эндпоинтов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
|
||||||
"Stripe/Creem 需在第三方平台创建商品并填入 ID": "Товары Stripe/Creem нужно создать на сторонней платформе и указать их ID",
|
"Stripe/Creem 需在第三方平台创建商品并填入 ID": "Товары Stripe/Creem нужно создать на сторонней платформе и указать их ID",
|
||||||
"暂无订阅套餐": "Нет тарифных планов",
|
"暂无订阅套餐": "Нет тарифных планов",
|
||||||
"订阅管理": "Управление подписками",
|
"订阅管理": "Управление подписками",
|
||||||
|
|||||||
@@ -3228,9 +3228,9 @@
|
|||||||
"仅用订阅": "Chỉ dùng đăng ký",
|
"仅用订阅": "Chỉ dùng đăng ký",
|
||||||
"仅用钱包": "Chỉ dùng ví",
|
"仅用钱包": "Chỉ dùng ví",
|
||||||
"我的订阅": "Đăng ký của tôi",
|
"我的订阅": "Đăng ký của tôi",
|
||||||
"个生效中": "đang hiệu lực",
|
"个生效中": "gói đăng ký đang hiệu lực",
|
||||||
"无生效": "Không có hiệu lực",
|
"无生效": "Không có gói đăng ký hiệu lực",
|
||||||
"个已过期": "đã hết hạn",
|
"个已过期": "gói đăng ký đã hết hạn",
|
||||||
"订阅": "Đăng ký",
|
"订阅": "Đăng ký",
|
||||||
"过期于": "Hết hạn vào",
|
"过期于": "Hết hạn vào",
|
||||||
"购买套餐后即可享受模型权益": "Mua gói để nhận quyền lợi mô hình",
|
"购买套餐后即可享受模型权益": "Mua gói để nhận quyền lợi mô hình",
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SubscriptionsTable from '../../components/table/subscriptions';
|
import SubscriptionsPage from '../../components/table/subscriptions';
|
||||||
|
|
||||||
const Subscription = () => {
|
const Subscription = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='mt-[60px] px-2'>
|
||||||
<SubscriptionsTable />
|
<SubscriptionsPage />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user