# Vue 3 自定义组件 JS 操作 SOP

## 问题
Vue 3 自定义组件（如 OxdSelect）通过 `addEventListener` 绑定事件，JS `dispatchEvent` 产生的事件 `isTrusted: false`，组件不响应。
- `element.click()` 无效（组件可能绑定 mousedown 而非 click）
- `dispatchEvent(new MouseEvent('mousedown'))` 无效（isTrusted:false）
- `element.focus()` 不触发 Vue 绑定的 focus handler

## 解决方案：直接操作 Vue 组件实例

### 1. 获取 Vue 3 根入口
```javascript
const rootVnode = document.getElementById('app')._vnode;
```

### 2. 遍历 vnode 树匹配 DOM 元素
```javascript
function findCompByEl(vnode, targetEl, depth = 0) {
    if (depth > 50 || !vnode) return null;
    const comp = vnode.component;
    if (comp) {
        if (comp.vnode?.el === targetEl || comp.subTree?.el === targetEl) return comp;
        if (comp.vnode?.el?.contains?.(targetEl)) {
            const result = findCompByEl(comp.subTree, targetEl, depth + 1);
            if (result) return result;
            return comp;
        }
        const subResult = findCompByEl(comp.subTree, targetEl, depth + 1);
        if (subResult) return subResult;
    }
    if (vnode.children && Array.isArray(vnode.children)) {
        for (const child of vnode.children) {
            const result = findCompByEl(child, targetEl, depth + 1);
            if (result) return result;
        }
    }
    if (vnode.dynamicChildren) {
        for (const child of vnode.dynamicChildren) {
            const result = findCompByEl(child, targetEl, depth + 1);
            if (result) return result;
        }
    }
    return null;
}
```

### 3. 调用组件方法
```javascript
// 目标DOM的parentElement通常是组件根元素
const comp = findCompByEl(rootVnode, targetElement.parentElement);
const ctx = comp.proxy;

// 查看可用方法
Object.keys(ctx).filter(k => !k.startsWith('_') && !k.startsWith('$'));

// Select 类组件：直接调用 onSelect
ctx.onSelect({id: 'USD', label: 'United States Dollar'});

// 获取选项列表
ctx.computedOptions; // [{id, label, _selected}, ...]
```

## 组件层级注意
- **展示层**（如 OxdSelectText）：只有 onToggle/onFocus/onBlur，调用无实际效果
- **逻辑层**（如 OxdSelectInput，是展示层的父组件）：有 openDropdown/onSelect/computedOptions/onCloseDropdown
- 定位逻辑层：用 `targetElement.parentElement` 而非 targetElement 本身

### 弹窗内 Select 同样纯 JS 优先（已验证）
- 弹窗（`.oxd-dialog-sheet`）内的 `.oxd-select-text` 用循环向上查找同样能命中 `OxdSelectInput`，`onSelect` 正常工作。
- 不需要 CDP 兜底。仅当循环 8 层仍找不到组件时才考虑 CDP 打开+JS 点 option。

### 循环向上查找模式（推荐）
单层 `parentElement` 可能不够，用循环更健壮：
```javascript
function findSelectComp(selectTextEl) {
  for (let el = selectTextEl, up = 0; el && up < 8; el = el.parentElement, up++) {
    const comp = findCompByEl(rootVnode, el);
    if (comp?.proxy?.onSelect && comp.proxy.computedOptions?.length) return comp;
  }
  return null; // 找不到再考虑CDP兜底
}
```

## 普通 Input/Textarea 操作（nativeSetter）

Vue 3 的 `v-model` 监听 input 事件，直接 `el.value = x` 不触发响应式。需用原型 setter：

```javascript
// Input
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
setter.call(inputEl, '新值');
inputEl.dispatchEvent(new Event('input', {bubbles: true}));
inputEl.dispatchEvent(new Event('change', {bubbles: true}));

// Textarea
const taSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
taSetter.call(textareaEl, '内容');
textareaEl.dispatchEvent(new Event('input', {bubbles: true}));
```

### Date Input 特殊处理
日期组件通常有 blur 校验，需要 focus→赋值→blur 完整链：
```javascript
dateInput.focus();
setter.call(dateInput, '2026-08-05');
dateInput.dispatchEvent(new Event('input', {bubbles: true}));
dateInput.dispatchEvent(new Event('change', {bubbles: true}));
dateInput.dispatchEvent(new Event('blur', {bubbles: true}));
```

### Button
普通 `.click()` 即可，Vue 3 不检查 button click 的 isTrusted。

### File Upload (input[type="file"])
浏览器安全模型禁止JS直接 `input.value='path'`，但可用 DataTransfer API 构造 FileList：
```javascript
const fileInput = document.querySelector('input[type="file"]');
const content = '文件内容';
const file = new File([content], 'filename.txt', { type: 'text/plain', lastModified: Date.now() });
const dt = new DataTransfer();
dt.items.add(file);
fileInput.files = dt.files;  // Chrome 62+ 支持
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
```
- 适用于任何框架（非Vue3特有），纯浏览器API
- 可构造任意类型文件（Blob/ArrayBuffer均可传入File构造器）
- ⚠ CDP `DOM.setFileInputFiles` 只设files属性不触发事件（Chrome通用行为），DataTransfer+dispatch是唯一纯JS方案
- ⚠ 确保弹窗/容器已打开再querySelector，否则input不在DOM中

## 泛化到其他 Vue3 站点（未逐一验证，思路层面）

本 SOP 的核心方法（根 vnode → findCompByEl → proxy）是 Vue3 通用的，但具体方法名/属性名因 UI 库而异。

面对陌生 Vue3 站点的探测思路：

1. **确认是 Vue3** — `document.getElementById('app')?.__vue_app__` 存在即可
2. **定位目标 DOM** — 用选择器找到要操作的元素（如某个 select wrapper）
3. **从 DOM 反查组件** — 用 findCompByEl 从目标元素及其父级向上找，拿到 component
4. **探测组件能力** — 拿到 comp 后查看：
   - `Object.keys(comp.proxy.$options.methods || {})` → 组件方法名
   - `Object.keys(comp.props || {})` → props
   - `Object.keys(comp.setupState || {})` → setup 暴露的响应式数据和函数
   - 重点找类似 onSelect/handleSelect/select/setValue 的方法，以及 options/items/computedOptions 之类的选项列表
5. **试调** — 找到疑似选中方法后，传入选项对象试调，观察 DOM 是否更新
6. **选项格式** — 不同库的 option 结构不同（可能是 `{id, label}` 也可能是 `{value, text}` 或纯字符串），从选项列表数据中取一个完整对象传入即可

注意事项：
- 有些库用 `emits` 而非 methods，选中逻辑可能在父组件而非子组件
- 有些库 prod build 会 minify 方法名，此时 setupState 里的 key 可能是短名，需结合行为猜测
- Composition API 组件的逻辑主要在 setupState 而非 $options.methods
- 如果 proxy 上找不到方法，试试 `comp.exposed`（`<script setup>` 用 defineExpose 暴露的）

## 适用场景
- Vue 3 自定义 Select/Dropdown/Autocomplete 组件 → vnode 实例方法
- Vue 3 普通 Input/Textarea（v-model）→ nativeSetter + input 事件
- Date 组件 → nativeSetter + focus/blur 链
- File Upload → DataTransfer + change 事件
- 需要绕过 isTrusted 检查的场景

## 验证于
- OrangeHRM (opensource-demo.orangehrmlive.com) Vue 3 + OXD 组件库
- 2026-05-08
