feat: awesome index 全功能实现
- M1-M2: 数据层与核心API(DuckDB, entries/tags/comments/search服务) - M3: SSR页面(EJS模板,接真实数据) - M4: 管理后台(登录/用户/内容源/AI ingest) - M5: 采集层(scheduler+rss/html/eryajf适配器) - M6: MCP只读接口(Streamable HTTP, 4工具) - M7: Docker部署(Dockerfile+docker-compose) - 测试: 10个用例全部通过 - 修复: DuckDB连接参数绑定、状态机迁移、标签树操作
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
<%- include('partials/head') %>
|
||||
<%- include('partials/topbar', { active: 'admin' }) %>
|
||||
|
||||
<div class="admin-shell">
|
||||
<nav class="admin-nav" aria-label="管理分区">
|
||||
<h4>管理台 · <%= user.username %></h4>
|
||||
<button type="button" data-section="sec-content" aria-current="true">内容管理</button>
|
||||
<button type="button" data-section="sec-tags" aria-current="false">标签管理</button>
|
||||
<button type="button" data-section="sec-comments" aria-current="false">评论管理</button>
|
||||
<button type="button" data-section="sec-sources" aria-current="false">内容源</button>
|
||||
<% if (isAdmin) { %>
|
||||
<button type="button" data-section="sec-accounts" aria-current="false">账号管理</button>
|
||||
<% } %>
|
||||
</nav>
|
||||
|
||||
<main class="admin-main">
|
||||
<!-- 内容管理 -->
|
||||
<section id="sec-content" class="active">
|
||||
<p class="anno">// table: entries · 状态 active | greyed | pending</p>
|
||||
<h1>内容管理</h1>
|
||||
<p class="admin-desc">全部收录条目。灰掉 = 暂不可见但保留数据;删除为物理删除,需确认。</p>
|
||||
<table class="data-table" style="margin-top:22px">
|
||||
<thead><tr><th>标题</th><th>类型</th><th>状态</th><th>更新时间</th><th style="text-align:right">操作</th></tr></thead>
|
||||
<tbody>
|
||||
<% for (const e of entries) { %>
|
||||
<tr>
|
||||
<td>
|
||||
<b><%= e.title %></b><br />
|
||||
<span class="mono" style="font-size:11px;color:var(--ink-dim)"><%= (e.tags || []).join(' · ') || '—' %></span>
|
||||
</td>
|
||||
<td><%= e.type %></td>
|
||||
<td>
|
||||
<% if (e.status === 'active') { %><span class="status-pill status-ok">active</span>
|
||||
<% } else if (e.status === 'pending') { %><span class="status-pill status-warn">pending</span>
|
||||
<% } else { %><span class="status-pill status-dim"><%= e.status %></span><% } %>
|
||||
</td>
|
||||
<td class="mono"><%= fmtDate(e.updated_at) %></td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<% if (e.status !== 'active') { %>
|
||||
<button class="btn btn-sm" type="button" data-action="entry-status" data-id="<%= e.id %>" data-status="active">上架</button>
|
||||
<% } else { %>
|
||||
<button class="btn btn-sm" type="button" data-action="entry-status" data-id="<%= e.id %>" data-status="greyed">灰掉</button>
|
||||
<% } %>
|
||||
<% if (isAdmin) { %>
|
||||
<button class="btn btn-sm btn-danger" type="button" data-action="entry-delete" data-id="<%= e.id %>">删除</button>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 标签管理 -->
|
||||
<section id="sec-tags">
|
||||
<p class="anno">// tree: tags · 删除父级时子级自动上移;移动到自己的子级会被拒绝</p>
|
||||
<h1>标签管理</h1>
|
||||
<div style="display:flex;gap:10px;margin-top:18px">
|
||||
<button class="btn btn-primary btn-sm" type="button" data-action="tag-add">新增顶层标签组</button>
|
||||
<a class="btn btn-sm" href="/entries">前台预览</a>
|
||||
</div>
|
||||
<div class="tagmgr">
|
||||
<div class="panel-box">
|
||||
<h3>标签树 <span class="mono" style="font-weight:400;font-size:12px;color:var(--ink-dim)">(#ID 用于「移动」时填写目标)</span></h3>
|
||||
<ul class="tag-tree">
|
||||
<% for (const root of tagTreeAdmin) { %><%- include('partials/tag-node', { node: root }) %><% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="panel-box">
|
||||
<h3>规则说明</h3>
|
||||
<p style="font-size:13.5px;color:var(--ink-dim)">
|
||||
标签可无限嵌套,列表页按组过滤时会自动包含所有后代标签的条目。
|
||||
「移动」填写目标标签 ID;移动到自身子级会被拒绝以防止成环。
|
||||
删除父标签时其子标签自动上移一级。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 评论管理 -->
|
||||
<section id="sec-comments">
|
||||
<p class="anno">// table: comments</p>
|
||||
<h1>评论管理</h1>
|
||||
<table class="data-table" style="margin-top:22px">
|
||||
<thead><tr><th>评论者</th><th>内容</th><th>所属条目</th><th>时间</th><th style="text-align:right">操作</th></tr></thead>
|
||||
<tbody>
|
||||
<% for (const c of comments) { %>
|
||||
<tr>
|
||||
<td><b><%= c.author_name %></b><% if (c.username) { %> <span class="mono" style="font-size:11px;color:var(--ink-dim)">@<%= c.username %></span><% } %></td>
|
||||
<td style="max-width:280px"><%= c.body.slice(0, 80) %><%= c.body.length > 80 ? '…' : '' %></td>
|
||||
<td><a href="/entries/<%= c.entry_slug %>" target="_blank"><%= c.entry_title %></a></td>
|
||||
<td class="mono"><%= fmtDate(c.created_at) %></td>
|
||||
<td><div class="row-actions">
|
||||
<% if (isAdmin) { %><button class="btn btn-sm btn-danger" type="button" data-action="comment-delete" data-id="<%= c.id %>">删除</button><% } %>
|
||||
</div></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- 内容源 -->
|
||||
<section id="sec-sources">
|
||||
<p class="anno">// sources: human + ai + feed · 自动拉取经 ingest/ 采集层进入待核验队列</p>
|
||||
<h1>内容源管理</h1>
|
||||
|
||||
<% for (const s of sources) { %>
|
||||
<div class="panel-box" style="margin-top:20px">
|
||||
<h3 style="display:flex;align-items:center;gap:10px">
|
||||
<span class="dot <%= s.enabled ? '' : 'dot-off' %>"></span> <%= s.name %>
|
||||
<span class="mono" style="font-weight:400;font-size:12px;color:var(--ink-dim)">kind: <%= s.kind %> · 已收录 <%= Number(s.entry_count) %> 条</span>
|
||||
</h3>
|
||||
|
||||
<% if (s.kind === 'human') { %>
|
||||
<p style="font-size:13.5px;color:var(--ink-dim)">管理员与编辑在条目接口直接写入,始终可用,改动写入审计日志。</p>
|
||||
<% } else if (s.kind === 'ai') { %>
|
||||
<div class="switchline">
|
||||
AI 录入接口启用
|
||||
<span class="mono"><%= s.enabled ? 'ON' : 'OFF' %></span>
|
||||
</div>
|
||||
<form id="ai-key-form">
|
||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">设置新的 X-Ingest-Key(留空 = 关闭通道)</p>
|
||||
<div style="display:flex;gap:10px;max-width:420px">
|
||||
<input class="input" name="key" type="text" placeholder="sk-…" />
|
||||
<button class="btn btn-sm" type="submit">保存密钥</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="mono" style="margin-top:12px;font-size:12px;color:var(--ink-dim)">
|
||||
调用方式:POST /api/ingest/entry · header X-Ingest-Key · 新条目默认 pending
|
||||
</p>
|
||||
<% } else { %>
|
||||
<form class="source-config" data-id="<%= s.id %>">
|
||||
<div class="switchline">
|
||||
启用定时拉取
|
||||
<input type="checkbox" name="enabled" <%= s.enabled ? 'checked' : '' %> style="accent-color:var(--riso-blue)" />
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px">
|
||||
<div>
|
||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">名称</p>
|
||||
<input class="input" name="name" value="<%= s.name %>" />
|
||||
</div>
|
||||
<div>
|
||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">拉取地址 / 计划(cron)</p>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input class="input" name="url" value="<%= s.url %>" />
|
||||
<input class="input mono" name="cron_expr" value="<%= s.cron_expr %>" style="max-width:130px" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">适配器</p>
|
||||
<select class="select" name="adapter">
|
||||
<% for (const a of ['rss', 'html', 'eryajf-weekly']) { %>
|
||||
<option value="<%= a %>" <%= s.adapter === a ? 'selected' : '' %>><%= a %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;align-items:flex-end;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" type="submit">保存配置</button>
|
||||
<button class="btn btn-sm" type="button" data-source-run="<%= s.id %>">立即拉取</button>
|
||||
<button class="btn btn-sm" type="button" data-source-runs="<%= s.id %>">运行记录</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<pre class="code-block mono" id="runs-<%= s.id %>" hidden style="margin-top:12px"></pre>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="panel-box" style="margin-top:20px">
|
||||
<h3>新增拉取源</h3>
|
||||
<form id="source-add-form" style="display:grid;grid-template-columns:1fr 1fr auto auto;gap:10px;align-items:end">
|
||||
<div><input class="input" name="name" placeholder="源名称,如:阮一峰周刊" required /></div>
|
||||
<div><input class="input" name="url" placeholder="https://…" required /></div>
|
||||
<select class="select" name="adapter">
|
||||
<option value="rss">rss</option>
|
||||
<option value="html">html</option>
|
||||
<option value="eryajf-weekly">eryajf-weekly</option>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" type="submit">创建</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 账号管理 -->
|
||||
<% if (isAdmin) { %>
|
||||
<section id="sec-accounts">
|
||||
<p class="anno">// table: accounts · role: admin | editor</p>
|
||||
<h1>账号管理</h1>
|
||||
<table class="data-table" style="margin-top:22px">
|
||||
<thead><tr><th>ID</th><th>用户名</th><th>角色</th><th>状态</th><th>最后登录</th><th style="text-align:right">操作</th></tr></thead>
|
||||
<tbody>
|
||||
<% for (const u of users) { %>
|
||||
<tr>
|
||||
<td class="mono">#<%= u.id %></td>
|
||||
<td><b><%= u.username %></b></td>
|
||||
<td><%= u.role %></td>
|
||||
<td><span class="status-pill <%= u.status === 'active' ? 'status-ok' : 'status-warn' %>"><%= u.status %></span></td>
|
||||
<td class="mono"><%= u.last_login_at ? fmtDate(u.last_login_at) : '从未登录' %></td>
|
||||
<td><div class="row-actions">
|
||||
<% if (u.id !== user.id) { %>
|
||||
<button class="btn btn-sm" type="button" data-action="user-disable" data-id="<%= u.id %>" data-status="<%= u.status %>">
|
||||
<%= u.status === 'active' ? '停用' : '启用' %>
|
||||
</button>
|
||||
<% } %>
|
||||
</div></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
<form id="user-add-form" style="display:flex;gap:10px;align-items:end;margin-top:18px;flex-wrap:wrap">
|
||||
<div><p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">用户名</p><input class="input" name="username" required /></div>
|
||||
<div><p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">初始密码</p><input class="input" name="password" type="password" required minlength="6" /></div>
|
||||
<div>
|
||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">角色</p>
|
||||
<select class="select" name="role"><option value="editor">editor</option><option value="admin">admin</option></select>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" type="submit">新增账号</button>
|
||||
</form>
|
||||
</section>
|
||||
<% } %>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>window.__ADMIN_TAG_TREE__ = <%- JSON.stringify(tagTreeAdmin) %>;</script>
|
||||
<%- include('partials/footer-bar') %>
|
||||
Reference in New Issue
Block a user