309 lines
9.2 KiB
Plaintext
309 lines
9.2 KiB
Plaintext
#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-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, 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-inline)
|
||
#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);条与条之间仅用 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 skills.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 skills.abilities.enumerate() {
|
||
skill-bar(
|
||
item.name,
|
||
item.level,
|
||
above: if i == 0 { space-none } else { space-skill-gap },
|
||
)
|
||
}
|
||
#skill-bar(
|
||
skills.language.name,
|
||
skills.language.level,
|
||
subtitle: skills.language.label,
|
||
above: space-skill-gap,
|
||
)
|
||
]
|
||
sidebar-subtitle("技术栈", above: space-subsection-gap)
|
||
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[
|
||
#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,
|
||
)
|
||
#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[
|
||
#text(fill: color-body, size: size-body, weight: "bold")[#entry.title]
|
||
#v(space-body)
|
||
#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)
|
||
}
|
||
}
|