From 5d3046b6536e33bdca8a3654080f449e8a551b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 25 Aug 2026 12:30:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E6=89=B9=E6=B3=A8/?= =?UTF-8?q?=E5=A4=87=E6=B3=A8/=E5=AF=B9=E9=BD=90=E5=88=86=E5=B8=83/?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6=E6=89=A9=E5=85=85/video=20=E5=85=83=E7=B4=A0?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=BC=96=E8=BE=91=E5=99=A8=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1168 +++++++++++++++++++++++++ package.json | 2 + src/App.vue | 8 +- src/components/editor/AddGrid.vue | 4 +- src/components/editor/ElementView.vue | 110 ++- src/components/editor/PropsPanel.vue | 400 ++++++++- src/components/modals/ImportModal.vue | 29 +- src/core/ai.ts | 19 +- src/core/importer.ts | 28 +- src/core/op.ts | 16 + src/core/sample.ts | 6 +- src/core/store.ts | 106 ++- src/core/types.ts | 34 +- src/styles/editor.css | 109 ++- 14 files changed, 1984 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index bde9ebf..fde9714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,47 @@ "devDependencies": { "@tauri-apps/cli": "^2.11.4", "@vitejs/plugin-vue": "^5.2.1", + "@vue/test-utils": "^2.4.11", + "jsdom": "^30.0.1", "typescript": "~5.6.3", "vite": "^6.0.5", "vitest": "^4.1.11", "vue-tsc": "^2.2.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -70,6 +105,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -512,6 +700,42 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -768,6 +992,24 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", @@ -1686,6 +1928,27 @@ "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.11", + "resolved": "https://registry.npmmirror.com/@vue/test-utils/-/test-utils-2.4.11.tgz", + "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^3.0.0" + }, + "peerDependencies": { + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz", @@ -1695,6 +1958,16 @@ "node": ">=10.0.0" } }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/alien-signals": { "version": "1.0.13", "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", @@ -1702,6 +1975,32 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", @@ -1748,6 +2047,16 @@ ], "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", @@ -1774,6 +2083,47 @@ "node": ">=18" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1787,12 +2137,70 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", @@ -1800,6 +2208,13 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/dingbat-to-unicode": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", @@ -1824,6 +2239,39 @@ "underscore": "^1.13.1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", @@ -1919,6 +2367,23 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1934,6 +2399,27 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", @@ -1944,6 +2430,19 @@ "he": "bin/he" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", @@ -1956,12 +2455,129 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmmirror.com/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", @@ -1994,6 +2610,16 @@ "underscore": "^1.13.1" } }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", @@ -2039,6 +2665,13 @@ "node": ">= 20" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", @@ -2055,6 +2688,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", @@ -2087,6 +2730,22 @@ "license": "MIT", "optional": true }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz", @@ -2107,12 +2766,45 @@ "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", "license": "BSD-2-Clause" }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", @@ -2129,6 +2821,40 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", @@ -2202,6 +2928,23 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2217,6 +2960,16 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", @@ -2268,12 +3021,61 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", @@ -2281,6 +3083,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2319,6 +3134,117 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", @@ -2363,6 +3289,52 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz", @@ -2383,6 +3355,16 @@ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2582,6 +3564,13 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", + "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-tsc": { "version": "2.2.12", "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", @@ -2599,6 +3588,70 @@ "typescript": ">=5.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2616,6 +3669,114 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz", @@ -2624,6 +3785,13 @@ "engines": { "node": ">=4.0" } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 8b63491..170e712 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "devDependencies": { "@tauri-apps/cli": "^2.11.4", "@vitejs/plugin-vue": "^5.2.1", + "@vue/test-utils": "^2.4.11", + "jsdom": "^30.0.1", "typescript": "~5.6.3", "vite": "^6.0.5", "vitest": "^4.1.11", diff --git a/src/App.vue b/src/App.vue index 4136ff3..e48f82d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,6 +6,7 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue' import { store } from './core/store' import { readAsDataURL, fileToImageElement } from './core/importer' import { checkImageQuota } from './core/ai' +import AppDialog from './components/common/AppDialog.vue' import Toolbar from './components/editor/Toolbar.vue' import ThumbBar from './components/editor/ThumbBar.vue' import Canvas from './components/editor/Canvas.vue' @@ -195,7 +196,9 @@ function onKey(e: KeyboardEvent) { return } - if (e.key === 'Delete' && !inField) { + /* Delete/Backspace:批注选中时让位给 AnnotationLayer 删批注,否则删元素 */ + if ((e.key === 'Delete' || e.key === 'Backspace') && !inField) { + if (store.selectedAnnoId.value) return const sel = store.getSelection() if (sel.length > 1) { e.preventDefault(); store.delElements(sel); return } const id = store.getSelectedId() @@ -436,4 +439,7 @@ onUnmounted(() => { .json 直接导入 · 图片/文档进入资料导入 + + + diff --git a/src/components/editor/AddGrid.vue b/src/components/editor/AddGrid.vue index e85515d..1025e30 100644 --- a/src/components/editor/AddGrid.vue +++ b/src/components/editor/AddGrid.vue @@ -5,10 +5,10 @@ import type { ElementType } from '../../core/types' import { elementTypes } from '../../core/sample' -const TYPES: ElementType[] = ['title', 'text', 'list', 'stat', 'quote', 'image', 'shape', 'chart', 'card', 'table', 'code', 'formula'] +const TYPES: ElementType[] = ['title', 'text', 'list', 'stat', 'quote', 'image', 'video', 'shape', 'chart', 'card', 'table', 'code', 'formula'] const ICONS: Record = { title: 'T', text: '¶', list: '☰', stat: '#', quote: '“”', - image: '🖼', shape: '▭', chart: '📊', card: '◰', + image: '🖼', video: '🎬', shape: '▭', chart: '📊', card: '◰', table: '▦', code: '>', formula: '∑' } diff --git a/src/components/editor/ElementView.vue b/src/components/editor/ElementView.vue index d10f798..118841d 100644 --- a/src/components/editor/ElementView.vue +++ b/src/components/editor/ElementView.vue @@ -53,6 +53,8 @@ const props = defineProps<{ edit?: boolean /** 是否显示八向缩放手柄 */ showHandles?: boolean + /** 是否为缩略图端(视频等重元素降级为静态渲染) */ + thumb?: boolean }>() const emit = defineEmits<{ @@ -108,8 +110,33 @@ const shapeBg = computed(() => { return fill }) -const isCircle = computed(() => s.value.shapeType === 'circle') -const isTriangle = computed(() => s.value.shapeType === 'triangle') +/** 各形状 clip-path(rect/circle 走 CSS 圆角,不走 clip) */ +const SHAPE_CLIPS: Partial> = { + triangle: 'polygon(50% 0, 100% 100%, 0 100%)', + diamond: 'polygon(50% 0, 100% 50%, 50% 100%, 0 50%)', + pentagon: 'polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%)', + hexagon: 'polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%)', + star: 'polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)', + arrow: 'polygon(0 20%, 75% 20%, 75% 0, 100% 50%, 75% 100%, 75% 80%, 0 80%)', + chevron: 'polygon(0 0, 75% 0, 100% 50%, 75% 100%, 0 100%, 25% 50%)', + bubble: 'polygon(0 0, 100% 0, 100% 75%, 25% 75%, 10% 100%, 15% 75%, 0 75%)' +} +const shapeClip = computed(() => SHAPE_CLIPS[s.value.shapeType || 'rect'] || null) + +/** 形状文字(md 渲染;segments 存在时优先) */ +const renderedShapeText = computed(() => { + if (props.el.segments && props.el.segments.length && hasFormatting(props.el.segments)) { + return segmentsToHtml(props.el.segments) + } + return segmentsToHtml(markdownToSegments(props.el.content || '')) +}) + +/** 形状文字层样式:对齐 + 未设色时默认白(填充色底) */ +const shapeTextStyle = computed(() => { + const css: Record = { textAlign: s.value.align || 'center' } + if (!s.value.color) css.color = '#fff' + return css +}) const cardAccentColor = computed(() => resolveColor(s.value.accent, false)) @@ -119,6 +146,19 @@ const cardParts = computed(() => { return { title: lines[0] || '', body: lines.slice(1).join('\n') } }) +/** 卡片标题/正文失焦:从 DOM 读两块文字拼回 content(标题在前的兄弟节点)。 + * 标题空但正文非空时保留空行占位,避免正文首行漂移成标题。 */ +function onBlurCard(e: Event) { + const node = e.target as HTMLElement + const card = node.closest('.el-card') + if (!card) return + const title = (card.querySelector('.card-title')?.textContent || '').trim() + const body = (card.querySelector('.card-body')?.textContent || '').trim() + if (!title && !body) { emit('blur', props.el.id, 'content', ''); return } + if (body) emit('blur', props.el.id, 'content', title + '\n' + body) + else emit('blur', props.el.id, 'content', title) +} + /* ---------- Rich text(segments 结构化富文本)---------- */ /** 是否有 segments(结构化富文本),优先于 content */ const hasSegments = computed(() => { @@ -161,6 +201,7 @@ function onBlur(e: Event, field: string) { :data-id="el.id" :data-type="el.type" :data-anim="s.anim" + :data-shape="el.type === 'shape' ? (s.shapeType || 'rect') : undefined" :style="{ left: el.x + '%', top: el.y + '%', @@ -234,15 +275,54 @@ function onBlur(e: Event, field: string) { + + + + 🎬 + 属性面板选择本地视频或填入 URL + + + + + ▶ + + + + + - - - - - - + + + + {{ el.content }} + @@ -255,8 +335,18 @@ function onBlur(e: Event, field: string) { {{ s.icon }} - {{ cardParts.title }} - {{ cardParts.body }} + {{ cardParts.title }} + {{ cardParts.body }} diff --git a/src/components/editor/PropsPanel.vue b/src/components/editor/PropsPanel.vue index b3e2738..518ccee 100644 --- a/src/components/editor/PropsPanel.vue +++ b/src/components/editor/PropsPanel.vue @@ -7,9 +7,10 @@ import { store } from '../../core/store' import { elementTypes } from '../../core/sample' import { generateImage, isImageConfigured, checkImageQuota } from '../../core/ai' import { readAsDataURL } from '../../core/importer' +import { appAlert, appConfirm, appPrompt } from '../../core/dialog' import { markdownToSegments, segmentsToPlain, hasFormatting } from '../../core/richtext' import AddGrid from './AddGrid.vue' -import type { ElementType, ChartType } from '../../core/types' +import type { ElementType, ChartType, ShapeType } from '../../core/types' const CHART_TYPES: Array<{ k: ChartType; label: string; icon: string }> = [ { k: 'bar', label: '柱状图', icon: '📊' }, @@ -31,7 +32,7 @@ const content = ref('') const fontSize = ref(24) const colorSel = ref('primary') const colorPicker = ref('#000000') -const shape = ref<'rect' | 'circle' | 'triangle'>('rect') +const shape = ref('rect') const chartType = ref('bar') const codeLang = ref('') const imgBusy = ref(false) @@ -66,11 +67,11 @@ watch(selected, (el) => { } }, { immediate: true }) -const hasText = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula'].includes(selected.value.type)) -const canFont = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula'].includes(selected.value.type)) -const canAlign = computed(() => selected.value && ['title', 'text', 'quote', 'stat', 'table', 'formula'].includes(selected.value.type)) -const canBI = computed(() => selected.value && ['title', 'text', 'quote', 'list'].includes(selected.value.type)) -const canColor = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula'].includes(selected.value.type)) +const hasText = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape', 'card'].includes(selected.value.type)) +const canFont = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape', 'card'].includes(selected.value.type)) +const canAlign = computed(() => selected.value && ['title', 'text', 'quote', 'stat', 'table', 'formula', 'shape'].includes(selected.value.type)) +const canBI = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'shape'].includes(selected.value.type)) +const canColor = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape'].includes(selected.value.type)) const isShape = computed(() => selected.value?.type === 'shape') const typeLabel = computed(() => { @@ -87,9 +88,20 @@ function onMultiCopy() { const sel = store.getSelection() if (sel.length) store.copyElements(sel) } -function onMultiDel() { +async function onMultiDel() { const sel = store.getSelection() - if (sel.length) store.delElements(sel) + if (!sel.length) return + if (await appConfirm('删除选中的 ' + sel.length + ' 个元素?', '可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) { + store.delElements(sel) + } +} +function onAlignOp(edge: 'left' | 'hcenter' | 'right' | 'top' | 'vmiddle' | 'bottom') { + const sel = store.getSelection() + if (sel.length > 1) store.alignElements(sel, edge) +} +function onDistribute(axis: 'h' | 'v') { + const sel = store.getSelection() + if (sel.length > 2) store.distributeElements(sel, axis) } function onContentInput() { @@ -152,9 +164,12 @@ function onZ(dir: number) { if (!selected.value) return store.moveElementZ(selected.value.id, dir) } -function onDel() { +async function onDel() { if (!selected.value) return - store.delElement(selected.value.id) + const label = elementTypes[selected.value.type]?.label || '元素' + if (await appConfirm('删除这个' + label + '?', '可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) { + store.delElement(selected.value.id) + } } function onBg(k: string) { store.setSlideBackground(k) @@ -189,23 +204,62 @@ async function onAiImage() { if (imgBusy.value) return const el = selected.value if (!el || el.type !== 'image') return - if (!isImageConfigured()) { alert('请先在「AI 设置」中配置 API Key'); return } - const promptText = window.prompt('描述你想要的图片,例如「现代办公室协作场景,俯拍,柔和光线」') + if (!isImageConfigured()) { appAlert('需要 API Key', '请先在「AI 设置」中配置'); return } + const promptText = await appPrompt('AI 配图', { placeholder: '描述你想要的图片,如「现代办公室协作场景,俯拍,柔和光线」' }) if (!promptText) return imgBusy.value = true imgAbort = new AbortController() try { const r = await generateImage({ prompt: promptText, signal: imgAbort.signal }) const quotaErr = checkImageQuota(r.url) - if (quotaErr) { alert(quotaErr); return } + if (quotaErr) { appAlert('配图受限', quotaErr); return } store.updateElement(el.id, { content: r.url }) } catch (e: any) { - if (e?.name !== 'AbortError') alert('配图失败:' + (e?.message || String(e))) + if (e?.name !== 'AbortError') appAlert('配图失败', e?.message || String(e)) } finally { imgBusy.value = false; imgAbort = null } } +/* ---------- 批注(仅图片元素) ---------- */ +/** 复用文件内颜色选项集合(与颜色 select 的 option values 同款) */ +const ANNO_COLORS: Array<{ k: string; label: string }> = [ + { k: 'primary', label: '主色' }, + { k: 'accent', label: '强调色' }, + { k: 'text', label: '正文色' }, + { k: 'muted', label: '次要色' }, + { k: '#ffffff', label: '白色' } +] + +/** 当前图片的批注列表 */ +const annotations = computed(() => selected.value?.style?.annotations || []) + +/** 折叠状态:记录「已展开」的批注 id(默认折叠,仅展开正在编辑的一条) */ +const expandedAnno = ref(null) +function toggleAnno(annoId: string) { + expandedAnno.value = expandedAnno.value === annoId ? null : annoId +} + +function addAnno() { + if (!selected.value) return + const id = store.addAnnotation(selected.value.id) + if (id) expandedAnno.value = id // 新增自动展开 +} +function delAnno(annoId: string) { + if (selected.value) store.delAnnotation(selected.value.id, annoId) + if (expandedAnno.value === annoId) expandedAnno.value = null +} +function patchAnno(annoId: string, patch: Record) { + if (!selected.value) return + store.updateAnnotation(selected.value.id, annoId, patch) +} + +/** 批注文字摘要(折叠态卡片头显示) */ +function annoSummary(anno: any): string { + const t = (anno.text || '').trim() + return t ? (t.length > 12 ? t.slice(0, 12) + '…' : t) : '(空批注)' +} + /** 本地上传图片替换当前图片元素内容 */ function onLocalImage() { const el = selected.value @@ -219,14 +273,43 @@ function onLocalImage() { try { const dataUrl = await readAsDataURL(file) const quotaErr = checkImageQuota(dataUrl) - if (quotaErr) { alert(quotaErr); return } + if (quotaErr) { appAlert('图片受限', quotaErr); return } store.updateElement(el.id, { content: dataUrl }) } catch (e: any) { - alert('读取图片失败:' + (e?.message || String(e))) + appAlert('读取图片失败', e?.message || String(e)) } } input.click() } + +/* ---------- 视频元素 ---------- */ +function onLocalVideo() { + const el = selected.value + if (!el) return + const input = document.createElement('input') + input.type = 'file' + input.accept = 'video/*' + input.onchange = async () => { + const f = input.files?.[0] + if (!f) return + // 注意:大视频会整体读入内存(dataURL),本地工具首版接受 + const dataUrl = await readAsDataURL(f) + store.updateElement(el.id, { content: dataUrl }) + } + input.click() +} +function onVideoUrl(e: Event) { + const el = selected.value + if (!el) return + const v = (e.target as HTMLInputElement).value.trim() + if (v) store.updateElement(el.id, { content: v }) +} +function toggleVideoOpt(key: 'autoplay' | 'loop' | 'muted') { + const el = selected.value + if (!el) return + const cur = (el.style as any)[key] + store.updateElement(el.id, { style: { [key]: !cur } }) +} @@ -247,6 +330,24 @@ function onLocalImage() { 🗑 删除 + + 对齐 + + ⇤ + ↔ + ⇥ + ⇧ + ↕ + ⇩ + + + + 分布 + + ⋯ + ⋮ + + 提示 @@ -262,7 +363,12 @@ function onLocalImage() { 内容 - + @@ -322,7 +428,15 @@ function onLocalImage() { 矩形 圆形 + 椭圆 三角 + 菱形 + 五边形 + 六边形 + 五角星 + 箭头 + 折角 + 气泡 @@ -384,6 +498,31 @@ function onLocalImage() { + + + 视频来源 + + 📁 本地视频 + + + + + + 播放选项 + + 自动 + 循环 + 静音 + + + 层级 @@ -393,6 +532,108 @@ function onLocalImage() { 🗑 + + + + 批注 + + + 添加批注 + + + + + {{ expandedAnno === anno.id ? '▾' : '▸' }} + 批注 {{ i + 1 }} + {{ annoSummary(anno) }} + × + + + + + + + 气泡文字 + + 字号 {{ anno.fontSize || 16 }}px + + + + + {{ c.label }} + + + B + I + + + + + ⬅ + ⬌ + ➡ + + + + + 连线 + + + 实线 + 虚线 + + + {{ c.label }} + + + + 线宽 {{ anno.line?.width ?? 0.5 }}px + + + + + 起点·无 + 起点·箭头 + 起点·圆点 + + + 终点·无 + 终点·箭头 + 终点·圆点 + + + + + + @@ -409,3 +650,126 @@ function onLocalImage() { + + + diff --git a/src/components/modals/ImportModal.vue b/src/components/modals/ImportModal.vue index 5a87fd7..f8f34e1 100644 --- a/src/components/modals/ImportModal.vue +++ b/src/components/modals/ImportModal.vue @@ -9,6 +9,7 @@ import { scanFiles, readEntries, aiAnalyzeDocument, fileToImageElement, imagesToSlideElements, + fileToVideoElement, ACCEPT_ATTR, type FileEntry, type ImportReport, describeReport @@ -31,18 +32,21 @@ const analyzeDone = ref(false) // AI 分析完成 const analyzeProgress = ref('') // AI 分析进度文案 const analyzeError = ref('') // AI 分析错误 const showImages = ref(true) +const showVideos = ref(true) const showDocs = ref(true) /* ---------- 过滤后的文件 ---------- */ const filteredEntries = computed(() => { return entries.value.filter(e => { if (e.kind === 'image' && !showImages.value) return false + if (e.kind === 'video' && !showVideos.value) return false if (e.kind === 'document' && !showDocs.value) return false return true }) }) const imageEntries = computed(() => filteredEntries.value.filter(e => e.kind === 'image')) +const videoEntries = computed(() => filteredEntries.value.filter(e => e.kind === 'video')) const docEntries = computed(() => filteredEntries.value.filter(e => e.kind === 'document')) const unsupportedCount = computed(() => entries.value.filter(e => e.kind === 'unsupported').length) @@ -115,7 +119,7 @@ async function handleFiles(fl: FileList | null) { const scanned = await scanFiles(fl) entries.value = scanned // 读取文件原始内容 - if (scanned.some(e => e.kind === 'image' || e.kind === 'document')) { + if (scanned.some(e => e.kind === 'image' || e.kind === 'video' || e.kind === 'document')) { await readEntries(scanned) } loaded.value = true @@ -165,9 +169,10 @@ async function runAiAnalysis() { /* ---------- 执行导入 ---------- */ function doImport() { const images = imageEntries.value.filter(e => e.data) + const videos = videoEntries.value.filter(e => e.data) const docs = docEntries.value.filter(e => e.slides && e.slides.length > 0) - if (images.length === 0 && docs.length === 0) { + if (images.length === 0 && videos.length === 0 && docs.length === 0) { // 如果有文档但还没 AI 分析,先分析 if (docEntries.value.filter(d => d.data && !d.slides).length > 0) { void runAiAnalysis() // fire-and-forget:分析完成后 UI 自动更新按钮 @@ -209,6 +214,21 @@ function doImport() { } } + // 1.5 视频 → 当前幻灯片(每个视频一页,元素铺满合理区域) + if (videos.length > 0) { + for (const v of videos) { + const el = fileToVideoElement(v.file, v.data!) + if (videos.length === 1) { + store.addElement('video', { content: el.content, x: el.x, y: el.y, w: el.w, h: el.h, style: el.style }, batch) + insertedImages++ + } else { + // 多视频:一视频一页 + store.appendSlide({ id: `s-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, background: 'bg', elements: [el] }, batch) + insertedSlides++ + } + } + } + // 2. AI 生成的幻灯片 → 追加到 deck if (docs.length > 0) { for (const d of docs) { @@ -313,6 +333,7 @@ function textPreview(data: string, maxLen = 80): string { 🖼 图片 ({{ imageEntries.length }}) + 🎬 视频 ({{ videoEntries.length }}) 📄 文档 ({{ docEntries.length }}) @@ -332,12 +353,14 @@ function textPreview(data: string, maxLen = 80): string { - {{ entry.kind === 'image' ? '🖼' : '📄' }} + {{ entry.kind === 'image' ? '🖼' : entry.kind === 'video' ? '🎬' : '📄' }} {{ entry.name }} {{ fmtSize(entry.file.size) }} ✅ 图片就绪 + + ✅ 视频就绪 ✅ AI → {{ entry.slides.length }} 页 ⏳ 待分析 diff --git a/src/core/ai.ts b/src/core/ai.ts index 340b05b..f1b2b49 100644 --- a/src/core/ai.ts +++ b/src/core/ai.ts @@ -13,7 +13,7 @@ import { normSegments } from './richtext' import { isTauri, aiProxy, aiProxyStream } from './bridge' const SEP = '%%PPT_JSON%%' // 对话模式中,自然语言回复与结构化操作的分隔标记 -const VALID_TYPES = ['title', 'text', 'list', 'stat', 'quote', 'image', 'shape', 'chart', 'card', 'table', 'code', 'formula'] as const +const VALID_TYPES = ['title', 'text', 'list', 'stat', 'quote', 'image', 'video', 'shape', 'chart', 'card', 'table', 'code', 'formula'] as const /* 智谱/通用错误码 → 可操作提示 */ const ERROR_HINTS: Record = { @@ -40,12 +40,12 @@ const SYS_BASE = '幻灯片模型:\n' + '{ "slides": [ { "background": "bg|panel|primary|accent|g-primary|g-deep|g-soft", "elements": [ 元素, ... ] } ] }\n' + '背景:bg/panel=浅底;primary/accent=纯色深底;g-primary=主→强调渐变(深);g-deep=深色渐变;g-soft=浅色渐变。\n\n' + - '元素类型 type:title 标题 | text 正文 | list 列表 | stat 数据 | quote 金句 | image 图片 | shape 形状 | chart 图表 | card 卡片 | table 表格 | code 代码 | formula 公式\n' + + '元素类型 type:title 标题 | text 正文 | list 列表 | stat 数据 | quote 金句 | image 图片 | video 视频 | shape 形状 | chart 图表 | card 卡片 | table 表格 | code 代码 | formula 公式\n' + '元素:{ "type":..., "x":数字,"y":数字,"w":数字,"h":数字 (0-100), "content":字符串, "style":{...} }\n' + ' - title/text/list/quote:content 为文字,list 用 \\n 分多行\n' + ' - stat:content 为大数字(如 "65%"),style.label 为说明\n' + ' - card:content 第一行=标题、其余行=正文;style.accent=顶部色条键,style.icon=emoji 图标\n' + - ' - shape:style.shapeType=rect|circle|triangle,style.fill=颜色键,style.gradient=true 渐变,style.opacity=0~1\n' + + ' - shape:style.shapeType=rect|circle|ellipse|triangle|diamond|pentagon|hexagon|star|arrow|chevron|bubble,style.fill=颜色键,style.gradient=true 渐变,style.opacity=0~1\n' + ' - chart:content 为 JSON,两种格式:\n' + ' 单系列:[{"label":"","value":数字}, ...]\n' + ' 多系列:{"series":["Q1","Q2"], "items":[{"label":"华东","values":[120,150]}, ...]}\n' + @@ -56,7 +56,8 @@ const SYS_BASE = ' - table:content 为 Markdown 管道表格字符串(首行表头,用 | 分列,换行分行),style.header=true 首行加粗\n' + ' - code:content 为代码文本(保留符原样,不要转义),style.lang=语言如 js/python(可选)\n' + ' - formula:content 为 LaTeX 公式字符串(不含 $$ 分隔符),如 "E = mc^2"、"\\sum_{i=1}^n x_i"\n' + - ' - image:content 留空\n\n' + + ' - image:content 留空\n' + + ' - video:content 填视频 URL\n\n' + '可选 segments 字段(结构化富文本,同一行内不同片段可有不同样式):\n' + ' segments: [[ {"text":"华东 "}, {"text":"增长 23%","bold":true,"color":"accent"} ], ...]\n' + ' 每个 segment 支持:bold/italic/underline/strike/color(主题键或#hex)/highlight(黄底)/code/sup/sub/fontSize/link\n' + @@ -106,11 +107,12 @@ function deckContext(currentIdx: number, selectedEl?: SlideElement | null): stri const deck: Deck = store.getDeck() /** 图片/超长 content 脱敏:base64 会撑爆上下文,替换为占位说明 */ const sanitizeEl = (el: SlideElement): SlideElement => { - if (el.type === 'image') { + if (el.type === 'image' || el.type === 'video') { const src = el.content || '' + const label = el.type === 'video' ? '视频' : '图片' const desc = src.startsWith('data:') - ? `[图片 base64 ${Math.round(src.length / 1024)}KB]` - : (src ? `[图片URL: ${src.slice(0, 80)}${src.length > 80 ? '…' : ''}]` : '[空图片]') + ? `[${label} base64 ${Math.round(src.length / 1024)}KB]` + : (src ? `[${label}URL: ${src.slice(0, 80)}${src.length > 80 ? '…' : ''}]` : `[空${label}]`) return { ...el, content: desc } } if (el.content && el.content.length > 2000) { @@ -415,6 +417,9 @@ function normStyle(st: any): ElementStyle { if (out.grid != null) out.grid = !!out.grid if (out.header != null) out.header = !!out.header if (out.inline != null) out.inline = !!out.inline + if (out.autoplay != null) out.autoplay = !!out.autoplay + if (out.loop != null) out.loop = !!out.loop + if (out.muted != null) out.muted = !!out.muted if (typeof out.lang === 'string' && out.lang.length > 16) out.lang = out.lang.slice(0, 16) ;['color', 'fill', 'accent', 'labelColor'].forEach(k => { if (validColor(out[k]) === undefined && out[k] != null) delete out[k] diff --git a/src/core/importer.ts b/src/core/importer.ts index 40b1957..1f3840b 100644 --- a/src/core/importer.ts +++ b/src/core/importer.ts @@ -17,15 +17,18 @@ export const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.s /** 允许的文档扩展名(由 LLM 解析) */ export const DOC_EXTS = new Set(['.md', '.markdown', '.txt', '.text', '.pdf', '.docx', '.doc']) +/** 允许的视频扩展名(实际判定以 MIME video/* 为准,此处用于 accept 与无 MIME 环境兜底) */ +export const VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv']) + /** 文件选择器 accept 属性(由支持清单派生,保持单一数据源) */ -export const ACCEPT_ATTR = [...IMAGE_EXTS, ...DOC_EXTS].join(',') +export const ACCEPT_ATTR = [...IMAGE_EXTS, ...VIDEO_EXTS, ...DOC_EXTS].join(',') /** 文件分类结果 */ export interface FileEntry { file: File name: string ext: string - kind: 'image' | 'document' | 'unsupported' + kind: 'image' | 'video' | 'document' | 'unsupported' /** 图片的 data URL,或文档的原始文本 */ data?: string /** AI 解析出的幻灯片(文档读取 + LLM 分析后填充) */ @@ -46,6 +49,8 @@ export function getFileKind(file: File): FileEntry['kind'] { const dot = name.lastIndexOf('.') const ext = dot >= 0 ? name.slice(dot) : '' if (IMAGE_EXTS.has(ext)) return 'image' + // 视频按 MIME 判定(扩展名太杂),无 MIME 时扩展名兜底 + if (file.type.startsWith('video/') || VIDEO_EXTS.has(ext)) return 'video' if (DOC_EXTS.has(ext)) return 'document' return 'unsupported' } @@ -163,8 +168,7 @@ export function fileToImageElement(file: File, dataUrl: string): SlideElement { export function imagesToSlideElements(files: Array<{ file: File; dataUrl: string }>): SlideElement[] { if (files.length === 1) { return [fileToImageElement(files[0].file, files[0].dataUrl)] - } - const cols = Math.ceil(Math.sqrt(files.length)) + } const cols = Math.ceil(Math.sqrt(files.length)) const rows = Math.ceil(files.length / cols) const cellW = Math.floor(80 / cols) const cellH = Math.floor(60 / rows) @@ -180,6 +184,19 @@ export function imagesToSlideElements(files: Array<{ file: File; dataUrl: string }) } +/* ============================================================ + * 视频导入 → SlideElement(video 类型) + * 注意:dataURL 会把整个视频读入内存,大视频占内存较高(本地工具首版接受) + * ============================================================ */ + +export function fileToVideoElement(file: File, dataUrl: string): SlideElement { + return createElement('video', { + content: dataUrl, + x: 20, y: 15, w: 60, h: 45, + style: { anim: 'fade' } + }) +} + /* ============================================================ * 批量文件扫描与读取 * ============================================================ */ @@ -204,6 +221,9 @@ export async function readEntries(entries: FileEntry[]): Promise { try { if (entry.kind === 'image') { entry.data = await readAsDataURL(entry.file) + } else if (entry.kind === 'video') { + // 视频 → data URL(与图片同管线,播放元素直接消费) + entry.data = await readAsDataURL(entry.file) } else if (entry.kind === 'document') { if (entry.ext === '.pdf') { entry.data = await extractPdfText(entry.file) diff --git a/src/core/op.ts b/src/core/op.ts index bfb10f3..35b1b0e 100644 --- a/src/core/op.ts +++ b/src/core/op.ts @@ -20,6 +20,7 @@ export type OpType = | 'move_slide' | 'replace_slide' | 'set_slide_bg' + | 'set_slide_note' | 'add_element' | 'update_element' | 'del_element' @@ -106,6 +107,15 @@ export function applyOp(inputDeck: Deck, op: Op): Deck { break } + case 'set_slide_note': { + const index = op.index as number + const note = op.note as string + if (deck.slides[index]) { + deck.slides[index].note = note + } + break + } + case 'add_element': { const slideIdx = op.slideIdx as number const element = op.element as SlideElement @@ -221,6 +231,12 @@ export function invertOp(inputDeck: Deck, op: Op): Op { return { type: 'set_slide_bg', index, bg: oldBg } } + case 'set_slide_note': { + const index = op.index as number + const oldNote = deck.slides[index]?.note ?? '' + return { type: 'set_slide_note', index, note: oldNote } + } + case 'add_element': { // 逆:删除加的元素 return { type: 'del_element', slideIdx: op.slideIdx, elementId: (op.element as SlideElement).id } diff --git a/src/core/sample.ts b/src/core/sample.ts index 85b92b5..6571b52 100644 --- a/src/core/sample.ts +++ b/src/core/sample.ts @@ -30,7 +30,8 @@ export const elementTypes: Record & stat: { w: 30, h: 35 }, quote: { w: 70, h: 25 }, image: { w: 40, h: 40 }, - shape: { w: 25, h: 25 }, + video: { w: 50, h: 32 }, + shape: { w: 30, h: 15 }, chart: { w: 45, h: 35 }, card: { w: 35, h: 35 }, table: { w: 60, h: 35 }, diff --git a/src/core/store.ts b/src/core/store.ts index 5a82f95..e20c0a8 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -3,7 +3,7 @@ * 由 store.js 迁移:用 Vue reactive 替代手写 pub/sub,组件自动追踪 * ===================================================================== */ import { reactive, computed } from 'vue' -import type { AiCfg, Deck, LibItem, PageTemplate, Slide, SlideElement, ThemeKey, ChatMessage } from './types' +import type { AiCfg, Deck, LibItem, PageTemplate, Slide, SlideElement, ThemeKey, ChatMessage, Annotation } from './types' import { DECK_VERSION, SAMPLE_DECK, createElement, themes, uid } from './sample' import { applyOp, invertOp, type Op, type HistoryEntry } from './op' @@ -185,12 +185,15 @@ const state = reactive<{ /** 多选集(框选/Shift加选时非空;selectedId 始终为锚点元素) */ selectedIds: string[] activeLibId: string | null + /** 当前选中的批注 id(纯 UI 态,不持久化;Delete 删除优先级高于宿主元素) */ + selectedAnnoId: string | null }>({ deck: JSON.parse(JSON.stringify(SAMPLE_DECK)), currentIndex: 0, selectedId: null, selectedIds: [], - activeLibId: null + activeLibId: null, + selectedAnnoId: null }) let saveTimer: ReturnType | null = null @@ -430,6 +433,10 @@ function selectElement(id: string | null) { state.selectedId = id || null; state /* ---------- 多选(框选 / Shift 加选 / Ctrl+A) ---------- */ const selectedIds = computed(() => state.selectedIds) +/* ---------- 批注选中(纯 UI 态) ---------- */ +const selectedAnnoId = computed(() => state.selectedAnnoId) +function setSelectedAnnoId(id: string | null) { state.selectedAnnoId = id } + /** 有效多选集:过滤掉已不存在的元素,非空(length>1)时表示多选态 */ function getSelection(): string[] { const s = currentSlide.value @@ -471,6 +478,53 @@ function moveElementsBy(ids: string[], dx: number, dy: number) { } } +/** 多选对齐:left左对齐 hcenter水平居中 right右对齐 top顶对齐 vmiddle垂直居中 bottom底对齐 */ +function alignElements(ids: string[], edge: 'left' | 'hcenter' | 'right' | 'top' | 'vmiddle' | 'bottom') { + const els = ids.map(findElement).filter(Boolean) as SlideElement[] + if (els.length < 2) return + const group = beginBatch() + const minX = Math.min(...els.map(e => e.x)) + const maxX = Math.max(...els.map(e => e.x + e.w)) + const minY = Math.min(...els.map(e => e.y)) + const maxY = Math.max(...els.map(e => e.y + e.h)) + for (const el of els) { + let x = el.x, y = el.y + if (edge === 'left') x = minX + if (edge === 'right') x = maxX - el.w + if (edge === 'hcenter') x = (minX + maxX) / 2 - el.w / 2 + if (edge === 'top') y = minY + if (edge === 'bottom') y = maxY - el.h + if (edge === 'vmiddle') y = (minY + maxY) / 2 - el.h / 2 + if (x !== el.x || y !== el.y) { + execOp({ type: 'update_element', slideIdx: state.currentIndex, elementId: el.id, patch: { x, y }, clientId: CLIENT_ID, timestamp: Date.now() }, { group }) + } + } +} + +/** 多选分布:h 水平等间距分布(x 方向)v 垂直等间距分布(y 方向);首尾不动,中间均匀排布 */ +function distributeElements(ids: string[], axis: 'h' | 'v') { + const els = ids.map(findElement).filter(Boolean) as SlideElement[] + if (els.length < 3) return + const group = beginBatch() + const sorted = axis === 'h' ? [...els].sort((a, b) => a.x - b.x) : [...els].sort((a, b) => a.y - b.y) + const first = sorted[0], last = sorted[sorted.length - 1] + let gapTotal: number + if (axis === 'h') { + // last.x - first.x - first.w 为首尾间总空隙(含中间元素占的宽),再减中间元素总宽 = 纯空隙 + gapTotal = (last.x - first.x - first.w) - sorted.slice(1, -1).reduce((s, e) => s + e.w, 0) + } else { + gapTotal = (last.y - first.y - first.h) - sorted.slice(1, -1).reduce((s, e) => s + e.h, 0) + } + const gap = gapTotal / (sorted.length - 1) + let cur = axis === 'h' ? first.x + first.w + gap : first.y + first.h + gap + for (let i = 1; i < sorted.length - 1; i++) { + const el = sorted[i] + const patch = axis === 'h' ? { x: cur, y: el.y } : { x: el.x, y: cur } + execOp({ type: 'update_element', slideIdx: state.currentIndex, elementId: el.id, patch, clientId: CLIENT_ID, timestamp: Date.now() }, { group }) + cur += (axis === 'h' ? el.w : el.h) + gap + } +} + /** 批量删除(同批 undo) */ function delElements(ids: string[]) { const group = beginBatch() @@ -527,6 +581,13 @@ function setSlideBackground(bg: string) { execOp({ type: 'set_slide_bg', index: state.currentIndex, bg: bg as Slide['background'], clientId: CLIENT_ID, timestamp: Date.now() }) } +function setSlideNote(note: string) { + execOp( + { type: 'set_slide_note', index: state.currentIndex, note, clientId: CLIENT_ID, timestamp: Date.now() }, + { coalesceKey: 'slide-note:' + state.currentIndex } + ) +} + /* ---------- 元素 CRUD ---------- */ function addElement(type: SlideElement['type'], over?: Parameters[1], group?: string) { const el = createElement(type, over) @@ -540,6 +601,40 @@ function updateElement(id: string, patch: Partial & { style?: Reco execOp({ type: 'update_element', slideIdx: state.currentIndex, elementId: id, patch, clientId: CLIENT_ID, timestamp: Date.now() }, { coalesceKey }) } +/* ---------- 批注(annotation):挂在 element.style.annotations,走 updateElement ---------- */ +function addAnnotation(elId: string) { + const el = findElement(elId) + if (!el) return + const list: Annotation[] = [...(el.style.annotations || [])] + const anno: Annotation = { + id: uid('an'), + text: '', + bx: Math.min(90, (el.x || 0) + (el.w || 20) + 4), by: Math.max(2, el.y || 10), + bw: 22, bh: 12, + ax: 50, ay: 50, + } + list.push(anno) + updateElement(elId, { style: { annotations: list } }) + return anno.id +} +function updateAnnotation(elId: string, annoId: string, patch: Partial) { + const el = findElement(elId) + if (!el) return + const list = (el.style.annotations || []).map(a => { + if (a.id !== annoId) return a + const next: Annotation = { ...a, ...patch } + if (patch.line) next.line = { ...(a.line || {}), ...patch.line } + return next + }) + updateElement(elId, { style: { annotations: list } }) +} +function delAnnotation(elId: string, annoId: string) { + const el = findElement(elId) + if (!el) return + const list = (el.style.annotations || []).filter(a => a.id !== annoId) + updateElement(elId, { style: { annotations: list } }) +} + function delElement(id: string) { const el = findElement(id) const deletedElement = el ? clone(el) : undefined @@ -988,14 +1083,17 @@ export const store = { // 主题 setTheme, // 幻灯片 CRUD - addSlide, dupSlide, delSlide, moveSlide, setSlideBackground, + addSlide, dupSlide, delSlide, moveSlide, setSlideBackground, setSlideNote, // 元素 CRUD addElement, updateElement, delElement, moveElementZ, findElement, + // 批注 + addAnnotation, updateAnnotation, delAnnotation, // 元素剪贴板 copyElement, copyElements, cutElement, cutElements, pasteElement, pasteElements, hasClipboard, // 多选 selectedIds, getSelection, selectMany, toggleSelect, selectAllElements, - moveElementsBy, delElements, + selectedAnnoId, setSelectedAnnoId, + moveElementsBy, delElements, alignElements, distributeElements, // undo/redo undo, redo, beginBatch, // 整体替换 diff --git a/src/core/types.ts b/src/core/types.ts index 0f4fbf4..1f26b6d 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -17,7 +17,7 @@ export type ColorKey = 'primary' | 'accent' | 'text' | 'muted' | (string & {}) /** 元素类型 */ export type ElementType = | 'title' | 'text' | 'list' | 'stat' | 'quote' - | 'image' | 'shape' | 'chart' | 'card' + | 'image' | 'video' | 'shape' | 'chart' | 'card' | 'table' | 'code' | 'formula' /** 图表子类型 */ @@ -31,7 +31,9 @@ export type AnimType = | 'pop' | 'rotate' | 'bounce' | 'flip' | 'blur' /** 形状类型 */ -export type ShapeType = 'rect' | 'circle' | 'triangle' +export type ShapeType = + | 'rect' | 'circle' | 'ellipse' | 'triangle' | 'diamond' + | 'pentagon' | 'hexagon' | 'star' | 'arrow' | 'chevron' | 'bubble' /** 主题对象 */ export interface Theme { @@ -44,6 +46,26 @@ export interface Theme { muted: string } +/** 批注(连线 + 气泡) */ +export interface Annotation { + id: string + text: string + /** 气泡框:百分比坐标,与元素同坐标系(画布 1280×720) */ + bx: number; by: number; bw: number; bh: number + /** 图片侧连线锚点(相对图片框的百分比 0~100),默认 50/50 */ + ax?: number; ay?: number + /** 气泡文字整体样式 */ + fontSize?: number; color?: ColorKey; bold?: boolean; italic?: boolean; align?: 'left' | 'center' | 'right' + /** 连线样式 */ + line?: { + style?: 'solid' | 'dashed' + width?: number + color?: ColorKey + startCap?: 'none' | 'arrow' | 'dot' + endCap?: 'none' | 'arrow' | 'dot' + } +} + /** 元素样式(所有字段可选,按 type 决定哪些有效) */ export interface ElementStyle { fontSize?: number @@ -58,6 +80,11 @@ export interface ElementStyle { labelSize?: number // image fit?: 'contain' | 'cover' + // video + poster?: string + autoplay?: boolean + loop?: boolean + muted?: boolean // shape shapeType?: ShapeType fill?: ColorKey @@ -85,6 +112,8 @@ export interface ElementStyle { // card icon?: string accent?: ColorKey + // annotation(批注气泡,image 起步,未来任意元素) + annotations?: Annotation[] } /** 幻灯片元素 */ @@ -106,6 +135,7 @@ export interface Slide { id: string background: BgKey elements: SlideElement[] + note?: string // 演讲者备注/页脚批注(不渲染到幻灯片) } /** 一份演示文稿 */ diff --git a/src/styles/editor.css b/src/styles/editor.css index d8856c4..d39e93a 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -72,7 +72,7 @@ } .canvas-frame { position: relative; - width: min(100%, calc((100vh - 54px - 56px) * 16 / 9)); + width: min(100%, calc((100vh - 54px - 116px) * 16 / 9)); max-width: 100%; aspect-ratio: 16 / 9; box-shadow: var(--shadow-md); border-radius: 4px; overflow: hidden; @@ -83,7 +83,78 @@ width: 1280px; height: 720px; transform-origin: top left; } -.canvas-status { margin-top: 10px; font-size: 12px; color: var(--ui-muted); } +.canvas-footer { + margin-top: 10px; + flex: none; + width: min(100%, calc((100vh - 54px - 116px) * 16 / 9)); + max-width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.canvas-status { font-size: 12px; color: var(--ui-muted); flex: none; } +.note-toggle { + display: inline-flex; + align-items: center; + gap: 5px; + height: 26px; + padding: 0 10px; + border: 1px solid transparent; + border-radius: 13px; + background: transparent; + color: var(--ui-muted); + font-size: 12px; + cursor: pointer; + transition: background .15s, color .15s, border-color .15s; +} +.note-toggle:hover { + background: var(--ui-primary-soft); + color: var(--ui-primary); +} +.note-toggle.active { + color: var(--ui-primary); + border-color: var(--ui-border); + background: var(--ui-panel); +} +.note-toggle-icon { font-size: 13px; line-height: 1; } +.note-dot { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--ui-primary); +} +.canvas-note { + margin-top: 8px; + flex: none; + /* 与 .canvas-frame 同宽,保持对齐 */ + width: min(100%, calc((100vh - 54px - 116px) * 16 / 9)); + max-width: 100%; +} +.canvas-note-input { + display: block; + width: 100%; + box-sizing: border-box; + resize: vertical; + min-height: 44px; + max-height: 120px; + padding: 8px 12px; + border: 1px solid var(--ui-border); + border-radius: 8px; + font-size: 13px; + line-height: 1.5; + color: var(--ui-text); + background: var(--ui-panel); + font-family: inherit; + outline: none; + transition: border-color .15s, box-shadow .15s; +} +.canvas-note-input:focus { + border-color: var(--ui-primary); + box-shadow: 0 0 0 3px var(--ui-primary-soft); +} +.canvas-note-input::placeholder { + color: var(--ui-muted); +} /* ---------- 画布元素 ---------- */ .el { position: absolute; overflow: hidden; } @@ -107,7 +178,41 @@ .el-stat .num { font-weight: 800; line-height: 1; } .el-stat .label { margin-top: .35em; text-align: center; } .el-shape { width: 100%; height: 100%; } +/* 形状文字层:覆盖在图形上方、居中;颜色默认白(形状为填充色底),字号/粗斜继承 style。 + padding 稍大:clip-path 多边形(箭头/星形等)边缘收窄,文字留出安全区 */ +.el-shape-text { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 8% 12%; + box-sizing: border-box; + white-space: pre-wrap; + word-break: break-word; + text-align: center; + outline: none; +} +/* rect/椭圆这类满幅形状,文字安全区可以小 */ +.el[data-type="shape"][data-shape="rect"] > .el-shape-text, +.el[data-type="shape"][data-shape="circle"] > .el-shape-text, +.el[data-type="shape"][data-shape="ellipse"] > .el-shape-text { + padding: 6px 10px; +} +/* 空形状不显示文字占位:装饰形状(分隔条/小三角)保持纯净, + 加文字的引导由属性面板「内容」输入框承担 */ .el-image { width: 100%; height: 100%; object-fit: cover; } +.el-video { width: 100%; height: 100%; background: #000; border-radius: 8px; display: block; } +/* 视频缩略图降级:不加载视频,poster 静态图 / 黑底▶占位 */ +.el-video-thumb { + width: 100%; height: 100%; + background: #0f172a; + border-radius: 8px; + display: flex; align-items: center; justify-content: center; + overflow: hidden; +} +.el-video-thumb img { width: 100%; height: 100%; object-fit: cover; } +.el-video-thumb-play { color: rgba(255,255,255,.75); font-size: 28px; } /* 空图片元素占位(未填 content 时不渲染裂图) */ .el-image-empty { width: 100%; height: 100%;