/* 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 . For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useMemo, useState } from 'react'; import { Button, Empty, Modal, Select, SideSheet, Space, Tag, Typography, } from '@douyinfe/semi-ui'; import { IconPlusCircle } from '@douyinfe/semi-icons'; import { IllustrationNoResult, IllustrationNoResultDark, } from '@douyinfe/semi-illustrations'; import { API, showError, showSuccess } from '../../../../helpers'; import { convertUSDToCurrency } from '../../../../helpers/render'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import CardTable from '../../../common/ui/CardTable'; const { Text } = Typography; function formatTs(ts) { if (!ts) return '-'; return new Date(ts * 1000).toLocaleString(); } function renderStatusTag(sub, t) { const now = Date.now() / 1000; const end = sub?.end_time || 0; const status = sub?.status || ''; const isExpiredByTime = end > 0 && end < now; const isActive = status === 'active' && !isExpiredByTime; if (isActive) { return ( {t('生效')} ); } if (status === 'cancelled') { return ( {t('已作废')} ); } return ( {t('已过期')} ); } const UserSubscriptionsModal = ({ visible, onCancel, user, t, onSuccess }) => { const isMobile = useIsMobile(); const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); const [plansLoading, setPlansLoading] = useState(false); const [plans, setPlans] = useState([]); const [selectedPlanId, setSelectedPlanId] = useState(null); const [subs, setSubs] = useState([]); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; const planTitleMap = useMemo(() => { const map = new Map(); (plans || []).forEach((p) => { const id = p?.plan?.id; const title = p?.plan?.title; if (id) map.set(id, title || `#${id}`); }); return map; }, [plans]); const pagedSubs = useMemo(() => { const start = Math.max(0, (Number(currentPage || 1) - 1) * pageSize); const end = start + pageSize; return (subs || []).slice(start, end); }, [subs, currentPage]); const planOptions = useMemo(() => { return (plans || []).map((p) => ({ label: `${p?.plan?.title || ''} (${convertUSDToCurrency( Number(p?.plan?.price_amount || 0), 2, )})`, value: p?.plan?.id, })); }, [plans]); const loadPlans = async () => { setPlansLoading(true); try { const res = await API.get('/api/subscription/admin/plans'); if (res.data?.success) { setPlans(res.data.data || []); } else { showError(res.data?.message || t('加载失败')); } } catch (e) { showError(t('请求失败')); } finally { setPlansLoading(false); } }; const loadUserSubscriptions = async () => { if (!user?.id) return; setLoading(true); try { const res = await API.get( `/api/subscription/admin/users/${user.id}/subscriptions`, ); if (res.data?.success) { const next = res.data.data || []; setSubs(next); setCurrentPage(1); } else { showError(res.data?.message || t('加载失败')); } } catch (e) { showError(t('请求失败')); } finally { setLoading(false); } }; useEffect(() => { if (!visible) return; setSelectedPlanId(null); setCurrentPage(1); loadPlans(); loadUserSubscriptions(); }, [visible]); const handlePageChange = (page) => { setCurrentPage(page); }; const createSubscription = async () => { if (!user?.id) { showError(t('用户信息缺失')); return; } if (!selectedPlanId) { showError(t('请选择订阅套餐')); return; } setCreating(true); try { const res = await API.post( `/api/subscription/admin/users/${user.id}/subscriptions`, { plan_id: selectedPlanId, }, ); if (res.data?.success) { const msg = res.data?.data?.message; showSuccess(msg ? msg : t('新增成功')); setSelectedPlanId(null); await loadUserSubscriptions(); onSuccess?.(); } else { showError(res.data?.message || t('新增失败')); } } catch (e) { showError(t('请求失败')); } finally { setCreating(false); } }; const invalidateSubscription = (subId) => { Modal.confirm({ title: t('确认作废'), content: t('作废后该订阅将立即失效,历史记录不受影响。是否继续?'), centered: true, onOk: async () => { try { const res = await API.post( `/api/subscription/admin/user_subscriptions/${subId}/invalidate`, ); if (res.data?.success) { const msg = res.data?.data?.message; showSuccess(msg ? msg : t('已作废')); await loadUserSubscriptions(); onSuccess?.(); } else { showError(res.data?.message || t('操作失败')); } } catch (e) { showError(t('请求失败')); } }, }); }; const deleteSubscription = (subId) => { Modal.confirm({ title: t('确认删除'), content: t('删除会彻底移除该订阅记录(含权益明细)。是否继续?'), centered: true, okType: 'danger', onOk: async () => { try { const res = await API.delete( `/api/subscription/admin/user_subscriptions/${subId}`, ); if (res.data?.success) { const msg = res.data?.data?.message; showSuccess(msg ? msg : t('已删除')); await loadUserSubscriptions(); onSuccess?.(); } else { showError(res.data?.message || t('删除失败')); } } catch (e) { showError(t('请求失败')); } }, }); }; const columns = useMemo(() => { return [ { title: 'ID', dataIndex: ['subscription', 'id'], key: 'id', width: 70, }, { title: t('套餐'), key: 'plan', width: 180, render: (_, record) => { const sub = record?.subscription; const planId = sub?.plan_id; const title = planTitleMap.get(planId) || (planId ? `#${planId}` : '-'); return (
{title}
{t('来源')}: {sub?.source || '-'}
); }, }, { title: t('状态'), key: 'status', width: 90, render: (_, record) => renderStatusTag(record?.subscription, t), }, { title: t('有效期'), key: 'validity', width: 200, render: (_, record) => { const sub = record?.subscription; return (
{t('开始')}: {formatTs(sub?.start_time)}
{t('结束')}: {formatTs(sub?.end_time)}
); }, }, { title: t('总额度'), key: 'total', width: 120, render: (_, record) => { const sub = record?.subscription; const total = Number(sub?.amount_total || 0); const used = Number(sub?.amount_used || 0); return ( 0 ? 'secondary' : 'tertiary'}> {total > 0 ? `${used}/${total}` : t('不限')} ); }, }, { title: '', key: 'operate', width: 140, fixed: 'right', render: (_, record) => { const sub = record?.subscription; const now = Date.now() / 1000; const isExpired = (sub?.end_time || 0) > 0 && (sub?.end_time || 0) < now; const isActive = sub?.status === 'active' && !isExpired; const isCancelled = sub?.status === 'cancelled'; return ( ); }, }, ]; }, [t, planTitleMap]); return ( {t('管理')} {t('用户订阅管理')} {user?.username || '-'} (ID: {user?.id || '-'}) } >
{/* 顶部操作栏:新增订阅 */}