This commit is contained in:
sjy
2026-08-13 17:56:58 +08:00
parent 780769ddee
commit da3f2cac49
4 changed files with 1109 additions and 161 deletions
File diff suppressed because one or more lines are too long
@@ -13,6 +13,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { gethistory, gethistoryItems } from '../api'; import { gethistory, gethistoryItems } from '../api';
import UploadResourceHistoryPanel from './uploadResource/UploadResourceHistoryPanel';
import { buildImagePreviewUrl } from '../utils/previewUrl'; import { buildImagePreviewUrl } from '../utils/previewUrl';
type FilterType = 'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'upload_resource'; type FilterType = 'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'upload_resource';
@@ -21,6 +22,7 @@ interface CreationRecordPickerProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
mediaType?: 'video' | 'image'; mediaType?: 'video' | 'image';
onSelect?: (item: any) => void;
} }
const TAB_ITEMS = [ const TAB_ITEMS = [
@@ -31,7 +33,7 @@ const TAB_ITEMS = [
{ key: 'upload_resource', label: <span><UploadOutlined /> </span> }, { key: 'upload_resource', label: <span><UploadOutlined /> </span> },
]; ];
const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClose, mediaType = 'video' }) => { const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClose, mediaType = 'video', onSelect }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [filterType, setFilterType] = useState<FilterType>('project'); const [filterType, setFilterType] = useState<FilterType>('project');
const [filterMedia, setFilterMedia] = useState<'video' | 'image'>(mediaType); const [filterMedia, setFilterMedia] = useState<'video' | 'image'>(mediaType);
@@ -166,6 +168,8 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setRecordList([]);
setMediaLoadStatus(new Map());
loadRecordList(); loadRecordList();
} }
}, [open, filterType, filterMedia, page, selectedDate]); }, [open, filterType, filterMedia, page, selectedDate]);
@@ -176,12 +180,19 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
setPage(1); setPage(1);
setSearchKeyword(''); setSearchKeyword('');
setSelectedDate(''); setSelectedDate('');
setRecordList([]);
setMediaLoadStatus(new Map());
} }
}, [open, mediaType]); }, [open, mediaType]);
const handlePreview = (item: any) => { const handleItemClick = (item: any) => {
if (onSelect) {
onSelect(item);
onClose();
} else {
setPreviewItem(item); setPreviewItem(item);
setPreviewVisible(true); setPreviewVisible(true);
}
}; };
const handleClosePreview = () => { const handleClosePreview = () => {
@@ -279,7 +290,7 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
boxShadow: '0 2px 8px rgba(0,0,0,0.1)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
transition: 'transform 0.2s, box-shadow 0.2s', transition: 'transform 0.2s, box-shadow 0.2s',
}} }}
onClick={() => handlePreview(item)} onClick={() => handleItemClick(item)}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.transform = 'scale(1.05)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)'; e.currentTarget.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
@@ -294,6 +305,16 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
src={displayUrl} src={displayUrl}
alt="预览" alt="预览"
loading="lazy" loading="lazy"
ref={(imgEl) => {
if (imgEl && imgEl.complete && mediaLoadStatus.get(resourceId) !== 'loaded') {
setMediaLoadStatus(prev => {
if (prev.get(resourceId) === 'loaded') return prev;
const newMap = new Map(prev);
newMap.set(resourceId, 'loaded');
return newMap;
});
}
}}
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
@@ -303,6 +324,15 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
}} }}
onLoad={() => { onLoad={() => {
setMediaLoadStatus(prev => { setMediaLoadStatus(prev => {
if (prev.get(resourceId) === 'loaded') return prev;
const newMap = new Map(prev);
newMap.set(resourceId, 'loaded');
return newMap;
});
}}
onError={() => {
setMediaLoadStatus(prev => {
if (prev.get(resourceId) === 'loaded') return prev;
const newMap = new Map(prev); const newMap = new Map(prev);
newMap.set(resourceId, 'loaded'); newMap.set(resourceId, 'loaded');
return newMap; return newMap;
@@ -344,6 +374,7 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
onCancel={onClose} onCancel={onClose}
width={900} width={900}
footer={null} footer={null}
destroyOnHidden
styles={{ body: { maxHeight: '70vh', overflow: 'auto' } }} styles={{ body: { maxHeight: '70vh', overflow: 'auto' } }}
> >
{/* 视频/图片切换 */} {/* 视频/图片切换 */}
@@ -380,10 +411,7 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
/> />
{filterType === 'upload_resource' ? ( {filterType === 'upload_resource' ? (
<div style={{ textAlign: 'center', padding: '40px 0', color: '#94a3b8' }}> <UploadResourceHistoryPanel />
<UploadOutlined style={{ fontSize: 32, marginBottom: 8 }} />
<div></div>
</div>
) : ( ) : (
<> <>
{/* 搜索栏 */} {/* 搜索栏 */}
+298 -96
View File
@@ -48,6 +48,7 @@ import {
CrownFilled, CrownFilled,
FireOutlined, FireOutlined,
BankFilled, BankFilled,
BankOutlined,
CheckCircleFilled, CheckCircleFilled,
WechatOutlined, WechatOutlined,
AlipayCircleOutlined, AlipayCircleOutlined,
@@ -303,14 +304,30 @@ const GRADIENTS = [
{ gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> }, { gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
]; ];
const MEMBERSHIP_GRADIENTS = [ const getGradientByCredits = (credits: number) => {
{ gradient: 'linear-gradient(135deg, #f6ad55, #dd6b20)', shadow: 'rgba(246,173,85,0.25)', icon: <CrownOutlined /> }, if (credits >= 10000) return GRADIENTS[3];
{ gradient: 'linear-gradient(135deg, #667eea, #5a67d8)', shadow: 'rgba(102,126,234,0.25)', icon: <CrownOutlined /> }, if (credits >= 5000) return GRADIENTS[2];
{ gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: <StarFilled /> }, if (credits >= 1000) return GRADIENTS[1];
{ gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> }, return GRADIENTS[0];
{ gradient: 'linear-gradient(135deg, #718096, #4a5568)', shadow: 'rgba(113,128,150,0.25)', icon: <CrownOutlined /> }, };
{ gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
]; const MEMBERSHIP_GRADIENTS: Record<string, { gradient: string; shadow: string; icon: React.ReactNode }> = {
basic: { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
starter: { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
entry: { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
: { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
: { gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
standard: { gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: <StarFilled /> },
: { gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: <StarFilled /> },
advanced: { gradient: 'linear-gradient(135deg, #667eea, #5a67d8)', shadow: 'rgba(102,126,234,0.25)', icon: <CrownOutlined /> },
: { gradient: 'linear-gradient(135deg, #667eea, #5a67d8)', shadow: 'rgba(102,126,234,0.25)', icon: <CrownOutlined /> },
premium: { gradient: 'linear-gradient(135deg, #f6ad55, #dd6b20)', shadow: 'rgba(246,173,85,0.25)', icon: <CrownOutlined /> },
pro: { gradient: 'linear-gradient(135deg, #f6ad55, #dd6b20)', shadow: 'rgba(246,173,85,0.25)', icon: <CrownOutlined /> },
: { gradient: 'linear-gradient(135deg, #f6ad55, #dd6b20)', shadow: 'rgba(246,173,85,0.25)', icon: <CrownOutlined /> },
enterprise: { gradient: 'linear-gradient(135deg, #718096, #4a5568)', shadow: 'rgba(113,128,150,0.25)', icon: <CrownOutlined /> },
vip: { gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
default: { gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: <StarFilled /> },
};
const FAQ_ITEMS = [ const FAQ_ITEMS = [
{ {
@@ -464,6 +481,18 @@ const AppLayout: React.FC = () => {
isSubscription?: boolean; isSubscription?: boolean;
} | null>(null); } | null>(null);
const [pendingProduct, setPendingProduct] = useState<{ id: string; product: any; } | null>(null);
const [qrRevealed, setQrRevealed] = useState(false);
const [paymentTab, setPaymentTab] = useState<'alipay' | 'corporate'>('alipay');
const [corporateTransferForm] = Form.useForm();
const [corporateSubmitting, setCorporateSubmitting] = useState(false);
const COMPANY_BANK_INFO = {
accountName: '深圳市某某科技有限公司',
bankName: '招商银行',
bankAccount: '7559 0188 1010 1001',
};
const PENDING_ORDER_KEY = 'pending_payment_order'; const PENDING_ORDER_KEY = 'pending_payment_order';
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@@ -849,8 +878,19 @@ const AppLayout: React.FC = () => {
message.error('暂无可用支付方式'); message.error('暂无可用支付方式');
return; return;
} }
setPendingProduct({ id: productId, product });
setQrRevealed(false);
setPaymentTab('alipay');
setCurrentPaymentInfo(null);
setCreditsModalOpen(false);
setQrCodeModalOpen(true);
}, [enabledMethods]);
const confirmPayment = useCallback(async () => {
if (!pendingProduct) return;
setPaying(true); setPaying(true);
try { try {
const { id: productId, product } = pendingProduct;
const order = await createRechargeOrder(productId, paymentMethod); const order = await createRechargeOrder(productId, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url; const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
const finalPrice = Number(order.amount ?? product.currentPrice ?? product.price ?? 0); const finalPrice = Number(order.amount ?? product.currentPrice ?? product.price ?? 0);
@@ -875,8 +915,7 @@ const AppLayout: React.FC = () => {
}; };
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) { if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
setCurrentPaymentInfo(paymentInfo); setCurrentPaymentInfo(paymentInfo);
setCreditsModalOpen(false); setQrRevealed(true);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo; currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({ localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo, orderNo: order.orderNo,
@@ -890,13 +929,59 @@ const AppLayout: React.FC = () => {
await useAuthStore.getState().refreshUser(); await useAuthStore.getState().refreshUser();
await loadCreditCatalog(); await loadCreditCatalog();
setSelectedPlan(null); setSelectedPlan(null);
setQrCodeModalOpen(false);
setPendingProduct(null);
} }
} catch (err: any) { } catch (err: any) {
message.error(err?.message || '创建订单失败'); message.error(err?.message || '创建订单失败');
} finally { } finally {
setPaying(false); setPaying(false);
} }
}, [paymentMethod, enabledMethods]); }, [pendingProduct, paymentMethod]);
const submitCorporateTransfer = useCallback(async () => {
if (!pendingProduct) return;
try {
const values: any = await corporateTransferForm.validateFields();
setCorporateSubmitting(true);
const { id: productId, product } = pendingProduct;
const order = await createRechargeOrder(productId, 'corporate');
const finalPrice = Number(order.amount ?? product.currentPrice ?? product.price ?? 0);
const credits = Number(product.grantCredits || product.monthlyGrantCredits || 0);
const originalPrice = product.regularPrice ?? product.originalPrice;
const cycleMap: Record<string, string> = { monthly: '月套餐', quarterly: '季套餐', yearly: '年套餐' };
const isSubscription = !!product.billingCycle;
const periodLabel = product.billingCycle ? cycleMap[product.billingCycle] : '';
const tierName = product.name || '积分充值';
const displayTitle = isSubscription ? `${tierName} ${periodLabel}` : tierName;
const paymentInfo = {
price: finalPrice,
credits,
qrCode: '',
method: 'corporate',
title: displayTitle,
originalPrice: originalPrice ? Number(originalPrice) : undefined,
tierName,
periodLabel,
isSubscription,
};
void values;
setCurrentPaymentInfo(paymentInfo);
message.success('已提交对公转账信息,客服将在1-3个工作日内审核到账');
setQrCodeModalOpen(false);
setPendingProduct(null);
corporateTransferForm.resetFields();
await useAuthStore.getState().refreshUser();
await loadCreditCatalog();
} catch (err: any) {
if (err?.errorFields) {
return;
}
message.error(err?.message || '提交失败,请重试');
} finally {
setCorporateSubmitting(false);
}
}, [pendingProduct, corporateTransferForm]);
const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => { const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => {
stopPolling(); stopPolling();
@@ -1631,8 +1716,8 @@ const AppLayout: React.FC = () => {
<div style={{ padding: 48, textAlign: 'center', color: '#94a3b8' }}></div> <div style={{ padding: 48, textAlign: 'center', color: '#94a3b8' }}></div>
) : ( ) : (
<div style={{ display: 'flex', justifyContent: 'center', gap: 16, flexWrap: 'wrap' }}> <div style={{ display: 'flex', justifyContent: 'center', gap: 16, flexWrap: 'wrap' }}>
{subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).map((product, index) => { {subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).sort((a, b) => ((a.tierRank || 0) as number) - ((b.tierRank || 0) as number)).map((product) => {
const gradient = MEMBERSHIP_GRADIENTS[index % MEMBERSHIP_GRADIENTS.length]; const gradient = MEMBERSHIP_GRADIENTS[product.tierCode || ''] || MEMBERSHIP_GRADIENTS.default;
const originalPrice = product.regularPrice || product.price; const originalPrice = product.regularPrice || product.price;
const currentPrice = product.currentPrice ?? product.price; const currentPrice = product.currentPrice ?? product.price;
const hasDiscount = originalPrice && Number(originalPrice) > Number(currentPrice); const hasDiscount = originalPrice && Number(originalPrice) > Number(currentPrice);
@@ -1719,7 +1804,7 @@ const AppLayout: React.FC = () => {
))} ))}
</div> </div>
)} )}
{(product.priceType === 'first_purchase' || product.priceType === 'activity') && ( {/* {(product.priceType === 'first_purchase' || product.priceType === 'activity') && (
<div style={{ padding: '10px 14px', background: '#fffbe6', borderTop: '1px solid #fff1b8' }}> <div style={{ padding: '10px 14px', background: '#fffbe6', borderTop: '1px solid #fff1b8' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<FireOutlined style={{ color: '#fa8c16', fontSize: 12 }} /> <FireOutlined style={{ color: '#fa8c16', fontSize: 12 }} />
@@ -1731,7 +1816,7 @@ const AppLayout: React.FC = () => {
</div> </div>
))} ))}
</div> </div>
)} )} */}
</div> </div>
</div> </div>
); );
@@ -1844,9 +1929,9 @@ const AppLayout: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, maxHeight: 400, overflowY: 'auto', paddingRight: 4 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, maxHeight: 400, overflowY: 'auto', paddingRight: 4 }}>
{rechargeOptions.length === 0 && <div style={{ width: '100%', padding: 32, textAlign: 'center', color: '#94a3b8' }}></div>} {rechargeOptions.length === 0 && <div style={{ width: '100%', padding: 32, textAlign: 'center', color: '#94a3b8' }}></div>}
{rechargeOptions.map((opt, idx) => { {rechargeOptions.map((opt) => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = Number(opt.grantCredits || 0); const totalCredits = Number(opt.grantCredits || 0);
const g = getGradientByCredits(totalCredits);
const currentPrice = opt.currentPrice || opt.price; const currentPrice = opt.currentPrice || opt.price;
return ( return (
<div key={opt.id} style={{ borderRadius: 12, overflow: 'hidden', background: '#fff', border: selectedPlan === opt.id ? '2px solid #e8eaed' : '1px solid #e8eaed', position: 'relative', transition: 'all 0.2s' }}> <div key={opt.id} style={{ borderRadius: 12, overflow: 'hidden', background: '#fff', border: selectedPlan === opt.id ? '2px solid #e8eaed' : '1px solid #e8eaed', position: 'relative', transition: 'all 0.2s' }}>
@@ -1885,9 +1970,153 @@ const AppLayout: React.FC = () => {
localStorage.removeItem(PENDING_ORDER_KEY); localStorage.removeItem(PENDING_ORDER_KEY);
setQrCodeModalOpen(false); setQrCodeModalOpen(false);
setCurrentPaymentInfo(null); setCurrentPaymentInfo(null);
setPendingProduct(null);
setQrRevealed(false);
setPaymentTab('alipay');
corporateTransferForm.resetFields();
}} }}
footer={ footer={null}
<div style={{ display: 'flex', justifyContent: 'center', padding: '8px 0' }}> width={760}
closable
title={
<Typography.Title level={5} style={{ margin: 0 }}>
{(currentPaymentInfo?.title || pendingProduct?.product?.name || '会员服务')}{(currentPaymentInfo?.credits || (pendingProduct?.product?.grantCredits || pendingProduct?.product?.monthlyGrantCredits)) ? ` ${Number(currentPaymentInfo?.credits || pendingProduct?.product?.grantCredits || pendingProduct?.product?.monthlyGrantCredits || 0).toLocaleString()} 积分` : ''}
</Typography.Title>
}
styles={{
body: { padding: 0, borderRadius: 16, overflow: 'hidden' },
footer: { borderRadius: 16, borderTop: '1px solid #f0f0f0', padding: '16px 24px' },
}}
>
<div style={{ display: 'flex', minHeight: 380 }}>
{/* 左侧:商品信息(共享) */}
<div style={{ flex: 1, padding: 24, background: '#fff', borderRight: '1px solid #f0f0f0' }}>
<div style={{ marginBottom: 20 }}>
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}></Typography.Text>
<div style={{ padding: 14, background: '#f8fafc', borderRadius: 10, border: '1px solid #e2e8f0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#1a1a2e' }}>
{currentPaymentInfo?.tierName || currentPaymentInfo?.title || pendingProduct?.product?.name || '积分充值'}
</div>
{(currentPaymentInfo?.periodLabel || pendingProduct?.product?.billingCycle) && (
<Tag color="purple" style={{ fontSize: 11, margin: 0, borderRadius: 6, padding: '0 6px' }}>
{currentPaymentInfo?.periodLabel || ({ monthly: '月套餐', quarterly: '季套餐', yearly: '年套餐' } as Record<string, string>)[pendingProduct?.product?.billingCycle as string] || ''}
</Tag>
)}
</div>
<div style={{ fontSize: 22, fontWeight: 700, color: '#6366f1', lineHeight: 1.4 }}>
¥{currentPaymentInfo?.price || pendingProduct?.product?.currentPrice || pendingProduct?.product?.price || 0}
{((currentPaymentInfo?.originalPrice || pendingProduct?.product?.regularPrice) && Number(currentPaymentInfo?.originalPrice || pendingProduct?.product?.regularPrice || 0) > Number(currentPaymentInfo?.price || pendingProduct?.product?.currentPrice || pendingProduct?.product?.price || 0)) && (
<span style={{ fontSize: 13, color: '#94a3b8', textDecoration: 'line-through', marginLeft: 8, fontWeight: 400 }}>
¥{currentPaymentInfo?.originalPrice || pendingProduct?.product?.regularPrice}
</span>
)}
</div>
</div>
</div>
<div>
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}></Typography.Text>
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e' }}>
{((currentPaymentInfo?.credits != null) ? currentPaymentInfo.credits : (pendingProduct?.product?.grantCredits || pendingProduct?.product?.monthlyGrantCredits)) ? `${Number(currentPaymentInfo?.credits || pendingProduct?.product?.grantCredits || pendingProduct?.product?.monthlyGrantCredits || 0).toLocaleString()} 积分` : '-'}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e' }}>
{(currentPaymentInfo?.isSubscription ?? !!pendingProduct?.product?.billingCycle) ? '次年按原价 可随时取消' : '一次性购买'}
</span>
</div>
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e' }}>¥{currentPaymentInfo?.originalPrice || pendingProduct?.product?.regularPrice || currentPaymentInfo?.price || pendingProduct?.product?.price || 0}</span>
</div>
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 0' }}>
<span style={{ fontSize: 14, fontWeight: 500, color: '#1a1a2e' }}></span>
<span style={{ fontSize: 18, fontWeight: 700, color: '#6366f1' }}>
¥{currentPaymentInfo?.price || pendingProduct?.product?.currentPrice || pendingProduct?.product?.price || 0}
</span>
</div>
</div>
</div>
</div>
{/* 右侧:支付方式 */}
<div style={{ width: 300, padding: 20, background: '#fff', display: 'flex', flexDirection: 'column' }}>
{/* Tab 切换 */}
<div style={{ display: 'flex', background: '#f3f4f6', borderRadius: 10, padding: 4, marginBottom: 20 }}>
<div
onClick={() => setPaymentTab('alipay')}
style={{
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
cursor: 'pointer', fontSize: 14, fontWeight: 600,
background: paymentTab === 'alipay' ? '#fff' : 'transparent',
color: paymentTab === 'alipay' ? '#1677ff' : '#64748b',
boxShadow: paymentTab === 'alipay' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
transition: 'all 0.2s',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
}}
>
<AlipayCircleOutlined style={{ color: paymentTab === 'alipay' ? '#1677ff' : '#94a3b8' }} />
</div>
<div
onClick={() => setPaymentTab('corporate')}
style={{
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
cursor: 'pointer', fontSize: 14, fontWeight: 600,
background: paymentTab === 'corporate' ? '#fff' : 'transparent',
color: paymentTab === 'corporate' ? '#6366f1' : '#64748b',
boxShadow: paymentTab === 'corporate' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
transition: 'all 0.2s',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
}}
>
<BankOutlined style={{ color: paymentTab === 'corporate' ? '#6366f1' : '#94a3b8' }} />
</div>
</div>
{/* 支付宝 Tab */}
{paymentTab === 'alipay' && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12 }}>
<AlipayCircleOutlined style={{ fontSize: 18, color: '#1677ff' }} />
<span style={{ fontSize: 14, color: '#64748b' }}></span>
</div>
<div style={{ position: 'relative', width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
{qrRevealed && currentPaymentInfo?.qrCode ? (
<QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" />
) : (
<div style={{ position: 'absolute', inset: 0, background: '#fff', borderRadius: 12, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, zIndex: 10 }}>
<div style={{ fontSize: 13, color: '#64748b', textAlign: 'center', padding: '0 10px' }}>
<br/>
</div>
<Button
type="primary"
size="large"
loading={paying}
onClick={confirmPayment}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #1677ff, #4096ff)', border: 'none', fontWeight: 600, height: 36, fontSize: 13, minWidth: 140 }}
>
使
</Button>
</div>
)}
</div>
{qrRevealed && (
<div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>
{countdown}
</div>
)}
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
<br/>
</div>
{qrRevealed && (
<Button <Button
danger danger
size="large" size="large"
@@ -1900,103 +2129,76 @@ const AppLayout: React.FC = () => {
localStorage.removeItem(PENDING_ORDER_KEY); localStorage.removeItem(PENDING_ORDER_KEY);
setQrCodeModalOpen(false); setQrCodeModalOpen(false);
setCurrentPaymentInfo(null); setCurrentPaymentInfo(null);
setPendingProduct(null);
setQrRevealed(false);
message.info('已取消支付'); message.info('已取消支付');
}} }}
style={{ borderRadius: 10, minWidth: 140, fontSize: 14 }} style={{ borderRadius: 10, minWidth: 140, fontSize: 14, marginTop: 16 }}
> >
</Button> </Button>
</div>
}
width={700}
closable
title={
<Typography.Title level={5} style={{ margin: 0 }}>
{currentPaymentInfo?.title || '会员服务'}{currentPaymentInfo?.credits ? ` ${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : ''}
</Typography.Title>
}
styles={{
body: { padding: 0, borderRadius: 16, overflow: 'hidden' },
footer: { borderRadius: 16, borderTop: '1px solid #f0f0f0', padding: '16px 24px' },
}}
>
<div style={{ display: 'flex', minHeight: 380 }}>
<div style={{ flex: 1, padding: 24, background: '#fff', borderRight: '1px solid #f0f0f0' }}>
<div style={{ marginBottom: 20 }}>
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}></Typography.Text>
<div style={{ padding: 14, background: '#f8fafc', borderRadius: 10, border: '1px solid #e2e8f0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#1a1a2e' }}>
{currentPaymentInfo?.tierName || currentPaymentInfo?.title || '积分充值'}
</div>
{currentPaymentInfo?.periodLabel && (
<Tag color="purple" style={{ fontSize: 11, margin: 0, borderRadius: 6, padding: '0 6px' }}>
{currentPaymentInfo.periodLabel}
</Tag>
)} )}
</div> </div>
<div style={{ fontSize: 22, fontWeight: 700, color: '#6366f1', lineHeight: 1.4 }}>
¥{currentPaymentInfo?.price || 0}
{currentPaymentInfo?.originalPrice && Number(currentPaymentInfo.originalPrice) > Number(currentPaymentInfo?.price || 0) && (
<span style={{ fontSize: 13, color: '#94a3b8', textDecoration: 'line-through', marginLeft: 8, fontWeight: 400 }}>
¥{currentPaymentInfo.originalPrice}
</span>
)} )}
</div>
</div> {/* 对公转账 Tab */}
</div> {paymentTab === 'corporate' && (
<div> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowY: 'auto' }}>
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}></Typography.Text> {/* 汇款信息 */}
<div style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 8, display: 'block', fontWeight: 600 }}></Typography.Text>
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0' }}> <div style={{ padding: 12, background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
<span style={{ color: '#64748b' }}></span> <span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e' }}> <span style={{ color: '#1a1a2e', fontWeight: 500 }}>{COMPANY_BANK_INFO.accountName}</span>
{currentPaymentInfo?.credits != null ? `${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : '-'}
</span>
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
<span style={{ color: '#64748b' }}></span> <span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e' }}> <span style={{ color: '#1a1a2e', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 4 }}>
{currentPaymentInfo?.isSubscription ? '次年按原价 可随时取消' : '一次性购买'} <BankFilled style={{ color: '#c8161d', fontSize: 14 }} />
</span> {COMPANY_BANK_INFO.bankName}
</div>
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e' }}>¥{currentPaymentInfo?.originalPrice || currentPaymentInfo?.price || 0}.00</span>
</div>
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 0' }}>
<span style={{ fontSize: 14, fontWeight: 500, color: '#1a1a2e' }}></span>
<span style={{ fontSize: 18, fontWeight: 700, color: '#6366f1' }}>
¥{currentPaymentInfo?.price || 0}.00
</span> </span>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e', fontWeight: 500, fontFamily: 'monospace' }}>{COMPANY_BANK_INFO.bankAccount}</span>
</div> </div>
</div> </div>
</div> </div>
<div style={{ width: 280, padding: 24, background: '#fff', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}> {/* 打款信息填写 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
{currentPaymentInfo?.method === 'alipay' <Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 4, display: 'block', fontWeight: 600 }}></Typography.Text>
? <AlipayCircleOutlined style={{ fontSize: 18, color: '#1677ff' }} /> <Typography.Text style={{ fontSize: 11, color: '#ef4444', marginBottom: 8, display: 'block' }}></Typography.Text>
: <WechatOutlined style={{ fontSize: 18, color: '#07c160' }} />} <Form form={corporateTransferForm} layout="vertical" size="small" requiredMark={false}>
<span style={{ fontSize: 14, color: '#64748b' }}> <Form.Item name="accountName" label="账户名称" rules={[{ required: true, message: '请输入账户名称' }]} style={{ marginBottom: 10 }}>
{currentPaymentInfo?.method === 'alipay' ? '支付宝' : '微信'} <Input placeholder="请输入付款方账户名称" />
</span> </Form.Item>
</div> <Form.Item name="bankName" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]} style={{ marginBottom: 10 }}>
<div style={{ width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}> <Input placeholder="请输入开户银行" prefix={<BankOutlined style={{ color: '#94a3b8' }} />} />
{currentPaymentInfo?.qrCode && ( </Form.Item>
<QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" /> <Form.Item name="bankAccount" label="账号" rules={[{ required: true, message: '请输入账号' }]} style={{ marginBottom: 8 }}>
)} <Input placeholder="请输入付款方账号" />
</div> </Form.Item>
<div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}> </Form>
{countdown}
</div> </div>
<Button
type="primary"
block
size="large"
loading={corporateSubmitting}
onClick={submitCorporateTransfer}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', fontWeight: 600, height: 38, fontSize: 14 }}
>
</Button>
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}> <div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
<br /> 12
</div> </div>
</div> </div>
)}
</div>
</div> </div>
</Modal> </Modal>
+182 -77
View File
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Button, Input, Slider, Tooltip, message, Modal, Tag } from 'antd'; import { Button, Input, Slider, Tooltip, message, Modal, Tag, Dropdown } from 'antd';
import { import {
VideoCameraOutlined, VideoCameraOutlined,
SoundOutlined, SoundOutlined,
@@ -27,6 +27,10 @@ import {
ArrowDownOutlined, ArrowDownOutlined,
LeftOutlined, LeftOutlined,
PlayCircleFilled, PlayCircleFilled,
DownOutlined,
FolderOutlined,
UserOutlined,
RobotOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import CreationRecordPicker from '../components/CreationRecordPicker'; import CreationRecordPicker from '../components/CreationRecordPicker';
@@ -107,7 +111,6 @@ const VideovEditing: React.FC = () => {
const [textModalValue, setTextModalValue] = useState(''); const [textModalValue, setTextModalValue] = useState('');
const [textModalType, setTextModalType] = useState<'text' | 'watermark'>('text'); const [textModalType, setTextModalType] = useState<'text' | 'watermark'>('text');
// 导入选择弹窗 & 创作记录选择器 // 导入选择弹窗 & 创作记录选择器
const [importPickerOpen, setImportPickerOpen] = useState(false);
const [recordPickerOpen, setRecordPickerOpen] = useState(false); const [recordPickerOpen, setRecordPickerOpen] = useState(false);
// ========== Ref引用 ========== // ========== Ref引用 ==========
@@ -261,22 +264,6 @@ const VideovEditing: React.FC = () => {
message.success(textModalType === 'watermark' ? '已添加水印' : '已添加文本'); message.success(textModalType === 'watermark' ? '已添加水印' : '已添加文本');
}; };
/**
* 处理导入按钮点击:
* - video/audio/image/watermark → 打开文件选择
* - text → 打开文本输入弹窗
*/
const handleImportClick = () => {
if (activeTab === 'text') {
openTextModal('text');
} else if (activeTab === 'watermark') {
openTextModal('watermark');
} else {
// 弹出选择:本地上传 or 创作记录
setImportPickerOpen(true);
}
};
/** /**
* 计算新片段的起始时间(追加到所有片段末尾) * 计算新片段的起始时间(追加到所有片段末尾)
*/ */
@@ -480,6 +467,111 @@ const VideovEditing: React.FC = () => {
message.success(`已添加水印:${item.text || item.name}`); message.success(`已添加水印:${item.text || item.name}`);
} }
}, []); }, []);
/**
* 序列化编辑器状态为 JSON
*/
const serializeState = useCallback(() => {
const state = {
version: '1.0',
exportedAt: new Date().toISOString(),
mediaLibrary: mediaLibrary.map(m => ({
id: m.id,
type: m.type,
name: m.name,
url: m.url,
duration: m.duration,
thumbnail: m.thumbnail,
text: m.text,
})),
tracks: tracks.map(t => ({
id: t.id,
mediaId: t.mediaId,
type: t.type,
start: t.start,
duration: t.duration,
originalDuration: t.originalDuration,
trimStart: t.trimStart,
url: t.url,
name: t.name,
text: t.text,
x: t.x,
y: t.y,
width: t.width,
height: t.height,
opacity: t.opacity,
zIndex: t.zIndex,
})),
currentTime,
duration,
playbackRate,
volume,
zoom,
activeTab,
};
return JSON.stringify(state, null, 2);
}, [mediaLibrary, tracks, currentTime, duration, playbackRate, volume, zoom, activeTab]);
/**
* 从 JSON 反序列化恢复编辑器状态
*/
const deserializeState = useCallback((json: string) => {
try {
const state = JSON.parse(json);
if (!state || !Array.isArray(state.tracks)) {
message.error('无效的编辑器状态文件');
return;
}
setMediaLibrary(state.mediaLibrary || []);
setTracks(state.tracks || []);
setCurrentTime(state.currentTime || 0);
setDuration(state.duration || 0);
setPlaybackRate(state.playbackRate || 1);
setVolume(state.volume ?? 80);
setZoom(state.zoom || 50);
if (state.activeTab) setActiveTab(state.activeTab);
message.success('已恢复编辑器状态');
} catch {
message.error('解析 JSON 失败');
}
}, []);
/**
* 保存为 JSON 文件下载
*/
const handleSaveJSON = useCallback(() => {
const json = serializeState();
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `video-edit-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
message.success('已导出编辑器状态 JSON');
}, [serializeState]);
/**
* 从 JSON 文件加载
*/
const jsonInputRef = useRef<HTMLInputElement>(null);
const handleLoadJSON = useCallback(() => {
jsonInputRef.current?.click();
}, []);
const handleJSONFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const json = ev.target?.result as string;
deserializeState(json);
};
reader.readAsText(file);
if (e.target) e.target.value = '';
}, [deserializeState]);
/** /**
* 时间 → 像素 映射(用于时间轴渲染) * 时间 → 像素 映射(用于时间轴渲染)
*/ */
@@ -1086,9 +1178,23 @@ const VideovEditing: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
<Button <Button
icon={<SaveOutlined />} icon={<SaveOutlined />}
onClick={handleSaveJSON}
> >
稿 稿
</Button> </Button>
<Button
icon={<ImportOutlined />}
onClick={handleLoadJSON}
>
稿
</Button>
<input
ref={jsonInputRef}
type="file"
accept=".json"
style={{ display: 'none' }}
onChange={handleJSONFileChange}
/>
<Button <Button
type="primary" type="primary"
icon={<ExportOutlined />} icon={<ExportOutlined />}
@@ -1149,15 +1255,49 @@ const VideovEditing: React.FC = () => {
))} ))}
</div> </div>
{/* 素材导入按钮 */} {/* 素材导入按钮 - 下拉菜单 */}
<div style={{ padding: '12px', flexShrink: 0 }}> <div style={{ padding: '12px', flexShrink: 0 }}>
{activeTab === 'text' || activeTab === 'watermark' ? (
<Button <Button
block block
icon={<ImportOutlined />} icon={<ImportOutlined />}
onClick={handleImportClick} onClick={() => {
if (activeTab === 'text') openTextModal('text');
else openTextModal('watermark');
}}
> >
{activeTab === 'text' ? '添加文本' : activeTab === 'watermark' ? '添加水印' : `导入${tabs.find(t => t.key === activeTab)?.label}`} {activeTab === 'text' ? '添加文本' : '添加水印'}
</Button> </Button>
) : (
<Dropdown
menu={{
items: [
{
key: 'local',
icon: <FolderOutlined />,
label: '本地上传',
onClick: () => fileInputRef.current?.click(),
},
{
key: 'record',
icon: <FileTextOutlined />,
label: '创作记录',
onClick: () => setRecordPickerOpen(true),
},
],
}}
placement="bottom"
trigger={['click']}
>
<Button
block
icon={<ImportOutlined />}
>
{tabs.find(t => t.key === activeTab)?.label}
<DownOutlined style={{ fontSize: 10, opacity: 0.7, marginLeft: 4 }} />
</Button>
</Dropdown>
)}
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
@@ -2211,67 +2351,32 @@ const VideovEditing: React.FC = () => {
/> />
</Modal> </Modal>
{/* 导入选择弹窗:本地上传 or 创作记录 */}
<Modal
title="选择导入方式"
open={importPickerOpen}
onCancel={() => setImportPickerOpen(false)}
footer={null}
width={400}
>
<div style={{ display: 'flex', gap: 16, padding: '12px 0' }}>
<div
onClick={() => {
setImportPickerOpen(false);
fileInputRef.current?.click();
}}
style={{
flex: 1, padding: '24px 12px', textAlign: 'center', cursor: 'pointer',
borderRadius: 12, border: '2px solid #f0f0f5', transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#6366f1';
e.currentTarget.style.background = 'rgba(99,102,241,0.04)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#f0f0f5';
e.currentTarget.style.background = 'transparent';
}}
>
<ImportOutlined style={{ fontSize: 32, color: '#6366f1', marginBottom: 8 }} />
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}></div>
<div style={{ fontSize: 12, color: '#94a3b8' }}></div>
</div>
<div
onClick={() => {
setImportPickerOpen(false);
setRecordPickerOpen(true);
}}
style={{
flex: 1, padding: '24px 12px', textAlign: 'center', cursor: 'pointer',
borderRadius: 12, border: '2px solid #f0f0f5', transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#6366f1';
e.currentTarget.style.background = 'rgba(99,102,241,0.04)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#f0f0f5';
e.currentTarget.style.background = 'transparent';
}}
>
<FileTextOutlined style={{ fontSize: 32, color: '#8b5cf6', marginBottom: 8 }} />
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}></div>
<div style={{ fontSize: 12, color: '#94a3b8' }}></div>
</div>
</div>
</Modal>
{/* 创作记录选择器 */} {/* 创作记录选择器 */}
<CreationRecordPicker <CreationRecordPicker
open={recordPickerOpen} open={recordPickerOpen}
onClose={() => setRecordPickerOpen(false)} onClose={() => setRecordPickerOpen(false)}
mediaType={activeTab === 'image' ? 'image' : 'video'} mediaType={activeTab === 'image' ? 'image' : 'video'}
onSelect={(item) => {
const mediaUrl = item.videoUrl || item.imageUrl || '';
if (!mediaUrl) {
message.warning('无法获取媒体地址');
return;
}
const type: TrackType = item.videoUrl ? 'video' : 'image';
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const fullUrl = mediaUrl.startsWith('http') ? mediaUrl : `${baseUrl}${mediaUrl}`;
const name = item.title || item.id || '导入素材';
const newItem: MediaItem = {
id: `${Date.now()}-${Math.random()}`,
type,
name,
url: fullUrl,
};
setMediaLibrary(prev => [...prev, newItem]);
setActiveTab(type);
addToTimeline(newItem);
message.success(`已从资产导入:${name}`);
}}
/> />
</div> </div>
); );