0.3.4 小图标功能增加
This commit is contained in:
@@ -23,6 +23,7 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
if (!doc) {
|
||||
notFound();
|
||||
}
|
||||
const readOnly = (doc as any).can_edit === false;
|
||||
|
||||
const initialOptions: PageOptionsState = {
|
||||
wideLayout: doc.wide_layout ?? false,
|
||||
@@ -52,6 +53,7 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
initialOptions={initialOptions}
|
||||
initialStats={initialStats}
|
||||
openTableId={openTableId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -12,6 +12,11 @@ export default function WolaiImportPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const parentId = useMemo(() => searchParams.get("parentId") ?? "", [searchParams]);
|
||||
|
||||
// 说明:该页面是工具页,避免开发环境下偶发的 hydration mismatch(SSR 与客户端首屏不一致)。
|
||||
// 首屏统一渲染一个稳定的占位内容,挂载后再渲染真实表单。
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [zipPath, setZipPath] = useState("");
|
||||
const [rootMdPath, setRootMdPath] = useState("");
|
||||
@@ -78,6 +83,19 @@ export default function WolaiImportPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-2xl p-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>导入 Wolai(Markdown ZIP)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">加载中...</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-2xl p-6">
|
||||
<Card>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useConvexAuth } from "convex/react";
|
||||
import { useConvexAuth, useMutation, useQuery } from "convex/react";
|
||||
import { useAuthActions } from "@convex-dev/auth/react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
type AuthStep = "signIn" | "signUp";
|
||||
|
||||
@@ -24,18 +25,24 @@ const TEST_CREDENTIALS = {
|
||||
export default function AuthPage() {
|
||||
const { isLoading, isAuthenticated } = useConvexAuth();
|
||||
const { signIn } = useAuthActions();
|
||||
const setMyUsername = useMutation(api.users.setMyUsername);
|
||||
const router = useRouter();
|
||||
|
||||
const currentUser = useQuery(api.users.currentUser, isAuthenticated ? {} : "skip");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && isAuthenticated) {
|
||||
if (isLoading) return;
|
||||
if (!isAuthenticated) return;
|
||||
if (currentUser === undefined) return;
|
||||
if (currentUser && currentUser.name) {
|
||||
router.replace("/");
|
||||
}
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
}, [currentUser, isAuthenticated, isLoading, router]);
|
||||
|
||||
const [flow, setFlow] = useState<AuthStep>("signIn");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [message, setMessage] = useState<{
|
||||
type: "success" | "error" | "info";
|
||||
text: string;
|
||||
@@ -46,17 +53,28 @@ export default function AuthPage() {
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
if (flow === "signUp" && !username.trim()) {
|
||||
setMessage({ type: "error", text: "用户名不能为空" });
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("email", email);
|
||||
formData.append("password", password);
|
||||
if (flow === "signUp") {
|
||||
formData.append("name", name);
|
||||
}
|
||||
formData.append("flow", flow);
|
||||
|
||||
const result = await signIn("password", formData);
|
||||
|
||||
if (result.signingIn) {
|
||||
if (flow === "signUp") {
|
||||
try {
|
||||
await setMyUsername({ username });
|
||||
} catch (e: any) {
|
||||
// 说明:注册已成功,但用户名可能重复;此时保持登录状态,转入“设置用户名”步骤继续处理。
|
||||
setMessage({ type: "error", text: e?.message ?? "用户名设置失败,请重试" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
setMessage({ type: "success", text: flow === "signIn" ? "登录成功!" : "注册成功!" });
|
||||
setTimeout(() => router.push("/"), 500);
|
||||
return;
|
||||
@@ -73,7 +91,7 @@ export default function AuthPage() {
|
||||
} catch (error: any) {
|
||||
setMessage({ type: "error", text: error.message || "操作失败,请重试" });
|
||||
}
|
||||
}, [name, signIn, router]);
|
||||
}, [router, setMyUsername, signIn, username]);
|
||||
|
||||
const handleSubmit = useCallback(async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -104,7 +122,7 @@ export default function AuthPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (isAuthenticated && currentUser && currentUser.name) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
@@ -115,6 +133,75 @@ export default function AuthPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (currentUser === undefined) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const saveUsername = async () => {
|
||||
setMessage(null);
|
||||
try {
|
||||
await setMyUsername({ username });
|
||||
setMessage({ type: "success", text: "用户名已保存!" });
|
||||
router.replace("/");
|
||||
} catch (error: any) {
|
||||
setMessage({ type: "error", text: error.message || "保存失败,请重试" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">设置用户名</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">用于分享功能的用户查找(全局唯一)</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`rounded-md p-4 ${
|
||||
message.type === "success" ? "bg-green-50 text-green-800" :
|
||||
message.type === "error" ? "bg-red-50 text-red-800" :
|
||||
"bg-blue-50 text-blue-800"
|
||||
}`}>
|
||||
<p className="text-sm">{message.text}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="sr-only">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="用户名(2-32 位,不含空格)"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveUsername()}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
保存并继续
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
@@ -155,18 +242,19 @@ export default function AuthPage() {
|
||||
</div>
|
||||
{flow === "signUp" && (
|
||||
<div>
|
||||
<label htmlFor="name" className="sr-only">姓名</label>
|
||||
<label htmlFor="username" className="sr-only">用户名</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="姓名(可选)"
|
||||
/>
|
||||
</div>
|
||||
autoComplete="username"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="用户名(2-32 位,不含空格)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">密码</label>
|
||||
|
||||
Reference in New Issue
Block a user