This commit is contained in:
2026-08-09 00:33:21 +08:00
parent 6be5ebc8f7
commit 9ea606b175
133 changed files with 7300 additions and 6289 deletions
+15 -14
View File
@@ -19,19 +19,19 @@ mean recreating the effect from screenshots.
| ----------------------- | --------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- | | ----------------------- | --------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- |
| `foundations`, `shared` | `style/tokens`, `style/themes`, shared UI contract | Migrated | Semantic color and overridable geometry contracts | | `foundations`, `shared` | `style/tokens`, `style/themes`, shared UI contract | Migrated | Semantic color and overridable geometry contracts |
| `button` | `ui/button` | Migrated | Preserved three-slice states; exposed density and width | | `button` | `ui/button` | Migrated | Preserved three-slice states; exposed density and width |
| `input` | `ui/input`, `ui/number-field` | Migrated | Removed forced inline width; normalized text metrics | | `input` | `ui/input`, `ui/number-field`, `ui/command-input` | Migrated | Removed forced inline width; normalized text metrics; retained FastStep and command-prefix anatomy |
| `form-controls` | `ui/checkbox`, `ui/radio-group`, `ui/switch`, `ui/rating` | Migrated | Tokenized state colors and aligned label/control anatomy | | `form-controls` | `ui/checkbox`, `ui/radio-group`, `ui/switch`, `ui/rating` | Migrated | Tokenized state colors and aligned label/control anatomy |
| `select` | `ui/select`, `ui/tree-select`, `ui/cascader` | Migrated | Flat, hierarchical and two-level responsive selection; TreeSelect preserves parent-toggle and rarity states | | `select` | `ui/select`, `ui/tree-select`, `ui/cascader` | Migrated | Flat, hierarchical and two-level responsive selection; TreeSelect preserves parent-toggle and rarity states |
| `tabs` | `ui/tabs`, `ui/pagination` | Migrated | Animated list marker and compact result navigation | | `tabs` | `ui/tabs`, `ui/pagination` | Migrated | Animated list marker and compact result navigation |
| `slider` | `ui/slider` | Migrated | Preserved the Reka coordinate system | | `slider` | `ui/slider` | Migrated | Preserved the Reka coordinate system |
| `data-table` | `ui/data-table` | Migrated | Ported hover, selected and density behavior | | `data-table` | `ui/data-table`, `ui/progress` | Migrated | Added compact density, row eligibility, note/summary/footer rows, empty state and multi-marker progress |
| `dialog` | `ui/dialog` | Migrated | Preserved the focused title effect and focus management | | `dialog` | `ui/dialog`, `ui/utility-sheet` | Migrated | Kept WindowDialog and UtilitySheet as separate families while sharing focus-safe dialog chrome |
| `menu` | `ui/dropdown-menu` | Migrated | Ported source menu spacing and state tokens | | `menu` | `ui/dropdown-menu`, `ui/settings-dropdown` | Migrated | Preserved the distinct compact-context and full settings-menu anatomies |
| `rarity` | `ui/item-slot` | Migrated | Retained rarity presentation as data-semantic tokens | | `rarity` | `ui/item-slot`, `ui/notice-badge` | Migrated | Retained rarity data semantics plus the native Notice raster and independent Level badge family |
| `autocomplete` | `ui/autocomplete` | Partial | Combobox is migrated; domain SearchTree remains deferred | | `autocomplete` | `ui/autocomplete`, `ui/search-tree` | Migrated | Kept flat Combobox and toolbar-plus-tree search as separate reusable families |
| `navigation` | `ui/navigation` | Migrated | Selectable navigation primitive with source assets | | `navigation` | `ui/navigation`, `ui/favorite-drawer` | Migrated | Preserved selectable SettingsNav and removable, scrollable FavoriteDrawer composition |
| `scrollbar` | `ui/scroll-area` | Migrated | Reusable Reka viewport/scrollbar composition | | `scrollbar` | `ui/scroll-area` | Migrated | Reusable Reka viewport/scrollbar composition |
| shared overlay help | `ui/popover`, rich `ui/tooltip` | Migrated | Portal z-index, collision and responsive width contract | | shared overlay help | `ui/popover`, simple/rich `ui/tooltip` | Migrated | Portal z-index, collision and responsive width contracts for both tooltip anatomies |
| `data-section` | `ui/data-section` | Migrated | Collapsible section and vertically centered data rows | | `data-section` | `ui/data-section` | Migrated | Collapsible section and vertically centered data rows |
| `shop-item-row` | `ui/shop-item-row` | Migrated | Ported source row structure and selectable states | | `shop-item-row` | `ui/shop-item-row` | Migrated | Ported source row structure and selectable states |
| `complex-dialog` | `ui/dialog` + migrated primitives | Composition | Kept domain-independent by composing existing primitives | | `complex-dialog` | `ui/dialog` + migrated primitives | Composition | Kept domain-independent by composing existing primitives |
@@ -39,7 +39,8 @@ mean recreating the effect from screenshots.
## Legacy replacement order ## Legacy replacement order
Deferred: this component-library pass does not change business call sites. Business replacement is now in progress. Each slice keeps its route, events, API calls and stored
values, and a legacy component is deleted only after repository-wide references reach zero.
1. Shared form controls and overlays used by DNF Helper and import dialogs. 1. Shared form controls and overlays used by DNF Helper and import dialogs.
2. Forge controls (`select`, `checkbox`, `slider`, tabs and panels). 2. Forge controls (`select`, `checkbox`, `slider`, tabs and panels).
@@ -49,9 +50,9 @@ Deferred: this component-library pass does not change business call sites.
## Remaining gaps ## Remaining gaps
- SearchTree/CommandInput still requires a domain-neutral data and filtering contract before it can - `SkillDamageDetail` and `ItemRow` remain domain compositions on top of `ui/data-table`; the
join `src/components/ui`. generic table contract now exposes the structural slots they require.
- Complex product dialogs are compositions, not a claim that every business dialog has reached - Complex product dialogs remain compositions, not a claim that every business dialog has reached
source-level visual parity. source-level visual parity.
- Existing `frontend` business call sites remain on their current components until each behavior - Business call sites in the active routes are migrated, the superseded equipment page is removed,
slice is migrated and browser-verified. and the repository-wide `components/calc` call-site check is now at zero.
-1
View File
@@ -20,7 +20,6 @@
"axios": "^1.8.2", "axios": "^1.8.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"hoci": "^0.6.0",
"pako": "^2.1.0", "pako": "^2.1.0",
"pinia": "^4.0.2", "pinia": "^4.0.2",
"query-string": "^9.1.1", "query-string": "^9.1.1",
-390
View File
@@ -20,9 +20,6 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
hoci:
specifier: ^0.6.0
version: 0.6.0(@babel/parser@7.29.8)(@vueuse/core@14.4.0(vue@3.5.40(typescript@6.0.3)))(rollup@4.35.0)(vue@3.5.40(typescript@6.0.3))
pako: pako:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
@@ -102,9 +99,6 @@ importers:
packages: packages:
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
'@babel/code-frame@7.29.7': '@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -272,23 +266,6 @@ packages:
'@floating-ui/vue@1.1.11': '@floating-ui/vue@1.1.11':
resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==} resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
'@hoci/components@0.6.0':
resolution: {integrity: sha512-FKWh6JmV5iezDGdQq/UOQ6PfJNRzWfjrN6C6s8Q2QoQMJTC3wAQXJjsKP+OsO2tdizYA1NBBwfow4Oi2X6GZcg==}
peerDependencies:
'@vueuse/core': '>=9.0.0'
vue: ^3.0.0-0
'@hoci/core@0.6.0':
resolution: {integrity: sha512-lFV5gFNNzdH5cTmXHonKRaRKt0FDZX9kQ18xwWP89yoDAXrpXbRt5ZK8oQgqCHdWYtwDcVrAMZ+IZmhkCBiO6g==}
peerDependencies:
vue: ^3.0.0-0
'@hoci/shared@0.6.0':
resolution: {integrity: sha512-XreSiu+AFisDKorgugX8zjNEsv889U6SbbkTLPHlYIcLvSYk33TmwQ5jHpHe1RAE2KE2Xxn9fBqYRlOCAn8ujg==}
peerDependencies:
'@vueuse/core': '>=9.0.0'
vue: ^3.0.0-0
'@internationalized/date@3.12.3': '@internationalized/date@3.12.3':
resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==}
@@ -325,18 +302,6 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-project/types@0.142.0': '@oxc-project/types@0.142.0':
resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==}
@@ -737,15 +702,6 @@ packages:
'@rolldown/pluginutils@1.0.1': '@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@rollup/pluginutils@5.1.4':
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.35.0': '@rollup/rollup-android-arm-eabi@4.35.0':
resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==}
cpu: [arm] cpu: [arm]
@@ -957,9 +913,6 @@ packages:
'@types/pako@2.0.3': '@types/pako@2.0.3':
resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==} resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
'@types/web-bluetooth@0.0.15':
resolution: {integrity: sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==}
'@types/web-bluetooth@0.0.21': '@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@@ -1075,23 +1028,14 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.5.0 vue: ^3.5.0
'@vueuse/core@9.0.0':
resolution: {integrity: sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==}
'@vueuse/metadata@14.4.0': '@vueuse/metadata@14.4.0':
resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==}
'@vueuse/metadata@9.0.0':
resolution: {integrity: sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ==}
'@vueuse/shared@14.4.0': '@vueuse/shared@14.4.0':
resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==}
peerDependencies: peerDependencies:
vue: ^3.5.0 vue: ^3.5.0
'@vueuse/shared@9.0.0':
resolution: {integrity: sha512-WRCyr/wIz5e/2gR/+qFucbCUcGMyJKkQZAzlECl3e71ebQQ9X/w3aBWT9FbnogJX+DNZ/t3Pj+TqPbC7TH1Yog==}
acorn@8.14.1: acorn@8.14.1:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@@ -1109,10 +1053,6 @@ packages:
resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==} resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==}
engines: {node: '>=22'} engines: {node: '>=22'}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
aria-hidden@1.2.6: aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1131,19 +1071,9 @@ packages:
axios@1.8.2: axios@1.8.2:
resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==} resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
birpc@2.9.0: birpc@2.9.0:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1160,10 +1090,6 @@ packages:
caniuse-lite@1.0.30001702: caniuse-lite@1.0.30001702:
resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==} resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
chokidar@5.0.0: chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
@@ -1192,9 +1118,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1211,9 +1134,6 @@ packages:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
defu@6.1.7: defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
@@ -1282,13 +1202,6 @@ packages:
exsolve@1.1.1: exsolve@1.1.1:
resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -1339,10 +1252,6 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
gopd@1.2.0: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1362,21 +1271,12 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hoci@0.6.0:
resolution: {integrity: sha512-PtSPRtQm5/OUKxSU8lVelhBc+3TVW2cB2hGlG9pfzQ0rbSBFgLHWSLImBgLZnau72wKOrMNjBxzprQ941+rTWw==}
peerDependencies:
vue: ^3.0.0
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
immutable@5.1.9: immutable@5.1.9:
resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-extglob@2.1.1: is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1560,10 +1460,6 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
local-pkg@0.5.1:
resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
engines: {node: '>=14'}
local-pkg@1.2.1: local-pkg@1.2.1:
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -1575,9 +1471,6 @@ packages:
resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1588,17 +1481,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
maybe-types@0.2.0:
resolution: {integrity: sha512-4ul+RHPcxNznI+Ke6xUT2/mcQTl54fYUdRZQ3QNkQsC4USfAxXkPTh+wEOx4UBsXYg214867v2ZeirQDJT7uOA==}
memorystream@0.3.1: memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8: micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
@@ -1611,10 +1497,6 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
mlly@1.7.4: mlly@1.7.4:
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
@@ -1638,10 +1520,6 @@ packages:
node-releases@2.0.19: node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
nostics@1.2.0: nostics@1.2.0:
resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==} resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==}
@@ -1753,17 +1631,10 @@ packages:
resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==} resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==}
engines: {node: '>=18'} engines: {node: '>=18'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-package-json-fast@6.0.0: read-package-json-fast@6.0.0:
resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==} resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==}
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
readdirp@5.0.0: readdirp@5.0.0:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
@@ -1773,10 +1644,6 @@ packages:
peerDependencies: peerDependencies:
vue: '>= 3.4.0' vue: '>= 3.4.0'
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rolldown@1.2.2: rolldown@1.2.2:
resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -1787,9 +1654,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
sass@1.102.0: sass@1.102.0:
resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==} resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
@@ -1850,13 +1714,6 @@ packages:
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tslx@0.1.1:
resolution: {integrity: sha512-MYh2I7iw8owRx3xFVq8DXp3JWoJ7G7oMA5uUrCwGvuOipV6w9XvlJKo3yVJMh75Y+5caIaYhCsUZ1WTnxQRk7A==}
type-fest@4.38.0:
resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==}
engines: {node: '>=16'}
typescript@6.0.3: typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -1899,19 +1756,6 @@ packages:
resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
unplugin-vue-components@0.27.5:
resolution: {integrity: sha512-m9j4goBeNwXyNN8oZHHxvIIYiG8FQ9UfmKWeNllpDvhU7btKNNELGPt+o3mckQKuPwrE7e0PvCsx+IWuDSD9Vg==}
engines: {node: '>=14'}
peerDependencies:
'@babel/parser': ^7.15.8
'@nuxt/kit': ^3.2.2
vue: 2 || 3
peerDependenciesMeta:
'@babel/parser':
optional: true
'@nuxt/kit':
optional: true
unplugin-vue-components@32.1.0: unplugin-vue-components@32.1.0:
resolution: {integrity: sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==} resolution: {integrity: sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==}
engines: {node: '>=20.19.0'} engines: {node: '>=20.19.0'}
@@ -1922,10 +1766,6 @@ packages:
'@nuxt/kit': '@nuxt/kit':
optional: true optional: true
unplugin@1.16.1:
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
engines: {node: '>=14.0.0'}
unplugin@3.3.0: unplugin@3.3.0:
resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -2077,9 +1917,6 @@ packages:
snapshots: snapshots:
'@antfu/utils@0.7.10':
optional: true
'@babel/code-frame@7.29.7': '@babel/code-frame@7.29.7':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.29.7 '@babel/helper-validator-identifier': 7.29.7
@@ -2302,43 +2139,6 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@hoci/components@0.6.0(@vueuse/core@14.4.0(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))':
dependencies:
'@hoci/core': 0.6.0(vue@3.5.40(typescript@6.0.3))
'@hoci/shared': 0.6.0(@vueuse/core@14.4.0(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))
'@vueuse/core': 14.4.0(vue@3.5.40(typescript@6.0.3))
maybe-types: 0.2.0
tslx: 0.1.1
vue: 3.5.40(typescript@6.0.3)
transitivePeerDependencies:
- '@vue/composition-api'
'@hoci/core@0.6.0(vue@3.5.40(typescript@6.0.3))':
dependencies:
'@hoci/shared': 0.6.0(@vueuse/core@9.0.0(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))
'@vueuse/core': 9.0.0(vue@3.5.40(typescript@6.0.3))
maybe-types: 0.2.0
tslx: 0.1.1
vue: 3.5.40(typescript@6.0.3)
transitivePeerDependencies:
- '@vue/composition-api'
'@hoci/shared@0.6.0(@vueuse/core@14.4.0(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))':
dependencies:
'@vueuse/core': 14.4.0(vue@3.5.40(typescript@6.0.3))
defu: 6.1.4
maybe-types: 0.2.0
type-fest: 4.38.0
vue: 3.5.40(typescript@6.0.3)
'@hoci/shared@0.6.0(@vueuse/core@9.0.0(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))':
dependencies:
'@vueuse/core': 9.0.0(vue@3.5.40(typescript@6.0.3))
defu: 6.1.4
maybe-types: 0.2.0
type-fest: 4.38.0
vue: 3.5.40(typescript@6.0.3)
'@internationalized/date@3.12.3': '@internationalized/date@3.12.3':
dependencies: dependencies:
'@swc/helpers': 0.5.23 '@swc/helpers': 0.5.23
@@ -2381,21 +2181,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
optional: true
'@nodelib/fs.stat@2.0.5':
optional: true
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
optional: true
'@oxc-project/types@0.142.0': {} '@oxc-project/types@0.142.0': {}
'@oxfmt/binding-android-arm-eabi@0.62.0': '@oxfmt/binding-android-arm-eabi@0.62.0':
@@ -2617,15 +2402,6 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {} '@rolldown/pluginutils@1.0.1': {}
'@rollup/pluginutils@5.1.4(rollup@4.35.0)':
dependencies:
'@types/estree': 1.0.6
estree-walker: 2.0.2
picomatch: 4.0.2
optionalDependencies:
rollup: 4.35.0
optional: true
'@rollup/rollup-android-arm-eabi@4.35.0': '@rollup/rollup-android-arm-eabi@4.35.0':
optional: true optional: true
@@ -2774,8 +2550,6 @@ snapshots:
'@types/pako@2.0.3': {} '@types/pako@2.0.3': {}
'@types/web-bluetooth@0.0.15': {}
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue-jsx@5.1.6(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3))': '@vitejs/plugin-vue-jsx@5.1.6(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3))':
@@ -2951,31 +2725,12 @@ snapshots:
'@vueuse/shared': 14.4.0(vue@3.5.40(typescript@6.0.3)) '@vueuse/shared': 14.4.0(vue@3.5.40(typescript@6.0.3))
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
'@vueuse/core@9.0.0(vue@3.5.40(typescript@6.0.3))':
dependencies:
'@types/web-bluetooth': 0.0.15
'@vueuse/metadata': 9.0.0
'@vueuse/shared': 9.0.0(vue@3.5.40(typescript@6.0.3))
vue-demi: 0.14.10(vue@3.5.40(typescript@6.0.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@14.4.0': {} '@vueuse/metadata@14.4.0': {}
'@vueuse/metadata@9.0.0': {}
'@vueuse/shared@14.4.0(vue@3.5.40(typescript@6.0.3))': '@vueuse/shared@14.4.0(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
'@vueuse/shared@9.0.0(vue@3.5.40(typescript@6.0.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.40(typescript@6.0.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
acorn@8.14.1: {} acorn@8.14.1: {}
acorn@8.18.0: {} acorn@8.18.0: {}
@@ -2984,12 +2739,6 @@ snapshots:
ansi-styles@7.0.0: {} ansi-styles@7.0.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
optional: true
aria-hidden@1.2.6: aria-hidden@1.2.6:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -3015,19 +2764,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
balanced-match@1.0.2:
optional: true
binary-extensions@2.3.0:
optional: true
birpc@2.9.0: {} birpc@2.9.0: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
optional: true
braces@3.0.3: braces@3.0.3:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
@@ -3047,19 +2785,6 @@ snapshots:
caniuse-lite@1.0.30001702: {} caniuse-lite@1.0.30001702: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
optional: true
chokidar@5.0.0: chokidar@5.0.0:
dependencies: dependencies:
readdirp: 5.0.0 readdirp: 5.0.0
@@ -3086,8 +2811,6 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
csstype@3.1.3: {}
csstype@3.2.3: {} csstype@3.2.3: {}
debug@4.4.0: debug@4.4.0:
@@ -3096,8 +2819,6 @@ snapshots:
decode-uri-component@0.4.1: {} decode-uri-component@0.4.1: {}
defu@6.1.4: {}
defu@6.1.7: {} defu@6.1.7: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
@@ -3151,20 +2872,6 @@ snapshots:
exsolve@1.1.1: {} exsolve@1.1.1: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
optional: true
fastq@1.19.1:
dependencies:
reusify: 1.1.0
optional: true
fdir@6.5.0(picomatch@4.0.5): fdir@6.5.0(picomatch@4.0.5):
optionalDependencies: optionalDependencies:
picomatch: 4.0.5 picomatch: 4.0.5
@@ -3210,11 +2917,6 @@ snapshots:
dunder-proto: 1.0.1 dunder-proto: 1.0.1
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
optional: true
gopd@1.2.0: {} gopd@1.2.0: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
@@ -3229,30 +2931,10 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hoci@0.6.0(@babel/parser@7.29.8)(@vueuse/core@14.4.0(vue@3.5.40(typescript@6.0.3)))(rollup@4.35.0)(vue@3.5.40(typescript@6.0.3)):
dependencies:
'@hoci/components': 0.6.0(@vueuse/core@14.4.0(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))
'@hoci/core': 0.6.0(vue@3.5.40(typescript@6.0.3))
vue: 3.5.40(typescript@6.0.3)
optionalDependencies:
unplugin-vue-components: 0.27.5(@babel/parser@7.29.8)(rollup@4.35.0)(vue@3.5.40(typescript@6.0.3))
transitivePeerDependencies:
- '@babel/parser'
- '@nuxt/kit'
- '@vue/composition-api'
- '@vueuse/core'
- rollup
- supports-color
hookable@5.5.3: {} hookable@5.5.3: {}
immutable@5.1.9: {} immutable@5.1.9: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
optional: true
is-extglob@2.1.1: is-extglob@2.1.1:
optional: true optional: true
@@ -3378,12 +3060,6 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0
local-pkg@0.5.1:
dependencies:
mlly: 1.7.4
pkg-types: 1.3.1
optional: true
local-pkg@1.2.1: local-pkg@1.2.1:
dependencies: dependencies:
mlly: 1.7.4 mlly: 1.7.4
@@ -3398,11 +3074,6 @@ snapshots:
dependencies: dependencies:
magic-string: 0.30.21 magic-string: 0.30.21
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
optional: true
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -3413,13 +3084,8 @@ snapshots:
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
maybe-types@0.2.0: {}
memorystream@0.3.1: {} memorystream@0.3.1: {}
merge2@1.4.1:
optional: true
micromatch@4.0.8: micromatch@4.0.8:
dependencies: dependencies:
braces: 3.0.3 braces: 3.0.3
@@ -3432,11 +3098,6 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
optional: true
mlly@1.7.4: mlly@1.7.4:
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.14.1
@@ -3462,9 +3123,6 @@ snapshots:
node-releases@2.0.19: {} node-releases@2.0.19: {}
normalize-path@3.0.0:
optional: true
nostics@1.2.0: {} nostics@1.2.0: {}
npm-normalize-package-bin@6.0.0: {} npm-normalize-package-bin@6.0.0: {}
@@ -3587,19 +3245,11 @@ snapshots:
filter-obj: 5.1.0 filter-obj: 5.1.0
split-on-first: 3.0.0 split-on-first: 3.0.0
queue-microtask@1.2.3:
optional: true
read-package-json-fast@6.0.0: read-package-json-fast@6.0.0:
dependencies: dependencies:
json-parse-even-better-errors: 6.0.0 json-parse-even-better-errors: 6.0.0
npm-normalize-package-bin: 6.0.0 npm-normalize-package-bin: 6.0.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
optional: true
readdirp@5.0.0: {} readdirp@5.0.0: {}
reka-ui@2.10.1(vue@3.5.40(typescript@6.0.3)): reka-ui@2.10.1(vue@3.5.40(typescript@6.0.3)):
@@ -3618,9 +3268,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
reusify@1.1.0:
optional: true
rolldown@1.2.2: rolldown@1.2.2:
dependencies: dependencies:
'@oxc-project/types': 0.142.0 '@oxc-project/types': 0.142.0
@@ -3667,11 +3314,6 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
optional: true optional: true
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
optional: true
sass@1.102.0: sass@1.102.0:
dependencies: dependencies:
chokidar: 5.0.0 chokidar: 5.0.0
@@ -3720,12 +3362,6 @@ snapshots:
tslib@2.8.1: {} tslib@2.8.1: {}
tslx@0.1.1:
dependencies:
csstype: 3.1.3
type-fest@4.38.0: {}
typescript@6.0.3: {} typescript@6.0.3: {}
ufo@1.5.4: {} ufo@1.5.4: {}
@@ -3789,26 +3425,6 @@ snapshots:
pathe: 2.0.3 pathe: 2.0.3
picomatch: 4.0.5 picomatch: 4.0.5
unplugin-vue-components@0.27.5(@babel/parser@7.29.8)(rollup@4.35.0)(vue@3.5.40(typescript@6.0.3)):
dependencies:
'@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.1.4(rollup@4.35.0)
chokidar: 3.6.0
debug: 4.4.0
fast-glob: 3.3.3
local-pkg: 0.5.1
magic-string: 0.30.17
minimatch: 9.0.5
mlly: 1.7.4
unplugin: 1.16.1
vue: 3.5.40(typescript@6.0.3)
optionalDependencies:
'@babel/parser': 7.29.8
transitivePeerDependencies:
- rollup
- supports-color
optional: true
unplugin-vue-components@32.1.0(rolldown@1.2.2)(rollup@4.35.0)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)): unplugin-vue-components@32.1.0(rolldown@1.2.2)(rollup@4.35.0)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)):
dependencies: dependencies:
chokidar: 5.0.0 chokidar: 5.0.0
@@ -3832,12 +3448,6 @@ snapshots:
- vite - vite
- webpack - webpack
unplugin@1.16.1:
dependencies:
acorn: 8.14.1
webpack-virtual-modules: 0.6.2
optional: true
unplugin@3.3.0(rolldown@1.2.2)(rollup@4.35.0)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)): unplugin@3.3.0(rolldown@1.2.2)(rollup@4.35.0)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)):
dependencies: dependencies:
'@jridgewell/remapping': 2.3.5 '@jridgewell/remapping': 2.3.5
-1
View File
@@ -1 +0,0 @@
DNF风格定制组件
@@ -1,207 +0,0 @@
<script lang="tsx">
import { onKeyStroke, syncRef, useVModel } from '@vueuse/core'
import type { PropType } from 'vue'
import { defineComponent, ref, renderSlot } from 'vue'
import CalcDialog from '@/components/calc/dialog/index.vue'
import CalcButton from '@/components/calc/button/index.vue'
import type { JSX } from 'vue/jsx-runtime'
export type ActionDialogResult = 'ok' | 'reject' | 'cancel' | 'none'
export default defineComponent({
name: 'IActionDialog',
props: {
visible: {
type: Boolean,
default: () => false,
},
class: {
type: String,
},
mask: {
type: Boolean,
default: () => false,
},
// 模态框
modal: {
type: Boolean,
default: () => false,
},
header: {
type: [String, Boolean] as PropType<string | boolean>,
default: () => true,
},
drag: {
type: String as PropType<'header' | 'all' | 'none'>,
default: () => 'header',
},
okButton: {
type: [String, Boolean],
default: '确定',
},
rejectButton: {
type: [String, Boolean],
default: false,
},
cancelButton: {
type: [String, Boolean],
default: '取消',
},
closeOnYes: {
type: Boolean,
default: () => true,
},
closeOnCancel: {
type: Boolean,
default: () => true,
},
closeOnReject: {
type: Boolean,
default: () => true,
},
closeOnEsc: {
type: Boolean,
default: () => true,
},
cache: {
// 维持窗口状态
type: Boolean,
default: () => true,
},
defaultStatus: {
type: String as PropType<ActionDialogResult>,
default: () => 'cancel',
},
appendTo: {
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
default: 'body',
},
scale: {
type: Number,
default: 1.0,
},
},
emits: ['close', 'ok', 'cancel', 'action', 'update:visible'],
setup(props, { emit, slots }) {
const visible = ref(props.visible)
syncRef(useVModel(props, 'visible'), visible, { direction: 'both' })
const result = ref<ActionDialogResult>(props.defaultStatus)
function close() {
visible.value = false
emit('close', result.value)
}
onKeyStroke('ESC', (e: Event) => {
if (visible.value && props.closeOnEsc) {
e.preventDefault()
close()
}
})
function onOkClick() {
result.value = 'ok'
if (props.closeOnYes) {
close()
}
}
function onRejectClick() {
result.value = 'reject'
if (props.closeOnReject) {
close()
}
}
function onCancelClick() {
result.value = 'cancel'
if (props.closeOnCancel) {
close()
}
}
function onCloseClick() {
close()
}
function renderAction() {
if ((props.okButton || props.cancelButton || props.rejectButton) && !slots.action) {
const buttons: JSX.Element[] = []
if (props.okButton) {
buttons.push(
<CalcButton class="mx-2px" type="primary" onClick={onOkClick}>
{props.okButton === true ? '确定' : props.okButton}
</CalcButton>,
)
}
if (props.rejectButton) {
buttons.push(
<CalcButton class="mx-2px" type="primary" onClick={onRejectClick}>
{props.rejectButton === true ? '拒绝' : props.rejectButton}
</CalcButton>,
)
}
if (props.cancelButton) {
buttons.push(
<CalcButton class="mx-2px" onClick={onCancelClick}>
{props.cancelButton === true ? '取消' : props.cancelButton}
</CalcButton>,
)
}
return <div class="flex pb-2 items-center justify-center">{buttons}</div>
}
return renderSlot(slots, 'action')
}
return () => {
return (
<CalcDialog
onClose={onCloseClick}
v-model:visible={visible.value}
drag={props.drag}
mask={props.mask}
modal={props.modal}
header={props.header == true ? '提示' : props.header}
appendTo={props.appendTo}
scale={props.scale}
>
<div class={['flex items-center justify-center']}> {renderSlot(slots, 'default')}</div>
{renderAction()}
</CalcDialog>
)
}
},
})
</script>
<style lang="scss" scoped>
.i-button {
text-shadow: none;
color: #a89679;
background: linear-gradient(#12396b, #081f3c);
border: 1px solid #554a3d;
&:hover {
color: #fee97d;
background: linear-gradient(#2d4e9f, #153051);
// border: 1px solid #e3c18a;
color: #d4d4b6;
background: linear-gradient(#195cab, #0b3260);
border: 1px solid #867760;
}
&:active {
color: #e9c556;
background: linear-gradient(#2d4e9f, #122438);
border: 1px solid #e3c18a;
color: #d4d4b6;
background: linear-gradient(#195cab, #0b3260);
border: 1px solid #918168;
}
}
</style>
@@ -1,250 +0,0 @@
<script lang="tsx">
import { debouncedWatch, onClickOutside, useVModel } from '@vueuse/core'
import type { CSSProperties, PropType } from 'vue'
import {
Teleport,
Transition,
computed,
defineComponent,
onDeactivated,
ref,
renderList,
watch,
} from 'vue'
import { getElementZoom } from '@/utils/position'
type HandleChangeFunction = (value: string | number) => string[] | Promise<string[]>
export default defineComponent({
props: {
modelValue: {
type: [String, Number] as PropType<string | number>,
default: () => '',
},
placeholder: {
type: String,
default: '',
},
handleChange: {
type: Function as PropType<HandleChangeFunction>,
default: () => () => [],
},
delay: {
type: Number,
default: 300,
},
},
setup(props, { emit }) {
const modelValue = useVModel(props, 'modelValue')
const isOpen = ref(false)
const triggerRef = ref<HTMLElement>()
watch(isOpen, onResize)
// 下拉框位置
const dropdownPosition = ref({ x: 0, y: 0, w: 0 })
// 下拉框位置
const dropdownStyle = computed<CSSProperties>(() => {
return {
left: `${dropdownPosition.value.x}px`,
top: `${dropdownPosition.value.y}px`,
width: `${dropdownPosition.value.w}px`,
}
})
function onResize() {
if (triggerRef.value) {
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
const zoom = getElementZoom('body')
dropdownPosition.value = {
w: width / zoom,
x: left / zoom,
y: (top + height + 2) / zoom,
}
}
}
onClickOutside(triggerRef, () => (isOpen.value = false))
window.addEventListener('resize', onResize)
window.addEventListener('scroll', onResize)
onDeactivated(() => {
window.removeEventListener('resize', onResize)
window.removeEventListener('scroll', onResize)
})
function selectItem(item: string) {
return () => {
modelValue.value = item
}
}
const delay = computed(() => props.delay)
const droplist = ref<string[]>([])
debouncedWatch(
modelValue,
async (value) => (droplist.value = await props.handleChange(value)),
{
debounce: delay,
},
)
function handleInput() {
isOpen.value = droplist.value.length > 0
}
function handleKeyPress(event: KeyboardEvent) {
if (event.code == 'Enter') {
emit('enter', modelValue.value)
}
}
return () => {
return (
<div class="i-autocomplete">
<div class="i-autocomplete-trigger" ref={triggerRef}>
<input
onKeypress={handleKeyPress}
class="i-autocomplete-input"
onFocus={handleInput}
onInput={handleInput}
type="text"
v-model={modelValue.value}
placeholder={props.placeholder}
/>
</div>
<Teleport to="body">
<Transition name="dropdown" mode="out-in">
<div
class="i-autocomplete-dropdown"
style={dropdownStyle.value}
v-show={isOpen.value}
>
{renderList(droplist.value, (item) => (
<div class="i-autocomplete-dropdown-item" onClick={selectItem(item)}>
{item}
</div>
))}
</div>
</Transition>
</Teleport>
</div>
)
}
},
})
</script>
<style scoped lang="scss">
$text-color: #937639;
.i-autocomplete {
min-width: 80px;
width: 160px;
user-select: none;
outline: none;
height: 24px;
min-height: 16px;
line-height: 100%;
// font-size: 12px;
color: $text-color;
background-color: rgba(0, 0, 0, 1);
border: 1px solid #5b472a;
border-radius: 2px;
display: block;
.i-autocomplete-trigger {
display: flex;
height: 100%;
opacity: 0.8;
justify-content: space-between;
align-items: center;
.i-autocomplete-input {
padding-left: 8px;
width: 100%;
border: none;
outline: none;
color: inherit;
// font-size: 12px;
background-color: black;
font-family: SimSun;
}
&.disabled {
color: gray;
.i-select-down-icon {
background-image: url('./img/select_down_disabled.png');
}
}
}
.i-autocomplete-trigger:hover {
opacity: 1;
}
}
.i-autocomplete-dropdown {
position: fixed;
max-height: 160px;
overflow-y: auto;
background: black;
// font-size: 12px;
z-index: 888;
$hoverColor: #002947;
$activeColor: color.scale($hoverColor, $lightness: 5%);
color: $text-color;
.i-autocomplete-dropdown-item {
// font-size: 12px;
height: 20px;
line-height: 20px;
margin: 0;
padding: 0 8px;
border: none;
outline: none;
appearance: none;
display: block;
overflow: hidden;
&.active {
background-color: $activeColor;
}
&:hover:not(.active) {
// font-size: 12px;
background-color: $hoverColor;
}
}
}
.dropdown-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
</style>
@@ -1,120 +0,0 @@
<script lang="tsx">
import { defineComponent, renderSlot } from 'vue'
import { RouterLink } from 'vue-router'
export default defineComponent({
name: 'CalcButton',
props: {
small: {
type: Boolean,
default: false,
},
to: {
type: String,
default: null,
},
class: {
type: [String],
default: '',
},
icon: {
type: String,
},
type: {
type: String,
default: 'primary',
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['click'],
setup(props, { slots, emit }) {
return () => {
const proerpties = {
to: props.to,
class: {
[props.class]: !!props.class,
'i-button cursor-pointer outline-none text-shadow': true,
small: props.small,
[`icon-${props.icon}`]: !!props.icon,
},
onClick: (e: MouseEvent) => {
if (props.to) {
e.preventDefault()
}
emit('click', e)
},
disabled: props.disabled,
}
const content = renderSlot(slots, 'default')
return props.to ? (
<RouterLink {...proerpties}>{content}</RouterLink>
) : (
<button {...proerpties}>{content}</button>
)
}
},
})
</script>
<style lang="scss">
.i-button {
min-width: 56px;
height: 24px;
// line-height: 20px;
// font-size: 12px;
color: #e9c556;
background: linear-gradient(#223768, #122438);
border: 1px solid #755f44;
border-radius: 2px;
display: block;
text-align: center;
text-decoration: none;
transition: all 200ms ease-in-out;
&[class*='icon-'] {
min-width: 1px;
width: 28px;
height: 28px;
padding: 0;
border: none !important;
transition: all 0.2s ease-in-out;
}
&:hover {
color: #fee97d;
background: linear-gradient(#2d4e9f, #153051);
// border: 1px solid #e3c18a;
}
&:active {
color: #e9c556;
background: linear-gradient(#2d4e9f, #122438);
border: 1px solid #e3c18a;
}
&:disabled {
color: #696969;
background: linear-gradient(#353535, #1c1c1c);
border: 1px solid #414141;
border-radius: 2px;
}
&.small {
min-width: 28px;
height: 20px;
// line-height: 20px;
font-size: 12px;
padding-top: 0;
padding-bottom: 0;
}
}
.i-href {
display: block;
text-decoration: none;
}
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

@@ -1,277 +0,0 @@
<script lang="tsx">
import { selectionEmits, selectionProps } from 'hoci'
import type { CSSProperties, PropType } from 'vue'
import { Teleport, Transition, computed, defineComponent, onDeactivated, ref, watch } from 'vue'
import { onClickOutside, useVModel } from '@vueuse/core'
import { getElementZoom } from '@/utils/position'
interface ICascaderItem {
label: string
id?: string | number
value?: any
children?: ICascaderItem[]
onSelect?: () => void
}
export default defineComponent({
name: 'ICascader',
props: {
...selectionProps,
disabled: {
type: Boolean,
default: false,
},
width: {
type: Number,
default: 120,
},
emptyLabel: {
type: String,
},
highlight: {
type: String,
// warn remind
default: '',
},
items: {
type: Array as PropType<ICascaderItem[]>,
default: () => [],
},
},
emits: [...selectionEmits, 'select'],
setup(props, { emit }) {
const modelValue = useVModel(props, 'modelValue')
const isOpen = ref(false)
const triggerRef = ref<HTMLElement>()
const selectRef = ref<HTMLElement>()
const dropdownRef = ref<HTMLElement>()
watch(isOpen, onResize)
function collapse() {
isOpen.value = !isOpen.value && !props.disabled
}
// 下拉框位置
const dropdownPosition = ref({ x: 0, y: 0, w: 0, z: 0 })
// 下拉框位置
const dropdownStyle = computed<CSSProperties>(() => {
if (dropdownPosition.value.z == 0) {
return {
left: `${dropdownPosition.value.x}px`,
top: `${dropdownPosition.value.y}px`,
width: `${dropdownPosition.value.w}px`,
}
} else {
return {
left: `${dropdownPosition.value.x}px`,
bottom: `${dropdownPosition.value.z}px`,
width: `${dropdownPosition.value.w}px`,
}
}
})
function onResize() {
if (triggerRef.value) {
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
const zoom = getElementZoom('body')
if (window.innerHeight - top > 160) {
dropdownPosition.value = {
w: width / zoom,
x: left / zoom,
y: (top + height + 2) / zoom,
z: 0,
}
} else {
dropdownPosition.value = {
w: width / zoom,
x: left / zoom,
z: (window.innerHeight - top + 2) / zoom,
y: 0,
}
}
}
}
window.addEventListener('resize', onResize)
window.addEventListener('scroll', onResize)
onDeactivated(() => {
window.removeEventListener('resize', onResize)
window.removeEventListener('scroll', onResize)
})
const selectItem = ref<ICascaderItem>()
function onSelect(item: ICascaderItem) {
if (!item.children?.length) {
isOpen.value = false
}
selectItem.value = item
emit('select', item.value)
}
function loadNode(item: ICascaderItem) {
const value = props.modelValue ?? props.defaultValue
if ((selectItem.value == null || selectItem.value == undefined) && value == item.value) {
selectItem.value = item
}
}
onClickOutside(triggerRef, () => (isOpen.value = false), { ignore: [dropdownRef] })
return () => {
return (
<div
class={['min-w-20 w-40 i-cascader'].concat(props.highlight)}
onClick={collapse}
ref={selectRef}
>
<div
class={{
'i-cascader-trigger': true,
disabled: props.disabled,
}}
ref={triggerRef}
>
<span class="i-cascader-label">{selectItem.value?.label}</span>
<calc-button icon="dropdown"></calc-button>
</div>
<Teleport to="body">
<Transition name="dropdown" mode="out-in">
<calc-tree
{...props}
v-model={modelValue.value}
class="py-1 i-cascader-dropdown"
style={dropdownStyle.value}
v-show={isOpen.value}
ref={dropdownRef}
onLoad={loadNode}
onSelect={onSelect}
data={props.items}
/>
</Transition>
</Teleport>
</div>
)
}
},
})
</script>
<style lang="scss">
@use 'sass:color';
$text-color: #937639;
.i-cascader {
min-width: 80px;
width: 160px;
user-select: none;
outline: none;
height: 16px;
min-height: 16px;
line-height: 100%;
// font-size: 12px;
color: $text-color;
background-color: rgba(0, 0, 0, 1);
border: 1px solid #5b472a;
border-radius: 2px;
padding: 0;
padding-right: 3px;
margin: 0;
display: block;
.i-cascader-trigger {
padding: 0;
display: flex;
height: 100%;
opacity: 0.9;
justify-content: space-between;
align-items: center;
.i-cascader-label {
padding-left: 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: calc(100% - 20px);
}
.i-cascader-down-icon {
background-image: url('./img/select_down.png');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
border-radius: 2px;
width: 15px;
height: 14px;
}
.i-cascader-down-icon:active {
background-image: url('./img/select_down_clicked.png');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
border-radius: 2px;
width: 15px;
height: 14px;
}
&.disabled {
color: gray;
.i-cascader-down-icon {
background-image: url('./img/select_down_disabled.png');
}
}
}
.i-cascader-trigger:hover {
opacity: 1;
}
}
.remind {
color: #32e432 !important;
border: 1px solid #aa8651;
}
.i-cascader-dropdown {
position: fixed;
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
background: black;
// font-size: 12px;
z-index: 888;
$hoverColor: #002947;
$activeColor: color.scale($hoverColor, $lightness: 5%);
color: $text-color;
}
.dropdown-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
</style>
@@ -1,73 +0,0 @@
<script lang="tsx">
import { defineComponent, provide } from 'vue'
import { selectionEmits, selectionProps, useSelectionList } from 'hoci'
import { CHECKBOX_GROUP } from '../shared'
export default defineComponent({
name: 'ICheckboxGroup',
components: {},
props: {
...selectionProps,
activeClass: {
type: String,
default: 'checked',
},
unactiveClass: {
type: String,
default: 'unchecked',
},
multiple: {
type: [Boolean, Number],
default: true,
},
},
emits: [...selectionEmits],
setup(props, context) {
provide(CHECKBOX_GROUP, true)
const { render } = useSelectionList(props, context)
return () => render()
},
})
</script>
<style lang="scss">
.i-checkbox {
display: flex;
align-items: center;
&.checked {
.i-checkbox-icon {
background-image: url('@/assets/img/control/checkbox_checked.png');
}
}
.i-checkbox-icon {
width: 12px;
min-width: 12px;
height: 12px;
margin-right: 4px;
background-image: url('@/assets/img/control/checkbox_uncheck.png');
}
.i-checkbox-icon-disable {
width: 12px;
min-width: 12px;
height: 12px;
margin-right: 4px;
background-image: url('@/assets/img/control/checkbox_disable.png');
}
&:hover:not(.checked) {
.i-checkbox-icon {
background-image: url('@/assets/img/control/checkbox_hover.png');
}
}
.i-checkbox-label {
line-height: 24px;
color: #937639;
}
}
</style>
@@ -1,226 +0,0 @@
<script lang="tsx">
import type { EmitsOptions, SetupContext } from 'vue'
import { computed, defineComponent, inject, nextTick, renderSlot } from 'vue'
import { selectionProps, switchEmits, switchProps, useSelectionItem, useSwitch } from 'hoci'
import { CHECKBOX_GROUP } from '../shared'
export default defineComponent({
name: 'ICheckbox',
components: {},
props: {
...switchProps,
...selectionProps,
label: {
type: String,
},
disabled: {
type: Boolean,
default: false,
},
round: {
type: Boolean,
default: () => false,
},
},
emits: [...switchEmits],
setup(props, context) {
const inGroup = computed(() => {
return inject(CHECKBOX_GROUP, false)
})
const { toggle, className: switchClass } = useSwitch(
() => ({
...props,
class: props.round
? 'i-switch text-xs h-auto cursor-pointer'
: 'i-checkbox text-xs h-auto cursor-pointer',
activeClass: 'checked',
}),
context,
)
const { activate, className: itemClass } = useSelectionItem(
props,
context as SetupContext<EmitsOptions>,
)
const baseClass = computed(() => (props.round ? 'i-switch' : 'i-checkbox'))
const { slots } = context
async function onClick() {
if (inGroup.value) {
activate()
} else {
toggle()
}
await nextTick()
// console.log(props.modelValue)
}
const className = computed(() => {
return inGroup.value ? itemClass.value : switchClass.value
})
return () => (
<div onClick={onClick} class={['flex', baseClass.value, className.value]}>
<span class={!props.disabled ? 'i-checkbox-icon' : 'i-checkbox-icon-disable'}></span>
<div class={['i-checkbox-label h-auto']}>{props.label ?? renderSlot(slots, 'default')}</div>
</div>
)
},
})
</script>
<style lang="scss">
.i-checkbox {
display: flex;
align-items: center;
justify-content: baseline;
&.checked {
.i-checkbox-icon {
background-image: url('@/assets/img/control/checkbox_checked.png');
}
}
.i-checkbox-icon {
width: 12px;
min-width: 12px;
height: 12px;
margin-right: 4px;
background-image: url('@/assets/img/control/checkbox_uncheck.png');
}
.i-checkbox-icon-disable {
width: 12px;
min-width: 12px;
height: 12px;
margin-right: 4px;
background-image: url('@/assets/img/control/checkbox_disable.png');
}
&:hover:not(.checked) {
.i-checkbox-icon {
background-image: url('@/assets/img/control/checkbox_hover.png');
}
}
.i-checkbox-label {
line-height: 24px;
color: #937639;
}
}
.i-switch {
display: flex;
align-items: center;
// justify-content: center;
&.checked {
.i-checkbox-icon {
background-image:
linear-gradient(to right, #8f6e29, #8f6e29),
linear-gradient(rgb(110, 89, 61), rgb(60, 46, 30));
}
.i-checkbox-icon::after {
left: 100%;
margin-left: -14.8px;
}
}
.i-checkbox-icon {
margin: 0;
display: inline-block;
position: relative;
width: 30px;
height: 18px;
outline: none;
border-radius: 10px;
box-sizing: border-box;
background: #dcdfe6;
cursor: pointer;
transition:
border-color 0.3s,
background-color 0.3s;
vertical-align: middle;
// border-radius: 16px;
border: 1.5px solid transparent;
background-clip: padding-box, border-box;
background-origin: padding-box, border-box;
background-image:
linear-gradient(to right, #141414, #141414),
linear-gradient(rgb(110, 89, 61), rgb(60, 46, 30));
box-shadow: inset 0 0 2px 1.5px black;
}
.i-checkbox-icon::after {
content: '';
position: absolute;
top: 1.6px;
left: 1px;
border-radius: 100%;
transition: all 0.3s;
width: 13px;
height: 13px;
outline: 0.8px solid black;
background: linear-gradient(rgb(15, 61, 116), rgb(8, 37, 71));
box-shadow: inset 0 0 2px 0.5px rgb(255, 255, 255, 0.1);
}
.i-checkbox-icon-disable {
margin: 0;
display: inline-block;
position: relative;
width: 30px;
height: 18px;
outline: none;
border-radius: 10px;
box-sizing: border-box;
background: #dcdfe6;
cursor: pointer;
transition:
border-color 0.3s,
background-color 0.3s;
vertical-align: middle;
// border-radius: 16px;
border: 1.5px solid transparent;
background-clip: padding-box, border-box;
background-origin: padding-box, border-box;
background-image:
linear-gradient(to right, #dcdfe6, #dcdfe6),
linear-gradient(rgb(110, 89, 61), rgb(60, 46, 30));
box-shadow: inset 0 0 2px 1.5px black;
}
.i-checkbox-icon-disable::after {
content: '';
position: absolute;
top: 1.6px;
left: 1px;
border-radius: 100%;
transition: all 0.3s;
width: 13px;
height: 13px;
// outline: 0.8px solid black;
background: white;
// box-shadow: inset 0 0 2px 0.5px rgb(255, 255, 255, 0.1);
}
&:hover {
.i-checkbox-icon::after {
background: linear-gradient(#15549f, #0e3c72);
}
}
.i-checkbox-label {
line-height: 18px;
height: 18px;
padding-left: 2px;
color: #937639;
}
}
</style>
@@ -1,41 +0,0 @@
<script lang="tsx">
import { useToggle, useVModel } from '@vueuse/core'
import { defineComponent, renderSlot } from 'vue'
export default defineComponent({
name: 'ICollapse',
props: {
title: {
type: String,
},
modelValue: {
type: Boolean,
default: () => true,
},
},
setup(props, { slots, emit }) {
const modelValue = useVModel(props, 'modelValue', emit, { passive: true })
const toggle = useToggle(modelValue)
return () => {
return (
<div class="rounded h-auto bg-hex-00000078 border-1 border-hex-ffffff28 my-1 overflow-hidden">
<div
onClick={() => toggle()}
v-text={props.title}
class="bg-gradient-to-b flex from-hex-244281 to-hex-122438 h-6 text-xs text-center w-full items-center justify-center"
></div>
<div
class={['transition-all', 'ease-out-in', 'h-auto'].concat(
modelValue.value ? ['max-h-200'] : ['max-h-0'],
)}
>
<div class="p-2">{renderSlot(slots, 'default')}</div>
</div>
</div>
)
}
},
})
</script>
@@ -1,201 +0,0 @@
<script lang="tsx">
import { onClickOutside, onKeyDown, syncRef, useDraggable, useVModel } from '@vueuse/core'
import type { PropType } from 'vue'
import {
Teleport,
Transition,
computed,
defineComponent,
provide,
ref,
renderSlot,
watch,
} from 'vue'
import { provideShow } from '@/components/hooks/show'
export default defineComponent({
name: 'IDialog',
props: {
visible: {
type: Boolean,
default: () => false,
},
class: {
type: String,
},
mask: {
type: Boolean,
default: () => false,
},
// 模态框
modal: {
type: Boolean,
default: () => false,
},
header: {
type: [String, Boolean],
default: () => '提示',
},
closeOnEsc: {
type: Boolean,
default: () => true,
},
closeButton: {
type: Boolean,
default: () => true,
},
cache: {
// 维持窗口状态
type: Boolean,
default: () => true,
},
lazy: {
type: Boolean,
default: () => false,
},
drag: {
type: String as PropType<'header' | 'all' | 'none'>,
default: () => 'header',
},
appendTo: {
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
default: 'body',
},
scale: {
type: Number,
default: 1,
},
},
emits: ['close', 'update:visible'],
setup(props, { emit, slots }) {
const dialogRef = ref<HTMLElement | null>(null)
const headerRef = ref<HTMLElement | null>(null)
const visible = ref(props.visible)
provideShow(visible)
provide('dialogRef', dialogRef)
const loaded = ref(!props.lazy)
const stopLoadWatch = watch(visible, (val) => {
if (val) {
loaded.value = true
stopLoadWatch()
}
})
syncRef(useVModel(props, 'visible'), visible, { direction: 'both' })
onClickOutside(dialogRef, () => {
if (props.modal) {
close()
}
})
function close() {
visible.value = false
emit('close')
}
onKeyDown(
'Escape',
() => {
if (visible.value && props.closeOnEsc) {
close()
}
},
{ target: document },
)
const init = ref(true)
const { x, y, style } = useDraggable(
computed(() => {
switch (props.drag) {
case 'header':
return headerRef.value
case 'all':
return dialogRef.value
case 'none':
default:
return null
}
}),
{
initialValue: { x: 0, y: 0 },
},
)
const isFixed = computed(() => x.value + y.value > 0)
const appendToRef = computed(() => {
return props.appendTo || 'body'
})
return () => {
return (
<Teleport to={appendToRef.value}>
<Transition name="dialog" mode="out-in">
{loaded.value && (props.cache || visible.value) && (
<div
v-show={visible.value}
class={[
'dialog-mask w-full h-full fixed top-0 left-0 z-999 flex justify-center items-center ',
].concat(props.mask ? 'bg-hex-000 bg-opacity-66' : '')}
style={`transform: scale(${props.scale});${x.value == 0 && y.value == 0 ? 'transform-origin:center' : 'transform-origin:left top'}`}
>
<div
ref={dialogRef}
class={[
'h-auto border-1 border-hex-2b2b2b border-solid shadow-sm round-1 dialog min-w-48',
isFixed.value ? 'fixed' : '',
props.class,
]}
style={`top:${y.value / props.scale}px;left:${x.value / props.scale}px`}
>
{props.header && (
<div
ref={headerRef}
class="flex h-4 px-1 leading-4 z-9999 justify-center relative app-header py-2px"
>
<div class="text-xs text-shadow ">{props.header}</div>
{props.closeButton && (
<div class="flex top-0 right-0 bottom-0 items-center absolute">
<div
onClick={close}
class="cursor-pointer flex h-4 text-center w-4 items-center close-icon"
></div>
</div>
)}
</div>
)}
<div class="bg-hex-000 bg-opacity-80 text-white text-xs">
{renderSlot(slots, 'default')}
</div>
</div>
</div>
)}
</Transition>
</Teleport>
)
}
},
})
</script>
<style lang="scss" scoped>
.dialog-enter-active {
animation: fade-in 400ms;
.dialog {
animation: zoom-in 400ms;
}
}
.dialog-leave-active {
animation: fade-in reverse 400ms;
.dialog {
animation: zoom-out 400ms;
}
}
</style>
@@ -1,257 +0,0 @@
<script lang="tsx">
import { selectionEmits, selectionProps, useSelectionList } from 'hoci'
import type { CSSProperties, PropType } from 'vue'
import {
Teleport,
Transition,
computed,
defineComponent,
onDeactivated,
ref,
renderSlot,
watch,
} from 'vue'
import { onClickOutside } from '@vueuse/core'
import { getElementZoom } from '@/utils/position'
export default defineComponent({
name: 'IIconselect',
props: {
...selectionProps,
disabled: {
type: Boolean,
default: false,
},
width: {
type: Number,
default: 28,
},
emptyLabel: {
type: String,
},
columnNum: {
type: Number,
default: 1,
},
appendTo: {
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
default: 'body',
},
},
emits: selectionEmits,
setup(props, context) {
// props.itemClass = "i-select-dropdown-item"
// const { active } = useSelectionList({ ...props, itemClass: "i-select-dropdown-item" }, context)
const { renderItem } = useSelectionList(() => {
return {
...props,
itemClass: 'i-option',
activeClass: 'active',
unactiveClass: 'unactive',
}
}, context)
// const { active } = useSelectionList(props, context)
const columnNum = computed(() => props.columnNum || 1)
// console.log({ ...props })
const isOpen = ref(false)
const triggerRef = ref<HTMLElement>()
watch(isOpen, onResize)
const appendToRef = computed(() => {
return props.appendTo || 'body'
})
function collapse() {
isOpen.value = !isOpen.value && !props.disabled
}
// 下拉框位置
const dropdownPosition = ref({ x: 0, y: 0, w: 0 })
// 下拉框位置
const dropdownStyle = computed<CSSProperties>(() => {
const zoom = getElementZoom(appendToRef.value)
return {
left: `${dropdownPosition.value.x}px`,
top: `${dropdownPosition.value.y}px`,
width: `${(columnNum.value * 32 + 11) / zoom}px`,
}
})
function onResize() {
if (triggerRef.value) {
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
const zoom = getElementZoom(appendToRef.value)
dropdownPosition.value = {
w: width / zoom,
x: (left - 2) / zoom,
y: (top + height + 2) / zoom,
}
}
}
onClickOutside(triggerRef, () => (isOpen.value = false))
window.addEventListener('resize', onResize)
window.addEventListener('scroll', onResize)
onDeactivated(() => {
window.removeEventListener('resize', onResize)
window.removeEventListener('scroll', onResize)
})
const { slots } = context
// console.log(active.value?.render())
// console.log(active.value)
// console.log(active.value?.render() ?? props.emptyLabel)
return () => {
return (
<div class="i-icon-select" onClick={collapse}>
<div
class={{
'i-select-trigger': true,
disabled: props.disabled,
}}
ref={triggerRef}
>
<span class="i-select-label">{renderItem() ?? props.emptyLabel}</span>
</div>
<Teleport to={appendToRef.value}>
<Transition name="dropdown" mode="out-in">
<div class="i-icon-select-dropdown" style={dropdownStyle.value} v-show={isOpen.value}>
{renderSlot(slots, 'default')}
</div>
</Transition>
</Teleport>
</div>
)
}
},
})
</script>
<style lang="scss">
@use 'sass:color';
$text-color: #e9c556;
.i-icon-select {
width: 28px;
min-width: 28px;
user-select: none;
outline: none;
height: 28px;
line-height: 100%;
// font-size: 12px;
color: $text-color;
background-color: rgba(0, 0, 0, 1);
border: 1px solid #5b472a;
border-radius: 2px;
padding: 0;
margin: 0;
display: block;
.i-select-trigger {
padding: 0;
display: flex;
height: 100%;
opacity: 1;
justify-content: center;
align-items: center;
&.disabled {
color: gray;
}
}
.i-select-trigger:hover {
opacity: 1;
}
}
.i-icon-select-dropdown {
display: flex;
flex-direction: row;
flex-wrap: wrap;
position: fixed;
overflow-y: auto;
overflow-x: hidden;
max-height: 480px;
// background: rgba(255, 255, 255, 0.1);
// font-size: 12px;
z-index: 888;
min-height: 28px;
$hoverColor: #002947;
$activeColor: color.scale($hoverColor, $lightness: 5%);
color: $text-color;
.i-option {
// font-size: 12px;
height: 28px;
line-height: 28px;
margin: 1px;
border: none;
outline: none;
appearance: none;
display: block;
overflow: hidden;
border: gray 1px solid;
// &.active {
// background-color: $activeColor;
// }
&.active::before {
content: '';
width: 28px;
height: 28px;
position: absolute;
background-image: url('/src/assets/img/control/item_checked.png');
background-size: 80% 80%;
background-repeat: no-repeat;
background-position: center;
z-index: 6;
}
&:hover:not(.active) ::before {
content: '';
width: 28px;
height: 28px;
position: absolute;
background-image: url('@/assets/img/control/item_hover.png');
background-size: 100% 100%;
z-index: 6;
}
}
}
.dropdown-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
</style>
-40
View File
@@ -1,40 +0,0 @@
import type { App } from 'vue'
import Button from './button/index.vue'
import Tabs from './tabs/index.vue'
import Tab from './tab/index.vue'
import Select from './select/index.vue'
import Option from './option/index.vue'
import Menu from './menu/menu.vue'
import Checkbox from './checkbox/index.vue'
import Collapse from './collapse/index.vue'
import Dialog from './dialog/index.vue'
import Tooltip from './tooltip/index.vue'
import Iconselect from './iconselect/index.vue'
import Selection from './selection/index.vue'
import Slider from './slider/index.vue'
export const components = {
Button,
Tabs,
Option,
Tab,
Select,
Menu,
Checkbox,
Collapse,
Dialog,
Tooltip,
Iconselect,
Selection,
Slider,
}
export type CalcComponents = typeof components
export function install(app: App) {
for (const [key, value] of Object.entries(components)) {
app.component(`calc-${key.toLocaleLowerCase()}`, value)
}
}
export default components
@@ -1,69 +0,0 @@
<script lang="tsx">
import { useVModel } from '@vueuse/core'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'IInput',
props: {
modelValue: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
min: {
type: Number,
default: 0,
},
},
setup(props) {
const modelValue = useVModel(props, 'modelValue')
return () => (
<div class="flex min-w-80px h-20px border-1px box-border border-solid border-[#5b472a] rounded-[2px] bg-[#000000] px-2px">
<input
class="flex-1 border-none w-full outline-none h-auto text-[#ffe2ac] m-0 bg-[#000000]"
type={props.type}
v-model={modelValue.value}
/>
{props.type == 'number' && (
<div class="h-full">
<div
class="input-number-upper h-50% w-10px"
onClick={() => (modelValue.value = Number(modelValue.value) + 1)}
></div>
<div
class="input-number-lower h-50% w-10px"
onClick={() => (modelValue.value = Math.max(Number(modelValue.value) - 1, props.min))}
></div>
</div>
)}
</div>
)
},
})
</script>
<style lang="scss">
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
.input-number-upper {
background-image: url('@/assets/img/icon/input_upper.png');
background-size: 8px auto;
background-position: bottom 1px center;
background-repeat: no-repeat;
}
.input-number-lower {
background-image: url('@/assets/img/icon/input_lower.png');
background-size: 8px auto;
background-position: top 1px center;
background-repeat: no-repeat;
}
</style>
@@ -1,4 +0,0 @@
<script lang="tsx">
import { HiItem } from 'hoci'
export default HiItem
</script>
@@ -1,95 +0,0 @@
<script lang="tsx">
import { computed, defineComponent, renderSlot } from 'vue'
import { classPropType } from '@/components/hooks/types'
export default defineComponent({
props: {
loading: {
type: Boolean,
default: () => false,
},
class: {
type: classPropType,
default: () => '',
},
},
setup(props, { slots }) {
const loading = computed(() => props.loading)
return () => {
return (
<div class="relative">
<div
v-show={loading.value}
class="flex h-full w-full z-99 absolute items-center justify-center"
>
<svg
class="i-loading-spin"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="200px"
height="200px"
viewBox="0 0 200 200"
>
<g>
<linearGradient
id="right"
gradientUnits="userSpaceOnUse"
x1="150"
y1="20"
x2="150"
y2="180"
>
<stop offset="0" style="stop-color:transparent" />
<stop offset="1" style="stop-color:currentColor" />
</linearGradient>
<path
fill="url(#right)"
d="M100,0v20c44.1,0,80,35.9,80,80c0,44.1-35.9,80-80,80v20c55.2,0,100-44.8,100-100S155.2,0,100,0z"
/>
<linearGradient
id="left"
gradientUnits="userSpaceOnUse"
x1="50"
y1="0"
x2="50"
y2="180"
>
<stop offset="0" style="stop-color:currentColor" />
<stop offset="1" style="stop-color:transparent" />
</linearGradient>
<path
fill="url(#left)"
d="M20,100c0-44.1,35.9-80,80-80V0C44.8,0,0,44.8,0,100s44.8,100,100,100v-20C55.9,180,20,144.1,20,100z"
/>
<circle fill="currentColor" cx="100" cy="10" r="10" />
</g>
</svg>
</div>
<div
class={[props.class].concat('duration-200').concat(loading.value ? 'opacity-0' : '')}
>
{renderSlot(slots, 'default', {}, () => [])}
</div>
</div>
)
}
},
})
</script>
<style lang="scss">
.i-loading-spin {
width: 32px;
height: 32px;
animation: spin infinite 1s linear;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1639282812376" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2304" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M85.312 85.312v853.376h853.376V85.312H85.312zM0 0h1024v1024H0V0z m554.624 213.312v256h256v85.376h-256v256H469.312v-256h-256V469.376h256v-256h85.312z" fill="currentColor" p-id="2305"></path></svg>

Before

Width:  |  Height:  |  Size: 557 B

@@ -1,73 +0,0 @@
<script lang="tsx">
import { useToggle } from '@vueuse/core'
import { defineComponent, ref, renderSlot } from 'vue'
export default defineComponent({
name: 'CalcMenu',
props: ['label'],
setup(props, { slots }) {
const expand = ref(false)
const toggle = useToggle(expand)
return () => {
return (
<div class="i-menu">
<div class="i-menu-content">
<div onClick={() => toggle()} class="i-menu-expand-icon">
<svg viewBox="0 0 1024 1024" width="100%" height="100%">
<path
d="M85.312 85.312v853.376h853.376V85.312H85.312zM0 0h1024v1024H0V0z m554.624 213.312v256h256v85.376h-256v256H469.312v-256h-256V469.376h256v-256h85.312z"
fill="currentColor"
></path>
</svg>
</div>
<div class="i-menu-label" v-trans={props.label}></div>
</div>
<div class={{ 'i-menu-items': true, expand: expand.value }}>
{renderSlot(slots, 'default')}
</div>
</div>
)
}
},
})
</script>
<style lang="scss">
.i-menu {
min-width: 120px;
width: 200px;
transition: all 0.3s ease;
.i-menu-content {
width: 100%;
height: 16px;
line-height: 16px;
display: flex;
background-color: rgba(37, 37, 37, 0.4);
padding: 4px;
.i-menu-expand-icon {
width: 12px;
height: 12px;
color: rgb(255, 247, 148);
margin-right: 8px;
}
.i-menu-label {
width: auto;
// font-size: 12px;
}
}
.i-menu-items {
height: 0;
overflow: hidden;
&.expand {
height: 32px;
}
}
}
</style>
@@ -1,4 +0,0 @@
<script lang="tsx">
import { HiItem } from 'hoci'
export default HiItem
</script>
@@ -1,97 +0,0 @@
<script lang="tsx">
import { onKeyStroke } from '@vueuse/core'
import type { PropType } from 'vue'
import { computed, defineComponent, onUnmounted } from 'vue'
export default defineComponent({
props: {
page: {
type: Number,
default: () => 0,
},
totalPage: {
type: Number,
default: () => 0,
},
minPage: {
type: Number,
default: () => 1,
},
parent: {
type: Object as PropType<HTMLElement>,
default: () => document,
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const totalPage = computed(() =>
Number.isNaN(props.totalPage) ? props.minPage : props.totalPage,
)
const page = computed({
get() {
if (Number.isNaN(props.page)) {
return props.minPage
}
return Math.max(Math.min(props.page, totalPage.value), props.minPage)
},
set(val) {
emit('update:page', val)
},
})
function prev(e: Event) {
e.preventDefault()
if (page.value > 1 && !props.disabled) {
page.value--
emit('prev')
emit('change', page.value)
}
}
function next(e: Event) {
e.preventDefault()
if (page.value < totalPage.value && !props.disabled) {
page.value = page.value + 1
emit('next')
emit('change', page.value)
}
}
const removePrev = onKeyStroke(['F1', 'PageUp'], prev, { target: props.parent })
const removeNext = onKeyStroke(['F2', 'PageDown'], next, { target: props.parent })
onUnmounted(() => {
removePrev()
removeNext()
})
return () => {
return (
<div class="flex space-x-1 h-10 w-full items-center justify-center">
<calc-button
icon="prev"
disabled={page.value <= 1 || props.disabled}
onClick={prev}
></calc-button>
<span class="text-center w-8">
{page.value}
/
{totalPage.value}
</span>
<calc-button
icon="next"
disabled={page.value >= totalPage.value || props.disabled}
onClick={next}
></calc-button>
</div>
)
}
},
})
</script>
<style lang="scss"></style>
@@ -1,51 +0,0 @@
<script lang="tsx">
import { computed, defineComponent } from 'vue'
export default defineComponent({
props: {
value: {
type: [Number, String],
default: 0,
},
total: {
type: [Number, String],
default: 100,
},
},
setup(props) {
const progress = computed(() => {
if (props.value == 0) {
return 0
}
return (Number(props.value) / Number(props.total)) * 100
})
return () => (
<div class="border-solid bg-hex-171717 border-hex-272521 border-2">
<div class="border-transparent border-solid border-2">
<div
style={progress.value == 0 ? 'width:0' : `width:${progress.value.toFixed(2)}%`}
class="h-2 duration-200 calc-progress"
></div>
</div>
</div>
)
},
})
</script>
<style lang="scss" scoped>
.calc-progress {
background: linear-gradient(
to bottom,
#48d0eb 0%,
#6cf4fe 15%,
#4fbdfe 30%,
#3080fe 40%,
#1d5bfe 55%,
#3283fd 70%,
#000456 100%
);
}
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

@@ -1,330 +0,0 @@
<script lang="tsx">
import { selectionEmits, selectionProps, useSelectionList } from 'hoci'
import type { CSSProperties, PropType } from 'vue'
import {
Teleport,
Transition,
computed,
defineComponent,
onDeactivated,
ref,
renderList,
renderSlot,
watch,
} from 'vue'
import { onClickOutside, useVModel } from '@vueuse/core'
import { classPropType, labelPropType } from '@/components/hooks/types'
import { getElementZoom } from '@/utils/position'
export default defineComponent({
name: 'ISelect',
props: {
...selectionProps,
disabled: {
type: Boolean,
default: false,
},
width: {
type: Number,
default: 120,
},
emptyLabel: {
type: labelPropType,
},
highlight: {
type: String,
// warn remind
default: '',
},
labelClass: {
type: String,
default: '',
},
inputClass: {
type: classPropType,
default: '',
},
appendTo: {
type: [Object, String] as PropType<string | HTMLElement | null | undefined>,
default() {
return 'body'
},
},
},
emits: [...selectionEmits],
setup(props, context) {
const modelValue = useVModel(props, 'modelValue', context.emit, { passive: true })
const { renderItem } = useSelectionList(() => {
return {
...props,
modelValue: modelValue.value,
itemClass: 'i-select-dropdown-item',
activeClass: 'active',
unactiveClass: 'unactive',
}
}, context)
const isOpen = ref(false)
const triggerRef = ref<HTMLElement>()
const selectRef = ref<HTMLElement>()
const dropdownRef = ref<HTMLElement>()
watch(isOpen, onResize)
function collapse() {
isOpen.value = !isOpen.value && !props.disabled
}
// 下拉框位置
const dropdownPosition = ref({ x: 0, y: 0, w: 0, z: 0 })
// 下拉框位置
const dropdownStyle = computed<CSSProperties>(() => {
if (dropdownPosition.value.z == 0) {
return {
left: `${dropdownPosition.value.x}px`,
top: `${dropdownPosition.value.y}px`,
width: `${dropdownPosition.value.w}px`,
}
} else {
return {
left: `${dropdownPosition.value.x}px`,
bottom: `${dropdownPosition.value.z}px`,
width: `${dropdownPosition.value.w}px`,
}
}
})
function onResize() {
if (triggerRef.value) {
const { width, height, left, top } = triggerRef.value.getBoundingClientRect()
const zoom = getElementZoom(appendToRef.value)
if (window.innerHeight - top > 160) {
dropdownPosition.value = {
w: width / zoom,
x: left / zoom,
y: (top + height + 2) / zoom,
z: 0,
}
} else {
dropdownPosition.value = {
w: width / zoom,
x: left / zoom,
z: (window.innerHeight - top + 2) / zoom,
y: 0,
}
}
}
}
const appendToRef = computed(() => {
return props.appendTo || 'body'
})
onClickOutside(triggerRef, () => (isOpen.value = false))
window.addEventListener('resize', onResize)
window.addEventListener('scroll', onResize)
onDeactivated(() => {
window.removeEventListener('resize', onResize)
window.removeEventListener('scroll', onResize)
})
const { slots } = context
function renderLabel() {
const children = renderItem()
if (children) {
if (!Array.isArray(children)) {
return <span class={['i-select-input'].concat(props.inputClass)}>{children}</span>
} else if (children.length > 0) {
return (
<span class={['i-select-input space-x-1'].concat(props.inputClass)}>
{renderList(children, (item) => (
<span>{item}</span>
))}
</span>
)
}
}
const emptyLabel =
typeof props.emptyLabel == 'function' ? props.emptyLabel() : props.emptyLabel
return <span class="w-full pl-2 i-select-input i-select-empty-label">{emptyLabel}</span>
}
return () => {
return (
<div
class={['min-w-20 w-40 i-select '].concat(props.highlight)}
onClick={collapse}
ref={selectRef}
>
<div
class={{
'i-select-trigger': true,
disabled: props.disabled,
}}
ref={triggerRef}
>
{renderLabel()}
<calc-button
disabled={props.disabled}
icon="dropdown"
class="cursor-pointer"
></calc-button>
</div>
<Teleport to={appendToRef.value}>
<Transition name="dropdown" mode="out-in">
<div
class="i-select-dropdown"
style={dropdownStyle.value}
v-show={isOpen.value}
ref={dropdownRef}
>
{renderSlot(slots, 'default')}
</div>
</Transition>
</Teleport>
</div>
)
}
},
})
</script>
<style lang="scss">
@use 'sass:color';
$text-color: #937639;
.i-select {
min-width: 80px;
width: 160px;
user-select: none;
outline: none;
height: 16px;
min-height: 16px;
line-height: 100%;
// font-size: 12px;
color: $text-color;
background-color: rgba(0, 0, 0, 1);
border: 1px solid #5b472a;
border-radius: 2px;
padding: 0;
padding-right: 3px;
margin: 0;
display: block;
.i-select-trigger {
padding: 0;
display: flex;
height: 100%;
justify-content: space-between;
align-items: center;
.i-select-input {
padding-left: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: calc(100% - 20px);
}
.i-select-empty-label {
color: #5e5e5e;
}
&:hover {
.i-select-empty-label {
color: #757575;
}
}
&.disabled {
color: gray;
}
}
.i-select-trigger:hover {
opacity: 1;
}
}
.warn {
color: red !important;
border: 1px solid #aa8651;
}
.remind {
color: #32e432 !important;
border: 1px solid #aa8651;
}
.i-select-dropdown {
position: fixed;
max-height: 160px;
overflow-y: auto;
background: black;
// font-size: 12px;
z-index: 888;
$hoverColor: #002947;
$activeColor: color.scale($hoverColor, $lightness: 5%);
color: $text-color;
.i-select-dropdown-item {
// font-size: 12px;
height: 20px;
line-height: 20px;
margin: 0;
padding: 0 5px;
border: none;
outline: none;
appearance: none;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.active {
// background-color: $activeColor;
color: #e9c556;
}
&.active::after {
content: '✓';
position: absolute;
right: 5px;
}
&:hover:not(.active) {
// font-size: 12px;
background-color: $hoverColor;
}
}
}
.dropdown-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
</style>
@@ -1,5 +0,0 @@
<script lang="tsx">
import { HiSelection } from 'hoci'
export default HiSelection
</script>
@@ -1,3 +0,0 @@
import type { InjectionKey } from 'vue'
export const CHECKBOX_GROUP: InjectionKey<boolean> = Symbol('checkbox-group')
@@ -1,211 +0,0 @@
<script lang="tsx">
import { defineComponent, ref, computed, shallowRef, watch } from 'vue'
import { useVModel, useDraggable, clamp } from '@vueuse/core'
export default defineComponent({
name: 'ISlider',
props: {
modelValue: {
type: Number,
default: 0,
},
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 100,
},
step: {
type: Number,
default: 1,
},
disabled: {
type: Boolean,
default: false,
},
showPercent: {
type: Boolean,
default: true,
},
formatValue: {
type: Function,
default: undefined,
},
},
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
const buttonRef = shallowRef<HTMLDivElement | null>(null)
const runwayRef = shallowRef<HTMLDivElement | null>(null)
const modelValue = useVModel(props, 'modelValue', emit, { passive: true })
const percentage = computed(() => {
return ((modelValue.value - props.min) / (props.max - props.min)) * 100
})
useDraggable(buttonRef, {
onMove(position) {
if (!runwayRef.value || props.disabled) {
return
}
const runwayRect = runwayRef.value?.getBoundingClientRect()
if (!runwayRect) {
return
}
// 使用runway的位置和宽度来计算百分比
const percentage = Math.max(
0,
Math.min(100, ((position.x - runwayRect.left) / runwayRect.width) * 100),
)
const range = props.max - props.min
let value = (percentage * range) / 100 + props.min
// 应用步长
value = Math.round(value / props.step) * props.step
// 确保值在范围内
value = Math.max(props.min, Math.min(props.max, value))
modelValue.value = value
emit('change', value)
},
})
return () => (
<div
class={[
'w-full h-8 relative cursor-pointer flex items-center gap-3 py-2',
{ 'cursor-not-allowed': props.disabled },
]}
>
<div class="flex-1 h-1 bg-[#4D524D] relative" ref={runwayRef}>
<div
class="h-1 bg-[#8E6D28] absolute left-0 top-0"
style={{ width: `${percentage.value}%` }}
></div>
<div
class="h-6 w-6 absolute top-1/2 -translate-y-1/2 bg-transparent text-center select-none"
style={{ left: `calc(${percentage.value}% - 0.75rem)` }}
ref={buttonRef}
>
<div
class={[
'w-4 h-4 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
'bg-gradient-to-b from-[#223768] to-[#122438]',
'border border-solid border-[#937639] rounded-full transition-all duration-200',
'shadow-[0_2px_4px_rgba(0,0,0,0.2)]',
{ 'bg-gradient-to-b from-[#353535] to-[#1c1c1c] border-[#414141]': props.disabled },
]}
></div>
</div>
</div>
{props.showPercent && (
<div
class={[
'w-auto min-w-5 text-center text-sm font-medium',
props.disabled ? 'text-[#696969]' : 'text-[#e9c556]',
]}
>
{props.formatValue
? props.formatValue(modelValue.value)
: `${percentage.value.toFixed(0)}%`}
</div>
)}
</div>
)
},
})
</script>
<style scoped>
.i-slider {
width: 100%;
height: 32px;
position: relative;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.i-slider.is-disabled {
cursor: not-allowed;
}
.i-slider.is-disabled .i-slider-button {
background: linear-gradient(#353535, #1c1c1c);
border-color: #414141;
}
.i-slider.is-disabled .i-slider-value {
color: #696969;
}
.i-slider-runway {
flex: 1;
height: 4px;
background-color: #122438;
border-radius: 2px;
position: relative;
}
.i-slider-bar {
height: 4px;
background: #c6b083;
border-radius: 2px;
position: absolute;
left: 0;
top: 0;
}
.i-slider-button-wrapper {
height: 24px;
width: 24px;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
background: transparent;
text-align: center;
user-select: none;
line-height: normal;
}
.i-slider-button {
width: 16px;
height: 16px;
background: linear-gradient(#223768, #122438);
border: 1px solid #755f44;
border-radius: 50%;
transition: all 0.2s ease-in-out;
user-select: none;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.i-slider-button:hover {
background: linear-gradient(#2d4e9f, #153051);
border-color: #e3c18a;
transform: translate(-50%, -50%) scale(1.1);
}
.i-slider-button:active {
background: linear-gradient(#2d4e9f, #122438);
border-color: #e3c18a;
}
.i-slider-value {
min-width: 45px;
text-align: right;
font-size: 14px;
color: #e9c556;
font-weight: 500;
}
</style>
@@ -1,24 +0,0 @@
<script lang="tsx">
import { defineComponent, renderSlot } from 'vue'
import { HiItem, selectionProps } from 'hoci'
export default defineComponent({
name: 'CalcTab',
props: {
...selectionProps,
value: {
type: [String, Number],
default: '',
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props, { slots }) {
return () => {
return <HiItem {...props}>{renderSlot(slots, 'default')}</HiItem>
}
},
})
</script>
@@ -1,5 +0,0 @@
export const AciveSymbol = Symbol('[i-tabs]active')
export const AciveClassSymbol = Symbol('[i-tabs]active-class')
export const ChangeActiveSymbol = Symbol('[i-tabs]change-active')
@@ -1,141 +0,0 @@
<script lang="tsx">
import { HiSelection, selectionProps } from 'hoci'
import type { PropType } from 'vue'
import { computed, defineComponent, renderSlot } from 'vue'
import type { LocationQuery } from 'vue-router'
import { parseQuery, useRouter } from 'vue-router'
import type { BaseType } from '@/components/hooks/types'
export default defineComponent({
name: 'CalcTabs',
props: {
...selectionProps,
vertical: {
type: Boolean,
},
route: {
type: Boolean,
},
query: {
type: [Object, String] as PropType<string | LocationQuery>,
},
},
setup(props, { emit, slots }) {
const router = useRouter()
const routeQuery = computed(() => {
if (typeof props.query == 'string') {
return parseQuery(props.query)
}
return props.query ?? {}
})
const modelValue = computed<BaseType>({
get() {
let val = props.modelValue
if (props.route) {
val = val ?? router.currentRoute.value.path
}
return val
},
set(val) {
if (props.route && val) {
val = decodeURIComponent(val.toString())
router.push({ path: val, query: routeQuery.value })
}
emit('update:modelValue', val)
emit('change', val)
},
})
return () => {
return (
<HiSelection
v-model={modelValue.value}
class={{ 'i-tabs': true, vertical: !!props.vertical }}
activeClass="active"
itemClass="i-tab cursor-pointer"
>
{renderSlot(slots, 'default')}
</HiSelection>
)
}
},
})
</script>
<style lang="scss">
.i-tabs {
display: flex;
list-style: none;
align-items: flex-end;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
width: 100%;
height: 25px;
font-size: 14px;
color: #c6b083;
margin: 0;
padding: 0;
a {
text-decoration: none;
color: #c6b083;
&:visited {
color: currentColor;
}
}
&.vertical {
flex-direction: column;
.i-tab {
border-radius: 0;
}
}
.i-tab {
// font-size: 12px;
margin-left: 1px;
width: fit-content;
padding: 0px 10px;
padding-bottom: 2px;
height: 20px;
text-align: center;
text-decoration: none;
display: flex;
align-items: end;
box-sizing: border-box;
text-shadow: none;
&:visited {
color: currentColor;
}
border: #4f4838 1px solid;
border-bottom: none;
border-radius: 5px 5px 0 0;
background: linear-gradient(to top, rgba(6, 6, 6, 0.9), rgba(52, 52, 52, 0.9));
transition: all 0.4s ease-in-out;
&:hover {
background: linear-gradient(#4a4b4e, #1d1e22);
}
&.active {
margin-bottom: -1px;
background: linear-gradient(#574d38, #25221d);
border-image: url('@/assets/img/control/active_top.png') 1 fill stretch;
height: 22px;
// color: #ffb400;
}
&.disabled {
filter: grayscale(100%);
cursor: not-allowed;
}
}
}
</style>
@@ -1,28 +0,0 @@
<script lang="tsx">
import { defineComponent } from 'vue'
import { NTooltip } from '@/components/base'
import { tooltipProps } from '@/components/base/tooltip'
export default defineComponent({
name: 'ITooltip',
components: { NTooltip },
props: {
...tooltipProps,
},
setup(props, { slots }) {
return () => {
return (
<n-tooltip
{...props}
pop-class="i-popper z-999 fixed top-0 left-0 bg-black bg-opacity-90 border-1.5 border-hex-272928 rounded-2px border-solid"
>
{slots}
</n-tooltip>
)
}
},
})
</script>
<style lang="scss"></style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

@@ -1,105 +0,0 @@
<script lang="tsx">
import { HiSelection, selectionProps } from 'hoci'
import { useVModel } from '@vueuse/core'
import type { PropType } from 'vue'
import { computed, defineComponent, renderList, watch } from 'vue'
import CalcTreeNode from './node.vue'
import type { TreeNode } from './types'
import { labelPropType, valuePropType } from '@/components/hooks/types'
const Tree = defineComponent({
name: 'CalcTree',
props: {
...selectionProps,
modelValue: {
type: valuePropType,
default: () => null,
},
activeItem: {
type: Object as PropType<TreeNode>,
default: () => null,
},
idKey: {
type: String,
default: 'id',
},
label: {
type: labelPropType,
},
data: {
type: Array as PropType<TreeNode[]>,
default: () => [],
},
depth: {
type: [Number, String] as PropType<number>,
default: () => 0,
},
depthStyle: {
type: Function as PropType<(depth: number) => string>,
},
},
emits: ['select', 'update:modelValue'],
setup(props, { emit }) {
const modelValue = useVModel(props, 'modelValue')
function onSelect(item?: TreeNode) {
if (item) {
emit('select', item)
item.onSelect?.()
}
}
function loadItem(total: TreeNode[], items: TreeNode[]) {
items?.forEach((item) => {
if (item.children) {
loadItem(total, item.children)
}
total.push(item)
})
return total
}
const children = computed(() => {
return loadItem([], props.data)
})
watch(modelValue, (val) => {
val = val ?? props.defaultValue
const item = children.value.find((item) => item.value === val)
onSelect(item)
})
return () => {
return (
<HiSelection
{...props}
activeClass="i-tree-node-active"
v-model={modelValue.value}
class="i-tree"
>
{renderList(props.data, (item) => {
return (
<CalcTreeNode
{...item}
onSelect={onSelect}
depth-style={props.depthStyle}
depth={props.depth}
/>
)
})}
</HiSelection>
)
}
},
})
export default Tree
</script>
<style>
.i-tree {
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>
@@ -1,171 +0,0 @@
<script lang="tsx">
import { HiItem } from 'hoci'
import type { PropType } from 'vue'
import { Transition, computed, defineComponent, ref, renderList } from 'vue'
import type { TreeNode } from './types'
const LegacySelectionItem = HiItem as any
const NTreeNode = defineComponent({
props: {
label: {
type: String,
default: '',
},
value: {
type: [String, Number, Boolean, Object, Array] as PropType<any>,
default: () => null,
},
children: {
type: Array as PropType<TreeNode[]>,
default: () => [],
},
depth: {
type: [String, Number] as PropType<number>,
default: 0,
},
depthStyle: {
type: Function as PropType<(depth: number) => string>,
},
depthOffset: {
type: String,
default: '12px',
},
},
emits: ['select'],
setup(props, { emit }) {
const isOpen = ref(false)
function open() {
isOpen.value = !isOpen.value
onSelect(props)
}
function onSelect(item: TreeNode) {
return emit('select', item)
}
const depthStyle = computed(() => {
if (props.depthStyle) {
return props.depthStyle(props.depth)
}
return `padding-left: calc(${props.depth}*${props.depthOffset} + 12px);`
})
return () => (
<div class="w-full i-tree-item relative">
<LegacySelectionItem
label={props.label}
value={props.value}
style={depthStyle.value}
class="flex i-tree-item-label items-center"
onClick={open}
>
<div class="top-0 right-0 bottom-0 left-0 absolute i-tree-item-active-mask"></div>
<div class="top-0 right-0 bottom-0 left-0 absolute i-tree-item-mask"></div>
<calc-button
v-show={props.children?.length}
icon={isOpen.value ? 'collapse-up' : 'collapse-down'}
></calc-button>
<div class="pl-1">{props.label}</div>
</LegacySelectionItem>
<Transition name="dropdown" mode="out-in">
<div v-show={isOpen.value}>
{renderList(props.children, (item) => {
return <NTreeNode onSelect={onSelect} {...item} depth={Number(props.depth) + 1} />
})}
</div>
</Transition>
</div>
)
},
})
export default NTreeNode
</script>
<style lang="scss">
.i-tree-item {
display: flex;
flex-direction: column;
cursor: pointer;
border: 1px solid transparent;
background-color: black;
}
.i-tree-item-label {
flex: 1;
height: 16px;
min-height: 16px;
color: #ddc593;
position: relative;
&:hover {
.i-tree-item-mask {
background: url('./img/item_hover.png') no-repeat;
background-size: 100% 100%;
}
}
&.i-tree-node-active {
.i-tree-item-active-mask {
background: url('./img/item_active.png') no-repeat;
background-size: 100% 100%;
}
}
}
.icon-collapse-up {
background-image: url('./img/collapse_up.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
min-width: 13px !important;
width: 13px !important;
height: 13px !important;
}
.icon-collapse-down {
background-image: url('./img/collapse_down.png');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
min-width: 13px !important;
width: 13px !important;
height: 13px !important;
}
.i-tree-dropdown {
width: 100%;
max-height: 0;
overflow: hidden;
background-color: #fff;
border-radius: 0.5rem;
box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.dropdown-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
.dropdown-enter-active {
animation: dropdown-enter 0.2s ease-in;
}
.dropdown-leave-active {
animation: dropdown-leave 0.2s ease-in;
}
</style>
@@ -1,10 +0,0 @@
export interface TreeNode {
label: string
id?: string | number
value?: any
children?: TreeNode[]
onSelect?: () => void
[key: string]: any
}
export interface TreeData {}
@@ -1,11 +1,7 @@
<template> <template>
<div class="pos-relative w-300px h-full"> <div class="avatar-list">
<template v-for="part in infoStore.avatarParts" :key="part"> <template v-for="part in infoStore.avatarParts" :key="part">
<div <div class="avatar-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
:style="infoStyle(part)"
@click="choosePart(part)"
>
{{ partName(part) }} {{ partName(part) }}
</div> </div>
</template> </template>
@@ -66,4 +62,26 @@ const partName = (part: string) => {
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.avatar-list {
position: relative;
width: var(--ui-avatar-board-inline-size, 300px);
height: var(--ui-avatar-board-block-size, 177px);
flex: 0 0 auto;
}
.avatar-list__slot {
position: absolute;
display: flex;
box-sizing: border-box;
width: var(--ui-avatar-slot-size, 26px);
height: var(--ui-avatar-slot-size, 26px);
align-items: center;
justify-content: center;
border: var(--ui-component-stroke-size, 1px) solid rgb(122 122 122 / 50%);
color: rgb(255 255 255 / 50%);
line-height: 1;
white-space: nowrap;
cursor: pointer;
}
</style>
@@ -1,18 +1,19 @@
<template> <template>
<div class="pos-relative w-248px h-full" :class="props.withBg ? 'emblem-bg' : ''"> <div class="emblem-list" :class="props.withBg ? 'emblem-bg' : ''">
<template v-for="part in parts" :key="part"> <template v-for="part in parts" :key="part">
<div <div class="emblem-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
class="pos-absolute text-white/50 flex items-center justify-center rounded-50%"
:style="infoStyle(part)"
@click="choosePart(part)"
>
<img <img
class="w-25px h-25px rounded-50%" class="emblem-list__rarity"
style="object-fit: none; object-position: hue; mix-blend-mode: screen" style="object-fit: none; object-position: hue; mix-blend-mode: screen"
:src="rarityIcon(part)" :src="rarityIcon(part)"
v-if="part != '白金'" v-if="part != '白金'"
/> />
<img class="pos-absolute" :style="lvStyle" :src="lvIcon(part)" v-if="part != '白金'" /> <img
class="emblem-list__level"
:style="lvStyle"
:src="lvIcon(part)"
v-if="part != '白金'"
/>
</div> </div>
</template> </template>
</div> </div>
@@ -102,7 +103,6 @@ const lvStyle = computed(() => {
const infoStyle = computed(() => { const infoStyle = computed(() => {
return (part: string) => { return (part: string) => {
console.log(`url(${rarityIcon.value(part)}),url(${partIcon.value(part)})`)
return { return {
left: `${position[part as keyof typeof position].x}px`, left: `${position[part as keyof typeof position].x}px`,
top: `${position[part as keyof typeof position].y}px`, top: `${position[part as keyof typeof position].y}px`,
@@ -129,6 +129,35 @@ const choosePart = (part: string) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.emblem-list {
position: relative;
width: var(--ui-emblem-board-inline-size, 248px);
height: var(--ui-emblem-board-block-size, 179px);
flex: 0 0 auto;
}
.emblem-list__slot {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: rgb(255 255 255 / 50%);
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
.emblem-list__rarity {
width: 25px;
height: 25px;
border-radius: 50%;
}
.emblem-list__level {
position: absolute;
}
.emblem-bg { .emblem-bg {
background-image: url(../Images/head.png); background-image: url(../Images/head.png);
} }
@@ -1,18 +1,23 @@
<template> <template>
<template v-if="props.equipment"> <template v-if="props.equipment">
<div class="relative w-28px h-28px shrink-0" :class="{ inactive: props.inactive }"> <div class="equipment-icon" :class="{ 'equipment-icon--inactive': props.inactive }">
<div <div
class="w-28px h-28px" class="equipment-icon__image"
:style="{ :style="{
backgroundImage: `url(${getImageURL((useDisplayUrl ? props.equipment?.displayUrl : props.equipment?.imageUrl) || '')})`, backgroundImage: `url(${getImageURL((useDisplayUrl ? props.equipment?.displayUrl : props.equipment?.imageUrl) || '')})`,
}" }"
> >
<img :src="getLocalImageURL(rarity)" /> <img
v-if="rarity"
class="equipment-icon__rarity-frame"
:src="getLocalImageURL(rarity)"
alt=""
/>
</div> </div>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="box-border w-28px h-28px shrink-0 border-1px border-solid border-#7a7a7a/50"></div> <div class="equipment-icon equipment-icon--empty"></div>
</template> </template>
</template> </template>
@@ -47,38 +52,43 @@ const getLocalImageURL = (rarity: string) => {
return new URL(`./rarity/${rarity}.png`, import.meta.url).href return new URL(`./rarity/${rarity}.png`, import.meta.url).href
} }
</script> </script>
``
<style lang="scss" scoped> <style lang="scss" scoped>
// @keyframes primeval { .equipment-icon {
// @for $i from 0 through 29 { position: relative;
// #{percentage($i / 29)} { width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
// background-image: url("./effect/primeval/#{$i}.png"); height: var(--ui-equipment-icon-size, var(--ui-item-image-size));
// } flex: 0 0 auto;
// }
// }
// .primeval_effect { &__image {
// width: 28px; position: relative;
// height: 28px; width: 100%;
// background-size: cover; height: 100%;
// animation: primeval 2s infinite; background-position: center;
// // 设置动画名称,持续时间,和循环次数 background-repeat: no-repeat;
// mix-blend-mode: color-dodge; background-size: contain;
// } }
.inactive { &__rarity-frame {
&::after {
content: '';
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0; width: 100%;
right: 0; height: 100%;
bottom: 0; pointer-events: none;
width: 28px; }
height: 28px;
background-color: #00000090; &--empty {
// filter: grayscale(100%); box-sizing: border-box;
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border, var(--ui-border));
}
&--inactive::after {
position: absolute;
z-index: 1;
inset: 0;
background: var(--ui-equipment-icon-inactive-overlay, var(--ui-item-slot-lock-overlay));
content: '';
pointer-events: none;
} }
} }
</style> </style>
@@ -1,51 +1,37 @@
<template> <template>
<div class="bg-hex-222222"> <div class="equipment-info">
<div <div class="equipment-info__panel">
:class="['bg-black text-shadow text-12px h-auto', 'text-#9D9476']" <div class="equipment-info__header">
style=" <div class="equipment-info__icon">
width: 220px;
float: left;
padding: 5px;
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
background-color: rgb(0, 0, 0);
"
>
<div class="h-30px flex items-left my-2px">
<div style="position: relative" class="w-28px mr-2px">
<Icon :equipment="props.equipment"></Icon> <Icon :equipment="props.equipment"></Icon>
</div> </div>
<div style="line-height: 15px"> <div class="equipment-info__identity">
<div :class="[rarity, 'pl-5px']">{{ props.equipment?.name }}</div> <div :class="[rarity, 'equipment-info__name']">{{ props.equipment?.name }}</div>
<div class="pl-5px text-#3ea74e">{{ props.equipment?.categorize }}</div> <div class="equipment-info__category">{{ props.equipment?.categorize }}</div>
</div> </div>
</div> </div>
<!-- 装备图标 装备名称 --> <!-- 装备图标 装备名称 -->
<div class="divide"></div> <div class="equipment-info__divider"></div>
<div class="flex flex-col items-end my-1px" style="line-height: 15px"> <div class="equipment-info__section equipment-info__section--end">
<div :class="rarity">{{ props.equipment?.rarity }}</div> <div :class="rarity">{{ props.equipment?.rarity }}</div>
<div class="text-#7C6430">{{ props.equipment?.itemDetailType }}</div> <div class="equipment-info__type">{{ props.equipment?.itemDetailType }}</div>
</div> </div>
<!-- 装备基础信息 --> <!-- 装备基础信息 -->
<div class="divide"></div> <div class="equipment-info__divider"></div>
<div <div class="equipment-info__section">
class="flex flex-col items-start my-1px" <div class="equipment-info__attributes" v-html="mainAttr"></div>
style="white-space: pre-wrap; line-height: 15px"
>
<div class="text-#908150 py-4px" v-html="mainAttr"></div>
</div> </div>
<!-- 装备属性 --> <!-- 装备属性 -->
<div class="divide"></div> <div class="equipment-info__divider"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px"> <div class="equipment-info__section">
<div class="text-#DED29A py-4px whitespace-pre-wrap" v-html="detail"></div> <div class="equipment-info__detail" v-html="detail"></div>
</div> </div>
<template v-if="bufferDetail"> <template v-if="bufferDetail">
<div class="divide"></div> <div class="equipment-info__divider"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px"> <div class="equipment-info__section">
<div class="text-#A48B2E mb-10px pt-4px">辅助职业专属属性</div> <div class="equipment-info__buffer-heading">辅助职业专属属性</div>
<div class="text-#DED29A pb-10px whitespace-pre-wrap" v-html="bufferDetail"></div> <div class="equipment-info__buffer-detail" v-html="bufferDetail"></div>
</div> </div>
</template> </template>
</div> </div>
@@ -93,7 +79,7 @@ const mainAttr = computed(() => {
const detail = computed(() => const detail = computed(() =>
props.equipment?.detail?.replace( props.equipment?.detail?.replace(
/\[(?![^\]]*技能)[^[\]]+\]/g, /\[(?![^\]]*技能)[^[\]]+\]/g,
(match) => `<span style="color:#3d9147">${match}</span>`, (match) => `<span class="equipment-info__markup">${match}</span>`,
), ),
) )
@@ -102,14 +88,114 @@ const bufferDetail = computed(() => {
if (!detail || detail == '-') return undefined if (!detail || detail == '-') return undefined
return props.equipment?.bufferDetail.replace( return props.equipment?.bufferDetail.replace(
/\[(?![^\]]*技能)[^[\]]+\]/g, /\[(?![^\]]*技能)[^[\]]+\]/g,
(match) => `<span style="color:#3d9147">${match}</span>`, (match) => `<span class="equipment-info__markup">${match}</span>`,
) )
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.divide { .equipment-info {
border-top: 1px solid rgba(243, 244, 246, 0.3); display: flow-root;
margin: 2px 0; width: fit-content;
background: var(--ui-equipment-info-shell-background, var(--ui-surface));
&__panel {
width: var(--ui-equipment-info-inline-size, 220px);
height: auto;
padding: var(--ui-equipment-info-padding, 5px);
background-color: var(--ui-equipment-info-background, var(--ui-tooltip-background));
background-position: center;
background-repeat: no-repeat;
background-size: 100%;
color: var(--ui-equipment-info-foreground, var(--ui-muted-foreground));
font-size: var(--ui-control-font-size-compact);
text-shadow:
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow));
}
&__header {
display: flex;
height: var(--ui-equipment-info-row-block-size, 30px);
align-items: center;
margin-block: var(--ui-equipment-info-section-gap, 2px);
}
&__icon {
position: relative;
width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
flex: 0 0 auto;
margin-right: var(--ui-equipment-info-section-gap, 2px);
}
&__identity,
&__section {
line-height: var(--ui-equipment-info-line-height, 15px);
}
&__identity {
min-width: 0;
}
&__name,
&__category {
padding-left: var(--ui-equipment-info-padding, 5px);
}
&__category {
color: var(--ui-equipment-info-category, var(--ui-success));
}
&__divider {
margin-block: var(--ui-equipment-info-section-gap, 2px);
border-top: var(--ui-component-stroke-size) solid
var(--ui-equipment-info-divider, var(--ui-border));
}
&__section {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-block: var(--ui-equipment-info-section-margin-block, 1px);
white-space: pre-wrap;
&--end {
align-items: flex-end;
}
}
&__type {
color: var(--ui-equipment-info-type, var(--ui-secondary-foreground));
}
&__attributes {
padding-block: var(--ui-equipment-info-detail-padding-block, 4px);
color: var(--ui-equipment-info-attribute, var(--ui-secondary-foreground));
}
&__detail,
&__buffer-detail {
color: var(--ui-equipment-info-detail, var(--ui-tooltip-foreground));
white-space: pre-wrap;
}
&__detail {
padding-block: var(--ui-equipment-info-detail-padding-block, 4px);
}
&__buffer-heading {
margin-bottom: var(--ui-equipment-info-buffer-gap, 10px);
padding-top: var(--ui-equipment-info-detail-padding-block, 4px);
color: var(--ui-equipment-info-buffer-heading, var(--ui-warning));
}
&__buffer-detail {
padding-bottom: var(--ui-equipment-info-buffer-gap, 10px);
}
:deep(.equipment-info__markup) {
color: var(--ui-equipment-info-markup, var(--ui-success));
}
} }
</style> </style>
@@ -1,14 +1,10 @@
<template> <template>
<div <div
class="pos-relative w-264px h-full" class="equipment-list"
:style="{ backgroundImage: withBg ? `url(${getImageURL('/common/head.png')})` : 'none' }" :style="{ backgroundImage: withBg ? `url(${getImageURL('/common/head.png')})` : 'none' }"
> >
<template v-for="part in parts" :key="part"> <template v-for="part in parts" :key="part">
<div <div class="equipment-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
:style="infoStyle(part)"
@click="choosePart(part)"
>
<template v-if="detail?.[part]?.id"> <template v-if="detail?.[part]?.id">
<EquipmentIcon :equipment="equ(detail[part].id)" /> <EquipmentIcon :equipment="equ(detail[part].id)" />
</template> </template>
@@ -16,14 +12,10 @@
{{ partName(part) }} {{ partName(part) }}
</template> </template>
</div> </div>
<!-- <div v-if="detail?.[part]?.fusion" class="pos-absolute" :style="fusionStyle(part)">
<EquipmentIcon :equipment="oaths(detail[part].fusion)" />
</div> -->
</template> </template>
<div <div
v-if="props.withSubWeapon" v-if="props.withSubWeapon"
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center" class="equipment-list__slot equipment-list__slot--subweapon"
style="left: 150px; top: 5px; z-index: 8"
@click="choosePart('副武器')" @click="choosePart('副武器')"
> >
<template v-if="detail?.['副武器']?.id"> <template v-if="detail?.['副武器']?.id">
@@ -31,12 +23,7 @@
</template> </template>
<template v-else> 副武 </template> <template v-else> 副武 </template>
</div> </div>
<img <img v-if="props.avatar" :src="props.avatar" alt="Avatar" class="equipment-list__avatar" />
v-if="props.avatar"
:src="props.avatar"
alt="Avatar"
class="pos-absolute w-70% left-1/2 translate-x--1/2 -bottom-10px"
/>
</div> </div>
</template> </template>
@@ -108,13 +95,10 @@ const infoStyle = (part: string) => {
x += (index % 2) * 39 x += (index % 2) * 39
y += Math.floor(index / 2) * 34 y += Math.floor(index / 2) * 34
const style = { return {
left: `${x}px`, '--ui-equipment-slot-left': `${x}px`,
top: `${y}px`, '--ui-equipment-slot-top': `${y}px`,
zIndex: 6,
// fontWeight: 900,
} }
return style
} }
// const fusionStyle = (part: string) => { // const fusionStyle = (part: string) => {
@@ -152,4 +136,50 @@ const partName = (part: string) => {
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.equipment-list {
position: relative;
width: var(--ui-equipment-board-inline-size, 264px);
/* Board contract: absolute-positioned slots need a definite block size in every host. */
height: var(--ui-equipment-board-block-size, 177px);
flex: 0 0 auto;
background-position: top center;
background-repeat: no-repeat;
background-size: var(--ui-equipment-board-inline-size, 264px) auto;
&__slot {
position: absolute;
z-index: var(--ui-equipment-slot-z-index, 6);
left: var(--ui-equipment-slot-left);
top: var(--ui-equipment-slot-top);
box-sizing: border-box;
display: flex;
width: var(--ui-equipment-slot-size, 26px);
height: var(--ui-equipment-slot-size, 26px);
align-items: center;
justify-content: center;
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border, var(--ui-border));
color: var(--ui-equipment-slot-foreground, var(--ui-muted-foreground));
font-size: var(--ui-equipment-preview-slot-font-size, 12px);
line-height: 1;
white-space: nowrap;
&--subweapon {
--ui-equipment-slot-left: var(--ui-equipment-subweapon-left, 150px);
--ui-equipment-slot-top: var(--ui-equipment-subweapon-top, 5px);
z-index: calc(var(--ui-equipment-slot-z-index, 6) + 2);
}
}
&__avatar {
position: absolute;
left: 50%;
bottom: calc(0px - var(--ui-equipment-avatar-bottom-offset, 10px));
width: var(--ui-equipment-avatar-inline-size, 70%);
max-height: 100%;
object-fit: contain;
transform: translateX(-50%);
}
}
</style>
@@ -1,14 +1,10 @@
<template> <template>
<div <div
class="pos-relative w-264px h-full" class="equipment-oath"
:style="{ backgroundImage: withBg ? `url(${getImageURL('/common/oath/head.png')})` : 'none' }" :style="{ backgroundImage: withBg ? `url(${getImageURL('/common/oath/head.png')})` : 'none' }"
> >
<template v-for="part in parts" :key="part"> <template v-for="part in parts" :key="part">
<div <div class="equipment-oath__slot" :style="infoStyle(part)" @click="choosePart(part)">
class="pos-absolute text-white/50 w-26px h-26px border-1px border-solid border-#7a7a7a/50 flex items-center justify-center"
:style="infoStyle(part)"
@click="choosePart(part)"
>
<template v-if="detail?.[part]?.id"> <template v-if="detail?.[part]?.id">
<EquipmentIcon :equipment="equ(detail[part].id, part)" useDisplayUrl /> <EquipmentIcon :equipment="equ(detail[part].id, part)" useDisplayUrl />
</template> </template>
@@ -16,16 +12,7 @@
{{ part != '11' ? '星蕴' : '誓约' }} {{ part != '11' ? '星蕴' : '誓约' }}
</template> </template>
</div> </div>
<!-- <div v-if="detail?.[part]?.fusion" class="pos-absolute" :style="fusionStyle(part)">
<EquipmentIcon :equipment="oaths(detail[part].fusion)" />
</div> -->
</template> </template>
<!-- <img
v-if="props.avatar"
:src="props.avatar"
alt="Avatar"
class="pos-absolute w-70% left-1/2 translate-x--1/2 -bottom-10px"
/> -->
</div> </div>
</template> </template>
@@ -158,20 +145,12 @@ const positionSimple = [
] ]
const infoStyle = (part: string) => { const infoStyle = (part: string) => {
if (props.withBg) const coordinates = (props.withBg ? position : positionSimple)[Number.parseInt(part)]
return {
left: `${position[parseInt(part)].x}px`, return {
top: `${position[parseInt(part)].y}px`, '--ui-equipment-slot-left': `${coordinates.x}px`,
zIndex: 6, '--ui-equipment-slot-top': `${coordinates.y}px`,
// fontWeight: 900, }
}
else
return {
left: `${positionSimple[parseInt(part)].x}px`,
top: `${positionSimple[parseInt(part)].y}px`,
zIndex: 6,
// fontWeight: 900,
}
} }
const choosePart = (part: string) => { const choosePart = (part: string) => {
@@ -179,4 +158,32 @@ const choosePart = (part: string) => {
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.equipment-oath {
position: relative;
width: var(--ui-equipment-board-inline-size, 264px);
height: 100%;
flex: 0 0 auto;
background-position: top center;
background-repeat: no-repeat;
background-size: var(--ui-equipment-board-inline-size, 264px) auto;
&__slot {
position: absolute;
z-index: var(--ui-equipment-slot-z-index, 6);
left: var(--ui-equipment-slot-left);
top: var(--ui-equipment-slot-top);
box-sizing: border-box;
display: flex;
width: var(--ui-equipment-slot-size, 26px);
height: var(--ui-equipment-slot-size, 26px);
align-items: center;
justify-content: center;
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border, var(--ui-border));
color: var(--ui-equipment-slot-foreground, var(--ui-muted-foreground));
font-size: var(--ui-equipment-preview-slot-font-size, 12px);
line-height: 1;
white-space: nowrap;
}
}
</style>
@@ -1,16 +1,19 @@
<template> <template>
<calc-cascader <UiTreeSelect
class="!h-5 flex-1" class="part-select"
v-model="selected" v-model="selected"
:items="items" :items="items"
select-parents
placeholder="请输入名称搜索" placeholder="请输入名称搜索"
></calc-cascader> label="装备部位"
@select="selectPart"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { TreeNode } from '@/components/calc/tree/types' import { UiTreeSelect, type UiTreeSelectItem } from '@/components/ui/tree-select'
import { useInfoStore } from '@/stores' import { useInfoStore } from '@/stores'
import { syncRef, useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
const props = defineProps({ const props = defineProps({
partList: { partList: {
@@ -27,7 +30,9 @@ const special_parts = ['辅助装备', '魔法石', '耳环']
const else_parts = ['称号', '宠物'] const else_parts = ['称号', '宠物']
const emit = defineEmits([]) const emit = defineEmits<{
'update:partList': [value: string[]]
}>()
const choose_part = useVModel(props, 'partList', emit, { passive: true }) const choose_part = useVModel(props, 'partList', emit, { passive: true })
@@ -35,95 +40,92 @@ const selected = ref('')
const infoStore = useInfoStore() const infoStore = useInfoStore()
const items = computed<TreeNode[]>(() => { const partGroups: Record<string, string[]> = {
防具: armor_parts,
首饰: jewel_parts,
特殊装备: special_parts,
其它: else_parts,
}
function selectPart(item: UiTreeSelectItem) {
if (!item.value) {
choose_part.value = []
return
}
if (item.value === '武器') {
choose_part.value = infoStore.infos?.weapons ?? []
return
}
choose_part.value = partGroups[item.value] ? [...partGroups[item.value]] : [item.value]
}
const items = computed<UiTreeSelectItem[]>(() => {
return [ return [
{ {
label: '全部类别', label: '全部类别',
value: '', value: '',
onSelect() {
choose_part.value = []
},
}, },
{ {
label: '武器', label: '武器',
value: '武器', value: '武器',
onSelect() {
choose_part.value = infoStore.infos?.weapons ?? []
},
children: infoStore.infos?.weapons.map((r) => { children: infoStore.infos?.weapons.map((r) => {
return { return {
label: r, label: r,
value: r, value: r,
onSelect() {
choose_part.value = [r]
},
} }
}), }),
}, },
{ {
label: '防具', label: '防具',
value: '防具', value: '防具',
onSelect() {
choose_part.value = [...armor_parts]
},
children: armor_parts.map((part) => { children: armor_parts.map((part) => {
return { return {
label: part, label: part,
value: part, value: part,
onSelect() {
choose_part.value = [part]
},
} }
}), }),
}, },
{ {
label: '首饰', label: '首饰',
value: '首饰', value: '首饰',
onSelect() {
choose_part.value = [...jewel_parts]
},
children: jewel_parts.map((part) => { children: jewel_parts.map((part) => {
return { return {
label: part, label: part,
value: part, value: part,
onSelect() {
choose_part.value = [part]
},
} }
}), }),
}, },
{ {
label: '特殊装备', label: '特殊装备',
value: '特殊装备', value: '特殊装备',
onSelect() {
choose_part.value = [...special_parts]
},
children: special_parts.map((part) => { children: special_parts.map((part) => {
return { return {
label: part, label: part,
value: part, value: part,
onSelect() {
choose_part.value = [part]
},
} }
}), }),
}, },
{ {
label: '其它', label: '其它',
value: '其它', value: '其它',
onSelect() {
choose_part.value = [...else_parts]
},
children: else_parts.map((part) => { children: else_parts.map((part) => {
return { return {
label: part, label: part,
value: part, value: part,
onSelect() {
choose_part.value = [part]
},
} }
}), }),
}, },
] ]
}) })
</script> </script>
<style scoped>
.part-select {
--ui-control-height-default: var(--ui-control-height-compact);
min-width: min(100%, var(--ui-tree-select-min-inline-size));
flex: 1 1 var(--ui-tree-select-min-inline-size);
}
</style>
@@ -1,42 +1,43 @@
<template> <template>
<div class="flex gap-8px"> <div class="equipment-rarity-select">
<template v-for="(rarity, _) in rarities" :key="_"> <template v-for="rarity in rarities" :key="rarity.value">
<div <div
class="w-14px h-14px shadow-[0px_0px_3px_#ffffff_inset,0px_0px_1px_#ffffff40]" class="equipment-rarity-select__swatch"
:style="{ background: rarity.background }" :class="rarity.className"
@click="choose(rarity.value)" @click="choose(rarity.value)"
> >
<div class="w-14px h-14px" :class="{ active: value === rarity.value }"></div> <div
class="equipment-rarity-select__mark"
:class="{ 'equipment-rarity-select__mark--active': value === rarity.value }"
></div>
</div> </div>
</template> </template>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps({})
const value = defineModel<string>() const value = defineModel<string>()
const rarities = [ const rarities = [
{ {
value: '稀有', value: '稀有',
background: '#9879da', className: 'equipment-rarity-select__swatch--rare',
}, },
{ {
value: '神器', value: '神器',
background: '#ef28c1', className: 'equipment-rarity-select__swatch--artifact',
}, },
{ {
value: '传说', value: '传说',
background: '#fc8711', className: 'equipment-rarity-select__swatch--legend',
}, },
{ {
value: '史诗', value: '史诗',
background: '#fcb600', className: 'equipment-rarity-select__swatch--epic',
}, },
{ {
value: '太初', value: '太初',
background: 'linear-gradient(to bottom, #58fa84 20%, #58decc 40%, #58c6ff 60%)', className: 'equipment-rarity-select__swatch--primeval',
}, },
] ]
@@ -50,8 +51,63 @@ const choose = (rarity: string) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.active { .equipment-rarity-select {
background: url('@/assets/img/control/item_checked.png') no-repeat center; display: flex;
background-size: 12px 10px; align-items: center;
gap: var(--ui-equipment-rarity-swatch-gap, 8px);
&__swatch,
&__mark {
width: var(--ui-equipment-rarity-swatch-size, 14px);
height: var(--ui-equipment-rarity-swatch-size, 14px);
}
&__swatch {
flex: 0 0 auto;
box-shadow:
inset 0 0 3px var(--ui-equipment-rarity-swatch-highlight, var(--ui-popover-foreground)),
0 0 1px
color-mix(
in srgb,
var(--ui-equipment-rarity-swatch-highlight, var(--ui-popover-foreground)) 25%,
transparent
);
&--rare {
background: var(--dcalc-quality-rare);
}
&--artifact {
background: var(--dcalc-quality-artifact);
}
&--legend {
background: var(--dcalc-quality-legend);
}
&--epic {
background: var(--dcalc-quality-epic);
}
&--primeval {
background: var(
--ui-equipment-rarity-primeval-background,
linear-gradient(
to bottom,
var(--dcalc-quality-primeval) 20%,
color-mix(in srgb, var(--dcalc-quality-primeval) 55%, var(--ui-info)) 40%,
var(--ui-info) 60%
)
);
}
}
&__mark--active {
background-image: url('@/assets/img/control/item_checked.png');
background-position: center;
background-repeat: no-repeat;
background-size: var(--ui-equipment-rarity-mark-inline-size, 12px)
var(--ui-equipment-rarity-mark-block-size, 10px);
}
} }
</style> </style>
@@ -1,21 +1,11 @@
<template> <template>
<div class="bg-hex-222222"> <div class="equipment-suit-info">
<div <div class="equipment-suit-info__panel">
:class="['bg-black text-shadow text-12px h-auto', 'text-#9D9476']"
style="
width: 220px;
float: left;
padding: 5px;
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
background-color: rgb(0, 0, 0);
"
>
<!-- 装备图标 装备名称 --> <!-- 装备图标 装备名称 -->
<div class="flex w-full items-center justify-center gap-5px"> <div class="equipment-suit-info__header">
<div> <div class="equipment-suit-info__icon">
<img <img
class="equipment-suit-info__icon-image"
:src="getImageURL('equipment' + props.suit?.imageUrl.replace('/suit/', '/suit/icon/'))" :src="getImageURL('equipment' + props.suit?.imageUrl.replace('/suit/', '/suit/icon/'))"
alt="装备图标" alt="装备图标"
/> />
@@ -23,25 +13,22 @@
<div :class="rarity">{{ props.suit?.name }}</div> <div :class="rarity">{{ props.suit?.name }}</div>
<div :class="rarity">{{ props.suit?.point }}点</div> <div :class="rarity">{{ props.suit?.point }}点</div>
</div> </div>
<div class="divide"></div> <div class="equipment-suit-info__divider"></div>
<!-- 装备基础信息 --> <!-- 装备基础信息 -->
<div <div class="equipment-suit-info__section">
class="flex flex-col items-start my-1px" <div class="equipment-suit-info__attributes" v-html="mainAttr"></div>
style="white-space: pre-wrap; line-height: 15px"
>
<div class="text-#908150" v-html="mainAttr"></div>
</div> </div>
<!-- 装备属性 --> <!-- 装备属性 -->
<div class="divide"></div> <div class="equipment-suit-info__divider"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px"> <div class="equipment-suit-info__section">
<div class="text-#DED29A py-4px whitespace-pre-wrap" v-html="detail"></div> <div class="equipment-suit-info__detail" v-html="detail"></div>
</div> </div>
<template v-if="bufferDetail"> <template v-if="bufferDetail">
<div class="divide"></div> <div class="equipment-suit-info__divider"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px"> <div class="equipment-suit-info__section">
<div class="text-#A48B2E mb-10px pt-4px">辅助职业专属属性</div> <div class="equipment-suit-info__buffer-heading">辅助职业专属属性</div>
<div class="text-#DED29A pb-10px whitespace-pre-wrap" v-html="bufferDetail"></div> <div class="equipment-suit-info__buffer-detail" v-html="bufferDetail"></div>
</div> </div>
</template> </template>
</div> </div>
@@ -76,7 +63,7 @@ const mainAttr = computed(() => {
const detail = computed(() => const detail = computed(() =>
props.suit?.value?.replace( props.suit?.value?.replace(
/\[(?![^\]]*技能)[^[\]]+\]/g, /\[(?![^\]]*技能)[^[\]]+\]/g,
(match) => `<span style="color:#3d9147">${match}</span>`, (match) => `<span class="equipment-suit-info__markup">${match}</span>`,
), ),
) )
@@ -85,14 +72,97 @@ const bufferDetail = computed(() => {
if (!detail || detail == '-') return undefined if (!detail || detail == '-') return undefined
return props.suit?.bufferValue?.replace( return props.suit?.bufferValue?.replace(
/\[(?![^\]]*技能)[^[\]]+\]/g, /\[(?![^\]]*技能)[^[\]]+\]/g,
(match) => `<span style="color:#3d9147">${match}</span>`, (match) => `<span class="equipment-suit-info__markup">${match}</span>`,
) )
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.divide { .equipment-suit-info {
border-top: 1px solid rgba(243, 244, 246, 0.3); display: flow-root;
margin: 2px 0; width: fit-content;
background: var(--ui-equipment-info-shell-background, var(--ui-surface));
&__panel {
width: var(--ui-equipment-info-inline-size, 220px);
height: auto;
padding: var(--ui-equipment-info-padding, 5px);
background-color: var(--ui-equipment-info-background, var(--ui-tooltip-background));
background-position: center;
background-repeat: no-repeat;
background-size: 100%;
color: var(--ui-equipment-info-foreground, var(--ui-muted-foreground));
font-size: var(--ui-control-font-size-compact);
text-shadow:
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow)),
0 0 1px var(--ui-equipment-info-text-shadow, var(--ui-tooltip-shadow));
}
&__header {
display: flex;
min-height: var(--ui-equipment-info-row-block-size, 30px);
align-items: center;
justify-content: center;
gap: var(--ui-equipment-info-padding, 5px);
line-height: var(--ui-equipment-info-line-height, 15px);
}
&__icon,
&__icon-image {
width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
height: var(--ui-equipment-icon-size, var(--ui-item-image-size));
}
&__icon {
flex: 0 0 auto;
}
&__icon-image {
object-fit: contain;
}
&__divider {
margin-block: var(--ui-equipment-info-section-gap, 2px);
border-top: var(--ui-component-stroke-size) solid
var(--ui-equipment-info-divider, var(--ui-border));
}
&__section {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-block: var(--ui-equipment-info-section-margin-block, 1px);
line-height: var(--ui-equipment-info-line-height, 15px);
white-space: pre-wrap;
}
&__attributes {
color: var(--ui-equipment-info-attribute, var(--ui-secondary-foreground));
}
&__detail,
&__buffer-detail {
color: var(--ui-equipment-info-detail, var(--ui-tooltip-foreground));
white-space: pre-wrap;
}
&__detail {
padding-block: var(--ui-equipment-info-detail-padding-block, 4px);
}
&__buffer-heading {
margin-bottom: var(--ui-equipment-info-buffer-gap, 10px);
padding-top: var(--ui-equipment-info-detail-padding-block, 4px);
color: var(--ui-equipment-info-buffer-heading, var(--ui-warning));
}
&__buffer-detail {
padding-bottom: var(--ui-equipment-info-buffer-gap, 10px);
}
:deep(.equipment-suit-info__markup) {
color: var(--ui-equipment-info-markup, var(--ui-success));
}
} }
</style> </style>
+9 -4
View File
@@ -1,6 +1,6 @@
import { h, reactive, renderList } from 'vue' import { h, reactive, renderList } from 'vue'
import { createSharedComposable } from '@vueuse/core' import { createSharedComposable } from '@vueuse/core'
import ActionDialog from '@/components/calc/action-dialog/index.vue' import { UiActionDialog } from '@/components/ui/dialog'
import type { JSX } from 'vue/jsx-runtime' import type { JSX } from 'vue/jsx-runtime'
export type ConfirmDialogOption = Omit<ShowDialogOption, 'okButton' | 'cancelButton'> export type ConfirmDialogOption = Omit<ShowDialogOption, 'okButton' | 'cancelButton'>
@@ -87,14 +87,14 @@ export const useDialog = createSharedComposable(() => {
return id return id
} }
function close(id: DialogID, status = 'cancel', duration = 400) { function close(id: DialogID, status: DialogResult['status'] = 'cancel', duration = 400) {
const dialog = dialogs.get(id) const dialog = dialogs.get(id)
if (!dialog) { if (!dialog) {
return return
} }
dialog.resolve({ dialog.resolve({
id, id,
status: 'ok', status,
get isOk() { get isOk() {
return status == 'ok' return status == 'ok'
}, },
@@ -119,6 +119,10 @@ export const useDialog = createSharedComposable(() => {
} }
async function show(option: ShowDialogOption = {}) { async function show(option: ShowDialogOption = {}) {
const activeElement = typeof document === 'undefined' ? null : document.activeElement
const returnFocusElement =
activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null
return new Promise<DialogResult>((resolve) => { return new Promise<DialogResult>((resolve) => {
const { const {
id = randomId(), id = randomId(),
@@ -145,7 +149,7 @@ export const useDialog = createSharedComposable(() => {
resolve, resolve,
render(visible = true) { render(visible = true) {
return h( return h(
ActionDialog, UiActionDialog,
{ {
header, header,
okButton, okButton,
@@ -154,6 +158,7 @@ export const useDialog = createSharedComposable(() => {
visible, visible,
onClose, onClose,
mask, mask,
returnFocusElement,
scale, scale,
}, },
{ {
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ButtonHTMLAttributes } from 'vue'
import type { AsTag } from 'reka-ui' import type { AsTag } from 'reka-ui'
import { Primitive } from 'reka-ui' import { Primitive } from 'reka-ui'
import { buttonVariants, type ButtonVariants } from './buttonVariants' import { buttonVariants, type ButtonVariants } from './buttonVariants'
@@ -12,22 +13,34 @@ withDefaults(
asChild?: boolean asChild?: boolean
variant?: ButtonVariants['variant'] variant?: ButtonVariants['variant']
size?: ButtonVariants['size'] size?: ButtonVariants['size']
disabled?: boolean
type?: ButtonHTMLAttributes['type']
title?: string
}>(), }>(),
{ {
as: 'button', as: 'button',
asChild: false, asChild: false,
variant: 'primary', variant: 'primary',
size: 'default', size: 'default',
type: 'button',
}, },
) )
const emits = defineEmits<{
click: [event: MouseEvent]
}>()
</script> </script>
<template> <template>
<Primitive <Primitive
:as="as" :as="as"
:as-child="asChild" :as-child="asChild"
:disabled="disabled"
:type="type"
:title="title"
:class="cn(buttonVariants({ variant, size }), $attrs.class ?? '')" :class="cn(buttonVariants({ variant, size }), $attrs.class ?? '')"
v-bind="$attrs" v-bind="$attrs"
@click="emits('click', $event)"
> >
<slot /> <slot />
</Primitive> </Primitive>
@@ -0,0 +1,420 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed, nextTick, ref, useId, watch } from 'vue'
import { cn } from '@/lib/utils'
export interface UiCommandInputOption {
value: string
label: string
prefix?: string
tone?: 'default' | 'success' | 'warning' | 'danger'
disabled?: boolean
}
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
suggestions?: UiCommandInputOption[]
prefix?: string
prefixTone?: UiCommandInputOption['tone']
placeholder?: string
disabled?: boolean
readonly?: boolean
openOnFocus?: boolean
filterSuggestions?: boolean
label?: string
}>(),
{
suggestions: () => [],
prefix: '',
prefixTone: 'default',
openOnFocus: false,
filterSuggestions: false,
label: '命令输入',
},
)
const emit = defineEmits<{
select: [option: UiCommandInputOption]
}>()
const model = defineModel<string>({ default: '' })
const open = defineModel<boolean>('open', { default: false })
const root = ref<HTMLElement>()
const input = ref<HTMLInputElement>()
const highlightedIndex = ref(-1)
const listboxId = `ui-command-input-${useId()}`
const visibleSuggestions = computed(() => {
if (!props.filterSuggestions || !model.value.trim()) return props.suggestions
const query = model.value.trim().toLocaleLowerCase()
return props.suggestions.filter((option) =>
[option.value, option.label, option.prefix]
.filter(Boolean)
.some((text) => text!.toLocaleLowerCase().includes(query)),
)
})
const listVisible = computed(
() => open.value && !props.disabled && visibleSuggestions.value.length > 0,
)
const activeDescendant = computed(() => {
if (!listVisible.value || highlightedIndex.value < 0) return undefined
return `${listboxId}-option-${highlightedIndex.value}`
})
function firstEnabledIndex(options = visibleSuggestions.value) {
return options.findIndex((option) => !option.disabled)
}
function lastEnabledIndex(options = visibleSuggestions.value) {
return options.findLastIndex((option) => !option.disabled)
}
function moveHighlight(direction: 1 | -1) {
const options = visibleSuggestions.value
if (!options.length) return
let index = highlightedIndex.value
for (let count = 0; count < options.length; count += 1) {
index = (index + direction + options.length) % options.length
if (!options[index]?.disabled) {
highlightedIndex.value = index
return
}
}
}
function showSuggestions() {
if (props.disabled || props.readonly || !visibleSuggestions.value.length) return
open.value = true
highlightedIndex.value = firstEnabledIndex()
}
function choose(option: UiCommandInputOption) {
if (props.disabled || props.readonly || option.disabled) return
model.value = option.value
open.value = false
emit('select', option)
void nextTick(() => input.value?.focus())
}
function handleInput() {
void nextTick(showSuggestions)
}
function handleFocus() {
if (props.openOnFocus) showSuggestions()
}
function handleFocusOut(event: FocusEvent) {
const nextTarget = event.relatedTarget
if (!(nextTarget instanceof Node) || !root.value?.contains(nextTarget)) open.value = false
}
function handleKeydown(event: KeyboardEvent) {
if (props.disabled || props.readonly) return
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
if (!listVisible.value) showSuggestions()
else moveHighlight(1)
break
case 'ArrowUp':
event.preventDefault()
if (!listVisible.value) {
showSuggestions()
highlightedIndex.value = lastEnabledIndex()
} else moveHighlight(-1)
break
case 'Home':
if (!listVisible.value) return
event.preventDefault()
highlightedIndex.value = firstEnabledIndex()
break
case 'End':
if (!listVisible.value) return
event.preventDefault()
highlightedIndex.value = lastEnabledIndex()
break
case 'Enter': {
if (!listVisible.value) return
event.preventDefault()
const option = visibleSuggestions.value[highlightedIndex.value]
if (option) choose(option)
break
}
case 'Escape':
if (!listVisible.value) return
event.preventDefault()
open.value = false
break
case 'Tab':
open.value = false
break
}
}
watch(
() => visibleSuggestions.value,
(options) => {
if (!options.length) {
open.value = false
highlightedIndex.value = -1
return
}
if (highlightedIndex.value < 0 || options[highlightedIndex.value]?.disabled) {
highlightedIndex.value = firstEnabledIndex(options)
}
},
)
defineExpose({ focus: () => input.value?.focus() })
</script>
<template>
<div
ref="root"
:class="cn('ui-command-input', disabled && 'is-disabled', props.class)"
@focusout="handleFocusOut"
>
<div class="ui-command-input__field" :class="{ 'is-open': listVisible }">
<span
v-if="prefix || $slots.prefix"
class="ui-command-input__prefix"
:class="`is-${prefixTone}`"
>
<slot name="prefix">{{ prefix }}</slot>
</span>
<input
ref="input"
v-model="model"
class="ui-command-input__input"
type="text"
role="combobox"
autocomplete="off"
aria-autocomplete="list"
:aria-label="label"
:aria-controls="listboxId"
:aria-expanded="listVisible"
:aria-activedescendant="activeDescendant"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
/>
</div>
<div v-if="listVisible" :id="listboxId" class="ui-command-input__suggestions" role="listbox">
<button
v-for="(option, index) in visibleSuggestions"
:id="`${listboxId}-option-${index}`"
:key="option.value"
type="button"
role="option"
class="ui-command-input__option"
:class="{ 'is-highlighted': highlightedIndex === index }"
:aria-selected="model === option.value"
:disabled="option.disabled"
@mouseenter="highlightedIndex = index"
@click="choose(option)"
>
<span
v-if="option.prefix ?? prefix"
class="ui-command-input__option-prefix"
:class="`is-${option.tone ?? prefixTone}`"
>
{{ option.prefix ?? prefix }}
</span>
<span class="ui-command-input__option-label">{{ option.label }}</span>
</button>
</div>
</div>
</template>
<style scoped lang="scss">
.ui-command-input {
position: relative;
width: 100%;
min-width: 0;
}
.ui-command-input__field {
box-sizing: border-box;
display: flex;
width: 100%;
height: var(--ui-control-height-default);
min-width: 0;
align-items: center;
overflow: hidden;
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-input);
background: var(--ui-field-background);
box-shadow: inset 0 0 0
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
var(--ui-field-inset);
&:focus-within,
&.is-open {
border-color: var(--ui-ring);
box-shadow:
inset 0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
var(--ui-field-focus-inset),
0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
var(--ui-field-focus-edge);
}
}
.ui-command-input__prefix,
.ui-command-input__option-prefix {
box-sizing: border-box;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border-color: var(--ui-command-prefix-border, var(--ui-disclosure-border));
background: var(--ui-command-prefix-background, var(--ui-settings-row-alt));
color: var(--ui-command-prefix-foreground, var(--ui-settings-section-foreground));
line-height: var(--ui-control-line-height);
&.is-success {
color: var(--ui-command-prefix-success, var(--ui-success));
}
&.is-warning {
color: var(--ui-warning);
}
&.is-danger {
color: var(--ui-destructive-foreground);
}
}
.ui-command-input__prefix {
align-self: stretch;
min-width: var(
--ui-command-prefix-min-inline-size,
calc(var(--ui-control-height-default) + var(--ui-control-padding-inline-compact) * 2)
);
padding-inline: var(--ui-field-padding-inline);
border-width: 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) 0 0;
border-style: solid;
}
.ui-command-input__input {
min-width: 0;
height: 100%;
flex: 1;
padding: 0 var(--ui-field-padding-inline);
border: 0;
outline: 0;
background: transparent;
color: var(--ui-foreground);
font: inherit;
font-size: var(--ui-control-font-size-default);
line-height: var(--ui-control-line-height);
&::placeholder {
color: var(--ui-muted-foreground);
}
}
.ui-command-input.is-disabled {
.ui-command-input__field {
border-color: var(--ui-control-disabled-border);
background: var(--ui-field-disabled-background);
}
.ui-command-input__prefix,
.ui-command-input__input {
color: var(--ui-control-disabled-foreground);
}
}
.ui-command-input__suggestions {
position: absolute;
z-index: var(--ui-popup-z-index, var(--ui-settings-dropdown-z-index));
top: calc(100% + var(--ui-autocomplete-trigger-gap));
right: 0;
left: 0;
box-sizing: border-box;
max-height: var(--ui-autocomplete-max-block-size);
overflow-y: auto;
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-popup-border);
background: var(--ui-popup-background);
box-shadow: var(--ui-panel-shadow);
}
.ui-command-input__option {
box-sizing: border-box;
display: flex;
width: 100%;
height: var(--ui-option-height);
align-items: center;
gap: var(--ui-control-gap);
padding: 0 var(--ui-field-padding-inline);
border: 0;
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-option-divider);
outline: 0;
background: transparent;
color: var(--ui-primary-foreground);
font: inherit;
line-height: var(--ui-control-line-height);
text-align: left;
cursor: pointer;
&:last-child {
border-bottom: 0;
}
&:hover:not(:disabled),
&.is-highlighted:not(:disabled) {
outline: var(
--ui-focus-outline-size,
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
)
solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
background: var(--ui-option-highlight-background);
color: var(--ui-option-highlight-foreground);
}
&[aria-selected='true'] {
background: var(--ui-disclosure-row-selected);
color: var(--ui-selected);
}
&:disabled {
color: var(--ui-control-disabled-foreground);
cursor: not-allowed;
}
}
.ui-command-input__option-prefix {
min-width: var(
--ui-command-option-prefix-min-inline-size,
calc(var(--ui-control-height-default) + var(--ui-control-padding-inline-compact))
);
height: var(--ui-badge-height);
padding-inline: var(--ui-badge-padding-inline);
border-width: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4));
border-style: solid;
font-size: var(--ui-badge-font-size);
}
.ui-command-input__option-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -0,0 +1,2 @@
export { default as UiCommandInput } from './UiCommandInput.vue'
export type { UiCommandInputOption } from './UiCommandInput.vue'
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue' import type { CSSProperties, HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export interface UiDataTableColumn { export interface UiDataTableColumn {
@@ -7,21 +7,38 @@ export interface UiDataTableColumn {
label: string label: string
sortable?: boolean sortable?: boolean
width?: string width?: string
align?: 'start' | 'center' | 'end'
} }
export interface UiDataTableRow { export interface UiDataTableRow {
id: string id: string
[key: string]: string kind?: 'data' | 'note'
selectable?: boolean
disabled?: boolean
[key: string]: unknown
} }
const props = defineProps<{ const props = withDefaults(
class?: HTMLAttributes['class'] defineProps<{
columns: UiDataTableColumn[] class?: HTMLAttributes['class']
rows: UiDataTableRow[] columns: UiDataTableColumn[]
selectedId?: string rows: UiDataTableRow[]
sortKey?: string selectedId?: string
sortDirection?: 'ascending' | 'descending' sortKey?: string
}>() sortDirection?: 'ascending' | 'descending'
density?: 'default' | 'compact'
selectionTone?: 'gold' | 'blue'
selectable?: boolean
emptyText?: string
maxBodyHeight?: string
}>(),
{
density: 'default',
selectionTone: 'gold',
selectable: true,
emptyText: '暂无数据',
},
)
const emits = defineEmits<{ const emits = defineEmits<{
select: [row: UiDataTableRow] select: [row: UiDataTableRow]
@@ -29,10 +46,32 @@ const emits = defineEmits<{
}>() }>()
const gridTemplate = computed(() => props.columns.map((column) => column.width ?? '1fr').join(' ')) const gridTemplate = computed(() => props.columns.map((column) => column.width ?? '1fr').join(' '))
const bodyStyle = computed<CSSProperties | undefined>(() =>
props.maxBodyHeight ? { '--ui-table-body-max-block-size': props.maxBodyHeight } : undefined,
)
function isSelectable(row: UiDataTableRow) {
return props.selectable && row.kind !== 'note' && row.selectable !== false && !row.disabled
}
function selectRow(row: UiDataTableRow) {
if (isSelectable(row)) emits('select', row)
}
</script> </script>
<template> <template>
<div :class="cn('ui-data-table', props.class)" role="table"> <div
:class="
cn(
'ui-data-table',
`ui-data-table--${density}`,
`ui-data-table--selection-${selectionTone}`,
maxBodyHeight && 'has-constrained-body',
props.class,
)
"
role="table"
>
<div <div
class="ui-data-table__row ui-data-table__header" class="ui-data-table__row ui-data-table__header"
role="row" role="row"
@@ -49,25 +88,76 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
> >
{{ column.label }}<i aria-hidden="true" /> {{ column.label }}<i aria-hidden="true" />
</button> </button>
<div v-else role="columnheader">{{ column.label }}</div> <div v-else role="columnheader" :class="`is-align-${column.align ?? 'start'}`">
{{ column.label }}
</div>
</template> </template>
</div> </div>
<button <div class="ui-data-table__body" role="rowgroup" :style="bodyStyle">
v-for="row in rows" <template v-if="rows.length">
:key="row.id" <template v-for="row in rows" :key="row.id">
type="button" <div
v-if="row.kind === 'note'"
class="ui-data-table__note"
role="row"
:aria-disabled="row.disabled || undefined"
>
<slot name="note" :row="row">{{ row.label ?? row.note ?? '' }}</slot>
</div>
<component
:is="isSelectable(row) ? 'button' : 'div'"
v-else
:type="isSelectable(row) ? 'button' : undefined"
role="row"
:tabindex="isSelectable(row) ? 0 : undefined"
:aria-disabled="row.disabled || undefined"
:class="[
'ui-data-table__row',
'ui-data-table__data-row',
{
'is-selectable': isSelectable(row),
'is-selected': row.id === selectedId,
'is-disabled': row.disabled,
},
]"
:style="{ gridTemplateColumns: gridTemplate }"
@click="selectRow(row)"
@keydown.enter.prevent="selectRow(row)"
@keydown.space.prevent="selectRow(row)"
>
<span
v-for="column in columns"
:key="column.key"
role="cell"
:class="`is-align-${column.align ?? 'start'}`"
>
<slot :name="`cell-${column.key}`" :row="row" :value="String(row[column.key] ?? '')">
{{ String(row[column.key] ?? '') }}
</slot>
</span>
</component>
<div v-if="$slots['after-row']" class="ui-data-table__after-row" role="row">
<slot name="after-row" :row="row" />
</div>
</template>
</template>
<div v-else class="ui-data-table__empty" role="row">
<slot name="empty">{{ emptyText }}</slot>
</div>
</div>
<div
v-if="$slots.summary"
class="ui-data-table__row ui-data-table__summary"
role="row" role="row"
:class="['ui-data-table__row', { 'is-selected': row.id === selectedId }]"
:style="{ gridTemplateColumns: gridTemplate }" :style="{ gridTemplateColumns: gridTemplate }"
@click="emits('select', row)"
> >
<span v-for="column in columns" :key="column.key" role="cell"> <slot name="summary" :columns="columns" />
<slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]"> </div>
{{ row[column.key] }} <div v-if="$slots.footer" class="ui-data-table__footer">
</slot> <slot name="footer" />
</span> </div>
</button>
</div> </div>
</template> </template>
@@ -81,6 +171,17 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
color: var(--ui-table-foreground); color: var(--ui-table-foreground);
} }
.ui-data-table__body {
min-width: var(--ui-table-scroll-inline-size);
}
.ui-data-table.has-constrained-body .ui-data-table__body {
max-height: var(--ui-table-body-max-block-size);
overflow-y: auto;
scrollbar-color: var(--ui-scrollbar-thumb-border) var(--ui-scrollbar-background);
scrollbar-width: thin;
}
.ui-data-table__row { .ui-data-table__row {
display: grid; display: grid;
width: 100%; width: 100%;
@@ -97,26 +198,31 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
min-width: var(--ui-table-scroll-inline-size); min-width: var(--ui-table-scroll-inline-size);
} }
button.ui-data-table__row { .ui-data-table__data-row.is-selectable {
cursor: pointer; cursor: pointer;
} }
button.ui-data-table__row:focus-visible { .ui-data-table__data-row.is-selectable:focus-visible {
outline: 1px solid var(--ui-ring); outline: 1px solid var(--ui-ring);
outline-offset: -2px; outline-offset: -2px;
} }
button.ui-data-table__row:nth-child(odd) { .ui-data-table__data-row:nth-child(odd) {
background: var(--ui-row-background); background: var(--ui-row-background);
} }
button.ui-data-table__row:hover { .ui-data-table__data-row.is-selectable:hover {
background: var(--ui-table-row-hover); background: var(--ui-table-row-hover);
outline: 1px solid var(--ui-table-row-hover-outline); outline: 1px solid var(--ui-table-row-hover-outline);
outline-offset: -1px; outline-offset: -1px;
} }
button.ui-data-table__row.is-selected { .ui-data-table__data-row.is-selected {
background: var(--ui-table-row-selected); background: var(--ui-table-selected-background);
outline: 1px solid var(--ui-table-row-selected-outline); outline: 1px solid var(--ui-table-selected-outline);
outline-offset: -1px; outline-offset: -1px;
box-shadow: inset 0 0 0 1px var(--ui-table-row-selected-inset); box-shadow: inset 0 0 0 1px var(--ui-table-selected-inset);
}
.ui-data-table__data-row.is-disabled {
color: var(--ui-control-disabled-foreground);
opacity: 0.56;
} }
.ui-data-table__row > * { .ui-data-table__row > * {
@@ -132,6 +238,16 @@ button.ui-data-table__row.is-selected {
white-space: nowrap; white-space: nowrap;
} }
.ui-data-table__row > .is-align-center {
justify-content: center;
text-align: center;
}
.ui-data-table__row > .is-align-end {
justify-content: flex-end;
text-align: right;
}
.ui-data-table__header { .ui-data-table__header {
min-height: var(--ui-table-header-block-size); min-height: var(--ui-table-header-block-size);
height: var(--ui-table-header-block-size); height: var(--ui-table-header-block-size);
@@ -172,4 +288,57 @@ button.ui-data-table__row.is-selected {
.ui-data-table__sort[aria-sort='descending'] i { .ui-data-table__sort[aria-sort='descending'] i {
transform: rotate(180deg); transform: rotate(180deg);
} }
.ui-data-table--selection-gold {
--ui-table-selected-background: var(--ui-table-row-selected);
--ui-table-selected-outline: var(--ui-table-row-selected-outline);
--ui-table-selected-inset: var(--ui-table-row-selected-inset);
}
.ui-data-table--selection-blue {
--ui-table-selected-background: var(--ui-table-row-hover);
--ui-table-selected-outline: var(--ui-table-row-hover-outline);
--ui-table-selected-inset: var(--ui-table-row-hover-outline);
}
.ui-data-table--compact .ui-data-table__data-row {
min-height: var(--ui-table-row-compact-block-size);
font-size: var(--ui-table-compact-font-size);
}
.ui-data-table__note,
.ui-data-table__after-row,
.ui-data-table__empty {
box-sizing: border-box;
display: flex;
min-height: var(--ui-table-note-row-min-block-size);
align-items: center;
padding: var(--ui-table-note-row-padding);
border-bottom: 1px solid var(--ui-table-row-divider);
background: var(--ui-table-note-row-background);
color: var(--ui-table-note-row-foreground);
}
.ui-data-table__empty {
justify-content: center;
min-height: var(--ui-table-empty-min-block-size);
color: var(--ui-muted-foreground);
}
.ui-data-table__summary {
border-top: 1px solid var(--ui-table-summary-border);
border-bottom: 0;
background: var(--ui-table-summary-background);
color: var(--ui-table-summary-foreground);
}
.ui-data-table__footer {
display: flex;
min-height: var(--ui-table-footer-min-block-size);
align-items: center;
justify-content: flex-end;
padding: var(--ui-table-footer-padding);
border-top: 1px solid var(--ui-table-footer-border);
background: var(--ui-table-footer-background);
}
</style> </style>
@@ -0,0 +1,162 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { DialogPortal, DialogRoot } from 'reka-ui'
import { UiButton } from '../button'
import UiDialogContent from './UiDialogContent.vue'
import UiDialogDescription from './UiDialogDescription.vue'
import UiDialogFooter from './UiDialogFooter.vue'
import UiDialogHeader from './UiDialogHeader.vue'
import UiDialogOverlay from './UiDialogOverlay.vue'
import UiDialogTitle from './UiDialogTitle.vue'
export type UiActionDialogResult = 'ok' | 'reject' | 'cancel' | 'none'
const props = withDefaults(
defineProps<{
visible?: boolean
header?: string | boolean
mask?: boolean
modal?: boolean
drag?: 'header' | 'all' | 'none'
okButton?: string | boolean
rejectButton?: string | boolean
cancelButton?: string | boolean
closeOnEsc?: boolean
defaultStatus?: UiActionDialogResult
returnFocusElement?: HTMLElement | null
scale?: number
}>(),
{
visible: false,
header: true,
mask: false,
modal: false,
drag: 'header',
okButton: '确定',
rejectButton: false,
cancelButton: '取消',
closeOnEsc: true,
defaultStatus: 'cancel',
scale: 1,
},
)
const emits = defineEmits<{
close: [status: UiActionDialogResult]
ok: []
reject: []
cancel: []
'update:visible': [visible: boolean]
}>()
const result = ref<UiActionDialogResult>(props.defaultStatus)
// Only a visible mask may make the dialog modal. A modal DialogRoot without an overlay
// disables body pointer events and leaves the page looking inexplicably unresponsive.
const hasBlockingOverlay = computed(() => props.mask)
const contentStyle = computed<CSSProperties>(() => ({
'--ui-dialog-scale': props.scale,
'--ui-dialog-content-inline-size': 'max-content',
'--ui-dialog-content-max-inline-size': 'var(--ui-dialog-viewport-inline-size)',
}))
watch(
() => props.visible,
(visible) => {
if (visible) result.value = props.defaultStatus
},
)
function close(status: UiActionDialogResult = result.value) {
result.value = status
emits('update:visible', false)
emits('close', status)
}
function updateOpen(open: boolean) {
if (open) emits('update:visible', true)
else close()
}
function handleEscape(event: KeyboardEvent) {
if (!props.closeOnEsc) event.preventDefault()
}
function handleInteractOutside(event: Event) {
// With no mask and no outside-dismiss request, keep the window open while allowing
// the underlying page to remain interactive. A visible mask always dismisses on click.
if (!props.mask && !props.modal) event.preventDefault()
}
function choose(status: Exclude<UiActionDialogResult, 'none'>) {
if (status === 'ok') emits('ok')
else if (status === 'reject') emits('reject')
else emits('cancel')
close(status)
}
</script>
<template>
<DialogRoot :open="visible" :modal="hasBlockingOverlay" @update:open="updateOpen">
<DialogPortal>
<UiDialogOverlay v-if="hasBlockingOverlay" />
<UiDialogContent
class="ui-action-dialog"
:draggable="drag !== 'none'"
:return-focus-element="returnFocusElement"
:style="contentStyle"
@escape-key-down="handleEscape"
@interact-outside="handleInteractOutside"
>
<UiDialogHeader v-if="header" :closable="true">
<UiDialogTitle>{{ header === true ? '提示' : header }}</UiDialogTitle>
</UiDialogHeader>
<UiDialogTitle v-else class="sr-only">提示</UiDialogTitle>
<UiDialogDescription class="sr-only">{{
header === true ? '提示' : header || '操作确认'
}}</UiDialogDescription>
<div class="ui-action-dialog__body">
<slot />
</div>
<UiDialogFooter
v-if="$slots.action || okButton || rejectButton || cancelButton"
class="ui-action-dialog__footer"
>
<slot v-if="$slots.action" name="action" />
<template v-else>
<UiButton v-if="okButton" @click="choose('ok')">
{{ okButton === true ? '确定' : okButton }}
</UiButton>
<UiButton v-if="rejectButton" variant="destructive" @click="choose('reject')">
{{ rejectButton === true ? '拒绝' : rejectButton }}
</UiButton>
<UiButton v-if="cancelButton" variant="outline" @click="choose('cancel')">
{{ cancelButton === true ? '取消' : cancelButton }}
</UiButton>
</template>
</UiDialogFooter>
</UiDialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style scoped>
.ui-action-dialog__body {
box-sizing: border-box;
display: flex;
min-width: min(100%, var(--ui-dialog-body-min-inline-size));
min-height: var(--ui-dialog-body-min-block-size);
max-width: var(--ui-dialog-viewport-inline-size);
max-height: var(--ui-dialog-body-max-block-size);
align-items: center;
justify-content: center;
padding: var(--ui-dialog-body-padding);
overflow: auto;
background: var(--ui-dialog-background);
}
.ui-action-dialog__footer {
justify-content: center;
}
</style>
@@ -13,18 +13,25 @@ const props = withDefaults(
DialogContentProps & { DialogContentProps & {
class?: HTMLAttributes['class'] class?: HTMLAttributes['class']
draggable?: boolean draggable?: boolean
returnFocusElement?: HTMLElement | null
} }
>(), >(),
{ draggable: true }, { draggable: true },
) )
const emits = defineEmits<DialogContentEmits>() const emits = defineEmits<DialogContentEmits>()
const dragOffset = ref({ x: 0, y: 0 }) const dragOffset = ref({ x: 0, y: 0 })
const capturedReturnFocusElement = shallowRef<HTMLElement | null>(null)
let activePointerId: number | undefined let activePointerId: number | undefined
let dragOrigin = { clientX: 0, clientY: 0, x: 0, y: 0 } let dragOrigin = { clientX: 0, clientY: 0, x: 0, y: 0 }
let dragBounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 } let dragBounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 }
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _class, draggable: _draggable, ...delegated } = props const {
class: _class,
draggable: _draggable,
returnFocusElement: _returnFocusElement,
...delegated
} = props
return delegated return delegated
}) })
@@ -51,6 +58,36 @@ function resetPosition() {
dragOffset.value = { x: 0, y: 0 } dragOffset.value = { x: 0, y: 0 }
} }
function handleOpenAutoFocus() {
resetPosition()
if (props.returnFocusElement) return
const activeElement = document.activeElement
capturedReturnFocusElement.value =
activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null
}
function handleCloseAutoFocus(event: Event) {
if (event.defaultPrevented) {
capturedReturnFocusElement.value = null
return
}
const explicitTarget = props.returnFocusElement
const capturedTarget = capturedReturnFocusElement.value
const target = explicitTarget?.isConnected
? explicitTarget
: capturedTarget?.isConnected
? capturedTarget
: null
capturedReturnFocusElement.value = null
if (!target) return
event.preventDefault()
nextTick(() => target.focus({ preventScroll: true }))
}
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
if (activePointerId !== event.pointerId) return if (activePointerId !== event.pointerId) return
const deltaX = event.clientX - dragOrigin.clientX const deltaX = event.clientX - dragOrigin.clientX
@@ -106,7 +143,8 @@ onBeforeUnmount(() => {
:style="dragStyle" :style="dragStyle"
:data-draggable="draggable ? '' : undefined" :data-draggable="draggable ? '' : undefined"
@pointerdown="handlePointerDown" @pointerdown="handlePointerDown"
@open-auto-focus="resetPosition" @open-auto-focus="handleOpenAutoFocus"
@close-auto-focus="handleCloseAutoFocus"
:class=" :class="
cn( cn(
'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]', 'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
@@ -120,8 +158,12 @@ onBeforeUnmount(() => {
<style scoped> <style scoped>
.ui-dialog-content { .ui-dialog-content {
width: min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size)); width: var(
max-height: calc(100dvh - 16px); --ui-dialog-content-inline-size,
min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size))
);
max-width: var(--ui-dialog-content-max-inline-size, none);
max-height: var(--ui-dialog-content-max-block-size, calc(100dvh - 16px));
overflow: hidden; overflow: hidden;
border: 1px solid var(--ui-dialog-border); border: 1px solid var(--ui-dialog-border);
border-radius: 0; border-radius: 0;
@@ -131,9 +173,10 @@ onBeforeUnmount(() => {
inset 0 0 0 1px var(--ui-dialog-inset); inset 0 0 0 1px var(--ui-dialog-inset);
color: var(--ui-surface-foreground); color: var(--ui-surface-foreground);
transform: translate( transform: translate(
calc(-50% + var(--ui-dialog-drag-x, 0px)), calc(-50% + var(--ui-dialog-drag-x, 0px)),
calc(-50% + var(--ui-dialog-drag-y, 0px)) calc(-50% + var(--ui-dialog-drag-y, 0px))
); )
scale(var(--ui-dialog-scale, 1));
will-change: transform; will-change: transform;
} }
@@ -1,4 +1,6 @@
export { DialogClose, DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui' export { DialogClose, DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
export { default as UiActionDialog } from './UiActionDialog.vue'
export type { UiActionDialogResult } from './UiActionDialog.vue'
export { default as UiDialogContent } from './UiDialogContent.vue' export { default as UiDialogContent } from './UiDialogContent.vue'
export { default as UiDialogDescription } from './UiDialogDescription.vue' export { default as UiDialogDescription } from './UiDialogDescription.vue'
export { default as UiDialogFooter } from './UiDialogFooter.vue' export { default as UiDialogFooter } from './UiDialogFooter.vue'
@@ -0,0 +1,309 @@
<script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from 'vue'
import { computed } from 'vue'
import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from 'reka-ui'
import { UiScrollArea } from '../scroll-area'
import { cn } from '@/lib/utils'
export interface UiFavoriteDrawerItem {
value: string
label: string
icon?: string
disabled?: boolean
}
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
items: UiFavoriteDrawerItem[]
title?: string
expandedLabel?: string
collapsedLabel?: string
emptyText?: string
disabled?: boolean
maxVisibleItems?: number
removeLabel?: (item: UiFavoriteDrawerItem) => string
}>(),
{
title: '收藏',
expandedLabel: '已展开',
collapsedLabel: '已收起',
emptyText: '暂无收藏',
maxVisibleItems: 6,
removeLabel: (item: UiFavoriteDrawerItem) => `移除 ${item.label}`,
},
)
const emit = defineEmits<{
select: [item: UiFavoriteDrawerItem]
remove: [item: UiFavoriteDrawerItem]
}>()
const open = defineModel<boolean>('open', { default: true })
const scrollAreaStyle = computed(
() =>
({
'--ui-favorite-drawer-visible-row-count': Math.max(
1,
Math.min(props.items.length, props.maxVisibleItems),
),
}) as CSSProperties,
)
function selectItem(item: UiFavoriteDrawerItem) {
if (!props.disabled && !item.disabled) emit('select', item)
}
function removeItem(item: UiFavoriteDrawerItem) {
if (!props.disabled && !item.disabled) emit('remove', item)
}
</script>
<template>
<CollapsibleRoot
v-model:open="open"
:disabled="disabled"
:class="cn('ui-favorite-drawer', disabled && 'is-disabled', props.class)"
>
<CollapsibleTrigger class="ui-favorite-drawer__trigger">
<span class="ui-favorite-drawer__mark" aria-hidden="true"><slot name="icon">★</slot></span>
<span class="ui-favorite-drawer__title">{{ title }}</span>
<span class="ui-favorite-drawer__state">
{{ open ? expandedLabel : collapsedLabel }}
</span>
<span class="ui-favorite-drawer__arrow" aria-hidden="true">{{ open ? '▲' : '▼' }}</span>
</CollapsibleTrigger>
<CollapsibleContent class="ui-favorite-drawer__content">
<UiScrollArea
v-if="items.length"
class="ui-favorite-drawer__scroll-area"
:style="scrollAreaStyle"
>
<div
v-for="item in items"
:key="item.value"
class="ui-favorite-drawer__row"
:class="{ 'is-disabled': item.disabled }"
>
<button
type="button"
class="ui-favorite-drawer__item"
:disabled="disabled || item.disabled"
@click="selectItem(item)"
>
<slot name="item" :item="item">
<img v-if="item.icon" :src="item.icon" alt="" aria-hidden="true" />
<span>{{ item.label }}</span>
</slot>
</button>
<button
type="button"
class="ui-favorite-drawer__remove"
:aria-label="removeLabel(item)"
:disabled="disabled || item.disabled"
@click="removeItem(item)"
>
×
</button>
</div>
</UiScrollArea>
<div v-else class="ui-favorite-drawer__empty">{{ emptyText }}</div>
</CollapsibleContent>
</CollapsibleRoot>
</template>
<style scoped lang="scss">
.ui-favorite-drawer {
box-sizing: border-box;
width: 100%;
overflow: hidden;
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-disclosure-border);
background: var(--ui-disclosure-background);
color: var(--ui-disclosure-row-foreground);
&[data-state='closed'] .ui-favorite-drawer__trigger {
border-bottom-color: transparent;
}
&.is-disabled {
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
}
}
.ui-favorite-drawer__trigger {
box-sizing: border-box;
display: flex;
width: 100%;
min-height: var(--ui-disclosure-header-min-block-size);
align-items: center;
gap: var(--ui-control-gap);
padding: 0 var(--ui-field-padding-inline);
border: 0;
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-disclosure-divider);
outline: 0;
background: var(--ui-disclosure-header-background);
color: inherit;
font: inherit;
line-height: var(--ui-control-line-height);
text-align: left;
cursor: pointer;
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
background: var(
--ui-favorite-drawer-header-hover-background,
var(--ui-control-background-hover)
);
}
&:focus-visible {
outline: var(
--ui-focus-outline-size,
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
)
solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
}
&:active:not(:disabled) > * {
transform: translateY(
var(
--ui-pressed-offset,
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
)
);
}
&:disabled {
cursor: not-allowed;
}
}
.ui-favorite-drawer__mark {
color: var(--ui-favorite-drawer-mark, var(--ui-warning));
}
.ui-favorite-drawer__title {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ui-favorite-drawer__state {
color: var(--ui-disclosure-state);
font-size: var(--ui-disclosure-state-font-size);
}
.ui-favorite-drawer__arrow {
width: var(--ui-navigation-indicator-inline-size);
color: var(--ui-favorite-drawer-arrow, var(--ui-settings-section-foreground));
font-size: var(--ui-badge-font-size);
text-align: center;
}
.ui-favorite-drawer__scroll-area {
--ui-scroll-area-height: var(
--ui-favorite-drawer-content-block-size,
calc(var(--ui-disclosure-row-block-size) * var(--ui-favorite-drawer-visible-row-count))
);
}
.ui-favorite-drawer__row {
display: grid;
height: var(--ui-disclosure-row-block-size);
grid-template-columns: minmax(0, 1fr) var(--ui-control-height-compact);
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-disclosure-row-divider);
&:last-child {
border-bottom: 0;
}
&:focus-within {
background: var(--ui-disclosure-row-focus);
}
&.is-disabled {
color: var(--ui-control-disabled-foreground);
}
}
.ui-favorite-drawer__item,
.ui-favorite-drawer__remove {
display: flex;
min-width: 0;
align-items: center;
border: 0;
outline: 0;
background: transparent;
color: inherit;
font: inherit;
line-height: var(--ui-control-line-height);
cursor: pointer;
&:focus-visible {
outline: var(
--ui-focus-outline-size,
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
)
solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
}
&:disabled {
color: var(--ui-control-disabled-foreground);
cursor: not-allowed;
}
}
.ui-favorite-drawer__item {
gap: var(--ui-disclosure-row-gap);
padding: 0 var(--ui-field-padding-inline);
text-align: left;
&:hover:not(:disabled) {
background: var(--ui-disclosure-row-focus);
}
img {
width: var(--ui-disclosure-icon-size);
height: var(--ui-disclosure-icon-size);
flex: 0 0 auto;
object-fit: contain;
image-rendering: pixelated;
}
span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.ui-favorite-drawer__remove {
justify-content: center;
border-left: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-disclosure-row-divider);
&:hover:not(:disabled) {
background: var(--ui-control-destructive-background);
color: var(--ui-control-destructive-foreground);
}
}
.ui-favorite-drawer__empty {
display: flex;
min-height: var(--ui-disclosure-row-block-size);
align-items: center;
justify-content: center;
padding: var(--ui-disclosure-content-padding-block) var(--ui-field-padding-inline);
color: var(--ui-disclosure-state);
}
</style>
@@ -0,0 +1,2 @@
export { default as UiFavoriteDrawer } from './UiFavoriteDrawer.vue'
export type { UiFavoriteDrawerItem } from './UiFavoriteDrawer.vue'
@@ -0,0 +1,55 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type AsTag } from 'reka-ui'
import { cn } from '@/lib/utils'
type LevelBadgeTone = 'default' | 'info' | 'success'
const props = withDefaults(
defineProps<{
as?: AsTag
class?: HTMLAttributes['class']
level?: number | string
suffix?: string
tone?: LevelBadgeTone
}>(),
{
as: 'span',
level: 1,
suffix: '级',
tone: 'default',
},
)
</script>
<template>
<Primitive :as="as" :class="cn('ui-level-badge', `ui-level-badge--${tone}`, props.class)">
<slot>{{ level }}{{ suffix }}</slot>
</Primitive>
</template>
<style scoped>
.ui-level-badge {
display: inline-flex;
min-width: var(--ui-level-badge-min-inline-size);
min-height: var(--ui-level-badge-min-block-size);
align-items: center;
justify-content: center;
padding-inline: var(--ui-level-badge-padding-inline);
border: 1px solid var(--ui-level-badge-border);
background: var(--ui-level-badge-background);
color: var(--ui-level-badge-foreground);
font-size: var(--ui-level-badge-font-size);
line-height: var(--ui-control-line-height);
text-align: center;
vertical-align: middle;
}
.ui-level-badge--info {
color: var(--ui-info);
}
.ui-level-badge--success {
color: var(--ui-success);
}
</style>
@@ -0,0 +1,87 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type AsTag } from 'reka-ui'
import noticeBadgeAsset from '../../../../../design/src-web/rarity/img/sprite_interface.NPK/windowcommon.chn.img/301.PNG'
import { cn } from '@/lib/utils'
type NoticeBadgePlacement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
const props = withDefaults(
defineProps<{
as?: AsTag
class?: HTMLAttributes['class']
label?: string
placement?: NoticeBadgePlacement
visible?: boolean
}>(),
{
as: 'span',
label: '有新通知',
placement: 'top-end',
visible: true,
},
)
</script>
<template>
<Primitive :as="as" :class="cn('ui-notice-badge', props.class)">
<slot />
<span
v-if="visible"
class="ui-notice-badge__marker"
:class="`ui-notice-badge__marker--${placement}`"
role="status"
:aria-label="label"
>
<img :src="noticeBadgeAsset" alt="" aria-hidden="true" />
</span>
</Primitive>
</template>
<style scoped>
.ui-notice-badge {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.ui-notice-badge__marker {
position: absolute;
z-index: 1;
display: flex;
width: var(--ui-notice-badge-size);
height: var(--ui-notice-badge-size);
align-items: center;
justify-content: center;
pointer-events: none;
}
.ui-notice-badge__marker img {
display: block;
width: 100%;
height: 100%;
image-rendering: pixelated;
}
.ui-notice-badge__marker--top-start {
top: var(--ui-notice-badge-offset);
left: var(--ui-notice-badge-offset);
}
.ui-notice-badge__marker--top-end {
top: var(--ui-notice-badge-offset);
right: var(--ui-notice-badge-offset);
}
.ui-notice-badge__marker--bottom-start {
bottom: var(--ui-notice-badge-offset);
left: var(--ui-notice-badge-offset);
}
.ui-notice-badge__marker--bottom-end {
right: var(--ui-notice-badge-offset);
bottom: var(--ui-notice-badge-offset);
}
</style>
@@ -0,0 +1,2 @@
export { default as UiLevelBadge } from './UiLevelBadge.vue'
export { default as UiNoticeBadge } from './UiNoticeBadge.vue'
@@ -4,23 +4,40 @@ import { ProgressIndicator, ProgressRoot, type ProgressRootProps } from 'reka-ui
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
const props = withDefaults( const props = withDefaults(
defineProps<ProgressRootProps & { class?: HTMLAttributes['class']; marker?: number }>(), defineProps<
ProgressRootProps & {
class?: HTMLAttributes['class']
/** Backward-compatible single milestone. Prefer `markers` for multiple thresholds. */
marker?: number
/** Milestones expressed as percentages from 0 to 100. */
markers?: number[]
}
>(),
{ max: 100 }, { max: 100 },
) )
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _class, marker: _marker, ...delegated } = props const { class: _class, marker: _marker, markers: _markers, ...delegated } = props
return delegated return delegated
}) })
const percentage = computed(() => const percentage = computed(() =>
Math.min(100, Math.max(0, ((props.modelValue ?? 0) / (props.max ?? 100)) * 100)), Math.min(100, Math.max(0, ((props.modelValue ?? 0) / (props.max ?? 100)) * 100)),
) )
const visibleMarkers = computed(() => {
const values = props.markers ?? (props.marker == null ? [] : [props.marker])
return [...new Set(values.map((value) => Math.min(100, Math.max(0, value))))]
})
</script> </script>
<template> <template>
<ProgressRoot v-bind="delegatedProps" :class="cn('ui-progress', props.class)"> <ProgressRoot v-bind="delegatedProps" :class="cn('ui-progress', props.class)">
<ProgressIndicator class="ui-progress__indicator" :style="{ width: `${percentage}%` }" /> <ProgressIndicator class="ui-progress__indicator" :style="{ width: `${percentage}%` }" />
<span v-if="marker != null" class="ui-progress__marker" :style="{ left: `${marker}%` }" /> <span
v-for="milestone in visibleMarkers"
:key="milestone"
class="ui-progress__marker"
:style="{ left: `${milestone}%` }"
/>
</ProgressRoot> </ProgressRoot>
</template> </template>
@@ -0,0 +1,413 @@
<script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from 'vue'
import { computed, ref, watch } from 'vue'
import { TreeItem, TreeRoot } from 'reka-ui'
import expandedIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/565.PNG'
import collapsedIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/561.PNG'
import clearIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/582.PNG'
import searchIconAsset from '../../../../../design/src-web/autocomplete/img/sprite_interface.NPK/windowcommon.chn.img/652.PNG'
import { UiButton } from '../button'
import { cn } from '@/lib/utils'
export interface UiSearchTreeNode {
value: string
label: string
icon?: string
expandedIcon?: string
collapsedIcon?: string
disabled?: boolean
children?: UiSearchTreeNode[]
}
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
items: UiSearchTreeNode[]
placeholder?: string
searchLabel?: string
refreshLabel?: string
clearLabel?: string
treeLabel?: string
searchIcon?: string
clearIcon?: string
disabled?: boolean
filter?: boolean
defaultExpandAll?: boolean
selectBranches?: boolean
}>(),
{
placeholder: '请输入关键词',
searchLabel: '搜索',
refreshLabel: '刷新',
clearLabel: '清除搜索',
treeLabel: '搜索结果',
searchIcon: searchIconAsset,
clearIcon: clearIconAsset,
filter: false,
defaultExpandAll: true,
selectBranches: false,
},
)
const emit = defineEmits<{
search: [query: string]
refresh: [query: string]
select: [item: UiSearchTreeNode]
}>()
const model = defineModel<string>()
const query = defineModel<string>('query', { default: '' })
const expanded = defineModel<string[]>('expanded', { default: () => [] })
const committedQuery = ref('')
const expansionInitialized = ref(false)
function findItem(
items: UiSearchTreeNode[],
value: string | undefined,
): UiSearchTreeNode | undefined {
for (const item of items) {
if (item.value === value) return item
const nested = item.children && findItem(item.children, value)
if (nested) return nested
}
}
function collectBranchValues(items: UiSearchTreeNode[]): string[] {
return items.flatMap((item) =>
item.children?.length ? [item.value, ...collectBranchValues(item.children)] : [],
)
}
function filterItems(items: UiSearchTreeNode[], normalizedQuery: string): UiSearchTreeNode[] {
return items.flatMap((item) => {
const children = item.children ? filterItems(item.children, normalizedQuery) : []
const matches = item.label.toLocaleLowerCase().includes(normalizedQuery)
if (!matches && !children.length) return []
return [{ ...item, children: matches ? item.children : children }]
})
}
const visibleItems = computed(() => {
const normalizedQuery = committedQuery.value.trim().toLocaleLowerCase()
if (!props.filter || !normalizedQuery) return props.items
return filterItems(props.items, normalizedQuery)
})
const selectedItem = computed(() => findItem(visibleItems.value, model.value))
function performSearch() {
if (props.disabled) return
committedQuery.value = query.value.trim()
if (props.filter && committedQuery.value) {
expanded.value = [...new Set([...expanded.value, ...collectBranchValues(visibleItems.value)])]
}
emit('search', query.value)
}
function clearSearch() {
if (props.disabled) return
query.value = ''
committedQuery.value = ''
emit('search', '')
}
function refresh() {
if (!props.disabled) emit('refresh', query.value)
}
function handleSelect(item: UiSearchTreeNode | undefined) {
if (!item || props.disabled || item.disabled) return
if (item.children?.length && !props.selectBranches) return
model.value = item.value
emit('select', item)
}
function preventParentSelection(event: Event, item: UiSearchTreeNode) {
if (item.children?.length && !props.selectBranches) event.preventDefault()
}
function nodeIcon(item: UiSearchTreeNode, isExpanded: boolean) {
if (item.icon) return item.icon
if (!item.children?.length) return undefined
return isExpanded
? (item.expandedIcon ?? expandedIconAsset)
: (item.collapsedIcon ?? collapsedIconAsset)
}
function depthStyle(level: number): CSSProperties {
return { '--ui-search-tree-depth': Math.max(0, level - 1) } as CSSProperties
}
watch(
() => props.items,
(items) => {
if (expansionInitialized.value || !items.length) return
if (props.defaultExpandAll && !expanded.value.length)
expanded.value = collectBranchValues(items)
expansionInitialized.value = true
},
{ immediate: true },
)
</script>
<template>
<section :class="cn('ui-search-tree', disabled && 'is-disabled', props.class)">
<div class="ui-search-tree__toolbar">
<img class="ui-search-tree__search-icon" :src="searchIcon" alt="" aria-hidden="true" />
<div class="ui-search-tree__field">
<input
v-model="query"
type="search"
autocomplete="off"
:placeholder="placeholder"
:disabled="disabled"
:aria-label="placeholder"
@keydown.enter.prevent="performSearch"
/>
<button
v-if="query"
type="button"
class="ui-search-tree__clear"
:aria-label="clearLabel"
:disabled="disabled"
@click="clearSearch"
>
<img :src="clearIcon" alt="" aria-hidden="true" />
</button>
</div>
<UiButton class="ui-search-tree__action" :disabled="disabled" @click="performSearch">
{{ searchLabel }}
</UiButton>
<UiButton class="ui-search-tree__action" :disabled="disabled" @click="refresh">
{{ refreshLabel }}
</UiButton>
</div>
<TreeRoot
v-slot="{ flattenItems }"
class="ui-search-tree__tree"
:model-value="selectedItem"
v-model:expanded="expanded"
:items="visibleItems"
:get-key="(item) => item.value"
:get-children="(item) => item.children"
:aria-label="treeLabel"
:aria-disabled="disabled"
@update:model-value="handleSelect"
>
<TreeItem
v-for="item in flattenItems"
:key="item._id"
v-slot="{ isExpanded }"
v-bind="item.bind"
class="ui-search-tree__row"
:class="{
'is-parent': item.hasChildren,
'is-root': item.level === 1,
}"
:disabled="disabled || item.value.disabled"
:style="depthStyle(item.level)"
@select="preventParentSelection($event, item.value)"
>
<slot name="node" :item="item.value" :expanded="isExpanded" :level="item.level">
<img
v-if="nodeIcon(item.value, isExpanded)"
:src="nodeIcon(item.value, isExpanded)"
alt=""
aria-hidden="true"
/>
<span>{{ item.value.label }}</span>
</slot>
</TreeItem>
</TreeRoot>
</section>
</template>
<style scoped lang="scss">
.ui-search-tree {
min-width: 0;
}
.ui-search-tree__toolbar {
display: flex;
min-width: 0;
align-items: center;
gap: var(--ui-control-gap);
}
.ui-search-tree__search-icon {
width: var(--ui-search-tree-icon-size, var(--ui-badge-height));
height: var(--ui-search-tree-icon-size, var(--ui-badge-height));
flex: 0 0 auto;
object-fit: contain;
image-rendering: pixelated;
}
.ui-search-tree__field {
box-sizing: border-box;
display: flex;
min-width: 0;
height: var(--ui-control-height-default);
flex: 1;
align-items: center;
padding-inline-start: var(--ui-field-padding-inline);
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-input);
background: var(--ui-field-background);
box-shadow: inset 0 0 0
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
var(--ui-field-inset);
&:focus-within {
border-color: var(--ui-ring);
box-shadow:
inset 0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
var(--ui-field-focus-inset),
0 0 0 var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
var(--ui-field-focus-edge);
}
input {
min-width: 0;
height: 100%;
flex: 1;
padding: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--ui-foreground);
font: inherit;
font-size: var(--ui-control-font-size-default);
line-height: var(--ui-control-line-height);
&::placeholder {
color: var(--ui-muted-foreground);
}
&::-webkit-search-cancel-button {
display: none;
}
}
}
.ui-search-tree__clear {
display: inline-flex;
width: var(--ui-control-height-compact);
height: 100%;
flex: 0 0 auto;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
outline: 0;
background: transparent;
cursor: pointer;
&:focus-visible {
outline: var(
--ui-focus-outline-size,
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
)
solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
}
img {
width: var(--ui-search-tree-clear-size, var(--ui-dialog-close-glyph-block-size));
height: var(--ui-search-tree-clear-size, var(--ui-dialog-close-glyph-block-size));
image-rendering: pixelated;
}
}
.ui-search-tree__action {
min-width: var(
--ui-search-tree-action-min-inline-size,
calc(var(--ui-control-height-default) * 2)
);
flex: 0 0 auto;
}
.ui-search-tree__tree {
box-sizing: border-box;
height: var(--ui-search-tree-block-size, var(--ui-autocomplete-max-block-size));
margin-block-start: var(--ui-autocomplete-trigger-gap);
margin-inline-start: var(--ui-search-tree-indent, var(--ui-navigation-icon-size));
padding-block: var(--ui-autocomplete-trigger-gap);
overflow-y: auto;
border: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-disclosure-border);
outline: 0;
background: var(--ui-tree-background);
list-style: none;
}
.ui-search-tree__row {
box-sizing: border-box;
display: flex;
min-width: 0;
height: var(--ui-search-tree-row-block-size, var(--ui-option-height));
align-items: center;
gap: var(--ui-disclosure-row-gap);
padding: 0 var(--ui-field-padding-inline);
padding-inline-start: calc(
var(--ui-field-padding-inline) + var(--ui-search-tree-depth) *
var(--ui-search-tree-depth-step, var(--ui-tree-depth-step))
);
border-bottom: var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4)) solid
var(--ui-disclosure-row-divider);
outline: 0;
color: var(--ui-disclosure-row-foreground);
line-height: var(--ui-control-line-height);
cursor: pointer;
&.is-parent:not(.is-root) {
color: var(--ui-tree-branch-foreground);
}
&:hover:not([data-disabled]),
&:focus-visible:not([data-disabled]) {
background: var(--ui-disclosure-row-focus);
color: var(--ui-option-highlight-foreground);
}
&:focus-visible {
outline: var(
--ui-focus-outline-size,
var(--ui-component-stroke-size, calc(var(--ui-autocomplete-trigger-gap) / 4))
)
solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset, calc(var(--ui-autocomplete-trigger-gap) / -2));
}
&[data-selected] {
background: var(--ui-disclosure-row-selected);
box-shadow: inset var(--ui-autocomplete-trigger-gap) 0 0 var(--ui-selected);
color: var(--ui-disclosure-row-selected-foreground);
}
&[data-disabled] {
color: var(--ui-control-disabled-foreground);
cursor: not-allowed;
}
img {
width: var(--ui-search-tree-node-icon-size, var(--ui-disclosure-icon-size));
height: var(--ui-search-tree-node-icon-size, var(--ui-disclosure-icon-size));
flex: 0 0 auto;
object-fit: contain;
image-rendering: pixelated;
}
span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.ui-search-tree.is-disabled {
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
}
</style>
@@ -0,0 +1,2 @@
export { default as UiSearchTree } from './UiSearchTree.vue'
export type { UiSearchTreeNode } from './UiSearchTree.vue'
@@ -0,0 +1,129 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import {
DropdownMenuContent,
type DropdownMenuContentEmits,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRoot,
DropdownMenuTrigger,
} from 'reka-ui'
import { cn } from '@/lib/utils'
type SettingsDropdownAlign = 'start' | 'center' | 'end'
type SettingsDropdownSide = 'top' | 'right' | 'bottom' | 'left'
const props = withDefaults(
defineProps<{
align?: SettingsDropdownAlign
class?: HTMLAttributes['class']
collisionPadding?: number
disabled?: boolean
modal?: boolean
side?: SettingsDropdownSide
sideOffset?: number
title?: string
}>(),
{
align: 'center',
collisionPadding: 8,
disabled: false,
modal: true,
side: 'bottom',
sideOffset: 4,
title: '设置菜单',
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const open = defineModel<boolean>('open', { default: false })
</script>
<template>
<DropdownMenuRoot v-model:open="open" :modal="modal">
<DropdownMenuTrigger v-if="$slots.trigger" as-child :disabled="disabled">
<slot name="trigger" />
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
:align="align"
:side="side"
:side-offset="sideOffset"
:collision-padding="collisionPadding"
:class="cn('ui-settings-dropdown', props.class)"
@close-auto-focus="emits('closeAutoFocus', $event)"
@escape-key-down="emits('escapeKeyDown', $event)"
@focus-outside="emits('focusOutside', $event)"
@interact-outside="emits('interactOutside', $event)"
@pointer-down-outside="emits('pointerDownOutside', $event)"
>
<DropdownMenuLabel class="ui-settings-dropdown__title">
<slot name="title">{{ title }}</slot>
</DropdownMenuLabel>
<div class="ui-settings-dropdown__items">
<slot />
</div>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</template>
<style scoped>
:global([data-reka-popper-content-wrapper]:has(> .ui-settings-dropdown)) {
z-index: var(--ui-settings-dropdown-z-index) !important;
}
.ui-settings-dropdown {
position: relative;
z-index: var(--ui-settings-dropdown-z-index);
width: min(
var(--ui-settings-dropdown-viewport-inline-size),
var(--ui-settings-dropdown-inline-size)
);
padding: var(--ui-settings-dropdown-padding);
border: 1px solid var(--ui-settings-dropdown-border);
border-radius: 0;
outline: none;
background: var(--ui-settings-dropdown-background);
box-shadow: var(--ui-settings-dropdown-shadow);
color: var(--ui-settings-dropdown-item-foreground);
transform-origin: var(--reka-dropdown-menu-content-transform-origin);
}
.ui-settings-dropdown[data-state='open'] {
animation: ui-settings-dropdown-in 100ms ease-out;
}
.ui-settings-dropdown__title {
display: flex;
min-height: var(--ui-settings-dropdown-title-min-block-size);
align-items: center;
justify-content: center;
margin-bottom: var(--ui-settings-dropdown-title-gap);
padding-inline: var(--ui-settings-dropdown-title-padding-inline);
border: 1px solid var(--ui-settings-dropdown-title-border);
background: var(--ui-settings-dropdown-title-background);
color: var(--ui-settings-dropdown-title-foreground);
font-size: var(--ui-control-font-size-default);
font-weight: 400;
line-height: var(--ui-control-line-height);
text-align: center;
}
.ui-settings-dropdown__items {
display: grid;
}
@keyframes ui-settings-dropdown-in {
from {
opacity: 0;
transform: translateY(var(--ui-settings-dropdown-enter-offset));
}
}
@media (prefers-reduced-motion: reduce) {
.ui-settings-dropdown[data-state='open'] {
animation: none;
}
}
</style>
@@ -0,0 +1,127 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import {
DropdownMenuItem,
type DropdownMenuItemEmits,
type DropdownMenuItemProps,
useForwardPropsEmits,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
DropdownMenuItemProps & {
class?: HTMLAttributes['class']
danger?: boolean
iconAlt?: string
iconSrc?: string
}
>()
const emits = defineEmits<DropdownMenuItemEmits>()
const delegatedProps = computed(() => {
const {
class: _class,
danger: _danger,
iconAlt: _iconAlt,
iconSrc: _iconSrc,
...delegated
} = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuItem
v-bind="forwarded"
:class="
cn(
'ui-settings-dropdown-item',
props.danger && 'ui-settings-dropdown-item--danger',
props.class,
)
"
>
<span class="ui-settings-dropdown-item__icon" aria-hidden="true">
<slot name="icon">
<img v-if="iconSrc" :src="iconSrc" :alt="iconAlt" />
</slot>
</span>
<span class="ui-settings-dropdown-item__label"><slot /></span>
<span v-if="$slots.trailing" class="ui-settings-dropdown-item__trailing">
<slot name="trailing" />
</span>
</DropdownMenuItem>
</template>
<style scoped>
.ui-settings-dropdown-item {
display: grid;
grid-template-columns:
var(--ui-settings-dropdown-icon-column-size) minmax(0, 1fr)
auto;
min-height: var(--ui-settings-dropdown-item-min-block-size);
align-items: center;
padding: 0 var(--ui-settings-dropdown-item-padding-inline) 0 0;
border: 1px solid var(--ui-settings-dropdown-item-border);
outline: none;
background: var(--ui-settings-dropdown-item-background);
color: var(--ui-settings-dropdown-item-foreground);
font-size: var(--ui-control-font-size-default);
line-height: var(--ui-control-line-height);
cursor: pointer;
user-select: none;
}
.ui-settings-dropdown-item + .ui-settings-dropdown-item {
border-top: 0;
}
.ui-settings-dropdown-item[data-highlighted] {
position: relative;
z-index: 1;
outline: var(--ui-settings-dropdown-focus-outline-size) solid
var(--ui-settings-dropdown-focus-border);
outline-offset: var(--ui-settings-dropdown-focus-outline-offset);
background: var(--ui-settings-dropdown-focus-background);
color: var(--ui-settings-dropdown-focus-foreground);
}
.ui-settings-dropdown-item[data-disabled] {
opacity: var(--ui-settings-dropdown-disabled-opacity);
pointer-events: none;
}
.ui-settings-dropdown-item--danger {
color: var(--ui-menu-danger);
}
.ui-settings-dropdown-item__icon {
display: flex;
min-width: 0;
align-items: center;
justify-content: center;
}
.ui-settings-dropdown-item__icon :deep(img) {
display: block;
max-width: var(--ui-settings-dropdown-icon-max-size);
max-height: var(--ui-settings-dropdown-icon-max-size);
object-fit: contain;
image-rendering: pixelated;
}
.ui-settings-dropdown-item__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ui-settings-dropdown-item__trailing {
display: inline-flex;
align-items: center;
justify-content: flex-end;
margin-left: var(--ui-control-gap);
}
</style>
@@ -0,0 +1,2 @@
export { default as UiSettingsDropdown } from './UiSettingsDropdown.vue'
export { default as UiSettingsDropdownItem } from './UiSettingsDropdownItem.vue'
@@ -25,10 +25,15 @@ const props = withDefaults(
placeholder?: string placeholder?: string
label?: string label?: string
disabled?: boolean disabled?: boolean
selectParents?: boolean
}>(), }>(),
{ placeholder: '请选择', label: '树形选择' }, { placeholder: '请选择', label: '树形选择', selectParents: false },
) )
const emits = defineEmits<{
select: [item: UiTreeSelectItem]
}>()
const model = defineModel<string>() const model = defineModel<string>()
const open = ref(false) const open = ref(false)
const expanded = ref<string[]>([]) const expanded = ref<string[]>([])
@@ -80,13 +85,14 @@ watch(
) )
function handleSelect(item: UiTreeSelectItem | undefined) { function handleSelect(item: UiTreeSelectItem | undefined) {
if (!item || item.disabled || item.children?.length) return if (!item || item.disabled || (item.children?.length && !props.selectParents)) return
model.value = item.value model.value = item.value
open.value = false emits('select', item)
if (!item.children?.length) open.value = false
} }
function preventParentSelection(event: Event, item: UiTreeSelectItem) { function preventParentSelection(event: Event, item: UiTreeSelectItem) {
if (item.children?.length) event.preventDefault() if (item.children?.length && !props.selectParents) event.preventDefault()
} }
function depthStyle(level: number): CSSProperties { function depthStyle(level: number): CSSProperties {
@@ -0,0 +1,131 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
import {
UiDialogContent,
UiDialogDescription,
UiDialogFooter,
UiDialogHeader,
UiDialogOverlay,
UiDialogTitle,
} from '../dialog'
import { cn } from '@/lib/utils'
type UtilitySheetSize = 'compact' | 'default' | 'wide'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
closeLabel?: string
closable?: boolean
draggable?: boolean
focused?: boolean
modal?: boolean
overlay?: boolean
size?: UtilitySheetSize
title?: string
}>(),
{
closeLabel: '关闭工具窗口',
closable: true,
draggable: true,
focused: true,
modal: true,
overlay: true,
size: 'default',
title: '工具窗口',
},
)
const open = defineModel<boolean>('open', { default: false })
const returnFocusElement = shallowRef<HTMLElement | null>(null)
const hasBlockingOverlay = computed(() => props.modal && props.overlay)
watch(
open,
(value) => {
if (!value) return
const activeElement = document.activeElement
returnFocusElement.value =
activeElement instanceof HTMLElement && activeElement !== document.body ? activeElement : null
},
{ flush: 'sync' },
)
</script>
<template>
<DialogRoot v-model:open="open" :modal="hasBlockingOverlay">
<DialogTrigger v-if="$slots.trigger" as-child>
<slot name="trigger" />
</DialogTrigger>
<DialogPortal>
<UiDialogOverlay v-if="hasBlockingOverlay" />
<UiDialogContent
:draggable="draggable"
:return-focus-element="returnFocusElement"
:class="cn('ui-utility-sheet', `ui-utility-sheet--${size}`, props.class)"
>
<UiDialogHeader :closable="closable" :close-label="closeLabel" :focused="focused">
<UiDialogTitle
><slot name="title">{{ title }}</slot></UiDialogTitle
>
<template v-if="$slots.actions" #actions>
<slot name="actions" />
</template>
</UiDialogHeader>
<UiDialogDescription class="sr-only"> {{ title }}的配置与快捷操作 </UiDialogDescription>
<div class="ui-utility-sheet__body">
<slot name="body"><slot /></slot>
</div>
<UiDialogFooter v-if="$slots.footer" class="ui-utility-sheet__footer">
<slot name="footer" />
</UiDialogFooter>
</UiDialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style scoped>
.ui-utility-sheet {
width: min(
var(--ui-utility-sheet-viewport-inline-size),
var(--ui-utility-sheet-active-inline-size)
);
max-width: none;
grid-template-rows: auto minmax(0, 1fr) auto;
}
.ui-utility-sheet--compact {
--ui-utility-sheet-active-inline-size: var(--ui-utility-sheet-compact-inline-size);
--ui-utility-sheet-active-body-min-block-size: var(
--ui-utility-sheet-compact-body-min-block-size
);
}
.ui-utility-sheet--default {
--ui-utility-sheet-active-inline-size: var(--ui-utility-sheet-default-inline-size);
--ui-utility-sheet-active-body-min-block-size: var(
--ui-utility-sheet-default-body-min-block-size
);
}
.ui-utility-sheet--wide {
--ui-utility-sheet-active-inline-size: var(--ui-utility-sheet-wide-inline-size);
--ui-utility-sheet-active-body-min-block-size: var(--ui-utility-sheet-wide-body-min-block-size);
}
.ui-utility-sheet__body {
min-width: 0;
min-height: var(--ui-utility-sheet-active-body-min-block-size);
max-height: var(--ui-utility-sheet-body-max-block-size);
padding: var(--ui-utility-sheet-body-padding);
overflow: auto;
background: var(--ui-utility-sheet-body-background);
}
.ui-utility-sheet__footer {
min-width: 0;
}
</style>
@@ -0,0 +1 @@
export { default as UiUtilitySheet } from './UiUtilitySheet.vue'
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { DialogPortal, DialogRoot } from 'reka-ui'
import {
UiDialogContent,
UiDialogDescription,
UiDialogHeader,
UiDialogOverlay,
UiDialogTitle,
} from '@/components/ui/dialog'
import { provideShow } from '@/components/hooks/show'
withDefaults(
defineProps<{
title: string
}>(),
{
title: '导入',
},
)
const open = defineModel<boolean>('open', { default: false })
let returnFocusElement: HTMLElement | null = null
// The existing import forms reset themselves through this injection whenever their dialog opens.
provideShow(open)
watch(open, (value) => {
if (value && document.activeElement instanceof HTMLElement) {
returnFocusElement = document.activeElement
}
})
function keepOpenOnOutsideInteraction(event: Event) {
event.preventDefault()
}
function restoreTriggerFocus(event: Event) {
event.preventDefault()
nextTick(() => returnFocusElement?.focus())
}
</script>
<template>
<DialogRoot v-model:open="open" modal>
<DialogPortal>
<UiDialogOverlay />
<UiDialogContent
class="character-import-dialog"
@interact-outside="keepOpenOnOutsideInteraction"
@pointer-down-outside="keepOpenOnOutsideInteraction"
@close-auto-focus="restoreTriggerFocus"
>
<UiDialogHeader>
<UiDialogTitle>{{ title }}</UiDialogTitle>
</UiDialogHeader>
<UiDialogDescription class="character-import-dialog__description">
{{ title }}窗口
</UiDialogDescription>
<div class="character-import-dialog__body">
<slot />
</div>
</UiDialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style scoped lang="scss">
.character-import-dialog {
--ui-dialog-viewport-inline-size: calc(100vw - 32px);
--ui-dialog-max-inline-size: 360px;
grid-template-rows: auto minmax(0, 1fr);
}
.character-import-dialog__description {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.character-import-dialog__body {
box-sizing: border-box;
display: flex;
min-width: 0;
min-height: 128px;
max-height: calc(100dvh - var(--ui-dialog-header-block-size) - 32px);
align-items: center;
justify-content: center;
padding: 8px;
overflow: auto;
}
</style>
@@ -5,6 +5,8 @@ import api from '@/api'
import { useDialog } from '@/components/hooks/dialog' import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show' import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores' import { useConfigStore } from '@/stores'
import { UiButton } from '@/components/ui/button'
import { UiInput } from '@/components/ui/input'
export default defineComponent({ export default defineComponent({
setup() { setup() {
@@ -48,20 +50,28 @@ export default defineComponent({
return () => { return () => {
return ( return (
<> <>
<div class="h-30 w-80 py-2 px-4 text-#e9c556"> <div class="box-border flex min-h-30 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
<div class="flex justify-around items-center"> <label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
助手社区ID <span>助手社区ID</span>
<calc-input class="!w-60" v-model={uid.value} /> <UiInput class="min-w-0" v-model={uid.value} />
</label>
<div class="leading-5">
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
点击导入后会从助手通过文字识别获取打造
</p>
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分打造无法获取,此处仅做快捷导入
</p>
</div> </div>
<div> <div class="flex items-center justify-center gap-3">
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p> <UiButton onClick={() => execute()} disabled={isLoading.value}>
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={isLoading.value}>
导入 导入
</calc-button> </UiButton>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button> <UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div> </div>
</div> </div>
</> </>
@@ -6,6 +6,7 @@ import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show' import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores' import { useConfigStore } from '@/stores'
import { type IHelperRoleInfo } from '@/api/info/type' import { type IHelperRoleInfo } from '@/api/info/type'
import { UiButton } from '@/components/ui/button'
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -54,22 +55,30 @@ export default defineComponent({
return () => { return () => {
return ( return (
<> <>
<div class="h-45 w-80 py-2 px-4 text-#e9c556"> <div class="box-border flex min-h-45 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
<div class="flex justify-around items-center flex-col gap-1"> <div class="flex flex-col items-center justify-center gap-1 text-center">
<div>检测到以下信息,是否导入助手数据?</div> <div>检测到以下信息,是否导入助手数据?</div>
<div class="text-#DED29A">{props.alter?.roleName}</div> <div class="text-[color:var(--ui-primary-foreground)]">{props.alter?.roleName}</div>
<div class="text-#908150">{props.alter?.roleJob}</div> <div class="text-[color:var(--ui-muted-foreground)]">{props.alter?.roleJob}</div>
<div class="text-#3ea74e">名望:{props.alter?.fame}</div> <div class="text-[color:var(--ui-success)]">名望:{props.alter?.fame}</div>
</div> </div>
<div> <div class="leading-5">
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p> <p class="m-0 text-[color:var(--ui-muted-foreground)]">
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p> *
点击导入后会从助手通过文字识别获取打造
</p>
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分打造无法获取,此处仅做快捷导入
</p>
</div> </div>
<div class="flex justify-around items-center"> <div class="flex items-center justify-center gap-3">
<calc-button onClick={execute} disabled={isLoading.value}> <UiButton onClick={() => execute()} disabled={isLoading.value}>
导入 导入
</calc-button> </UiButton>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button> <UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div> </div>
</div> </div>
</> </>
@@ -5,6 +5,8 @@ import api from '@/api'
import { useDialog } from '@/components/hooks/dialog' import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show' import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores' import { useConfigStore } from '@/stores'
import { UiButton } from '@/components/ui/button'
import { UiInput } from '@/components/ui/input'
export default defineComponent({ export default defineComponent({
setup() { setup() {
@@ -44,23 +46,28 @@ export default defineComponent({
return () => { return () => {
return ( return (
<> <>
<div class="h-35 w-80 py-2 px-4 text-#e9c556"> <div class="box-border flex min-h-35 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
<div class="flex justify-around items-center"> <label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
技能加点代码 <span>技能加点代码</span>
<calc-input class="!w-60" v-model={skillCode.value} /> <UiInput class="min-w-0" v-model={skillCode.value} />
</div> </label>
<div> <div class="leading-5">
<p class="text-#e9c556/80"> <p class="m-0 text-[color:var(--ui-muted-foreground)]">
* *
点击导入后会根据规则解析出技能加点、技能进化、技能强化 点击导入后会根据规则解析出技能加点、技能进化、技能强化
</p> </p>
<p class="text-#e9c556/80">* 部分技能可能存在无法识别的情况,需要手动调整</p> <p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分技能可能存在无法识别的情况,需要手动调整
</p>
</div> </div>
<div class="flex justify-around items-center"> <div class="flex items-center justify-center gap-3">
<calc-button onClick={execute} disabled={!skillCode.value || isLoading.value}> <UiButton onClick={() => execute()} disabled={!skillCode.value || isLoading.value}>
导入 导入
</calc-button> </UiButton>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button> <UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div> </div>
</div> </div>
</> </>
@@ -1,49 +0,0 @@
<template>
<div class="mx-10px">
<calc-collapse title="武器/称号/宠物/秘宝">
<calc-tabs v-model="anotherTab" class="!mb-2">
<calc-tab :value="0">武器</calc-tab>
<calc-tab :value="1">称号/宠物/秘宝</calc-tab>
<calc-tab :value="2" v-if="infoStore.infos?.alter.includes('vegabond')">副武器</calc-tab>
</calc-tabs>
<Weapon v-show="anotherTab === 0"></Weapon>
<Else v-show="anotherTab === 1"></Else>
<SubWeapon v-show="anotherTab === 2"></SubWeapon>
</calc-collapse>
<calc-collapse title="套装/通宝">
<calc-tabs v-model="equTab" class="!mb-2">
<calc-tab :value="0">套装</calc-tab>
<calc-tab :value="1">通宝</calc-tab>
</calc-tabs>
<Suit v-show="equTab === 0"></Suit>
<Universal v-show="equTab === 1"></Universal>
</calc-collapse>
<calc-collapse title="誓约">
<calc-tabs v-model="stoneTab" class="!mb-2">
<calc-tab :value="0">套装</calc-tab>
<calc-tab :value="1">通宝</calc-tab>
<calc-tab :value="2">其他</calc-tab>
</calc-tabs>
<Stone v-show="stoneTab == 0"></Stone>
<StoneUniversal v-show="stoneTab == 1"></StoneUniversal>
<StoneOld v-show="stoneTab == 2"></StoneOld>
</calc-collapse>
</div>
</template>
<script lang="ts" setup name="Index">
import Suit from './components/Suit.vue'
import Weapon from './components/Weapon.vue'
import Stone from './components/Stone.vue'
import Universal from './components/Universal.vue'
import StoneUniversal from './components/StoneUniversal.vue'
import StoneOld from './components/StoneOld.vue'
import Else from './components/Else.vue'
import { useInfoStore } from '@/stores'
import SubWeapon from './components/SubWeapon.vue'
const infoStore = useInfoStore()
const equTab = ref(0)
const stoneTab = ref(0)
const anotherTab = ref(0)
</script>
@@ -1,49 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equ, _) in equs" :key="_">
<EquipmentIcon
:equipment="equ"
:inactive="configStore.config.equips[equ?.itemDetailType ?? '']?.id != equ?.id"
@click="configStore.chooseEqu(equ)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const equs = computed(() => {
const items = infoStore.equips.filter((equip) => ['称号', '宠物'].includes(equip.itemDetailType))
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const equList = items.filter((item) => item.itemDetailType === type)
equList.sort((a, b) => {
if (a.itemType !== b.itemType) {
return b.itemType.localeCompare(a.itemType)
}
const rarityOrder = ['传说', '神器', '稀有']
return rarityOrder.indexOf(a.rarity) - rarityOrder.indexOf(b.rarity)
})
res.push(...equList)
const remind = rowCount - (res.length % rowCount)
res.push(...Array(remind + rowCount).fill(undefined))
})
const treasure = infoStore.equips.filter((equip) => equip.categorize.includes('秘宝'))
res.push(...treasure, ...Array(rowCount - treasure.length).fill(undefined))
return res
})
</script>
@@ -1,97 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div>
<div class="flex flex-col gap-10px pb-10px">
<!-- 套装选择tab -->
<div class="flex flex-wrap w-100%">
<template v-for="(suit, _) in suits" :key="_">
<div class="flex items-center justify-center">
<img
class="w-35px h-35px object-contain"
:src="getImageURL('equipment' + suit.imageUrl)"
:alt="suit.name"
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
@click="curSuit = suit.value"
@dblclick="handleDoubleClick"
/>
</div>
</template>
</div>
<EquipmentRaritySelect v-model="curRarity" />
</div>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in suitEquipments" :key="_">
<template v-if="equip?.rarity == curRarity || !curRarity">
<EquipmentIcon
:equipment="equip"
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.fusion != equip?.id"
@click="configStore.chooseEqu(equip, true)"
/>
</template>
<template v-else>
<EquipmentIcon />
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
const infoStore = useInfoStore()
const configStore = useConfigStore()
const curSuit = ref(16201)
const curRarity = ref()
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const suits = computed(() => {
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
infoStore.suits.forEach((a) => {
if (!uniqueSuits.has(a.suitId)) {
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
}
})
return Array.from(uniqueSuits.values())
})
const suitEquipments = computed(() => {
const equs = infoStore.stones.filter(
(a) => a.suit.includes(curSuit.value.toString()) && a.suit.length === 1,
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器', '传说', '史诗']
raritiyList.forEach((rarity) => {
const equ: (IEquipment | undefined)[] = equs.filter((a) => a.rarity === rarity)
equ.splice(5, 0, undefined)
equ.splice(9, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
return res
})
const handleDoubleClick = () => {
if (!curRarity.value) return
if (curRarity.value == '太初') return
suitEquipments.value
.filter((a) => !!a && a.rarity == curRarity.value)
.splice(0, 11)
.forEach((a) => {
configStore.chooseEqu(a, true)
})
}
</script>
@@ -1,45 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in equs" :key="_">
<EquipmentIcon
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.fusion != equip?.id"
:equipment="equip"
@click="configStore.chooseEqu(equip, true)"
/>
</template>
</div>
</template>
<script lang="ts" setup StoneUniversal>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const equs = computed(() => {
const items = infoStore.stones.filter((equip) => equip.categorize.includes('通宝'))
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器', '史诗']
const categorize = Array.from(new Set(items.map((item) => item.categorize)))
raritiyList.forEach((rarity) => {
categorize.forEach((cat) => {
const equ: (IEquipment | undefined)[] = items.filter(
(a) => a.rarity === rarity && a.categorize === cat,
)
equ.splice(5, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
})
return res
})
</script>
@@ -1,47 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(weapon, _) in weapons" :key="_">
<EquipmentIcon
:equipment="weapon"
:inactive="configStore.config.equips['副武器']?.id != weapon?.id"
@click="configStore.chooseEqu(weapon, false, true)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const weapons = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.itemType === '武器' && equip.itemDetailType == '光剑',
)
// console.log(items)
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const weapon = items.filter((item) => item.itemDetailType === type)
res.push(
...weapon.filter((item) => item.categorize === '制式武器'),
undefined,
...weapon.filter((item) => item.categorize === '传世武器'),
)
const remind = rowCount - (res.length % rowCount)
res.push(...Array(remind).fill(undefined))
})
return res
})
</script>
@@ -1,111 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div>
<div class="flex flex-col gap-10px pb-10px">
<!-- 套装选择tab -->
<div class="flex flex-wrap w-100%">
<template v-for="(suit, _) in suits" :key="_">
<div class="flex items-center justify-center">
<img
class="w-35px h-35px object-contain"
:src="getImageURL('equipment' + suit.imageUrl)"
:alt="suit.name"
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
@click="curSuit = suit.value"
@dblclick="handleDoubleClick"
/>
</div>
</template>
</div>
<EquipmentRaritySelect v-model="curRarity" />
</div>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in suitEquipments" :key="_">
<template v-if="equip?.rarity == curRarity || !curRarity">
<EquipmentIcon
:equipment="equip"
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.id != equip?.id"
@click="configStore.chooseEqu(equip)"
/>
</template>
<template v-else>
<EquipmentIcon />
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
const infoStore = useInfoStore()
const configStore = useConfigStore()
const curSuit = ref(16201)
const curRarity = ref()
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const suits = computed(() => {
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
infoStore.suits.forEach((a) => {
if (!uniqueSuits.has(a.suitId)) {
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
}
})
return Array.from(uniqueSuits.values())
})
const suitEquipments = computed(() => {
const equs = infoStore.equips.filter(
(a) => a.suit.includes(curSuit.value.toString()) && a.suit.length === 1,
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器', '传说', '史诗']
raritiyList.forEach((rarity) => {
const equ: (IEquipment | undefined)[] = equs.filter(
(a) => a.rarity === rarity && !a.name.includes('黑牙'),
)
equ.splice(5, 0, undefined)
equ.splice(9, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
const remind = equs
.filter((a) => a.rarity == '太初' || a.name.includes('黑牙'))
.sort((a, b) => (a.rarity == '太初' ? 1 : 0) - (b.rarity == '太初' ? 1 : 0))
res.push(
undefined,
undefined,
...remind.filter((a) => a.itemDetailType == '项链'),
undefined,
...remind.filter((a) => a.itemDetailType == '手镯'),
undefined,
...remind.filter((a) => a.itemDetailType == '戒指'),
...Array(Math.max(0, rowCount - remind.length - 4)).fill(undefined),
)
return res
})
const handleDoubleClick = () => {
if (!curRarity.value) return
if (curRarity.value == '太初') return
suitEquipments.value
.filter((a) => !!a && a.rarity == curRarity.value)
.splice(0, 11)
.forEach((a) => {
configStore.chooseEqu(a)
})
}
</script>
@@ -1,43 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in equs" :key="_">
<EquipmentIcon
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.id != equip?.id"
:equipment="equip"
@click="configStore.chooseEqu(equip)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const equs = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.categorize === '通宝装备' || equip.categorize === '稀有装备 套装',
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['稀有', '神器', '传说', '史诗']
raritiyList.forEach((rarity) => {
const equ: (IEquipment | undefined)[] = items.filter((a) => a.rarity === rarity)
equ.splice(5, 0, undefined)
equ.splice(9, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
return res
})
</script>
@@ -1,45 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(weapon, _) in weapons" :key="_">
<EquipmentIcon
:equipment="weapon"
:inactive="configStore.config.equips['武器']?.id != weapon?.id"
@click="configStore.chooseEqu(weapon)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const weapons = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.itemType === '武器' && infoStore.infos?.weapons.includes(equip.itemDetailType),
)
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const weapon = items.filter((item) => item.itemDetailType === type)
res.push(
...weapon.filter((item) => item.categorize === '制式武器'),
undefined,
...weapon.filter((item) => item.categorize === '传世武器'),
)
const remind = rowCount - (res.length % rowCount)
res.push(...Array(remind).fill(undefined))
})
return res
})
</script>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { computed } from 'vue'
import { UiButton } from '@/components/ui/button'
import { rarityClass } from '@/utils'
import increaseIcon from '@/assets/img/icon/increase.png'
import maxIcon from '@/assets/img/icon/max.png'
import minIcon from '@/assets/img/icon/min.png'
import reduceIcon from '@/assets/img/icon/reduce.png'
export type EquipmentLevelChange = 'preRarity' | 'nextRarity' | 'preLv' | 'nextLv'
const props = defineProps<{
rarity: string
level: number
rarityList: string[]
}>()
const emit = defineEmits<{
change: [direction: EquipmentLevelChange]
}>()
const rarityIndex = computed(() => props.rarityList.findIndex((rarity) => rarity === props.rarity))
const atFirstRarity = computed(() => rarityIndex.value <= 0)
const atLastRarity = computed(() => rarityIndex.value >= props.rarityList.length - 1)
function formatLevel(level: number, rarity: string) {
if (rarity === '太初') return ''
const greekNumerals = ['Ι', 'ΙΙ', 'ΙΙΙ', 'ΙV', 'V', 'VΙ', 'VΙΙ', 'VΙΙΙ', 'ΙX', 'X']
return greekNumerals[level - 1] || level.toString()
}
</script>
<template>
<div class="equipment-level-controls" aria-label="装备等级">
<UiButton
size="icon"
title="降低品质"
aria-label="降低品质"
:disabled="atFirstRarity"
@click="emit('change', 'preRarity')"
>
<img data-icon class="equipment-level-controls__icon" :src="minIcon" alt="" />
</UiButton>
<UiButton
size="icon"
title="降低等级"
aria-label="降低等级"
:disabled="atFirstRarity && level <= 1"
@click="emit('change', 'preLv')"
>
<img data-icon class="equipment-level-controls__icon" :src="reduceIcon" alt="" />
</UiButton>
<output class="equipment-level-controls__value" :class="rarityClass(rarity)">
<span>{{ rarity }}</span>
<span>{{ formatLevel(level, rarity) }}</span>
</output>
<UiButton
size="icon"
title="提升等级"
aria-label="提升等级"
:disabled="atLastRarity"
@click="emit('change', 'nextLv')"
>
<img data-icon class="equipment-level-controls__icon" :src="increaseIcon" alt="" />
</UiButton>
<UiButton
size="icon"
title="提升品质"
aria-label="提升品质"
:disabled="atLastRarity"
@click="emit('change', 'nextRarity')"
>
<img data-icon class="equipment-level-controls__icon" :src="maxIcon" alt="" />
</UiButton>
</div>
</template>
<style scoped>
.equipment-level-controls {
box-sizing: border-box;
display: flex;
min-width: 0;
align-items: center;
justify-content: center;
gap: var(--ui-autocomplete-trigger-gap);
padding: var(--ui-disclosure-content-padding-block);
background: var(--ui-settings-row-alt);
}
.equipment-level-controls__value {
display: flex;
min-width: 0;
min-height: var(--ui-control-height-default);
flex: 1;
align-items: center;
justify-content: center;
gap: var(--ui-control-gap);
padding-inline: var(--ui-control-padding-inline-compact);
border: 1px solid var(--ui-disclosure-border);
background: var(--ui-field-background);
line-height: var(--ui-control-line-height);
}
.equipment-level-controls__icon {
width: var(--ui-badge-height);
height: var(--ui-badge-height);
object-fit: contain;
}
:deep(.ui-button:disabled) .equipment-level-controls__icon {
filter: grayscale(1);
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
}
</style>
@@ -1,23 +1,53 @@
<script lang="ts" setup>
import {
SelectRoot,
SelectValue,
UiSelectContent,
UiSelectItem,
UiSelectTrigger,
} from '@/components/ui/select'
import { useConfigStore, useInfoStore } from '@/stores'
const configStore = useConfigStore()
const infoStore = useInfoStore()
</script>
<template> <template>
<div class="flex flex-col gap-2 w-250px p-2"> <form class="equipment-options" @submit.prevent>
<div v-for="option in info.options" :key="option.id" class="flex items-center gap-2"> <label v-for="option in infoStore.options" :key="option.id" class="equipment-options__row">
<div class="w-60px text-end">{{ option.name }}</div> <span>{{ option.name }}</span>
<calc-select <SelectRoot v-model="configStore.config.options[option.id.toString()]">
class="flex-1 !h-20px" <UiSelectTrigger :aria-label="option.name">
:appendTo="dialogRef" <SelectValue placeholder="请选择" />
v-model="config.config.options[option.id.toString()]" </UiSelectTrigger>
> <UiSelectContent>
<calc-option v-for="(item, index) in option.options" :key="index" :value="index"> <UiSelectItem v-for="(item, index) in option.options" :key="index" :value="index">
{{ item }} {{ item }}
</calc-option> </UiSelectItem>
</calc-select> </UiSelectContent>
</div> </SelectRoot>
</div> </label>
</form>
</template> </template>
<script lang="ts" setup> <style scoped>
import { useConfigStore, useInfoStore } from '@/stores' .equipment-options {
const dialogRef = inject('dialogRef', ref(null)) display: grid;
const config = useConfigStore() width: 100%;
const info = useInfoStore() min-width: 0;
</script> gap: var(--ui-control-gap);
}
.equipment-options__row {
display: grid;
min-width: 0;
grid-template-columns: minmax(var(--ui-settings-row-min-block-size), auto) minmax(0, 1fr);
align-items: center;
gap: var(--ui-control-gap);
color: var(--ui-settings-row-foreground);
}
.equipment-options__row > span {
text-align: right;
}
</style>
@@ -164,7 +164,7 @@ export const formatEqu = (total: IEquipment[], subweapons?: string[], curSuit?:
equs: treasures, equs: treasures,
}, },
{ {
name: `${curSuit} 套装`, name: curSuit ? `${curSuit} 套装` : '套装',
equs: suits, equs: suits,
}, },
{ {
@@ -270,7 +270,7 @@ export const formatStone = (total: IEquipment[], curSuit?: string) => {
// rzs.push(...temp, ...Array(temp.length == 0 ? rowCount-remind : remind).fill(undefined)) // rzs.push(...temp, ...Array(temp.length == 0 ? rowCount-remind : remind).fill(undefined))
return [ return [
{ {
name: `${curSuit} 套装`, name: curSuit ? `${curSuit} 套装` : '套装',
equs: suits, equs: suits,
}, },
{ {
@@ -315,11 +315,11 @@ export const formatOath = (
} }
return [ return [
{ {
name: `${curSuit?.name} 誓约`, name: curSuit?.name ? `${curSuit.name} 誓约` : '誓约',
equs: oath, equs: oath,
}, },
{ {
name: `${curSuit?.name} 星蕴石`, name: curSuit?.name ? `${curSuit.name} 星蕴石` : '星蕴石',
equs: primer, equs: primer,
}, },
] ]

Some files were not shown because too many files have changed in this diff Show More