feat(comments): 添加 Waline 自定义样式文件以适配网站主题

fix(blog): 删除不再使用的博客工具函数文件
This commit is contained in:
joyzhao
2026-01-12 14:36:11 +08:00
parent 78c468a7d2
commit b8a8cc474b
3 changed files with 343 additions and 309 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
import type { WalineInstance, WalineInitOptions } from '@waline/client';
import { init } from '@waline/client';
import '@waline/client/style';
import '@/styles/waline-custom.css';
import type { Lang } from '@/types/i18n';

View File

@@ -0,0 +1,342 @@
/**
* Waline Custom Styles
* 适配网站主题风格 - 紫色渐变主题
*/
/* ========== 基础变量覆盖 ========== */
:root {
/* 主题色 - 使用网站的紫色渐变accent */
--waline-theme-color: #8B5CF6;
--waline-active-color: #EC4899;
/* 背景色 */
--waline-bg-color: oklch(1 0 0);
--waline-bg-color-light: oklch(0.97 0 0);
--waline-bg-color-hover: oklch(0.93 0 0);
/* 文字颜色 */
--waline-color: oklch(0.4 0 0);
--waline-light-grey: oklch(0.65 0 0);
--waline-dark-grey: oklch(0.3 0 0);
/* 边框颜色 */
--waline-border-color: oklch(0.87 0 0);
/* 其他颜色 */
--waline-badge-color: #8B5CF6;
--waline-info-bg-color: oklch(0.97 0 0);
--waline-info-color: oklch(0.55 0 0);
--waline-bq-color: oklch(0.93 0 0);
/* 代码块背景 */
--waline-code-bg-color: oklch(0.25 0 0);
/* 圆角 - 与网站一致 */
--waline-border-radius: var(--radius, 0.75rem);
/* 阴影 */
--waline-box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));
/* 头像圆角 - 使用网站风格 */
--waline-avatar-radius: var(--radius-md, 0.5rem);
/* 按钮样式 */
--waline-btn-radius: var(--radius, 0.75rem);
}
/* ========== 暗黑模式 ========== */
.dark {
--waline-bg-color: oklch(0.205 0 0);
--waline-bg-color-light: oklch(0.269 0 0);
--waline-bg-color-hover: oklch(0.32 0 0);
--waline-color: oklch(0.9 0 0);
--waline-light-grey: oklch(0.6 0 0);
--waline-dark-grey: oklch(0.75 0 0);
--waline-border-color: oklch(1 0 0 / 15%);
--waline-info-bg-color: oklch(0.269 0 0);
--waline-info-color: oklch(0.6 0 0);
--waline-bq-color: oklch(0.269 0 0);
--waline-code-bg-color: oklch(0.12 0 0);
--waline-box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.25));
}
/* ========== 评论面板样式 ========== */
.wl-panel {
border-radius: var(--waline-border-radius) !important;
border: 1px solid var(--waline-border-color) !important;
box-shadow: var(--waline-box-shadow) !important;
background: var(--waline-bg-color) !important;
}
/* ========== 输入框样式 ========== */
.wl-header {
border-bottom-color: var(--waline-border-color) !important;
}
.wl-header label {
color: var(--waline-color) !important;
}
.wl-header input {
background: transparent !important;
}
.wl-editor {
background: var(--waline-bg-color-light) !important;
border-radius: var(--waline-btn-radius) !important;
}
.wl-editor:focus {
background: var(--waline-bg-color-hover) !important;
}
/* ========== 按钮样式 ========== */
.wl-btn {
border-radius: var(--waline-btn-radius) !important;
font-size: 0.875em !important;
transition: all 0.2s ease !important;
}
.wl-btn:hover {
border-color: var(--waline-theme-color) !important;
color: var(--waline-theme-color) !important;
background: var(--waline-bg-color-light) !important;
}
.wl-btn.primary {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
border: none !important;
color: white !important;
font-weight: 500 !important;
}
.wl-btn.primary:hover {
background: linear-gradient(135deg, #7C3AED, #DB2777) !important;
border: none !important;
color: white !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
/* ========== 头部标签页 ========== */
.wl-header label {
transition: all 0.2s ease !important;
}
.wl-header label:hover {
background: var(--waline-bg-color-light) !important;
}
/* ========== 评论卡片样式 ========== */
.wl-card {
border-bottom-color: var(--waline-border-color) !important;
}
.wl-card:first-child {
margin-inline-start: 0 !important;
}
.wl-nick {
color: var(--waline-color) !important;
}
.wl-time {
color: var(--waline-info-color) !important;
}
.wl-content {
color: var(--waline-color) !important;
line-height: 1.8 !important;
}
/* ========== 徽章样式 ========== */
.wl-badge {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
border: none !important;
color: white !important;
font-size: 0.7em !important;
padding: 2px 8px !important;
border-radius: 9999px !important;
}
/* ========== 操作按钮 ========== */
.wl-action {
color: var(--waline-light-grey) !important;
transition: all 0.2s ease !important;
}
.wl-action:hover {
color: var(--waline-theme-color) !important;
transform: scale(1.1);
}
.wl-action.active {
color: var(--waline-active-color) !important;
}
/* ========== 评论操作 ========== */
.wl-delete:hover,
.wl-like:hover,
.wl-reply:hover,
.wl-edit:hover {
color: var(--waline-theme-color) !important;
}
.wl-delete.active,
.wl-like.active,
.wl-reply.active,
.wl-edit.active {
color: var(--waline-active-color) !important;
}
/* ========== 排序按钮 ========== */
.wl-sort li {
color: var(--waline-info-color) !important;
transition: all 0.2s ease !important;
}
.wl-sort li:hover {
color: var(--waline-theme-color) !important;
}
.wl-sort li.active {
color: var(--waline-theme-color) !important;
font-weight: 600;
}
/* ========== 表情弹窗 ========== */
.wl-emoji-popup {
border-radius: var(--waline-border-radius) !important;
box-shadow: var(--waline-box-shadow) !important;
background: var(--waline-bg-color) !important;
border: 1px solid var(--waline-border-color) !important;
}
.wl-emoji-popup .wl-tab.active {
background: var(--waline-bg-color-light) !important;
}
.wl-emoji-popup button:hover {
background: var(--waline-bg-color-hover) !important;
}
/* ========== GIF 弹窗 ========== */
.wl-gif-popup {
border-radius: var(--waline-border-radius) !important;
box-shadow: var(--waline-box-shadow) !important;
background: var(--waline-bg-color) !important;
border: 1px solid var(--waline-border-color) !important;
}
/* ========== 评论统计 ========== */
.wl-count {
color: var(--waline-theme-color) !important;
font-size: 1.5em !important;
font-weight: 700 !important;
}
/* ========== 空状态 ========== */
.wl-empty {
color: var(--waline-info-color) !important;
}
/* ========== 头像样式 ========== */
.wl-avatar {
border-radius: var(--waline-avatar-radius) !important;
border: 2px solid var(--waline-border-color) !important;
box-shadow: var(--waline-box-shadow) !important;
}
/* ========== 回复引用框 ========== */
.wl-quote {
border-left-color: var(--waline-border-color) !important;
}
/* ========== 预览区域 ========== */
.wl-preview .wl-content {
background: var(--waline-bg-color-light) !important;
border-radius: var(--waline-btn-radius) !important;
}
/* ========== 反应区域 ========== */
.wl-reaction-title {
color: var(--waline-color) !important;
}
.wl-reaction-item.active .wl-reaction-text {
color: var(--waline-theme-color) !important;
}
.wl-reaction-votes {
border-color: var(--waline-theme-color) !important;
color: var(--waline-theme-color) !important;
}
.wl-reaction-item.active .wl-reaction-votes {
background: var(--waline-theme-color) !important;
color: white !important;
}
/* ========== 加载动画 ========== */
.wl-loading svg {
color: var(--waline-theme-color) !important;
}
/* ========== 滚动条样式 ========== */
[data-waline] ::-webkit-scrollbar {
width: 6px;
height: 6px;
}
[data-waline] ::-webkit-scrollbar-track-piece {
background: var(--waline-bg-color-light);
border-radius: 3px;
}
[data-waline] ::-webkit-scrollbar-thumb {
background: var(--waline-theme-color);
border-radius: 3px;
}
[data-waline] ::-webkit-scrollbar-thumb:hover {
background: var(--waline-active-color);
}
/* ========== 链接样式 ========== */
[data-waline] a {
color: var(--waline-theme-color) !important;
text-decoration: none !important;
transition: all 0.2s ease !important;
}
[data-waline] a:hover {
color: var(--waline-active-color) !important;
}
/* ========== 代码块样式 ========== */
.wl-content pre,
.wl-content pre[class*="language-"] {
background: var(--waline-code-bg-color) !important;
border-radius: var(--waline-btn-radius) !important;
}
/* ========== 移动端适配 ========== */
@media (max-width: 640px) {
.wl-panel {
border-radius: var(--radius-md, 0.5rem) !important;
}
.wl-btn {
font-size: 0.8em !important;
padding: 0.4em 0.8em !important;
}
.wl-editor {
min-height: 6em !important;
}
}

View File

@@ -1,309 +0,0 @@
/**
* Blog utility functions
* This file contains common functions used across blog components
*/
import { type BlogPost, type Lang } from '@/types';
import { defaultLang } from '@/i18n/ui';
/**
* Get blog posts based on language
* @param lang - Current language
* @param postsGlob - Object containing imported posts from glob
* @returns Processed blog posts array
*/
export async function getBlogPosts(lang: Lang, postsGlob: Record<string, any>): Promise<BlogPost[]> {
// Get posts based on language
let allPosts = [];
if (lang === 'zh') {
allPosts = postsGlob.zh;
} else {
allPosts = postsGlob.en;
}
// Process blog post data
const posts: BlogPost[] = allPosts.map((post: any) => {
const slug = post.url?.split('/').filter(Boolean).pop() || '';
// Default image if not specified in frontmatter
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
return {
title: post.frontmatter.title,
description: post.frontmatter.description || '',
image: post.frontmatter.image || defaultImage,
slug: slug,
tags: post.frontmatter.tags || [],
tagId: post.frontmatter.tagId || [],
category: Array.isArray(post.frontmatter.category) ? post.frontmatter.category : post.frontmatter.category ? [post.frontmatter.category] : [],
categoryId: Array.isArray(post.frontmatter.categoryId) ? post.frontmatter.categoryId : post.frontmatter.categoryId ? [post.frontmatter.categoryId] : [],
date: post.frontmatter.date || post.frontmatter.pubDate || '',
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 min read',
url: post.url || '',
};
});
return posts;
}
/**
* Sort blog posts by date (newest first)
* @param posts - Array of blog posts
* @returns Sorted array of blog posts
*/
export function sortPostsByDate(posts: BlogPost[]): BlogPost[] {
return posts
.filter(post => post.date) // Filter out posts without dates
.sort((a, b) => {
const dateA = new Date(a.date).getTime();
const dateB = new Date(b.date).getTime();
return dateB - dateA; // Descending order, newest first
});
}
/**
* Filter posts by category
* @param posts - Array of blog posts
* @param category - Category to filter by
* @returns Filtered array of blog posts
*/
export function filterPostsByCategory(posts: BlogPost[], category: string): BlogPost[] {
if (!category) return posts;
return posts.filter(post => {
// First check if post contains the current category ID
if (post.categoryId && Array.isArray(post.categoryId)) {
return post.categoryId.some(catId =>
catId.toLowerCase() === category.toLowerCase()
);
}
// If no categoryId, check category
else if (post.category && Array.isArray(post.category)) {
return post.category.some(cat =>
cat.toLowerCase() === category.toLowerCase()
);
}
return false;
});
}
/**
* Filter posts by tag
* @param posts - Array of blog posts
* @param tag - Tag to filter by
* @returns Filtered array of blog posts
*/
export function filterPostsByTag(posts: BlogPost[], tag: string): BlogPost[] {
if (!tag) return posts;
return posts.filter(post => {
// First check if post contains the current tag ID
if (post.tagId && Array.isArray(post.tagId)) {
return post.tagId.some(tagId =>
tagId.toLowerCase() === tag.toLowerCase()
);
}
// If no tagId, check tags
else if (post.tags && Array.isArray(post.tags)) {
return post.tags.some(postTag =>
postTag.toLowerCase() === tag.toLowerCase()
);
}
return false;
});
}
/**
* Extract categories from posts
* @param posts - Array of blog posts
* @returns Map of category names to category IDs
*/
export function extractCategories(posts: any[]): Map<string, string> {
const allCategories = new Set<string>();
const categoryMap = new Map<string, string>(); // Map of category name to ID
posts.forEach(post => {
// Process categories
if (post.frontmatter?.category) {
const categories = Array.isArray(post.frontmatter.category)
? post.frontmatter.category
: [post.frontmatter.category];
const categoryIds = Array.isArray(post.frontmatter.categoryId)
? post.frontmatter.categoryId
: post.frontmatter.categoryId ? [post.frontmatter.categoryId] : [];
// If categoryId exists, use mapping between categoryId and category
if (categoryIds.length > 0 && categoryIds.length === categories.length) {
categories.forEach((cat: string, index: number) => {
if (cat && categoryIds[index]) {
allCategories.add(cat);
categoryMap.set(cat, categoryIds[index]);
}
});
} else {
// If no categoryId or lengths don't match, use category name as ID
categories.forEach((cat: string) => {
if (cat) {
allCategories.add(cat);
categoryMap.set(cat, cat.toLowerCase());
}
});
}
}
});
return categoryMap;
}
/**
* Extract tags from posts
* @param posts - Array of blog posts
* @returns Map of tag names to tag IDs
*/
export function extractTags(posts: any[]): Map<string, string> {
const allTags = new Set<string>();
const tagMap = new Map<string, string>(); // Map of tag name to ID
posts.forEach(post => {
// Process tags
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
const tags = post.frontmatter.tags;
const tagIds = Array.isArray(post.frontmatter.tagId)
? post.frontmatter.tagId
: post.frontmatter.tagId ? [post.frontmatter.tagId] : [];
// If tagId exists, use mapping between tagId and tag
if (tagIds.length > 0 && tagIds.length === tags.length) {
tags.forEach((tag: string, index: number) => {
if (tag && tagIds[index]) {
allTags.add(tag);
tagMap.set(tag, tagIds[index]);
}
});
} else {
// If no tagId or lengths don't match, use tag name as ID
tags.forEach((tag: string) => {
if (tag) {
allTags.add(tag);
tagMap.set(tag, tag.toLowerCase());
}
});
}
}
});
return tagMap;
}
/**
* Get base URL for blog based on language
* @param lang - Current language
* @returns Base URL string
*/
export function getBlogBaseUrl(lang: Lang): string {
return lang === defaultLang ? '/blog' : `/${lang}/blog`;
}
/**
* Process raw blog post data from import.meta.glob
* @param allPosts - Raw posts data from import.meta.glob
* @param lang - Current language
* @returns Processed blog posts array
*/
export function processRawBlogPosts(allPosts: Record<string, any>[] | any[], lang: Lang): BlogPost[] {
// Process blog post data
const posts: BlogPost[] = allPosts.map((post: any) => {
const slug = post.url?.split('/').filter(Boolean).pop() || '';
// Default image if not specified in frontmatter
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
// Default read time text based on language
const defaultReadTime = lang === 'zh' ? '5 分钟阅读' : '5 min read';
return {
title: post.frontmatter.title,
description: post.frontmatter.description || '',
image: post.frontmatter.image || defaultImage,
slug: slug,
tags: post.frontmatter.tags || [],
tagId: post.frontmatter.tagId || [],
category: Array.isArray(post.frontmatter.category) ? post.frontmatter.category : post.frontmatter.category ? [post.frontmatter.category] : [],
categoryId: Array.isArray(post.frontmatter.categoryId) ? post.frontmatter.categoryId : post.frontmatter.categoryId ? [post.frontmatter.categoryId] : [],
date: post.frontmatter.date || post.frontmatter.pubDate || '',
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || defaultReadTime,
url: post.url || '',
};
});
return posts;
}
/**
* Extract post metadata for categories and tags
* @param posts - Array of blog posts
* @returns Array of post metadata
*/
export function extractPostMetadata(posts: any[]) {
return posts.map((post: any) => ({
category: post.frontmatter?.category || [],
categoryId: post.frontmatter?.categoryId || [],
tags: post.frontmatter?.tags || [],
tagId: post.frontmatter?.tagId || []
}));
}
/**
* Get all blog posts with processing for a taxonomy page (category or tag)
* @param lang - Current language
* @param options - Additional options for processing
* @returns Object containing processed posts and metadata
*/
export async function getTaxonomyPageData(lang: Lang, options?: {
category?: string;
tag?: string;
}) {
// Read all blog posts
// Note: import.meta.glob only accepts literal strings, not variables
let allPosts;
// Use specific glob patterns based on language
if (lang === 'zh') {
allPosts = await import.meta.glob('/src/pages/zh/blog/posts/*.{md,mdx}', { eager: true });
} else {
allPosts = await import.meta.glob('/src/pages/blog/posts/*.{md,mdx}', { eager: true });
}
// Process blog post data
const blogPosts = processRawBlogPosts(Object.values(allPosts), lang);
// Apply filters if specified
let filteredPosts = blogPosts;
if (options?.category) {
filteredPosts = filterPostsByCategory(filteredPosts, options.category);
}
if (options?.tag) {
filteredPosts = filterPostsByTag(filteredPosts, options.tag);
}
// Sort posts by date
const sortedBlogPosts = sortPostsByDate(filteredPosts);
// Extract categories and tags from all posts for sidebar
const allPostsMetadata = extractPostMetadata(Object.values(allPosts));
// Get categories and tags for sidebar
const categories = extractCategories(allPostsMetadata);
const tags = extractTags(allPostsMetadata);
return {
posts: sortedBlogPosts,
categories,
tags,
allPosts
};
}