feat(i18n): 为评论系统添加国际化支持

- 在管理后台引入 vue-i18n 并添加多语言文件
- 为前端评论组件添加翻译功能,支持自动语言检测
- 在功能设置中新增管理后台和组件语言配置选项
- 更新 API 接口以支持语言设置存储
- 优化菜单项文本溢出显示样式
This commit is contained in:
anghunk
2026-02-10 14:26:38 +08:00
parent 860830cee2
commit 4d9e2d8d31
39 changed files with 6023 additions and 332 deletions

View File

@@ -1,42 +1,42 @@
<template>
<div v-if="visible" class="modal-overlay">
<div class="modal">
<h3 class="modal-title">编辑评论</h3>
<h3 class="modal-title">{{ t('comments.editModal.title') }}</h3>
<div v-if="form" class="modal-body">
<div class="form-item">
<label class="form-label">访客昵称</label>
<label class="form-label">{{ t('comments.editModal.name') }}</label>
<input v-model="form.name" class="form-input" type="text" />
</div>
<div class="form-item">
<label class="form-label">访客邮箱</label>
<label class="form-label">{{ t('comments.editModal.email') }}</label>
<input v-model="form.email" class="form-input" type="email" />
</div>
<div class="form-item">
<label class="form-label">访客网址</label>
<label class="form-label">{{ t('comments.editModal.url') }}</label>
<input v-model="form.url" class="form-input" type="text" />
</div>
<div class="form-item">
<label class="form-label">页面标识</label>
<label class="form-label">{{ t('comments.editModal.postSlug') }}</label>
<input v-model="form.postSlug" class="form-input" type="text" />
</div>
<div class="form-item">
<label class="form-label">评论地址</label>
<label class="form-label">{{ t('comments.editModal.postUrl') }}</label>
<input v-model="form.postUrl" class="form-input" type="text" />
</div>
<div class="form-item">
<label class="form-label">评论内容</label>
<label class="form-label">{{ t('comments.editModal.content') }}</label>
<textarea v-model="form.contentText" class="form-input" rows="4"></textarea>
</div>
<div class="form-item">
<label class="form-label">评论状态</label>
<label class="form-label">{{ t('comments.editModal.status') }}</label>
<select v-model="form.status" class="form-input">
<option value="approved">已通过</option>
<option value="pending">待审核</option>
<option value="rejected">已拒绝</option>
<option value="approved">{{ t('comments.statusFilter.approved') }}</option>
<option value="pending">{{ t('comments.statusFilter.pending') }}</option>
<option value="rejected">{{ t('comments.statusFilter.rejected') }}</option>
</select>
</div>
<div class="form-item">
<label class="form-label">置顶权重1 为不置顶数值越大越靠前</label>
<label class="form-label">{{ t('comments.editModal.priority') }}</label>
<input
v-model.number="form.priority"
class="form-input"
@@ -47,7 +47,7 @@
</div>
<div class="modal-actions">
<button class="modal-btn secondary" type="button" @click="handleClose">
取消
{{ t('comments.editModal.cancel') }}
</button>
<button
class="modal-btn primary"
@@ -55,8 +55,8 @@
:disabled="saving"
@click="handleSubmit"
>
<span v-if="saving">保存中...</span>
<span v-else>保存</span>
<span v-if="saving">{{ t('comments.editModal.saving') }}</span>
<span v-else>{{ t('comments.editModal.save') }}</span>
</button>
</div>
</div>
@@ -64,6 +64,10 @@
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
interface EditForm {
id: number;
name: string;

View File

@@ -1,29 +1,29 @@
<template>
<div class="page">
<h2 class="page-title">评论管理</h2>
<h2 class="page-title">{{ t('comments.title') }}</h2>
<div class="toolbar">
<div class="toolbar-left">
<select v-model="statusFilter" class="toolbar-select">
<option value="">全部状态</option>
<option value="approved">已通过</option>
<option value="pending">待审核</option>
<option value="rejected">已拒绝</option>
<option value="">{{ t('comments.statusFilter.all') }}</option>
<option value="approved">{{ t('comments.statusFilter.approved') }}</option>
<option value="pending">{{ t('comments.statusFilter.pending') }}</option>
<option value="rejected">{{ t('comments.statusFilter.rejected') }}</option>
</select>
</div>
<div class="toolbar-right">
<button class="toolbar-button" @click="goPage(1)">刷新</button>
<button class="toolbar-button" @click="goPage(1)">{{ t('comments.refresh') }}</button>
</div>
</div>
<div v-if="loading" class="page-hint">加载中...</div>
<div v-if="loading" class="page-hint">{{ t('common.loading') }}</div>
<div v-else-if="error" class="page-error">{{ error }}</div>
<div v-else>
<div class="comment-table">
<div class="table-header">
<div class="table-cell table-cell-author">用户</div>
<div class="table-cell table-cell-content">评论信息</div>
<div class="table-cell table-cell-path">评论地址</div>
<div class="table-cell table-cell-status">状态</div>
<div class="table-cell table-cell-actions">操作</div>
<div class="table-cell table-cell-author">{{ t('comments.table.author') }}</div>
<div class="table-cell table-cell-content">{{ t('comments.table.content') }}</div>
<div class="table-cell table-cell-path">{{ t('comments.table.path') }}</div>
<div class="table-cell table-cell-status">{{ t('comments.table.status') }}</div>
<div class="table-cell table-cell-actions">{{ t('comments.table.actions') }}</div>
</div>
<div v-for="item in filteredComments" :key="item.id" class="table-row">
<div class="table-cell table-cell-author">
@@ -40,7 +40,7 @@
<span v-if="item.isAdmin" class="cell-admin-tag">
<svg
viewBox="0 0 22 22"
aria-label="网站管理员"
:aria-label="t('comments.table.admin')"
role="img"
class="cwd-admin-icon"
style="
@@ -63,7 +63,7 @@
<span
class="cell-email-text"
@click="handleBlockEmail(item)"
title="屏蔽该邮箱"
:title="t('comments.actions.blockEmail')"
>
{{ item.email }}
</span>
@@ -73,7 +73,7 @@
<span
class="cell-ip-text"
@click="handleBlockIp(item)"
title="屏蔽该 IP"
:title="t('comments.actions.blockIp')"
>{{ item.ipAddress }}</span
>
</div>
@@ -104,7 +104,7 @@
:title="String(item.priority)"
class="cell-status cell-pin-flag"
>
置顶
{{ t('comments.actions.pin') }}
</span>
<span class="cell-status cell-likes-number" v-if="item.likes !== 0">
<PhThumbsUp :size="13" />
@@ -126,21 +126,21 @@
:value="item.status"
@change="handleStatusChange(item, $event)"
>
<option value="approved">通过</option>
<option value="pending">待审</option>
<option value="rejected">拒绝</option>
<option value="approved">{{ t('comments.actions.approve') }}</option>
<option value="pending">{{ t('comments.actions.pending') }}</option>
<option value="rejected">{{ t('comments.actions.reject') }}</option>
</select>
<button class="table-action" @click="openEdit(item)">编辑</button>
<button class="table-action" @click="openEdit(item)">{{ t('comments.actions.edit') }}</button>
<button
class="table-action table-action-danger"
@click="removeComment(item)"
>
删除
{{ t('comments.actions.delete') }}
</button>
</div>
</div>
</div>
<div v-if="filteredComments.length === 0" class="table-empty">暂无数据</div>
<div v-if="filteredComments.length === 0" class="table-empty">{{ t('comments.empty') }}</div>
</div>
<div v-if="pagination.total > 1" class="pagination">
<button
@@ -148,7 +148,7 @@
:disabled="pagination.page <= 1"
@click="goPage(pagination.page - 1)"
>
上一页
{{ t('comments.pagination.prev') }}
</button>
<button
class="pagination-button"
@@ -199,10 +199,10 @@
:disabled="pagination.page >= pagination.total"
@click="goPage(pagination.page + 1)"
>
下一页
{{ t('comments.pagination.next') }}
</button>
<div class="pagination-jump">
<span>跳转到</span>
<span>{{ t('comments.pagination.jumpTo') }}</span>
<input
v-model="jumpPageInput"
class="pagination-input"
@@ -211,8 +211,8 @@
:max="pagination.total"
@keyup.enter="handleJumpPage"
/>
<span></span>
<button class="pagination-button" @click="handleJumpPage">确定</button>
<span>{{ t('comments.pagination.page') }}</span>
<button class="pagination-button" @click="handleJumpPage">{{ t('comments.pagination.confirm') }}</button>
</div>
</div>
</div>
@@ -231,6 +231,10 @@ import "../../styles/markdown.css";
import { onMounted, ref, computed, watch, inject } from "vue";
import type { Ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { marked } from "marked";
import DOMPurify from "dompurify";
import ModalEdit from "./components/ModalEdit.vue";
import {
CommentItem,
CommentListResponse,
@@ -241,12 +245,13 @@ import {
blockIp,
blockEmail,
} from "../../api/admin";
import ModalEdit from "./components/ModalEdit.vue";
import { useSite } from "../../composables/useSite";
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
// ... existing refs ...
const comments = ref<CommentItem[]>([]);
const pagination = ref<{ page: number; total: number }>({ page: 1, total: 1 });
const loading = ref(false);
@@ -268,6 +273,11 @@ const editForm = ref<{
priority: number;
} | null>(null);
const searchKeyword = ref(""); // Missing ref in original code snippet but used in template?
// Wait, I saw searchKeyword in original code snippet. I must ensure I don't lose it.
// I will just use the replace string for the specific parts if possible, but I am replacing whole template.
// I need to be careful with script content.
const filteredComments = computed(() => {
if (!statusFilter.value) {
return comments.value;
@@ -275,6 +285,7 @@ const filteredComments = computed(() => {
return comments.value.filter((item) => item.status === statusFilter.value);
});
// ... visiblePages computed ...
const visiblePages = computed(() => {
const total = pagination.value.total;
const current = pagination.value.page;
@@ -308,17 +319,18 @@ function formatDate(value: number) {
function formatStatus(status: string) {
if (status === "approved") {
return "已通过";
return t('comments.statusFilter.approved');
}
if (status === "pending") {
return "待审核";
return t('comments.statusFilter.pending');
}
if (status === "rejected") {
return "已拒绝";
return t('comments.statusFilter.rejected');
}
return status;
}
// ... other functions ...
async function loadComments(page?: number) {
const targetPage = typeof page === "number" ? page : 1;
loading.value = true;
@@ -341,9 +353,7 @@ function updateRoutePage(page: number) {
} else {
query.p = String(page);
}
// Removed domain from query as it's now global state
delete query.domain;
router.push({ query });
}
@@ -388,7 +398,7 @@ function handleStatusChange(item: CommentItem, event: Event) {
}
async function removeComment(item: CommentItem) {
if (!window.confirm(`确认删除评论 ${item.id} 吗`)) {
if (!window.confirm(t('comments.confirmDelete', { id: item.id }))) {
return;
}
try {
@@ -403,12 +413,12 @@ async function handleBlockIp(item: CommentItem) {
if (!item.ipAddress) {
return;
}
if (!window.confirm(`确认将 IP ${item.ipAddress} 加入黑名单吗?`)) {
if (!window.confirm(t('comments.confirmBlockIp', { ip: item.ipAddress }))) {
return;
}
try {
const res = await blockIp(item.ipAddress);
window.alert(res.message || "已加入 IP 黑名单");
window.alert(res.message || t('comments.successBlockIp'));
} catch (e: any) {
error.value = e.message || "屏蔽 IP 失败";
}
@@ -418,12 +428,12 @@ async function handleBlockEmail(item: CommentItem) {
if (!item.email) {
return;
}
if (!window.confirm(`确认将邮箱 ${item.email} 加入黑名单吗?`)) {
if (!window.confirm(t('comments.confirmBlockEmail', { email: item.email }))) {
return;
}
try {
const res = await blockEmail(item.email);
window.alert(res.message || "已加入邮箱黑名单");
window.alert(res.message || t('comments.successBlockEmail'));
} catch (e: any) {
error.value = e.message || "屏蔽邮箱失败";
}
@@ -535,6 +545,7 @@ watch(currentSiteId, () => {
updateRoutePage(1);
loadComments(1);
});
</script>
<style scoped lang="less">