#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 profile.photo-path != "" { image(profile.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)[照片] ] ] } } /// 左上 / 右下装饰条(通过 page foreground 绘制,每页自动出现) #let page-corners-layer() = box(width: page-paper-width, height: page-paper-height)[ #place( top + left, dx: corner-bar-inset-x, dy: corner-bar-inset-y, block(width: corner-bar-width, height: corner-bar-height, fill: color-skill-bar), ) #place( bottom + right, dx: -corner-bar-inset-x, dy: -corner-bar-inset-y, 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)[#profile.name] #v(space-hero-name-gap) #text(font: heading-font, size: size-heading, weight: "bold", fill: color-accent-bright)[#profile.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-summary)[#profile.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-main-section } let title-below = if below != auto { below } else if sidebar { space-title-gap } else { space-main-title-below } 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, contact.address) #contact-line(icon-phone, link("tel:" + contact.phone, contact.phone)) #contact-line(icon-mail, link("mailto:" + contact.email, contact.email)) #if contact.website != "" { contact-line(icon-globe, link(contact.website, contact.website-label)) } ] /// 教育背景条目(notes 可选,一段普通正文) #let edu-entry(degree, school, period, location, notes: "") = block(above: space-none, below: space-none)[ #text(fill: color-body, size: size-body)[#degree] #v(space-edu-meta-gap) #text(fill: color-text-light, size: size-secondary)[ #school#if period != "" [ · #period]#if location != "" [ · #location] ] #if notes != "" [ #v(space-edu-notes-before) #set par(leading: leading-edu-notes, spacing: space-none, justify: true) #text(fill: color-text-light, size: size-secondary)[#notes] ] ] #let education-block(show-notes: false) = { for (i, item) in education.enumerate() { if i > 0 { v(space-between-entries) } edu-entry( item.degree, item.school, item.period, item.location, notes: if show-notes { item.at("notes", default: "") } else { "" }, ) } } /// 侧栏技能补充列表(skills.notes 为空则不渲染) #let skill-notes-block() = { if skills.notes.len() > 0 { block(above: space-none)[ #set text(fill: color-text-light, size: size-secondary) #set par(leading: list-item-leading, spacing: space-none, justify: true) #set list( tight: true, indent: list-indent, body-indent: list-body-indent, ) #for line in skills.notes [ - #line ] ] } } /// 侧栏技能子标题 #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);config 提供 abilities / language 时由 abilities-block 渲染 #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 has-abilities-section() = { let ability-list = skills.at("abilities", default: ()) let has-lang = skills.at("language", default: none) != none ability-list.len() > 0 or has-lang } /// 「能力」进度条区块(skills.abilities / skills.language 有数据时才显示) #let abilities-block() = { if has-abilities-section() { sidebar-subtitle("能力", above: space-none) block(below: space-none)[ #for (i, item) in skills.at("abilities", default: ()).enumerate() { skill-bar( item.name, item.level, above: if i == 0 { space-none } else { space-skill-gap }, ) } #let lang = skills.at("language", default: none) #if lang != none { skill-bar( lang.name, lang.level, subtitle: lang.at("label", default: none), above: if skills.at("abilities", default: ()).len() == 0 { space-none } else { space-skill-gap }, ) } ] } } #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 skills.tags.enumerate() [ #if i > 0 [#h(pill-gap)] #tech-pill(tag) ] ] ] #let skills-section-block() = { abilities-block() sidebar-subtitle( "技术栈", above: if has-abilities-section() { space-subsection-gap } else { space-none }, ) tech-stack-block() if skills.notes.len() > 0 { sidebar-subtitle("补充", above: space-subsection-gap) skill-notes-block() } } /// 主栏经历 / 项目条目 #let timeline-entry(entry, title-key: "company", subtitle-key: "role") = block[ #block(above: space-entry-title-above, below: space-entry-title-below)[ #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) ] ] ] #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, ) #for line in entry.bullets [ - #line ] ] #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) } } /// 获奖经历条目(bullets 可留空) #let award-entry(entry) = block[ #block(above: space-entry-title-above, below: space-entry-title-below)[ #text(fill: color-body, size: size-body, weight: "bold")[#entry.title] ] #text(fill: color-text-light, size: size-body)[#entry.meta] #if entry.bullets.len() > 0 [ #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, ) #for line in entry.bullets [ - #line ] ] ] #let awards-block() = { for (i, entry) in awards.enumerate() { if i > 0 { v(space-between-entries) } award-entry(entry) } }