clearlove2.1
1{{/* ============ 后台登录 ============ */}}
2{{define "pg_admin_login"}}
3<div class="admin-login-wrap">
4 <form class="auth card" method="post" action="/admin/login">
5 <h2>管理员登录</h2>
6 <input type="hidden" name="_csrf" value="{{.csrf}}">
7 {{with .err}}<p class="form-err">{{.}}</p>{{end}}
8 <label>用户名<input type="text" name="username" required maxlength="20"></label>
9 <label>密码<input type="password" name="password" required minlength="6"></label>
10 <button class="btn btn-primary btn-block" type="submit">登录后台</button>
11 <p class="auth-alt"><a href="/">返回前台</a></p>
12 </form>
13</div>
14{{end}}
15
16{{/* ============ 仪表盘 ============ */}}
17{{define "pg_dashboard"}}
18<h1 class="page-title">仪表盘</h1>
19<div class="stat-grid">
20 <div class="stat card"><span class="stat-num">{{.users}}</span><span class="stat-label">注册用户</span></div>
21 <div class="stat card"><span class="stat-num">{{.posts}}</span><span class="stat-label">帖子总数</span></div>
22 <div class="stat card"><span class="stat-num">{{.today}}</span><span class="stat-label">今日新增帖子</span></div>
23 <div class="stat card"><span class="stat-num">{{.active7}}</span><span class="stat-label">近7天活跃用户</span></div>
24 <div class="stat card"><span class="stat-num">{{.comments}}</span><span class="stat-label">评论总数</span></div>
25 <div class="stat card"><span class="stat-num {{if gt .reports 0}}warn{{end}}">{{.reports}}</span><span class="stat-label">待处理举报</span></div>
26</div>
27<div class="card">
28 <h3>快捷操作</h3>
29 <div class="btn-row">
30 <a class="btn btn-ghost" href="/admin/posts">帖子管理</a>
31 <a class="btn btn-ghost" href="/admin/reports">处理举报</a>
32 <a class="btn btn-ghost" href="/admin/settings">网站设置</a>
33 <a class="btn btn-ghost" href="/admin/settings#ai">AI 接入</a>
34 </div>
35</div>
36{{.admin_dashboard_after}}
37{{end}}
38
39{{/* ============ 帖子管理 ============ */}}
40{{define "pg_admin_posts"}}
41<h1 class="page-title">帖子管理 <small>共 {{.total}} 条</small></h1>
42<div class="card table-card">
43 <div class="table-scroll">
44 <table class="table">
45 <thead><tr><th>ID</th><th>作者</th><th>内容</th><th>话题</th><th>IP</th><th>指纹</th><th>状态</th><th>互动</th><th>时间</th><th>操作</th></tr></thead>
46 <tbody>
47 {{range .posts}}
48 <tr>
49 <td>{{.ID}}</td>
50 <td>{{.Nickname}}</td>
51 <td class="cell-content"><a href="/post/{{.ID}}" target="_blank">{{snippet .Content 60}}</a></td>
52 <td>{{if .Topic}}# {{.Topic}}{{else}}-{{end}}</td>
53 <td class="mono">{{.IP}}</td>
54 <td class="mono">{{snippet .Fingerprint 10}}</td>
55 <td>{{if eq .Status 1}}<span class="tag ok">可见</span>{{else}}<span class="tag warn">隐藏</span>{{end}}</td>
56 <td>{{.LikeCount}} / {{.CommentCount}}</td>
57 <td class="mono">{{datefmt .CreatedAt}}</td>
58 <td class="ops">
59 <a class="mini-btn" href="/post/{{.ID}}/edit" target="_blank">编辑</a>
60 <form method="post" action="/admin/posts/delete" class="inline-form" data-confirm="确定删除该帖子吗?">
61 <input type="hidden" name="_csrf" value="{{$.csrf}}">
62 <input type="hidden" name="id" value="{{.ID}}">
63 <button class="mini-btn danger" type="submit">删除</button>
64 </form>
65 <form method="post" action="/admin/posts/ban" class="inline-form" data-confirm="确定封禁该用户吗?">
66 <input type="hidden" name="_csrf" value="{{$.csrf}}">
67 <input type="hidden" name="id" value="{{.ID}}">
68 <select name="btype" class="mini-select">
69 <option value="ip">封禁IP</option>
70 <option value="fingerprint">封禁指纹</option>
71 <option value="both">两者都封</option>
72 </select>
73 <button class="mini-btn danger" type="submit">封禁</button>
74 </form>
75 </td>
76 </tr>
77 {{else}}<tr><td colspan="10" class="empty">暂无帖子</td></tr>{{end}}
78 </tbody>
79 </table>
80 </div>
81 <div class="pager">
82 {{if gt .page 1}}<a class="btn btn-ghost" href="/admin/posts?page={{.page | minus1}}">上一页</a>{{end}}
83 <span>第 {{.page}} / {{.pages}} 页</span>
84 {{if lt .page .pages}}<a class="btn btn-ghost" href="/admin/posts?page={{.page | plus1}}">下一页</a>{{end}}
85 </div>
86</div>
87{{end}}
88
89{{/* ============ 网站设置 ============ */}}
90{{define "pg_admin_settings"}}
91<h1 class="page-title">网站设置</h1>
92<form method="post" action="/admin/settings" class="settings-form">
93 <input type="hidden" name="_csrf" value="{{.csrf}}">
94
95 <div class="card">
96 <h3>基本信息</h3>
97 <label>网站名称<input type="text" name="site_name" maxlength="30" value="{{.s.site_name}}"></label>
98 </div>
99
100 <div class="card">
101 <h3>主题管理</h3>
102 <div class="grid2">
103 <label>当前主题
104 <select name="theme">
105 {{$cur := .s.theme}}
106 {{range .themes}}<option value="{{.name}}" {{if eq .name $cur}}selected{{end}}>{{.label}}</option>{{end}}
107 </select>
108 </label>
109 <label>自定义背景图地址(留空使用主题默认)
110 <input type="text" name="theme_bg" value="{{.s.theme_bg}}" placeholder="/uploads/xxx.webp 或完整 URL">
111 </label>
112 </div>
113 <p class="muted">主题开发请参考项目 docs/THEME.md,支持自定义 CSS 变量、背景图与完整样式覆盖。</p>
114 </div>
115
116 <div class="card">
117 <h3>登录与发帖开关</h3>
118 <label class="check-row"><input type="checkbox" name="allow_register" value="1" {{if eq .s.allow_register "1"}}checked{{end}}> 允许新用户注册</label>
119 <label class="check-row"><input type="checkbox" name="require_login_post" value="1" {{if eq .s.require_login_post "1"}}checked{{end}}> 发帖必须登录(已登录用户自动匿名发帖)</label>
120 </div>
121
122 <div class="card">
123 <h3>SMTP 邮箱配置 <small>用于发送注册验证码</small></h3>
124 <div class="grid2">
125 <label>SMTP 主机<input type="text" name="smtp_host" value="{{.s.smtp_host}}" placeholder="smtp.qq.com"></label>
126 <label>端口<input type="text" name="smtp_port" value="{{.s.smtp_port}}" placeholder="465 或 587"></label>
127 </div>
128 <div class="grid2">
129 <label>账号<input type="text" name="smtp_user" value="{{.s.smtp_user}}"></label>
130 <label>密码/授权码<input type="password" name="smtp_pass" value="{{.s.smtp_pass}}"></label>
131 </div>
132 <label>发件人地址<input type="text" name="smtp_from" value="{{.s.smtp_from}}" placeholder="与你邮箱一致"></label>
133 <p class="muted">未配置 SMTP 时注册不发送验证码;允许注册即可直接注册。</p>
134 </div>
135
136 <div class="card" id="ai">
137 <h3>AI 模型接入 <small>OpenAI 格式接口,开启后启用全站 AI 功能</small></h3>
138 <label class="check-row"><input type="checkbox" name="ai_enabled" value="1" {{if eq .s.ai_enabled "1"}}checked{{end}}> 启用 AI 功能</label>
139 <div class="grid2">
140 <label>接口地址<input type="text" name="ai_base" value="{{.s.ai_base}}" placeholder="https://api.deepseek.com/v1"></label>
141 <label>模型名<input type="text" name="ai_model" value="{{.s.ai_model}}" placeholder="deepseek-chat"></label>
142 </div>
143 <label>API Key<input type="password" name="ai_key" value="{{.s.ai_key}}"></label>
144 <label class="check-row"><input type="checkbox" name="ai_autopatrol" value="1" {{if eq .s.ai_autopatrol "1"}}checked{{end}}> 开启 AI 智能体自动巡查(每10分钟一次,对未审过的帖子和评论执行保留/隐藏/删除并留痕)</label>
145 <label class="check-row"><input type="checkbox" name="ai_precheck" value="1" {{if eq .s.ai_precheck "1"}}checked{{end}}> 发帖与评论自动审核(违规阻止发布并提示用户修改)</label>
146 <label class="check-row"><input type="checkbox" name="ai_vision" value="1" {{if eq .s.ai_vision "1"}}checked{{end}}> 启用图片审核(需多模态模型,如 gpt-4o / qwen-vl;发帖与巡查时自动识别图片违规内容)</label>
147 <label class="check-row"><input type="checkbox" name="ai_auto_delete" value="1" {{if eq .s.ai_auto_delete "1"}}checked{{end}}> 允许 AI 直接删除严重违规内容(默认关闭:AI 判定删除时降级为隐藏,由人工确认删除,防误判丢数据)</label>
148 <label class="check-row"><input type="checkbox" name="ai_auto_ban" value="1" {{if eq .s.ai_auto_ban "1"}}checked{{end}}> AI 删除时自动封禁发布者(IP 与浏览器指纹,仅配合上一项生效)</label>
149 <label class="check-row"><input type="checkbox" name="ai_precheck_fail" value="closed" {{if eq .s.ai_precheck_fail "closed"}}checked{{end}}> 审核服务异常时阻止发布(默认关闭:异常时放行内容,仅记录日志;开启则提示用户稍后重试)</label>
150 </div>
151
152 <div class="card">
153 <h3>其他</h3>
154 <label>检查更新接口地址<input type="text" name="update_url" value="{{.s.update_url}}"></label>
155 <label>插件商店云端地址<input type="text" name="cloud_api" value="{{.s.cloud_api}}" placeholder="https://clearlove.kazx.top"></label>
156 <p class="muted">插件商店从该地址拉取插件市场数据,默认使用官方云端(https://clearlove.kazx.top)。</p>
157 <label>管理员专享昵称(多个用逗号或换行分隔)
158 <textarea name="admin_nicknames" rows="3" placeholder="例如:官方, 管理员, ClearLove">{{.s.admin_nicknames}}</textarea>
159 </label>
160 <p class="muted">使用这些昵称发帖时必须验证后台管理员账号与密码,通过后帖子会显示「管理员」标识;这些昵称同时禁止被注册。</p>
161 <label>捐赠图片地址<input type="text" name="donate_img" value="{{.s.donate_img}}"></label>
162 </div>
163
164 <button class="btn btn-primary" type="submit">保存设置</button>
165</form>
166
167<div class="card">
168 <h3>主题安装</h3>
169 <form method="post" action="/admin/theme/upload" enctype="multipart/form-data" class="inline-form">
170 <input type="hidden" name="_csrf" value="{{.csrf}}">
171 <input type="file" name="file" accept=".zip" required>
172 <button class="btn btn-ghost" type="submit">上传安装主题</button>
173 </form>
174 <form method="post" action="/admin/theme/delete" class="inline-form" data-confirm="确定删除该主题吗?">
175 <input type="hidden" name="_csrf" value="{{.csrf}}">
176 <input type="text" name="name" placeholder="要删除的主题目录名">
177 <button class="btn btn-ghost danger" type="submit">删除主题</button>
178 </form>
179</div>
180{{.admin_settings_after}}
181{{end}}
182
183{{/* ============ 举报管理 ============ */}}
184{{define "pg_admin_reports"}}
185<h1 class="page-title">举报管理</h1>
186<div class="card">
187 <h3>AI 巡查状态</h3>
188 <p class="muted">
189 AI 功能:{{if .aiOn}}<span class="tag ok">已接入</span>{{else}}<span class="tag warn">未接入</span>{{end}}
190 自动巡查:{{if .patrol}}<span class="tag ok">已开启(每10分钟)</span>{{else}}<span class="tag warn">未开启</span>{{end}}
191 发帖预审:{{if .precheck}}<span class="tag ok">已开启</span>{{else}}<span class="tag warn">未开启</span>{{end}}
192 </p>
193 <div class="btn-row">
194 {{/* 采用 POST 表单:刷新页面不会重复触发巡查(JS 可用时拦截提交并播放动画) */}}
195 <form method="post" action="/admin/reports/patrol" id="patrol-form" class="inline-form">
196 <input type="hidden" name="_csrf" value="{{.csrf}}">
197 <button class="btn btn-primary" type="submit" id="patrol-btn" {{if not .aiReady}}disabled{{end}}>立即执行一轮 AI 巡查</button>
198 </form>
199 <a class="btn btn-ghost" href="/admin/settings#ai">前往 AI 设置</a>
200 </div>
201 {{with .errMsg}}<p class="form-err">{{.}}</p>{{end}}
202 {{if .progress.Running}}<p class="muted">AI 巡查正在后台进行中,完成后刷新本页即可查看结果。</p>{{end}}
203 {{if not .aiReady}}<p class="muted">尚未接入 AI 模型:请先在「网站设置 → AI 模型接入」填写接口地址、模型名与 API Key,巡查与发帖预审方可生效。</p>{{end}}
204</div>
205
206<div class="card table-card">
207 <h3>举报列表</h3>
208 <div class="table-scroll">
209 <table class="table">
210 <thead><tr><th>ID</th><th>帖子</th><th>内容</th><th>举报理由</th><th>状态</th><th>AI/处理结果</th><th>时间</th><th>操作</th></tr></thead>
211 <tbody>
212 {{range .reports}}
213 <tr>
214 <td>{{.ID}}</td>
215 <td>{{if .PostExists}}<a href="/post/{{.PostID}}" target="_blank">#{{.PostID}}</a>{{else}}<span class="muted">已删除</span>{{end}}</td>
216 <td class="cell-content">{{snippet .Content 50}}</td>
217 <td>{{.Reason}}</td>
218 <td>{{if eq .Status 0}}<span class="tag warn">待处理</span>{{else}}<span class="tag ok">已处理</span>{{end}}</td>
219 <td class="cell-content">{{.AIResult}}</td>
220 <td class="mono">{{datefmt .CreatedAt}}</td>
221 <td class="ops">
222 {{if .PostExists}}
223 <form method="post" action="/admin/reports/handle" class="inline-form">
224 <input type="hidden" name="_csrf" value="{{$.csrf}}">
225 <input type="hidden" name="id" value="{{.ID}}">
226 <button class="mini-btn" name="action" value="keep" type="submit">保留</button>
227 <button class="mini-btn" name="action" value="hide" type="submit">隐藏</button>
228 <button class="mini-btn danger" name="action" value="delete" type="submit">删除</button>
229 </form>
230 {{else}}-{{end}}
231 </td>
232 </tr>
233 {{else}}<tr><td colspan="8" class="empty">暂无举报</td></tr>{{end}}
234 </tbody>
235 </table>
236 </div>
237</div>
238
239<div class="card table-card">
240 <h3>AI 动作留痕(最近100条)</h3>
241 <div class="table-scroll">
242 <table class="table">
243 <thead><tr><th>ID</th><th>目标</th><th>动作</th><th>原因</th><th>时间</th></tr></thead>
244 <tbody>
245 {{range .logs}}
246 <tr>
247 <td>{{.ID}}</td>
248 <td>{{if eq .Target "comment"}}<span class="tag">评论 #{{.PostID}}</span>{{else}}<span class="tag">帖子 #{{.PostID}}</span>{{end}}</td>
249 <td>{{if eq .Action "keep"}}<span class="tag ok">保留</span>{{else if eq .Action "hide"}}<span class="tag warn">隐藏</span>{{else if eq .Action "delete"}}<span class="tag danger">删除</span>{{else}}<code>{{.Action}}</code>{{end}}</td>
250 <td class="cell-content">{{.Reason}}</td>
251 <td class="mono">{{datefmt .CreatedAt}}</td>
252 </tr>
253 {{else}}<tr><td colspan="5" class="empty">暂无记录</td></tr>{{end}}
254 </tbody>
255 </table>
256 </div>
257</div>
258
259{{/* AI 巡查进度动画(前端轮询刷新,非阻塞执行) */}}
260<div class="scan-overlay" id="patrol-overlay" hidden data-running="{{if .progress.Running}}1{{else}}0{{end}}">
261 <div class="scan-mask"></div>
262 <div class="scan-card" id="patrol-card">
263 <div class="scan-rings">
264 <svg class="ring" viewBox="0 0 120 120" width="120" height="120" aria-hidden="true">
265 <circle class="ring-bg" cx="60" cy="60" r="52"></circle>
266 <circle class="ring-fg" cx="60" cy="60" r="52" id="patrol-ring"></circle>
267 </svg>
268 <div class="scan-sweep"></div>
269 <div class="ring-center"><span id="patrol-pct">0</span><small>%</small></div>
270 </div>
271 <h3 class="scan-title" id="patrol-title">AI 智能巡查中</h3>
272 <p class="scan-sub" id="patrol-sub">正在读取待检查内容<span class="dots"><i></i><i></i><i></i></span></p>
273 <div class="scan-bar"><span id="patrol-bar"></span></div>
274 <div class="scan-stats">
275 <div class="scan-stat"><b id="patrol-total">0</b><small>待检查</small></div>
276 <div class="scan-stat"><b id="patrol-done">0</b><small>已检查</small></div>
277 <div class="scan-stat ok"><b id="patrol-keep">0</b><small>保留</small></div>
278 <div class="scan-stat warn"><b id="patrol-hide">0</b><small>隐藏</small></div>
279 <div class="scan-stat danger"><b id="patrol-del">0</b><small>删除</small></div>
280 </div>
281 <div class="btn-row center">
282 <button class="btn btn-primary" type="button" id="patrol-finish" hidden>查看巡查结果</button>
283 <button class="btn btn-ghost" type="button" id="patrol-hide-btn">转入后台运行</button>
284 </div>
285 </div>
286</div>
287{{end}}
288
289{{/* ============ 用户管理 ============ */}}
290{{define "pg_admin_users"}}
291<h1 class="page-title">用户管理</h1>
292<div class="card table-card">
293 <div class="table-scroll">
294 <table class="table">
295 <thead><tr><th>ID</th><th>用户名</th><th>邮箱</th><th>帖子数</th><th>状态</th><th>注册时间</th><th>操作</th></tr></thead>
296 <tbody>
297 {{range .users}}
298 <tr>
299 <td>{{.ID}}</td>
300 <td><a href="/u/{{.ID}}" target="_blank">{{.Username}}</a></td>
301 <td>{{.Email}}</td>
302 <td>{{.Posts}}</td>
303 <td>{{if eq .Status 1}}<span class="tag ok">正常</span>{{else}}<span class="tag warn">已禁用</span>{{end}}</td>
304 <td class="mono">{{datefmt .CreatedAt}}</td>
305 <td class="ops">
306 <form method="post" action="/admin/users/status" class="inline-form">
307 <input type="hidden" name="_csrf" value="{{$.csrf}}">
308 <input type="hidden" name="id" value="{{.ID}}">
309 {{if eq .Status 1}}
310 <input type="hidden" name="status" value="0">
311 <button class="mini-btn danger" type="submit">禁用</button>
312 {{else}}
313 <input type="hidden" name="status" value="1">
314 <button class="mini-btn" type="submit">启用</button>
315 {{end}}
316 </form>
317 <form method="post" action="/admin/users/delete" class="inline-form" data-confirm="确定删除该用户及其全部帖子吗?">
318 <input type="hidden" name="_csrf" value="{{$.csrf}}">
319 <input type="hidden" name="id" value="{{.ID}}">
320 <button class="mini-btn danger" type="submit">删除</button>
321 </form>
322 </td>
323 </tr>
324 {{else}}<tr><td colspan="7" class="empty">暂无用户</td></tr>{{end}}
325 </tbody>
326 </table>
327 </div>
328</div>
329{{end}}
330
331{{/* ============ 管理员管理 ============ */}}
332{{define "pg_admin_admins"}}
333<h1 class="page-title">管理员管理</h1>
334<div class="card table-card">
335 <h3>管理员列表</h3>
336 <div class="table-scroll">
337 <table class="table">
338 <thead><tr><th>ID</th><th>用户名</th><th>角色</th><th>权限组</th><th>创建时间</th><th>操作</th></tr></thead>
339 <tbody>
340 {{range .admins}}
341 <tr>
342 <td>{{.ID}}</td>
343 <td>{{.Username}}</td>
344 <td>{{if eq .Role "super"}}<span class="tag ok">超级管理员</span>{{else}}<span class="tag">自定义</span>{{end}}</td>
345 <td class="mono">{{snippet .Perms 80}}</td>
346 <td class="mono">{{datefmt .CreatedAt}}</td>
347 <td class="ops">
348 {{if ne .ID $.me}}
349 <form method="post" action="/admin/admins/delete" class="inline-form" data-confirm="确定删除该管理员吗?">
350 <input type="hidden" name="_csrf" value="{{$.csrf}}">
351 <input type="hidden" name="id" value="{{.ID}}">
352 <button class="mini-btn danger" type="submit">删除</button>
353 </form>
354 {{else}}<span class="muted">当前账号</span>{{end}}
355 </td>
356 </tr>
357 {{end}}
358 </tbody>
359 </table>
360 </div>
361</div>
362
363<div class="card">
364 <h3>添加管理员</h3>
365 <form method="post" action="/admin/admins/create">
366 <input type="hidden" name="_csrf" value="{{.csrf}}">
367 <div class="grid2">
368 <label>用户名<input type="text" name="username" required minlength="3" maxlength="20"></label>
369 <label>密码<input type="password" name="password" required minlength="6"></label>
370 </div>
371 <label>角色
372 <select name="role">
373 <option value="custom">自定义权限</option>
374 <option value="super">超级管理员(全部权限)</option>
375 </select>
376 </label>
377 <fieldset class="perm-fieldset">
378 <legend>权限组(自定义角色勾选生效)</legend>
379 <label class="check-row"><input type="checkbox" name="perm_posts" value="1"> 帖子管理</label>
380 <label class="check-row"><input type="checkbox" name="perm_reports" value="1"> 举报管理</label>
381 <label class="check-row"><input type="checkbox" name="perm_users" value="1"> 用户管理</label>
382 <label class="check-row"><input type="checkbox" name="perm_settings" value="1"> 网站设置</label>
383 <label class="check-row"><input type="checkbox" name="perm_notices" value="1"> 公告与话题</label>
384 <label class="check-row"><input type="checkbox" name="perm_plugins" value="1"> 插件管理</label>
385 <label class="check-row"><input type="checkbox" name="perm_api" value="1"> API 接口</label>
386 <label class="check-row"><input type="checkbox" name="perm_admins" value="1"> 管理员管理</label>
387 </fieldset>
388 <button class="btn btn-primary" type="submit">添加管理员</button>
389 </form>
390</div>
391{{end}}
392
393{{/* ============ 公告与话题 ============ */}}
394{{define "pg_admin_notices"}}
395<h1 class="page-title">公告与话题</h1>
396<div class="grid2-cards">
397 <div class="card">
398 <h3>发布公告</h3>
399 <form method="post" action="/admin/notices">
400 <input type="hidden" name="_csrf" value="{{.csrf}}">
401 <input type="hidden" name="op" value="notice_create">
402 <label>标题<input type="text" name="title" maxlength="50" required></label>
403 <label>内容<textarea name="content" maxlength="1000" rows="4" required></textarea></label>
404 <button class="btn btn-primary" type="submit">发布公告</button>
405 </form>
406 <p class="muted">公告发布后将在前台展示,同一访客每天展示一次。</p>
407 </div>
408 <div class="card">
409 <h3>公告列表</h3>
410 <ul class="simple-list">
411 {{range .notices}}
412 <li>
413 <div><b>{{.title}}</b><small>{{snippet .content 40}}</small></div>
414 <form method="post" action="/admin/notices" class="inline-form" data-confirm="确定删除该公告吗?">
415 <input type="hidden" name="_csrf" value="{{$.csrf}}">
416 <input type="hidden" name="op" value="notice_delete">
417 <input type="hidden" name="id" value="{{.id}}">
418 <button class="mini-btn danger" type="submit">删除</button>
419 </form>
420 </li>
421 {{else}}<li class="empty">暂无公告</li>{{end}}
422 </ul>
423 </div>
424</div>
425
426<div class="grid2-cards">
427 <div class="card">
428 <h3>话题管理</h3>
429 <div class="topic-chips topic-manage">
430 {{range .topics}}
431 <span class="chip"># {{.name}}
432 <form method="post" action="/admin/notices" class="inline-form" data-confirm="确定删除该话题吗?">
433 <input type="hidden" name="_csrf" value="{{$.csrf}}">
434 <input type="hidden" name="op" value="topic_delete">
435 <input type="hidden" name="id" value="{{.id}}">
436 <button class="chip-x" type="submit" aria-label="删除"><svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" stroke-width="3" fill="none"><path d="M5 5l14 14M19 5L5 19"/></svg></button>
437 </form>
438 </span>
439 {{else}}<span class="empty">暂无话题</span>{{end}}
440 </div>
441 <form method="post" action="/admin/notices" class="inline-form">
442 <input type="hidden" name="_csrf" value="{{.csrf}}">
443 <input type="hidden" name="op" value="topic_create">
444 <input type="text" name="name" maxlength="30" placeholder="新话题名称" required>
445 <button class="btn btn-ghost" type="submit">新建话题</button>
446 </form>
447 </div>
448 <div class="card">
449 <h3>社区守则</h3>
450 <form method="post" action="/admin/notices">
451 <input type="hidden" name="_csrf" value="{{.csrf}}">
452 <input type="hidden" name="op" value="rules_save">
453 <textarea name="rules" rows="8" maxlength="3000">{{.rules}}</textarea>
454 <button class="btn btn-primary" type="submit">保存守则</button>
455 </form>
456 <p class="muted">守则会展示在帖子详情页,同时作为 AI 审核与巡查的判定依据。</p>
457 </div>
458</div>
459{{end}}
460
461{{/* ============ 插件管理 ============ */}}
462{{define "pg_admin_plugins"}}
463<h1 class="page-title">插件管理</h1>
464<div class="card">
465 <h3>安装插件</h3>
466 <form method="post" action="/admin/plugins/upload" enctype="multipart/form-data" class="inline-form">
467 <input type="hidden" name="_csrf" value="{{.csrf}}">
468 <input type="file" name="file" accept=".zip" required>
469 <button class="btn btn-primary" type="submit">上传并安装</button>
470 </form>
471 <p class="muted">插件为 zip 包,需包含 plugin.json。两种形态:声明式(html 注入 / filter 内容过滤 / http 事件 Webhook / guard 发帖守卫 / badge 帖子标识)与应用型(kind=app,脚本注册路由 / 数据表 / 后台菜单 / 定时任务)。开发文档见项目 docs/PLUGIN.md。</p>
472</div>
473
474{{if .adminTop}}
475<div class="card">{{.adminTop}}</div>
476{{end}}
477
478{{range .plugins}}
479{{if .Config}}
480<details class="card plugin-config{{if not .Enabled}} plugin-config-disabled{{end}}">
481 <summary class="plugin-config-head">
482 <span class="plugin-config-arrow" aria-hidden="true">▶</span>
483 <span class="plugin-config-title">{{.Name}}</span>
484 <small class="muted">v{{.Version}} 插件设置</small>
485 {{if .Enabled}}<span class="tag ok">已启用</span>{{else}}<span class="tag warn">插件未启用</span>{{end}}
486 </summary>
487 {{if .Enabled}}
488 <div class="plugin-config-body">
489 <form method="post" action="/admin/plugins/config" enctype="multipart/form-data">
490 <input type="hidden" name="_csrf" value="{{$.csrf}}">
491 <input type="hidden" name="plugin" value="{{.Name}}">
492 {{$cfg := index $.configs .Name}}
493 {{range .Config}}
494 {{if eq .Type "audio"}}
495 {{/* 上传型字段:不套 label,避免点击试听控件时触发文件选择 */}}
496 {{$audio := index $cfg .Key}}
497 <div class="plugin-audio">
498 <span class="field-label">{{.Label}}</span>
499 {{if $audio}}
500 <audio src="{{$audio}}" controls preload="none" class="plugin-audio-prev"></audio>
501 <label class="check-row"><input type="checkbox" name="cfg_{{.Key}}__clear" value="1"> 删除当前音乐</label>
502 {{else}}<span class="muted small">尚未上传</span>{{end}}
503 <input type="file" name="cfg_{{.Key}}__file" accept="audio/*,.mp3,.m4a,.aac,.ogg,.oga,.opus,.wav,.flac">
504 <input type="hidden" name="cfg_{{.Key}}" value="{{$audio}}">
505 </div>
506 {{else}}
507 <label>{{.Label}}
508 {{if eq .Type "textarea"}}
509 <textarea name="cfg_{{.Key}}" rows="3" placeholder="{{.Default}}">{{index $cfg .Key}}</textarea>
510 {{else if eq .Type "switch"}}
511 <span class="check-row"><input type="checkbox" name="cfg_{{.Key}}" value="1" {{if eq (index $cfg .Key) "1"}}checked{{end}}> 开启</span>
512 {{else if eq .Type "select"}}
513 {{$cur := index $cfg .Key}}
514 <select name="cfg_{{.Key}}">{{range .Options}}<option value="{{.}}" {{if eq . $cur}}selected{{end}}>{{.}}</option>{{end}}</select>
515 {{else}}
516 <input type="text" name="cfg_{{.Key}}" value="{{index $cfg .Key}}" placeholder="{{.Default}}">
517 {{end}}
518 </label>
519 {{end}}
520 {{if .Help}}<p class="muted small plugin-help">{{.Help}}</p>{{end}}
521 {{end}}
522 <div class="btn-row"><button class="btn btn-primary" type="submit">保存设置</button></div>
523</form>
524 </div>
525 {{else}}
526 <p class="muted plugin-config-tip">插件未启用,请先在下方「已安装插件」列表中启用该插件,再进行配置。</p>
527 {{end}}
528</details>
529{{end}}
530{{end}}
531
532<div class="card table-card">
533 <h3>已安装插件</h3>
534 <div class="table-scroll">
535 <table class="table">
536 <thead><tr><th>名称</th><th>类型</th><th>版本</th><th>作者</th><th>说明</th><th>钩子数</th><th>状态</th><th>操作</th></tr></thead>
537 <tbody>
538 {{range $p := .plugins}}
539 {{$info := index $.infos $p.Name}}
540 <tr>
541 <td><b>{{$p.Name}}</b>{{if $p.Warn}}<br><small class="warn">⚠ {{$p.Warn}}</small>{{end}}{{if $p.App}}<br><small class="muted">/x/{{$p.Slug}}/</small>{{end}}</td>
542 <td>{{if $p.App}}<span class="tag">应用型</span>{{else}}<span class="tag">声明式</span>{{end}}</td>
543 <td>{{$p.Version}}</td>
544 <td>{{$p.Author}}</td>
545 <td class="cell-content">{{$p.Description}}
546 {{if $p.App}}
547 <br><small class="muted">路由 {{$info.Routes}} · 页面 {{$info.Pages}} · Slot {{$info.Slots}} · 事件 {{$info.Events}} · 过滤器 {{$info.Filters}}</small>
548 {{if $info.Perms}}<br><small class="muted">权限:{{range $info.Perms}}<code>{{.}}</code> {{end}}</small>{{end}}
549 {{if $info.Jobs}}<br><small class="muted">定时任务:{{range $info.Jobs}}{{.Name}}({{.Every}}) {{end}}</small>{{end}}
550 {{if $info.Fails}}<br><small class="warn">连续失败 {{$info.Fails}} 次(达 20 次将自动禁用)</small>{{end}}
551 {{end}}
552 </td>
553 <td>{{len $p.Hooks}}</td>
554 <td>{{if $p.Enabled}}{{if $p.App}}{{if $info.Running}}<span class="tag ok">运行中</span>{{else}}<span class="tag warn">未运行</span>{{end}}{{else}}<span class="tag ok">已启用</span>{{end}}{{else}}<span class="tag warn">已禁用</span>{{end}}</td>
555 <td class="ops">
556 <form method="post" action="/admin/plugins/toggle" class="inline-form"{{if and $p.App (not $p.Enabled)}}{{if $info.Perms}} data-confirm="该插件声明了权限:{{range $info.Perms}}{{.}} {{end}};确定启用吗?"{{end}}{{end}}>
557 <input type="hidden" name="_csrf" value="{{$.csrf}}">
558 <input type="hidden" name="name" value="{{$p.Name}}">
559 {{if $p.Enabled}}
560 <input type="hidden" name="on" value="0">
561 <button class="mini-btn" type="submit">禁用</button>
562 {{else}}
563 <input type="hidden" name="on" value="1">
564 <button class="mini-btn" type="submit">启用</button>
565 {{end}}
566 </form>
567 {{if and $p.App $p.Enabled}}
568 <form method="post" action="/admin/plugins/reload" class="inline-form">
569 <input type="hidden" name="_csrf" value="{{$.csrf}}">
570 <input type="hidden" name="name" value="{{$p.Name}}">
571 <button class="mini-btn" type="submit">重载</button>
572 </form>
573 {{end}}
574 <a class="mini-btn" href="/admin/plugin-logs?slug={{$p.Slug}}">日志</a>
575 <form method="post" action="/admin/plugins/delete" class="inline-form" data-confirm="确定卸载该插件吗?">
576 <input type="hidden" name="_csrf" value="{{$.csrf}}">
577 <input type="hidden" name="name" value="{{$p.Name}}">
578 <button class="mini-btn danger" type="submit">卸载</button>
579 </form>
580 </td>
581 </tr>
582 {{else}}<tr><td colspan="8" class="empty">暂无插件</td></tr>{{end}}
583 </tbody>
584 </table>
585 </div>
586</div>
587{{end}}
588
589{{/* ============ 插件商店(对接云端插件社区) ============ */}}
590{{define "pg_admin_store"}}
591<div class="page-head row">
592 <div>
593 <h1 class="page-title">插件商店</h1>
594 <p class="muted">来自 ClearLove 云端插件社区 · <span class="mono">{{.cloud}}</span></p>
595 </div>
596 <form method="get" action="/admin/store" class="code-row">
597 <input type="text" name="q" value="{{.q}}" placeholder="搜索插件名称 / 描述">
598 <select name="kind">
599 <option value="">全部形态</option>
600 <option value="declarative" {{if eq .kind "declarative"}}selected{{end}}>声明式</option>
601 <option value="app" {{if eq .kind "app"}}selected{{end}}>应用型</option>
602 </select>
603 <button class="btn btn-ghost" type="submit">搜索</button>
604 </form>
605</div>
606
607{{if .notice}}<p class="form-ok">{{.notice}}</p>{{end}}
608{{if .errMsg}}<p class="form-err">{{.errMsg}}</p>{{end}}
609
610{{if .plugins}}
611<div class="store-grid">
612 {{range .plugins}}
613 <article class="card store-card">
614 <div class="store-head">
615 <span class="avatar">{{substr0 .Name}}</span>
616 <div class="meta">
617 <span class="nickname">{{.Name}}</span>
618 <span class="muted mono">v{{.Version}} · {{.CategoryTxt}}</span>
619 </div>
620 </div>
621 <p class="store-desc">{{if .Description}}{{snippet .Description 96}}{{else}}暂无描述{{end}}</p>
622 <div class="store-meta">
623 <span>作者:{{.Author}}</span>
624 <span>↓ {{.Downloads}}</span>
625 <span>安装 {{.Installs}}</span>
626 </div>
627 {{if eq .Kind "app"}}
628 <p class="muted"><span class="tag">应用型</span>{{if .Permissions}} 权限:{{range .Permissions}}<code>{{.}}</code> {{end}}{{end}}</p>
629 {{end}}
630 <div class="store-actions">
631 {{if index $.installed .Name}}
632 <span class="tag ok">已安装</span>
633 <a class="mini-btn" href="/admin/plugins">去启用</a>
634 {{else}}
635 {{if .Version}}
636 <form method="post" action="/admin/store/install" class="inline-form" data-confirm="确定从云端安装插件「{{.Name}}」吗?{{if .Permissions}}该插件声明权限:{{range .Permissions}}{{.}} {{end}}{{end}}">
637 <input type="hidden" name="_csrf" value="{{$.csrf}}">
638 <input type="hidden" name="slug" value="{{.Slug}}">
639 <input type="hidden" name="version" value="{{.Version}}">
640 <input type="hidden" name="sha256" value="{{.SHA256}}">
641 <button class="mini-btn" type="submit">安装</button>
642 </form>
643 {{end}}
644 <a class="mini-btn" href="{{.PageURL}}" target="_blank" rel="noopener">详情</a>
645 {{end}}
646 </div>
647 </article>
648 {{end}}
649</div>
650{{else if not .errMsg}}
651<p class="empty">云端暂时没有可安装的插件</p>
652{{end}}
653{{end}}
654
655{{/* ============ API 接口 ============ */}}
656{{define "pg_admin_apikeys"}}
657<h1 class="page-title">API 接口</h1>
658<div class="card">
659 <h3>生成 API Key</h3>
660 <form method="post" action="/admin/apikeys/create" class="inline-form">
661 <input type="hidden" name="_csrf" value="{{.csrf}}">
662 <input type="text" name="name" maxlength="30" placeholder="用途备注(例如:iOS 客户端)">
663 <button class="btn btn-primary" type="submit">生成密钥</button>
664 </form>
665 <p class="muted">第三方客户端在请求头携带 X-API-Key 即可调用 /api/v1 全部接口,无需登录。</p>
666</div>
667<div class="card table-card">
668 <h3>密钥列表</h3>
669 <div class="table-scroll">
670 <table class="table">
671 <thead><tr><th>ID</th><th>备注</th><th>密钥</th><th>最近使用</th><th>创建时间</th><th>操作</th></tr></thead>
672 <tbody>
673 {{range .keys}}
674 <tr>
675 <td>{{.ID}}</td><td>{{.Name}}</td>
676 <td class="mono">{{.Key}}</td>
677 <td class="mono">{{if .LastUsed}}{{datefmt .LastUsed}}{{else}}从未{{end}}</td>
678 <td class="mono">{{datefmt .CreatedAt}}</td>
679 <td class="ops">
680 <form method="post" action="/admin/apikeys/delete" class="inline-form" data-confirm="确定删除该密钥吗?">
681 <input type="hidden" name="_csrf" value="{{$.csrf}}">
682 <input type="hidden" name="id" value="{{.ID}}">
683 <button class="mini-btn danger" type="submit">删除</button>
684 </form>
685 </td>
686 </tr>
687 {{else}}<tr><td colspan="6" class="empty">暂无密钥</td></tr>{{end}}
688 </tbody>
689 </table>
690 </div>
691</div>
692<div class="card">
693 <h3>接口速览</h3>
694 <ul class="api-list">
695 <li><span class="method get">GET</span> /api/v1/posts?before=&limit=&topic= 帖子列表</li>
696 <li><span class="method get">GET</span> /api/v1/posts/{id} 帖子详情</li>
697 <li><span class="method post">POST</span> /api/v1/posts 发布帖子(JSON:content/nickname/topic/images/videos)</li>
698 <li><span class="method post">POST</span> /api/v1/upload 上传媒体(multipart:file)→ 返回 url</li>
699 <li><span class="method post">POST</span> /api/v1/posts/{id}/like 点赞/取消</li>
700 <li><span class="method post">POST</span> /api/v1/comments 发表评论(post_id/content/nickname)</li>
701 <li><span class="method post">POST</span> /api/v1/reports 举报(post_id/reason)</li>
702 <li><span class="method get">GET</span> /api/v1/topics 话题列表</li>
703 </ul>
704</div>
705{{end}}
706
707{{/* ============ 关于 ============ */}}
708{{/* ============ 数据迁移(旧版 PHP 1.x → 2.0) ============ */}}
709{{define "pg_admin_migrate"}}
710<div class="page-head">
711 <h1 class="page-title">数据迁移</h1>
712 <p class="muted">把 ClearLove PHP 旧版(1.x)的帖子、评论、用户等数据迁移到当前站点</p>
713</div>
714
715<div class="card">
716 <h3>迁移说明</h3>
717 <ul class="tips-list">
718 <li>支持旧版 <b>SQLite</b> 数据库文件(旧版根目录的 <code>data.db</code>)与 <b>MySQL</b> 导出的 <code>.sql</code> 文件(宝塔「数据库 → 备份」或 <code>mysqldump</code>),上传后自动识别</li>
719 <li>迁移会<b>保留原有 ID 与发布时间</b>,旧版的「正常 / 隐藏」状态会自动换算为新版语义(不会出现帖子集体消失或隐藏帖冒出来)</li>
720 <li>用户与管理员密码同为 bcrypt,<b>迁移后可直接用原密码登录</b></li>
721 <li>可重复执行:已存在的记录会自动跳过,不会产生重复数据</li>
722 <li>迁移期间请勿发帖;数据量大时耗时较长,可以关闭页面稍后回来查看结果</li>
723 </ul>
724</div>
725
726<div class="card">
727 <h3>上传旧版数据库</h3>
728 <form id="migrate-form" enctype="multipart/form-data">
729 <input type="hidden" name="_csrf" value="{{.csrf}}">
730 <label>数据库文件
731 <input type="file" name="file" id="migrate-file" accept=".db,.sql,.sqlite,.sqlite3" required>
732 </label>
733 <p class="muted small">单个文件不超过 {{.maxMB}}MB。SQLite 请上传旧版根目录的 <code>data.db</code>;MySQL 请上传导出的 <code>.sql</code>。</p>
734
735 <label>旧版 uploads 目录路径(可选)
736 <input type="text" name="uploads_dir" id="migrate-uploads" placeholder="/www/wwwroot/old/uploads">
737 </label>
738 <p class="muted small">填写服务器上的绝对路径时,会把旧站图片 / 视频复制到新版 <code>uploads/legacy/</code>;留空则只迁移文字数据(图片会缺失,但不影响帖子本身)。</p>
739
740 <label class="check-row"><input type="checkbox" name="dry" value="1" id="migrate-dry" checked> 试运行(只统计不写入,建议先跑一次)</label>
741
742 <div class="btn-row">
743 <button class="btn btn-primary" type="submit" id="migrate-start">开始迁移</button>
744 </div>
745 </form>
746 <div class="progress" id="migrate-progress" hidden><span></span></div>
747 <p class="form-ok" id="migrate-ok" hidden></p>
748 <p class="form-err" id="migrate-err" hidden></p>
749</div>
750
751<div class="card" id="migrate-result" hidden>
752 <h3>迁移结果</h3>
753 <div id="migrate-result-body"></div>
754</div>
755{{end}}
756
757{{define "pg_admin_about"}}
758<h1 class="page-title">关于</h1>
759<div class="card about-card">
760 <svg viewBox="0 0 24 24" width="52" height="52" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 21s-8-5.2-8-10.4C4 7 6.2 5 8.8 5c1.6 0 2.7.8 3.2 1.7C12.5 5.8 13.6 5 15.2 5 17.8 5 20 7 20 10.6 20 15.8 12 21 12 21z"/></svg>
761 <h2>ClearLove 表白墙</h2>
762 <p class="muted">仰望星辰工作室,夏日之瓜,出品</p>
763</div>
764{{end}}
765
766{{/* ============ 检查更新 ============ */}}
767{{define "pg_admin_update"}}
768<h1 class="page-title">检查更新</h1>
769<div class="card">
770 <table class="kv-table">
771 <tr><th>当前版本</th><td>{{.result.current}}</td></tr>
772 <tr><th>运行平台</th><td>{{.platform}} {{if .canSelfUpdate}}<span class="tag ok">支持一键更新</span>{{else}}<span class="tag warn">一键更新仅支持 Linux x86_64</span>{{end}}</td></tr>
773 <tr><th>更新接口</th><td class="mono">{{.result.url}}</td></tr>
774 {{with .result.latest}}<tr><th>最新版本</th><td>{{.}}</td></tr>{{end}}
775 {{with .result.notes}}<tr><th>更新说明</th><td>{{.}}</td></tr>{{end}}
776 {{with .result.download}}<tr><th>下载地址</th><td class="mono">{{.}}</td></tr>{{end}}
777 {{with .result.sha256}}<tr><th>SHA256</th><td class="mono">{{.}}</td></tr>{{end}}
778 {{with .result.error}}<tr><th>结果</th><td class="warn">{{.}}</td></tr>{{end}}
779 </table>
780 {{if .result.upToDate}}<p class="tag ok">当前已是最新版本</p>
781 {{else if .result.latest}}
782 <p class="tag warn">发现新版本</p>
783 {{if .canSelfUpdate}}
784 <div class="btn-row" style="margin-top:10px">
785 <button class="btn btn-primary" id="self-update-btn" type="button">立即更新</button>
786 <span id="self-update-msg" class="muted"></span>
787 </div>
788 {{else}}
789 <p class="muted small">请前往上方下载地址手动替换二进制文件后重启服务。</p>
790 {{end}}
791 {{end}}
792 <div class="btn-row">
793 <a class="btn btn-ghost" href="/admin/update">重新检查</a>
794 </div>
795 <p class="muted small">云端接口(网站设置 → 更新接口地址)返回 JSON,例如:
796 <code>{"version":"2.0.1","notes":"更新说明","packages":{"linux-amd64":{"url":"二进制直链","sha256":"文件SHA256"}}}</code>;
797 一键更新必须提供 linux-amd64 的 sha256,更新时旧程序会备份为「程序名.bak」。</p>
798</div>
799<script>
800(function () {
801 var btn = document.getElementById("self-update-btn");
802 if (!btn) return;
803 var msg = document.getElementById("self-update-msg");
804 var csrf = "";
805 var m = document.querySelector('meta[name="csrf"]');
806 if (m) csrf = m.content || "";
807 btn.addEventListener("click", function () {
808 if (!confirm("确定立即更新到最新版本吗?\\n更新过程中服务会短暂重启(原地换载,一般 1-2 秒)。")) return;
809 btn.disabled = true;
810 msg.textContent = "正在下载并校验新版本,请勿关闭页面…";
811 fetch("/admin/update/apply", { method: "POST", headers: { "X-CSRF-Token": csrf } })
812 .then(function (r) { return r.json(); })
813 .then(function (res) {
814 if (res.ok) {
815 msg.textContent = (res.msg || "更新完成") + ",3 秒后自动刷新…";
816 setTimeout(function () { location.reload(); }, 3000);
817 } else {
818 msg.textContent = res.msg || "更新失败";
819 btn.disabled = false;
820 }
821 })
822 .catch(function () {
823 msg.textContent = "连接中断:若由重启所致,说明更新可能已成功,请刷新页面查看版本";
824 btn.disabled = false;
825 });
826 });
827})();
828</script>
829{{end}}
830
831{{/* ============ 插件运行日志 ============ */}}
832{{define "pg_admin_plugin_logs"}}
833<h1 class="page-title">插件运行日志{{if .slug}} <small>已筛选:{{.slug}}</small>{{end}}</h1>
834<div class="card table-card">
835 <div class="btn-row" style="margin-bottom:12px">
836 <a class="btn btn-ghost" href="/admin/plugins">返回插件管理</a>
837 {{if .slug}}<a class="btn btn-ghost" href="/admin/plugin-logs">查看全部</a>{{end}}
838 <form method="post" action="/admin/plugin-logs/clear" class="inline-form" data-confirm="确定清空全部插件运行日志吗?">
839 <input type="hidden" name="_csrf" value="{{.csrf}}">
840 <button class="mini-btn danger" type="submit">清空日志</button>
841 </form>
842 </div>
843 <div class="table-scroll">
844 <table class="table">
845 <thead><tr><th>时间</th><th>插件</th><th>动作</th><th>详情</th><th>耗时</th></tr></thead>
846 <tbody>
847 {{range .logs}}
848 <tr>
849 <td>{{datefmt .created_at}}</td>
850 <td><a href="/admin/plugin-logs?slug={{.plugin_slug}}">{{.plugin_slug}}</a></td>
851 <td><code>{{.action}}</code></td>
852 <td class="cell-content">{{.detail}}</td>
853 <td>{{.duration_ms}} ms</td>
854 </tr>
855 {{else}}<tr><td colspan="5" class="empty">暂无日志</td></tr>{{end}}
856 </tbody>
857 </table>
858 </div>
859</div>
860{{end}}