#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 profile-photo = if author-photo-path != "" { image(author-photo-path, width: 100%, height: 100%, fit: "cover") } else { none } #let profile-photo-frame(content) = { if content != none { box(width: 100%, height: 100%, clip: true, content) } else { rect( width: 100%, height: 100%, fill: color-bar-bg, stroke: stroke-placeholder, )[ #align(center + horizon)[ #text(fill: color-muted, size: size-body)[照片] ] ] } } /// 左上 / 右下装饰条 #let page-corners() = { 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-skill-bar), ) 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-skill-bar), ) } /// 页眉:姓名 + 职位 + 简介 + 2 寸照片 #let hero-section() = block(width: 100%, below: space-none)[ #grid( columns: (1fr, photo-width), column-gutter: space-hero-to-photo, align: (top, top), [ #text(font: heading-font, size: size-name, weight: "bold", fill: color-ink)[#author-name] #v(space-hero-name-gap) #text(font: heading-font, size: size-heading, weight: "bold", fill: color-accent-bright)[#author-title] #v(space-hero-title-gap) #set text(weight: "regular") #set par(leading: leading-summary, spacing: space-none, justify: true) #text(fill: color-text-light, size: size-body)[#author-summary] ], [ #align(right)[ #box(width: photo-width, height: photo-height, clip: true, stroke: stroke-photo)[ #profile-photo-frame(profile-photo) ] ] ], ) ] /// 页眉与正文之间的横线 #let header-divider() = block(width: 100%, above: space-block, below: space-block)[ #line(length: 100%, stroke: stroke-divider) ] /// 分栏标题 #let section-title(title, sidebar: false, divider: false, above: auto, below: auto) = { let title-above = if above != auto { above } else if sidebar { space-none } else { space-block } let title-below = if below != auto { below } else { space-title-gap } block(width: 100%, above: title-above, below: title-below)[ #text(font: heading-font, size: size-heading, weight: "bold", fill: color-ink)[#title] #if divider [ #v(space-inline) #line(length: 100%, stroke: stroke-divider) ] ] } /// 侧栏区块分隔线 #let sidebar-section-divider() = block(width: 100%, above: space-block, below: space-block)[ #line(length: 100%, stroke: stroke-divider) ] #let contact-line(icon, body) = block(below: space-inline)[ #grid( columns: (icon-column-width, 1fr), column-gutter: space-micro, align: (center + horizon, left), box(width: icon-box-size, height: icon-box-size, baseline: 0.08em, icon), text(fill: color-body, size: size-secondary)[#body], ) ] #let contact-block() = block(above: space-none)[ #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) = block(above: space-none, below: space-none)[ #text(fill: color-body, size: size-body)[#degree] #v(space-inline) #text(fill: color-text-light, size: size-secondary)[ #school#if period != "" [ · #period]#if location != "" [ · #location] ] ] #let education-block() = { for (i, item) in education.enumerate() { if i > 0 { v(space-between-entries) } edu-entry(item.degree, item.school, item.period, item.location) } } /// 侧栏技能子标题 #let sidebar-subtitle(title, above: auto, below: auto) = { let gap-above = if above != auto { above } else { space-inline } let gap-below = if below != auto { below } else { space-subtitle-gap } block(above: gap-above, below: gap-below)[ #text(font: heading-font, size: size-body, fill: color-body)[#title] ] } /// 技能进度条(level: 0–1);条与条之间仅用 above 控制间距 #let skill-bar(name, level, subtitle: none, above: space-none) = block(above: above, below: space-none)[ #if subtitle != none [ #grid( columns: (1fr, auto), align: horizon, text(fill: color-text-light, size: size-secondary)[#name], text(fill: color-text-light, size: size-secondary)[#subtitle], ) ] else [ #text(fill: color-text-light, size: size-secondary)[#name] ] #v(space-inline) #box(width: 100%, height: skill-bar-height, radius: skill-bar-radius, clip: true)[ #place(left + top, block(width: 100%, height: skill-bar-height, fill: color-bar-bg)) #place(left + top, block(width: level * 100%, height: skill-bar-height, fill: color-skill-bar)) ] ] #let tech-pill(label) = box( fill: color-page, stroke: stroke-pill, radius: pill-radius, inset: (x: pill-inset-x, y: pill-inset-y), baseline: 0.15em, text(fill: color-text-light, size: size-secondary)[#label], ) #let tech-stack-block() = block(width: 100%, above: space-none, below: space-inline)[ #box(width: 100%)[ #set par(spacing: pill-gap, leading: leading-pill-wrap) #for (i, tag) in tech-tags.enumerate() [ #if i > 0 [#h(pill-gap)] #tech-pill(tag) ] ] ] #let skills-section-block() = { sidebar-subtitle("能力", above: space-none) block(below: space-none)[ #for (i, item) in abilities.enumerate() { skill-bar( item.name, item.level, above: if i == 0 { space-none } else { space-skill-gap }, ) } #skill-bar( language_profile.name, language_profile.level, subtitle: language_profile.label, above: space-skill-gap, ) ] sidebar-subtitle("技术栈", above: space-subsection-gap) tech-stack-block() } /// 主栏经历 / 项目条目 #let timeline-entry(entry, title-key: "company", subtitle-key: "role") = block[ #text(fill: color-body, size: size-body, weight: "bold")[ #if subtitle-key != none [ #entry.at(title-key) / #entry.at(subtitle-key) ] else [ #entry.at(title-key) ] ] #v(space-body) #text(fill: color-text-light, size: size-body)[ #entry.location#if entry.location != "" [,]#entry.start - #entry.end ] #v(space-body) #set text(weight: "regular", fill: color-text-light, size: size-body) #set par(leading: list-item-leading, spacing: space-none, justify: true) #set list( tight: true, indent: list-indent, body-indent: list-body-indent, ) #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) } }