master
lee 2025-05-30 18:42:56 +08:00
parent c1f10af49f
commit 2eef9619c2
45 changed files with 361 additions and 260 deletions

View File

@ -1,7 +1,7 @@
import type { AppConfig } from "@/libs/types"; import type { AppConfig } from "@/libs/types";
export const appConfig: AppConfig = { export const appConfig: AppConfig = {
baseUrl: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000", baseUrl: process.env.NEXT_PUBLIC_APP_URL || "http://127.0.0.1:3001/",
apiUrl: apiUrl:
process.env.NEXT_PUBLIC_APi_URL || `${process.env.NEXT_PUBLIC_APP_URL}/api`, process.env.NEXT_PUBLIC_APi_URL || `${process.env.NEXT_PUBLIC_APP_URL}/api`,
}; };

View File

@ -4,11 +4,11 @@ import { app } from "./server/main";
serve( serve(
{ {
fetch: app.fetch, fetch: app.fetch,
port: 4000, port: 3001,
hostname: "localhost", hostname: "localhost",
}, },
async () => { async () => {
console.log("Server started on http://localhost:4000/api/"); console.log("Server started on http://localhost:3001/api/");
}, },
).listen(); ).listen();

View File

@ -1,6 +1,7 @@
import { zValidator } from "@/middleware/zValidator.middleware";
import { z } from "zod"; import { z } from "zod";
import { zValidator } from "@/middleware/zValidator.middleware";
import { createErrorResult, createHonoApp } from "../common/utils"; import { createErrorResult, createHonoApp } from "../common/utils";
import { createCategorySchema } from "./schema"; import { createCategorySchema } from "./schema";
import { import {

View File

@ -1,6 +1,7 @@
import db from "@/libs/db/prismaClient";
import { isNil } from "lodash"; import { isNil } from "lodash";
import db from "@/libs/db/prismaClient";
import type { CategoryItem, CreateCategoryParams } from "./type"; import type { CategoryItem, CreateCategoryParams } from "./type";
/** /**

View File

@ -1,4 +1,3 @@
import { getCookie } from "hono/cookie";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { authApi, categoryApi, postApi, tagApi } from "."; import { authApi, categoryApi, postApi, tagApi } from ".";
@ -12,11 +11,6 @@ const routes = app
.route("/auth", authApi) .route("/auth", authApi)
.route("/categories", categoryApi); .route("/categories", categoryApi);
app.use("*", cors()); app.use("*", cors());
app.use("*", async (c, next) => {
const allCookies = getCookie(c);
console.log(allCookies, "cookies111");
await next();
});
type AppType = typeof routes; type AppType = typeof routes;

View File

@ -1,7 +1,6 @@
import { isNil } from "lodash"; import { isNil } from "lodash";
import db from "@/libs/db/prismaClient"; import db from "@/libs/db/prismaClient";
import { getUserMiddleware } from "@/middleware/user.middleware";
import { zValidator } from "@/middleware/zValidator.middleware"; import { zValidator } from "@/middleware/zValidator.middleware";
import { createErrorResult, createHonoApp } from "../common/utils"; import { createErrorResult, createHonoApp } from "../common/utils";
@ -12,16 +11,11 @@ import {
postPagesNumberRequestQuerySchema, postPagesNumberRequestQuerySchema,
postPaginateRequestQuerySchema, postPaginateRequestQuerySchema,
} from "./schema"; } from "./schema";
import { import { createPostItem, queryPostPaginate } from "./service";
createPostItem,
getPostByTag,
isSlugUnique,
queryPostPaginate,
} from "./service";
const app = createHonoApp(); const app = createHonoApp();
export const postApi = app export const postApi = app
.use("*", getUserMiddleware)
.get("/", async (c) => { .get("/", async (c) => {
const q = c.req.query(); const q = c.req.query();
const validated = postPaginateRequestQuerySchema.safeParse(q); const validated = postPaginateRequestQuerySchema.safeParse(q);
@ -137,20 +131,9 @@ export const postApi = app
) )
: c.json(createErrorResult("参数错误"), 400); : c.json(createErrorResult("参数错误"), 400);
}) })
.post( .post("/", zValidator("json", getPostItemRequestSchema), async (c) => {
"/", const res = c.req.valid("json");
zValidator("json", getPostItemRequestSchema(isSlugUnique())), console.log(res, "res");
async (c) => { const post = await createPostItem(res);
const res = c.req.valid("json");
return createPostItem(res)
.then((result) => c.json(result, 200))
.catch((error) =>
c.json(createErrorResult("服务器内部错误", error), 500),
);
},
)
.get("/test/:tag", async (c) => {
const { tag } = c.req.param();
const post = await getPostByTag(tag);
return c.json(post, 200); return c.json(post, 200);
}); });

View File

@ -1,4 +1,3 @@
import { isNil } from "lodash";
import { z } from "zod"; import { z } from "zod";
import { tagListResponesSchema } from "../tag/schema"; import { tagListResponesSchema } from "../tag/schema";
@ -105,49 +104,39 @@ export const postIdParamsSchema = z.object({
* @param slugUniqueValidator Slug * @param slugUniqueValidator Slug
* @param id * @param id
*/ */
export const getPostItemRequestSchema = ( export const getPostItemRequestSchema = z
slugUniqueValidator?: (val?: string | null) => Promise<boolean>, .object({
) => { title: z
let slug = z .string()
.string() .min(10, {
.max(250, { message: "标题不得少于10个字符",
message: "slug不得超过250个字符", })
}) .max(200, {
message: "标题不得超过200个字符",
}),
summary: z.string().max(300, {
message: "摘要不得超过300个字符",
}),
body: z.string().min(1, {
message: "标题不得少于1个字符",
}),
slug: z
.string()
.max(250, {
message: "slug不得超过250个字符",
})
.optional(),
.optional(); thumb: z.string().url(),
if (!isNil(slugUniqueValidator)) { keywords: z.string().max(200, {
slug = slug.refine(slugUniqueValidator, { message: "描述不得超过200个字符",
message: "slug必须是唯一的,请重新设置", }),
}) as any;
}
return z
.object({
title: z
.string()
.min(10, {
message: "标题不得少于10个字符",
})
.max(200, {
message: "标题不得超过200个字符",
}),
summary: z.string().max(300, {
message: "摘要不得超过300个字符",
}),
body: z.string().min(1, {
message: "标题不得少于1个字符",
}),
slug,
thumb: z.string().url(),
keywords: z.string().max(200, {
message: "描述不得超过200个字符",
}),
description: z.string().max(300, { description: z.string().max(300, {
message: "描述不得超过300个字符", message: "描述不得超过300个字符",
}), }),
authorId: z.string().uuid({ message: "not uuid" }), authorId: z.string().uuid({ message: "not uuid" }),
tags: tagListResponesSchema.optional(), tags: tagListResponesSchema.optional(),
categoryId: z.string().uuid({ message: "not uuid" }), categoryId: z.string().uuid({ message: "not uuid" }),
}) })
.strict(); .strict();
};

View File

@ -12,14 +12,6 @@ import type { PostCreateOrUpdateData } from "./type";
* IDslug false * IDslug false
* @param id * @param id
*/ */
export const isSlugUnique = (id?: string) => async (val?: string | null) => {
if (isNil(val)) return true;
console.log("isSlugUnique", val, id);
return db.post
.findFirst({ where: { slug: val } })
.then((res) => (isNil(res) ? true : res.id === id));
};
/** /**
* *
@ -36,6 +28,10 @@ export const queryPostPaginate = async (
orderBy: { orderBy: {
id: orderBy, id: orderBy,
}, },
include: {
tags: true,
author: true,
},
}); });
const totalPages = const totalPages =
totalItems % limit === 0 totalItems % limit === 0
@ -113,7 +109,7 @@ export const createPostItem = async (
data: PostCreateOrUpdateData, data: PostCreateOrUpdateData,
): Promise<Post> => { ): Promise<Post> => {
const tagPromises = data.tags?.map(async (tag) => { const tagPromises = data.tags?.map(async (tag) => {
const tagItem = await db.tag.findUnique({ where: { text: tag } }); const tagItem = await db.tag.findUnique({ where: { text: tag.text } });
return tagItem; return tagItem;
}); });
const resolvedTags = tagPromises ? await Promise.all(tagPromises) : []; const resolvedTags = tagPromises ? await Promise.all(tagPromises) : [];

View File

@ -23,6 +23,4 @@ export type PostPageNumbers = z.infer<typeof postPageNumbersResponseSchema>;
/** /**
* () * ()
*/ */
export type PostCreateOrUpdateData = z.infer< export type PostCreateOrUpdateData = z.infer<typeof getPostItemRequestSchema>;
ReturnType<typeof getPostItemRequestSchema>
>;

View File

@ -1,6 +1,7 @@
import { zValidator } from "@/middleware/zValidator.middleware";
import { isNil } from "lodash"; import { isNil } from "lodash";
import { zValidator } from "@/middleware/zValidator.middleware";
import { createErrorResult, createHonoApp } from "../common/utils"; import { createErrorResult, createHonoApp } from "../common/utils";
import { createTagRequestSchema, tagItemRequestParamsSchema } from "./schema"; import { createTagRequestSchema, tagItemRequestParamsSchema } from "./schema";
// import { tagItemRequestParamsSchema } from './schema'; // import { tagItemRequestParamsSchema } from './schema';

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -13,6 +13,7 @@ async function bootstrap() {
const BootModule = AppModule.forRoot(configure); const BootModule = AppModule.forRoot(configure);
const app = await NestFactory.create(BootModule, { const app = await NestFactory.create(BootModule, {
logger: WinstonModule.createLogger({ instance }), logger: WinstonModule.createLogger({ instance }),
cors: true,
}); });
// 为class-validator添加容器以便在自定义约束中可以注入dataSource等依赖 // 为class-validator添加容器以便在自定义约束中可以注入dataSource等依赖
useContainer(app.select(BootModule), { fallbackOnErrors: true }); useContainer(app.select(BootModule), { fallbackOnErrors: true });
@ -26,6 +27,7 @@ async function bootstrap() {
defaultVersion: ["1", "2"], defaultVersion: ["1", "2"],
type: VersioningType.URI, type: VersioningType.URI,
}); });
await app.listen(port); await app.listen(port);
} }
bootstrap(); bootstrap();

View File

@ -26,7 +26,10 @@ export class FileController {
filename: (req, file, cb) => { filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`; const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
const ext = extname(file.originalname); const ext = extname(file.originalname);
const filename = `${file.originalname.split(ext)[0]}-${uniqueSuffix}${ext}`; const baseName = decodeURIComponent(file.originalname.split(ext)[0]);
console.log(baseName);
file.originalname = baseName + ext;
const filename = `${baseName}-${uniqueSuffix}${ext}`;
return cb(null, filename); return cb(null, filename);
}, },
}), }),

View File

@ -1,5 +1,5 @@
### ###
@baseurl=http://127.0.0.1:3000/v1 @baseurl=http://127.0.0.1:4000/v1
### ###
POST {{baseurl}}/auth/register HTTP/1.1 POST {{baseurl}}/auth/register HTTP/1.1

View File

@ -9,6 +9,16 @@ const nextConfig: NextConfig = {
reactStrictMode: true, // 开启react严格模式 reactStrictMode: true, // 开启react严格模式
serverExternalPackages: externals, serverExternalPackages: externals,
transpilePackages: ["@repo/api", "@repo/ui"], transpilePackages: ["@repo/api", "@repo/ui"],
images: {
remotePatterns: [
{
protocol: "http",
hostname: "192.168.31.43",
port: "4000",
pathname: "/v1/file/**",
},
],
},
}; };
export default nextConfig; export default nextConfig;

View File

@ -1,22 +1,18 @@
import { PostList } from "@/app/_components/paginate/simple";
import { toast } from "@/app/_components/ui/sonner"; import { toast } from "@/app/_components/ui/sonner";
import { fetchApi } from "@/lib/api"; import { fetchApi } from "@/lib/api";
export default async function PostPage() { export default async function PostPage() {
const result = await fetchApi((c) => c.api.posts.$get()); const result = await fetchApi((c) =>
c.api.posts.$get({ query: { limit: 50, page: 1, orderBy: "asc" } }),
);
if (!result.ok) { if (!result.ok) {
toast.error((await result.json()).message); toast.error((await result.json()).message);
return <div></div>; return <div />;
} else { } else {
const data = await result.json(); const data = await result.json();
return ( return (
<div> <div className="tw-container">
<p>{data.meta.itemCount}</p> <PostList posts={data.items}></PostList>
{data.items.map((item) => (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</div>
))}
</div> </div>
); );
} }

View File

@ -0,0 +1,3 @@
export default function TestPage() {
return <div></div>;
}

View File

@ -18,7 +18,6 @@ import {
FormMessage, FormMessage,
} from "@/app/_components/ui/form"; } from "@/app/_components/ui/form";
import { Input } from "@/app/_components/ui/input"; import { Input } from "@/app/_components/ui/input";
import { toast } from "@/app/_components/ui/sonner";
import { setAccessToken } from "@/lib/token"; import { setAccessToken } from "@/lib/token";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -41,21 +40,17 @@ export function LoginForm({
const router = useRouter(); const router = useRouter();
const setauth = useAuthStore((state) => state.setAuth); const setauth = useAuthStore((state) => state.setAuth);
const useloginHandler = async (data: AuthLoginRequest) => { const useloginHandler = async (data: AuthLoginRequest) => {
try { const { token } = await loginApi(data);
const { token } = await loginApi(data); setAccessToken(token);
setAccessToken(token); const user = await getUser();
const user = await getUser(); console.log(user);
if (user) { if (user) {
setauth(user); setauth(user);
}
router.push("/");
} catch (err) {
console.log(err);
toast.error("Invalid username or password.");
} }
router.push("/");
}; };
return ( return (
<div className={cn("tw-flex tw-flex-col tw-gap-6", className)} {...props}> <div className={cn("tw-flex tw-flex-col tw-gap-6", className)} {...props}>
<Card className="tw-overflow-hidden"> <Card className="tw-overflow-hidden">

View File

@ -2,7 +2,7 @@
import type { AuthItem } from "@repo/api/types"; import type { AuthItem } from "@repo/api/types";
import { createStore } from "@/lib/store"; import { createPersistStore } from "@/lib/store";
interface AuthAction { interface AuthAction {
setAuth: (auth: Date2String<AuthItem>) => void; setAuth: (auth: Date2String<AuthItem>) => void;
@ -15,8 +15,13 @@ export type Date2String<T> = {
interface AuthState { interface AuthState {
auth: Date2String<AuthItem> | null; auth: Date2String<AuthItem> | null;
} }
export const useAuthStore = createStore<AuthState & AuthAction>((set) => ({ export const useAuthStore = createPersistStore<AuthState & AuthAction>(
auth: null, (set) => ({
setAuth: (auth: Date2String<AuthItem>) => set(() => ({ auth })), auth: null,
removeAuth: () => set(() => ({ auth: null })), setAuth: (auth: Date2String<AuthItem>) => set(() => ({ auth })),
})); removeAuth: () => set(() => ({ auth: null })),
}),
{
name: "auth",
},
);

View File

@ -0,0 +1,19 @@
POST http://127.0.0.1:3001/api/posts/ HTTP/1.1
Content-Type: application/json
{ "slug": "useFormls11111111111111",
"title": "useFormls1111111111111",
"summary": "useFormlsuseFormlsuseFormls",
"body": "useFormlsuseFormlsuseFormlsuseFormls",
"authorId": "0e5d2a48-c480-44a5-a22b-2e3201d47e5e",
"keywords": "useFormlsuseFormlsuseFormls",
"description": "useFormlsuseFormlsuseFormls",
"categoryId": "a6a5caac-e283-47be-9028-018a8df173dd",
"thumb": "http://192.168.31.43:4000/v1/file/fcbcc067-e812-43cf-8390-0ce361278290",
"tags": [
{
"id": "3d9c01bf-eaec-4512-b3d0-bda0b3abf799",
"text": "测试2"
}
]
}

View File

@ -9,6 +9,9 @@ import type { DeepNonNullable } from "utility-types";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import type { Option } from "@/app/_components/ui/multiple-selector/multiple-selector";
import { useAuthStore } from "@/app/_components/auth/store";
import { Details } from "@/app/_components/collapsible/details"; import { Details } from "@/app/_components/collapsible/details";
import { MdxEditor } from "@/app/_components/mdx/editor"; import { MdxEditor } from "@/app/_components/mdx/editor";
import { Button } from "@/app/_components/ui/button"; import { Button } from "@/app/_components/ui/button";
@ -24,6 +27,7 @@ import {
import { Input } from "@/app/_components/ui/input"; import { Input } from "@/app/_components/ui/input";
import MultipleSelector from "@/app/_components/ui/multiple-selector/multiple-selector"; import MultipleSelector from "@/app/_components/ui/multiple-selector/multiple-selector";
import { Textarea } from "@/app/_components/ui/textarea"; import { Textarea } from "@/app/_components/ui/textarea";
import InputFile from "@/app/_components/upload/upload";
import { CategorySelect } from "./category-select"; import { CategorySelect } from "./category-select";
@ -32,16 +36,33 @@ export const PostActionForm: React.FC<{
categoryList: CategoryList; categoryList: CategoryList;
tagList: TagListResponse; tagList: TagListResponse;
}> = ({ item, categoryList, tagList }) => { }> = ({ item, categoryList, tagList }) => {
const form = useForm<DeepNonNullable<PostCreateOrUpdateData>>({ const auth = useAuthStore((state) => state.auth);
defaultValues: item, const useFormls = (auth: string) =>
}); useForm<DeepNonNullable<PostCreateOrUpdateData>>({
defaultValues: { ...item, authorId: auth },
});
const form = useFormls(auth?.id || "");
return ( return (
<div className="tw-w-2/3 tw-mx-auto tw-pt-6"> <div className="tw-w-2/3 tw-mx-auto tw-pt-6">
<Form {...form}> <Form {...form}>
<form <form
className=" tw-space-y-4" className=" tw-space-y-4"
onSubmit={form.handleSubmit((data) => { onSubmit={form.handleSubmit(async (data) => {
console.log(data); const tags = data.tags as unknown as Option[];
const newtags = tags.map((tag) => ({
id: tag.value,
text: tag.label,
}));
const newdata = { ...data, tags: newtags };
console.log(newdata, "newdata");
const res = await fetch("http://127.0.0.1:3001/api/posts/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newdata),
});
console.log(await res.json(), "res");
})} })}
> >
<FormField <FormField
@ -63,26 +84,25 @@ export const PostActionForm: React.FC<{
); );
}} }}
/> />
<Details summary="可选字段"> <FormField
<FormField control={form.control}
control={form.control} name="slug"
name="summary" render={({ field }) => {
render={({ field }) => ( return (
<FormItem className="tw-mt-2 tw-border-b tw-border-dashed tw-pb-1"> <FormItem>
<FormLabel></FormLabel> <FormLabel>bieming</FormLabel>
<FormControl> <FormControl>
<Textarea <Input
{...field} {...field}
placeholder="请输入文章摘要" placeholder="请输入标题"
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
/> />
</FormControl> </FormControl>
<FormDescription></FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} );
/> }}
</Details> />
<FormField <FormField
control={form.control} control={form.control}
name="categoryId" name="categoryId"
@ -134,7 +154,85 @@ export const PostActionForm: React.FC<{
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="thumb"
render={({ field }) => (
<FormItem className="tw-mt-2 tw-border-b tw-border-dashed tw-pb-1">
<FormLabel></FormLabel>
<FormControl>
<InputFile
imageUrl={field.value}
setImageUrl={field.onChange}
></InputFile>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
></FormField>
<Details summary="SEO相关字段" defaultOpen={false}>
<FormField
control={form.control}
name="summary"
render={({ field }) => (
<FormItem className="tw-mt-2 tw-border-b tw-border-dashed tw-pb-1">
<FormLabel></FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="请输入文章摘要"
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keywords"
render={({ field }) => (
<FormItem className="tw-mt-2 tw-border-b tw-border-dashed tw-pb-1">
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
placeholder="请输入关键字,用逗号分割(关键字是可选的)"
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormDescription>
,SEO.(,)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="tw-mt-2 tw-border-b tw-border-dashed tw-pb-1">
<FormLabel></FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="请输入文章描述"
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormDescription>
,SEO
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</Details>
<FormField <FormField
control={form.control} control={form.control}
name="body" name="body"

View File

@ -16,10 +16,5 @@ export const slugUniqueValidator = async (slug: string): Promise<boolean> => {
}; };
export const createPostApi = async (data: PostCreateOrUpdateData) => { export const createPostApi = async (data: PostCreateOrUpdateData) => {
const result = await fetchApi((c) => c.api.posts.$post({ json: data })); const result = await fetchApi((c) => c.api.posts.$post({ json: data }));
switch (result.status) { return result;
case 200:
return await result.json();
default:
return null;
}
}; };

View File

@ -32,7 +32,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/app/_components/ui/tooltip"; } from "@/app/_components/ui/tooltip";
import { deleteCookie } from "@/lib/cookies"; import { deleteCookie } from "@/lib/cookies";
import { ACCESS_TOKEN_COOKIE_NAME } from "@/lib/token";
import { useAuthStore } from "../../auth/store"; import { useAuthStore } from "../../auth/store";
@ -58,7 +57,7 @@ const UserAction = ({ value }: { value: Date2String<AuthItem> }) => {
const removeAuth = useAuthStore((state) => state.removeAuth); const removeAuth = useAuthStore((state) => state.removeAuth);
const loginout = () => { const loginout = () => {
removeAuth(); removeAuth();
deleteCookie(ACCESS_TOKEN_COOKIE_NAME); deleteCookie("auth_token");
}; };
return ( return (
<DropdownMenu modal={false}> <DropdownMenu modal={false}>

View File

@ -1,78 +1,55 @@
"use client"; "use client";
import type { PostItem } from "@repo/api/types";
import type { FC } from "react"; import Image from "next/image";
import { useEffect, useState } from "react";
import clsx from "clsx"; export function PostList({ posts }: { posts: PostItem[] }) {
import { usePathname, useRouter, useSearchParams } from "next/navigation"; const [displayCount, setDisplayCount] = useState(5);
import { useCallback, useEffect } from "react";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/app/_components/ui/pagination";
const SimplePaginate: FC<{ totalPages: number; currentPage: number }> = ({
totalPages,
currentPage,
}) => {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const getPageUrl = useCallback(
(page: number) => {
const parems = new URLSearchParams(searchParams);
page <= 1 ? parems.delete("page") : parems.set("page", page.toString());
return pathname + (parems.toString() ? `?${parems.toString()}` : "");
},
[searchParams],
);
useEffect(() => { useEffect(() => {
// 在当前页面小于等于1时删除URL中的页面查询参数 const handleScroll = () => {
const params = new URLSearchParams(searchParams); const distanceFromBottom =
if (currentPage <= 1) params.delete("page"); document.documentElement.scrollHeight -
router.replace( (window.innerHeight + document.documentElement.scrollTop);
pathname + (params.toString() ? `?${params.toString()}` : ""), if (distanceFromBottom < 500 && displayCount < posts.length) {
); // 100px 是阈值
}, [currentPage]); setDisplayCount((prevCount) => prevCount + 1);
if (currentPage < 1) return null; }
setDisplayCount((prevCount) => prevCount + 1);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [displayCount]);
return ( return (
<Pagination> <div className=" tw-border-2 tw-flex tw-items-center tw-flex-col">
<PaginationContent> {posts.slice(0, displayCount).map((item) => (
<PaginationItem> <div
<PaginationPrevious className="tw-flex tw-justify-between tw-w-4/5 tw-h-full tw-my-5 tw-border tw-p-2"
size={"default"} key={item.id}
className={clsx( >
"tw-rounded-sm,", <div>
currentPage <= 1 <Image src={item.thumb} alt={item.title} width={500} height={500} />
? " tw-shadow-gray-50 tw-bg-slate-50/70" </div>
: " tw-bg-white/90 hover:tw-shadow-nylg", <div className="tw-flex-1 tw-flex tw-flex-col tw-items-start ">
)} <h1 className="tw-pt-6">{item.title}</h1>
href={getPageUrl(currentPage - 1)}
isActive={currentPage <= 1} <p className="tw-flex-1 tw-mt-5">
aria-label="访问上一页" <span className="tw-whitespace-normal tw-break-words ">
title="上一页" {(item.description || "")?.length < 200
/> ? item.description
</PaginationItem> : `${item.description?.slice(0, 200)}...`}
<PaginationItem> </span>
<PaginationNext </p>
size={"default"} <p>{(item.tags || []).map((tag: any) => `#${tag.text} `)}</p>
className={clsx( <p>
"tw-rounded-sm,", {" "}
currentPage >= totalPages <time dateTime={item.createdAt}>
? " tw-shadow-gray-50 tw-bg-slate-50/70" {new Date(item.createdAt).toDateString()}
: " tw-bg-white/90 hover:tw-shadow-nylg", </time>
)} </p>
href={getPageUrl(currentPage + 1)} </div>
isActive={totalPages <= currentPage} </div>
aria-label="访问下一页" ))}
title="下一页" </div>
/>
</PaginationItem>
</PaginationContent>
</Pagination>
); );
}; }
export default SimplePaginate;

View File

@ -133,7 +133,7 @@ const SelectItem = React.forwardRef<
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText style={{ color: "red" }}> <SelectPrimitive.ItemText style={{ color: "red" }}>
{children}122 {children}
</SelectPrimitive.ItemText> </SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)); ));

View File

@ -1,47 +1,81 @@
"use client"; "use client";
import React from "react";
import { useDropzone } from "react-dropzone";
import { getCookie } from "@/lib/cookies"; import type { ChangeEvent } from "react";
import { cn } from "@/lib/utils";
export function UploadPage({ import Image from "next/image";
url, import React, { useState } from "react";
className,
}: { import { Button } from "../ui/button";
url: string; import { Input } from "../ui/input";
className?: string;
}) { interface InputFileProps {
const onDrop = (acceptedFiles: Blob[]) => { imageUrl: string;
acceptedFiles.forEach((file: Blob) => { setImageUrl: (url: string) => void;
}
export default function InputFile(props: InputFileProps) {
const { imageUrl, setImageUrl } = props;
const [selectedFile, setSelectedFile] = useState<string | undefined>(
imageUrl,
);
const [file, setFile] = useState<File | null>(null);
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const fileObj = e.target.files?.[0];
if (fileObj) {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsArrayBuffer(file); reader.onloadend = () => {
reader.onabort = () => console.log("file reading was aborted"); setSelectedFile(reader.result as string);
reader.onerror = () => console.log("file reading has failed"); setFile(fileObj);
reader.onload = async () => {
const data = new FormData();
data.append("file", file);
fetch(url, {
method: "POST",
body: data,
headers: {
Authorization: `Bearer ${await getCookie("auth_token")}`,
},
});
}; };
}); reader.readAsDataURL(fileObj);
}
}; };
const { getRootProps, getInputProps } = useDropzone({ onDrop });
const handleRemoveClick = () => {
setSelectedFile(undefined);
};
const handleUpload = async () => {
if (!file) return;
const formData = new FormData();
formData.append("file", file, encodeURIComponent(file.name)); // 直接用 File 对象
const res = await fetch("http://192.168.31.43:4000/v1/file/upload", {
method: "POST",
body: formData,
});
// 处理响应
const data = await res.json();
setImageUrl(`http://192.168.31.43:4000/v1/file/${data.id}`);
};
return ( return (
<div <div className="tw-grid tw-w-full tw-max-w-sm tw-items-center tw-gap-1.5">
{...getRootProps({ <Input id="picture" type="file" onChange={handleFileChange} />
className: cn( {selectedFile && (
"tw-size-12 tw-pt-96 tw-border tw-border-red-100", <div className="tw-mt-2 tw-relative">
className, <Image src={selectedFile} alt="Preview" width={400} height={400} />
), <button
})} type="button"
> onClick={handleRemoveClick}
<input {...getInputProps()} /> className="tw-absolute tw-top-0 tw-right-0 tw-bg-red-500 tw-text-white tw-py-1 tw-px-2"
aria-label="Remove image"
>
X
</button>
</div>
)}
<Button
className="tw-w-24"
variant="secondary"
type="button"
onClick={handleUpload}
>
</Button>
</div> </div>
); );
} }

View File

@ -59,6 +59,8 @@ const getAccessTokenOptions = (token: string): AccessTokenCookieOptions => {
*/ */
const setAccessToken = (token: string) => { const setAccessToken = (token: string) => {
const options = getAccessTokenOptions(token); const options = getAccessTokenOptions(token);
console.log("set access token", options);
setCookie( setCookie(
options.name, options.name,
token, token,