feat: 10.10线上最新代码提交
This commit is contained in:
40
src/components/Arrows/index.js
Normal file
40
src/components/Arrows/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
type ArrowsProps = {
|
||||
className?: string;
|
||||
prevClassName?: string;
|
||||
nextClassName?: string;
|
||||
onPrev?: () => void;
|
||||
onNext?: () => void;
|
||||
};
|
||||
|
||||
const Arrows = ({
|
||||
className,
|
||||
prevClassName,
|
||||
nextClassName,
|
||||
onPrev,
|
||||
onNext,
|
||||
}: ArrowsProps) => (
|
||||
<div className={`splide__arrows relative z-10 flex ${className || ""}`}>
|
||||
<button
|
||||
className={`splide__arrow splide__arrow--prev ${
|
||||
prevClassName || ""
|
||||
}`}
|
||||
onClick={onPrev}
|
||||
>
|
||||
<svg className="w-4 h-4 fill-n-4">
|
||||
<path d="M8.707 1.707a1 1 0 0 0 0-1.414 1 1 0 0 0-1.414 0l-7 7a1 1 0 0 0 0 1.414l7 7a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 1 0 0-2H3.414l5.293-5.293z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={`splide__arrow splide__arrow--next ${
|
||||
nextClassName || ""
|
||||
}`}
|
||||
onClick={onNext}
|
||||
>
|
||||
<svg className="w-4 h-4 fill-n-4">
|
||||
<path d="M7.293 1.707a1 1 0 0 1 0-1.414 1 1 0 0 1 1.414 0l7 7a1 1 0 0 1 0 1.414l-7 7a1 1 0 0 1-1.414-1.414L12.586 9H1a1 1 0 1 1 0-2h11.586L7.293 1.707z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Arrows;
|
||||
40
src/components/Button/index.js
Normal file
40
src/components/Button/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { svgs } from "./svgs";
|
||||
|
||||
const Button = ({
|
||||
className,
|
||||
href,
|
||||
onClick,
|
||||
children,
|
||||
px,
|
||||
white,
|
||||
}) => {
|
||||
const classes = `button relative inline-flex items-center justify-center h-11 ${
|
||||
px || "px-7"
|
||||
} ${white ? "text-n-8" : "text-n-1"} transition-colors hover:text-color-1 ${
|
||||
className || ""
|
||||
}`;
|
||||
|
||||
const spanClasses = `relative z-10`;
|
||||
|
||||
return href ? (
|
||||
href.startsWith("mailto:") ? (
|
||||
<a href={href} className={classes}>
|
||||
<span className={spanClasses}>{children}</span>
|
||||
{svgs(white)}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href} className={classes}>
|
||||
<span className={spanClasses}>{children}</span>
|
||||
{svgs(white)}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<button className={classes} onClick={onClick}>
|
||||
<span className={spanClasses}>{children}</span>
|
||||
{svgs(white)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
58
src/components/Button/svgs.js
Normal file
58
src/components/Button/svgs.js
Normal file
@@ -0,0 +1,58 @@
|
||||
export const svgs = (white: boolean | undefined) => (
|
||||
<>
|
||||
<svg
|
||||
className="absolute top-0 left-0"
|
||||
width="21"
|
||||
height="44"
|
||||
viewBox="0 0 21 44"
|
||||
>
|
||||
<path
|
||||
fill={white ? "white" : "none"}
|
||||
stroke={white ? "white" : "url(#btn-left)"}
|
||||
strokeWidth="2"
|
||||
d="M21,43.00005 L8.11111,43.00005 C4.18375,43.00005 1,39.58105 1,35.36365 L1,8.63637 C1,4.41892 4.18375,1 8.11111,1 L21,1"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className="absolute top-0 left-[1.3125rem] w-[calc(100%-2.625rem)]"
|
||||
height="44"
|
||||
viewBox="0 0 100 44"
|
||||
preserveAspectRatio="none"
|
||||
fill={white ? "white" : "none"}
|
||||
>
|
||||
{white ? (
|
||||
<polygon
|
||||
fill="white"
|
||||
fillRule="nonzero"
|
||||
points="100 0 100 44 0 44 0 0"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<polygon
|
||||
fill="url(#btn-top)"
|
||||
fillRule="nonzero"
|
||||
points="100 42 100 44 0 44 0 42"
|
||||
/>
|
||||
<polygon
|
||||
fill="url(#btn-bottom)"
|
||||
fillRule="nonzero"
|
||||
points="100 0 100 2 0 2 0 0"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
<svg
|
||||
className="absolute top-0 right-0"
|
||||
width="21"
|
||||
height="44"
|
||||
viewBox="0 0 21 44"
|
||||
>
|
||||
<path
|
||||
fill={white ? "white" : "none"}
|
||||
stroke={white ? "white" : "url(#btn-right)"}
|
||||
strokeWidth="2"
|
||||
d="M0,43.00005 L5.028,43.00005 L12.24,43.00005 C16.526,43.00005 20,39.58105 20,35.36365 L20,16.85855 C20,14.59295 18.978,12.44425 17.209,10.99335 L7.187,2.77111 C5.792,1.62675 4.034,1 2.217,1 L0,1"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
0
src/components/Calendars/EventCalendar.js
Normal file → Executable file
0
src/components/Calendars/EventCalendar.js
Normal file → Executable file
0
src/components/Card/Card.js
Normal file → Executable file
0
src/components/Card/Card.js
Normal file → Executable file
0
src/components/Card/CardBody.js
Normal file → Executable file
0
src/components/Card/CardBody.js
Normal file → Executable file
0
src/components/Card/CardHeader.js
Normal file → Executable file
0
src/components/Card/CardHeader.js
Normal file → Executable file
0
src/components/Charts/BarChart.js
Normal file → Executable file
0
src/components/Charts/BarChart.js
Normal file → Executable file
0
src/components/Charts/BubbleChart.js
Normal file → Executable file
0
src/components/Charts/BubbleChart.js
Normal file → Executable file
0
src/components/Charts/DonutChart.js
Normal file → Executable file
0
src/components/Charts/DonutChart.js
Normal file → Executable file
0
src/components/Charts/LineBarChart.js
Normal file → Executable file
0
src/components/Charts/LineBarChart.js
Normal file → Executable file
0
src/components/Charts/LineChart.js
Normal file → Executable file
0
src/components/Charts/LineChart.js
Normal file → Executable file
0
src/components/Charts/PieChart.js
Normal file → Executable file
0
src/components/Charts/PieChart.js
Normal file → Executable file
0
src/components/Charts/PolarChart.js
Normal file → Executable file
0
src/components/Charts/PolarChart.js
Normal file → Executable file
0
src/components/Charts/RadarChart.js
Normal file → Executable file
0
src/components/Charts/RadarChart.js
Normal file → Executable file
0
src/components/Configurator/Configurator.js
Normal file → Executable file
0
src/components/Configurator/Configurator.js
Normal file → Executable file
0
src/components/Editor/Editor.js
Normal file → Executable file
0
src/components/Editor/Editor.js
Normal file → Executable file
0
src/components/ErrorBoundary.js
Normal file → Executable file
0
src/components/ErrorBoundary.js
Normal file → Executable file
0
src/components/FixedPlugin/FixedPlugin.js
Normal file → Executable file
0
src/components/FixedPlugin/FixedPlugin.js
Normal file → Executable file
0
src/components/Footer/Footer.js
Normal file → Executable file
0
src/components/Footer/Footer.js
Normal file → Executable file
@@ -30,21 +30,10 @@ const Footer = () => (
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<div className="flex justify-center items-center">
|
||||
<p className="caption text-n-4 lg:block">
|
||||
© 2024 价值前沿. 保留所有权利.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center space-y-1 sm:space-y-0 sm:space-x-4 text-xs text-n-4">
|
||||
<a
|
||||
href="https://beian.mps.gov.cn/#/query/webSearch?code=11010802046286"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
className="hover:text-n-1 transition-colors"
|
||||
>
|
||||
京公网安备11010802046286号
|
||||
</a>
|
||||
<span className="text-n-4">京ICP备2025107343号-1</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
|
||||
20
src/components/Generating/index.js
Normal file
20
src/components/Generating/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Image from "../Image";
|
||||
|
||||
const Generating = ({ className }) => (
|
||||
<div
|
||||
className={`flex items-center h-[3.375rem] px-6 bg-n-8/80 rounded-[1.6875rem] ${
|
||||
className || ""
|
||||
} text-base`}
|
||||
>
|
||||
<Image
|
||||
className="w-5 h-5 mr-4"
|
||||
src="/images/loading.png"
|
||||
width={20}
|
||||
height={20}
|
||||
alt="Loading"
|
||||
/>
|
||||
AI is generating|
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Generating;
|
||||
0
src/components/Icons/IconBox.js
Normal file → Executable file
0
src/components/Icons/IconBox.js
Normal file → Executable file
54
src/components/Icons/Icons.js
Normal file → Executable file
54
src/components/Icons/Icons.js
Normal file → Executable file
@@ -680,3 +680,57 @@ export const ArgonLogoMinifiedLight = createIcon({
|
||||
</svg>
|
||||
),
|
||||
});
|
||||
|
||||
export const WechatPayIcon = createIcon({
|
||||
displayName: 'WechatPayIcon',
|
||||
viewBox: '0 0 24 24',
|
||||
path: (
|
||||
<g>
|
||||
<path
|
||||
d='M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 00.167-.054l1.903-1.114a.864.864 0 01.717-.098 10.16 10.16 0 002.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-5.523 2.619-7.21C11.923 8.507 11.695 8.5 11.465 8.5c-1.66 0-3.274.374-4.774 1.061-.276-.651-.43-1.353-.43-2.081C6.261 4.824 7.321 2.188 8.691 2.188zM18.639 11.698c-4.118 0-7.45 2.649-7.45 5.918 0 3.27 3.332 5.918 7.45 5.918.858 0 1.676-.109 2.435-.304.276-.097.574-.074.858.098l1.549.904c.065.035.13.054.167.054.163 0 .276-.132.276-.295a.516.516 0 00-.035-.195l-.325-1.235a.59.59 0 01.177-.665c1.543-1.235 2.524-2.982 2.524-4.98 0-3.269-3.332-5.918-7.45-5.918h-.176z'
|
||||
fill='#00D924'
|
||||
/>
|
||||
<circle cx='5.5' cy='7.5' r='1' fill='white' />
|
||||
<circle cx='8.5' cy='7.5' r='1' fill='white' />
|
||||
<circle cx='11.5' cy='7.5' r='1' fill='white' />
|
||||
<circle cx='15.5' cy='15.5' r='0.8' fill='white' />
|
||||
<circle cx='18' cy='15.5' r='0.8' fill='white' />
|
||||
<circle cx='20.5' cy='15.5' r='0.8' fill='white' />
|
||||
</g>
|
||||
),
|
||||
});
|
||||
|
||||
export const QRCodeIcon = createIcon({
|
||||
displayName: 'QRCodeIcon',
|
||||
viewBox: '0 0 24 24',
|
||||
path: (
|
||||
<g fill='currentColor'>
|
||||
<rect x='1' y='1' width='6' height='6' rx='1' stroke='currentColor' strokeWidth='1.5' fill='none' />
|
||||
<rect x='17' y='1' width='6' height='6' rx='1' stroke='currentColor' strokeWidth='1.5' fill='none' />
|
||||
<rect x='1' y='17' width='6' height='6' rx='1' stroke='currentColor' strokeWidth='1.5' fill='none' />
|
||||
<rect x='3' y='3' width='2' height='2' />
|
||||
<rect x='19' y='3' width='2' height='2' />
|
||||
<rect x='3' y='19' width='2' height='2' />
|
||||
<rect x='9' y='1' width='2' height='2' />
|
||||
<rect x='13' y='1' width='2' height='2' />
|
||||
<rect x='9' y='5' width='2' height='2' />
|
||||
<rect x='13' y='5' width='2' height='2' />
|
||||
<rect x='1' y='9' width='2' height='2' />
|
||||
<rect x='5' y='9' width='2' height='2' />
|
||||
<rect x='1' y='13' width='2' height='2' />
|
||||
<rect x='5' y='13' width='2' height='2' />
|
||||
<rect x='9' y='9' width='6' height='6' rx='1' stroke='currentColor' strokeWidth='1.5' fill='none' />
|
||||
<rect x='11' y='11' width='2' height='2' />
|
||||
<rect x='17' y='9' width='2' height='2' />
|
||||
<rect x='21' y='9' width='2' height='2' />
|
||||
<rect x='17' y='13' width='2' height='2' />
|
||||
<rect x='21' y='13' width='2' height='2' />
|
||||
<rect x='9' y='17' width='2' height='2' />
|
||||
<rect x='13' y='17' width='2' height='2' />
|
||||
<rect x='9' y='21' width='2' height='2' />
|
||||
<rect x='13' y='21' width='2' height='2' />
|
||||
<rect x='17' y='17' width='6' height='2' />
|
||||
<rect x='17' y='21' width='6' height='2' />
|
||||
</g>
|
||||
),
|
||||
});
|
||||
|
||||
17
src/components/Image/index.js
Normal file
17
src/components/Image/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const Image = ({ className, ...props }) => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
return (
|
||||
<img
|
||||
className={`inline-block align-top opacity-0 transition-opacity ${
|
||||
loaded && "opacity-100"
|
||||
} ${className}`}
|
||||
onLoad={() => setLoaded(true)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
||||
73
src/components/Join/index.js
Normal file
73
src/components/Join/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import Section from "@/components/Section";
|
||||
import Image from "@/components/Image";
|
||||
import Button from "@/components/Button";
|
||||
|
||||
type JoinProps = {};
|
||||
|
||||
const Join = ({}: JoinProps) => (
|
||||
<Section crosses>
|
||||
<div className="container">
|
||||
<div className="relative max-w-[43.125rem] mx-auto py-8 md:py-14 xl:py-0">
|
||||
<div className="relative z-1 text-center">
|
||||
<h1 className="h1 mb-6">
|
||||
Be part of the future of{" "}
|
||||
<span className="inline-block relative">
|
||||
Brainwave
|
||||
<Image
|
||||
className="absolute top-full left-0 w-full"
|
||||
src="/images/curve.png"
|
||||
width={624}
|
||||
height={28}
|
||||
alt="Curve"
|
||||
/>
|
||||
</span>
|
||||
</h1>
|
||||
<p className="body-1 mb-8 text-n-4">
|
||||
Unleash the power of AI within Brainwave. Upgrade your
|
||||
productivity with Brainwave, the open AI chat app.
|
||||
</p>
|
||||
<Button href="/pricing" white>
|
||||
Get started
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-1/2 w-[46.5rem] h-[46.5rem] border border-n-2/5 rounded-full -translate-x-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 w-[39.25rem] h-[39.25rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-[30.625rem] h-[30.625rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-[21.5rem] h-[21.5rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-[13.75rem] h-[13.75rem] border border-n-2/10 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-1/2 w-[46.5rem] h-[46.5rem] border border-n-2/5 rounded-full -translate-x-1/2 -translate-y-1/2 opacity-60 mix-blend-color-dodge pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 w-[58.85rem] h-[58.85rem] -translate-x-3/4 -translate-y-1/2">
|
||||
<Image
|
||||
className="w-full"
|
||||
src="/images/gradient.png"
|
||||
width={942}
|
||||
height={942}
|
||||
alt="Gradient"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-[5.75rem] left-[18.5rem] -z-1 w-[19.8125rem] pointer-events-none lg:-top-15 lg:left-[5.5rem]">
|
||||
<Image
|
||||
className="w-full"
|
||||
src="/images/join/shapes-1.svg"
|
||||
width={317}
|
||||
height={293}
|
||||
alt="Shapes 1"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-[15rem] -bottom-[7rem] -z-1 w-[28.1875rem] pointer-events-none lg:right-7 lg:-bottom-[5rem]">
|
||||
<Image
|
||||
className="w-full"
|
||||
src="/images/join/shapes-2.svg"
|
||||
width={451}
|
||||
height={266}
|
||||
alt="Shapes 2"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
|
||||
export default Join;
|
||||
0
src/components/Layout/MainPanel.js
Normal file → Executable file
0
src/components/Layout/MainPanel.js
Normal file → Executable file
0
src/components/Layout/PanelContainer.js
Normal file → Executable file
0
src/components/Layout/PanelContainer.js
Normal file → Executable file
0
src/components/Layout/PanelContent.js
Normal file → Executable file
0
src/components/Layout/PanelContent.js
Normal file → Executable file
53
src/components/Logos/index.js
Normal file
53
src/components/Logos/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import Image from "../Image";
|
||||
|
||||
const Logos = ({ className }) => (
|
||||
<div className={className}>
|
||||
<h5 className="tagline mb-6 text-center text-n-1/50">
|
||||
Helping people create beautiful content at
|
||||
</h5>
|
||||
<ul className="flex">
|
||||
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
|
||||
<Image
|
||||
src="/images/yourlogo.svg"
|
||||
width={134}
|
||||
height={28}
|
||||
alt="Logo 3"
|
||||
/>
|
||||
</li>
|
||||
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
|
||||
<Image
|
||||
src="/images/yourlogo.svg"
|
||||
width={134}
|
||||
height={28}
|
||||
alt="Logo 3"
|
||||
/>
|
||||
</li>
|
||||
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
|
||||
<Image
|
||||
src="/images/yourlogo.svg"
|
||||
width={134}
|
||||
height={28}
|
||||
alt="Logo 3"
|
||||
/>
|
||||
</li>
|
||||
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
|
||||
<Image
|
||||
src="/images/yourlogo.svg"
|
||||
width={134}
|
||||
height={28}
|
||||
alt="Logo 3"
|
||||
/>
|
||||
</li>
|
||||
<li className="flex items-center justify-center flex-1 h-[8.5rem]">
|
||||
<Image
|
||||
src="/images/yourlogo.svg"
|
||||
width={134}
|
||||
height={28}
|
||||
alt="Logo 3"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Logos;
|
||||
0
src/components/Map/Map.css
Normal file → Executable file
0
src/components/Map/Map.css
Normal file → Executable file
0
src/components/Map/Map.js
Normal file → Executable file
0
src/components/Map/Map.js
Normal file → Executable file
0
src/components/Menu/ItemContent.js
Normal file → Executable file
0
src/components/Menu/ItemContent.js
Normal file → Executable file
0
src/components/Navbars/AdminNavbar.js
Normal file → Executable file
0
src/components/Navbars/AdminNavbar.js
Normal file → Executable file
0
src/components/Navbars/AdminNavbarLinks.js
Normal file → Executable file
0
src/components/Navbars/AdminNavbarLinks.js
Normal file → Executable file
0
src/components/Navbars/AuthNavbar.js
Normal file → Executable file
0
src/components/Navbars/AuthNavbar.js
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
0
src/components/Navbars/SearchBar/SearchBar.js
Normal file → Executable file
0
src/components/Navbars/SearchBar/SearchBar.js
Normal file → Executable file
0
src/components/Navbars/Settings/SettingsBar.js
Normal file → Executable file
0
src/components/Navbars/Settings/SettingsBar.js
Normal file → Executable file
49
src/components/Notification/index.js
Normal file
49
src/components/Notification/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import Image from "../Image";
|
||||
|
||||
const Notification = ({ className, title }) => (
|
||||
<div
|
||||
className={`flex items-center p-4 pr-6 bg-[#474060]/40 backdrop-blur border border-n-1/10 rounded-2xl ${
|
||||
className || ""
|
||||
}`}
|
||||
>
|
||||
<div className="mr-5">
|
||||
<Image
|
||||
className="w-full rounded-xl"
|
||||
src="/images/notification/image-1.png"
|
||||
width={52}
|
||||
height={52}
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h6 className="mb-1 font-semibold text-base">{title}</h6>
|
||||
<div className="flex items-center justify-between">
|
||||
<ul className="flex -m-0.5">
|
||||
{[
|
||||
"/images/notification/image-4.png",
|
||||
"/images/notification/image-3.png",
|
||||
"/images/notification/image-2.png",
|
||||
].map((item, index) => (
|
||||
<li
|
||||
className={`flex w-6 h-6 border-2 border-[#2E2A41] rounded-full overflow-hidden ${
|
||||
index !== 0 ? "-ml-2" : ""
|
||||
}`}
|
||||
key={index}
|
||||
>
|
||||
<Image
|
||||
className="w-full"
|
||||
src={item}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={item}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="body-2 text-[#6C7275]">1m ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Notification;
|
||||
0
src/components/PinInput/PinInput.js
Normal file → Executable file
0
src/components/PinInput/PinInput.js
Normal file → Executable file
138
src/components/PricingList/index.js
Normal file
138
src/components/PricingList/index.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Splide, SplideTrack, SplideSlide } from "@splidejs/react-splide";
|
||||
import Button from "@/components/Button";
|
||||
import Image from "@/components/Image";
|
||||
|
||||
import { pricing } from "@/mocks/pricing";
|
||||
|
||||
type PricingListProps = {
|
||||
monthly?: boolean;
|
||||
};
|
||||
|
||||
const PricingList = ({ monthly = true }: PricingListProps) => {
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
ref.current?.go(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Splide
|
||||
className="splide-pricing splide-visible"
|
||||
options={{
|
||||
mediaQuery: "min",
|
||||
autoWidth: true,
|
||||
pagination: false,
|
||||
arrows: false,
|
||||
gap: "1rem",
|
||||
breakpoints: {
|
||||
1024: {
|
||||
destroy: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
onMoved={(e, newIndex) => setActiveIndex(newIndex)}
|
||||
hasTrack={false}
|
||||
ref={ref}
|
||||
>
|
||||
<SplideTrack>
|
||||
{pricing.map((item, index) => (
|
||||
<SplideSlide
|
||||
className={`${index === 1 ? "" : "py-3"}`}
|
||||
key={item.id}
|
||||
>
|
||||
<div
|
||||
className={`w-[19rem] h-full px-6 ${
|
||||
index === 1 ? "py-12" : "py-8"
|
||||
} bg-n-8 border border-n-6 rounded-[2rem] lg:w-auto`}
|
||||
key={item.id}
|
||||
>
|
||||
<h4
|
||||
className={`h4 mb-4 ${
|
||||
index === 0 ? "text-color-2" : ""
|
||||
} ${index === 1 ? "text-color-1" : ""} ${
|
||||
index === 2 ? "text-color-3" : ""
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="body-2 min-h-[4rem] mb-3 text-n-1/50">
|
||||
{item.description}
|
||||
</p>
|
||||
<div className="flex items-center h-[5.5rem] mb-6">
|
||||
{item.price && (
|
||||
<>
|
||||
<div className="h3">$</div>
|
||||
<div className="text-[5.5rem] leading-none font-bold">
|
||||
{monthly
|
||||
? item.price
|
||||
: item.price !== "0"
|
||||
? (
|
||||
+item.price *
|
||||
12 *
|
||||
0.9
|
||||
).toFixed(1)
|
||||
: item.price}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className="w-full mb-6"
|
||||
href={
|
||||
item.price
|
||||
? "/pricing"
|
||||
: "mailto:info@ui8.net"
|
||||
}
|
||||
white={!!item.price}
|
||||
>
|
||||
{item.price ? "Get started" : "Contact us"}
|
||||
</Button>
|
||||
<ul>
|
||||
{item.features.map((feature, index) => (
|
||||
<li
|
||||
className="flex items-start py-5 border-t border-n-6"
|
||||
key={index}
|
||||
>
|
||||
<Image
|
||||
src="/images/check.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Check"
|
||||
/>
|
||||
<p className="body-2 ml-4">{feature}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
))}
|
||||
</SplideTrack>
|
||||
<div className="flex justify-center mt-8 -mx-2 md:mt-15 lg:hidden">
|
||||
{pricing.map((item, index) => (
|
||||
<button
|
||||
className="relative w-6 h-6 mx-2"
|
||||
onClick={() => handleClick(index)}
|
||||
key={item.id}
|
||||
>
|
||||
<span
|
||||
className={`absolute inset-0 bg-conic-gradient rounded-full transition-opacity ${
|
||||
index === activeIndex
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
></span>
|
||||
<span className="absolute inset-0.25 bg-n-8 rounded-full">
|
||||
<span className="absolute inset-2 bg-n-1 rounded-full"></span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Splide>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingList;
|
||||
19
src/components/ProtectedRoute.js
Normal file → Executable file
19
src/components/ProtectedRoute.js
Normal file → Executable file
@@ -25,17 +25,26 @@ const ProtectedRoute = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// 记录当前路径,登录后可以回到这里
|
||||
let currentPath = window.location.pathname + window.location.search;
|
||||
let redirectUrl = `/auth/signin?redirect=${encodeURIComponent(currentPath)}`;
|
||||
|
||||
// 检查是否已登录
|
||||
if (!isAuthenticated || !user) {
|
||||
// 记录当前路径,登录后可以回到这里
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
const redirectUrl = `/auth/signin?redirect=${encodeURIComponent(currentPath)}`;
|
||||
|
||||
return <Navigate to={redirectUrl} replace />;
|
||||
}
|
||||
|
||||
// 已登录,渲染子组件
|
||||
return children;
|
||||
// return children;
|
||||
|
||||
// 更新逻辑 如果 currentPath 是首页 登陆成功后跳转到个人中心
|
||||
if (currentPath === '/' || currentPath === '/home') {
|
||||
currentPath = '/profile';
|
||||
redirectUrl = `/auth/signin?redirect=${encodeURIComponent(currentPath)}`;
|
||||
return <Navigate to={redirectUrl} replace />;
|
||||
} else { // 否则正常渲染
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
0
src/components/RTLProvider/RTLProvider.js
Normal file → Executable file
0
src/components/RTLProvider/RTLProvider.js
Normal file → Executable file
0
src/components/Scrollbar/Scrollbar.js
Normal file → Executable file
0
src/components/Scrollbar/Scrollbar.js
Normal file → Executable file
57
src/components/Section/index.js
Normal file
57
src/components/Section/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const Section = ({
|
||||
className,
|
||||
crosses,
|
||||
crossesOffset,
|
||||
customPaddings,
|
||||
children,
|
||||
}) => (
|
||||
<div
|
||||
className={`relative ${
|
||||
customPaddings ||
|
||||
`py-10 lg:py-16 xl:py-20 ${crosses ? "lg:py-32 xl:py-40" : ""}`
|
||||
} ${className || ""}`}
|
||||
>
|
||||
{children}
|
||||
<div className="hidden absolute top-0 left-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:left-7.5 xl:left-10"></div>
|
||||
<div className="hidden absolute top-0 right-5 w-0.25 h-full bg-stroke-1 pointer-events-none md:block lg:right-7.5 xl:right-10"></div>
|
||||
{crosses && (
|
||||
<>
|
||||
<div
|
||||
className={`hidden absolute top-0 left-7.5 right-7.5 h-0.25 bg-stroke-1 ${
|
||||
crossesOffset && crossesOffset
|
||||
} pointer-events-none lg:block xl:left-10 right-10`}
|
||||
></div>
|
||||
<svg
|
||||
className={`hidden absolute -top-[0.3125rem] left-[1.5625rem] ${
|
||||
crossesOffset && crossesOffset
|
||||
} pointer-events-none lg:block xl:left-[2.1875rem]`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="11"
|
||||
height="11"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
|
||||
fill="#ada8c4"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className={`hidden absolute -top-[0.3125rem] right-[1.5625rem] ${
|
||||
crossesOffset && crossesOffset
|
||||
} pointer-events-none lg:block xl:right-[2.1875rem]`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="11"
|
||||
height="11"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7 1a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V8a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H8a1 1 0 0 1-1-1V1z"
|
||||
fill="#ada8c4"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Section;
|
||||
0
src/components/Separator/Separator.js
Normal file → Executable file
0
src/components/Separator/Separator.js
Normal file → Executable file
195
src/components/Services/index.js
Normal file
195
src/components/Services/index.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import Section from "@/components/Section";
|
||||
import Generating from "@/components/Generating";
|
||||
import Image from "@/components/Image";
|
||||
import Heading from "@/components/Heading";
|
||||
|
||||
type ServicesProps = {
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
const Services = ({ containerClassName }: ServicesProps) => (
|
||||
<Section>
|
||||
<div className={`container ${containerClassName || ""}`}>
|
||||
<Heading
|
||||
title="Generative AI made for creators."
|
||||
text="Brainwave unlocks the potential of AI-powered applications"
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="relative z-1 flex items-center h-[38.75rem] mb-5 p-8 border border-n-1/10 rounded-3xl overflow-hidden lg:h-[38.75rem] lg:p-20 xl:h-[45.75rem]">
|
||||
<div className="absolute top-0 left-0 w-full h-full pointer-events-none md:w-3/5 xl:w-auto">
|
||||
<Image
|
||||
className="w-full h-full object-cover md:object-right"
|
||||
src="/images/services/service-1.png"
|
||||
width={797}
|
||||
height={733}
|
||||
alt="Smartest AI"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-1 max-w-[17rem] ml-auto">
|
||||
<h4 className="h4 mb-4">Smartest AI</h4>
|
||||
<p className="bpdy-2 mb-[3.125rem] text-n-3">
|
||||
Brainwave unlocks the potential of AI-powered
|
||||
applications
|
||||
</p>
|
||||
<ul className="body-2">
|
||||
{[
|
||||
"Photo generating",
|
||||
"Photo enhance",
|
||||
"Seamless Integration",
|
||||
].map((item, index) => (
|
||||
<li
|
||||
className="flex items-start py-4 border-t border-n-6"
|
||||
key={index}
|
||||
>
|
||||
<Image
|
||||
src="/images/check.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Check"
|
||||
/>
|
||||
<p className="ml-4">{item}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Generating className="absolute left-4 right-4 bottom-4 border border-n-1/10 lg:left-1/2 lg-right-auto lg:bottom-8 lg:-translate-x-1/2" />
|
||||
</div>
|
||||
<div className="relative z-1 grid gap-5 lg:grid-cols-2">
|
||||
<div className="relative min-h-[38.75rem] border border-n-1/10 rounded-3xl overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
className="w-full h-full object-cover"
|
||||
src="/images/services/service-2.png"
|
||||
width={630}
|
||||
height={748}
|
||||
alt="Smartest AI"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex flex-col justify-end p-8 bg-gradient-to-b from-n-8/0 to-n-8/90 lg:p-15">
|
||||
<h4 className="h4 mb-4">Photo editing</h4>
|
||||
<p className="body-2 text-n-3">
|
||||
{`Automatically enhance your photos using our AI app's
|
||||
photo editing feature. Try it now!`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute top-8 right-8 max-w-[17.5rem] py-6 px-8 bg-black rounded-t-xl rounded-bl-xl font-code text-base lg:top-16 lg:right-[8.75rem] lg:max-w-[17.5rem]">
|
||||
Hey Brainwave, enhance this photo
|
||||
<svg
|
||||
className="absolute left-full bottom-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="26"
|
||||
height="37"
|
||||
>
|
||||
<path d="M21.843 37.001c3.564 0 5.348-4.309 2.829-6.828L3.515 9.015A12 12 0 0 1 0 .53v36.471h21.843z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-n-7 rounded-3xl overflow-hidden lg:min-h-[45.75rem]">
|
||||
<div className="py-12 px-4 xl:px-8">
|
||||
<h4 className="h4 mb-4">Video generation</h4>
|
||||
<p className="body-2 mb-[2.25rem] text-n-3">
|
||||
The world’s most powerful AI photo and video art
|
||||
generation engine.What will you create?
|
||||
</p>
|
||||
<ul className="flex items-center justify-between">
|
||||
{[
|
||||
"/images/icons/recording-03.svg",
|
||||
"/images/icons/recording-01.svg",
|
||||
"/images/icons/disc-02.svg",
|
||||
"/images/icons/chrome-cast.svg",
|
||||
"/images/icons/sliders-04.svg",
|
||||
].map((item, index) => (
|
||||
<li
|
||||
className={`flex items-center justify-center ${
|
||||
index === 2
|
||||
? "w-[3rem] h-[3rem] p-0.25 bg-conic-gradient rounded-2xl md:w-[4.5rem] md:h-[4.5rem]"
|
||||
: "flex w-10 h-10 bg-n-6 rounded-2xl md:w-15 md:h-15"
|
||||
}`}
|
||||
key={index}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
index === 2
|
||||
? "flex items-center justify-center w-full h-full bg-n-7 rounded-[0.9375rem]"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={item}
|
||||
width={24}
|
||||
height={24}
|
||||
alt={item}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="relative h-[20.5rem] bg-n-8 rounded-xl overflow-hidden md:h-[25rem]">
|
||||
<Image
|
||||
className="w-full h-full object-cover"
|
||||
src="/images/services/service-3.png"
|
||||
width={517}
|
||||
height={400}
|
||||
alt="Smartest AI"
|
||||
/>
|
||||
<div className="absolute top-8 left-[3.125rem] w-full max-w-[14rem] pt-2.5 pr-2.5 pb-7 pl-5 bg-n-6 rounded-t-xl rounded-br-xl font-code text-base md:max-w-[17.5rem]">
|
||||
Video generated!
|
||||
<div className="absolute left-5 -bottom-[1.125rem] flex items-center justify-center w-[2.25rem] h-[2.25rem] bg-color-1 rounded-[0.75rem]">
|
||||
<Image
|
||||
src="/images/brainwave-symbol-white.svg"
|
||||
width={26}
|
||||
height={26}
|
||||
alt="Brainwave"
|
||||
/>
|
||||
</div>
|
||||
<div className="tagline absolute right-2.5 bottom-1 text-[0.625rem] text-n-3 uppercase">
|
||||
just now
|
||||
</div>
|
||||
<svg
|
||||
className="absolute right-full bottom-0 -scale-x-100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="26"
|
||||
height="37"
|
||||
>
|
||||
<path
|
||||
className="fill-n-6"
|
||||
d="M21.843 37.001c3.564 0 5.348-4.309 2.829-6.828L3.515 9.015A12 12 0 0 1 0 .53v36.471h21.843z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="absolute left-0 bottom-0 w-full flex items-center p-6">
|
||||
<svg
|
||||
className="mr-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M8.006 2.802l.036.024 10.549 7.032.805.567c.227.183.494.437.648.808a2 2 0 0 1 0 1.532c-.154.371-.421.625-.648.808-.217.175-.5.364-.805.567L8.006 21.198l-.993.627c-.285.154-.676.331-1.132.303a2 2 0 0 1-1.476-.79c-.276-.365-.346-.788-.375-1.111S4 19.502 4 19.054V4.99v-.043l.029-1.174c.03-.323.1-.746.375-1.11a2 2 0 0 1 1.476-.79c.456-.027.847.149 1.132.304s.62.378.993.627z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex-1 bg-[#D9D9D9]">
|
||||
<div className="w-1/2 h-0.5 bg-color-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 -left-[10rem] w-[56.625rem] h-[56.625rem] opacity-50 mix-blend-color-dodge pointer-events-none">
|
||||
<Image
|
||||
className="absolute top-1/2 left-1/2 w-[79.5625rem] max-w-[79.5625rem] h-[88.5625rem] -translate-x-1/2 -translate-y-1/2"
|
||||
src="/images/gradient.png"
|
||||
width={1417}
|
||||
height={1417}
|
||||
alt="Gradient"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
|
||||
export default Services;
|
||||
0
src/components/Sidebar/Sidebar.js
Normal file → Executable file
0
src/components/Sidebar/Sidebar.js
Normal file → Executable file
0
src/components/Sidebar/SidebarDocs.js
Normal file → Executable file
0
src/components/Sidebar/SidebarDocs.js
Normal file → Executable file
0
src/components/Tables/BasicTable.js
Normal file → Executable file
0
src/components/Tables/BasicTable.js
Normal file → Executable file
0
src/components/Tables/BillingRow.js
Normal file → Executable file
0
src/components/Tables/BillingRow.js
Normal file → Executable file
0
src/components/Tables/DashboardTableRow.js
Normal file → Executable file
0
src/components/Tables/DashboardTableRow.js
Normal file → Executable file
0
src/components/Tables/InvoicesRow.js
Normal file → Executable file
0
src/components/Tables/InvoicesRow.js
Normal file → Executable file
0
src/components/Tables/SearchTable1.js
Normal file → Executable file
0
src/components/Tables/SearchTable1.js
Normal file → Executable file
0
src/components/Tables/SearchTable2.js
Normal file → Executable file
0
src/components/Tables/SearchTable2.js
Normal file → Executable file
0
src/components/Tables/TablesProjectRow.js
Normal file → Executable file
0
src/components/Tables/TablesProjectRow.js
Normal file → Executable file
0
src/components/Tables/TablesReportsRow.js
Normal file → Executable file
0
src/components/Tables/TablesReportsRow.js
Normal file → Executable file
0
src/components/Tables/TablesTableRow.js
Normal file → Executable file
0
src/components/Tables/TablesTableRow.js
Normal file → Executable file
0
src/components/Tables/TimelineRow.js
Normal file → Executable file
0
src/components/Tables/TimelineRow.js
Normal file → Executable file
0
src/components/Tables/TransactionRow.js
Normal file → Executable file
0
src/components/Tables/TransactionRow.js
Normal file → Executable file
0
src/components/VisxPieChart/VisxPieChart.js
Normal file → Executable file
0
src/components/VisxPieChart/VisxPieChart.js
Normal file → Executable file
570
src/components/contexts/AuthContext.js
Normal file
570
src/components/contexts/AuthContext.js
Normal file
@@ -0,0 +1,570 @@
|
||||
// src/contexts/AuthContext.js - Session版本
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
|
||||
// API基础URL配置
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL || "http://49.232.185.254:5000";
|
||||
|
||||
// 创建认证上下文
|
||||
const AuthContext = createContext();
|
||||
|
||||
// 自定义Hook
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// 认证提供者组件
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
// 检查Session状态
|
||||
const checkSession = async () => {
|
||||
try {
|
||||
console.log('🔍 检查Session状态...');
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include', // 重要:包含cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Session检查失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('📦 Session数据:', data);
|
||||
|
||||
if (data.isAuthenticated && data.user) {
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Session检查错误:', error);
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化时检查Session
|
||||
useEffect(() => {
|
||||
checkSession();
|
||||
}, []);
|
||||
|
||||
// 监听路由变化,检查session(处理微信登录回调)
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
// 如果是从微信回调返回的,重新检查session
|
||||
if (window.location.pathname === '/home' && !isAuthenticated) {
|
||||
checkSession();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
return () => window.removeEventListener('popstate', handleRouteChange);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// 更新本地用户的便捷方法
|
||||
const updateUser = (partial) => {
|
||||
setUser((prev) => ({ ...(prev || {}), ...partial }));
|
||||
};
|
||||
|
||||
// 传统登录方法
|
||||
const login = async (credential, password, loginType = 'email') => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log('🔐 开始登录流程:', { credential, loginType });
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('password', password);
|
||||
|
||||
if (loginType === 'username') {
|
||||
formData.append('username', credential);
|
||||
} else if (loginType === 'email') {
|
||||
formData.append('email', credential);
|
||||
} else if (loginType === 'phone') {
|
||||
formData.append('username', credential);
|
||||
}
|
||||
|
||||
console.log('📤 发送登录请求到:', `${API_BASE_URL}/api/auth/login`);
|
||||
console.log('📝 请求数据:', {
|
||||
credential,
|
||||
loginType,
|
||||
formData: formData.toString()
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
credentials: 'include', // 包含cookie
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('📨 响应状态:', response.status, response.statusText);
|
||||
console.log('📨 响应头:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
// 获取响应文本,然后尝试解析JSON
|
||||
const responseText = await response.text();
|
||||
console.log('📨 响应原始内容:', responseText);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
console.log('✅ JSON解析成功:', data);
|
||||
} catch (parseError) {
|
||||
console.error('❌ JSON解析失败:', parseError);
|
||||
console.error('📄 响应内容:', responseText);
|
||||
throw new Error(`服务器响应格式错误: ${responseText.substring(0, 100)}...`);
|
||||
}
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '登录失败');
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
toast({
|
||||
title: "登录成功",
|
||||
description: "欢迎回来!",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 登录错误:', error);
|
||||
|
||||
toast({
|
||||
title: "登录失败",
|
||||
description: error.message || "请检查您的登录信息",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 注册方法
|
||||
const register = async (username, email, password) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', username);
|
||||
formData.append('email', email);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '注册失败');
|
||||
}
|
||||
|
||||
// 注册成功后自动登录
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
toast({
|
||||
title: "注册成功",
|
||||
description: "欢迎加入价值前沿!",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('注册错误:', error);
|
||||
|
||||
toast({
|
||||
title: "注册失败",
|
||||
description: error.message || "注册失败,请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 手机号注册
|
||||
const registerWithPhone = async (phone, code, username, password) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/register/phone`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
phone,
|
||||
code,
|
||||
username,
|
||||
password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '注册失败');
|
||||
}
|
||||
|
||||
// 注册成功后自动登录
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
toast({
|
||||
title: "注册成功",
|
||||
description: "欢迎加入价值前沿!",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('手机注册错误:', error);
|
||||
|
||||
toast({
|
||||
title: "注册失败",
|
||||
description: error.message || "注册失败,请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 邮箱注册
|
||||
const registerWithEmail = async (email, code, username, password) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/register/email`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code,
|
||||
username,
|
||||
password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '注册失败');
|
||||
}
|
||||
|
||||
// 注册成功后自动登录
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
toast({
|
||||
title: "注册成功",
|
||||
description: "欢迎加入价值前沿!",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('邮箱注册错误:', error);
|
||||
|
||||
toast({
|
||||
title: "注册失败",
|
||||
description: error.message || "注册失败,请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 发送手机验证码
|
||||
const sendSmsCode = async (phone) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/send-sms-code`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ phone })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '发送失败');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "验证码已发送",
|
||||
description: "请查收短信",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('SMS code error:', error);
|
||||
|
||||
toast({
|
||||
title: "发送失败",
|
||||
description: error.message || "请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// 发送邮箱验证码
|
||||
const sendEmailCode = async (email) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/send-email-code`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '发送失败');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "验证码已发送",
|
||||
description: "请查收邮件",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('Email code error:', error);
|
||||
|
||||
toast({
|
||||
title: "发送失败",
|
||||
description: error.message || "请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// 登出方法
|
||||
const logout = async () => {
|
||||
try {
|
||||
// 调用后端登出API
|
||||
await fetch(`${API_BASE_URL}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
// 清除本地状态
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
|
||||
toast({
|
||||
title: "已登出",
|
||||
description: "您已成功退出登录",
|
||||
status: "info",
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 跳转到登录页面
|
||||
navigate('/auth/signin');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
// 即使API调用失败也清除本地状态
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
navigate('/auth/signin');
|
||||
}
|
||||
};
|
||||
|
||||
// 检查用户是否有特定权限
|
||||
const hasRole = (role) => {
|
||||
return user && user.role === role;
|
||||
};
|
||||
|
||||
// 检查用户订阅权限
|
||||
const hasSubscriptionPermission = async (featureName) => {
|
||||
try {
|
||||
// 如果用户未登录,返回免费权限
|
||||
if (!isAuthenticated) {
|
||||
const freePermissions = {
|
||||
'related_stocks': false,
|
||||
'related_concepts': false,
|
||||
'transmission_chain': false,
|
||||
'historical_events': 'limited'
|
||||
};
|
||||
return freePermissions[featureName] || false;
|
||||
}
|
||||
|
||||
// 获取用户权限信息
|
||||
const response = await fetch(`${API_BASE_URL}/api/subscription/permissions`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取权限信息失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
return data.data.permissions[featureName] || false;
|
||||
}
|
||||
|
||||
// 如果API调用失败,返回默认权限
|
||||
const defaultPermissions = {
|
||||
'related_stocks': user?.subscription_type === 'pro' || user?.subscription_type === 'max',
|
||||
'related_concepts': user?.subscription_type === 'pro' || user?.subscription_type === 'max',
|
||||
'transmission_chain': user?.subscription_type === 'max',
|
||||
'historical_events': user?.subscription_type === 'pro' || user?.subscription_type === 'max' ? 'full' : 'limited'
|
||||
};
|
||||
|
||||
return defaultPermissions[featureName] || false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查订阅权限失败:', error);
|
||||
|
||||
// 降级处理:基于用户现有的订阅信息
|
||||
const fallbackPermissions = {
|
||||
'related_stocks': user?.subscription_type === 'pro' || user?.subscription_type === 'max',
|
||||
'related_concepts': user?.subscription_type === 'pro' || user?.subscription_type === 'max',
|
||||
'transmission_chain': user?.subscription_type === 'max',
|
||||
'historical_events': user?.subscription_type === 'pro' || user?.subscription_type === 'max' ? 'full' : 'limited'
|
||||
};
|
||||
|
||||
return fallbackPermissions[featureName] || false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户订阅级别
|
||||
const getSubscriptionLevel = () => {
|
||||
if (!isAuthenticated) return 'free';
|
||||
return user?.subscription_type || 'free';
|
||||
};
|
||||
|
||||
// 检查是否需要升级订阅
|
||||
const requiresUpgrade = (requiredLevel) => {
|
||||
const currentLevel = getSubscriptionLevel();
|
||||
const levels = { 'free': 0, 'pro': 1, 'max': 2 };
|
||||
|
||||
const currentLevelValue = levels[currentLevel] || 0;
|
||||
const requiredLevelValue = levels[requiredLevel] || 0;
|
||||
|
||||
return currentLevelValue < requiredLevelValue;
|
||||
};
|
||||
|
||||
// 刷新session(可选)
|
||||
const refreshSession = async () => {
|
||||
await checkSession();
|
||||
};
|
||||
|
||||
// 提供给子组件的值
|
||||
const value = {
|
||||
user,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
updateUser,
|
||||
login,
|
||||
register,
|
||||
registerWithPhone,
|
||||
registerWithEmail,
|
||||
sendSmsCode,
|
||||
sendEmailCode,
|
||||
logout,
|
||||
hasRole,
|
||||
hasSubscriptionPermission,
|
||||
getSubscriptionLevel,
|
||||
requiresUpgrade,
|
||||
refreshSession,
|
||||
checkSession
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
20
src/components/contexts/SidebarContext.js
Normal file
20
src/components/contexts/SidebarContext.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Argon Dashboard Chakra PRO - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
|
||||
|
||||
* Designed and Coded by Simmmple & Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
|
||||
import { createContext } from "react";
|
||||
|
||||
export const SidebarContext = createContext();
|
||||
Reference in New Issue
Block a user