feat(admin): 添加评论按域名筛选功能并调整统计页面布局
- 在评论管理页面添加域名筛选功能 - 调整统计页面中卡片顺序,将图表移至概览下方 - 更新文档添加版本更新警告提示 - 修改评论列表API支持域名参数查询
This commit is contained in:
@@ -93,8 +93,13 @@ export function logoutAdmin(): void {
|
|||||||
localStorage.removeItem('cwd_admin_token');
|
localStorage.removeItem('cwd_admin_token');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchComments(page: number): Promise<CommentListResponse> {
|
export function fetchComments(page: number, domain?: string): Promise<CommentListResponse> {
|
||||||
return get<CommentListResponse>(`/admin/comments/list?page=${page}`);
|
const searchParams = new URLSearchParams();
|
||||||
|
searchParams.set('page', String(page));
|
||||||
|
if (domain) {
|
||||||
|
searchParams.set('domain', domain);
|
||||||
|
}
|
||||||
|
return get<CommentListResponse>(`/admin/comments/list?${searchParams.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteComment(id: number): Promise<{ message: string }> {
|
export function deleteComment(id: number): Promise<{ message: string }> {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
<option value="pending">待审核</option>
|
<option value="pending">待审核</option>
|
||||||
<option value="rejected">已拒绝</option>
|
<option value="rejected">已拒绝</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select v-model="domainFilter" class="toolbar-select">
|
||||||
|
<option value="">全部域名</option>
|
||||||
|
<option v-for="item in domainOptions" :key="item" :value="item">
|
||||||
|
{{ item }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<button class="toolbar-button" @click="loadComments">刷新</button>
|
<button class="toolbar-button" @click="loadComments">刷新</button>
|
||||||
@@ -191,6 +197,7 @@ import {
|
|||||||
CommentItem,
|
CommentItem,
|
||||||
CommentListResponse,
|
CommentListResponse,
|
||||||
fetchComments,
|
fetchComments,
|
||||||
|
fetchCommentStats,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
updateCommentStatus,
|
updateCommentStatus,
|
||||||
blockIp,
|
blockIp,
|
||||||
@@ -205,8 +212,11 @@ const pagination = ref<{ page: number; total: number }>({ page: 1, total: 1 });
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
const statusFilter = ref("");
|
const statusFilter = ref("");
|
||||||
|
const domainFilter = ref("");
|
||||||
const jumpPageInput = ref("");
|
const jumpPageInput = ref("");
|
||||||
|
|
||||||
|
const domainOptions = ref<string[]>([]);
|
||||||
|
|
||||||
const filteredComments = computed(() => {
|
const filteredComments = computed(() => {
|
||||||
if (!statusFilter.value) {
|
if (!statusFilter.value) {
|
||||||
return comments.value;
|
return comments.value;
|
||||||
@@ -263,7 +273,7 @@ async function loadComments(page?: number) {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = "";
|
error.value = "";
|
||||||
try {
|
try {
|
||||||
const res = await fetchComments(targetPage);
|
const res = await fetchComments(targetPage, domainFilter.value || undefined);
|
||||||
comments.value = res.data;
|
comments.value = res.data;
|
||||||
pagination.value = { page: res.pagination.page, total: res.pagination.total };
|
pagination.value = { page: res.pagination.page, total: res.pagination.total };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -280,6 +290,11 @@ function updateRoutePage(page: number) {
|
|||||||
} else {
|
} else {
|
||||||
query.p = String(page);
|
query.p = String(page);
|
||||||
}
|
}
|
||||||
|
if (domainFilter.value) {
|
||||||
|
query.domain = domainFilter.value;
|
||||||
|
} else {
|
||||||
|
delete query.domain;
|
||||||
|
}
|
||||||
router.push({ query });
|
router.push({ query });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,6 +385,20 @@ onMounted(() => {
|
|||||||
initialPage = Math.floor(value);
|
initialPage = Math.floor(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const d = route.query.domain;
|
||||||
|
if (typeof d === "string" && d.trim()) {
|
||||||
|
domainFilter.value = d.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCommentStats()
|
||||||
|
.then((res: CommentListResponse | any) => {
|
||||||
|
const domains = Array.isArray(res.domains) ? res.domains.map((item: any) => item.domain) : [];
|
||||||
|
domainOptions.value = Array.from(new Set(domains));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
domainOptions.value = [];
|
||||||
|
});
|
||||||
|
|
||||||
loadComments(initialPage);
|
loadComments(initialPage);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,15 +9,6 @@
|
|||||||
{{ toastMessage }}
|
{{ toastMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3 class="card-title">最近 7 天评论数趋势</h3>
|
|
||||||
<div v-if="statsLoading" class="page-hint">加载中...</div>
|
|
||||||
<div v-else-if="statsError" class="page-error">{{ statsError }}</div>
|
|
||||||
<div v-else class="chart-wrapper">
|
|
||||||
<div ref="chartEl" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-title">整体概览</h3>
|
<h3 class="card-title">整体概览</h3>
|
||||||
<div v-if="statsLoading" class="page-hint">加载中...</div>
|
<div v-if="statsLoading" class="page-hint">加载中...</div>
|
||||||
@@ -30,7 +21,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stats-item">
|
<div class="stats-item">
|
||||||
<div class="stats-label">已通过</div>
|
<div class="stats-label">已通过</div>
|
||||||
<div class="stats-value stats-value-approved">{{ statsSummary.approved }}</div>
|
<div class="stats-value stats-value-approved">
|
||||||
|
{{ statsSummary.approved }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-item">
|
<div class="stats-item">
|
||||||
<div class="stats-label">待审核</div>
|
<div class="stats-label">待审核</div>
|
||||||
@@ -38,12 +31,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stats-item">
|
<div class="stats-item">
|
||||||
<div class="stats-label">已拒绝</div>
|
<div class="stats-label">已拒绝</div>
|
||||||
<div class="stats-value stats-value-rejected">{{ statsSummary.rejected }}</div>
|
<div class="stats-value stats-value-rejected">
|
||||||
|
{{ statsSummary.rejected }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-title">最近 7 天评论数趋势</h3>
|
||||||
|
<div v-if="statsLoading" class="page-hint">加载中...</div>
|
||||||
|
<div v-else-if="statsError" class="page-error">{{ statsError }}</div>
|
||||||
|
<div v-else class="chart-wrapper">
|
||||||
|
<div ref="chartEl" class="chart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-title">按域名统计</h3>
|
<h3 class="card-title">按域名统计</h3>
|
||||||
<div v-if="statsLoading" class="page-hint">加载中...</div>
|
<div v-if="statsLoading" class="page-hint">加载中...</div>
|
||||||
|
|||||||
@@ -9,14 +9,27 @@ export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
const limit = 10;
|
const limit = 10;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const rawDomain = c.req.query('domain') || '';
|
||||||
|
const domain = rawDomain.trim();
|
||||||
|
|
||||||
|
let whereSql = '';
|
||||||
|
const params: (string | number)[] = [];
|
||||||
|
if (domain) {
|
||||||
|
const pattern = `%://${domain}/%`;
|
||||||
|
whereSql = 'WHERE post_slug LIKE ? OR url LIKE ?';
|
||||||
|
params.push(pattern, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
const totalCount = await c.env.CWD_DB.prepare(
|
const totalCount = await c.env.CWD_DB.prepare(
|
||||||
'SELECT COUNT(*) as count FROM Comment'
|
`SELECT COUNT(*) as count FROM Comment ${whereSql}`
|
||||||
).first<{ count: number }>();
|
)
|
||||||
|
.bind(...params)
|
||||||
|
.first<{ count: number }>();
|
||||||
|
|
||||||
const { results } = await c.env.CWD_DB.prepare(
|
const { results } = await c.env.CWD_DB.prepare(
|
||||||
'SELECT * FROM Comment ORDER BY created DESC LIMIT ? OFFSET ?'
|
`SELECT * FROM Comment ${whereSql} ORDER BY created DESC LIMIT ? OFFSET ?`
|
||||||
)
|
)
|
||||||
.bind(limit, offset)
|
.bind(...params, limit, offset)
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
await c.env.CWD_DB.prepare(
|
await c.env.CWD_DB.prepare(
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ npm run deploy
|
|||||||
|
|
||||||
如果使用官方后台不需要更新。
|
如果使用官方后台不需要更新。
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 存在一个弊端,如果官方后台版本有了大更新,而你部署的 api 端没有及时更新部署,可能会导致管理后台无法正常使用(无法获取到最新的接口),正常情况下无需担心。
|
||||||
|
|
||||||
```
|
```
|
||||||
cd cwd-admin
|
cd cwd-admin
|
||||||
npm install
|
npm install
|
||||||
|
|||||||
Reference in New Issue
Block a user