Compare commits
10 Commits
123b3edc64
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d255d0634b | ||
|
|
d9a5945b66 | ||
|
|
340c3db383 | ||
|
|
eb6bef3726 | ||
|
|
45bdc497d8 | ||
|
|
6327cf4d47 | ||
|
|
8c7c6da03b | ||
|
|
ff9dde98c7 | ||
|
|
71d8af996a | ||
|
|
014430e1b7 |
BIN
.playwright-mcp/page-2026-03-17T05-13-50-415Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-17T05-13-50-415Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
@@ -31,6 +31,16 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
|
build: {
|
||||||
|
cssCodeSplit: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vendor-react': ['react', 'react-dom'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
// The default locale to fall back to if a page isn't available in the active locale
|
// The default locale to fall back to if a page isn't available in the active locale
|
||||||
|
|||||||
BIN
public/fonts/archivo-400.ttf
Normal file
BIN
public/fonts/archivo-400.ttf
Normal file
Binary file not shown.
BIN
public/fonts/archivo-400.woff2
Normal file
BIN
public/fonts/archivo-400.woff2
Normal file
Binary file not shown.
BIN
public/fonts/archivo-700.ttf
Normal file
BIN
public/fonts/archivo-700.ttf
Normal file
Binary file not shown.
BIN
public/fonts/archivo-700.woff2
Normal file
BIN
public/fonts/archivo-700.woff2
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-400.ttf
Normal file
BIN
public/fonts/space-grotesk-400.ttf
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-400.woff2
Normal file
BIN
public/fonts/space-grotesk-400.woff2
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-700.ttf
Normal file
BIN
public/fonts/space-grotesk-700.ttf
Normal file
Binary file not shown.
BIN
public/fonts/space-grotesk-700.woff2
Normal file
BIN
public/fonts/space-grotesk-700.woff2
Normal file
Binary file not shown.
13
public/robots.txt
Normal file
13
public/robots.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# robots.txt for zhaoguiyang.com
|
||||||
|
# https://zhaoguiyang.com
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap
|
||||||
|
Sitemap: https://zhaoguiyang.com/sitemap-index.xml
|
||||||
|
|
||||||
|
# Disallow admin/private areas (if any)
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /_astro/
|
||||||
|
Disallow: /assets/
|
||||||
@@ -29,6 +29,8 @@ export default function AuthorCard({ lang, author }: AuthorCardProps) {
|
|||||||
<img
|
<img
|
||||||
src={authorInfo.avatar}
|
src={authorInfo.avatar}
|
||||||
alt={authorInfo.name}
|
alt={authorInfo.name}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
className="w-full h-full rounded-full object-cover"
|
className="w-full h-full rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useTranslations } from "@/i18n/utils";
|
import { useTranslations } from "@/i18n/utils";
|
||||||
import type { Lang } from "@/types/i18n";
|
import type { Lang } from "@/types/i18n";
|
||||||
import { personalInfo } from "@/lib/data/index";
|
import { personalInfo } from "@/lib/data/index";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { defaultLang } from "@/i18n/ui";
|
import { defaultLang } from "@/i18n/ui";
|
||||||
import Container from "./ui/Container.tsx";
|
import Container from "./ui/Container.tsx";
|
||||||
@@ -21,60 +20,38 @@ export default function Footer({ lang: propLang }: FooterProps) {
|
|||||||
return (
|
return (
|
||||||
<footer className="border-t border-primary/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
|
<footer className="border-t border-primary/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
|
||||||
<Container>
|
<Container>
|
||||||
<motion.div
|
<div
|
||||||
className="flex flex-col md:flex-row justify-between items-center"
|
className="flex flex-col md:flex-row justify-between items-center"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
>
|
||||||
<motion.div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<motion.p
|
<p
|
||||||
className="text-sm text-muted-foreground text-center md:text-left"
|
className="text-sm text-muted-foreground text-center md:text-left hover:scale-[1.01] transition-transform"
|
||||||
whileHover={{ scale: 1.01 }}
|
|
||||||
>
|
>
|
||||||
© {new Date().getFullYear()} { personalInfo.name }. {t('footer.rights')} ✨
|
© {new Date().getFullYear()} { personalInfo.name }. {t('footer.rights')} ✨
|
||||||
</motion.p>
|
</p>
|
||||||
<motion.p
|
<p
|
||||||
className="text-sm text-primary font-medium text-center md:text-left"
|
className="text-sm text-primary font-medium text-center md:text-left hover:scale-[1.01] transition-transform"
|
||||||
whileHover={{ scale: 1.01 }}
|
|
||||||
>
|
>
|
||||||
{lang === 'zh' ? '正在招聘远程工程师或寻求项目合作?欢迎联系我。' : 'Hiring for a remote engineering role or looking for project collaboration? Let’s connect.'}
|
{lang === 'zh' ? '正在招聘远程工程师或寻求项目合作?欢迎联系我。' : 'Hiring for a remote engineering role or looking for project collaboration? Let’s connect.'}
|
||||||
</motion.p>
|
</p>
|
||||||
</motion.div>
|
</div>
|
||||||
<motion.p
|
<p
|
||||||
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left"
|
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left hover:scale-[1.01] transition-transform"
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
whileInView={{ opacity: 1 }}
|
|
||||||
transition={{ delay: 0.2, duration: 0.5 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
whileHover={{ scale: 1.01 }}
|
|
||||||
>
|
>
|
||||||
Built with{" "}
|
Built with{" "}
|
||||||
<motion.span
|
<span
|
||||||
className="inline-block"
|
className="inline-block hover:rotate-360 transition-transform duration-500"
|
||||||
initial={{ rotate: 0 }}
|
|
||||||
whileHover={{ rotate: 360 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
>
|
||||||
💻
|
💻
|
||||||
</motion.span>{" "}
|
</span>{" "}
|
||||||
and{" "}
|
and{" "}
|
||||||
<motion.span
|
<span
|
||||||
className="inline-block"
|
className="inline-block animate-pulse"
|
||||||
animate={{
|
|
||||||
scale: [1, 1.2, 1],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
repeat: Infinity,
|
|
||||||
repeatType: "reverse",
|
|
||||||
duration: 1.5,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
❤️
|
❤️
|
||||||
</motion.span>
|
</span>
|
||||||
</motion.p>
|
</p>
|
||||||
</motion.div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useState, useEffect } from "react";
|
|||||||
import { Menu, X, Home, Rocket, PenTool, User, Briefcase, Clock3 } from "lucide-react";
|
import { Menu, X, Home, Rocket, PenTool, User, Briefcase, Clock3 } from "lucide-react";
|
||||||
import { defaultLang } from "@/i18n/ui";
|
import { defaultLang } from "@/i18n/ui";
|
||||||
import { type GlassHeaderProps } from "@/types";
|
import { type GlassHeaderProps } from "@/types";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
||||||
@@ -98,7 +97,7 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.header
|
<header
|
||||||
className={`fixed top-0 z-50 w-full transition-all duration-300 ${
|
className={`fixed top-0 z-50 w-full transition-all duration-300 ${
|
||||||
isScrolled
|
isScrolled
|
||||||
? 'backdrop-blur-xl backdrop-saturate-150 bg-white/70 dark:bg-black/70 border-b border-border/50 shadow-lg shadow-black/5 dark:shadow-primary/5'
|
? 'backdrop-blur-xl backdrop-saturate-150 bg-white/70 dark:bg-black/70 border-b border-border/50 shadow-lg shadow-black/5 dark:shadow-primary/5'
|
||||||
@@ -106,10 +105,8 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Container className="p-4 flex justify-between items-center">
|
<Container className="p-4 flex justify-between items-center">
|
||||||
<motion.a
|
<a
|
||||||
whileHover={{ scale: 1.05 }}
|
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80 hover:scale-105 active:scale-95"
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80"
|
|
||||||
href={getLocalizedPath('/', lang)}
|
href={getLocalizedPath('/', lang)}
|
||||||
>
|
>
|
||||||
<div className="w-6 h-6 mr-2 flex items-center justify-center">
|
<div className="w-6 h-6 mr-2 flex items-center justify-center">
|
||||||
@@ -119,14 +116,14 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="gradient-text font-bold">{personalInfo.name}</span>
|
<span className="gradient-text font-bold">{personalInfo.name}</span>
|
||||||
</motion.a>
|
</a>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
|
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const active = isActive(item.href);
|
const active = isActive(item.href);
|
||||||
return (
|
return (
|
||||||
<motion.a
|
<a
|
||||||
key={item.key}
|
key={item.key}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -135,34 +132,32 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
|||||||
? "text-primary bg-primary/5 font-semibold"
|
? "text-primary bg-primary/5 font-semibold"
|
||||||
: "text-foreground/70 hover:text-primary"
|
: "text-foreground/70 hover:text-primary"
|
||||||
)}
|
)}
|
||||||
whileHover={{ y: -2 }}
|
|
||||||
>
|
>
|
||||||
<item.icon size={16} />
|
<item.icon size={16} />
|
||||||
{t(item.key as any)}
|
{t(item.key as any)}
|
||||||
</motion.a>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{/* Language Switcher added here */}
|
{/* Language Switcher added here */}
|
||||||
<motion.div>
|
<div>
|
||||||
<LanguageSwitcher lang={lang} />
|
<LanguageSwitcher lang={lang} />
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
<motion.div>
|
<div>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<motion.button
|
<button
|
||||||
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md"
|
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md active:scale-90"
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
>
|
>
|
||||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||||
</motion.button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
@@ -179,7 +174,7 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
|||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const active = isActive(item.href);
|
const active = isActive(item.href);
|
||||||
return (
|
return (
|
||||||
<motion.a
|
<a
|
||||||
key={item.key}
|
key={item.key}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -192,12 +187,12 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
|
|||||||
>
|
>
|
||||||
<item.icon size={18} />
|
<item.icon size={18} />
|
||||||
{t(item.key as any)}
|
{t(item.key as any)}
|
||||||
</motion.a>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { getLocalizedPath } from "@/i18n/utils";
|
|||||||
import type { Lang } from "@/types/i18n";
|
import type { Lang } from "@/types/i18n";
|
||||||
import { languages as i18nLanguages, defaultLang } from "@/i18n/ui";
|
import { languages as i18nLanguages, defaultLang } from "@/i18n/ui";
|
||||||
import { Check, ChevronDown } from "lucide-react";
|
import { Check, ChevronDown } from "lucide-react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import { type LanguageSwitcherProps } from "@/types";
|
import { type LanguageSwitcherProps } from "@/types";
|
||||||
|
|
||||||
const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({
|
const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({
|
||||||
@@ -65,45 +64,38 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<motion.button
|
<button
|
||||||
onClick={toggleOpen}
|
onClick={toggleOpen}
|
||||||
className="flex items-center p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-sm"
|
className="flex items-center p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-sm active:scale-95"
|
||||||
aria-label={`Change language, current language is ${selectedLanguage.name}`}
|
aria-label={`Change language, current language is ${selectedLanguage.name}`}
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
>
|
||||||
<span className="mr-1.5">{selectedLanguage.icon}</span>
|
<span className="mr-1.5">{selectedLanguage.icon}</span>
|
||||||
<span className="hidden sm:inline">{selectedLanguage.name}</span>
|
<span className="hidden sm:inline">{selectedLanguage.name}</span>
|
||||||
<span className="sm:hidden">{selectedLanguage.code.toUpperCase()}</span>
|
<span className="sm:hidden">{selectedLanguage.code.toUpperCase()}</span>
|
||||||
<ChevronDown size={16} className={`ml-1.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
|
<ChevronDown size={16} className={`ml-1.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
|
||||||
</motion.button>
|
</button>
|
||||||
<AnimatePresence>
|
{isOpen && (
|
||||||
{isOpen && (
|
<div
|
||||||
<motion.div
|
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20 animate-in fade-in slide-in-from-top-2"
|
||||||
initial={{ opacity: 0, y: -10 }}
|
>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||||
exit={{ opacity: 0, y: -10 }}
|
{availableLanguages.map((lang) => (
|
||||||
transition={{ duration: 0.2 }}
|
<button
|
||||||
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20"
|
key={lang.code}
|
||||||
>
|
onClick={() => handleSelectLanguage(lang)}
|
||||||
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
|
||||||
{availableLanguages.map((lang) => (
|
role="menuitem"
|
||||||
<button
|
>
|
||||||
key={lang.code}
|
<span className="flex items-center">
|
||||||
onClick={() => handleSelectLanguage(lang)}
|
<span className="mr-2">{lang.icon}</span>
|
||||||
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
|
{lang.name}
|
||||||
role="menuitem"
|
</span>
|
||||||
>
|
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
|
||||||
<span className="flex items-center">
|
</button>
|
||||||
<span className="mr-2">{lang.icon}</span>
|
))}
|
||||||
{lang.name}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
|
)}
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
|
|||||||
<img
|
<img
|
||||||
src={post.image}
|
src={post.image}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
class="w-full h-48 md:h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
class="w-full h-48 md:h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-background/50 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-background/50 to-transparent"></div>
|
||||||
@@ -92,7 +94,7 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
|
|||||||
<div class="p-6 flex-1 flex flex-col justify-between">
|
<div class="p-6 flex-1 flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-card-foreground mb-3 group-hover:text-primary transition-colors duration-200">
|
<h2 class="text-xl font-bold text-card-foreground mb-3 group-hover:text-primary transition-colors duration-200">
|
||||||
<a href={`${postBaseUrl}${post.slug}`}>
|
<a href={`${postBaseUrl}${post.slug}`} data-astro-reload>
|
||||||
{post.title}
|
{post.title}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -135,6 +137,7 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href={`${postBaseUrl}${post.slug}`}
|
href={`${postBaseUrl}${post.slug}`}
|
||||||
|
data-astro-reload
|
||||||
class="text-primary hover:text-primary font-medium flex items-center group"
|
class="text-primary hover:text-primary font-medium flex items-center group"
|
||||||
>
|
>
|
||||||
{readMoreText}
|
{readMoreText}
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ export default function Comments({ lang = 'en', ...props }: CommentsProps) {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const walineLang = lang === 'zh' ? 'zh-CN' : 'en';
|
const walineLang = lang === 'zh' ? 'zh-CN' : 'en';
|
||||||
|
const getTheme = () => (document.documentElement.classList.contains('dark') ? 'html.dark' : false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
|
|
||||||
|
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
const postsMatch = pathname.match(/\/posts\/([^/]+)/);
|
const postsMatch = pathname.match(/\/posts\/([^/]+)/);
|
||||||
const path = postsMatch ? postsMatch[1] : pathname;
|
const path = postsMatch ? postsMatch[1] : pathname;
|
||||||
@@ -34,18 +33,6 @@ export default function Comments({ lang = 'en', ...props }: CommentsProps) {
|
|||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => walineInstanceRef.current?.destroy();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
|
|
||||||
|
|
||||||
walineInstanceRef.current?.update({
|
|
||||||
...props,
|
|
||||||
lang: walineLang,
|
|
||||||
dark: getTheme(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleThemeChange = () => {
|
const handleThemeChange = () => {
|
||||||
walineInstanceRef.current?.update({
|
walineInstanceRef.current?.update({
|
||||||
...props,
|
...props,
|
||||||
@@ -57,8 +44,22 @@ export default function Comments({ lang = 'en', ...props }: CommentsProps) {
|
|||||||
const observer = new MutationObserver(handleThemeChange);
|
const observer = new MutationObserver(handleThemeChange);
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => {
|
||||||
}, [lang, props, walineLang]);
|
observer.disconnect();
|
||||||
|
const instance = walineInstanceRef.current;
|
||||||
|
walineInstanceRef.current = null;
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
try {
|
||||||
|
instance.destroy();
|
||||||
|
} catch {
|
||||||
|
if (containerRef.current) {
|
||||||
|
containerRef.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [props, walineLang]);
|
||||||
|
|
||||||
return <div ref={containerRef} className="waline-container" />;
|
return <div ref={containerRef} className="waline-container" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const title = lang === 'zh' ? '目录' : 'Table of Contents';
|
|||||||
</svg>
|
</svg>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<ScrollArea className="w-full" client:load>
|
<ScrollArea className="w-full" client:visible>
|
||||||
<div class="text-neutral-500 dark:text-neutral-300 pr-4">
|
<div class="text-neutral-500 dark:text-neutral-300 pr-4">
|
||||||
<ul id="toc-list" class="leading-relaxed text-sm sm:text-base border-l dark:border-neutral-500/20 border-blacktext/20 mt-4">
|
<ul id="toc-list" class="leading-relaxed text-sm sm:text-base border-l dark:border-neutral-500/20 border-blacktext/20 mt-4">
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
|
|||||||
|
|
||||||
<!-- Comments Section -->
|
<!-- Comments Section -->
|
||||||
<div class="mt-10 sm:mt-16 border-t border-border pt-10">
|
<div class="mt-10 sm:mt-16 border-t border-border pt-10">
|
||||||
<Comments client:only="react" lang={lang} />
|
<Comments client:visible lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Author Card moved to bottom with enhanced styling -->
|
<!-- Author Card moved to bottom with enhanced styling -->
|
||||||
|
|||||||
@@ -4,17 +4,27 @@ import BackToTop from "@/components/ui/back-to-top";
|
|||||||
import { useTranslations } from "@/i18n/utils";
|
import { useTranslations } from "@/i18n/utils";
|
||||||
import type { Lang } from "@/types/i18n";
|
import type { Lang } from "@/types/i18n";
|
||||||
import { defaultLang } from "@/i18n/ui";
|
import { defaultLang } from "@/i18n/ui";
|
||||||
|
import { personalInfo } from "@/lib/data";
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
articleDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
const { title = "Joey Z. - Portfolio", description = "Engineering-focused personal website" } =
|
const isZh = lang === 'zh';
|
||||||
Astro.props;
|
|
||||||
const t = useTranslations(lang);
|
const t = useTranslations(lang);
|
||||||
|
const { title, description, image, articleDate } = Astro.props;
|
||||||
|
const siteTitle = t("site.title");
|
||||||
|
const fullTitle = title ? `${title} | ${siteTitle}` : siteTitle;
|
||||||
|
const siteUrl = "https://zhaoguiyang.com";
|
||||||
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1200&h=630&fit=crop";
|
||||||
|
|
||||||
|
const currentUrl = Astro.url.href;
|
||||||
|
const ogImage = image || defaultImage;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -23,9 +33,35 @@ const t = useTranslations(lang);
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<!-- Preload critical fonts - using WOFF2 for better compression -->
|
||||||
|
<link rel="preload" href="/fonts/archivo-700.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
<link rel="preload" href="/fonts/space-grotesk-400.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<meta name="description" content={description} />
|
|
||||||
<title>{title} | {t("site.title")}</title>
|
<!-- Basic SEO -->
|
||||||
|
<meta name="description" content={description || "AI Full-stack Engineer - 8 years building enterprise systems, financial platforms, and blockchain infrastructure."} />
|
||||||
|
<link rel="canonical" href={currentUrl} />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content={articleDate ? "article" : "website"} />
|
||||||
|
<meta property="og:url" content={currentUrl} />
|
||||||
|
<meta property="og:title" content={fullTitle} />
|
||||||
|
<meta property="og:description" content={description || "AI Full-stack Engineer - 8 years building enterprise systems, financial platforms, and blockchain infrastructure."} />
|
||||||
|
<meta property="og:image" content={ogImage} />
|
||||||
|
<meta property="og:site_name" content={siteTitle} />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:url" content={currentUrl} />
|
||||||
|
<meta name="twitter:title" content={fullTitle} />
|
||||||
|
<meta name="twitter:description" content={description || "AI Full-stack Engineer - 8 years building enterprise systems, financial platforms, and blockchain infrastructure."} />
|
||||||
|
<meta name="twitter:image" content={ogImage} />
|
||||||
|
|
||||||
|
<!-- Article specific -->
|
||||||
|
{articleDate && <meta property="article:published_time" content={articleDate} />}
|
||||||
|
|
||||||
|
<title>{fullTitle}</title>
|
||||||
|
|
||||||
<!-- View Transitions for smooth page transitions -->
|
<!-- View Transitions for smooth page transitions -->
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
<meta name="view-transition" content="same-origin" />
|
<meta name="view-transition" content="same-origin" />
|
||||||
@@ -38,6 +74,14 @@ const t = useTranslations(lang);
|
|||||||
<body
|
<body
|
||||||
class="min-h-screen bg-background font-sans antialiased selection:bg-primary/20 selection:text-primary"
|
class="min-h-screen bg-background font-sans antialiased selection:bg-primary/20 selection:text-primary"
|
||||||
>
|
>
|
||||||
|
<!-- Skip link for accessibility -->
|
||||||
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg focus:font-semibold focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
{isZh ? '跳到主要内容' : 'Skip to main content'}
|
||||||
|
</a>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 -z-10 h-full w-full bg-background"
|
class="fixed inset-0 -z-10 h-full w-full bg-background"
|
||||||
>
|
>
|
||||||
@@ -46,7 +90,7 @@ const t = useTranslations(lang);
|
|||||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.05),transparent_40%)]"></div>
|
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.05),transparent_40%)]"></div>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
<BackToTop client:load />
|
<BackToTop client:idle />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const heroPrinciples: LocalizedText[] = [
|
|||||||
zh: '目前寻求远程工作机会,如你正在招聘,欢迎联系我。',
|
zh: '目前寻求远程工作机会,如你正在招聘,欢迎联系我。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'Evolving from frontend toward full-stack, and building as an AI-era product engineer with an always-learning mindset.',
|
en: 'Started as a frontend dev, now building full-stack skills while embracing AI as a productivity tool.',
|
||||||
zh: '我正在从前端走向全栈,持续进化为 AI 时代的产品工程师,不给自己设限。',
|
zh: '我正在从前端走向全栈,持续进化为 AI 时代的产品工程师,不给自己设限。',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -90,15 +90,15 @@ export const brandProofPoints: LocalizedText[] = [
|
|||||||
zh: '8 年企业级、金融科技、区块链系统经验',
|
zh: '8 年企业级、金融科技、区块链系统经验',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'Hands-on lead for architecture, core modules, and engineering workflows',
|
en: 'Led architecture and core modules for multiple products',
|
||||||
zh: '可主导架构、核心模块和工程化流程落地',
|
zh: '可主导架构、核心模块和工程化流程落地',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'Strong in complex interactions, visual systems, and high-volume data workflows',
|
en: 'Good at complex interactions, visual systems, and data-heavy workflows',
|
||||||
zh: '擅长复杂交互、可视化系统和大数据量业务流程',
|
zh: '擅长复杂交互、可视化系统和大数据量业务流程',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'Combines AI tooling with delivery to improve speed and clarity',
|
en: 'Using AI tools to speed up development and understand codebases faster',
|
||||||
zh: '将 AI 工具用于研发提效、项目理解与文档沉淀',
|
zh: '将 AI 工具用于研发提效、项目理解与文档沉淀',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -110,7 +110,7 @@ export const brandMethodCards: LocalizedCard[] = [
|
|||||||
zh: '先定义问题',
|
zh: '先定义问题',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
en: 'I align business context, technical boundaries, and delivery constraints before implementation.',
|
en: 'I make sure we understand the problem before writing code — business context, technical limits, and timeline all need to be clear.',
|
||||||
zh: '编码之前先对齐业务上下文、技术边界和交付约束。',
|
zh: '编码之前先对齐业务上下文、技术边界和交付约束。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -120,7 +120,7 @@ export const brandMethodCards: LocalizedCard[] = [
|
|||||||
zh: '为迭代设计架构',
|
zh: '为迭代设计架构',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
en: 'I build module boundaries and component conventions so systems can evolve safely.',
|
en: 'I design module boundaries and component patterns so the system can grow without becoming a mess.',
|
||||||
zh: '通过模块边界与组件规范,确保系统能稳定演进。',
|
zh: '通过模块边界与组件规范,确保系统能稳定演进。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -130,7 +130,7 @@ export const brandMethodCards: LocalizedCard[] = [
|
|||||||
zh: '透明化执行',
|
zh: '透明化执行',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
en: 'I keep progress visible via milestones, docs, and explicit decision records.',
|
en: 'I share progress regularly — milestones, docs, and decisions are all visible to everyone involved.',
|
||||||
zh: '通过里程碑、文档和决策记录,让推进过程可视、可追踪。',
|
zh: '通过里程碑、文档和决策记录,让推进过程可视、可追踪。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -267,35 +267,35 @@ export const brandTimeline: TimelineItem[] = [
|
|||||||
|
|
||||||
export const aboutNarrative: LocalizedText[] = [
|
export const aboutNarrative: LocalizedText[] = [
|
||||||
{
|
{
|
||||||
en: 'I focus on complex business systems where architecture and long-term maintainability are critical.',
|
en: '8 years of frontend experience, mostly working on complex business systems — fintech, enterprise tools, and products with heavy interactions. I care about code that survives the first release.',
|
||||||
zh: '我长期专注复杂业务系统,核心关注点是架构质量和长期可维护性。',
|
zh: '做了 8 年前端,大部分时间在跟复杂业务系统打交道——金融、企业后台、交互密集型产品。代码要能经得起后续迭代,这是我关心的。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'My strengths combine frontend architecture, engineering processes, and cross-role collaboration.',
|
en: 'Spent a lot of time building from scratch and extracting reusable patterns. Led frontend architecture for 3+ core products, set up internal component libraries, and pushed engineering standards across teams.',
|
||||||
zh: '我的优势在于前端架构能力、工程化推进能力和跨角色协作。',
|
zh: '从 0 到 1 搭过不少项目,也从业务里抽过不少通用组件。带过 3+ 核心产品的前端架构,做过内部组件库,推过团队工程规范。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'This website is my long-term knowledge base for projects, methods, and practical lessons.',
|
en: 'Recently started bringing AI into my workflow — not to chase trends, but because it genuinely helps me read code, analyze logic, and write docs faster.',
|
||||||
zh: '这个网站会持续沉淀项目案例、工作方法与可复用经验。',
|
zh: '这两年在把 AI 融入日常开发流程,不是赶潮流,是真的拿它来帮忙读代码、分析逻辑、写文档。确实省时间。',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const aboutCapabilities: LocalizedText[] = [
|
export const aboutCapabilities: LocalizedText[] = [
|
||||||
{
|
{
|
||||||
en: 'Complex frontend system architecture and module governance',
|
en: 'Frontend architecture & module design for complex products',
|
||||||
zh: '复杂前端系统架构与模块治理',
|
zh: '复杂产品的模块设计与前端架构',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'Data visualization and real-time interaction system implementation',
|
en: 'Component libraries & engineering standardization (Vite / Webpack)',
|
||||||
zh: '数据可视化与实时交互系统开发',
|
zh: '组件封装与工程化体系建设',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'From-zero-to-one product frontend setup and engineering standardization',
|
en: 'Complex interactions, data visualization, and real-time systems',
|
||||||
zh: '从 0 到 1 前端体系建设与工程规范落地',
|
zh: '复杂交互、可视化与实时数据处理',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
en: 'AI-assisted code understanding, documentation, and troubleshooting',
|
en: 'AI-enhanced development workflow & cross-team collaboration',
|
||||||
zh: 'AI 辅助代码理解、文档沉淀与问题排查',
|
zh: 'AI 提效开发与跨角色协作',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -382,6 +382,16 @@ export const collaborationProcess: LocalizedText[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const collaborationFaq: QAItem[] = [
|
export const collaborationFaq: QAItem[] = [
|
||||||
|
{
|
||||||
|
question: {
|
||||||
|
en: 'Payment terms?',
|
||||||
|
zh: '付款方式?',
|
||||||
|
},
|
||||||
|
answer: {
|
||||||
|
en: '40% upfront, 40% at mid-point review, 20% upon completion.',
|
||||||
|
zh: '40% 预付款,40% 中期验收,20% 尾款。',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
question: {
|
question: {
|
||||||
en: 'Do you only do project-based work?',
|
en: 'Do you only do project-based work?',
|
||||||
|
|||||||
@@ -159,32 +159,109 @@ const sharedCases: Project[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'elynd',
|
id: 'elynd',
|
||||||
featured: false,
|
featured: true,
|
||||||
type: 'experiment',
|
type: 'product',
|
||||||
status: 'building',
|
status: 'building',
|
||||||
role: 'Founder & Developer',
|
role: 'Founder & Developer',
|
||||||
impact: 'Exploring AI-assisted development workflows in production-like scenarios',
|
impact: 'Built an AI-powered English reading and learning platform for language learners',
|
||||||
systemType: 'AI-assisted workspace',
|
systemType: 'AI-powered English reading & learning platform',
|
||||||
context: 'An open workspace product exploring practical AI collaboration in software development.',
|
context: 'An AI-assisted English reading learning tool that combines reading, listening, word lookup, and AI Q&A to help users practice low-barrier language input.',
|
||||||
title: 'Elynd',
|
title: 'Elynd - AI English Learning Platform',
|
||||||
icon: 'Zap',
|
icon: 'BookOpen',
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
image: {
|
image: {
|
||||||
bg: 'from-purple-500/20 to-blue-500/20',
|
bg: 'from-purple-500/20 to-blue-500/20',
|
||||||
hover: 'from-purple-500/30 to-blue-500/30',
|
hover: 'from-purple-500/30 to-blue-500/30',
|
||||||
text: 'text-purple-500',
|
text: 'text-purple-500',
|
||||||
},
|
},
|
||||||
challenges: ['Designing useful AI workflows without adding process burden'],
|
challenges: [
|
||||||
responsibilities: ['Product exploration and end-to-end implementation'],
|
'Designing a smooth graded reading experience with content difficulty adaptation',
|
||||||
outcomes: ['Validated several practical AI-assisted development patterns'],
|
'Implementing real-time TTS audio playback with Azure Speech SDK',
|
||||||
description: [
|
'Building an efficient word lookup system with instant definitions',
|
||||||
'An open AI workspace for builders.',
|
'Creating AI-powered Q&A that understands reading context',
|
||||||
'Used as an R&D track, not the primary professional narrative.',
|
|
||||||
],
|
],
|
||||||
coverImage: '',
|
responsibilities: [
|
||||||
coverImageAlt: 'Elynd project cover',
|
'End-to-end product development from concept to launch',
|
||||||
tech: ['TypeScript', 'React', 'AI Workflow', 'Open Source'],
|
'Designed and implemented backend API with AdonisJS v6',
|
||||||
link: '#',
|
'Built frontend with Vue 3, TypeScript, and modern UI components',
|
||||||
|
'Integrated OpenAI SDK for AI conversation features',
|
||||||
|
'Implemented EPUB import and content parsing pipeline',
|
||||||
|
],
|
||||||
|
outcomes: [
|
||||||
|
'Launched a complete learning platform with 4 core features',
|
||||||
|
'Achieved smooth reading experience with progress tracking',
|
||||||
|
'Integrated Azure TTS for immersive listening practice',
|
||||||
|
'Enabled AI-powered interactive learning with context awareness',
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
'AI-powered English reading and learning platform.',
|
||||||
|
'Combines reading, listening, word lookup, and AI Q&A for comprehensive language learning.',
|
||||||
|
],
|
||||||
|
tech: [
|
||||||
|
'AdonisJS v6',
|
||||||
|
'PostgreSQL',
|
||||||
|
'Redis',
|
||||||
|
'Vue 3',
|
||||||
|
'TypeScript',
|
||||||
|
'Pinia',
|
||||||
|
'Reka UI',
|
||||||
|
'shadcn-vue',
|
||||||
|
'Tailwind CSS 4',
|
||||||
|
'OpenAI SDK',
|
||||||
|
'Azure Speech SDK',
|
||||||
|
'pnpm workspace',
|
||||||
|
],
|
||||||
|
link: 'https://github.com/zguiyang/elynd',
|
||||||
|
coverImage: 'https://cloud.zgyk.cc/f/VoFa/elynd-home.png',
|
||||||
|
coverImageAlt: 'Elynd Dashboard Preview',
|
||||||
|
links: {
|
||||||
|
github: 'https://github.com/zguiyang/elynd',
|
||||||
|
demo: 'https://elynd.zhaoguiyang.com/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'linky',
|
||||||
|
featured: true,
|
||||||
|
type: 'product',
|
||||||
|
status: 'completed',
|
||||||
|
role: 'Founder & Developer',
|
||||||
|
impact: 'Built a self-hosted knowledge management tool for bookmarks and notes',
|
||||||
|
systemType: 'Personal knowledge management system',
|
||||||
|
context: 'A simple, self-hosted tool for organizing bookmarks and writing notes with full user data control.',
|
||||||
|
title: 'Linky',
|
||||||
|
icon: 'Link',
|
||||||
|
color: 'cyan',
|
||||||
|
image: {
|
||||||
|
bg: 'from-cyan-500/20 to-blue-500/20',
|
||||||
|
hover: 'from-cyan-500/30 to-blue-500/30',
|
||||||
|
text: 'text-cyan-500',
|
||||||
|
},
|
||||||
|
challenges: [
|
||||||
|
'Designing intuitive bookmark organization with tags',
|
||||||
|
'Implementing AI-powered auto-tagging feature',
|
||||||
|
'Building self-hosted authentication system',
|
||||||
|
],
|
||||||
|
responsibilities: [
|
||||||
|
'End-to-end product development',
|
||||||
|
'Backend API design with AdonisJS',
|
||||||
|
'Frontend implementation with Nuxt',
|
||||||
|
],
|
||||||
|
outcomes: [
|
||||||
|
'Launched self-hosted version with full user data ownership',
|
||||||
|
'Integrated AI auto-tagging with OpenAI compatible APIs',
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
'A self-hosted bookmark manager and note-taking tool.',
|
||||||
|
'Focus on privacy, simplicity, and AI-assisted organization.',
|
||||||
|
],
|
||||||
|
tech: ['AdonisJS', 'Nuxt', 'PostgreSQL', 'Vue', 'Pinia', 'Tailwind CSS'],
|
||||||
|
link: 'https://github.com/zguiyang/linky',
|
||||||
|
coverImage: 'https://cloud.zgyk.cc/f/nYHM/linky-home.png',
|
||||||
|
coverImageAlt: 'Linky Dashboard Preview',
|
||||||
|
links: {
|
||||||
|
github: 'https://github.com/zguiyang/linky',
|
||||||
|
demo: 'https://linky.zhaoguiyang.com/',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -246,15 +323,74 @@ export const projects = {
|
|||||||
{
|
{
|
||||||
...sharedCases[4],
|
...sharedCases[4],
|
||||||
role: '创始人 & 开发者',
|
role: '创始人 & 开发者',
|
||||||
impact: '在真实场景中探索 AI 协作开发工作流',
|
impact: '打造 AI 辅助英语阅读学习平台,帮助语言学习者提升阅读能力',
|
||||||
systemType: 'AI 协作工作空间',
|
systemType: 'AI 驱动英语阅读学习平台',
|
||||||
context: '探索 AI 在软件工程中可落地协作方式的开放产品。',
|
context: 'AI 辅助的英语阅读学习工具,通过「阅读 + 听读 + 查词 + AI 提问」四位一体帮助用户完成低门槛的语言输入练习。',
|
||||||
title: 'Elynd',
|
title: 'Elynd - AI 英语学习平台',
|
||||||
challenges: ['在不增加流程负担前提下设计有效 AI 工作流'],
|
challenges: [
|
||||||
responsibilities: ['产品探索与端到端实现'],
|
'设计流畅的分级阅读体验,实现内容难度自适应',
|
||||||
outcomes: ['验证多种可实践的 AI 协作开发模式'],
|
'集成 Azure Speech SDK 实现实时 TTS 音频播放',
|
||||||
description: ['一个面向构建者的开放 AI 工作空间。', '作为研发探索方向,不作为职业主叙事。'],
|
'构建高效的即点查词系统,支持即时释义显示',
|
||||||
tech: ['TypeScript', 'React', 'AI 协作', '开源'],
|
'打造理解阅读上下文的 AI 智能问答功能',
|
||||||
|
],
|
||||||
|
responsibilities: [
|
||||||
|
'从概念到上线的端到端产品开发',
|
||||||
|
'使用 AdonisJS v6 设计与实现后端 API',
|
||||||
|
'使用 Vue 3、TypeScript 和现代 UI 组件构建前端',
|
||||||
|
'集成 OpenAI SDK 实现 AI 对话功能',
|
||||||
|
'实现 EPUB 导入与内容解析流程',
|
||||||
|
],
|
||||||
|
outcomes: [
|
||||||
|
'上线具备 4 大核心功能的完整学习平台',
|
||||||
|
'实现流畅阅读体验与学习进度跟踪',
|
||||||
|
'集成 Azure TTS 实现沉浸式听读练习',
|
||||||
|
'支持上下文感知的 AI 智能交互学习',
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
'AI 驱动的英语阅读学习平台。',
|
||||||
|
'融合阅读、听读、查词、AI 问答,一站式语言学习体验。',
|
||||||
|
],
|
||||||
|
tech: [
|
||||||
|
'AdonisJS v6',
|
||||||
|
'PostgreSQL',
|
||||||
|
'Redis',
|
||||||
|
'Vue 3',
|
||||||
|
'TypeScript',
|
||||||
|
'Pinia',
|
||||||
|
'Reka UI',
|
||||||
|
'shadcn-vue',
|
||||||
|
'Tailwind CSS 4',
|
||||||
|
'OpenAI SDK',
|
||||||
|
'Azure Speech SDK',
|
||||||
|
'pnpm workspace',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...sharedCases[5],
|
||||||
|
role: '创始人 & 开发者',
|
||||||
|
impact: '打造自托管的书签管理与笔记工具,让用户完全掌控自己的数据',
|
||||||
|
systemType: '个人知识管理系统',
|
||||||
|
context: '简单、自托管的工具,用于组织书签和写笔记,数据完全由用户自己掌控。',
|
||||||
|
title: 'Linky',
|
||||||
|
challenges: [
|
||||||
|
'设计直观的书签组织系统,支持标签化管理',
|
||||||
|
'实现 AI 自动标签功能',
|
||||||
|
'构建自托管认证系统',
|
||||||
|
],
|
||||||
|
responsibilities: [
|
||||||
|
'端到端产品开发',
|
||||||
|
'使用 AdonisJS 设计后端 API',
|
||||||
|
'使用 Nuxt 实现前端',
|
||||||
|
],
|
||||||
|
outcomes: [
|
||||||
|
'上线自托管版本,用户完全拥有数据主权',
|
||||||
|
'集成 AI 自动标签功能,支持 OpenAI 兼容 API',
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
'自托管的书签管理器和笔记工具。',
|
||||||
|
'注重隐私、简洁和 AI 辅助整理。',
|
||||||
|
],
|
||||||
|
tech: ['AdonisJS', 'Nuxt', 'PostgreSQL', 'Vue', 'Pinia', 'Tailwind CSS'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
52
src/pages/404.astro
Normal file
52
src/pages/404.astro
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
import Layout from '@/layouts/Layout.astro';
|
||||||
|
import GlassHeader from '@/components/GlassHeader';
|
||||||
|
import Footer from '@/components/Footer';
|
||||||
|
import Container from '@/components/ui/Container.astro';
|
||||||
|
import { getLocalizedPath } from '@/i18n/utils';
|
||||||
|
import type { Lang } from '@/types/i18n';
|
||||||
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
|
||||||
|
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
||||||
|
const isZh = lang === 'zh';
|
||||||
|
const homePath = getLocalizedPath('/', lang);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout
|
||||||
|
title={isZh ? '页面未找到' : 'Page Not Found'}
|
||||||
|
description={isZh ? '抱歉,您访问的页面不存在。' : 'Sorry, the page you are looking for does not exist.'}
|
||||||
|
>
|
||||||
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
|
<Container>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-[60vh] text-center">
|
||||||
|
<h1 class="text-9xl font-bold text-primary">404</h1>
|
||||||
|
<h2 class="text-2xl font-semibold mt-4">
|
||||||
|
{isZh ? '页面未找到' : 'Page Not Found'}
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted-foreground mt-4 max-w-md">
|
||||||
|
{isZh
|
||||||
|
? '抱歉,您访问的页面不存在或已被移除。'
|
||||||
|
: 'Sorry, the page you are looking for does not exist or has been moved.'}
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex flex-col sm:flex-row gap-4">
|
||||||
|
<a
|
||||||
|
href={homePath}
|
||||||
|
class="inline-flex items-center justify-center px-6 py-3 text-sm font-semibold rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
{isZh ? '返回首页' : 'Go Home'}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={isZh ? '/zh/blog' : '/blog'}
|
||||||
|
class="inline-flex items-center justify-center px-6 py-3 text-sm font-semibold rounded-lg border border-border hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
{isZh ? '浏览博客' : 'Browse Blog'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer lang={lang} client:load />
|
||||||
|
</Layout>
|
||||||
@@ -23,7 +23,7 @@ const prefix = isZh ? '/zh' : '';
|
|||||||
<Layout title={isZh ? '关于' : 'About'}>
|
<Layout title={isZh ? '关于' : 'About'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main" id="overview">
|
<section class="page-content-main" id="overview">
|
||||||
<div class="mb-6 flex flex-wrap gap-2 text-sm">
|
<div class="mb-6 flex flex-wrap gap-2 text-sm">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const isZh = lang === 'zh';
|
|||||||
<Layout title={isZh ? '合作' : 'Hire'}>
|
<Layout title={isZh ? '合作' : 'Hire'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main">
|
<section class="page-content-main">
|
||||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1>
|
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1>
|
||||||
@@ -32,7 +32,7 @@ const isZh = lang === 'zh';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-10">
|
<section class="page-content-main mt-10">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型(Mock)' : 'Collaboration Models (Mock)'}</h2>
|
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型' : 'Collaboration Models'}</h2>
|
||||||
<div class="mt-4 grid gap-4 md:grid-cols-3">
|
<div class="mt-4 grid gap-4 md:grid-cols-3">
|
||||||
{collaborationModels.map((model) => (
|
{collaborationModels.map((model) => (
|
||||||
<article class="page-surface p-5">
|
<article class="page-surface p-5">
|
||||||
@@ -69,7 +69,25 @@ const isZh = lang === 'zh';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-6 page-surface p-6">
|
<section class="page-content-main mt-6 page-surface p-6">
|
||||||
<h2 class="text-xl font-bold">{isZh ? '常见问题(Mock)' : 'FAQ (Mock)'}</h2>
|
<h2 class="text-xl font-bold">{isZh ? '付款方式' : 'Payment Terms'}</h2>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-3">
|
||||||
|
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
|
||||||
|
<p class="text-2xl font-bold text-primary">40%</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{isZh ? '预付款' : 'Upfront'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
|
||||||
|
<p class="text-2xl font-bold text-primary">40%</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{isZh ? '中期验收' : 'Mid Review'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
|
||||||
|
<p class="text-2xl font-bold text-primary">20%</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{isZh ? '尾款' : 'Completion'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-content-main mt-6 page-surface p-6">
|
||||||
|
<h2 class="text-xl font-bold">{isZh ? '常见问题' : 'FAQ'}</h2>
|
||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
{collaborationFaq.map((item) => (
|
{collaborationFaq.map((item) => (
|
||||||
<div class="rounded-md border border-border/70 p-4">
|
<div class="rounded-md border border-border/70 p-4">
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ const latestPosts = sortPostsByDate(
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={isZh ? '首页' : 'Home'}>
|
<Layout title={isZh ? '首页' : 'Home'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:idle transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main space-y-6">
|
<section class="page-content-main space-y-6">
|
||||||
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p>
|
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p>
|
||||||
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
|
|||||||
</Container>
|
</Container>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer lang={lang} client:load />
|
<Footer lang={lang} client:idle />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -9,55 +9,122 @@ import { defaultLang } from '@/i18n/ui';
|
|||||||
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
||||||
const isZh = lang === 'zh';
|
const isZh = lang === 'zh';
|
||||||
|
|
||||||
const doing = isZh
|
const focuses = [
|
||||||
? ['推进工程化案例化的个人站升级', '优化远程协作下的交付流程与文档标准', '持续打磨 AI 协作开发实践']
|
{
|
||||||
: ['Upgrading this portfolio into an engineering case-study site', 'Improving remote-first delivery workflow and documentation quality', 'Refining practical AI-assisted development workflows'];
|
title: 'Cloud Bookmark Tool',
|
||||||
|
status: 'Live',
|
||||||
|
desc: 'A self-hosted bookmark manager I built for myself — then realized others might want it too. Handles AI tagging, content extraction, and search.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ELYND English Learning Tool',
|
||||||
|
status: 'Coming Soon',
|
||||||
|
desc: 'Reading English articles with synced audio, instant word lookup, and AI chat to ask questions about what I am reading.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const exploring = isZh
|
const problems = [
|
||||||
? ['复杂权限系统的前端边界设计', '大规模业务表单与状态流管理', '更稳定的跨团队异步协作机制']
|
{
|
||||||
: ['Frontend boundary design for complex permission systems', 'Scalable state management for large business forms', 'More stable async collaboration practices across teams'];
|
title: 'The "Understanding Gap" in Language Learning',
|
||||||
|
desc: 'Vocabulary memorization lacks context, original texts are hard to understand, and there is no instant feedback when stuck.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Productizing AI Capabilities',
|
||||||
|
desc: 'Making AI useful in real products, not just demos: tagging, text processing, and conversation that actually help.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UX Design for AI-Powered Tools',
|
||||||
|
desc: 'Adding AI without making things complicated — keeping tools simple while powerful.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const shipping = isZh
|
const outputs = [
|
||||||
? ['工程案例库结构重构', '导航与路由转化路径优化', '双语文案统一与信息层级简化']
|
'Cloud Bookmark Tool v1.0 launched',
|
||||||
: ['Refactoring project pages into engineering case studies', 'Optimizing navigation and conversion routes', 'Unifying EN/ZH copy and simplifying information hierarchy'];
|
'ELYND coming soon',
|
||||||
|
];
|
||||||
|
|
||||||
|
const availability = {
|
||||||
|
jobs: 'Remote / Full-stack / AI',
|
||||||
|
projects: 'Freelance / Subcontracting / Independent development',
|
||||||
|
preferences: 'Tool products / AI integration / Web applications',
|
||||||
|
};
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={isZh ? '现在' : 'Now'}>
|
<Layout title="Now">
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main">
|
<section class="page-content-main">
|
||||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '现在' : 'Now'}</h1>
|
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">Now</h1>
|
||||||
<p class="mt-4 text-lg text-muted-foreground">{isZh ? '我当前的工作重点与近期交付。' : 'What I am focusing on right now and what I am shipping recently.'}</p>
|
<p class="mt-4 text-lg text-muted-foreground">What I am working on and what I have shipped.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-10 grid gap-6 md:grid-cols-3">
|
<!-- Current Focus -->
|
||||||
<article class="page-surface p-6">
|
<section class="page-content-main mt-10">
|
||||||
<h2 class="text-lg font-bold">{isZh ? '在做什么' : 'Doing'}</h2>
|
<h2 class="text-xl font-bold">Current Focus</h2>
|
||||||
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
|
<div class="mt-4 space-y-4">
|
||||||
{doing.map((item) => <li>• {item}</li>)}
|
{focuses.map((item) => (
|
||||||
</ul>
|
<article class="page-surface p-5">
|
||||||
</article>
|
<div class="flex items-center gap-3">
|
||||||
|
<h3 class="text-base font-bold">{item.title}</h3>
|
||||||
<article class="page-surface p-6">
|
<span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">{item.status}</span>
|
||||||
<h2 class="text-lg font-bold">{isZh ? '在研究什么' : 'Exploring'}</h2>
|
</div>
|
||||||
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
|
<p class="mt-2 text-sm text-muted-foreground">{item.desc}</p>
|
||||||
{exploring.map((item) => <li>• {item}</li>)}
|
</article>
|
||||||
</ul>
|
))}
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<article class="page-surface p-6">
|
|
||||||
<h2 class="text-lg font-bold">{isZh ? '最近交付' : 'Shipping'}</h2>
|
|
||||||
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
|
|
||||||
{shipping.map((item) => <li>• {item}</li>)}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-8 page-surface p-6">
|
<!-- Problems I'm Solving -->
|
||||||
|
<section class="page-content-main mt-8">
|
||||||
|
<h2 class="text-xl font-bold">Problems I'm Solving</h2>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
{problems.map((item) => (
|
||||||
|
<article class="rounded-lg border border-border/70 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">{item.title}</h3>
|
||||||
|
<p class="mt-1.5 text-sm text-muted-foreground">{item.desc}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Recent Outputs -->
|
||||||
|
<section class="page-content-main mt-8">
|
||||||
|
<h2 class="text-xl font-bold">Recent Outputs</h2>
|
||||||
|
<ul class="mt-4 space-y-2">
|
||||||
|
{outputs.map((item) => (
|
||||||
|
<li class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-accent"></span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Availability -->
|
||||||
|
<section class="page-content-main mt-8 page-surface p-5">
|
||||||
|
<h2 class="text-xl font-bold">Availability</h2>
|
||||||
|
<div class="mt-4 grid gap-3 text-sm md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Open to: </span>
|
||||||
|
<span class="text-muted-foreground">{availability.jobs}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Projects: </span>
|
||||||
|
<span class="text-muted-foreground">{availability.projects}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Preferred: </span>
|
||||||
|
<span class="text-muted-foreground">{availability.preferences}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-content-main mt-8 page-surface p-4">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{isZh ? '最后更新:2026-03-16' : 'Last updated: 2026-03-16'}
|
Last updated: {today}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
|
|||||||
const isZh = lang === 'zh';
|
const isZh = lang === 'zh';
|
||||||
const pageProjects = projects[lang].map((project) => ({
|
const pageProjects = projects[lang].map((project) => ({
|
||||||
...project,
|
...project,
|
||||||
category: project.id === 'elynd' ? 'indie' : 'enterprise',
|
category: project.id === 'elynd' || project.id === 'linky' ? 'indie' : 'enterprise',
|
||||||
}));
|
}));
|
||||||
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
|
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
|
||||||
const indieProjects = pageProjects.filter((project) => project.category === 'indie');
|
const indieProjects = pageProjects.filter((project) => project.category === 'indie');
|
||||||
@@ -20,7 +20,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
|
|||||||
<Layout title={isZh ? '项目' : 'Projects'}>
|
<Layout title={isZh ? '项目' : 'Projects'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main">
|
<section class="page-content-main">
|
||||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>
|
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>
|
||||||
@@ -66,13 +66,13 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{indieProjects.map((project) => (
|
{indieProjects.map((project) => (
|
||||||
<article class="page-surface overflow-hidden">
|
<article class="page-surface h-full overflow-hidden flex flex-col">
|
||||||
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
|
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
|
||||||
{project.coverImage ? (
|
{project.coverImage ? (
|
||||||
<img
|
<img
|
||||||
src={project.coverImage}
|
src={project.coverImage}
|
||||||
alt={project.coverImageAlt || project.title}
|
alt={project.coverImageAlt || project.title}
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover transition-transform duration-300 ease-out hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
@@ -83,22 +83,59 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
|
|||||||
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
|
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{/* Status Badge */}
|
||||||
<div class="p-5">
|
<div class="absolute right-3 top-3">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p>
|
<span
|
||||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
|
||||||
<p class="text-sm text-muted-foreground">{project.context}</p>
|
project.status === 'completed'
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
|
||||||
{project.tech.slice(0, 5).map((tech) => (
|
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
|
||||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
}`}
|
||||||
))}
|
>
|
||||||
|
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 flex-col p-5">
|
||||||
|
<div class="flex flex-1 flex-col pb-4">
|
||||||
|
<h3 class="text-lg font-bold">{project.title}</h3>
|
||||||
|
{project.context && <p class="mt-2 text-sm text-muted-foreground">{project.context}</p>}
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
{project.tech.slice(0, 5).map((tech) => (
|
||||||
|
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div class="mt-auto flex gap-3 border-t border-border/50 pt-5">
|
||||||
|
{project.links?.demo && (
|
||||||
|
<a
|
||||||
|
href={project.links.demo}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
||||||
|
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.139 2.5 2.5 0 0 1-.142-3.635l3-3Z" />
|
||||||
|
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.139 2.5 2.5 0 0 1 .142 3.635l-3 3a4 4 0 0 0 5.656 5.656l1.224-1.224a.75.75 0 0 0-1.06-1.06l-1.224 1.224a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0-.225 5.865Z" />
|
||||||
|
</svg>
|
||||||
|
{isZh ? '预览' : 'Preview'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{project.links?.github && (
|
||||||
|
<a
|
||||||
|
href={project.links.github}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3.5 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-muted"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
|
|
||||||
{project.link !== '#' && (
|
|
||||||
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
|
|
||||||
{isZh ? '查看项目' : 'Open Project'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Moved to Uses"
|
|
||||||
description: "This page has moved."
|
|
||||||
layout: "../layouts/AboutLayout.astro"
|
|
||||||
---
|
|
||||||
|
|
||||||
# This page moved
|
|
||||||
|
|
||||||
The old **Services** page has been replaced by focused pages:
|
|
||||||
|
|
||||||
- [Uses](/uses)
|
|
||||||
- [About (Contact Card)](/about#contact-card)
|
|
||||||
|
|
||||||
If you are hiring for a remote role, please start from [Hire](/hire) and use the contact card on that page.
|
|
||||||
@@ -23,7 +23,7 @@ const prefix = isZh ? '/zh' : '';
|
|||||||
<Layout title={isZh ? '关于' : 'About'}>
|
<Layout title={isZh ? '关于' : 'About'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main" id="overview">
|
<section class="page-content-main" id="overview">
|
||||||
<div class="mb-6 flex flex-wrap gap-2 text-sm">
|
<div class="mb-6 flex flex-wrap gap-2 text-sm">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const isZh = lang === 'zh';
|
|||||||
<Layout title={isZh ? '合作' : 'Hire'}>
|
<Layout title={isZh ? '合作' : 'Hire'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main">
|
<section class="page-content-main">
|
||||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1>
|
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1>
|
||||||
@@ -32,7 +32,7 @@ const isZh = lang === 'zh';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-10">
|
<section class="page-content-main mt-10">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型(Mock)' : 'Collaboration Models (Mock)'}</h2>
|
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型' : 'Collaboration Models'}</h2>
|
||||||
<div class="mt-4 grid gap-4 md:grid-cols-3">
|
<div class="mt-4 grid gap-4 md:grid-cols-3">
|
||||||
{collaborationModels.map((model) => (
|
{collaborationModels.map((model) => (
|
||||||
<article class="page-surface p-5">
|
<article class="page-surface p-5">
|
||||||
@@ -69,7 +69,25 @@ const isZh = lang === 'zh';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-6 page-surface p-6">
|
<section class="page-content-main mt-6 page-surface p-6">
|
||||||
<h2 class="text-xl font-bold">{isZh ? '常见问题(Mock)' : 'FAQ (Mock)'}</h2>
|
<h2 class="text-xl font-bold">{isZh ? '付款方式' : 'Payment Terms'}</h2>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-3">
|
||||||
|
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
|
||||||
|
<p class="text-2xl font-bold text-primary">40%</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{isZh ? '预付款' : 'Upfront'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
|
||||||
|
<p class="text-2xl font-bold text-primary">40%</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{isZh ? '中期验收' : 'Mid Review'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
|
||||||
|
<p class="text-2xl font-bold text-primary">20%</p>
|
||||||
|
<p class="text-sm text-muted-foreground">{isZh ? '尾款' : 'Completion'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-content-main mt-6 page-surface p-6">
|
||||||
|
<h2 class="text-xl font-bold">{isZh ? '常见问题' : 'FAQ'}</h2>
|
||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
{collaborationFaq.map((item) => (
|
{collaborationFaq.map((item) => (
|
||||||
<div class="rounded-md border border-border/70 p-4">
|
<div class="rounded-md border border-border/70 p-4">
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ const latestPosts = sortPostsByDate(
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={isZh ? '首页' : 'Home'}>
|
<Layout title={isZh ? '首页' : 'Home'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:idle transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main space-y-6">
|
<section class="page-content-main space-y-6">
|
||||||
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p>
|
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p>
|
||||||
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
|
|||||||
</Container>
|
</Container>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer lang={lang} client:load />
|
<Footer lang={lang} client:idle />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -9,55 +9,122 @@ import { defaultLang } from '@/i18n/ui';
|
|||||||
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
||||||
const isZh = lang === 'zh';
|
const isZh = lang === 'zh';
|
||||||
|
|
||||||
const doing = isZh
|
const focuses = [
|
||||||
? ['推进工程化案例化的个人站升级', '优化远程协作下的交付流程与文档标准', '持续打磨 AI 协作开发实践']
|
{
|
||||||
: ['Upgrading this portfolio into an engineering case-study site', 'Improving remote-first delivery workflow and documentation quality', 'Refining practical AI-assisted development workflows'];
|
title: '云端书签工具',
|
||||||
|
status: '已上线',
|
||||||
|
desc: '面向开发者与信息工作者的轻量级自部署工具,数据完全可控。支持 AI 自动标签、内容识别与语义搜索。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ELYND 英语学习工具',
|
||||||
|
status: '即将上线',
|
||||||
|
desc: '基于阅读场景的英语学习产品,提供电子书与同步音频。支持点击单词/句子即时解释,AI 对话辅助理解。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const exploring = isZh
|
const problems = [
|
||||||
? ['复杂权限系统的前端边界设计', '大规模业务表单与状态流管理', '更稳定的跨团队异步协作机制']
|
{
|
||||||
: ['Frontend boundary design for complex permission systems', 'Scalable state management for large business forms', 'More stable async collaboration practices across teams'];
|
title: '英语学习中的「理解断点」问题',
|
||||||
|
desc: '背单词脱离语境、阅读原文有障碍、遇到问题缺乏即时反馈——想解决学习过程中的理解断裂。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'AI 能力的产品化落地',
|
||||||
|
desc: '不只是 Demo 层面的展示,而是在真实产品中接入 AI 标签、文本处理、对话能力。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工具型 AI 产品的用户体验',
|
||||||
|
desc: '如何在保持工具简洁性的同时,融入 AI 能力而不造成负担。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const shipping = isZh
|
const outputs = [
|
||||||
? ['工程案例库结构重构', '导航与路由转化路径优化', '双语文案统一与信息层级简化']
|
'云端书签工具 v1.0 上线',
|
||||||
: ['Refactoring project pages into engineering case studies', 'Optimizing navigation and conversion routes', 'Unifying EN/ZH copy and simplifying information hierarchy'];
|
'ELYND 即将发布',
|
||||||
|
];
|
||||||
|
|
||||||
|
const availability = {
|
||||||
|
jobs: '远程 / 全栈 / AI 方向',
|
||||||
|
projects: '外包 / 分包 / 独立项目开发',
|
||||||
|
preferences: '工具类产品 / AI 能力接入 / Web 应用',
|
||||||
|
};
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={isZh ? '现在' : 'Now'}>
|
<Layout title="现在">
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main">
|
<section class="page-content-main">
|
||||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '现在' : 'Now'}</h1>
|
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">现在</h1>
|
||||||
<p class="mt-4 text-lg text-muted-foreground">{isZh ? '我当前的工作重点与近期交付。' : 'What I am focusing on right now and what I am shipping recently.'}</p>
|
<p class="mt-4 text-lg text-muted-foreground">我当前的工作状态与产出。</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-10 grid gap-6 md:grid-cols-3">
|
<!-- 当前主线 -->
|
||||||
<article class="page-surface p-6">
|
<section class="page-content-main mt-10">
|
||||||
<h2 class="text-lg font-bold">{isZh ? '在做什么' : 'Doing'}</h2>
|
<h2 class="text-xl font-bold">当前主线</h2>
|
||||||
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
|
<div class="mt-4 space-y-4">
|
||||||
{doing.map((item) => <li>• {item}</li>)}
|
{focuses.map((item) => (
|
||||||
</ul>
|
<article class="page-surface p-5">
|
||||||
</article>
|
<div class="flex items-center gap-3">
|
||||||
|
<h3 class="text-base font-bold">{item.title}</h3>
|
||||||
<article class="page-surface p-6">
|
<span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">{item.status}</span>
|
||||||
<h2 class="text-lg font-bold">{isZh ? '在研究什么' : 'Exploring'}</h2>
|
</div>
|
||||||
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
|
<p class="mt-2 text-sm text-muted-foreground">{item.desc}</p>
|
||||||
{exploring.map((item) => <li>• {item}</li>)}
|
</article>
|
||||||
</ul>
|
))}
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<article class="page-surface p-6">
|
|
||||||
<h2 class="text-lg font-bold">{isZh ? '最近交付' : 'Shipping'}</h2>
|
|
||||||
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
|
|
||||||
{shipping.map((item) => <li>• {item}</li>)}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="page-content-main mt-8 page-surface p-6">
|
<!-- 当前关注的问题 -->
|
||||||
|
<section class="page-content-main mt-8">
|
||||||
|
<h2 class="text-xl font-bold">当前关注的问题</h2>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
{problems.map((item) => (
|
||||||
|
<article class="rounded-lg border border-border/70 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">{item.title}</h3>
|
||||||
|
<p class="mt-1.5 text-sm text-muted-foreground">{item.desc}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 近期产出 -->
|
||||||
|
<section class="page-content-main mt-8">
|
||||||
|
<h2 class="text-xl font-bold">近期产出</h2>
|
||||||
|
<ul class="mt-4 space-y-2">
|
||||||
|
{outputs.map((item) => (
|
||||||
|
<li class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-accent"></span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 当前状态 -->
|
||||||
|
<section class="page-content-main mt-8 page-surface p-5">
|
||||||
|
<h2 class="text-xl font-bold">当前状态</h2>
|
||||||
|
<div class="mt-4 grid gap-3 text-sm md:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">求职:</span>
|
||||||
|
<span class="text-muted-foreground">{availability.jobs}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">接单:</span>
|
||||||
|
<span class="text-muted-foreground">{availability.projects}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">偏好:</span>
|
||||||
|
<span class="text-muted-foreground">{availability.preferences}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-content-main mt-8 page-surface p-4">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{isZh ? '最后更新:2026-03-16' : 'Last updated: 2026-03-16'}
|
最后更新:{today}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
|
|||||||
const isZh = lang === 'zh';
|
const isZh = lang === 'zh';
|
||||||
const pageProjects = projects[lang].map((project) => ({
|
const pageProjects = projects[lang].map((project) => ({
|
||||||
...project,
|
...project,
|
||||||
category: project.id === 'elynd' ? 'indie' : 'enterprise',
|
category: project.id === 'elynd' || project.id === 'linky' ? 'indie' : 'enterprise',
|
||||||
}));
|
}));
|
||||||
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
|
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
|
||||||
const indieProjects = pageProjects.filter((project) => project.category === 'indie');
|
const indieProjects = pageProjects.filter((project) => project.category === 'indie');
|
||||||
@@ -20,7 +20,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
|
|||||||
<Layout title={isZh ? '项目' : 'Projects'}>
|
<Layout title={isZh ? '项目' : 'Projects'}>
|
||||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||||
|
|
||||||
<main class="min-h-screen pt-24 pb-20">
|
<main id="main-content" class="min-h-screen pt-24 pb-20">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="page-content-main">
|
<section class="page-content-main">
|
||||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>
|
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>
|
||||||
@@ -66,13 +66,13 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{indieProjects.map((project) => (
|
{indieProjects.map((project) => (
|
||||||
<article class="page-surface overflow-hidden">
|
<article class="page-surface h-full overflow-hidden flex flex-col">
|
||||||
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
|
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
|
||||||
{project.coverImage ? (
|
{project.coverImage ? (
|
||||||
<img
|
<img
|
||||||
src={project.coverImage}
|
src={project.coverImage}
|
||||||
alt={project.coverImageAlt || project.title}
|
alt={project.coverImageAlt || project.title}
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover transition-transform duration-300 ease-out hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
@@ -83,22 +83,59 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
|
|||||||
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
|
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{/* Status Badge */}
|
||||||
<div class="p-5">
|
<div class="absolute right-3 top-3">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p>
|
<span
|
||||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
|
||||||
<p class="text-sm text-muted-foreground">{project.context}</p>
|
project.status === 'completed'
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
|
||||||
{project.tech.slice(0, 5).map((tech) => (
|
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
|
||||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
}`}
|
||||||
))}
|
>
|
||||||
|
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 flex-col p-5">
|
||||||
|
<div class="flex flex-1 flex-col pb-4">
|
||||||
|
<h3 class="text-lg font-bold">{project.title}</h3>
|
||||||
|
{project.context && <p class="mt-2 text-sm text-muted-foreground">{project.context}</p>}
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
{project.tech.slice(0, 5).map((tech) => (
|
||||||
|
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div class="mt-auto flex gap-3 border-t border-border/50 pt-5">
|
||||||
|
{project.links?.demo && (
|
||||||
|
<a
|
||||||
|
href={project.links.demo}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
||||||
|
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.139 2.5 2.5 0 0 1-.142-3.635l3-3Z" />
|
||||||
|
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.139 2.5 2.5 0 0 1 .142 3.635l-3 3a4 4 0 0 0 5.656 5.656l1.224-1.224a.75.75 0 0 0-1.06-1.06l-1.224 1.224a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0-.225 5.865Z" />
|
||||||
|
</svg>
|
||||||
|
{isZh ? '预览' : 'Preview'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{project.links?.github && (
|
||||||
|
<a
|
||||||
|
href={project.links.github}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3.5 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-muted"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
|
|
||||||
{project.link !== '#' && (
|
|
||||||
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
|
|
||||||
{isZh ? '查看项目' : 'Open Project'}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
title: "页面已迁移"
|
|
||||||
description: "该页面已迁移到新的结构。"
|
|
||||||
layout: "../../layouts/AboutLayout.astro"
|
|
||||||
---
|
|
||||||
|
|
||||||
# 页面已迁移
|
|
||||||
|
|
||||||
原 **服务** 页面已拆分为更清晰的页面:
|
|
||||||
|
|
||||||
- [工具](/zh/uses)
|
|
||||||
- [关于(联系卡片)](/zh/about#contact-card)
|
|
||||||
|
|
||||||
如需沟通远程岗位,请优先查看 [合作](/zh/hire),并使用页面内的联系卡片。
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-stretch: normal;
|
font-stretch: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('./fonts/archivo-400.ttf') format('truetype');
|
src: url('/fonts/archivo-400.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Archivo';
|
font-family: 'Archivo';
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-stretch: normal;
|
font-stretch: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('./fonts/archivo-700.ttf') format('truetype');
|
src: url('/fonts/archivo-700.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Local Fonts - Space Grotesk */
|
/* Local Fonts - Space Grotesk */
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('./fonts/space-grotesk-400.ttf') format('truetype');
|
src: url('/fonts/space-grotesk-400.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Space Grotesk';
|
font-family: 'Space Grotesk';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('./fonts/space-grotesk-700.ttf') format('truetype');
|
src: url('/fonts/space-grotesk-700.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
/* ========== 基础变量覆盖 ========== */
|
/* ========== 基础变量覆盖 ========== */
|
||||||
:root {
|
:root {
|
||||||
/* 主题色 - 使用网站的紫色渐变accent */
|
/* 主题色 - 使用网站的蓝色主题色 */
|
||||||
--waline-theme-color: #8B5CF6;
|
--waline-theme-color: #2563EB;
|
||||||
--waline-active-color: #EC4899;
|
--waline-active-color: #F97316;
|
||||||
|
|
||||||
/* 背景色 */
|
/* 背景色 */
|
||||||
--waline-bg-color: oklch(1 0 0);
|
--waline-bg-color: oklch(1 0 0);
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
--waline-border-color: oklch(0.87 0 0);
|
--waline-border-color: oklch(0.87 0 0);
|
||||||
|
|
||||||
/* 其他颜色 */
|
/* 其他颜色 */
|
||||||
--waline-badge-color: #8B5CF6;
|
--waline-badge-color: #2563EB;
|
||||||
--waline-info-bg-color: oklch(0.97 0 0);
|
--waline-info-bg-color: oklch(0.97 0 0);
|
||||||
--waline-info-color: oklch(0.55 0 0);
|
--waline-info-color: oklch(0.55 0 0);
|
||||||
--waline-bq-color: oklch(0.93 0 0);
|
--waline-bq-color: oklch(0.93 0 0);
|
||||||
@@ -109,18 +109,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wl-btn.primary {
|
.wl-btn.primary {
|
||||||
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
|
background: #2563EB !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wl-btn.primary:hover {
|
.wl-btn.primary:hover {
|
||||||
background: linear-gradient(135deg, #7C3AED, #DB2777) !important;
|
background: #1D4ED8 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 头部标签页 ========== */
|
/* ========== 头部标签页 ========== */
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
|
|
||||||
/* ========== 徽章样式 ========== */
|
/* ========== 徽章样式 ========== */
|
||||||
.wl-badge {
|
.wl-badge {
|
||||||
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
|
background: linear-gradient(135deg, #2563EB, #F97316) !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
font-size: 0.7em !important;
|
font-size: 0.7em !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user