写作增强:翻译服务商选了谁才显示谁对应的账号输入框 #26

之前四个密钥框(百度 APPID/密钥、有道应用 ID/密钥)无论选哪家服务商
都全摆在设置页上,四组输入框里永远有三组用不上。

改动:
- inc/theme-options.php:新增 $yang_prov_class() 闭包,统一生成字段
  的 class。服务端就按「当前已保存的服务商」把 yq-cond-hide 打好,
  首帧不闪。分组标记 yang-prov / yang-prov-<key> 写在每个字段的
  'class' 上,是**唯一的分组声明处**,JS 不再抄服务商清单。
  原本一个 shared 的 info 块拆成三块,每家一句自己的说明。
- inc/options-framework/js/options-custom.js:重写
  yq_toggle_writing_slug_fields()。两层条件(总开关 + 服务商归属)
  必须在同一函数内合成一个结果再 toggleClass —— toggleClass 的
  force 是绝对赋值,拆成两个函数各 toggle 一次的话结果只取决于
  注册顺序。change 事件同时监听 #w_on_slug 与 #w_provider。
- style.css:3.2.57 -> 3.2.58

两处刻意避开的老坑:
- 用 class + display:none !important,不用 jQuery show()/hide(),
  后者会把行内 display 写成 block,回退 3.2.28 的 flow-root 修复。
- 隐藏不 disabled 输入框,密钥字段是「留空=不修改」语义,disabled
  不随表单提交,会把已保存的密钥冲掉。
This commit is contained in:
2026-10-27 21:40:00 +08:00
parent 0a5486e195
commit 1a31e412b0
3 changed files with 141 additions and 34 deletions
+36 -12
View File
@@ -135,25 +135,49 @@ jQuery(document).ready(function ($) {
// 硬编码的 jQuery)。所以主题里其它 19 处 'requires' 其实都是死代码, // 硬编码的 jQuery)。所以主题里其它 19 处 'requires' 其实都是死代码,
// 这里不再往那个坑里加,直接写显式逻辑。 // 这里不再往那个坑里加,直接写显式逻辑。
// //
// 为什么连说明文字一起藏:用户没勾别名生成时,那段「去百度申请 APPID」 // ── 3.2.58 起这里变成**两层**条件 ──────────────────────────────────
// 只会是噪声。
// //
// ⚠️ 用 delegation + toggleClass('yq-cond-hide'),不直接绑 input.hide(): // 第一层(原有):w_on_slug 勾了 → 这一整块才出现
// 原因与上面的自动目录一致(.section 的 display 是 flow-root, // 第二层(本次):w_provider 选了谁 → 只出现那一家的输入框
// jQuery 会把行内 display 写成 block,回退 3.2.28 的布局修复)。 //
// info 那块没有 .section 类,靠 CSS 里补的 .section-info 规则命中。 // 两层是「与」的关系:没勾别名生成时,整块都不该出现(不管下拉框选的谁)。
//
// 🔴🔴 最容易踩的坑:这两层必须**在同一个函数里一起算**,不能各写一个
// yq_toggle_xxx() 分别去 toggleClass 同一批元素。因为 toggleClass 的
// 第二参数是「绝对赋值」不是「与」:
// 函数A 按第一层算出 true → 去掉 yq-cond-hide(显示)
// 函数B 按第二层算出 false → 加上 yq-cond-hide(隐藏)
// 两个都跑完之后,结果**只取决于谁后注册**,跟真实业务无关。
// 表现就是「勾了别名生成、选了百度,百度框却时有时无」这种鬼故事。
//
// ── 分组是数据驱动的,不是 JS 里写死映射表 ────────────────────────
// 服务商清单只有 theme-writing.php 的 yang_writing_providers() 一份。
// 若在这里抄一遍 {baidu:[...], youdao:[...]},加一家要改两处,且不一致时
// **不报错**,只表现成「新服务商的框永远不出现」。所以分组标记写在 PHP
// 每个字段的 'class' 上:
// yang-prov → 我参与「按服务商显示」
// yang-prov-<key> → 我属于 <key> 这家
// 这里只读 class、和下拉框当前 value 比对,零服务商知识。
function yq_toggle_writing_slug_fields () { function yq_toggle_writing_slug_fields () {
var on = jQuery('#w_on_slug').is(':checked') var on = jQuery('#w_on_slug').is(':checked')
// 整块的两个「共同成员」(不区分服务商的部分)
jQuery('#section-w_provider').toggleClass('yq-cond-hide', !on) jQuery('#section-w_provider').toggleClass('yq-cond-hide', !on)
jQuery('#section-w_baidu_appid').toggleClass('yq-cond-hide', !on)
jQuery('#section-w_baidu_key').toggleClass('yq-cond-hide', !on)
jQuery('#section-w_youdao_appid').toggleClass('yq-cond-hide', !on)
jQuery('#section-w_youdao_key').toggleClass('yq-cond-hide', !on)
jQuery('#section-w_timeout').toggleClass('yq-cond-hide', !on) jQuery('#section-w_timeout').toggleClass('yq-cond-hide', !on)
jQuery('#w_provider_help').toggleClass('yq-cond-hide', !on)
// 按服务商分组的字段:两层条件合成一个结果再赋值
var provider = String(jQuery('#w_provider').val() || '')
jQuery('.yang-prov').each(function () {
var el = jQuery(this)
var belongs = el.hasClass('yang-prov-' + provider)
// 第一层不成立时,第二层算什么都不重要 —— 一律隐藏
el.toggleClass('yq-cond-hide', !(on && belongs))
})
} }
jQuery(document).on('change', '#w_on_slug', yq_toggle_writing_slug_fields) // 两个控件任一变化都要重算:下拉框换了服务商、或者勾掉别名生成
jQuery(document).on('change', '#w_on_slug, #w_provider', yq_toggle_writing_slug_fields)
yq_toggle_writing_slug_fields() yq_toggle_writing_slug_fields()
// 写作增强:「每篇最多新增几个标签」只在「自动生成标签」勾选时才有意义。 // 写作增强:「每篇最多新增几个标签」只在「自动生成标签」勾选时才有意义。
+104 -21
View File
@@ -1364,42 +1364,111 @@ function kratos_options()
$prov_sel[$k] = $v['label']; $prov_sel[$k] = $v['label'];
} }
/* ── 首帧显隐:在服务端就把它算对 ──────────────────────────
*
* 不这么做的后果:进设置页时四个密钥框**全部先显示**,等
* options-custom.js 跑到 yq_toggle_writing_slug_fields() 才被藏起来 ——
* 每次进页面、每次保存后重载,都闪一下。
*
* inc/theme-avatar.php 那边踩过同一个坑,结论一样:
* 「首帧的显隐由 PHP 打 class 上(不闪),JS 只管‘点了之后’。」
*
* ⚠️ 这里读的是**当前已保存值**,不是 std。用户没保存过时
* kratos_option 会给回传的默认值,正好也是表单上显示的值,一致。 */
$yang_prov_now = (string) kratos_option('w_provider', 'mymemory');
if (!isset($prov_sel[$yang_prov_now])) {
/* 库里的值不在清单里(配置被改坏 / 服务商下架)时,按第一项算,
与 select 实际会选中的那项保持一致 —— 否则首帧和 JS 修正后不一样。
🔴 别用 key($prov_sel) 取「第一项」:那读的是数组**内部指针**,
而指针位置取决于前面有没有跑过 foreach / reset,属于隐式状态。
PHP 7 起 foreach 按值遍历不动指针,所以现在恰好对;但这是巧合,
不是保证 —— 中间插一句 reset() 或改成按引用遍历就会变。
线上硬基线是 PHP 7.4,用 array_key_first()(7.3+)才是显式的。 */
$yang_first = function_exists('array_key_first') ? array_key_first($prov_sel) : null;
if ($yang_first === null && !empty($prov_sel)) {
$yang_keys = array_keys($prov_sel);
$yang_first = $yang_keys[0];
}
$yang_prov_now = (string) $yang_first;
}
$yang_on_slug = function_exists('yang_writing_on') ? yang_writing_on('w_on_slug') : false;
/* 生成某个字段的 class。$key 为 null 表示「不按服务商分组、只看总开关」。 */
$yang_prov_class = function ($key = null) use ($yang_on_slug, $yang_prov_now) {
if ($key === null) {
return $yang_on_slug ? '' : 'yq-cond-hide';
}
$cls = 'yang-prov yang-prov-' . $key;
if (!$yang_on_slug || $key !== $yang_prov_now) {
$cls .= ' yq-cond-hide';
}
return $cls;
};
$options[] = array( $options[] = array(
'name' => __('翻译服务商', 'kratos'), 'name' => __('翻译服务商', 'kratos'),
'desc' => __('别名生成用的翻译服务。<b>MyMemory 无需密钥,开箱可用</b>,' 'desc' => __('别名生成用的翻译服务。<b>MyMemory 无需密钥,开箱可用</b>,'
. '但它是国外服务、有每日字符限额;被限流时换成百度即可。', 'kratos'), . '但它是国外服务、有每日字符限额;被限流时换成百度即可。'
. '<br>选定之后,<b>下方只出现这一家的账号输入框</b> —— 用不上的不会摆在那里碍事。', 'kratos'),
'std' => 'mymemory', 'std' => 'mymemory',
'id' => 'w_provider', 'id' => 'w_provider',
'type' => 'select', 'type' => 'select',
'options' => $prov_sel, 'options' => $prov_sel,
'class' => $yang_prov_class(),
); );
/* ── 按服务商分组的账号字段 ────────────────────────────────
*
* 需求(3.2.58):选了谁才显示谁的输入框,别把四家的密钥框全摆着。
*
* 实现要点 —— `class` 里那两个勾子不是装饰,是**唯一的分组声明处**:
* yang-prov 「我说哪家就显示哪家」的成员标记
* yang-prov-<key> 属于哪一家,key 必须与 yang_writing_providers() 一致
*
* 为什么把分组写在 PHP 而不是 JS 里写死一张映射表:
* 服务商清单本来就只有 theme-writing.php 一份(yang_writing_providers())。
* 若 JS 里再抄一份 {baidu:[...], youdao:[...]},将来加一家就得改两处,
* 而且两边不一致时**不会报错**,只会表现成「新服务商的框永远不出现」。
* 现在 JS 只做一件事:读元素上的 class,和下拉框当前值比对。
*
* ⚠️ 框架确实会把这里的 class 拼到包裹 div 上(见
* class-options-interface.php:70 的 `$class .= ' ' . $value['class']`),
* 所以 text/password 会得到 `section section-text yang-prov ...`。
*
* ⚠️ info 类型走的是另一条分支(:272),它拼出来的 class 是
* `section section-info yang-prov ...` —— 注意它**也有 .section 类**。
* 本文件下面那段 CSS 注释里写的「info 不生成 .section 包裹层」只对
* `id="section-xxx"` 这个 id 成立,class 是有的。别再被那句话误导。
*/
/* MyMemory:无需任何账号,说明只有一句。 */
$options[] = array( $options[] = array(
'name' => __('服务商账号填写说明', 'kratos'), 'name' => __('服务商账号填写说明', 'kratos'),
/* 给 info 一个 id:它不生成 section- 包裹层(框架对 info 特殊处理), 'id' => 'w_provider_help_mymemory',
所以只能靠这个 id 让 JS 能把它一起折叠。 */ 'desc' => __('<b>MyMemory 不用填任何东西</b>,选它就已经可用。'
'id' => 'w_provider_help', . '<br>它是免费公共服务,每日有字符限额;某天开始报错、别名生成不出来,'
'desc' => __('<b>MyMemory</b>:不用填任何东西。<br>' . '多半是限额用完了 —— 换百度即可。', 'kratos'),
. '<b>百度翻译</b>:访问 <a href="https://fanyi-api.baidu.com/" target="_blank" rel="noopener">fanyi-api.baidu.com</a> '
. '→ 注册登录 → 进入「管理控制台」→ 在「开发者信息」里可看到 <b>APPID</b> 与 <b>密钥</b>。'
. '免费版每月 5 万字符。<br>'
. '<b>有道翻译</b>:访问 <a href="https://ai.youdao.com/" target="_blank" rel="noopener">ai.youdao.com</a> '
. '→ 注册登录 → 「创建应用」选「文本翻译」→ 在「应用管理」里可看到 <b>应用 ID</b> 与 <b>应用密钥</b>。', 'kratos'),
'type' => 'info', 'type' => 'info',
'class' => $yang_prov_class('mymemory'),
);
$options[] = array(
'name' => __('百度翻译 · 账号怎么拿', 'kratos'),
'id' => 'w_provider_help_baidu',
'desc' => __('访问 <a href="https://fanyi-api.baidu.com/" target="_blank" rel="noopener">fanyi-api.baidu.com</a> '
. '→ 注册登录 → 进入「管理控制台」→ 在「开发者信息」里可看到 <b>APPID</b> 与 <b>密钥</b>。'
. '<br>免费版每月 5 万字符,对个人博客够用。', 'kratos'),
'type' => 'info',
'class' => $yang_prov_class('baidu'),
); );
/* 密钥字段用 password 类型。
*
* ⚠️ 注意:框架会把真实值写进 HTML 的 value 属性(浏览器里 F12
* 就能看到)。SMTP 密码字段现在也是这样。这里保持与之一致,
* 不单独另做一套脱敏 —— 否则同一个页面上两种行为,更容易混淆。
* 想彻底避免,只能改成「不回显、留空即不修改」,那会牵动整个
* 框架的保存语义,不在本次范围。 */
$options[] = array( $options[] = array(
'name' => __('百度 APPID', 'kratos'), 'name' => __('百度 APPID', 'kratos'),
'desc' => __('选「百度翻译」时填写。', 'kratos'), 'desc' => __('在百度控制台的「开发者信息」里,形如 <code>20240xxx</code> 的一串数字。', 'kratos'),
'id' => 'w_baidu_appid', 'id' => 'w_baidu_appid',
'type' => 'text', 'type' => 'text',
'class' => $yang_prov_class('baidu'),
); );
/* 密钥字段的特殊处理(只对这两项生效)。 /* 密钥字段的特殊处理(只对这两项生效)。
@@ -1417,29 +1486,42 @@ function kratos_options()
$options[] = array( $options[] = array(
'name' => __('百度密钥', 'kratos'), 'name' => __('百度密钥', 'kratos'),
'desc' => __('选「百度翻译」时填写。<b>留空表示不修改</b>已保存的密钥。', 'kratos') 'desc' => __('<b>留空表示不修改</b>已保存的密钥。', 'kratos')
. ($yang_baidu_hint !== '' ? '<br>当前已保存:<code>' . esc_html($yang_baidu_hint) . '</code>' : ''), . ($yang_baidu_hint !== '' ? '<br>当前已保存:<code>' . esc_html($yang_baidu_hint) . '</code>' : ''),
'id' => 'w_baidu_key', 'id' => 'w_baidu_key',
'type' => 'password', 'type' => 'password',
'std' => '', 'std' => '',
'class' => $yang_prov_class('baidu'),
);
$options[] = array(
'name' => __('有道翻译 · 账号怎么拿', 'kratos'),
'id' => 'w_provider_help_youdao',
'desc' => __('访问 <a href="https://ai.youdao.com/" target="_blank" rel="noopener">ai.youdao.com</a> '
. '→ 注册登录 → 「创建应用」选「文本翻译」→ 在「应用管理」里可看到 <b>应用 ID</b> 与 <b>应用密钥</b>。', 'kratos'),
'type' => 'info',
'class' => $yang_prov_class('youdao'),
); );
$options[] = array( $options[] = array(
'name' => __('有道应用 ID', 'kratos'), 'name' => __('有道应用 ID', 'kratos'),
'desc' => __('选「有道翻译」时填写。', 'kratos'), 'desc' => __('在「应用管理」里查。', 'kratos'),
'id' => 'w_youdao_appid', 'id' => 'w_youdao_appid',
'type' => 'text', 'type' => 'text',
'class' => $yang_prov_class('youdao'),
); );
$options[] = array( $options[] = array(
'name' => __('有道应用密钥', 'kratos'), 'name' => __('有道应用密钥', 'kratos'),
'desc' => __('选「有道翻译」时填写。<b>留空表示不修改</b>已保存的密钥。', 'kratos') 'desc' => __('<b>留空表示不修改</b>已保存的密钥。', 'kratos')
. ($yang_youdao_hint !== '' ? '<br>当前已保存:<code>' . esc_html($yang_youdao_hint) . '</code>' : ''), . ($yang_youdao_hint !== '' ? '<br>当前已保存:<code>' . esc_html($yang_youdao_hint) . '</code>' : ''),
'id' => 'w_youdao_key', 'id' => 'w_youdao_key',
'type' => 'password', 'type' => 'password',
'std' => '', 'std' => '',
'class' => $yang_prov_class('youdao'),
); );
/* 超时对所有服务商通用,所以只跟第一层(w_on_slug)走,不分服务商分组。 */
$options[] = array( $options[] = array(
'name' => __('接口超时(秒)', 'kratos'), 'name' => __('接口超时(秒)', 'kratos'),
'desc' => __('调用翻译接口的最长等待时间,建议 8 秒。' 'desc' => __('调用翻译接口的最长等待时间,建议 8 秒。'
@@ -1447,6 +1529,7 @@ function kratos_options()
'std' => '8', 'std' => '8',
'id' => 'w_timeout', 'id' => 'w_timeout',
'type' => 'text', 'type' => 'text',
'class' => $yang_prov_class(),
); );
/* ══════════════════════════════════════════════════════════ /* ══════════════════════════════════════════════════════════
+1 -1
View File
@@ -1,7 +1,7 @@
/* /*
Theme Name: Kratos_Yang Theme Name: Kratos_Yang
Text Domain: kratos_Yang Text Domain: kratos_Yang
Version: 3.2.57 Version: 3.2.58
Requires PHP: 7.0 Requires PHP: 7.0
Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。 Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。
Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好 Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好