0041
This commit is contained in:
1 parent
ed14361e7b
commit
d664326648
8 files changed
+39
-14
No files matched your search
@@ -29,6 +29,7 @@ services:
|
|||||||
- PORT=8016
|
- PORT=8016
|
||||||
- BLOG_ROOT=/blog
|
- BLOG_ROOT=/blog
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
- HUGO_PREVIEW=http://127.0.0.1:1313
|
||||||
networks:
|
networks:
|
||||||
- write-network
|
- write-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ fi
|
|||||||
# 配置 git(容器内 nextjs 用户)
|
# 配置 git(容器内 nextjs 用户)
|
||||||
su-exec nextjs git config --global user.name "Write Server" 2>/dev/null
|
su-exec nextjs git config --global user.name "Write Server" 2>/dev/null
|
||||||
su-exec nextjs git config --global user.email "write@usj.cc" 2>/dev/null
|
su-exec nextjs git config --global user.email "write@usj.cc" 2>/dev/null
|
||||||
|
su-exec nextjs git config --global --add safe.directory /blog 2>/dev/null
|
||||||
|
|
||||||
# 以 nextjs 用户启动应用
|
# 以 nextjs 用户启动应用
|
||||||
exec su-exec nextjs "$@"
|
exec su-exec nextjs "$@"
|
||||||
@@ -4,7 +4,6 @@ import { useState, useEffect, use } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ArrowLeft, Eye } from "lucide-react";
|
import { ArrowLeft, Eye } from "lucide-react";
|
||||||
import PostEditor from "@/components/PostEditor";
|
import PostEditor from "@/components/PostEditor";
|
||||||
import { HUGO_PREVIEW } from "@/lib/config";
|
|
||||||
import type { Post } from "@/lib/posts";
|
import type { Post } from "@/lib/posts";
|
||||||
|
|
||||||
export default function EditPostPage({
|
export default function EditPostPage({
|
||||||
@@ -74,7 +73,8 @@ export default function EditPostPage({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await fetch("/api/hugo");
|
await fetch("/api/hugo");
|
||||||
window.open(`${HUGO_PREVIEW}/${post.frontMatter.slug}.html`, "_blank");
|
const previewBase = `${location.protocol}//${location.hostname}:1313`;
|
||||||
|
window.open(`${previewBase}/${post.frontMatter.slug}.html`, "_blank");
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] rounded-md border border-line text-muted hover:text-accent hover:border-accent/30 transition-colors"
|
className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] rounded-md border border-line text-muted hover:text-accent hover:border-accent/30 transition-colors"
|
||||||
title="预览"
|
title="预览"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
FileText as PostIcon, Rss, Link2, GripVertical, X, CheckSquare, Square,
|
FileText as PostIcon, Rss, Link2, GripVertical, X, CheckSquare, Square,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Pagination from "@/components/Pagination";
|
import Pagination from "@/components/Pagination";
|
||||||
import { HUGO_PREVIEW } from "@/lib/config";
|
|
||||||
|
|
||||||
interface PostItem {
|
interface PostItem {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -64,13 +63,23 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const handleDelete = async (slug: string, title: string) => {
|
const handleDelete = async (slug: string, title: string) => {
|
||||||
if (!confirm(`确定删除「${title}」吗?`)) return;
|
if (!confirm(`确定删除「${title}」吗?`)) return;
|
||||||
await fetch(`/api/posts/${slug}`, { method: "DELETE" });
|
try {
|
||||||
fetchPosts();
|
const res = await fetch(`/api/posts/${slug}`, { method: "DELETE" });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
alert("删除失败:" + (data.error || "未知错误"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchPosts();
|
||||||
|
} catch (e) {
|
||||||
|
alert("删除失败:" + (e instanceof Error ? e.message : String(e)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreview = async (slug: string) => {
|
const handlePreview = async (slug: string) => {
|
||||||
await fetch("/api/hugo");
|
await fetch("/api/hugo");
|
||||||
window.open(`${HUGO_PREVIEW}/${slug}.html`, "_blank");
|
const previewBase = `${location.protocol}//${location.hostname}:1313`;
|
||||||
|
window.open(`${previewBase}/${slug}.html`, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelect = (slug: string) => {
|
const toggleSelect = (slug: string) => {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ function computeDirPath(title: string, date?: string): string {
|
|||||||
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
||||||
.replace(/^-+|-+$/g, "")
|
.replace(/^-+|-+$/g, "")
|
||||||
.toLowerCase() || "post";
|
.toLowerCase() || "post";
|
||||||
return path.join(CONTENT_DIR, year, `${dateStr}-${titlePart}`);
|
// 加时间戳避免同标题同天冲突
|
||||||
|
const ts = Date.now().toString(36);
|
||||||
|
return path.join(CONTENT_DIR, year, `${dateStr}-${titlePart}-${ts}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions = new Map<number, Session>();
|
const sessions = new Map<number, Session>();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ARTALK_SERVER = process.env.ARTALK_SERVER || "https://artalk.usj.cc
|
|||||||
export const ARTALK_SITE = process.env.ARTALK_SITE || "优世界";
|
export const ARTALK_SITE = process.env.ARTALK_SITE || "优世界";
|
||||||
export const ITEMS_PER_PAGE = parseInt(process.env.ITEMS_PER_PAGE || "20", 10);
|
export const ITEMS_PER_PAGE = parseInt(process.env.ITEMS_PER_PAGE || "20", 10);
|
||||||
export const RECYCLE_DAYS = parseInt(process.env.RECYCLE_DAYS || "30", 10);
|
export const RECYCLE_DAYS = parseInt(process.env.RECYCLE_DAYS || "30", 10);
|
||||||
export const HUGO_PREVIEW = process.env.HUGO_PREVIEW || "http://localhost:1313";
|
export const HUGO_PREVIEW = process.env.NEXT_PUBLIC_HUGO_PREVIEW || process.env.HUGO_PREVIEW || "http://localhost:1313";
|
||||||
export const RSS_API_BASE = process.env.RSS_API_BASE || "https://api.usj.cc";
|
export const RSS_API_BASE = process.env.RSS_API_BASE || "https://api.usj.cc";
|
||||||
export const RSS_API_TOKEN = process.env.RSS_API_TOKEN || "";
|
export const RSS_API_TOKEN = process.env.RSS_API_TOKEN || "";
|
||||||
export const WECHAT_APP_ID = process.env.WECHAT_APP_ID || "";
|
export const WECHAT_APP_ID = process.env.WECHAT_APP_ID || "";
|
||||||
|
|||||||
@@ -155,12 +155,13 @@ export function createPost(frontMatter: PostFrontMatter, content: string): strin
|
|||||||
const date = new Date(frontMatter.date || new Date());
|
const date = new Date(frontMatter.date || new Date());
|
||||||
const year = date.getFullYear().toString();
|
const year = date.getFullYear().toString();
|
||||||
const dateStr = date.toISOString().slice(0, 10);
|
const dateStr = date.toISOString().slice(0, 10);
|
||||||
// Folder name: date + sanitized title (human-readable on disk)
|
|
||||||
const titlePart = (frontMatter.title || "untitled")
|
const titlePart = (frontMatter.title || "untitled")
|
||||||
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
||||||
.replace(/^-+|-+$/g, "")
|
.replace(/^-+|-+$/g, "")
|
||||||
.toLowerCase() || "post";
|
.toLowerCase() || "post";
|
||||||
const dirName = `${dateStr}-${titlePart}`;
|
// 用 slug 或时间戳确保目录唯一
|
||||||
|
const uniquePart = frontMatter.slug || Date.now().toString(36);
|
||||||
|
const dirName = `${dateStr}-${titlePart}-${uniquePart}`;
|
||||||
const dirPath = path.join(CONTENT_DIR, year, dirName);
|
const dirPath = path.join(CONTENT_DIR, year, dirName);
|
||||||
|
|
||||||
if (fs.existsSync(path.join(dirPath, "index.md"))) {
|
if (fs.existsSync(path.join(dirPath, "index.md"))) {
|
||||||
@@ -206,7 +207,7 @@ export function updatePost(
|
|||||||
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
||||||
.replace(/^-+|-+$/g, "")
|
.replace(/^-+|-+$/g, "")
|
||||||
.toLowerCase() || "post";
|
.toLowerCase() || "post";
|
||||||
const newDirName = `${newDateStr}-${titlePart}`;
|
const newDirName = `${newDateStr}-${titlePart}-${frontMatter.slug}`;
|
||||||
const newDirPath = path.join(CONTENT_DIR, newYear, newDirName);
|
const newDirPath = path.join(CONTENT_DIR, newYear, newDirName);
|
||||||
|
|
||||||
if (fs.existsSync(newDirPath) && newDirPath !== post.dirPath) {
|
if (fs.existsSync(newDirPath) && newDirPath !== post.dirPath) {
|
||||||
|
|||||||
@@ -47,12 +47,18 @@ export function moveToRecycle(
|
|||||||
expiresAt: expires.toISOString(),
|
expiresAt: expires.toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move post directory to recycle
|
// Move post directory to recycle (fallback to copy+delete for cross-filesystem)
|
||||||
const targetDir = path.join(RECYCLE_POSTS_DIR, slug);
|
const targetDir = path.join(RECYCLE_POSTS_DIR, slug);
|
||||||
if (fs.existsSync(targetDir)) {
|
if (fs.existsSync(targetDir)) {
|
||||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
fs.renameSync(sourceDir, targetDir);
|
try {
|
||||||
|
fs.renameSync(sourceDir, targetDir);
|
||||||
|
} catch (_e) {
|
||||||
|
// Cross-filesystem: copy then delete
|
||||||
|
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
||||||
|
fs.rmSync(sourceDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
// Save metadata
|
// Save metadata
|
||||||
const metaPath = path.join(RECYCLE_META_DIR, `${slug}.json`);
|
const metaPath = path.join(RECYCLE_META_DIR, `${slug}.json`);
|
||||||
@@ -110,7 +116,12 @@ export function restoreFromRecycle(slug: string): boolean {
|
|||||||
fs.rmSync(meta.originalPath, { recursive: true, force: true });
|
fs.rmSync(meta.originalPath, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(recyclePostPath, meta.originalPath);
|
try {
|
||||||
|
fs.renameSync(recyclePostPath, meta.originalPath);
|
||||||
|
} catch (_e) {
|
||||||
|
fs.cpSync(recyclePostPath, meta.originalPath, { recursive: true });
|
||||||
|
fs.rmSync(recyclePostPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
fs.unlinkSync(metaPath);
|
fs.unlinkSync(metaPath);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in new issue
Block a user