v2.2
This commit is contained in:
20
ecosystem.config.js
Normal file
20
ecosystem.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'nextjs-app', // 应用名称
|
||||
script: 'npm', // 使用 npm 脚本
|
||||
args: 'run start', // 启动应用的命令
|
||||
cwd: './', // 项目的路径
|
||||
watch: false, // 是否监控文件变化
|
||||
env: {
|
||||
NODE_ENV: 'production', // 生产环境变量
|
||||
},
|
||||
// 在启动前执行构建
|
||||
exec_mode: 'fork', // 使用 fork 模式
|
||||
instances: 1, // 启动的实例数量
|
||||
autorestart: true, // 自动重启
|
||||
max_memory_restart: '1G', // 内存超过 1G 时自动重启
|
||||
pre_start: 'npm run build', // 启动前执行构建
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export default function Page() {
|
||||
className="absolute top-0 left-0 w-full h-full object-cover z-[-1]"
|
||||
>
|
||||
<source
|
||||
src="http://file.typeframes.com.cn/video_2024-09-16 11_18_33.webm"
|
||||
src="https://file.typeframes.com.cn/video_2024-09-16 11_18_33.webm"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
@@ -28,41 +28,41 @@ export default function CreateTabs() {
|
||||
value: "text",
|
||||
component: <TextVideo />,
|
||||
videoTemp:
|
||||
"http://file.typeframes.com.cn/img-to-video/cn/demo_video.webm",
|
||||
"https://file.typeframes.com.cn/img-to-video/cn/demo_video.webm",
|
||||
},
|
||||
{
|
||||
name: t("aiImageVideoGenerator"),
|
||||
value: "image",
|
||||
component: <ImageVideo />,
|
||||
videoTemp:
|
||||
"http://file.typeframes.com.cn/img-to-video/cn/demo_image.webm",
|
||||
"https://file.typeframes.com.cn/img-to-video/cn/demo_image.webm",
|
||||
},
|
||||
{
|
||||
name: t("aiTiktokVideoGenerator"),
|
||||
value: "tiktok",
|
||||
component: <Tiktok />,
|
||||
videoTemp:
|
||||
"http://file.typeframes.com.cn/create-tiktok-video/en/demo_video.webm",
|
||||
"https://file.typeframes.com.cn/create-tiktok-video/en/demo_video.webm",
|
||||
ImageTemp:
|
||||
"http://file.typeframes.com.cn/create-tiktok-video/en/demo_demo.webm",
|
||||
"https://file.typeframes.com.cn/create-tiktok-video/en/demo_demo.webm",
|
||||
},
|
||||
{
|
||||
name: t("aiTalkingAvatarVideoCreator"),
|
||||
value: "avatar",
|
||||
component: <Avatar />,
|
||||
videoTemp:
|
||||
"http://file.typeframes.com.cn/create-Avatar-video/en/demo_video.webm",
|
||||
"https://file.typeframes.com.cn/create-Avatar-video/en/demo_video.webm",
|
||||
ImageTemp:
|
||||
"http://file.typeframes.com.cn/create-Avatar-video/en/demo_video.webm",
|
||||
"https://file.typeframes.com.cn/create-Avatar-video/en/demo_video.webm",
|
||||
},
|
||||
{
|
||||
name: t("aiMusicVideoGenerator"),
|
||||
value: "music",
|
||||
component: <Music />,
|
||||
videoTemp:
|
||||
"http://file.typeframes.com.cn/music-to-video/cn/demo_video.webm",
|
||||
"https://file.typeframes.com.cn/music-to-video/cn/demo_video.webm",
|
||||
ImageTemp:
|
||||
"http://file.typeframes.com.cn/music-to-video/cn/demo_video.webm",
|
||||
"https://file.typeframes.com.cn/music-to-video/cn/demo_video.webm",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -12,275 +12,296 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
interface LoginFormProps {
|
||||
toRegister: () => void;
|
||||
toRegister: () => void;
|
||||
}
|
||||
|
||||
type LoginMethod = "password" | "code";
|
||||
|
||||
export default function LoginForm({ toRegister }: LoginFormProps) {
|
||||
const t = useTranslations("loginForm");
|
||||
const tTerms = useTranslations("terms"); // 用于获取用户协议的标题
|
||||
const tGlobal = useTranslations(); // 用于获取全局翻译,如 "agreeTo" 和 "agreeToTerms"
|
||||
const [loginMethod, setLoginMethod] = useState<LoginMethod>("password");
|
||||
const [username, setUsername] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [captchaTimer, setCaptchaTimer] = useState(0);
|
||||
const [agreed, setAgreed] = useState(true); // 默认勾选
|
||||
const t = useTranslations("loginForm");
|
||||
const tTerms = useTranslations("terms"); // 用于获取用户协议的标题
|
||||
const tGlobal = useTranslations(); // 用于获取全局翻译,如 "agreeTo" 和 "agreeToTerms"
|
||||
const [loginMethod, setLoginMethod] = useState<LoginMethod>("password");
|
||||
const [username, setUsername] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [captchaTimer, setCaptchaTimer] = useState(0);
|
||||
const [agreed, setAgreed] = useState(true); // 默认勾选
|
||||
|
||||
const router = useRouter();
|
||||
const { addToast } = useToast();
|
||||
const router = useRouter();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
|
||||
const {
|
||||
fetchData: login,
|
||||
loading: loginLoading,
|
||||
error: loginError,
|
||||
data: loginData,
|
||||
} = useFetch({
|
||||
url: "/api/login/",
|
||||
method: "POST",
|
||||
});
|
||||
const {
|
||||
fetchData: login,
|
||||
loading: loginLoading,
|
||||
error: loginError,
|
||||
data: loginData,
|
||||
} = useFetch({
|
||||
url: "/api/login/",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const {
|
||||
fetchData: fetchCaptcha,
|
||||
loading: captchaLoading,
|
||||
error: captchaError,
|
||||
data: captchaData,
|
||||
} = useFetch({
|
||||
url: "/api/send-verification-sms/",
|
||||
method: "POST",
|
||||
});
|
||||
const {
|
||||
fetchData: GetUserinfo,
|
||||
loading: userinfoLoading,
|
||||
data: userinfoData,
|
||||
} = useFetch({
|
||||
url: "/api/profile/",
|
||||
method: "POST",
|
||||
});
|
||||
const {
|
||||
fetchData: fetchCaptcha,
|
||||
loading: captchaLoading,
|
||||
error: captchaError,
|
||||
data: captchaData,
|
||||
} = useFetch({
|
||||
url: "/api/send-verification-sms/",
|
||||
method: "POST",
|
||||
});
|
||||
const {
|
||||
fetchData: GetUserinfo,
|
||||
loading: userinfoLoading,
|
||||
data: userinfoData,
|
||||
} = useFetch({
|
||||
url: "/api/profile/",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (captchaTimer > 0) {
|
||||
timer = setInterval(() => {
|
||||
setCaptchaTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [captchaTimer]);
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (captchaTimer > 0) {
|
||||
timer = setInterval(() => {
|
||||
setCaptchaTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [captchaTimer]);
|
||||
|
||||
const handleGetCaptcha = async () => {
|
||||
if (!phone) {
|
||||
addToast(t("enterPhoneNumber"), "error");
|
||||
return;
|
||||
}
|
||||
await fetchCaptcha({ phone_number: phone });
|
||||
setCaptchaTimer(60);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!agreed) {
|
||||
addToast(tGlobal("agreeToTerms"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验非空字段
|
||||
if (loginMethod === "password" && !username) {
|
||||
addToast(t("enterUsername"), "error");
|
||||
return;
|
||||
}
|
||||
if (loginMethod === "code" && !phone) {
|
||||
addToast(t("enterPhone"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginMethod === "password" && !password) {
|
||||
addToast(t("enterPassword"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginMethod === "code" && !captcha) {
|
||||
addToast(t("enterCaptcha"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const loginData = {
|
||||
login_type: loginMethod,
|
||||
...(loginMethod === "password" && { username, password }),
|
||||
...(loginMethod === "code" && { phone, code: captcha }),
|
||||
const handleGetCaptcha = async () => {
|
||||
if (!phone) {
|
||||
addToast(t("enterPhoneNumber"), "error");
|
||||
return;
|
||||
}
|
||||
await fetchCaptcha({ phone_number: phone });
|
||||
setCaptchaTimer(60);
|
||||
};
|
||||
|
||||
// 调用登录函数发送登录请求
|
||||
login(loginData).then(() => {
|
||||
addToast(t("loginSuccess"), "success");
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
GetUserinfo().then((res) => {
|
||||
console.log("userinfo", res);
|
||||
setUser(res);
|
||||
});
|
||||
router.replace("/create");
|
||||
});
|
||||
};
|
||||
if (!agreed) {
|
||||
addToast(tGlobal("agreeToTerms"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-[25px] py-8 bg-black/80 rounded-lg shadow-lg px-8 w-[500px]">
|
||||
<div className="mt-auto w-full items-center justify-stretch flex flex-col gap-1.5">
|
||||
<GoogleLogin />
|
||||
<div className="z-0 w-[80%] relative flex justify-center items-center my-3">
|
||||
<small className="font-normal text-xs bg-transparent text-gray-300 px-2.5 py-1 tracking-wide border-0 border-t border-gray-600 leading-none">
|
||||
{t("orUseLoginMethod")}
|
||||
</small>
|
||||
<div className="z-[-1] absolute h-[1px] w-full bg-info opacity-50"></div>
|
||||
// 校验非空字段
|
||||
if (loginMethod === "password" && !username) {
|
||||
addToast(t("enterUsername"), "error");
|
||||
return;
|
||||
}
|
||||
if (loginMethod === "code" && !phone) {
|
||||
addToast(t("enterPhone"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginMethod === "password" && !password) {
|
||||
addToast(t("enterPassword"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginMethod === "code" && !captcha) {
|
||||
addToast(t("enterCaptcha"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const loginData = {
|
||||
login_type: loginMethod,
|
||||
...(loginMethod === "password" && { username, password }),
|
||||
...(loginMethod === "code" && { phone, code: captcha }),
|
||||
};
|
||||
|
||||
// 调用登录函数发送登录请求
|
||||
login(loginData).then(() => {
|
||||
addToast(t("loginSuccess"), "success");
|
||||
|
||||
GetUserinfo().then((res) => {
|
||||
console.log("userinfo", res);
|
||||
setUser(res);
|
||||
});
|
||||
router.replace("/create");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-[25px] py-8 bg-black/80 rounded-lg shadow-lg px-8 w-[full] sm:w-[500px]">
|
||||
<div className="mt-auto w-full items-center justify-stretch flex flex-col gap-1.5">
|
||||
<GoogleLogin />
|
||||
<div className="z-0 w-[80%] relative flex justify-center items-center my-3">
|
||||
<small className="font-normal text-xs bg-transparent text-gray-300 px-2.5 py-1 tracking-wide border-0 border-t border-gray-600 leading-none">
|
||||
{t("orUseLoginMethod")}
|
||||
</small>
|
||||
<div className="z-[-1] absolute h-[1px] w-full bg-info opacity-50"></div>
|
||||
</div>
|
||||
<Form.Root className="w-full" onSubmit={handleSubmit}>
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
<Form.Field name="loginMethod">
|
||||
<RadioGroup
|
||||
value={loginMethod}
|
||||
onChange={setLoginMethod}
|
||||
aria-label="Login method"
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio
|
||||
id="password"
|
||||
value="password"
|
||||
className="cursor-pointer group flex size-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||||
>
|
||||
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
</Radio>
|
||||
<Form.Label
|
||||
htmlFor="password"
|
||||
className="text-gray-300"
|
||||
>
|
||||
{t("passwordLogin")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio
|
||||
id="code"
|
||||
value="code"
|
||||
className="cursor-pointer group flex size-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||||
>
|
||||
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
</Radio>
|
||||
<Form.Label
|
||||
htmlFor="code"
|
||||
className="text-gray-300"
|
||||
>
|
||||
{t("codeLogin")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</Form.Field>
|
||||
|
||||
{loginMethod === "password" && (
|
||||
<>
|
||||
<Form.Field name="username">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) =>
|
||||
setUsername(e.target.value)
|
||||
}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("emailOrPhone")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field name="password">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) =>
|
||||
setPassword(e.target.value)
|
||||
}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("yourPassword")}
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{loginMethod === "code" && (
|
||||
<>
|
||||
<Form.Field name="phone">
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) =>
|
||||
setPhone(e.target.value)
|
||||
}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("phone")}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field name="captcha">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="captcha"
|
||||
value={captcha}
|
||||
onChange={(e) =>
|
||||
setCaptcha(e.target.value)
|
||||
}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("captcha")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetCaptcha}
|
||||
className="btn h-10 btn-sm w-32 ml-2 bg-transparent hover:bg-green-500 hover:text-white text-gray-500 rounded-lg"
|
||||
disabled={
|
||||
captchaTimer > 0 ||
|
||||
captchaLoading
|
||||
}
|
||||
>
|
||||
{captchaLoading ? (
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
) : captchaTimer > 0 ? (
|
||||
`${captchaTimer}s`
|
||||
) : (
|
||||
t("getCaptcha")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form.Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 用户协议复选框 */}
|
||||
<Form.Field name="agreement">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center text-gray-300 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={(e) =>
|
||||
setAgreed(e.target.checked)
|
||||
}
|
||||
className="h-4 w-4 text-green-500 bg-gray-700 border-gray-600 rounded focus:ring-green-500"
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{tGlobal("agreeTo")}{" "}
|
||||
<Link
|
||||
href="/terms"
|
||||
className="text-green-500 underline"
|
||||
>
|
||||
{tTerms("title")}
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Submit
|
||||
disabled={loginLoading}
|
||||
className="btn bg-transparent text-gray-200 w-full rounded-lg hover:text-green-500 hover:bg-secondary/20"
|
||||
>
|
||||
{loginLoading ? (
|
||||
<span className="loading loading-spinner loading-md text-gray-200"></span>
|
||||
) : (
|
||||
<span>{t("login")}</span>
|
||||
)}
|
||||
</Form.Submit>
|
||||
</div>
|
||||
</Form.Root>
|
||||
|
||||
<button
|
||||
onClick={toRegister}
|
||||
className="btn btn-outline btn-neutral whitespace-nowrap z-0 text-sm font-medium w-full mt-3 bg-transparent border-gray-500 text-gray-200 hover:bg-gray-700"
|
||||
>
|
||||
{t("register")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Root className="w-full" onSubmit={handleSubmit}>
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
<Form.Field name="loginMethod">
|
||||
<RadioGroup
|
||||
value={loginMethod}
|
||||
onChange={setLoginMethod}
|
||||
aria-label="Login method"
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio
|
||||
id="password"
|
||||
value="password"
|
||||
className="cursor-pointer group flex size-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||||
>
|
||||
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
</Radio>
|
||||
<Form.Label htmlFor="password" className="text-gray-300">
|
||||
{t("passwordLogin")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio
|
||||
id="code"
|
||||
value="code"
|
||||
className="cursor-pointer group flex size-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||||
>
|
||||
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
</Radio>
|
||||
<Form.Label htmlFor="code" className="text-gray-300">
|
||||
{t("codeLogin")}
|
||||
</Form.Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</Form.Field>
|
||||
|
||||
{loginMethod === "password" && (
|
||||
<>
|
||||
<Form.Field name="username">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("emailOrPhone")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field name="password">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("yourPassword")}
|
||||
/>
|
||||
</Form.Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{loginMethod === "code" && (
|
||||
<>
|
||||
<Form.Field name="phone">
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("phone")}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field name="captcha">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="captcha"
|
||||
value={captcha}
|
||||
onChange={(e) => setCaptcha(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 outline-none focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder:text-gray-500"
|
||||
placeholder={t("captcha")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetCaptcha}
|
||||
className="btn h-10 btn-sm w-32 ml-2 bg-transparent hover:bg-green-500 hover:text-white text-gray-500 rounded-lg"
|
||||
disabled={captchaTimer > 0 || captchaLoading}
|
||||
>
|
||||
{captchaLoading ? (
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
) : captchaTimer > 0 ? (
|
||||
`${captchaTimer}s`
|
||||
) : (
|
||||
t("getCaptcha")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form.Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 用户协议复选框 */}
|
||||
<Form.Field name="agreement">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center text-gray-300 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={(e) => setAgreed(e.target.checked)}
|
||||
className="h-4 w-4 text-green-500 bg-gray-700 border-gray-600 rounded focus:ring-green-500"
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{tGlobal("agreeTo")}{" "}
|
||||
<Link href="/terms" className="text-green-500 underline">
|
||||
{tTerms("title")}
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Submit
|
||||
disabled={loginLoading}
|
||||
className="btn bg-transparent hover:bg-transparent text-gray-500 w-full rounded-lg hover:text-green-500"
|
||||
>
|
||||
{t("login")}
|
||||
{loginLoading ? (
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Form.Submit>
|
||||
</div>
|
||||
</Form.Root>
|
||||
|
||||
<button
|
||||
onClick={toRegister}
|
||||
className="btn btn-outline btn-neutral whitespace-nowrap z-0 text-sm font-medium w-full mt-3 bg-transparent border-gray-500 text-gray-300 hover:bg-gray-700"
|
||||
>
|
||||
{t("register")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
interface RegisterFormProps {
|
||||
back: () => void;
|
||||
back: () => void;
|
||||
}
|
||||
|
||||
const plans: Plan[] = ["phone", "email"];
|
||||
@@ -19,283 +19,296 @@ const plans: Plan[] = ["phone", "email"];
|
||||
type Plan = "phone" | "email";
|
||||
|
||||
export default function RegisterForm({ back }: RegisterFormProps) {
|
||||
const t = useTranslations("registerForm");
|
||||
const tTerms = useTranslations("terms"); // 用于获取用户协议的标题
|
||||
const tGlobal = useTranslations(); // 用于获取全局翻译,如 "agreeTo" 和 "agreeToTerms"
|
||||
const [selected, setSelected] = useState<Plan>(plans[0]);
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [captchaTimer, setCaptchaTimer] = useState(0);
|
||||
const [agreed, setAgreed] = useState(true); // 默认勾选
|
||||
const t = useTranslations("registerForm");
|
||||
const tTerms = useTranslations("terms"); // 用于获取用户协议的标题
|
||||
const tGlobal = useTranslations(); // 用于获取全局翻译,如 "agreeTo" 和 "agreeToTerms"
|
||||
const [selected, setSelected] = useState<Plan>(plans[0]);
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [captchaTimer, setCaptchaTimer] = useState(0);
|
||||
const [agreed, setAgreed] = useState(true); // 默认勾选
|
||||
|
||||
const { addToast } = useToast();
|
||||
const { addToast } = useToast();
|
||||
|
||||
// 获取验证码
|
||||
const {
|
||||
fetchData: fetchVerificationCode,
|
||||
data: verificationData,
|
||||
loading: verificationLoading,
|
||||
error: verificationError,
|
||||
} = useFetch({
|
||||
url: "/api/send-verification-sms/",
|
||||
method: "POST",
|
||||
});
|
||||
// 获取验证码
|
||||
const {
|
||||
fetchData: fetchVerificationCode,
|
||||
data: verificationData,
|
||||
loading: verificationLoading,
|
||||
error: verificationError,
|
||||
} = useFetch({
|
||||
url: "/api/send-verification-sms/",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
// 注册
|
||||
const {
|
||||
fetchData: register,
|
||||
data: registerData,
|
||||
loading: registerLoading,
|
||||
error: registerError,
|
||||
} = useFetch({
|
||||
url: "/api/register/",
|
||||
method: "POST",
|
||||
});
|
||||
// 注册
|
||||
const {
|
||||
fetchData: register,
|
||||
data: registerData,
|
||||
loading: registerLoading,
|
||||
error: registerError,
|
||||
} = useFetch({
|
||||
url: "/api/register/",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (captchaTimer > 0) {
|
||||
timer = setInterval(() => {
|
||||
setCaptchaTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [captchaTimer]);
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (captchaTimer > 0) {
|
||||
timer = setInterval(() => {
|
||||
setCaptchaTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [captchaTimer]);
|
||||
|
||||
const handleGetCaptcha = async () => {
|
||||
if (selected === "phone" && !isValidPhoneNumber(phone)) {
|
||||
addToast(t("invalidPhoneNumber"), "error");
|
||||
return;
|
||||
}
|
||||
const handleGetCaptcha = async () => {
|
||||
if (selected === "phone" && !isValidPhoneNumber(phone)) {
|
||||
addToast(t("invalidPhoneNumber"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected === "email" && !email) {
|
||||
addToast(t("enterEmail"), "error");
|
||||
return;
|
||||
}
|
||||
if (selected === "email" && !email) {
|
||||
addToast(t("enterEmail"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data =
|
||||
selected === "phone" ? { phone_number: phone } : { email: email };
|
||||
const data =
|
||||
selected === "phone" ? { phone_number: phone } : { email: email };
|
||||
|
||||
await fetchVerificationCode(data);
|
||||
setCaptchaTimer(60);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (verificationData?.code === 200) {
|
||||
addToast(t("captchaSent"), "success");
|
||||
setCaptchaTimer(60);
|
||||
}
|
||||
}, [verificationData, verificationError]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); // 阻止表单默认提交行为
|
||||
|
||||
if (!agreed) {
|
||||
addToast(tGlobal("agreeToTerms"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验非空字段
|
||||
if (!username || !password || !confirmPassword || !captcha) {
|
||||
addToast(t("completeInformation"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
addToast(t("passwordMismatch"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 email 或 phone 是否有效
|
||||
if (selected === "email" && !email) {
|
||||
addToast(t("enterEmail"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected === "phone" && !phone) {
|
||||
addToast(t("enterPhoneNumber"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建注册请求参数
|
||||
const registerData = {
|
||||
code: captcha,
|
||||
confirm_password: confirmPassword,
|
||||
password,
|
||||
username,
|
||||
registerMethod: selected,
|
||||
...(selected === "email" && { email }), // 仅当 selected 为 "email" 时添加 email 字段
|
||||
...(selected === "phone" && { phone }), // 仅当 selected 为 "phone" 时添加 phone 字段
|
||||
await fetchVerificationCode(data);
|
||||
setCaptchaTimer(60);
|
||||
};
|
||||
|
||||
// 调用 register 函数发送注册请求
|
||||
await register(registerData);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (verificationData?.code === 200) {
|
||||
addToast(t("captchaSent"), "success");
|
||||
setCaptchaTimer(60);
|
||||
}
|
||||
}, [verificationData, verificationError]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-[25px] py-8 bg-black/80 rounded-lg shadow-lg px-8 w-[400px]">
|
||||
<div className="w-full flex items-center justify-between mb-4 ">
|
||||
<button
|
||||
className="text-gray-300 hover:text-white"
|
||||
onClick={back}
|
||||
>
|
||||
<IoIosArrowBack size={24} />
|
||||
</button>
|
||||
<div className="text-xl font-bold text-center text-gray-300">
|
||||
{t("register")}
|
||||
</div>
|
||||
<div className="w-6"></div> {/* 占位符,使标题居中 */}
|
||||
</div>
|
||||
<Form.Root className="w-full" onSubmit={handleSubmit}>
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<Form.Field name="radio">
|
||||
<RadioGroup
|
||||
value={selected}
|
||||
onChange={setSelected}
|
||||
aria-label={t("registrationMethod")}
|
||||
className="flex items-center justify-center gap-6"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div key={plan} className="flex items-center gap-2">
|
||||
<Radio
|
||||
id={plan}
|
||||
value={plan}
|
||||
className="cursor-pointer group flex h-5 w-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||||
>
|
||||
<span className="invisible h-2 w-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
</Radio>
|
||||
<Form.Label htmlFor={plan} className="text-gray-300">
|
||||
{t(plan)}
|
||||
</Form.Label>
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); // 阻止表单默认提交行为
|
||||
|
||||
if (!agreed) {
|
||||
addToast(tGlobal("agreeToTerms"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验非空字段
|
||||
if (!username || !password || !confirmPassword || !captcha) {
|
||||
addToast(t("completeInformation"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
addToast(t("passwordMismatch"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 email 或 phone 是否有效
|
||||
if (selected === "email" && !email) {
|
||||
addToast(t("enterEmail"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected === "phone" && !phone) {
|
||||
addToast(t("enterPhoneNumber"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建注册请求参数
|
||||
const registerData = {
|
||||
code: captcha,
|
||||
confirm_password: confirmPassword,
|
||||
password,
|
||||
username,
|
||||
registerMethod: selected,
|
||||
...(selected === "email" && { email }), // 仅当 selected 为 "email" 时添加 email 字段
|
||||
...(selected === "phone" && { phone }), // 仅当 selected 为 "phone" 时添加 phone 字段
|
||||
};
|
||||
|
||||
// 调用 register 函数发送注册请求
|
||||
await register(registerData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-[25px] py-8 bg-black/80 rounded-lg shadow-lg px-8 w-[full] sm:w-[500px]">
|
||||
<div className="w-full flex items-center justify-between mb-4 ">
|
||||
<button
|
||||
className="text-gray-300 hover:text-white"
|
||||
onClick={back}
|
||||
>
|
||||
<IoIosArrowBack size={24} />
|
||||
</button>
|
||||
<div className="text-xl font-bold text-center text-gray-300">
|
||||
{t("register")}
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Form.Field>
|
||||
|
||||
{selected === "email" && (
|
||||
<Form.Field name="email">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("email")}
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
{selected === "phone" && (
|
||||
<Form.Field name="phone">
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("phoneNumber")}
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
|
||||
{/* 用户名 */}
|
||||
<Form.Field name="username">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("username")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{/* 密码 */}
|
||||
<Form.Field name="password">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("password")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{/* 确认密码 */}
|
||||
<Form.Field name="confirmPassword">
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("confirmPassword")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{/* 验证码 */}
|
||||
<Form.Field name="captcha">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="captcha"
|
||||
value={captcha}
|
||||
onChange={(e) => setCaptcha(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("captcha")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetCaptcha}
|
||||
className="btn h-10 btn-sm w-32 ml-2 bg-transparent hover:bg-green-500 hover:text-white text-gray-500 rounded-lg"
|
||||
disabled={captchaTimer > 0 || verificationLoading}
|
||||
>
|
||||
{verificationLoading ? (
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
) : captchaTimer > 0 ? (
|
||||
`${captchaTimer}s`
|
||||
) : (
|
||||
t("getCode")
|
||||
)}
|
||||
</button>
|
||||
<div className="w-6"></div> {/* 占位符,使标题居中 */}
|
||||
</div>
|
||||
</Form.Field>
|
||||
<Form.Root className="w-full" onSubmit={handleSubmit}>
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<Form.Field name="radio">
|
||||
<RadioGroup
|
||||
value={selected}
|
||||
onChange={setSelected}
|
||||
aria-label={t("registrationMethod")}
|
||||
className="flex items-center justify-center gap-6"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Radio
|
||||
id={plan}
|
||||
value={plan}
|
||||
className="cursor-pointer group flex h-5 w-5 items-center justify-center rounded-full border bg-gray-700 data-[checked]:bg-green-500"
|
||||
>
|
||||
<span className="invisible h-2 w-2 rounded-full bg-white group-data-[checked]:visible" />
|
||||
</Radio>
|
||||
<Form.Label
|
||||
htmlFor={plan}
|
||||
className="text-gray-300"
|
||||
>
|
||||
{t(plan)}
|
||||
</Form.Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Form.Field>
|
||||
|
||||
{/* 用户协议复选框 */}
|
||||
<Form.Field name="agreement">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center text-gray-300 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={(e) => setAgreed(e.target.checked)}
|
||||
className="h-4 w-4 text-green-500 bg-gray-700 border-gray-600 rounded focus:ring-green-500"
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{tGlobal("agreeTo")}{" "}
|
||||
<Link href="/terms" className="text-green-500 underline">
|
||||
{tTerms("title")}
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Form.Field>
|
||||
{selected === "email" && (
|
||||
<Form.Field name="email">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("email")}
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
{selected === "phone" && (
|
||||
<Form.Field name="phone">
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("phoneNumber")}
|
||||
/>
|
||||
</Form.Field>
|
||||
)}
|
||||
|
||||
<Form.Submit
|
||||
disabled={registerLoading}
|
||||
className="btn bg-transparent hover:bg-green-500 text-gray-300 w-full rounded-lg hover:text-white border border-gray-600"
|
||||
>
|
||||
{t("submit")}
|
||||
{registerLoading ? (
|
||||
<span className="loading loading-spinner loading-md ml-2"></span>
|
||||
) : null}
|
||||
</Form.Submit>
|
||||
{/* 用户名 */}
|
||||
<Form.Field name="username">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("username")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{/* 密码 */}
|
||||
<Form.Field name="password">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("password")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{/* 确认密码 */}
|
||||
<Form.Field name="confirmPassword">
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("confirmPassword")}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{/* 验证码 */}
|
||||
<Form.Field name="captcha">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
id="captcha"
|
||||
value={captcha}
|
||||
onChange={(e) => setCaptcha(e.target.value)}
|
||||
className="h-10 input border-gray-600 bg-gray-800 focus:outline-none w-full rounded-lg text-sm text-gray-300 placeholder-gray-500"
|
||||
placeholder={t("captcha")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetCaptcha}
|
||||
className="btn h-10 btn-sm w-32 ml-2 bg-transparent hover:bg-green-500 hover:text-white text-gray-200 rounded-lg"
|
||||
disabled={
|
||||
captchaTimer > 0 || verificationLoading
|
||||
}
|
||||
>
|
||||
{verificationLoading ? (
|
||||
<span className="loading loading-spinner loading-md text-gray-200"></span>
|
||||
) : captchaTimer > 0 ? (
|
||||
`${captchaTimer}s`
|
||||
) : (
|
||||
<span>{t("getCode")}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form.Field>
|
||||
|
||||
{/* 用户协议复选框 */}
|
||||
<Form.Field name="agreement">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center text-gray-300 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={(e) =>
|
||||
setAgreed(e.target.checked)
|
||||
}
|
||||
className="h-4 w-4 text-green-500 bg-gray-700 border-gray-600 rounded focus:ring-green-500"
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{tGlobal("agreeTo")}{" "}
|
||||
<Link
|
||||
href="/terms"
|
||||
className="text-green-500 underline"
|
||||
>
|
||||
{tTerms("title")}
|
||||
</Link>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Submit
|
||||
disabled={registerLoading}
|
||||
className="btn bg-transparent hover:bg-green-500 text-gray-300 w-full rounded-lg hover:text-white border border-gray-600"
|
||||
>
|
||||
{t("submit")}
|
||||
{registerLoading ? (
|
||||
<span className="loading loading-spinner loading-md ml-2"></span>
|
||||
) : null}
|
||||
</Form.Submit>
|
||||
</div>
|
||||
</Form.Root>
|
||||
</div>
|
||||
</Form.Root>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function PageExpect() {
|
||||
loop
|
||||
muted
|
||||
autoPlay
|
||||
src="http://file.typeframes.com.cn/create-tiktok-video/en/demo_image.webm"
|
||||
src="https://file.typeframes.com.cn/create-tiktok-video/en/demo_image.webm"
|
||||
preload="auto"
|
||||
></video>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@ export default function PageExpect() {
|
||||
loop
|
||||
muted
|
||||
autoPlay
|
||||
src="http://file.typeframes.com.cn/music-to-video/cn/demo_image.webm"
|
||||
src="https://file.typeframes.com.cn/music-to-video/cn/demo_image.webm"
|
||||
preload="auto"
|
||||
></video>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user