diff --git a/src/components/blog/Comments.tsx b/src/components/blog/Comments.tsx index db8bbf0..2022a4c 100644 --- a/src/components/blog/Comments.tsx +++ b/src/components/blog/Comments.tsx @@ -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'; diff --git a/src/styles/waline-custom.css b/src/styles/waline-custom.css new file mode 100644 index 0000000..cf61afb --- /dev/null +++ b/src/styles/waline-custom.css @@ -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; + } +} diff --git a/src/utils/blog-utils.ts b/src/utils/blog-utils.ts deleted file mode 100644 index 526c7f4..0000000 --- a/src/utils/blog-utils.ts +++ /dev/null @@ -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): Promise { - // 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 { - const allCategories = new Set(); - const categoryMap = new Map(); // 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 { - const allTags = new Set(); - const tagMap = new Map(); // 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[] | 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 - }; -} \ No newline at end of file