新增「扁平简约图示组件」:9 个纯 TikZ 绘图元素

设计基调:圆角色块 + 实心纯色 + 白字 + 细箭头。
**无渐变、无阴影、无立体**(已量化验证:PDF 中 shading 对象数为 0)。
不依赖 smartdiagram 等外部绘图宏包,全部用 TikZ 自绘。

九个组件(颜色列表写在方括号里,可写一个或多个):
  \flatflow[颜色]{条目1, 条目2, ...}          横向箭头流程,自动等分满宽
  \flatstack[颜色]{...}                       纵向流程
  \flatcycle[颜色]{...}                       循环图(弧线箭头)
  \flathub[颜色]{中心}{环绕1, 环绕2, ...}      中心辐射
  \flatsteps[颜色]{...}                       阶梯递进
  \flattime[颜色]{年份列表}{事件列表}           时间轴(上下交替)
  \flatquad[颜色]{左上}{右上}{左下}{右下}       四象限
  \flatpyramid[颜色]{顶层, ..., 底层}          金字塔
  \flatcompare[左色][右色]{左标题}{左内容}{右标题}{右内容}   左右对比

颜色支持列表循环:\flatflow[oucblue,oucred]{...} 两个块不同色;
\flatflow[oucred,oucblue]{六个块} 则按红蓝红蓝循环取色。

防错:颜色是可选参数,若误写成花括号,会报
「图示组件的颜色要写在方括号里」的明确错误(不会静默出错)。

实现中踩到的三个本机 expl3/pgf 坑(均已写进代码注释):
 - 长度做乘除一律用 \pgfmathsetlength,且表达式里只能是「长度」或「纯数字」,
   绝不能出现计数器(否则静默算成 0pt)。
 - \foreach 每轮迭代会开组,内层 \def 出组即失效,需用 \global\def。
 - \ifstrequal 在本机恒为假,字符串比较必须用 \ifx。

另修正一处定义顺序问题:组件必须放在 \ExplSyntaxOff 之后,
否则 `@` 不是字母类别,\fl@... / fl@i@... 这类名字会被解析错。

已验证:TL2022~2026 全部 0 错误 0 溢出;跨版本渲染逐像素一致;
slides.tex 新增 5 页演示,README 新增「图示组件(扁平风格)」一节。
This commit is contained in:
2026-10-13 15:06:04 +08:00
parent 5532dd2fc7
commit e60e1d768b
5 changed files with 398 additions and 1 deletions
+59
View File
@@ -308,6 +308,65 @@ Tex文件的开头,是基本信息部分,可以根据需要添加`\title`{
`\l_ouc_pbg_vgap`(纵向,默认 6pt)。 `\l_ouc_pbg_vgap`(纵向,默认 6pt)。
4. 单个要点框仍然可以像以前那样手写 `columns` + `pointbox`,两种用法并存。 4. 单个要点框仍然可以像以前那样手写 `columns` + `pointbox`,两种用法并存。
## 图示组件(扁平风格)
模板自带 9 个**扁平简约**图示组件:圆角色块 + 实心纯色 + 白字 + 细箭头,
**无渐变、无阴影、无立体**。全部用纯 TikZ 绘制,不依赖任何外部绘图宏包。
![图示组件](/figs/fig16.png)
### 颜色:一个或多个都可以
所有组件的**颜色列表写在方括号里**(可选参数)。写一个就是全部同色,
写多个就**按顺序取色、不够时自动循环**:
```
\flatflow[oucblue]{数据采集, 数据清洗, 模型训练, 结果评估} % 全部海大蓝
\flatflow[oucblue,oucred,oucpine,oucamber]{数据采集, 数据清洗, 模型训练, 结果评估}
% ← 4 个块 4 种颜色
\flatflow[oucred,oucblue]{块一, 块二, 块三, 块四, 块五, 块六}
% ← 2 色循环:红蓝红蓝红蓝
```
> ⚠️ 颜色**必须写成方括号 `[ ]`**。若误写成花括号 `{ }`,颜色会被当成第一个条目,
> 编译时会报 `图示组件的颜色要写在方括号里` 的明确错误(不会静默出错)。
### 九个组件速查
| 命令 | 说明 |
|------|------|
| `\flatflow[颜色]{条目1, 条目2, ...}` | **横向箭头流程**,自动等分满宽 |
| `\flatstack[颜色]{条目1, 条目2, ...}` | 纵向流程 |
| `\flatcycle[颜色]{条目1, ...}` | 循环图(弧线箭头首尾相接) |
| `\flathub[颜色]{中心文字}{环绕1, 环绕2, ...}` | **中心辐射**(中心圆 + 环绕卡片) |
| `\flatsteps[颜色]{条目1, ...}` | 阶梯递进 |
| `\flattime[颜色]{年份列表}{事件列表}` | 时间轴(节点上下交替) |
| `\flatquad[颜色]{左上}{右上}{左下}{右下}` | 四象限 |
| `\flatpyramid[颜色]{顶层, ..., 底层}` | 金字塔(上窄下宽) |
| `\flatcompare[左色][右色]{左标题}{左内容}{右标题}{右内容}` | 左右对比 |
示例(`slides.tex` 里有完整的 5 页演示):
```
\flatflow[oucblue,oucred,oucpine,oucamber]{数据采集, 数据清洗, 模型训练, 结果评估}
\flathub[oucblue,oucred,oucpine,oucamber,oucdusk]{核心}{可行性, 必要性, 创新性, 实用性}
\flattime[oucblue,oucred,oucpine,oucdusk]{2020, 2021, 2023, 2025}{立项, 方案设计, 中期检查, 结题验收}
\flatcompare[oucpine][oucred]{优势}{响应快、成本低、易于维护}{不足}{精度受限、依赖数据质量}
```
`\flatquad` 与 `\flatcompare` 的颜色是独立的固定参数(分别 1 个和 2 个),
不参与循环取色。
### 几点说明
1. 颜色可以直接用模板的 10 个颜色(`oucblue`、`oucred`、`oucpine`、`oucdusk`、`oucamber` 等)。
2. 横向流程会自动把 N 个块加间隙**摊满整行**,不需要手算宽度。
3. 纵向对比、四象限这类元素较宽,建议用 `\begin{center}...\end{center}` 居中。
4. 这套组件已用 TeX Live 2022~2026 逐一编译验证:0 错误、0 溢出,且跨版本渲染逐像素一致。
## 代码 ## 代码
模板可以插入代码块,使用`lstlisting`即可。 模板可以插入代码块,使用`lstlisting`即可。
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
View File
Binary file not shown.
+70
View File
@@ -308,6 +308,76 @@
\end{frame} \end{frame}
\subsection{图示组件}
\begin{frame}
\frametitle{横向流程}
流程类图示用 \texttt{\textbackslash flatflow},条目用逗号分隔,自动等分排布。\\
颜色写在方括号里:写一个则全部同色,写多个则按顺序取色。\\
\vspace{4pt}
\textbf{单色:}
\begin{center}
\flatflow[oucblue]{数据采集, 数据清洗, 模型训练, 结果评估}
\end{center}
\vspace{4pt}
\textbf{每个块不同颜色:}
\begin{center}
\flatflow[oucblue,oucred,oucpine,oucamber]{数据采集, 数据清洗, 模型训练, 结果评估}
\end{center}
\end{frame}
\begin{frame}
\frametitle{纵向流程与循环}
\begin{columns}
\begin{column}{0.42\textwidth}
\centering
\flatstack[oucblue,oucred,oucpine,oucdusk]{提出问题, 建立模型, 求解验证}
\end{column}
\begin{column}{0.42\textwidth}
\centering
\flatcycle[oucblue,oucred,oucpine,oucamber]{计划, 执行, 检查, 改进}
\end{column}
\end{columns}
\end{frame}
\begin{frame}
\frametitle{中心辐射}
\begin{center}
\flathub[oucblue,oucred,oucpine,oucamber,oucdusk]{核心}{可行性, 必要性, 创新性, 实用性}
\end{center}
\end{frame}
\begin{frame}
\frametitle{阶梯递进}
\begin{center}
\flatsteps[oucblue,oucred,oucpine,oucamber]{基础研究, 技术开发, 集成验证, 示范应用}
\end{center}
\end{frame}
\begin{frame}
\frametitle{时间轴与四象限}
\begin{center}
\flattime[oucblue,oucred,oucpine,oucdusk]{2020, 2021, 2023, 2025}{立项, 方案设计, 中期检查, 结题验收}
\end{center}
\vspace{8pt}
\begin{center}
\flatquad[oucblue,oucred,oucpine,oucamber]{重要且紧急}{重要不紧急}{紧急不重要}{不重要不紧急}
\end{center}
\end{frame}
\begin{frame}
\frametitle{金字塔与对比}
\begin{center}
\flatpyramid[oucblue,oucred,oucpine,oucamber]{总体目标, 实施策略, 具体措施, 落地行动}
\end{center}
\vspace{6pt}
\begin{center}
\flatcompare[oucpine][oucred]{优势}{响应快、成本低、易于维护}{不足}{精度受限、依赖数据质量}
\end{center}
\end{frame}
\subsection{块} \subsection{块}
\begin{frame} \begin{frame}
+269 -1
View File
@@ -299,6 +299,9 @@
{ \ouc_pbg_typeset: } { \ouc_pbg_typeset: }
\ExplSyntaxOff \ExplSyntaxOff
%% ======================================================================
%文本框textbox %文本框textbox
\newtcolorbox{textbox}[1]{ \newtcolorbox{textbox}[1]{
colback=#1!5!white, colback=#1!5!white,
@@ -323,4 +326,269 @@
\renewcommand{\figurename}{图} \renewcommand{\figurename}{图}
\renewcommand{\tablename}{表} \renewcommand{\tablename}{表}
\renewcommand{\lstlistingname}{代码} \renewcommand{\lstlistingname}{代码}
\renewcommand{\abstractname}{摘要} \renewcommand{\abstractname}{摘要}
%% ======================================================================
%% 扁平简约图示组件 (flat diagrams)
%% ---------------------------------------------------------------------
%% 设计基调:圆角块 + 实心纯色 + 白字 + 细箭头
%% **无渐变、无阴影、无立体**
%%
%% 所有组件都支持「颜色列表」:写一个颜色则全部同色;
%% 写多个颜色(逗号分隔)则按顺序循环取色。
%% \flatflow{oucblue} % 4 个块全为海大蓝
%% \flatflow{oucblue,oucred,oucpine} % 4 个块循环取这 3 色
%% \flatflow{oucblue,oucred,oucpine,oucdusk,oucamber} % 5 色各不同
%% ======================================================================
\RequirePackage{tikz}
\usetikzlibrary{positioning,arrows.meta,calc,shapes.geometric}
%% ---------------- 防错:颜色参数必须写在 [] 里 ----------------
%% \flatflow 这类命令的「颜色列表」是可选参数,必须写成方括号 [颜色]。
%% 若误写成花括号 {颜色},颜色会被当成第一个条目(静默出错、极难排查),
%% 因此这里检查每个条目是不是模板的颜色名,是则明确报错。
\newcommand{\flcheckitems}[1]{%
\foreach \flci in {#1}{%
%% 注意:\foreach 每轮迭代会开组,内层的 \def 出组即失效,
%% 因此这里必须用 \global\def(否则检测永远不触发)。
\global\def\flhit{0}%
\foreach \flck in {oucblue,ouclightblue,oucred,ouccherry,oucbrown,%
oucgolden,oucsilver,oucpine,oucdusk,oucamber}{%
%% 注意:此处 \ifstrequal 在本机不生效(恒为假),必须用 \ifx
\ifx\flci\flck\global\def\flhit{1}\fi
}%
\ifnum\flhit=1
\PackageError{oucslide}{图示组件的颜色要写在方括号里}{%
命令 \string\flatflow[颜色列表]{条目1, 条目2} 中的颜色是可选参数,%
必须用方括号 [ ],不能用花括号 { }。\MessageBreak
现在「\flci」被当成了条目内容(它看起来是颜色名)。}%
\fi
}%
}
%% ---------------- 辅助:长度 / 计数 / 取色 ----------------
\newlength\flgap
\newlength\flbw
\newcount\flcnt
\def\flcntval{0}
%% 数条目个数,并存成**纯数字宏**(本机 pgfmath 的算式里不能出现计数器)
\newcommand{\flcount}[1]{%
\flcnt=0
\foreach \flzz in {#1}{\global\advance\flcnt by 1}%
\xdef\flcntval{\the\flcnt}%
}
\newcount\flcolcnt
\def\flcoln{0}
\newcommand{\flcolcount}[1]{%
\flcolcnt=0
\foreach \flzz in {#1}{\global\advance\flcolcnt by 1}%
\xdef\flcoln{\the\flcolcnt}%
}
\def\flcur{oucblue}
\def\flmain{oucblue}
%% 第 #2 个块取颜色列表 #1 中的颜色(超出则循环),结果存进 \flcur
\newcommand{\flpick}[2]{%
\pgfmathtruncatemacro{\flidx}{mod(#2-1,\flcoln)+1}%
\foreach \flzz [count=\fljj] in {#1}{%
\ifnum\fljj=\flidx \xdef\flcur{\flzz}\fi
}%
}
%% 取列表第一项作为「主色」(箭头、轴线等用)
\newcommand{\flsetmain}[1]{\flpick{#1}{1}\xdef\flmain{\flcur}}
%% ---------------- 公共样式 ----------------
\tikzset{
flblk/.style={rectangle, rounded corners=2pt, draw=none,
text=white, align=center, inner sep=4pt, font=\small},
flblkl/.style={rectangle, rounded corners=2pt, draw=none,
align=center, inner sep=4pt, font=\footnotesize},
flarr/.style={line width=1pt, -{Stealth[length=1.7mm,width=1.4mm]}}
}
%% ================== 1. 横向箭头流程 ==================
%% \flatflow[颜色列表]{条目1, 条目2, ...}
\NewDocumentCommand \flatflow { O{oucblue} m }{{%
\flcheckitems{#2}\flcount{#2}\flcolcount{#1}\flsetmain{#1}%
\setlength{\flgap}{16pt}%
\pgfmathsetlength{\flbw}{(0.99\textwidth-(\flcntval-1)*\flgap)/\flcntval}%
\begin{tikzpicture}
\foreach \flx [count=\fli] in {#2} {
\flpick{#1}{\fli}%
\pgfmathtruncatemacro{\flprev}{\fli-1}
\ifnum\fli=1
\node[flblk, fill=\flcur, minimum height=20pt,
text width=\dimexpr\flbw-8pt\relax] (fn1) at (0,0) {\flx};
\else
\node[flblk, fill=\flcur, minimum height=20pt,
text width=\dimexpr\flbw-8pt\relax,
right=\flgap of fn\flprev] (fn\fli) {\flx};
\fi
}%
\foreach \flx [count=\fli] in {#2} {
\ifnum\fli>1
\pgfmathtruncatemacro{\flprev}{\fli-1}
\draw[flarr, draw=\flmain!70!black] (fn\flprev.east) -- (fn\fli.west);
\fi
}%
\end{tikzpicture}%
}}
%% ================== 2. 纵向流程 ==================
\NewDocumentCommand \flatstack { O{oucblue} m }{{%
\flcheckitems{#2}\flcount{#2}\flcolcount{#1}\flsetmain{#1}%
\begin{tikzpicture}
\foreach \flx [count=\fli] in {#2} {
\flpick{#1}{\fli}%
\pgfmathtruncatemacro{\flprev}{\fli-1}
\ifnum\fli=1
\node[flblk, fill=\flcur, minimum height=18pt,
minimum width=0.7\textwidth] (fn1) at (0,0) {\flx};
\else
\node[flblk, fill=\flcur, minimum height=18pt,
minimum width=0.7\textwidth, below=8pt of fn\flprev] (fn\fli) {\flx};
\fi
}%
\foreach \flx [count=\fli] in {#2} {
\ifnum\fli>1
\pgfmathtruncatemacro{\flprev}{\fli-1}
\draw[flarr, draw=\flmain!70!black] (fn\flprev.south) -- (fn\fli.north);
\fi
}%
\end{tikzpicture}%
}}
%% ================== 3. 循环图 ==================
\NewDocumentCommand \flatcycle { O{oucblue} m }{{%
\flcheckitems{#2}\flcount{#2}\flcolcount{#1}\flsetmain{#1}%
\begin{tikzpicture}
\foreach \flx [count=\fli] in {#2} {
\flpick{#1}{\fli}%
\pgfmathsetmacro{\flang}{90-360/\flcntval*(\fli-1)}
\node[circle, fill=\flcur, text=white, align=center, inner sep=1pt,
minimum size=32pt, font=\footnotesize] (cn\fli) at (\flang:34pt) {\flx};
}%
\foreach \flx [count=\fli] in {#2} {
\pgfmathtruncatemacro{\flnext}{mod(\fli,\flcntval)+1}
\draw[flarr, draw=\flmain!50!black] (cn\fli) to[bend left=20] (cn\flnext);
}%
\end{tikzpicture}%
}}
%% ================== 4. 中心辐射 ==================
%% \flathub[颜色列表]{中心文字}{环绕1, 环绕2, ...}
\NewDocumentCommand \flathub { O{oucblue} m m }{{%
\flcheckitems{#3}\flcount{#3}\flcolcount{#1}\flsetmain{#1}%
\begin{tikzpicture}
\foreach \flx [count=\fli] in {#3} {
\flpick{#1}{\fli+1}% % 环绕从颜色列表第 2 项起取色
\pgfmathsetmacro{\flang}{90-360/\flcntval*(\fli-1)}
\node[rectangle, rounded corners=2pt, draw=\flcur!30!white, line width=0.6pt,
fill=\flcur!7!white, text=\flcur!55!black, align=center, inner sep=3pt,
font=\footnotesize, minimum height=17pt] (ln\fli) at (\flang:3.1cm) {\flx};
\draw[draw=\flcur!40!white, line width=0.7pt] (0,0) -- (ln\fli);
}%
\flsetmain{#1}%
\node[circle, fill=\flmain, text=white, align=center, inner sep=2pt,
minimum size=40pt, font=\small\bfseries] at (0,0) {#2};
\end{tikzpicture}%
}}
%% ================== 5. 阶梯递进 ==================
\NewDocumentCommand \flatsteps { O{oucblue} m }{{%
\flcheckitems{#2}\flcolcount{#1}%
\begin{tikzpicture}
\foreach \flx [count=\fli] in {#2} {
\flpick{#1}{\fli}%
\pgfmathsetlengthmacro{\flsx}{(\fli-1)*2.6cm}
\pgfmathsetlengthmacro{\flsy}{(\fli-1)*13pt}
\node[flblk, fill=\flcur, minimum height=16pt, minimum width=2.4cm,
anchor=south west] at (\flsx,\flsy) {\flx};
}%
\end{tikzpicture}%
}}
%% ================== 6. 时间轴 ==================
%% \flattime[颜色列表]{年份列表}{事件列表}
\NewDocumentCommand \flattime { O{oucblue} m m }{{%
\flcheckitems{#2}\flcheckitems{#3}\flcount{#2}\flcolcount{#1}\flsetmain{#1}%
\begin{tikzpicture}
\pgfmathsetlengthmacro{\flstep}{0.9\textwidth/\flcntval}%
\draw[line width=1.8pt, draw=\flmain,
-{Stealth[length=2mm,width=1.6mm]}] (0,0) -- (0.9\textwidth,0);
\foreach \fly [count=\fli] in {#2} {
\flpick{#1}{\fli}%
\pgfmathsetlengthmacro{\flxx}{(\fli-0.5)*\flstep}
\node[circle, fill=\flcur, minimum size=6pt, inner sep=0pt] at (\flxx,0) {};
\node[rectangle, fill=\flcur, text=white, inner sep=2.5pt,
font=\footnotesize\bfseries, rounded corners=1pt]
at (\flxx,0.22cm) {\fly};
}%
\foreach \fle [count=\fli] in {#3} {
\flpick{#1}{\fli}%
\pgfmathsetlengthmacro{\flxx}{(\fli-0.5)*\flstep}
\ifodd\fli
\node[flblkl, fill=\flcur!7!white, draw=\flcur!25!white, line width=0.5pt,
text=\flcur!55!black, text width=2.3cm, anchor=south]
at (\flxx,0.45cm) {\fle};
\else
\node[flblkl, fill=\flcur!7!white, draw=\flcur!25!white, line width=0.5pt,
text=\flcur!55!black, text width=2.3cm, anchor=north]
at (\flxx,-0.22cm) {\fle};
\fi
}%
\end{tikzpicture}%
}}
%% ================== 7. 四象限 ==================
%% \flatquad[颜色列表]{左上}{右上}{左下}{右下}
\NewDocumentCommand \flatquad { O{oucblue} m m m m }{{%
\flcolcount{#1}%
\flpick{#1}{1}\xdef\flcA{\flcur}%
\flpick{#1}{2}\xdef\flcB{\flcur}%
\flpick{#1}{3}\xdef\flcC{\flcur}%
\flpick{#1}{4}\xdef\flcD{\flcur}%
\begin{tikzpicture}
\node[flblkl, fill=\flcA!8!white, text=\flcA!55!black, text width=4.5cm,
minimum height=34pt] (q1) at (0,0) {#2};
\node[flblkl, fill=\flcB!14!white, text=\flcB!55!black, text width=4.5cm,
minimum height=34pt, right=3pt of q1] (q2) {#3};
\node[flblkl, fill=\flcC!14!white, text=\flcC!55!black, text width=4.5cm,
minimum height=34pt, below=3pt of q1] (q3) {#4};
\node[flblkl, fill=\flcD!8!white, text=\flcD!55!black, text width=4.5cm,
minimum height=34pt, right=3pt of q3] (q4) {#5};
\end{tikzpicture}%
}}
%% ================== 8. 金字塔 ==================
%% \flatpyramid[颜色列表]{顶层, 第二层, ...} (按书写顺序自上而下)
\NewDocumentCommand \flatpyramid { O{oucblue} m }{{%
\flcheckitems{#2}\flcolcount{#1}%
\begin{tikzpicture}
\foreach \flx [count=\fli] in {#2} {
\flpick{#1}{\fli}%
\pgfmathsetlengthmacro{\flw}{(1.6+(\fli-1)*1.6)*1cm}
\pgfmathsetlengthmacro{\flyy}{-(\fli-1)*15.5pt}
\node[trapezium, trapezium left angle=76, trapezium right angle=104,
draw=none, fill=\flcur, text=white, align=center,
inner sep=1pt, minimum height=15pt, minimum width=\flw]
at (0,\flyy) {\flx};
}%
\end{tikzpicture}%
}}
%% ================== 9. 左右对比 ==================
%% \flatcompare[左色][右色]{左标题}{左内容}{右标题}{右内容}
\NewDocumentCommand \flatcompare { O{oucblue} O{oucamber} m m m m }{{%
\begin{tikzpicture}
\node[flblk, fill=#1, minimum width=4.7cm, minimum height=19pt] (h1) at (0,0) {#3};
\node[flblk, fill=#2, minimum width=4.7cm, minimum height=19pt,
right=8pt of h1] (h2) {#5};
\node[flblkl, fill=#1!7!white, text=#1!55!black, text width=4.5cm,
minimum height=54pt, below=3pt of h1] {#4};
\node[flblkl, fill=#2!7!white, text=#2!55!black, text width=4.5cm,
minimum height=54pt, below=3pt of h2] {#6};
\end{tikzpicture}%
}}