修复主题一些细节优化问题

This commit is contained in:
zqlit committed 2026-05-24 12:17:43 +08:00
1 parent 578bac0e12
commit a8f49d8651
24 files changed
+645 -67

No files matched your search

+180 -1
View File
@@ -2994,6 +2994,87 @@ input[type=submit] {
contain-intrinsic-size: 700px auto; contain-intrinsic-size: 700px auto;
} }
.post-content .sen-image-frame,
.entry-content .sen-image-frame {
position: relative;
display: block;
width: fit-content;
max-width: 100%;
margin: 1.2rem auto;
line-height: 0;
opacity: 0;
transform: translateY(14px);
transition: opacity 0.6s cubic-bezier(0.22, 0.61, 0.36, 1),
transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.post-content .sen-image-frame.is-visible,
.entry-content .sen-image-frame.is-visible {
opacity: 1;
transform: translateY(0);
}
.post-content .sen-image-frame>img,
.entry-content .sen-image-frame>img {
display: block;
width: auto;
max-width: 100%;
margin: 0;
box-shadow: none;
}
.post-content .sen-image-frame[data-title]::after,
.entry-content .sen-image-frame[data-title]::after {
content: attr(data-title);
position: absolute;
left: 10px;
bottom: 10px;
max-width: calc(100% - 20px);
padding: 5px 10px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.08);
background: rgba(255, 255, 255, 0.72);
color: rgba(0, 0, 0, 0.82);
-webkit-backdrop-filter: blur(16px) saturate(180%);
backdrop-filter: blur(16px) saturate(180%);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.03em;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.4s 0.2s cubic-bezier(0.22, 0.61, 0.36, 1),
transform 0.4s 0.2s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.post-content .sen-image-frame.is-visible[data-title]::after,
.entry-content .sen-image-frame.is-visible[data-title]::after {
opacity: 1;
transform: translateY(0);
}
[data-theme="dark"] .post-content .sen-image-frame[data-title]::after,
[data-theme="dark"] .entry-content .sen-image-frame[data-title]::after {
border-color: rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.62);
color: rgba(255, 255, 255, 0.92);
}
@media screen and (max-width: 768px) {
.post-content .sen-image-frame[data-title]::after,
.entry-content .sen-image-frame[data-title]::after {
left: 8px;
bottom: 8px;
max-width: calc(100% - 16px);
padding: 4px 8px;
font-size: 12px;
}
}
/* 设置进度条的背景色 */ /* 设置进度条的背景色 */
.bar { .bar {
background: #2aae67 !important; background: #2aae67 !important;
@@ -3504,9 +3585,107 @@ input[type=submit] {
pointer-events: none !important; pointer-events: none !important;
} }
/* Override ViewImage z-index to ensure it is above floating tools */ /* ── ViewImage: Apple frosted glass lightbox ── */
.view-image { .view-image {
z-index: 2000 !important; z-index: 2000 !important;
background: rgba(0, 0, 0, 0.45) !important;
}
@supports (backdrop-filter: blur(20px)) {
.view-image {
background: rgba(0, 0, 0, 0.32) !important;
}
}
/* Bottom toolbar — frosted glass bar */
.view-image-tools {
background: rgba(28, 28, 30, 0.78) !important;
backdrop-filter: blur(44px) saturate(200%) !important;
-webkit-backdrop-filter: blur(44px) saturate(200%) !important;
border: 1px solid rgba(255, 255, 255, 0.16) !important;
border-radius: 8px !important;
padding: 6px 12px !important;
max-width: 360px !important;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.38),
0 0 0 0.5px rgba(255, 255, 255, 0.1) inset !important;
}
/* Counter text */
.view-image-tools__count {
font-size: 13px;
font-weight: 500;
letter-spacing: 0.03em;
opacity: 0.9;
}
/* Glass buttons */
.view-image-btn {
width: 34px !important;
height: 34px !important;
border-radius: 6px !important;
background: rgba(255, 255, 255, 0.14) !important;
border: 1px solid rgba(255, 255, 255, 0.12);
transition: background 0.2s ease, transform 0.15s ease;
}
.view-image-btn:hover {
background: rgba(255, 255, 255, 0.28) !important;
transform: scale(1.06);
}
.view-image-btn:active {
transform: scale(0.96);
}
/* Close button in toolbar */
.view-image-tools .view-image-close {
margin: 0 0 0 10px !important;
}
/* Close area overlay */
.view-image-close__full {
cursor: zoom-out;
}
/* Image */
.view-image-lead img {
border-radius: 2px !important;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.06) !important;
}
/* Dark mode tweaks */
[data-theme="dark"] .view-image {
background: rgba(0, 0, 0, 0.55) !important;
}
@supports (backdrop-filter: blur(20px)) {
[data-theme="dark"] .view-image {
background: rgba(0, 0, 0, 0.4) !important;
}
}
[data-theme="dark"] .view-image-tools {
background: rgba(18, 18, 22, 0.82) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
/* Mobile */
@media screen and (max-width: 768px) {
.view-image {
padding: 4% !important;
}
.view-image-tools {
max-width: 100% !important;
border-radius: 6px !important;
padding: 5px 10px !important;
}
.view-image-btn {
width: 30px !important;
height: 30px !important;
}
} }
.floating-tools a { .floating-tools a {
+42
View File
@@ -32,6 +32,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize Typewriter Effect for Mobile Header // Initialize Typewriter Effect for Mobile Header
initTypewriter(); initTypewriter();
// Image frame scroll reveal
initImageFrameReveal();
}); });
/** /**
@@ -71,3 +74,42 @@ function initTypewriter() {
// Start typing with a small delay // Start typing with a small delay
setTimeout(typeWriter, 500); setTimeout(typeWriter, 500);
} }
/**
* Scroll-triggered reveal for sen-image-frame
*/
var senFrameObserver = null;
function initImageFrameReveal() {
var frames = document.querySelectorAll('.sen-image-frame');
if (!frames.length) return;
if (!window.IntersectionObserver) {
frames.forEach(function(frame) {
frame.classList.add('is-visible');
});
return;
}
if (senFrameObserver) {
senFrameObserver.disconnect();
}
senFrameObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
senFrameObserver.unobserve(entry.target);
}
});
}, {
rootMargin: "0px 0px 50px 0px",
threshold: 0.08
});
frames.forEach(function(frame) {
senFrameObserver.observe(frame);
});
}
window.initImageFrameReveal = initImageFrameReveal;
+7
View File
@@ -152,5 +152,12 @@ document.addEventListener("pjax:complete", function () {
} catch(e) { console.error('initSearch error:', e); } } catch(e) { console.error('initSearch error:', e); }
} }
// 重载图片框架滚动显示
if (typeof window.initImageFrameReveal === 'function') {
try {
window.initImageFrameReveal();
} catch(e) { console.error('initImageFrameReveal error:', e); }
}
refreshHeatmap(); refreshHeatmap();
}); });
@@ -14,4 +14,6 @@
{{- $src = $src | relURL -}} {{- $src = $src | relURL -}}
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}
<img src="{{ $src }}" alt="{{ $alt }}" class="lazyload lazy-loaded" loading="lazy" decoding="async"{{ with $width }} width="{{ . }}"{{ end }}{{ with $height }} height="{{ . }}"{{ end }}> <span class="sen-image-frame sen-media-host"{{ with $alt }} data-title="{{ . }}"{{ end }}>
<img src="{{ $src }}" alt="{{ $alt }}"{{ with $alt }} title="{{ . }}"{{ end }} class="lazyload lazy-loaded" loading="lazy" decoding="async"{{ with $width }} width="{{ . }}"{{ end }}{{ with $height }} height="{{ . }}"{{ end }}>
</span>
+7
View File
@@ -0,0 +1,7 @@
{
"slug": "202605232244",
"title": "rss的优化点",
"deletedAt": "2026-05-23T14:45:08.470Z",
"originalPath": "E:\\GitHub\\blog\\content\\post\\2026\\2026-05-23-rss的优化点",
"expiresAt": "2026-06-22T14:45:08.470Z"
}
+7
View File
@@ -0,0 +1,7 @@
{
"slug": "202605232251",
"title": "测试",
"deletedAt": "2026-05-23T15:59:30.678Z",
"originalPath": "E:\\GitHub\\blog\\content\\post\\2026\\2026-05-23-测试",
"expiresAt": "2026-06-22T15:59:30.678Z"
}
@@ -0,0 +1,14 @@
---
title: 测试
date: '2026-05-23T00:00:00.000Z'
slug: '202605232251'
draft: false
categories:
- 测试
tags:
- 测试
author: 小赵
layout: post
---
![](mpih806u5rw5.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+64 -26
View File
@@ -1,45 +1,83 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { exec } from "child_process"; import { execFile } from "child_process";
import { promisify } from "util"; import { promisify } from "util";
import { BLOG_ROOT } from "@/lib/config"; import { BLOG_ROOT } from "@/lib/config";
const execAsync = promisify(exec); const execFileAsync = promisify(execFile);
type GitExecError = Error & {
stdout?: string;
stderr?: string;
code?: number;
};
async function runGit(args: string[], allowFailure = false) {
try {
const { stdout, stderr } = await execFileAsync("git", args, {
cwd: BLOG_ROOT,
windowsHide: true,
maxBuffer: 1024 * 1024,
});
return { stdout, stderr, code: 0 };
} catch (error) {
const err = error as GitExecError;
const result = {
stdout: err.stdout || "",
stderr: err.stderr || err.message,
code: typeof err.code === "number" ? err.code : 1,
};
if (allowFailure) return result;
throw new Error((result.stderr || result.stdout || err.message).trim());
}
}
function isRebaseConflict(message: string) {
return /conflict|CONFLICT|Resolve all conflicts|Automatic merge failed|could not apply|rebase/i.test(message);
}
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json(); const body = await request.json();
const { title, slug } = body; const { title } = body;
if (!title) { if (!title) {
return NextResponse.json({ error: "Title is required" }, { status: 400 }); return NextResponse.json({ error: "Title is required" }, { status: 400 });
} }
const commitMsg = `发布: ${title}`;
// git add, commit, push from the blog root
const commands = [
`cd "${BLOG_ROOT}" && git add -A`,
`cd "${BLOG_ROOT}" && git commit -m "${commitMsg.replace(/"/g, '\\"')}"`,
`cd "${BLOG_ROOT}" && git push`,
];
let output = ""; let output = "";
for (const cmd of commands) {
try { const addResult = await runGit(["add", "content/post", "static"]);
const { stdout, stderr } = await execAsync(cmd); output += addResult.stdout + addResult.stderr;
output += stdout + (stderr || "");
} catch (err: unknown) { const diffResult = await runGit(["diff", "--cached", "--quiet"], true);
const msg = err instanceof Error ? err.message : String(err); if (diffResult.code === 1) {
// git commit may fail if nothing to commit — that's ok const commitResult = await runGit(["commit", "-m", `发布: ${title}`]);
if (msg.includes("nothing to commit") || msg.includes("nothing added")) { output += commitResult.stdout + commitResult.stderr;
continue; } else if (diffResult.code !== 0) {
} throw new Error((diffResult.stderr || diffResult.stdout || "检查暂存区失败").trim());
// git push failure is real
throw new Error(`Git command failed: ${msg}`);
}
} }
return NextResponse.json({ success: true, output: output.slice(0, 500) }); try {
const pullResult = await runGit(["pull", "--rebase", "--autostash", "origin", "main"]);
output += pullResult.stdout + pullResult.stderr;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (isRebaseConflict(message)) {
return NextResponse.json(
{
error: "远程仓库有新内容,自动合并失败。请在终端解决冲突后执行 git rebase --continue,再重新发布。",
detail: message.slice(0, 1000),
},
{ status: 409 }
);
}
throw error;
}
const pushResult = await runGit(["push"]);
output += pushResult.stdout + pushResult.stderr;
return NextResponse.json({ success: true, output: output.slice(0, 1000) });
} catch (error: unknown) { } catch (error: unknown) {
return NextResponse.json( return NextResponse.json(
{ error: error instanceof Error ? error.message : "Deploy failed" }, { error: error instanceof Error ? error.message : "Deploy failed" },
+9 -9
View File
@@ -390,7 +390,7 @@ export default function CommentsPage() {
if (!loggedIn) { if (!loggedIn) {
return ( return (
<div className="min-h-full flex items-center justify-center px-8"> <div className="min-h-full flex items-center justify-center px-8">
<div className="w-[340px] bg-white border border-line rounded-lg p-8"> <div className="w-[340px] bg-card border border-line rounded-lg p-8">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="w-10 h-10 bg-accent/8 rounded-full flex items-center justify-center mx-auto mb-3"> <div className="w-10 h-10 bg-accent/8 rounded-full flex items-center justify-center mx-auto mb-3">
<Key size={18} className="text-accent" /> <Key size={18} className="text-accent" />
@@ -480,7 +480,7 @@ export default function CommentsPage() {
{ label: "评论总数", value: stats.totalComments }, { label: "评论总数", value: stats.totalComments },
{ label: "评论用户", value: stats.uniqueUsers }, { label: "评论用户", value: stats.uniqueUsers },
].map((s) => ( ].map((s) => (
<div key={s.label} className="bg-white border border-line rounded-lg p-4 text-center"> <div key={s.label} className="bg-card border border-line rounded-lg p-4 text-center">
<div className="text-[11px] text-muted mb-1">{s.label}</div> <div className="text-[11px] text-muted mb-1">{s.label}</div>
<div className="text-[22px] font-bold text-accent">{s.value}</div> <div className="text-[22px] font-bold text-accent">{s.value}</div>
</div> </div>
@@ -490,7 +490,7 @@ export default function CommentsPage() {
{/* Top articles & top users side by side */} {/* Top articles & top users side by side */}
<div className="flex gap-5 min-h-0"> <div className="flex gap-5 min-h-0">
{/* Top articles */} {/* Top articles */}
<div className="flex-1 bg-white border border-line rounded-lg p-5"> <div className="flex-1 bg-card border border-line rounded-lg p-5">
<h3 className="text-[13px] font-bold text-text mb-4">评论最多的文章 Top 20</h3> <h3 className="text-[13px] font-bold text-text mb-4">评论最多的文章 Top 20</h3>
<div className="space-y-1"> <div className="space-y-1">
{stats.topArticles.map((a, i) => ( {stats.topArticles.map((a, i) => (
@@ -506,7 +506,7 @@ export default function CommentsPage() {
</div> </div>
{/* Top commenters */} {/* Top commenters */}
<div className="flex-1 bg-white border border-line rounded-lg p-5"> <div className="flex-1 bg-card border border-line rounded-lg p-5">
<h3 className="text-[13px] font-bold text-text mb-4">最活跃的用户 Top 30</h3> <h3 className="text-[13px] font-bold text-text mb-4">最活跃的用户 Top 30</h3>
<div className="space-y-1"> <div className="space-y-1">
{stats.topUsers.map((u, i) => ( {stats.topUsers.map((u, i) => (
@@ -540,7 +540,7 @@ export default function CommentsPage() {
<Loader2 size={13} className="animate-spin" /> 加载中 <Loader2 size={13} className="animate-spin" /> 加载中
</div> </div>
) : ( ) : (
<div className="flex-1 overflow-y-auto bg-white border border-line rounded-md"> <div className="flex-1 overflow-y-auto bg-card border border-line rounded-md">
{pages.map((p) => ( {pages.map((p) => (
<div key={p.id} <div key={p.id}
className={`flex items-center border-b border-line/50 text-[13px] transition-colors hover:bg-surface ${ className={`flex items-center border-b border-line/50 text-[13px] transition-colors hover:bg-surface ${
@@ -578,7 +578,7 @@ export default function CommentsPage() {
type="text" type="text"
value={editTitleText} value={editTitleText}
onChange={(e) => setEditTitleText(e.target.value)} onChange={(e) => setEditTitleText(e.target.value)}
className="flex-1 bg-white border border-line rounded-md px-2 py-1 text-[13px] text-text focus:outline-none focus:border-accent/30" className="flex-1 bg-card border border-line rounded-md px-2 py-1 text-[13px] text-text focus:outline-none focus:border-accent/30"
onKeyDown={(e) => { if (e.key === "Enter") handleSaveTitle(); if (e.key === "Escape") setEditingTitle(false); }} onKeyDown={(e) => { if (e.key === "Enter") handleSaveTitle(); if (e.key === "Escape") setEditingTitle(false); }}
autoFocus autoFocus
/> />
@@ -610,13 +610,13 @@ export default function CommentsPage() {
) : ( ) : (
<div className="flex-1 overflow-y-auto space-y-1.5"> <div className="flex-1 overflow-y-auto space-y-1.5">
{comments.length === 0 && ( {comments.length === 0 && (
<div className="flex items-center justify-center h-32 bg-white border border-line rounded-md text-[13px] text-muted"> <div className="flex items-center justify-center h-32 bg-card border border-line rounded-md text-[13px] text-muted">
暂无评论 暂无评论
</div> </div>
)} )}
{comments.map((c) => ( {comments.map((c) => (
<div key={c.id} className={`px-4 py-3 rounded-md border ${ <div key={c.id} className={`px-4 py-3 rounded-md border ${
c.is_collapsed ? "bg-surface border-line/40 opacity-55" : "bg-white border-line" c.is_collapsed ? "bg-surface border-line/40 opacity-55" : "bg-card border-line"
}`}> }`}>
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -695,7 +695,7 @@ export default function CommentsPage() {
)} )}
</> </>
) : ( ) : (
<div className="flex-1 flex items-center justify-center bg-white border border-line rounded-md text-[13px] text-muted"> <div className="flex-1 flex items-center justify-center bg-card border border-line rounded-md text-[13px] text-muted">
<MessageSquare size={18} className="mr-2 text-muted/30" /> <MessageSquare size={18} className="mr-2 text-muted/30" />
选择左侧文章查看评论 选择左侧文章查看评论
</div> </div>
+2 -2
View File
@@ -152,7 +152,7 @@ export default function FeedsPage() {
)} )}
{showAdd && ( {showAdd && (
<div className="mb-6 p-5 bg-white border border-line rounded-lg"> <div className="mb-6 p-5 bg-card border border-line rounded-lg">
<form onSubmit={handleAdd} className="flex gap-3 items-end"> <form onSubmit={handleAdd} className="flex gap-3 items-end">
<div className="flex-1"> <div className="flex-1">
<label className="block text-[12px] font-medium text-text/70 mb-1.5">博客名称</label> <label className="block text-[12px] font-medium text-text/70 mb-1.5">博客名称</label>
@@ -184,7 +184,7 @@ export default function FeedsPage() {
const h = health.get(f.url); const h = health.get(f.url);
const ck = checkingMap.get(f.url); const ck = checkingMap.get(f.url);
return ( return (
<div key={f.url} className={`flex items-center gap-4 px-5 py-3.5 bg-white border rounded-md group transition-all ${ <div key={f.url} className={`flex items-center gap-4 px-5 py-3.5 bg-card border rounded-md group transition-all ${
editTarget === f.url ? "ring-2 ring-accent/20 border-accent" : "border-line" editTarget === f.url ? "ring-2 ring-accent/20 border-accent" : "border-line"
}`}> }`}>
{ck ? <Loader2 size={13} className="animate-spin text-muted shrink-0" /> {ck ? <Loader2 size={13} className="animate-spin text-muted shrink-0" />
+46
View File
@@ -1,8 +1,10 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
color-scheme: light;
--bg: #fcfaf5; --bg: #fcfaf5;
--surface: #f6f1e7; --surface: #f6f1e7;
--card: #fffdf8;
--text: #223d66; --text: #223d66;
--muted: #6f7f96; --muted: #6f7f96;
--line: #e4dfd2; --line: #e4dfd2;
@@ -14,9 +16,26 @@
--warning: #c09a46; --warning: #c09a46;
} }
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #111827;
--surface: #1b2638;
--card: #243247;
--text: #f3f7fd;
--muted: #c1ccdc;
--line: #51627a;
--accent: #9fc4f4;
--accent-hover: #bfdbfe;
--danger: #fca5a5;
--danger-hover: #fecaca;
--success: #86efac;
--warning: #f3d58a;
}
@theme inline { @theme inline {
--color-bg: var(--bg); --color-bg: var(--bg);
--color-surface: var(--surface); --color-surface: var(--surface);
--color-card: var(--card);
--color-text: var(--text); --color-text: var(--text);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-line: var(--line); --color-line: var(--line);
@@ -36,6 +55,33 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
:root[data-theme="dark"] input,
:root[data-theme="dark"] textarea,
:root[data-theme="dark"] select {
color: var(--text);
}
:root[data-theme="dark"] input::placeholder,
:root[data-theme="dark"] textarea::placeholder {
color: #aebbd0;
}
:root[data-theme="dark"] .text-muted\/30,
:root[data-theme="dark"] .text-muted\/35,
:root[data-theme="dark"] .text-muted\/40,
:root[data-theme="dark"] .text-muted\/50 {
color: #aebbd0;
}
:root[data-theme="dark"] .text-text\/70,
:root[data-theme="dark"] .text-text\/80 {
color: #d8e2f0;
}
:root[data-theme="dark"] .border-line\/50 {
border-color: #43546d;
}
/* Scrollbar */ /* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }
+1 -1
View File
@@ -86,7 +86,7 @@ export default function ImagesPage() {
) : ( ) : (
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
{images.map((img) => ( {images.map((img) => (
<div key={img.path} className="bg-white border border-line rounded-lg overflow-hidden group hover:border-accent/20 transition-all"> <div key={img.path} className="bg-card border border-line rounded-lg overflow-hidden group hover:border-accent/20 transition-all">
<div className="aspect-square bg-surface flex items-center justify-center overflow-hidden"> <div className="aspect-square bg-surface flex items-center justify-center overflow-hidden">
<img <img
src={img.localUrl} src={img.localUrl}
+13 -2
View File
@@ -12,9 +12,20 @@ export default function RootLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const themeScript = `
try {
if (localStorage.getItem("write-theme") === "dark") {
document.documentElement.dataset.theme = "dark";
}
} catch {}
`;
return ( return (
<html lang="zh-CN" className="h-full"> <html lang="zh-CN" className="h-full" suppressHydrationWarning>
<body className="h-full flex bg-bg"> <head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="h-full flex bg-bg text-text">
<Sidebar /> <Sidebar />
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
{children} {children}
+2 -2
View File
@@ -191,7 +191,7 @@ export default function LinksPage() {
{/* Add form */} {/* Add form */}
{showAdd && ( {showAdd && (
<div className="mb-6 p-5 bg-white border border-line rounded-lg"> <div className="mb-6 p-5 bg-card border border-line rounded-lg">
<form onSubmit={handleAdd} className="flex gap-3 items-end"> <form onSubmit={handleAdd} className="flex gap-3 items-end">
<div className="flex-1"> <div className="flex-1">
<label className="block text-[12px] font-medium text-text/70 mb-1.5">网站名称</label> <label className="block text-[12px] font-medium text-text/70 mb-1.5">网站名称</label>
@@ -229,7 +229,7 @@ export default function LinksPage() {
return ( return (
<div key={l.url} <div key={l.url}
className={`flex items-center gap-4 px-5 py-3.5 border rounded-md group transition-all ${ className={`flex items-center gap-4 px-5 py-3.5 border rounded-md group transition-all ${
l.hidden ? "bg-surface/50 border-line/50 opacity-60" : "bg-white border-line" l.hidden ? "bg-surface/50 border-line/50 opacity-60" : "bg-card border-line"
} ${editTarget === l.url ? "ring-2 ring-accent/20 border-accent" : ""}`}> } ${editTarget === l.url ? "ring-2 ring-accent/20 border-accent" : ""}`}>
{/* Health indicator */} {/* Health indicator */}
+8 -8
View File
@@ -125,7 +125,7 @@ export default function HomePage() {
{/* Dashboard stats */} {/* Dashboard stats */}
{stats && ( {stats && (
<div className="grid grid-cols-3 gap-4 mb-8"> <div className="grid grid-cols-3 gap-4 mb-8">
<div className="bg-white border border-line rounded-lg p-4 flex items-center gap-3"> <div className="bg-card border border-line rounded-lg p-4 flex items-center gap-3">
<div className="w-9 h-9 bg-accent/8 rounded-lg flex items-center justify-center"> <div className="w-9 h-9 bg-accent/8 rounded-lg flex items-center justify-center">
<PostIcon size={17} className="text-accent" /> <PostIcon size={17} className="text-accent" />
</div> </div>
@@ -134,7 +134,7 @@ export default function HomePage() {
<div className="text-lg font-bold text-text">{stats.posts}</div> <div className="text-lg font-bold text-text">{stats.posts}</div>
</div> </div>
</div> </div>
<div className="bg-white border border-line rounded-lg p-4 flex items-center gap-3"> <div className="bg-card border border-line rounded-lg p-4 flex items-center gap-3">
<div className="w-9 h-9 bg-success/10 rounded-lg flex items-center justify-center"> <div className="w-9 h-9 bg-success/10 rounded-lg flex items-center justify-center">
<Rss size={17} className="text-success" /> <Rss size={17} className="text-success" />
</div> </div>
@@ -143,7 +143,7 @@ export default function HomePage() {
<div className="text-lg font-bold text-text">{stats.feeds}</div> <div className="text-lg font-bold text-text">{stats.feeds}</div>
</div> </div>
</div> </div>
<div className="bg-white border border-line rounded-lg p-4 flex items-center gap-3"> <div className="bg-card border border-line rounded-lg p-4 flex items-center gap-3">
<div className="w-9 h-9 bg-[#c09a46]/10 rounded-lg flex items-center justify-center"> <div className="w-9 h-9 bg-[#c09a46]/10 rounded-lg flex items-center justify-center">
<Link2 size={17} className="text-[#c09a46]" /> <Link2 size={17} className="text-[#c09a46]" />
</div> </div>
@@ -191,7 +191,7 @@ export default function HomePage() {
<select <select
value={batchAction || ""} value={batchAction || ""}
onChange={(e) => { setBatchAction(e.target.value as typeof batchAction || null); setBatchValue(""); }} onChange={(e) => { setBatchAction(e.target.value as typeof batchAction || null); setBatchValue(""); }}
className="bg-white border border-line rounded-md px-2 py-1.5 text-[12px] focus:outline-none focus:border-accent/30" className="bg-card border border-line rounded-md px-2 py-1.5 text-[12px] focus:outline-none focus:border-accent/30"
> >
<option value="">选择操作...</option> <option value="">选择操作...</option>
<option value="categories">修改分类</option> <option value="categories">修改分类</option>
@@ -200,14 +200,14 @@ export default function HomePage() {
</select> </select>
{batchAction === "draft" ? ( {batchAction === "draft" ? (
<select value={batchValue} onChange={(e) => setBatchValue(e.target.value)} <select value={batchValue} onChange={(e) => setBatchValue(e.target.value)}
className="bg-white border border-line rounded-md px-2 py-1.5 text-[12px] focus:outline-none"> className="bg-card border border-line rounded-md px-2 py-1.5 text-[12px] focus:outline-none">
<option value="">选择状态...</option> <option value="">选择状态...</option>
<option value="true">设为草稿</option> <option value="true">设为草稿</option>
<option value="false">设为发布</option> <option value="false">设为发布</option>
</select> </select>
) : batchAction ? ( ) : batchAction ? (
<input type="text" value={batchValue} onChange={(e) => setBatchValue(e.target.value)} <input type="text" value={batchValue} onChange={(e) => setBatchValue(e.target.value)}
className="flex-1 bg-white border border-line rounded-md px-2 py-1.5 text-[12px] focus:outline-none focus:border-accent/30" className="flex-1 bg-card border border-line rounded-md px-2 py-1.5 text-[12px] focus:outline-none focus:border-accent/30"
placeholder={batchAction === "tags" ? "用顿号分隔" : "用顿号分隔"} /> placeholder={batchAction === "tags" ? "用顿号分隔" : "用顿号分隔"} />
) : null} ) : null}
{batchAction && batchValue && ( {batchAction && batchValue && (
@@ -224,7 +224,7 @@ export default function HomePage() {
<div className="relative mb-6"> <div className="relative mb-6">
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted/50" /> <Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted/50" />
<input type="text" value={search} onChange={(e) => { setSearch(e.target.value); setPage(1); }} <input type="text" value={search} onChange={(e) => { setSearch(e.target.value); setPage(1); }}
placeholder="搜索标题、分类..." className="w-full pl-9 pr-4 py-2.5 bg-white border border-line rounded-md text-[13px] text-text placeholder:text-muted/40 focus:outline-none focus:border-accent/30 transition-colors" /> placeholder="搜索标题、分类..." className="w-full pl-9 pr-4 py-2.5 bg-card border border-line rounded-md text-[13px] text-text placeholder:text-muted/40 focus:outline-none focus:border-accent/30 transition-colors" />
</div> </div>
{/* Article list */} {/* Article list */}
@@ -247,7 +247,7 @@ export default function HomePage() {
)} )}
{posts.map((post, i) => ( {posts.map((post, i) => (
<div key={`${post.slug}-${i}`} <div key={`${post.slug}-${i}`}
className="group flex items-center gap-4 px-5 py-3.5 bg-white border border-line rounded-md hover:border-accent/15 transition-all"> className="group flex items-center gap-4 px-5 py-3.5 bg-card border border-line rounded-md hover:border-accent/15 transition-all">
{/* Select checkbox */} {/* Select checkbox */}
{selectMode && ( {selectMode && (
<button type="button" onClick={() => toggleSelect(post.slug)} className="shrink-0"> <button type="button" onClick={() => toggleSelect(post.slug)} className="shrink-0">
+1 -1
View File
@@ -77,7 +77,7 @@ export default function RecyclePage() {
<div className="space-y-2"> <div className="space-y-2">
{items.map((item) => ( {items.map((item) => (
<div key={item.slug} <div key={item.slug}
className="flex items-center gap-4 px-5 py-3.5 bg-white border border-line rounded-md hover:border-line/70 transition-all"> className="flex items-center gap-4 px-5 py-3.5 bg-card border border-line rounded-md hover:border-line/70 transition-all">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<span className="font-medium text-[14px] text-text">{item.title}</span> <span className="font-medium text-[14px] text-text">{item.title}</span>
<div className="flex items-center gap-3 mt-0.5 text-[12px] text-muted"> <div className="flex items-center gap-3 mt-0.5 text-[12px] text-muted">
+101 -7
View File
@@ -21,6 +21,8 @@ export default function WechatPage() {
const [selected, setSelected] = useState<PostItem | null>(null); const [selected, setSelected] = useState<PostItem | null>(null);
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
const [ipWhitelistError, setIpWhitelistError] = useState<{ ip: string; detail: string } | null>(null);
const [syncSuccess, setSyncSuccess] = useState<{ title: string } | null>(null);
const [status, setStatus] = useState<{ configured: boolean; appId?: string } | null>(null); const [status, setStatus] = useState<{ configured: boolean; appId?: string } | null>(null);
const [coverUrl, setCoverUrl] = useState(""); const [coverUrl, setCoverUrl] = useState("");
@@ -77,12 +79,17 @@ export default function WechatPage() {
const data = await syncRes.json(); const data = await syncRes.json();
if (!syncRes.ok || !data.ok) throw new Error(data.error || data.message || "同步失败"); if (!syncRes.ok || !data.ok) throw new Error(data.error || data.message || "同步失败");
setMessage({ type: "success", text: `已同步到微信草稿箱` }); setSyncSuccess({ title: selected.title });
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") { if (err instanceof DOMException && err.name === "AbortError") {
setMessage({ type: "error", text: "请求超时,微信接口响应较慢,请重试" }); setMessage({ type: "error", text: "请求超时,微信接口响应较慢,请重试" });
} else { } else {
setMessage({ type: "error", text: err instanceof Error ? err.message : "网络错误,请检查连接后重试" }); const text = err instanceof Error ? err.message : "网络错误,请检查连接后重试";
const ipMatch = text.match(/invalid ip\s+([^\s,]+)/i);
if (/not in whitelist|code=40164|invalid ip/i.test(text)) {
setIpWhitelistError({ ip: ipMatch?.[1] || "报错中显示的 IP", detail: text });
}
setMessage({ type: "error", text });
} }
} finally { setSyncing(false); } } finally { setSyncing(false); }
}; };
@@ -91,7 +98,7 @@ export default function WechatPage() {
<div className="h-full flex flex-col px-8 py-6"> <div className="h-full flex flex-col px-8 py-6">
<div className="flex items-center justify-between mb-6 shrink-0"> <div className="flex items-center justify-between mb-6 shrink-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button type="button" onClick={() => router.back()} className="p-1 -ml-1 rounded hover:bg-surface text-muted hover:text-text transition-colors"> <button type="button" aria-label="返回上一页" onClick={() => router.back()} className="p-1 -ml-1 rounded hover:bg-surface text-muted hover:text-text transition-colors">
<ArrowLeft size={16} /> <ArrowLeft size={16} />
</button> </button>
<h2 className="text-lg font-bold text-text">微信公众号</h2> <h2 className="text-lg font-bold text-text">微信公众号</h2>
@@ -120,13 +127,13 @@ export default function WechatPage() {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="搜索文章..." placeholder="搜索文章..."
className="w-full bg-white border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30 mb-3 shrink-0" className="w-full bg-card border border-line rounded-md px-3 py-2 text-[13px] focus:outline-none focus:border-accent/30 mb-3 shrink-0"
/> />
{loading ? ( {loading ? (
<div className="flex-1 flex items-center justify-center text-muted text-sm"><Loader2 size={14} className="animate-spin mr-2" />加载中</div> <div className="flex-1 flex items-center justify-center text-muted text-sm"><Loader2 size={14} className="animate-spin mr-2" />加载中</div>
) : ( ) : (
<div className="flex-1 overflow-y-auto bg-white border border-line rounded-md"> <div className="flex-1 overflow-y-auto bg-card border border-line rounded-md">
{filtered.map((p, i) => ( {filtered.map((p, i) => (
<button key={`${p.slug}-${i}`} type="button" onClick={() => setSelected(p)} <button key={`${p.slug}-${i}`} type="button" onClick={() => setSelected(p)}
className={`w-full text-left px-3.5 py-2.5 border-b border-line/50 text-[13px] transition-colors hover:bg-surface ${ className={`w-full text-left px-3.5 py-2.5 border-b border-line/50 text-[13px] transition-colors hover:bg-surface ${
@@ -159,7 +166,7 @@ export default function WechatPage() {
</button> </button>
</div> </div>
<div className="flex-1 bg-white border border-line rounded-md p-4 overflow-y-auto"> <div className="flex-1 bg-card border border-line rounded-md p-4 overflow-y-auto">
<div className="text-[11px] text-muted/60 mb-3 flex items-center gap-2"> <div className="text-[11px] text-muted/60 mb-3 flex items-center gap-2">
<span>预览</span> <span>预览</span>
<span className="text-line">|</span> <span className="text-line">|</span>
@@ -197,13 +204,100 @@ export default function WechatPage() {
</div> </div>
</> </>
) : ( ) : (
<div className="flex-1 flex items-center justify-center bg-white border border-line rounded-md text-[13px] text-muted"> <div className="flex-1 flex items-center justify-center bg-card border border-line rounded-md text-[13px] text-muted">
<FileText size={18} className="mr-2 text-muted/30" /> <FileText size={18} className="mr-2 text-muted/30" />
选择左侧文章同步到微信草稿箱 选择左侧文章同步到微信草稿箱
</div> </div>
)} )}
</div> </div>
</div> </div>
{syncSuccess && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#223d66]/25 px-4 backdrop-blur-sm">
<div className="w-full max-w-[460px] rounded-2xl border border-line bg-bg p-6 shadow-[0_24px_80px_rgba(34,61,102,0.22)]">
<div className="mb-4 flex items-start gap-3">
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#d1fae5] text-[#065f46]">
<Check size={18} />
</div>
<div>
<h3 className="text-[17px] font-semibold text-text">已同步到微信草稿箱</h3>
<p className="mt-1 text-[13px] leading-6 text-muted">
文章已经进入公众号草稿箱,可以去微信公众平台预览、调整排版,然后再决定是否群发。
</p>
</div>
</div>
<div className="rounded-xl border border-line bg-surface/70 px-4 py-3 text-[12px] leading-5 text-muted">
当前文章:<span className="font-medium text-text">{syncSuccess.title}</span>
</div>
<div className="mt-6 flex justify-end gap-3">
<button
type="button"
onClick={() => setSyncSuccess(null)}
className="rounded-md border border-line px-4 py-2 text-[13px] text-muted transition-colors hover:bg-surface hover:text-text"
>
我知道了
</button>
<a
href="https://mp.weixin.qq.com/"
target="_blank"
rel="noopener noreferrer"
className="rounded-md bg-accent px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-accent-hover"
>
打开微信公众平台
</a>
</div>
</div>
</div>
)}
{ipWhitelistError && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#223d66]/25 px-4 backdrop-blur-sm">
<div className="w-full max-w-[460px] rounded-2xl border border-line bg-bg p-6 shadow-[0_24px_80px_rgba(34,61,102,0.22)]">
<div className="mb-4 flex items-start gap-3">
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#fef3c7] text-[#92400e]">
!
</div>
<div>
<h3 className="text-[17px] font-semibold text-text">需要添加微信公众号 IP 白名单</h3>
<p className="mt-1 text-[13px] leading-6 text-muted">
微信拒绝了当前接口的出口 IP,请到微信公众号后台的「设置与开发 → 基本配置 → IP 白名单」添加下面这个 IP 后再重试。
</p>
</div>
</div>
<div className="rounded-xl border border-line bg-surface/70 px-4 py-3">
<div className="text-[11px] text-muted">需要添加的 IP</div>
<div className="mt-1 select-all font-mono text-[18px] font-semibold text-text">{ipWhitelistError.ip}</div>
</div>
<div className="mt-3 max-h-24 overflow-y-auto rounded-lg bg-card px-3 py-2 text-[11px] leading-5 text-muted/80">
{ipWhitelistError.detail}
</div>
<div className="mt-6 flex justify-end gap-3">
<a
href="https://mp.weixin.qq.com/"
target="_blank"
rel="noopener noreferrer"
className="rounded-md border border-line px-4 py-2 text-[13px] text-muted transition-colors hover:bg-surface hover:text-text"
>
打开微信公众平台
</a>
<button
type="button"
onClick={() => navigator.clipboard?.writeText(ipWhitelistError.ip)}
className="rounded-md border border-line px-4 py-2 text-[13px] text-muted transition-colors hover:bg-surface hover:text-text"
>
复制 IP
</button>
<button
type="button"
onClick={() => setIpWhitelistError(null)}
className="rounded-md bg-accent px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-accent-hover"
>
我知道了
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }
+2 -2
View File
@@ -76,7 +76,7 @@ export default function AIPanel({ selectedText, onApplyText, title, content, onA
<div> <div>
<h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">AI 助手</h3> <h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">AI 助手</h3>
<div className="bg-white border border-line rounded-lg overflow-hidden"> <div className="bg-card border border-line rounded-lg overflow-hidden">
{/* Text operations */} {/* Text operations */}
<div className="p-3 border-b border-line/50"> <div className="p-3 border-b border-line/50">
<div className="text-[10px] font-medium text-muted/70 uppercase mb-2">文本处理</div> <div className="text-[10px] font-medium text-muted/70 uppercase mb-2">文本处理</div>
@@ -135,7 +135,7 @@ export default function AIPanel({ selectedText, onApplyText, title, content, onA
{/* Result panel */} {/* Result panel */}
{(result || error) && ( {(result || error) && (
<div className="mt-3 p-3 bg-white border border-line rounded-lg"> <div className="mt-3 p-3 bg-card border border-line rounded-lg">
{error ? ( {error ? (
<div className="text-[12px] text-danger">{error}</div> <div className="text-[12px] text-danger">{error}</div>
) : ( ) : (
+85 -4
View File
@@ -36,6 +36,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const [selectedText, setSelectedText] = useState(""); const [selectedText, setSelectedText] = useState("");
const [autoSaved, setAutoSaved] = useState(false); const [autoSaved, setAutoSaved] = useState(false);
const [showPublishConfirm, setShowPublishConfirm] = useState(false);
// Auto-save: restore from localStorage on mount // Auto-save: restore from localStorage on mount
const cacheKey = isEditing ? `draft-edit-${initialData?.frontMatter.slug}` : "draft-new"; const cacheKey = isEditing ? `draft-edit-${initialData?.frontMatter.slug}` : "draft-new";
@@ -84,7 +85,45 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
} }
}; };
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => { const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
const imageFile = Array.from(e.clipboardData.files).find((file) => file.type.startsWith("image/"));
if (imageFile) {
e.preventDefault();
if (!dirPath) {
setMessage({ type: "error", text: "请先保存文章,再直接粘贴图片。这样图片才能放到文章同级文件夹。" });
return;
}
const el = e.currentTarget;
const start = el.selectionStart;
const end = el.selectionEnd;
setUploading(true);
setMessage(null);
try {
const fd = new FormData();
fd.append("file", imageFile);
fd.append("dirPath", dirPath);
const res = await fetch("/api/upload", { method: "POST", body: fd });
if (!res.ok) throw new Error("上传失败");
const data = await res.json();
const markdown = `![](${data.url})`;
const nextContent = content.slice(0, start) + markdown + content.slice(end);
const nextCursor = start + markdown.length;
setContent(nextContent);
requestAnimationFrame(() => {
el.focus();
el.selectionStart = nextCursor;
el.selectionEnd = nextCursor;
});
} catch {
setMessage({ type: "error", text: "粘贴图片上传失败" });
} finally {
setUploading(false);
}
return;
}
const text = e.clipboardData.getData("text/plain"); const text = e.clipboardData.getData("text/plain");
if (!text) return; if (!text) return;
@@ -179,6 +218,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
}; };
const handlePublish = async () => { const handlePublish = async () => {
setShowPublishConfirm(false);
setSaving(true); setMessage(null); setSaving(true); setMessage(null);
try { try {
// First save with draft:false // First save with draft:false
@@ -221,9 +261,10 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
}; };
const inputCls = const inputCls =
"w-full bg-white border border-line rounded-md px-3 py-2 text-[13px] text-text placeholder:text-muted/35 focus:outline-none focus:border-accent/30 transition-colors"; "w-full bg-card border border-line rounded-md px-3 py-2 text-[13px] text-text placeholder:text-muted/35 focus:outline-none focus:border-accent/30 transition-colors";
return ( return (
<>
<div className="flex gap-8"> <div className="flex gap-8">
<div className="flex-1 min-w-0 space-y-8"> <div className="flex-1 min-w-0 space-y-8">
{message && ( {message && (
@@ -355,7 +396,7 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
</button> </button>
<button <button
type="button" type="button"
onClick={handlePublish} onClick={() => setShowPublishConfirm(true)}
disabled={saving} disabled={saving}
className="flex items-center gap-1.5 px-5 py-2 text-[13px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover disabled:opacity-50 transition-colors" className="flex items-center gap-1.5 px-5 py-2 text-[13px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover disabled:opacity-50 transition-colors"
> >
@@ -382,6 +423,46 @@ export default function PostEditor({ initialData, isEditing }: PostEditorProps)
/> />
</div> </div>
</div> </div>
</div> </div>
{showPublishConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#223d66]/25 px-4 backdrop-blur-sm">
<div className="w-full max-w-[420px] rounded-2xl border border-line bg-bg p-6 shadow-[0_24px_80px_rgba(34,61,102,0.22)]">
<div className="mb-4 flex items-start gap-3">
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent">
<Send size={18} />
</div>
<div>
<h3 className="text-[17px] font-semibold text-text">确认发布到公网?</h3>
<p className="mt-1 text-[13px] leading-6 text-muted">
发布后会自动保存文章、生成 Git 提交,并推送到 GitHub。公网会同步更新,请确认内容已经检查无误。
</p>
</div>
</div>
<div className="rounded-xl border border-line bg-surface/70 px-4 py-3 text-[12px] leading-5 text-muted">
当前文章:<span className="font-medium text-text">{form.title || "未命名文章"}</span>
</div>
<div className="mt-6 flex justify-end gap-3">
<button
type="button"
onClick={() => setShowPublishConfirm(false)}
className="rounded-md border border-line px-4 py-2 text-[13px] text-muted transition-colors hover:bg-surface hover:text-text"
>
取消
</button>
<button
type="button"
onClick={handlePublish}
disabled={saving}
className="flex items-center gap-1.5 rounded-md bg-accent px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-accent-hover disabled:opacity-50"
>
<Send size={13} />
确认发布
</button>
</div>
</div>
</div>
)}
</>
); );
} }
+1 -1
View File
@@ -43,7 +43,7 @@ export default function ShortcutPanel({ textareaRef, value, onChange }: Shortcut
return ( return (
<div> <div>
<h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">快捷插入</h3> <h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">快捷插入</h3>
<div className="bg-white border border-line rounded-lg p-3 space-y-1"> <div className="bg-card border border-line rounded-lg p-3 space-y-1">
{shortcuts.map((s) => ( {shortcuts.map((s) => (
<button <button
key={s.label} key={s.label}
+2
View File
@@ -3,6 +3,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { FileText, PenSquare, MessageSquare, Trash2, Rss, Link2, MessageCircle, Image } from "lucide-react"; import { FileText, PenSquare, MessageSquare, Trash2, Rss, Link2, MessageCircle, Image } from "lucide-react";
import ThemeToggle from "./ThemeToggle";
const navItems = [ const navItems = [
{ href: "/", label: "文章", icon: FileText }, { href: "/", label: "文章", icon: FileText },
@@ -56,6 +57,7 @@ export default function Sidebar() {
{/* Footer */} {/* Footer */}
<div className="px-4 py-3 border-t border-line"> <div className="px-4 py-3 border-t border-line">
<ThemeToggle />
<p className="text-[10px] text-muted/50">localhost:8016</p> <p className="text-[10px] text-muted/50">localhost:8016</p>
</div> </div>
</aside> </aside>
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { useEffect, useState } from "react";
import { Moon, Sun } from "lucide-react";
type Theme = "light" | "dark";
export default function ThemeToggle() {
const [theme, setTheme] = useState<Theme>("light");
useEffect(() => {
const saved = localStorage.getItem("write-theme");
const nextTheme: Theme = saved === "dark" ? "dark" : "light";
setTheme(nextTheme);
if (nextTheme === "dark") {
document.documentElement.dataset.theme = "dark";
} else {
delete document.documentElement.dataset.theme;
}
}, []);
const toggleTheme = () => {
const nextTheme: Theme = theme === "dark" ? "light" : "dark";
setTheme(nextTheme);
localStorage.setItem("write-theme", nextTheme);
if (nextTheme === "dark") {
document.documentElement.dataset.theme = "dark";
} else {
delete document.documentElement.dataset.theme;
}
};
const isDark = theme === "dark";
const Icon = isDark ? Sun : Moon;
return (
<button
type="button"
onClick={toggleTheme}
className="mb-3 flex w-full items-center justify-between rounded-md border border-line px-3 py-2 text-[12px] text-muted transition-colors hover:bg-bg hover:text-accent"
title={isDark ? "切换到浅色模式" : "切换到夜间模式"}
aria-label={isDark ? "切换到浅色模式" : "切换到夜间模式"}
>
<span>{isDark ? "浅色模式" : "夜间模式"}</span>
<Icon size={14} />
</button>
);
}