重写为 React 组件 — 原来的 artifact 是 HTML 格式,存在语法解析错误,现在改为 React 组件,更稳定 内置富文本编辑器 — 使用 contentEditable 实现,支持:
- 加粗 / 斜体 / 下划线
- H1 / H2 标题
- 左对齐 / 居中
- 有序 / 无序列表
- 撤销 / 重做
无需 API Key — 不依赖 TinyMCE 外部CDN,直接可用
import { useState, useEffect, useRef } from “react”; const CATEGORIES = [“科技”, “环保”, “经济”, “社会”, “文化”, “体育”]; const initArticles = [ { id: 1, title: “科技创新推动产业升级”, content: “<p>随着人工智能、大数据等新技术的不断发展,传统制造业正在经历深刻的数字化转型。企业通过引入智能制造技术,不仅提高了生产效率,还降低了运营成本。</p><p>专家预测,未来十年将是数字化转型的关键期。</p>”, author: “张三”, category: “科技”, tags: “人工智能,数字化转型”, date: new Date().toISOString(), }, { id: 2, title: “环保新政策助力绿色发展”, content: “<p>国家出台系列环保政策,鼓励企业采用清洁能源,推动绿色低碳发展。新政策包括碳排放交易、绿色金融支持、清洁能源补贴等多项措施。</p>”, author: “李四”, category: “环保”, tags: “环保政策,清洁能源”, date: new Date(Date.now() – 86400000).toISOString(), }, ]; function RichEditor({ value, onChange }) { const ref = useRef(null); const skip = useRef(false); useEffect(() => { if (ref.current && !skip.current) { ref.current.innerHTML = value || “”; } skip.current = false; }, [value]); const exec = (cmd, val) => { ref.current.focus(); document.execCommand(cmd, false, val || null); skip.current = true; onChange(ref.current.innerHTML); }; const tools = [ { label: “B”, cmd: “bold”, s: { fontWeight: “bold” } }, { label: “I”, cmd: “italic”, s: { fontStyle: “italic” } }, { label: “U”, cmd: “underline”, s: { textDecoration: “underline” } }, { label: “H1”, cmd: “formatBlock”, val: “H1” }, { label: “H2”, cmd: “formatBlock”, val: “H2” }, { label: “≡”, cmd: “justifyLeft” }, { label: “≡̈”, cmd: “justifyCenter” }, { label: “• 列表”, cmd: “insertUnorderedList” }, { label: “1. 列表”, cmd: “insertOrderedList” }, { label: “↩ 撤销”, cmd: “undo” }, { label: “↪ 重做”, cmd: “redo” }, ]; return ( <div style={{ border: “2px solid #e1e5e9”, borderRadius: 12, overflow: “hidden” }}> <div style={{ display: “flex”, flexWrap: “wrap”, gap: 4, padding: “8px 10px”, background: “#f8f9fa”, borderBottom: “1px solid #e1e5e9″ }}> {tools.map((t, i) => ( <button key={i} type=”button” onMouseDown={(e) => { e.preventDefault(); exec(t.cmd, t.val); }} style={{ padding: “4px 10px”, border: “1px solid #ddd”, borderRadius: 6, background: “#fff”, cursor: “pointer”, fontSize: 13, …(t.s || {}) }}> {t.label} </button> ))} </div> <div ref={ref} contentEditable suppressContentEditableWarning onInput={() => { skip.current = true; onChange(ref.current.innerHTML); }} style={{ minHeight: 200, padding: 16, fontSize: 15, lineHeight: 1.7, outline: “none”, background: “#fff” }} /> </div> ); } const fmtDate = (d) => new Date(d).toLocaleDateString(“zh-CN”); const S = { wrap: { minHeight: “100vh”, background: “linear-gradient(135deg,#667eea,#764ba2)”, padding: 20, fontFamily: “Microsoft YaHei,Arial,sans-serif” }, inner: { maxWidth: 1100, margin: “0 auto” }, card: { background: “rgba(255,255,255,0.97)”, borderRadius: 20, padding: 28, marginBottom: 24, boxShadow: “0 8px 32px rgba(0,0,0,.1)” }, btn: { padding: “10px 22px”, borderRadius: 50, border: “none”, cursor: “pointer”, fontWeight: 500, fontSize: 14, background: “linear-gradient(45deg,#667eea,#764ba2)”, color: “#fff” }, btnSec: { padding: “10px 22px”, borderRadius: 50, border: “none”, cursor: “pointer”, fontWeight: 500, fontSize: 14, background: “linear-gradient(45deg,#6c757d,#495057)”, color: “#fff” }, btnSm: { padding: “7px 14px”, borderRadius: 50, border: “none”, cursor: “pointer”, fontWeight: 500, fontSize: 13, background: “linear-gradient(45deg,#667eea,#764ba2)”, color: “#fff” }, btnSmSec: { padding: “7px 14px”, borderRadius: 50, border: “none”, cursor: “pointer”, fontWeight: 500, fontSize: 13, background: “linear-gradient(45deg,#6c757d,#495057)”, color: “#fff” }, btnDanger: { padding: “7px 14px”, borderRadius: 50, border: “none”, cursor: “pointer”, fontWeight: 500, fontSize: 13, background: “linear-gradient(45deg,#dc3545,#c82333)”, color: “#fff” }, input: { width: “100%”, padding: “12px 15px”, border: “2px solid #e1e5e9”, borderRadius: 12, fontSize: 15, boxSizing: “border-box”, background: “#f8f9fa” }, label: { display: “block”, fontWeight: 600, marginBottom: 8, color: “#333” }, }; export default function App() { const [page, setPage] = useState(“list”); const [articles, setArticles] = useState(initArticles); const [nid, setNid] = useState(3); const [search, setSearch] = useState(“”); const [catF, setCatF] = useState(“”); const [cur, setCur] = useState(null); const [msg, setMsg] = useState(null); const blank = { id: “”, title: “”, author: “”, category: “”, tags: “”, content: “” }; const [form, setForm] = useState(blank); const showMsg = (text, type = “success”) => { setMsg({ text, type }); setTimeout(() => setMsg(null), 3000); }; const goList = () => setPage(“list”); const goCreate = () => { setForm(blank); setPage(“create”); }; const goEdit = (a) => { setForm({ …a }); setPage(“edit”); }; const goView = (a) => { setCur(a); setPage(“view”); }; const filtered = articles.filter((a) => { const s = search.toLowerCase(); return (!s || a.title.toLowerCase().includes(s) || a.content.toLowerCase().includes(s)) && (!catF || a.category === catF); }); const handleSave = () => { if (!form.title || !form.author || !form.category || !form.content) { showMsg(“请填写所有必需字段!”, “error”); return; } if (form.id) { setArticles((p) => p.map((a) => (a.id === form.id ? { …a, …form } : a))); showMsg(“文章更新成功!”); } else { setArticles((p) => [{ …form, id: nid, date: new Date().toISOString() }, …p]); setNid((n) => n + 1); showMsg(“文章发布成功!”); } goList(); }; const delArticle = (id) => { if (!window.confirm(“确定要删除这篇文章吗?”)) return; setArticles((p) => p.filter((a) => a.id !== id)); showMsg(“文章删除成功!”); if (page === “view”) goList(); }; const set = (k) => (e) => setForm((f) => ({ …f, [k]: e.target.value })); return ( <div style={S.wrap}> <div style={S.inner}> {/* Header */} <div style={{ …S.card, textAlign: “center” }}> <h1 style={{ fontSize: “2em”, background: “linear-gradient(45deg,#667eea,#764ba2)”, WebkitBackgroundClip: “text”, WebkitTextFillColor: “transparent”, marginBottom: 6 }}> 📰 PHP新闻发布系统 </h1> <p style={{ color: “#666”, marginBottom: 16 }}>专业的内容管理平台,让创作更简单</p> <div style={{ display: “flex”, justifyContent: “center”, gap: 16 }}> <button style={S.btn} onClick={goList}>📋 文章管理</button> <button style={S.btn} onClick={goCreate}>✏️ 发布文章</button> </div> </div> {/* Message */} {msg && ( <div style={{ padding: “14px 20px”, borderRadius: 12, marginBottom: 20, fontWeight: 500, color: “#fff”, background: msg.type === “success” ? “linear-gradient(45deg,#28a745,#20c997)” : “linear-gradient(45deg,#dc3545,#fd7e14)” }}> {msg.text} </div> )} {/* LIST */} {page === “list” && ( <> <div style={S.card}> <div style={{ display: “flex”, justifyContent: “space-between”, alignItems: “center”, marginBottom: 20 }}> <h2 style={{ color: “#333” }}>文章管理中心</h2> <button style={S.btn} onClick={goCreate}>+ 发布新文章</button> </div> <div style={{ display: “grid”, gridTemplateColumns: “1fr auto auto”, gap: 12 }}> <input style={S.input} placeholder=”🔍 搜索文章…” value={search} onChange={(e) => setSearch(e.target.value)} /> <select style={{ …S.input, width: “auto” }} value={catF} onChange={(e) => setCatF(e.target.value)}> <option value=””>📂 全部</option> {CATEGORIES.map((c) => <option key={c}>{c}</option>)} </select> <button style={S.btn}>搜索</button> </div> </div> {filtered.length === 0 ? ( <div style={{ …S.card, textAlign: “center”, color: “#888”, padding: “60px 20px” }}>📝 暂无文章,点击按钮开始创作吧!</div> ) : ( <div style={{ display: “grid”, gridTemplateColumns: “repeat(auto-fill,minmax(320px,1fr))”, gap: 22 }}> {filtered.map((a) => ( <div key={a.id} style={{ …S.card, marginBottom: 0 }}> <div style={{ display: “flex”, justifyContent: “space-between”, alignItems: “center”, marginBottom: 10 }}> <span style={{ background: “linear-gradient(45deg,#667eea,#764ba2)”, color: “#fff”, padding: “3px 14px”, borderRadius: 20, fontSize: 12 }}>{a.category}</span> <span style={{ color: “#888”, fontSize: 13 }}>📅 {fmtDate(a.date)}</span> </div> <h3 style={{ fontSize: “1.1em”, marginBottom: 10, color: “#333” }}>{a.title}</h3> <div style={{ color: “#666”, fontSize: 14, marginBottom: 10, overflow: “hidden”, display: “-webkit-box”, WebkitLineClamp: 3, WebkitBoxOrient: “vertical” }} dangerouslySetInnerHTML={{ __html: a.content }} /> <div style={{ color: “#666”, fontSize: 13, marginBottom: 10 }}>👤 {a.author}</div> {a.tags && ( <div style={{ display: “flex”, flexWrap: “wrap”, gap: 6, marginBottom: 14 }}> {a.tags.split(“,”).slice(0, 3).map((t) => ( <span key={t} style={{ background: “#f1f3f4”, color: “#5f6368”, padding: “3px 10px”, borderRadius: 12, fontSize: 12 }}>#{t.trim()}</span> ))} </div> )} <div style={{ display: “flex”, gap: 8 }}> <button style={S.btnSm} onClick={() => goView(a)}>👁️ 查看</button> <button style={S.btnSmSec} onClick={() => goEdit(a)}>✏️ 编辑</button> <button style={S.btnDanger} onClick={() => delArticle(a.id)}>🗑️ 删除</button> </div> </div> ))} </div> )} </> )} {/* CREATE / EDIT */} {(page === “create” || page === “edit”) && ( <div style={S.card}> <div style={{ display: “flex”, justifyContent: “space-between”, alignItems: “center”, marginBottom: 24 }}> <h2 style={{ color: “#333” }}>{page === “edit” ? “✏️ 编辑文章” : “📝 发布新文章”}</h2> <button style={S.btnSec} onClick={goList}>← 返回列表</button> </div> <div style={{ marginBottom: 20 }}> <label style={S.label}>📝 文章标题 *</label> <input style={S.input} placeholder=”请输入吸引人的标题…” value={form.title} onChange={set(“title”)} /> </div> <div style={{ display: “grid”, gridTemplateColumns: “1fr 1fr”, gap: 20, marginBottom: 20 }}> <div> <label style={S.label}>👤 作者 *</label> <input style={S.input} placeholder=”请输入作者姓名” value={form.author} onChange={set(“author”)} /> </div> <div> <label style={S.label}>📂 分类 *</label> <select style={S.input} value={form.category} onChange={set(“category”)}> <option value=””>请选择分类</option> {CATEGORIES.map((c) => <option key={c}>{c}</option>)} </select> </div> </div> <div style={{ marginBottom: 20 }}> <label style={S.label}>🏷️ 标签(用逗号分隔)</label> <input style={S.input} placeholder=”例如:科技,创新,人工智能” value={form.tags} onChange={set(“tags”)} /> </div> <div style={{ marginBottom: 24 }}> <label style={S.label}>📄 文章内容(富文本编辑器)*</label> <RichEditor value={form.content} onChange={(v) => setForm((f) => ({ …f, content: v }))} /> </div> <div style={{ textAlign: “center” }}> <button style={{ …S.btn, marginRight: 12 }} onClick={handleSave}> 💾 {page === “edit” ? “更新文章” : “发布文章”} </button> <button style={S.btnSec} onClick={goList}>取消</button> </div> </div> )} {/* VIEW */} {page === “view” && cur && ( <div style={{ …S.card, maxWidth: 800, margin: “0 auto” }}> <div style={{ display: “flex”, justifyContent: “space-between”, alignItems: “center”, marginBottom: 24 }}> <button style={S.btnSec} onClick={goList}>← 返回列表</button> <div style={{ display: “flex”, gap: 8 }}> <button style={S.btnSmSec} onClick={() => goEdit(cur)}>✏️ 编辑</button> <button style={S.btnDanger} onClick={() => delArticle(cur.id)}>🗑️ 删除</button> </div> </div> <h1 style={{ color: “#333”, marginBottom: 20, fontSize: “1.8em”, lineHeight: 1.3 }}>{cur.title}</h1> <div style={{ background: “#f8f9fa”, padding: 18, borderRadius: 14, marginBottom: 24, display: “grid”, gridTemplateColumns: “repeat(auto-fit,minmax(160px,1fr))”, gap: 12, fontSize: 14 }}> <div><strong>👤 作者:</strong>{cur.author}</div> <div><strong>📂 分类:</strong>{cur.category}</div> <div><strong>📅 时间:</strong>{fmtDate(cur.date)}</div> {cur.tags && <div><strong>🏷️ 标签:</strong>{cur.tags}</div>} </div> <div style={{ lineHeight: 1.8, color: “#444”, fontSize: “1.05em” }} dangerouslySetInnerHTML={{ __html: cur.content }} /> </div> )} </div> </div> ); }