1 Commits

Author SHA1 Message Date
李启源
61156eca02 refactor(ui): remove Ant Design Vue dependency 2026-07-22 13:13:17 +08:00
12 changed files with 520 additions and 355 deletions

View File

@@ -28,11 +28,11 @@ pnpm install
pnpm dev
```
组件库将 Vue、D3、Ant Design Vue 和 vue3-colorpicker 作为 peer dependency直接安装到业务项目时请一并
组件库将 Vue、D3 和 vue3-colorpicker 作为 peer dependency直接安装到业务项目时请一并
安装这些依赖:
```bash
pnpm add waveform-analysis vue d3 ant-design-vue vue3-colorpicker
pnpm add waveform-analysis vue d3 vue3-colorpicker
```
### 运行时版本要求
@@ -40,11 +40,11 @@ pnpm add waveform-analysis vue d3 ant-design-vue vue3-colorpicker
组件库支持以下运行时版本:
| 依赖 | 支持版本 |
| -------------- | ------------- |
| ---- | ------------- |
| Vue | `>=3.2.33 <4` |
| Ant Design Vue | `>=3.2.20 <4` |
| D3 | `>=7.9.0 <8` |
安装时请确保业务项目中的 Vue 与 Ant Design Vue 版本满足上述范围。
安装时请确保业务项目中的 Vue 与 D3 版本满足上述范围。
## 发布

View File

@@ -35,7 +35,6 @@
"test:coverage": "vitest run --coverage"
},
"peerDependencies": {
"ant-design-vue": ">=3.2.20 <4",
"d3": ">=7.9.0 <8",
"vue": ">=3.2.33 <4",
"vue3-colorpicker": ">=2.3.0 <3"
@@ -47,7 +46,6 @@
"@vitejs/plugin-vue": "6.0.8",
"@vitest/coverage-v8": "4.1.10",
"@vue/test-utils": "2.4.11",
"ant-design-vue": "4.2.6",
"d3": "7.9.0",
"eslint": "10.7.0",
"eslint-config-prettier": "10.1.8",

198
pnpm-lock.yaml generated
View File

@@ -26,9 +26,6 @@ importers:
'@vue/test-utils':
specifier: 2.4.11
version: 2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@6.0.3))
ant-design-vue:
specifier: 4.2.6
version: 4.2.6(vue@3.5.40(typescript@6.0.3))
d3:
specifier: 7.9.0
version: 7.9.0
@@ -77,17 +74,6 @@ packages:
'@aesoper/normal-utils@0.1.5':
resolution: {integrity: sha512-LFF/6y6h5mfwhnJaWqqxuC8zzDaHCG62kMRkd8xhDtq62TQj9dM17A9DhE87W7DhiARJsHLgcina/9P4eNCN1w==}
'@ant-design/colors@6.0.0':
resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==}
'@ant-design/icons-svg@4.5.0':
resolution: {integrity: sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==}
'@ant-design/icons-vue@7.0.1':
resolution: {integrity: sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==}
peerDependencies:
vue: '>=3.0.3'
'@asamuzakjp/css-color@5.1.11':
resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -116,10 +102,6 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.29.7':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
@@ -168,10 +150,6 @@ packages:
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@emnapi/core@1.11.1':
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
@@ -181,12 +159,6 @@ packages:
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
'@emotion/unitless@0.8.1':
resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -386,9 +358,6 @@ packages:
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@simonwep/pickr@1.8.2':
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -710,15 +679,6 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
ant-design-vue@4.2.6:
resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==}
engines: {node: '>=12.22.0'}
peerDependencies:
vue: '>=3.2.0'
array-tree-filter@2.1.0:
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -726,9 +686,6 @@ packages:
ast-v8-to-istanbul@1.0.5:
resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==}
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -768,18 +725,12 @@ packages:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
core-js@3.49.0:
resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -927,9 +878,6 @@ packages:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
dayjs@1.11.21:
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -952,12 +900,6 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dom-align@1.12.4:
resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==}
dom-scroll-into-view@2.0.1:
resolution: {integrity: sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -1163,10 +1105,6 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-plain-object@3.0.1:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
@@ -1203,9 +1141,6 @@ packages:
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
jsdom@29.1.1:
resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
@@ -1312,13 +1247,6 @@ packages:
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -1362,9 +1290,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanopop@2.4.2:
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -1451,9 +1376,6 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
robust-predicates@3.0.3:
resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
@@ -1472,17 +1394,11 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scroll-into-view-if-needed@2.2.31:
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -1524,9 +1440,6 @@ packages:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
stylis@4.4.0:
resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -1534,10 +1447,6 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1722,12 +1631,6 @@ packages:
peerDependencies:
typescript: '>=5.0.0'
vue-types@3.0.2:
resolution: {integrity: sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==}
engines: {node: '>=10.15.0'}
peerDependencies:
vue: ^3.0.0
vue-types@4.2.1:
resolution: {integrity: sha512-DNQZmJuOvovLUIp0BENRkdnZHbI0V4e2mNvjAZOAXKD56YGvRchtUYOXA/XqTxdv7Ng5SJLZqRKRpAhm5NLaPQ==}
engines: {node: '>=12.16.0'}
@@ -1758,9 +1661,6 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
webidl-conversions@8.0.1:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
@@ -1814,18 +1714,6 @@ snapshots:
'@aesoper/normal-utils@0.1.5': {}
'@ant-design/colors@6.0.0':
dependencies:
'@ctrl/tinycolor': 3.6.1
'@ant-design/icons-svg@4.5.0': {}
'@ant-design/icons-vue@7.0.1(vue@3.5.40(typescript@6.0.3))':
dependencies:
'@ant-design/colors': 6.0.0
'@ant-design/icons-svg': 4.5.0
vue: 3.5.40(typescript@6.0.3)
'@asamuzakjp/css-color@5.1.11':
dependencies:
'@asamuzakjp/generational-cache': 1.0.1
@@ -1854,8 +1742,6 @@ snapshots:
dependencies:
'@babel/types': 7.29.7
'@babel/runtime@7.29.7': {}
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
@@ -1891,8 +1777,6 @@ snapshots:
'@csstools/css-tokenizer@4.0.0': {}
'@ctrl/tinycolor@3.6.1': {}
'@emnapi/core@1.11.1':
dependencies:
'@emnapi/wasi-threads': 1.2.2
@@ -1909,10 +1793,6 @@ snapshots:
tslib: 2.8.1
optional: true
'@emotion/hash@0.9.2': {}
'@emotion/unitless@0.8.1': {}
'@eslint-community/eslint-utils@4.9.1(eslint@10.7.0)':
dependencies:
eslint: 10.7.0
@@ -2050,11 +1930,6 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {}
'@simonwep/pickr@1.8.2':
dependencies:
core-js: 3.49.0
nanopop: 2.4.2
'@standard-schema/spec@1.1.0': {}
'@tybys/wasm-util@0.10.3':
@@ -2483,34 +2358,6 @@ snapshots:
ansi-styles@6.2.3: {}
ant-design-vue@4.2.6(vue@3.5.40(typescript@6.0.3)):
dependencies:
'@ant-design/colors': 6.0.0
'@ant-design/icons-vue': 7.0.1(vue@3.5.40(typescript@6.0.3))
'@babel/runtime': 7.29.7
'@ctrl/tinycolor': 3.6.1
'@emotion/hash': 0.9.2
'@emotion/unitless': 0.8.1
'@simonwep/pickr': 1.8.2
array-tree-filter: 2.1.0
async-validator: 4.2.5
csstype: 3.2.3
dayjs: 1.11.21
dom-align: 1.12.4
dom-scroll-into-view: 2.0.1
lodash: 4.18.1
lodash-es: 4.18.1
resize-observer-polyfill: 1.5.1
scroll-into-view-if-needed: 2.2.31
shallow-equal: 1.2.1
stylis: 4.4.0
throttle-debounce: 5.0.2
vue: 3.5.40(typescript@6.0.3)
vue-types: 3.0.2(vue@3.5.40(typescript@6.0.3))
warning: 4.0.3
array-tree-filter@2.1.0: {}
assertion-error@2.0.1: {}
ast-v8-to-istanbul@1.0.5:
@@ -2519,8 +2366,6 @@ snapshots:
estree-walker: 3.0.3
js-tokens: 10.0.0
async-validator@4.2.5: {}
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
@@ -2551,8 +2396,6 @@ snapshots:
commander@7.2.0: {}
compute-scroll-into-view@1.0.20: {}
config-chain@1.1.13:
dependencies:
ini: 1.3.8
@@ -2560,8 +2403,6 @@ snapshots:
convert-source-map@2.0.0: {}
core-js@3.49.0: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2736,8 +2577,6 @@ snapshots:
transitivePeerDependencies:
- '@noble/hashes'
dayjs@1.11.21: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -2752,10 +2591,6 @@ snapshots:
detect-libc@2.1.2: {}
dom-align@1.12.4: {}
dom-scroll-into-view@2.0.1: {}
eastasianwidth@0.2.0: {}
editorconfig@1.0.7:
@@ -2949,8 +2784,6 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-plain-object@3.0.1: {}
is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1: {}
@@ -2988,8 +2821,6 @@ snapshots:
js-tokens@10.0.0: {}
js-tokens@4.0.0: {}
jsdom@29.1.1:
dependencies:
'@asamuzakjp/css-color': 5.1.11
@@ -3086,12 +2917,6 @@ snapshots:
lodash-es@4.18.1: {}
lodash@4.18.1: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
lru-cache@10.4.3: {}
lru-cache@11.5.2: {}
@@ -3128,8 +2953,6 @@ snapshots:
nanoid@3.3.16: {}
nanopop@2.4.2: {}
natural-compare@1.4.0: {}
nopt@7.2.1:
@@ -3203,8 +3026,6 @@ snapshots:
require-from-string@2.0.2: {}
resize-observer-polyfill@1.5.1: {}
robust-predicates@3.0.3: {}
rolldown@1.1.5:
@@ -3236,14 +3057,8 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scroll-into-view-if-needed@2.2.31:
dependencies:
compute-scroll-into-view: 1.0.20
semver@7.8.5: {}
shallow-equal@1.2.1: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -3280,16 +3095,12 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
stylis@4.4.0: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
symbol-tree@3.2.4: {}
throttle-debounce@5.0.2: {}
tinybench@2.9.0: {}
tinycolor2@1.6.0: {}
@@ -3417,11 +3228,6 @@ snapshots:
'@vue/language-core': 3.3.7
typescript: 6.0.3
vue-types@3.0.2(vue@3.5.40(typescript@6.0.3)):
dependencies:
is-plain-object: 3.0.1
vue: 3.5.40(typescript@6.0.3)
vue-types@4.2.1(vue@3.5.40(typescript@6.0.3)):
dependencies:
is-plain-object: 5.0.0
@@ -3452,10 +3258,6 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
warning@4.0.3:
dependencies:
loose-envify: 1.4.0
webidl-conversions@8.0.1: {}
whatwg-mimetype@5.0.0: {}

View File

@@ -1,5 +1,4 @@
import { flushPromises, mount } from '@vue/test-utils'
import { InputNumber, Select } from 'ant-design-vue'
import { describe, expect, it, vi } from 'vitest'
import { ColorPicker } from 'vue3-colorpicker'
@@ -53,9 +52,9 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
expect(frameControls.find('[aria-label="图框线宽"]').exists()).toBe(true)
expect(frameControls.find('[aria-label="图框线型"]').exists()).toBe(true)
expect(frameControls.find('[aria-label="显示图框水印"]').exists()).toBe(true)
expect(frameControls.get('.frame-style-control--switch .ant-switch').classes()).toContain(
'ant-switch-small',
)
expect(
frameControls.get('.frame-style-control--switch .native-switch').attributes('role'),
).toBe('switch')
const titleControls = panel.get('.title-controls')
expect(panel.find('[aria-label="显示标题"]').exists()).toBe(true)
expect(titleControls.find('[aria-label="标题名称"]').exists()).toBe(true)
@@ -85,8 +84,8 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
expect(chart.props('cleanView')).toBe(false)
expect(chart.props('zeroLine')).toMatchObject({ visible: false, color: '#98a2b3', width: 1 })
await wrapper.get('[aria-label="净图模式"]').trigger('click')
await wrapper.get('[aria-label="显示零值参考线"]').trigger('click')
await wrapper.get('[aria-label="净图模式"]').setValue(true)
await wrapper.get('[aria-label="显示零值参考线"]').setValue(true)
await flushPromises()
expect(chart.props('cleanView')).toBe(true)
@@ -237,7 +236,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
expect(renderedTitle().attributes('style')).toContain('font-style: normal')
expect(renderedTitle().attributes('style')).toContain('text-decoration: none')
await wrapper.get('[aria-label="显示标题"]').trigger('click')
await wrapper.get('[aria-label="显示标题"]').setValue(false)
await flushPromises()
expect(wrapper.find('.waveform-chart__title-area').exists()).toBe(false)
@@ -250,17 +249,17 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
const frameControls = wrapper.get('.frame-style-controls')
const colorPickers = frameControls.findAllComponents(ColorPicker)
const widthInput = frameControls.findAllComponents(InputNumber)[0]
const styleSelect = frameControls.findAllComponents(Select)[0]
const widthInput = frameControls.get('[aria-label="图框线宽"]')
const styleSelect = frameControls.get('[aria-label="图框线型"]')
expect(colorPickers).toHaveLength(2)
expect(widthInput).toBeDefined()
expect(styleSelect).toBeDefined()
expect(widthInput.element.tagName).toBe('INPUT')
expect(styleSelect.element.tagName).toBe('SELECT')
colorPickers[0].vm.$emit('update:pureColor', 'rgba(220, 38, 38, 0.8)')
colorPickers[1].vm.$emit('update:pureColor', 'rgba(14, 165, 233, 0.25)')
widthInput?.vm.$emit('update:value', 3)
styleSelect?.vm.$emit('update:value', 'dashed')
await widthInput.setValue('3')
await styleSelect.setValue('dashed')
await flushPromises()
const frames = wrapper.findAll('.waveform-chart__plot-frame')
@@ -291,11 +290,11 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
expect(initialWatermarks.length).toBeGreaterThan(1)
await watermarkToggle.trigger('click')
await watermarkToggle.setValue(false)
await flushPromises()
expect(wrapper.findAll('.waveform-chart__watermark')).toHaveLength(0)
await watermarkToggle.trigger('click')
await watermarkToggle.setValue(true)
await flushPromises()
expect(
wrapper.findAll('.waveform-chart__watermark').map((watermark) => watermark.text()),

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { Button, Input, InputNumber, Radio, Select, Switch } from 'ant-design-vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ColorPicker } from 'vue3-colorpicker'
import 'vue3-colorpicker/style.css'
@@ -313,6 +312,12 @@ function resetTitleTextStyle() {
titleUnderline.value = false
}
function clampNumber(value: unknown, min: number, max: number, fallback: number) {
const number = typeof value === 'number' ? value : Number(value)
if (!Number.isFinite(number)) return fallback
return Math.min(max, Math.max(min, number))
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeControls()
}
@@ -323,15 +328,15 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
<template>
<main class="workspace">
<Button
<button
type="button"
class="mobile-control-toggle"
size="small"
:aria-expanded="controlsOpen"
aria-controls="waveform-control-panel"
@click="controlsOpen = true"
>
控制面板
</Button>
</button>
<button
v-if="controlsOpen"
@@ -348,32 +353,50 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
aria-label="波形图控制"
>
<div class="control-panel__scroll">
<Button class="control-panel__close" type="text" size="small" @click="closeControls">
关闭
</Button>
<button class="control-panel__close" type="button" @click="closeControls">关闭</button>
<section class="control-section">
<h2>显示方式</h2>
<Radio.Group
v-model:value="displayMode"
class="display-mode-control"
button-style="solid"
size="small"
aria-label="波形展示方式"
<div class="display-mode-control" role="radiogroup" aria-label="波形展示方式">
<label
><input v-model="displayMode" type="radio" value="independent" /><span
>单独坐标</span
></label
>
<Radio.Button value="independent">单独坐标</Radio.Button>
<Radio.Button value="separated">多道分离</Radio.Button>
<Radio.Button value="compact">多道紧凑</Radio.Button>
</Radio.Group>
<label
><input v-model="displayMode" type="radio" value="separated" /><span
>多道分离</span
></label
>
<label
><input v-model="displayMode" type="radio" value="compact" /><span
>多道紧凑</span
></label
>
</div>
</section>
<section class="control-section">
<h2>视图</h2>
<Button block aria-label="重置波形视图" @click="resetWaveformViewport">重置视图</Button>
<button
type="button"
class="control-action control-action--block"
aria-label="重置波形视图"
@click="resetWaveformViewport"
>
重置视图
</button>
<div class="auxiliary-style-controls" style="margin-top: 10px">
<label class="frame-style-control frame-style-control--switch">
<span>净图</span>
<Switch v-model:checked="cleanView" size="small" aria-label="净图模式" />
<input
v-model="cleanView"
class="native-switch"
type="checkbox"
role="switch"
aria-label="净图模式"
@click.stop
/>
</label>
</div>
</section>
@@ -381,7 +404,14 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
<section class="control-section">
<div class="control-section__header">
<h2>零值参考线</h2>
<Switch v-model:checked="zeroLineVisible" size="small" aria-label="显示零值参考线" />
<input
v-model="zeroLineVisible"
class="native-switch"
type="checkbox"
role="switch"
aria-label="显示零值参考线"
@click.stop
/>
</div>
<div class="auxiliary-style-controls zero-line-controls" style="margin-top: 10px">
<label class="frame-style-control">
@@ -398,48 +428,73 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
</label>
<label class="frame-style-control">
<span>线宽</span>
<InputNumber
v-model:value="zeroLineWidth"
<input
v-model.number="zeroLineWidth"
class="native-input"
type="number"
:min="0.5"
:max="10"
:step="0.5"
size="small"
aria-label="零值参考线线宽"
@blur="zeroLineWidth = clampNumber(zeroLineWidth, 0.5, 10, 1)"
/>
</label>
<label class="frame-style-control">
<span>线型</span>
<Select
v-model:value="zeroLineDash"
:options="zeroLineDashOptions"
size="small"
aria-label="零值参考线线型"
/>
<select v-model="zeroLineDash" class="native-select" aria-label="零值参考线线型">
<option
v-for="option in zeroLineDashOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
</div>
</section>
<section class="control-section">
<h2>叠加方式</h2>
<Radio.Group
v-model:value="overlayMode"
class="display-mode-control"
button-style="solid"
size="small"
aria-label="波形叠加方式"
<div class="display-mode-control" role="radiogroup" aria-label="波形叠加方式">
<label
><input v-model="overlayMode" type="radio" value="single-axis" /><span
>单值轴</span
></label
>
<Radio.Button value="single-axis">单值轴</Radio.Button>
<Radio.Button value="multi-axis">多值轴</Radio.Button>
</Radio.Group>
<label
><input v-model="overlayMode" type="radio" value="multi-axis" /><span
>多值轴</span
></label
>
</div>
</section>
<section class="control-section">
<h2>图框布局</h2>
<div class="grid-size-control" aria-label="波形网格尺寸">
<InputNumber v-model:value="rowCount" :min="1" :max="10" size="small" />
<input
v-model.number="rowCount"
class="native-input"
type="number"
min="1"
max="10"
step="1"
aria-label="波形网格行数"
@blur="rowCount = clampNumber(rowCount, 1, 10, 2)"
/>
<span></span>
<span class="control-separator">×</span>
<InputNumber v-model:value="columnCount" :min="1" :max="10" size="small" />
<input
v-model.number="columnCount"
class="native-input"
type="number"
min="1"
max="10"
step="1"
aria-label="波形网格列数"
@blur="columnCount = clampNumber(columnCount, 1, 10, 1)"
/>
<span></span>
</div>
</section>
@@ -473,30 +528,38 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
</label>
<label class="frame-style-control">
<span>线宽</span>
<InputNumber
v-model:value="frameBorderWidth"
<input
v-model.number="frameBorderWidth"
class="native-input"
type="number"
:min="0"
:max="10"
:step="0.5"
size="small"
aria-label="图框线宽"
@blur="frameBorderWidth = clampNumber(frameBorderWidth, 0, 10, 1)"
/>
</label>
<label class="frame-style-control">
<span>线型</span>
<Select
v-model:value="frameBorderStyle"
:options="frameBorderStyleOptions"
size="small"
aria-label="图框线型"
/>
<select v-model="frameBorderStyle" class="native-select" aria-label="图框线型">
<option
v-for="option in frameBorderStyleOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
<label class="frame-style-control frame-style-control--switch">
<span>水印</span>
<Switch
v-model:checked="frameWatermarkVisible"
size="small"
<input
v-model="frameWatermarkVisible"
class="native-switch"
type="checkbox"
role="switch"
aria-label="显示图框水印"
@click.stop
/>
</label>
</div>
@@ -505,41 +568,56 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
<section class="control-section title-control-section">
<div class="control-section__header">
<h2>标题</h2>
<Switch v-model:checked="titleVisible" size="small" aria-label="显示标题" />
<input
v-model="titleVisible"
class="native-switch"
type="checkbox"
role="switch"
aria-label="显示标题"
@click.stop
/>
</div>
<div class="title-controls">
<label class="title-control title-control--wide">
<span>标题名称</span>
<Input v-model:value="titleText" size="small" aria-label="标题名称" />
<input v-model="titleText" class="native-input" type="text" aria-label="标题名称" />
</label>
<label class="title-control title-control--wide">
<span>对齐方式</span>
<Select
v-model:value="titleAlign"
:options="titleAlignOptions"
size="small"
aria-label="标题对齐方式"
/>
<select v-model="titleAlign" class="native-select" aria-label="标题对齐方式">
<option
v-for="option in titleAlignOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
<label class="title-control">
<span>字体</span>
<Select
v-model:value="titleFontFamily"
:options="titleFontFamilyOptions"
size="small"
aria-label="标题字体"
/>
<select v-model="titleFontFamily" class="native-select" aria-label="标题字体">
<option
v-for="option in titleFontFamilyOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
<label class="title-control">
<span>字号</span>
<InputNumber
v-model:value="titleFontSize"
<input
v-model.number="titleFontSize"
class="native-input"
type="number"
:min="8"
:max="72"
:step="1"
size="small"
aria-label="标题字号"
@blur="titleFontSize = clampNumber(titleFontSize, 8, 72, 14)"
/>
</label>
<div class="title-control">
@@ -587,14 +665,15 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
</div>
<label class="title-control">
<span>旋转</span>
<InputNumber
v-model:value="titleRotation"
<input
v-model.number="titleRotation"
class="native-input"
type="number"
:min="-180"
:max="180"
:step="1"
addon-after="°"
size="small"
aria-label="标题旋转角度"
@blur="titleRotation = clampNumber(titleRotation, -180, 180, 0)"
/>
</label>
<div class="title-control title-control--color">
@@ -616,21 +695,27 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
<h2>图例</h2>
<label class="select-control">
<span>位置</span>
<Select
v-model:value="legendPosition"
:options="legendPositionOptions"
size="small"
aria-label="图例位置"
/>
<select v-model="legendPosition" class="native-select" aria-label="图例位置">
<option
v-for="option in legendPositionOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
<label class="select-control">
<span>排列</span>
<Select
v-model:value="legendOrientation"
:options="legendOrientationOptions"
size="small"
aria-label="图例排列"
/>
<select v-model="legendOrientation" class="native-select" aria-label="图例排列">
<option
v-for="option in legendOrientationOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
<label class="legend-color-control">
<span>背景</span>

View File

@@ -387,7 +387,7 @@ describe('WaveformChart', () => {
expect(wrapper.find('.waveform-chart__label').exists()).toBe(false)
expect(wrapper.find('.waveform-chart__zero-line').exists()).toBe(false)
expect(wrapper.find('.waveform-annotation-layer').exists()).toBe(false)
expect(wrapper.find('.ant-pagination').exists()).toBe(false)
expect(wrapper.find('.waveform-chart__pagination').exists()).toBe(false)
})
it('preserves every track geometry in a multi-column clean view', async () => {
@@ -804,11 +804,11 @@ describe('WaveformChart', () => {
expect(
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
).toEqual(['channel-0', 'channel-1'])
const previousButton = () => wrapper.get('.ant-pagination-prev button')
const nextButton = () => wrapper.get('.ant-pagination-next button')
const previousButton = () => wrapper.get('[aria-label="上一页"]')
const nextButton = () => wrapper.get('[aria-label="下一页"]')
expect(wrapper.get('.ant-pagination-item-1').classes()).toContain('ant-pagination-item-active')
expect(wrapper.get('.ant-pagination-prev').classes()).toContain('ant-pagination-disabled')
expect(wrapper.get('[aria-current="page"]').text()).toBe('1')
expect(previousButton().attributes('disabled')).toBeDefined()
await nextButton().trigger('click')
expect(
@@ -817,7 +817,7 @@ describe('WaveformChart', () => {
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([2, 3])
await previousButton().trigger('click')
expect(wrapper.get('.ant-pagination-item-1').classes()).toContain('ant-pagination-item-active')
expect(wrapper.get('[aria-current="page"]').text()).toBe('1')
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([1, 3])
await nextButton().trigger('click')
@@ -825,8 +825,8 @@ describe('WaveformChart', () => {
expect(
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
).toEqual(['channel-4'])
expect(wrapper.get('.ant-pagination-item-3').classes()).toContain('ant-pagination-item-active')
expect(wrapper.get('.ant-pagination-next').classes()).toContain('ant-pagination-disabled')
expect(wrapper.get('[aria-current="page"]').text()).toBe('3')
expect(nextButton().attributes('disabled')).toBeDefined()
})
it('overlays series with the same track ID without changing the next frame', async () => {
@@ -905,7 +905,7 @@ describe('WaveformChart', () => {
'1',
'2',
])
expect(wrapper.find('.ant-pagination').exists()).toBe(false)
expect(wrapper.find('.waveform-chart__pagination').exists()).toBe(false)
const firstTrackOverlay = tracks[0].get('.waveform-chart__overlay')
const overlayWidth = Number(firstTrackOverlay.attributes('width'))
@@ -1362,7 +1362,7 @@ describe('WaveformChart', () => {
const initialMargin = wrapper.attributes('data-chart-left-margin')
const initialLabelX = wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')
await wrapper.get('.ant-pagination-next button').trigger('click')
await wrapper.get('[aria-label="下一页"]').trigger('click')
expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe(
@@ -1467,12 +1467,12 @@ describe('WaveformChart', () => {
const wrapper = await mountSizedChart(gridSeries(5), {
grid: { rowCount: 1, columnCount: 1 },
})
await wrapper.get('.ant-pagination-next button').trigger('click')
expect(wrapper.get('.ant-pagination-item-2').classes()).toContain('ant-pagination-item-active')
await wrapper.get('[aria-label="下一页"]').trigger('click')
expect(wrapper.get('[aria-current="page"]').text()).toBe('2')
await wrapper.setProps({ grid: { rowCount: 2, columnCount: 1 } })
await flushPromises()
expect(wrapper.get('.ant-pagination-item-1').classes()).toContain('ant-pagination-item-active')
expect(wrapper.get('[aria-current="page"]').text()).toBe('1')
})
it('keeps the shared x domain stable while paging separated and compact grids', async () => {
@@ -1482,7 +1482,7 @@ describe('WaveformChart', () => {
grid: { rowCount: 1, columnCount: 1 },
})
const initialEnd = wrapper.get('.waveform-chart__axis-endpoint--end').text()
await wrapper.get('.ant-pagination-next button').trigger('click')
await wrapper.get('[aria-label="下一页"]').trigger('click')
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe(initialEnd)
}
})
@@ -1564,7 +1564,7 @@ describe('WaveformChart', () => {
],
})
expect(wrapper.find('[data-annotation-id="channel-2-note"]').exists()).toBe(false)
await wrapper.get('.ant-pagination-next button').trigger('click')
await wrapper.get('[aria-label="下一页"]').trigger('click')
expect(wrapper.find('[data-annotation-id="channel-2-note"]').exists()).toBe(true)
})
it('renders a path for sample data and responds to width changes', async () => {

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { Pagination } from 'ant-design-vue'
import {
bisector,
pointer,
@@ -66,7 +65,6 @@ import {
import {
getGridGap,
getPageCount,
getPageSize,
normalizeGridOptions,
paginateSeries,
resolveGridCellGeometry,
@@ -78,6 +76,7 @@ import { buildTrackLayouts, measureTrackYAxisClearance, Y_AXIS_EXPONENT_GAP } fr
import { calculateRotatedTitleLayout, TITLE_AREA_HORIZONTAL_PADDING } from './core/title'
import { usePreparedWaveformSeries } from './core/useWaveformData'
import WaveformAnnotationEditor from './annotation/WaveformAnnotationEditor.vue'
import WaveformPagination from './WaveformPagination.vue'
import { useWaveformInstanceId } from '../utils/waveformId'
const props = withDefaults(
@@ -1990,15 +1989,11 @@ onBeforeUnmount(() => {
</text>
</svg>
<Pagination
<WaveformPagination
v-if="gridOptions.showPagination && pageCount > 1 && !isCleanView"
class="waveform-chart__pagination"
aria-label="波形分页"
:current="currentPage"
:page-size="getPageSize(gridOptions)"
:total="chartTracks.length"
:show-size-changer="false"
:show-quick-jumper="false"
:page-count="pageCount"
@change="goToPage"
/>
@@ -2097,17 +2092,6 @@ onBeforeUnmount(() => {
transform-origin: center;
}
.waveform-chart__pagination :deep(.ant-pagination-item),
.waveform-chart__pagination :deep(.ant-pagination-prev .ant-pagination-item-link),
.waveform-chart__pagination :deep(.ant-pagination-next .ant-pagination-item-link) {
background: #fff;
border-color: #d9d9d9;
}
.waveform-chart__pagination :deep(.ant-pagination-item-active) {
border-color: #1677ff;
}
.waveform-chart__grid-slot-placeholder {
fill: #fafbfc;
stroke: #e4e7ec;

View File

@@ -0,0 +1,42 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import WaveformPagination from './WaveformPagination.vue'
describe('WaveformPagination', () => {
it('renders all pages and disables the first-page previous button', () => {
const wrapper = mount(WaveformPagination, { props: { current: 1, pageCount: 3 } })
expect(wrapper.findAll('.waveform-pagination__page').map((page) => page.text())).toEqual([
'1',
'2',
'3',
])
expect(wrapper.get('[aria-label="上一页"]').attributes('disabled')).toBeDefined()
expect(wrapper.get('[aria-current="page"]').text()).toBe('1')
})
it('uses ellipses for larger page counts and emits valid page changes', async () => {
const wrapper = mount(WaveformPagination, { props: { current: 5, pageCount: 12 } })
expect(wrapper.findAll('.waveform-pagination__page').map((page) => page.text())).toEqual([
'1',
'4',
'5',
'6',
'12',
])
expect(wrapper.findAll('.waveform-pagination__ellipsis')).toHaveLength(2)
await wrapper.get('[aria-label="第 6 页"]').trigger('click')
expect(wrapper.emitted('change')).toEqual([[6]])
})
it('clamps invalid current values and does not emit when already at a boundary', async () => {
const wrapper = mount(WaveformPagination, { props: { current: 99, pageCount: 3 } })
expect(wrapper.get('[aria-current="page"]').text()).toBe('3')
await wrapper.get('[aria-label="下一页"]').trigger('click')
expect(wrapper.emitted('change')).toBeUndefined()
})
})

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
current: number
pageCount: number
}>()
const emit = defineEmits<{
change: [page: number]
}>()
type PageToken = number | 'start-ellipsis' | 'end-ellipsis'
const normalizedPageCount = computed(() => Math.max(1, Math.floor(props.pageCount) || 1))
const normalizedCurrent = computed(() =>
Math.min(normalizedPageCount.value, Math.max(1, Math.floor(props.current) || 1)),
)
const pageTokens = computed<PageToken[]>(() => {
const pageCount = normalizedPageCount.value
const current = normalizedCurrent.value
if (pageCount <= 7) return Array.from({ length: pageCount }, (_, index) => index + 1)
if (current <= 4) return [1, 2, 3, 4, 5, 'end-ellipsis', pageCount]
if (current >= pageCount - 3) {
return [
1,
'start-ellipsis',
pageCount - 4,
pageCount - 3,
pageCount - 2,
pageCount - 1,
pageCount,
]
}
return [1, 'start-ellipsis', current - 1, current, current + 1, 'end-ellipsis', pageCount]
})
function selectPage(page: number) {
const nextPage = Math.min(normalizedPageCount.value, Math.max(1, Math.floor(page)))
if (nextPage !== normalizedCurrent.value) emit('change', nextPage)
}
</script>
<template>
<nav class="waveform-pagination" aria-label="波形分页">
<button
type="button"
class="waveform-pagination__previous"
aria-label="上一页"
:disabled="normalizedCurrent === 1"
@click="selectPage(normalizedCurrent - 1)"
>
</button>
<template v-for="token in pageTokens" :key="token">
<span
v-if="typeof token !== 'number'"
class="waveform-pagination__ellipsis"
aria-hidden="true"
>
</span>
<button
v-else
type="button"
class="waveform-pagination__page"
:class="{ 'is-active': token === normalizedCurrent }"
:aria-current="token === normalizedCurrent ? 'page' : undefined"
:aria-label="` ${token} `"
@click="selectPage(token)"
>
{{ token }}
</button>
</template>
<button
type="button"
class="waveform-pagination__next"
aria-label="下一页"
:disabled="normalizedCurrent === normalizedPageCount"
@click="selectPage(normalizedCurrent + 1)"
>
</button>
</nav>
</template>
<style scoped>
.waveform-pagination {
display: inline-flex;
gap: 4px;
align-items: center;
padding: 3px;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 1px 2px rgb(16 24 40 / 8%);
}
.waveform-pagination button {
display: inline-grid;
min-width: 28px;
height: 28px;
padding: 0 6px;
color: #344054;
font: inherit;
font-size: 12px;
line-height: 1;
background: #fff;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
place-items: center;
}
.waveform-pagination button:hover:not(:disabled),
.waveform-pagination button:focus-visible {
color: #0958d9;
border-color: #1677ff;
outline: none;
}
.waveform-pagination button.is-active {
color: #0958d9;
background: #e6f4ff;
border-color: #1677ff;
}
.waveform-pagination button:disabled {
color: #bfc4cc;
cursor: not-allowed;
}
.waveform-pagination__ellipsis {
display: inline-grid;
min-width: 20px;
height: 28px;
color: #98a2b3;
font-size: 12px;
place-items: center;
}
</style>

View File

@@ -1,5 +1,4 @@
import { createApp } from 'vue'
import 'ant-design-vue/dist/reset.css'
import App from './App.vue'
import './styles.css'

View File

@@ -17,6 +17,12 @@
box-sizing: border-box;
}
button,
input,
select {
font: inherit;
}
html,
body,
#app {
@@ -91,16 +97,124 @@ body {
margin-bottom: 0;
}
.control-panel .native-input,
.control-panel .native-select {
min-width: 0;
height: 28px;
padding: 4px 8px;
color: #101828;
font-size: 12px;
background: #fff;
border: 1px solid #d0d5dd;
border-radius: 4px;
outline: none;
}
.control-panel .native-input:focus-visible,
.control-panel .native-select:focus-visible,
.control-panel button:focus-visible {
border-color: #1677ff;
outline: 2px solid rgb(22 119 255 / 20%);
outline-offset: 1px;
}
.control-panel .native-switch {
position: relative;
width: 30px;
height: 18px;
margin: 0;
appearance: none;
background: #d0d5dd;
border: 0;
border-radius: 999px;
cursor: pointer;
transition: background-color 120ms ease;
}
.control-panel .native-switch::after {
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
content: '';
background: #fff;
border-radius: 50%;
box-shadow: 0 1px 2px rgb(16 24 40 / 20%);
transition: transform 120ms ease;
}
.control-panel .native-switch:checked {
background: #1677ff;
}
.control-panel .native-switch:checked::after {
transform: translateX(12px);
}
.control-panel .native-switch:focus-visible {
outline: 2px solid rgb(22 119 255 / 35%);
outline-offset: 2px;
}
.control-action,
.mobile-control-toggle,
.control-panel__close {
padding: 5px 10px;
color: #344054;
font-size: 12px;
line-height: 18px;
background: #fff;
border: 1px solid #d0d5dd;
border-radius: 4px;
cursor: pointer;
}
.control-action:hover,
.mobile-control-toggle:hover,
.control-panel__close:hover {
color: #0958d9;
border-color: #1677ff;
}
.control-action--block {
width: 100%;
}
.display-mode-control {
display: flex;
width: 100%;
white-space: nowrap;
overflow: hidden;
border: 1px solid #d0d5dd;
border-radius: 4px;
}
.display-mode-control .ant-radio-button-wrapper {
.display-mode-control label {
position: relative;
flex: 1 1 0;
padding-inline: 7px;
min-width: 0;
padding: 5px 7px;
color: #475467;
font-size: 12px;
text-align: center;
cursor: pointer;
}
.display-mode-control label + label {
border-left: 1px solid #d0d5dd;
}
.display-mode-control input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.display-mode-control input:checked + span {
color: #0958d9;
font-weight: 600;
}
.grid-size-control {
@@ -112,7 +226,7 @@ body {
font-size: 12px;
}
.grid-size-control .ant-input-number {
.grid-size-control .native-input {
width: 58px;
}
@@ -134,7 +248,7 @@ body {
margin-top: 10px;
}
.select-control .ant-select {
.select-control .native-select {
width: 100%;
}
@@ -171,12 +285,12 @@ body {
font-size: 12px;
}
.frame-style-control .ant-input-number,
.frame-style-control .ant-select {
.frame-style-control .native-input,
.frame-style-control .native-select {
width: 100%;
}
.frame-style-control--switch .ant-switch {
.frame-style-control--switch .native-switch {
width: auto;
justify-self: start;
}
@@ -210,8 +324,8 @@ body {
grid-column: 1 / -1;
}
.title-control .ant-select,
.title-control .ant-input-number {
.title-control .native-select,
.title-control .native-input {
width: 100%;
}

View File

@@ -3,7 +3,7 @@ import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
const peerDependencies = ['vue', 'd3', 'ant-design-vue', 'vue3-colorpicker']
const peerDependencies = ['vue', 'd3', 'vue3-colorpicker']
export default defineConfig({
plugins: [vue()],