{"_id":"@composy/plate-editor-vue","_rev":"4-194e96edd851ce64a84d52851127bedd","name":"@composy/plate-editor-vue","dist-tags":{"latest":"0.0.5"},"versions":{"0.0.2":{"name":"@composy/plate-editor-vue","version":"0.0.2","keywords":["plate-editor","license-plate","vue","canvas"],"license":"MIT","_id":"@composy/plate-editor-vue@0.0.2","maintainers":[{"name":"swimly","email":"979741120@qq.com"}],"dist":{"shasum":"cb95cc59ef654142f79f3a47cb9f7e1567e02ea0","tarball":"https://registry.npmjs.org/@composy/plate-editor-vue/-/plate-editor-vue-0.0.2.tgz","fileCount":55,"integrity":"sha512-MfLOhxKTGVcQg0gsngFyRh2vAilqRFKeeJbil8xT4uImbwPXXq3bQIKiJfteTCre5Psh1Q3y/ijFQ0P0epp7iQ==","signatures":[{"sig":"MEYCIQDW/1xvzo5vy+qXeCm+jOT26wx4xywEaIdCsX4F2gkpHQIhALB7EQ336uEzzArKLlqoi6oNLJgG9iIH/vBrjFVwY6Xd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":270241},"main":"./lib/index.cjs","type":"module","style":"./dist/index.css","types":"./es/index.d.ts","unpkg":"./dist/index.min.js","module":"./es/index.js","browser":"./dist/index.js","exports":{".":{"types":"./es/index.d.ts","import":"./es/index.js","default":"./es/index.js","require":"./lib/index.cjs"},"./esm":{"types":"./esm/index.d.ts","import":"./esm/index.js","default":"./esm/index.js"},"./esm/*":{"types":"./esm/*.d.ts","import":"./esm/*.js","default":"./esm/*.js"},"./index.css":"./dist/index.css","./package.json":"./package.json","./index.min.css":"./dist/index.min.css","./style/index.css":{"import":"./es/style/index.css","default":"./es/style/index.css","require":"./lib/style/index.css"},"./es/style/index.css":"./es/style/index.css","./lib/style/index.css":"./lib/style/index.css"},"scripts":{"lint":"eslint . --cache","build":"ldesign-builder build","lint:check":"eslint . --cache --max-warnings 0","type-check":"vue-tsc --noEmit"},"typings":"./es/index.d.ts","_npmUser":{"name":"swimly","email":"979741120@qq.com"},"jsdelivr":"./dist/index.min.js","_npmVersion":"10.9.2","description":"Vue 3 adapter for @composy/plate-editor-core","directories":{},"sideEffects":["*.vue","*.tsx","*.jsx","dist/*","es/**/style/**","esm/**/style/**"],"_nodeVersion":"23.11.1","dependencies":{"vue-demi":"^0.14.10","@composy/plate-editor-core":"^0.0.2"},"_hasShrinkwrap":false,"devDependencies":{"vue":"3.5.39","eslint":"10.7.0","vue-tsc":"^3.3.7","typescript":"5.9.3","@composy/builder":"^0.0.1","@antfu/eslint-config":"9.1.0"},"peerDependencies":{"vue":"^3.5.0"},"_npmOperationalInternal":{"tmp":"tmp/plate-editor-vue_0.0.2_1786949231016_0.41699182674794466","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@composy/plate-editor-vue","version":"0.0.3","keywords":["plate-editor","license-plate","vue","canvas"],"license":"MIT","_id":"@composy/plate-editor-vue@0.0.3","maintainers":[{"name":"swimly","email":"979741120@qq.com"}],"dist":{"shasum":"af31dee660b2bd499cb2c5f0f2086a55ff2bf98b","tarball":"https://registry.npmjs.org/@composy/plate-editor-vue/-/plate-editor-vue-0.0.3.tgz","fileCount":56,"integrity":"sha512-RK7ew+ATU5FpAe1CiZ7AaBRnNDUnLf+Kyy+KmwzWuBN8iM3C2XcyQXZ6C+KDs6nIgsavTe3Uq7IO88eVIYUgDg==","signatures":[{"sig":"MEUCIQCdSvxhOxEV9dA6A++wuPPyJIKKYiunuybRFoQUJBtkTAIgWXMP4w/PLUMCHjZDihgYaBrw2v4h5ePV3TiXoEi324U=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":327461},"main":"./lib/index.cjs","type":"module","_from":"file:composy-plate-editor-vue-0.0.3.tgz","style":"./dist/index.css","types":"./es/index.d.ts","unpkg":"./dist/index.min.js","module":"./es/index.js","browser":"./dist/index.js","exports":{".":{"types":"./es/index.d.ts","import":"./es/index.js","default":"./es/index.js","require":"./lib/index.cjs"},"./esm":{"types":"./esm/index.d.ts","import":"./esm/index.js","default":"./esm/index.js"},"./esm/*":{"types":"./esm/*.d.ts","import":"./esm/*.js","default":"./esm/*.js"},"./index.css":"./dist/index.css","./package.json":"./package.json","./index.min.css":"./dist/index.min.css","./style/index.css":{"import":"./es/style/index.css","default":"./es/style/index.css","require":"./lib/style/index.css"},"./es/style/index.css":"./es/style/index.css","./lib/style/index.css":"./lib/style/index.css"},"scripts":{"lint":"eslint . --cache","build":"ldesign-builder build","release":"pnpm publish --no-git-checks","lint:check":"eslint . --cache --max-warnings 0","type-check":"vue-tsc --noEmit"},"typings":"./es/index.d.ts","_npmUser":{"name":"swimly","email":"979741120@qq.com"},"jsdelivr":"./dist/index.min.js","_resolved":"D:\\migrated\\Temp\\ea38a3733e23deb5795999c47c197ac8\\composy-plate-editor-vue-0.0.3.tgz","_integrity":"sha512-RK7ew+ATU5FpAe1CiZ7AaBRnNDUnLf+Kyy+KmwzWuBN8iM3C2XcyQXZ6C+KDs6nIgsavTe3Uq7IO88eVIYUgDg==","_npmVersion":"10.9.8","description":"Vue 3 adapter for @composy/plate-editor-core","directories":{},"sideEffects":["*.vue","*.tsx","*.jsx","dist/*","es/**/style/**","esm/**/style/**"],"_nodeVersion":"22.23.1","dependencies":{"vue-demi":"^0.14.10","@composy/plate-editor-core":"^0.0.3"},"_hasShrinkwrap":false,"devDependencies":{"vue":"3.5.39","eslint":"10.7.0","vue-tsc":"^3.3.7","typescript":"5.9.3","@composy/builder":"^0.0.1","@antfu/eslint-config":"9.1.0"},"peerDependencies":{"vue":"^3.5.0"},"_npmOperationalInternal":{"tmp":"tmp/plate-editor-vue_0.0.3_1787105340127_0.6612381647414889","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@composy/plate-editor-vue","version":"0.0.4","keywords":["plate-editor","license-plate","vue","canvas"],"license":"MIT","_id":"@composy/plate-editor-vue@0.0.4","maintainers":[{"name":"swimly","email":"979741120@qq.com"}],"dist":{"shasum":"76c4d6c99a93be8a517ffb55f88c326f5ad357af","tarball":"https://registry.npmjs.org/@composy/plate-editor-vue/-/plate-editor-vue-0.0.4.tgz","fileCount":57,"integrity":"sha512-P8+ZoxK8ejiJFszxSiAYBk1eoteuEtcYCXElYxv/FH0Iq3rzUDNWrztS/oo5pz6hadD+Lg5hyHOOUJoCcXFhBw==","signatures":[{"sig":"MEUCIDXEbj3nX+CNEMvT3qkd9TiOkQCrYSdclBkj+q1wGoGLAiEA+w9wiLV+3z/l4akVDXxZBeSapTfHfRXnthYXtdmL2ZY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":332397},"main":"./lib/index.cjs","type":"module","style":"./dist/index.css","types":"./es/index.d.ts","unpkg":"./dist/index.min.js","module":"./es/index.js","browser":"./dist/index.js","exports":{".":{"types":"./es/index.d.ts","import":"./es/index.js","default":"./es/index.js","require":"./lib/index.cjs"},"./esm":{"types":"./esm/index.d.ts","import":"./esm/index.js","default":"./esm/index.js"},"./esm/*":{"types":"./esm/*.d.ts","import":"./esm/*.js","default":"./esm/*.js"},"./index.css":"./dist/index.css","./package.json":"./package.json","./index.min.css":"./dist/index.min.css","./style/index.css":{"import":"./es/style/index.css","default":"./es/style/index.css","require":"./lib/style/index.css"},"./es/style/index.css":"./es/style/index.css","./lib/style/index.css":"./lib/style/index.css"},"scripts":{"lint":"eslint . --cache","build":"ldesign-builder build","release":"ldesign-release publish --package-dir .","lint:check":"eslint . --cache --max-warnings 0","type-check":"vue-tsc --noEmit"},"typings":"./es/index.d.ts","_npmUser":{"name":"swimly","email":"979741120@qq.com"},"jsdelivr":"./dist/index.min.js","_npmVersion":"10.9.8","description":"Vue 3 adapter for @composy/plate-editor-core","directories":{},"sideEffects":["*.vue","*.tsx","*.jsx","dist/*","es/**/style/**","esm/**/style/**"],"_nodeVersion":"22.23.1","dependencies":{"vue-demi":"^0.14.10","@composy/plate-editor-core":"^0.0.3"},"_hasShrinkwrap":false,"devDependencies":{"vue":"3.5.39","eslint":"10.7.0","vue-tsc":"^3.3.7","typescript":"5.9.3","@composy/builder":"^0.0.1","@composy/release":"^0.0.1","@antfu/eslint-config":"9.1.0"},"peerDependencies":{"vue":"^3.5.0"},"_npmOperationalInternal":{"tmp":"tmp/plate-editor-vue_0.0.4_1787664498684_0.8127955199375936","host":"s3://npm-registry-packages-npm-production"}},"0.0.5":{"name":"@composy/plate-editor-vue","version":"0.0.5","description":"Vue 3 adapter for @composy/plate-editor-core","keywords":["plate-editor","license-plate","vue","canvas"],"license":"MIT","type":"module","sideEffects":["*.vue","*.tsx","*.jsx","dist/*","es/**/style/**","esm/**/style/**"],"exports":{".":{"types":"./es/index.d.ts","import":"./es/index.js","require":"./lib/index.cjs","default":"./es/index.js"},"./esm":{"types":"./esm/index.d.ts","import":"./esm/index.js","default":"./esm/index.js"},"./esm/*":{"types":"./esm/*.d.ts","import":"./esm/*.js","default":"./esm/*.js"},"./index.css":"./dist/index.css","./index.min.css":"./dist/index.min.css","./style/index.css":{"import":"./es/style/index.css","require":"./lib/style/index.css","default":"./es/style/index.css"},"./es/style/index.css":"./es/style/index.css","./lib/style/index.css":"./lib/style/index.css","./package.json":"./package.json"},"main":"./lib/index.cjs","module":"./es/index.js","types":"./es/index.d.ts","unpkg":"./dist/index.min.js","jsdelivr":"./dist/index.min.js","scripts":{"build":"ldesign-builder build","type-check":"vue-tsc --noEmit","lint":"eslint . --cache","lint:check":"eslint . --cache --max-warnings 0","release":"ldesign-release publish --package-dir ."},"dependencies":{"@composy/plate-editor-core":"^0.0.4","vue-demi":"^0.14.10"},"peerDependencies":{"vue":"^3.5.0"},"devDependencies":{"@antfu/eslint-config":"9.1.0","@composy/builder":"^0.0.1","eslint":"10.7.0","typescript":"5.9.3","vue":"3.5.39","vue-tsc":"^3.3.7","@composy/release":"^0.0.1"},"browser":"./dist/index.js","style":"./dist/index.css","typings":"./es/index.d.ts","_id":"@composy/plate-editor-vue@0.0.5","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-ETv1oxXjx/vgt+SScOvQ03+0nOh2gZbkdijEsVWQPSBktva6WZu8CwkvRotyhDpHFpBuG1B1rcQiuqTrN1vUkw==","shasum":"a414d56cb74bd1e9a99b5f4596464b6fdbccf7e4","tarball":"https://registry.npmjs.org/@composy/plate-editor-vue/-/plate-editor-vue-0.0.5.tgz","fileCount":57,"unpackedSize":332397,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEbhIcZ6cQVkGVHd80FK036jTaAPVu/SbRZtQRMz6KHzAiEAm4UX9T+Upz1kxTfDBEKAihT3J3YWivaGLYkPw4tdHYY="}]},"_npmUser":{"name":"swimly","email":"979741120@qq.com"},"directories":{},"maintainers":[{"name":"swimly","email":"979741120@qq.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/plate-editor-vue_0.0.5_1787727524121_0.00015007716764414702"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-17T06:47:10.745Z","modified":"2026-08-26T06:58:44.417Z","0.0.2":"2026-08-17T06:47:11.211Z","0.0.3":"2026-08-19T02:09:00.286Z","0.0.4":"2026-08-25T13:28:18.796Z","0.0.5":"2026-08-26T06:58:44.245Z"},"license":"MIT","keywords":["plate-editor","license-plate","vue","canvas"],"description":"Vue 3 adapter for @composy/plate-editor-core","maintainers":[{"name":"swimly","email":"979741120@qq.com"}],"readme":"# @composy/plate-editor-vue\n\n`@composy/plate-editor-vue` 是 `@composy/plate-editor-core` 的 Vue 3 适配包，提供完整的 `<PlateEditor />` 组件、Props、事件转发和模板 `ref` API。\n\nVue 包负责 Vue 生命周期和响应式入口；Core 负责 Canvas 编辑、图片加载、检测器、透视渲染、工具栏、预设、历史、状态和导出。需要框架无关 API 时请直接使用 [Core 完整文档](../core/README.md)。\n\n组件内部创建的是 Core `PlateEditorStudio`，因此 `mode=\"dialog\"` 的编辑器弹窗和预设弹窗会挂载到 `document.body` 下的 portal，并使用高层级固定定位，不会被业务容器的 `overflow`、`transform`、`isolation` 或局部 stacking context 截断。\n\n## 目录\n\n- [安装与环境](#安装与环境)\n- [产物、CSS 与导入](#产物css-与导入)\n- [最小示例](#最小示例)\n- [完整 Vue 示例](#完整-vue-示例)\n- [inline 与 dialog](#inline-与-dialog)\n- [自定义触发按钮和弹窗](#自定义触发按钮和弹窗)\n- [保存、上传和导出](#保存上传和导出)\n- [Props 完整说明](#props-完整说明)\n- [options：Core 参数透传](#optionscore-参数透传)\n- [toolbar：工具栏定制](#toolbar工具栏定制)\n- [全部组件事件](#全部组件事件)\n- [ref 暴露 API](#ref-暴露-api)\n- [响应式更新边界](#响应式更新边界)\n- [图片、模型、检测和预设](#图片模型检测和预设)\n- [类型和数据结构](#类型和数据结构)\n- [样式产物](#样式产物)\n- [UMD 浏览器用法](#umd-浏览器用法)\n- [CommonJS 用法](#commonjs-用法)\n- [示例和 Playground](#示例和-playground)\n- [构建和销毁](#构建和销毁)\n\n## 安装与环境\n\n```bash\npnpm add vue @composy/plate-editor-vue\n```\n\n`vue` 是 peer dependency，需要由应用提供。使用 Core 内置 ONNX 检测器时，再安装：\n\n```bash\npnpm add onnxruntime-web\n```\n\n如果应用只使用手动四角选区、自定义检测器或远程检测器，则不需要安装 `onnxruntime-web`。\n\n组件运行在浏览器，需要 DOM、Canvas、Blob/File 和 URL API。服务端渲染时不要在服务端创建编辑器；在 Nuxt 等 SSR 应用中使用 client-only 包装，或仅在客户端挂载组件。\n\n## 产物、CSS 与导入\n\n普通 Vue 应用优先从包根导入，npm `exports` 会自动选择正确的运行时产物：\n\n| 目录或文件 | 类型 | 用法 |\n| --- | --- | --- |\n| `es/` | preserve-modules ESM JavaScript + 声明 | 包根 `import` 默认解析到这里，适合 Vite、Webpack、Rollup |\n| `esm/` | 独立 preserve-modules ESM JavaScript + 声明 | `import ... from '@composy/plate-editor-vue/esm'` |\n| `lib/` | preserve-modules CommonJS JavaScript + 声明 | `require('@composy/plate-editor-vue')` |\n| `dist/index.js` | 未压缩 UMD JavaScript | 静态页面调试 |\n| `dist/index.min.js` | 压缩 UMD JavaScript | CDN 或生产 `<script>` |\n| `dist/index.css` | 未压缩 Vue 宿主样式 | 调试或开发环境 |\n| `dist/index.min.css` | 压缩 Vue 宿主样式 | 生产环境 |\n| `es/style/index.css` | ESM 样式副本 | 需要 preserve-modules 样式路径时使用 |\n| `lib/style/index.css` | CommonJS 样式副本 | 需要 CommonJS 样式路径时使用 |\n| `*.d.ts` | TypeScript 声明 | Props、事件、ref、模型和状态类型 |\n\n### 包根 ESM 导入\n\n```ts\nimport { PlateEditor } from '@composy/plate-editor-vue'\nimport '@composy/plate-editor-vue/index.css'\n```\n\n### 显式使用 `esm/`\n\n```ts\nimport {\n  PlateEditor,\n  BUILT_IN_PLATE_PRESETS,\n} from '@composy/plate-editor-vue/esm'\n```\n\n`esm/` 现在包含真实的 `.js` 运行时模块，不是只有声明文件的目录。普通项目仍建议使用包根入口，以便未来版本调整构建目录时保持兼容。\n\n### CommonJS\n\n```js\nconst {\n  PlateEditor,\n  BUILT_IN_PLATE_PRESETS,\n} = require('@composy/plate-editor-vue')\n```\n\nCommonJS 适配层仍然需要 Vue 和浏览器 DOM，不能在没有 DOM 的 Node 进程中直接挂载组件。\n\nCore 的 Canvas 和 Studio 样式由 Core 自动注入；Vue 样式主要负责 wrapper、trigger 和 host 的基础布局。生产环境可以导入压缩 CSS：\n\n```ts\nimport '@composy/plate-editor-vue/index.min.css'\n```\n\n也可以使用 preserve-modules 样式入口：\n\n```ts\nimport '@composy/plate-editor-vue/style/index.css'\n```\n\n## 最小示例\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { PlateEditor } from '@composy/plate-editor-vue'\nimport '@composy/plate-editor-vue/index.css'\n\nconst image = ref<File>()\n\nfunction onFileChange(event: Event) {\n  image.value = (event.target as HTMLInputElement).files?.[0]\n}\n</script>\n\n<template>\n  <input type=\"file\" accept=\"image/jpeg,image/png,image/webp\" @change=\"onFileChange\" />\n  <PlateEditor :src=\"image\" :auto-detect=\"false\" height=\"640px\" />\n</template>\n```\n\n`src` 变化时，新值会调用 Core `loadImage()`；变为 `undefined` 时调用 `clearImage()`。图片错误会触发 `image-error`，同时触发兼容别名 `error`。\n\n## 完整 Vue 示例\n\n下面的示例包含模型配置、预设、图片加载、检测、事件、组件 `ref` 和导出：\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport {\n  BUILT_IN_PLATE_PRESETS,\n  PlateEditor,\n} from '@composy/plate-editor-vue'\nimport type {\n  Editor,\n  ModelOptions,\n  PlateEditorExpose,\n} from '@composy/plate-editor-vue'\nimport '@composy/plate-editor-vue/index.css'\n\nconst editorRef = ref<PlateEditorExpose>()\nconst image = ref<File | string>()\nconst status = ref('等待图片')\n\nconst model: ModelOptions = {\n  url: '/models/license-plate-detector.onnx',\n  outputFormat: 'corners',\n  coordinateFormat: 'normalized',\n  preferredBackends: ['webgpu', 'wasm'],\n}\n\nfunction onImageChange(event: Event) {\n  image.value = (event.target as HTMLInputElement).files?.[0]\n}\n\nfunction onReady(editor: Editor) {\n  editor.setAddMode(true)\n  status.value = '编辑器已就绪'\n}\n\nasync function detect() {\n  status.value = '识别中…'\n  const plates = await editorRef.value?.detectPlates({ precise: true })\n  status.value = `识别到 ${plates?.length ?? 0} 个车牌`\n}\n\nasync function exportImage() {\n  const blob = await editorRef.value?.export({\n    quality: 0.8,\n    useOriginalResolution: true,\n  })\n  if (!blob) return\n\n  const url = URL.createObjectURL(blob)\n  const link = document.createElement('a')\n  link.href = url\n  link.download = 'plate-edited.png'\n  link.click()\n  URL.revokeObjectURL(url)\n}\n</script>\n\n<template>\n  <section class=\"plate-page\">\n    <input type=\"file\" accept=\"image/*\" @change=\"onImageChange\" />\n    <button type=\"button\" @click=\"detect\">开始识别</button>\n    <button type=\"button\" @click=\"exportImage\">导出 PNG</button>\n    <span>{{ status }}</span>\n\n    <PlateEditor\n      ref=\"editorRef\"\n      :src=\"image\"\n      :options=\"{\n        model,\n        presets: BUILT_IN_PLATE_PRESETS,\n        showBuiltInPresets: true,\n        defaultPresetId: 'cn-blue-standard',\n      }\"\n      :auto-detect=\"false\"\n      theme=\"auto\"\n      @ready=\"onReady\"\n      @image-loaded=\"payload => status = `${payload.width} × ${payload.height}`\"\n      @detect-complete=\"payload => status = `完成：${payload.plates.length} 个候选`\"\n      @error=\"error => status = error.message\"\n    />\n  </section>\n</template>\n```\n\n如果你的应用不需要自动识别，可以删除 `model` 和 `@detect-complete`，保留 `auto-detect=\"false\"`，使用手动选区即可。\n\n## inline 与 dialog\n\n`PlateEditor` 通过 `mode` 支持两种完整交互：\n\n### 直接展示编辑面板\n\n这是默认模式，组件直接渲染工作区、工具栏以及工具栏下方的“保存图片”按钮：\n\n```vue\n  <PlateEditor\n    :src=\"image\"\n  mode=\"inline\"\n  :options=\"{ presets: customPresets, showBuiltInPresets: false }\"\n  :auto-detect=\"false\"\n  theme=\"light\"\n/>\n```\n\n### 选择图片后弹出对话框\n\n`dialog` 模式初始只显示一个选择图片按钮。用户选中图片后，组件自动打开编辑对话框；对话框右下角的保存按钮会调用 Core 的 `save()`：\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { PlateEditor } from '@composy/plate-editor-vue'\nimport type { PlateEditorExpose } from '@composy/plate-editor-vue'\n\nconst editorRef = ref<PlateEditorExpose>()\nconst customPresets = [\n  {\n    id: 'gochi-white',\n    name: 'GOCHI 白牌',\n    style: {\n      contentMode: 'image',\n      image: { source: '/assets/gochi-logo.png', fit: 'contain' },\n      backgroundColor: '#ffffff',\n      text: '',\n      textColor: '#1f2933',\n      aspectRatio: 440 / 140,\n      mountingBolts: { enabled: true, size: 4.5, inset: 16 },\n    },\n  },\n]\n\nasync function save() {\n  await editorRef.value?.save('vehicle-plate', {\n    type: 'image/png',\n    useOriginalResolution: true,\n  })\n}\n</script>\n\n<template>\n  <PlateEditor\n    ref=\"editorRef\"\n    mode=\"dialog\"\n    :options=\"{\n      presets: customPresets,\n      showBuiltInPresets: false,\n      defaultPresetId: 'gochi-white',\n    }\"\n    :auto-detect=\"false\"\n    :export-options=\"{ quality: 0.8 }\"\n  />\n  <button type=\"button\" @click=\"save\">保存当前图片</button>\n</template>\n```\n\n`dialog` 模式下也可以通过 `editorRef.value.loadImage(file)` 主动打开弹窗；关闭弹窗只隐藏界面，不会清除当前编辑状态。`inline` 与 `dialog` 都提供底部保存按钮，业务需要自定义保存位置时，可以调用 `ref` 暴露的 `save()`、`export()` 或 `download()`。\n\nDialog 模式下的编辑器弹窗和预设弹窗会直接挂到 `document.body` 的独立 portal，使用高层级固定定位。业务容器不需要额外设置 z-index；组件卸载时会移除 portal。\n\n### 自定义保存操作\n\n如果保存结果需要上传到业务接口，可以通过 `saveHandler` Prop 接管组件内置保存按钮和 `ref.save()`：\n\n```vue\n<script setup lang=\"ts\">\nimport { PlateEditor } from '@composy/plate-editor-vue'\nimport type { PlateEditorSaveContext } from '@composy/plate-editor-vue'\n\nconst saveHandler = async ({ blob, filename, options, download }: PlateEditorSaveContext) => {\n  const body = new FormData()\n  body.append('file', blob, `${filename}.${options.type === 'image/jpeg' ? 'jpg' : 'png'}`)\n  await fetch('/api/plate-images', { method: 'POST', body })\n  // 不调用 download() 就不会触发浏览器下载；需要双重留存时再调用它。\n  void download\n}\n</script>\n\n<template>\n  <PlateEditor :save-handler=\"saveHandler\" :auto-detect=\"false\" />\n</template>\n```\n\n保存回调拿到的是导出结果 `Blob`；发生编辑或显式指定导出参数时，它是完成透视合成后的编码结果。未指定 `type` 时会沿用最近导入图片的格式；需要固定格式时可在 `ref.export()` / `ref.download()` / `ref.save()` 的第二个参数中传入 `{ type: 'image/webp', quality: 0.75 }`，或通过 `export-options` 配置 Studio 默认值。若直接导入 `File`/`Blob` 且没有任何编辑或导出参数，组件会复用原始 Blob，避免保存后体积无故增大。保存成功后触发 `save-complete`，回调抛错时触发 `save-error` 并继续向调用方抛出异常；未传 `saveHandler` 时保持默认浏览器下载。\n\n### 使用业务自定义触发按钮\n\n在 `dialog` 模式下，可以使用具名 `trigger` 插槽替换内置启动卡片。插槽按钮会被 Core 绑定点击事件，选图后自动打开编辑对话框：\n\n```vue\n<template>\n  <PlateEditor\n    mode=\"dialog\"\n    :options=\"{ presets: customPresets, showBuiltInPresets: false }\"\n    :auto-detect=\"false\"\n  >\n    <template #trigger>\n      <button type=\"button\" class=\"choose-vehicle-button\">选择车辆图片并编辑</button>\n    </template>\n  </PlateEditor>\n</template>\n```\n\n也可以把外部已有元素的 `HTMLElement` 或 CSS 选择器传给 `trigger` Prop：\n\n```vue\n<button id=\"choose-vehicle\" type=\"button\">选择图片</button>\n<PlateEditor mode=\"dialog\" trigger=\"#choose-vehicle\" :auto-detect=\"false\" />\n```\n\n通过 `ref` 调用 `openPicker()` 可以从快捷键或业务流程主动打开系统文件选择器。自定义触发按钮只替换入口，不会影响对话框内的工具栏和保存按钮。\n\n### 自定义工具栏按钮与 Lucide 图标\n\n`toolbar` 是 Vue 组件提供的顶层 Prop，会原样传给 Core Studio。内置工具栏使用框架无关的 Lucide SVG；`icon` 可以写内置名称，也可以写 SVG body 或完整 `<svg>`。`visible: false` 隐藏按钮，`label` 同时影响无障碍名称和悬浮提示，`attributes` 可写入原生 HTML 属性。工具栏的 `compression` 按钮提供 100%、92%、80%、60%、40% 五档质量选择，也可隐藏：\n\n```vue\n<script setup lang=\"ts\">\nconst toolbar = {\n  buttons: {\n    upload: { label: '选择车辆图片', icon: 'image-plus' },\n    detect: { label: '重新识别', icon: 'scan-search' },\n    manual: {\n      label: '矩形框选车牌',\n      icon: '<path d=\"M5 4h14v16H5z\"/><path d=\"M8 8h8M8 12h5\" />',\n    },\n    'manual-corners': { label: '四角点选车牌', icon: 'scan-line' },\n    pan: { label: '移动画布', icon: 'hand' },\n    compression: { label: '选择图片压缩质量', attributes: { 'data-role': 'compression' } },\n    compare: { visible: false },\n  },\n} as const\n</script>\n\n<template>\n  <PlateEditor\n    :toolbar=\"toolbar\"\n    :options=\"{ presets: customPresets, showBuiltInPresets: false }\"\n    :auto-detect=\"false\"\n  />\n</template>\n```\n\n如果需要替换整个按钮，可以在 `toolbar.buttons.<id>.element` 中传入业务创建的 `HTMLButtonElement`；如果只需要改变行为，传入 `onClick` 即可。`save` 也是可配置按钮：`inline` 模式会把它放在工具栏最右侧，`dialog` 模式会把它放在编辑对话框右下角。默认动作可以通过 `ref` 的 `save()` 复用：\n\n```ts\nconst saveButton = document.createElement('button')\nsaveButton.type = 'button'\nsaveButton.textContent = '保存到业务系统'\n\nconst toolbar = {\n  buttons: {\n    save: {\n      element: saveButton,\n      onClick: () => void editorRef.value?.save('vehicle-plate'),\n    },\n  },\n}\n```\n\nCore 的 `handleSize` 与 `edgeHandleSize` 放在 `options` 中配置。选中车牌后会显示四个小角点和四个边中点手柄：上/下边调整高度，左/右边调整宽度，结果会限制在原图范围内，并支持撤销/重做：\n\n```vue\n<PlateEditor\n  :options=\"{\n    handleSize: 6,\n    edgeHandleSize: 6,\n    presets: customPresets,\n    showBuiltInPresets: false,\n  }\"\n  :auto-detect=\"false\"\n/>\n```\n\n## Props 完整说明\n\n| Prop | 类型 | 默认值 | 说明 |\n| --- | --- | --- | --- |\n| `src` | `PlateSource` | 无 | 初始图片，支持 URL、File、Blob、ArrayBuffer、HTMLImageElement 和 ImageBitmap |\n| `options` | `Omit<PlateEditorOptions, 'container' \\| 'image' \\| 'autoDetect'>` | `{}` | 透传 Core 编辑器配置；`container`、`image`、`autoDetect` 由 Vue 顶层 Props 接管 |\n| `readonly` | `boolean` | `undefined` | 是否只读；未设置时继承 `options.readonly` |\n| `autoDetect` | `boolean` | `true` | 图片加载完成后是否自动识别；变化时同步到当前 Studio。仅在 `options` 里配置了 ONNX 模型或自定义 `detector` 时才会真正自动识别，否则跳过 |\n| `theme` | `'light' \\| 'dark' \\| 'auto'` | `'auto'` | 工具栏主题 |\n| `primaryColor` | `string` | `undefined` | 工具栏主色；变化时同步，传 `undefined` 恢复默认主题色 |\n| `mode` | `'inline' \\| 'dialog'` | `'inline'` | 直接展示编辑器，或选择图片后打开对话框 |\n| `trigger` | `HTMLElement \\| string` | `undefined` | dialog 模式复用已有触发元素；也可使用 `#trigger` 插槽；创建时读取 |\n| `toolbar` | `PlateEditorToolbarOptions` | `{}` | 自定义工具栏按钮；创建时传给 Studio；`compression` 可隐藏质量选择 |\n| `exportOptions` | `PlateExportOptions` | `{}` | Studio 工具栏和无参数导出/保存的默认格式、质量、尺寸与底色 |\n| `saveHandler` | `PlateEditorSaveHandler` | `undefined` | 接管保存按钮和 `save()` 的 Blob 处理；ref 的单次 handler 优先级更高 |\n| `height` | `string \\| number` | `'min(760px, 78vh)'` | Vue 宿主容器高度；数字会转换为像素 |\n\n如果需要同时配置多个 Core 参数，推荐使用 `options`：\n\n```vue\n<PlateEditor\n  :options=\"{\n    maxPlates: 2,\n    minimumDetectionConfidence: 0.5,\n  }\"\n  :src=\"image\"\n/>\n```\n\n组件会将 Vue Props 转换为 Core 配置。顶层 `src`、`autoDetect`、`theme` 和 `primaryColor` 会建立 watcher；其它大多数 Core 初始化参数见后文的响应式边界。\n\n`readonly` 的优先级是：显式传入的 `readonly` Prop > `options.readonly` > Core 默认值。`height` 为数字时转换为 `${value}px`；dialog 模式使用 trigger 插槽或 trigger Prop 时，页面流中的 host 高度会自动变为 `auto`。\n\n## `options`：Core 参数透传\n\n`options` 类型是 `Omit<PlateEditorOptions, 'container' | 'image' | 'autoDetect'>`，可传以下 Core 参数：\n\n| 分组 | 参数 |\n| --- | --- |\n| 检测器 | `detector`、`model`、`useHeuristicFallback`、`minimumDetectionConfidence`、`maxPlates` |\n| 编辑行为 | `readonly`、`historyLimit`、`handleSize`、`edgeHandleSize` |\n| 图片限制 | `maxFileSize`、`maxImageDimension`、`maxPixels`、`crossOrigin`、`background` |\n| 样式与预设 | `initialStyle`、`showBuiltInPresets`、`presets`、`defaultPresetId` |\n| 元数据 | `locale` |\n\n```vue\n<PlateEditor\n  :options=\"{\n    model: {\n      url: '/models/license-plate-detector.onnx',\n      preferredBackends: ['webgpu', 'wasm'],\n      outputFormat: 'corners',\n      coordinateFormat: 'normalized',\n    },\n    maxPlates: 2,\n    minimumDetectionConfidence: 0.5,\n    maxFileSize: 10 * 1024 * 1024,\n    maxImageDimension: 8000,\n    maxPixels: 40_000_000,\n    showBuiltInPresets: true,\n  }\"\n  :src=\"image\"\n  :auto-detect=\"false\"\n/>\n```\n\nCore 参数的默认值、嵌套类型和模型字段详见 [Core 构造参数](../core/README.md#全部构造参数) 和 [Core ONNX 参数](../core/README.md#onnx-参数完整说明)。\n\n## `toolbar`：工具栏定制\n\n按钮 ID：`upload`、`detect`、`manual`、`manual-corners`、`pan`、`presets`、`zoom-out`、`zoom-in`、`fit`、`undo`、`redo`、`compare`、`compression`、`download`、`theme`、`save`。\n\n```ts\nconst toolbar = {\n  buttons: {\n    upload: { label: '选择车辆图片', icon: 'image-plus' },\n    detect: { label: '重新识别车牌', icon: 'scan-search' },\n    manual: {\n      label: '矩形框选车牌',\n      icon: '<path d=\"M5 4h14v16H5z\"/><path d=\"M8 8h8M8 12h5\"/>',\n    },\n    'manual-corners': { label: '四角点选车牌', icon: 'scan-line' },\n    pan: { label: '移动画布', icon: 'hand' },\n    compare: { visible: false },\n  },\n} as const\n```\n\n| 字段 | 类型 | 说明 |\n| --- | --- | --- |\n| `visible` | `boolean` | `false` 隐藏按钮 |\n| `label` | `string` | 同时影响无障碍名称、title 和 tooltip |\n| `icon` | `string` | 内置 Lucide 名称、SVG body 或完整 SVG |\n| `element` | `HTMLButtonElement` | 替换默认原生按钮 |\n| `onClick` | `(event: MouseEvent) => void` | 替换默认动作 |\n| `attributes` | `Record<string, string \\| number \\| boolean \\| null \\| undefined>` | 写入原生 HTML 属性；`false`/空值会移除属性 |\n\n```vue\n<PlateEditor :toolbar=\"toolbar\" :auto-detect=\"false\" />\n```\n\n## 全部组件事件\n\n| 事件 | 载荷 | 触发时机 |\n| --- | --- | --- |\n| `ready` | `Editor` | Core 编辑器实例创建完成 |\n| `lifecycle-ready` | `{ width, height }` | 初始布局和历史状态完成 |\n| `before-destroy` | 无 | Core 销毁开始前 |\n| `lifecycle-destroy` | 无 | Core 销毁完成 |\n| `image-load-start` | `{ source }` | 开始加载图片 |\n| `image-loaded` | `{ width, height }` | 图片加载完成 |\n| `image-error` | `{ error }` | 图片加载失败 |\n| `image-clear` | 无 | 清空当前图片 |\n| `model-ready` | `{ backend }` | 模型初始化完成 |\n| `model-error` | `{ error }` | 模型初始化失败 |\n| `detect-start` | `{ precise }` | 开始识别 |\n| `detect-complete` | `{ plates, precise }` | 检测完成 |\n| `detect-error` | `{ error }` | 检测失败 |\n| `interaction-start` | `{ type, plateId? }` | 开始角点、边、图层移动、平移或手动框选 |\n| `interaction-end` | `{ type, plateId?, changed }` | 交互结束；changed 表示是否真的改变 |\n| `viewport-change` | `{ fitScale, scale, zoom, x, y }` | 视口发生变化 |\n| `plate-add` | `{ plate }` | 图层新增 |\n| `plate-select` | `{ plate: Plate \\| null }` | 图层选择变化 |\n| `plate-update` | `{ plate }` | 图层更新 |\n| `plate-remove` | `{ plateId }` | 图层删除 |\n| `preset-change` | `{ presets, defaultPresetId }` | 预设目录或默认预设变化 |\n| `history-change` | `{ canUndo, canRedo }` | 历史栈变化 |\n| `render-start` / `render-complete` | `{ width, height }` | 预览画布开始 / 完成绘制 |\n| `export-start` | `{ options }` | 开始编码导出图片 |\n| `export-complete` | `{ blob, options }` | 导出完成 |\n| `export-error` | `{ error }` | 导出失败 |\n| `save-start` | `{ filename, options }` | 开始执行保存 |\n| `save-complete` | `{ blob, filename, options, download }` | 自定义保存回调完成或默认下载完成 |\n| `save-error` | `{ error, filename, options }` | 保存失败 |\n| `select` | `Plate \\| null` | 当前车牌图层变化 |\n| `change` | `{ plates, selectedPlateId }` | 任意可持久化编辑状态变化 |\n| `export` | `{ blob, options }` | 导出完成 |\n| `error` | `Error` | `image-error` 和 `detect-error` 的错误兼容别名 |\n| `dialog-open` / `dialog-close` | `{ type: 'editor' \\| 'presets' }` | 编辑对话框或预设弹窗打开 / 关闭 |\n| `destroy` | 无 | 组件销毁 |\n\n除 `ready`、`select`、`export` 和 `error` 这几个兼容性别名外，Vue 事件名称是 Core 事件名的 kebab-case 映射。例如 Core 的 `plate:update` 对应 `@plate-update`，`save:complete` 对应 `@save-complete`。所有事件都可以直接在模板上监听，也可以通过 `getEditor()` / `getStudio()` 使用 Core 的 `on/off`。\n\nCore 的冒号事件映射为 kebab-case，例如 `plate:update` 对应 `@plate-update`，`save:complete` 对应 `@save-complete`。`ready`、`select`、`export`、`error` 是 Vue 额外提供的语义别名。\n\n```vue\n<PlateEditor\n  @image-error=\"({ error }) => console.error(error)\"\n  @detect-complete=\"({ plates }) => console.log(plates)\"\n  @plate-update=\"({ plate }) => console.log(plate.id)\"\n  @save-complete=\"context => console.log(context.blob.size)\"\n/>\n```\n\n## ref 暴露 API\n\n```ts\nconst editorRef = ref<PlateEditorExpose>()\n\nconst editor = editorRef.value?.getEditor()\nconst studio = editorRef.value?.getStudio()\n\nawait editorRef.value?.loadImage(file)\nawait editorRef.value?.detectPlates({ precise: true })\nconst blob = await editorRef.value?.export({ type: 'image/png' })\n```\n\n### 实例访问\n\n| 方法 | 说明 |\n| --- | --- |\n| `getEditor()` | 获取底层 Core `Editor` |\n| `getStudio()` | 获取 `PlateEditorStudio` |\n| `editor` | 当前 Core 编辑器引用 |\n| `studio` | 当前 Studio 引用 |\n\n### 图片、模型和检测\n\n| 方法 | 说明 |\n| --- | --- |\n| `loadImage(source)` | 加载图片 |\n| `preload()` | 预加载模型和检测器 |\n| `setModel(model)` | `setModel(model: ModelOptions & { url: string }): Promise<void>`；切换 ONNX 模型 |\n| `detectPlates(options?)` | `detectPlates(options?: { precise?: boolean; replace?: boolean }): Promise<Plate[]>`；执行识别 |\n\n### 主题和视口\n\n| 方法 | 说明 |\n| --- | --- |\n| `setTheme(theme)` | 设置 `light`、`dark` 或 `auto` |\n| `setPrimaryColor(color?)` | 设置主色，传 `undefined` 可恢复全局主题色 |\n| `setZoom(zoom)` | `setZoom(zoom: number): void`；设置相对适配视口的缩放倍数 |\n| `zoomBy(factor)` | `zoomBy(factor: number): void`；按倍数放大或缩小 |\n| `resetViewport()` | `resetViewport(): void`；恢复适配视口 |\n| `getViewport()` | `getViewport(): PlateEditorViewport \\| undefined`；获取当前视口状态 |\n\n### 图层图片、预设、状态和导出\n\n| 方法 | 说明 |\n| --- | --- |\n| `setAddMode(mode)` | `setAddMode(true \\| 'drag' \\| 'corners' \\| false)`；选择拖拽矩形框选、依次点击四角，或取消添加 |\n| `addPlate(corners)` | `addPlate(corners: Plate['corners']): Plate \\| undefined`；使用四个角点手动新增图层 |\n| `setPlateImage(id, source, options?)` | `setPlateImage(id, source, { fit?, opacity?, contentMode? }): Promise<Plate \\| null \\| undefined>`；设置图层图片 |\n| `clearPlateImage(id)` | `clearPlateImage(id: string): Plate \\| null \\| undefined`；清除图层图片 |\n| `serialize()` / `restore(state)` | `serialize(): PlateEditorState \\| undefined`、`restore(state): Promise<void>`；保存或恢复状态 |\n| `getPresets()` | `getPresets(): PlatePreset[]`；获取当前预设副本 |\n| `registerPreset(preset)` | `registerPreset(input): PlatePreset`；注册预设 |\n| `updatePreset(id, patch)` | `updatePreset(id, input): PlatePreset \\| null`；更新预设 |\n| `removePreset(id)` | `removePreset(id: string): boolean`；删除预设 |\n| `setDefaultPreset(id)` | `setDefaultPreset(id: string \\| null): boolean`；设置或清空默认预设 |\n| `applyPreset(id, plateId?)` | `applyPreset(id: string, plateId?: string \\| null): Promise<Plate \\| null>`；应用预设 |\n| `export(options?)` | `export(options?: PlateExportOptions): Promise<Blob>`；导出 Blob |\n| `download(filename?, options?)` | `download(filename?: string, options?: PlateExportOptions): Promise<void>`；导出并触发浏览器下载 |\n| `save(filename?, options?, handler?)` | `save(filename?, options?, handler?): Promise<void>`；单次 handler 优先于 Prop 的 `saveHandler` |\n| `openPicker()` | `openPicker(): void`；主动打开文件选择器，适合自定义按钮、快捷键和外部表单流程 |\n\n`PlateEditorExpose` 提供 `setAddMode(true | 'drag' | 'corners' | false)` 便于直接切换两种添加方式。需要撤销重做、修改已有图层或访问更底层能力时，先通过 `getEditor()` 获取 Core `Editor`，再调用 [Core API](../core/README.md#全部实例-api)。\n\n内置 Studio 工具栏会将矩形框选与四角点选呈现为一个互斥的“添加方式”按钮选择器，而不是两个同时激活的按钮。点击当前方式后，选项面板会从 toolbar 上方向上滑出，并在每个选项前显示对应图标。\n\n通过 `getStudio()` 可以访问 Studio 的 `on/off` 事件代理、`autoDetectionEnabled`、`setAutoDetect()` 等 Core 未在 Vue expose 中重复包装的能力；通过 `getEditor()` 可以使用 `undo()`、`redo()`、`setDetector()`、`detectWithLargeModel()`、`updatePlate()`、`updatePlateStyle()`、`removePlate()`、`selectPlate()`、`copyStyleToAll()`、`setAddMode()` 和 `setComparison()` 等底层 API。\n\n例如：\n\n```ts\nconst editor = editorRef.value?.getEditor()\neditor?.setAddMode(true)\neditor?.undo()\neditor?.updatePlateStyle(plateId, { text: '粤A·88888' })\n\nconst studio = editorRef.value?.getStudio()\nstudio?.setAutoDetect(false)\nstudio?.on('dialog:open', payload => console.log(payload.type))\n```\n\n### 在模板中调用 ref\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport type { PlateEditorExpose } from '@composy/plate-editor-vue'\n\nconst editorRef = ref<PlateEditorExpose>()\n\nasync function save() {\n  await editorRef.value?.download('plate-result', {\n    type: 'image/jpeg',\n    quality: 0.92,\n  })\n}\n</script>\n\n<template>\n  <PlateEditor ref=\"editorRef\" :auto-detect=\"false\" />\n  <button type=\"button\" @click=\"save\">保存</button>\n</template>\n```\n\n## 响应式更新边界\n\n组件创建时会把 Props 合并为一次 `PlateEditorStudio` 配置。以下 Props 在组件已经挂载后仍会同步到当前实例：\n\n| Prop | 运行时行为 |\n| --- | --- |\n| `src` | 新值调用 `loadImage()`；变为 `undefined` 时调用 `clearImage()` |\n| `theme` | 调用 `studio.setTheme()` |\n| `primaryColor` | 调用 `studio.setPrimaryColor()`；传 `undefined` 恢复全局颜色 |\n| `autoDetect` | 调用 `studio.setAutoDetect()` |\n\n`options`、`readonly`、`mode`、`trigger`、`toolbar` 和 `saveHandler` 主要是创建时配置。改变这些值不会自动重建 Studio，也不会替换已经创建的 DOM 按钮或保存回调；如果需要切换它们，建议使用 `v-if` 销毁后重新挂载，或直接通过 `getStudio()` / `getEditor()` 调用对应运行时 API。`height` 会参与宿主容器样式计算，但不会改变 Studio 的初始化模式。\n\n组件尚未 `mounted` 或已经 `unmounted` 时，ref 方法可能返回 `undefined`、`null`、空数组或直接等待一个未创建实例；需要强制保证编辑器就绪时，应在 `ready` 或 `lifecycle-ready` 事件之后调用。\n\n## 图片、模型、检测和预设\n\nVue 组件不会内置模型文件。Vite 项目可以将模型放在：\n\n```text\npublic/models/license-plate-detector.onnx\n```\n\n然后通过 `options` 配置模型：\n\n```vue\n<PlateEditor\n  :options=\"{\n    model: {\n      url: '/models/license-plate-detector.onnx',\n      outputFormat: 'corners',\n      coordinateFormat: 'normalized',\n      preferredBackends: ['webgpu', 'wasm'],\n    },\n  }\"\n  :src=\"image\"\n  :auto-detect=\"false\"\n  @detect-complete=\"({ plates }) => console.log(plates)\"\n/>\n```\n\n预设默认完全由业务配置，`showBuiltInPresets` 默认是 `false`。需要内置样式时显式传入 `showBuiltInPresets: true`，也可以只传一个或多个自定义 `presets`；只有一个自定义预设时它会自动成为默认样式，多个预设可以通过 `defaultPresetId` 指定默认项。四颗安装螺帽是可选细节，默认不显示，仅在预设里显式设置 `style.mountingBolts: { enabled: true }` 时才绘制；文字、图片和混合内容的详细选项请查看 [Core 车牌内容文档](../core/README.md#文字图片和混合内容)。\n\n### 内置 ONNX 检测器\n\n`options.model` 支持 `url`、后端顺序、输入输出布局、坐标格式、阈值、WASM 路径和候选校验等参数。模型必须能被浏览器通过 `fetch` 读取；跨域模型服务器需要正确的 CORS 响应。完整字段和默认值见 [Core ONNX 参数](../core/README.md#onnx-参数完整说明)。\n\n```ts\nconst options = {\n  model: {\n    url: '/models/license-plate-detector.onnx',\n    preferredBackends: ['webgpu', 'wasm'],\n    scoreThreshold: 0.35,\n    nmsThreshold: 0.45,\n    inputWidth: 640,\n    inputHeight: 640,\n    inputLayout: 'nchw',\n    channelOrder: 'rgb',\n    normalization: 'zero-one',\n    outputFormat: 'corners',\n    coordinateFormat: 'normalized',\n    verifyCandidates: true,\n    heuristicFallback: true,\n  },\n}\n```\n\n### 远程大模型检测\n\n远程大模型不是 `options.model` 的替代品，而是通过 Core `Editor` 的显式方法按需执行。Vue ref 没有复制这个低频底层方法，可在 `ready` 后通过 `getEditor()` 调用：\n\n```ts\nconst editor = editorRef.value?.getEditor()\nconst plates = await editor?.detectWithLargeModel({\n  endpoint: 'https://api.example.com/v1/chat/completions',\n  model: 'vision-model',\n  apiKey: runtimeApiKey,\n  coordinateFormat: 'auto',\n  timeout: 30_000,\n  imageQuality: 0.9,\n}, { replace: true })\n```\n\n`endpoint`、`model` 是必填项；`apiKey`、自定义 `headers`、`prompt`、`coordinateFormat`、`maxImageSide`、`imageMimeType`、`imageQuality`、`timeout`、`maxTokens`、`temperature` 和 `extraBody` 都属于运行时请求参数。不要把 API key 写入源代码、Props 默认值或 `serialize()` 的持久化状态。远程服务应返回能被 Core 解析为车牌四角点的结构，并自行处理鉴权、限流和隐私合规。\n\n### 自定义检测器\n\n需要接入业务服务或其它推理引擎时，可通过 `options.detector` 传入 `PlateEditorDetector`。检测器接收当前图片的 Canvas 上下文，返回图片坐标系中的四角点：\n\n```ts\nimport type { PlateEditorDetector } from '@composy/plate-editor-vue'\n\nconst detector: PlateEditorDetector = {\n  backend: 'custom',\n  async preload() {\n    // 加载业务模型或建立连接\n  },\n  async detect({ image, width, height }, { precise, maxResults }) {\n    const result = await requestPlateDetection({ image, width, height, precise })\n    return result.slice(0, maxResults ?? 4).map(item => ({\n      corners: item.corners,\n      confidence: item.confidence,\n      type: 'unknown',\n      source: 'detected',\n    }))\n  },\n  destroy() {\n    // 释放业务检测器资源\n  },\n}\n```\n\n`corners` 顺序为左上、右上、右下、左下；`confidence`、`cornerConfidences`、`type` 和 `source` 可选。若检测器实现了 `preload`，`ref.preload()` 会等待它完成；若实现了 `destroy`，组件卸载时 Core 会调用它。\n\n## 类型和数据结构\n\nVue 包重新导出了常用 Core 类型，推荐直接从 Vue 包导入组件相关类型：\n\n```ts\nimport type {\n  ModelOptions,\n  Plate,\n  PlateEditorExpose,\n  PlateEditorOptions,\n  PlateEditorSaveContext,\n  PlateEditorState,\n  PlateEditorViewport,\n  PlateExportOptions,\n  PlatePreset,\n  PlateSource,\n  PlateStyle,\n} from '@composy/plate-editor-vue'\n```\n\n### `PlateSource`\n\n`src`、`loadImage()` 和 `setPlateImage()` 支持 `string | Blob | File | ArrayBuffer | HTMLImageElement | ImageBitmap`。字符串可以是图片 URL、Data URL 或浏览器可访问的路径。URL 图片受 CORS 规则约束；导出、检测和 Canvas 读取时，远程图片服务器必须允许当前来源。\n\n### `Plate` 与 `PlateStyle`\n\n`Plate` 至少包含 `id`、`corners`、`type`、`source`、`confidence`、`cornerConfidences`、`visible`、`locked` 和 `style`。`corners` 是四个 `{ x, y }` 点，坐标以原图像素为单位。`style` 支持 `contentMode: 'text' | 'image' | 'both'`、背景渐变、文字排版、`image: { source, fit, opacity }`、边框、阴影、圆角、内边距和 `mountingBolts`。\n\n### `PlateEditorState` 与 `PlateEditorViewport`\n\n`serialize()` 返回包含 `version`、`width`、`height`、`plates` 和 `selectedPlateId` 的状态对象，可能带有 `imageSource`。它适合保存编辑状态，但不应当用来保存 API key 或其它凭证。`getViewport()` 返回 `fitScale`、`scale`、`zoom`、`x`、`y`；`viewport-change` 事件也会发送同样的结构。\n\n### 导出与保存类型\n\n`PlateExportOptions` 支持 `type`（PNG/JPEG/WebP）、`quality`、`useOriginalResolution`、`scale`、`maxWidth`、`maxHeight` 和 `backgroundColor`。JPEG/WebP 重新编码时默认质量为 `0.8`；`saveHandler` 接收 `PlateEditorSaveContext`：`blob` 是导出结果，`filename` 是建议文件名，`options` 是本次导出参数，`download()` 可继续调用默认浏览器下载。\n\n## 样式产物\n\n### 推荐：导入包根 CSS\n\n```ts\nimport '@composy/plate-editor-vue/index.css'\n```\n\n### 生产环境导入压缩 CSS\n\n```ts\nimport '@composy/plate-editor-vue/index.min.css'\n```\n\n### 使用 preserve-modules 样式入口\n\n```ts\nimport '@composy/plate-editor-vue/style/index.css'\n```\n\n对于 ESM，`style/index.css` 解析到 `es/style/index.css`；对于 CommonJS，解析到 `lib/style/index.css`。Core 的 Canvas 和工具栏运行时样式由 Core 自动注入，Vue CSS 主要负责 Vue 宿主容器的基础布局。\n\n## UMD 浏览器用法\n\nUMD 页面需要按顺序加载 Vue、Core UMD 和 Vue 适配包 UMD：\n\n```html\n<script src=\"./vendor/vue.global.prod.js\"></script>\n<script src=\"./core/dist/index.min.js\"></script>\n<script src=\"./vue/dist/index.min.js\"></script>\n\n<div id=\"app\"></div>\n\n<script>\n  const app = Vue.createApp({\n    components: {\n      PlateEditor: LDesignPlateEditorVue.PlateEditor,\n    },\n    template: `<PlateEditor :options=\"{ autoDetect: false }\" />`,\n  })\n\n  app.mount('#app')\n</script>\n```\n\n全局对象名称：\n\n- Core：`LDesignPlateEditor`；\n- Vue 适配包：`LDesignPlateEditorVue`。\n\nUMD 不会自动加载 Vue、模型权重、`onnxruntime-web` 或 WASM 文件。完整静态页面请参考 [Vue UMD 示例](examples/umd/index.html)。\n\n## CommonJS 用法\n\n```js\nconst { createApp } = require('vue')\nconst {\n  PlateEditor,\n  BUILT_IN_PLATE_PRESETS,\n} = require('@composy/plate-editor-vue')\n\nconst app = createApp(App)\napp.component('PlateEditor', PlateEditor)\napp.provide('platePresets', BUILT_IN_PLATE_PRESETS)\napp.mount('#app')\n```\n\nCommonJS 入口适合 CommonJS bundler。浏览器页面中仍然需要由 bundler 提供 Vue、DOM 和 CSS。\n\n## 示例和 Playground\n\n- [Vue 示例索引](examples/README.md)：SFC、ESM、CommonJS、UMD 和 CSS 产物；\n- [完整 Vue SFC](examples/basic/PlateEditorExample.vue)：模型、预设、事件、ref 和导出；\n- [CommonJS 示例](examples/cjs/main.cjs)：注册 `PlateEditor`；\n- [UMD 示例](examples/umd/index.html)：Vue 全局对象、Core UMD 和 Vue UMD 的完整加载顺序；\n- [完整 Playground](../../playground)：首页、Core/Vanilla 演示、Vue 演示、使用文档和 API 文档。\n\n## 构建和销毁\n\n```bash\npnpm --dir libraries/plate-editor/packages/core run build\npnpm --dir libraries/plate-editor/packages/vue run type-check\npnpm --dir libraries/plate-editor/packages/vue run build\n```\n\n组件卸载时会自动销毁 Core 实例。若业务绕过 Vue 组件、手动创建了 `PlateEditorStudio`，则需要自行调用 `studio.destroy()`。这可以释放事件监听、ResizeObserver、模型资源、对象 URL 和编辑器 DOM。\n","readmeFilename":"README.md"}