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";
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:
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(
{
fetch: app.fetch,
port: 4000,
port: 3001,
hostname: "localhost",
},
async () => {
console.log("Server started on http://localhost:4000/api/");
console.log("Server started on http://localhost:3001/api/");
},
).listen();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { isNil } from "lodash";
import { z } from "zod";
import { tagListResponesSchema } from "../tag/schema";
@ -105,49 +104,39 @@ export const postIdParamsSchema = z.object({
* @param slugUniqueValidator Slug
* @param id
*/
export const getPostItemRequestSchema = (
slugUniqueValidator?: (val?: string | null) => Promise<boolean>,
) => {
let slug = z
.string()
.max(250, {
message: "slug不得超过250个字符",
})
export const getPostItemRequestSchema = 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: z
.string()
.max(250, {
message: "slug不得超过250个字符",
})
.optional(),
.optional();
if (!isNil(slugUniqueValidator)) {
slug = slug.refine(slugUniqueValidator, {
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个字符",
}),
thumb: z.string().url(),
keywords: z.string().max(200, {
message: "描述不得超过200个字符",
}),
description: z.string().max(300, {
message: "描述不得超过300个字符",
}),
authorId: z.string().uuid({ message: "not uuid" }),
tags: tagListResponesSchema.optional(),
categoryId: z.string().uuid({ message: "not uuid" }),
})
.strict();
};
description: z.string().max(300, {
message: "描述不得超过300个字符",
}),
authorId: z.string().uuid({ message: "not uuid" }),
tags: tagListResponesSchema.optional(),
categoryId: z.string().uuid({ message: "not uuid" }),
})
.strict();

View File

@ -12,14 +12,6 @@ import type { PostCreateOrUpdateData } from "./type";
* IDslug false
* @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: {
id: orderBy,
},
include: {
tags: true,
author: true,
},
});
const totalPages =
totalItems % limit === 0
@ -113,7 +109,7 @@ export const createPostItem = async (
data: PostCreateOrUpdateData,
): Promise<Post> => {
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;
});
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<
ReturnType<typeof getPostItemRequestSchema>
>;
export type PostCreateOrUpdateData = z.infer<typeof getPostItemRequestSchema>;

View File

@ -1,6 +1,7 @@
import { zValidator } from "@/middleware/zValidator.middleware";
import { isNil } from "lodash";
import { zValidator } from "@/middleware/zValidator.middleware";
import { createErrorResult, createHonoApp } from "../common/utils";
import { createTagRequestSchema, 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 app = await NestFactory.create(BootModule, {
logger: WinstonModule.createLogger({ instance }),
cors: true,
});
// 为class-validator添加容器以便在自定义约束中可以注入dataSource等依赖
useContainer(app.select(BootModule), { fallbackOnErrors: true });
@ -26,6 +27,7 @@ async function bootstrap() {
defaultVersion: ["1", "2"],
type: VersioningType.URI,
});
await app.listen(port);
}
bootstrap();

View File

@ -26,7 +26,10 @@ export class FileController {
filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
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);
},
}),

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

View File

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

View File

@ -1,22 +1,18 @@
import { PostList } from "@/app/_components/paginate/simple";
import { toast } from "@/app/_components/ui/sonner";
import { fetchApi } from "@/lib/api";
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) {
toast.error((await result.json()).message);
return <div></div>;
return <div />;
} else {
const data = await result.json();
return (
<div>
<p>{data.meta.itemCount}</p>
{data.items.map((item) => (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</div>
))}
<div className="tw-container">
<PostList posts={data.items}></PostList>
</div>
);
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@
import type { AuthItem } from "@repo/api/types";
import { createStore } from "@/lib/store";
import { createPersistStore } from "@/lib/store";
interface AuthAction {
setAuth: (auth: Date2String<AuthItem>) => void;
@ -15,8 +15,13 @@ export type Date2String<T> = {
interface AuthState {
auth: Date2String<AuthItem> | null;
}
export const useAuthStore = createStore<AuthState & AuthAction>((set) => ({
auth: null,
setAuth: (auth: Date2String<AuthItem>) => set(() => ({ auth })),
removeAuth: () => set(() => ({ auth: null })),
}));
export const useAuthStore = createPersistStore<AuthState & AuthAction>(
(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 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 { MdxEditor } from "@/app/_components/mdx/editor";
import { Button } from "@/app/_components/ui/button";
@ -24,6 +27,7 @@ import {
import { Input } from "@/app/_components/ui/input";
import MultipleSelector from "@/app/_components/ui/multiple-selector/multiple-selector";
import { Textarea } from "@/app/_components/ui/textarea";
import InputFile from "@/app/_components/upload/upload";
import { CategorySelect } from "./category-select";
@ -32,16 +36,33 @@ export const PostActionForm: React.FC<{
categoryList: CategoryList;
tagList: TagListResponse;
}> = ({ item, categoryList, tagList }) => {
const form = useForm<DeepNonNullable<PostCreateOrUpdateData>>({
defaultValues: item,
});
const auth = useAuthStore((state) => state.auth);
const useFormls = (auth: string) =>
useForm<DeepNonNullable<PostCreateOrUpdateData>>({
defaultValues: { ...item, authorId: auth },
});
const form = useFormls(auth?.id || "");
return (
<div className="tw-w-2/3 tw-mx-auto tw-pt-6">
<Form {...form}>
<form
className=" tw-space-y-4"
onSubmit={form.handleSubmit((data) => {
console.log(data);
onSubmit={form.handleSubmit(async (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
@ -63,26 +84,25 @@ export const PostActionForm: React.FC<{
);
}}
/>
<Details summary="可选字段">
<FormField
control={form.control}
name="summary"
render={({ field }) => (
<FormItem className="tw-mt-2 tw-border-b tw-border-dashed tw-pb-1">
<FormLabel></FormLabel>
<FormField
control={form.control}
name="slug"
render={({ field }) => {
return (
<FormItem>
<FormLabel>bieming</FormLabel>
<FormControl>
<Textarea
<Input
{...field}
placeholder="请输入文章摘要"
placeholder="请输入标题"
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
)}
/>
</Details>
);
}}
/>
<FormField
control={form.control}
name="categoryId"
@ -134,7 +154,85 @@ export const PostActionForm: React.FC<{
</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
control={form.control}
name="body"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,81 @@
"use client";
import React from "react";
import { useDropzone } from "react-dropzone";
import { getCookie } from "@/lib/cookies";
import { cn } from "@/lib/utils";
import type { ChangeEvent } from "react";
export function UploadPage({
url,
className,
}: {
url: string;
className?: string;
}) {
const onDrop = (acceptedFiles: Blob[]) => {
acceptedFiles.forEach((file: Blob) => {
import Image from "next/image";
import React, { useState } from "react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
interface InputFileProps {
imageUrl: string;
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();
reader.readAsArrayBuffer(file);
reader.onabort = () => console.log("file reading was aborted");
reader.onerror = () => console.log("file reading has failed");
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.onloadend = () => {
setSelectedFile(reader.result as string);
setFile(fileObj);
};
});
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 (
<div
{...getRootProps({
className: cn(
"tw-size-12 tw-pt-96 tw-border tw-border-red-100",
className,
),
})}
>
<input {...getInputProps()} />
<div className="tw-grid tw-w-full tw-max-w-sm tw-items-center tw-gap-1.5">
<Input id="picture" type="file" onChange={handleFileChange} />
{selectedFile && (
<div className="tw-mt-2 tw-relative">
<Image src={selectedFile} alt="Preview" width={400} height={400} />
<button
type="button"
onClick={handleRemoveClick}
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>
);
}

View File

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