added:增加Linux-server
This commit is contained in:
1 parent
6fe4e9d23f
commit
d78f3f811b
90 files changed
+16784
-1
No files matched your search
@@ -0,0 +1,303 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowLeft, Send, Loader2, Check, FileText } from "lucide-react";
|
||||
|
||||
interface PostItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
draft: boolean;
|
||||
categories: string[];
|
||||
author: string;
|
||||
}
|
||||
|
||||
export default function WechatPage() {
|
||||
const router = useRouter();
|
||||
const [posts, setPosts] = useState<PostItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
const [selected, setSelected] = useState<PostItem | null>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
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 [coverUrl, setCoverUrl] = useState("");
|
||||
|
||||
const fetchPosts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/posts?perPage=999&includeDrafts=false");
|
||||
const data = await res.json();
|
||||
setPosts(data.posts || []);
|
||||
} catch {} finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPosts();
|
||||
fetch("/api/rss/wechat").then(r => r.json()).then(setStatus).catch(() => {});
|
||||
}, [fetchPosts]);
|
||||
|
||||
const filtered = search
|
||||
? posts.filter((p) => p.title.includes(search) || p.slug.includes(search))
|
||||
: posts;
|
||||
|
||||
const handleSync = async () => {
|
||||
if (!selected) return;
|
||||
setSyncing(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
// Fetch full post content
|
||||
const res = await fetch(`/api/posts/${selected.slug}`);
|
||||
if (!res.ok) throw new Error("文章加载失败");
|
||||
const post = await res.json();
|
||||
|
||||
// Send to WeChat via RSS API proxy (60s timeout)
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 60000);
|
||||
const syncRes = await fetch("/api/rss/wechat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
articles: [{
|
||||
title: post.frontMatter.title.slice(0, 32),
|
||||
author: (post.frontMatter.author || "小赵").slice(0, 16),
|
||||
content: post.content,
|
||||
slug: post.frontMatter.slug,
|
||||
content_source_url: `https://usj.cc/${post.frontMatter.slug}.html`,
|
||||
thumb_media_url: coverUrl || "https://usj.cc/image/avatar/tx7.jpg",
|
||||
show_cover_pic: 1,
|
||||
need_open_comment: 0,
|
||||
}],
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timer);
|
||||
const data = await syncRes.json();
|
||||
if (!syncRes.ok || !data.ok) throw new Error(data.error || data.message || "同步失败");
|
||||
|
||||
setSyncSuccess({ title: selected.title });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
setMessage({ type: "error", text: "请求超时,微信接口响应较慢,请重试" });
|
||||
} else {
|
||||
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); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col px-4 py-4 md:px-8 md:py-6">
|
||||
<div className="flex items-center justify-between mb-6 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<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} />
|
||||
</button>
|
||||
<h2 className="text-lg font-bold text-text">微信公众号</h2>
|
||||
{status && (
|
||||
<span className={`text-[11px] px-2 py-0.5 rounded ${status.configured ? "bg-[#d1fae5] text-[#065f46]" : "bg-[#fef3c7] text-[#92400e]"}`}>
|
||||
{status.configured ? `已配置 · ${status.appId}` : "未配置"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`mb-4 px-4 py-2.5 rounded-md text-[13px] flex items-center justify-between shrink-0 ${
|
||||
message.type === "success" ? "bg-[#d1fae5] text-[#065f46] border border-[#a7f3d0]" : "bg-[#fee2e2] text-[#991b1b] border border-[#fecaca]"
|
||||
}`}>
|
||||
{message.text}
|
||||
<button type="button" onClick={() => setMessage(null)} className="underline">关闭</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 flex-1 min-h-0">
|
||||
{/* Left: article list */}
|
||||
<div className="w-full sm:w-[260px] sm:max-w-[300px] shrink-0 flex flex-col min-h-0 sm:max-h-full">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="搜索文章..."
|
||||
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 ? (
|
||||
<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-card border border-line rounded-md">
|
||||
{filtered.map((p, i) => (
|
||||
<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 ${
|
||||
selected?.slug === p.slug ? "bg-accent/5 border-l-[3px] border-l-accent pl-[11px]" : ""
|
||||
}`}>
|
||||
<div className="text-text truncate font-medium">{p.title}</div>
|
||||
<div className="text-[11px] text-muted mt-0.5">
|
||||
{new Date(p.date).toLocaleDateString("zh-CN")} · {p.author}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: preview & sync */}
|
||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-3 shrink-0">
|
||||
<div className="text-[13px] font-medium text-text truncate min-w-0">{selected.title}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSync}
|
||||
disabled={syncing}
|
||||
className="shrink-0 w-full sm:w-auto inline-flex items-center justify-center gap-1.5 px-4 py-2 text-[13px] font-medium rounded-md bg-accent text-white hover:bg-accent-hover disabled:opacity-50 transition-colors whitespace-nowrap"
|
||||
>
|
||||
{syncing ? <Loader2 size={13} className="animate-spin" /> : <Send size={13} />}
|
||||
<span>{syncing ? "同步中..." : "同步到微信草稿"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span>预览</span>
|
||||
<span className="text-line">|</span>
|
||||
<span>slug: {selected.slug}</span>
|
||||
<span className="text-line">|</span>
|
||||
<span>{new Date(selected.date).toLocaleDateString("zh-CN")}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-[12px] text-muted">
|
||||
<div><span className="font-medium text-text/70">标题:</span>{(selected.title || "").slice(0, 32)}</div>
|
||||
<div><span className="font-medium text-text/70">作者:</span>{(selected.author || "小赵").slice(0, 16)}</div>
|
||||
<div><span className="font-medium text-text/70">原文链接:</span>https://usj.cc/{selected.slug}.html</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-text/70 shrink-0">封面图:</span>
|
||||
<input
|
||||
type="text"
|
||||
value={coverUrl}
|
||||
onChange={(e) => setCoverUrl(e.target.value)}
|
||||
placeholder="https://... 留空则无封面"
|
||||
className="flex-1 bg-bg border border-line rounded px-2 py-1 text-[12px] focus:outline-none focus:border-accent/30"
|
||||
/>
|
||||
</div>
|
||||
<div><span className="font-medium text-text/70">分类:</span>{selected.categories?.join("、") || "-"}</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-surface rounded-md border border-line/50">
|
||||
<div className="text-[11px] font-medium text-muted mb-1">同步说明</div>
|
||||
<ul className="text-[11px] text-muted/70 space-y-0.5">
|
||||
<li>· 文章将同步到公众号草稿箱,不会自动群发</li>
|
||||
<li>· 封面图使用文章内第一张图片(如有)</li>
|
||||
<li>· Markdown 将由 RSS API 转为微信富文本</li>
|
||||
<li>· 在公众号后台「草稿箱」可预览和修改</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<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" />
|
||||
选择左侧文章同步到微信草稿箱
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user