277 lines
7.7 KiB
Plaintext
277 lines
7.7 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 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)
|
||
}
|
||
}
|