From 1a7a9db88f95615a2c16e803c7503d7c194c85f0 Mon Sep 17 00:00:00 2001 From: anghunk Date: Sat, 31 Jan 2026 10:32:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=B7=BB=E5=8A=A0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BF=A1=E6=81=AF=E6=98=BE=E7=A4=BA=E4=B8=8E=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在布局侧边栏底部添加版本信息区域,显示 API 和后台版本 - 点击版本信息可打开模态框,展示详细的版本信息和兼容性状态 - 当 API 版本与后台版本不一致时提示用户更新以避免兼容性问题 - 移除登录页面中已注释的 GitHub 链接 - 更新 package.json 版本号至 0.0.11 - 优化 API 根路径响应,返回 JSON 格式的版本信息 - 清理重复的导入语句和冗余代码 --- cwd-admin/src/styles/layout.less | 27 +++- cwd-admin/src/views/LayoutView/index.vue | 169 +++++++++++++++++++++++ cwd-admin/src/views/LoginView/index.vue | 3 - cwd-api/src/index.ts | 162 ++++++++-------------- package.json | 2 +- 5 files changed, 250 insertions(+), 113 deletions(-) diff --git a/cwd-admin/src/styles/layout.less b/cwd-admin/src/styles/layout.less index 314d21e..3356a2c 100644 --- a/cwd-admin/src/styles/layout.less +++ b/cwd-admin/src/styles/layout.less @@ -114,12 +114,37 @@ width: 180px; background-color: var(--bg-sider); border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; } .menu { list-style: none; margin: 0; padding: 12px 0; + flex: 1; + overflow-y: auto; +} + +.layout-sider-footer { + padding: 12px 16px; + border-top: 1px solid var(--border-color); + font-size: 12px; + color: var(--text-tertiary); + text-align: center; + line-height: 1.5; + cursor: pointer; + user-select: none; +} + +.layout-sider-footer:hover { + background-color: var(--bg-hover); +} + +.layout-sider-footer-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .menu-item { @@ -275,4 +300,4 @@ .layout-actions-item-danger:hover { background-color: rgba(209, 36, 47, 0.1); } -} \ No newline at end of file +} diff --git a/cwd-admin/src/views/LayoutView/index.vue b/cwd-admin/src/views/LayoutView/index.vue index 2b0ecb2..5c06a1c 100644 --- a/cwd-admin/src/views/LayoutView/index.vue +++ b/cwd-admin/src/views/LayoutView/index.vue @@ -114,12 +114,54 @@ 数据管理 +
+ @@ -128,8 +170,10 @@ import { ref, onMounted, watch, provide, computed } from "vue"; import { useRouter, useRoute } from "vue-router"; import { logoutAdmin, fetchDomainList } from "../../api/admin"; import { useTheme } from "../../composables/useTheme"; +import packageJson from "../../../../package.json"; const DOMAIN_STORAGE_KEY = "cwd_admin_domain_filter"; +const API_BASE_URL_KEY = "cwd_admin_api_base_url"; const router = useRouter(); const route = useRoute(); @@ -137,6 +181,11 @@ const { theme, setTheme } = useTheme(); const isMobileSiderOpen = ref(false); const isActionsOpen = ref(false); +const adminVersion = ref(packageJson.version || "0.0.0"); +const apiVersion = ref(""); +const checkedApiBaseUrl = ref(""); +const apiVersionError = ref(""); +const versionModalVisible = ref(false); const themeTitle = computed(() => { if (theme.value === "light") return "明亮模式"; @@ -171,10 +220,40 @@ async function loadDomains() { } } +async function loadVersion() { + const baseUrl = (localStorage.getItem(API_BASE_URL_KEY) || "").trim(); + if (!baseUrl) { + checkedApiBaseUrl.value = ""; + apiVersionError.value = ""; + return; + } + checkedApiBaseUrl.value = baseUrl; + apiVersionError.value = ""; + try { + const res = await fetch(baseUrl); + const contentType = res.headers.get("content-type") || ""; + if (!res.ok || !contentType.includes("application/json")) { + apiVersionError.value = + "当前 API 版本较旧,未提供版本信息接口。推荐后续升级到最新版本以获得完整的版本检测能力(不影响当前使用)。"; + return; + } + const data = await res.json().catch(() => null); + if (data && typeof data.version === "string") { + apiVersion.value = data.version; + } else { + apiVersionError.value = + "当前 API 版本较旧,未提供版本信息接口。推荐后续升级到最新版本以获得完整的版本检测能力(不影响当前使用)。"; + } + } catch (e) { + apiVersionError.value = (e as Error).message || "获取接口版本失败"; + } +} + provide("domainFilter", domainFilter); onMounted(() => { loadDomains(); + loadVersion(); }); watch(domainFilter, (value) => { @@ -248,8 +327,98 @@ function handleLogoutFromActions() { closeActions(); handleLogout(); } + +function openVersionModal() { + loadVersion(); + versionModalVisible.value = true; +} + +function closeVersionModal() { + versionModalVisible.value = false; +} diff --git a/cwd-admin/src/views/LoginView/index.vue b/cwd-admin/src/views/LoginView/index.vue index 71249f8..1d96325 100644 --- a/cwd-admin/src/views/LoginView/index.vue +++ b/cwd-admin/src/views/LoginView/index.vue @@ -42,9 +42,6 @@ - diff --git a/cwd-api/src/index.ts b/cwd-api/src/index.ts index c292d19..3db361f 100644 --- a/cwd-api/src/index.ts +++ b/cwd-api/src/index.ts @@ -2,11 +2,7 @@ import { Hono } from 'hono'; import { Bindings } from './bindings'; import { customCors } from './utils/cors'; import { adminAuth } from './utils/auth'; -import { - isValidEmail, - loadEmailNotificationSettings, - saveEmailNotificationSettings -} from './utils/email'; +import { isValidEmail, loadEmailNotificationSettings, saveEmailNotificationSettings } from './utils/email'; import { loadFeatureSettings } from './utils/featureSettings'; import packageJson from '../../package.json'; @@ -24,12 +20,6 @@ import { exportStats } from './api/admin/exportStats'; import { importStats } from './api/admin/importStats'; import { exportBackup } from './api/admin/exportBackup'; import { importBackup } from './api/admin/importBackup'; -import { exportConfig } from './api/admin/exportConfig'; -import { importConfig } from './api/admin/importConfig'; -import { exportStats } from './api/admin/exportStats'; -import { importStats } from './api/admin/importStats'; -import { exportBackup } from './api/admin/exportBackup'; -import { importBackup } from './api/admin/importBackup'; import { updateStatus } from './api/admin/updateStatus'; import { updateComment } from './api/admin/updateComment'; import { getAdminEmail } from './api/admin/getAdminEmail'; @@ -43,20 +33,12 @@ import { getLikeStatus, likePage } from './api/public/like'; import { likeComment } from './api/public/likeComment'; import { listLikes } from './api/admin/listLikes'; import { getLikeStats } from './api/admin/likeStats'; -import { - getFeatureSettings, - updateFeatureSettings -} from './api/admin/featureSettings'; -import { - getTelegramSettings, - updateTelegramSettings, - setupTelegramWebhook, - testTelegramMessage -} from './api/admin/telegramSettings'; +import { getFeatureSettings, updateFeatureSettings } from './api/admin/featureSettings'; +import { getTelegramSettings, updateTelegramSettings, setupTelegramWebhook, testTelegramMessage } from './api/admin/telegramSettings'; import { telegramWebhook } from './api/telegram/webhook'; const app = new Hono<{ Bindings: Bindings }>(); -const VERSION = `v${packageJson.version}`; +const VERSION = `${packageJson.version}`; const COMMENT_ADMIN_EMAIL_KEY = 'comment_admin_email'; const COMMENT_ADMIN_BADGE_KEY = 'comment_admin_badge'; @@ -68,11 +50,8 @@ const COMMENT_REQUIRE_REVIEW_KEY = 'comment_require_review'; const COMMENT_BLOCKED_IPS_KEY = 'comment_blocked_ips'; const COMMENT_BLOCKED_EMAILS_KEY = 'comment_blocked_emails'; - async function loadCommentSettings(env: Bindings) { - await env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' - ).run(); + await env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run(); const keys = [ COMMENT_ADMIN_EMAIL_KEY, COMMENT_ADMIN_BADGE_KEY, @@ -82,11 +61,9 @@ async function loadCommentSettings(env: Bindings) { COMMENT_ADMIN_KEY_HASH_KEY, COMMENT_REQUIRE_REVIEW_KEY, COMMENT_BLOCKED_IPS_KEY, - COMMENT_BLOCKED_EMAILS_KEY + COMMENT_BLOCKED_EMAILS_KEY, ]; - const { results } = await env.CWD_DB.prepare( - 'SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?, ?)' - ) + const { results } = await env.CWD_DB.prepare('SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?, ?)') .bind(...keys) .all<{ key: string; value: string }>(); @@ -105,18 +82,27 @@ async function loadCommentSettings(env: Bindings) { const blockedIpsRaw = map.get(COMMENT_BLOCKED_IPS_KEY) ?? ''; const blockedIps = blockedIpsRaw - ? blockedIpsRaw.split(',').map((d) => d.trim()).filter(Boolean) + ? blockedIpsRaw + .split(',') + .map((d) => d.trim()) + .filter(Boolean) : []; const blockedEmailsRaw = map.get(COMMENT_BLOCKED_EMAILS_KEY) ?? ''; const blockedEmails = blockedEmailsRaw - ? blockedEmailsRaw.split(',').map((d) => d.trim()).filter(Boolean) + ? blockedEmailsRaw + .split(',') + .map((d) => d.trim()) + .filter(Boolean) : []; // 解析允许的域名列表 const allowedDomainsRaw = map.get(COMMENT_ALLOWED_DOMAINS_KEY) ?? ''; const allowedDomains = allowedDomainsRaw - ? allowedDomainsRaw.split(',').map((d) => d.trim()).filter(Boolean) + ? allowedDomainsRaw + .split(',') + .map((d) => d.trim()) + .filter(Boolean) : []; return { @@ -129,7 +115,7 @@ async function loadCommentSettings(env: Bindings) { blockedIps, blockedEmails, adminKey: map.get(COMMENT_ADMIN_KEY_HASH_KEY) ?? null, - adminKeySet: !!map.get(COMMENT_ADMIN_KEY_HASH_KEY) + adminKeySet: !!map.get(COMMENT_ADMIN_KEY_HASH_KEY), }; } @@ -147,9 +133,7 @@ async function saveCommentSettings( blockedEmails?: string[]; } ) { - await env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' - ).run(); + await env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run(); let adminKeyValue: string | undefined; if (settings.adminKey !== undefined) { @@ -162,38 +146,28 @@ async function saveCommentSettings( { key: COMMENT_AVATAR_PREFIX_KEY, value: settings.avatarPrefix }, { key: COMMENT_ADMIN_ENABLED_KEY, - value: - typeof settings.adminEnabled === 'boolean' - ? settings.adminEnabled - ? '1' - : '0' - : undefined + value: typeof settings.adminEnabled === 'boolean' ? (settings.adminEnabled ? '1' : '0') : undefined, }, { key: COMMENT_ALLOWED_DOMAINS_KEY, - value: settings.allowedDomains ? settings.allowedDomains.join(',') : undefined + value: settings.allowedDomains ? settings.allowedDomains.join(',') : undefined, }, { key: COMMENT_ADMIN_KEY_HASH_KEY, - value: adminKeyValue + value: adminKeyValue, }, { key: COMMENT_REQUIRE_REVIEW_KEY, - value: - typeof settings.requireReview === 'boolean' - ? settings.requireReview - ? '1' - : '0' - : undefined + value: typeof settings.requireReview === 'boolean' ? (settings.requireReview ? '1' : '0') : undefined, }, { key: COMMENT_BLOCKED_IPS_KEY, - value: settings.blockedIps ? settings.blockedIps.join(',') : undefined + value: settings.blockedIps ? settings.blockedIps.join(',') : undefined, }, { key: COMMENT_BLOCKED_EMAILS_KEY, - value: settings.blockedEmails ? settings.blockedEmails.join(',') : undefined - } + value: settings.blockedEmails ? settings.blockedEmails.join(',') : undefined, + }, ]; for (const entry of entries) { @@ -201,9 +175,7 @@ async function saveCommentSettings( const value = entry.value === null ? '' : entry.value; const trimmed = typeof value === 'string' ? value.trim() : value; if (trimmed) { - await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)') - .bind(entry.key, trimmed) - .run(); + await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)').bind(entry.key, trimmed).run(); } else { await env.CWD_DB.prepare('DELETE FROM Settings WHERE key = ?').bind(entry.key).run(); } @@ -217,12 +189,12 @@ app.use('*', async (c, next) => { path: c.req.path, url: c.req.url, hasDb: !!c.env.CWD_DB, - hasAuthKv: !!c.env.CWD_AUTH_KV + hasAuthKv: !!c.env.CWD_AUTH_KV, }); const res = await next(); console.log('Request:end', { method: c.req.method, - path: c.req.path + path: c.req.path, }); return res; }); @@ -237,9 +209,8 @@ app.use('/admin/*', async (c, next) => { }); app.get('/', (c) => { - return c.html( - `CWD 评论部署成功,当前版本 ${VERSION},查看文档` - ); + c.header('Access-Control-Allow-Origin', '*'); + return c.json({ version: VERSION, data: '成功部署 CWD 评论系统 API!文档地址 https://cwd.js.org' }); }); app.get('/api/comments', getComments); @@ -254,13 +225,7 @@ app.get('/api/config/comments', async (c) => { try { const settings = await loadCommentSettings(c.env); const featureSettings = await loadFeatureSettings(c.env); - const { - adminKey, - adminKeySet, - blockedIps, - blockedEmails, - ...publicSettings - } = settings as any; + const { adminKey, adminKeySet, blockedIps, blockedEmails, ...publicSettings } = settings as any; return c.json({ ...publicSettings, ...featureSettings }); } catch (e: any) { @@ -303,15 +268,14 @@ app.get('/admin/settings/email-notify', async (c) => { app.put('/admin/settings/email-notify', async (c) => { try { const body = await c.req.json(); - const globalEnabled = - typeof body.globalEnabled === 'boolean' ? body.globalEnabled : undefined; + const globalEnabled = typeof body.globalEnabled === 'boolean' ? body.globalEnabled : undefined; const smtp = body.smtp && typeof body.smtp === 'object' ? body.smtp : undefined; const templates = body.templates && typeof body.templates === 'object' ? body.templates : undefined; await saveEmailNotificationSettings(c.env, { globalEnabled, smtp, - templates + templates, }); return c.json({ message: '保存成功' }); @@ -351,24 +315,12 @@ app.put('/admin/settings/comments', async (c) => { const adminEmail = rawAdminEmail.trim(); const adminBadge = rawAdminBadge.trim(); const avatarPrefix = rawAvatarPrefix.trim(); - const adminEnabled = - typeof rawAdminEnabled === 'boolean' - ? rawAdminEnabled - : rawAdminEnabled === '1' || rawAdminEnabled === 1; - const allowedDomains = rawAllowedDomains - .map((d: any) => (typeof d === 'string' ? d.trim() : '')) - .filter(Boolean); + const adminEnabled = typeof rawAdminEnabled === 'boolean' ? rawAdminEnabled : rawAdminEnabled === '1' || rawAdminEnabled === 1; + const allowedDomains = rawAllowedDomains.map((d: any) => (typeof d === 'string' ? d.trim() : '')).filter(Boolean); const adminKey = rawAdminKey; // Can be undefined or empty string - const requireReview = - typeof rawRequireReview === 'boolean' - ? rawRequireReview - : rawRequireReview === '1' || rawRequireReview === 1; - const blockedIps = rawBlockedIps - .map((d: any) => (typeof d === 'string' ? d.trim() : '')) - .filter(Boolean); - const blockedEmails = rawBlockedEmails - .map((d: any) => (typeof d === 'string' ? d.trim() : '')) - .filter(Boolean); + const requireReview = typeof rawRequireReview === 'boolean' ? rawRequireReview : rawRequireReview === '1' || rawRequireReview === 1; + const blockedIps = rawBlockedIps.map((d: any) => (typeof d === 'string' ? d.trim() : '')).filter(Boolean); + const blockedEmails = rawBlockedEmails.map((d: any) => (typeof d === 'string' ? d.trim() : '')).filter(Boolean); if (adminEmail && !isValidEmail(adminEmail)) { return c.json({ message: '邮箱格式不正确' }, 400); @@ -383,7 +335,7 @@ app.put('/admin/settings/comments', async (c) => { adminKey, requireReview, blockedIps, - blockedEmails + blockedEmails, }); return c.json({ message: '保存成功' }); @@ -402,9 +354,7 @@ app.post('/admin/comments/block-ip', async (c) => { return c.json({ message: 'IP 地址不能为空' }, 400); } - await c.env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' - ).run(); + await c.env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run(); const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?') .bind(COMMENT_BLOCKED_IPS_KEY) @@ -412,17 +362,16 @@ app.post('/admin/comments/block-ip', async (c) => { const existing = row?.value || ''; const list = existing - ? existing.split(',').map((d) => d.trim()).filter(Boolean) + ? existing + .split(',') + .map((d) => d.trim()) + .filter(Boolean) : []; if (!list.includes(ip)) { list.push(ip); const joined = list.join(','); - await c.env.CWD_DB.prepare( - 'REPLACE INTO Settings (key, value) VALUES (?, ?)' - ) - .bind(COMMENT_BLOCKED_IPS_KEY, joined) - .run(); + await c.env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)').bind(COMMENT_BLOCKED_IPS_KEY, joined).run(); } return c.json({ message: '已加入 IP 黑名单' }); @@ -445,9 +394,7 @@ app.post('/admin/comments/block-email', async (c) => { return c.json({ message: '邮箱格式不正确' }, 400); } - await c.env.CWD_DB.prepare( - 'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)' - ).run(); + await c.env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run(); const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?') .bind(COMMENT_BLOCKED_EMAILS_KEY) @@ -455,17 +402,16 @@ app.post('/admin/comments/block-email', async (c) => { const existing = row?.value || ''; const list = existing - ? existing.split(',').map((d) => d.trim()).filter(Boolean) + ? existing + .split(',') + .map((d) => d.trim()) + .filter(Boolean) : []; if (!list.includes(email)) { list.push(email); const joined = list.join(','); - await c.env.CWD_DB.prepare( - 'REPLACE INTO Settings (key, value) VALUES (?, ?)' - ) - .bind(COMMENT_BLOCKED_EMAILS_KEY, joined) - .run(); + await c.env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)').bind(COMMENT_BLOCKED_EMAILS_KEY, joined).run(); } return c.json({ message: '已加入邮箱黑名单' }); diff --git a/package.json b/package.json index 9281169..8ea734d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cwd", - "version": "0.0.10", + "version": "0.0.11", "license": "Apache-2.0", "repository": { "type": "git",