feat: About Me

This commit is contained in:
2026-08-19 18:05:20 +08:00
commit 214acfd33c
37 changed files with 6118 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# sandbox internal files
.snowflake/
.vercel/
# Environment variables
.env*.local
# Common ignores
node_modules
.next/
.DS_Store
+71
View File
@@ -0,0 +1,71 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@theme inline {
--font-sans: 'Inter', 'Inter Fallback', 'Noto Sans SC', 'Noto Sans SC Fallback', system-ui, sans-serif;
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--color-sage: var(--sage);
--color-ember: var(--ember);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
}
:root {
color-scheme: dark;
/* near-black base */
--background: oklch(0.13 0.002 90);
--foreground: oklch(0.9 0.004 90);
--card: oklch(0.16 0.003 90);
--card-foreground: oklch(0.9 0.004 90);
--popover: oklch(0.16 0.003 90);
--popover-foreground: oklch(0.9 0.004 90);
--primary: oklch(0.9 0.004 90);
--primary-foreground: oklch(0.13 0.002 90);
--secondary: oklch(0.2 0.003 90);
--secondary-foreground: oklch(0.85 0.004 90);
--muted: oklch(0.2 0.003 90);
--muted-foreground: oklch(0.58 0.006 90);
--accent: oklch(0.22 0.004 90);
--accent-foreground: oklch(0.9 0.004 90);
--destructive: oklch(0.6 0.18 25);
--border: oklch(0.98 0.004 90 / 9%);
--input: oklch(0.98 0.004 90 / 12%);
--ring: oklch(0.5 0.006 90);
/* restrained accents: soft dark green + faint warm amber */
--sage: oklch(0.62 0.05 155);
--ember: oklch(0.72 0.07 75);
--radius: 0.625rem;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-background text-foreground font-sans;
}
}
+44
View File
@@ -0,0 +1,44 @@
import { Analytics } from '@vercel/analytics/next'
import type { Metadata, Viewport } from 'next'
import { Inter, Noto_Sans_SC } from 'next/font/google'
import './globals.css'
const _inter = Inter({ subsets: ['latin'] })
const _notoSansSC = Noto_Sans_SC({ subsets: ['latin'], weight: ['400', '500'] })
export const metadata: Metadata = {
title: 'AboutMe',
description: '精选项目、技术栈与 AI 工作流展示',
icons: {
icon: [
{
url: '/icon-dark-32x32.png',
},
{
url: '/icon.svg',
type: 'image/svg+xml',
},
],
apple: '/apple-icon.png',
},
}
export const viewport: Viewport = {
colorScheme: 'dark',
themeColor: '#0a0a0a',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="zh-CN" className="bg-background">
<body className="antialiased font-sans">
{children}
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { Hero } from '@/components/hero'
import { TechStack } from '@/components/tech-stack'
import { PersonalProjects } from '@/components/personal-projects'
import { SiteFooter } from '@/components/site-footer'
export default function Page() {
return (
<main>
<Hero />
<TechStack />
<PersonalProjects />
<SiteFooter />
</main>
)
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+144
View File
@@ -0,0 +1,144 @@
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
export function Hero() {
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
// 细线缓慢绘制
gsap.fromTo(
".hero-line",
{ strokeDashoffset: 600 },
{
strokeDashoffset: 0,
duration: 2.4,
ease: "power2.inOut",
stagger: 0.3
}
);
gsap.fromTo(
".hero-shape",
{ opacity: 0, scale: 0.96 },
{
opacity: 1,
scale: 1,
duration: 1.8,
ease: "power2.out",
stagger: 0.2
}
);
// 缓慢呼吸浮动
gsap.to(".hero-float", {
y: -10,
duration: 4,
ease: "sine.inOut",
yoyo: true,
repeat: -1
});
gsap.to(".hero-breathe", {
opacity: 0.35,
duration: 3.2,
ease: "sine.inOut",
yoyo: true,
repeat: -1
});
gsap.fromTo(
".hero-hint",
{ opacity: 0 },
{ opacity: 1, duration: 1.5, delay: 2, ease: "power2.out" }
);
}, rootRef);
return () => ctx.revert();
}, []);
return (
<section
ref={rootRef}
aria-label="首屏"
className="relative flex h-svh w-full items-center justify-center overflow-hidden"
>
<div className="hero-float relative -translate-x-4">
<svg
width="320"
height="320"
viewBox="0 0 320 320"
fill="none"
aria-hidden="true"
className="h-56 w-56 md:h-80 md:w-80"
>
{/* 外圈细线圆 */}
<circle
className="hero-line"
cx="160"
cy="160"
r="120"
stroke="oklch(0.98 0.004 90 / 0.16)"
strokeWidth="1"
strokeDasharray="600"
strokeDashoffset="600"
/>
{/* 内部偏移圆 */}
<circle
className="hero-line"
cx="176"
cy="144"
r="72"
stroke="oklch(0.62 0.05 155 / 0.45)"
strokeWidth="1"
strokeDasharray="600"
strokeDashoffset="600"
/>
{/* 对角细线 */}
<line
className="hero-line"
x1="48"
y1="272"
x2="272"
y2="48"
stroke="oklch(0.98 0.004 90 / 0.12)"
strokeWidth="1"
strokeDasharray="600"
strokeDashoffset="600"
/>
{/* 小方形节点 */}
<rect
className="hero-shape"
x="156"
y="156"
width="8"
height="8"
fill="oklch(0.72 0.07 75 / 0.7)"
/>
<circle
className="hero-shape hero-breathe"
cx="232"
cy="88"
r="3"
fill="oklch(0.62 0.05 155 / 0.8)"
/>
<circle
className="hero-shape hero-breathe"
cx="88"
cy="232"
r="2"
fill="oklch(0.98 0.004 90 / 0.4)"
/>
</svg>
</div>
<div className="hero-hint absolute bottom-10 left-1/2 -translate-x-1/2 opacity-0">
<span className="text-[11px] uppercase tracking-[0.3em] text-muted-foreground/60">
Scroll
</span>
</div>
</section>
);
}
+150
View File
@@ -0,0 +1,150 @@
"use client";
import { useEffect, useState } from "react";
import { SectionLabel } from "./section-label";
import { ZoomableImage } from "./zoomable-image";
import { ProjectIntroDialog } from "./project-intro-dialog";
import {
personalProjects,
type PersonalProject
} from "@/lib/personal-projects";
const MD_COLUMNS_QUERY = "(min-width: 768px)";
/** 是否处于双列断点(与 md:columns-2 对齐) */
const useMdColumns = () => {
const [isMd, setIsMd] = useState(false);
useEffect(() => {
const media = window.matchMedia(MD_COLUMNS_QUERY);
const onChange = () => setIsMd(media.matches);
onChange();
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, []);
return isMd;
};
/** 双列按 mdOrder 排;没填则回退到原数组位置 */
const sortByMdOrder = (items: PersonalProject[]) => {
return items
.map((item, index) => ({ item, index }))
.sort((a, b) => {
const aOrder = a.item.mdOrder ?? a.index;
const bOrder = b.item.mdOrder ?? b.index;
return aOrder - bOrder || a.index - b.index;
})
.map(({ item }) => item);
};
const ProjectLink = ({
href,
variant,
children
}: {
href: string;
variant: "primary" | "outline";
children: React.ReactNode;
}) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={
variant === "primary"
? "inline-flex items-center gap-2 rounded-full bg-foreground px-5 py-2.5 text-xs font-medium uppercase tracking-[0.15em] text-background transition-opacity duration-300 hover:opacity-80"
: "inline-flex items-center gap-2 rounded-full border border-border px-5 py-2.5 text-xs font-medium uppercase tracking-[0.15em] text-foreground transition-colors duration-300 hover:border-foreground/50 hover:bg-secondary"
}
>
{children}
<span aria-hidden="true" className="text-[10px]">
</span>
</a>
);
};
const PersonalCard = ({ project }: { project: PersonalProject }) => {
return (
<article className="mb-6 break-inside-avoid overflow-hidden rounded-lg border border-border bg-card transition-all duration-300 hover:border-foreground/20 hover:shadow-[0_0_20px_-6px_oklch(0.62_0.05_155_/_0.25)]">
<div className="overflow-hidden border-b border-border">
<ZoomableImage
src={project.image}
alt={`${project.title} 预览`}
width={960}
height={600}
/>
</div>
<div className="flex flex-col gap-4 p-5">
<div className="flex flex-col gap-1">
<h3 className="text-lg font-medium text-foreground text-balance">
{project.title}
</h3>
{(project.meta?.company || project.meta?.period) && (
<p className="text-xs tracking-wide text-muted-foreground/80">
{[project.meta.company, project.meta.period]
.filter(Boolean)
.join(" · ")}
</p>
)}
</div>
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
{project.description}
</p>
{(project.live ||
project.gitea ||
project.showIntro ||
project.meta?.status) && (
<div className="flex flex-wrap items-center gap-3">
{project.live && (
<ProjectLink href={project.live} variant="primary">
Live
</ProjectLink>
)}
{project.gitea && (
<ProjectLink href={project.gitea} variant="outline">
Gitea
</ProjectLink>
)}
{project.showIntro && (
<ProjectIntroDialog
title={project.title}
intro={project.intro}
/>
)}
{project.meta?.status && (
<span className="inline-flex items-center rounded-full border border-dashed border-border px-5 py-2.5 text-xs uppercase tracking-[0.15em] text-muted-foreground">
{project.meta.status}
</span>
)}
</div>
)}
</div>
</article>
);
};
/** 实习项目下方的个人作品:PC 两列瀑布流 */
export const PersonalProjects = ({
items = personalProjects
}: {
items?: PersonalProject[];
}) => {
const isMdColumns = useMdColumns();
const visibleItems = isMdColumns ? sortByMdOrder(items) : items;
return (
<section aria-label="项目经历" className="py-32 md:py-24">
<div className="mx-auto max-w-6xl px-6">
<SectionLabel>Project</SectionLabel>
<div className="columns-1 md:columns-2 md:gap-6">
{visibleItems.map((project) => (
<PersonalCard key={project.id} project={project} />
))}
</div>
</div>
</section>
);
};
+83
View File
@@ -0,0 +1,83 @@
"use client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
type ProjectIntroDialogProps = {
title: string;
intro?: string;
};
const markdownComponents: Components = {
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
img: ({ src, alt }) => (
<img
src={typeof src === "string" ? src : undefined}
alt={alt ?? ""}
className="my-3 h-auto w-full rounded-lg border border-border"
/>
),
table: ({ children }) => (
<div className="mb-3 overflow-x-auto">
<table className="w-full border-collapse text-xs">{children}</table>
</div>
),
th: ({ children }) => (
<th className="border border-border bg-muted/50 px-2 py-1.5 text-left font-medium text-foreground">
{children}
</th>
),
td: ({ children }) => (
<td className="border border-border px-2 py-1.5">{children}</td>
)
};
/** 可选的项目介绍:shadcn Button 打开 Dialog,正文按 Markdown 渲染 */
export const ProjectIntroDialog = ({
title,
intro = ""
}: ProjectIntroDialogProps) => {
const body = intro.trim() || "暂无介绍";
return (
<Dialog>
<DialogTrigger
render={
<Button
variant="outline"
className="h-auto rounded-full border-border px-5 py-2.5 text-xs font-medium uppercase tracking-[0.15em]"
/>
}
>
Intro
</DialogTrigger>
<DialogContent className="max-w-2xl p-6">
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">{title} </DialogDescription>
<div
className="mt-3 max-h-[70vh] overflow-y-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden text-sm leading-relaxed text-muted-foreground [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs [&_h1]:mb-2 [&_h1]:text-base [&_h1]:font-medium [&_h1]:text-foreground [&_h2]:mt-5 [&_h2]:mb-2 [&_h2]:text-sm [&_h2]:font-medium [&_h2]:text-foreground [&_h3]:mt-4 [&_h3]:mb-2 [&_h3]:text-sm [&_h3]:font-medium [&_h3]:text-foreground [&_ol]:mb-3 [&_ol]:list-decimal [&_ol]:space-y-1 [&_ol]:pl-5 [&_p]:mb-3 [&_p]:last:mb-0 [&_pre]:mb-3 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_strong]:font-medium [&_strong]:text-foreground [&_ul]:mb-3 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5"
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={markdownComponents}
>
{body}
</ReactMarkdown>
</div>
</DialogContent>
</Dialog>
);
};
+7
View File
@@ -0,0 +1,7 @@
export function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<h2 className="mb-12 text-[11px] font-medium uppercase tracking-[0.3em] text-muted-foreground">
{children}
</h2>
);
}
+9
View File
@@ -0,0 +1,9 @@
export function SiteFooter() {
return (
<footer className="mx-auto max-w-6xl px-6 pb-16 pt-8">
<p className="text-center text-xs tracking-wide text-muted-foreground/50">
{"2026"}
</p>
</footer>
);
}
+102
View File
@@ -0,0 +1,102 @@
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SectionLabel } from "./section-label";
gsap.registerPlugin(ScrollTrigger);
const groups: { name: string; items: string[] }[] = [
{
name: "语言",
items: ["TypeScript", "JavaScript", "Python"]
},
{
name: "前端",
items: ["Vue 3", "Nuxt", "Pinia", "Tailwind CSS", "Sass / Less"]
},
{
name: "UI 与可视化",
items: ["Naive UI", "Ant Design", "shadcn/ui", "ECharts"]
},
{
name: "后端",
items: ["Node.js", "Koa", "FastAPI"]
},
{
name: "工程化",
items: ["Vite", "pnpm", "Git"]
},
{
name: "设计",
items: ["Figma", "Pixso", "蓝湖"]
},
{
name: "AI 编程",
items: ["Cursor", "Copilot", "Claude Code", "Codex", "V0"]
},
{
name: "AI 模型",
items: ["GPT", "Claude", "Grok", "DeepSeek", "MiMo"]
}
];
export function TechStack() {
const rootRef = useRef<HTMLElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.fromTo(
".tech-item",
{ opacity: 0, y: 24 },
{
opacity: 1,
y: 0,
duration: 0.7,
ease: "power2.out",
stagger: 0.06,
scrollTrigger: {
trigger: rootRef.current,
start: "top 75%"
}
}
);
}, rootRef);
return () => ctx.revert();
}, []);
return (
<section
ref={rootRef}
aria-label="技术栈"
className="mx-auto max-w-6xl px-6 py-32 md:py-40"
>
<SectionLabel>Tech Stack</SectionLabel>
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 md:gap-x-16">
{groups.map((group) => (
<div key={group.name} className="tech-item">
<p className="mb-5 text-xs uppercase tracking-[0.2em] text-muted-foreground/60">
{group.name}
</p>
<ul className="flex flex-wrap gap-3">
{group.items.map((item) => (
<li key={item} className="tech-item">
<span className="inline-flex items-center gap-2 rounded-md border border-border bg-card px-4 py-2 text-sm text-secondary-foreground transition-all duration-300 hover:scale-[1.04] hover:border-foreground/20 hover:shadow-[0_0_20px_-6px_oklch(0.62_0.05_155_/_0.25)]">
<span
aria-hidden="true"
className="h-1 w-1 rounded-full bg-sage/70"
/>
{item}
</span>
</li>
))}
</ul>
</div>
))}
</div>
</section>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
+117
View File
@@ -0,0 +1,117 @@
"use client";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
const Dialog = (props: DialogPrimitive.Root.Props) => {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
};
const DialogTrigger = (props: DialogPrimitive.Trigger.Props) => {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
};
const DialogPortal = (props: DialogPrimitive.Portal.Props) => {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
};
const DialogClose = (props: DialogPrimitive.Close.Props) => {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
};
const DialogOverlay = ({
className,
...props
}: DialogPrimitive.Backdrop.Props) => {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0",
className
)}
{...props}
/>
);
};
const DialogContent = ({
className,
children,
showCloseButton = true,
overlayClassName,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
overlayClassName?: string;
}) => {
return (
<DialogPortal>
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 outline-none duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
render={
<Button
variant="ghost"
size="icon-sm"
className="absolute top-2 right-2"
/>
}
>
<XIcon />
<span className="sr-only"></span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
);
};
const DialogTitle = ({
className,
...props
}: DialogPrimitive.Title.Props) => {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-base font-medium", className)}
{...props}
/>
);
};
const DialogDescription = ({
className,
...props
}: DialogPrimitive.Description.Props) => {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
};
export {
Dialog,
DialogTrigger,
DialogPortal,
DialogClose,
DialogOverlay,
DialogContent,
DialogTitle,
DialogDescription
};
+65
View File
@@ -0,0 +1,65 @@
"use client";
import Image from "next/image";
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
type ZoomableImageProps = {
src: string;
alt: string;
width: number;
height: number;
className?: string;
};
/** 点击图片放大预览,再点任意区域关闭 */
export const ZoomableImage = ({
src,
alt,
width,
height,
className
}: ZoomableImageProps) => {
const imageSrc = src || "/placeholder.svg";
const previewLabel = alt || "图片预览";
return (
<Dialog>
<DialogTrigger
className={`block w-full cursor-zoom-in border-0 bg-transparent p-0 ${className ?? ""}`}
aria-label={alt ? `放大查看:${alt}` : "放大查看图片"}
>
<Image
src={imageSrc}
alt={alt}
width={width}
height={height}
className="h-auto w-full"
/>
</DialogTrigger>
<DialogContent
showCloseButton={false}
overlayClassName="cursor-zoom-out bg-background/90 backdrop-blur-sm"
className="w-full max-w-none border-0 bg-transparent p-0 px-2 shadow-none ring-0 md:w-auto md:px-0"
>
<DialogTitle className="sr-only">{previewLabel}</DialogTitle>
{/* 点击预览图本身也会关闭 */}
<DialogClose className="block w-full cursor-zoom-out border-0 bg-transparent p-0 md:w-[80vw]">
<Image
src={imageSrc}
alt={alt}
width={width * 2}
height={height * 2}
className="h-auto max-h-svh w-full object-contain"
/>
</DialogClose>
</DialogContent>
</Dialog>
);
};
+233
View File
@@ -0,0 +1,233 @@
export type PersonalProject = {
id: string;
title: string;
description: string;
image: string;
live: string | null;
gitea: string | null;
/** 项目背景:公司、时间段、状态(如已下架) */
meta?: {
company?: string;
period?: string;
status?: string;
};
/** 为 true 时展示 Intro 按钮 */
showIntro?: boolean;
/** 弹窗正文,按 Markdown 渲染 */
intro?: string;
/** 双列时的排序,数字越小越靠前;单列仍按数组顺序 */
mdOrder?: number;
};
export const personalProjects: PersonalProject[] = [
{
id: "aiping",
mdOrder: 1,
title: "AI Ping",
description:
"面向大模型使用者的服务评测与信息汇总平台,提供长周期、高频率、多时段的客观评测数据,助力高效完成大模型服务选型与服务商评估。参与平台前端核心功能 0-1 开发",
image: "/images/aiping-cover.webp",
live: "https://aiping.cn",
gitea: null,
meta: {
company: "北京清程极智科技 · 实习",
period: "2025.07 2026.04"
},
showIntro: true,
intro: `面向大模型使用者的服务评测与信息汇总平台,提供长周期、高频率、多时段的客观评测数据,助力高效完成大模型服务选型与服务商评估。参与平台前端核心功能 0-1 开发。
**Tech Stack** · Vue 3 · TypeScript · ECharts · SSE · MarkStream · Mitt
## 通用泛型表格组件
针对多个业务页面表格逻辑重复、定制需求多的问题,基于 TypeScript 泛型封装通用表格组件:集成多字段排序、行展开折叠、数据缓冲防闪烁、行置顶等能力,通过插槽支持灵活列配置,已在 4 个页面复用。
![通用泛型表格组件](https://core.qflink.xyz/img/20260816/c859bc76cdc209581f55d31548964ffd_medium.webp)
TypeScript 泛型 · 插槽列配置 · 4 页面复用
## AI 响应实时流式渲染
集成 SSE 实现模型响应的实时流式传输,配合 MarkStream 完成 Markdown 增量渲染。
重构开源代码块行号插件,新增指定行高亮与样式标记能力。
![AI 响应实时流式渲染](https://core.qflink.xyz/img/20260816/f7ad62460fde3e02587d0fe50f7bc114_medium.webp)
SSE · MarkStream · 插件重构
## 数据可视化与联动
使用 ECharts 可视化评测数据并实现图表与表格的联动交互。
引入 Mitt 事件总线降低模块耦合,抽离通用函数与业务组件提升复用性,并优化响应式布局与多端适配。
![数据可视化与联动](https://core.qflink.xyz/img/20260816/e55462a5f0095ec099d884e6bb8867c7_medium.webp)
ECharts · Mitt · 多端适配
## 深度参与
从项目初期持续迭代至今,高频参与核心模块的开发与重构,移动端适配几乎全程独立负责,提交记录见证了平台从 0 到 1 的完整演进过程。
![提交记录](/images/aiping-commits.webp)
移动端适配 · 核心模块开发 · 平台迭代`
},
{
id: "aihua",
mdOrder: 2,
title: "清程爱画",
description:
"AI 图像与视频生成平台。负责作品发布、作品管理、模型训练、AI 润色提示词、提示词打标等多个核心模块的开发与迭代,覆盖从创作到内容管理的完整链路",
image: "/images/aihua-cover.webp",
live: null,
gitea: null,
meta: {
company: "北京清程极智科技 · 实习",
period: "2024.11 2025.07",
status: "已下架"
},
showIntro: true,
intro: `AI 图像与视频生成平台。负责作品发布、作品管理、模型训练、AI 润色提示词、提示词打标等多个核心模块的开发与迭代,覆盖从创作到内容管理的完整链路。
**Tech Stack** · Vue 3 · TypeScript · Naive UI · Vite
## 高频可复用表单组件
设计并封装基于 Naive UI 的高频可复用表单组件,减少复杂表单场景下的重复代码。
同时将 SVG Icon 转化为可交互的 Vue 组件,统一图标的使用与维护方式。
Naive UI · 表单封装 · SVG 组件化
## 协同交付与体验优化
与后端协同梳理接口字段、参数约束与异常处理逻辑,完成接口联调、问题排查与上线验收。
持续优化复杂表单和内容管理场景下的交互体验与页面稳定性,并深度参与移动端适配。
接口联调 · 移动端适配`
},
{
id: "my-v0",
mdOrder: 5,
title: "大屏低代码编辑器",
description:
"Vue 3 + Vite 大屏低代码编辑器,支持物料拖拽、画布编排、属性配置与实时预览,面向数据可视化大屏的快速搭建与交付。",
image:
"https://core.qflink.xyz/img/20260817/c56c26447560c0be6e4a77173b78e106_medium.webp",
live: "https://www.qflink.xyz/my-v0",
gitea: "https://git.qflink.xyz/jiawei/my-v0",
showIntro: true,
intro: `Vue 3 + Vite 大屏低代码编辑器前端,支持物料拖拽、画布编排、属性配置与实时预览。
## 技术栈
- **框架**Vue 3、Vite、TypeScript、Pinia、Vue Router
- **样式 / UI**Tailwind CSS、Element Plus、Iconify
- **画布**vue3-moveable(拖拽缩放)、vue3-selecto(框选)、vue3-sketch-ruler(标尺)、 vue-draggable-plus(图层拖拽排序)
- **图表 / 编辑器**ECharts、Monaco Editor
- **数据**Axios、Mock.js
## 开发
Node \`^22.18.0 || >=24.12.0\`,包管理器用 pnpm。
\`\`\`bash
pnpm install
pnpm dev
pnpm build # vue-tsc --build + vite build
pnpm type-check
pnpm preview
pnpm lint # oxlint + eslint(都会 --fix
pnpm format # prettier --write src/
\`\`\`
## 构建分包与性能
生产构建通过 Vite 8 的 Rolldown \`codeSplitting.groups\` 拆分 Vue、Element Plus 和画布交互依赖;编辑器、ECharts 图表和 Monaco 编辑器均按实际使用时异步加载。异步模块加载期间显示 loading,避免首次打开编辑器、首次拖入图表或打开 JSON 编辑器时出现空白区域。
### 首屏效果
- 页面加载时间:实测约从 6 s 降至 1 s
- 首屏入口 JS:从 6,585.31 kBgzip 1,751.85 kB)降至 165.12 kBgzip 60.45 kB
- 入口 JS 未压缩体积减少约 97.5%gzip 体积减少约 96.5%
### 生产构建产物
数据来自 \`pnpm build\`
| Chunk | 未压缩体积 | gzip 体积 | 加载时机 |
| --- | --- | --- | --- |
| 入口 index JS | 165.12 kB | 60.45 kB | 首屏 |
| vue | 96.61 kB | 37.58 kB | 首屏 |
| element-plus JS | 294.87 kB | 99.25 kB | 编辑器加载后 |
| canvas JS | 292.04 kB | 95.24 kB | 编辑器加载后 |
| 编辑器业务 JS | 79.36 kB | 25.17 kB | 编辑器加载后 |
| 图表 component / ECharts | 1,166.65 kB | 389.20 kB | 首次渲染图表节点 |
| MonacoEditor JS | 58.71 kB | 15.63 kB | 首次打开 JSON 编辑器 |
| Monaco editor.api JS | 3,625.00 kB | 926.34 kB | 首次打开 JSON 编辑器 |
Monaco 的 CSS 为 308.18 kBgzip 101.96 kB),同样延迟到首次打开 JSON 编辑器后加载。
Element Plus CSS 为 116.01 kBgzip 16.05 kB),随编辑器模块加载。`
},
{
id: "paper-mind",
mdOrder: 4,
title: "Paper Mind",
description:
"现代化 PDF 翻译平台,专业的 AI 驱动翻译服务。负责平台的快速重构,借助 AI 辅助开发与 Svelte,两天内完成整站重构并上线",
image: "/images/papermind-cover.webp",
live: "https://fanyi.qc-ai.cn",
gitea: null,
meta: {
company: "北京清程极智科技 · 实习",
period: "2026.01"
},
showIntro: true,
intro: `现代化 PDF 翻译平台,专业的 AI 驱动翻译服务。负责平台的快速重构,借助 AI 辅助开发与 Svelte,两天内完成整站重构并上线。
**Tech Stack** · Svelte · TypeScript · AI-assisted Dev
## 两天重构上线
以 AI 辅助开发为核心工作流,快速梳理原有业务逻辑并用 Svelte 重写:从技术选型、页面重构到部署上线全流程压缩至两天,验证了 AI + 轻量框架在紧急交付场景下的工程效率。
AI 辅助开发 · Svelte · 48h 交付`
},
{
id: "pta",
mdOrder: 3,
title: "多模态智能对话",
description:
"2025 暑期实训作业,一个前后端分离的多模态智能对话演示站,覆盖文字聊天、文生图演示、语音识别、语音合成,以及语音连续对话等能力。",
image:
"https://core.qflink.xyz/img/20260817/a4f7a1f6c3e4769b5916ba076e9b66e9_medium.webp",
live: "https://www.qflink.xyz/pta",
gitea: "https://git.qflink.xyz/jiawei/Practical_Training_Assignment"
},
{
id: "infinite-slide",
mdOrder: 6,
title: "Infinite Slide",
description:
"基于 Leafer-UI 的 React 无限滑动画布组件。卡片以网格平铺,支持四向无限环绕、拖拽惯性、响应式布局、视口虚拟化,并可监听可视区域内的卡片。",
image:
"https://core.qflink.xyz/img/20260816/c54029c728fb4fe86bab5de43b5c7dc9_medium.webp",
live: "https://www.qflink.xyz/infinite-slide",
gitea: "https://git.qflink.xyz/jiawei/infinite-slide"
},
{
id: "ocs-service",
mdOrder: 7,
title: "OCS Nuxt AI Answer Service",
description:
"基于 Nuxt/Nitro 的 OCS AI 答题服务,迁移自 Python Flask 服务。使用 OpenAI 兼容的 Chat Completions 流式接口生成答案,同时兼容 OCS 题库接口,并提供多种请求方式、题型提示词处理、可选令牌校验、内存缓存、健康检查与运行统计。",
image:
"https://core.qflink.xyz/img/20260817/ebf2d71656ae76f5b48f10f1d5a3e8f7_medium.webp",
live: "https://ocs.qflink.xyz",
gitea: "https://git.qflink.xyz/jiawei/OCS_service"
}
];
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/dev/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
}
export default nextConfig
+41
View File
@@ -0,0 +1,41 @@
{
"name": "aboutme",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint ."
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@vercel/analytics": "1.6.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gsap": "^3.15.0",
"lucide-react": "^1.16.0",
"next": "16.3.0",
"react": "^19",
"react-dom": "^19",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"shadcn": "^4.8.0",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
"@types/node": "^24",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8.5",
"tailwindcss": "^4.3.3",
"typescript": "5.7.3"
},
"pnpm": {
"overrides": {
"hono": "4.12.25"
}
}
}
+4851
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
msw: true
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

+26
View File
@@ -0,0 +1,26 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: light) {
.background { fill: black; }
.foreground { fill: white; }
}
@media (prefers-color-scheme: dark) {
.background { fill: white; }
.foreground { fill: black; }
}
</style>
<g clip-path="url(#clip0_7960_43945)">
<rect class="background" width="180" height="180" rx="37" />
<g style="transform: scale(95%); transform-origin: center">
<path class="foreground"
d="M101.141 53H136.632C151.023 53 162.689 64.6662 162.689 79.0573V112.904H148.112V79.0573C148.112 78.7105 148.098 78.3662 148.072 78.0251L112.581 112.898C112.701 112.902 112.821 112.904 112.941 112.904H148.112V126.672H112.941C98.5504 126.672 86.5638 114.891 86.5638 100.5V66.7434H101.141V100.5C101.141 101.15 101.191 101.792 101.289 102.422L137.56 66.7816C137.255 66.7563 136.945 66.7434 136.632 66.7434H101.141V53Z" />
<path class="foreground"
d="M65.2926 124.136L14 66.7372H34.6355L64.7495 100.436V66.7372H80.1365V118.47C80.1365 126.278 70.4953 129.958 65.2926 124.136Z" />
</g>
</g>
<defs>
<clipPath id="clip0_7960_43945">
<rect width="180" height="180" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+33
View File
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
File diff suppressed because one or more lines are too long