修复问题,通过浏览器插件发布过的笔记后,在web页面的笔记的状态不会更改,还有一个问题,如果上传失败以后,笔记的状态就变成了在未审核页面的改成了失败,应该是在已审核页面中笔记列表的“使用状态”变成失败,可以批量将使用失败的笔记改为待发布的状态,已经发布成功的状态改为已使用。分析一下现在的流程,感觉有点混乱

This commit is contained in:
taiyi 2026-07-22 09:34:55 +08:00
parent 2a3eac2d49
commit 320e130ea5
6 changed files with 131 additions and 23 deletions

View File

@ -58,9 +58,9 @@ app.get('/api/stats', (req, res) => {
const shopCount = db.prepare('SELECT COUNT(*) as cnt FROM shops').get().cnt; const shopCount = db.prepare('SELECT COUNT(*) as cnt FROM shops').get().cnt;
const productCount = db.prepare('SELECT COUNT(*) as cnt FROM products').get().cnt; const productCount = db.prepare('SELECT COUNT(*) as cnt FROM products').get().cnt;
const noteCount = db.prepare('SELECT COUNT(*) as cnt FROM notes').get().cnt; const noteCount = db.prepare('SELECT COUNT(*) as cnt FROM notes').get().cnt;
const publishedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'published'").get().cnt; const publishedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE use_status = 'used'").get().cnt;
const pendingReviewCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'generated'").get().cnt; const pendingReviewCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'generated'").get().cnt;
const failedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'failed'").get().cnt; const failedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'failed' OR use_status = 'failed'").get().cnt;
res.json({ shops: shopCount, products: productCount, notes: noteCount, published: publishedCount, pendingReview: pendingReviewCount, failed: failedCount }); res.json({ shops: shopCount, products: productCount, notes: noteCount, published: publishedCount, pendingReview: pendingReviewCount, failed: failedCount });
}); });
@ -78,7 +78,7 @@ app.use((req, res, next) => {
setInterval(() => { setInterval(() => {
try { try {
const stale = db.prepare( const stale = db.prepare(
"UPDATE notes SET status = 'approved', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')" "UPDATE notes SET status = 'approved', use_status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')"
).run(); ).run();
if (stale.changes > 0) { if (stale.changes > 0) {
console.log(`[定时任务] 释放了 ${stale.changes} 条超时的 publishing 笔记`); console.log(`[定时任务] 释放了 ${stale.changes} 条超时的 publishing 笔记`);

View File

@ -37,6 +37,7 @@ function initDatabase() {
topics TEXT DEFAULT '[]', topics TEXT DEFAULT '[]',
image_paths TEXT DEFAULT '[]', image_paths TEXT DEFAULT '[]',
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated', 'draft', 'approved', 'publishing', 'published', 'failed', 'rejected')), status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated', 'draft', 'approved', 'publishing', 'published', 'failed', 'rejected')),
use_status TEXT DEFAULT 'pending',
error_message TEXT DEFAULT '', error_message TEXT DEFAULT '',
publish_result TEXT DEFAULT NULL, publish_result TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@ -67,12 +68,30 @@ function initDatabase() {
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
); );
CREATE TABLE IF NOT EXISTS generation_jobs (
id TEXT PRIMARY KEY,
shop_id TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
total_products INTEGER DEFAULT 0,
total_notes INTEGER DEFAULT 0,
completed_notes INTEGER DEFAULT 0,
failed_notes INTEGER DEFAULT 0,
current_product_id TEXT DEFAULT NULL,
current_product_title TEXT DEFAULT '',
message TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_products_shop_id ON products(shop_id); CREATE INDEX IF NOT EXISTS idx_products_shop_id ON products(shop_id);
CREATE INDEX IF NOT EXISTS idx_notes_product_id ON notes(product_id); CREATE INDEX IF NOT EXISTS idx_notes_product_id ON notes(product_id);
CREATE INDEX IF NOT EXISTS idx_notes_shop_id ON notes(shop_id); CREATE INDEX IF NOT EXISTS idx_notes_shop_id ON notes(shop_id);
CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status); CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status);
CREATE INDEX IF NOT EXISTS idx_publish_tasks_status ON publish_tasks(status); CREATE INDEX IF NOT EXISTS idx_publish_tasks_status ON publish_tasks(status);
CREATE INDEX IF NOT EXISTS idx_publish_tasks_shop_id ON publish_tasks(shop_id); CREATE INDEX IF NOT EXISTS idx_publish_tasks_shop_id ON publish_tasks(shop_id);
CREATE INDEX IF NOT EXISTS idx_generation_jobs_status ON generation_jobs(status);
CREATE INDEX IF NOT EXISTS idx_generation_jobs_shop_id ON generation_jobs(shop_id);
CREATE TABLE IF NOT EXISTS image_hashes ( CREATE TABLE IF NOT EXISTS image_hashes (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -90,12 +109,33 @@ function initDatabase() {
"ALTER TABLE notes ADD COLUMN image_prompt TEXT DEFAULT ''", "ALTER TABLE notes ADD COLUMN image_prompt TEXT DEFAULT ''",
"ALTER TABLE notes ADD COLUMN worker_id TEXT DEFAULT NULL", "ALTER TABLE notes ADD COLUMN worker_id TEXT DEFAULT NULL",
"ALTER TABLE notes ADD COLUMN claimed_at DATETIME DEFAULT NULL", "ALTER TABLE notes ADD COLUMN claimed_at DATETIME DEFAULT NULL",
"ALTER TABLE notes ADD COLUMN use_status TEXT DEFAULT 'pending'",
"ALTER TABLE products ADD COLUMN reference_image TEXT DEFAULT ''", "ALTER TABLE products ADD COLUMN reference_image TEXT DEFAULT ''",
]; ];
for (const sql of migrate) { for (const sql of migrate) {
try { db.exec(sql); } catch (e) { /* column already exists */ } try { db.exec(sql); } catch (e) { /* column already exists */ }
} }
db.exec(`
UPDATE notes
SET use_status = 'used', status = 'approved'
WHERE status = 'published';
UPDATE notes
SET use_status = 'failed', status = 'approved'
WHERE status = 'failed'
AND EXISTS (
SELECT 1 FROM publish_tasks
WHERE publish_tasks.note_id = notes.id
AND publish_tasks.status = 'failed'
);
UPDATE notes
SET use_status = 'pending'
WHERE status IN ('approved', 'publishing')
AND (use_status IS NULL OR use_status = '');
`);
// Insert default configs // Insert default configs
const insertConfig = db.prepare(` const insertConfig = db.prepare(`

View File

@ -122,7 +122,7 @@ function createNoteRoutes(db) {
return res.status(400).json({ error: 'ids array required' }); return res.status(400).json({ error: 'ids array required' });
} }
const placeholders = ids.map(() => '?').join(','); const placeholders = ids.map(() => '?').join(',');
db.prepare('UPDATE notes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('approved', ...ids); db.prepare('UPDATE notes SET status = ?, use_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('approved', 'pending', ...ids);
const batchNotes = db.prepare('SELECT * FROM notes WHERE id IN (' + placeholders + ')').all(...ids); const batchNotes = db.prepare('SELECT * FROM notes WHERE id IN (' + placeholders + ')').all(...ids);
for (const note of batchNotes) { for (const note of batchNotes) {
createPublishTask(note); createPublishTask(note);
@ -136,21 +136,47 @@ function createNoteRoutes(db) {
return res.status(400).json({ error: 'ids array required' }); return res.status(400).json({ error: 'ids array required' });
} }
const placeholders = ids.map(() => '?').join(','); const placeholders = ids.map(() => '?').join(',');
db.prepare('UPDATE notes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('pending', ...ids); db.prepare('UPDATE notes SET status = ?, use_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('pending', 'pending', ...ids);
res.json({ updated: ids.length }); res.json({ updated: ids.length });
}); });
router.post('/batch/publish-reset', (req, res) => {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'ids array required' });
}
const placeholders = ids.map(() => '?').join(',');
const resetNotes = db.prepare(
'SELECT * FROM notes WHERE id IN (' + placeholders + ") AND status = 'approved' AND use_status = 'failed'"
).all(...ids);
const reset = db.transaction(() => {
const result = db.prepare(
'UPDATE notes SET use_status = ?, error_message = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ") AND status = 'approved' AND use_status = 'failed'"
).run('pending', '', ...ids);
for (const note of resetNotes) {
createPublishTask({ ...note, use_status: 'pending' });
}
return result.changes;
});
res.json({ updated: reset() });
});
router.put('/:id', (req, res) => { router.put('/:id', (req, res) => {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id); const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id);
if (!note) return res.status(404).json({ error: 'Note not found' }); if (!note) return res.status(404).json({ error: 'Note not found' });
const { title, content, topics, image_paths, status, image_prompt, worker_id } = req.body; const { title, content, topics, image_paths, status, image_prompt, worker_id } = req.body;
db.prepare(`UPDATE notes SET title=?, content=?, topics=?, image_paths=?, status=?, image_prompt=?, worker_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`) const nextStatus = status || note.status;
const nextUseStatus = nextStatus === 'approved' && note.status !== 'approved' ? 'pending' : note.use_status;
db.prepare(`UPDATE notes SET title=?, content=?, topics=?, image_paths=?, status=?, use_status=?, image_prompt=?, worker_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`)
.run( .run(
title || note.title, title || note.title,
content !== undefined ? content : note.content, content !== undefined ? content : note.content,
topics ? JSON.stringify(topics) : note.topics, topics ? JSON.stringify(topics) : note.topics,
image_paths ? JSON.stringify(image_paths) : note.image_paths, image_paths ? JSON.stringify(image_paths) : note.image_paths,
status || note.status, nextStatus,
nextUseStatus,
image_prompt !== undefined ? image_prompt : note.image_prompt, image_prompt !== undefined ? image_prompt : note.image_prompt,
worker_id !== undefined ? worker_id : note.worker_id, worker_id !== undefined ? worker_id : note.worker_id,
req.params.id req.params.id
@ -177,7 +203,7 @@ function createNoteRoutes(db) {
const product = db.prepare('SELECT * FROM products WHERE id = ?').get(note.product_id); const product = db.prepare('SELECT * FROM products WHERE id = ?').get(note.product_id);
const productTitle = product?.title || note.title; const productTitle = product?.title || note.title;
db.prepare("UPDATE notes SET status='generating', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(req.params.id); db.prepare("UPDATE notes SET status='generating', use_status='pending', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(req.params.id);
// Cancel any pending publish tasks for this note // Cancel any pending publish tasks for this note
db.prepare("UPDATE publish_tasks SET status='failed', updated_at=CURRENT_TIMESTAMP WHERE note_id = ? AND status IN ('pending', 'claimed', 'executing')").run(req.params.id); db.prepare("UPDATE publish_tasks SET status='failed', updated_at=CURRENT_TIMESTAMP WHERE note_id = ? AND status IN ('pending', 'claimed', 'executing')").run(req.params.id);
@ -225,7 +251,7 @@ function createNoteRoutes(db) {
router.post('/:id/complete', (req, res) => { router.post('/:id/complete', (req, res) => {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id); const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id);
if (!note) return res.status(404).json({ error: 'Note not found' }); if (!note) return res.status(404).json({ error: 'Note not found' });
db.prepare("UPDATE notes SET status='published', publish_result=?, published_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?") db.prepare("UPDATE notes SET status='approved', use_status='used', publish_result=?, error_message='', published_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?")
.run(JSON.stringify(req.body.result || {}), req.params.id); .run(JSON.stringify(req.body.result || {}), req.params.id);
db.prepare("UPDATE publish_tasks SET status='completed', updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'") db.prepare("UPDATE publish_tasks SET status='completed', updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'")
.run(req.params.id); .run(req.params.id);
@ -236,7 +262,7 @@ function createNoteRoutes(db) {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id); const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id);
if (!note) return res.status(404).json({ error: 'Note not found' }); if (!note) return res.status(404).json({ error: 'Note not found' });
const { error_message } = req.body; const { error_message } = req.body;
db.prepare("UPDATE notes SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE id=?") db.prepare("UPDATE notes SET status='approved', use_status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE id=?")
.run(error_message || 'Unknown error', req.params.id); .run(error_message || 'Unknown error', req.params.id);
db.prepare("UPDATE publish_tasks SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'") db.prepare("UPDATE publish_tasks SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'")
.run(error_message || 'Unknown error', req.params.id); .run(error_message || 'Unknown error', req.params.id);

View File

@ -60,7 +60,7 @@ function createTaskRoutes(db) {
"UPDATE publish_tasks SET status = 'pending', claimed_by = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status IN ('claimed', 'executing') AND claimed_at < datetime('now', '-10 minutes')" "UPDATE publish_tasks SET status = 'pending', claimed_by = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status IN ('claimed', 'executing') AND claimed_at < datetime('now', '-10 minutes')"
).run(shop_id); ).run(shop_id);
db.prepare( db.prepare(
"UPDATE notes SET status = 'approved', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')" "UPDATE notes SET status = 'approved', use_status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')"
).run(shop_id); ).run(shop_id);
const claim = db.transaction(() => { const claim = db.transaction(() => {
@ -72,7 +72,7 @@ function createTaskRoutes(db) {
"UPDATE publish_tasks SET status = 'claimed', claimed_by = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'" "UPDATE publish_tasks SET status = 'claimed', claimed_by = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'"
).run(claimed_by, task.id); ).run(claimed_by, task.id);
if (result.changes === 0) return null; if (result.changes === 0) return null;
db.prepare("UPDATE notes SET status = 'publishing', worker_id = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?") db.prepare("UPDATE notes SET status = 'publishing', use_status = 'publishing', worker_id = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
.run(claimed_by, task.note_id); .run(claimed_by, task.note_id);
return task; return task;
}); });
@ -112,7 +112,7 @@ function createTaskRoutes(db) {
).run(req.params.id); ).run(req.params.id);
if (result.changes === 0) return false; if (result.changes === 0) return false;
db.prepare( db.prepare(
"UPDATE notes SET status = 'published', publish_result = ?, published_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?" "UPDATE notes SET status = 'approved', use_status = 'used', publish_result = ?, error_message = '', published_at = CURRENT_TIMESTAMP, worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
).run(JSON.stringify(req.body.result || {}), task.note_id); ).run(JSON.stringify(req.body.result || {}), task.note_id);
return true; return true;
}); });
@ -142,7 +142,7 @@ function createTaskRoutes(db) {
).run(errorMessage, req.params.id); ).run(errorMessage, req.params.id);
if (result.changes === 0) return false; if (result.changes === 0) return false;
db.prepare( db.prepare(
"UPDATE notes SET status = 'failed', error_message = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?" "UPDATE notes SET status = 'approved', use_status = 'failed', error_message = ?, worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
).run(errorMessage, task.note_id); ).run(errorMessage, task.note_id);
return true; return true;
}); });
@ -157,7 +157,7 @@ function createTaskRoutes(db) {
if (!shop_id) return res.status(400).json({ error: 'shop_id required' }); if (!shop_id) return res.status(400).json({ error: 'shop_id required' });
const approvedNotes = db.prepare( const approvedNotes = db.prepare(
"SELECT id, shop_id FROM notes WHERE shop_id = ? AND status = 'approved'" "SELECT id, shop_id FROM notes WHERE shop_id = ? AND status = 'approved' AND use_status = 'pending'"
).all(shop_id); ).all(shop_id);
if (approvedNotes.length === 0) { if (approvedNotes.length === 0) {

View File

@ -46,6 +46,7 @@ export const notes = {
batchDelete: (ids) => request("/api/notes/batch-delete", { method: "POST", body: JSON.stringify({ ids }) }), batchDelete: (ids) => request("/api/notes/batch-delete", { method: "POST", body: JSON.stringify({ ids }) }),
batchApprove: (ids) => request("/api/notes/batch/approve", { method: "POST", body: JSON.stringify({ ids }) }), batchApprove: (ids) => request("/api/notes/batch/approve", { method: "POST", body: JSON.stringify({ ids }) }),
batchReject: (ids) => request("/api/notes/batch/reject", { method: "POST", body: JSON.stringify({ ids }) }), batchReject: (ids) => request("/api/notes/batch/reject", { method: "POST", body: JSON.stringify({ ids }) }),
batchResetPublish: (ids) => request("/api/notes/batch/publish-reset", { method: "POST", body: JSON.stringify({ ids }) }),
rewrite: (id) => request(`/api/notes/${id}/rewrite`, { method: "POST" }), rewrite: (id) => request(`/api/notes/${id}/rewrite`, { method: "POST" }),
}; };
@ -64,6 +65,7 @@ export const generate = {
note: (data) => request("/api/generate/note", { method: "POST", body: JSON.stringify(data) }), note: (data) => request("/api/generate/note", { method: "POST", body: JSON.stringify(data) }),
batch: (data) => request("/api/generate/batch", { method: "POST", body: JSON.stringify(data) }), batch: (data) => request("/api/generate/batch", { method: "POST", body: JSON.stringify(data) }),
image: (data) => request("/api/generate/image", { method: "POST", body: JSON.stringify(data) }), image: (data) => request("/api/generate/image", { method: "POST", body: JSON.stringify(data) }),
jobs: (shopId) => request(`/api/generate/jobs${shopId ? "?shop_id=" + shopId : ""}`),
}; };
export const stats = { export const stats = {

View File

@ -19,8 +19,27 @@ const STATUS_LABEL = {
}; };
const APPROVED_STATUSES = ["draft", "approved", "publishing", "published"]; const APPROVED_STATUSES = ["draft", "approved", "publishing", "published"];
const USE_STATUS_MAP = {
pending: "bg-gray-100 text-gray-600",
publishing: "bg-orange-100 text-orange-700",
used: "bg-emerald-100 text-emerald-800",
failed: "bg-red-100 text-red-700",
};
const USE_STATUS_LABEL = {
pending: "待发布",
publishing: "发布中",
used: "已使用",
failed: "使用失败",
};
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
function getUseStatus(note) {
if (note.use_status) return note.use_status;
if (note.status === "published") return "used";
if (note.status === "publishing") return "publishing";
return "pending";
}
export default function NotesApproved() { export default function NotesApproved() {
const [list, setList] = useState([]); const [list, setList] = useState([]);
const [shopList, setShopList] = useState([]); const [shopList, setShopList] = useState([]);
@ -57,7 +76,7 @@ export default function NotesApproved() {
const filteredList = useMemo(() => { const filteredList = useMemo(() => {
let result = list; let result = list;
if (filterStatus) { if (filterStatus) {
result = result.filter((n) => n.status === filterStatus); result = result.filter((n) => getUseStatus(n) === filterStatus);
} }
if (search.trim()) { if (search.trim()) {
const q = search.trim().toLowerCase(); const q = search.trim().toLowerCase();
@ -107,12 +126,24 @@ export default function NotesApproved() {
} catch (e) { toast(e.message, "error"); } } catch (e) { toast(e.message, "error"); }
}; };
const batchResetPublish = async () => {
if (selected.size === 0) { toast("请先选择笔记", "error"); return; }
if (!window.confirm("确定将选中的使用失败笔记重置为待发布吗?")) return;
try {
const result = await notes.batchResetPublish([...selected]);
toast(`已重置 ${result.updated || 0} 条使用失败笔记`, "success");
setSelected(new Set());
load();
} catch (e) { toast(e.message, "error"); }
};
const handleExport = () => { const handleExport = () => {
const data = filteredList.map((n) => ({ const data = filteredList.map((n) => ({
红薯名称: shopList.find((s) => s.id === n.shop_id)?.shop_name || "", 红薯名称: shopList.find((s) => s.id === n.shop_id)?.shop_name || "",
标题: n.title, 标题: n.title,
正文内容: n.content, 正文内容: n.content,
热推话题: Array.isArray(n.topics) ? n.topics.join(", ") : n.topics, 热推话题: Array.isArray(n.topics) ? n.topics.join(", ") : n.topics,
使用状态: USE_STATUS_LABEL[getUseStatus(n)] || getUseStatus(n),
状态: STATUS_LABEL[n.status] || n.status, 状态: STATUS_LABEL[n.status] || n.status,
创建时间: n.created_at, 创建时间: n.created_at,
更新时间: n.updated_at, 更新时间: n.updated_at,
@ -152,11 +183,11 @@ export default function NotesApproved() {
</select> </select>
<input className="border rounded-lg px-3 py-2 text-sm w-48" placeholder="商品标题" value={search} onChange={(e) => setSearch(e.target.value)} /> <input className="border rounded-lg px-3 py-2 text-sm w-48" placeholder="商品标题" value={search} onChange={(e) => setSearch(e.target.value)} />
<select className="border rounded-lg px-3 py-2 text-sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}> <select className="border rounded-lg px-3 py-2 text-sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<option value="">全部状态</option> <option value="">全部使用状态</option>
<option value="generated">待审核</option> <option value="pending">待发布</option>
<option value="approved">已通过</option> <option value="publishing">发布中</option>
<option value="published">已发布</option> <option value="used">已使用</option>
<option value="failed">失败</option> <option value="failed">使用失败</option>
</select> </select>
<select className="border rounded-lg px-3 py-2 text-sm" value={sortMode} onChange={(e) => setSortMode(e.target.value)}> <select className="border rounded-lg px-3 py-2 text-sm" value={sortMode} onChange={(e) => setSortMode(e.target.value)}>
<option value="">默认排序</option> <option value="">默认排序</option>
@ -165,6 +196,7 @@ export default function NotesApproved() {
</div> </div>
<div className="flex items-center gap-2 mt-3"> <div className="flex items-center gap-2 mt-3">
<button onClick={batchDelete} disabled={selected.size === 0} className="px-3 py-1.5 bg-red-500 text-white text-sm rounded-lg hover:bg-red-600 disabled:opacity-40">删除</button> <button onClick={batchDelete} disabled={selected.size === 0} className="px-3 py-1.5 bg-red-500 text-white text-sm rounded-lg hover:bg-red-600 disabled:opacity-40">删除</button>
<button onClick={batchResetPublish} disabled={selected.size === 0} className="px-3 py-1.5 bg-orange-500 text-white text-sm rounded-lg hover:bg-orange-600 disabled:opacity-40">使用失败改待发布</button>
<button onClick={batchRewrite} disabled={selected.size === 0} className="px-3 py-1.5 bg-blue-500 text-white text-sm rounded-lg hover:bg-blue-600 disabled:opacity-40">+ 一键重新生成笔记</button> <button onClick={batchRewrite} disabled={selected.size === 0} className="px-3 py-1.5 bg-blue-500 text-white text-sm rounded-lg hover:bg-blue-600 disabled:opacity-40">+ 一键重新生成笔记</button>
<button onClick={handleExport} className="px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600">导出</button> <button onClick={handleExport} className="px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600">导出</button>
</div> </div>
@ -206,8 +238,16 @@ export default function NotesApproved() {
<button onClick={() => setImagePreview(n)} className="text-blue-600 hover:underline text-xs">查看图片({n.image_paths.length})</button> <button onClick={() => setImagePreview(n)} className="text-blue-600 hover:underline text-xs">查看图片({n.image_paths.length})</button>
) : <span className="text-gray-400 text-xs">暂无图片</span>} ) : <span className="text-gray-400 text-xs">暂无图片</span>}
</td> </td>
<td className="px-3 py-3"><span className="text-xs text-gray-500">未使用</span></td> <td className="px-3 py-3">
<td className="px-3 py-3"><span className="text-xs text-green-600">正常</span></td> <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${USE_STATUS_MAP[getUseStatus(n)] || "bg-gray-100 text-gray-600"}`}>
{USE_STATUS_LABEL[getUseStatus(n)] || getUseStatus(n)}
</span>
</td>
<td className="px-3 py-3">
<span className={`text-xs ${getUseStatus(n) === "failed" ? "text-red-600" : "text-green-600"}`}>
{getUseStatus(n) === "failed" ? "失败" : "正常"}
</span>
</td>
<td className="px-3 py-3 text-gray-400 text-xs whitespace-nowrap">{n.created_at?.slice(0, 19).replace("T", " ")}</td> <td className="px-3 py-3 text-gray-400 text-xs whitespace-nowrap">{n.created_at?.slice(0, 19).replace("T", " ")}</td>
<td className="px-3 py-3 text-gray-400 text-xs whitespace-nowrap">{n.updated_at?.slice(0, 19).replace("T", " ")}</td> <td className="px-3 py-3 text-gray-400 text-xs whitespace-nowrap">{n.updated_at?.slice(0, 19).replace("T", " ")}</td>
<td className="px-3 py-3 space-x-2 whitespace-nowrap"> <td className="px-3 py-3 space-x-2 whitespace-nowrap">