247 lines
7.4 KiB
Plaintext
247 lines
7.4 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 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)
|
||
}
|
||
}
|