1
This commit is contained in:
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_APPID=wxbc438492e3efab70 #appid
|
||||
VITE_WEB_URL=https://ting.lifebanktech.com
|
||||
2
.env.production
Normal file
2
.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_APPID=wxbc438492e3efab70 #appid
|
||||
VITE_WEB_URL=https://ting.lifebanktech.com
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal 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
11
README.md
Normal 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
19
index.html
Normal 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
28
package.json
Normal 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
1268
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
src/App.tsx
Normal file
84
src/App.tsx
Normal 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
39
src/api/service.ts
Normal 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
25
src/api/wxchat.ts
Normal 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")
|
||||
}
|
||||
281
src/components/Screen1Hook.tsx
Normal file
281
src/components/Screen1Hook.tsx
Normal 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时代,1个懂AI的员工 > 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>
|
||||
);
|
||||
}
|
||||
205
src/components/Screen2Upload.tsx
Normal file
205
src/components/Screen2Upload.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
253
src/components/Screen3Analysis.tsx
Normal file
253
src/components/Screen3Analysis.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
285
src/components/Screen4Payment.tsx
Normal file
285
src/components/Screen4Payment.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
512
src/components/Screen5Report.tsx
Normal file
512
src/components/Screen5Report.tsx
Normal 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>限时赠送《2025企业AI工具白皮书》</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">点击获取 1对1定制化AI落地方案</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
6
src/main.tsx
Normal 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
36
src/store/user-store.ts
Normal 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
185
src/styles/globals.css
Normal 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
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
3
src/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
declare module "weixin-js-sdk"
|
||||
|
||||
13
src/utils/http/error.ts
Normal file
13
src/utils/http/error.ts
Normal 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
68
src/utils/http/request.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//封装post,get
|
||||
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
48
src/wx/wxLogin.ts
Normal 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
23
src/wx/wxPay.ts
Normal 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
82
src/wx/wxShare.ts
Normal 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
35
tsconfig.app.json
Normal 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
6
tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
]
|
||||
}
|
||||
25
vite.config.ts
Normal file
25
vite.config.ts
Normal 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',
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user