This commit is contained in:
2026-08-04 18:05:03 +08:00
6 changed files with 927 additions and 716 deletions
+5 -1
View File
@@ -1 +1,5 @@
{} {
"src/pages/CreativePlazaPage.tsx": {
"description": "创意广场"
}
}
@@ -131,6 +131,17 @@
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5); box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5);
} }
.recharge-drawer .ant-drawer-content-wrapper {
border-top-left-radius: 20px !important;
border-top-right-radius: 20px !important;
overflow: hidden !important;
}
.recharge-drawer .ant-drawer-content {
border-top-left-radius: 20px !important;
border-top-right-radius: 20px !important;
}
@media (max-width: 767px) { @media (max-width: 767px) {
.desktop-sidebar { .desktop-sidebar {
display: none !important; display: none !important;
+405 -24
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, useRef } from 'react'; import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer, Tabs } from 'antd'; import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer, Tabs, Collapse } from 'antd';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { import {
PlayCircleOutlined, PlayCircleOutlined,
@@ -20,7 +20,9 @@ import {
StarFilled, StarFilled,
FireFilled, FireFilled,
CrownFilled, CrownFilled,
CrownOutlined,
BankFilled, BankFilled,
CheckCircleFilled,
CloseOutlined, CloseOutlined,
WechatOutlined, WechatOutlined,
AlipayCircleOutlined, AlipayCircleOutlined,
@@ -310,6 +312,72 @@ 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 = [
{ 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: <CrownFilled /> },
{ gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
];
const MEMBERSHIP_FEATURES = [
'每日赠送积分 当日清零',
'可充值更多积分',
'基础加速通道',
'作品去除水印',
'可使用网页版和APP上全部AI功能',
'高分辨率',
'批量创作 更多同时生成任务',
];
const MEMBERSHIP_PLANS = [
{
tier: '基础会员',
desc: '开启创作之旅',
monthlyPrice: 79,
monthlyCredits: 1000,
onlyMonthly: true,
features: MEMBERSHIP_FEATURES.map((f) => f.replace('基础加速通道', '基础加速通道')),
speedLabel: '基础加速通道',
},
{
tier: '标准会员',
desc: '畅享基础创作权益',
monthlyPrice: 154,
monthlyCredits: 2000,
quarterlyPrice: 1029,
quarterlyCredits: 5000,
yearlyPrice: 7308,
yearlyCredits: 10000,
features: MEMBERSHIP_FEATURES,
speedLabel: '标准加速通道',
},
{
tier: '高级会员',
desc: '解锁高级创作权益',
monthlyPrice: 221,
monthlyCredits: 3000,
quarterlyPrice: 1197,
quarterlyCredits: 6000,
yearlyPrice: 10710,
yearlyCredits: 15000,
features: MEMBERSHIP_FEATURES,
speedLabel: '高级加速通道',
},
{
tier: '超级会员',
desc: '释放无限创作生产力',
monthlyPrice: 280,
monthlyCredits: 4000,
quarterlyPrice: 1367,
quarterlyCredits: 7000,
yearlyPrice: 13440,
yearlyCredits: 20000,
features: MEMBERSHIP_FEATURES,
speedLabel: '高级加速通道',
hot: true,
},
];
const AppLayout: React.FC = () => { const AppLayout: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -322,6 +390,9 @@ const AppLayout: React.FC = () => {
const [selectedPlan, setSelectedPlan] = useState<number | null>(null); const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]); const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]); const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
const [creditsModalOpen, setCreditsModalOpen] = useState(false);
const [selectedTierIndex, setSelectedTierIndex] = useState<number | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<'monthly' | 'quarterly' | 'yearly'>('monthly');
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [contactHovered, setContactHovered] = useState(false); const [contactHovered, setContactHovered] = useState(false);
@@ -1357,15 +1428,33 @@ const AppLayout: React.FC = () => {
</Tabs> </Tabs>
</Modal> </Modal>
<Modal title={<Space><GiftOutlined /></Space>} open={rechargeModalOpen} <Drawer
onCancel={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} title={null}
width={680} placement="bottom"
className="recharge-modal" open={rechargeModalOpen}
onClose={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }}
height="95vh"
className="recharge-drawer"
styles={{
header: { display: 'none' },
body: { padding: '20px 24px 0', overflowY: 'auto', position: 'relative' },
}}
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '12px 0 0', borderTop: '1px solid #f0f0f0' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', borderTop: '1px solid #f0f0f0' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button> <Space>
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying} <WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b' }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 18 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
<div style={{ display: 'flex', gap: 12 }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }} style={{ borderRadius: 10 }}></Button>
<Button
type="primary"
size="large"
disabled={!selectedPlan && selectedTierIndex === null}
loading={paying}
onClick={async () => { onClick={async () => {
if (selectedPlan) {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan); const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return; if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0); const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
@@ -1384,7 +1473,6 @@ const AppLayout: React.FC = () => {
setRechargeModalOpen(false); setRechargeModalOpen(false);
setQrCodeModalOpen(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,
price: plan.price, price: plan.price,
@@ -1394,7 +1482,6 @@ const AppLayout: React.FC = () => {
createdAt: order.createdAt || new Date().toISOString(), createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180, timeoutSeconds: 180,
})); }));
startPolling(order.orderNo); startPolling(order.orderNo);
} else { } else {
message.success('充值成功!积分已到账'); message.success('充值成功!积分已到账');
@@ -1407,6 +1494,7 @@ const AppLayout: React.FC = () => {
} finally { } finally {
setPaying(false); setPaying(false);
} }
}
}} }}
style={{ style={{
borderRadius: 10, fontWeight: 600, borderRadius: 10, fontWeight: 600,
@@ -1416,13 +1504,198 @@ const AppLayout: React.FC = () => {
</Button> </Button>
</div> </div>
}> </div>
<div style={{ marginTop: 16 }}> }
<Space style={{ marginBottom: 16 }}> >
<WalletOutlined style={{ color: '#6366f1' }} /> <div>
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}></Typography.Text> <div style={{ position: 'absolute', top: 8, right: 16, zIndex: 10 }}>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text> <CloseOutlined
</Space> style={{ fontSize: 18, color: '#94a3b8', cursor: 'pointer', padding: 8, borderRadius: '50%', transition: 'color 0.2s' }}
onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }}
onMouseEnter={(e) => { (e.target as HTMLElement).style.color = '#6366f1'; }}
onMouseLeave={(e) => { (e.target as HTMLElement).style.color = '#94a3b8'; }}
/>
</div>
<div style={{ textAlign: 'center', marginBottom: 20, marginTop: 16 }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 20, fontWeight: 700, color: '#1a1a2e' }}>
</Typography.Title>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 12, marginTop: 12 }}>
<div style={{
display: 'inline-flex', background: '#f5f5fa', borderRadius: 10, padding: 4,
gap: 4,
}}>
{[
{ key: 'yearly', label: '连续包年 8折' },
{ key: 'quarterly', label: '连续包季 8折' },
{ key: 'monthly', label: '连续包月' },
].map((p) => (
<div
key={p.key}
onClick={() => {
const period = p.key as 'monthly' | 'quarterly' | 'yearly';
setSelectedPeriod(period);
if (period !== 'monthly' && selectedTierIndex !== null && MEMBERSHIP_PLANS[selectedTierIndex]?.onlyMonthly) {
setSelectedTierIndex(null);
}
}}
style={{
padding: '6px 16px', borderRadius: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500,
background: selectedPeriod === p.key ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : 'transparent',
color: selectedPeriod === p.key ? '#fff' : '#64748b',
transition: 'all 0.2s', whiteSpace: 'nowrap',
}}
>
{p.label}
</div>
))}
</div>
{/* <Tag
style={{
borderRadius: 8, fontSize: 12, padding: '4px 12px', cursor: 'pointer',
background: '#f0f0ff', color: '#6366f1', border: '1px solid #e0e0ff',
}}
>
API服务
</Tag> */}
</div>
</div>
<div style={{ position: 'absolute', top: 12, right: 48, zIndex: 10 }}>
<div
onClick={() => setCreditsModalOpen(true)}
style={{
display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer',
padding: '6px 14px', borderRadius: 20,
background: 'linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.08))',
border: '1px solid rgba(99,102,241,0.2)',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.15))';
e.currentTarget.style.borderColor = 'rgba(99,102,241,0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.08))';
e.currentTarget.style.borderColor = 'rgba(99,102,241,0.2)';
}}
>
<WalletOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<span style={{ fontSize: 13, fontWeight: 600, color: '#6366f1' }}></span>
{/* <Tag color="purple" style={{ marginLeft: 2, borderRadius: 6, fontSize: 10, margin: 0, padding: '0 6px', lineHeight: '18px' }}>8折</Tag> */}
</div>
</div>
<div style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
{MEMBERSHIP_PLANS
.map((plan, idx) => ({ plan, idx }))
.filter(({ plan }) => selectedPeriod === 'monthly' || !plan.onlyMonthly)
.map(({ plan, idx }) => {
const g = MEMBERSHIP_GRADIENTS[idx % MEMBERSHIP_GRADIENTS.length];
const price = selectedPeriod === 'monthly' ? plan.monthlyPrice
: selectedPeriod === 'quarterly' ? plan.quarterlyPrice : plan.yearlyPrice;
const credits = selectedPeriod === 'monthly' ? plan.monthlyCredits
: selectedPeriod === 'quarterly' ? plan.quarterlyCredits : plan.yearlyCredits;
const creditsPer10 = Math.round((credits / price) * 10);
const isSelected = selectedTierIndex === idx;
const isHot = plan.hot;
return (
<div
key={idx}
onClick={() => setSelectedTierIndex(idx)}
style={{
flex: 1,
borderRadius: 16,
padding: '20px 16px 16px',
background: isHot
? 'linear-gradient(180deg, #f0edff 0%, #fafbff 30%)'
: '#fafbff',
border: isSelected
? '2px solid #6366f1'
: isHot
? '2px solid #8b5cf6'
: '1px solid #f0f0f5',
cursor: 'pointer',
position: 'relative',
transition: 'all 0.2s',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
}}
>
{isHot && (
<div style={{
position: 'absolute', top: -10, right: 12,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', color: '#fff',
fontSize: 11, padding: '3px 10px', borderRadius: 8, fontWeight: 600,
}}></div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<Typography.Text strong style={{ fontSize: 15, color: '#1a1a2e' }}>{plan.tier}</Typography.Text>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>{plan.desc}</Typography.Text>
</div>
<div style={{ marginTop: 8, marginBottom: 4 }}>
<span style={{
fontSize: 28, fontWeight: 800,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{price}</span>
<span style={{ fontSize: 12, color: '#94a3b8', marginLeft: 4 }}>/</span>
</div>
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', marginBottom: 8 }}>
</Typography.Text>
<div style={{
display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap',
}}>
<div style={{
fontSize: 11, color: '#6366f1', background: 'rgba(99,102,241,0.08)',
padding: '3px 8px', borderRadius: 6, fontWeight: 600,
}}>
{credits.toLocaleString()}
</div>
<div style={{
fontSize: 11, color: '#94a3b8', background: '#f5f5fa',
padding: '3px 8px', borderRadius: 6,
}}>
¥10 = {creditsPer10}
</div>
</div>
<Button
block
size="small"
onClick={(e) => { e.stopPropagation(); setSelectedTierIndex(idx); }}
style={{
borderRadius: 10, marginBottom: 12, fontWeight: 600, height: 36,
background: isSelected
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: isHot
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: '#1a1a2e',
border: 'none', color: '#fff', fontSize: 13,
}}
>
{plan.tier}
</Button>
<div style={{
borderTop: '1px solid #f0f0f5', paddingTop: 10,
display: 'flex', flexDirection: 'column', gap: 6,
}}>
{plan.features.map((feature: string, fi: number) => (
<div key={fi} style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<CheckCircleFilled style={{ fontSize: 12, color: '#6366f1', flexShrink: 0, marginTop: 2 }} />
<Typography.Text style={{ fontSize: 11, color: '#64748b', lineHeight: 1.5 }}>{feature}</Typography.Text>
</div>
))}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<CheckCircleFilled style={{ fontSize: 12, color: '#6366f1', flexShrink: 0, marginTop: 2 }} />
<Typography.Text style={{ fontSize: 11, color: '#64748b', lineHeight: 1.5 }}>
</Typography.Text>
</div>
</div>
</div>
);
})}
</div>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? ( {(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginBottom: 16, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}> <div style={{ marginBottom: 16, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
@@ -1481,13 +1754,121 @@ const AppLayout: React.FC = () => {
></Typography.Text> ></Typography.Text>
</Typography.Text> </Typography.Text>
</div> </div>
</div>
</Drawer>
{/* 积分充值独立弹窗 */}
<Modal
title={<Space><WalletOutlined style={{ color: '#6366f1' }} /></Space>}
open={creditsModalOpen}
onCancel={() => { setCreditsModalOpen(false); setSelectedPlan(null); }}
width={560}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '12px 0 0', borderTop: '1px solid #f0f0f0' }}>
<Button size="large" onClick={() => { setCreditsModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying}
onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
try {
setPaying(true);
const order = await createRechargeOrder(plan.id, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
const paymentInfo = {
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
};
setCurrentPaymentInfo(paymentInfo);
setCreditsModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
startPolling(order.orderNo);
} else {
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
setCreditsModalOpen(false);
setSelectedPlan(null);
}
} catch (err: any) {
message.error(err?.message || '创建订单失败,请重试');
} finally {
setPaying(false);
}
}}
style={{
borderRadius: 10, fontWeight: 600,
background: selectedPlan ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : '#d1d5db',
border: 'none', boxShadow: selectedPlan ? '0 8px 24px rgba(99,102,241,0.3)' : 'none',
}}>
</Button>
</div>
}
>
<div style={{ marginTop: 16 }}>
<Space style={{ marginBottom: 16 }}>
<WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginBottom: 16, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
</Typography.Text>
</div>
) : (
<div style={{ marginBottom: 16 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
{enabledMethods.alipay && (
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
{enabledMethods.wechat && (
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
</Radio.Group>
</div>
)}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{rechargeOptions.map((opt, idx) => { {rechargeOptions.map((opt, idx) => {
const g = GRADIENTS[idx % GRADIENTS.length]; const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0); const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
return ( return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{ <div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px', flex: '1 1 45%', minWidth: 180, borderRadius: 16, padding: '18px 14px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff', background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5', border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s', cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
@@ -1495,19 +1876,19 @@ const AppLayout: React.FC = () => {
{opt.description && ( {opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag> <Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)} )}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ <div style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0, width: 40, height: 40, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center', background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`, fontSize: 16, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div> }}>{g.icon}</div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text> <Typography.Text strong style={{ fontSize: 14 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 14, fontWeight: 600, color: '#6366f1' }}>{totalCredits.toLocaleString()} </Typography.Text> <Typography.Text style={{ fontSize: 13, fontWeight: 600, color: '#6366f1' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div> </div>
<div style={{ <div style={{
fontSize: 22, fontWeight: 800, marginTop: 4, fontSize: 20, fontWeight: 800, marginTop: 2,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div> }}>¥{opt.price}</div>
</div> </div>
+380 -177
View File
@@ -1,56 +1,184 @@
import React from 'react'; import React, { useState, useMemo } from 'react';
import { Button, Card, message, Typography } from 'antd'; import { Button, Typography, Modal, Input, Select } from 'antd';
import { import {
StarOutlined, StarOutlined,
LockOutlined,
GiftOutlined, GiftOutlined,
ThunderboltOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
HeartOutlined,
EyeOutlined,
ArrowRightOutlined,
CrownOutlined, CrownOutlined,
DownloadOutlined,
VideoCameraOutlined,
PictureOutlined,
PlayCircleOutlined,
SearchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
const CreativePlazaPage: React.FC = () => { const CreativePlazaPage: React.FC = () => {
const handleUnlock = () => { const [previewVisible, setPreviewVisible] = useState(false);
message.info('请联系客服开通会员'); const [previewUrl, setPreviewUrl] = useState('');
const [previewType, setPreviewType] = useState<'image' | 'video'>('image');
const [searchText, setSearchText] = useState('');
const [selectedIndustry, setSelectedIndustry] = useState<string>('all');
const openPreview = (url: string, type: 'image' | 'video') => {
setPreviewUrl(url);
setPreviewType(type);
setPreviewVisible(true);
}; };
// 顶部特性卡片 const handleDownload = (url: string, filename: string) => {
const features = [ const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 行业选项
const industryOptions = [
{ value: 'all', label: '全部行业' },
{ value: 'fashion', label: '时尚穿搭' },
{ value: 'food', label: '美食探店' },
{ value: 'travel', label: '旅行风景' },
{ value: 'tech', label: '数码科技' },
{ value: 'beauty', label: '美妆护肤' },
{ value: 'home', label: '家居生活' },
{ value: '母婴', label: '母婴亲子' },
];
// 瀑布流数据 - 包含图片和视频
const waterfallItems = [
{ {
icon: <ThunderboltOutlined style={{ fontSize: 26, color: '#8b5cf6' }} />, id: 1,
title: 'AI 智能生成', type: 'image' as const,
description: '先进 AI 技术一键产出高质量视频内容', url: 'https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=400&q=80',
title: '时尚街拍',
tag: '热门',
industry: 'fashion',
aspectRatio: 1.3,
}, },
{ {
icon: <PlayCircleOutlined style={{ fontSize: 26, color: '#ec4899' }} />, id: 2,
title: '多格式输出', type: 'video' as const,
description: '支持多种视频格式,满足不同场景需求', url: 'https://www.w3schools.com/html/mov_bbb.mp4',
poster: 'https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?w=400&q=80',
title: '自然风景',
tag: '推荐',
industry: 'travel',
aspectRatio: 1.5,
}, },
{ {
icon: <CheckCircleOutlined style={{ fontSize: 26, color: '#10b981' }} />, id: 3,
title: '品质保证', type: 'image' as const,
description: '专业级画质,细节清晰,色彩鲜艳', url: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80',
title: '湖光山色',
tag: '热门',
industry: 'travel',
aspectRatio: 0.75,
}, },
{ {
icon: <HeartOutlined style={{ fontSize: 26, color: '#f59e0b' }} />, id: 4,
title: '创意无限', type: 'image' as const,
description: '丰富模板与风格,激发创作灵感', url: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&q=80',
title: '山脉风光',
tag: '新品',
industry: 'travel',
aspectRatio: 1.4,
},
{
id: 5,
type: 'video' as const,
url: 'https://www.w3schools.com/html/movie.mp4',
poster: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400&q=80',
title: '森林探索',
tag: '推荐',
industry: 'travel',
aspectRatio: 0.67,
},
{
id: 6,
type: 'image' as const,
url: 'https://images.unsplash.com/photo-1470770841072-f978cf4d019e?w=400&q=80',
title: '山间湖泊',
tag: '热门',
industry: 'travel',
aspectRatio: 1.7,
},
{
id: 7,
type: 'image' as const,
url: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=400&q=80',
title: '海岸风光',
tag: '推荐',
industry: 'travel',
aspectRatio: 0.8,
},
{
id: 8,
type: 'video' as const,
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
poster: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=400&q=80',
title: '城市夜景',
tag: '新品',
industry: 'travel',
aspectRatio: 1.33,
},
{
id: 9,
type: 'image' as const,
url: 'https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=400&q=80',
title: '森林小径',
tag: '热门',
industry: 'travel',
aspectRatio: 1.5,
},
{
id: 10,
type: 'image' as const,
url: 'https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=400&q=80',
title: '美食摆盘',
tag: '热门',
industry: 'food',
aspectRatio: 1.2,
},
{
id: 11,
type: 'image' as const,
url: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=400&q=80',
title: '时尚穿搭',
tag: '推荐',
industry: 'fashion',
aspectRatio: 1.0,
},
{
id: 12,
type: 'video' as const,
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
poster: 'https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&q=80',
title: '数码产品',
tag: '新品',
industry: 'tech',
aspectRatio: 1.4,
}, },
]; ];
// 精选案例占位 // 筛选后的数据
const caseList = [ const filteredItems = useMemo(() => {
{ title: '美妆直播切片', tag: '热门', tagColor: '#ef4444' }, return waterfallItems.filter((item) => {
{ title: '3C 数码测评', tag: '推荐', tagColor: '#6366f1' }, const matchSearch = searchText
{ title: '美食探店 vlog', tag: '推荐', tagColor: '#6366f1' }, ? item.title.toLowerCase().includes(searchText.toLowerCase())
{ title: '服饰穿搭合集', tag: '热门', tagColor: '#ef4444' }, : true;
{ title: '家居场景展示', tag: '推荐', tagColor: '#6366f1' }, const matchIndustry = selectedIndustry === 'all' ? true : item.industry === selectedIndustry;
{ title: '母婴亲子内容', tag: '热门', tagColor: '#ef4444' }, return matchSearch && matchIndustry;
]; });
}, [searchText, selectedIndustry]);
const handleItemClick = (item: typeof waterfallItems[0]) => {
const previewUrl = item.type === 'image' ? item.url : item.url;
openPreview(previewUrl, item.type);
};
return ( return (
<div className="content_box"> <div className="content_box">
@@ -100,54 +228,7 @@ const CreativePlazaPage: React.FC = () => {
</div> </div>
{/* 特性卡片 */} {/* 特性卡片 */}
{/* <div
className="animate-fadeInUp stagger-children"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 16,
marginBottom: 24,
}}
>
{features.map((feature, index) => (
<Card
key={index}
bordered={false}
hoverable
className="project-card"
style={{
borderRadius: 16,
background: '#fff',
border: '1px solid #e2e8f0',
padding: '4px 0',
}}
styles={{ body: { padding: 20 } }}
>
<div style={{ textAlign: 'left' }}>
<div
style={{
width: 48,
height: 48,
borderRadius: 12,
background: 'linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(99, 102, 241, 0.08))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 14,
}}
>
{feature.icon}
</div>
<div style={{ fontSize: 15, fontWeight: 600, color: '#1e293b', marginBottom: 6 }}>
{feature.title}
</div>
<div style={{ fontSize: 13, color: '#64748b', lineHeight: 1.6 }}>
{feature.description}
</div>
</div>
</Card>
))}
</div> */}
{/* 精选案例展示区域 */} {/* 精选案例展示区域 */}
<div <div
@@ -182,150 +263,272 @@ const CreativePlazaPage: React.FC = () => {
<span style={{ fontSize: 17, fontWeight: 600, color: '#1f2937' }}></span> <span style={{ fontSize: 17, fontWeight: 600, color: '#1f2937' }}></span>
</div> </div>
{/* <Button <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
type="primary" <Select
value={selectedIndustry}
onChange={(value) => setSelectedIndustry(value)}
style={{ width: 140 }}
options={industryOptions}
/>
<Input.Search
placeholder="搜索行业案例"
allowClear
enterButton={<SearchOutlined />}
size="middle" size="middle"
onClick={handleUnlock} value={searchText}
style={{ onChange={(e) => setSearchText(e.target.value)}
borderRadius: 10, onSearch={(value) => setSearchText(value)}
fontWeight: 500, style={{ width: 240 }}
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', />
border: 'none', </div>
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.28)',
}}
>
<LockOutlined style={{ marginRight: 6 }} />
解锁全部案例
<ArrowRightOutlined style={{ marginLeft: 4, fontSize: 12 }} />
</Button> */}
</div> </div>
{/* 案例网格 */} {/* 统计信息 */}
<div style={{ marginBottom: 16, color: '#64748b', fontSize: 13 }}>
<span style={{ color: '#6366f1', fontWeight: 600 }}>{filteredItems.length}</span>
</div>
{/* 瀑布流展示 */}
<div <div
className="stagger-children" className="stagger-children"
style={{ style={{
display: 'grid', columnCount: 5,
gridTemplateColumns: 'repeat(3, 1fr)', columnGap: 12,
gap: 16,
marginBottom: 16, marginBottom: 16,
position: 'relative',
}} }}
> >
{filteredItems.map((item, index) => (
<div
key={item.id}
onClick={() => handleItemClick(item)}
style={{
breakInside: 'avoid',
marginBottom: 12,
borderRadius: 10,
overflow: 'hidden',
cursor: 'pointer',
position: 'relative',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
animation: `fadeInUp 0.5s ease ${index * 0.1}s both`,
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.15)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.08)';
}}
>
{/* 图片或视频 */}
<div
style={{
position: 'relative',
width: '100%',
paddingBottom: `${(1 / item.aspectRatio) * 100}%`,
backgroundColor: '#f3f4f6',
}}
>
{item.type === 'image' ? (
<img
src={item.url}
alt={item.title}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
) : (
<>
<img
src={item.poster}
alt={item.title}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
inset: 0, top: '50%',
zIndex: 1, left: '50%',
background: 'rgb(255,255,255)', transform: 'translate(-50%, -50%)',
// background: width: 48,
// 'linear-gradient(180deg, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.55) 100%)', height: 48,
borderRadius: '50%',
background: 'rgba(0, 0, 0, 0.6)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexDirection: 'column',
borderRadius: 14,
// boxShadow: '0px 4px 16px rgba(0, 0, 0, 0.08)',
gap: 4,
color: '#fff',
}} }}
> >
<Button <PlayCircleOutlined style={{ fontSize: 32, color: '#fff' }} />
type="primary" </div>
size="middle" </>
onClick={handleUnlock} )}
{/* 类型标签 */}
<div
style={{ style={{
borderRadius: 10, position: 'absolute',
fontWeight: 500, top: 12,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', left: 12,
border: 'none', padding: '4px 8px',
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.28)', borderRadius: 6,
background: 'rgba(0, 0, 0, 0.6)',
backdropFilter: 'blur(4px)',
display: 'flex',
alignItems: 'center',
gap: 4,
}} }}
> >
<LockOutlined style={{ marginRight: 6 }} /> {item.type === 'image' ? (
<PictureOutlined style={{ fontSize: 12, color: '#fff' }} />
<ArrowRightOutlined style={{ marginLeft: 4, fontSize: 12 }} /> ) : (
</Button> <VideoCameraOutlined style={{ fontSize: 12, color: '#fff' }} />
{/* <LockOutlined style={{ fontSize: 22, color: '#94a3b8', cursor: 'pointer' }} )}
onClick={handleUnlock} <span style={{ fontSize: 11, color: '#fff', fontWeight: 500 }}>
/> {item.type === 'image' ? '图片' : '视频'}
<span style={{ fontSize: 16, color: '#1f2937', opacity: 0.9 }}>会员可查看</span> */} </span>
</div>
</div> </div>
{caseList.map((item, index) => ( {/* 底部信息 */}
<div <div
key={index}
className="project-card animate-fadeInUp"
style={{ style={{
position: 'relative', padding: 12,
borderRadius: 14,
overflow: 'hidden',
cursor: 'pointer',
border: '1px solid #e2e8f0',
background: '#fff', background: '#fff',
}} }}
> >
<div <div
style={{ style={{
position: 'relative',
aspectRatio: '16/9',
background:
'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #4c1d95 100%)',
display: 'flex', display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* 模糊视频缩略图占位 */}
<PlayCircleOutlined style={{ fontSize: 36, color: 'rgba(255,255,255,0.6)' }} />
{/* 标签 */}
<div
style={{
position: 'absolute',
top: 10,
left: 10,
padding: '2px 8px',
borderRadius: 6,
background: item.tagColor,
color: '#fff',
fontSize: 11,
fontWeight: 500,
}}
>
{item.tag}
</div>
</div>
<div
style={{
padding: '10px 14px',
background: '#f8fafc',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center',
}} }}
> >
<span <span
style={{ style={{
fontSize: 13, fontSize: 14,
color: '#334155',
fontWeight: 500, fontWeight: 500,
whiteSpace: 'nowrap', color: '#1f2937',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
marginRight: 8,
}} }}
> >
{item.title} {item.title}
</span> </span>
<EyeOutlined style={{ fontSize: 13, color: '#94a3b8' }} /> <span
style={{
fontSize: 11,
color: '#fff',
padding: '2px 8px',
borderRadius: 4,
background: item.tag === '热门' ? '#ef4444' : item.tag === '推荐' ? '#6366f1' : '#f59e0b',
whiteSpace: 'nowrap',
}}
>
{item.tag}
</span>
</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{/* 锁定遮罩 */}
{/* 空状态 */}
{filteredItems.length === 0 && (
<div
style={{
textAlign: 'center',
padding: '60px 20px',
color: '#94a3b8',
}}
>
<SearchOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.5 }} />
<div style={{ fontSize: 15 }}></div>
<div style={{ fontSize: 13, marginTop: 8 }}></div>
<Button
style={{ marginTop: 16, borderRadius: 8 }}
onClick={() => {
setSearchText('');
setSelectedIndustry('all');
}}
>
</Button>
</div>
)}
</div> </div>
{/* 预览弹窗 */}
<Modal
open={previewVisible}
onCancel={() => setPreviewVisible(false)}
footer={[
<Button
key="download"
icon={<DownloadOutlined />}
onClick={() => handleDownload(previewUrl, previewType === 'image' ? 'image.jpg' : 'video.mp4')}
type="primary"
style={{ borderRadius: 8 }}
>
</Button>,
]}
width={800}
centered
styles={{ body: { padding: 0 } }}
>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: 400,
// background: '#1a1a2e',
borderRadius: 8,
overflow: 'hidden',
}}
>
{previewType === 'image' ? (
<img
src={previewUrl}
alt="预览"
style={{
maxWidth: '100%',
maxHeight: '60vh',
objectFit: 'contain',
}}
/>
) : (
<video
src={previewUrl}
controls
autoPlay
style={{
maxWidth: '100%',
maxHeight: '60vh',
objectFit: 'contain',
}}
/>
)}
</div>
</Modal>
</div> </div>
); );
}; };
+67 -114
View File
@@ -18,10 +18,6 @@ import {
App, App,
} from 'antd'; } from 'antd';
import bg1 from '../assets/bg1.png';
import bg2 from '../assets/bg2.png';
import bg3 from '../assets/bg3.png';
import text from '../assets/testb.png';
import UploadSelector from '../components/UploadSelector'; import UploadSelector from '../components/UploadSelector';
import GenerationTaskResourceGrid from '../components/generation/GenerationTaskResourceGrid'; import GenerationTaskResourceGrid from '../components/generation/GenerationTaskResourceGrid';
@@ -313,8 +309,7 @@ const AIChatPage: React.FC = () => {
} }
}, [previewVisible, previewType]); }, [previewVisible, previewType]);
// 提示词展开状态
const [expandedPrompts, setExpandedPrompts] = useState<Set<string>>(new Set());
// 从URL中提取exp时间戳(支持相对路径和完整URL) // 从URL中提取exp时间戳(支持相对路径和完整URL)
const extractExpTimestamp = (url: string): number | null => { const extractExpTimestamp = (url: string): number | null => {
@@ -947,56 +942,6 @@ const AIChatPage: React.FC = () => {
setLastFrame(null); setLastFrame(null);
}; };
// ==================== 事处理函数 ====================
const handleNewChat = () => {
// 生成唯一ID(使用时间戳)
const newId = Date.now().toString();
const newConversation: Conversation = {
id: newId,
title: '新对话',
lastMessage: '',
timestamp: new Date().toLocaleString('zh-CN'),
messages: [],
};
// 将新对话插入到列表顶部
setConversations((prev) => [newConversation, ...prev]);
// 切换到新对话
setCurrentConversationId(newId);
// 清空输入框和已上传媒体
setInputValue('');
setCurrentMedia([]);
setFirstFrame(null);
setLastFrame(null);
// 显示提示消息
antdMessage.info('已开启新对话');
};
const handleDeleteChat = (conversationId: string) => {
// 过滤掉要删除的对话
setConversations((prev) => prev.filter((c) => c.id !== conversationId));
// 如果删除的是当前选中的对话,切换到其他对话
if (currentConversationId === conversationId) {
setCurrentConversationId(
conversations[0]?.id === conversationId
? conversations[1]?.id || null
: conversations[0]?.id || null
);
}
// 显示成功提示
antdMessage.success('对话已删除');
};
const handleSelectChat = (conversationId: string) => {
setCurrentConversationId(conversationId);
setCurrentMedia([]);
setFirstFrame(null);
setLastFrame(null);
};
const handleSend = async () => { const handleSend = async () => {
@@ -1873,12 +1818,60 @@ const AIChatPage: React.FC = () => {
return false; return false;
}; };
// 检测文本是否包含中文字符
const isChineseText = (text: string): boolean => {
return /[\u4e00-\u9fa5]/.test(text);
};
// 统计字数:中文按字符计数,英文按单词计数
const countInput = (text: string): number => {
if (!text.trim()) return 0;
if (isChineseText(text)) {
// 中文:按字符数(不含空白)
return text.replace(/\s/g, '').length;
} else {
// 英文:按单词数
const words = text.trim().split(/\s+/);
return words[0] === '' ? 0 : words.length;
}
};
// 获取最大限制,默认显示500
const getMaxLimit = (text: string): number => {
if (!text.trim()) return 500;
return isChineseText(text) ? 500 : 1000;
};
// 输入框变化处理 // 输入框变化处理
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value; const value = e.target.value;
const maxLimit = getMaxLimit(value);
const count = countInput(value);
if (count <= maxLimit) {
setInputValue(value); setInputValue(value);
const textarea = e.target; checkMention(e.target, value);
checkMention(textarea, value); } else {
// 截断到限制内
if (isChineseText(value)) {
let trimmed = value;
let cjkCount = 0;
for (let i = 0; i < value.length; i++) {
if (/\s/.test(value[i])) continue;
cjkCount++;
if (cjkCount > maxLimit) {
trimmed = value.slice(0, i);
break;
}
}
setInputValue(trimmed);
checkMention(e.target, trimmed);
} else {
const words = value.trim().split(/\s+/);
const limited = words.slice(0, maxLimit).join(' ');
setInputValue(limited);
checkMention(e.target, limited);
}
}
}; };
// 键盘事件处理(ESC 关闭提及、Tab/Enter 选择) // 键盘事件处理(ESC 关闭提及、Tab/Enter 选择)
@@ -2414,60 +2407,7 @@ const AIChatPage: React.FC = () => {
</Popconfirm> </Popconfirm>
</div> </div>
{/* 文本内容 */}
{/* <div
style={{
width: '100%',
position: 'relative',
margin: '8px 0',
padding: '12px 16px',
backgroundColor: 'rgba(139, 92, 246, 0.04)',
borderRadius: 10,
border: '1px solid rgba(139, 92, 246, 0.08)',
fontSize: 13,
color: '#475467',
lineHeight: 1.6,
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 1px 3px rgba(47, 52, 64, 0.035)',
}}
onMouseEnter={() => {
setExpandedPrompts(prev => {
const newSet = new Set(prev);
newSet.add(msg.id);
return newSet;
});
}}
onMouseLeave={() => {
setExpandedPrompts(prev => {
const newSet = new Set(prev);
newSet.delete(msg.id);
return newSet;
});
}}
>
默认显示:一行省略
<div style={{
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: expandedPrompts.has(msg.id) ? 'none' : 'block',
}}>
{msg.originalPrompt}
</div>
鼠标移入显示:完整内容
<div style={{
width: '100%',
maxHeight: 200,
overflowY: 'auto',
display: expandedPrompts.has(msg.id) ? 'block' : 'none',
wordBreak: 'break-word',
}}>
{msg.originalPrompt}
</div>
</div> */}
{/* 根据 status 显示不同内容 */} {/* 根据 status 显示不同内容 */}
{(msg.status === 'generating' || msg.status === 'failed' || msg.status === 'completed') && ( {(msg.status === 'generating' || msg.status === 'failed' || msg.status === 'completed') && (
@@ -3304,6 +3244,19 @@ const AIChatPage: React.FC = () => {
disabled={loading} disabled={loading}
/> />
{/* 字数统计 */}
<div style={{
display: 'flex', justifyContent: 'flex-end',
paddingRight: 10, marginTop: 2,
}}>
<span style={{
fontSize: 11,
color: countInput(inputValue) > getMaxLimit(inputValue) ? '#ef4444' : '#9ca3af',
}}>
{countInput(inputValue)} / {getMaxLimit(inputValue)}
</span>
</div>
{/* @ 提及下拉列表 */} {/* @ 提及下拉列表 */}
{mentionVisible && currentMedia.length > 0 && referenceMode === 'universal' && ( {mentionVisible && currentMedia.length > 0 && referenceMode === 'universal' && (
<div <div
@@ -4736,7 +4689,7 @@ const AIChatPage: React.FC = () => {
width={800} width={800}
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(139, 92, 246, 0.08)' }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(139, 92, 246, 0.08)' }}>
<Button {/* <Button
type="primary" type="primary"
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
onClick={() => { onClick={() => {
@@ -4751,7 +4704,7 @@ const AIChatPage: React.FC = () => {
style={{ borderRadius: 10, background: 'linear-gradient(135deg, #8b5cf6 0%, #ddd6fe 100%)', border: 'none', boxShadow: '0 8px 18px rgba(47, 52, 64, 0.15)' }} style={{ borderRadius: 10, background: 'linear-gradient(135deg, #8b5cf6 0%, #ddd6fe 100%)', border: 'none', boxShadow: '0 8px 18px rgba(47, 52, 64, 0.15)' }}
> >
下载 下载
</Button> </Button> */}
</div> </div>
} }
centered centered
@@ -4769,7 +4722,7 @@ const AIChatPage: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
{attachmentPreviewType === 'image' ? ( {attachmentPreviewType === 'image' ? (
<img <img
src={buildPreviewUrl(attachmentPreviewUrl)} src={buildPreviewUrl(`/static${attachmentPreviewUrl}?w=300&q=50`)}
alt="预览" alt="预览"
style={{ width: '100%', maxHeight: '400px', objectFit: 'contain' }} style={{ width: '100%', maxHeight: '400px', objectFit: 'contain' }}
/> />
+1 -342
View File
@@ -83,82 +83,7 @@ const CaseAssetCard: React.FC<CaseAssetCardProps> = ({ asset, index, onPreview }
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
)} )}
{/* <div
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0,0,0,0.6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.4s ease',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 10,
padding: '24px 36px',
background: 'linear-gradient(135deg, rgba(6,182,212,0.2), rgba(99,102,241,0.2))',
borderRadius: 16,
backdropFilter: 'blur(20px)',
border: '1px solid rgba(6,182,212,0.3)',
boxShadow: isHovered ? '0 0 30px rgba(6,182,212,0.4), 0 0 60px rgba(99,102,241,0.2)' : 'none',
transform: isHovered ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(10px)',
transition: 'transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease',
}}
>
<div
style={{
position: 'relative',
width: 56,
height: 56,
borderRadius: '50%',
background: 'linear-gradient(135deg, #06b6d4, #6366f1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
animation: isHovered ? 'pulse-glow 2s ease-in-out infinite' : 'none',
}}
>
<CopyOutlined style={{ fontSize: 28, color: '#fff' }} />
<div
style={{
position: 'absolute',
inset: '-4px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #06b6d4, #6366f1)',
opacity: 0.3,
animation: isHovered ? 'ripple 2s ease-out infinite' : 'none',
}}
/>
</div>
<span
style={{
fontSize: 14,
color: '#fff',
fontWeight: 600,
letterSpacing: 1,
textShadow: '0 0 10px rgba(6,182,212,0.5)',
}}
>
点击预览
</span>
<style>{`
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(6,182,212,0.5); }
50% { box-shadow: 0 0 40px rgba(99,102,241,0.6), 0 0 60px rgba(6,182,212,0.3); }
}
@keyframes ripple {
0% { transform: scale(1); opacity: 0.6; }
100% { transform: scale(1.8); opacity: 0; }
}
`}</style>
</div>
</div> */}
</div> </div>
</div> </div>
); );
@@ -416,273 +341,7 @@ const HomePage: React.FC = () => {
</p> </p>
</div> </div>
{/* ========== 顶部工作台引导区域(三步流程) ========== */}
{/* <div className="animate-fadeInUp" style={{
padding: '24px 28px',
borderRadius: 16,
background: 'linear-gradient(135deg, #f0f9ff 0%, #faf5ff 50%, #fef3c7 100%)',
border: '1px solid rgba(99,102,241,0.10)',
margin: '0 0 20px',
position: 'relative',
overflow: 'hidden',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 4, height: 18, borderRadius: 2,
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
}} />
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
我的项目<span style={{ color: '#a8a8a8ff', fontSize: 12 }}>(操作步骤)</span>
</div>
</div>
</div>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
}}>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 14,
padding: '20px 18px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.04)',
}}>
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
第1步
</div>
<div style={{
flex: 1,
minHeight: 140,
border: '1px dashed #c7d2fe',
borderRadius: 8,
background: '#fafbff',
padding: 10,
display: 'flex',
flexDirection: 'column',
gap: 8,
marginBottom: 12,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 8, height: 8, borderRadius: 2, background: '#6366f1' }} />
<div style={{ flex: 1, height: 22, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4, display: 'flex', alignItems: 'center', padding: '0 8px', fontSize: 10, color: '#94a3b8' }}>
项目名称...
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<div style={{ padding: '2px 8px', background: '#eef2ff', color: '#6366f1', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #c7d2fe' }}>美妆</div>
<div style={{ padding: '2px 8px', background: '#fff7ed', color: '#f97316', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #fed7aa' }}>美食</div>
<div style={{ padding: '2px 8px', background: '#ecfdf5', color: '#10b981', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #a7f3d0' }}>3C数码</div>
<div style={{ padding: '2px 8px', background: '#f5f3ff', color: '#8b5cf6', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #ddd6fe' }}>服饰</div>
</div>
<div style={{ height: 16, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
<div style={{ height: 16, width: '70%', background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
</div>
<div style={{
background: '#f1f5f9',
borderRadius: 10,
padding: '14px 12px',
textAlign: 'center',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
}}>
创建新项目,自定义项目名称并选择对应行业分类
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: 22 }}>
<ArrowRightOutlined style={{ transform: 'rotate(0deg)' }} />
</div>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 14,
padding: '20px 18px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.04)',
}}>
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
第2步
</div>
<div style={{
flex: 1,
minHeight: 140,
border: '1px solid #e2e8f0',
borderRadius: 8,
background: '#fafafa',
padding: 10,
display: 'flex',
flexDirection: 'column',
gap: 8,
marginBottom: 12,
}}>
<div style={{ display: 'flex', background: '#f1f5f9', borderRadius: 6, padding: 2, gap: 2 }}>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px 0', background: '#fff', borderRadius: 4, fontSize: 11, fontWeight: 600, color: '#6366f1', boxShadow: '0 1px 3px rgba(99,102,241,0.15)' }}>
<VideoCameraOutlined style={{ fontSize: 11 }} />视频
</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px 0', fontSize: 11, color: '#94a3b8' }}>
<PictureOutlined style={{ fontSize: 11 }} /> 图片
</div>
</div>
<div>
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>尺寸比例</div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#eef2ff', color: '#6366f1', border: '1px solid #c7d2fe', borderRadius: 4, fontSize: 10, fontWeight: 600 }}>9:16</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>16:9</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>1:1</div>
</div>
</div>
<div>
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>时长</div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>5s</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff7ed', color: '#f97316', border: '1px solid #fed7aa', borderRadius: 4, fontSize: 10, fontWeight: 600 }}>10s</div>
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>15s</div>
</div>
</div>
</div>
<div style={{
background: '#f1f5f9',
borderRadius: 10,
padding: '14px 12px',
textAlign: 'center',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
}}>
选择生成图片或视频,设置尺寸、时长等参数,点击生成即可一键优化提示词
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: 22 }}>
<ArrowRightOutlined />
</div>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 14,
padding: '20px 18px',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 8px rgba(15, 23, 42, 0.04)',
}}>
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
第3步
</div>
<div style={{
flex: 1,
minHeight: 140,
border: '1px solid #e2e8f0',
borderRadius: 8,
background: '#fafafa',
padding: 10,
display: 'flex',
flexDirection: 'column',
gap: 6,
marginBottom: 12,
justifyContent: 'space-between',
}}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 5 }}>
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#10b981', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>✓</div>
<div style={{ fontSize: 10, color: '#065f46', fontWeight: 500 }}>提示词已智能优化</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 5 }}>
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#10b981', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>✓</div>
<div style={{ fontSize: 10, color: '#065f46', fontWeight: 500 }}>尺寸 9:16 · 时长 5s</div>
</div>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
padding: '6px 0',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
color: '#fff',
borderRadius: 5,
fontSize: 11,
fontWeight: 600,
boxShadow: '0 2px 6px rgba(99,102,241,0.25)',
}}>
<ThunderboltOutlined style={{ fontSize: 11 }} />
确认并生成素材
</div>
</div>
<div style={{
background: '#f1f5f9',
borderRadius: 10,
padding: '14px 12px',
textAlign: 'center',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
}}>
核对图片 / 视频参数与优化后的提示词,确认后一键生成素材
</div>
</div>
<div
onClick={() => navigate('/projects')}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = '0 14px 32px rgba(99,102,241,0.45)';
const arrow = e.currentTarget.querySelector('.round-arrow') as HTMLElement | null;
if (arrow) arrow.style.transform = 'translateX(3px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = '0 8px 20px rgba(99,102,241,0.30)';
const arrow = e.currentTarget.querySelector('.round-arrow') as HTMLElement | null;
if (arrow) arrow.style.transform = 'translateX(0)';
}}
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
width: 64,
height: 64,
borderRadius: '50%',
textAlign: 'center',
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
color: '#fff',
fontSize: 14,
fontWeight: 600,
letterSpacing: 0.5,
boxShadow: '0 8px 20px rgba(99,102,241,0.30)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
userSelect: 'none',
position: 'relative',
}}
>
<span style={{
position: 'absolute',
inset: 0,
borderRadius: '50%',
background: 'radial-gradient(circle at 30% 25%, rgba(255,255,255,0.35) 0%, transparent 55%)',
pointerEvents: 'none',
}} />
前往
<ArrowRightOutlined
className="round-arrow"
style={{ fontSize: 13, transition: 'transform 0.3s ease' }}
/>
</div>
</div>
</div> */}
{/* ========== AI 创作入口区域 ========== */} {/* ========== AI 创作入口区域 ========== */}
<div className="animate-fadeInUp" style={{ <div className="animate-fadeInUp" style={{