This commit is contained in:
zhutao
2025-12-12 18:00:15 +08:00
commit 05f2882e93
28 changed files with 5662 additions and 0 deletions

2
.env.development Normal file
View File

@@ -0,0 +1,2 @@
VITE_APPID=wxbc438492e3efab70 #appid
VITE_WEB_URL=https://ting.lifebanktech.com

2
.env.production Normal file
View File

@@ -0,0 +1,2 @@
VITE_APPID=wxbc438492e3efab70 #appid
VITE_WEB_URL=https://ting.lifebanktech.com

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
*dist
*xiaoling_dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# H5活动落地页设计
This is a code bundle for H5活动落地页设计. The original project is available at https://www.figma.com/design/ony5Rjg3GoqH4QHmTSRILU/H5%E6%B4%BB%E5%8A%A8%E8%90%BD%E5%9C%B0%E9%A1%B5%E8%AE%BE%E8%AE%A1.
## Running the code
Run `npm i` to install the dependencies.
Run `npm run dev` to start the development server.

19
index.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>效灵AI</title>
</head>
<body>
<div id="root"></div>
<script src="/src/main.tsx" type="module"></script>
<!--<script src="https://cdn.bootcdn.net/ajax/libs/vConsole/3.15.1/vconsole.min.js"></script>-->
<!--<script>-->
<!-- new VConsole();-->
<!--</script>-->
</body>
</html>

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "ai",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.13.2",
"immer": "^11.0.1",
"lucide-react": "^0.487.0",
"motion": "^12.23.26",
"qs": "^6.14.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.6.0",
"weixin-js-sdk": "^1.6.5",
"zustand": "^5.0.9"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^3.10.2",
"vite": "6.3.5"
},
"scripts": {
"dev": "vite",
"build": "vite build"
}
}

1268
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

84
src/App.tsx Normal file
View File

@@ -0,0 +1,84 @@
import {useEffect, useState} from 'react';
import {Screen1Hook} from './components/Screen1Hook';
import {Screen2Upload} from './components/Screen2Upload';
import {Screen3Analysis} from './components/Screen3Analysis';
import {Screen4Payment} from './components/Screen4Payment';
import {Screen5Report} from './components/Screen5Report';
import wxLogin from "@/wx/wxLogin";
import toast, {Toaster} from "react-hot-toast";
import wxShare from "@/wx/wxShare";
import {enterpriseAnalyzeApi} from "@/api/service";
import {useUserStore} from "@/store/user-store";
export default function App() {
const userStore = useUserStore()
//初始化
const [init, setInit] = useState(false)
const [currentScreen, setCurrentScreen] = useState(1);
/**
* 步骤往下
*/
const handleNextScreen = () => {
setCurrentScreen(prev => Math.min(prev + 1, 5));
};
/**
* 上传文件
*/
const handleUpload = async (file) => {
handleNextScreen()
let res = await enterpriseAnalyzeApi({
analys_image: file,
analys_type: null,
}) as any
if (res.analysis_result.analyze_ret != "success") {
toast.error("请重新上传结构清晰的组织架构图")
setCurrentScreen(prev => prev = 2)
return
}
userStore.setAnalysis(res)
handleNextScreen()
};
const handlePayment = () => {
handleNextScreen();
};
/**
* 授权
*/
useEffect(() => {
wxLogin().then(() => {
setInit(true)
wxShare().then()
})
}, []);
if (!init) {
return <></>
}
return (
<>
<Toaster position="top-center"/>
<div className="min-h-screen bg-[#0A0F24] text-white overflow-x-hidden relative">
{/* 背景色 */}
<div className="fixed inset-0 pointer-events-none">
<div
className="absolute inset-0 bg-gradient-to-b from-[#7B61FF]/10 via-transparent to-[#00F0FF]/10"/>
</div>
{/* Main content */}
<div className="relative z-10">
{currentScreen === 1 && <Screen1Hook onNext={handleNextScreen}/>}
{currentScreen === 2 && <Screen2Upload onSuccess={handleUpload}/>}
{currentScreen === 3 && <Screen3Analysis/>}
{currentScreen === 4 && <Screen4Payment onPayment={handlePayment}/>}
{currentScreen === 5 && <Screen5Report/>}
</div>
</div>
</>
);
}

39
src/api/service.ts Normal file
View File

@@ -0,0 +1,39 @@
import request from "@/utils/http/request";
type EnterpriseAnalyzeApi = {
analys_image: File,
analys_type: string,
}
/**
* 企业架构图分析
*/
export function enterpriseAnalyzeApi(data: EnterpriseAnalyzeApi) {
let formData = new FormData()
formData.append("analys_image", data.analys_image)
formData.append("analys_type", data.analys_type)
return request.post("/enterprise/analyze", formData);
}
type SubmitContactInfoApi = {
record_id: string | number,
contact_name: string,
contact_phone: string,
enterprise_name: string
}
/**
* 提交联系方式
*/
export function submitContactInfoApi(data: SubmitContactInfoApi) {
return request.post("/enterprise/submit_contact_info", data)
}
/**
* 创建分析订单
*/
export function createAnalyzeOrderApi(data:{record_id:number | string}) {
return request.post("/analysis_order/create",data)
}

25
src/api/wxchat.ts Normal file
View File

@@ -0,0 +1,25 @@
import request from "@/utils/http/request";
/**
* 微信授权
* @param data
* @constructor
*/
export function Login(data: any): Promise<any> {
return request.post('/login', data)
}
/**
* 获取jssdk
* @param params
*/
export function getJsSdk(params: any): Promise<any> {
return request.get('/get_jssdk', params)
}
/**
* 获取微信分享文案配置
*/
export function getShareConfig() {
return request.get("/get_share_config")
}

View File

@@ -0,0 +1,281 @@
import {motion} from 'motion/react';
import {useEffect, useState} from 'react';
import {Brain, Cpu, Database, Sparkles, TrendingDown, Zap} from 'lucide-react';
interface Screen1HookProps {
onNext: () => void;
}
export function Screen1Hook({onNext}: Screen1HookProps) {
const [count, setCount] = useState(1000);
const [typedText, setTypedText] = useState('');
const fullText = '你的企业,正在为「低效」支付多少冤枉钱?';
useEffect(() => {
// Typewriter effect
let index = 0;
const timer = setInterval(() => {
if (index <= fullText.length) {
setTypedText(fullText.slice(0, index));
index++;
} else {
clearInterval(timer);
}
}, 80);
return () => clearInterval(timer);
}, []);
useEffect(() => {
// Counter animation
const timer = setInterval(() => {
setCount(prev => prev + Math.floor(Math.random() * 3));
}, 2000);
return () => clearInterval(timer);
}, []);
return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12 relative overflow-hidden">
{/* Animated background elements */}
<motion.div
className="absolute top-1/4 left-1/4 w-64 h-64 bg-[#7B61FF]/20 rounded-full blur-3xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute bottom-1/4 right-1/4 w-64 h-64 bg-[#00F0FF]/20 rounded-full blur-3xl"
animate={{
scale: [1.2, 1, 1.2],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* AI Brain with orbiting icons */}
<motion.div
className="mb-12 relative"
initial={{opacity: 0, scale: 0.5}}
animate={{opacity: 1, scale: 1}}
transition={{duration: 1, type: 'spring'}}>
{/* Central AI Brain with breathing effect */}
<motion.div
className="relative w-32 h-32 flex items-center justify-center"
animate={{
scale: [1, 1.1, 1],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
>
{/* Glow effect */}
<motion.div
className="absolute inset-0 bg-gradient-to-br from-[#7B61FF] to-[#00F0FF] rounded-full blur-2xl"
animate={{
opacity: [0.3, 0.8, 0.3],
scale: [0.8, 1.2, 0.8],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* Brain icon */}
<div
className="relative z-10 w-24 h-24 bg-gradient-to-br from-[#7B61FF] to-[#00F0FF] rounded-full flex items-center justify-center">
<Brain className="w-14 h-14 text-white"/>
</div>
{/* Orbiting icons */}
{[
{Icon: Sparkles, delay: 0, color: '#00F0FF'},
{Icon: Cpu, delay: 1, color: '#7B61FF'},
{Icon: Database, delay: 2, color: '#00F0FF'},
{Icon: Zap, delay: 3, color: '#7B61FF'},
].map((item, index) => (
<motion.div
key={index}
className="absolute top-1/2 left-1/2 w-10 h-10 -ml-5 -mt-5"
animate={{
rotate: [0, 360],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'linear',
delay: item.delay * 2,
}}
>
<motion.div
className="absolute"
style={{
transform: `translateX(70px) rotate(-${index * 90}deg)`,
}}
animate={{
rotate: [0, -360],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: 'linear',
delay: item.delay * 2,
}}
>
<motion.div
className="w-10 h-10 bg-[#0A0F24] border-2 rounded-full flex items-center justify-center"
style={{borderColor: item.color}}
animate={{
boxShadow: [
`0 0 10px ${item.color}40`,
`0 0 20px ${item.color}80`,
`0 0 10px ${item.color}40`,
],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeInOut',
}}
>
<item.Icon className="w-5 h-5" style={{color: item.color}}/>
</motion.div>
</motion.div>
</motion.div>
))}
</motion.div>
{/* Particle effects */}
{Array.from({length: 8}).map((_, i) => (
<motion.div
key={i}
className="absolute w-1 h-1 bg-[#00F0FF] rounded-full"
style={{
left: '50%',
top: '50%',
}}
animate={{
x: [0, Math.cos(i * 45 * Math.PI / 180) * 100],
y: [0, Math.sin(i * 45 * Math.PI / 180) * 100],
opacity: [0, 1, 0],
scale: [0, 1, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
delay: i * 0.2,
ease: 'easeOut',
}}
/>
))}
</motion.div>
{/* Main title with typewriter effect */}
<motion.h1
className="text-3xl md:text-4xl text-center mb-6 min-h-[6rem] px-4"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 1.5}}
>
{typedText}
<motion.span
className="inline-block w-1 h-8 bg-[#00F0FF] ml-1 align-middle"
animate={{opacity: [1, 0, 1]}}
transition={{duration: 0.8, repeat: Infinity}}
/>
</motion.h1>
{/* Subtitle */}
<motion.p
className="text-center text-gray-300 mb-8 px-4 max-w-xl"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 2.5}}
>
AI时代1AI的员工 &gt; 5<br/>AI一键测算你的
<span className="text-[#00F0FF]"></span>
</motion.p>
{/* Counter */}
<motion.div
className="flex items-center gap-2 mb-12 bg-white/5 backdrop-blur-sm border border-[#00F0FF]/30 rounded-full px-6 py-3"
initial={{opacity: 0, scale: 0.8}}
animate={{opacity: 1, scale: 1}}
transition={{delay: 3}}
>
<Zap className="w-5 h-5 text-[#00F0FF]"/>
<span className="text-gray-300"></span>
<motion.span
className="text-[#00F0FF] text-xl"
key={count}
initial={{y: -10, opacity: 0}}
animate={{y: 0, opacity: 1}}
>
{count.toLocaleString()}
</motion.span>
<span className="text-gray-300"></span>
</motion.div>
{/* CTA Button */}
<motion.button
className="relative px-12 py-4 bg-gradient-to-r from-[#7B61FF] to-[#00F0FF] rounded-full overflow-hidden group"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 3.5}}
onClick={onNext}
>
{/* Flowing light effect */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent"
animate={{
x: ['-100%', '200%'],
}}
transition={{
duration: 2,
repeat: Infinity,
repeatDelay: 1,
}}
/>
<span className="relative z-10 flex items-center gap-2">
<TrendingDown className="w-5 h-5"/>
</span>
</motion.button>
{/* Trust indicators */}
<motion.div
className="mt-12 flex gap-6 text-xs text-gray-400"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 4}}
>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-400 rounded-full"/>
<span></span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-400 rounded-full"/>
<span>3</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-400 rounded-full"/>
<span></span>
</div>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,205 @@
import {motion} from 'motion/react';
import {type ChangeEvent, useState} from 'react';
import {FileImage, Shield, Upload} from 'lucide-react';
interface Screen2UploadProps {
onSuccess: (file:File) => void;
}
const templates = [
{id: 'ecommerce', name: '电商型', icon: '🛒'},
{id: 'traditional', name: '传统型', icon: '🏢'},
{id: 'tech', name: '科技型', icon: '💻'},
];
export function Screen2Upload(props: Screen2UploadProps) {
// const userStore = useUserStore()
const [scanning, setScanning] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const handleFileSelect = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
props.onSuccess(file)
}
};
// const handleTemplateSelect = (templateId: string) => {
// if (scanning) return;
// setSelectedTemplate(templateId);
// };
return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12 relative">
{/* Title */}
<motion.h2
className="text-2xl md:text-3xl text-center mb-4"
initial={{opacity: 0, y: -20}}
animate={{opacity: 1, y: 0}}
>
</motion.h2>
<motion.p
className="text-gray-400 text-center mb-12"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 0.2}}
>
AI将智能分析各岗位的优化潜力
</motion.p>
{/* Upload area */}
<motion.div
className={`relative w-full max-w-md h-72 border-2 border-dashed rounded-2xl transition-all duration-300
border-[#7B61FF]/50 bg-white/5
${scanning ? 'border-[#00F0FF] bg-[#00F0FF]/5 shadow-[0_0_30px_rgba(0,240,255,0.3)]' : ''}`}
initial={{opacity: 0, scale: 0.9}}
animate={{
opacity: 1,
scale: scanning ? 1.02 : 1,
}}
transition={{delay: 0.3}}>
{scanning ? (
// Scanning animation
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3">
{/* Pulsing background */}
<motion.div
className="absolute inset-0 bg-[#00F0FF]/10 rounded-2xl"
animate={{
opacity: [0.1, 0.3, 0.1],
}}
transition={{
duration: 1,
repeat: Infinity,
}}
/>
{/* Scanning line */}
<motion.div
className="absolute inset-x-0 h-1 bg-gradient-to-r from-transparent via-[#00F0FF] to-transparent shadow-[0_0_10px_rgba(0,240,255,0.8)]"
animate={{
top: ['0%', '100%'],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'linear',
}}
/>
{/* Content */}
<motion.div
initial={{scale: 0.8, opacity: 0}}
animate={{scale: 1, opacity: 1}}
transition={{duration: 0.3}}
className="relative z-10 flex flex-col items-center">
<FileImage className="w-16 h-16 text-[#00F0FF] mb-4"/>
<p className="text-[#00F0FF] text-lg">...</p>
{/* Selected template info */}
{selectedTemplate && (
<motion.div
initial={{opacity: 0, y: 10}}
animate={{opacity: 1, y: 0}}
className="mt-4 flex items-center gap-2 bg-white/10 px-4 py-2 rounded-lg"
>
<span
className="text-2xl">{templates.find(t => t.id === selectedTemplate)?.icon}</span>
<span
className="text-sm text-white">{templates.find(t => t.id === selectedTemplate)?.name}</span>
</motion.div>
)}
</motion.div>
</div>
) : (
// Upload interface
<div className="absolute inset-0 flex flex-col items-center justify-center p-6">
<Upload className="w-12 h-12 text-[#7B61FF] mb-4"/>
<p className="text-gray-300 text-center mb-2"></p>
<p className="text-gray-500 text-sm text-center mb-6"></p>
<div className="flex ">
<label className="cursor-pointer">
<input
type="file"
accept="image/*"
className="hidden"
onChange={handleFileSelect}
/>
<div
className="flex items-center gap-2 px-4 py-2 bg-[#7B61FF]/20 border border-[#7B61FF] rounded-lg hover:bg-[#7B61FF]/30 transition-colors">
<FileImage className="w-4 h-4"/>
<span className="text-sm"></span>
</div>
</label>
</div>
</div>
)}
{/* Corner decorations */}
<div
className="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-[#00F0FF]/50 rounded-tl-2xl"/>
<div
className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-[#00F0FF]/50 rounded-tr-2xl"/>
<div
className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-[#00F0FF]/50 rounded-bl-2xl"/>
<div
className="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-[#00F0FF]/50 rounded-br-2xl"/>
</motion.div>
{/* Divider */}
{/*<motion.div*/}
{/* className="flex items-center gap-4 my-8 w-full max-w-md"*/}
{/* initial={{opacity: 0}}*/}
{/* animate={{opacity: 1}}*/}
{/* transition={{delay: 0.5}}*/}
{/*>*/}
{/* <div className="flex-1 h-px bg-gradient-to-r from-transparent to-gray-600"/>*/}
{/* <span className="text-gray-500 text-sm">选择分析类型</span>*/}
{/* <div className="flex-1 h-px bg-gradient-to-l from-transparent to-gray-600"/>*/}
{/*</motion.div>*/}
{/* Templates */}
{/*<motion.div*/}
{/* className="flex gap-4 mb-12"*/}
{/* initial={{opacity: 0, y: 20}}*/}
{/* animate={{opacity: 1, y: 0}}*/}
{/* transition={{delay: 0.6}}*/}
{/*>*/}
{/* {templates.map((template, index) => (*/}
{/* <motion.button*/}
{/* key={template.id}*/}
{/* className={`flex flex-col items-center gap-2 px-6 py-4 rounded-xl border transition-all ${*/}
{/* selectedTemplate === template.id*/}
{/* ? 'border-[#00F0FF] bg-[#00F0FF]/10'*/}
{/* : 'border-gray-600 bg-white/5 hover:border-[#7B61FF]'*/}
{/* }`}*/}
{/* initial={{opacity: 0, y: 20}}*/}
{/* animate={{opacity: 1, y: 0}}*/}
{/* transition={{delay: 0.7 + index * 0.1}}*/}
{/* onClick={() => handleTemplateSelect(template.id)}*/}
{/* whileHover={{scale: 1.05}}*/}
{/* whileTap={{scale: 0.95}}*/}
{/* >*/}
{/* <span className="text-3xl">{template.icon}</span>*/}
{/* <span className="text-sm">{template.name}</span>*/}
{/* </motion.button>*/}
{/* ))}*/}
{/*</motion.div>*/}
{/* Privacy notice */}
<motion.div
className="flex items-center gap-2 text-xs text-gray-500 bg-white/5 backdrop-blur-sm px-4 py-2 rounded-full mt-12"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 0.9}}
>
<Shield className="w-4 h-4 text-green-400"/>
<span></span>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,253 @@
import {motion} from 'motion/react';
import {useEffect, useState} from 'react';
import {Brain, Cpu, TrendingUp, Zap} from 'lucide-react';
const analysisSteps = [
{ dept: '组织架构', message: '发现重复劳动节点...', icon: '🎨' },
{ dept: 'AI替代方案', message: 'AI替代率 85%...', icon: '✍️' },
{ dept: 'AI优化方案', message: '智能接入可节省 70%...', icon: '💬' },
{ dept: 'AI优化空间', message: '数据分析优化空间 60%...', icon: '📊' },
{ dept: 'AI部署方案', message: '自动化流程提升 75%...', icon: '⚙️' },
];
export function Screen3Analysis() {
const [currentStep, setCurrentStep] = useState(0);
const [nodes, setNodes] = useState<Array<{ x: number; y: number; id: number }>>([]);
useEffect(() => {
// Generate random nodes
const newNodes = Array.from({ length: 15 }, (_, i) => ({
x: Math.random() * 100,
y: Math.random() * 100,
id: i,
}));
setNodes(newNodes);
}, []);
useEffect(() => {
// Progress through analysis steps
const timer = setInterval(() => {
setCurrentStep((prev) => {
if (prev < analysisSteps.length - 1) {
return prev + 1;
} else {
clearInterval(timer);
return prev;
}
});
}, 800);
return () => clearInterval(timer);
}, []);
return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12 relative overflow-hidden">
{/* Radar scanning effect */}
<div className="absolute inset-0 flex items-center justify-center">
<motion.div
className="absolute w-96 h-96 border-2 border-[#00F0FF]/30 rounded-full"
animate={{
scale: [1, 1.5, 1],
opacity: [0.5, 0, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeOut',
}}
/>
<motion.div
className="absolute w-96 h-96 border-2 border-[#7B61FF]/30 rounded-full"
animate={{
scale: [1, 1.5, 1],
opacity: [0.5, 0, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeOut',
delay: 0.5,
}}
/>
{/* Rotating radar beam */}
<motion.div
className="absolute w-96 h-1 bg-gradient-to-r from-[#00F0FF]/0 via-[#00F0FF]/80 to-[#00F0FF]/0 origin-left"
style={{ left: '50%', top: '50%' }}
animate={{
rotate: [0, 360],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: 'linear',
}}
/>
</div>
{/* Node visualization */}
<div className="absolute inset-0 pointer-events-none">
{nodes.map((node) => (
<motion.div
key={node.id}
className="absolute w-3 h-3 bg-[#00F0FF] rounded-full"
style={{
left: `${node.x}%`,
top: `${node.y}%`,
}}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: [0, 1, 0.8],
opacity: [0, 1, 0.6],
}}
transition={{
duration: 1,
delay: node.id * 0.1,
}}
>
{/* Pulse effect */}
<motion.div
className="absolute inset-0 bg-[#00F0FF] rounded-full"
animate={{
scale: [1, 2, 1],
opacity: [0.8, 0, 0.8],
}}
transition={{
duration: 2,
repeat: Infinity,
delay: node.id * 0.1,
}}
/>
</motion.div>
))}
</div>
{/* Central AI brain */}
<motion.div
className="relative z-10 mb-12"
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ duration: 1, type: 'spring' }}
>
<div className="relative">
<motion.div
className="w-24 h-24 bg-gradient-to-br from-[#7B61FF] to-[#00F0FF] rounded-full flex items-center justify-center"
animate={{
boxShadow: [
'0 0 20px rgba(123, 97, 255, 0.5)',
'0 0 40px rgba(0, 240, 255, 0.8)',
'0 0 20px rgba(123, 97, 255, 0.5)',
],
}}
transition={{
duration: 2,
repeat: Infinity,
}}
>
<Brain className="w-12 h-12 text-white" />
</motion.div>
{/* Orbiting icons */}
{[Cpu, Zap, TrendingUp].map((Icon, index) => (
<motion.div
key={index}
className="absolute top-1/2 left-1/2 w-8 h-8 -ml-4 -mt-4"
animate={{
rotate: [0, 360],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: 'linear',
delay: index * 0.6,
}}
>
<motion.div
className="absolute"
style={{
transform: `translateX(60px) rotate(-${index * 120}deg)`,
}}
animate={{
rotate: [0, -360],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: 'linear',
delay: index * 0.6,
}}
>
<div className="w-8 h-8 bg-[#0A0F24] border-2 border-[#00F0FF] rounded-full flex items-center justify-center">
<Icon className="w-4 h-4 text-[#00F0FF]" />
</div>
</motion.div>
</motion.div>
))}
</div>
</motion.div>
{/* Analysis progress */}
<div className="relative z-10 w-full max-w-md space-y-4">
{analysisSteps.map((step, index) => (
<motion.div
key={index}
className={`flex items-center gap-4 p-4 rounded-xl backdrop-blur-sm transition-all ${
index === currentStep
? 'bg-[#00F0FF]/20 border-2 border-[#00F0FF]'
: index < currentStep
? 'bg-white/5 border border-gray-600'
: 'bg-white/5 border border-gray-800 opacity-30'
}`}
initial={{ opacity: 0, x: -50 }}
animate={{
opacity: index <= currentStep ? 1 : 0.3,
x: 0,
}}
transition={{ delay: index * 0.1 }}
>
<span className="text-2xl">{step.icon}</span>
<div className="flex-1">
<div className="text-sm text-gray-300">{step.dept}</div>
<div className="text-xs text-[#00F0FF] mt-1">{step.message}</div>
</div>
{index === currentStep && (
<motion.div
className="w-6 h-6 border-2 border-[#00F0FF] border-t-transparent rounded-full"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
/>
)}
{index < currentStep && (
<div className="w-6 h-6 bg-[#00F0FF] rounded-full flex items-center justify-center">
<svg className="w-4 h-4 text-[#0A0F24]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
)}
</motion.div>
))}
</div>
{/* Status text */}
<motion.div
className="mt-12 text-center"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 2, repeat: Infinity }}
>
<p className="text-[#00F0FF] text-sm">AI深度分析中...</p>
</motion.div>
{/* Progress bar */}
<motion.div className="mt-6 w-64 h-1 bg-gray-800 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-[#7B61FF] to-[#00F0FF]"
initial={{ width: '0%' }}
animate={{ width: `${((currentStep + 1) / analysisSteps.length) * 100}%` }}
transition={{ duration: 0.5 }}
/>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,285 @@
import {motion} from 'motion/react';
import {useEffect, useState} from 'react';
import {AlertCircle, Check, Lock, Sparkles, TrendingDown, Users} from 'lucide-react';
import {createAnalyzeOrderApi} from "@/api/service";
import {wxPay} from "@/wx/wxPay";
import {useUserStore} from "@/store/user-store";
import toast from "react-hot-toast";
interface Screen4PaymentProps {
onPayment: () => void;
}
const testimonials = [
'某杭州电商公司使用后设计部成本降低60%',
'某深圳外贸公司使用后客服响应速度提升5倍',
'某上海科技公司使用后年度成本节省120万',
];
export function Screen4Payment({onPayment}: Screen4PaymentProps) {
const userStore = useUserStore()
const [countdown, setCountdown] = useState(599); // 9:59
const [currentTestimonial, setCurrentTestimonial] = useState(0);
//防抖
let debounce = true
/**
* 创建订单,吊起微信支付
*/
const onCreateOrder = async () => {
if (!debounce) return
debounce = false
let res = await createAnalyzeOrderApi({
record_id: userStore.analysis.record_id
});
wxPay(res).then((state) => {
debounce = true
if (state) {
onPayment()
} else {
toast.error("支付失败")
}
})
}
useEffect(() => {
const timer = setInterval(() => {
setCountdown((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const timer = setInterval(() => {
setCurrentTestimonial((prev) => (prev + 1) % testimonials.length);
}, 3000);
return () => clearInterval(timer);
}, []);
const minutes = Math.floor(countdown / 60);
const seconds = countdown % 60;
return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12 relative">
{/* Blurred background report preview */}
<div className="absolute inset-0 opacity-30 blur-xl pointer-events-none">
<div
className="absolute top-1/4 left-1/2 -translate-x-1/2 w-80 h-96 bg-gradient-to-br from-red-500 to-gray-800 rounded-2xl p-6">
<div className="space-y-4">
<div className="h-8 bg-white/20 rounded"/>
<div className="h-32 bg-white/20 rounded"/>
<div className="h-8 bg-white/20 rounded w-2/3"/>
</div>
</div>
</div>
{/* Main content */}
<motion.div
className="relative z-10 w-full max-w-md"
initial={{opacity: 0, y: 30}}
animate={{opacity: 1, y: 0}}
>
{/* Alert header */}
<motion.div
className="flex items-center justify-center gap-2 mb-6"
initial={{scale: 0}}
animate={{scale: 1}}
transition={{type: 'spring', delay: 0.2}}
>
<AlertCircle className="w-8 h-8 text-[#00F0FF]"/>
<span className="text-xl"></span>
</motion.div>
{/* Blurred teaser */}
<motion.div
className="bg-white/5 backdrop-blur-md border border-[#7B61FF]/30 rounded-2xl p-6 mb-6 relative overflow-hidden"
initial={{opacity: 0, scale: 0.9}}
animate={{opacity: 1, scale: 1}}
transition={{delay: 0.3}}
>
{/* Blur overlay with preview data */}
<div
className="absolute inset-0 backdrop-blur-sm bg-white/5 z-10 flex flex-col items-center justify-center gap-4">
<Lock className="w-16 h-16 text-[#00F0FF]/50"/>
<div className="text-center px-4">
<motion.div
className="mb-2"
animate={{opacity: [0.7, 1, 0.7]}}
transition={{duration: 2, repeat: Infinity}}
>
<p className="text-sm text-gray-400 mb-1"></p>
<div
className="text-3xl bg-gradient-to-r from-[#00F0FF] to-[#7B61FF] bg-clip-text text-transparent">
¥ {userStore.analysis.analysis_result.analyze_data.annual_savings_cost}
</div>
</motion.div>
<motion.div
animate={{opacity: [0.7, 1, 0.7]}}
transition={{duration: 2, repeat: Infinity, delay: 0.5}}
>
<p className="text-sm text-gray-400 mb-1"></p>
<div className="text-2xl text-[#00F0FF]">
{userStore.analysis.analysis_result.analyze_data.efficiency_improvement} %
</div>
</motion.div>
</div>
</div>
<div className="space-y-4 blur-sm select-none">
<div className="text-center">
<p className="text-gray-400 text-sm mb-2">使AI</p>
<p className="text-2xl text-red-400 mb-1"></p>
<div
className="text-5xl bg-gradient-to-r from-red-400 to-orange-400 bg-clip-text text-transparent">
¥???{','}???
</div>
</div>
<div className="h-px bg-gradient-to-r from-transparent via-gray-600 to-transparent"/>
<div className="text-center">
<p className="text-gray-400 text-sm mb-2"></p>
<div
className="text-4xl bg-gradient-to-r from-[#00F0FF] to-[#7B61FF] bg-clip-text text-transparent">
¥???{','}???
</div>
</div>
</div>
</motion.div>
{/* Unlock section */}
<motion.div
className="bg-gradient-to-br from-[#7B61FF]/20 to-[#00F0FF]/20 backdrop-blur-md border-2 border-[#00F0FF] rounded-2xl p-6 mb-6"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 0.5}}
>
<div className="flex items-center justify-center gap-2 mb-4">
<Sparkles className="w-6 h-6 text-[#00F0FF]"/>
<h3 className="text-xl"></h3>
</div>
<div className="space-y-3 mb-6">
{[
'各部门AI替代率精准评估',
'具体的降本金额明细',
'推荐的AI工具组合清单',
].map((item, index) => (
<motion.div
key={index}
className="flex items-center gap-3 text-sm text-gray-300"
initial={{opacity: 0, x: -20}}
animate={{opacity: 1, x: 0}}
transition={{delay: 0.6 + index * 0.1}}
>
<div
className="w-5 h-5 bg-[#00F0FF]/20 rounded-full flex items-center justify-center flex-shrink-0">
<Check className="w-3 h-3 text-[#00F0FF]"/>
</div>
<span>{item}</span>
</motion.div>
))}
</div>
{/* Price */}
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-3 mb-2">
<span className="text-gray-500 line-through text-lg">¥999</span>
<span className="bg-red-500 text-white text-xs px-2 py-1 rounded"></span>
</div>
<div className="flex items-baseline justify-center gap-1">
<span className="text-4xl">¥</span>
<span
className="text-6xl bg-gradient-to-r from-[#00F0FF] to-[#7B61FF] bg-clip-text text-transparent">
9.9
</span>
</div>
</div>
{/* Payment button */}
<motion.button
className="w-full py-4 bg-gradient-to-r from-[#7B61FF] to-[#00F0FF] rounded-xl relative overflow-hidden group"
whileHover={{scale: 1.02}}
whileTap={{scale: 0.98}}
onClick={onCreateOrder}
>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent"
animate={{
x: ['-100%', '200%'],
}}
transition={{
duration: 2,
repeat: Infinity,
repeatDelay: 1,
}}
/>
<span className="relative z-10 flex items-center justify-center gap-2">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path
d="M8.5 2C6.57 2 5 3.57 5 5.5V6H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-1v-.5C19 3.57 17.43 2 15.5 2h-7zM7 6V5.5C7 4.67 7.67 4 8.5 4h7c.83 0 1.5.67 1.5 1.5V6H7zm5.99 6c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
</svg>
¥9.9
</span>
</motion.button>
{/* Countdown */}
<motion.div
className="mt-4 text-center text-sm text-orange-400 flex items-center justify-center gap-2"
animate={{opacity: [1, 0.5, 1]}}
transition={{duration: 1, repeat: Infinity}}
>
<AlertCircle className="w-4 h-4"/>
<span>
{' '}
<span className="text-lg">
{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
</span>{' '}
</span>
</motion.div>
</motion.div>
{/* Trust indicators */}
<motion.div
className="bg-white/5 backdrop-blur-sm rounded-xl p-4 overflow-hidden"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 0.8}}
>
<motion.div
key={currentTestimonial}
className="text-center text-sm text-gray-400 flex items-center justify-center gap-2"
initial={{opacity: 0, y: 10}}
animate={{opacity: 1, y: 0}}
exit={{opacity: 0, y: -10}}
>
<Check className="w-4 h-4 text-green-400 flex-shrink-0"/>
<span>{testimonials[currentTestimonial]}</span>
</motion.div>
</motion.div>
{/* Footer guarantees */}
<motion.div
className="mt-6 flex justify-center gap-6 text-xs text-gray-500"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 1}}
>
<div className="flex items-center gap-1">
<Lock className="w-3 h-3"/>
<span></span>
</div>
<div className="flex items-center gap-1">
<Users className="w-3 h-3"/>
<span>1000+</span>
</div>
<div className="flex items-center gap-1">
<TrendingDown className="w-3 h-3"/>
<span>40%</span>
</div>
</motion.div>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,512 @@
import {AnimatePresence, motion} from 'motion/react';
import {FormEvent, useState} from 'react';
import {ArrowRight, CheckCircle, FileText, Gift, MessageSquare, Sparkles, Users, X, Zap} from 'lucide-react';
import {submitContactInfoApi} from "@/api/service";
import {useUserStore} from "@/store/user-store";
export function Screen5Report() {
const userStore = useUserStore()
const [formData, setFormData] = useState({
name: '',
phone: '',
company: '',
});
const [showReport, setShowReport] = useState(false);
const [showAdvisorModal, setShowAdvisorModal] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
await submitContactInfoApi({
contact_name: formData.name,
contact_phone: formData.phone,
enterprise_name: formData.company,
record_id: userStore.analysis.record_id,
})
setShowReport(true);
};
if (!showReport) {
return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12">
<motion.div
className="w-full max-w-md"
initial={{opacity: 0, scale: 0.9}}
animate={{opacity: 1, scale: 1}}
>
{/* Success icon */}
<motion.div
className="flex justify-center mb-8"
initial={{scale: 0}}
animate={{scale: 1}}
transition={{type: 'spring', delay: 0.2}}
>
<div
className="w-20 h-20 bg-gradient-to-br from-[#7B61FF] to-[#00F0FF] rounded-full flex items-center justify-center">
<CheckCircle className="w-12 h-12 text-white"/>
</div>
</motion.div>
<motion.h2
className="text-2xl text-center mb-2"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 0.3}}
>
</motion.h2>
<motion.p
className="text-gray-400 text-center mb-8"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 0.4}}
>
</motion.p>
{/* Form */}
<motion.form
className="space-y-4"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 0.5}}
onSubmit={handleSubmit}
>
<div>
<label className="block text-sm text-gray-400 mb-2"> *</label>
<input
type="text"
required
className="w-full px-4 py-3 bg-white/5 border border-gray-600 rounded-xl focus:border-[#00F0FF] focus:outline-none transition-colors"
placeholder="请输入您的姓名"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2"> *</label>
<input
type="tel"
required
maxLength={11}
pattern="[0-9]{11}"
className="w-full px-4 py-3 bg-white/5 border border-gray-600 rounded-xl focus:border-[#00F0FF] focus:outline-none transition-colors"
placeholder="请输入手机号"
value={formData.phone}
onChange={(e) => setFormData({...formData, phone: e.target.value})}
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2"></label>
<input
type="text"
className="w-full px-4 py-3 bg-white/5 border border-gray-600 rounded-xl focus:border-[#00F0FF] focus:outline-none transition-colors"
placeholder="请输入企业名称"
value={formData.company}
onChange={(e) => setFormData({...formData, company: e.target.value})}
/>
</div>
<motion.button
type="submit"
className="w-full py-4 bg-gradient-to-r from-[#7B61FF] to-[#00F0FF] rounded-xl flex items-center justify-center gap-2"
whileHover={{scale: 1.02}}
whileTap={{scale: 0.98}}
>
<FileText className="w-5 h-5"/>
</motion.button>
</motion.form>
</motion.div>
</div>
);
}
return (
<div className="min-h-screen px-6 py-12 pb-32">
<motion.div
className="max-w-2xl mx-auto"
initial={{opacity: 0}}
animate={{opacity: 1}}
>
{/* Header */}
<motion.div
className="text-center mb-12"
initial={{opacity: 0, y: -20}}
animate={{opacity: 1, y: 0}}
>
<h1 className="text-3xl mb-2">AI降本增效诊断报告</h1>
<p className="text-gray-400"> {formData.company || formData.name} </p>
</motion.div>
{/* Summary cards */}
<div className="grid grid-cols-2 gap-4 mb-8">
<motion.div
className="bg-gradient-to-br from-red-500/20 to-red-500/5 border border-red-500/30 rounded-xl p-6"
initial={{opacity: 0, x: -20}}
animate={{opacity: 1, x: 0}}
transition={{delay: 0.2}}
>
<div className="text-red-400 text-sm mb-2"></div>
<div className="text-3xl">
¥{userStore.analysis.analysis_result.analyze_data.annual_original_cost}
</div>
</motion.div>
<motion.div
className="bg-gradient-to-br from-[#00F0FF]/20 to-[#00F0FF]/5 border border-[#00F0FF]/30 rounded-xl p-6"
initial={{opacity: 0, x: 20}}
animate={{opacity: 1, x: 0}}
transition={{delay: 0.3}}
>
<div className="text-[#00F0FF] text-sm mb-2"></div>
<div className="text-3xl">
¥{userStore.analysis.analysis_result.analyze_data.annual_savings_cost}
</div>
</motion.div>
</div>
{/* Department analysis */}
<motion.div
className="bg-white/5 backdrop-blur-sm border border-gray-600 rounded-2xl p-6 mb-8"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 0.4}}
>
<h3 className="text-xl mb-6 flex items-center gap-2">
<Users className="w-6 h-6 text-[#00F0FF]"/>
</h3>
<div className="space-y-4">
{
userStore.analysis.analysis_result.analyze_data.analyze_postion_detail.map((dept: any, index) => {
let level = 0; // 0低1普通2高
if (dept.replace_save_rate < 30) {
level = 0
} else if (dept.replace_save_rate < 70) {
level = 1
} else {
level = 2
}
return (
<motion.div
key={dept.position}
className={`p-4 rounded-xl border ${
level == 2
? 'bg-red-500/10 border-red-500/30'
: level == 1
? 'bg-orange-500/10 border-orange-500/30'
: 'bg-yellow-500/10 border-yellow-500/30'
}`}
initial={{opacity: 0, x: -20}}
animate={{opacity: 1, x: 0}}
transition={{delay: 0.5 + index * 0.1}}
>
<div className="flex items-center justify-between mb-3">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{dept.position}</span>
{level == 2 && (
<span
className="text-xs bg-red-500 text-white px-2 py-0.5 rounded"></span>
)}
</div>
<div className="text-sm text-gray-400">
{dept.original_number} {dept.replace_with_ai_number} + AI
</div>
</div>
<div className="text-right">
<div
className={`text-2xl ${
level == 2
? 'text-red-400'
: level == 1
? 'text-orange-400'
: 'text-yellow-400'
}`}
>
-{dept.replace_save_rate}%
</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
{/* Progress bar */}
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<motion.div
className={`h-full ${
level == 2
? 'bg-red-400'
: level == 1
? 'bg-orange-400'
: 'bg-yellow-400'
}`}
initial={{width: 0}}
animate={{width: `${dept.replace_save_rate}%`}}
transition={{delay: 0.7 + index * 0.1, duration: 1}}
/>
</div>
</motion.div>
)
})
}
</div>
</motion.div>
{/* AI Tools recommendation */}
<motion.div
className="bg-gradient-to-br from-[#7B61FF]/20 to-[#00F0FF]/20 backdrop-blur-sm border border-[#00F0FF]/30 rounded-2xl p-6 mb-8"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 0.9}}
>
<h3 className="text-xl mb-4 flex items-center gap-2">
<Zap className="w-6 h-6 text-[#00F0FF]"/>
AI工具组合
</h3>
<div className="grid grid-cols-2 gap-3">
{['文案生成AI', '设计辅助AI', '智能客服系统', '数据分析AI', '自动化运营工具', '会议记录AI'].map(
(tool, index) => (
<motion.div
key={tool}
className="bg-white/5 border border-gray-600 rounded-lg px-4 py-3 text-sm text-center"
initial={{opacity: 0, scale: 0.8}}
animate={{opacity: 1, scale: 1}}
transition={{delay: 1 + index * 0.05}}
>
{tool}
</motion.div>
)
)}
</div>
</motion.div>
{/* CTA section - Redesigned */}
<motion.div
className="relative bg-gradient-to-br from-[#7B61FF] to-[#00F0FF] rounded-3xl p-8 text-center overflow-hidden"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 1.2}}
>
{/* Animated background pattern */}
<motion.div
className="absolute inset-0 opacity-20"
animate={{
backgroundPosition: ['0% 0%', '100% 100%'],
}}
transition={{
duration: 20,
repeat: Infinity,
repeatType: 'reverse',
}}
style={{
backgroundImage: 'radial-gradient(circle, white 1px, transparent 1px)',
backgroundSize: '30px 30px',
}}
/>
<div className="relative z-10">
<motion.div
className="flex items-center justify-center gap-2 mb-4"
animate={{scale: [1, 1.05, 1]}}
transition={{duration: 2, repeat: Infinity}}
>
<Sparkles className="w-8 h-8 text-white"/>
<h3 className="text-2xl text-white"></h3>
</motion.div>
<p className="text-white/90 mb-6 text-lg">
AI顾问为您提供
<br/>
<span className="text-white">AI工具包 + </span>
</p>
<motion.button
className="w-full max-w-sm mx-auto py-5 bg-white text-[#7B61FF] rounded-2xl flex items-center justify-center gap-3 shadow-2xl"
whileHover={{scale: 1.05, boxShadow: '0 20px 40px rgba(0,0,0,0.3)'}}
whileTap={{scale: 0.95}}
onClick={() => setShowAdvisorModal(true)}
>
<MessageSquare className="w-6 h-6"/>
<span className="text-lg">AI顾问</span>
<ArrowRight className="w-6 h-6"/>
</motion.button>
<motion.div
className="mt-4 flex items-center justify-center gap-2 text-white/90"
animate={{opacity: [0.7, 1, 0.7]}}
transition={{duration: 2, repeat: Infinity}}
>
<Gift className="w-5 h-5"/>
<span>2025AI工具白皮书</span>
</motion.div>
</div>
</motion.div>
</motion.div>
{/* Floating Action Button */}
<motion.button
className="fixed bottom-6 left-1/2 -translate-x-1/2 px-8 py-4 bg-gradient-to-r from-[#7B61FF] to-[#00F0FF] rounded-full shadow-2xl flex items-center justify-center gap-3 z-50 max-w-[90vw]"
initial={{y: 100, opacity: 0}}
animate={{y: 0, opacity: 1}}
transition={{delay: 1.5, type: 'spring'}}
whileHover={{scale: 1.05, y: -5}}
whileTap={{scale: 0.95}}
onClick={() => setShowAdvisorModal(true)}
>
<MessageSquare className="w-6 h-6 text-white"/>
<span className="text-white whitespace-nowrap"> 11AI落地方案</span>
{/* Ripple effect */}
<motion.div
className="absolute inset-0 rounded-full border-2 border-white"
animate={{
scale: [1, 1.2, 1],
opacity: [0.6, 0, 0.6],
}}
transition={{
duration: 2,
repeat: Infinity,
}}
/>
{/* Glow effect */}
<motion.div
className="absolute inset-0 rounded-full bg-gradient-to-r from-[#7B61FF] to-[#00F0FF] blur-xl opacity-50"
animate={{
opacity: [0.3, 0.6, 0.3],
}}
transition={{
duration: 2,
repeat: Infinity,
}}
/>
</motion.button>
{/* Advisor Modal */}
<AnimatePresence>
{showAdvisorModal && (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center px-6"
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
>
{/* Backdrop */}
<motion.div
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
onClick={() => setShowAdvisorModal(false)}
/>
{/* Modal */}
<motion.div
className="relative bg-gradient-to-br from-[#0A0F24] to-[#1a1f3a] border-2 border-[#00F0FF] rounded-3xl p-8 max-w-md w-full"
initial={{scale: 0.8, y: 50}}
animate={{scale: 1, y: 0}}
exit={{scale: 0.8, y: 50}}
transition={{type: 'spring'}}
>
{/* Close button */}
<button
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
onClick={() => setShowAdvisorModal(false)}
>
<X className="w-6 h-6"/>
</button>
{/* Animated icon */}
<motion.div
className="flex justify-center mb-6"
initial={{scale: 0}}
animate={{scale: 1}}
transition={{type: 'spring', delay: 0.2}}
>
<motion.div
className="w-20 h-20 bg-gradient-to-br from-[#7B61FF] to-[#00F0FF] rounded-full flex items-center justify-center"
animate={{
boxShadow: [
'0 0 20px rgba(0, 240, 255, 0.5)',
'0 0 40px rgba(123, 97, 255, 0.8)',
'0 0 20px rgba(0, 240, 255, 0.5)',
],
}}
transition={{duration: 2, repeat: Infinity}}
>
<MessageSquare className="w-10 h-10 text-white"/>
</motion.div>
</motion.div>
{/* Content */}
<motion.div
className="text-center mb-8"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{delay: 0.3}}>
<h3 className="text-2xl mb-4">AI顾问</h3>
<p className="text-gray-300 mb-6">
</p>
<div className="space-y-3 mb-6 text-left">
{[
'1对1定制化AI落地方案',
'行业专属AI工具包推荐',
'全员AI技能培训指导',
'《2025企业AI工具白皮书》PDF',
].map((item, index) => (
<motion.div
key={index}
className="flex items-center gap-3 text-sm text-gray-300"
initial={{opacity: 0, x: -20}}
animate={{opacity: 1, x: 0}}
transition={{delay: 0.4 + index * 0.1}}>
<div
className="w-6 h-6 bg-[#00F0FF]/20 rounded-full flex items-center justify-center flex-shrink-0">
<CheckCircle className="w-4 h-4 text-[#00F0FF]"/>
</div>
<span>{item}</span>
</motion.div>
))}
</div>
{/* QR Code */}
<motion.div
className="bg-white rounded-2xl p-4 inline-block mb-4"
initial={{opacity: 0, scale: 0.8}}
animate={{opacity: 1, scale: 1}}
transition={{delay: 0.6}}>
<div
className="w-48 h-48 bg-gray-300 rounded-xl flex items-center justify-center text-gray-600 text-xs text-center px-4">
<img src={"https://keyang2.tuzuu.com/%E6%95%88%E7%81%B5/kf.jpg"}/>
</div>
</motion.div>
<motion.p
className="text-sm text-gray-400"
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{delay: 0.8}}>
<br/>
{formData.company || formData.name}
<br/>
</motion.p>
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

6
src/main.tsx Normal file
View File

@@ -0,0 +1,6 @@
import {createRoot} from "react-dom/client";
import App from "./App";
import "./styles/index.css";
createRoot(document.getElementById("root")!).render(<App />);

36
src/store/user-store.ts Normal file
View File

@@ -0,0 +1,36 @@
import {create} from 'zustand';
import {createJSONStorage, persist} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';
interface State {
token: string,
analysis: any,
}
type Action = {
setToken: (value: string) => void,
setAnalysis: (value: any) => void,
}
export const useUserStore = create<State & Action>()(
persist(
immer((set) => ({
token: "",
analysis: null,
setToken(val) {
set((state) => {
state.token = val
})
},
setAnalysis(val) {
set((state) => {
state.analysis = val
})
}
})),
{
name: 'zustand_storage',
storage: createJSONStorage(() => localStorage),
}
)
)

185
src/styles/globals.css Normal file
View File

@@ -0,0 +1,185 @@
@custom-variant dark (&:is(.dark *));
:root {
--font-size: 16px;
--background: #ffffff;
--foreground: oklch(0.145 0 0);
--card: #ffffff;
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #030213;
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0.0058 264.53);
--secondary-foreground: #030213;
--muted: #ececf0;
--muted-foreground: #717182;
--accent: #e9ebef;
--accent-foreground: #030213;
--destructive: #d4183d;
--destructive-foreground: #ffffff;
--border: rgba(0, 0, 0, 0.1);
--input: transparent;
--input-background: #f3f3f5;
--switch-background: #cbced4;
--font-weight-medium: 500;
--font-weight-normal: 400;
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: #030213;
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--font-weight-medium: 500;
--font-weight-normal: 400;
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-input-background: var(--input-background);
--color-switch-background: var(--switch-background);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
/**
* Base typography. This is not applied to elements which have an ancestor with a Tailwind text class.
*/
@layer base {
:where(:not(:has([class*=' text-']), :not(:has([class^='text-'])))) {
h1 {
font-size: var(--text-2xl);
font-weight: var(--font-weight-medium);
line-height: 1.5;
}
h2 {
font-size: var(--text-xl);
font-weight: var(--font-weight-medium);
line-height: 1.5;
}
h3 {
font-size: var(--text-lg);
font-weight: var(--font-weight-medium);
line-height: 1.5;
}
h4 {
font-size: var(--text-base);
font-weight: var(--font-weight-medium);
line-height: 1.5;
}
label {
font-size: var(--text-base);
font-weight: var(--font-weight-medium);
line-height: 1.5;
}
button {
font-size: var(--text-base);
font-weight: var(--font-weight-medium);
line-height: 1.5;
}
input {
font-size: var(--text-base);
font-weight: var(--font-weight-normal);
line-height: 1.5;
}
}
}
html {
font-size: var(--font-size);
}

2093
src/styles/index.css Normal file

File diff suppressed because it is too large Load Diff

3
src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module "weixin-js-sdk"

13
src/utils/http/error.ts Normal file
View File

@@ -0,0 +1,13 @@
import qs from "qs";
/**
* 错误处理
*/
export function errorHand() {
window.localStorage.removeItem("zustand_storage")
let location = window.location
let query = qs.parse(location.href.split('?')[1])
delete query.code
let query2 = qs.stringify(query)
window.location.href = location.origin + location.pathname + '?' + query2
}

68
src/utils/http/request.ts Normal file
View File

@@ -0,0 +1,68 @@
import axios, {type AxiosRequestHeaders, type AxiosResponse} from "axios"
import {errorHand} from "@/utils/http/error";
import {useUserStore} from "@/store/user-store";
import toast from "react-hot-toast";
const instance = axios.create({
baseURL: import.meta.env.VITE_WEB_URL + "/api",
timeout: 1000*300,
})
//拦截器
instance.interceptors.request.use((config) => {
//当数据为formData自动修改请求头
if ((config.data instanceof FormData)) {
config.headers["Content-Type"] = "multipart/form-data"
}
let token = useUserStore.getState().token
if (token) {
(config.headers as AxiosRequestHeaders).Authorization = `Bearer ${token}`
}
return config
})
instance.interceptors.response.use((response: AxiosResponse) => {
const {code, data, message} = response.data
if (code === 1) {
return data
} else if (code === 0) {
toast.error(message)
return Promise.reject(new Error(message))
} else {
if (code === 401 || code === 403) {
errorHand()
} else {
toast.error(message)
}
}
}, error => {
if (error.code === 'ECONNABORTED') {
toast.error('网速较慢,请耐心等待');
error.config.timeout = 1000 * 60 * 3
return instance(error.config)
} else if (error.message === 'Network Error') {
toast.error('网络中断');
return Promise.reject()
} else {
toast.error('网络异常');
return Promise.reject()
}
})
//封装postget
function requestPost(url: string, data = {}) {
return instance.post(url, data)
}
function requestGet(url: string, params = {}) {
return instance.get(url, {params})
}
const request = {
get: requestGet,
post: requestPost
}
export default request

48
src/wx/wxLogin.ts Normal file
View File

@@ -0,0 +1,48 @@
import qs from "qs"
import {Login} from "@/api/wxchat";
import {useUserStore} from "@/store/user-store";
/**
* snsapi_base 不弹出授权页面直接跳转只能获取用户openid
* snsapi_userinfo 弹出授权页面可通过openid拿到昵称、性别、所在地。并且 即使在未关注的情况下,只要用户授权,也能获取其信息
*/
const scope: Array<string> = ['snsapi_base', 'snsapi_userinfo']
const appid = import.meta.env.VITE_APPID
let locationUrl: string = window.location.href
let webUrl: string = locationUrl.split("?")[0]
export default function (): Promise<boolean> {
return new Promise((resolve) => {
let token = useUserStore.getState().token
if (token) {
resolve(true)
return
}
let query = qs.parse(locationUrl.split('?')[1])
if (query.code) {
Login({
wx_code: query.code,
...query
}).then(res => {
if (!res.accessToken) {
delete query.code
delete query.state
webUrl = webUrl + qs.stringify(query)
hrefLogin(webUrl)
return
}
useUserStore.getState().setToken(res.accessToken);
resolve(true)
})
} else {
hrefLogin(locationUrl)
}
})
}
//防止登录时多次在url沙上添加code
function hrefLogin(redirectUrl: string) {
redirectUrl = encodeURIComponent(redirectUrl)
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope[1]}#wechat_redirect`
}

23
src/wx/wxPay.ts Normal file
View File

@@ -0,0 +1,23 @@
import wx from "weixin-js-sdk"
//打开支付,会返回回调1为支付成功2为取消支付
export function wxPay(payInfoConfig: any): Promise<boolean> {
return new Promise((resolve) => {
wx.chooseWXPay({
timestamp: payInfoConfig['timestamp'], // 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: payInfoConfig['nonceStr'], // 支付签名随机串,不长于 32 位
package: payInfoConfig['package'], // 统一支付接口返回的prepay_id参数值提交格式如prepay_id=\*\*\*
signType: payInfoConfig['signType'], // 微信支付V3的传入RSA,微信支付V2的传入格式与V2统一下单的签名格式保持一致
paySign: payInfoConfig['paySign'], // 支付签名
success: function () {
resolve(true)
},
cancel: function (err: any) {
console.log(err)
resolve(false)
}
})
})
}

82
src/wx/wxShare.ts Normal file
View File

@@ -0,0 +1,82 @@
import wx from "weixin-js-sdk"
import {getJsSdk, getShareConfig} from "@/api/wxchat";
import qs from "qs"
export default async function () {
let isWeXin = navigator.userAgent.toLowerCase().indexOf("micromessenger") !== -1
if (!isWeXin) return
let query = qs.parse(window.location.search.split('?')[1])
delete query.code
//默认分享路径
// let defaultHref = window.location.origin + window.location.pathname + '?' + qs.stringify(query)
let data = await getJsSdk({
url: window.location.href
})
let shareConfig = await getShareConfig() as any
const shareDataObj = {
title: shareConfig.share_title,
desc: shareConfig.share_sub_title,
link: window.location.href,
imgUrl: 'https://keyang2.tuzuu.com/%E6%95%88%E7%81%B5/logo.jpg'
}
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
appId: data.jssdk.appId, // 必填,公众号的唯一标识
timestamp: data.jssdk.timestamp, // 必填,生成签名的时间戳
nonceStr: data.jssdk.nonceStr, // 必填,生成签名的随机串
signature: data.jssdk.signature,// 必填,签名
jsApiList: data.jssdk.jsApiList, // 必填需要使用的JS接口列表
openTagList: ['wx-open-launch-weapp']
})
wx.ready(function () {
if (wx.updateAppMessageShareData) {
wx.updateAppMessageShareData({
title: shareDataObj.title,
desc: shareDataObj.desc,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('分享朋友成功')
},
})
}
if (wx.updateTimelineShareData) {
wx.updateTimelineShareData({
title: shareDataObj.title,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('分享朋友圈')
},
})
}
if (wx.onMenuShareAppMessage) {
wx.onMenuShareAppMessage({
title: shareDataObj.title,
desc: shareDataObj.desc,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('旧版本分享朋友成功')
},
cancel: () => {
}
})
}
if (wx.onMenuShareTimeline) {
wx.onMenuShareTimeline({
title: shareDataObj.title,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('旧版本分享朋友圈')
},
cancel: () => {
}
})
}
})
}

35
tsconfig.app.json Normal file
View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"types": [
"vite/client"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"verbatimModuleSyntax": false,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": false,
"noUnusedParameters": false,
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src"
]
}

6
tsconfig.json Normal file
View File

@@ -0,0 +1,6 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
]
}

25
vite.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react-swc';
import {resolve} from "path";
const pathResolve = (dir: string): any => {
return resolve(__dirname, ".", dir)
}
const alias: Record<string, string> = {
'@': pathResolve("src")
}
export default defineConfig({
plugins: [
react(),
],
server: {
host: true,
},
resolve: {
alias
},
build: {
outDir: './xiaoling_dist',
}
});