#import "theme.typ": * #import "config.typ": * #let icon-map-pin = image("../assets/icons/map-pin.svg") #let icon-phone = image("../assets/icons/phone.svg") #let icon-mail = image("../assets/icons/mail.svg") #let icon-globe = image("../assets/icons/globe.svg") #let icon-tech-frontend = image("../assets/icons/tech-frontend.svg") #let icon-tech-backend = image("../assets/icons/tech-backend.svg") #let icon-tech-ai = image("../assets/icons/tech-ai.svg") #let icon-tech-database = image("../assets/icons/tech-database.svg") #let icon-tech-devops = image("../assets/icons/tech-devops.svg") #let tech-category-icon(name) = { if name == "frontend" { icon-tech-frontend } else if name == "backend" { icon-tech-backend } else if name == "ai" { icon-tech-ai } else if name == "database" { icon-tech-database } else { icon-tech-devops } } #let profile-photo = if author-photo != none { box(width: 100%, height: 100%, clip: true, author-photo) } else { rect( width: 100%, height: 100%, fill: color-bar-bg, stroke: color-divider + 0.5pt, )[ #align(center + horizon)[ #text(fill: color-muted, size: size-caption)[照片] ] ] } /// 左上 / 右下装饰条 #let page-corners() = context { place( top + left, dx: -page.margin.left, dy: -page.margin.top + corner-bar-inset, block(width: corner-bar-width, height: corner-bar-height, fill: color-accent-bright), ) place( bottom + right, dx: page.margin.right, dy: page.margin.bottom - corner-bar-inset, block(width: corner-bar-width, height: corner-bar-height, fill: color-accent-bright), ) } /// 页眉:姓名 + 职位 + 简介 + 2 寸照片 #let hero-section() = block(width: 100%, below: space-md)[ #grid( columns: (1fr, photo-width), column-gutter: space-hero-gutter, align: (top, top), [ #text( font: heading-font, size: size-name, weight: "bold", fill: color-ink, )[#author-name] #v(space-xs) #text( font: heading-font, size: size-subtitle, weight: "bold", fill: color-accent-bright, )[#author-title] #v(space-md) #set text(weight: "regular") #set par(leading: leading-summary, spacing: par-spacing-tight, justify: true) #text(fill: color-text-light, size: size-body)[#author-summary] ], [ #align(right)[ #box( width: photo-width, height: photo-height, clip: true, stroke: color-divider + 0.6pt, profile-photo, ) ] ], ) ] /// 页眉与正文之间的横线 #let header-divider() = block(width: 100%, above: space-xs, below: space-section)[ #line(length: 100%, stroke: color-divider + 0.55pt) ] /// 分栏标题 #let section-title(title, sidebar: false, divider: false) = { let above = if sidebar { space-section } else { space-between-sections } let below = if sidebar { space-sidebar-title-gap } else { space-sm } block(width: 100%, above: above, below: below)[ #text( font: heading-font, size: size-section, weight: "bold", fill: color-ink, )[#title] #if divider [ #v(space-sm) #line(length: 100%, stroke: color-divider + 0.55pt) ] ] } /// 侧栏区块分隔线 #let sidebar-section-divider() = block( width: 100%, above: space-sidebar-divider, below: space-xs, )[ #line(length: 100%, stroke: color-divider + 0.55pt) ] #let accent-icon(icon) = box( width: 1em, height: 1em, baseline: 0.08em, icon, ) #let contact-line(icon, body) = block(below: space-xs)[ #grid( columns: (1em, 1fr), column-gutter: space-xs, align: (center + horizon, left), box(width: 0.95em, height: 0.95em, baseline: 0.08em, icon), text(fill: color-text-light, size: size-contact)[#body], ) ] #let contact-block() = block(above: 0pt)[ #contact-line(icon-map-pin, author-address) #contact-line(icon-phone, link("tel:" + author-phone, author-phone)) #contact-line(icon-mail, link("mailto:" + author-email, author-email)) #if author-website != "" { contact-line(icon-globe, link(author-website, author-website-label)) } ] /// 教育背景条目 #let edu-entry(degree, school, period, location, note: none) = block(above: space-sm, below: space-xs)[ #text(fill: color-body, size: size-body)[#degree] #v(space-xxs) #text(fill: color-muted, size: size-meta)[ #school#if period != "" [ · #period]#if location != "" [ · #location] ] #if note != none [ #v(space-xxs) #text(fill: color-muted, size: size-small)[#note] ] ] #let education-block() = { for item in education { edu-entry(item.degree, item.school, item.period, item.location, note: item.note) } } /// 侧栏技能子标题 #let sidebar-subtitle(title) = block(above: space-md, below: space-xs)[ #text( font: heading-font, size: size-small, fill: color-body, )[#title] ] /// 技能进度条(level: 0–1) #let skill-bar(name, level, subtitle: none) = block(above: space-sm, below: space-xs)[ #if subtitle != none [ #grid( columns: (1fr, auto), align: horizon, text(fill: color-body, size: size-body)[#name], text(fill: color-muted, size: size-meta)[#subtitle], ) ] else [ #text(fill: color-body, size: size-body)[#name] ] #v(space-xs) #box(width: 100%, height: 4pt, radius: 1pt, clip: true)[ #place(left + top, block(width: 100%, height: 4pt, fill: color-bar-bg)) #place(left + top, block(width: level * 100%, height: 4pt, fill: color-skill-bar)) ] ] #let tech-pill(label) = box( fill: color-page, stroke: color-divider + 0.65pt, radius: 3pt, inset: (x: 5pt, y: 2pt), baseline: 0.15em, text(fill: color-body, size: size-tech-pill)[#label], ) #let tech-category-block(category) = block(above: space-sm, below: space-xxs)[ #grid( columns: (0.8em, 1fr), column-gutter: space-xs, align: horizon, box(width: 0.8em, height: 0.8em, baseline: 0.1em, tech-category-icon(category.icon)), text(font: heading-font, weight: "bold", size: size-tech-category, fill: color-ink)[#category.title] ) #v(space-xs) #box(width: 100%)[ #for (i, tag) in category.tags.enumerate() [ #if i > 0 [#h(3pt)] #tech-pill(tag) ] ] ] #let tech-stack-block() = block(width: 100%, above: space-xxs)[ #for category in tech-categories { tech-category-block(category) } ] #let skills-section-block() = { sidebar-subtitle("能力") for item in abilities { skill-bar(item.name, item.level) } sidebar-subtitle("技术栈") tech-stack-block() } /// 主栏经历 / 项目条目 #let timeline-entry(entry, title-key: "company", subtitle-key: "role") = block[ #text(fill: color-accent, size: size-subtitle, weight: "bold")[ #if subtitle-key != none [ #entry.at(title-key) / #entry.at(subtitle-key) ] else [ #entry.at(title-key) ] ] #v(space-xs) #text(fill: color-muted, size: size-meta)[ #entry.location#if entry.location != "" [,]#entry.start - #entry.end ] #v(space-sm) #set text(weight: "regular", fill: color-text-light, size: size-body) #set par(leading: leading-entry, spacing: par-spacing-tight, justify: true) #set list(spacing: par-spacing-tight, indent: 0.8em, body-indent: 0.4em) #entry.description ] #let project-entry(entry) = timeline-entry(entry, title-key: "name", subtitle-key: "org") /// 工作经历条目 #let experience-entry(entry) = timeline-entry(entry, title-key: "company", subtitle-key: "role") #let experiences-block() = { for (i, entry) in experiences.enumerate() { if i > 0 { v(space-between-entries) } experience-entry(entry) } } #let projects-block() = { for (i, entry) in projects.enumerate() { if i > 0 { v(space-between-entries) } project-entry(entry) } }