feat(i18n): 为评论系统添加国际化支持
- 在管理后台引入 vue-i18n 并添加多语言文件 - 为前端评论组件添加翻译功能,支持自动语言检测 - 在功能设置中新增管理后台和组件语言配置选项 - 更新 API 接口以支持语言设置存储 - 优化菜单项文本溢出显示样式
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user