feat(评论): 新增功能开关管理及评论点赞功能优化
- 在管理后台添加功能开关页面,支持控制评论点赞和文章点赞功能的开启/关闭 - 优化评论点赞功能,支持取消点赞和本地存储点赞状态 - 更新点赞UI样式,提升用户体验 - 添加相关API接口和文档说明
This commit is contained in:
@@ -123,6 +123,11 @@ export type LikeStatsResponse = {
|
||||
items: LikeStatsItem[];
|
||||
};
|
||||
|
||||
export type FeatureSettingsResponse = {
|
||||
enableCommentLike: boolean;
|
||||
enableArticleLike: boolean;
|
||||
};
|
||||
|
||||
export async function loginAdmin(name: string, password: string): Promise<string> {
|
||||
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
|
||||
const key = res.data.key;
|
||||
@@ -289,3 +294,14 @@ export function fetchDomainList(): Promise<DomainListResponse> {
|
||||
export function fetchLikeStats(): Promise<LikeStatsResponse> {
|
||||
return get<LikeStatsResponse>('/admin/likes/stats');
|
||||
}
|
||||
|
||||
export function fetchFeatureSettings(): Promise<FeatureSettingsResponse> {
|
||||
return get<FeatureSettingsResponse>('/admin/settings/features');
|
||||
}
|
||||
|
||||
export function saveFeatureSettings(data: {
|
||||
enableCommentLike?: boolean;
|
||||
enableArticleLike?: boolean;
|
||||
}): Promise<{ message: string }> {
|
||||
return put<{ message: string }>('/admin/settings/features', data);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import LoginView from '../views/LoginView.vue';
|
||||
import LayoutView from '../views/LayoutView.vue';
|
||||
import CommentsView from '../views/CommentsView.vue';
|
||||
import SettingsView from '../views/SettingsView.vue';
|
||||
import FeatureSettingsView from '../views/FeatureSettingsView.vue';
|
||||
import DataView from '../views/DataView.vue';
|
||||
import StatsView from '../views/StatsView.vue';
|
||||
import AnalyticsVisitView from '../views/AnalyticsVisitView.vue';
|
||||
@@ -41,6 +42,11 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'settings',
|
||||
component: SettingsView
|
||||
},
|
||||
{
|
||||
path: 'settings/features',
|
||||
name: 'feature-settings',
|
||||
component: FeatureSettingsView
|
||||
},
|
||||
{
|
||||
path: 'data',
|
||||
name: 'data',
|
||||
|
||||
262
cwd-admin/src/views/FeatureSettingsView.vue
Normal file
262
cwd-admin/src/views/FeatureSettingsView.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h2 class="page-title">评论功能开关</h2>
|
||||
<div
|
||||
v-if="toastVisible"
|
||||
class="toast"
|
||||
:class="toastType === 'error' ? 'toast-error' : 'toast-success'"
|
||||
>
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
<div v-if="loading" class="page-hint">加载中...</div>
|
||||
<div v-else>
|
||||
<div class="card">
|
||||
<h3 class="card-title">显示功能设置</h3>
|
||||
|
||||
<div class="form-item">
|
||||
<label class="form-label">开启文章点赞功能</label>
|
||||
<label class="switch">
|
||||
<input v-model="enableArticleLike" type="checkbox" />
|
||||
<span class="slider" />
|
||||
</label>
|
||||
<div class="form-hint">开启后,评论区顶部会显示文章点赞(喜欢)按钮。</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label class="form-label">开启评论点赞功能</label>
|
||||
<label class="switch">
|
||||
<input v-model="enableCommentLike" type="checkbox" />
|
||||
<span class="slider" />
|
||||
</label>
|
||||
<div class="form-hint">开启后,评论列表中的每条评论都会显示点赞按钮。</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message && messageType === 'error'"
|
||||
class="form-message form-message-error"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button class="card-button" :disabled="saving" @click="save">
|
||||
<span v-if="saving">保存中...</span>
|
||||
<span v-else>保存</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { fetchFeatureSettings, saveFeatureSettings } from "../api/admin";
|
||||
|
||||
const enableCommentLike = ref(true);
|
||||
const enableArticleLike = ref(true);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const message = ref("");
|
||||
const messageType = ref<"success" | "error">("success");
|
||||
const toastMessage = ref("");
|
||||
const toastType = ref<"success" | "error">("success");
|
||||
const toastVisible = ref(false);
|
||||
|
||||
function showToast(msg: string, type: "success" | "error" = "success") {
|
||||
toastMessage.value = msg;
|
||||
toastType.value = type;
|
||||
toastVisible.value = true;
|
||||
window.setTimeout(() => {
|
||||
toastVisible.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetchFeatureSettings();
|
||||
enableCommentLike.value = res.enableCommentLike;
|
||||
enableArticleLike.value = res.enableArticleLike;
|
||||
} catch (e: any) {
|
||||
message.value = e.message || "加载失败";
|
||||
messageType.value = "error";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
message.value = "";
|
||||
try {
|
||||
const res = await saveFeatureSettings({
|
||||
enableCommentLike: enableCommentLike.value,
|
||||
enableArticleLike: enableArticleLike.value,
|
||||
});
|
||||
showToast(res.message || "保存成功", "success");
|
||||
} catch (e: any) {
|
||||
message.value = e.message || "保存失败";
|
||||
messageType.value = "error";
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d0d7de;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #1a7f37;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #d1242f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #d0d7de;
|
||||
transition: 0.2s;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
background-color: #ffffff;
|
||||
transition: 0.2s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px rgba(27, 31, 36, 0.15);
|
||||
}
|
||||
|
||||
.switch input:checked + .slider {
|
||||
background-color: #0969da;
|
||||
}
|
||||
|
||||
.switch input:checked + .slider::before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.card-button {
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background-color: #0969da;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.card-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.form-message {
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-message-error {
|
||||
color: #d1242f;
|
||||
}
|
||||
|
||||
.page-hint {
|
||||
font-size: 14px;
|
||||
color: #57606a;
|
||||
}
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #57606a;
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -97,6 +97,13 @@
|
||||
>
|
||||
网站设置
|
||||
</li>
|
||||
<li
|
||||
class="menu-item"
|
||||
:class="{ active: isRouteActive('feature-settings') }"
|
||||
@click="goFeatureSettings"
|
||||
>
|
||||
功能开关
|
||||
</li>
|
||||
<li
|
||||
class="menu-item"
|
||||
:class="{ active: isRouteActive('data') }"
|
||||
@@ -205,6 +212,11 @@ function goSettings() {
|
||||
closeSider();
|
||||
}
|
||||
|
||||
function goFeatureSettings() {
|
||||
router.push({ name: "feature-settings" });
|
||||
closeSider();
|
||||
}
|
||||
|
||||
function openDocs() {
|
||||
window.open("https://cwd.js.org", "_blank");
|
||||
closeActions();
|
||||
|
||||
Reference in New Issue
Block a user