From fac54b5667768ed77fb2cd22194120446e9b3f91 Mon Sep 17 00:00:00 2001 From: GinHa <15201596918@163.com> Date: Fri, 14 Aug 2026 15:23:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=A2=E9=98=9F=E7=A7=AF=E5=88=86V1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-admin/src/api/index.ts | 120 +-- .../src/pages/AdminCreditProducts.tsx | 186 ++-- .../src/pages/AdminCreditRecords.tsx | 118 ++- .../src/pages/AdminPaymentStats.tsx | 372 ++----- video-gen-admin/src/pages/AdminTeams.tsx | 431 ++------- video-gen-admin/src/pages/AdminUsers.tsx | 156 ++- video-gen-admin/src/types/index.ts | 114 ++- ...14_multi_subscription_team_subscription.py | 444 +++++++++ .../20260814_restore_product_renewal.py | 50 + .../20260814_subscription_business_no.py | 92 ++ .../app/api/admin/credit_management.py | 299 ++++-- .../app/api/admin/recharge_package.py | 5 +- video-gen-api/app/api/admin/team.py | 126 ++- video-gen-api/app/api/v1/admin.py | 308 +++--- video-gen-api/app/api/v1/credits.py | 71 +- video-gen-api/app/api/v1/payments.py | 41 +- video-gen-api/app/api/v1/recharge_packages.py | 1 + video-gen-api/app/api/v1/team.py | 475 +++++---- video-gen-api/app/enums/common.py | 28 + video-gen-api/app/enums/credit_balance.py | 21 +- video-gen-api/app/enums/credit_product.py | 31 +- .../app/enums/credit_subscription.py | 22 +- video-gen-api/app/enums/team.py | 13 + video-gen-api/app/models/__init__.py | 3 + video-gen-api/app/models/credit/__init__.py | 4 + video-gen-api/app/models/credit/allocation.py | 23 +- video-gen-api/app/models/credit/balance.py | 35 +- video-gen-api/app/models/credit/product.py | 61 +- .../app/models/credit/subscription.py | 34 +- .../app/models/credit/subscription_period.py | 7 +- video-gen-api/app/models/credit/team_seat.py | 41 + .../app/models/credit/team_seat_usage.py | 34 + video-gen-api/app/models/payment_order.py | 37 +- video-gen-api/app/models/team.py | 22 +- .../app/models/team_manager_history.py | 32 + video-gen-api/app/schemas/admin.py | 53 + video-gen-api/app/schemas/credit.py | 11 +- video-gen-api/app/schemas/credit_balance.py | 9 + video-gen-api/app/schemas/credit_product.py | 69 +- .../app/schemas/credit_subscription.py | 31 +- video-gen-api/app/schemas/payment.py | 71 +- video-gen-api/app/schemas/team.py | 4 + video-gen-api/app/schemas/team_manager.py | 13 +- .../app/schemas/team_subscription.py | 74 ++ .../services/admin_credit_record_service.py | 160 ++- .../services/credit/entitlement_service.py | 112 +++ .../app/services/credit/expiration_service.py | 17 +- .../app/services/credit/ledger_service.py | 915 ++++++++++-------- video-gen-api/app/services/credit/locking.py | 28 +- .../credit/offline_subscription_service.py | 218 +++++ .../app/services/credit/product_service.py | 323 ++++--- .../app/services/credit/query_service.py | 191 ++-- .../services/credit/subscription_service.py | 884 +++++++---------- .../credit/team_subscription_service.py | 890 +++++++++++++++++ .../app/services/credit/upgrade_service.py | 163 ---- video-gen-api/app/services/credits.py | 4 + video-gen-api/app/services/invoice.py | 5 + video-gen-api/app/services/payment.py | 350 ++++--- .../services/team_credit_record_service.py | 163 ++++ .../app/services/team_invitation_service.py | 27 +- .../app/services/team_manager_service.py | 244 ++++- video-gen-api/app/services/team_service.py | 343 +++++-- video-gen-api/app/tasks/credit_tasks.py | 3 +- video-gen-app/src/api/index.ts | 131 +-- .../src/components/Layout/AppLayout.tsx | 353 ++----- video-gen-app/src/pages/CreditRecordsPage.tsx | 202 +--- video-gen-app/src/pages/CreditsPage.tsx | 209 ++-- video-gen-app/src/pages/OrderRecordsPage.tsx | 228 +---- .../src/pages/TeamManagementPage.tsx | 831 ++++------------ video-gen-app/src/types/index.ts | 133 ++- 70 files changed, 6670 insertions(+), 4649 deletions(-) create mode 100644 video-gen-api/alembic/versions/20260814_multi_subscription_team_subscription.py create mode 100644 video-gen-api/alembic/versions/20260814_restore_product_renewal.py create mode 100644 video-gen-api/alembic/versions/20260814_subscription_business_no.py create mode 100644 video-gen-api/app/models/credit/team_seat.py create mode 100644 video-gen-api/app/models/credit/team_seat_usage.py create mode 100644 video-gen-api/app/models/team_manager_history.py create mode 100644 video-gen-api/app/schemas/team_subscription.py create mode 100644 video-gen-api/app/services/credit/entitlement_service.py create mode 100644 video-gen-api/app/services/credit/offline_subscription_service.py create mode 100644 video-gen-api/app/services/credit/team_subscription_service.py delete mode 100644 video-gen-api/app/services/credit/upgrade_service.py create mode 100644 video-gen-api/app/services/team_credit_record_service.py diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 0a764ade..d7ed2956 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -209,6 +209,13 @@ export async function getTeamMembersForAdmin(teamId: string, page = 1, pageSize return api.get(`/admin/users?${params.toString()}`); } +export async function getAdminTeamDetail(teamId: string): Promise { return api.get(`/admin/teams/${teamId}`); } +export async function getAdminTeamSubscriptions(teamId: string): Promise { return api.get(`/admin/teams/${teamId}/subscriptions`); } +export async function getAdminTeamMemberUsage(teamId: string, subscriptionId?: string): Promise { + const q = subscriptionId ? `?subscription_id=${encodeURIComponent(subscriptionId)}` : ''; return api.get(`/admin/teams/${teamId}/member-usage${q}`); +} +export async function getAdminTeamManagerHistory(teamId: string): Promise { return api.get(`/admin/teams/${teamId}/manager-history`); } + export async function adjustCredits(userId: string, amount: number, description: string): Promise { await api.post(`/admin/users/${userId}/credits`, { amount, description }); } @@ -427,6 +434,7 @@ export async function getCreditRecords(filters?: AdminCreditRecordQueryParams): setMaybe(params, 'user_type', filters?.userType); setMaybe(params, 'frontend_user_kind', filters?.frontendUserKind); setMaybe(params, 'team_id', filters?.teamId); + setMaybe(params, 'subscription_no', filters?.subscriptionNo); setMaybe(params, 'record_type', filters?.recordType || filters?.type); setMaybe(params, 'credit_subject', filters?.creditSubject); setMaybe(params, 'media_type', filters?.mediaType); @@ -632,42 +640,23 @@ export async function batchUpdatePaymentConfigs(configs: Record) await api.put('/admin/payment-configs/batch', configs); } -export async function getPaymentStats(params?: { - paymentMethod?: string; - status?: string; - startDate?: string; - endDate?: string; -}): Promise<{ - byStatus: Record; - today: { paidCount: number; paidAmount: number }; - month: { paidCount: number; paidAmount: number }; - recent: any[]; -}> { - const searchParams = new URLSearchParams(); - if (params?.paymentMethod) searchParams.set('payment_method', params.paymentMethod); - if (params?.status) searchParams.set('status', params.status); - if (params?.startDate) searchParams.set('start_date', params.startDate); - if (params?.endDate) searchParams.set('end_date', params.endDate); - const queryString = searchParams.toString(); - const url = queryString ? `/admin/payment-stats?${queryString}` : '/admin/payment-stats'; - return api.get(url); +export async function getPaymentStats(params?: { paymentMethod?: string; orderSource?: string; status?: string; startDate?: string; endDate?: string; }): Promise { + const q = new URLSearchParams(); + if (params?.paymentMethod) q.set('payment_method', params.paymentMethod); if (params?.orderSource) q.set('order_source', params.orderSource); + if (params?.status) q.set('status', params.status); if (params?.startDate) q.set('start_date', params.startDate); if (params?.endDate) q.set('end_date', params.endDate); + return api.get(`/admin/payment-stats${q.toString() ? `?${q.toString()}` : ''}`); } -export async function getAdminPaymentOrders(params?: { method?: string; status?: string; phone?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number }): Promise<{ items: any[]; total: number }> { +export async function getAdminPaymentOrders(params?: { method?: string; orderSource?: string; status?: string; phone?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number }): Promise<{ items: any[]; total: number }> { const qs = new URLSearchParams(); - if (params?.method) qs.set('payment_method', params.method); - if (params?.status) qs.set('status', params.status); - if (params?.phone) qs.set('phone', params.phone); - if (params?.startDate) qs.set('start_date', params.startDate); - if (params?.endDate) qs.set('end_date', params.endDate); - if (params?.page) qs.set('page', String(params.page)); - if (params?.pageSize) qs.set('page_size', String(params.pageSize)); + if (params?.method) qs.set('payment_method', params.method); if (params?.orderSource) qs.set('order_source', params.orderSource); if (params?.status) qs.set('status', params.status); + if (params?.phone) qs.set('phone', params.phone); if (params?.startDate) qs.set('start_date', params.startDate); if (params?.endDate) qs.set('end_date', params.endDate); + if (params?.page) qs.set('page', String(params.page)); if (params?.pageSize) qs.set('page_size', String(params.pageSize)); return api.get(`/admin/payment-orders?${qs.toString()}`); } -export async function refundPaymentOrder(orderNo: string): Promise { - await api.post(`/admin/payment-orders/${orderNo}/refund`); -} +// 退款接口后端仍保留用于兼容旧调用,但当前版本固定返回“暂未开放订单退款”。 +export async function refundPaymentOrder(orderNo: string): Promise { await api.post(`/admin/payment-orders/${orderNo}/refund`); } // ── Invoice Management ─────────────────────────────────── @@ -1371,63 +1360,26 @@ export async function adminUpdatePrivatePortraitConfig(userId: string, limit: nu // ── Dynamic Credit Products ─────────────────────────────── function normalizeCreditProduct(raw: CreditProduct): CreditProduct { - return { - ...raw, - renewalEnabled: raw.renewalEnabled === true, - tierRank: raw.tierRank == null ? null : Number(raw.tierRank), - monthlyGrantCredits: Number(raw.monthlyGrantCredits || 0), - firstPurchasePrice: Number(raw.firstPurchasePrice || 0), - regularPrice: Number(raw.regularPrice || 0), - activityPrice: raw.activityPrice == null ? null : Number(raw.activityPrice), - price: Number(raw.price || 0), - grantCredits: Number(raw.grantCredits || 0), - sortOrder: Number(raw.sortOrder || 0), - isActive: raw.isActive === true, - }; + return { ...raw, renewalEnabled: raw.renewalEnabled === true, tierRank: raw.tierRank == null ? null : Number(raw.tierRank), monthlyGrantCredits: Number(raw.monthlyGrantCredits || 0), + firstPurchasePrice: Number(raw.firstPurchasePrice || 0), regularPrice: Number(raw.regularPrice || 0), activityPrice: raw.activityPrice == null ? null : Number(raw.activityPrice), + price: Number(raw.price || 0), grantCredits: Number(raw.grantCredits || 0), sortOrder: Number(raw.sortOrder || 0), isActive: raw.isActive === true, isDeleted: raw.isDeleted === true }; } - -export async function getCreditProducts(productType?: 'subscription' | 'credit_addon'): Promise { - const query = productType ? `?product_type=${encodeURIComponent(productType)}` : ''; - const products = await api.get(`/admin/credit-management/products${query}`); - return products.map(normalizeCreditProduct); +export async function getCreditProducts(productType?: 'subscription' | 'team_subscription' | 'credit_addon'): Promise { + const query = productType ? `?product_type=${encodeURIComponent(productType)}` : ''; return (await api.get(`/admin/credit-management/products${query}`)).map(normalizeCreditProduct); } - -export async function createCreditProduct(payload: Record): Promise { - return normalizeCreditProduct(await api.post('/admin/credit-management/products', payload)); -} - -export async function updateCreditProduct(id: string, payload: Record): Promise { - return normalizeCreditProduct(await api.put(`/admin/credit-management/products/${id}`, payload)); -} - -export async function setCreditProductRenewal(id: string, renewalEnabled: boolean): Promise { - return normalizeCreditProduct(await api.put( - `/admin/credit-management/products/${id}/renewal`, - { renewal_enabled: renewalEnabled }, - )); -} - -export async function disableCreditProduct(id: string): Promise { - await api.delete(`/admin/credit-management/products/${id}`); -} - -export async function getAdminUserCreditSummary(userId: string): Promise { - return api.get(`/admin/credit-management/users/${userId}/summary`); -} - -export async function getAdminUserCreditBalances(userId: string, page = 1, pageSize = 50, status?: string): Promise { - const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); - if (status) params.set('status', status); - return api.get(`/admin/credit-management/users/${userId}/balances?${params.toString()}`); -} - -export async function adminGrantCredits(userId: string, payload: Record): Promise { - return api.post(`/admin/credit-management/users/${userId}/grant`, payload); -} - -export async function adminDeductCredits(userId: string, payload: Record): Promise { - return api.post(`/admin/credit-management/users/${userId}/deduct`, payload); +export async function createCreditProduct(payload: Record): Promise { return normalizeCreditProduct(await api.post('/admin/credit-management/products', payload)); } +export async function updateCreditProduct(id: string, payload: Record): Promise { return normalizeCreditProduct(await api.put(`/admin/credit-management/products/${id}`, payload)); } +export async function setCreditProductRenewal(id: string, renewalEnabled: boolean): Promise { return normalizeCreditProduct(await api.put(`/admin/credit-management/products/${id}/renewal`, { renewal_enabled: renewalEnabled })); } +export async function setCreditProductStatus(id: string, isActive: boolean): Promise { return normalizeCreditProduct(await api.put(`/admin/credit-management/products/${id}/status`, { is_active: isActive })); } +export async function softDeleteCreditProduct(id: string): Promise { await api.delete(`/admin/credit-management/products/${id}`); } +export async function getAdminUserCreditSummary(userId: string): Promise { return api.get(`/admin/credit-management/users/${userId}/summary`); } +export async function getAdminUserCreditBalances(userId: string, page = 1, pageSize = 50, status?: string): Promise { const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); if (status) params.set('status', status); return api.get(`/admin/credit-management/users/${userId}/balances?${params.toString()}`); } +export async function getAdminUserSubscriptions(userId: string): Promise { return api.get(`/admin/credit-management/users/${userId}/subscriptions`); } +export async function createAdminOfflineSubscription(userId: string, payload: { productId: string; quantity: number; paymentMethod: 'bank_transfer' | 'cash' | 'other'; actualPaidAmount?: number; offlineTradeNo?: string; offlinePaymentDetail?: string; remark?: string; }): Promise { + return api.post(`/admin/credit-management/users/${userId}/offline-subscriptions`, { product_id: payload.productId, quantity: payload.quantity, payment_method: payload.paymentMethod, actual_paid_amount: payload.actualPaidAmount, offline_trade_no: payload.offlineTradeNo || null, offline_payment_detail: payload.offlinePaymentDetail || null, remark: payload.remark || null }); } +export async function adminGrantCredits(userId: string, payload: Record): Promise { return api.post(`/admin/credit-management/users/${userId}/grant`, payload); } +export async function adminDeductCredits(userId: string, payload: Record): Promise { return api.post(`/admin/credit-management/users/${userId}/deduct`, payload); } // ── LLM 场景积分配置 ─────────────────────────────────────── export async function getLlmBillingPolicies(): Promise { diff --git a/video-gen-admin/src/pages/AdminCreditProducts.tsx b/video-gen-admin/src/pages/AdminCreditProducts.tsx index 9cf3ace5..8f80efe3 100644 --- a/video-gen-admin/src/pages/AdminCreditProducts.tsx +++ b/video-gen-admin/src/pages/AdminCreditProducts.tsx @@ -3,10 +3,17 @@ import { Button, Card, DatePicker, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography, } from 'antd'; -import { EditOutlined, PlusOutlined, ShoppingOutlined, StopOutlined } from '@ant-design/icons'; +import { + CheckCircleOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ShoppingOutlined, StopOutlined, +} from '@ant-design/icons'; import dayjs from 'dayjs'; import { - createCreditProduct, disableCreditProduct, getCreditProducts, setCreditProductRenewal, updateCreditProduct, + createCreditProduct, + getCreditProducts, + setCreditProductRenewal, + setCreditProductStatus, + softDeleteCreditProduct, + updateCreditProduct, } from '../api'; import type { CreditProduct, CreditProductType } from '../types'; @@ -17,6 +24,8 @@ const formatMoney = (value?: number | null): string => { return Number.isFinite(amount) ? `¥${amount.toFixed(2)}` : '-'; }; +const isSubscriptionType = (value: CreditProductType) => value === 'subscription' || value === 'team_subscription'; + const AdminCreditProducts: React.FC = () => { const [type, setType] = useState('subscription'); const [items, setItems] = useState([]); @@ -85,7 +94,7 @@ const AdminCreditProducts: React.FC = () => { sortOrder: 0, billingCycle: 'monthly', tierRank: 1, - renewalEnabled: nextRenewalEnabled, + renewalEnabled: true, validityMonths: 1, }); }; @@ -95,8 +104,6 @@ const AdminCreditProducts: React.FC = () => { const values = await form.validateFields(); const activityRange = values.activityRange || []; const payload: Record = { - product_code: values.productCode, - product_type: type, name: values.name, description: values.description || null, features: String(values.featuresText || '') @@ -105,14 +112,23 @@ const AdminCreditProducts: React.FC = () => { .filter(Boolean), credit_level: 'general', currency: 'CNY', - is_active: values.isActive ?? true, sort_order: values.sortOrder ?? 0, }; - if (type === 'subscription') { + if (!editing) { + payload.product_code = values.productCode; + payload.product_type = type; + payload.is_active = values.isActive ?? true; + } + + if (isSubscriptionType(type)) { + if (!editing) { + Object.assign(payload, { + tier_code: values.tierCode, + tier_rank: values.tierRank, + billing_cycle: values.billingCycle, + }); + } Object.assign(payload, { - tier_code: values.tierCode, - tier_rank: values.tierRank, - billing_cycle: values.billingCycle, monthly_grant_credits: values.monthlyGrantCredits, first_purchase_price: values.firstPurchasePrice, regular_price: values.regularPrice, @@ -129,11 +145,16 @@ const AdminCreditProducts: React.FC = () => { validity_months: values.validityMonths, }); } - const savedProduct = editing + + let savedProduct = editing ? await updateCreditProduct(editing.id, payload) : await createCreditProduct(payload); - if (type === 'subscription' && savedProduct.renewalEnabled !== renewalEnabled) { + if (editing && savedProduct.isActive !== (values.isActive === true)) { + savedProduct = await setCreditProductStatus(editing.id, values.isActive === true); + } + + if (isSubscriptionType(type) && savedProduct.renewalEnabled !== renewalEnabled) { throw new Error('续费开关保存结果与提交值不一致,请刷新后重试'); } @@ -190,7 +211,27 @@ const AdminCreditProducts: React.FC = () => { } }; - const columns = useMemo(() => type === 'subscription' ? [ + const toggleStatus = async (row: CreditProduct) => { + try { + const saved = await setCreditProductStatus(row.id, !row.isActive); + replaceItem(saved); + message.success(saved.isActive ? '已上架' : '已下架'); + } catch (error: any) { + message.error(error?.message || '商品状态更新失败'); + } + }; + + const removeProduct = async (row: CreditProduct) => { + try { + await softDeleteCreditProduct(row.id); + message.success('商品已软删除,商品编码永久保留且不能恢复'); + await load(); + } catch (error: any) { + message.error(error?.message || '商品删除失败'); + } + }; + + const columns = useMemo(() => isSubscriptionType(type) ? [ { title: '套餐', key: 'name', @@ -234,36 +275,45 @@ const AdminCreditProducts: React.FC = () => { { title: '允许续费', dataIndex: 'renewalEnabled', - render: (enabled: boolean, row: CreditProduct) => ( - row.isDeleted + ? - + : void toggleRenewal(row, checked)} - /> - ), + />, }, { title: '状态', - dataIndex: 'isActive', - render: (enabled: boolean) => {enabled ? '上架' : '下架'}, + key: 'status', + render: (_: unknown, row: CreditProduct) => + {row.isDeleted ? '已删除' : row.isActive ? '上架' : '下架'} + , }, { title: '操作', key: 'action', - render: (_: unknown, row: CreditProduct) => - - { - await disableCreditProduct(row.id); - message.success('已下架'); - await load(); - }} - > - - - , + render: (_: unknown, row: CreditProduct) => row.isDeleted + ? 已软删除,不可恢复 + : + + void toggleStatus(row)} + > + + + void removeProduct(row)} + > + + + , }, ] : [ { @@ -277,37 +327,53 @@ const AdminCreditProducts: React.FC = () => { { title: '积分', dataIndex: 'grantCredits', render: (value?: number) => Number(value || 0).toLocaleString() }, { title: '价格', dataIndex: 'price', render: (value?: number | null) => formatMoney(value) }, { title: '有效期', dataIndex: 'validityMonths', render: (value?: number | null) => `${Number(value || 1)} 个月` }, - { title: '状态', dataIndex: 'isActive', render: (enabled: boolean) => {enabled ? '上架' : '下架'} }, + { + title: '状态', key: 'status', render: (_: unknown, row: CreditProduct) => + {row.isDeleted ? '已删除' : row.isActive ? '上架' : '下架'} + , + }, { title: '操作', key: 'action', - render: (_: unknown, row: CreditProduct) => - - { - await disableCreditProduct(row.id); - message.success('已下架'); - await load(); - }} - > - - - , + render: (_: unknown, row: CreditProduct) => row.isDeleted + ? 已软删除,不可恢复 + : + + void toggleStatus(row)} + > + + + void removeProduct(row)} + > + + + , }, - ], [type, items]); + ], [type, items, editing]); + + const createButtonLabel = type === 'subscription' + ? '新增个人订阅套餐' + : type === 'team_subscription' + ? '新增团队订阅套餐' + : '新增积分增值包'; return 积分产品} - extra={} + extra={} > setType(key as CreditProductType)} items={[ - { key: 'subscription', label: '订阅套餐' }, + { key: 'subscription', label: '个人订阅套餐' }, + { key: 'team_subscription', label: '团队订阅套餐' }, { key: 'credit_addon', label: '积分增值包' }, ]} /> @@ -322,7 +388,12 @@ const AdminCreditProducts: React.FC = () => { >
- + @@ -331,12 +402,13 @@ const AdminCreditProducts: React.FC = () => { - {type === 'subscription' ? <> + {isSubscriptionType(type) ? <> - - + + { setPage(1); setSubscriptionNoFilter(e.target.value); }} style={{ width: 190 }} allowClear /> { setPage(1); setCreditSubject(v); }} style={{ width: 180 }} options={creditSubjectOptions} /> { setFilters(prev => ({ ...prev, paymentMethod: value })); setOrderPage(1); }} - > - - - - - - 状态 - - - - 手机号 - setFilters(prev => ({ ...prev, phone: e.target.value }))} - onPressEnter={() => { setOrderPage(1); load(); }} - suffix={} - /> - - - 日期范围 - - - - - - - - `共 ${t} 条`, - }} - scroll={{ x: 1000 }} - size="middle" - /> - - - - ); + return
+ +
} />线上 {Number(today.onlinePaidAmount || 0).toFixed(2)} 元 · 线下 {Number(today.offlinePaidAmount || 0).toFixed(2)} 元 + } />线上 {Number(month.onlinePaidAmount || 0).toFixed(2)} 元 · 线下 {Number(month.offlinePaidAmount || 0).toFixed(2)} 元 + } />{Number(stats?.totalIncome?.count || 0)} 笔已支付成交 + + {['online_payment','admin_offline'].map((source) =>
{bySource[source]?.label || SOURCE_LABELS[source]}
¥{Number(bySource[source]?.amount || 0).toFixed(2)}
{Number(bySource[source]?.count || 0)} 笔
)} + + + { setFilters((x:any) => ({...x, paymentMethod:v})); setPage(1); }} options={Object.entries(METHOD_LABELS).map(([value,label]) => ({value,label}))} /> + setFilters((x:any) => ({...x, phone:e.target.value}))} onPressEnter={() => { setPage(1); void load(); }} suffix={} /> + { setFilters((x:any) => ({...x, startDate:v?.[0]?.format('YYYY-MM-DD'), endDate:v?.[1]?.format('YYYY-MM-DD')})); setPage(1); }} /> + + +
`共 ${t} 条`, onChange:(p,ps)=>{setPage(p);setPageSize(ps);} }} /> + + ; }; export default AdminPaymentStats; diff --git a/video-gen-admin/src/pages/AdminTeams.tsx b/video-gen-admin/src/pages/AdminTeams.tsx index 8fd7f01a..1cbc47d4 100644 --- a/video-gen-admin/src/pages/AdminTeams.tsx +++ b/video-gen-admin/src/pages/AdminTeams.tsx @@ -1,378 +1,79 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd'; -import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons'; -import { deleteAdminTeam, getAdminTeams, getTeamMembersForAdmin, saveAdminTeam, setTeamManager } from '../api'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Alert, Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Typography } from 'antd'; +import { DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, TeamOutlined, UserSwitchOutlined } from '@ant-design/icons'; +import { + deleteAdminTeam, getAdminTeamManagerHistory, getAdminTeamMemberUsage, getAdminTeams, getAdminTeamSubscriptions, + getTeamMembersForAdmin, saveAdminTeam, setTeamManager, +} from '../api'; import type { AdminTeam, AdminUser } from '../types'; -import { formatDate } from '../utils/formatDate'; -const statusOptions = [ - { value: '', label: '全部状态' }, - { value: 'active', label: '启用' }, - { value: 'disabled', label: '禁用' }, -]; - -const editStatusOptions = [ - { value: 'active', label: '启用' }, - { value: 'disabled', label: '禁用' }, -]; - -function statusLabel(status?: string): string { - if (status === 'active') return '启用'; - if (status === 'disabled') return '禁用'; - return status || '-'; -} +const STATUS = { active: '启用', disabled: '禁用' } as Record; +const fmt = (v?: string | null) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-'; const AdminTeams: React.FC = () => { - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(20); - const [total, setTotal] = useState(0); - const [keyword, setKeyword] = useState(''); - const [status, setStatus] = useState(''); - const [modal, setModal] = useState<{ open: boolean; item: AdminTeam | null }>({ open: false, item: null }); - const [form] = Form.useForm(); + const [items, setItems] = useState([]); const [loading,setLoading]=useState(false); const [page,setPage]=useState(1); const [pageSize,setPageSize]=useState(20); const [total,setTotal]=useState(0); + const [keyword,setKeyword]=useState(''); const [status,setStatus]=useState(); const [form]=Form.useForm(); const [edit,setEdit]=useState(null); const [editOpen,setEditOpen]=useState(false); + const [detail,setDetail]=useState(null); const [members,setMembers]=useState([]); const [subscriptions,setSubscriptions]=useState([]); const [usage,setUsage]=useState([]); const [history,setHistory]=useState([]); const [detailLoading,setDetailLoading]=useState(false); + const query=useMemo(()=>({page,pageSize,keyword:keyword||undefined,status}),[page,pageSize,keyword,status]); + const load=useCallback(async()=>{setLoading(true);try{const r=await getAdminTeams(query);setItems(r.items||[]);setTotal(Number(r.total||0));}catch(e:any){message.error(e?.message||'团队列表加载失败');}finally{setLoading(false);}},[query]); + useEffect(()=>{void load();},[load]); - // 管理人弹窗状态 - const [managerModal, setManagerModal] = useState<{ open: boolean; team: AdminTeam | null }>({ open: false, team: null }); - const [managerMembers, setManagerMembers] = useState([]); - const [managerLoading, setManagerLoading] = useState(false); - const [selectedManagerId, setSelectedManagerId] = useState(null); + const openEdit=(team?:AdminTeam)=>{const item=team||null;setEdit(item);form.resetFields();form.setFieldsValue(item?{name:item.name,code:item.code,description:item.description,status:item.status,sortOrder:item.sortOrder}:{status:'active',sortOrder:0});setEditOpen(true);}; + const save=async()=>{try{const v=await form.validateFields();await saveAdminTeam({id:edit?.id,name:v.name,code:v.code||null,description:v.description||null,status:v.status,sort_order:Number(v.sortOrder||0)});message.success(edit?'团队已更新':'团队已创建');setEditOpen(false);await load();}catch(e:any){if(!e?.errorFields)message.error(e?.message||'保存失败');}}; + const openDetail=async(team:AdminTeam)=>{setDetail(team);setDetailLoading(true);try{const [m,s,u,h]=await Promise.all([getTeamMembersForAdmin(team.id,1,500),getAdminTeamSubscriptions(team.id),getAdminTeamMemberUsage(team.id),getAdminTeamManagerHistory(team.id)]);setMembers(m.items||[]);setSubscriptions(s||[]);setUsage(u||[]);setHistory(h||[]);}catch(e:any){message.error(e?.message||'团队详情加载失败');}finally{setDetailLoading(false);}}; - // 查看成员弹窗状态 - const [membersModal, setMembersModal] = useState<{ open: boolean; team: AdminTeam | null }>({ open: false, team: null }); - const [teamMembers, setTeamMembers] = useState([]); - const [membersLoading, setMembersLoading] = useState(false); - - const query = useMemo(() => ({ - page, - pageSize, - keyword: keyword || undefined, - status: status || undefined, - }), [page, pageSize, keyword, status]); - - const load = async () => { - setLoading(true); - try { - const res = await getAdminTeams(query); - setItems(res.items || []); - setTotal(res.total || 0); - } catch (e: any) { - message.error(e?.message || '加载团队列表失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { load(); }, [query]); - - const openCreate = () => { - form.resetFields(); - form.setFieldsValue({ status: 'active', sortOrder: 0 }); - setModal({ open: true, item: null }); - }; - - const openEdit = (item: AdminTeam) => { - form.setFieldsValue({ - name: item.name, - code: item.code || '', - description: item.description || '', - status: item.status || 'active', - sortOrder: item.sortOrder || 0, - }); - setModal({ open: true, item }); - }; - - const handleSave = async () => { - try { - const values = await form.validateFields(); - setSaving(true); - await saveAdminTeam({ - id: modal.item?.id, - name: values.name, - code: values.code || null, - description: values.description || null, - status: values.status || 'active', - sort_order: values.sortOrder || 0, - }); - message.success(modal.item ? '团队已更新' : '团队已创建'); - setModal({ open: false, item: null }); - form.resetFields(); - load(); - } catch (e: any) { - if (e?.errorFields) return; - message.error(e?.message || '保存失败'); - } finally { - setSaving(false); - } - }; - - const handleDelete = async (item: AdminTeam) => { - try { - await deleteAdminTeam(item.id); - message.success('团队已删除'); - load(); - } catch (e: any) { - message.error(e?.message || '删除失败'); - } - }; - - const loadTeamMembers = async (teamId: string): Promise => { - try { - const res = await getTeamMembersForAdmin(teamId); - return res.items || []; - } catch (e: any) { - message.error(e?.message || '加载成员失败'); - return []; - } - }; - - const openManagerModal = async (team: AdminTeam) => { - setManagerModal({ open: true, team }); - setSelectedManagerId(team.managerId || null); - setManagerLoading(true); - const data = await loadTeamMembers(team.id); - setManagerMembers(data); - setManagerLoading(false); - }; - - const openMembersModal = async (team: AdminTeam) => { - setMembersModal({ open: true, team }); - setMembersLoading(true); - const data = await loadTeamMembers(team.id); - setTeamMembers(data); - setMembersLoading(false); - }; - - const handleSetManager = async () => { - if (!managerModal.team) return; - try { - setManagerLoading(true); - await setTeamManager(managerModal.team.id, selectedManagerId); - message.success(selectedManagerId ? '已设置管理人' : '已取消管理人'); - setManagerModal({ open: false, team: null }); - load(); - } catch (e: any) { - message.error(e?.message || '设置失败'); - } finally { - setManagerLoading(false); - } - }; - - const columns = [ - { - title: '团队名称', - dataIndex: 'name', - width: 220, - render: (v: string, r: AdminTeam) => ( - -
- -
-
- {v} -
{r.code || '-'}
-
-
- ), - }, - { - title: '状态', - dataIndex: 'status', - width: 100, - render: (v: string) => {statusLabel(v)}, - }, - { - title: '成员数', - dataIndex: 'memberCount', - width: 100, - render: (v: number) => {Number(v || 0).toLocaleString()}, - }, - { - title: '管理人', - key: 'manager', - width: 140, - render: (_: any, r: AdminTeam) => ( - - {r.managerName || 未设置} - - ), - }, - { - title: '排序', - dataIndex: 'sortOrder', - width: 90, - render: (v: number) => v ?? 0, - }, - { - title: '备注', - dataIndex: 'description', - ellipsis: true, - render: (v: string) => v || '-', - }, - { - title: '创建时间', - dataIndex: 'createdAt', - width: 160, - render: (v: string) => {formatDate(v)}, - }, - { - title: '操作', - key: 'action', - width: 230, - fixed: 'right' as const, - render: (_: any, r: AdminTeam) => ( - - - - - 0 ? '该团队下仍有成员,后端会拒绝删除。' : '删除后团队不再出现在设置下拉中。'} - onConfirm={() => handleDelete(r)} - > - - - - ), - }, + const columns=[ + {title:'团队',dataIndex:'name',width:220,render:(v:string,r:AdminTeam)=>
{v}
{r.code||'-'}
}, + {title:'状态',dataIndex:'status',width:100,render:(v:string,r:AdminTeam)=>{r.statusLabel||STATUS[v]||'其他状态'}}, + {title:'成员数',dataIndex:'memberCount',width:90,align:'right' as const}, + {title:'当前队长',dataIndex:'managerName',width:140,render:(v:string)=>v||'-'}, + {title:'团队首购时间',dataIndex:'firstSubscriptionPaidAt',width:180,render:(v:string)=>fmt(v)}, + {title:'排序',dataIndex:'sortOrder',width:80}, + {title:'备注',dataIndex:'description',ellipsis:true,render:(v:string)=>v||'-'}, + {title:'操作',fixed:'right' as const,width:220,render:(_:any,r:AdminTeam)=>{try{await deleteAdminTeam(r.id);message.success('团队已删除');await load();}catch(e:any){message.error(e?.message||'删除失败');}}}>}, ]; - return ( -
- -
- - } - value={keyword} - onChange={(e) => { setPage(1); setKeyword(e.target.value); }} - onPressEnter={load} - style={{ width: 260 }} - allowClear - /> -
{ setPage(p); setPageSize(ps); }, - showSizeChanger: true, - showTotal: (t) => `共 ${t} 个团队`, - }} - scroll={{ x: 1100 }} - /> - + return
{setKeyword(e.target.value);setPage(1);}} placeholder="团队名称/编码/备注" prefix={} style={{width:260}}/>
{setPage(p);setPageSize(ps);}}}/> - {modal.item ? '编辑团队' : '新增团队'}} - open={modal.open} - confirmLoading={saving} - onOk={handleSave} - onCancel={() => { setModal({ open: false, item: null }); form.resetFields(); }} - okText="保存" - cancelText="取消" - width={520} - > - - - - - - - - - setSelectedManagerId(v || null)} - allowClear - loading={managerLoading} - optionFilterProp="label" - options={managerMembers.map((m) => ({ - value: m.id, - label: `${m.username}${m.phone ? ` (${m.phone})` : ''}`, - }))} - showSearch - /> - - - - {/* 查看成员弹窗 */} - 团队成员 - {membersModal.team?.name}} - open={membersModal.open} - onCancel={() => setMembersModal({ open: false, team: null })} - footer={null} - width={600} - > -
- {membersModal.team && ( -
{v} }, - { title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => v || '-' }, - { title: '积分', dataIndex: 'credits', width: 100, render: (v: number) => {(v ?? 0).toFixed(2)} }, - { - title: '状态', dataIndex: 'isActive', width: 80, - render: (v: boolean) => {v ? '启用' : '禁用'}, - }, - ]} - locale={{ emptyText: '该团队暂无成员' }} - /> - )} - - - - ); + setDetail(null)}> + {detail?.status==='disabled'&&} + r.subscription?.id} dataSource={subscriptions} columns={subColumns} pagination={false} scroll={{x:1120}} expandable={{expandedRowRender:(r:any)=>
}}/>}, + {key:'members',label:'成员与队长',children:
}, + {key:'usage',label:'成员月净消耗',children:
`${r.userId}-${r.subscriptionPeriodId}`} + dataSource={usage} + pagination={false} + scroll={{x:1400}} + columns={[ + {title:'成员',dataIndex:'username',width:120}, + {title:'订阅实例',dataIndex:'subscriptionNo',width:190,render:(v:string)=>{v||'历史订阅'}}, + {title:'套餐名称',dataIndex:'subscriptionName',width:180,render:(v:string)=>{v||'历史团队订阅'}}, + {title:'等级编码',width:190,render:(_:any,r:any)=>{r.tierLabel||'未知等级'}{r.tierCode?`(${r.tierCode})`:''}{r.tierRank?` / ${r.tierRank}`:''}}, + {title:'套餐周期',dataIndex:'billingCycleLabel',width:110,render:(v:string)=>{v||'未知周期'}}, + {title:'月度周期',dataIndex:'periodLabel',width:110,render:(v:string,r:any)=>v||(Number(r.periodSequence)>0?`第${r.periodSequence}个月`:'历史周期')}, + {title:'周期时间',width:320,render:(_:any,r:any)=>`${fmt(r.periodStartAt)} ~ ${fmt(r.periodExpiresAt)}`}, + {title:'本期净消耗',dataIndex:'consumedCredits',width:130,align:'right' as const,render:(v:number)=>Number(v||0).toLocaleString()}, + ]} + />}, + {key:'history',label:'队长任期历史',children:
fmt(v)},{title:'结束',dataIndex:'endedAt',render:(v:string)=>v?fmt(v):当前任期}]}/>}, + ]}/> + + ; }; - export default AdminTeams; diff --git a/video-gen-admin/src/pages/AdminUsers.tsx b/video-gen-admin/src/pages/AdminUsers.tsx index ede1e692..a1bd2ef6 100644 --- a/video-gen-admin/src/pages/AdminUsers.tsx +++ b/video-gen-admin/src/pages/AdminUsers.tsx @@ -11,8 +11,10 @@ import { adminGetPrivatePortraitConfig, adminUpdatePrivatePortraitConfig, createUser, + createAdminOfflineSubscription, deleteUserResourceCapacity, getAdminUsers, + getCreditProducts, getAdminUserCreditBalances, getAdminUserCreditSummary, getMenuConfigs, @@ -29,7 +31,7 @@ import { updateUserMenus, updateUserAdminStatus, } from '../api'; -import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, PrivatePortraitConfig, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types'; +import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, CreditProduct, PrivatePortraitConfig, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types'; import { formatDate } from '../utils/formatDate'; const TEAM_UNASSIGNED_VALUE = '__none__'; @@ -71,6 +73,9 @@ const AdminUsers: React.FC = () => { const [teamOptions, setTeamOptions] = useState([]); const [creditModal, setCreditModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); const [creditDetailModal, setCreditDetailModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); + const [offlineModal, setOfflineModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); + const [offlineProducts, setOfflineProducts] = useState([]); + const [offlineSaving, setOfflineSaving] = useState(false); const [creditDetailLoading, setCreditDetailLoading] = useState(false); const [creditSummary, setCreditSummary] = useState(null); const [creditBalances, setCreditBalances] = useState([]); @@ -100,6 +105,9 @@ const AdminUsers: React.FC = () => { const [capacityForm] = Form.useForm(); const [teamForm] = Form.useForm(); const [portraitForm] = Form.useForm(); + const [offlineForm] = Form.useForm(); + const offlineProductId = Form.useWatch('productId', offlineForm); + const selectedOfflineProduct = offlineProducts.find((item) => item.id === offlineProductId); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); @@ -203,6 +211,56 @@ const AdminUsers: React.FC = () => { } }; + const openOfflineModal = async (user: AdminUser) => { + try { + const products = await getCreditProducts(); + const available = products.filter((item) => + (item.productType === 'subscription' || item.productType === 'team_subscription') + && item.isActive + && !item.isDeleted + ); + setOfflineProducts(available); + offlineForm.resetFields(); + offlineForm.setFieldsValue({ quantity: 1, paymentMethod: 'bank_transfer' }); + setOfflineModal({ open: true, user }); + } catch (e: any) { + message.error(e?.message || '加载可成交套餐失败'); + } + }; + + const handleOfflineSubscription = async () => { + const user = offlineModal.user; + if (!user) return; + try { + const values = await offlineForm.validateFields(); + const product = offlineProducts.find((item) => item.id === values.productId); + if (!product) { + message.error('请选择有效套餐'); + return; + } + const quantity = product.productType === 'team_subscription' ? Number(values.quantity || 2) : 1; + setOfflineSaving(true); + await createAdminOfflineSubscription(user.id, { + productId: product.id, + quantity, + paymentMethod: values.paymentMethod, + actualPaidAmount: values.actualPaidAmount === undefined || values.actualPaidAmount === null ? undefined : Number(values.actualPaidAmount), + offlineTradeNo: values.offlineTradeNo, + offlinePaymentDetail: values.offlinePaymentDetail, + remark: values.remark, + }); + message.success('线下订阅成交已完成,订单、套餐与首期权益已同步生效'); + setOfflineModal({ open: false, user: null }); + offlineForm.resetFields(); + await load(); + } catch (e: any) { + if (e?.errorFields) return; + message.error(e?.message || '线下订阅成交失败'); + } finally { + setOfflineSaving(false); + } + }; + const openCreditDetailModal = async (user: AdminUser, status = '') => { setCreditDetailModal({ open: true, user }); setCreditBalanceStatus(status); @@ -480,11 +538,15 @@ const AdminUsers: React.FC = () => { ), }, ...(!isAdminTab ? [{ - title: '积分余额', dataIndex: 'credits', width: 120, sorter: (a: AdminUser, b: AdminUser) => a.credits - b.credits, - render: (v: number) => ( - 0 ? '#10b981' : '#ef4444', fontSize: 15 }}> - {v.toLocaleString()} - + title: '积分余额', dataIndex: 'credits', width: 210, sorter: (a: AdminUser, b: AdminUser) => a.credits - b.credits, + render: (v: number, r: AdminUser) => ( +
+ 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>可消费 {Number(v || 0).toLocaleString()} +
+ 个人 {Number(r.personalCredits || 0).toLocaleString()} / 团队 {Number(r.teamAvailableCredits || 0).toLocaleString()} +
+ {Number(r.teamFrozenCredits || 0) > 0 &&
团队冻结 {Number(r.teamFrozenCredits || 0).toLocaleString()}
} +
), }] : []), { @@ -551,7 +613,7 @@ const AdminUsers: React.FC = () => { render: (v: string) => {formatDate(v)}, }, { - title: '操作', key: 'action', width: 560, fixed: 'right' as const, + title: '操作', key: 'action', width: 650, fixed: 'right' as const, render: (_: any, r: AdminUser) => ( {!isAdminTab && ( @@ -570,6 +632,11 @@ const AdminUsers: React.FC = () => { 积分明细 )} + {!isAdminTab && ( + + )} {!isAdminTab && ( } + {enabledMethods.wechat && } - - {/* 支付宝 Tab */} - {paymentTab === 'alipay' && ( -
-
- - 请用支付宝扫码支付 -
-
- {qrRevealed && currentPaymentInfo?.qrCode ? ( - - ) : ( -
-
- 确认支付金额后
生成二维码 -
- -
- )} -
- {qrRevealed && ( -
- {countdown}秒后二维码失效 +
+
{paymentMethod === 'wechat' ? '请用微信扫码支付' : '请用支付宝扫码支付'}
+
+ {qrRevealed && currentPaymentInfo?.qrCode ? : ( +
+
确认商品和金额后生成二维码
+
)} -
- 开通即代表同意
《用户服务协议》 -
- {qrRevealed && ( - - )}
- )} - - {/* 对公转账 Tab */} - {paymentTab === 'corporate' && ( -
- {/* 汇款信息 */} -
- 收款账户信息 -
-
- 账户名称 - {COMPANY_BANK_INFO.accountName} -
-
- 开户银行 - - - {COMPANY_BANK_INFO.bankName} - -
-
- 账号 - {COMPANY_BANK_INFO.bankAccount} -
-
-
- - {/* 打款信息填写 */} -
- 填写您的打款信息 - 注:打款金额需与所标注金额一致,不然无法开通,银行信息需正确填写。 -
- - - - - } /> - - - - - -
- - -
- 提交后客服将在12小时内审核到账。如有疑问,请联系客服。 -
-
- )} + {qrRevealed &&
{countdown} 秒后二维码失效
} + {qrRevealed && } +
+
线上订单仅支持已启用的支付宝/微信支付;后台线下成交请联系客服处理。
diff --git a/video-gen-app/src/pages/CreditRecordsPage.tsx b/video-gen-app/src/pages/CreditRecordsPage.tsx index c5375455..336937a7 100644 --- a/video-gen-app/src/pages/CreditRecordsPage.tsx +++ b/video-gen-app/src/pages/CreditRecordsPage.tsx @@ -1,186 +1,50 @@ import React, { useEffect, useState } from 'react'; -import { Table, Tag, Empty, Spin, Typography, Row, Col, Pagination } from 'antd'; -import { WalletOutlined, PlusCircleOutlined, ThunderboltOutlined } from '@ant-design/icons'; +import { Col, Empty, Pagination, Row, Spin, Table, Tag, Typography } from 'antd'; +import { LockOutlined, TeamOutlined, ThunderboltOutlined, WalletOutlined } from '@ant-design/icons'; import { getCredits } from '../api'; const CreditRecordsPage: React.FC = () => { const [loading, setLoading] = useState(true); - const [records, setRecords] = useState([]); - const [credits, setCredits] = useState(0); - const [total, setTotal] = useState(0); - const [totalGranted, setTotalGranted] = useState(0); - const [totalConsumed, setTotalConsumed] = useState(0); + const [data, setData] = useState({ records: [], total: 0, credits: 0, personalCredits: 0, teamAvailableCredits: 0, teamFrozenCredits: 0, totalGranted: 0, totalConsumed: 0 }); const [page, setPage] = useState(1); - const [pageSize] = useState(10); + const [pageSize, setPageSize] = useState(10); useEffect(() => { - loadData(); - }, [page]); - - const loadData = async () => { - setLoading(true); - try { - const data = await getCredits(page, pageSize); - setRecords(data.records || []); - setCredits(data.availableCredits ?? data.credits ?? 0); - setTotal(data.total || 0); - setTotalGranted(data.totalGranted ?? 0); - setTotalConsumed(data.totalConsumed ?? 0); - } catch { - setRecords([]); - } - setLoading(false); - }; - - const handlePageChange = (newPage: number) => { - setPage(newPage); - }; + const load = async () => { + setLoading(true); + try { setData(await getCredits(page, pageSize)); } finally { setLoading(false); } + }; + void load(); + }, [page, pageSize]); const columns = [ - { - title: '变动类型', - dataIndex: 'type', - key: 'type', - width: 120, - render: (text: string) => { - const typeConfig: Record = { - recharge: { color: '#10b981', label: '充值', bg: 'rgba(16,185,129,0.1)' }, - consume: { color: '#ef4444', label: '消费', bg: 'rgba(239,68,68,0.1)' }, - admin: { color: '#f59e0b', label: '管理员调整', bg: 'rgba(245,158,11,0.1)' }, - refund: { color: '#8b5cf6', label: '退款', bg: 'rgba(139,92,246,0.1)' }, - team_internal: { color: '#0958d9', label: '历史团队流转', bg: 'rgba(9,88,217,0.1)' }, - expire: { color: '#64748b', label: '积分过期', bg: 'rgba(100,116,139,0.1)' }, - revoke: { color: '#dc2626', label: '积分撤销', bg: 'rgba(220,38,38,0.1)' }, - }; - const config = typeConfig[text] || { color: '#64748b', label: text, bg: 'rgba(100,116,139,0.1)' }; - return ( - - {config.label} - - ); - }, - }, - { - title: '描述', - dataIndex: 'description', - key: 'description', - ellipsis: true, - }, - { - title: '变动积分', - dataIndex: 'amount', - key: 'amount', - width: 140, - align: 'right' as const, - render: (value: number) => { - const delta = Number(value ?? 0); - return ( - 0 ? '#10b981' : delta < 0 ? '#ef4444' : '#64748b', fontSize: 15 }}> - {delta > 0 ? '+' : ''}{delta} - - ); - }, - }, - { - title: '创建时间', - key: 'createdAt', - width: 180, - render: (_, record: any) => { - const createdAt = record.createdAt || record.created_at || record.createTime || record.create_time; - return createdAt ? new Date(createdAt).toLocaleString('zh-CN') : '-'; - }, - }, + { title: '类型', dataIndex: 'typeLabel', width: 110, render: (v: string, r: any) => {v || ({ recharge: '发放/充值', consume: '消费', refund: '业务退款', expire: '积分过期', revoke: '积分撤销', team_internal: '历史团队流转' } as any)[r.type] || '其他'} }, + { title: '说明', dataIndex: 'description', ellipsis: true }, + { title: '总变动', dataIndex: 'amount', width: 110, align: 'right' as const, render: (v: number) => {Number(v) > 0 ? '+' : ''}{Number(v || 0)} }, + { title: '个人积分', dataIndex: 'personalAmount', width: 110, align: 'right' as const, render: (v: number) => Number(v || 0) === 0 ? '-' : Number(v || 0) }, + { title: '团队积分', dataIndex: 'teamAmount', width: 110, align: 'right' as const, render: (v: number) => Number(v || 0) === 0 ? '-' : Number(v || 0) }, + { title: '业务场景', dataIndex: 'billingSceneLabel', width: 140, render: (v: string) => v || '-' }, + { title: 'Token', dataIndex: 'totalTokens', width: 100, align: 'right' as const, render: (v: number) => v == null ? '-' : Number(v).toLocaleString() }, + { title: '时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-' }, + ]; + + const cards = [ + ['总可消费积分', Number(data.credits || 0), ], + ['个人可用积分', Number(data.personalCredits || 0), ], + ['团队可用积分', Number(data.teamAvailableCredits || 0), ], + ['团队冻结积分', Number(data.teamFrozenCredits || 0), ], ]; return ( -
- -
-
-
-
-
- - 可用积分 -
-
- {credits.toLocaleString()} -
-
-
- -
-
-
-
- -
- 累计获得 -
-
- {totalGranted.toLocaleString()} -
-
- - -
-
-
- -
- 累计消耗 -
-
- {totalConsumed.toLocaleString()} -
-
- + + + {cards.map(([label, value, icon]: any) =>
{icon} {label}
{Number(value).toLocaleString()}
)} - -
-
- - 积分明细 -
-
- - {records.length === 0 ? ( - 暂无积分变动记录} - /> - ) : ( - <> -
-
- -
- - )} - - - - +
积分流水累计获得 {Number(data.totalGranted || 0).toLocaleString()} · 累计消耗 {Number(data.totalConsumed || 0).toLocaleString()}
+ {(data.records || []).length === 0 ? :
} +
{ setPage(p); setPageSize(ps); }} />
+ ); }; -export default CreditRecordsPage; \ No newline at end of file +export default CreditRecordsPage; diff --git a/video-gen-app/src/pages/CreditsPage.tsx b/video-gen-app/src/pages/CreditsPage.tsx index 75ed3650..78d66d6a 100644 --- a/video-gen-app/src/pages/CreditsPage.tsx +++ b/video-gen-app/src/pages/CreditsPage.tsx @@ -1,175 +1,78 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { - Col, Row, Space, Table, Tag, Typography, -} from 'antd'; -import { - WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, - ThunderboltOutlined, -} from '@ant-design/icons'; -import { getCredits } from '../api'; -import { useAuthStore } from '../store/useAuthStore'; -import { formatDate, formatDatePrecise } from '../utils/formatDate'; -import type { CreditRecord } from '../types'; - -const AnimatedNumber: React.FC<{ value: number; duration?: number }> = ({ value, duration = 1200 }) => { - const [display, setDisplay] = useState(0); - const ref = useRef(0); - useEffect(() => { - const start = display; - const diff = value - start; - if (diff === 0) return; - const startTime = performance.now(); - const animate = (now: number) => { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const eased = 1 - Math.pow(1 - progress, 3); - setDisplay(start + diff * eased); - if (progress < 1) ref.current = requestAnimationFrame(animate); - }; - ref.current = requestAnimationFrame(animate); - return () => cancelAnimationFrame(ref.current); - }, [value]); - return <>{display.toLocaleString()}; -}; +import React, { useEffect, useState } from 'react'; +import { Col, Empty, Pagination, Row, Spin, Table, Tag, Typography } from 'antd'; +import { LockOutlined, TeamOutlined, WalletOutlined } from '@ant-design/icons'; +import { getCreditBalances, getCredits } from '../api'; +import { formatDatePrecise } from '../utils/formatDate'; const CreditsPage: React.FC = () => { - const { user } = useAuthStore(); - const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); + const [summary, setSummary] = useState({ personalCredits: 0, teamAvailableCredits: 0, teamFrozenCredits: 0, credits: 0, nextExpiringCredits: 0, nextLastUsableAt: null as string | null }); + const [balances, setBalances] = useState([]); const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); - const [availableCredits, setAvailableCredits] = useState(0); - const [nextExpiringCredits, setNextExpiringCredits] = useState(0); - const [nextLastUsableAt, setNextLastUsableAt] = useState(null); - const [totalConsumed, setTotalConsumed] = useState(0); + const [pageSize, setPageSize] = useState(20); useEffect(() => { - const fetch = async () => { + const load = async () => { setLoading(true); - const data = await getCredits(page, pageSize); - setRecords(data.records); - setTotal(data.total); - setAvailableCredits(data.availableCredits ?? data.credits ?? 0); - setNextExpiringCredits(data.nextExpiringCredits ?? 0); - setNextLastUsableAt(data.nextLastUsableAt ?? null); - setTotalConsumed(data.totalConsumed ?? 0); - setLoading(false); + try { + const [creditData, balanceData] = await Promise.all([getCredits(1, 1), getCreditBalances(page, pageSize)]); + setSummary({ + personalCredits: Number(creditData.personalCredits || 0), + teamAvailableCredits: Number(creditData.teamAvailableCredits || 0), + teamFrozenCredits: Number(creditData.teamFrozenCredits || 0), + credits: Number(creditData.credits || creditData.availableCredits || 0), + nextExpiringCredits: Number(creditData.nextExpiringCredits || 0), + nextLastUsableAt: creditData.nextLastUsableAt || null, + }); + setBalances(balanceData || []); + } finally { + setLoading(false); + } }; - fetch(); + void load(); }, [page, pageSize]); - const handlePageChange = (p: number, ps: number) => { - setPage(p); - setPageSize(ps); - }; + const cards = [ + { title: '个人可用积分', value: summary.personalCredits, icon: , note: '个人积分可正常用于业务消费' }, + { title: '团队可用积分', value: summary.teamAvailableCredits, icon: , note: '仅统计当前有效席位可消费额度' }, + { title: '团队冻结积分', value: summary.teamFrozenCredits, icon: , note: '团队禁用期间继续发放和过期,但不可消费' }, + { title: '总可消费积分', value: summary.credits, icon: , note: '个人积分 + 当前可用团队积分' }, + ]; const columns = [ - { title: '类型', dataIndex: 'type', key: 'type', width: 100, - render: (type: string) => { - const labels: Record = { - recharge: { label: '发放/充值', color: 'green', positive: true }, - consume: { label: '消费', color: 'orange', positive: false }, - refund: { label: '业务退款', color: 'purple', positive: true }, - expire: { label: '积分过期', color: 'default', positive: false }, - revoke: { label: '积分撤销', color: 'red', positive: false }, - team_internal: { label: '历史团队流转', color: 'blue', positive: false }, - }; - const config = labels[type] || { label: type, color: 'default', positive: false }; - return : }>{config.label}; - }, - }, - { title: '有效积分变动', key: 'balanceDelta', width: 150, - render: (_: unknown, record: CreditRecord) => { - const delta = record.balanceDelta ?? record.amount ?? 0; - const expired = record.expiredAmount ?? 0; - return ( -
- 0 ? '#10b981' : delta < 0 ? '#ef4444' : '#64748b', fontSize: 15 }}> - {delta > 0 ? '+' : ''}{delta} - - {expired > 0 &&
已过期 {expired}
} -
- ); - }, - }, - { title: '说明', dataIndex: 'description', key: 'description' }, - { title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, - render: (d: string) => {formatDate(d)}, - }, + { title: '资金域', dataIndex: 'creditScopeLabel', width: 110, render: (v: string, r: any) => {v || (r.creditScope === 'team' ? '团队积分' : '个人积分')} }, + { title: '积分等级', dataIndex: 'creditLevelLabel', width: 120, render: (v: string) => v || '-' }, + { title: '来源', dataIndex: 'sourceTypeLabel', width: 150, render: (v: string) => v || '-' }, + { title: '发放积分', dataIndex: 'grantAmount', width: 110, align: 'right' as const }, + { title: '剩余积分', dataIndex: 'unspentAmount', width: 110, align: 'right' as const, render: (v: number) => {Number(v || 0).toLocaleString()} }, + { title: '状态', dataIndex: 'statusLabel', width: 100, render: (v: string) => {v || '其他状态'} }, + { title: '最后可用时间', dataIndex: 'lastUsableAt', width: 190, render: (v: string) => v ? formatDatePrecise(v) : '-' }, ]; return ( -
- {/* Balance cards */} - -
-
-
-
- - - 当前可用积分 - -
- -
+ + + {cards.map((card) => ( +
+
+
{card.icon}{card.title}
+
{card.value.toLocaleString()}
+
{card.note}
- - - -
- -
- -
- 最近即将过期 -
-
-
{nextLastUsableAt ? `最后可用:${formatDatePrecise(nextLastUsableAt)}` : '暂无即将过期积分'}
-
- - -
- -
- -
- 累计消耗 -
-
-
- + + ))} - - {/* Records */} -
-
- - 积分明细 -
-
-
`共 ${t} 条`, - }} - scroll={{ x: 500 }} /> + {summary.nextExpiringCredits > 0 && ( +
+ 最近将有 {summary.nextExpiringCredits.toLocaleString()} 积分到期,最后可用时间:{summary.nextLastUsableAt ? formatDatePrecise(summary.nextLastUsableAt) : '-'}。
+ )} + 个人积分批次 + {balances.length === 0 ? :
} +
+ { setPage(p); setPageSize(ps); }} showSizeChanger pageSizeOptions={[20, 50, 100]} />
- + ); }; diff --git a/video-gen-app/src/pages/OrderRecordsPage.tsx b/video-gen-app/src/pages/OrderRecordsPage.tsx index 87cc23bc..237740c3 100644 --- a/video-gen-app/src/pages/OrderRecordsPage.tsx +++ b/video-gen-app/src/pages/OrderRecordsPage.tsx @@ -1,215 +1,49 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Table, Tag, Empty, Spin, Typography, Pagination, Select, DatePicker, Space } from 'antd'; -import { FileTextOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons'; +import { DatePicker, Empty, Pagination, Select, Spin, Table, Tag, Typography } from 'antd'; +import { FileTextOutlined } from '@ant-design/icons'; import { getPaymentOrders } from '../api'; +const PAYMENT_METHOD_LABELS: Record = { alipay: '支付宝', wechat: '微信支付', bank_transfer: '银行转账', cash: '现金', other: '其他-线下收款' }; +const SOURCE_LABELS: Record = { online_payment: '线上支付', admin_offline: '后台线下成交' }; +const STATUS_LABELS: Record = { pending: '待支付', paid: '已支付', fulfilled: '已履约', refunded: '已退款', cancelled: '已取消', expired: '已过期', failed: '失败', processing: '处理中' }; + const OrderRecordsPage: React.FC = () => { const [loading, setLoading] = useState(true); const [orders, setOrders] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); - const [pageSize] = useState(10); - const [statusFilter, setStatusFilter] = useState(''); + const [pageSize, setPageSize] = useState(10); + const [statusFilter, setStatusFilter] = useState(); const [dateRange, setDateRange] = useState([null, null]); - const loadData = useCallback(async () => { + const load = useCallback(async () => { setLoading(true); try { - const data = await getPaymentOrders(page, pageSize, { - statusFilter: statusFilter || undefined, - startDate: dateRange[0]?.format?.('YYYY-MM-DD'), - endDate: dateRange[1]?.format?.('YYYY-MM-DD'), - }); - setOrders(data.items || []); - setTotal(data.total || 0); - } catch { - setOrders([]); - } - setLoading(false); - }, [page, statusFilter, dateRange]); - - useEffect(() => { - loadData(); - }, [loadData]); - - const handlePageChange = (newPage: number) => { - setPage(newPage); - }; - - const handleReset = () => { - setStatusFilter(''); - setDateRange([null, null]); - setPage(1); - }; + const data = await getPaymentOrders(page, pageSize, { statusFilter, startDate: dateRange[0]?.format?.('YYYY-MM-DD'), endDate: dateRange[1]?.format?.('YYYY-MM-DD') }); + setOrders(data.items || []); setTotal(Number(data.total || 0)); + } finally { setLoading(false); } + }, [page, pageSize, statusFilter, dateRange]); + useEffect(() => { void load(); }, [load]); const columns = [ - { - title: '订单号', - key: 'orderNo', - width: 200, - render: (_, record: any) => { - const orderNo = record.order_no || record.orderNo || record.id; - return ( - - {orderNo} - - ); - }, - }, - { - title: '支付方式', - key: 'paymentMethod', - width: 100, - render: (_, record: any) => { - const method = record.payment_method || record.paymentMethod || 'wechat'; - const methodConfig: Record = { - alipay: { icon: , label: '支付宝', color: '#1677ff' }, - wechat: { icon: , label: '微信支付', color: '#07c160' }, - }; - const config = methodConfig[method] || methodConfig.wechat; - return ( -
- {config.icon} - {config.label} -
- ); - }, - }, - { - title: '金额', - key: 'amount', - width: 100, - align: 'right' as const, - render: (_, record: any) => { - const amount = record.amount || record.total_amount || 0; - return ( - - ¥{amount} - - ); - }, - }, - { - title: '获得积分', - key: 'credits', - width: 100, - align: 'center' as const, - render: (_, record: any) => { - const credits = record.credits || record.credit_amount || 0; - return ( - - {credits} - - ); - }, - }, - { - title: '状态', - key: 'status', - width: 100, - render: (_, record: any) => { - const status = record.status || 'pending'; - const statusConfig: Record = { - pending: { color: '#f59e0b', label: '待支付', bg: 'rgba(245,158,11,0.1)' }, - paid: { color: '#10b981', label: '已支付', bg: 'rgba(16,185,129,0.1)' }, - refunded: { color: '#94a3b8', label: '已退款', bg: 'rgba(148,163,184,0.1)' }, - failed: { color: '#ef4444', label: '支付失败', bg: 'rgba(239,68,68,0.1)' }, - cancelled: { color: '#64748b', label: '已取消', bg: 'rgba(100,116,139,0.1)' }, - processing: { color: '#0ea5e9', label: '处理中', bg: 'rgba(14,165,233,0.1)' }, - }; - const config = statusConfig[status] || { color: '#64748b', label: status, bg: 'rgba(100,116,139,0.1)' }; - return ( - - {config.label} - - ); - }, - }, - { - title: '创建时间', - key: 'createdAt', - width: 180, - render: (_, record: any) => { - const createdAt = record.created_at || record.createdAt; - return createdAt ? new Date(createdAt).toLocaleString('zh-CN') : '-'; - }, - }, - { - title: '支付时间', - key: 'paidAt', - width: 180, - render: (_, record: any) => { - const paidAt = record.paid_at || record.paidAt; - return paidAt ? new Date(paidAt).toLocaleString('zh-CN') : '-'; - }, - }, + { title: '订单号', dataIndex: 'orderNo', width: 210, render: (v: string) => {v} }, + { title: '订单来源', dataIndex: 'orderSource', width: 120, render: (v: string, r: any) => {r.orderSourceLabel || SOURCE_LABELS[v] || '其他来源'} }, + { title: '商品', dataIndex: 'productNameSnapshot', width: 180, ellipsis: true, render: (v: string, r: any) => {v || '-'}{Number(r.quantity || 1) > 1 ? ` × ${r.quantity}` : ''} }, + { title: '支付方式', dataIndex: 'paymentMethod', width: 125, render: (v: string, r: any) => r.paymentMethodLabel || (v === 'other' && r.offlinePaymentDetail ? r.offlinePaymentDetail : PAYMENT_METHOD_LABELS[v]) || '其他支付方式' }, + { title: '实际金额', dataIndex: 'amount', width: 110, align: 'right' as const, render: (v: number) => `¥${Number(v || 0).toFixed(2)}` }, + { title: '状态', dataIndex: 'status', width: 100, render: (v: string, r: any) => {r.statusLabel || STATUS_LABELS[v] || '其他状态'} }, + { title: '创建时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-' }, + { title: '支付时间', dataIndex: 'paidAt', width: 180, render: (v: string) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-' }, ]; - return ( -
-
-
- - 订单记录 - 共 {total} 条记录 -
- {/* 搜索栏 */} -
-
-
- -
- - )} - - - + return
+
订单记录线下售后订单同样在此只读展示
+
+
} +
{ setPage(p); setPageSize(ps); }} />
+ ; }; - -export default OrderRecordsPage; \ No newline at end of file +export default OrderRecordsPage; diff --git a/video-gen-app/src/pages/TeamManagementPage.tsx b/video-gen-app/src/pages/TeamManagementPage.tsx index ac81041e..3dfda808 100644 --- a/video-gen-app/src/pages/TeamManagementPage.tsx +++ b/video-gen-app/src/pages/TeamManagementPage.tsx @@ -1,676 +1,239 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { - Button, Empty, Form, Input, InputNumber, message, Modal, Pagination, Radio, Select, Segmented, Space, Table, Tabs, Tag, Tooltip, Typography, + Alert, Button, Card, Col, DatePicker, Empty, Form, Input, InputNumber, message, Modal, + Pagination, Popconfirm, Row, Select, Space, Table, Tabs, Tag, Typography, } from 'antd'; -import { DatePicker } from 'antd'; +import { + DownloadOutlined, LockOutlined, PlusOutlined, ReloadOutlined, + TeamOutlined, UserSwitchOutlined, WalletOutlined, +} from '@ant-design/icons'; import dayjs from 'dayjs'; import { - CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined, BellOutlined, ClockCircleOutlined, CheckOutlined, CloseOutlined, -} from '@ant-design/icons'; + cancelTeamSeat, createTeamInvitation, createTeamSeat, getManagerAccessTeams, getManagedTeam, + getPendingJoinRequests, getTeamCreditExportUrl, getTeamCreditRecords, getTeamInvitations, + getTeamManagerHistory, getTeamMembers, getTeamMemberUsage, getTeamSubscriptions, + handleJoinRequest, revokeInvitation, transferTeamManager, updateTeamSeat, +} from '../api'; +import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember, TeamMemberUsage, TeamSeat, TeamSubscriptionManage } from '../types'; const { RangePicker } = DatePicker; -import { - createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests, - getTeamCreditExportUrl, getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, -} from '../api'; -import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types'; -import { useAuthStore } from '../store/useAuthStore'; +const fmt = (value?: string | null) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'; -/* ── 工具函数 ────────────────────────────────────────── */ -function formatDateTime(value: any): string { - if (!value) return '-'; - try { - return new Date(value).toLocaleString('zh-CN', { hour12: false }); - } catch { - return '-'; - } -} - -const RECORD_TYPE_CONFIG: Record = { - recharge: { color: 'green', label: '充值' }, - consume: { color: 'red', label: '消费' }, - refund: { color: 'orange', label: '退款' }, - team_internal: { color: 'blue', label: '团队内部' }, -}; - -/* ── 主组件 ──────────────────────────────────────────── */ const TeamManagementPage: React.FC = () => { const [team, setTeam] = useState(null); - const [teamLoading, setTeamLoading] = useState(false); - - const loadTeam = useCallback(async () => { - setTeamLoading(true); - try { - const data = await getManagedTeam(); - setTeam(data); - } catch (e: any) { - message.error(e?.message || '获取团队信息失败'); - } finally { - setTeamLoading(false); - } - }, []); - - useEffect(() => { loadTeam(); }, [loadTeam]); - - // ── Tab 1: 成员 ── + const [accessTeams, setAccessTeams] = useState([]); + const [selectedFlowTeamId, setSelectedFlowTeamId] = useState(); const [members, setMembers] = useState([]); - const [membersTotal, setMembersTotal] = useState(0); - const [membersLoading, setMembersLoading] = useState(false); - const [membersPage, setMembersPage] = useState(1); - - const loadMembers = useCallback(async () => { - if (!team) return; - setMembersLoading(true); - try { - const res = await getTeamMembers(membersPage); - setMembers(res.items || []); - setMembersTotal(res.total || 0); - } catch (e: any) { - message.error(e?.message || '加载成员失败'); - } finally { - setMembersLoading(false); - } - }, [team, membersPage]); - - useEffect(() => { loadMembers(); }, [loadMembers]); - - // ── Tab 2: 邀请码 ── + const [memberTotal, setMemberTotal] = useState(0); + const [memberPage, setMemberPage] = useState(1); + const [subscriptions, setSubscriptions] = useState([]); + const [usage, setUsage] = useState([]); + const [history, setHistory] = useState([]); const [invitations, setInvitations] = useState([]); - const [invLoading, setInvLoading] = useState(false); - const [invModal, setInvModal] = useState(false); - const [invForm] = Form.useForm(); - const [invSaving, setInvSaving] = useState(false); - - const loadInvitations = useCallback(async () => { - setInvLoading(true); - try { - const data = await getTeamInvitations(); - setInvitations(data || []); - } catch (e: any) { - message.error(e?.message || '加载邀请码失败'); - } finally { - setInvLoading(false); - } - }, []); - - useEffect(() => { loadInvitations(); }, [loadInvitations]); - - const handleCreateInvitation = async () => { - try { - const values = await invForm.validateFields(); - setInvSaving(true); - await createTeamInvitation(values.maxUses || null, null); - message.success('邀请码已生成'); - setInvModal(false); - invForm.resetFields(); - loadInvitations(); - } catch (e: any) { - if (e?.errorFields) return; - message.error(e?.message || '创建失败'); - } finally { - setInvSaving(false); - } - }; - - const handleRevoke = async (invId: string) => { - try { - await revokeInvitation(invId); - message.success('已撤销'); - loadInvitations(); - } catch (e: any) { - message.error(e?.message || '撤销失败'); - } - }; - - const copyInviteLink = (link: string) => { - if (navigator.clipboard && window.isSecureContext) { - navigator.clipboard.writeText(link).then(() => { - message.success('邀请链接已复制'); - }).catch(() => { - fallbackCopy(link); - }); - } else { - fallbackCopy(link); - } - }; - - const fallbackCopy = (text: string) => { - const textArea = document.createElement('textarea'); - textArea.value = text; - textArea.style.position = 'fixed'; - textArea.style.left = '-9999px'; - textArea.style.top = '-9999px'; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - try { - document.execCommand('copy'); - message.success('邀请链接已复制'); - } catch { - message.warning('复制失败,请手动复制'); - } - document.body.removeChild(textArea); - }; - - // ── Tab 3: 加入申请 ── const [requests, setRequests] = useState([]); - const [reqLoading, setReqLoading] = useState(false); - const [reqStatusFilter, setReqStatusFilter] = useState('pending'); - const [activeTab, setActiveTab] = useState('members'); - const initialNoticeShownRef = useRef(false); - const lastRequestCountRef = useRef(0); + const [flow, setFlow] = useState({ items: [], total: 0 }); + const [flowPage, setFlowPage] = useState(1); + const [flowType, setFlowType] = useState(); + const [flowPhone, setFlowPhone] = useState(''); + const [flowSubscriptionId, setFlowSubscriptionId] = useState(); + const [flowDates, setFlowDates] = useState([null, null]); + const [loading, setLoading] = useState(false); + const [seatModal, setSeatModal] = useState<{ open: boolean; subscriptionId?: string; seat?: TeamSeat }>({ open: false }); + const [seatForm] = Form.useForm(); + const [inviteModal, setInviteModal] = useState(false); + const [inviteForm] = Form.useForm(); - const loadRequests = useCallback(async (status: string, isInitial = false) => { - setReqLoading(true); + const readOnly = !!team?.isReadOnly; + const currentManager = !!team; + + const loadBase = useCallback(async () => { + setLoading(true); try { - const data = await getPendingJoinRequests(status); - const currentCount = data?.length || 0; - setRequests(data || []); - - // 只有待处理状态才显示弹窗通知 - if (status === 'pending' && currentCount > 0) { - if (isInitial && !initialNoticeShownRef.current) { - initialNoticeShownRef.current = true; - Modal.confirm({ - title: ( - - - 待处理的加入申请 - - ), - content: ( -
-

您有 {currentCount} 条待处理的团队加入申请。

-

请及时处理新成员的加入申请。

-
- ), - okText: '立即处理', - cancelText: '稍后处理', - onOk: () => { - setActiveTab('requests'); - }, - }); - } else if (!isInitial && currentCount > lastRequestCountRef.current) { - message.info({ - content: `有 ${currentCount - lastRequestCountRef.current} 条新的加入申请待处理`, - duration: 5, - }); - } + const access = await getManagerAccessTeams().catch(() => []); + setAccessTeams(access || []); + const current = await getManagedTeam().catch(() => null); + setTeam(current); + const defaultTeam = current?.id || access?.[0]?.id; + setSelectedFlowTeamId((old) => old || defaultTeam); + if (current) { + const [memberData, subData, usageData, historyData, invData, requestData] = await Promise.all([ + getTeamMembers(memberPage, 20), getTeamSubscriptions(), getTeamMemberUsage(), getTeamManagerHistory(), getTeamInvitations(), getPendingJoinRequests(), + ]); + setMembers(memberData?.items || []); setMemberTotal(Number(memberData?.total || 0)); + setSubscriptions(subData || []); setUsage(usageData || []); setHistory(historyData || []); + setInvitations(invData || []); setRequests(requestData || []); + } else { + setMembers([]); setSubscriptions([]); setUsage([]); setHistory([]); setInvitations([]); setRequests([]); } + } finally { setLoading(false); } + }, [memberPage]); - if (status === 'pending') { - lastRequestCountRef.current = currentCount; - } - } catch (e: any) { - message.error(e?.message || '加载申请失败'); - } finally { - setReqLoading(false); - } - }, []); - - useEffect(() => { - loadRequests('pending', true); - }, [loadRequests]); - - useEffect(() => { - if (!team) return; - const interval = setInterval(() => { - loadRequests('pending', false); - }, 60000); - return () => clearInterval(interval); - }, [team, loadRequests]); - - const handleRequest = async (requestId: string, action: 'approve' | 'reject', note?: string) => { + const loadFlow = useCallback(async () => { + if (!selectedFlowTeamId) { setFlow({ items: [], total: 0 }); return; } try { - await handleJoinRequest(requestId, action, note); - message.success(action === 'approve' ? '已通过' : '已拒绝'); - loadRequests(reqStatusFilter); - loadMembers(); - } catch (e: any) { - message.error(e?.message || '操作失败'); - } - }; - - // ── Tab 4: 团队积分变动 ── - const [creditRecords, setCreditRecords] = useState([]); - const [creditTotal, setCreditTotal] = useState(0); - const [creditSummary, setCreditSummary] = useState(null); - const [creditLoading, setCreditLoading] = useState(false); - const [creditPage, setCreditPage] = useState(1); - const [creditFilterType, setCreditFilterType] = useState(''); - const [creditFilterPhone, setCreditFilterPhone] = useState(''); - const [creditDateRange, setCreditDateRange] = useState<[string, string] | null>(() => { - const today = dayjs().format('YYYY-MM-DD'); - return [today, today]; - }); - - const loadCreditRecords = useCallback(async () => { - setCreditLoading(true); - try { - const res = await getTeamCreditRecords({ - page: creditPage, - pageSize: 10, - phone: creditFilterPhone || undefined, - recordType: creditFilterType || undefined, - startDate: creditDateRange?.[0] || undefined, - endDate: creditDateRange?.[1] || undefined, + const data = await getTeamCreditRecords({ + teamId: selectedFlowTeamId, page: flowPage, pageSize: 20, phone: flowPhone || undefined, + subscriptionId: flowSubscriptionId, recordType: flowType, + startDate: flowDates[0]?.format?.('YYYY-MM-DD'), endDate: flowDates[1]?.format?.('YYYY-MM-DD'), }); - setCreditRecords(res.items || []); - setCreditTotal(res.total || 0); - setCreditSummary(res.summary || null); - } catch (e: any) { - message.error(e?.message || '加载积分记录失败'); - } finally { - setCreditLoading(false); - } - }, [creditPage, creditFilterType, creditFilterPhone, creditDateRange]); + setFlow(data || { items: [], total: 0 }); + } catch (e: any) { message.error(e?.message || '团队流水加载失败'); } + }, [selectedFlowTeamId, flowPage, flowPhone, flowSubscriptionId, flowType, flowDates]); - useEffect(() => { loadCreditRecords(); }, [loadCreditRecords]); + useEffect(() => { void loadBase(); }, [loadBase]); + useEffect(() => { void loadFlow(); }, [loadFlow]); - const resetCreditFilters = () => { - setCreditFilterType(''); - setCreditFilterPhone(''); - const today = dayjs().format('YYYY-MM-DD'); - setCreditDateRange([today, today]); - setCreditPage(1); + const openSeatCreate = (subscriptionId: string) => { + seatForm.resetFields(); + const target = subscriptions.find((item) => item.subscription?.id === subscriptionId); + const activeSeats = Math.max(1, Number(target?.seatLimit || 1) - Number(target?.activeSeatCount || 0)); + const defaultAmount = Number(target?.periodUnallocatedCredits || 0) > 0 ? Math.floor((Number(target?.periodUnallocatedCredits || 0) / activeSeats) * 100) / 100 : undefined; + seatForm.setFieldsValue({ monthlyAllocatedCredits: defaultAmount }); + setSeatModal({ open: true, subscriptionId }); }; - const handleExportCredits = () => { - const url = getTeamCreditExportUrl({ - phone: creditFilterPhone || undefined, - recordType: creditFilterType || undefined, - startDate: creditDateRange?.[0] || undefined, - endDate: creditDateRange?.[1] || undefined, - }); - const token = localStorage.getItem('auth_token'); - const headers: Record = token ? { Authorization: `Bearer ${token}` } : {}; - fetch(url, { headers }) - .then((res) => res.blob()) - .then((blob) => { - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = `团队积分_${dayjs().format('YYYYMMDD_HHmmss')}.csv`; - a.click(); - URL.revokeObjectURL(a.href); - }) - .catch(() => message.error('导出失败')); + const openSeatEdit = (seat: TeamSeat) => { + seatForm.setFieldsValue({ userId: seat.userId, monthlyAllocatedCredits: seat.monthlyAllocatedCredits }); + setSeatModal({ open: true, subscriptionId: seat.subscriptionId, seat }); + }; + + const saveSeat = async () => { + try { + const values = await seatForm.validateFields(); + if (seatModal.seat) await updateTeamSeat(seatModal.seat.id, Number(values.monthlyAllocatedCredits)); + else await createTeamSeat(String(seatModal.subscriptionId), String(values.userId), Number(values.monthlyAllocatedCredits)); + message.success(seatModal.seat ? '席位额度已更新' : '席位已创建'); + setSeatModal({ open: false }); await loadBase(); + } catch (e: any) { if (!e?.errorFields) message.error(e?.message || '席位保存失败'); } + }; + + const removeSeat = async (seatId: string) => { + try { await cancelTeamSeat(seatId); message.success('席位已取消,未使用额度已释放'); await loadBase(); } + catch (e: any) { message.error(e?.message || '席位取消失败'); } }; - /* ── 表格列定义 ──────────────────────────────────────── */ const memberColumns = [ - { title: '用户名', dataIndex: 'username', width: 140, render: (v: string) => {v} }, - { title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => v || '-' }, - { title: '积分', dataIndex: 'credits', width: 100, render: (v: number) => {(v ?? 0).toFixed(2)} }, - { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => {v ? '启用' : '禁用'} }, - { title: '加入时间', dataIndex: 'joinedAt', width: 170, render: (v: string) => formatDateTime(v) }, - { - title: '操作', key: 'action', width: 100, - render: () => ( - - - - ), - }, + { title: '成员', dataIndex: 'username', render: (v: string, r: TeamMember) => {v}{r.id === team?.managerId && 队长} }, + { title: '手机号', dataIndex: 'phone', render: (v: string) => v || '-' }, + { title: '个人积分', dataIndex: 'personalCredits', align: 'right' as const, render: (v: number) => Number(v || 0).toLocaleString() }, + { title: '团队可用', dataIndex: 'teamAvailableCredits', align: 'right' as const, render: (v: number) => Number(v || 0).toLocaleString() }, + { title: '团队冻结', dataIndex: 'teamFrozenCredits', align: 'right' as const, render: (v: number) => Number(v || 0).toLocaleString() }, + { title: '操作', width: 110, render: (_: any, r: TeamMember) => r.id !== team?.managerId ? { try { await transferTeamManager(r.id); message.success('团队队长已更换'); await loadBase(); } catch (e: any) { message.error(e?.message || '更换队长失败'); } }}> : '-' }, ]; - const buildInviteLink = (code: string) => { - const base = window.location.origin; - return `${base}/join-team?code=${code}`; - }; - - const invColumns = [ - { title: '邀请码', dataIndex: 'code', width: 200, render: (v: string) => {v} }, - { - title: '邀请链接', dataIndex: 'code', - render: (v: string) => { - const link = buildInviteLink(v); - return ( - - - {link} - - - - ) : null, - }, + const seatColumns = (subscription: TeamSubscriptionManage) => [ + { title: '成员', dataIndex: 'username', render: (v: string) => v || '-' }, + { title: '月分配额度', dataIndex: 'monthlyAllocatedCredits', align: 'right' as const }, + { title: '本期净消耗', dataIndex: 'currentPeriodUsedCredits', align: 'right' as const }, + { title: '本期剩余额度', dataIndex: 'currentPeriodRemainingCredits', align: 'right' as const }, + { title: '状态', dataIndex: 'statusLabel', render: (v: string) => {v || '其他状态'} }, + { title: '操作', width: 140, render: (_: any, seat: TeamSeat) => seat.status === 'active' ? removeSeat(seat.id)}> : '-' }, ]; - const renderStatusTag = (status: string) => { - const map: Record = { - pending: { color: 'orange', text: '待处理' }, - approved: { color: 'green', text: '已通过' }, - rejected: { color: 'red', text: '已拒绝' }, - }; - const info = map[status] || { color: 'default', text: status }; - return {info.text}; - }; + const subscriptionView = subscriptions.length === 0 ? : + {subscriptions.map((item) => { + const sub = item.subscription || {}; + return {sub.productNameSnapshot || '团队订阅套餐'}{sub.billingCycleLabel || '订阅周期'}{sub.statusLabel || '其他状态'}} extra={}> + +
席位:{item.activeSeatCount}/{item.seatLimit}本期总积分:{Number(item.periodTotalCredits || 0).toLocaleString()} + 本期剩余资金:{Number(item.periodUnspentCredits || 0).toLocaleString()}未分配积分:{Number(item.periodUnallocatedCredits || 0).toLocaleString()} + 订阅实例:{sub.subscriptionNo || '-'}订阅到期:{fmt(sub.expiresAt)} + 本期:{fmt(item.currentPeriodStartAt)} ~ {fmt(item.currentPeriodExpiresAt)} + +
+ ; + })} + ; - const reqColumns = [ - { title: '申请人', dataIndex: 'username', width: 140, render: (v: string) => {v} }, - { title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => v || '-' }, - { title: '状态', dataIndex: 'status', width: 100, render: (v: string) => renderStatusTag(v) }, - { title: '申请时间', dataIndex: 'createdAt', width: 170, render: (v: string) => formatDateTime(v) }, - { title: '处理时间', dataIndex: 'handledAt', width: 170, render: (v: string) => v ? formatDateTime(v) : '-' }, - { title: '备注', dataIndex: 'note', width: 180, ellipsis: true, render: (v: string) => v || '-' }, - { - title: '操作', key: 'action', width: 160, - render: (_: any, r: TeamJoinRequest) => { - if (r.status !== 'pending') return 已处理; - return ( - - - - - ); - }, - }, + const invitationColumns = [ + { title: '邀请码', dataIndex: 'code', render: (v: string) => {v} }, + { title: '状态', dataIndex: 'status', render: (v: string) => {v === 'active' ? '有效' : '已撤销'} }, + { title: '使用次数', render: (_: any, r: TeamInvitation) => `${r.useCount || 0}${r.maxUses ? ` / ${r.maxUses}` : ''}` }, + { title: '过期时间', dataIndex: 'expiresAt', render: (v: string) => v ? fmt(v) : '长期有效' }, + { title: '操作', render: (_: any, r: TeamInvitation) => r.status === 'active' ? : '-' }, ]; - const creditColumns = [ - { title: '用户名', dataIndex: 'username', width: 120, render: (v: string) => {v || '-'} }, - { title: '手机号', dataIndex: 'phone', width: 120, render: (v: string) => v || '-' }, - { - title: '类型', dataIndex: 'type', width: 100, - render: (_: any, r: any) => { - const cfg = RECORD_TYPE_CONFIG[r.type] || { color: 'default', label: r.type || '-' }; - return {cfg.label}; - }, - }, - { - title: '变动积分', dataIndex: 'amount', width: 110, align: 'right' as const, - render: (_: any, r: any) => ( - = 0 ? '#10b981' : '#ef4444', fontSize: 14 }}> - {r.amount >= 0 ? '+' : ''}{(r.amount ?? 0).toFixed(2)} - - ), - }, - { title: '余额', dataIndex: 'balanceAfter', width: 100, align: 'right' as const, render: (v: number) => (v ?? 0).toFixed(2) }, - { title: '说明', dataIndex: 'description', ellipsis: true, minWidth: 160, render: (v: string) => v || '-' }, - { title: '时间', dataIndex: 'createdAt', width: 170, render: (v: string) => formatDateTime(v) }, + const requestColumns = [ + { title: '申请人', dataIndex: 'username' }, { title: '手机号', dataIndex: 'phone', render: (v: string) => v || '-' }, + { title: '状态', dataIndex: 'status', render: (v: string) => {({ pending: '待审批', approved: '已通过', rejected: '已拒绝' } as any)[v] || '其他状态'} }, + { title: '申请时间', dataIndex: 'createdAt', render: (v: string) => fmt(v) }, + { title: '操作', render: (_: any, r: TeamJoinRequest) => r.status === 'pending' ? : '-' }, ]; - /* ── Tab 配置 ────────────────────────────────────────── */ - const tableWrapper: React.CSSProperties = { borderRadius: 16, background: '#fff', border: '1px solid #f0f0f5', overflow: 'hidden' }; - const paginationStyle: React.CSSProperties = { padding: '16px', textAlign: 'right' }; - - const tabItems = [ - { - key: 'members', - label: 成员列表{membersTotal > 0 && ({membersTotal})}, - children: ( -
-
}} - /> - {membersTotal > 0 && ( -
- setMembersPage(p)} size="small" /> -
- )} - - ), - }, - { - key: 'credits', - label: 团队积分变动, - children: ( -
- {/* 搜索栏 */} -
- { setCreditFilterPhone(e.target.value); setCreditPage(1); }} - style={{ width: 160 }} - allowClear - /> - { - if (dates && dates[0] && dates[1]) { - setCreditDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]); - } else { - setCreditDateRange(null); - } - setCreditPage(1); - }} - /> - - -
- - {/* 汇总统计:净消耗 = 消费 - 退款,同时展示消费 / 退款 / 充值辅助详情 */} -
- - 总净消耗积分: - - {creditSummary?.netConsume ?? 0} - - - | - - 消费合计: - - {creditSummary?.totalConsume ?? 0} - - - - 退款合计: - - − {creditSummary?.totalRefund ?? 0} - - - - 充值合计: - - + {creditSummary?.totalRecharge ?? 0} - - -
- -
-
}} - /> - {creditTotal > 0 && ( -
- setCreditPage(p)} size="small" showTotal={(t) => `共 ${t} 条`} /> -
- )} - - - ), - }, - { - key: 'invitations', - label: 邀请管理, - children: ( -
-
- -
-
}} - /> - - ), - }, - { - key: 'requests', - label: 加入申请{requests.length > 0 && reqStatusFilter === 'pending' && {requests.length}}, - children: ( - <> -
- { - setReqStatusFilter(v as string); - loadRequests(v as string); - }} - options={[ - { label: '待处理', value: 'pending' }, - { label: '已通过', value: 'approved' }, - { label: '已拒绝', value: 'rejected' }, - { label: '全部', value: '' }, - ]} - style={{ - borderRadius: 12, - border: '1px solid #e2e8f0', - padding: 3, - background: '#f8fafc', - boxShadow: '0 2px 8px rgba(0,0,0,0.06)', - '--ant-segmented-item-selected-bg': '#6366f1', - '--ant-segmented-item-selected-color': '#ffffff', - } as React.CSSProperties} - size="middle" - /> -
-
-
}} - /> - - - ), - }, + const flowColumns = [ + { title: '成员', dataIndex: 'username', width: 120 }, { title: '类型', dataIndex: 'recordTypeLabel', width: 110, render: (v: string) => {v || '其他'} }, + { title: '团队积分变动', dataIndex: 'teamAmount', width: 130, align: 'right' as const, render: (v: number) => {Number(v) > 0 ? '+' : ''}{Number(v || 0)} }, + { title: '说明', dataIndex: 'description', ellipsis: true }, { title: '订阅实例', dataIndex: 'subscriptionNo', width: 190, render: (v: string) => {v || '历史订阅'} }, + { title: '周期ID', dataIndex: 'subscriptionPeriodId', width: 180, ellipsis: true }, { title: '席位ID', dataIndex: 'seatId', width: 180, ellipsis: true }, + { title: '时间', dataIndex: 'createdAt', width: 180, render: (v: string) => fmt(v) }, ]; - /* ── 渲染 ────────────────────────────────────────────── */ - return ( -
- {/* 顶部团队信息 */} -
- {teamLoading ? ( - 加载中... - ) : team ? ( -
-
- {team.name} - 代码: {team.code || '-'} 成员: {team.memberCount} 人 -
- -
- ) : ( - 无法获取团队信息 - )} -
+ const currentOnlyTabs = currentManager ? [ + { key: 'subscriptions', label: '订阅与席位', children: subscriptionView }, + { key: 'members', label: '团队成员', children: <>
}, + { key: 'usage', label: '成员月消耗', children: usage.length ?
`${r.userId}-${r.subscriptionPeriodId}`} + pagination={false} + dataSource={usage} + scroll={{ x: 1400 }} + columns={[ + { title: '成员', dataIndex: 'username', width: 120 }, + { title: '订阅实例', dataIndex: 'subscriptionNo', width: 190, render: (v: string) => {v || '历史订阅'} }, + { title: '套餐名称', dataIndex: 'subscriptionName', width: 180, render: (v: string) => {v || '历史团队订阅'} }, + { + title: '等级编码', + width: 190, + render: (_: any, r: TeamMemberUsage) => + {r.tierLabel || '未知等级'}{r.tierCode ? `(${r.tierCode})` : ''}{r.tierRank ? ` / ${r.tierRank}` : ''} + , + }, + { title: '套餐周期', dataIndex: 'billingCycleLabel', width: 110, render: (v: string) => {v || '未知周期'} }, + { title: '月度周期', dataIndex: 'periodLabel', width: 110, render: (v: string, r: TeamMemberUsage) => v || (r.periodSequence > 0 ? `第${r.periodSequence}个月` : '历史周期') }, + { + title: '周期时间', + width: 320, + render: (_: any, r: TeamMemberUsage) => `${fmt(r.periodStartAt)} ~ ${fmt(r.periodExpiresAt)}`, + }, + { title: '本期净消耗', dataIndex: 'consumedCredits', width: 130, align: 'right' as const, render: (v: number) => Number(v || 0).toLocaleString() }, + ]} + /> : }, + { key: 'invite', label: '邀请与审批', children:
加入申请
}, + { key: 'history', label: '队长任期', children: history.length ?
fmt(v) }, { title: '任期结束', dataIndex: 'endedAt', render: (v) => v ? fmt(v) : 当前任期 }]} /> : }, + ] : []; - {/* 标签页 */} - + const flowTab = { key: 'flow', label: '团队积分流水', children: <> + + setFlowPhone(e.target.value)} /> + { setFlowSubscriptionId(v); setFlowPage(1); }} options={subscriptions.map((s) => ({ value: s.subscription.id, label: `${s.subscription.subscriptionNo || '未编号'} · ${s.subscription.productNameSnapshot || '团队订阅套餐'}` }))} />} + { setFlowDates(v || [null, null]); setFlowPage(1); }} /> + + + +
+ + }; + if (!team && accessTeams.length === 0 && !loading) return ; - {/* 生成邀请码弹窗 */} - 生成邀请码} - open={invModal} - confirmLoading={invSaving} - onOk={handleCreateInvitation} - onCancel={() => { setInvModal(false); invForm.resetFields(); }} - okText="生成" - width={480} - > -
- - - -
- - 邀请码自生成起 24 小时 内有效,过期自动失效。 -
- -
- - ); + return
+ {team &&
{team.name}{team.statusLabel || (team.status === 'active' ? '启用' : '禁用')}
成员 {team.memberCount} 人 · 当前队长 {team.managerName || '-'}
{readOnly && } color="red">团队已禁用,仅允许只读查看}} + {readOnly && } + {!currentManager && } + + + setSeatModal({ open: false })} onOk={saveSeat} okText="确认保存" cancelText="取消"> +
+ {!seatModal.seat &&