Compare commits
11 Commits
13ec36aa8f
...
83bdc924b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 83bdc924b9 | |||
| c75a67c0d9 | |||
| 0b9963bb29 | |||
| b48ed4d903 | |||
| b9d09a16ec | |||
| 8c43f5fa73 | |||
| 3ea57ba023 | |||
| a932178509 | |||
| 2c76d1380f | |||
| 58b7f592fe | |||
| a2e8ddebca |
@@ -54,6 +54,55 @@ jobs:
|
||||
-t localhost:5000/frontend:${IMAGE_TAG} .
|
||||
docker push localhost:5000/frontend:${IMAGE_TAG}
|
||||
|
||||
- name: Run database migrations with temporary container
|
||||
run: |
|
||||
echo "Running database migrations using backend image: localhost:5000/backend:${IMAGE_TAG}"
|
||||
|
||||
echo "Waiting for PostgreSQL service to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=postgres --timeout=30s
|
||||
|
||||
# 获取密码等敏感信息
|
||||
DB_PASSWORD=$(kubectl get secret backend-secret -o jsonpath='{.data.DATABASE_PASSWORD}' | base64 -d)
|
||||
ALIYUN_ACCESS_KEY_ID=$(kubectl get secret backend-secret -o jsonpath='{.data.ALIYUN_ACCESS_KEY_ID}' | base64 -d)
|
||||
ALIYUN_ACCESS_KEY_SECRET=$(kubectl get secret backend-secret -o jsonpath='{.data.ALIYUN_ACCESS_KEY_SECRET}' | base64 -d)
|
||||
ALIYUN_OSS_STS_ROLE_ARN=$(kubectl get secret backend-secret -o jsonpath='{.data.ALIYUN_OSS_STS_ROLE_ARN}' | base64 -d)
|
||||
JWT_SECRET=$(kubectl get secret backend-secret -o jsonpath='{.data.JWT_SECRET}' | base64 -d)
|
||||
WEBAUTHN_RP_ID=$(kubectl get secret backend-secret -o jsonpath='{.data.WEBAUTHN_RP_ID}' | base64 -d)
|
||||
WEBAUTHN_ORIGIN=$(kubectl get secret backend-secret -o jsonpath='{.data.WEBAUTHN_ORIGIN}' | base64 -d)
|
||||
WEBAUTHN_RP_NAME=$(kubectl get secret backend-secret -o jsonpath='{.data.WEBAUTHN_RP_NAME}' | base64 -d)
|
||||
|
||||
# 检查是否成功获取了密码
|
||||
if [ -z "$DB_PASSWORD" ]; then
|
||||
echo "Error: Could not retrieve DATABASE_PASSWORD from backend-secret."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run --rm \
|
||||
-e NODE_ENV=production \
|
||||
-e DATABASE_HOST=postgres-service \
|
||||
-e DATABASE_PORT=5432 \
|
||||
-e DATABASE_NAME=tone_page \
|
||||
-e DATABASE_USERNAME=tone_page \
|
||||
-e DATABASE_PASSWORD="$DB_PASSWORD" \
|
||||
-e JWT_SECRET="$JWT_SECRET" \
|
||||
-e JWT_EXPIRES_IN=1d \
|
||||
-e ALIYUN_ACCESS_KEY_ID="$ALIYUN_ACCESS_KEY_ID" \
|
||||
-e ALIYUN_ACCESS_KEY_SECRET="$ALIYUN_ACCESS_KEY_SECRET" \
|
||||
-e ALIYUN_OSS_STS_ROLE_ARN="$ALIYUN_OSS_STS_ROLE_ARN" \
|
||||
-e WEBAUTHN_RP_ID="$WEBAUTHN_RP_ID" \
|
||||
-e WEBAUTHN_ORIGIN="$WEBAUTHN_ORIGIN" \
|
||||
-e WEBAUTHN_RP_NAME="$WEBAUTHN_RP_NAME" \
|
||||
localhost:5000/backend:${IMAGE_TAG} \
|
||||
pnpm run migration:run
|
||||
|
||||
# 检查上一步命令是否成功
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Database migration failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Database migrations completed successfully."
|
||||
|
||||
- name: Deploy to K3s
|
||||
run: |
|
||||
cd /workspace/tone/tonePage/apps/deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ export class CreateBlogDto {
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
slug: string;// 允许空串,但如果为空则需要手动设置为null,防止数据库唯一键冲突
|
||||
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ export class UpdateBlogDto {
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
slug: string;
|
||||
|
||||
@IsString()
|
||||
contentUrl: string;
|
||||
|
||||
|
||||
@@ -31,12 +31,16 @@ export class BlogController {
|
||||
return this.blogService.list();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getBlog(
|
||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||
@Get(':id/slug')
|
||||
async getBlogBySlug(
|
||||
@Param('id') slug: string,
|
||||
@Query('p') password?: string,
|
||||
) {
|
||||
const blog = await this.blogService.findById(id);
|
||||
if (slug.trim().length === 0) {
|
||||
throw new BadRequestException('文章不存在');
|
||||
}
|
||||
|
||||
const blog = await this.blogService.findBySlug(slug);
|
||||
if (!blog) throw new BadRequestException('文章不存在或无权限访问');
|
||||
|
||||
if (!blog.permissions.includes(BlogPermission.Public)) {
|
||||
@@ -46,7 +50,7 @@ export class BlogController {
|
||||
} else {
|
||||
// 判断密码是否正确
|
||||
if (
|
||||
!password ||
|
||||
typeof password !== 'string' ||
|
||||
this.blogService.hashPassword(password) !== blog.password_hash
|
||||
) {
|
||||
throw new BadRequestException('文章不存在或无权限访问');
|
||||
@@ -57,7 +61,7 @@ export class BlogController {
|
||||
const blogDataRes = await fetch(`${blog.contentUrl}`);
|
||||
const blogContent = await blogDataRes.text();
|
||||
|
||||
await this.blogService.incrementViewCount(id);
|
||||
this.blogService.incrementViewCount(blog.id).catch(() => null);
|
||||
return {
|
||||
id: blog.id,
|
||||
title: blog.title,
|
||||
|
||||
@@ -35,13 +35,13 @@ export class BlogService {
|
||||
return i;
|
||||
}
|
||||
|
||||
const { createdAt, updatedAt, deletedAt, id, title, viewCount, description } = i;
|
||||
const { createdAt, updatedAt, id, title, viewCount, description, slug } = i;
|
||||
return {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
id,
|
||||
title,
|
||||
slug,
|
||||
viewCount,
|
||||
description,
|
||||
};
|
||||
@@ -57,6 +57,9 @@ export class BlogService {
|
||||
.digest('hex');
|
||||
}
|
||||
}
|
||||
if (typeof blog.slug === 'string' && blog.slug.trim().length === 0) {
|
||||
blog.slug = null;
|
||||
}
|
||||
|
||||
const newBlog = this.blogRepository.create(blog);
|
||||
return this.blogRepository.save(newBlog);
|
||||
@@ -93,6 +96,12 @@ export class BlogService {
|
||||
return await this.blogRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
async findBySlug(slug: string) {
|
||||
return this.blogRepository.findOne({
|
||||
where: { slug }
|
||||
})
|
||||
}
|
||||
|
||||
async incrementViewCount(id: string) {
|
||||
await this.blogRepository.increment({ id }, 'viewCount', 1);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ export class Blog {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true, nullable: true })
|
||||
slug: string;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
|
||||
16
apps/backend/src/migrations/1766809565876-AddSlugToBlog.ts
Normal file
16
apps/backend/src/migrations/1766809565876-AddSlugToBlog.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddSlugToBlog1766809565876 implements MigrationInterface {
|
||||
name = 'AddSlugToBlog1766809565876'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "blog" ADD "slug" character varying`);
|
||||
await queryRunner.query(`ALTER TABLE "blog" ADD CONSTRAINT "UQ_0dc7e58d73a1390874a663bd599" UNIQUE ("slug")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "blog" DROP CONSTRAINT "UQ_0dc7e58d73a1390874a663bd599"`);
|
||||
await queryRunner.query(`ALTER TABLE "blog" DROP COLUMN "slug"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,17 +27,8 @@ async function parseBlogParams({ params: paramsPromise, searchParams: searchPara
|
||||
}
|
||||
}
|
||||
|
||||
const hex = Array.from(base62.decode(params.id as string)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
const id = [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20, 32)
|
||||
].join('-');
|
||||
|
||||
return {
|
||||
id,
|
||||
id: params.id,
|
||||
p: searchParams.p,
|
||||
}
|
||||
}
|
||||
@@ -50,7 +41,7 @@ async function getBlog(paramsResult: ReturnType<typeof parseBlogParams>) {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const data = await BlogAPI.getBlog(`${id}`, p);
|
||||
const data = await BlogAPI.getBlogBySlug(`${id}`, p);
|
||||
return {
|
||||
data,
|
||||
}
|
||||
@@ -83,7 +74,7 @@ export default async function Page({ params, searchParams }: PageRouteProps) {
|
||||
let { errorMsg } = res;
|
||||
|
||||
const data = errorMsg ? null
|
||||
: await BlogAPI.getBlog(`${id}`, p).catch(e => handleAPIError(e, ({ message }) => { errorMsg = message; return null }));
|
||||
: await BlogAPI.getBlogBySlug(`${id}`, p).catch(e => handleAPIError(e, ({ message }) => { errorMsg = message; return null }));
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-hidden">
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { base62 } from "@/lib/utils";
|
||||
import { BlogAPI } from "@/lib/api/server";
|
||||
import { handleAPIError } from "@/lib/api/common";
|
||||
|
||||
@@ -18,13 +17,10 @@ const formatNumber = (num: number): string => {
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const getBlogDetailUrl = (id: string): string => {
|
||||
const cleanId = id.replace(/-/g, '');
|
||||
const encoded = base62.encode(Buffer.from(cleanId, 'hex'));
|
||||
return `/blog/${encoded}`;
|
||||
const getBlogDetailUrl = (slug: string): string => {
|
||||
return `/blog/${slug}`;
|
||||
};
|
||||
|
||||
|
||||
export const metadata = {
|
||||
title: '日志 - 特恩的日志',
|
||||
description: '我随便发点,你也随便看看~',
|
||||
@@ -56,7 +52,7 @@ export default async function Blog() {
|
||||
<h2 className="text-2xl font-medium">
|
||||
<a
|
||||
className="hover:underline focus:outline-none focus:ring-2 focus:ring-zinc-400 rounded"
|
||||
href={getBlogDetailUrl(blog.id)}
|
||||
href={getBlogDetailUrl(blog.slug)}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{blog.title}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { BlogPermissionCheckBoxs } from "./BlogPermissionCheckBoxs";
|
||||
import { AdminAPI } from "@/lib/api/client";
|
||||
import { copyShareURL } from "./utils";
|
||||
|
||||
interface AddBlogProps {
|
||||
children: React.ReactNode;
|
||||
@@ -27,6 +28,7 @@ export default function AddBlog({ children, onRefresh }: AddBlogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [blog, setBlog] = useState({
|
||||
title: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
contentUrl: "",
|
||||
permissions: [] as BlogPermission[],
|
||||
@@ -44,6 +46,7 @@ export default function AddBlog({ children, onRefresh }: AddBlogProps) {
|
||||
toast.success("添加成功");
|
||||
setBlog({
|
||||
title: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
contentUrl: '',
|
||||
permissions: [],
|
||||
@@ -92,6 +95,17 @@ export default function AddBlog({ children, onRefresh }: AddBlogProps) {
|
||||
onChange={(e) => setBlog({ ...blog, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="slug" className="text-right">
|
||||
Slug
|
||||
</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
className="col-span-3"
|
||||
value={blog.slug}
|
||||
onChange={(e) => setBlog({ ...blog, slug: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="contentUrl" className="text-right">
|
||||
文章URL
|
||||
@@ -136,9 +150,18 @@ export default function AddBlog({ children, onRefresh }: AddBlogProps) {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant='secondary' onClick={() => setOpen(false)}>取消</Button>
|
||||
<Button type="button" onClick={handleSubmit}>保存</Button>
|
||||
<DialogFooter >
|
||||
<div className="flex justify-between w-full">
|
||||
<Button type="button" variant='outline' onClick={() => copyShareURL({
|
||||
slug: blog.slug,
|
||||
password: blog.password,
|
||||
permissions: blog.permissions,
|
||||
})}>复制分享链接</Button>
|
||||
<div>
|
||||
<Button type="button" variant='secondary' onClick={() => setOpen(false)}>取消</Button>
|
||||
<Button type="button" onClick={handleSubmit}>保存</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { BlogPermissionCheckBoxs } from "./BlogPermissionCheckBoxs"
|
||||
import { BlogPermission } from "@/lib/types/Blog.Permission.enum"
|
||||
import { SetPasswordDialog } from "./SetPasswordDialog"
|
||||
import { AdminAPI } from "@/lib/api/client"
|
||||
import { copyShareURL } from "./utils"
|
||||
|
||||
interface BlogEditProps {
|
||||
id: string;
|
||||
@@ -46,6 +47,7 @@ export default function BlogEdit({ id, children, onRefresh }: BlogEditProps) {
|
||||
await AdminAPI.updateBlog(id, {
|
||||
title: blog.title,
|
||||
description: blog.description,
|
||||
slug: blog.slug,
|
||||
contentUrl: blog.contentUrl,
|
||||
permissions: blog.permissions,
|
||||
});
|
||||
@@ -106,6 +108,17 @@ export default function BlogEdit({ id, children, onRefresh }: BlogEditProps) {
|
||||
onChange={(e) => mutate({ ...blog, description: e.target.value }, false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="slug" className="text-right">
|
||||
Slug
|
||||
</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
className="col-span-3"
|
||||
value={blog.slug}
|
||||
onChange={(e) => mutate({ ...blog, slug: e.target.value }, false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="contentUrl" className="text-right">
|
||||
文章URL
|
||||
@@ -151,6 +164,11 @@ export default function BlogEdit({ id, children, onRefresh }: BlogEditProps) {
|
||||
<div className="w-full flex justify-between">
|
||||
<div>
|
||||
<Button variant='destructive' onClick={handleDelete}>删除</Button>
|
||||
<Button variant='outline' className="ml-2" onClick={() => copyShareURL({
|
||||
slug: blog.slug,
|
||||
permissions: blog.permissions,
|
||||
password: ''
|
||||
})}>复制链接</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="button" variant='secondary' onClick={() => setOpen(false)}>取消</Button>
|
||||
|
||||
@@ -28,10 +28,11 @@ export default function BlogTable({ blogs, error, onRefresh }: BlogTableProps) {
|
||||
}
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-25">Id</TableHead>
|
||||
<TableHead className="w-15">Id</TableHead>
|
||||
<TableHead>标题</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>文章URL</TableHead>
|
||||
<TableHead>Slug</TableHead>
|
||||
<TableHead className="w-25">文章URL</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -42,7 +43,7 @@ export default function BlogTable({ blogs, error, onRefresh }: BlogTableProps) {
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="max-w-[100px] overflow-hidden text-ellipsis">{blog.id}</div>
|
||||
<div className="max-w-15 overflow-hidden text-ellipsis">{blog.id}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{blog.id}</p>
|
||||
@@ -52,7 +53,19 @@ export default function BlogTable({ blogs, error, onRefresh }: BlogTableProps) {
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-normal break-all">{blog.title}</TableCell>
|
||||
<TableCell className="whitespace-normal break-all">{blog.description}</TableCell>
|
||||
<TableCell className="whitespace-normal break-all">{blog.contentUrl}</TableCell>
|
||||
<TableCell className="whitespace-normal break-all">{blog.slug}</TableCell>
|
||||
<TableCell>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="max-w-20 overflow-hidden text-ellipsis">{blog.contentUrl}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{blog.contentUrl}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<BlogEdit id={blog.id} onRefresh={() => onRefresh()}>
|
||||
<Button variant={'outline'} size={'sm'}>编辑</Button>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BlogPermission } from "@/lib/types/Blog.Permission.enum";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function copyShareURL(data: {
|
||||
slug: string;
|
||||
password: string;
|
||||
permissions: BlogPermission[];
|
||||
}) {
|
||||
const slug = data.slug.trim();
|
||||
const password = data.password.trim();
|
||||
const permissions = data.permissions;
|
||||
|
||||
if (slug.length === 0) {
|
||||
return toast.warning('请先填写Slug')
|
||||
}
|
||||
|
||||
let url = `${window.location.origin}/blog/${slug}`;
|
||||
|
||||
if (permissions.includes(BlogPermission.ByPassword)) {
|
||||
if (password.length === 0) {
|
||||
return toast.warning('开启了密码保护,但无法获取有效的密码,无法生成有效URL')
|
||||
} else {
|
||||
url += `?p=${password}`;
|
||||
}
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
toast.success('复制成功');
|
||||
}, () => {
|
||||
toast.error('复制失败,请手动复制');
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BlogAPI } from '@/lib/api/server'
|
||||
import { base62 } from '@/lib/utils'
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export const revalidate = 3600;
|
||||
@@ -9,10 +8,8 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const blogs = await BlogAPI.list().catch(() => [])
|
||||
|
||||
const blogUrls = blogs.map(blog => {
|
||||
const cleanId = blog.id.replace(/-/g, '')
|
||||
const encoded = base62.encode(Buffer.from(cleanId, 'hex'))
|
||||
return {
|
||||
url: `https://www.tonesc.cn/blog/${encoded}`,
|
||||
url: `https://www.tonesc.cn/blog/${blog.slug}`,
|
||||
lastModified: new Date(blog.updatedAt),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.8,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { clientFetch } from "../client";
|
||||
import { Blog } from "@/lib/types/blog";
|
||||
import { BlogPermission } from "@/lib/types/Blog.Permission.enum";
|
||||
import { Role } from "@/lib/types/role";
|
||||
import { APIError } from "../common";
|
||||
|
||||
export interface UserEntity {
|
||||
userId: string;
|
||||
@@ -87,9 +88,31 @@ export async function updateResource(id: string, data: UpdateResourceParams) {
|
||||
interface CreateBlogParams {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
contentUrl: string;
|
||||
permissions: BlogPermission[];
|
||||
password: string;
|
||||
}
|
||||
export async function createBlog(data: CreateBlogParams) {
|
||||
data.title = data.title.trim()
|
||||
data.description = data.description.trim()
|
||||
data.slug = data.slug.trim()
|
||||
data.contentUrl = data.contentUrl.trim()
|
||||
data.password = data.password.trim()
|
||||
|
||||
if (data.title.length === 0) {
|
||||
throw new APIError('标题不得为空')
|
||||
}
|
||||
if (data.description.length === 0) {
|
||||
throw new APIError('描述不得为空')
|
||||
}
|
||||
if (data.slug.length === 0) {
|
||||
throw new APIError('Slug不得为空')
|
||||
}
|
||||
if (data.contentUrl.length === 0) {
|
||||
throw new APIError('文章URL不得为空')
|
||||
}
|
||||
|
||||
return clientFetch<Blog>('/api/admin/web/blog', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
@@ -115,14 +138,29 @@ export async function removeBlog(id: string) {
|
||||
interface UpdateBlogParams {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
contentUrl: string;
|
||||
permissions: BlogPermission[],
|
||||
}
|
||||
export async function updateBlog(id: string, data: UpdateBlogParams) {
|
||||
data.title = data.title.trim();
|
||||
data.description = data.description.trim();
|
||||
data.slug = data.slug.trim();
|
||||
data.contentUrl = data.contentUrl.trim();
|
||||
|
||||
if (data.title.length === 0) {
|
||||
throw new APIError('标题不得为空')
|
||||
}
|
||||
if (data.description.length === 0) {
|
||||
throw new APIError('描述不得为空')
|
||||
}
|
||||
if (data.slug.length === 0) {
|
||||
throw new APIError('Slug不得为空')
|
||||
}
|
||||
if (data.contentUrl.length === 0) {
|
||||
throw new APIError('文章URL不得为空')
|
||||
}
|
||||
|
||||
return clientFetch<Blog>(`/api/admin/web/blog/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
|
||||
@@ -3,16 +3,17 @@ import { serverFetch } from "../server";
|
||||
|
||||
export async function list() {
|
||||
return serverFetch<Pick<Blog,
|
||||
'id' | 'title' | 'description' | 'viewCount' | 'createdAt' | 'updatedAt' | 'deletedAt'
|
||||
'id' | 'title' | 'slug' | 'description' | 'viewCount' | 'createdAt' | 'updatedAt'
|
||||
>[]>('/api/blog')
|
||||
}
|
||||
|
||||
export async function getBlog(id: string, password?: string) {
|
||||
export async function getBlogBySlug(slug: string, password?: string) {
|
||||
return serverFetch<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
content: string;
|
||||
}>(`/api/blog/${id}` + (password ? `?p=${password}` : ''));
|
||||
}
|
||||
}>(`/api/blog/${slug}/slug` + (password ? `?p=${password}` : ''));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BlogPermission } from "./Blog.Permission.enum";
|
||||
export interface Blog {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
viewCount: number;
|
||||
contentUrl: string;
|
||||
|
||||
Reference in New Issue
Block a user