初始化:Astro 站点 + Sveltia CMS 后台 + 部署配置
This commit is contained in:
39
src/components/Footer.astro
Normal file
39
src/components/Footer.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import { SITE, SOCIAL } from '../site.config';
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container inner">
|
||||
<div>
|
||||
<span class="mono">{SITE.title}</span>
|
||||
<span class="muted"> · {SITE.subtitle}</span>
|
||||
</div>
|
||||
<div class="links">
|
||||
{SOCIAL.map((s) => <a href={s.href} target="_blank" rel="noopener">{s.label}</a>)}
|
||||
<span class="muted">© {year}</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.site-footer {
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 4rem;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
.inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
84
src/components/Header.astro
Normal file
84
src/components/Header.astro
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
import { SITE, NAV } from '../site.config';
|
||||
const path = Astro.url.pathname;
|
||||
const isActive = (href: string) =>
|
||||
href === '/' ? path === '/' : path.startsWith(href);
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container bar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark">>_</span>
|
||||
<span class="brand-name">{SITE.title}</span>
|
||||
</a>
|
||||
<nav class="nav">
|
||||
{
|
||||
NAV.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class={isActive(item.href) ? 'active' : ''}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: rgba(10, 14, 20, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
.brand-mark {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent);
|
||||
}
|
||||
.brand-name {
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.nav a {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.92rem;
|
||||
position: relative;
|
||||
}
|
||||
.nav a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
.nav a.active {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.nav {
|
||||
gap: 1rem;
|
||||
}
|
||||
.nav a {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.brand-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
src/components/ProjectCard.astro
Normal file
54
src/components/ProjectCard.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
href: string;
|
||||
}
|
||||
const { title, description, category, tags, href } = Astro.props;
|
||||
---
|
||||
|
||||
<a class="card project-card" href={href}>
|
||||
<div class="top">
|
||||
<span class="tag">{category}</span>
|
||||
</div>
|
||||
<h3>{title}</h3>
|
||||
<p class="muted desc">{description}</p>
|
||||
{
|
||||
tags.length > 0 && (
|
||||
<div class="tags">
|
||||
{tags.map((t) => <span class="mono tagtext">#{t}</span>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.project-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
color: var(--text);
|
||||
height: 100%;
|
||||
}
|
||||
.project-card h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.desc {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
flex: 1;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
.tagtext {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
</style>
|
||||
31
src/content.config.ts
Normal file
31
src/content.config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
|
||||
const blog = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
draft: z.boolean().default(false),
|
||||
}),
|
||||
});
|
||||
|
||||
const projects = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/projects' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
category: z.enum(['硬件', '软件', '通信', '机器人', '其他']).default('其他'),
|
||||
tags: z.array(z.string()).default([]),
|
||||
date: z.coerce.date().optional(),
|
||||
repo: z.string().url().optional(),
|
||||
link: z.string().url().optional(),
|
||||
featured: z.boolean().default(false),
|
||||
order: z.number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog, projects };
|
||||
29
src/content/blog/hello-world.md
Normal file
29
src/content/blog/hello-world.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: 你好,世界
|
||||
description: 站点上线的第一篇文章,说明这个博客打算写什么。
|
||||
pubDate: 2026-02-01
|
||||
tags: ["随笔"]
|
||||
---
|
||||
|
||||
> 这是一篇示例文章。复制 `src/content/blog/` 下的任意 `.md` 文件,修改 frontmatter 与正文即可发布新文章。
|
||||
|
||||
## 这里会写什么
|
||||
|
||||
- 嵌入式 / 硬件设计的实践与踩坑
|
||||
- 软件与工具链
|
||||
- 通信协议与无线
|
||||
- 机器人系统
|
||||
|
||||
## Markdown 支持
|
||||
|
||||
支持标准 Markdown,包括代码块:
|
||||
|
||||
```c
|
||||
int main(void) {
|
||||
while (1) {
|
||||
// hello, embedded world
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以及列表、引用、链接等。开始写吧。
|
||||
32
src/content/projects/sample-robot-arm.md
Normal file
32
src/content/projects/sample-robot-arm.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: 示例项目 · 桌面机械臂
|
||||
description: 基于 STM32 与步进电机的四轴桌面机械臂,含上位机控制软件。
|
||||
category: 机器人
|
||||
tags: ["STM32", "运动控制", "上位机"]
|
||||
date: 2026-01-15
|
||||
featured: true
|
||||
order: 10
|
||||
# repo: https://github.com/yourname/robot-arm
|
||||
# link: https://example.com
|
||||
---
|
||||
|
||||
> 这是一篇示例项目文档。复制本文件、修改 frontmatter 与正文即可添加你自己的项目。
|
||||
|
||||
## 概述
|
||||
|
||||
简要介绍项目背景、目标与最终效果。
|
||||
|
||||
## 硬件
|
||||
|
||||
- 主控:STM32F4
|
||||
- 驱动:A4988 步进驱动
|
||||
- 结构:3D 打印 + 铝型材
|
||||
|
||||
## 软件
|
||||
|
||||
- 固件:基于 FreeRTOS 的运动插补
|
||||
- 上位机:Python + Qt
|
||||
|
||||
## 总结
|
||||
|
||||
记录踩坑、改进点与后续计划。
|
||||
44
src/layouts/BaseLayout.astro
Normal file
44
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import { SITE } from '../site.config';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
const pageTitle = title ? `${title} · ${SITE.title}` : `${SITE.title} · ${SITE.subtitle}`;
|
||||
const pageDesc = description ?? SITE.description;
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site ?? SITE.url).toString();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={pageDesc} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={pageTitle} />
|
||||
<meta property="og:description" content={pageDesc} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<link rel="alternate" type="application/rss+xml" title={SITE.title} href="/rss.xml" />
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main class="container">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
30
src/pages/404.astro
Normal file
30
src/pages/404.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="404">
|
||||
<section class="nf">
|
||||
<span class="code mono">404</span>
|
||||
<h1>页面不存在</h1>
|
||||
<p class="muted">你访问的页面找不到了。</p>
|
||||
<a class="btn btn-primary" href="/">← 返回首页</a>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.nf {
|
||||
text-align: center;
|
||||
padding: 6rem 0;
|
||||
}
|
||||
.code {
|
||||
font-size: 3rem;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
.nf h1 {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.nf .btn {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
75
src/pages/about.astro
Normal file
75
src/pages/about.astro
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
const skills = [
|
||||
{ area: '嵌入式 / 硬件', items: ['MCU (STM32 / ESP32)', 'PCB 设计', '电源与信号', 'RTOS'] },
|
||||
{ area: '软件', items: ['C / C++', 'Python', '前端 / 工具链', 'Linux'] },
|
||||
{ area: '通信', items: ['UART / SPI / I2C / CAN', '无线 (BLE / LoRa / WiFi)', '协议设计'] },
|
||||
{ area: '机器人', items: ['运动控制', '传感融合', 'ROS', '上位机'] },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout title="关于" description="关于 ShiZhui">
|
||||
<section class="page-head">
|
||||
<h1>关于</h1>
|
||||
<p class="lead muted">
|
||||
嵌入式工程师,专注电子、软件、通信与机器人方向的产品与系统开发。
|
||||
这里记录我做过的项目与一些技术思考。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="skills">
|
||||
<h2>技术方向</h2>
|
||||
<div class="grid">
|
||||
{
|
||||
skills.map((s) => (
|
||||
<div class="card">
|
||||
<h3>{s.area}</h3>
|
||||
<ul>
|
||||
{s.items.map((i) => <li>{i}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-head {
|
||||
padding: 3rem 0 1rem;
|
||||
}
|
||||
.page-head h1 {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.lead {
|
||||
max-width: 60ch;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.skills {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
.skills h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.card h3 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 1rem;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
.card ul {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.card li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
</style>
|
||||
81
src/pages/blog/[...id].astro
Normal file
81
src/pages/blog/[...id].astro
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((p) => ({
|
||||
params: { id: p.id },
|
||||
props: { post: p },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
const d = post.data;
|
||||
---
|
||||
|
||||
<BaseLayout title={d.title} description={d.description}>
|
||||
<article class="prose">
|
||||
<a href="/blog" class="back mono">← 返回博客</a>
|
||||
<h1>{d.title}</h1>
|
||||
<div class="meta mono muted">
|
||||
<time>{d.pubDate.toISOString().slice(0, 10)}</time>
|
||||
{d.updatedDate && <span>· 更新于 {d.updatedDate.toISOString().slice(0, 10)}</span>}
|
||||
</div>
|
||||
{
|
||||
d.tags.length > 0 && (
|
||||
<div class="tags mono">
|
||||
{d.tags.map((t) => <span>#{t}</span>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<hr />
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.prose {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 0;
|
||||
}
|
||||
.back {
|
||||
font-size: 0.85rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.prose h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: clamp(1.6rem, 4vw, 2.2rem);
|
||||
}
|
||||
.meta {
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.content :global(h2) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.content :global(img) {
|
||||
max-width: 100%;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.content :global(blockquote) {
|
||||
border-left: 3px solid var(--accent-dim);
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.25rem 1rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
</style>
|
||||
96
src/pages/blog/index.astro
Normal file
96
src/pages/blog/index.astro
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((p) => !p.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
---
|
||||
|
||||
<BaseLayout title="博客" description="技术博客 · 电子、软件、通信与机器人">
|
||||
<section class="page-head">
|
||||
<h1>技术博客</h1>
|
||||
<p class="muted">记录工程实践、踩坑与思考。</p>
|
||||
</section>
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<ul class="post-list">
|
||||
{posts.map((p) => (
|
||||
<li>
|
||||
<a href={`/blog/${p.id}`}>
|
||||
<div class="row">
|
||||
<span class="post-title">{p.data.title}</span>
|
||||
<time class="mono muted">
|
||||
{p.data.pubDate.toISOString().slice(0, 10)}
|
||||
</time>
|
||||
</div>
|
||||
{p.data.description && (
|
||||
<p class="muted desc">{p.data.description}</p>
|
||||
)}
|
||||
{p.data.tags.length > 0 && (
|
||||
<div class="tags mono">
|
||||
{p.data.tags.map((t) => <span>#{t}</span>)}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="muted">
|
||||
暂无文章。在 <code>src/content/blog/</code> 新建 Markdown 文件即可发布。
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-head {
|
||||
padding: 3rem 0 1.5rem;
|
||||
}
|
||||
.page-head h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
.post-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.post-list li {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.post-list a {
|
||||
display: block;
|
||||
padding: 1.1rem 0;
|
||||
color: var(--text);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
}
|
||||
.post-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 550;
|
||||
}
|
||||
.post-list a:hover .post-title {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
.row time {
|
||||
font-size: 0.82rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.desc {
|
||||
margin: 0.4rem 0 0;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.78rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
63
src/pages/contact.astro
Normal file
63
src/pages/contact.astro
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { SITE, SOCIAL } from '../site.config';
|
||||
---
|
||||
|
||||
<BaseLayout title="联系" description="联系方式">
|
||||
<section class="page-head">
|
||||
<h1>联系</h1>
|
||||
<p class="lead muted">
|
||||
欢迎就项目合作、技术交流或问题反馈与我联系。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="contact">
|
||||
<a class="card row" href={`mailto:${SITE.email}`}>
|
||||
<span class="label mono">Email</span>
|
||||
<span class="value">{SITE.email}</span>
|
||||
</a>
|
||||
{
|
||||
SOCIAL.map((s) => (
|
||||
<a class="card row" href={s.href} target="_blank" rel="noopener">
|
||||
<span class="label mono">{s.label}</span>
|
||||
<span class="value">{s.href}</span>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-head {
|
||||
padding: 3rem 0 1rem;
|
||||
}
|
||||
.page-head h1 {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.lead {
|
||||
max-width: 56ch;
|
||||
}
|
||||
.contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 560px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.label {
|
||||
color: var(--accent);
|
||||
font-size: 0.85rem;
|
||||
min-width: 70px;
|
||||
}
|
||||
.value {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.row:hover .value {
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
148
src/pages/index.astro
Normal file
148
src/pages/index.astro
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ProjectCard from '../components/ProjectCard.astro';
|
||||
import { SITE } from '../site.config';
|
||||
|
||||
const allProjects = (await getCollection('projects')).sort(
|
||||
(a, b) => b.data.order - a.data.order
|
||||
);
|
||||
const featured = allProjects.filter((p) => p.data.featured).slice(0, 4);
|
||||
const projects = featured.length > 0 ? featured : allProjects.slice(0, 4);
|
||||
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((p) => !p.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
|
||||
.slice(0, 3);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<section class="hero">
|
||||
<span class="tag">{SITE.subtitle}</span>
|
||||
<h1>构建电子、软件与机器人系统</h1>
|
||||
<p class="lead muted">
|
||||
{SITE.description}
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" href="/projects">查看项目 →</a>
|
||||
<a class="btn" href="/blog">技术博客</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="block-head">
|
||||
<h2>精选项目</h2>
|
||||
<a href="/projects" class="more mono">全部 →</a>
|
||||
</div>
|
||||
{
|
||||
projects.length > 0 ? (
|
||||
<div class="grid">
|
||||
{projects.map((p) => (
|
||||
<ProjectCard
|
||||
title={p.data.title}
|
||||
description={p.data.description}
|
||||
category={p.data.category}
|
||||
tags={p.data.tags}
|
||||
href={`/projects/${p.id}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="muted">暂无项目,去 <code>src/content/projects/</code> 添加。</p>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="block-head">
|
||||
<h2>最新文章</h2>
|
||||
<a href="/blog" class="more mono">全部 →</a>
|
||||
</div>
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<ul class="post-list">
|
||||
{posts.map((p) => (
|
||||
<li>
|
||||
<a href={`/blog/${p.id}`}>
|
||||
<span class="post-title">{p.data.title}</span>
|
||||
<time class="mono muted">
|
||||
{p.data.pubDate.toISOString().slice(0, 10)}
|
||||
</time>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="muted">暂无文章,去 <code>src/content/blog/</code> 添加。</p>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: 4rem 0 3rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
margin: 1rem 0 0.75rem;
|
||||
max-width: 18ch;
|
||||
}
|
||||
.lead {
|
||||
max-width: 56ch;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
.block {
|
||||
padding: 2.5rem 0 0;
|
||||
}
|
||||
.block-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.block-head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.more {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.post-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.post-list li {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.post-list a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.85rem 0;
|
||||
color: var(--text);
|
||||
}
|
||||
.post-list a:hover .post-title {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
.post-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
.post-list time {
|
||||
font-size: 0.82rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
87
src/pages/projects/[...id].astro
Normal file
87
src/pages/projects/[...id].astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const projects = await getCollection('projects');
|
||||
return projects.map((p) => ({
|
||||
params: { id: p.id },
|
||||
props: { project: p },
|
||||
}));
|
||||
}
|
||||
|
||||
const { project } = Astro.props;
|
||||
const { Content } = await render(project);
|
||||
const d = project.data;
|
||||
---
|
||||
|
||||
<BaseLayout title={d.title} description={d.description}>
|
||||
<article class="prose">
|
||||
<a href="/projects" class="back mono">← 返回项目</a>
|
||||
<div class="meta">
|
||||
<span class="tag">{d.category}</span>
|
||||
{d.date && <time class="mono muted">{d.date.toISOString().slice(0, 10)}</time>}
|
||||
</div>
|
||||
<h1>{d.title}</h1>
|
||||
<p class="lead muted">{d.description}</p>
|
||||
{
|
||||
d.tags.length > 0 && (
|
||||
<div class="tags mono">
|
||||
{d.tags.map((t) => <span>#{t}</span>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(d.repo || d.link) && (
|
||||
<div class="links">
|
||||
{d.repo && <a class="btn" href={d.repo} target="_blank" rel="noopener">源码</a>}
|
||||
{d.link && <a class="btn btn-primary" href={d.link} target="_blank" rel="noopener">访问</a>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<hr />
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.prose {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 0;
|
||||
}
|
||||
.back {
|
||||
font-size: 0.85rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.meta time {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.prose h1 {
|
||||
margin: 0.25rem 0 0.5rem;
|
||||
}
|
||||
.lead {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
106
src/pages/projects/index.astro
Normal file
106
src/pages/projects/index.astro
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ProjectCard from '../../components/ProjectCard.astro';
|
||||
|
||||
const projects = (await getCollection('projects')).sort(
|
||||
(a, b) => b.data.order - a.data.order
|
||||
);
|
||||
|
||||
const categories = ['全部', '硬件', '软件', '通信', '机器人', '其他'];
|
||||
---
|
||||
|
||||
<BaseLayout title="项目" description="电子、软件、通信与机器人方向的项目展示">
|
||||
<section class="page-head">
|
||||
<h1>项目</h1>
|
||||
<p class="muted">电子工程、软件、通信与机器人方向的作品与产品。</p>
|
||||
</section>
|
||||
|
||||
{
|
||||
projects.length > 0 ? (
|
||||
<>
|
||||
<div class="filters mono" id="filters">
|
||||
{categories.map((c) => (
|
||||
<button class="filter" data-cat={c}>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div class="grid" id="grid">
|
||||
{projects.map((p) => (
|
||||
<div class="grid-item" data-cat={p.data.category}>
|
||||
<ProjectCard
|
||||
title={p.data.title}
|
||||
description={p.data.description}
|
||||
category={p.data.category}
|
||||
tags={p.data.tags}
|
||||
href={`/projects/${p.id}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p class="muted">
|
||||
暂无项目。在 <code>src/content/projects/</code> 新建 Markdown 文件即可添加。
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const buttons = document.querySelectorAll<HTMLButtonElement>('.filter');
|
||||
const items = document.querySelectorAll<HTMLElement>('.grid-item');
|
||||
buttons[0]?.classList.add('active');
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
buttons.forEach((b) => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const cat = btn.dataset.cat;
|
||||
items.forEach((item) => {
|
||||
item.style.display =
|
||||
cat === '全部' || item.dataset.cat === cat ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-head {
|
||||
padding: 3rem 0 1.5rem;
|
||||
}
|
||||
.page-head h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.filter {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
border-radius: 999px;
|
||||
padding: 0.3rem 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.filter:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
.filter.active {
|
||||
color: var(--accent-strong);
|
||||
border-color: var(--accent-dim);
|
||||
background: rgba(57, 208, 216, 0.1);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
21
src/pages/rss.xml.js
Normal file
21
src/pages/rss.xml.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE } from '../site.config';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = (await getCollection('blog'))
|
||||
.filter((p) => !p.data.draft)
|
||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
return rss({
|
||||
title: SITE.title,
|
||||
description: SITE.description,
|
||||
site: context.site ?? SITE.url,
|
||||
items: posts.map((post) => ({
|
||||
title: post.data.title,
|
||||
pubDate: post.data.pubDate,
|
||||
description: post.data.description ?? '',
|
||||
link: `/blog/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
22
src/site.config.ts
Normal file
22
src/site.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 站点全局配置 —— 改这里即可调整站点信息、导航、社交链接
|
||||
export const SITE = {
|
||||
title: 'ShiZhui',
|
||||
subtitle: '电子 · 软件 · 通信 · 机器人',
|
||||
description:
|
||||
'ShiZhui —— 电子工程、软件、通信与机器人方向的项目展示与技术博客。',
|
||||
author: 'ShiZhui',
|
||||
url: 'https://shizhui.xyz',
|
||||
email: 'hello@shizhui.xyz',
|
||||
};
|
||||
|
||||
export const NAV = [
|
||||
{ label: '首页', href: '/' },
|
||||
{ label: '项目', href: '/projects' },
|
||||
{ label: '博客', href: '/blog' },
|
||||
{ label: '关于', href: '/about' },
|
||||
{ label: '联系', href: '/contact' },
|
||||
];
|
||||
|
||||
export const SOCIAL: { label: string; href: string }[] = [
|
||||
// { label: 'GitHub', href: 'https://github.com/yourname' },
|
||||
];
|
||||
153
src/styles/global.css
Normal file
153
src/styles/global.css
Normal file
@@ -0,0 +1,153 @@
|
||||
:root {
|
||||
/* 暗色调 · 工程/极简科技风 */
|
||||
--bg: #0a0e14;
|
||||
--bg-elev: #11161f;
|
||||
--bg-elev-2: #161d29;
|
||||
--border: #232c3b;
|
||||
--border-strong: #2f3a4d;
|
||||
|
||||
--text: #d6deeb;
|
||||
--text-dim: #8a97ad;
|
||||
--text-faint: #5b6678;
|
||||
|
||||
--accent: #39d0d8; /* 青色科技感 */
|
||||
--accent-strong: #5ce1e6;
|
||||
--accent-dim: #1f6b6f;
|
||||
--warn: #f2b757;
|
||||
|
||||
--radius: 8px;
|
||||
--radius-lg: 14px;
|
||||
--maxw: 1080px;
|
||||
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono",
|
||||
Menlo, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 80% -10%, rgba(57, 208, 216, 0.06), transparent 60%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
line-height: 1.25;
|
||||
font-weight: 650;
|
||||
color: #eef3fb;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
background: var(--bg-elev-2);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--bg-elev) !important;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: var(--maxw);
|
||||
margin: 0 auto;
|
||||
padding: 0 1.25rem;
|
||||
}
|
||||
|
||||
/* 通用工具类 */
|
||||
.muted { color: var(--text-dim); }
|
||||
.mono { font-family: var(--font-mono); }
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--accent);
|
||||
background: rgba(57, 208, 216, 0.08);
|
||||
border: 1px solid var(--accent-dim);
|
||||
border-radius: 999px;
|
||||
padding: 0.15rem 0.6rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text);
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.55rem 1rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
.btn-primary {
|
||||
background: rgba(57, 208, 216, 0.1);
|
||||
border-color: var(--accent-dim);
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem 1.35rem;
|
||||
transition: border-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: var(--border-strong);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(57, 208, 216, 0.25);
|
||||
}
|
||||
Reference in New Issue
Block a user