feat(评论系统): 添加评论配置和头像显示功能

- 新增评论配置管理功能,包括博主邮箱、标签和头像前缀设置
- 在评论列表显示用户头像
- 实现服务端配置存储和获取逻辑
- 优化前端设置页面,拆分不同配置项
- 修改开发预览页面配置保存逻辑
This commit is contained in:
anghunk
2026-01-20 09:23:46 +08:00
parent fe463125ea
commit 456771bae3
8 changed files with 304 additions and 66 deletions

View File

@@ -11,6 +11,7 @@ export type CommentItem = {
pubDate: string;
author: string;
email: string;
avatar: string;
postSlug: string;
url: string | null;
ipAddress: string | null;
@@ -32,6 +33,12 @@ export type AdminEmailResponse = {
email: string | null;
};
export type CommentSettingsResponse = {
adminEmail: string | null;
adminBadge: string | null;
avatarPrefix: string | null;
};
export async function loginAdmin(name: string, password: string): Promise<string> {
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
const key = res.data.key;
@@ -63,3 +70,14 @@ export function saveAdminEmail(email: string): Promise<{ message: string }> {
return put<{ message: string }>('/admin/settings/email', { email });
}
export function fetchCommentSettings(): Promise<CommentSettingsResponse> {
return get<CommentSettingsResponse>('/admin/settings/comments');
}
export function saveCommentSettings(data: {
adminEmail?: string;
adminBadge?: string;
avatarPrefix?: string;
}): Promise<{ message: string }> {
return put<{ message: string }>('/admin/settings/comments', data);
}

View File

@@ -27,9 +27,19 @@
</div>
<div v-for="item in filteredComments" :key="item.id" class="table-row">
<div class="table-cell table-cell-author">
<div class="cell-author-name">{{ item.author }}</div>
<div class="cell-author-email">{{ item.email }}</div>
<span class="cell-time">{{ formatDate(item.pubDate) }}</span>
<div class="cell-author-wrapper">
<img
v-if="item.avatar"
:src="item.avatar"
class="cell-avatar"
:alt="item.author"
/>
<div class="cell-author-main">
<div class="cell-author-name">{{ item.author }}</div>
<div class="cell-author-email">{{ item.email }}</div>
<span class="cell-time">{{ formatDate(item.pubDate) }}</span>
</div>
</div>
</div>
<div class="table-cell table-cell-content">
<div class="cell-content-text">{{ item.contentText }}</div>
@@ -291,8 +301,6 @@ onMounted(() => {
.table-cell-author {
width: 180px;
flex-direction: column;
align-items: flex-start !important;
flex-shrink: 0;
}
@@ -369,6 +377,24 @@ onMounted(() => {
color: #57606a;
}
.cell-author-wrapper {
display: flex;
align-items: flex-start;
gap: 8px;
}
.cell-author-main {
display: flex;
flex-direction: column;
}
.cell-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
flex-shrink: 0;
}
.cell-status {
padding: 3px 8px;
border-radius: 999px;

View File

@@ -19,8 +19,30 @@
{{ message }}
</div>
<div class="card-actions">
<button class="card-button" :disabled="saving" @click="save">
<span v-if="saving">保存中...</span>
<button class="card-button" :disabled="savingEmail" @click="saveEmail">
<span v-if="savingEmail">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
<div class="card">
<h3 class="card-title">评论显示配置</h3>
<div class="form-item">
<label class="form-label">评论博主邮箱用于前台标记</label>
<input v-model="commentAdminEmail" class="form-input" type="email" />
</div>
<div class="form-item">
<label class="form-label">博主标签文字</label>
<input v-model="commentAdminBadge" class="form-input" type="text" />
</div>
<div class="form-item">
<label class="form-label">头像前缀Gravatar/Cravatar</label>
<input v-model="avatarPrefix" class="form-input" type="text" />
</div>
<div class="card-actions">
<button class="card-button" :disabled="savingComment" @click="saveComment">
<span v-if="savingComment">保存中...</span>
<span v-else>保存</span>
</button>
</div>
@@ -31,10 +53,19 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { fetchAdminEmail, saveAdminEmail } from "../api/admin";
import {
fetchAdminEmail,
saveAdminEmail,
fetchCommentSettings,
saveCommentSettings,
} from "../api/admin";
const email = ref("");
const saving = ref(false);
const commentAdminEmail = ref("");
const commentAdminBadge = ref("");
const avatarPrefix = ref("");
const savingEmail = ref(false);
const savingComment = ref(false);
const loading = ref(false);
const message = ref("");
const messageType = ref<"success" | "error">("success");
@@ -42,8 +73,14 @@ const messageType = ref<"success" | "error">("success");
async function load() {
loading.value = true;
try {
const res = await fetchAdminEmail();
email.value = res.email || "";
const [notifyRes, commentRes] = await Promise.all([
fetchAdminEmail(),
fetchCommentSettings(),
]);
email.value = notifyRes.email || "";
commentAdminEmail.value = commentRes.adminEmail || "";
commentAdminBadge.value = commentRes.adminBadge || "博主";
avatarPrefix.value = commentRes.avatarPrefix || "";
} catch (e: any) {
message.value = e.message || "加载失败";
messageType.value = "error";
@@ -52,13 +89,13 @@ async function load() {
}
}
async function save() {
async function saveEmail() {
if (!email.value) {
message.value = "请输入邮箱";
messageType.value = "error";
return;
}
saving.value = true;
savingEmail.value = true;
message.value = "";
try {
const res = await saveAdminEmail(email.value);
@@ -68,7 +105,26 @@ async function save() {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
saving.value = false;
savingEmail.value = false;
}
}
async function saveComment() {
savingComment.value = true;
message.value = "";
try {
const res = await saveCommentSettings({
adminEmail: commentAdminEmail.value,
adminBadge: commentAdminBadge.value,
avatarPrefix: avatarPrefix.value,
});
message.value = res.message || "保存成功";
messageType.value = "success";
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingComment.value = false;
}
}