chore: reorganize project into monorepo

This commit is contained in:
2026-03-28 10:40:22 +08:00
parent 60367a5f51
commit 1455d93246
201 changed files with 30081 additions and 93 deletions

View File

@@ -0,0 +1,404 @@
import type {
Category as UiCategory,
FriendLink as UiFriendLink,
Post as UiPost,
SiteSettings,
Tag as UiTag,
} from '../types';
export const API_BASE_URL = 'http://localhost:5150/api';
export interface ApiPost {
id: number;
title: string;
slug: string;
description: string;
content: string;
category: string;
tags: string[];
post_type: 'article' | 'tweet';
image: string | null;
pinned: boolean;
created_at: string;
updated_at: string;
}
export interface Comment {
id: number;
post_id: string | null;
post_slug: string | null;
author: string | null;
email: string | null;
avatar: string | null;
content: string | null;
reply_to: string | null;
approved: boolean | null;
created_at: string;
updated_at: string;
}
export interface CreateCommentInput {
postSlug: string;
nickname: string;
email?: string;
content: string;
replyTo?: string | null;
}
export interface ApiTag {
id: number;
name: string;
slug: string;
created_at: string;
updated_at: string;
}
export interface ApiCategory {
id: number;
name: string;
slug: string;
count: number;
}
export interface ApiFriendLink {
id: number;
site_name: string;
site_url: string;
avatar_url: string | null;
description: string | null;
category: string | null;
status: 'pending' | 'approved' | 'rejected';
created_at: string;
updated_at: string;
}
export interface CreateFriendLinkInput {
siteName: string;
siteUrl: string;
avatarUrl?: string;
description: string;
category?: string;
}
export interface ApiSiteSettings {
id: number;
site_name: string | null;
site_short_name: string | null;
site_url: string | null;
site_title: string | null;
site_description: string | null;
hero_title: string | null;
hero_subtitle: string | null;
owner_name: string | null;
owner_title: string | null;
owner_bio: string | null;
owner_avatar_url: string | null;
social_github: string | null;
social_twitter: string | null;
social_email: string | null;
location: string | null;
tech_stack: string[] | null;
}
export interface ApiSearchResult {
id: number;
title: string | null;
slug: string;
description: string | null;
content: string | null;
category: string | null;
tags: string[] | null;
post_type: 'article' | 'tweet' | null;
image: string | null;
pinned: boolean | null;
created_at: string;
updated_at: string;
rank: number;
}
export interface Review {
id: number;
title: string;
review_type: 'game' | 'anime' | 'music' | 'book' | 'movie';
rating: number;
review_date: string;
status: 'completed' | 'in-progress' | 'dropped';
description: string;
tags: string;
cover: string;
created_at: string;
updated_at: string;
}
export type AppFriendLink = UiFriendLink & {
status: ApiFriendLink['status'];
};
export const DEFAULT_SITE_SETTINGS: SiteSettings = {
id: '1',
siteName: 'InitCool',
siteShortName: 'Termi',
siteUrl: 'https://termi.dev',
siteTitle: 'InitCool - 终端风格的内容平台',
siteDescription: '一个基于终端美学的个人内容站,记录代码、设计和生活。',
heroTitle: '欢迎来到我的极客终端博客',
heroSubtitle: '这里记录技术、代码和生活点滴',
ownerName: 'InitCool',
ownerTitle: '前端开发者 / 技术博主',
ownerBio: '一名热爱技术的前端开发者,专注于构建高性能、优雅的用户界面。相信代码不仅是工具,更是一种艺术表达。',
location: 'Hong Kong',
social: {
github: 'https://github.com',
twitter: 'https://twitter.com',
email: 'mailto:hello@termi.dev',
},
techStack: ['Astro', 'Svelte', 'Tailwind CSS', 'TypeScript'],
};
const formatPostDate = (dateString: string) => dateString.slice(0, 10);
const estimateReadTime = (content: string | null | undefined) => {
const text = content?.trim() || '';
const minutes = Math.max(1, Math.ceil(text.length / 300));
return `${minutes} 分钟`;
};
const normalizePost = (post: ApiPost): UiPost => ({
id: String(post.id),
slug: post.slug,
title: post.title,
description: post.description,
content: post.content,
date: formatPostDate(post.created_at),
readTime: estimateReadTime(post.content || post.description),
type: post.post_type,
tags: post.tags ?? [],
category: post.category,
image: post.image ?? undefined,
pinned: post.pinned,
});
const normalizeTag = (tag: ApiTag): UiTag => ({
id: String(tag.id),
name: tag.name,
slug: tag.slug,
});
const normalizeCategory = (category: ApiCategory): UiCategory => ({
id: String(category.id),
name: category.name,
slug: category.slug,
count: category.count,
});
const normalizeAvatarUrl = (value: string | null | undefined) => {
if (!value) {
return undefined;
}
try {
const host = new URL(value).hostname.toLowerCase();
const isReservedExampleHost =
host === 'example.com' ||
host === 'example.org' ||
host === 'example.net' ||
host.endsWith('.example.com') ||
host.endsWith('.example.org') ||
host.endsWith('.example.net');
return isReservedExampleHost ? undefined : value;
} catch {
return undefined;
}
};
const normalizeTagToken = (value: string) => value.trim().toLowerCase();
const normalizeFriendLink = (friendLink: ApiFriendLink): AppFriendLink => ({
id: String(friendLink.id),
name: friendLink.site_name,
url: friendLink.site_url,
avatar: normalizeAvatarUrl(friendLink.avatar_url),
description: friendLink.description ?? undefined,
category: friendLink.category ?? undefined,
status: friendLink.status,
});
const normalizeSiteSettings = (settings: ApiSiteSettings): SiteSettings => ({
id: String(settings.id),
siteName: settings.site_name || DEFAULT_SITE_SETTINGS.siteName,
siteShortName: settings.site_short_name || DEFAULT_SITE_SETTINGS.siteShortName,
siteUrl: settings.site_url || DEFAULT_SITE_SETTINGS.siteUrl,
siteTitle: settings.site_title || DEFAULT_SITE_SETTINGS.siteTitle,
siteDescription: settings.site_description || DEFAULT_SITE_SETTINGS.siteDescription,
heroTitle: settings.hero_title || DEFAULT_SITE_SETTINGS.heroTitle,
heroSubtitle: settings.hero_subtitle || DEFAULT_SITE_SETTINGS.heroSubtitle,
ownerName: settings.owner_name || DEFAULT_SITE_SETTINGS.ownerName,
ownerTitle: settings.owner_title || DEFAULT_SITE_SETTINGS.ownerTitle,
ownerBio: settings.owner_bio || DEFAULT_SITE_SETTINGS.ownerBio,
ownerAvatarUrl: settings.owner_avatar_url ?? undefined,
location: settings.location || DEFAULT_SITE_SETTINGS.location,
social: {
github: settings.social_github || DEFAULT_SITE_SETTINGS.social.github,
twitter: settings.social_twitter || DEFAULT_SITE_SETTINGS.social.twitter,
email: settings.social_email || DEFAULT_SITE_SETTINGS.social.email,
},
techStack: settings.tech_stack?.length ? settings.tech_stack : DEFAULT_SITE_SETTINGS.techStack,
});
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async fetch<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => '');
throw new Error(errorText || `API error: ${response.status} ${response.statusText}`);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
async getRawPosts(): Promise<ApiPost[]> {
return this.fetch<ApiPost[]>('/posts');
}
async getPosts(): Promise<UiPost[]> {
const posts = await this.getRawPosts();
return posts.map(normalizePost);
}
async getPost(id: number): Promise<UiPost> {
const post = await this.fetch<ApiPost>(`/posts/${id}`);
return normalizePost(post);
}
async getPostBySlug(slug: string): Promise<UiPost | null> {
const posts = await this.getPosts();
return posts.find(post => post.slug === slug) || null;
}
async getComments(postSlug: string, options?: { approved?: boolean }): Promise<Comment[]> {
const params = new URLSearchParams({ post_slug: postSlug });
if (options?.approved !== undefined) {
params.set('approved', String(options.approved));
}
return this.fetch<Comment[]>(`/comments?${params.toString()}`);
}
async createComment(comment: CreateCommentInput): Promise<Comment> {
return this.fetch<Comment>('/comments', {
method: 'POST',
body: JSON.stringify({
postSlug: comment.postSlug,
nickname: comment.nickname,
email: comment.email,
content: comment.content,
replyTo: comment.replyTo,
}),
});
}
async getReviews(): Promise<Review[]> {
return this.fetch<Review[]>('/reviews');
}
async getReview(id: number): Promise<Review> {
return this.fetch<Review>(`/reviews/${id}`);
}
async getRawFriendLinks(): Promise<ApiFriendLink[]> {
return this.fetch<ApiFriendLink[]>('/friend_links');
}
async getFriendLinks(): Promise<AppFriendLink[]> {
const friendLinks = await this.getRawFriendLinks();
return friendLinks.map(normalizeFriendLink);
}
async createFriendLink(friendLink: CreateFriendLinkInput): Promise<ApiFriendLink> {
return this.fetch<ApiFriendLink>('/friend_links', {
method: 'POST',
body: JSON.stringify(friendLink),
});
}
async getRawTags(): Promise<ApiTag[]> {
return this.fetch<ApiTag[]>('/tags');
}
async getTags(): Promise<UiTag[]> {
const tags = await this.getRawTags();
return tags.map(normalizeTag);
}
async getRawSiteSettings(): Promise<ApiSiteSettings> {
return this.fetch<ApiSiteSettings>('/site_settings');
}
async getSiteSettings(): Promise<SiteSettings> {
const settings = await this.getRawSiteSettings();
return normalizeSiteSettings(settings);
}
async getCategories(): Promise<UiCategory[]> {
const categories = await this.fetch<ApiCategory[]>('/categories');
return categories.map(normalizeCategory);
}
async getPostsByCategory(category: string): Promise<UiPost[]> {
const posts = await this.getPosts();
return posts.filter(post => post.category?.toLowerCase() === category.toLowerCase());
}
async getPostsByTag(tag: string): Promise<UiPost[]> {
const posts = await this.getPosts();
const normalizedTag = normalizeTagToken(tag);
return posts.filter(post =>
post.tags?.some(item => normalizeTagToken(item) === normalizedTag)
);
}
async searchPosts(query: string, limit = 20): Promise<UiPost[]> {
const params = new URLSearchParams({
q: query,
limit: String(limit),
});
const results = await this.fetch<ApiSearchResult[]>(`/search?${params.toString()}`);
return results.map(result =>
normalizePost({
id: result.id,
title: result.title || 'Untitled',
slug: result.slug,
description: result.description || '',
content: result.content || '',
category: result.category || '',
tags: result.tags ?? [],
post_type: result.post_type || 'article',
image: result.image,
pinned: result.pinned ?? false,
created_at: result.created_at,
updated_at: result.updated_at,
})
);
}
}
export const api = new ApiClient(API_BASE_URL);
export const apiClient = api;

View File

@@ -0,0 +1,167 @@
export interface TerminalConfig {
defaultCategory: string;
welcomeMessage: string;
prompt: {
prefix: string;
separator: string;
path: string;
suffix: string;
mobile: string;
};
asciiArt: string;
title: string;
welcome: {
title: string;
subtitle: string;
};
navLinks: Array<{
icon: string;
text: string;
href: string;
}>;
categories: {
[key: string]: {
title: string;
description: string;
items: Array<{
command: string;
description: string;
shortDesc?: string;
url?: string;
}>;
};
};
postTypes: {
article: { color: string; label: string };
tweet: { color: string; label: string };
};
pinnedPost?: {
title: string;
description: string;
date: string;
readTime: string;
type: 'article' | 'tweet';
tags: string[];
link: string;
};
socialLinks: {
github: string;
twitter: string;
email: string;
};
tools: Array<{
icon: string;
href: string;
title: string;
}>;
search?: {
placeholders: {
default: string;
small: string;
medium: string;
};
promptText: string;
emptyResultText: string;
};
terminal?: {
defaultWindowTitle: string;
controls: {
colors: {
close: string;
minimize: string;
expand: string;
};
};
animation?: {
glowDuration: string;
};
};
branding?: {
name: string;
shortName?: string;
};
}
export const terminalConfig: TerminalConfig = {
defaultCategory: 'blog',
welcomeMessage: '欢迎来到我的博客!',
prompt: {
prefix: 'user@blog',
separator: ':',
path: '~/',
suffix: '$',
mobile: '~$'
},
asciiArt: `
I N N I TTTTT CCCC OOO OOO L
I NN N I T C O O O O L
I N N N I T C O O O O L
I N NN I T C O O O O L
I N N I T CCCC OOO OOO LLLLL`,
title: '~/blog',
welcome: {
title: '欢迎来到我的极客终端博客',
subtitle: '这里记录技术、代码和生活点滴'
},
navLinks: [
{ icon: 'fa-file-code', text: '文章', href: '/articles' },
{ icon: 'fa-folder', text: '分类', href: '/categories' },
{ icon: 'fa-tags', text: '标签', href: '/tags' },
{ icon: 'fa-stream', text: '时间轴', href: '/timeline' },
{ icon: 'fa-star', text: '评价', href: '/reviews' },
{ icon: 'fa-link', text: '友链', href: '/friends' },
{ icon: 'fa-user-secret', text: '关于', href: '/about' }
],
categories: {
blog: {
title: '博客',
description: '我的个人博客文章',
items: [
{
command: 'help',
description: '显示帮助信息',
shortDesc: '显示帮助信息'
}
]
}
},
postTypes: {
article: { color: '#00ff9d', label: '博客文章' },
tweet: { color: '#00b8ff', label: '推文' }
},
socialLinks: {
github: '',
twitter: '',
email: ''
},
tools: [
{ icon: 'fa-sitemap', href: '/sitemap.xml', title: '站点地图' },
{ icon: 'fa-rss', href: '/rss.xml', title: 'RSS订阅' }
],
search: {
placeholders: {
default: "'关键词' articles/*.md",
small: "搜索...",
medium: "搜索文章..."
},
promptText: "grep -i",
emptyResultText: "输入关键词搜索文章"
},
terminal: {
defaultWindowTitle: 'user@terminal: ~/blog',
controls: {
colors: {
close: '#ff5f56',
minimize: '#ffbd2e',
expand: '#27c93f'
}
},
animation: {
glowDuration: '4s'
}
},
branding: {
name: 'InitCool',
shortName: 'Termi'
}
};

View File

@@ -0,0 +1,435 @@
/* 现代化主题系统 - 使用 CSS 变量 + 媒体查询 */
:root {
/* 声明支持的颜色方案 */
color-scheme: light dark;
/* 全局变量 */
--transition-duration: 0.3s;
/* 亮色模式默认 */
--primary: #4285f4;
--primary-rgb: 66 133 244;
--primary-light: #4285f433;
--primary-dark: #3367d6;
--secondary: #ea580c;
--secondary-rgb: 234 88 12;
--secondary-light: #ea580c33;
--bg: #f3f4f6;
--bg-rgb: 243 244 246;
--bg-secondary: #e5e7eb;
--bg-tertiary: #d1d5db;
--terminal-bg: #ffffff;
--text: #1a1a1a;
--text-rgb: 26 26 26;
--text-secondary: #9ca3af;
--text-tertiary: #cbd5e1;
--terminal-text: #1a1a1a;
--title-color: #1a1a1a;
--button-text: #1a1a1a;
--border-color: #e5e7eb;
--border-color-rgb: 229 231 235;
--terminal-border: #e5e7eb;
--tag-bg: #f3f4f6;
--tag-text: #1a1a1a;
--header-bg: #f9fafb;
--code-bg: #f3f4f6;
/* 终端窗口控制按钮 */
--btn-close: #ff5f56;
--btn-minimize: #ffbd2e;
--btn-expand: #27c93f;
/* 状态颜色 */
--success: #10b981;
--success-rgb: 16 185 129;
--success-light: #d1fae5;
--success-dark: #065f46;
--warning: #f59e0b;
--warning-rgb: 245 158 11;
--warning-light: #fef3c7;
--warning-dark: #92400e;
--danger: #ef4444;
--danger-rgb: 239 68 68;
--danger-light: #fee2e2;
--danger-dark: #991b1b;
--gray-light: #f3f4f6;
--gray-dark: #374151;
/* 全局样式变量 */
--border-radius: 0.5rem;
--box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}
/* 暗色模式 */
@media (prefers-color-scheme: dark) {
:root {
--primary: #00ff9d;
--primary-rgb: 0 255 157;
--primary-light: #00ff9d33;
--primary-dark: #00b8ff;
--secondary: #00b8ff;
--secondary-rgb: 0 184 255;
--secondary-light: #00b8ff33;
--bg: #0a0e17;
--bg-rgb: 10 14 23;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--terminal-bg: #0d1117;
--text: #e6e6e6;
--text-rgb: 230 230 230;
--text-secondary: #d1d5db;
--text-tertiary: #6b7280;
--terminal-text: #e6e6e6;
--title-color: #ffffff;
--button-text: #e6e6e6;
--border-color: rgba(255, 255, 255, 0.1);
--border-color-rgb: 255 255 255;
--terminal-border: rgba(255, 255, 255, 0.1);
--tag-bg: #161b22;
--tag-text: #e6e6e6;
--header-bg: #161b22;
--code-bg: #161b22;
--success-light: #064e3b;
--success-dark: #d1fae5;
--warning-light: #78350f;
--warning-dark: #fef3c7;
--danger-light: #7f1d1d;
--danger-dark: #fee2e2;
--gray-light: #1f2937;
--gray-dark: #e5e7eb;
}
}
/* 手动暗色模式覆盖 - 使用 html.dark */
html.dark {
--primary: #00ff9d;
--primary-rgb: 0 255 157;
--primary-light: #00ff9d33;
--primary-dark: #00b8ff;
--secondary: #00b8ff;
--secondary-rgb: 0 184 255;
--secondary-light: #00b8ff33;
--bg: #0a0e17;
--bg-rgb: 10 14 23;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--terminal-bg: #0d1117;
--text: #e6e6e6;
--text-rgb: 230 230 230;
--text-secondary: #d1d5db;
--text-tertiary: #6b7280;
--terminal-text: #e6e6e6;
--title-color: #ffffff;
--button-text: #e6e6e6;
--border-color: rgba(255, 255, 255, 0.1);
--border-color-rgb: 255 255 255;
--terminal-border: rgba(255, 255, 255, 0.1);
--tag-bg: #161b22;
--tag-text: #e6e6e6;
--header-bg: #161b22;
--code-bg: #161b22;
--success-light: #064e3b;
--success-dark: #d1fae5;
--warning-light: #78350f;
--warning-dark: #fef3c7;
--danger-light: #7f1d1d;
--danger-dark: #fee2e2;
--gray-light: #1f2937;
--gray-dark: #e5e7eb;
}
/* 优化的平滑过渡 - 只应用到需要的元素 */
body,
button,
input,
select,
textarea,
a {
transition: background-color var(--transition-duration) ease,
color var(--transition-duration) ease,
border-color var(--transition-duration) ease;
}
/* 主题切换按钮动画 */
.theme-toggle {
transition: all 0.3s ease;
}
.theme-toggle i {
transition: transform 0.3s ease;
}
.theme-toggle:hover i {
transform: rotate(30deg);
}
.terminal-input {
width: 100%;
background-color: color-mix(in oklab, var(--terminal-bg) 84%, var(--bg-secondary)) !important;
background-image:
linear-gradient(to bottom, rgba(var(--primary-rgb), 0.04), rgba(var(--primary-rgb), 0.0));
border: 1px solid rgba(var(--primary-rgb), 0.42) !important;
outline: 1px solid rgba(var(--primary-rgb), 0.22);
outline-offset: -1px;
color: var(--text) !important;
font-family: var(--font-mono);
padding: 0.55rem 0.7rem;
border-radius: var(--border-radius);
font-size: 0.9rem;
line-height: 1.25;
letter-spacing: 0.01em;
caret-color: var(--primary);
appearance: none;
display: block;
box-shadow:
inset 0 0 0 1px rgba(var(--primary-rgb), 0.16),
inset 0 10px 20px rgba(0, 0, 0, 0.10),
inset 0 0 26px rgba(var(--primary-rgb), 0.08),
0 0 0 1px rgba(0, 0, 0, 0.14);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
@media (prefers-color-scheme: dark) {
.terminal-input {
background-color: color-mix(in oklab, var(--bg-tertiary) 88%, var(--terminal-bg)) !important;
background-image:
linear-gradient(to bottom, rgba(var(--primary-rgb), 0.06), rgba(var(--primary-rgb), 0.0)),
repeating-linear-gradient(
to bottom,
rgba(var(--primary-rgb), 0.06) 0px,
rgba(var(--primary-rgb), 0.06) 1px,
transparent 1px,
transparent 6px
);
border: 1px solid rgba(var(--primary-rgb), 0.48) !important;
outline: 1px solid rgba(var(--primary-rgb), 0.22);
box-shadow:
inset 0 0 0 1px rgba(var(--primary-rgb), 0.16),
inset 0 10px 20px rgba(0, 0, 0, 0.32),
inset 0 0 26px rgba(var(--primary-rgb), 0.12),
0 0 0 1px rgba(0, 0, 0, 0.26);
}
}
html.dark .terminal-input {
background-color: color-mix(in oklab, var(--bg-tertiary) 88%, var(--terminal-bg)) !important;
background-image:
linear-gradient(to bottom, rgba(var(--primary-rgb), 0.06), rgba(var(--primary-rgb), 0.0)),
repeating-linear-gradient(
to bottom,
rgba(var(--primary-rgb), 0.06) 0px,
rgba(var(--primary-rgb), 0.06) 1px,
transparent 1px,
transparent 6px
);
border: 1px solid rgba(var(--primary-rgb), 0.48) !important;
outline: 1px solid rgba(var(--primary-rgb), 0.22);
box-shadow:
inset 0 0 0 1px rgba(var(--primary-rgb), 0.16),
inset 0 10px 20px rgba(0, 0, 0, 0.32),
inset 0 0 26px rgba(var(--primary-rgb), 0.12),
0 0 0 1px rgba(0, 0, 0, 0.26);
}
.terminal-input::placeholder {
color: rgba(0, 0, 0, 0.45);
letter-spacing: 0.02em;
}
@media (prefers-color-scheme: dark) {
.terminal-input::placeholder {
color: rgba(255, 255, 255, 0.45);
}
}
html.dark .terminal-input::placeholder {
color: rgba(255, 255, 255, 0.45);
}
.terminal-input:focus {
outline: none;
border-color: var(--primary) !important;
box-shadow:
inset 0 0 0 1px rgba(var(--primary-rgb), 0.22),
inset 0 0 26px rgba(var(--primary-rgb), 0.14),
0 0 0 2px rgba(var(--primary-rgb), 0.22),
0 0 28px rgba(var(--primary-rgb), 0.24);
}
.terminal-input.textarea {
resize: vertical;
min-height: 4.5rem;
}
/* Terminal Window Glow Effects */
.terminal-window {
background-color: var(--terminal-bg);
border-radius: 8px !important;
border: 1px solid var(--primary) !important;
box-shadow:
0 0 8px rgba(var(--primary-rgb), 0.4),
0 0 20px rgba(var(--primary-rgb), 0.2),
0 10px 30px rgba(var(--primary-rgb), 0.2) !important;
overflow: hidden;
position: relative;
animation: terminal-glow 4s ease-in-out infinite alternate !important;
}
@keyframes terminal-glow {
0% {
box-shadow:
0 0 8px rgba(var(--primary-rgb), 0.4),
0 0 20px rgba(var(--primary-rgb), 0.2),
0 10px 30px rgba(var(--primary-rgb), 0.2);
}
100% {
box-shadow:
0 0 12px rgba(var(--primary-rgb), 0.5),
0 0 25px rgba(var(--primary-rgb), 0.3),
0 10px 30px rgba(var(--primary-rgb), 0.2);
}
}
/* Dark mode glow adjustments */
@media (prefers-color-scheme: dark) {
.terminal-window {
animation: terminal-glow-dark 4s ease-in-out infinite alternate !important;
}
@keyframes terminal-glow-dark {
0% {
box-shadow:
0 0 8px rgba(var(--primary-rgb), 0.3),
0 0 20px rgba(var(--primary-rgb), 0.15),
0 10px 40px rgba(var(--primary-rgb), 0.1);
border-color: rgba(var(--primary-rgb), 0.5) !important;
}
100% {
box-shadow:
0 0 15px rgba(var(--primary-rgb), 0.5),
0 0 30px rgba(var(--primary-rgb), 0.25),
0 10px 50px rgba(var(--primary-rgb), 0.15);
border-color: rgba(var(--primary-rgb), 0.8) !important;
}
}
}
html.dark .terminal-window {
animation: terminal-glow-dark 4s ease-in-out infinite alternate !important;
}
/* Terminal Header */
.terminal-header {
border-bottom: 1px solid var(--primary) !important;
box-shadow: 0 1px 5px rgba(var(--primary-rgb), 0.2) !important;
}
/* Glow Text Effect */
.glow-text {
text-shadow: 0 0 10px rgba(var(--primary-rgb), 0.5);
}
/* Post Card Hover Effects */
.post-card {
position: relative;
transition: all 0.3s ease;
}
.post-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
border-radius: 4px 0 0 4px;
opacity: 0;
transition: opacity 0.3s ease;
background-color: var(--post-border-color, var(--primary));
}
.post-card:hover {
transform: translateX(8px);
}
.post-card:hover::before {
opacity: 1;
}
/* Cursor Blink Animation */
.cursor {
display: inline-block;
width: 10px;
height: 18px;
background-color: var(--primary);
animation: blink 1s infinite;
vertical-align: middle;
margin-left: 2px;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* ASCII Art Styling */
.ascii-art {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.2;
color: var(--primary);
white-space: pre;
margin: 0;
padding: 0;
text-align: left;
letter-spacing: 0;
overflow-x: auto;
}
@media (max-width: 640px) {
.ascii-art {
font-size: 0.625rem;
}
}
/* Link Hover Effects */
a {
transition: all 0.3s ease;
}
/* Button Hover Glow */
button:hover,
a:hover {
text-shadow: 0 0 8px rgba(var(--primary-rgb), 0.3);
}

View File

@@ -0,0 +1,88 @@
export interface Post {
id: string;
slug: string;
title: string;
description: string;
content?: string;
date: string;
readTime: string;
type: 'article' | 'tweet';
tags: string[];
category: string;
image?: string;
images?: string[];
code?: string;
language?: string;
pinned?: boolean;
}
export interface Category {
id: string;
name: string;
slug: string;
description?: string;
icon?: string;
count?: number;
}
export interface Tag {
id: string;
name: string;
slug: string;
count?: number;
}
export interface FriendLink {
id: string;
name: string;
url: string;
avatar?: string;
description?: string;
category?: string;
}
export interface SiteSettings {
id: string;
siteName: string;
siteShortName: string;
siteUrl: string;
siteTitle: string;
siteDescription: string;
heroTitle: string;
heroSubtitle: string;
ownerName: string;
ownerTitle: string;
ownerBio: string;
ownerAvatarUrl?: string;
location?: string;
social: {
github?: string;
twitter?: string;
email?: string;
};
techStack: string[];
}
export interface SiteConfig {
name: string;
description: string;
author: string;
url: string;
social: {
github?: string;
twitter?: string;
email?: string;
};
}
export interface SystemStat {
label: string;
value: string;
icon?: string;
}
export interface TechStackItem {
name: string;
icon?: string;
level?: string;
}

View File

@@ -0,0 +1,194 @@
import type { Post, Category, Tag, FriendLink } from '../types';
// Mock data for static site generation
export const mockPosts: Post[] = [
{
id: '1',
slug: 'welcome-to-termi',
title: '欢迎来到 Termi 终端博客',
description: '这是一个基于终端风格的现代博客平台,结合了极客美学与极致性能。',
date: '2024-03-20',
readTime: '3 分钟',
type: 'article',
tags: ['astro', 'svelte', 'tailwind'],
category: '技术',
pinned: true,
image: 'https://picsum.photos/1200/600?random=1'
},
{
id: '2',
slug: 'astro-ssg-guide',
title: 'Astro 静态站点生成指南',
description: '学习如何使用 Astro 构建高性能的静态网站,掌握群岛架构的核心概念。',
date: '2024-03-18',
readTime: '5 分钟',
type: 'article',
tags: ['astro', 'ssg', 'performance'],
category: '前端'
},
{
id: '3',
slug: 'tailwind-v4-features',
title: 'Tailwind CSS v4 新特性解析',
description: '探索 Tailwind CSS v4 带来的全新特性,包括改进的性能和更简洁的配置。',
date: '2024-03-15',
readTime: '4 分钟',
type: 'article',
tags: ['tailwind', 'css', 'design'],
category: '前端'
},
{
id: '4',
slug: 'daily-thought-1',
title: '关于代码与咖啡的思考',
description: '写代码就像冲咖啡,需要耐心和恰到好处的温度。今天尝试了几款新豆子,每一杯都有不同的风味。',
date: '2024-03-14',
readTime: '1 分钟',
type: 'tweet',
tags: ['life', 'coding'],
category: '随笔',
images: [
'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=400&h=400&fit=crop',
'https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=400&h=400&fit=crop',
'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?w=400&h=400&fit=crop'
]
},
{
id: '5',
slug: 'svelte-5-runes',
title: 'Svelte 5 Runes 完全指南',
description: '深入了解 Svelte 5 的 Runes 系统,掌握下一代响应式编程范式。',
date: '2024-03-10',
readTime: '8 分钟',
type: 'article',
tags: ['svelte', 'javascript', 'frontend'],
category: '前端'
}
];
export const mockCategories: Category[] = [
{ id: '1', name: '技术', slug: 'tech', icon: 'fa-code', count: 3 },
{ id: '2', name: '前端', slug: 'frontend', icon: 'fa-laptop-code', count: 3 },
{ id: '3', name: '随笔', slug: 'essay', icon: 'fa-pen', count: 1 },
{ id: '4', name: '生活', slug: 'life', icon: 'fa-coffee', count: 1 }
];
export const mockTags: Tag[] = [
{ id: '1', name: 'astro', slug: 'astro', count: 1 },
{ id: '2', name: 'svelte', slug: 'svelte', count: 2 },
{ id: '3', name: 'tailwind', slug: 'tailwind', count: 2 },
{ id: '4', name: 'frontend', slug: 'frontend', count: 2 },
{ id: '5', name: 'ssg', slug: 'ssg', count: 1 },
{ id: '6', name: 'css', slug: 'css', count: 1 },
{ id: '7', name: 'javascript', slug: 'javascript', count: 1 },
{ id: '8', name: 'life', slug: 'life', count: 1 },
{ id: '9', name: 'coding', slug: 'coding', count: 1 }
];
export const mockFriendLinks: FriendLink[] = [
{
id: '1',
name: 'Astro 官网',
url: 'https://astro.build',
avatar: 'https://astro.build/favicon.svg',
description: '极速内容驱动的网站框架',
category: '技术博客'
},
{
id: '2',
name: 'Svelte 官网',
url: 'https://svelte.dev',
avatar: 'https://svelte.dev/favicon.png',
description: '控制论增强的 Web 应用',
category: '技术博客'
},
{
id: '3',
name: 'Tailwind CSS',
url: 'https://tailwindcss.com',
avatar: 'https://tailwindcss.com/favicons/favicon-32x32.png',
description: '实用优先的 CSS 框架',
category: '技术博客'
}
];
export const mockSiteConfig = {
name: 'Termi',
description: '终端风格的内容平台',
author: 'InitCool',
url: 'https://termi.dev',
social: {
github: 'https://github.com',
twitter: 'https://twitter.com',
email: 'mailto:hello@termi.dev'
}
};
export const mockSystemStats = [
{ label: 'Last Update', value: '2024-03-20' },
{ label: 'Posts', value: '12' },
{ label: 'Visitors', value: '1.2k' }
];
export const mockTechStack = [
{ name: 'Astro' },
{ name: 'Svelte' },
{ name: 'Tailwind CSS' },
{ name: 'TypeScript' },
{ name: 'Vercel' }
];
export const mockHomeAboutIntro = '一名热爱技术的前端开发者,专注于构建高性能、优雅的用户界面。相信代码不仅是工具,更是一种艺术表达。';
// Helper functions
export function getPinnedPost(): Post | null {
return mockPosts.find(p => p.pinned) || null;
}
export function getRecentPosts(limit: number = 5): Post[] {
return mockPosts.slice(0, limit);
}
export function getAllPosts(): Post[] {
return mockPosts;
}
export function getPostBySlug(slug: string): Post | undefined {
return mockPosts.find(p => p.slug === slug);
}
export function getPostsByTag(tag: string): Post[] {
return mockPosts.filter(p => p.tags.includes(tag));
}
export function getPostsByCategory(category: string): Post[] {
return mockPosts.filter(p => p.category === category);
}
export function getAllCategories(): Category[] {
return mockCategories;
}
export function getAllTags(): Tag[] {
return mockTags;
}
export function getAllFriendLinks(): FriendLink[] {
return mockFriendLinks;
}
export function getSiteConfig() {
return mockSiteConfig;
}
export function getSystemStats() {
return mockSystemStats;
}
export function getTechStack() {
return mockTechStack;
}
export function getHomeAboutIntro() {
return mockHomeAboutIntro;
}

View File

@@ -0,0 +1,94 @@
/**
* Format a date string to a more readable format
*/
export function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
/**
* Truncate text to a specified length with ellipsis
*/
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
/**
* Normalize FontAwesome icon class
*/
export function normalizeFaIcon(icon: unknown): string {
const raw = typeof icon === 'string' ? icon.trim() : '';
if (!raw) return 'fa-folder';
if (raw.includes('fa-')) {
const parts = raw.split(/\s+/);
for (let i = parts.length - 1; i >= 0; i -= 1) {
const t = parts[i];
if (t?.startsWith('fa-')) return t;
}
return 'fa-folder';
}
return 'fa-folder';
}
/**
* Resolve file reference (for images)
*/
export function resolveFileRef(ref: string): string {
if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('/')) {
return ref;
}
return `/uploads/${ref}`;
}
/**
* Generate a unique ID
*/
export function generateId(): string {
return Math.random().toString(36).substring(2, 9);
}
/**
* Debounce function
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* Filter posts by type and tag
*/
export function filterPosts(
posts: Array<{
type: string;
tags: string[];
}>,
postType: string,
tag: string
): typeof posts {
return posts.filter(post => {
if (postType !== 'all' && post.type !== postType) return false;
if (tag && !post.tags.includes(tag)) return false;
return true;
});
}
/**
* Get color for post type
*/
export function getPostTypeColor(type: string): string {
return type === 'article' ? 'var(--primary)' : 'var(--secondary)';
}