Files
laoyang 3906bb49a4 清理代码库中的开发过程内容
- 删除 WORKFLOW.md(开发流程文档,已移入记忆库)
- build.sh 去掉 WSL 相关说明
- 33 个源文件的注释:去掉踩坑记录、实测推导、原文件对比、工单号引用
- 保留技术参数、约束说明、命令含义

净删 399 行。全库 16/16 编译通过。
2026-11-01 09:29:00 +08:00

588 lines
28 KiB
TeX
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
%======================================================================
% ouckey.sty — 中国海洋大学 期末考试 参考答案及评分标准 版式包
%
% 设计依据:学校无统一模板,本包按国内高校通行体例提炼,要点四条——
% ① 抬头须有:学年学期、课程名、试卷类型、学院/年级/专业、
% 开课学院、任课教师、考试方式、(可选)满分与考试日期;
% ② 客观题(填空/选择/判断)答案集中列出,附统一给分规则;
% ③ 主观题(名词解释/简答/论述/分析/计算/作图…)答案内嵌
% "分步给分"标记,每个得分点后直接标(X 分);
% ④ 每个大题末尾给【评分标准】,写清"答错/多选/漏选/未答"如何计分。
%
% 与 oucexam.sty(试卷版式)配套使用,字号字体同名同义:
% \Fivhao 五号正文 / \Sihao 小四 / \Xiaowu 小五
% \TNR{} Times / \TNRB{} Times Bold / \SUN{} 宋体 / \heiti 黑体
%
% 纸张:默认 A4(阅卷、归档、打印都按 A4 走)。
% 需要与试卷同尺寸(195mm×270mm 专用试卷纸)时加选项 exampaper:
% \usepackage[exampaper]{ouckey}
%
% 用法见同目录 参考答案与评分标准示例.tex
%======================================================================
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{ouckey}[2026/10/22 v1.0 OUC answer key & marking scheme]
% 本包大量使用 \ouckey@xxx 私有名,@ 必须当字母
\makeatletter
\RequirePackage{fontspec}
\RequirePackage{xeCJK}
\RequirePackage{graphicx}
\RequirePackage{lastpage}
\RequirePackage{keyval}
\RequirePackage{array}
\RequirePackage{tikz} % 作图题的参考答案要配图
\usetikzlibrary{calc,positioning}
%----------------------------------------------------------------------
% 纸张选项
% exampaper → 195mm × 270mm(与试卷同纸,便于和试卷一起装订归档)
% 默认 → A4(阅卷现场用,字大好认)
%----------------------------------------------------------------------
\newif\ifouckey@exampaper \ouckey@exampaperfalse
\DeclareOption{exampaper}{\ouckey@exampapertrue}
\ProcessOptions\relax
\ifouckey@exampaper
\RequirePackage[paperwidth=195mm,paperheight=270mm,
left=82.2bp,textwidth=436.55bp,
top=46bp,textheight=650bp,
headheight=0pt,headsep=0pt,footskip=30bp]{geometry}
\else
\RequirePackage[paperwidth=210mm,paperheight=297mm,
left=25mm,right=25mm,top=25mm,bottom=25mm,
headheight=0pt,headsep=0pt,footskip=28bp]{geometry}
\fi
\setlength{\parindent}{0pt}
\setlength{\parskip}{0pt}
\pagestyle{empty}
%----------------------------------------------------------------------
% 字体(与 oucexam.sty 同步,全部统一为思源宋体)
% 全部取自公共字体包 ../fonts.tex,再在包内重定向。
%
% ⚠️ 必须和 oucexam.sty 做**完全相同的处理**,否则两份文件观感不一致:
% oucexam 是试卷、ouckey 是答案卷,学校通常一起印发。
%
% ⚠️⚠️ 只重定向 \tnr / \tnrb 不够 —— 模板里大量文字没走它们:
% · \textbf{...} 的裸西文 → 走 \setmainfont
% · 「(4 分)」里的数字 → 走 \setmainfont
% · \thepage / \pageref 页码 → 走 \setmainfont
% ⇒ 必须连 \setmainfont 一起改,否则 PDF 里会残留 Termes span。
% 实际第一轮只做 \let 时,答案卷残留 197 个 Termes span。
%
% ⚠️ 数学公式($x$ / $\dfrac{}{}$ / $\sum$)**不在统一范围内**,
% 仍走 Computer Modern。原因见 oucexam.sty 的说明:思源宋体没有
% MATH 表,不能作数学字体。用户已确认保持不动。
%----------------------------------------------------------------------
\input{../fonts.tex}
%----------------------------------------------------------------------
% 西文字族:思源宋体(与 oucexam.sty 同一套定义,复制而非 \input,
% 因为两个 .sty 是并列关系,谁先加载不确定,不能互相依赖)
%----------------------------------------------------------------------
\newfontfamily\OUCLatinSerif{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
\newfontfamily\OUCLatinSerifBold{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Bold,
BoldFont = *-Bold,
ItalicFont = *-Bold,
BoldItalicFont = *-Bold,
]
% 默认西文字体 → 思源宋体(这一步不能省,见上面的说明)
\setmainfont{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
\setsansfont{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
% \sunfont 原指 SimSun,现统一指思源宋体
\let\sunfont\OUCLatinSerif
\let\tnr\OUCLatinSerif
\let\tnrb\OUCLatinSerifBold
% 答案卷里的黑体小标题(\heiti,共 37 处)一并换成思源宋体。
% ⚠️ 用 \let 而非改 \def\heiti —— 后者会破坏 ../fonts.tex,影响别的模板。
% ⚠️ 若将来要恢复黑体标题,注释掉下面这行即可。
\let\heiti\songti
% 局部字体切换必须显式收尾,否则 xeCJK 字体会"溢出"到后一段
\newcommand{\TNR}[1]{{\tnr #1\normalfont}}
\newcommand{\TNRB}[1]{{\tnrb #1\normalfont}}
\newcommand{\SUN}[1]{{\sunfont #1\normalfont}}
% 中西文间距 2.70bp(原卷取值,不随字号缩放)。
\xeCJKsetup{CJKecglue={\hskip 2.70bp\relax},PunctStyle=plain}
%----------------------------------------------------------------------
% 直引号 " (U+0022) 归入 CJK 字符类(补充修正)
%
% ⚠️ 问题:本文件正文用 ASCII 直引号 "" 表示中文引号(共 32 处),
% 而 xeCJK 按 Unicode 区段分类 —— U+0022 在 Basic Latin,被判成**西文**,
% 于是引号走 \setmainfont(原为 Termes),与两侧汉字不同字体、不同宽度。
% 换字体后表现为一行里出现 TeXGyreTermes-Regular 的 ",且因为西文引号
% 比中文引号窄、字距算法不同,挤压出 Overfull(实际第 88 行 +8.07pt)。
%
% ⚠️ 为什么不在 fonts.tex 里改:那里是**全库共用**的,把 U+0022 全局归 CJK
% 会让正文里真正的英文引号(如 code 片段 "hello world")也变成全角,
% 破坏那些地方。这是试卷的局部需求 —— 试卷正文基本是中文,直引号就是
% 中文引号的意思。
% ⚠️ 必须在 \input{../fonts.tex} **之后**声明,否则被随后加载的默认分类覆盖。
%----------------------------------------------------------------------
\xeCJKDeclareCharClass{CJK}{"0022} % " 直引号(当中文引号用)
%----------------------------------------------------------------------
% 【技术说明】换字体引起的 Overfull 怎么修(结论:改源码断行,别调参数)
%
% 现象:字体统一为思源宋体后,参考答案与评分标准示例.tex 第 123-125 行
% (\keyitem{3}{4})出现 Overfull 2.61pt,渲染后压出版心右边界
% (最右墨迹 527.01bp,版心止于 524.41bp)。
% 基线同一行本来就有 0.21pt 轻微 Overfull,换字体后西文变宽
% (数字 +6%、小写字母 +13~20%),把 0.21 放大到 2.61。
%
% 根因:该行末尾是「…正迁移与负迁移(1 分),」。
% · 「,」按中文标点禁则不能出现在行首 ⇒ 不许在「)」与「,」之间断
% · \pt 定义里有 `~`(\nobreakspace)⇒ 「(1 分)」内部也不可断
% ⇒ 「(1 分),」约 16bp,是不可断尾部,必须整体塞进本行。
% 而本行没有弹性胶可拉(汉字定宽、唯一的西文空格已满),
% 往前找更早的合法断点(句号「。」之后)的话,第一行只填 65%,
% 欠宽 150bp 无处拉伸 ⇒ badness 爆掉 ⇒ TeX 宁可撑破 2.61bp。
%
% ① \emergencystretch 1.5em —— 无效。只追加"可拉伸量",
% 本行没有胶可拉,给多少额度都无从施展。
% ② \tolerance 3000 —— 无效。不是不满意断点,是没有更早的**合法**断点。
% ③ PunctStyle=kaiming —— 能消 Overfull,但句末标点悬挂探出版心
% 6.5bp(第 3 页右缘 530.93 > 524.41),比不修更难看。
% ④ PunctStyle=quanjiao —— 同样探出 8bp(532.5),一样不可接受。
% 根子在于 xeCJK 的这几个 PunctStyle 做的是"行末标点**悬挂**",
% 不是"标点**压缩**"—— 中文排版里这是两回事,指望它消 Overfull 必翻车。
% ⑤ \rightskip 0pt plus 6bp(含 \AtBeginDocument 版)—— 无效。
% 6bp 远小于需要的 150bp,TeX 仍选"撑破"这个解。
% 注意:preamble 里直接 \setlength 还会被 ctex/xeCJK 初始化重置,
% 必须 \AtBeginDocument 才可能生效 —— 这类"设了没反应"的常见原因。
%
% ✅ 正解:**在源码的自然断点处手动断行**。
% 第 124 行末尾句号「。」之后本来就是语义上的句子边界,加 \\ 即可:
% \keyitem{3}{4}{%
% \textbf{学习迁移}:指一种学习对另一种学习的影响\pt{2}。\\
% 按方向分正迁移与负迁移\pt{1},其发生依赖于共同要素与概括水平\pt{1}。}
% 已Overfull 归零,三页最右墨迹均为 524.41bp,与版心严丝合缝。
%
% 为什么这是对的做法而不是"绕过问题":
% 分项给分的答案本来就该一句一行,读者(阅卷人)靠句号扫分点。
% 手动断行既解决排版,又让内容结构更清楚 —— 不是妥协,是双赢。
% ⚠️ 若将来修改了这段文字导致又不齐,先看这里:多半还是在句号处补 \\。
%----------------------------------------------------------------------
\newcommand{\Fivhao}{\fontsize{10.5bp}{16bp}\selectfont}
\newcommand{\Sihao}{\fontsize{12bp}{18bp}\selectfont}
\newcommand{\Xiaowu}{\fontsize{9bp}{13bp}\selectfont}
\AtBeginDocument{\Fivhao\selectfont}
%======================================================================
% 1. 抬头信息
% \keysetup{ ... } 里填;留空的项自动不印(不留空横线)
%======================================================================
\def\ouckey@year{}\def\ouckey@term{}\def\ouckey@course{}
\def\ouckey@type{}\def\ouckey@college{}\def\ouckey@grade{}
\def\ouckey@major{}\def\ouckey@dept{}\def\ouckey@teacher{}
\def\ouckey@examiner{}\def\ouckey@mode{}\def\ouckey@date{}
\def\ouckey@total{}
%----------------------------------------------------------------------
% key 赋值:空值规范成空串
%
% ⚠️ 所有 key 一律走这里,不要退回 `\def\ouckey@xxx{#1}`。
% 原因见文件下方「判空」一节的完整说明:那样 dept={} 会存成
% 「一个空组」,后续 \detokenize 展开后仍非空,判空失败,
% 抬头会印出孤立的「开课学院:」而没有值。
%
% ⚠️ 必须定义在 \define@key 之**前**,否则展开时 undefined。
%----------------------------------------------------------------------
\newcommand{\ouckey@setkey}[2]{%
\if\relax\detokenize{#2}\relax
\def#1{}%
\else
\def#1{#2}%
\fi}
% 所有 key 一律走 \ouckey@setkey
\define@key{ouckey}{year}{\ouckey@setkey{\ouckey@year}{#1}}
\define@key{ouckey}{term}{\ouckey@setkey{\ouckey@term}{#1}}
\define@key{ouckey}{course}{\ouckey@setkey{\ouckey@course}{#1}}
\define@key{ouckey}{type}{\ouckey@setkey{\ouckey@type}{#1}}
\define@key{ouckey}{college}{\ouckey@setkey{\ouckey@college}{#1}}
\define@key{ouckey}{grade}{\ouckey@setkey{\ouckey@grade}{#1}}
\define@key{ouckey}{major}{\ouckey@setkey{\ouckey@major}{#1}}
\define@key{ouckey}{dept}{\ouckey@setkey{\ouckey@dept}{#1}}
\define@key{ouckey}{teacher}{\ouckey@setkey{\ouckey@teacher}{#1}}
\define@key{ouckey}{examiner}{\ouckey@setkey{\ouckey@examiner}{#1}}
\define@key{ouckey}{mode}{\ouckey@setkey{\ouckey@mode}{#1}}
\define@key{ouckey}{date}{\ouckey@setkey{\ouckey@date}{#1}}
\define@key{ouckey}{total}{\ouckey@setkey{\ouckey@total}{#1}}
\newcommand{\keysetup}[1]{\setkeys{ouckey}{#1}}
%----------------------------------------------------------------------
% 判空(修正:原实现有真 bug)
%
% ⚠️⚠️ 旧实现的坑
% 旧代码是
% \newcommand{\ouckey@ifempty}[1]{%
% \if\relax\detokenize{#1}\relax ... }
% 调用处写 `\ouckey@ifempty{\ouckey@dept}{空}{非空}`。
% **\detokenize 不展开参数** —— 它看到的是字符串 `\ouckey@dept`
% (宏名本身),永远非空 ⇒ 判空永远失败。
% 后果:`dept = {}` 时抬头仍印出孤立的「开课学院:」,后面没有值。
% 这是旧版就有的 bug(三格布局下不明显)。
%
% 为什么不在宏里修
% 试过「先 \edef 展开再 \detokenize」的版本,会报
% ! Incomplete \iffalse; all text was ignored after line 49.
% 因为 \if...\else...\fi 被 \expandafter 链打散,TeX 匹配 \fi 时错位。
% 这类宏层展开的修补很脆,不值得。
%
% ✅ 正确做法:**在 \define@key 里就把空值规范化**(见文件上方
% \ouckey@setkey)。空值存成真正的空串,判空就能工作。
%
% ⚠️ 这里只留判空宏本身。它拿到的是**已经规范化过的值**
% (`\ouckey@dept` 展开后就是纯文本),所以 \detokenize 有效。
%----------------------------------------------------------------------
\newcommand{\ouckey@ifempty}[1]{%
\if\relax\detokenize{#1}\relax
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi}
%----------------------------------------------------------------------
% 一项信息:标签加粗 + 值,**独占一行**
%
% ⚠️⚠️ 为什么从"一行三格"改成"一行一项"
%
% 旧实现是三个 \makebox[0.3333\linewidth][l] 并排:
% \makebox[固定宽][l]{内容}
% \makebox 是**不可断的盒子**,行为是:
% · 内容比盒宽短 → 左对齐,右侧留白(正常)
% · 内容比盒宽长 → **直接溢出盒外,压到右边一格上**(就是 bug)
% · 盒内中文**不能换行**(LR 模式,不做段落断行)
% 所以三格布局对短字段友好,对长字段必然重叠。
%
% 把 dept 填成
% 计算机科学与技术学院(人工智能与大数据技术方向实验班)
% 后,读取
% 开课学院:(70.87~123.37) 值:(123.37~406.87)
% 而第二格「课程名称」本该从 216.0 开始 ⇒ **重叠 190.87bp**,
% 把「课程名称 / 试卷类型」整个压住。
%
% ⚠️ **编译完全不报错,也没有 Overfull 警告**(rc=0 over=0)。
% \makebox 内容溢出盒外时 TeX 不认为是"坏行"。
% ⇒ 这类 bug 只能靠看渲染结果或量 span 坐标发现,看日志发现不了。
%
% 现在的做法:每项 `\par\noindent\textbf{标签:}值\par`,普通段落。
% · 超长自动换行(中文可在任意汉字间断行)
% · 不存在并排,也就不存在互相压住
% · 抬头是"查阅"性质,一项一行比三格更容易扫读
% 代价:抬头从 3 行变 9 行,多占约 6 行。答案卷 A4、内容不满页,
% 多 6 行不影响版面。
%
% ⚠️ 留空的项仍**整项不印**(也不留空行),逻辑不变。
%----------------------------------------------------------------------
\newcommand{\ouckey@inforow}[2]{%
\ouckey@ifempty{#2}{}{%
% 行距收紧到 15bp(\Fivhao 的基线距是 16bp)。抬头是"查阅"信息,
% 不需要正文那种行距;9 行 × 省 5.8bp ≈ 省 52bp,将近 3 行正文。
{\fontsize{10.5bp}{15bp}\selectfont
\par\noindent\textbf{#1:}#2\par}}}
%----------------------------------------------------------------------
% 抬头:标题 + 信息栏 + 一条分隔线
% 标题 = 学年学期 + 课程名 + 试卷类型 +「参考答案及评分标准」
%----------------------------------------------------------------------
\newcommand{\makekeyhead}{%
\par\noindent
\begin{minipage}{\linewidth}\centering
{\fontsize{16bp}{24bp}\selectfont\heiti
中国海洋大学%
\ouckey@ifempty{\ouckey@year}{}{\quad\TNR{\ouckey@year}—\TNR{\the\numexpr\ouckey@year+1\relax}~学年}%
\ouckey@ifempty{\ouckey@term}{}{\ouckey@term 学期}\par}
\vspace*{6bp}
{\fontsize{15bp}{22bp}\selectfont\heiti
《\ouckey@course》期末考试%
\ouckey@ifempty{\ouckey@type}{}{(\ouckey@type~卷)}%
参考答案及评分标准\par}
\end{minipage}\par
\vspace*{10bp}
\hrule height 1.2bp
\vspace*{8bp}
% 每项一行。顺序按"读者关心程度"排:
% 先课程、后学院、再人与考试形式,最后分值。
% 留空的自动跳过,所以实际行数随填写项变化。
\ouckey@inforow{课程名称}{\ouckey@course}
\ouckey@inforow{开课学院}{\ouckey@dept}
\ouckey@inforow{学生学院}{\ouckey@college}
\ouckey@inforow{年级专业}{\ouckey@major}
\ouckey@inforow{试卷类型}{\ouckey@ifempty{\ouckey@type}{}{\ouckey@type~卷}}
\ouckey@inforow{考试方式}{\ouckey@mode}
\ouckey@inforow{任课教师}{\ouckey@teacher}
\ouckey@inforow{命题人}{\ouckey@examiner}
\ouckey@inforow{满分}{\ouckey@ifempty{\ouckey@total}{}{\TNR{\ouckey@total}~分}}
% ★ 抬头【不印考试日期】。
% date 这个键仍保留(老文件写了 date= 不会报 unknown key),
% 但不再渲染,对话框里也不显示,避免抬头行数随日期有无而浮动。
\vspace*{6bp}
\hrule height 0.6bp
\vspace*{10bp}}
%======================================================================
% 2. 分值标记
% \pt{2} → 「(2 分)」——嵌在答案文字中间,标"这一点值 2 分"
% \ptline{2}{要点} → 要点靠左、分值靠右对齐(长要点更清爽)
%======================================================================
% 分值标注「(X 分)」。
% ⚠️ 里面用 `~`(\nobreakspace)是**有意为之** —— 不让「X」和「分」
% 分处两行,否则会看到「(1」在行末、「分)」在下一行,很难看。
% 代价是「(X 分)」成了不可断的整体,极端行宽下会撑破版心。
% ⇒ 这个代价接受,解决办法是在**源码的句子边界手动断行**,
\newcommand{\pt}[1]{{\small(\TNR{#1}~分)}}
\newcommand{\ptline}[2]{%
\par\noindent #2\unskip\par\nobreak
\vspace*{-1.2\baselineskip}%
\par\noindent\hfill{\small(\TNR{#1}~分)}\par\vspace*{2bp}}
% 阿拉伯数字 → 中文(1..16)★必须在使用之前定义
\newcommand{\ouckey@cnnum}[1]{%
\ifcase\numexpr#1\relax\or 一\or 二\or 三\or 四\or 五\or 六%
\or 七\or 八\or 九\or 十\or 十一\or 十二%
\or 十三\or 十四\or 十五\or 十六\fi}
%======================================================================
% 3. 大题标题
% \keysection{一}{单项选择题}{10}{2}
% → 一、单项选择题(本题共 10 题,每小题 2 分,共 20 分)
% 小计自动算,不用手填。
% \keysectionfree{二}{填空题}{共 10 空,每空 1 分}
% → 二、填空题(共 10 空,每空 1 分) —— 分值不整齐时用这个
%======================================================================
\newcommand{\keysection}[4]{%
\par\addvspace{10bp}%
{\Sihao\heiti
#1、#2(本题共~\TNR{#3}~题,每小题~\TNR{#4}~分,共~\TNR{\the\numexpr#3*#4\relax}~分)\par}%
\addvspace{5bp}}
\newcommand{\keysectionfree}[3]{%
\par\addvspace{10bp}%
{\Sihao\heiti #1、#2\par}%
\addvspace{5bp}}
%======================================================================
% 4. 客观题答案
% \objkeys{C,B,A,B,C,A,C,A,C,A} → 每行 6 个,自动换行
% \objkeys[5]{...} → 每行 5 个
% 答案串逗号分隔;题号自动递增。
%
% ★ 这个宏反复改过很多次,下面几条是实际结论,动之前先看清:
%
% 【不能用 \par 分行】
% 在 \@for 循环体里写 "\hbox to \textwidth{...}\par\noindent",
% 整段放进文档正文是好的,但一旦包进宏体就失效——
% 十几项全排在同一个基线(pdftotext -bbox 的 yMin 完全相同),
% 后续直接溢出纸边。\@for 先把循环体抓成参数,\par 的展开时机不对。
%
% 【不能用 \unvbox + \prevdepth 思路】
% \unvbox 执行完寄存器即被清空,紧跟的 \ifdim\ht...>0pt 恒为假;
% 而且 \unvbox 后 TeX 把 \prevdepth 设成 -1000pt 抑制行间胶水,
% 各行会叠在一起(实际间距只有一个行高)。
%
% 【不能用 \makebox[<变宽>]】
% 在 \@for 循环里 \makebox[\dimexpr\textwidth/\perrow\relax] 或
% \makebox[\cw] 会**静默吞掉内容**(PDF 里整行消失,且不报错,
% 只在 log 里留 Undefined control sequence / Missing number)。
% → 用 \hbox to <定宽> 代替,定宽预先算进 \dimen 寄存器。
%
% 可靠做法(本实现):
% 逐项往 \ouckey@cellbox 塞一个 "\hbox to \ouckey@cellw" 单元格;
% 攒满 \\ouckey@perrow 个就整理成一行,追加进 \ouckey@allbox(\vbox);
% 行与行之间自己补胶水 = \baselineskip - \ht(行) - \dp(行);
% 最后 \unvbox 一次性铺出。全程不碰 \par、不碰 \makebox。
%======================================================================
\newbox\ouckey@cellbox
\newbox\ouckey@linebox
\newbox\ouckey@allbox
\newcount\ouckey@objn
\newcount\ouckey@objrow
\newcount\ouckey@perrow
\newcount\ouckey@rowsdone
\newdimen\ouckey@cellw
\newdimen\ouckey@textw
\newdimen\ouckey@gap
\newcommand{\ouckey@flushline}{%
\setbox\ouckey@linebox\hbox to \ouckey@textw{\unhbox\ouckey@linebox\hss}%
\ouckey@gap=\dimexpr\baselineskip-\ht\ouckey@linebox
-\dp\ouckey@linebox\relax
\ifnum\ouckey@rowsdone>0
\setbox\ouckey@allbox\vbox{\unvbox\ouckey@allbox
\vskip\ouckey@gap \box\ouckey@linebox}%
\else
\setbox\ouckey@allbox\vbox{\unvbox\ouckey@allbox
\box\ouckey@linebox}%
\fi
\advance\ouckey@rowsdone by 1
\setbox\ouckey@linebox\hbox{}}
\newcommand{\ouckey@addcell}[1]{%
\advance\ouckey@objn by 1
\setbox\ouckey@cellbox\hbox to \ouckey@cellw{%
\TNR{\the\ouckey@objn}.#1\hss}%
\setbox\ouckey@linebox\hbox{\unhbox\ouckey@linebox
\box\ouckey@cellbox}%
\ouckey@objrow=\ouckey@objn
\divide\ouckey@objrow by \ouckey@perrow
\multiply\ouckey@objrow by \ouckey@perrow
\ifnum\ouckey@objn=\ouckey@objrow \ouckey@flushline\fi}
\newcommand{\objkeys}[2][6]{%
\par\addvspace{4bp}%
\begingroup
\ouckey@textw=\textwidth
\ouckey@perrow=#1
\ouckey@cellw=\dimexpr\ouckey@textw/\ouckey@perrow\relax
\ouckey@objn=0
\ouckey@rowsdone=0
\setbox\ouckey@linebox\hbox{}%
\@for\ouckey@item:=#2\do{\expandafter\ouckey@addcell\expandafter{\ouckey@item}}%
\ouckey@flushline
\noindent\unvbox\ouckey@allbox
\par
\endgroup
\addvspace{4bp}}
%======================================================================
% 5. 主观题
% \keyitem{1}{8}{答案正文……} → 「1.(8 分)答案正文……」
% \keyitemstar{2}{答案} → 只出题号+答案,不印分值
%
% ★ 注意:参数正文里可以正常用 \\ 换行与 \pt{..};
% 但**不要在里面嵌 tabular/tikzpicture 环境**——那些环境的 & 与 \\
% 会被当成参数分隔符,导致 "File ended while scanning"。
% 表格/图请写在 \keyitem 之后(先写完文字,再另起环境)。
%======================================================================
\newcommand{\keyitem}[3]{%
\par\addvspace{5bp}\noindent
{\bfseries \TNR{#1}.}\pt{#2}\hspace*{3bp}#3\relax\par}
\newcommand{\keyitemstar}[2]{%
\par\addvspace{5bp}\noindent
{\bfseries \TNR{#1}.}\hspace*{3bp}#2\relax\par}
%======================================================================
% 6. 【评分标准】块
% 左侧一条 2bp 竖线 + 灰底标题,视觉上和"答案正文"分开,但不抢眼
% \scoring{内容}
% \scoring*{内容} → 不带左侧竖线(行内短说明用)
%======================================================================
\newcommand{\scoring}{%
\@ifstar{\ouckey@scoring@plain}{\ouckey@scoring@rule}}
\newcommand{\ouckey@scoring@rule}[1]{%
\par\addvspace{4bp}%
\noindent
\begin{minipage}{\linewidth}%
\hspace*{2bp}%
\vrule width 2bp \hspace*{6bp}%
\begin{minipage}{\dimexpr\linewidth-10bp\relax}%
{\bfseries【评分标准】}#1%
\end{minipage}%
\end{minipage}%
\par\addvspace{4bp}}
\newcommand{\ouckey@scoring@plain}[1]{%
\par\addvspace{2bp}\noindent{\bfseries【评分标准】}#1\par\addvspace{2bp}}
% 备注 / 说明(比评分标准低一级,用于"其他解法同等给分"这类话)
\newcommand{\keynote}[1]{%
\par\addvspace{3bp}\noindent
{\small\bfseries 说明:}#1\par\addvspace{3bp}}
%======================================================================
% 7. 分值汇总表
% \keytotal{20,20,15,10,15,20}
% → 自动画"题号 / 一 二 三 … / 合计"三行表,合计自动求和
% 用于阅卷前核对"各大题分值相加是否等于满分"。
%======================================================================
\newcount\ouckey@sum
\newcount\ouckey@cnt
%----------------------------------------------------------------------
%
% 坑1 \edef\head{\head&...} 自引用 → 无限展开,parameter stack 爆掉。
% 坑2 \g@addto@macro / \toks 存 token 序列 → 存进去的是 \ouckey@cur
% 这个"宏名",等表格排版时循环早走完,16 格全变成最后一个值。
% 坑3 本想用 \xdef 拼,但在里面包 \TNR{...}(内含 \normalfont,
% 不可展开)→ 又撞栈。
%
% 正解:\xdef 拼装,且**只拼纯字符**,字体命令留到表格里再加。
% 数字用 Times 是题号行的既有风格,直接靠正文西文字体即可。
%----------------------------------------------------------------------
\newcommand{\keytotal}[1]{%
\par\addvspace{12bp}%
\ouckey@sum=0 \ouckey@cnt=0
\def\ouckey@head{}\def\ouckey@body{}%
\@for\ouckey@v:=#1\do{%
\edef\ouckey@cur{\ouckey@v}%
\advance\ouckey@cnt by 1
\advance\ouckey@sum by \ouckey@cur\relax
\edef\ouckey@cn{\ouckey@cnnum{\the\ouckey@cnt}}%
\xdef\ouckey@head{\ouckey@head&\ouckey@cn}%
\xdef\ouckey@body{\ouckey@body&\ouckey@cur}}%
\noindent
\begin{tabular}{|c|*{\the\ouckey@cnt}{c|}|c|}
\hline
\textbf{题号}\ouckey@head&\textbf{合计}\\
\hline
\textbf{满分}\ouckey@body&\the\ouckey@sum\\
\hline
\end{tabular}%
\par\addvspace{12bp}}
%======================================================================
% 8. 结尾签署栏
% ★ 只保留命题人一行。审核人、系(教研室)主任、日期都不印——
% 这三个位置是纸质归档时由相应人员手签,答案稿本身不预印。
% 换标签就直接改下面这行文字;想整栏不要,删掉 tex 里的 \keysign 即可。
%======================================================================
\newcommand{\keysign}{%
\par\addvspace{18bp}%
\noindent
\makebox[0.5\linewidth][l]{命题人(签字):\hrulefill}\par}
%======================================================================
% 9. 页脚:第 X 页 共 Y 页(居中)
%======================================================================
\def\ps@ouckeypage{%
\def\@oddfoot{\hfill{\small 第\,\thepage\,页\quad 共\,\pageref{LastPage}\,页}\hfill}%
\let\@evenfoot\@oddfoot
\def\@oddhead{}\let\@evenhead\@oddhead}
\pagestyle{ouckeypage}
% 包结尾必须恢复,否则会污染文档类里 @ 的处理
\makeatother
\endinput