refactor(widget): 简化前端配置并重构核心逻辑

移除前端配置中的 postSlug 和 avatarPrefix 参数,改为从后端接口获取
重构 CWDComments 核心类,自动推导 postSlug 等参数并异步加载服务端配置
更新文档和示例页面以反映配置变更
This commit is contained in:
anghunk
2026-01-20 10:39:48 +08:00
parent 1c0c4b5657
commit 0485b1320c
11 changed files with 149 additions and 86 deletions

View File

@@ -1,7 +1,7 @@
> [!WARNING]
> 目前仍处于 Beta 测试阶段,欢迎反馈测试结果。
<img src="https://github.com/anghunk/cwd-comments/blob/main/icon.png?raw=true" width="128" />
<img src="https://github.com/anghunk/cwd-comments/blob/main/icon.png?raw=true" width="72" />
# cwd-comments

View File

@@ -0,0 +1,38 @@
<template>
<div id="zcomments" ref="commentsRoot"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
const commentsRoot = ref(null);
onMounted(async () => {
if (!commentsRoot.value || typeof window === "undefined") return;
const apiBaseUrl = "https://cwd-comments-api.anghunk.workers.dev";
if (!apiBaseUrl) return;
if (!window.CWDComments) {
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://cwd-comments.zishu.me/cwd-comments.js";
script.async = true;
script.onload = () => resolve();
script.onerror = (e) => reject(e);
document.head.appendChild(script);
}).catch(() => {});
}
if (!window.CWDComments) return;
const comments = new window.CWDComments({
el: commentsRoot.value,
apiBaseUrl,
postSlug: window.location.pathname,
});
comments.mount();
});
</script>

View File

@@ -24,6 +24,7 @@ export default defineConfig({
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '后端配置', link: '/guide/backend-config' },
{ text: '前端配置', link: '/guide/frontend-config' },
{ text: '管理后台', link: '/guide/admin-panel' },
]
},
{

View File

@@ -0,0 +1,15 @@
import DefaultTheme from 'vitepress/theme'
import { h } from 'vue'
import FooterDoc from '../components/footerDoc.vue'
export default {
...DefaultTheme,
Layout() {
const Layout = DefaultTheme.Layout
return h(Layout, null, {
'doc-footer-before': () => h(FooterDoc),
})
},
}

View File

@@ -13,7 +13,6 @@
const comments = new CWDComments({
el: '#comments',
apiBaseUrl: 'https://your-api.example.com',
postSlug: window.location.origin + window.location.pathname,
});
comments.mount();
</script>
@@ -25,10 +24,11 @@
| ------------ | ----------------------- | ---- | ---------------------------------------------------------------- | ------------------------- |
| `el` | `string \| HTMLElement` | 是 | - | 挂载元素选择器或 DOM 元素 |
| `apiBaseUrl` | `string` | 是 | - | API 基础地址 |
| `postSlug` | `string` | 是 | 当前页面 URL `window.location.origin + window.location.pathname` | 文章唯一标识符 |
| `theme` | `'light' \| 'dark'` | 否 | `'light'` | 主题模式 |
| `pageSize` | `number` | 否 | `20` | 每页显示评论数 |
头像前缀、博主邮箱和标识等信息由后端接口 `/api/config/comments` 提供,无需在前端进行配置。
## 实例方法
| 方法 | 说明 |
@@ -43,7 +43,4 @@
```javascript
// 动态切换主题
comments.updateConfig({ theme: 'dark' });
// 切换文章
comments.updateConfig({ postSlug: 'another-post' });
```

View File

@@ -129,10 +129,6 @@
<label>API 地址</label>
<input type="text" id="apiBaseUrl" value="http://localhost:8788" />
</div>
<div class="config-item">
<label>文章标识符</label>
<input type="text" id="postSlug" value="demo-post" />
</div>
<div class="config-item">
<label>主题</label>
<select id="theme">
@@ -140,10 +136,6 @@
<option value="dark">深色</option>
</select>
</div>
<div class="config-item">
<label>头像源前缀</label>
<input type="text" id="avatarPrefix" value="https://gravatar.com/avatar" />
</div>
<div class="config-actions">
<button class="btn btn-primary" onclick="initWidget()">保存</button>
<button class="btn btn-secondary" onclick="toggleTheme()">切换主题</button>
@@ -155,7 +147,7 @@
<div class="demo-post">
<h2>这是一篇示例文章</h2>
<p>
这是一个演示 CWD Comments Widget 的示例页面。你可以在上方的配置面板中修改 API 地址和文章标识符
这是一个演示 CWD Comments Widget 的示例页面。你可以在上方的配置面板中修改 API 地址和主题
然后点击"应用配置"按钮重新加载评论组件。
</p>
</div>
@@ -169,4 +161,4 @@
<script type="module" src="/src/dev.js"></script>
</body>
</html>
</html>

9
widget/public/test.html Normal file
View File

@@ -0,0 +1,9 @@
<script src="./cwd-comments.js"></script>
<div id="comments"></div>
<script>
const comments = new CWDComments({
el: '#comments',
apiBaseUrl: 'https://cwd-comments-api.anghunk.workers.dev',
});
comments.mount();
</script>

View File

@@ -17,21 +17,24 @@ export class CWDComments {
* @param {Object} config - 配置对象
* @param {string|HTMLElement} config.el - 挂载元素选择器或 DOM 元素
* @param {string} config.apiBaseUrl - API 基础地址
* @param {string} config.postSlug - 文章标识符
* @param {string} config.postTitle - 文章标题(可选
* @param {string} config.postUrl - 文章 URL可选
* @param {'light'|'dark'} config.theme - 主题
* @param {number} config.pageSize - 每页评论数
* @param {string} config.avatarPrefix - 头像服务前缀(可选,默认 https://gravatar.com/avatar/
* @param {string} config.adminEmail - 博主邮箱(可选,用于显示博主标识)
* @param {string} config.adminBadge - 博主标识文字(可选,默认"博主"
* @param {'light'|'dark'} [config.theme] - 主题(可选)
* @param {number} [config.pageSize] - 每页评论数(可选,默认 20
*
* 以下字段由组件自动推导或从后端读取,无需通过 config 传入:
* - postSlugwindow.location.origin + window.location.pathname
* - postTitledocument.title 或 postSlug
* - postUrlwindow.location.href
* - avatarPrefix/adminEmail/adminBadge通过 /api/config/comments 接口获取
*/
constructor(config) {
this.config = { ...config };
if (!this.config.postTitle && typeof document !== 'undefined') {
if (typeof window !== 'undefined') {
this.config.postSlug = window.location.origin + window.location.pathname;
}
if (typeof document !== 'undefined') {
this.config.postTitle = document.title || this.config.postSlug;
}
if (!this.config.postUrl && typeof window !== 'undefined') {
if (typeof window !== 'undefined') {
this.config.postUrl = window.location.href;
}
this.hostElement = this._resolveElement(config.el);
@@ -64,6 +67,30 @@ export class CWDComments {
return el;
}
async _loadServerConfig() {
try {
const base = this.config.apiBaseUrl;
if (!base) {
return {};
}
const apiBaseUrl = base.replace(/\/$/, '');
const res = await fetch(`${apiBaseUrl}/api/config/comments`);
if (!res.ok) {
return {};
}
const data = await res.json();
return {
adminEmail: data.adminEmail || '',
adminBadge: data.adminBadge || '',
adminEnabled: !!data.adminEnabled,
avatarPrefix: data.avatarPrefix || ''
};
} catch (e) {
console.warn('[CWDComments] 加载服务端评论配置失败:', e);
return {};
}
}
/**
* 挂载组件
*/
@@ -94,24 +121,35 @@ export class CWDComments {
this.mountPoint.setAttribute('data-theme', this.config.theme);
}
// 创建 API 客户端和 Store
const api = createApiClient(this.config);
this.store = createCommentStore(
this.config,
api.fetchComments.bind(api),
api.submitComment.bind(api)
);
(async () => {
const serverConfig = await this._loadServerConfig();
if (!this._mounted) {
return;
}
if (serverConfig.avatarPrefix) {
this.config.avatarPrefix = serverConfig.avatarPrefix;
}
if (serverConfig.adminEnabled && serverConfig.adminEmail) {
this.config.adminEmail = serverConfig.adminEmail;
}
if (serverConfig.adminEnabled && serverConfig.adminBadge) {
this.config.adminBadge = serverConfig.adminBadge;
}
// 订阅状态变化
this.unsubscribe = this.store.store.subscribe((state) => {
this._onStateChange(state);
});
const api = createApiClient(this.config);
this.store = createCommentStore(
this.config,
api.fetchComments.bind(api),
api.submitComment.bind(api)
);
// 渲染组件
this._render();
this.unsubscribe = this.store.store.subscribe((state) => {
this._onStateChange(state);
});
// 加载评论
this.store.loadComments();
this._render();
this.store.loadComments();
})();
this._mounted = true;
}
@@ -339,35 +377,43 @@ export class CWDComments {
const prevConfig = { ...this.config };
Object.assign(this.config, newConfig);
if (typeof window !== 'undefined') {
this.config.postSlug = window.location.origin + window.location.pathname;
}
if (typeof document !== 'undefined') {
this.config.postTitle = document.title || this.config.postSlug;
}
if (typeof window !== 'undefined') {
this.config.postUrl = window.location.href;
}
// 更新主题
if (newConfig.theme && this.mountPoint) {
this.mountPoint.setAttribute('data-theme', newConfig.theme);
}
// 如果 postSlug 变化,重新加载评论
if (newConfig.postSlug && newConfig.postSlug !== prevConfig.postSlug) {
// 重新创建 API 客户端和 Store
const shouldReload =
this.config.apiBaseUrl !== prevConfig.apiBaseUrl ||
this.config.pageSize !== prevConfig.pageSize ||
this.config.postSlug !== prevConfig.postSlug;
if (shouldReload) {
const api = createApiClient(this.config);
// 取消旧订阅
if (this.unsubscribe) {
this.unsubscribe();
}
// 创建新 store
this.store = createCommentStore(
this.config,
api.fetchComments.bind(api),
api.submitComment.bind(api)
);
// 重新订阅
this.unsubscribe = this.store.store.subscribe((state) => {
this._onStateChange(state);
});
// 加载评论
this.store.loadComments();
}
}

View File

@@ -11,7 +11,6 @@ const STORAGE_KEY = 'cwd-dev-config';
const DEFAULT_CONFIG = {
el: '#comments',
apiBaseUrl: 'http://localhost:8788',
postSlug: window.location.origin + window.location.pathname,
theme: 'light', // 默认 light / dark
};
@@ -48,12 +47,10 @@ function saveConfigToStorage(config) {
*/
function populateInputs(config) {
const apiBaseUrlInput = document.getElementById('apiBaseUrl');
const postSlugInput = document.getElementById('postSlug');
const themeSelect = document.getElementById('theme');
const avatarPrefixInput = document.getElementById('avatarPrefix');
if (apiBaseUrlInput) apiBaseUrlInput.value = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
if (postSlugInput) postSlugInput.value = config.postSlug || DEFAULT_CONFIG.postSlug;
if (themeSelect) themeSelect.value = config.theme || DEFAULT_CONFIG.theme;
if (avatarPrefixInput) avatarPrefixInput.value = config.avatarPrefix || DEFAULT_CONFIG.avatarPrefix;
}
@@ -63,29 +60,8 @@ function populateInputs(config) {
*/
function getConfigFromInputs() {
const apiBaseUrl = document.getElementById('apiBaseUrl')?.value || DEFAULT_CONFIG.apiBaseUrl;
const postSlug = document.getElementById('postSlug')?.value || DEFAULT_CONFIG.postSlug;
const theme = document.getElementById('theme')?.value || DEFAULT_CONFIG.theme;
const avatarPrefix = document.getElementById('avatarPrefix')?.value || DEFAULT_CONFIG.avatarPrefix;
return { apiBaseUrl, postSlug, theme, avatarPrefix };
}
async function loadServerCommentConfig(apiBaseUrl) {
try {
const res = await fetch(`${apiBaseUrl}/api/config/comments`);
if (!res.ok) {
return {};
}
const data = await res.json();
return {
adminEmail: data.adminEmail || '',
adminBadge: data.adminBadge || '',
adminEnabled: !!data.adminEnabled,
avatarPrefix: data.avatarPrefix || '',
};
} catch (e) {
console.warn('[CWDComments] 加载服务端评论配置失败:', e);
return {};
}
return { apiBaseUrl, theme };
}
/**
@@ -111,21 +87,11 @@ async function initWidget() {
// 创建新实例
try {
const serverConfig = await loadServerCommentConfig(config.apiBaseUrl);
widgetInstance = new CWDComments({
el: '#comments',
apiBaseUrl: config.apiBaseUrl,
postSlug: config.postSlug,
theme: config.theme,
avatarPrefix: serverConfig.avatarPrefix || config.avatarPrefix,
pageSize: 20,
...(serverConfig.adminEnabled && serverConfig.adminEmail
? { adminEmail: serverConfig.adminEmail }
: {}),
...(serverConfig.adminEnabled && serverConfig.adminBadge
? { adminBadge: serverConfig.adminBadge }
: {}),
});
widgetInstance.mount();
console.log('[CWDComments] Widget 初始化成功', config);
@@ -193,7 +159,7 @@ document.addEventListener('DOMContentLoaded', () => {
// 监听输入框变化,实时保存
document.addEventListener('DOMContentLoaded', () => {
const inputs = ['apiBaseUrl', 'postSlug', 'theme', 'avatarPrefix'];
const inputs = ['apiBaseUrl', 'theme'];
inputs.forEach((id) => {
const element = document.getElementById(id);
if (element) {

View File

@@ -8,8 +8,7 @@
* <script>
* new CWDComments({
* el: '#comments',
* apiBaseUrl: 'https://api.example.com',
* postSlug: 'my-post'
* apiBaseUrl: 'https://api.example.com'
* }).mount();
* </script>
* ```