PVQ-UI
v1.4.3
Gitee

文档

PVQ-UI 的开发方案、项目规则、组件清单与调用方法。文档为代码唯一真源,本页直接渲染仓库中的真实 Markdown。

调用手册

PVQ-UI 组件库调用手册(插件实操)

状态:使用指南(插件开发者速查) 适用:任何要调用 pvq-ui 的 PVQ 插件(新插件 / 存量迁移) 关联:《PVQ-UI项目规则.md》(铁律)、《PVQ-UI组件库开发方案.md》(架构)、《库演进治理方案.md》(版本化)


1. 一句话流程

*复制 loader → 一行 enqueue → 直接写 `.pvq-` BEM 类 → 自身 CSS 只写插件专有样式。**

不要自己注册库句柄、不要复制库 CSS、不要自造通用组件。


2. 接入步骤(新插件)

2.1 复制 loader

把 pvq-ui 仓库里的 pvq-ui-loader.php 复制到插件 includes/ 目录(或任意合适位置)。这是唯一需要"复制"的文件,其余全走 CDN。

2.2 一行 enqueue

在插件的 admin_enqueue_scripts / enqueue_block_editor_assets / wp_enqueue_scripts 钩子里:

require_once __DIR__ . '/includes/pvq-ui-loader.php';

// 后台场景(最常用)
PVQ_UI_Loader::enqueue( array( 'admin' ) );

// 块编辑器场景
PVQ_UI_Loader::enqueue( array( 'editor' ) );

// 前端场景
PVQ_UI_Loader::enqueue( array( 'frontend' ) );

// 多场景一次性
PVQ_UI_Loader::enqueue( array( 'admin', 'frontend' ) );

第二参数 $version 推荐留空,由站点级 PVQ_UI_VERSION 统一决定(见 §2.3);仅在需要"某插件单独锁版本"时才显式传,且须理解其先到先得语义。 版本下限:选择器 Select / 删除二次确认需 ≥1.2.0、Search 需 ≥1.2.8、Table/DataGrid/Weight 需 ≥1.2.9、Modal/Range 可编辑需 ≥1.2.10、Toast/Popover 需 ≥1.3.0(逻辑随 pvq-ui.js 发布,低于该版本不生效)。

  • 第一个参数 $scenes:需要哪些层就传哪些(admin / editor / frontend)。base 层 pvq-ui 会自动随依赖加载,无需手动传。
  • 第二个参数 $version:锁定库版本。推荐做法(v1.4.0 起):站点级统一定义一次,调用处不传版本。

2.3 版本定义:全站一处(推荐,v1.4.0 起)

loader 里的 PVQ_UI_VERSION 是 defined() 守卫的先到先得常量,且 converge() 会按它把所有 pvq-ui* 句柄的 URL 对齐过去——也就是说全站实际生效的库版本 = 第一次调用 enqueue() 时那个插件传的版本(不传则用它自己副本的 DEFAULT_VERSION)。这带来两个隐患:

  • 谁先加载不确定 → 某个插件把版本钉旧(如硬编码 '1.2.10'),全站都被拉回旧版,其他插件即使副本已升版也无效;
  • 插件副本的 DEFAULT_VERSION 只有在"该插件恰好第一个调用"时才起作用,升级副本常常是空转的。

⚠️ 还有更隐蔽的一层(v1.4.2 补充,务必知道):每份副本的类定义都被 if ( ! class_exists( 'PVQ_UI_Loader' ) ) 包裹,所以最先 require loader 的那个插件的副本决定了整个类——其后所有副本的整份文件被跳过,它们的 DEFAULT_VERSION / register() / converge() 从未执行。此时所有调用点都"没传版本"、看起来完全合规,但全站版本其实是最先加载的那份副本说了算。若那份副本恰好落后,就会出现「新版插件里 .pvq-popover / .pvq-toast 等组件"不存在"、浮层打不开、操作无反馈」这类静默故障,排查时完全想不到是组件库版本问题。因此:所有 PVQ 插件的 loader 副本必须保持同版本,只升其中一个是不够的;并且强烈建议同时在 wp-config.php 定义 PVQ_UI_VERSION——否则"不传版本"等于把版本交给加载顺序。

v1.4.2 起 loader 自带兜底:未定义站点常量时在文件作用域按「各副本自述版本的最高者」就地定义 PVQ_UI_VERSION(因此即便最先加载的旧副本赢得了类归属、其类代码被执行,也会沿用这个正确值——旧副本再也无法把全站拉回旧版);并且当 站点生效版本 < 最高副本版本 时一次性告警(_doing_it_wrong() + error_log,列出各副本版本、落后项与修复命令)。健康站点零输出。但这只是防御,正常做法仍是"先定义站点常量 + 同步全部副本"。

因此推荐在站点级定义一次,插件调用处一律不传版本:

// wp-config.php(在 /* That's all, stop editing! */ 之前)
define( 'PVQ_UI_VERSION', '1.4.3' );   // 全站 PVQ-UI 版本唯一真源
// 各插件:不再传版本
PVQ_UI_Loader::enqueue( array( 'admin' ) );
  • 好处:版本只改一处、与插件加载顺序无关、插件副本的 DEFAULT_VERSION 不再影响全站(两层"先到先得"同时被钉死)。
  • 需要"某插件单独锁旧版"时(临时隔离),才在调用处显式传版本;但注意上述先到先得语义——只有最先加载者有话语权,故这种方式不适合长期共存多版本。插件不要写版本字面量(如 enqueue(['frontend'], '1.2.10')),那是最典型的钉版本来源。
  • 升版后别忘了同步站点常量:站点常量低于最高副本版本时 loader 会告警(提示常量落后)。
  • 站点若因内网/离线需要指向本地副本,仍用 PVQ_UI_URL 等常量覆盖(见 §6)。

2.4 loader 副本同步(升版必做,已脚本化)

pvq-ui-loader.php 是复制分发的(每个插件 cp 一份放自己 includes/),因此库升版或 loader 逻辑改动后,插件侧副本会过期。库仓库提供两个脚本(scripts/ 下):

php scripts/check-loader-copies.php      # 只读体检:列出全部副本的版本/漂移/调用点,发现落后则退出码非 0
php scripts/sync-loaders.php             # 干跑(默认):打印将要做的事
php scripts/sync-loaders.php --apply     # 实际同步(留 .bak、逐个 php -l)

判定规则(与《项目规则》§4.4 一致,脚本已固化):

差异性质 处置
与真源逐字节一致 跳过
仅注释 / DEFAULT_VERSION 差异 用真源 cp 覆盖(插件侧副本应以真源为准)
衍生副本(自有文档头、if (...): 语法等) 最小侵入:只改 DEFAULT_VERSION 与其行前注释,保留本地内容

.bak 约定:脚本写前留 .bak(便于即时回退),验证通过后请清除(find <插件目录> -name '*.bak*' -delete);.bak 不入库、不长期堆积。

当前全站副本(2026-09-19 二次核实,4 份):

副本 性质 处置
/home/muyu/pvq-weixinyuedu/includes/ 与真源逐字节一致(安装处是软链;该目录非 git 仓库) cp 覆盖
/www/wwwroot/wordpress/wp-content/plugins/pvq-wp/includes/ 与真源逐字节一致 cp 覆盖
/www/wwwroot/wordpress/wp-content/plugins/pvq-threads-sync/includes/ 衍生副本(自有文档头 / 冒号语法),依赖逻辑已核实与真源一致 只改版本行
/mnt/c/Users/muyu/Documents/code/Gastronomy/wordpress/pvq-workfolio/includes/ 安装处是软链;独立 git 仓库 oh-af/pvq-workfolio。1.4.2 前为 1.2.10 且缺 font-awesome-frontend 逻辑 头部是标准头 → 整份 cp

⚠️ 找副本必须跟随符号链接:默认 find 不跟随符号链接,会整份漏掉以软链安装的插件——pvq-workfolio 那份陈旧副本就是这样长期未被发现的(它指向 /mnt/c/...,不在任何扫描根下)。请用:

find -L /www/wwwroot/wordpress/wp-content /home/muyu -maxdepth 8 -name 'pvq-ui-loader.php'

体检脚本已不依赖 find:它直接遍历 wp-content/plugins/* 并 realpath() 解析,因此软链插件也能被发现。新增插件后重跑体检,看是否多出第 5 份。库自身的 site/pvq-ui-loader.php 必须与仓库根真源逐字节一致(体检一并核对)。

2.5 自身 CSS 声明依赖

插件自己的样式要"站在库肩上",声明依赖即可:

wp_enqueue_style(
    'my-plugin-admin',
    plugins_url( 'assets/css/admin.css', __FILE__ ),
    array( 'pvq-ui', 'pvq-ui-admin' ), // 依赖库,不复制库样式
    MY_PLUGIN_VER
);

2.6 直接写组件

在 PHP 渲染的 HTML / 模板里,直接用库提供的 .pvq-* 类,无需任何额外注册:

<div class="pvq-card">
  <div class="pvq-card__header">
    <span class="pvq-card__icon pvq-card__icon--primary"><i class="fas fa-cog"></i></span>
    <div>
      <h3 class="pvq-card__title">连接设置</h3>
      <p class="pvq-card__desc">配置你的账号信息</p>
    </div>
  </div>
  <div class="pvq-card__action">
    <button class="pvq-btn pvq-btn--primary">保存</button>
  </div>
</div>

图标用 FontAwesome class(如 fas fa-cog)。loader 在 admin 与 frontend 两层均声明对 font-awesome 句柄的依赖并保证其已注册,故这两层直接写 fas 图标即可;base / editor 层若需 FA 图标,请额外 enqueue(['admin']) 或手动注册 font-awesome。


3. 场景与层对照

你想用在哪 enqueue 传参 自动加载 典型组件
后台设置页 ['admin'] pvq-ui + pvq-ui-admin 卡片 / 表单 / 开关 / 勾选框 / 单选 / 选项卡 / 横幅 / 统计 / 帮助
块编辑器 ['editor'] pvq-ui + pvq-ui-editor 块骨架 placeholder / loading / preview
前端页面 ['frontend'] pvq-ui + pvq-ui-frontend callout / quote / prose / 进度条 / 书签 / 卡片化表格 / 数据表格(DataGrid) / 权重输入框(Weight)
全场景基础 (不需要单独传,base 随依赖带) pvq-ui 按钮 / 令牌 / 动画

4. 常用组件速查(仅列类名,完整 API 见 doc/模块文档/设计令牌与组件清单.md;机器真源为 site/inc/components.json)

完整清单与示例以 doc/模块文档/设计令牌与组件清单.md 为准。

  • 按钮:.pvq-btn .pvq-btn--primary/--secondary/--danger/--warning/--ghost .pvq-btn--sm/--icon .pvq-btn-group(破坏性操作加 data-pvq-confirm 二次确认,见 §4.2)

  • 卡片:.pvq-card .pvq-card__header/__icon/__icon--primary/__title/__desc/__action

  • 表单:.pvq-field .pvq-field__label .pvq-field__help .pvq-field__error(红字错误文案槽).pvq-field__success(绿字成功文案槽,与 error 对称).pvq-input .pvq-input__wrap .pvq-input__wrap--has-icon .pvq-input__icon .pvq-input__toggle;校验态:.pvq-field--error(红框+红辉光).pvq-field--success(绿框+绿辉光).pvq-field__label--required(必填 *);变体:前后缀 .pvq-affix-group/.pvq-affix/--suffix、输入组 .pvq-input-group/.pvq-input-group__btn(--ghost)、清除 .pvq-input__clear+.pvq-input__wrap--has-clear、密码显隐切换 .pvq-input__toggle+.pvq-input__wrap--has-toggle(由 pvq-ui.js 自动初始化)

  • 开关(语义化,纯 CSS):

    <label class="pvq-switch">
      <input type="checkbox" class="pvq-switch__input" checked>
      <span class="pvq-switch__slider"></span>
    </label>
  • 选项卡 + 磁吸滑块(胶囊 pill):.pvq-tabs .pvq-tabs__slider .pvq-tab .pvq-tab--active .pvq-tab__icon .pvq-tab-content .pvq-tab-content--active(容器/tab/滑块全圆角 999px,width:fit-content 紧凑;形态细节见《设计令牌与组件清单.md》Tabs 条目)

  • 状态横幅:.pvq-banner .pvq-banner--success/danger/warning/info .pvq-banner__dot

  • 状态指示:.pvq-status .pvq-status--success/warning/error .pvq-status__dot

  • 统计:.pvq-stat-grid .pvq-stat .pvq-stat__value .pvq-stat__label

  • 帮助:.pvq-help .pvq-help__icon .pvq-help__title .pvq-help__text

  • 范围:.pvq-range .pvq-range__group .pvq-range__value .pvq-range__labels(v1.2.10:.pvq-range__value 用 <input type="number"> 即可直接在框内改数字,回写滑块,clamp + step 对齐;用 <span> 保持只读)

  • 二次确认弹窗(v1.2.10 新增,stable,含 JS):.pvq-modal[data-pvq-modal] > .pvq-modal__backdrop + .pvq-modal__panel(__close/__icon/__title/__desc/可选__list/__actions>__cancel+__confirm)。弹窗式二次确认,与就地确认 data-pvq-confirm 互为补充。颜色由 --pvq-modal-accent(默认 --pvq-danger 红)驱动,变体 --warning/--primary 只换令牌。确认按钮为实色 accent(须用 (0,4,1) 特度压过库 button.pvq-btn.pvq-btn)。库 initConfirms() 自动接管:触发器 [data-pvq-modal-open="<id>"]、关闭 [data-pvq-modal-close]、确认 [data-pvq-modal-confirm](派发 pvq-modal:confirm,detail:{id, modal, trigger},插件监听执行业务);异步加 [data-pvq-modal-async] 转 spinner 并禁用,业务完成后调 modal.closeModal()。含焦点陷阱、默认聚焦取消、Esc、焦点归还、body 滚动锁。

  • 其它:.pvq-divider .pvq-spinner .pvq-alert .pvq-alert--success/error/loading .pvq-meta-list .pvq-meta-list__row/__key/__value

  • 选择器 Select(1.1.2 新增,experimental):

    <div class="pvq-select">
      <select name="threads_sync_settings[mode]">
        <option value="auto">自动同步</option>
        <option value="manual" selected>手动同步</option>
        <option value="off" disabled>关闭(维护中)</option>
      </select>
    </div>

    详见下方 §4.1 完整调用接口。

    • 变体 --sm(紧凑):高度 30px、字号 13px、解除库默认 min-width:160px,适配 WP 后台密集行(tablenav / bulkactions / 媒体工具栏)。纯 CSS,JS 无需改;加在外层 .pvq-select--sm 即可。
    • 变体 --glass(玻璃磨砂下拉,见 §4.1.9):容器加 pvq-select--glass,下拉面板半透明白 + backdrop-filter blur 磨砂;JS 自动把容器状态带到 body 级面板。
  • 搜索 Search(1.2.8 新增,stable):.pvq-search .pvq-search__icon .pvq-search__input .pvq-search__clear(清除 ✕).pvq-search__submit(提交).pvq-search__spinner(加载态);功能变体:--with-btn(提交按钮,joined)--with-filter(复用 .pvq-select 作筛选段)--loading(图标位转 spinner)--sm(高 34px);形状变体:--pill/--underline(无框仅底线)/--ghost(填底无边框)/--glass(半透明 + blur);颜色令牌 --pvq-search-accent(默认 --pvq-primary 绿,hover 用 color-mix 压暗,插件可覆盖任意色)。库 initSearches() 自动初始化,提交/筛选 change 派发 pvq-search 事件。

    详见下方 §4.3 完整调用接口。

  • 进度条 Progress(1.2.9 并入,stable):.pvq-progress .pvq-progress__fill(设 width 控制进度).pvq-progress__text;变体 .pvq-progress--lg(大号).pvq-progress--danger(危险红,走 --pvq-danger)。颜色令牌 --pvq-progress-accent(默认 --pvq-primary 绿)。

  • 书签 Bookmark(1.2.9 并入,stable):.pvq-bookmark .pvq-bookmark__image(左图).pvq-bookmark__content .pvq-bookmark__url .pvq-bookmark__favicon .pvq-bookmark__title .pvq-bookmark__description(右文);去掉 __image 即纯文字链接卡。纯 CSS。

  • 勾选框 Checkbox(base 层,1.2.9 并入;末次修订由 frontend 层提升至 base,故 enqueue(['admin']) 的后台也可用):结构 label.pvq-cb-item > 隐藏 <input type=checkbox> + span.pvq-custom-check(.pvq-cb-box+span.pvq-cb-mark ✓)+ span.pvq-cb-text,容器 .pvq-cb-list;三变体 pvq-cb-variant-strike(勾选删除线)/pvq-cb-variant-ghost(透明底圆角)/pvq-cb-variant-ripple(圆形+波纹);尺寸 --sm(24px)/默认(32px)/--lg(40px);支持 :indeterminate(列表页"全选"主框)。纯 CSS,颜色全走 --pvq-primary。

  • 单选按钮 Radio(base 层,1.2.9 并入;末次修订由 frontend 层提升至 base):结构 label.pvq-rb-item > 隐藏 <input type=radio> + span.pvq-custom-radio(.pvq-rb-outer+span.pvq-rb-dot)+ span.pvq-rb-label,容器 .pvq-rb-group;四变体 pvq-rb-variant-card(选中高亮整行)/pvq-rb-variant-ghost/pvq-rb-variant-ripple/pvq-rb-variant-solid(纯色填充);尺寸 --sm/--lg。纯 CSS,颜色全走 --pvq-primary。

  • 卡片化表格 Table(1.2.9 并入,stable):.pvq-table(配 .pvq-table-wrap 加 [data-pvq-table] 触发入场动画)即原生 table/thead/tbody;表头绿底、单元格独立圆角卡片;--striped(斑马纹)、__col--right/center(列对齐)、caption。普通 .pvq-table 不加 data 则完全可见、无动画。

  • 数据表格 DataGrid(1.2.9 并入,stable,含 JS):.pvq-datagrid[data-pvq-datagrid] > .pvq-datagrid__toolbar(搜索/排序/筛选/重置)+ .pvq-datagrid__content > .pvq-datagrid__table + .pvq-datagrid__empty。搜索/筛选为自包含可展开胶囊(点图标原地展开,点栏外空白收起);排序三态循环(点头列/按钮);重置清空全部。库 initDataGrids() 自动接管,筛选下拉按 th[data-col] 与单元格值自动生成。

  • 权重输入框 Weight(v1.2.10 新增,随 1.2.11 固化发布,stable,含 JS):.pvq-weight[data-pvq-weight] > .pvq-weight__list > .pvq-weight__item(label / input-wrap / slider-wrap 三段横向,窄屏 flex-wrap)。数字框 .pvq-weight__input(input[type="number"],CSS 写 input.pvq-weight__input 压过 WP 核心)+ 单位 .pvq-weight__unit(绝对定位右下);滑块 .pvq-weight__slider(input[type="range"])。底部 .pvq-weight__action 含总和 .pvq-weight__total-val 与「重置 / 归一化」按钮(data-pvq-weight-reset / data-pvq-weight-normalize,按钮复用库 .pvq-btn--primary/.pvq-btn--ghost + .pvq-weight__btn-pill 胶囊)。库 initWeights() 自动接管:输入↔滑块双向同步、实时总和、归一化(总和归 100,差值补首行)并派发 pvq-weight:normalized(detail.weights)供插件持久化,重置派发 pvq-weight:reset。设计原则:形状归库、颜色归令牌——组件定义 --pvq-weight-accent(默认 --pvq-primary 绿),聚焦/滑块/徽章全走它,零硬编码;变体 .pvq-weight--sm(紧凑)/.pvq-weight--glass(玻璃拟态)。

    详见下方 §4.4 完整调用接口。

  • 锚点浮层 Popover(v1.3.1 新增,experimental,含 JS,回流自 pvq-weixinyuedu):.pvq-popover .pvq-popover__body .pvq-popover__foot .pvq-popover__arrow,修饰符 --open(打开态,JS 加)/--above(JS 判定的上方位置)。以元素为锚、脱离文档流、按需显隐(术语解释 / 作者卡 / 预览 / 迷你详情)——card 是常驻容器(无定位/开合)、collapse 是原地展开会撑高文档流、confirm-modal 是模态阻断、toast 固定角落不锚定,四者都不能替代。开关只有两态:hidden 属性(彻底不显示)+ --open(淡入显示)。position:fixed 实时定位(不依赖祖先 position),视口边缘自动翻转并水平/垂直双向夹取;桌面悬停(移开延时 160ms 关闭)、触屏点按开合、键盘聚焦 + Escape(关闭并交还焦点)。JS 双入口:PVQUI.popover(anchor, opts) 与声明式 [data-pvq-popover="<浮层 id>"],initPopovers() 随 initAll 自动接管且幂等;事件 pvq-popover:open / pvq-popover:close。

    详见下方 §4.7 完整调用接口。


5. JS 交互(无需你写,库已委托)

pvq-ui.js 已用事件委托处理以下交互,插件只监听自定义事件即可:

交互 库行为 插件要做的
选项卡切换 点击 .pvq-tab → 自动切 .pvq-tab--active + 移动 .pvq-tabs__slider 无需处理(纯视觉)
开关 真实 checkbox,纯 CSS 切换,无需 JS 监听 change 事件或不监听(表单原生提交即带值)
range 拖动 同步 .pvq-range__value(<span> 写 text;<input> 写 value) 如需存值,监听 input 事件
range 数值框编辑(v1.2.10) .pvq-range__value 为 <input type="number"> 时可直接改数字 → 实时回写滑块(clamp min/max + step 对齐),并对滑块派发 input/change。双向实时联动互不失效:拖滑块同步框内、改框内同步滑块;输入过程中不改写框内文本,回车/失焦才校正显示 无需处理;监听滑块 input/change 即可拿到值
搜索 输入回车 / 提交按钮 → 派发 pvq-search CustomEvent(detail: { query, filter, element });筛选 <select> change(仅当有词)同 监听 pvq-search 调后端检索;异步期间自行加/去 .pvq-search--loading
卡片化表格 [data-pvq-table] 由 IntersectionObserver 触发入场动画(pvq-is-visible),尊重 prefers-reduced-motion 无需处理(纯视觉);普通 .pvq-table 不加 data-pvq-table 则完全可见
数据表格 DataGrid [data-pvq-datagrid] 由库 initDataGrids() 接管:搜索实时过滤行、排序重排(数值/中文)、筛选按列+值多选、重置恢复、入场动画;筛选下拉按 th[data-col] + 单元格值自动生成 如需把排序/筛选结果回传后端,可监听工具栏按钮(.pvq-datagrid__btn--sort/--reset)的 click;默认纯前端过滤
二次确认弹窗 Confirm Modal(v1.2.10) [data-pvq-modal-open="<id>"] 打开、[data-pvq-modal-close] 关闭(X/取消/点遮罩)、[data-pvq-modal-confirm] 确认并派发 pvq-modal:confirm(detail: {id, modal, trigger});含焦点陷阱、默认聚焦取消、Esc、焦点归还、滚动锁(<html> 加 .pvq-scroll-locked,关闭 / pagehide 释放,bfcache 恢复自愈),库 initConfirms() 自动接管 监听 pvq-modal:confirm 执行真实业务(删除请求等);异步场景给确认按钮加 [data-pvq-modal-async](转 spinner 并禁用防重复提交),业务完成后调 e.detail.modal.closeModal()

严禁在插件里写 inline onclick / onkeydown 调库组件;一律用 data-* + 委托 / 原生事件。


4.1 选择器 Select 调用接口(1.1.2 新增 · experimental)

完整 API 以 doc/模块文档/设计令牌与组件清单.md「选择器 Select」条目为准。下方是插件调用的实操契约。

4.1.1 调用前提

  1. 版本必须 ≥ 1.1.2(选择器逻辑随 pvq-ui.js 发布,旧版无此功能):
    PVQ_UI_Loader::enqueue( array( 'admin' ), '1.2.0' ); // 后台场景;selector 逻辑随 base 自动加载

    pvq-ui.js 在所有场景的 enqueue() 里都会被 wp_enqueue_script('pvq-ui') 带出,无需单独 enqueue JS。

  2. 插件不复制、不重写任何下拉 JS/CSS;选择器纯靠写 HTML 类自动生效。

4.1.2 HTML 契约(必须这样写)

<div class="pvq-select">
  <select name="your_form[field]">            <!-- ① 原生 select 必须保留,且 name 照常 -->
    <option value="a">选项 A</option>
    <option value="b" selected>选项 B</option> <!-- ② selected 决定初始显示 -->
    <option value="c" disabled>选项 C(禁用)</option> <!-- ③ disabled 自动识别 -->
  </select>
</div>
  • 类名唯一要求:外层包一个 .pvq-select,里面放一个原生 <select>。
  • 原生 <select> 仅视觉隐藏、DOM 仍在 → 表单照常提交,WP 里 $_POST['your_form']['field'] 正常拿到值。.pvq-select 可放进 .pvq-field 内做行布局(见展示页)。
  • 不要手写 .pvq-select__trigger / .pvq-select__dropdown / .pvq-select__option —— 这些由库 JS 自动生成(触发器生成在 .pvq-select 内,下拉面板挂在 <body>)。

4.1.3 自动初始化(无需你调用)

  • pvq-ui.js 在 DOMContentLoaded 时自动扫描文档内所有 .pvq-select 并增强;已初始化的打 data-pvqSelectInit="1" 标记,重复扫描不会重复渲染(幂等)。
  • 因此静态 HTML 写完即生效,你不需要在插件 JS 里调任何初始化函数。

4.1.4 取值方式(两种,任选)

  • 表单原生提交:name 已挂在原生 select 上,随 <form> 提交自动带值(与旧 jQuery 版完全一致)。
  • JS 监听变更:监听原生 select 的 change 事件即可(库在选中时会 dispatchEvent(new Event('change'))):
    document.querySelector('select[name="your_form[field]"]')
      .addEventListener('change', function (e) {
        console.log('当前值:', e.target.value);
      });

4.1.5 动态内容(AJAX 注入后重挂)

若 select 是 AJAX 后来插入 DOM 的,注入后调一次即可:

if (window.PVQUI && window.PVQUI.initSelects) { window.PVQUI.initSelects(); }

(已初始化的会跳过,未初始化的会被接管。)

4.1.6 交互能力清单(库已内置,无需插件写)

开/关、箭头翻转、空间不足自动向上弹(flip)、右边界贴合、滚动/缩放重定位、键盘(Enter/Space 切换、ArrowDown 打开、方向键移动高亮、Enter 选中、Esc 关闭)、点击外部关闭、同时只开一个、选中同步原生 select 并派发 change、禁用项不可选。

4.1.7 可访问性现状(experimental 边界,知悉即可)

已含 role="button"/aria-haspopup/aria-expanded/aria-selected + 键盘高亮 --active。完整 listbox/option 方向键语义化聚焦留待 stable,不影响键盘可用性与表单提交。

4.1.8 threads 迁移配方(存量替换 .pvq-ts-*)

旧 threads 用 pvqSelect($sel)(jQuery)把原生 select 改成 .pvq-ts-* 结构。改用库后:

步骤 旧(jQuery) 新(库,原生)
1. 调库 wp_enqueue_script 自己的 admin.js + FontAwesome PVQ_UI_Loader::enqueue(['admin'], '1.2.0')(自带 FA + pvq-ui.js)
2. 初始化 PHP 渲染 <select> + JS 调 pvqSelect($sel) 删掉 pvqSelect(...) 调用;HTML 改为 .pvq-select > select(§4.1.2)
3. 结构类 .pvq-ts-wrap .pvq-ts-trigger .pvq-ts-dropdown .pvq-ts-option 自动生成(无需手写)
4. 取值 旧 change 监听照常 原生 select change 监听照常(name 不变)

Before(旧,勿保留):

<!-- PHP 渲染原生 select -->
<select name="threads_sync_settings[mode]">…</select>
// admin.js(删)
$('select[name="threads_sync_settings[mode]"]').each(function(){ pvqSelect($(this)); });

After(新):

<div class="pvq-select">
  <select name="threads_sync_settings[mode]">…</select>
</div>
// functions.php / 加载点:仅一行 enqueue,无自定义下拉 JS
require_once __DIR__ . '/includes/pvq-ui-loader.php';
PVQ_UI_Loader::enqueue( array( 'admin' ), '1.2.0' );

旧 .pvq-ts-* 的 CSS/JS 建议先保留(改名或注释),迁移出问题时可即时切回;确认无回归后再删。另见《库演进治理方案》防回潮。

4.1.9 玻璃磨砂变体 --glass(装饰性,可选)

下拉面板默认是不透明纸面(--pvq-surface)。想让下拉背景变半透明磨砂,只需在容器上加一个类,trigger 与表单行为不变:

<div class="pvq-select pvq-select--glass">
  <select name="your_form[field]">
    <option value="a">选项 A</option>
    <option value="b" selected>选项 B</option>
  </select>
</div>
  • 效果:下拉面板半透明白(rgba(255,255,255,.52))+ backdrop-filter: blur(20px) saturate(1.4),背后内容被磨砂成柔和色斑。背后需有彩色/图片内容才明显;白底上仅轻微通透。
  • 兼容:不支持 backdrop-filter 的环境自动退回 rgba(255,255,255,.72) 高不透明度,文字始终可读。无需插件额外 JS。
  • 说明:下拉面板由库 JS 渲染并挂 <body>(position:fixed),容器上的类不会自动落到面板——库在打开时检测 pvq-select--glass 并把状态带到面板(.pvq-select__dropdown--glass),插件无需关心。
  • 与 --sm 可叠加:<div class="pvq-select pvq-select--sm pvq-select--glass">。

4.2 删除按钮二次确认(1.2.0 新增 · stable)

完整 API 以 doc/模块文档/设计令牌与组件清单.md「删除按钮二次确认」条目为准。下方是插件调用的实操契约。

4.2.1 调用前提

  1. 版本必须 ≥ 1.2.0(二次确认逻辑随 pvq-ui.js 发布):
    PVQ_UI_Loader::enqueue( array( 'admin' ), '1.2.0' );

    pvq-ui.js 在所有场景的 enqueue() 里都会被 wp_enqueue_script('pvq-ui') 带出,无需单独 enqueue JS。

  2. 删除按钮视觉复用 .pvq-btn--danger + fa-trash;二次确认是*纯 `data-` 增强**,无需插件写任何 JS。

4.2.2 HTML 契约

<button class="pvq-btn pvq-btn--danger" data-pvq-confirm="确定删除该项?">
  <i class="fas fa-trash"></i> 删除
</button>
  • 仅多一个 data-pvq-confirm 属性(值为确认态显示的文案);其余按普通按钮写。
  • 图标-only / 小号变体同样可用:.pvq-btn--danger.pvq-btn--icon[data-pvq-confirm="确定?"]、.pvq-btn--danger.pvq-btn--sm。

4.2.3 行为契约(库已内置,无需插件写)

  1. 首次点击:仅进入确认态——按钮变实色危险红(.pvq-btn--armed)+ 文案替换为确认语,不触发任何原行为(不提交表单、不跑插件 handler)。
  2. 确认态内再次点击:恢复外观并放行原行为(表单提交 / 插件自有 click 监听照常)。
  3. 超时(3s)/ Esc / 点击页面其它处:自动取消恢复原样。
  4. WordPress:按钮在 <form> 内(如 upload.php)时,库在捕获阶段 preventDefault,不会整页重载;第二次点击才放行。

4.2.4 插件要做的

  • 无需监听、无需初始化;把真正删除逻辑挂在按钮原本的 click / 表单提交上即可(二次确认由库透明拦截首次点击)。
  • 严禁为二次确认写 inline onclick;一律靠 data-pvq-confirm + 库委托。

4.3 搜索 Search 调用接口(1.2.8 新增 · stable)

完整 API 以 doc/模块文档/设计令牌与组件清单.md「搜索组件 Search」条目为准。下方是插件调用的实操契约。

4.3.1 调用前提

  1. 版本必须 ≥ 1.2.8(搜索逻辑随 pvq-ui.js 发布):PVQ_UI_Loader::enqueue( array( 'admin' ), '1.2.8' );,JS 随 base 自动加载,无需单独 enqueue。
  2. 插件不复制、不重写任何搜索 JS/CSS;纯靠写 HTML 类自动生效。

4.3.2 HTML 契约(必须这样写)

<!-- 基础:图标 + 输入 + 清除 -->
<div class="pvq-search" data-pvq-search>
  <i class="pvq-search__icon fas fa-magnifying-glass"></i>
  <input class="pvq-search__input" type="text" placeholder="搜索文章、用户、标签…">
  <button class="pvq-search__clear" type="button" aria-label="清除"><i class="fas fa-xmark"></i></button>
</div>

<!-- 带提交按钮 -->
<div class="pvq-search pvq-search--with-btn" data-pvq-search>
  <i class="pvq-search__icon fas fa-magnifying-glass"></i>
  <input class="pvq-search__input" type="text" placeholder="输入关键词后点搜索">
  <button class="pvq-search__submit" type="button"><i class="fas fa-magnifying-glass"></i> 搜索</button>
</div>

<!-- 筛选前置(复用库 .pvq-select) -->
<div class="pvq-search pvq-search--with-filter" data-pvq-search>
  <div class="pvq-select pvq-search__filter">
    <select name="pvq_search_scope">
      <option>全部</option><option>标题</option><option>正文</option><option>作者</option>
    </select>
  </div>
  <i class="pvq-search__icon fas fa-magnifying-glass"></i>
  <input class="pvq-search__input" type="text" placeholder="在所选范围内搜索…">
</div>
  • 类名唯一要求:容器 .pvq-search(加 data-pvq-search 让库接管),内部 <input class="pvq-search__input">;可选 .pvq-search__clear / .pvq-search__submit;筛选段用 .pvq-select.pvq-search__filter 包裹原生 <select>,库自动增强。
  • --with-filter 几何(v1.4.1 修正):筛选段固定 110px 且是流内 flex 子项,故输入文字用元素自身坐标的 padding-left: 40px(= 容器目标 150px − 段宽 110px);插件无需也无法自定义这两个数值(改库 CSS 即可,或提缺陷;见《项目规则》§2 通用组件只在库)。
  • 暗色不是变体:暗色由库 --pvq-surface/--pvq-text/--pvq-border/--pvq-primary 令牌主题切换,.pvq-search 自动适配。

4.3.3 自动初始化(无需你调用)

pvq-ui.js 的 initSearches()(initAll 自动调用,幂等)会:① 清除按钮显隐/清空;② 回车或点提交按钮 → 读取词与(若有)筛选值;③ 筛选 <select> 改选(仅当有词)即时重搜;④ 派发 pvq-search CustomEvent(bubbles,detail: { query, filter, element })。

4.3.4 插件要做的事(接检索后端)

document.addEventListener('pvq-search', function (e) {
  var q = e.detail.query, scope = e.detail.filter;   // scope 为 --with-filter 时的范围值,无则 undefined
  var box = e.target.closest('[data-pvq-search]');
  // 调真实检索接口;异步期间自行加/去 .pvq-search--loading 显示 spinner
  box.classList.add('pvq-search--loading');
  fetch('/search?q=' + encodeURIComponent(q) + (scope ? '&scope=' + scope : ''))
    .then(function (r) { return r.json(); })
    .finally(function () { box.classList.remove('pvq-search--loading'); });
});
  • 库不假设后端、不自动切换 loading;loading 态由插件控制(加 .pvq-search--loading 即显示 spinner 并隐藏放大镜)。

4.4 卡片化表格 Table / 数据表格 DataGrid 调用接口(1.2.9 并入 · stable)

完整 API 以 doc/模块文档/设计令牌与组件清单.md「卡片化表格 Table / 数据表格 DataGrid」两条目为准。下方是插件调用的实操契约。

4.4.1 调用前提

  1. 版本必须 ≥ 1.2.9(表格逻辑随 pvq-ui.js 发布):PVQ_UI_Loader::enqueue( array( 'frontend' ), '1.2.9' );,JS 随 base 自动加载,无需单独 enqueue。
  2. 前端表格纯靠写 HTML 类自动生效;DataGrid 的搜索/排序/筛选/重置由库 initDataGrids()(initAll 自动调用、幂等)接管。

4.4.2 简单表格 HTML 契约

<!-- 卡片化表格:加 [data-pvq-table] 触发入场动画;纯展示可省略 data -->
<div class="pvq-table-wrap" data-pvq-table>
  <table class="pvq-table pvq-table--striped">
    <caption>语言速览</caption>
    <thead>
      <tr><th>语言</th><th class="pvq-table__col--right">诞生年</th></tr>
    </thead>
    <tbody>
      <tr><td>PHP</td><td class="pvq-table__col--right">1995</td></tr>
      <tr><td>Python</td><td class="pvq-table__col--right">1991</td></tr>
    </tbody>
  </table>
</div>
  • 单元格独立圆角卡片、表头绿底、悬停微放大;--striped 加斑马纹;__col--right/center 仅做列级对齐。

4.4.3 DataGrid HTML 契约(必须这样写)

<div class="pvq-datagrid" data-pvq-datagrid>
  <div class="pvq-datagrid__toolbar">
    <!-- 搜索:自包含胶囊,折叠为圆图标,点击原地展开 -->
    <div class="pvq-datagrid__search">
      <button class="pvq-datagrid__toggle pvq-datagrid__toggle--search" type="button"><i class="fas fa-magnifying-glass"></i></button>
      <input class="pvq-datagrid__search-input" type="text" placeholder="搜索…">
      <button class="pvq-datagrid__search-clear" type="button" aria-label="清除"><i class="fas fa-xmark"></i></button>
    </div>
    <!-- 排序:点头列或此按钮三态循环 无/升/降 -->
    <button class="pvq-datagrid__btn pvq-datagrid__btn--sort" type="button"><i class="fa-solid fa-up-down"></i></button>
    <!-- 筛选:自包含胶囊,展开为「列 + 值」双下拉,菜单由库自动生成 -->
    <div class="pvq-datagrid__filter">
      <button class="pvq-datagrid__toggle pvq-datagrid__toggle--filter" type="button"><i class="fas fa-filter"></i></button>
      <div class="pvq-dd" data-pvq-dd="col"><button class="pvq-dd__trigger" type="button"><span class="pvq-dd__text">选择列</span><i class="pvq-dd__arrow fas fa-chevron-down"></i></button><div class="pvq-dd__menu"></div></div>
      <div class="pvq-dd" data-pvq-dd="val"><button class="pvq-dd__trigger" type="button"><span class="pvq-dd__text">选择值</span><i class="pvq-dd__arrow fas fa-chevron-down"></i></button><div class="pvq-dd__menu"></div></div>
    </div>
    <!-- 重置:清空搜索/筛选/排序 -->
    <button class="pvq-datagrid__btn pvq-datagrid__btn--reset" type="button"><i class="fas fa-rotate-left"></i></button>
  </div>
  <div class="pvq-datagrid__content">
    <table class="pvq-datagrid__table">
      <thead>
        <!-- th 加 data-col 供筛选列菜单识别;排序图标槽 .pvq-sort-icon 可省略(库 JS 自动注入每个表头) -->
        <tr><th data-col="项目">项目<i class="pvq-sort-icon"></i></th><th data-col="数量" class="pvq-table__col--right">数量<i class="pvq-sort-icon"></i></th></tr>
      </thead>
      <tbody>
        <tr><td>示例 A</td><td class="pvq-table__col--right">12</td></tr>
        <tr><td>示例 B</td><td class="pvq-table__col--right">3</td></tr>
      </tbody>
    </table>
    <div class="pvq-datagrid__empty">无匹配结果</div>
  </div>
</div>
  • 类名唯一要求:容器 .pvq-datagrid[data-pvq-datagrid];工具栏四件套齐全;表格用 .pvq-datagrid__table,表头 th[data-col] 给筛选列菜单用。
  • 筛选下拉菜单由库自动生成:写空 .pvq-dd__menu 即可,库按 th[data-col] 与单元格值填充列/值;无需手写 <option>。
  • 搜索/筛选点击栏外空白自动收起;排序点头列或排序按钮三态循环;重置恢复全部。

4.4.4 自动初始化(无需你调用)

pvq-ui.js 的 initTables() / initDataGrids()(initAll 自动调用、幂等)会:① 简单表格 IntersectionObserver 入场动画;② DataGrid 搜索实时过滤、排序重排(数值按大小、中文按 localeCompare)、筛选按列+值多选、重置恢复、入场动画。已初始化的打 data-pvqTableInit="1" / data-pvqDgInit="1",重复扫描不重复渲染。

4.4.5 动态内容(AJAX 注入后重挂)

若表格是 AJAX 后来插入 DOM 的,注入后调一次即可:

if (window.PVQUI && window.PVQUI.initTables) { window.PVQUI.initTables(); }
if (window.PVQUI && window.PVQUI.initDataGrids) { window.PVQUI.initDataGrids(); }

4.5 二次确认弹窗 Confirm Modal(v1.2.10 新增)

破坏性 / 重要操作的弹窗式二次确认。与 §4.x 的 data-pvq-confirm(按钮就地二次确认)互为补充:就地确认适合表格行内删除等轻量场景;弹窗适合需要说明后果、列出影响对象、异步执行的重量级场景。

最简用法(三属性即可)

<!-- 1) 触发按钮:指向弹窗 id -->
<button class="pvq-btn pvq-btn--danger" data-pvq-modal-open="del-modal">删除</button>

<!-- 2) 弹窗本体(放在页面任意位置,推荐 body 末尾) -->
<div class="pvq-modal" id="del-modal" data-pvq-modal role="dialog"
     aria-modal="true" aria-labelledby="del-t">
  <div class="pvq-modal__backdrop" data-pvq-modal-close></div>
  <div class="pvq-modal__panel">
    <button class="pvq-modal__close" data-pvq-modal-close aria-label="关闭">✕</button>
    <div class="pvq-modal__icon"><i class="fas fa-triangle-exclamation"></i></div>
    <h3 class="pvq-modal__title" id="del-t">确认删除?</h3>
    <p class="pvq-modal__desc">此操作将永久删除 <strong>“项目报告 Q3”</strong>,且<strong>无法恢复</strong>。</p>
    <div class="pvq-modal__actions">
      <button type="button" class="pvq-btn pvq-modal__cancel" data-pvq-modal-close>取消</button>
      <button type="button" class="pvq-btn pvq-modal__confirm" data-pvq-modal-confirm>确认删除</button>
    </div>
  </div>
</div>

插件要做的:监听 confirm 执行业务

document.addEventListener('pvq-modal:confirm', function (e) {
  if (e.detail.id !== 'del-modal') { return; }
  // e.detail = { id, modal, trigger }
  // …在这里发删除请求;库只管 UI,不碰业务
});

异步场景(防重复提交)

给确认按钮加 data-pvq-modal-async,点击后按钮转 spinner 并禁用,库不会自动关闭,由业务完成后调 closeModal():

<button type="button" class="pvq-btn pvq-modal__confirm"
        data-pvq-modal-confirm data-pvq-modal-async>
  <span class="pvq-modal__spinner"></span>
  <span class="pvq-modal__confirm-text">开始同步</span>
</button>
document.addEventListener('pvq-modal:confirm', function (e) {
  if (e.detail.id !== 'sync-modal') { return; }
  fetch('/api/sync', { method: 'POST' })
    .then(function () { e.detail.modal.closeModal(); })   // 成功后关
    .catch(function () { e.detail.modal.closeModal(); }); // 失败也要关,或自行恢复按钮
});

语义变体(只换一个令牌)

<div class="pvq-modal pvq-modal--warning" …>   <!-- 黄:覆盖 / 重置 -->
<div class="pvq-modal pvq-modal--primary" …>   <!-- 森林绿:发布等非破坏性 -->
<div class="pvq-modal" …>                      <!-- 默认红:删除等破坏性 -->

也可自行覆盖:style="--pvq-modal-accent:#8b5cf6"(图标、清单圆点、确认按钮、投影同步变化)。

带影响对象清单

在 __desc 后插入即可,圆点自动用 accent 色:

<ul class="pvq-modal__list">
  <li>设计令牌与组件清单.md</li>
  <li>PVQ-UI 组件库开发方案.md</li>
</ul>

类名与行为契约

属性 / 类 作用
.pvq-modal[data-pvq-modal] + id 弹窗容器(默认 display:none,库加 --open 显形)
[data-pvq-modal-open="<id>"] 触发器,值为目标弹窗 id
[data-pvq-modal-close] 关闭:右上角 X / 取消按钮 / 遮罩均用它
[data-pvq-modal-confirm] 确认:派发 pvq-modal:confirm 后关闭
[data-pvq-modal-async] 确认后转 spinner + 禁用,等业务调 closeModal()
.pvq-modal--busy 异步进行态(库自动加/去),显示 spinner

事件:pvq-modal:open / pvq-modal:close / pvq-modal:confirm(均 bubbles,detail:{id, modal, trigger})。

无障碍(库已内置,插件无需处理):role="dialog" + aria-modal + aria-labelledby;打开时默认聚焦取消按钮(防回车误触发破坏性操作);焦点陷阱 Tab/Shift+Tab 锁在弹窗内;Esc 关闭;关闭后焦点归还触发元素;滚动锁定(库在 <html> 上加 .pvq-scroll-locked,关闭与 pagehide 时释放,bfcache 恢复时自愈复位);prefers-reduced-motion 降级。

⚠️ 改写按钮颜色的插件注意:库基础 button.pvq-btn.pvq-btn(0,3,1)会把 background 刷成 --pvq-surface 白色,覆盖按钮色必须用 ≥(0,4,1) 的选择器(如 button.pvq-btn.pvq-modal__confirm.pvq-modal__confirm),否则会出现"只有彩色阴影、按钮本体是白的"。


4.6 浮层通知 Toast 与可关闭横幅 Banner __close(experimental)

4.6.1 先选对组件(反馈分级)

场景 用什么
一次操作的即时结论(保存成功 / 连接失败 / 删除完成) .pvq-toast(本次新增)
结果内联在表单旁(加上 ImGui? 校验结论) .pvq-alert(已有)
常驻的待办 / 提醒(未配置 Token、升级提示) .pvq-banner + __close(本次新增关闭能力)
结果是一组多行数据 页面内常驻区(如 .pvq-meta-list),toast 只给一句结论
长任务进行中 内联 .pvq-alert--loading,不要用 toast

4.6.2 调用前提

PVQ_UI_Loader::enqueue(['base'])(或 ['admin'])后自动生效——initToasts() / initBanners() 已并入 initAll,页面上无需写初始化代码。

4.6.3 Toast:命令式

PVQUI.toast({ type: 'success', title: '设置已保存' });
PVQUI.toast({ type: 'error', title: '连接失败', desc: 'Token 无效或已过期', duration: 0 }); // 0 = 不自动关闭
字段 默认 说明
type info success / error / warning / info
title 必填 主文案
desc — 次要文案
icon 按 type FontAwesome 类名,如 fas fa-circle-check
duration success/info 3000、warning 5000、error 8000 毫秒;0 = 常驻直到手动关闭

返回值即实例:{ el, type, duration, close(reason) },可自行 inst.close()。

⚠️ 文案走 textContent,不接受 HTML——库刻意不支持富文本,避免插件把后端返回拼进 toast 造成 XSS。

4.6.4 Toast:声明式(后端渲染友好,无 inline handler)

<button type="button" class="pvq-btn pvq-btn--primary"
        data-pvq-toast data-pvq-toast-type="success"
        data-pvq-toast-title="已同步 12 条" data-pvq-toast-duration="3000">同步</button>

data-pvq-toast-title 省略时取元素文本;duration 支持 0。

4.6.5 让开 WP admin bar(后台必做)

库默认值 --pvq-toast-offset-y: 20px 是中性值(前台不留多余空白)。后台需要在 admin 样式里覆写:

body.wp-admin { --pvq-toast-offset-y: 44px; }              /* admin bar 32px */
@media (max-width: 782px) { body.wp-admin { --pvq-toast-offset-y: 50px; } }  /* admin bar 46px */

4.6.6 事件与幂等

document.addEventListener('pvq-toast:close', function (e) {
  // e.detail: { toast, el, type, reason }  reason: 'timer' | 'close' | 'displace'
});
  • 同屏最多 4 条,超出挤走最旧一条(reason: 'displace');悬停暂停倒计时、移开继续。
  • 幂等提醒:initAll() 同时绑在 DOMContentLoaded 与 window resize 上,故 initToasts() 只确保容器存在、绝不清理正在显示的 toast。插件若要"清场",请自行遍历 .pvq-toast 调 inst.close(),不要试图复位库状态。
  • AJAX 注入横幅后,调一次 PVQUI.initBanners() 重挂(幂等,重复调用安全)。

4.6.7 可关闭横幅 + 关闭记忆

<div class="pvq-banner pvq-banner--warning" data-pvq-banner-key="setup-todo">
  <span class="pvq-banner__dot"></span>
  <div><div class="pvq-banner__title">尚未配置 Token</div><div class="pvq-banner__text">前往「设置 → 连接」完成授权。</div></div>
  <button type="button" class="pvq-banner__close" data-pvq-banner-close aria-label="关闭">✕</button>
</div>
// 库只负责 UI:播离场、移除节点、派发事件;记忆策略归插件
document.addEventListener('pvq-banner:close', function (e) {
  var key = e.detail.banner.dataset.pvqBannerKey;
  if (key) { fetch(ajaxurl, { method: 'POST', body: new URLSearchParams({ action: 'pvq_dismiss_banner', key: key }) }); }
});

库不读写 storage(localStorage / user_meta 都不碰),以免跨插件猜测键名。不含 __close 的横幅行为完全不变,向后兼容。


4.7 锚点浮层 Popover 调用接口(v1.3.1 新增 · experimental · 回流自 pvq-weixinyuedu)

4.7.1 先选对组件

「要显示一块内容」有五种常见诉求,选错会导致布局位移或过度阻断:

诉求 用哪个
常驻内容块(不点不消失) .pvq-card
原地展开(内容撑开文档流) .pvq-collapse
强阻断、必须做决定 .pvq-modal(见 §4.5)
操作结果的即时结论(角落、自动消失) .pvq-toast(见 §4.6)
以某元素为锚、脱离文档流、按需显隐(术语解释 / 作者卡 / 预览 / 迷你详情) .pvq-popover(本节)

4.7.2 调用前提

  • 已 enqueue base 层(popover 在 base)。
  • 必须有 pvq-ui.js:定位与开合全靠库 JS。缺 JS 时浮层保持不可见(库默认即浮层态);如需兜底请在服务端另给静态呈现(如 <noscript> 块)。

4.7.3 HTML 契约(必须这样写)

<!-- 锚点:任意可聚焦元素。data-pvq-popover 指向浮层 id;aria-describedby 供读屏关联 -->
<button type="button" tabindex="0"
        data-pvq-popover="pvq-reading-note-1" aria-describedby="pvq-reading-note-1">引自原文</button>

<!-- 浮层本体:位置随意(库不移动 DOM,靠 fixed 定位),但不要放在带 transform 的容器内 -->
<div class="pvq-popover" id="pvq-reading-note-1" role="note" hidden>
  <div class="pvq-popover__body">…点评内容…</div>
  <div class="pvq-popover__foot">…出处 / 时间…</div>
  <span class="pvq-popover__arrow"></span>
</div>
  • hidden 属性必须写(初始不显示);打开/关闭由库 JS 增删 --open 与 hidden。
  • role="note"(不要写 tooltip:本组件允许浮层内含链接等富内容,而 tooltip 语义要求纯说明文本)。
  • __arrow 可选;写了则由库自动对准锚点中心。
  • 一个浮层配一个锚点;多个锚点请各写一份。

4.7.4 自动初始化(无需你调用)

initPopovers() 随 initAll()(DOMContentLoaded + resize)自动执行:扫描 [data-pvq-popover] → 取值为浮层 id(值为空时回退读 aria-describedby)→ 目标必须存在且带 .pvq-popover 类(避免误绑普通 aria-describedby 说明文本)→ 建立关联。幂等:同一浮层只初始化一次,document/window 级监听只绑一次。

4.7.5 交互清单(库已内置,无需插件写)

操作 行为
桌面悬停锚点 打开;移开后延时 160ms 关闭(给鼠标移入浮层的时间)
触屏点按锚点 开合切换(库用 matchMedia('(hover: hover)') 判定,不信任伪悬停)
键盘 Tab 聚焦锚点 打开;焦点离开浮层与锚点 → 关闭
Escape 关闭并把焦点交还锚点
点击浮层与锚点之外 关闭
锚点滚动 / 窗口缩放 打开态重新定位(捕获阶段监听,任意滚动容器均跟随)
打开 / 关闭动画 淡入淡出 180ms;prefers-reduced-motion 下降级为瞬时

4.7.6 事件(插件可选监听)

事件 时机 detail
pvq-popover:open 打开后 { popover, el, anchor, reason }
pvq-popover:close 关闭时(元素仍在,过渡结束后才 hidden) { popover, el, anchor, reason }

reason ∈ pointer(悬停)/ focus(聚焦)/ click(触屏点按)/ escape / outside(点外部)/ api(代码调用)。

4.7.7 命令式(浮层内容动态生成时)

var pop = PVQUI.popover(anchorEl, {
  content: '<p>…</p>',   // 或传 DOM 节点
  placement: 'auto',     // auto | below | above
  gap: 8,
  closeDelay: 160,
  onOpen: function (inst) {},
  onClose: function (inst) {}
});
pop.open();
pop.destroy();  // 解绑(库创建的浮层节点一并移除;destroy 后可重新 popover() 挂载)

⚠️ content 传字符串时按 HTML 插入(点评可能含格式)。不得拼接未转义的用户输入,只处理可信内容时建议改传 DOM 节点。

4.7.8 插件要做的

  1. HTML 按 §4.7.3 写(hidden + role="note" + aria-describedby 齐全)。
  2. 无需写任何交互 JS;动态注入(AJAX)后如需重挂,再调一次 PVQUI.initPopovers() 即可(幂等)。
  3. 浮层内容由后端渲染时,不要放在带 transform / filter / backdrop-filter / will-change 的容器内(会成为 fixed 包含块致坐标偏移),必要时把浮层挪到该容器之外。

4.7.9 迁移配方(pvq-weixinyuedu 想法卡片)

插件现状 收敛为
.pvq-wr-thought 外壳(fixed / --open / 定位全在插件 CSS 与 thought.js) 外壳改 .pvq-popover,只保留业务内容样式(引文、点评体、时间)
.pvq-wr-js 分支(head 同步脚本加类消 FOUC) 全部删除——库默认即浮层态,thought-boot.js 可删
data-pvq-thought-trigger + aria-describedby 绑定 data-pvq-popover="<浮层 id>"(aria-describedby 仍被兼容)
thought.js 的 place() / 翻转 / CLOSE_DELAY / EXIT_MS 删除,改由库 initPopovers() 接管

6. 内网站点 / 本地副本覆盖

若站点无法访问公共 CDN,用常量把库指向本地副本(在 wp-config.php 或插件最早执行处定义):

define( 'PVQ_UI_URL',        'https://内网域名/pvq-ui/v1.2.0/pvq-ui.css' );
define( 'PVQ_UI_ADMIN_URL',  'https://内网域名/pvq-ui/v1.2.0/pvq-ui-admin.css' );
define( 'PVQ_UI_EDITOR_URL', 'https://内网域名/pvq-ui/v1.2.0/pvq-ui-editor.css' );
define( 'PVQ_UI_FRONT_URL',  'https://内网域名/pvq-ui/v1.2.0/pvq-ui-frontend.css' );
define( 'PVQ_UI_VERSION',    '1.2.0' );

loader 用 defined() 守卫,仅当未定义时才用内置默认值——所以站点的覆盖优先。

如需完全关闭 loader 的版本收敛,可 define('PVQ_UI_NO_CONVERGE', true);。


7. 存量插件迁移(wolai / pvq-wp / threads)

对照《PVQ-UI组件库开发方案.md》§9 与《组件提取对照表.md》§6 映射表:

  1. 复制 pvq-ui-loader.php,改三个 enqueue 点为 PVQ_UI_Loader::enqueue(...)。
  2. 删插件自有 CSS 中已进库的通用类 + :root 令牌;保留插件专有类(如 .pvq-wolai-*),硬编码色值改为 --pvq-*。
  3. HTML 旧类全量改新 BEM 名(注意旧类多为双类组合,勿当单类替换)。
  4. 剥离所有 inline handler(§5)。
  5. grep 校验:已进库的类在插件 CSS/HTML 中零残留定义(防回潮)。
  6. 同步 CHANGELOG / readme / site/inc/components.json 与模块文档的稳定性标记(版本七处同步见《PVQ-UI项目规则.md》§1.3)。

8. 自检清单(提交插件前)

  • [ ] 只复制了 pvq-ui-loader.php,没有复制任何库 CSS/JS
  • [ ] 调用了 PVQ_UI_Loader::enqueue(...) 且显式传了版本
  • [ ] 自身 CSS 声明了对 pvq-ui / pvq-ui-<场景> 的依赖
  • [ ] HTML 只用 .pvq-* BEM 类,没有自建通用组件
  • [ ] 无 inline onclick / onkeydown
  • [ ] 颜色 / 圆角 / 阴影均用 --pvq-* 令牌,无硬编码
  • [ ] 迁移插件已完成 grep 防回潮校验