Files
wordpress-kratos-yang/inc/theme-mail-template.php
T
laoyang 269364bed4 版本号 3.2.6 -> 3.2.7(纪念日全部默认不勾 + 彩色说明页 + 邮件彩色 + 双引用块)
哀悼默认值全部改为不勾选
- 九个纪念日的 default 全改成 0。之前法定那三个是 1,导致全新安装的站点
  一到日子就自己变黑白,而站主根本没同意过这件事。
- 后台多选组件的 std 全为 0,打开设置页一个都不勾。
- 「从未保存过」与「保存过但全不勾」两条路径都返回空数组,
  不能因为结果为空就回退到默认 —— 那等于用户取消勾选无效。

新增彩色说明页(inc/theme-mourning-page.php + pages/page-mourning.php)
- 站点变黑白后必须有个地方解释清楚,否则访客只看到一片灰、以为站点坏了。
- 三处入口:顶部提示条(wp_body_open,首屏可见)、页脚常驻链接、
  [yang_mourning] 短代码。
- 页面内容由真实配置驱动,不是写死的文案:没勾的日期不会出现,
  今天在列表里带「今天」标记,一个都没勾时明说「不会生效」。
- 页面必须保持彩色,否则解释「为什么是灰的」的页面自己也是灰的。
  取消灰度做了两层:filter:none 与 grayscale(0%) 都写,且带 !important;
  优先级放在 wp_head 的 2,晚于灰度样式的 1。
- 路由走 parse_request 优先级 5 而非 template_redirect:
  避免先走一遍 404 判定与 404 模板。带子目录安装时先剥掉前缀再比对。
- 不抢占用户自己建的页面:如果站里已有同名 slug 的页面就放手。
  否则用户会觉得「我建的页面坏了」,且没有任何提示。

邮件不受影响,且回复邮件同时给出双方内容
- 邮件是独立 HTML,不走 wp_head,结构上就不可能被灰度,已断言验证。
- theme-mail-template 的单个 quote 改成 quotes 列表,一个模板同时渲染
  「被回复的那条」与「新的回复」,最后一块用主色高亮(回复是重点),
  前面的用弱化色。只有一块时退化为主色,与旧行为一致。
- 通过 wpdiscuz_email_content 重建整封邮件,不需要改插件。
  审核通过通知用 wpdiscuz_comment_approved_email_content 走同一模板,
  但它只给 post_id、不给评论对象,能放的内容受此限制。

两个必须记下来的坑
- wpDiscuz 在过滤器之后还会做一次 html_entity_decode 再发信。
  esc_html 的输出会被那一步还原成真标签,评论里的 <script> 直接生效。
  所以返回前先把 & 编码一次。本地 stub 没有这一步,测试看不出来。
- header.php 从来没有调用 wp_body_open,挂在上面的东西全部静默失效。
  不调用不会报错,只会「什么都没发生」。已补在 <body> 之后。

yang_mail_text 保留换行
- 原来用 wp_strip_all_tags($t, true),第二个参数 remove_breaks 会把换行
  压成空格;而 strip_tags 遇到 <br> 是直接删掉不补换行的。
  结果整条评论被拍成一行,模板里的 nl2br 成了死代码。
- 改为先把块级边界降级成 \n,再去标签(remove_breaks=false)。
  收行内空白用 [^\S\n] 而不是 [ \t\f\v]:PCRE 的 \v 是垂直空白类,
  会把 \n 一起匹配掉。

说明页对比度修正(WCAG AA)
- 金色 #b8860b 在白底只有 3.06:1,当装饰色没问题,当 12-15px 文字偏虚。
  拆成 --ym-gold(装饰)与 --ym-gold-text(#946600,4.75:1,仍属金色)。
- 分组小标题 #9aa5b1 只有 2.36:1,改用 #5f6b78(5.12:1)。
- 「今天」标签白字压金底 3.25:1,改用 #8a6100(5.54:1)。
2026-10-23 14:43:47 +08:00

423 lines
18 KiB
PHP
Raw 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.
<?php
/**
* 邮件模板渲染
*
* 设计约束(写这套模板的全部理由):
*
* 1. **必须是表格布局**。Outlook 2007-2019 用 Word 的排版引擎,完全不支持
* flex / grid,连 `max-width` 都不稳。只有 `<table>` + `cellpadding` 在老
* Outlook 里可靠。现在的 web 布局写法在邮件里必崩。
*
* 2. **样式必须内联**。Gmail 会把 `<head>` 里的 `<style>` 全部剥掉(含 media query),
* QQ 邮箱、网易邮箱同样。所以每个 `td` 都要带 `style="..."`,不能依赖 class。
*
* 3. **宽度 600px**。这是邮件客户端预览窗格的通用安全宽度。用
* `width="600"` 属性 + `style="width:600px;max-width:600px"` 双保险。
*
* 4. **不用外部图片做装饰**。邮件里的外链图默认被 Gmail / QQ 邮箱屏蔽,
* 用户看到的是一堆灰框。所有视觉元素用纯色块 + 圆角 + 文字实现。
*
* 5. **不用 CSS 变量**。邮件客户端不支持,写了就是丢失样式。
*
* 6. 深色模式:Android Gmail / Apple Mail 会反色。这里显式声明
* `color-scheme` 与 `supported-color-schemes`,并避免纯黑纯白。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* 站点主色(可被选项覆盖)
*
* 取主题的主色选项;取不到就用一个稳妥的深蓝灰。
* 不用亮色,因为邮件背景是浅灰,亮色正文对比度不够。
*/
function yang_mail_brand_color()
{
$c = kratos_option('top_color', '#24292e');
if (!preg_match('/^#[0-9a-fA-F]{6}$/', (string) $c)) {
$c = '#24292e';
}
return $c;
}
/**
* 把主色调暗一档,用作渐变末端 / 描边
*
* 邮件里不能用 `filter: brightness()`(不支持),只能算。
*/
function yang_mail_shade($hex, $ratio = 0.82)
{
$hex = ltrim($hex, '#');
if (strlen($hex) !== 6) {
return '#24292e';
}
$out = '#';
foreach (str_split($hex, 2) as $pair) {
$v = max(0, min(255, (int) round(hexdec($pair) * $ratio)));
$out .= str_pad(dechex($v), 2, '0', STR_PAD_LEFT);
}
return $out;
}
/**
* 判断一个颜色是深是浅,决定叠在上面的文字用白还是深色
*
* 用相对亮度公式(WCAG),不是简单取平均 —— 人眼对绿最敏感、蓝最不敏感,
* 直接平均会把 #00ff00 判成中等亮度。
*/
function yang_mail_is_dark($hex)
{
$hex = ltrim((string) $hex, '#');
if (strlen($hex) !== 6) {
return true;
}
$r = hexdec(substr($hex, 0, 2)) / 255;
$g = hexdec(substr($hex, 2, 2)) / 255;
$b = hexdec(substr($hex, 4, 2)) / 255;
$lin = function ($c) {
return $c <= 0.03928 ? $c / 12.92 : pow(($c + 0.055) / 1.055, 2.4);
};
$L = 0.2126 * $lin($r) + 0.7152 * $lin($g) + 0.0722 * $lin($b);
return $L < 0.5;
}
/**
* 把可能含 HTML 的片段转成邮件安全的纯文本(保留换行)
*
* 邮件正文里塞用户提交的评论内容,必须防注入:邮件客户端也会执行
* `<script>`(部分 webmail 会),而且一旦渲染出畸形 HTML,整封邮件布局就崩。
*
* ── 换行为什么要专门处理 ─────────────────────────────────────
* 两个坑都会把整条评论拍成一行:
* 1. `wp_strip_all_tags($x, true)` 的第二个参数是 remove_breaks,
* 真实实现里会 `preg_replace('/[\r\n\t ]+/', ' ', $x)` —— 换行直接没了。
* 所以这里必须传 false。
* 2. `strip_tags()` 遇到 `<br>` 是**直接删掉、不补换行**的,`</p>` 同理。
* 评论里手写的换行和段落因此在去标签阶段就无痕消失。
* 结论:先把块级边界换成 `\n`,再去标签,最后才收拾空白。
* 这样模板里的 `nl2br()` 才有换行可转,不是一段死代码。
*/
function yang_mail_text($html, $limit = 0)
{
$t = (string) $html;
// 1. 先把"视觉上的换行"降级成真正的 \n
$t = preg_replace('@<br\s*/?>@i', "\n", $t);
$t = preg_replace('@</(p|div|blockquote|pre|h[1-6])\s*>@i', "\n", $t);
// <li> 自己带前缀,就不要</li>再加换行,否则列表每项之间会多一个空行
$t = preg_replace('@<li[^>]*>@i', "\n- ", $t);
// 2. 去标签,但不要 remove_breaks(那是换行消失的真正原因)
$t = wp_strip_all_tags($t, false);
// 3. 收拾空白:行内空白收成一个空格,换行保留,最多一个空行
$t = html_entity_decode($t, ENT_QUOTES, 'UTF-8');
$t = str_replace(array("\r\n", "\r"), "\n", $t);
/* 用 [^\S\n]("非空白"取反 + 排除换行 = 除换行外的空白)而不是
[ \t\f\v]:PCRE 里 \v 是**垂直空白类**,把 \n 也算进去,
写成 [ \t\f\v] 会连换行一起替换成空格 —— 而且只在带 /u 时表现如此,
极易漏看。语义直接写出来,别依赖转义字母的记忆。 */
$t = preg_replace('/[^\S\n]+/u', ' ', $t);
$t = preg_replace('/ *\n */u', "\n", $t);
$t = preg_replace('/\n{3,}/u', "\n\n", $t);
$t = trim($t);
if ($limit > 0 && mb_strlen($t, 'UTF-8') > $limit) {
$t = mb_substr($t, 0, $limit, 'UTF-8') . '…';
}
return $t;
}
/**
* 把主色与白色按比例混合,得到一档浅色
*
* 为什么不用 rgba:Outlook(Word 引擎)完全不支持 rgba(),
* 写了会当成非法值丢弃 ⇒ 半透明背景变成完全透明,badge 直接"消失"。
* 所以这里预先算出实色。$ratio 是白色的占比(0=纯主色,1=纯白)。
*/
function yang_mail_tint($hex, $ratio = 0.18, $onto = '#ffffff')
{
$hex = ltrim((string) $hex, '#');
$onto = ltrim((string) $onto, '#');
if (strlen($hex) !== 6 || strlen($onto) !== 6) {
return '#ffffff';
}
$out = '#';
for ($i = 0; $i < 3; $i++) {
$a = hexdec(substr($hex, $i * 2, 2));
$b = hexdec(substr($onto, $i * 2, 2));
$v = (int) round($a + ($b - $a) * $ratio);
$out .= str_pad(dechex(max(0, min(255, $v))), 2, '0', STR_PAD_LEFT);
}
return $out;
}
/**
* 生成一个按钮(邮件里的按钮只能用表格单元格做,不能用 <button>)
*
* Outlook 不认 `<a>` 的 `padding`,必须把 padding 放在 `<td>` 上。
*/
function yang_mail_button($text, $url, $bg = null)
{
if ($bg === null) {
$bg = yang_mail_brand_color();
}
$fg = yang_mail_is_dark($bg) ? '#ffffff' : '#1a1a1a';
$url = esc_url($url);
$text = esc_html($text);
return '<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto;">'
. '<tr><td align="center" bgcolor="' . esc_attr($bg) . '" style="border-radius:6px;padding:13px 34px;">'
. '<a href="' . $url . '" target="_blank" rel="noopener" '
. 'style="display:inline-block;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,\'Helvetica Neue\',Arial,\'PingFang SC\',\'Microsoft YaHei\',sans-serif;'
. 'font-size:15px;font-weight:600;line-height:1;color:' . $fg . ';text-decoration:none;letter-spacing:0.3px;">'
. $text . '</a></td></tr></table>';
}
/**
* 渲染整封邮件
*
* @param array $args {
* @type string $title 邮件主标题(收件箱预览也会看到)
* @type string $preheader 预读文字,显示在收件箱标题后面,不影响正文
* @type string $heading 正文第一行大字
* @type string $intro 正文引导段落(纯文本)
* @type array $rows 表格数据,每项 array('label'=>, 'value'=>, 'strong'=>bool)
* @type array $quote 引用块 array('label'=>, 'content'=>, 'meta'=>)
* @type string $cta_text 按钮文字
* @type string $cta_url 按钮链接
* @type string $note 底部提示(纯文本)
* @type string $badge 顶部小标签,如「新评论」
* }
* @return string 完整 HTML
*/
function yang_mail_render($args)
{
$d = wp_parse_args($args, array(
'title' => get_bloginfo('name'),
'preheader' => '',
'heading' => '',
'intro' => '',
'rows' => array(),
'quote' => array(), // 单个引用块(旧接口,保留兼容)
'quotes' => array(), // 多个引用块,按顺序渲染
'cta_text' => '',
'cta_url' => '',
'note' => '',
'badge' => '',
));
/* 统一成 $quoteList:
* 调用方可能只给一个 quote,也可能给 quotes 数组(评论回复场景要同时放
* 「被回复的那条」和「新的回复」,两段各自有作者和时间,必须分开渲染)。
* 两者都给时以 quotes 为准,避免重复出现。 */
$quoteList = array();
if (!empty($d['quotes']) && is_array($d['quotes'])) {
$quoteList = $d['quotes'];
} elseif (!empty($d['quote'])) {
$quoteList = array($d['quote']);
}
$brand = yang_mail_brand_color();
$brandD = yang_mail_shade($brand, 0.78);
$onBrand = yang_mail_is_dark($brand) ? '#ffffff' : '#1a1a1a';
// badge 的背景:把主色往白色方向提亮一档,得到比主色浅、但同色相的实色。
// 主色本身是深色时提亮 22%,浅色时压暗 —— 否则浅主色上叠浅 badge 会看不见。
$badgeBg = yang_mail_is_dark($brand)
? yang_mail_tint($brand, 0.22)
: yang_mail_shade($brand, 0.88);
$badgeBd = yang_mail_is_dark($brand)
? yang_mail_tint($brand, 0.34)
: yang_mail_shade($brand, 0.72);
$site = get_bloginfo('name');
$home = home_url('/');
$year = date('Y');
// 字体栈:中文优先 PingFang / 微软雅黑,英文走系统默认。
// 邮件里不能用 @font-face(Gmail 剥掉),只能靠系统字体。
$sans = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif";
ob_start();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="x-apple-disable-message-reformatting">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<title><?php echo esc_html($d['title']); ?></title>
<!--[if mso]>
<style type="text/css">
body,table,td,p,a{font-family:Arial,'Microsoft YaHei',sans-serif !important;}
</style>
<![endif]-->
</head>
<body style="margin:0;padding:0;width:100%;background-color:#eef1f4;">
<?php /* 预读文字:显示在收件箱列表里标题的后面。藏起来但必须真实存在,且要足够长,
否则客户端会把正文第一句抓上去当摘要。 */ ?>
<div style="display:none;font-size:1px;color:#eef1f4;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">
<?php echo esc_html($d['preheader'] ? $d['preheader'] : $d['heading']); ?>
</div>
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="background-color:#eef1f4;">
<tr><td align="center" style="padding:32px 12px;">
<?php /* 宽度写法是「混合式」:
· Outlook(Word 引擎)只认 HTML 属性 width="600",CSS 的 max-width 它当没看见;
· 其它客户端认 CSS,于是 width:100% + max-width:600px 让手机窄屏自动收窄。
两套并存,各取所需。只写死 600px 会让手机横向滚动——实测过。 */ ?>
<table border="0" cellpadding="0" cellspacing="0" width="600" role="presentation" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e3e7ec;">
<?php /* ── 顶部品牌条 ── */ ?>
<tr>
<td bgcolor="<?php echo esc_attr($brand); ?>" style="background-color:<?php echo esc_attr($brand); ?>;padding:0;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation">
<tr>
<td style="padding:26px 32px;">
<div style="font-family:<?php echo $sans; ?>;font-size:19px;font-weight:600;color:<?php echo esc_attr($onBrand); ?>;letter-spacing:0.2px;line-height:1.3;">
<?php echo esc_html($site); ?>
</div>
<?php if ($d['badge'] !== ''): ?>
<?php /* badge 用预算好的实色,不用 rgba —— Outlook 不认 rgba,会直接丢掉背景 */ ?>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin-top:10px;">
<tr>
<td bgcolor="<?php echo esc_attr($badgeBg); ?>" style="background-color:<?php echo esc_attr($badgeBg); ?>;border:1px solid <?php echo esc_attr($badgeBd); ?>;border-radius:20px;padding:4px 12px;font-family:<?php echo $sans; ?>;font-size:12px;font-weight:500;line-height:1.4;">
<span style="color:<?php echo esc_attr($onBrand); ?>;"><?php echo esc_html($d['badge']); ?></span>
</td>
</tr>
</table>
<?php endif; ?>
</td>
</tr>
</table>
</td>
</tr>
<?php /* 主色条下沿压一条深色,给扁平色块一点层次(渐变在 Outlook 里不生效,用纯色)。
height 必须**同时**写成 HTML 属性和 CSS:Outlook 只认属性,
只写 CSS 高度时这条 3px 带会撑成一行文字高(约 18px),比例就毁了。 */ ?>
<tr>
<td bgcolor="<?php echo esc_attr($brandD); ?>" height="3" style="background-color:<?php echo esc_attr($brandD); ?>;height:3px;line-height:3px;font-size:0;mso-line-height-rule:exactly;">&nbsp;</td>
</tr>
<?php /* ── 正文 ── */ ?>
<tr>
<td style="padding:34px 32px 8px 32px;">
<?php if ($d['heading'] !== ''): ?>
<div style="font-family:<?php echo $sans; ?>;font-size:21px;font-weight:600;color:#1a2028;line-height:1.45;margin:0 0 12px 0;">
<?php echo esc_html($d['heading']); ?>
</div>
<?php endif; ?>
<?php if ($d['intro'] !== ''): ?>
<div style="font-family:<?php echo $sans; ?>;font-size:15px;color:#4a5560;line-height:1.7;margin:0 0 22px 0;">
<?php echo esc_html($d['intro']); ?>
</div>
<?php endif; ?>
<?php foreach ($quoteList as $qi => $q): ?>
<?php
/* 引用块:左边一条竖线,用 border-left 而不是伪元素(邮件不支持 ::before)。
*
* 配色规则:**最后一块用主色高亮,前面的用弱化灰**。
* 为什么是「最后」而不是「第一」:评论回复场景里顺序是
* 「被回复的原评论」→「新的回复」,语义重点在新的回复上,所以让它高亮。
* 只有一个块时它既是第一也是最后 ⇒ 拿到主色,与旧版行为一致。
*/
$isLast = ($qi === count($quoteList) - 1);
$barColor = $isLast ? $brand : '#c2cbd4';
$barBg = $isLast ? '#f6f8fa' : '#fafbfc';
$barMargin = $isLast ? '22px' : '14px';
?>
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="margin:0 0 <?php echo $barMargin; ?> 0;">
<tr>
<td style="border-left:3px solid <?php echo esc_attr($barColor); ?>;background-color:<?php echo esc_attr($barBg); ?>;padding:16px 18px;border-radius:0 8px 8px 0;">
<?php if (!empty($q['label'])): ?>
<div style="font-family:<?php echo $sans; ?>;font-size:12px;font-weight:600;color:#5f6b78;letter-spacing:0.6px;text-transform:uppercase;margin:0 0 8px 0;">
<?php echo esc_html($q['label']); ?>
</div>
<?php endif; ?>
<div style="font-family:<?php echo $sans; ?>;font-size:15px;color:#2d3740;line-height:1.7;word-break:break-word;">
<?php echo nl2br(esc_html(isset($q['content']) ? $q['content'] : '')); ?>
</div>
<?php if (!empty($q['meta'])): ?>
<div style="font-family:<?php echo $sans; ?>;font-size:13px;color:#5f6b78;line-height:1.6;margin:9px 0 0 0;">
<?php echo esc_html($q['meta']); ?>
</div>
<?php endif; ?>
</td>
</tr>
</table>
<?php endforeach; ?>
<?php if (!empty($d['rows'])): ?>
<?php /* 信息表:两列。label 列定宽 88px 并右对齐,value 左对齐,视觉上最稳。
不用 table-layout:fixed 之外的花样,Outlook 只认这个。 */ ?>
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="margin:0 0 24px 0;border-top:1px solid #edf0f3;">
<?php foreach ($d['rows'] as $row): ?>
<tr>
<td width="88" valign="top" style="width:88px;padding:11px 0;border-bottom:1px solid #edf0f3;font-family:<?php echo $sans; ?>;font-size:13px;color:#5f6b78;line-height:1.6;white-space:nowrap;">
<?php echo esc_html($row['label']); ?>
</td>
<td valign="top" style="padding:11px 0 11px 16px;border-bottom:1px solid #edf0f3;font-family:<?php echo $sans; ?>;font-size:14px;color:#2d3740;line-height:1.6;word-break:break-word;">
<?php
$v = isset($row['value']) ? $row['value'] : '';
if (!empty($row['strong'])) {
echo '<strong style="font-weight:600;">' . esc_html($v) . '</strong>';
} else {
echo esc_html($v);
}
?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<?php if ($d['cta_text'] !== '' && $d['cta_url'] !== ''): ?>
<div style="margin:0 0 28px 0;">
<?php echo yang_mail_button($d['cta_text'], $d['cta_url']); ?>
</div>
<?php endif; ?>
<?php if ($d['note'] !== ''): ?>
<div style="font-family:<?php echo $sans; ?>;font-size:13px;color:#5f6b78;line-height:1.7;padding:16px 0 0 0;border-top:1px solid #edf0f3;">
<?php echo esc_html($d['note']); ?>
</div>
<?php endif; ?>
</td>
</tr>
<?php /* ── 页脚 ── */ ?>
<tr>
<td style="padding:24px 32px 30px 32px;">
<div style="border-top:1px solid #edf0f3;padding-top:20px;font-family:<?php echo $sans; ?>;font-size:12px;color:#5f6b78;line-height:1.9;">
<div style="margin:0 0 6px 0;">
<a href="<?php echo esc_url($home); ?>" target="_blank" rel="noopener" style="color:#3d4752;text-decoration:none;font-weight:600;"><?php echo esc_html($site); ?></a>
</div>
<div style="margin:0;">此邮件由系统自动发出,请勿直接回复。</div>
<div style="margin:6px 0 0 0;color:#6b7684;">&copy; <?php echo esc_html($year); ?> <?php echo esc_html($site); ?></div>
</div>
</td>
</tr>
</table>
<?php /* 底部说明:为什么收到这封邮件。放在卡片外面,视觉上属于系统信息 */ ?>
<div style="font-family:<?php echo $sans; ?>;font-size:12px;color:#5f6b78;line-height:1.7;padding:18px 8px 0 8px;max-width:600px;">
您收到这封邮件,是因为本站记录了与您邮箱相关的订阅或互动。
</div>
</td></tr>
</table>
</body>
</html>
<?php
return ob_get_clean();
}