"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([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [selected, setSelected] = useState(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 (

微信公众号

{status && ( {status.configured ? `已配置 · ${status.appId}` : "未配置"} )}
{message && (
{message.text}
)}
{/* Left: article list */}
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 ? (
加载中
) : (
{filtered.map((p, i) => ( ))}
)}
{/* Right: preview & sync */}
{selected ? ( <>
{selected.title}
预览 | slug: {selected.slug} | {new Date(selected.date).toLocaleDateString("zh-CN")}
标题:{(selected.title || "").slice(0, 32)}
作者:{(selected.author || "小赵").slice(0, 16)}
原文链接:https://usj.cc/{selected.slug}.html
封面图: 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" />
分类:{selected.categories?.join("、") || "-"}
同步说明
  • · 文章将同步到公众号草稿箱,不会自动群发
  • · 封面图使用文章内第一张图片(如有)
  • · Markdown 将由 RSS API 转为微信富文本
  • · 在公众号后台「草稿箱」可预览和修改
) : (
选择左侧文章同步到微信草稿箱
)}
{syncSuccess && (

已同步到微信草稿箱

文章已经进入公众号草稿箱,可以去微信公众平台预览、调整排版,然后再决定是否群发。

当前文章:{syncSuccess.title}
打开微信公众平台
)} {ipWhitelistError && (
!

需要添加微信公众号 IP 白名单

微信拒绝了当前接口的出口 IP,请到微信公众号后台的「设置与开发 → 基本配置 → IP 白名单」添加下面这个 IP 后再重试。

需要添加的 IP
{ipWhitelistError.ip}
{ipWhitelistError.detail}
打开微信公众平台
)}
); }