2026-10-23 13:55:23 +08:00
|
|
|
|
<?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 判成中等亮度。
|
2026-10-23 16:13:48 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 阈值为什么是 0.179 而不是 0.5 ────────────────────────────
|
|
|
|
|
|
* 「白字对比度 = 黑字对比度」的那个点,可以解出来:
|
|
|
|
|
|
* 白字对比度 = 1.05 / (L + 0.05)
|
|
|
|
|
|
* 黑字对比度 = (L + 0.05) / 0.05
|
|
|
|
|
|
* 两者相等 ⇒ (L + 0.05)² = 0.0525 ⇒ L ≈ 0.179
|
|
|
|
|
|
* 所以在 L = 0.179 处,白字与黑字一样清楚;低于它白字更好,高于它黑字更好。
|
|
|
|
|
|
* 之前写 0.5,把一整段"其实该用深色字"的中间亮度全划成了"深色",
|
|
|
|
|
|
* 典型受害者就是亮蓝 #00a2ff(L=0.331):被判成深色 ⇒ 压白字 ⇒ 只有 2.76:1,
|
|
|
|
|
|
* 远低于 AA 的 4.5,品牌条上的站名其实很难看清。按 0.179 判定后走深色字,6.31:1。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 这个函数只回答"深还是浅",**不负责保证对比度**。中灰 #808080 落在
|
|
|
|
|
|
* 交叉点附近,白字 3.95、深字 4.41 —— 两个都不到 4.5,但必须选一个。
|
|
|
|
|
|
* 所以配字时不要直接用它,用 yang_mail_fg_on():那里是"黑白里取更优",
|
|
|
|
|
|
* 顺带在都不达标时退回纯黑/纯白,能多榨出一两档。
|
2026-10-23 17:08:55 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 本函数目前**没有生产调用点**(配字一律走 fg_on,配色档位一律走
|
|
|
|
|
|
* deepen_to)。保留它是为了给调用方一个便宜的"这是深色吗"判断,
|
|
|
|
|
|
* 以及 run_tests 里那三条亮度分类断言。别在配字场景重新启用它 ——
|
|
|
|
|
|
* 那正是 2.76:1 那个 bug 的来源。
|
2026-10-23 13:55:23 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_is_dark($hex)
|
2026-10-23 16:13:48 +08:00
|
|
|
|
{
|
|
|
|
|
|
return yang_mail_lum($hex) < 0.179;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 相对亮度(WCAG,0=纯黑 1=纯白)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_lum($hex)
|
2026-10-23 13:55:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
$hex = ltrim((string) $hex, '#');
|
2026-10-23 16:13:48 +08:00
|
|
|
|
if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
|
|
|
|
|
|
return 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
$lin = function ($c) {
|
|
|
|
|
|
$c /= 255;
|
|
|
|
|
|
return $c <= 0.03928 ? $c / 12.92 : pow(($c + 0.055) / 1.055, 2.4);
|
|
|
|
|
|
};
|
|
|
|
|
|
return 0.2126 * $lin(hexdec(substr($hex, 0, 2)))
|
|
|
|
|
|
+ 0.7152 * $lin(hexdec(substr($hex, 2, 2)))
|
|
|
|
|
|
+ 0.0722 * $lin(hexdec(substr($hex, 4, 2)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 给某个底色配文字色:在白与深之间取**对比度更高**的那个
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么不是"深色就配白、浅色就配深":中灰区(如 #808080)两边都不达标
|
|
|
|
|
|
* (白字 3.95、深字 4.41),二元判定的答案取决于阈值落在哪一侧,
|
|
|
|
|
|
* 而阈值本身对两个都不达标的情况毫无意义。取更优的那个至少是当前最好的选择,
|
|
|
|
|
|
* 而且这个判据跟"阈值该定多少"完全解耦 —— 换阈值也不会选错。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_fg_on($bg)
|
|
|
|
|
|
{
|
|
|
|
|
|
$white = yang_mail_contrast('#ffffff', $bg);
|
|
|
|
|
|
$dark = yang_mail_contrast('#1a1a1a', $bg);
|
|
|
|
|
|
if ($white >= $dark) {
|
|
|
|
|
|
return '#ffffff';
|
|
|
|
|
|
}
|
|
|
|
|
|
// 深字够不着 AA 时,纯黑还能再挤一点(#808080 上 4.41 → 5.32)
|
|
|
|
|
|
return yang_mail_contrast('#000000', $bg) > $dark ? '#000000' : '#1a1a1a';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 取一个颜色的「色度」(chroma)—— 三个通道的最大差
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么不用 HSL 饱和度判断"有没有颜色" ────────────────────
|
|
|
|
|
|
* HSL 的 S 在高亮度区会虚高。实测派生底 #f0f9ff(几乎纯白带一点点蓝)
|
|
|
|
|
|
* 的 HSL 饱和度是 **1.00**,而 #e5e5e6(纯灰)是 0.005 —— 看起来判据很灵。
|
|
|
|
|
|
* 但 #e9eaec 这种"肉眼就是灰"的颜色,HSL 饱和度也有 0.12,
|
|
|
|
|
|
* 会被判成"有颜色"。真正决定人眼能否看出颜色的,是通道极差(色度):
|
|
|
|
|
|
* #f0f9ff → 0.059(看得出是极浅蓝)
|
|
|
|
|
|
* #e9eaec → 0.012(看不出,就是灰)
|
|
|
|
|
|
* 两者 HSL 饱和度一个是 1.00、一个 0.12,顺序跟人的感受**正好相反**。
|
|
|
|
|
|
* 所以这里一律用色度,不用 S。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_chroma($hex)
|
|
|
|
|
|
{
|
|
|
|
|
|
$hex = ltrim((string) $hex, '#');
|
|
|
|
|
|
if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
|
|
|
|
|
|
return 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
$v = array(
|
|
|
|
|
|
hexdec(substr($hex, 0, 2)),
|
|
|
|
|
|
hexdec(substr($hex, 2, 2)),
|
|
|
|
|
|
hexdec(substr($hex, 4, 2)),
|
|
|
|
|
|
);
|
|
|
|
|
|
return (max($v) - min($v)) / 255;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把「有色相但太淡」的颜色沿**它自己的色相**加饱和,直到色度达到下限
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么需要这一步 ────────────────────────────────────────
|
|
|
|
|
|
* 主题默认主色是 #24292e,色度只有 0.039(几乎是中性深灰)。
|
|
|
|
|
|
* 往白里兑出浅底后色度只剩 0.002 —— 物理上不可见,整封邮件仍旧是黑白的,
|
|
|
|
|
|
* 用户看到的还是「模板没做好」。但「从灰里调出彩色」在数学上不可能,
|
|
|
|
|
|
* 而主题默认值就是它,所以必须有一个补饱和的环节。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 只动饱和度,**色相与亮度一律不动** —— 派生色仍属同一色系,品牌识别没丢。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_ensure_chroma($hex, $minChroma)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (yang_mail_chroma($hex) >= $minChroma) {
|
|
|
|
|
|
return $hex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
list($hue, $l) = yang_mail_hsl($hex);
|
|
|
|
|
|
$den = 1 - abs(2 * $l - 1);
|
|
|
|
|
|
if ($den < 1e-6) {
|
|
|
|
|
|
// 纯白或纯黑:没有色相可言
|
|
|
|
|
|
return $hex;
|
|
|
|
|
|
}
|
|
|
|
|
|
$s0 = yang_mail_chroma($hex) / $den;
|
|
|
|
|
|
|
|
|
|
|
|
/* 逐档搜索而不是解析求解:色度是 max-min,跟 S 的关系在 l>0.5 与
|
|
|
|
|
|
* l<0.5 两侧公式不同,硬推容易在边界写错;这里步数只有几十。 */
|
|
|
|
|
|
for ($s = $s0; $s <= 0.95; $s += 0.02) {
|
|
|
|
|
|
$out = yang_mail_hsl_hex($hue, $s, $l);
|
|
|
|
|
|
if (yang_mail_chroma($out) >= $minChroma) {
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return yang_mail_hsl_hex($hue, 0.95, $l);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 这个颜色有没有「真实色相」(可以放心拿它的色相去染色)
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么判据放在源头,而不是放在每个派生色上 ──────────────────
|
|
|
|
|
|
* 与白色混合是**同比压缩**色度:0.94 那一档压 16 倍。
|
|
|
|
|
|
* 所以"派生出的浅底色度很低"是**必然现象**,不是"这个颜色没有色相"。
|
|
|
|
|
|
* 早先的实现在 yang_mail_ensure_chroma() 里对入参做 0.02 死区判断,
|
|
|
|
|
|
* 于是极浅底(混合后 0.008)被当成"没有色相"直接跳过,
|
|
|
|
|
|
* 兜底等于没做 —— 恰恰是最需要兜底的那一档失效了。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 真正该判断的是**种子**:种子的色度 ≥ 0.02 说明它有一个真实可辨的色相
|
|
|
|
|
|
* (#24292e 是 0.039,属"有一点点真实的蓝",可以放大);
|
|
|
|
|
|
* 低于 0.02 的(#808080 这种纯中性灰)色相是浮点噪声,按噪声染色
|
|
|
|
|
|
* 等于随机配色,还会让「主色本来就没有色相」变得不可观测 —— 那就不染。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_has_hue($hex)
|
|
|
|
|
|
{
|
|
|
|
|
|
return yang_mail_chroma($hex) >= 0.02;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* #rrggbb → array(色相 0~1, 亮度 0~1)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_hsl($hex)
|
|
|
|
|
|
{
|
|
|
|
|
|
$hex = ltrim((string) $hex, '#');
|
|
|
|
|
|
if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
|
|
|
|
|
|
return array(0.0, 0.0);
|
2026-10-23 13:55:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
$r = hexdec(substr($hex, 0, 2)) / 255;
|
|
|
|
|
|
$g = hexdec(substr($hex, 2, 2)) / 255;
|
|
|
|
|
|
$b = hexdec(substr($hex, 4, 2)) / 255;
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$mx = max($r, $g, $b);
|
|
|
|
|
|
$mn = min($r, $g, $b);
|
|
|
|
|
|
$l = ($mx + $mn) / 2;
|
|
|
|
|
|
$d = $mx - $mn;
|
|
|
|
|
|
if ($d < 1e-9) {
|
|
|
|
|
|
return array(0.0, $l);
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($mx === $r) {
|
|
|
|
|
|
$h = (($g - $b) / $d + ($g < $b ? 6 : 0)) / 6;
|
|
|
|
|
|
} elseif ($mx === $g) {
|
|
|
|
|
|
$h = (($b - $r) / $d + 2) / 6;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$h = (($r - $g) / $d + 4) / 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
return array($h, $l);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* HSL → #rrggbb(h 为 0~1)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_hsl_hex($h, $s, $l)
|
|
|
|
|
|
{
|
|
|
|
|
|
$hue2rgb = function ($p, $q, $t) {
|
|
|
|
|
|
if ($t < 0) {
|
|
|
|
|
|
$t += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($t > 1) {
|
|
|
|
|
|
$t -= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($t < 1 / 6) {
|
|
|
|
|
|
return $p + ($q - $p) * 6 * $t;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($t < 1 / 2) {
|
|
|
|
|
|
return $q;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($t < 2 / 3) {
|
|
|
|
|
|
return $p + ($q - $p) * (2 / 3 - $t) * 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $p;
|
2026-10-23 13:55:23 +08:00
|
|
|
|
};
|
2026-10-23 16:13:48 +08:00
|
|
|
|
if ($s <= 0) {
|
|
|
|
|
|
$v = (int) round($l * 255);
|
|
|
|
|
|
return sprintf('#%02x%02x%02x', $v, $v, $v);
|
|
|
|
|
|
}
|
|
|
|
|
|
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
|
|
|
|
|
|
$p = 2 * $l - $q;
|
|
|
|
|
|
return sprintf(
|
|
|
|
|
|
'#%02x%02x%02x',
|
|
|
|
|
|
(int) round(255 * $hue2rgb($p, $q, $h + 1 / 3)),
|
|
|
|
|
|
(int) round(255 * $hue2rgb($p, $q, $h)),
|
|
|
|
|
|
(int) round(255 * $hue2rgb($p, $q, $h - 1 / 3))
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-23 17:08:55 +08:00
|
|
|
|
* 取颜色的 HSL 饱和度
|
|
|
|
|
|
*
|
|
|
|
|
|
* 色度(通道极差)= S × (1 - |2L-1|),这里就是它的反解。
|
|
|
|
|
|
* 单独拿出来是因为「按亮度重设颜色」时必须保住原来的饱和度 ——
|
|
|
|
|
|
* 否则一个鲜艳的蓝压暗后会变成中性灰,头部就成了「深灰条」而不是「深蓝」。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_hsl_s($hex)
|
|
|
|
|
|
{
|
|
|
|
|
|
list(, $l) = yang_mail_hsl($hex);
|
|
|
|
|
|
$den = 1 - abs(2 * $l - 1);
|
|
|
|
|
|
if ($den < 1e-6) {
|
|
|
|
|
|
return 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
return min(1.0, yang_mail_chroma($hex) / $den);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 沿自身色相与饱和度,把颜色放到指定的 HSL 亮度上
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用途:头部品牌条要「足够深、但不至于近黑」。只改亮度,
|
|
|
|
|
|
* 色相和饱和度是主色的原班底子,所以它仍认得出是哪个品牌色。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_at_lightness($hex, $l)
|
|
|
|
|
|
{
|
|
|
|
|
|
list($h, ) = yang_mail_hsl($hex);
|
|
|
|
|
|
return yang_mail_hsl_hex($h, yang_mail_hsl_s($hex), max(0.0, min(1.0, (float) $l)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把颜色压深,直到叠在上面的文字达到目标对比度
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么不是「取一个固定深色」────────────────────────────────
|
|
|
|
|
|
* 主色是用户填的,可能是亮蓝、亮绿、浅黄。同一个固定深色放在这些主色上
|
|
|
|
|
|
* 都不「属于」它们,品牌识别就没了。这里只沿它**自己的色相**往下压亮度,
|
|
|
|
|
|
* 压到「刚好够文字清楚」就停 —— 保住色相,也不会一路压成近黑。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_deepen_to($hex, $target = 4.5, $fg = '#ffffff')
|
|
|
|
|
|
{
|
|
|
|
|
|
list($h, $l) = yang_mail_hsl($hex);
|
|
|
|
|
|
$s = yang_mail_hsl_s($hex);
|
|
|
|
|
|
for ($t = $l; $t >= 0.04; $t -= 0.01) {
|
|
|
|
|
|
$c = yang_mail_hsl_hex($h, $s, $t);
|
|
|
|
|
|
if (yang_mail_contrast($fg, $c) >= $target) {
|
|
|
|
|
|
return $c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return yang_mail_hsl_hex($h, $s, 0.04);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 两个颜色的色相距离(0 = 同色相,0.5 = 正互补)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 判「撞色够不够」必须用它,不能比较具体色值 —— 色值是用户主色的函数,
|
|
|
|
|
|
* 写死任何一个都会在主色变化时假红。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_hue_dist($a, $b)
|
|
|
|
|
|
{
|
|
|
|
|
|
list($ha, ) = yang_mail_hsl($a);
|
|
|
|
|
|
list($hb, ) = yang_mail_hsl($b);
|
|
|
|
|
|
$d = abs($ha - $hb);
|
|
|
|
|
|
return $d > 0.5 ? 1 - $d : $d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 暖色判定(红 / 橙 / 黄)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用户对「新回复」标签的要求是「不能用蓝色了,用黄色或者红色」。
|
|
|
|
|
|
* 判据直接写成语义:色相落在 -30°~75° 之外就是偏绿/偏蓝了,
|
|
|
|
|
|
* 换算成圈是 [0.917, 1] ∪ [0, 0.208]。绿色 #27ae60(h≈0.42)、
|
|
|
|
|
|
* 蓝色 #00a2ff(h≈0.56)都不在区间内。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_is_warm($hex)
|
|
|
|
|
|
{
|
|
|
|
|
|
list($h, ) = yang_mail_hsl($hex);
|
|
|
|
|
|
return $h >= 0.917 || $h <= 0.208;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从暖色候选里挑「与头部拉开足够距离」的那个
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用户的要求有两句:「标签不能再用蓝色,用黄或红」「要和背景有区分度」。
|
|
|
|
|
|
* 第一句决定了只能是暖色族,第二句决定了**不能固定一个色号**:
|
|
|
|
|
|
* 品牌色本身偏红时(#e74c3c 压深成 #a82315),一个红按钮与它只差 6°,
|
|
|
|
|
|
* 贴在深红头部下方等于"看不见的撞色"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 判据 ────────────────────────────────────────────────────
|
|
|
|
|
|
* 按候选顺序取**第一个色相距离 ≥ 0.10(36°)**的。
|
|
|
|
|
|
* 头部本身就是暖色、暖色族里挑不出更远的那个时,取距离最大的 ——
|
|
|
|
|
|
* 此时靠**亮度差**拉开层次(深色头部 L≈0.10 上的金黄 L 很高)。
|
|
|
|
|
|
* 候选顺序把金黄放第一:与冷色头部撞得最开,压黑字对比度也最高(14.5:1),
|
|
|
|
|
|
* 最符合用户说的「更醒目」。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $headerBg 头部底色
|
|
|
|
|
|
* @param array $cands 暖色候选,顺序即优先级
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_pick_warm($headerBg, $cands)
|
|
|
|
|
|
{
|
|
|
|
|
|
$best = $cands[0];
|
|
|
|
|
|
$bestD = -1;
|
|
|
|
|
|
foreach ($cands as $c) {
|
|
|
|
|
|
$d = yang_mail_hue_dist($c, $headerBg);
|
|
|
|
|
|
if ($d >= 0.10) {
|
|
|
|
|
|
return $c;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($d > $bestD) {
|
|
|
|
|
|
$bestD = $d;
|
|
|
|
|
|
$best = $c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return $best;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成整封邮件用的配色表
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成整封邮件用的配色表
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 版式骨架(用户指定的审美,别再自己发挥)──────────────────
|
|
|
|
|
|
* · 外层背景**白色**。不是整片浅色底 —— 那样一眼看上去是「一张彩色纸」,
|
|
|
|
|
|
* 而不是「纸上一张卡片」。
|
|
|
|
|
|
* · 顶部品牌条**深色底 + 白字**。深色从主色压下来,色相仍是主色。
|
|
|
|
|
|
* · 「新回复」标签**暖色(黄/红)**,与冷色头部撞开。
|
|
|
|
|
|
* 早先它的底色是主色提亮一档 —— 与头部同色系,远看糊成一片,
|
|
|
|
|
|
* 用户的原话是「和背景蓝色没区分度」。
|
|
|
|
|
|
* · CTA 按钮**红底白字**,同样与头部撞开。
|
|
|
|
|
|
* 结论:这封邮件的色彩逻辑是「**白底 + 深头 + 暖色点缀**」,
|
|
|
|
|
|
* 而不是上一版那样「整封同色系」。
|
2026-10-23 16:13:48 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么整封邮件不能是灰的 ──────────────────────────────────
|
|
|
|
|
|
* 早先模板里只有品牌条、徽标、按钮、最后一块引用块的竖线用主色,
|
|
|
|
|
|
* 其余 90% 面积(标题、正文、引用块底色、信息表分隔线、页脚)全是中性灰
|
|
|
|
|
|
* (#1a2028 / #5f6b78 / #c2cbd4 / #edf0f3)。主色若本身不够艳
|
|
|
|
|
|
* (比如主题默认的 #24292e 是接近中性的深灰蓝),整封信看起来就是黑白的。
|
|
|
|
|
|
* 「彩色」不是靠一个大色块,而是让**底色、描边、强调文字**都在同一色系里。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么要算而不是写死 ──────────────────────────────────────
|
|
|
|
|
|
* 主色是用户在主题选项里改的,写死一套蓝就没有意义了。
|
|
|
|
|
|
* 这里从主色派生出:极浅底(区块背景)、浅底(页脚背景)、
|
|
|
|
|
|
* 中档描边、深档文字(保证对浅底 ≥4.5:1)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 派生为什么不能只是「与白混合」────────────────────────────
|
|
|
|
|
|
* 混合是同比压缩色度:0.94 那一档把色度压 16 倍。主色是 #24292e 这种
|
|
|
|
|
|
* 近中性深灰(色度 0.039)时,兑完只剩 0.002,肉眼与纯灰无异 ——
|
|
|
|
|
|
* 这正是「换成默认主色后邮件又变黑白」的机制。
|
|
|
|
|
|
* 所以浅底在混合之后还要过一道 yang_mail_ensure_chroma(),按**它自己所在
|
|
|
|
|
|
* 亮度**补一个可见的色度下限(浅底用 MIN_CHROMA_TINT,中档用 MIN_CHROMA_MID)。
|
|
|
|
|
|
* 这一步只动饱和度、不动色相和亮度,颜色仍是主色的同一色系。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 品牌条为什么仍用原色 ────────────────────────────────────
|
|
|
|
|
|
* 用户直接看到主色的地方(品牌条、按钮、徽标)一律用他填的原色。
|
|
|
|
|
|
* 他选了深灰蓝,就不该在标题栏上看到它被换成亮蓝。补饱和只用于
|
|
|
|
|
|
* 「主色的稀释/加深」,那是他自己没指定过的颜色。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 全部输出**实色**,不用 rgba:Outlook(Word 引擎)不认 rgba,会整条丢掉背景。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_palette($brand = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($brand === null) {
|
|
|
|
|
|
$brand = yang_mail_brand_color();
|
|
|
|
|
|
}
|
|
|
|
|
|
$brand = ltrim((string) $brand, '#');
|
|
|
|
|
|
if (!preg_match('/^[0-9a-fA-F]{6}$/', $brand)) {
|
|
|
|
|
|
$brand = '24292e';
|
|
|
|
|
|
}
|
|
|
|
|
|
$brand = '#' . $brand;
|
|
|
|
|
|
|
|
|
|
|
|
/* 派生用的种子:中性一点的先提饱和,否则这一整套派生色都将是灰的。
|
|
|
|
|
|
* 只在主色有**真实色相**时才放大(见 yang_mail_has_hue);
|
|
|
|
|
|
* 零色相的纯灰宁可保持灰,也不要凭空编一个色相。 */
|
|
|
|
|
|
$seed = yang_mail_has_hue($brand) ? yang_mail_ensure_chroma($brand, 0.30) : $brand;
|
|
|
|
|
|
|
2026-10-23 17:08:55 +08:00
|
|
|
|
/* ── 头部:深色底 + 白字 ────────────────────────────────────
|
|
|
|
|
|
* 直接拿主色当头部底色在两种情况会翻:主色本身很亮(亮蓝/亮黄)时
|
|
|
|
|
|
* 白字看不清;主色本身很深时与正文的深色文字层次不分。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么压到 7:1 而不是只压到 AA 4.5 ────────────────────
|
|
|
|
|
|
* 只压到刚好 4.5 会得到一个「界限色」:亮蓝 #00a2ff 压出来是
|
|
|
|
|
|
* #007bc2,白字 4.55:1 —— 合规,但它是个中蓝,肉眼既不"深"也不"亮",
|
|
|
|
|
|
* 用户说的「深蓝色的背景」根本看不出来。压到 7:1 才真的落进深色区
|
|
|
|
|
|
* (#007bc2 的 L=0.181 恰好卡在 0.179 的深浅分界线上,这也是它
|
|
|
|
|
|
* 看着不深的原因)。白字 7:1 同时是 AAA 级正文标准,留有余量。
|
|
|
|
|
|
* 色相与饱和度仍是主色的原班底子,所以它认得出是哪个品牌色的深版。 */
|
|
|
|
|
|
$headerBg = yang_mail_deepen_to($seed, 7.0, '#ffffff');
|
|
|
|
|
|
$headerFg = yang_mail_fg_on($headerBg);
|
|
|
|
|
|
$headerDark = yang_mail_shade($headerBg, 0.80); // 下沿 3px 深色带
|
2026-10-23 16:13:48 +08:00
|
|
|
|
|
2026-10-23 17:08:55 +08:00
|
|
|
|
/* ── 强调色:暖色,与头部撞开 ───────────────────────────────
|
|
|
|
|
|
* 用户要求「标签不能用蓝色,要黄或红」。但从主色反推一个暖色是编造:
|
|
|
|
|
|
* 主色是 #27ae60(绿)时,把色相掰到红就等于替用户改了配色。
|
|
|
|
|
|
* 所以强调色用**语义固定**的暖色族,具体哪一个由「与头部的色相距离」
|
|
|
|
|
|
* 决定(见 yang_mail_pick_warm)—— 主色偏红时第一个候选贴上了,
|
|
|
|
|
|
* 就往暖色族里更远的那个退。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 文字色走 fg_on():金黄 #ffd21f(L 高)压黑字 14.5:1,压白字只有 1.5:1。
|
|
|
|
|
|
* 这类"中亮度暖色"正是二元阈值最容易判错的地方。 */
|
|
|
|
|
|
$accent = yang_mail_pick_warm($headerBg, array('#ffd21f', '#f2a00b', '#e53935'));
|
|
|
|
|
|
$accentFg = yang_mail_fg_on($accent);
|
|
|
|
|
|
$accentBd = yang_mail_shade($accent, 0.86);
|
|
|
|
|
|
|
|
|
|
|
|
/* 按钮:用户明确要「红底白字」,就用红。
|
|
|
|
|
|
* 为什么不也按"与头部撞色"去挑:按钮在卡片最下方,中间隔着整段正文,
|
|
|
|
|
|
* 视觉上不与头部相邻 —— 用户说的「区分度」针对的是**贴在头部里面**的
|
|
|
|
|
|
* 标签。为它再引入一套撞色规则,只会在红主色站点上把按钮挑成橙色,
|
|
|
|
|
|
* 与「红白色的文字」这个明确指令冲突。
|
|
|
|
|
|
* 白字走 fg_on():#c62828 上 5.62:1,达 AA。 */
|
|
|
|
|
|
$ctaBg = '#c62828';
|
|
|
|
|
|
$ctaFg = yang_mail_fg_on($ctaBg);
|
|
|
|
|
|
|
|
|
|
|
|
/* ── 浅底色 ────────────────────────────────────────────────
|
|
|
|
|
|
* 外层背景必须是白(用户明确要求),卡片本身也是白;
|
|
|
|
|
|
* 浅底只留给「引用块」和「页脚」——没有它们,白底上全是散落的字,
|
|
|
|
|
|
* 分不出哪段是别人说的话。 */
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$keep = yang_mail_has_hue($brand);
|
|
|
|
|
|
$tintXX = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.94), 0.045)
|
|
|
|
|
|
: yang_mail_tint($seed, 0.94);
|
|
|
|
|
|
$tintX = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.88), 0.075)
|
|
|
|
|
|
: yang_mail_tint($seed, 0.88);
|
|
|
|
|
|
$tintLine = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.80), 0.110)
|
|
|
|
|
|
: yang_mail_tint($seed, 0.80);
|
|
|
|
|
|
$tintMid = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.55), 0.220)
|
|
|
|
|
|
: yang_mail_tint($seed, 0.55);
|
|
|
|
|
|
|
2026-10-23 17:08:55 +08:00
|
|
|
|
/* 强调文字色:要「主色的深版」,不能直接拿主色当文字色 ——
|
|
|
|
|
|
* 亮蓝 #00a2ff 在白底只有 2.76:1。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 循环方向 ──────────────────────────────────────────────
|
|
|
|
|
|
* shade() 是**按比例乘通道值**,所以 ratio 越小越暗。
|
|
|
|
|
|
* 从 0.90(最亮)往下扫到 0.20(最暗),取**第一个**达标的,
|
|
|
|
|
|
* 得到的是「最亮的达标档」:仍看得出属于主色,又刚够清楚。
|
|
|
|
|
|
* 反过来从 0.20 往上扫会取到最暗档 —— 亮蓝压成 #002033,标题看着是黑的。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么还要卡色度 ──────────────────────────────────────
|
|
|
|
|
|
* 只卡对比度时,主色很浅(如 #ffd21f 亮黄)会一路压到几乎发黑才达标,
|
|
|
|
|
|
* 那就不是"主色的深版"而是"黑色"了。要求色度不低于种子的 70%,
|
|
|
|
|
|
* 保证它还能被认出是同色系(浅主色达不到时退到最接近的那档)。 */
|
|
|
|
|
|
$seedC = yang_mail_chroma($seed);
|
|
|
|
|
|
$textOnTint = yang_mail_shade($seed, 0.90);
|
|
|
|
|
|
for ($s = 0.90; $s >= 0.20; $s -= 0.02) {
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$c = yang_mail_shade($seed, $s);
|
|
|
|
|
|
if (yang_mail_contrast($c, '#ffffff') >= 4.5
|
2026-10-23 17:08:55 +08:00
|
|
|
|
&& yang_mail_contrast($c, $tintXX) >= 4.5
|
|
|
|
|
|
&& yang_mail_chroma($c) >= $seedC * 0.70) {
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$textOnTint = $c;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-23 17:08:55 +08:00
|
|
|
|
/* 主色本身没有色相(纯灰)时,上面那条色度条件退化成恒真,
|
|
|
|
|
|
* 于是会选到 #717171 这种"最亮的中灰"当 21px 标题 —— 合规但看着发虚。
|
|
|
|
|
|
* 中性主色本来就没有"深版"可言,直接给一个真正的深墨色,
|
|
|
|
|
|
* 层次反而清楚(与正文 #243040 同为深色,靠字号/字重区分)。 */
|
|
|
|
|
|
if (!$keep) {
|
|
|
|
|
|
$textOnTint = '#1a1a1a';
|
|
|
|
|
|
}
|
2026-10-23 16:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
/* 次要文字色:原来是固定的 #5c6b7a,但页脚底色改成了主色浅底之后,
|
|
|
|
|
|
* 这个固定值在浅蓝上只有 4.26:1 —— 12px 小字不达 AA。
|
|
|
|
|
|
* 「文字色写死、背景色随主色变」这个组合本身就是错的:背景一变浅,
|
|
|
|
|
|
* 固定灰就压不住。所以改成从基准灰往下压,压到在**它实际会遇到的
|
2026-10-23 17:08:55 +08:00
|
|
|
|
* 三种背景**(页脚浅底、引用块浅底、白卡片)上都达标为止。 */
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$muted = '#5c6b7a';
|
|
|
|
|
|
for ($t = 1.0; $t >= 0.55; $t -= 0.02) {
|
|
|
|
|
|
$cand = $t >= 0.999 ? $muted : yang_mail_shade($muted, $t);
|
|
|
|
|
|
if (yang_mail_contrast($cand, $tintX) >= 4.5
|
2026-10-23 17:08:55 +08:00
|
|
|
|
&& yang_mail_contrast($cand, $tintXX) >= 4.5
|
|
|
|
|
|
&& yang_mail_contrast($cand, '#ffffff') >= 4.5) {
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$muted = $cand;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return array(
|
2026-10-23 17:08:55 +08:00
|
|
|
|
'brand' => $brand, // 主色原件(引用块高亮竖线等)
|
|
|
|
|
|
'brand_dark' => yang_mail_shade($brand, 0.78), // 主色深版(备用)
|
|
|
|
|
|
'on_brand' => $headerFg, // 压在头部上的字
|
|
|
|
|
|
'header_bg' => $headerBg, // 顶部品牌条底色(深色)
|
|
|
|
|
|
'header_fg' => $headerFg, // 头部白字
|
|
|
|
|
|
'header_dark'=> $headerDark, // 头部下沿深色带
|
|
|
|
|
|
'accent' => $accent, // 强调色(badge 底,暖色)
|
|
|
|
|
|
'accent_fg' => $accentFg, // badge 上的字
|
|
|
|
|
|
'accent_bd' => $accentBd, // badge 描边
|
|
|
|
|
|
'cta_bg' => $ctaBg, // 按钮底(红)
|
|
|
|
|
|
'cta_fg' => $ctaFg, // 按钮字(白)
|
2026-10-23 16:13:48 +08:00
|
|
|
|
'text' => $textOnTint, // 同色系的强调文字(标题/标签)
|
|
|
|
|
|
'tint_xx' => $tintXX, // 极浅底:引用块、信息表
|
|
|
|
|
|
'tint_x' => $tintX, // 稍深一点的浅底:页脚
|
|
|
|
|
|
'tint_line' => $tintLine, // 浅底上的描边/分隔线
|
|
|
|
|
|
'tint_mid' => $tintMid, // 中档:弱化竖线、次要描边
|
2026-10-23 17:08:55 +08:00
|
|
|
|
'page_bg' => '#ffffff', // 邮件最外层背景(纯白,用户指定)
|
2026-10-23 16:13:48 +08:00
|
|
|
|
'body' => '#243040', // 正文(保持深灰蓝,长文好读)
|
|
|
|
|
|
'muted' => $muted, // 次要文字(时间、页脚),按背景调过
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算两色的 WCAG 对比度
|
|
|
|
|
|
*
|
|
|
|
|
|
* 给配色表自检用:派生颜色是否达标,靠算而不是靠眼睛看。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_contrast($fg, $bg)
|
|
|
|
|
|
{
|
|
|
|
|
|
$lumOf = function ($hex) {
|
|
|
|
|
|
$hex = ltrim((string) $hex, '#');
|
|
|
|
|
|
if (strlen($hex) !== 6) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
$lin = function ($c) {
|
|
|
|
|
|
$c /= 255;
|
|
|
|
|
|
return $c <= 0.03928 ? $c / 12.92 : pow(($c + 0.055) / 1.055, 2.4);
|
|
|
|
|
|
};
|
|
|
|
|
|
return 0.2126 * $lin(hexdec(substr($hex, 0, 2)))
|
|
|
|
|
|
+ 0.7152 * $lin(hexdec(substr($hex, 2, 2)))
|
|
|
|
|
|
+ 0.0722 * $lin(hexdec(substr($hex, 4, 2)));
|
|
|
|
|
|
};
|
|
|
|
|
|
$a = $lumOf($fg);
|
|
|
|
|
|
$b = $lumOf($bg);
|
|
|
|
|
|
return (max($a, $b) + 0.05) / (min($a, $b) + 0.05);
|
2026-10-23 13:55:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-23 14:43:47 +08:00
|
|
|
|
* 把可能含 HTML 的片段转成邮件安全的纯文本(保留换行)
|
2026-10-23 13:55:23 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 邮件正文里塞用户提交的评论内容,必须防注入:邮件客户端也会执行
|
|
|
|
|
|
* `<script>`(部分 webmail 会),而且一旦渲染出畸形 HTML,整封邮件布局就崩。
|
2026-10-23 14:43:47 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 换行为什么要专门处理 ─────────────────────────────────────
|
|
|
|
|
|
* 两个坑都会把整条评论拍成一行:
|
|
|
|
|
|
* 1. `wp_strip_all_tags($x, true)` 的第二个参数是 remove_breaks,
|
|
|
|
|
|
* 真实实现里会 `preg_replace('/[\r\n\t ]+/', ' ', $x)` —— 换行直接没了。
|
|
|
|
|
|
* 所以这里必须传 false。
|
|
|
|
|
|
* 2. `strip_tags()` 遇到 `<br>` 是**直接删掉、不补换行**的,`</p>` 同理。
|
|
|
|
|
|
* 评论里手写的换行和段落因此在去标签阶段就无痕消失。
|
|
|
|
|
|
* 结论:先把块级边界换成 `\n`,再去标签,最后才收拾空白。
|
|
|
|
|
|
* 这样模板里的 `nl2br()` 才有换行可转,不是一段死代码。
|
2026-10-23 13:55:23 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_text($html, $limit = 0)
|
|
|
|
|
|
{
|
2026-10-23 14:43:47 +08:00
|
|
|
|
$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. 收拾空白:行内空白收成一个空格,换行保留,最多一个空行
|
2026-10-23 13:55:23 +08:00
|
|
|
|
$t = html_entity_decode($t, ENT_QUOTES, 'UTF-8');
|
2026-10-23 14:43:47 +08:00
|
|
|
|
$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);
|
2026-10-23 13:55:23 +08:00
|
|
|
|
$t = trim($t);
|
|
|
|
|
|
if ($limit > 0 && mb_strlen($t, 'UTF-8') > $limit) {
|
|
|
|
|
|
$t = mb_substr($t, 0, $limit, 'UTF-8') . '…';
|
|
|
|
|
|
}
|
|
|
|
|
|
return $t;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 17:29:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 页脚版权行的文字
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么不用字符实体 ───────────────────────────────────────
|
|
|
|
|
|
* 邮件的 HTML 会被 yang_mail_escape_for_wpdiscuz() 统一把 `&` 换成 `&`
|
|
|
|
|
|
* (那是为了防 wpDiscuz 在过滤器之后又 decode 一次时把评论里的
|
|
|
|
|
|
* `<script>` 还原成活标签)。这个替换是**无差别**的,连我们自己写在
|
|
|
|
|
|
* 骨架里的字符实体也会被一起加倍。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 正常路径下 wpDiscuz 会再 decode 一次,加倍过的实体恰好还原,
|
|
|
|
|
|
* 浏览器解析成 `©` —— 看起来没问题。但**只要有任何一条路径没有走那次
|
|
|
|
|
|
* decode**(插件版本差异、别的过滤器、后台预览、或是我们自己直接调
|
|
|
|
|
|
* wp_mail),加倍过的实体就只会被解析一次,收件人看到就是那串实体原文。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 实体是"靠对家配合才好使"的东西,裸 UTF-8 字符不是。整封邮件都声明了
|
|
|
|
|
|
* `Content-Type: text/html; charset=UTF-8`,直接写 `©` 在多解码几次的
|
|
|
|
|
|
* 情况下也不会变形,所以这里一律输出字符本身,不输出实体。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 站点名走 esc_html():站名可能带 `&`(如「A & B」),这个 `&` 是**用户数据**
|
|
|
|
|
|
* 的转义,被加倍后能由那次 decode 还原,是应当保留的那一类。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_footer_copy($year, $site)
|
|
|
|
|
|
{
|
|
|
|
|
|
return '© ' . esc_html($year) . ' ' . esc_html($site);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 不换行空格
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用途只有一个:给 3px 高的分隔条撑出行高(Outlook 里 td 不能是空的,
|
|
|
|
|
|
* 否则高度会被忽略)。同理不用那个常见的空格字符实体 —— 换成 U+00A0
|
|
|
|
|
|
* 字符本身,免得被 `&` 加倍后在某些路径下变成字面量文本,反而撑高那一行。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_mail_nbsp()
|
|
|
|
|
|
{
|
|
|
|
|
|
return "\xC2\xA0";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 13:55:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 把主色与白色按比例混合,得到一档浅色
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么不用 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>` 上。
|
2026-10-23 17:08:55 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ── 文字色为什么不写死 #ffffff ────────────────────────────────
|
|
|
|
|
|
* 按钮底色是可传入的(主题里默认给红 #c62828)。写死白字在浅底按钮上
|
|
|
|
|
|
* 会掉到 2:1 以下,而且「深还是浅」这个问题本身有结构性盲区(中灰两边
|
|
|
|
|
|
* 都不达标)。统一走 yang_mail_fg_on():在白与深之间取对比度更高的,
|
|
|
|
|
|
* 判据与任何阈值都解耦。
|
2026-10-23 13:55:23 +08:00
|
|
|
|
*/
|
2026-10-23 17:08:55 +08:00
|
|
|
|
function yang_mail_button($text, $url, $bg = null, $fg = null)
|
2026-10-23 13:55:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
if ($bg === null) {
|
2026-10-23 17:08:55 +08:00
|
|
|
|
$pal = yang_mail_palette();
|
|
|
|
|
|
$bg = $pal['cta_bg'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($fg === null) {
|
|
|
|
|
|
$fg = yang_mail_fg_on($bg);
|
2026-10-23 13:55:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
$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(),
|
2026-10-23 14:43:47 +08:00
|
|
|
|
'quote' => array(), // 单个引用块(旧接口,保留兼容)
|
|
|
|
|
|
'quotes' => array(), // 多个引用块,按顺序渲染
|
2026-10-23 13:55:23 +08:00
|
|
|
|
'cta_text' => '',
|
|
|
|
|
|
'cta_url' => '',
|
|
|
|
|
|
'note' => '',
|
|
|
|
|
|
'badge' => '',
|
|
|
|
|
|
));
|
|
|
|
|
|
|
2026-10-23 14:43:47 +08:00
|
|
|
|
/* 统一成 $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']);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 16:13:48 +08:00
|
|
|
|
/* 整套配色从主色派生一次,后面所有颜色都从这里取。
|
2026-10-23 17:08:55 +08:00
|
|
|
|
* 不在这里散落写死 #xxxxxx —— 那样主色一改,配色就断了。 */
|
|
|
|
|
|
$pal = yang_mail_palette();
|
|
|
|
|
|
$brand = $pal['brand'];
|
|
|
|
|
|
$cHeader = $pal['header_bg'];
|
|
|
|
|
|
$cHeaderFg = $pal['header_fg'];
|
|
|
|
|
|
$cHeaderD = $pal['header_dark'];
|
|
|
|
|
|
$cAccent = $pal['accent'];
|
|
|
|
|
|
$cAccentFg = $pal['accent_fg'];
|
|
|
|
|
|
$cAccentBd = $pal['accent_bd'];
|
|
|
|
|
|
$cCtaBg = $pal['cta_bg'];
|
|
|
|
|
|
$cText = $pal['text'];
|
|
|
|
|
|
$cBody = $pal['body'];
|
|
|
|
|
|
$cMuted = $pal['muted'];
|
|
|
|
|
|
$cTintXX = $pal['tint_xx'];
|
|
|
|
|
|
$cTintX = $pal['tint_x'];
|
|
|
|
|
|
$cTintLine = $pal['tint_line'];
|
|
|
|
|
|
$cTintMid = $pal['tint_mid'];
|
|
|
|
|
|
$cPageBg = $pal['page_bg'];
|
2026-10-23 13:55:23 +08:00
|
|
|
|
|
2026-10-23 17:08:55 +08:00
|
|
|
|
/* badge 不再复用头部配色。
|
|
|
|
|
|
* 早先 $badgeBg 是主色提亮一档、文字色复用 $onBrand ⇒ 与头部同色系,
|
|
|
|
|
|
* 远看就是头部里的一块「差不多颜色」,用户原话「和背景蓝色没区分度」。
|
|
|
|
|
|
* 现在用暖色强调色,与深色头部是撞色关系,红/黄一眼能挑出来。 */
|
|
|
|
|
|
$badgeBg = $cAccent;
|
|
|
|
|
|
$badgeBd = $cAccentBd;
|
|
|
|
|
|
|
|
|
|
|
|
/* 引用块高亮竖线:用与按钮同一个红(比 badge 的琥珀更沉),
|
|
|
|
|
|
避免 12px 琥珀色小字旁边出现一块同样亮的竖线而互相抢眼。 */
|
|
|
|
|
|
$ctaBarColor = $cCtaBg;
|
2026-10-23 13:55:23 +08:00
|
|
|
|
|
|
|
|
|
|
$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>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<body style="margin:0;padding:0;width:100%;background-color:<?php echo esc_attr($cPageBg); ?>;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
|
|
|
|
|
|
<?php /* 预读文字:显示在收件箱列表里标题的后面。藏起来但必须真实存在,且要足够长,
|
|
|
|
|
|
否则客户端会把正文第一句抓上去当摘要。 */ ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="display:none;font-size:1px;color:<?php echo esc_attr($cPageBg); ?>;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php echo esc_html($d['preheader'] ? $d['preheader'] : $d['heading']); ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="background-color:<?php echo esc_attr($cPageBg); ?>;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<tr><td align="center" style="padding:32px 12px;">
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* 宽度写法是「混合式」:
|
|
|
|
|
|
· Outlook(Word 引擎)只认 HTML 属性 width="600",CSS 的 max-width 它当没看见;
|
|
|
|
|
|
· 其它客户端认 CSS,于是 width:100% + max-width:600px 让手机窄屏自动收窄。
|
|
|
|
|
|
两套并存,各取所需。只写死 600px 会让手机横向滚动——实测过。 */ ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<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 <?php echo esc_attr($cTintLine); ?>;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<?php /* ── 顶部品牌条 ──
|
|
|
|
|
|
* 深色底 + 白字。深色是从主色压下来的,色相仍是主色;
|
|
|
|
|
|
* 白字保证在深底上过 AA(详见 yang_mail_deepen_to 的推导)。 */ ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<tr>
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<td bgcolor="<?php echo esc_attr($cHeader); ?>" style="background-color:<?php echo esc_attr($cHeader); ?>;padding:0;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td style="padding:26px 32px;">
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:19px;font-weight:600;color:<?php echo esc_attr($cHeaderFg); ?>;letter-spacing:0.2px;line-height:1.3;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php echo esc_html($site); ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php if ($d['badge'] !== ''): ?>
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<?php /* badge 用暖色强调色,衬在深色头部上是撞色 —— 一眼能挑出来。
|
|
|
|
|
|
仍用预算好的实色,不用 rgba:Outlook 不认 rgba,会直接丢掉背景。 */ ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin-top:10px;">
|
|
|
|
|
|
<tr>
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<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:600;line-height:1.4;">
|
|
|
|
|
|
<span style="color:<?php echo esc_attr($cAccentFg); ?>;"><?php echo esc_html($d['badge']); ?></span>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<?php /* 头部下沿压一条深色,给扁平色块一点层次(渐变在 Outlook 里不生效,用纯色)。
|
2026-10-23 13:55:23 +08:00
|
|
|
|
height 必须**同时**写成 HTML 属性和 CSS:Outlook 只认属性,
|
|
|
|
|
|
只写 CSS 高度时这条 3px 带会撑成一行文字高(约 18px),比例就毁了。 */ ?>
|
|
|
|
|
|
<tr>
|
2026-10-23 17:29:37 +08:00
|
|
|
|
<td bgcolor="<?php echo esc_attr($cHeaderD); ?>" height="3" style="background-color:<?php echo esc_attr($cHeaderD); ?>;height:3px;line-height:3px;font-size:0;mso-line-height-rule:exactly;"><?php echo yang_mail_nbsp(); ?></td>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* ── 正文 ── */ ?>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td style="padding:34px 32px 8px 32px;">
|
|
|
|
|
|
|
|
|
|
|
|
<?php if ($d['heading'] !== ''): ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:21px;font-weight:600;color:<?php echo esc_attr($cText); ?>;line-height:1.45;margin:0 0 12px 0;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php echo esc_html($d['heading']); ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<?php if ($d['intro'] !== ''): ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:15px;color:<?php echo esc_attr($cBody); ?>;line-height:1.7;margin:0 0 22px 0;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php echo esc_html($d['intro']); ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<?php foreach ($quoteList as $qi => $q): ?>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
/* 引用块:左边一条竖线,用 border-left 而不是伪元素(邮件不支持 ::before)。
|
|
|
|
|
|
*
|
2026-10-23 17:08:55 +08:00
|
|
|
|
* 配色规则:**最后一块用强调暖色高亮,前面的用主色系弱化档**。
|
2026-10-23 14:43:47 +08:00
|
|
|
|
* 为什么是「最后」而不是「第一」:评论回复场景里顺序是
|
|
|
|
|
|
* 「被回复的原评论」→「新的回复」,语义重点在新的回复上,所以让它高亮。
|
2026-10-23 17:08:55 +08:00
|
|
|
|
* 只有一个块时它既是第一也是最后 ⇒ 拿到强调色,与旧版行为一致。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 高亮色从主色换成强调暖色,是为了与深色头部、红按钮形成同一套
|
|
|
|
|
|
* 「暖色点缀」语言;同时竖线与 badge 同色,收件人一眼把
|
|
|
|
|
|
* 「新回复」标签和「新回复那段字」连起来。 */
|
2026-10-23 14:43:47 +08:00
|
|
|
|
$isLast = ($qi === count($quoteList) - 1);
|
2026-10-23 17:08:55 +08:00
|
|
|
|
$barColor = $isLast ? $ctaBarColor : $cTintMid;
|
2026-10-23 16:13:48 +08:00
|
|
|
|
$barBg = $isLast ? $cTintXX : $cTintX;
|
2026-10-23 14:43:47 +08:00
|
|
|
|
$barMargin = $isLast ? '22px' : '14px';
|
|
|
|
|
|
?>
|
|
|
|
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="margin:0 0 <?php echo $barMargin; ?> 0;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<tr>
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<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'])): ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:12px;font-weight:600;color:<?php echo esc_attr($cText); ?>;letter-spacing:0.6px;text-transform:uppercase;margin:0 0 8px 0;">
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<?php echo esc_html($q['label']); ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:15px;color:<?php echo esc_attr($cBody); ?>;line-height:1.7;word-break:break-word;">
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<?php echo nl2br(esc_html(isset($q['content']) ? $q['content'] : '')); ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</div>
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<?php if (!empty($q['meta'])): ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:13px;color:<?php echo esc_attr($cMuted); ?>;line-height:1.6;margin:9px 0 0 0;">
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<?php echo esc_html($q['meta']); ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
2026-10-23 14:43:47 +08:00
|
|
|
|
<?php endforeach; ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
|
|
|
|
|
|
<?php if (!empty($d['rows'])): ?>
|
|
|
|
|
|
<?php /* 信息表:两列。label 列定宽 88px 并右对齐,value 左对齐,视觉上最稳。
|
|
|
|
|
|
不用 table-layout:fixed 之外的花样,Outlook 只认这个。 */ ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation" style="margin:0 0 24px 0;border-top:1px solid <?php echo esc_attr($cTintLine); ?>;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php foreach ($d['rows'] as $row): ?>
|
|
|
|
|
|
<tr>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<td width="88" valign="top" style="width:88px;padding:11px 0;border-bottom:1px solid <?php echo esc_attr($cTintLine); ?>;font-family:<?php echo $sans; ?>;font-size:13px;color:<?php echo esc_attr($cMuted); ?>;line-height:1.6;white-space:nowrap;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php echo esc_html($row['label']); ?>
|
|
|
|
|
|
</td>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<td valign="top" style="padding:11px 0 11px 16px;border-bottom:1px solid <?php echo esc_attr($cTintLine); ?>;font-family:<?php echo $sans; ?>;font-size:14px;color:<?php echo esc_attr($cBody); ?>;line-height:1.6;word-break:break-word;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?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;">
|
2026-10-23 17:08:55 +08:00
|
|
|
|
<?php /* 按钮用红底白字:与深色头部是撞色,位置又在正文末尾,
|
|
|
|
|
|
是全信第二个视觉落点。用户明确要求「红白色的文字」。 */ ?>
|
|
|
|
|
|
<?php echo yang_mail_button($d['cta_text'], $d['cta_url'], $cCtaBg); ?>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
<?php if ($d['note'] !== ''): ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:13px;color:<?php echo esc_attr($cMuted); ?>;line-height:1.7;padding:16px 0 0 0;border-top:1px solid <?php echo esc_attr($cTintLine); ?>;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<?php echo esc_html($d['note']); ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php endif; ?>
|
|
|
|
|
|
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* ── 页脚 ── */ ?>
|
|
|
|
|
|
<tr>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<td bgcolor="<?php echo esc_attr($cTintX); ?>" style="background-color:<?php echo esc_attr($cTintX); ?>;padding:24px 32px 30px 32px;">
|
|
|
|
|
|
<div style="border-top:1px solid <?php echo esc_attr($cTintLine); ?>;padding-top:20px;font-family:<?php echo $sans; ?>;font-size:12px;color:<?php echo esc_attr($cMuted); ?>;line-height:1.9;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
<div style="margin:0 0 6px 0;">
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<a href="<?php echo esc_url($home); ?>" target="_blank" rel="noopener" style="color:<?php echo esc_attr($cText); ?>;text-decoration:none;font-weight:600;"><?php echo esc_html($site); ?></a>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin:0;">此邮件由系统自动发出,请勿直接回复。</div>
|
2026-10-23 17:29:37 +08:00
|
|
|
|
<div style="margin:6px 0 0 0;color:<?php echo esc_attr($cMuted); ?>;"><?php echo yang_mail_footer_copy($year, $site); ?></div>
|
2026-10-23 13:55:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* 底部说明:为什么收到这封邮件。放在卡片外面,视觉上属于系统信息 */ ?>
|
2026-10-23 16:13:48 +08:00
|
|
|
|
<div style="font-family:<?php echo $sans; ?>;font-size:12px;color:<?php echo esc_attr($cMuted); ?>;line-height:1.7;padding:18px 8px 0 8px;max-width:600px;">
|
2026-10-23 13:55:23 +08:00
|
|
|
|
您收到这封邮件,是因为本站记录了与您邮箱相关的订阅或互动。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
<?php
|
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
|
}
|