冲突
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';
|
} 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) => {
|
||||||
setPreviewItem(item);
|
if (onSelect) {
|
||||||
setPreviewVisible(true);
|
onSelect(item);
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
setPreviewItem(item);
|
||||||
|
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>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
|
|||||||
@@ -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,34 +1970,17 @@ 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}
|
||||||
<Button
|
|
||||||
danger
|
|
||||||
size="large"
|
|
||||||
onClick={async () => {
|
|
||||||
stopPolling();
|
|
||||||
if (currentOrderNoRef.current) {
|
|
||||||
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
|
|
||||||
currentOrderNoRef.current = null;
|
|
||||||
}
|
|
||||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
|
||||||
setQrCodeModalOpen(false);
|
|
||||||
setCurrentPaymentInfo(null);
|
|
||||||
message.info('已取消支付');
|
|
||||||
}}
|
|
||||||
style={{ borderRadius: 10, minWidth: 140, fontSize: 14 }}
|
|
||||||
>
|
|
||||||
取消支付
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
width={700}
|
|
||||||
closable
|
closable
|
||||||
title={
|
title={
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
开通{currentPaymentInfo?.title || '会员服务'}{currentPaymentInfo?.credits ? ` ${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : ''}
|
开通{(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>
|
</Typography.Title>
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -1921,25 +1989,26 @@ const AppLayout: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', minHeight: 380 }}>
|
<div style={{ display: 'flex', minHeight: 380 }}>
|
||||||
|
{/* 左侧:商品信息(共享) */}
|
||||||
<div style={{ flex: 1, padding: 24, background: '#fff', borderRight: '1px solid #f0f0f0' }}>
|
<div style={{ flex: 1, padding: 24, background: '#fff', borderRight: '1px solid #f0f0f0' }}>
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<Typography.Text style={{ fontSize: 13, color: '#64748b', marginBottom: 8, display: 'block' }}>商品信息</Typography.Text>
|
<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={{ padding: 14, background: '#f8fafc', borderRadius: 10, border: '1px solid #e2e8f0' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||||
<div style={{ fontSize: 14, fontWeight: 600, color: '#1a1a2e' }}>
|
<div style={{ fontSize: 14, fontWeight: 600, color: '#1a1a2e' }}>
|
||||||
{currentPaymentInfo?.tierName || currentPaymentInfo?.title || '积分充值'}
|
{currentPaymentInfo?.tierName || currentPaymentInfo?.title || pendingProduct?.product?.name || '积分充值'}
|
||||||
</div>
|
</div>
|
||||||
{currentPaymentInfo?.periodLabel && (
|
{(currentPaymentInfo?.periodLabel || pendingProduct?.product?.billingCycle) && (
|
||||||
<Tag color="purple" style={{ fontSize: 11, margin: 0, borderRadius: 6, padding: '0 6px' }}>
|
<Tag color="purple" style={{ fontSize: 11, margin: 0, borderRadius: 6, padding: '0 6px' }}>
|
||||||
{currentPaymentInfo.periodLabel}
|
{currentPaymentInfo?.periodLabel || ({ monthly: '月套餐', quarterly: '季套餐', yearly: '年套餐' } as Record<string, string>)[pendingProduct?.product?.billingCycle as string] || ''}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#6366f1', lineHeight: 1.4 }}>
|
<div style={{ fontSize: 22, fontWeight: 700, color: '#6366f1', lineHeight: 1.4 }}>
|
||||||
¥{currentPaymentInfo?.price || 0}
|
¥{currentPaymentInfo?.price || pendingProduct?.product?.currentPrice || pendingProduct?.product?.price || 0}
|
||||||
{currentPaymentInfo?.originalPrice && Number(currentPaymentInfo.originalPrice) > Number(currentPaymentInfo?.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 }}>
|
<span style={{ fontSize: 13, color: '#94a3b8', textDecoration: 'line-through', marginLeft: 8, fontWeight: 400 }}>
|
||||||
¥{currentPaymentInfo.originalPrice}
|
¥{currentPaymentInfo?.originalPrice || pendingProduct?.product?.regularPrice}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1951,51 +2020,184 @@ const AppLayout: React.FC = () => {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
||||||
<span style={{ color: '#64748b' }}>积分数量</span>
|
<span style={{ color: '#64748b' }}>积分数量</span>
|
||||||
<span style={{ color: '#1a1a2e' }}>
|
<span style={{ color: '#1a1a2e' }}>
|
||||||
{currentPaymentInfo?.credits != null ? `${Number(currentPaymentInfo.credits).toLocaleString()} 积分` : '-'}
|
{((currentPaymentInfo?.credits != null) ? currentPaymentInfo.credits : (pendingProduct?.product?.grantCredits || pendingProduct?.product?.monthlyGrantCredits)) ? `${Number(currentPaymentInfo?.credits || pendingProduct?.product?.grantCredits || pendingProduct?.product?.monthlyGrantCredits || 0).toLocaleString()} 积分` : '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
||||||
<span style={{ color: '#64748b' }}>续费信息</span>
|
<span style={{ color: '#64748b' }}>续费信息</span>
|
||||||
<span style={{ color: '#1a1a2e' }}>
|
<span style={{ color: '#1a1a2e' }}>
|
||||||
{currentPaymentInfo?.isSubscription ? '次年按原价 可随时取消' : '一次性购买'}
|
{(currentPaymentInfo?.isSubscription ?? !!pendingProduct?.product?.billingCycle) ? '次年按原价 可随时取消' : '一次性购买'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
|
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
||||||
<span style={{ color: '#64748b' }}>商品原价</span>
|
<span style={{ color: '#64748b' }}>商品原价</span>
|
||||||
<span style={{ color: '#1a1a2e' }}>¥{currentPaymentInfo?.originalPrice || currentPaymentInfo?.price || 0}.00</span>
|
<span style={{ color: '#1a1a2e' }}>¥{currentPaymentInfo?.originalPrice || pendingProduct?.product?.regularPrice || currentPaymentInfo?.price || pendingProduct?.product?.price || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
|
<div style={{ height: 1, background: '#e2e8f0', margin: '8px 0' }} />
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 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: 14, fontWeight: 500, color: '#1a1a2e' }}>应付金额</span>
|
||||||
<span style={{ fontSize: 18, fontWeight: 700, color: '#6366f1' }}>
|
<span style={{ fontSize: 18, fontWeight: 700, color: '#6366f1' }}>
|
||||||
¥{currentPaymentInfo?.price || 0}.00
|
¥{currentPaymentInfo?.price || pendingProduct?.product?.currentPrice || pendingProduct?.product?.price || 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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={{ width: 300, padding: 20, background: '#fff', display: 'flex', flexDirection: 'column' }}>
|
||||||
{currentPaymentInfo?.method === 'alipay'
|
{/* Tab 切换 */}
|
||||||
? <AlipayCircleOutlined style={{ fontSize: 18, color: '#1677ff' }} />
|
<div style={{ display: 'flex', background: '#f3f4f6', borderRadius: 10, padding: 4, marginBottom: 20 }}>
|
||||||
: <WechatOutlined style={{ fontSize: 18, color: '#07c160' }} />}
|
<div
|
||||||
<span style={{ fontSize: 14, color: '#64748b' }}>
|
onClick={() => setPaymentTab('alipay')}
|
||||||
请用{currentPaymentInfo?.method === 'alipay' ? '支付宝' : '微信'}扫码支付
|
style={{
|
||||||
</span>
|
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
|
||||||
</div>
|
cursor: 'pointer', fontSize: 14, fontWeight: 600,
|
||||||
<div style={{ width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
|
background: paymentTab === 'alipay' ? '#fff' : 'transparent',
|
||||||
{currentPaymentInfo?.qrCode && (
|
color: paymentTab === 'alipay' ? '#1677ff' : '#64748b',
|
||||||
<QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" />
|
boxShadow: paymentTab === 'alipay' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
|
||||||
)}
|
transition: 'all 0.2s',
|
||||||
</div>
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||||
<div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>
|
}}
|
||||||
{countdown}秒后二维码失效
|
>
|
||||||
</div>
|
<AlipayCircleOutlined style={{ color: paymentTab === 'alipay' ? '#1677ff' : '#94a3b8' }} />
|
||||||
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
|
支付宝支付
|
||||||
开通即代表同意<br />《用户服务协议》
|
</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>
|
</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"
|
||||||
|
onClick={async () => {
|
||||||
|
stopPolling();
|
||||||
|
if (currentOrderNoRef.current) {
|
||||||
|
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
|
||||||
|
currentOrderNoRef.current = null;
|
||||||
|
}
|
||||||
|
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||||
|
setQrCodeModalOpen(false);
|
||||||
|
setCurrentPaymentInfo(null);
|
||||||
|
setPendingProduct(null);
|
||||||
|
setQrRevealed(false);
|
||||||
|
message.info('已取消支付');
|
||||||
|
}}
|
||||||
|
style={{ borderRadius: 10, minWidth: 140, fontSize: 14, marginTop: 16 }}
|
||||||
|
>
|
||||||
|
取消支付
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 对公转账 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', 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', 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={{ 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' }}>
|
||||||
|
提交后客服将在12小时内审核到账。如有疑问,请联系客服。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
<Button
|
{activeTab === 'text' || activeTab === 'watermark' ? (
|
||||||
block
|
<Button
|
||||||
icon={<ImportOutlined />}
|
block
|
||||||
onClick={handleImportClick}
|
icon={<ImportOutlined />}
|
||||||
>
|
onClick={() => {
|
||||||
{activeTab === 'text' ? '添加文本' : activeTab === 'watermark' ? '添加水印' : `导入${tabs.find(t => t.key === activeTab)?.label}`}
|
if (activeTab === 'text') openTextModal('text');
|
||||||
</Button>
|
else openTextModal('watermark');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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
|
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user