实现添加博客

This commit is contained in:
2025-05-16 21:58:33 +08:00
parent 65303ac988
commit 59a68b372b
13 changed files with 260 additions and 39 deletions

View File

@@ -13,6 +13,7 @@ import { AdminUserRoleController } from './controller/admin-user-role.controller
import { AdminWebResourceController } from './controller/web/admin-web-resource.controller'; import { AdminWebResourceController } from './controller/web/admin-web-resource.controller';
import { AdminWebBlogController } from './controller/web/admin-web-blog.controller'; import { AdminWebBlogController } from './controller/web/admin-web-blog.controller';
import { ResourceModule } from 'src/resource/resource.module'; import { ResourceModule } from 'src/resource/resource.module';
import { BlogModule } from 'src/blog/blog.module';
@Module({ @Module({
imports: [ imports: [
@@ -22,6 +23,7 @@ import { ResourceModule } from 'src/resource/resource.module';
UserModule, UserModule,
RoleModule, RoleModule,
ResourceModule, ResourceModule,
BlogModule,
], ],
controllers: [ controllers: [
AdminController, AdminController,

View File

@@ -1,6 +1,45 @@
import { Controller } from "@nestjs/common"; import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post, Put } from "@nestjs/common";
import { CreateBlogDto } from "src/admin/dto/admin-web/create-blog.dto";
import { BlogService } from "src/blog/blog.service";
@Controller('/admin/web/blog') @Controller('/admin/web/blog')
export class AdminWebBlogController { export class AdminWebBlogController {
constructor(
private readonly adminWebBlogService: BlogService,
) { }
@Get()
async list() {
return this.adminWebBlogService.list();
}
@Post()
async create(
@Body() dto: CreateBlogDto,
) {
return this.adminWebBlogService.create(dto);
}
@Put(':id')
async update(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: CreateBlogDto,
) {
return this.adminWebBlogService.update(id, dto);
}
@Get(':id')
async get(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.adminWebBlogService.findById(id);
}
@Delete(':id')
async remove(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.adminWebBlogService.remove(id);
}
} }

View File

@@ -26,14 +26,16 @@ export class AdminWebResourceController {
@Put(':id') @Put(':id')
async update( async update(
@Param('id') id: string, @Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() data: CreateResourceDto @Body() data: CreateResourceDto
) { ) {
return this.resourceService.update(id, data); return this.resourceService.update(id, data);
} }
@Delete(':id') @Delete(':id')
async delete(@Param('id') id: string) { async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.resourceService.delete(id); return this.resourceService.delete(id);
} }
} }

View File

@@ -0,0 +1,12 @@
import { IsString } from "class-validator";
export class CreateBlogDto {
@IsString()
title: string;
@IsString()
description: string;
@IsString()
contentUrl: string;
}

View File

@@ -7,6 +7,7 @@ import { Blog } from './entity/Blog.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Blog])], imports: [TypeOrmModule.forFeature([Blog])],
controllers: [BlogController], controllers: [BlogController],
providers: [BlogService] providers: [BlogService],
exports: [BlogService],
}) })
export class BlogModule { } export class BlogModule { }

View File

@@ -15,9 +15,28 @@ export class BlogService {
return this.blogRepository.find({ return this.blogRepository.find({
where: { deletedAt: null }, where: { deletedAt: null },
order: { order: {
publishAt: 'DESC', createdAt: 'DESC',
} }
}) })
} }
async create(blog: Partial<Blog>) {
const newBlog = this.blogRepository.create(blog);
return this.blogRepository.save(newBlog);
}
async update(id: string, blog: Partial<Blog>) {
await this.blogRepository.update(id, blog);
return this.blogRepository.findOneBy({ id });
}
async remove(id: string) {
const blog = await this.blogRepository.findOneBy({ id });
if (!blog) return null;
return this.blogRepository.softRemove(blog);
}
async findById(id: string) {
return this.blogRepository.findOneBy({ id });
}
} }

View File

@@ -14,9 +14,6 @@ export class Blog {
@Column() @Column()
contentUrl: string; contentUrl: string;
@Column({ precision: 3 })
publishAt: Date;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;

View File

@@ -0,0 +1,99 @@
'use client'
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminApi } from "@/lib/api";
import { useState } from "react";
import { toast } from "sonner";
interface AddBlogProps {
children: React.ReactNode;
onRefresh: () => void;
}
export default function AddBlog({ children, onRefresh }: AddBlogProps) {
const [open, setOpen] = useState(false);
const [blog, setBlog] = useState({
title: "",
description: "",
contentUrl: "",
});
const handleSubmit = async () => {
try {
const res = await AdminApi.web.blog.create({
...blog,
});
if (res) {
setOpen(false);
onRefresh();
toast.success("添加成功");
} else {
throw new Error();
}
} catch (error) {
toast.error((error as Error).message || "添加失败");
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
</Label>
<Input
id="title"
className="col-span-3"
value={blog.title}
onChange={(e) => setBlog({ ...blog, title: e.target.value })}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
</Label>
<Input
id="description"
className="col-span-3"
value={blog.description}
onChange={(e) => setBlog({ ...blog, description: e.target.value })}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="contentUrl" className="text-right">
URL
</Label>
<Input
id="contentUrl"
className="col-span-3"
value={blog.contentUrl}
onChange={(e) => setBlog({ ...blog, contentUrl: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant='secondary' onClick={() => setOpen(false)}></Button>
<Button type="button" onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -8,41 +8,58 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Blog } from "@/lib/types/blog"
interface BlogTableProps { interface BlogTableProps {
blogs: { blogs: Blog[],
error?: string,
}[] onRefresh?: () => void,
} }
export default function BlogTable({ blogs }: BlogTableProps) { export default function BlogTable({ blogs, error, onRefresh }: BlogTableProps) {
return ( return (
<Table> <Table>
<TableCaption>A list of your recent invoices.</TableCaption> {
error && (
<TableCaption>{error}</TableCaption>
)
}
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="w-[100px]">Invoice</TableHead> <TableHead className="w-[100px]">Id</TableHead>
<TableHead>Status</TableHead> <TableHead></TableHead>
<TableHead>Method</TableHead> <TableHead></TableHead>
<TableHead className="text-right">Amount</TableHead> <TableHead>URL</TableHead>
<TableHead className="text-right"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{/* {invoices.map((invoice) => ( {blogs.map((blog) => (
<TableRow key={invoice.invoice}> <TableRow key={blog.id}>
<TableCell className="font-medium">{invoice.invoice}</TableCell> <TableCell className="font-medium">
<TableCell>{invoice.paymentStatus}</TableCell> <TooltipProvider>
<TableCell>{invoice.paymentMethod}</TableCell> <Tooltip>
<TableCell className="text-right">{invoice.totalAmount}</TableCell> <TooltipTrigger asChild>
<div className="max-w-[100px] overflow-hidden text-ellipsis">{blog.id}</div>
</TooltipTrigger>
<TooltipContent>
<p>{blog.id}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell>{blog.title}</TableCell>
<TableCell>{blog.description}</TableCell>
<TableCell>{blog.contentUrl}</TableCell>
<TableCell className="text-right">
{/* <ResourceEdit id={resource.id} onRefresh={() => onRefresh()}>
<Button variant={'outline'} size={'sm'}>编辑</Button>
</ResourceEdit> */}
</TableCell>
</TableRow> </TableRow>
))} */} ))}
</TableBody> </TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table> </Table>
) )
} }

View File

@@ -1,7 +1,21 @@
"use client"
import { useBlogList } from "@/hooks/admin/web/blog/use-blog-list"
import BlogTable from "./components/BlogTable"
import AddBlog from "./components/AddBlog";
import { Button } from "@/components/ui/button";
export default function Page() { export default function Page() {
const { blogs, error, isLoading, refresh } = useBlogList();
return ( return (
<> <>
<div>
<AddBlog onRefresh={refresh}>
<Button></Button>
</AddBlog>
</div>
<BlogTable blogs={blogs || []} onRefresh={refresh} />
</> </>
) )
} }

View File

@@ -0,0 +1,24 @@
"use client"
import { AdminApi } from "@/lib/api";
import { useCallback } from "react";
import useSWR from "swr";
export function useBlogList() {
const { data, error, isLoading, mutate } = useSWR(
['/admin/web/blog'],
() => AdminApi.web.blog.list()
)
const refresh = useCallback(() => {
return mutate()
}, [mutate])
return {
blogs: data,
error,
isLoading,
mutate,
refresh,
}
}

View File

@@ -1,5 +1,6 @@
import fetcher from "@/lib/api/fetcher"; import fetcher from "@/lib/api/fetcher";
import { Blog } from "@/lib/types/blog";
export async function list() { export async function list() {
return fetcher('/api/admin/web/blog') return fetcher<Blog[]>('/api/admin/web/blog')
} }

View File

@@ -1,12 +1,6 @@
export type BlogPermission =
'public' |
'password' |
'listed';
export interface Blog { export interface Blog {
id: string; id: string;
title: string; title: string;
description: string; description: string;
publish_at: string; contentUrl: string;
permissions: BlogPermission[];
} }