冲突
This commit is contained in:
+613
File diff suppressed because one or more lines are too long
@@ -13,6 +13,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { gethistory, gethistoryItems } from '../api';
|
||||
import UploadResourceHistoryPanel from './uploadResource/UploadResourceHistoryPanel';
|
||||
import { buildImagePreviewUrl } from '../utils/previewUrl';
|
||||
|
||||
type FilterType = 'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'upload_resource';
|
||||
@@ -21,6 +22,7 @@ interface CreationRecordPickerProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
mediaType?: 'video' | 'image';
|
||||
onSelect?: (item: any) => void;
|
||||
}
|
||||
|
||||
const TAB_ITEMS = [
|
||||
@@ -31,7 +33,7 @@ const TAB_ITEMS = [
|
||||
{ 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 [filterType, setFilterType] = useState<FilterType>('project');
|
||||
const [filterMedia, setFilterMedia] = useState<'video' | 'image'>(mediaType);
|
||||
@@ -166,6 +168,8 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setRecordList([]);
|
||||
setMediaLoadStatus(new Map());
|
||||
loadRecordList();
|
||||
}
|
||||
}, [open, filterType, filterMedia, page, selectedDate]);
|
||||
@@ -176,12 +180,19 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
|
||||
setPage(1);
|
||||
setSearchKeyword('');
|
||||
setSelectedDate('');
|
||||
setRecordList([]);
|
||||
setMediaLoadStatus(new Map());
|
||||
}
|
||||
}, [open, mediaType]);
|
||||
|
||||
const handlePreview = (item: any) => {
|
||||
const handleItemClick = (item: any) => {
|
||||
if (onSelect) {
|
||||
onSelect(item);
|
||||
onClose();
|
||||
} else {
|
||||
setPreviewItem(item);
|
||||
setPreviewVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClosePreview = () => {
|
||||
@@ -279,7 +290,7 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
}}
|
||||
onClick={() => handlePreview(item)}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1.05)';
|
||||
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}
|
||||
alt="预览"
|
||||
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={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@@ -303,6 +324,15 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
|
||||
}}
|
||||
onLoad={() => {
|
||||
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);
|
||||
newMap.set(resourceId, 'loaded');
|
||||
return newMap;
|
||||
@@ -344,6 +374,7 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
|
||||
onCancel={onClose}
|
||||
width={900}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
styles={{ body: { maxHeight: '70vh', overflow: 'auto' } }}
|
||||
>
|
||||
{/* 视频/图片切换 */}
|
||||
@@ -380,10 +411,7 @@ const CreationRecordPicker: React.FC<CreationRecordPickerProps> = ({ open, onClo
|
||||
/>
|
||||
|
||||
{filterType === 'upload_resource' ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: '#94a3b8' }}>
|
||||
<UploadOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
<div>历史素材请在历史素材页面查看</div>
|
||||
</div>
|
||||
<UploadResourceHistoryPanel />
|
||||
) : (
|
||||
<>
|
||||
{/* 搜索栏 */}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
CrownFilled,
|
||||
FireOutlined,
|
||||
BankFilled,
|
||||
BankOutlined,
|
||||
CheckCircleFilled,
|
||||
WechatOutlined,
|
||||
AlipayCircleOutlined,
|
||||
@@ -303,14 +304,30 @@ const GRADIENTS = [
|
||||
{ gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
|
||||
];
|
||||
|
||||
const MEMBERSHIP_GRADIENTS = [
|
||||
{ gradient: 'linear-gradient(135deg, #f6ad55, #dd6b20)', shadow: 'rgba(246,173,85,0.25)', icon: <CrownOutlined /> },
|
||||
{ gradient: 'linear-gradient(135deg, #667eea, #5a67d8)', shadow: 'rgba(102,126,234,0.25)', icon: <CrownOutlined /> },
|
||||
{ gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: <StarFilled /> },
|
||||
{ gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
|
||||
{ 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 getGradientByCredits = (credits: number) => {
|
||||
if (credits >= 10000) return GRADIENTS[3];
|
||||
if (credits >= 5000) return GRADIENTS[2];
|
||||
if (credits >= 1000) return GRADIENTS[1];
|
||||
return GRADIENTS[0];
|
||||
};
|
||||
|
||||
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 = [
|
||||
{
|
||||
@@ -464,6 +481,18 @@ const AppLayout: React.FC = () => {
|
||||
isSubscription?: boolean;
|
||||
} | 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 [unreadCount, setUnreadCount] = useState(0);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
@@ -849,8 +878,19 @@ const AppLayout: React.FC = () => {
|
||||
message.error('暂无可用支付方式');
|
||||
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);
|
||||
try {
|
||||
const { id: productId, product } = pendingProduct;
|
||||
const order = await createRechargeOrder(productId, paymentMethod);
|
||||
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
|
||||
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) {
|
||||
setCurrentPaymentInfo(paymentInfo);
|
||||
setCreditsModalOpen(false);
|
||||
setQrCodeModalOpen(true);
|
||||
setQrRevealed(true);
|
||||
currentOrderNoRef.current = order.orderNo;
|
||||
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
|
||||
orderNo: order.orderNo,
|
||||
@@ -890,13 +929,59 @@ const AppLayout: React.FC = () => {
|
||||
await useAuthStore.getState().refreshUser();
|
||||
await loadCreditCatalog();
|
||||
setSelectedPlan(null);
|
||||
setQrCodeModalOpen(false);
|
||||
setPendingProduct(null);
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '创建订单失败');
|
||||
} finally {
|
||||
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) => {
|
||||
stopPolling();
|
||||
@@ -1631,8 +1716,8 @@ const AppLayout: React.FC = () => {
|
||||
<div style={{ padding: 48, textAlign: 'center', color: '#94a3b8' }}>暂无可订阅套餐,请联系管理员配置并上架套餐。</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 16, flexWrap: 'wrap' }}>
|
||||
{subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).map((product, index) => {
|
||||
const gradient = MEMBERSHIP_GRADIENTS[index % MEMBERSHIP_GRADIENTS.length];
|
||||
{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[product.tierCode || ''] || MEMBERSHIP_GRADIENTS.default;
|
||||
const originalPrice = product.regularPrice || product.price;
|
||||
const currentPrice = product.currentPrice ?? product.price;
|
||||
const hasDiscount = originalPrice && Number(originalPrice) > Number(currentPrice);
|
||||
@@ -1719,7 +1804,7 @@ const AppLayout: React.FC = () => {
|
||||
))}
|
||||
</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={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
|
||||
<FireOutlined style={{ color: '#fa8c16', fontSize: 12 }} />
|
||||
@@ -1731,7 +1816,7 @@ const AppLayout: React.FC = () => {
|
||||
</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 }}>
|
||||
{rechargeOptions.length === 0 && <div style={{ width: '100%', padding: 32, textAlign: 'center', color: '#94a3b8' }}>暂无可购买的积分增值包</div>}
|
||||
{rechargeOptions.map((opt, idx) => {
|
||||
const g = GRADIENTS[idx % GRADIENTS.length];
|
||||
{rechargeOptions.map((opt) => {
|
||||
const totalCredits = Number(opt.grantCredits || 0);
|
||||
const g = getGradientByCredits(totalCredits);
|
||||
const currentPrice = opt.currentPrice || opt.price;
|
||||
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' }}>
|
||||
@@ -1885,9 +1970,153 @@ const AppLayout: React.FC = () => {
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
setQrCodeModalOpen(false);
|
||||
setCurrentPaymentInfo(null);
|
||||
setPendingProduct(null);
|
||||
setQrRevealed(false);
|
||||
setPaymentTab('alipay');
|
||||
corporateTransferForm.resetFields();
|
||||
}}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '8px 0' }}>
|
||||
footer={null}
|
||||
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
|
||||
danger
|
||||
size="large"
|
||||
@@ -1900,103 +2129,76 @@ const AppLayout: React.FC = () => {
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
setQrCodeModalOpen(false);
|
||||
setCurrentPaymentInfo(null);
|
||||
setPendingProduct(null);
|
||||
setQrRevealed(false);
|
||||
message.info('已取消支付');
|
||||
}}
|
||||
style={{ borderRadius: 10, minWidth: 140, fontSize: 14 }}
|
||||
style={{ borderRadius: 10, minWidth: 140, fontSize: 14, marginTop: 16 }}
|
||||
>
|
||||
取消支付
|
||||
</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 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>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}>订单明细</Typography.Text>
|
||||
|
||||
{/* 对公转账 Tab */}
|
||||
{paymentTab === 'corporate' && (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowY: 'auto' }}>
|
||||
{/* 汇款信息 */}
|
||||
<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={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
||||
<span style={{ color: '#64748b' }}>积分数量</span>
|
||||
<span style={{ color: '#1a1a2e' }}>
|
||||
{currentPaymentInfo?.credits != null ? `${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : '-'}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
|
||||
<span style={{ color: '#64748b' }}>账户名称</span>
|
||||
<span style={{ color: '#1a1a2e', fontWeight: 500 }}>{COMPANY_BANK_INFO.accountName}</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 ? '次年按原价 可随时取消' : '一次性购买'}
|
||||
</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 || 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
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
|
||||
<span style={{ color: '#64748b' }}>开户银行</span>
|
||||
<span style={{ color: '#1a1a2e', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<BankFilled style={{ color: '#c8161d', fontSize: 14 }} />
|
||||
{COMPANY_BANK_INFO.bankName}
|
||||
</span>
|
||||
</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 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 }}>
|
||||
{currentPaymentInfo?.method === 'alipay'
|
||||
? <AlipayCircleOutlined style={{ fontSize: 18, color: '#1677ff' }} />
|
||||
: <WechatOutlined style={{ fontSize: 18, color: '#07c160' }} />}
|
||||
<span style={{ fontSize: 14, color: '#64748b' }}>
|
||||
请用{currentPaymentInfo?.method === 'alipay' ? '支付宝' : '微信'}扫码支付
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
|
||||
{currentPaymentInfo?.qrCode && (
|
||||
<QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>
|
||||
{countdown}秒后二维码失效
|
||||
{/* 打款信息填写 */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 4, display: 'block', fontWeight: 600 }}>填写您的打款信息</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#ef4444', marginBottom: 8, display: 'block' }}>注:打款金额需与所标注金额一致,不然无法开通,银行信息需正确填写。</Typography.Text>
|
||||
<Form form={corporateTransferForm} layout="vertical" size="small" requiredMark={false}>
|
||||
<Form.Item name="accountName" label="账户名称" rules={[{ required: true, message: '请输入账户名称' }]} style={{ marginBottom: 10 }}>
|
||||
<Input placeholder="请输入付款方账户名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankName" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]} style={{ marginBottom: 10 }}>
|
||||
<Input placeholder="请输入开户银行" prefix={<BankOutlined style={{ color: '#94a3b8' }} />} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccount" label="账号" rules={[{ required: true, message: '请输入账号' }]} style={{ marginBottom: 8 }}>
|
||||
<Input placeholder="请输入付款方账号" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</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' }}>
|
||||
开通即代表同意<br />《用户服务协议》
|
||||
提交后客服将在12小时内审核到账。如有疑问,请联系客服。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
VideoCameraOutlined,
|
||||
SoundOutlined,
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
ArrowDownOutlined,
|
||||
LeftOutlined,
|
||||
PlayCircleFilled,
|
||||
DownOutlined,
|
||||
FolderOutlined,
|
||||
UserOutlined,
|
||||
RobotOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import CreationRecordPicker from '../components/CreationRecordPicker';
|
||||
@@ -107,7 +111,6 @@ const VideovEditing: React.FC = () => {
|
||||
const [textModalValue, setTextModalValue] = useState('');
|
||||
const [textModalType, setTextModalType] = useState<'text' | 'watermark'>('text');
|
||||
// 导入选择弹窗 & 创作记录选择器
|
||||
const [importPickerOpen, setImportPickerOpen] = useState(false);
|
||||
const [recordPickerOpen, setRecordPickerOpen] = useState(false);
|
||||
|
||||
// ========== Ref引用 ==========
|
||||
@@ -261,22 +264,6 @@ const VideovEditing: React.FC = () => {
|
||||
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}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 序列化编辑器状态为 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 }}>
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveJSON}
|
||||
>
|
||||
保存草稿
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ImportOutlined />}
|
||||
onClick={handleLoadJSON}
|
||||
>
|
||||
加载草稿
|
||||
</Button>
|
||||
<input
|
||||
ref={jsonInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleJSONFileChange}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ExportOutlined />}
|
||||
@@ -1149,15 +1255,49 @@ const VideovEditing: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 素材导入按钮 */}
|
||||
{/* 素材导入按钮 - 下拉菜单 */}
|
||||
<div style={{ padding: '12px', flexShrink: 0 }}>
|
||||
{activeTab === 'text' || activeTab === 'watermark' ? (
|
||||
<Button
|
||||
block
|
||||
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>
|
||||
) : (
|
||||
<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
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -2211,67 +2351,32 @@ const VideovEditing: React.FC = () => {
|
||||
/>
|
||||
</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
|
||||
open={recordPickerOpen}
|
||||
onClose={() => setRecordPickerOpen(false)}
|
||||
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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user