diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index aef8443..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.port": 5501 -} \ No newline at end of file diff --git a/cwd-comments-admin/index.html b/cwd-comments-admin/index.html new file mode 100644 index 0000000..092c0b2 --- /dev/null +++ b/cwd-comments-admin/index.html @@ -0,0 +1,13 @@ + + + + + + CWD 评论系统后台 + + +
+ + + + diff --git a/cwd-comments-admin/package.json b/cwd-comments-admin/package.json new file mode 100644 index 0000000..b3a62da --- /dev/null +++ b/cwd-comments-admin/package.json @@ -0,0 +1,22 @@ +{ + "name": "cwd-comments-admin", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.13", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.4", + "typescript": "^5.5.2", + "vite": "^6.0.11", + "vue-tsc": "^2.1.10" + } +} + diff --git a/cwd-comments-admin/src/App.vue b/cwd-comments-admin/src/App.vue new file mode 100644 index 0000000..b8e6c22 --- /dev/null +++ b/cwd-comments-admin/src/App.vue @@ -0,0 +1,23 @@ + + + + + + diff --git a/cwd-comments-admin/src/api/admin.ts b/cwd-comments-admin/src/api/admin.ts new file mode 100644 index 0000000..fa85f49 --- /dev/null +++ b/cwd-comments-admin/src/api/admin.ts @@ -0,0 +1,65 @@ +import { get, post, put, del } from './http'; + +export type AdminLoginResponse = { + data: { + key: string; + }; +}; + +export type CommentItem = { + id: number; + pubDate: string; + author: string; + email: string; + postSlug: string; + url: string | null; + ipAddress: string | null; + contentText: string; + contentHtml: string; + status: string; +}; + +export type CommentListResponse = { + data: CommentItem[]; + pagination: { + page: number; + limit: number; + total: number; + }; +}; + +export type AdminEmailResponse = { + email: string | null; +}; + +export async function loginAdmin(name: string, password: string): Promise { + const res = await post('/admin/login', { name, password }); + const key = res.data.key; + localStorage.setItem('cwd_admin_token', key); + return key; +} + +export function logoutAdmin(): void { + localStorage.removeItem('cwd_admin_token'); +} + +export function fetchComments(page: number): Promise { + return get(`/admin/comments/list?page=${page}`); +} + +export function deleteComment(id: number): Promise<{ message: string }> { + return del<{ message: string }>(`/admin/comments/delete?id=${id}`); +} + +export function updateCommentStatus(id: number, status: string): Promise<{ message: string }> { + return put<{ message: string }>(`/admin/comments/status?id=${id}&status=${encodeURIComponent(status)}`); +} + +export function fetchAdminEmail(): Promise { + return get('/admin/settings/email'); +} + +export function saveAdminEmail(email: string): Promise<{ message: string }> { + return put<{ message: string }>('/admin/settings/email', { email }); +} + diff --git a/cwd-comments-admin/src/api/http.ts b/cwd-comments-admin/src/api/http.ts new file mode 100644 index 0000000..8c66460 --- /dev/null +++ b/cwd-comments-admin/src/api/http.ts @@ -0,0 +1,47 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, ''); + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; + +async function request(method: HttpMethod, path: string, body?: unknown): Promise { + const token = localStorage.getItem('cwd_admin_token'); + const headers: HeadersInit = {}; + if (body !== undefined) { + headers['Content-Type'] = 'application/json'; + } + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const res = await fetch(`${API_BASE_URL}${path}`, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined + }); + let data: any = null; + try { + data = await res.json(); + } catch { + data = null; + } + if (!res.ok) { + const message = data && data.message ? data.message : `请求失败,状态码 ${res.status}`; + throw new Error(message); + } + return data as T; +} + +export function get(path: string): Promise { + return request('GET', path); +} + +export function post(path: string, body?: unknown): Promise { + return request('POST', path, body); +} + +export function put(path: string, body?: unknown): Promise { + return request('PUT', path, body); +} + +export function del(path: string): Promise { + return request('DELETE', path); +} + diff --git a/cwd-comments-admin/src/env.d.ts b/cwd-comments-admin/src/env.d.ts new file mode 100644 index 0000000..fd5d2e7 --- /dev/null +++ b/cwd-comments-admin/src/env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + diff --git a/cwd-comments-admin/src/main.ts b/cwd-comments-admin/src/main.ts new file mode 100644 index 0000000..69181f8 --- /dev/null +++ b/cwd-comments-admin/src/main.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import { router } from './router'; + +const app = createApp(App); +app.use(router); +app.mount('#app'); + diff --git a/cwd-comments-admin/src/router/index.ts b/cwd-comments-admin/src/router/index.ts new file mode 100644 index 0000000..51d5c50 --- /dev/null +++ b/cwd-comments-admin/src/router/index.ts @@ -0,0 +1,52 @@ +import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; +import LoginView from '../views/LoginView.vue'; +import LayoutView from '../views/LayoutView.vue'; +import CommentsView from '../views/CommentsView.vue'; +import SettingsView from '../views/SettingsView.vue'; + +const routes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'login', + component: LoginView + }, + { + path: '/', + component: LayoutView, + children: [ + { + path: '', + redirect: '/comments' + }, + { + path: 'comments', + name: 'comments', + component: CommentsView + }, + { + path: 'settings', + name: 'settings', + component: SettingsView + } + ] + } +]; + +export const router = createRouter({ + history: createWebHistory(), + routes +}); + +router.beforeEach((to, from, next) => { + if (to.name === 'login') { + next(); + return; + } + const token = localStorage.getItem('cwd_admin_token'); + if (!token) { + next({ name: 'login' }); + return; + } + next(); +}); + diff --git a/cwd-comments-admin/src/views/CommentsView.vue b/cwd-comments-admin/src/views/CommentsView.vue new file mode 100644 index 0000000..20fef71 --- /dev/null +++ b/cwd-comments-admin/src/views/CommentsView.vue @@ -0,0 +1,254 @@ + + + + + + diff --git a/cwd-comments-admin/src/views/LayoutView.vue b/cwd-comments-admin/src/views/LayoutView.vue new file mode 100644 index 0000000..3d21ae3 --- /dev/null +++ b/cwd-comments-admin/src/views/LayoutView.vue @@ -0,0 +1,130 @@ + + + + + + diff --git a/cwd-comments-admin/src/views/LoginView.vue b/cwd-comments-admin/src/views/LoginView.vue new file mode 100644 index 0000000..2135780 --- /dev/null +++ b/cwd-comments-admin/src/views/LoginView.vue @@ -0,0 +1,127 @@ + + + + + + diff --git a/cwd-comments-admin/src/views/SettingsView.vue b/cwd-comments-admin/src/views/SettingsView.vue new file mode 100644 index 0000000..6868304 --- /dev/null +++ b/cwd-comments-admin/src/views/SettingsView.vue @@ -0,0 +1,151 @@ + + + + + + diff --git a/cwd-comments-admin/tsconfig.json b/cwd-comments-admin/tsconfig.json new file mode 100644 index 0000000..f932362 --- /dev/null +++ b/cwd-comments-admin/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src"] +} + diff --git a/cwd-comments-admin/vite.config.ts b/cwd-comments-admin/vite.config.ts new file mode 100644 index 0000000..59b4f41 --- /dev/null +++ b/cwd-comments-admin/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5176 + } +}); + diff --git a/.dev.vars.example b/cwd-comments-api/.dev.vars.example similarity index 51% rename from .dev.vars.example rename to cwd-comments-api/.dev.vars.example index 61c3520..a865df8 100644 --- a/.dev.vars.example +++ b/cwd-comments-api/.dev.vars.example @@ -1,6 +1,5 @@ ALLOW_ORIGIN="http://localhost:4321,https://blog.example.top" -RESEND_API_KEY="re_xxxxxxxx" -RESEND_FROM_EMAIL="" +CF_FROM_EMAIL="noreply@yourdomain.com" EMAIL_ADDRESS="admin@example.top" ADMIN_NAME="Admin" -ADMIN_PASSWORD="password" \ No newline at end of file +ADMIN_PASSWORD="password" diff --git a/.editorconfig b/cwd-comments-api/.editorconfig similarity index 100% rename from .editorconfig rename to cwd-comments-api/.editorconfig diff --git a/.gitattributes b/cwd-comments-api/.gitattributes similarity index 100% rename from .gitattributes rename to cwd-comments-api/.gitattributes diff --git a/package.json b/cwd-comments-api/package.json similarity index 61% rename from package.json rename to cwd-comments-api/package.json index 6edda5d..d55f0e6 100644 --- a/package.json +++ b/cwd-comments-api/package.json @@ -7,12 +7,7 @@ "dev": "wrangler dev", "start": "wrangler dev", "test": "vitest", - "cf-typegen": "wrangler types", - "widget:dev": "cd widget && npm install && npm run dev", - "widget:build": "cd widget && npm run build", - "docs:dev": "cd docs && npm run dev", - "docs:build": "cd docs && npm run build", - "docs:preview": "cd docs && npm run preview" + "cf-typegen": "wrangler types" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.19", diff --git a/pnpm-lock.yaml b/cwd-comments-api/pnpm-lock.yaml similarity index 100% rename from pnpm-lock.yaml rename to cwd-comments-api/pnpm-lock.yaml diff --git a/schemas/comment.sql b/cwd-comments-api/schemas/comment.sql similarity index 100% rename from schemas/comment.sql rename to cwd-comments-api/schemas/comment.sql diff --git a/src/api/admin/deleteComment.ts b/cwd-comments-api/src/api/admin/deleteComment.ts similarity index 100% rename from src/api/admin/deleteComment.ts rename to cwd-comments-api/src/api/admin/deleteComment.ts diff --git a/cwd-comments-api/src/api/admin/getAdminEmail.ts b/cwd-comments-api/src/api/admin/getAdminEmail.ts new file mode 100644 index 0000000..88c7a7f --- /dev/null +++ b/cwd-comments-api/src/api/admin/getAdminEmail.ts @@ -0,0 +1,18 @@ +import { Context } from 'hono'; +import { Bindings } from '../../bindings'; + +export const getAdminEmail = async (c: Context<{ Bindings: Bindings }>) => { + try { + let email: string | null = null; + if (c.env.CWD_CONFIG_KV) { + email = await c.env.CWD_CONFIG_KV.get('settings:admin_notify_email'); + } + if (!email) { + email = c.env.EMAIL_ADDRESS || null; + } + return c.json({ email }); + } catch (e: any) { + return c.json({ message: e.message }, 500); + } +}; + diff --git a/src/api/admin/listComments.ts b/cwd-comments-api/src/api/admin/listComments.ts similarity index 100% rename from src/api/admin/listComments.ts rename to cwd-comments-api/src/api/admin/listComments.ts diff --git a/src/api/admin/login.ts b/cwd-comments-api/src/api/admin/login.ts similarity index 100% rename from src/api/admin/login.ts rename to cwd-comments-api/src/api/admin/login.ts diff --git a/cwd-comments-api/src/api/admin/setAdminEmail.ts b/cwd-comments-api/src/api/admin/setAdminEmail.ts new file mode 100644 index 0000000..d66c2e1 --- /dev/null +++ b/cwd-comments-api/src/api/admin/setAdminEmail.ts @@ -0,0 +1,23 @@ +import { Context } from 'hono'; +import { Bindings } from '../../bindings'; + +function isValidEmail(email: string) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +export const setAdminEmail = async (c: Context<{ Bindings: Bindings }>) => { + try { + const { email } = await c.req.json(); + if (!email || !isValidEmail(email)) { + return c.json({ message: '邮箱格式不正确' }, 400); + } + if (!c.env.CWD_CONFIG_KV) { + return c.json({ message: '未配置 CWD_CONFIG_KV,无法保存设置' }, 500); + } + await c.env.CWD_CONFIG_KV.put('settings:admin_notify_email', email); + return c.json({ message: '保存成功' }); + } catch (e: any) { + return c.json({ message: e.message }, 500); + } +}; + diff --git a/src/api/admin/updateStatus.ts b/cwd-comments-api/src/api/admin/updateStatus.ts similarity index 100% rename from src/api/admin/updateStatus.ts rename to cwd-comments-api/src/api/admin/updateStatus.ts diff --git a/src/api/public/getComments.ts b/cwd-comments-api/src/api/public/getComments.ts similarity index 100% rename from src/api/public/getComments.ts rename to cwd-comments-api/src/api/public/getComments.ts diff --git a/cwd-comments-api/src/api/public/postComment.ts b/cwd-comments-api/src/api/public/postComment.ts new file mode 100644 index 0000000..f08136b --- /dev/null +++ b/cwd-comments-api/src/api/public/postComment.ts @@ -0,0 +1,151 @@ +import { Context } from 'hono'; +import { UAParser } from 'ua-parser-js'; +import { Bindings } from '../../bindings'; +import { sendCommentNotification, sendCommentReplyNotification } from '../../utils/email'; + +// 检查内容,将 - - - - - -
- - -
- - {{ toast.message }} -
-
- - - - - -
-
-

评论管理

- -
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - -
信息内容状态操作
暂无评论数据
-
-
-
- - () -
- {{ comment.url }} -
IP:
-
{{ formatDate(comment.pubDate) }}
-
-
-
-
- - {{ config.blogDomain + comment.postSlug }} - -
- - {{ comment.status }} - - - - - -
-
- - -
- -
-
-
-
- - - - -`; diff --git a/src/views/login.ts b/src/views/login.ts deleted file mode 100644 index 569577e..0000000 --- a/src/views/login.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { html } from 'hono/html'; - -export const loginView = (isDev: boolean, adminName?: string, adminPassword?: string) => html` - - - - - - 登录 - CWD 评论后台 - - - - - - -
-
-

管理员登录

-
-
- - -
-
- - -
-
- - -
- -
-

{{ error }}

-
-
- - - - -`; diff --git a/src/views/settings.ts b/src/views/settings.ts deleted file mode 100644 index cd4a496..0000000 --- a/src/views/settings.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { html } from 'hono/html'; - -export const SettingsView = html` - - - - - - 设置 - CWD 评论后台 - - - - - - -
- - -
- - {{ toast.message }} -
-
- - - - - -
-

设置

-
-
- - -

设置后,文章链接将自动拼接此前缀

-
-
-
- -
-
-
- - - - -`;