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 |
| `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 |
| `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 |
| `slider` | `ui/slider` | Migrated | Preserved the Reka coordinate system |
| `data-table` | `ui/data-table` | Migrated | Ported hover, selected and density behavior |
| `dialog` | `ui/dialog` | Migrated | Preserved the focused title effect and focus management |
| `menu` | `ui/dropdown-menu` | Migrated | Ported source menu spacing and state tokens |
| `rarity` | `ui/item-slot` | Migrated | Retained rarity presentation as data-semantic tokens |
| `autocomplete` | `ui/autocomplete` | Partial | Combobox is migrated; domain SearchTree remains deferred |
| `navigation` | `ui/navigation` | Migrated | Selectable navigation primitive with source assets |
| `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`, `ui/utility-sheet` | Migrated | Kept WindowDialog and UtilitySheet as separate families while sharing focus-safe dialog chrome |
| `menu` | `ui/dropdown-menu`, `ui/settings-dropdown` | Migrated | Preserved the distinct compact-context and full settings-menu anatomies |
| `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`, `ui/search-tree` | Migrated | Kept flat Combobox and toolbar-plus-tree search as separate reusable families |
| `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 |
| 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 |
| `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 |
@@ -39,7 +39,8 @@ mean recreating the effect from screenshots.
## 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.
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
- SearchTree/CommandInput still requires a domain-neutral data and filtering contract before it can
join `src/components/ui`.
- Complex product dialogs are compositions, not a claim that every business dialog has reached
- `SkillDamageDetail` and `ItemRow` remain domain compositions on top of `ui/data-table`; the
generic table contract now exposes the structural slots they require.
- Complex product dialogs remain compositions, not a claim that every business dialog has reached
source-level visual parity.
- Existing `frontend` business call sites remain on their current components until each behavior
slice is migrated and browser-verified.
- Business call sites in the active routes are migrated, the superseded equipment page is removed,
and the repository-wide `components/calc` call-site check is now at zero.
-1
View File
@@ -20,7 +20,6 @@
"axios": "^1.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hoci": "^0.6.0",
"pako": "^2.1.0",
"pinia": "^4.0.2",
"query-string": "^9.1.1",
-390
View File
@@ -20,9 +20,6 @@ importers:
clsx:
specifier: ^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:
specifier: ^2.1.0
version: 2.1.0
@@ -102,9 +99,6 @@ importers:
packages:
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
@@ -272,23 +266,6 @@ packages:
'@floating-ui/vue@1.1.11':
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':
resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==}
@@ -325,18 +302,6 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
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':
resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==}
@@ -737,15 +702,6 @@ packages:
'@rolldown/pluginutils@1.0.1':
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':
resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==}
cpu: [arm]
@@ -957,9 +913,6 @@ packages:
'@types/pako@2.0.3':
resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
'@types/web-bluetooth@0.0.15':
resolution: {integrity: sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@@ -1075,23 +1028,14 @@ packages:
peerDependencies:
vue: ^3.5.0
'@vueuse/core@9.0.0':
resolution: {integrity: sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==}
'@vueuse/metadata@14.4.0':
resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==}
'@vueuse/metadata@9.0.0':
resolution: {integrity: sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ==}
'@vueuse/shared@14.4.0':
resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==}
peerDependencies:
vue: ^3.5.0
'@vueuse/shared@9.0.0':
resolution: {integrity: sha512-WRCyr/wIz5e/2gR/+qFucbCUcGMyJKkQZAzlECl3e71ebQQ9X/w3aBWT9FbnogJX+DNZ/t3Pj+TqPbC7TH1Yog==}
acorn@8.14.1:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
engines: {node: '>=0.4.0'}
@@ -1109,10 +1053,6 @@ packages:
resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==}
engines: {node: '>=22'}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
@@ -1131,19 +1071,9 @@ packages:
axios@1.8.2:
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:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -1160,10 +1090,6 @@ packages:
caniuse-lite@1.0.30001702:
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:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
@@ -1192,9 +1118,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1211,9 +1134,6 @@ packages:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
@@ -1282,13 +1202,6 @@ packages:
exsolve@1.1.1:
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:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -1339,10 +1252,6 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -1362,21 +1271,12 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hoci@0.6.0:
resolution: {integrity: sha512-PtSPRtQm5/OUKxSU8lVelhBc+3TVW2cB2hGlG9pfzQ0rbSBFgLHWSLImBgLZnau72wKOrMNjBxzprQ941+rTWw==}
peerDependencies:
vue: ^3.0.0
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
immutable@5.1.9:
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:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -1560,10 +1460,6 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
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:
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
engines: {node: '>=14'}
@@ -1575,9 +1471,6 @@ packages:
resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
engines: {node: '>=20.19.0'}
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1588,17 +1481,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
maybe-types@0.2.0:
resolution: {integrity: sha512-4ul+RHPcxNznI+Ke6xUT2/mcQTl54fYUdRZQ3QNkQsC4USfAxXkPTh+wEOx4UBsXYg214867v2ZeirQDJT7uOA==}
memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -1611,10 +1497,6 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
mlly@1.7.4:
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
@@ -1638,10 +1520,6 @@ packages:
node-releases@2.0.19:
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:
resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==}
@@ -1753,17 +1631,10 @@ packages:
resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==}
engines: {node: '>=18'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-package-json-fast@6.0.0:
resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==}
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:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
@@ -1773,10 +1644,6 @@ packages:
peerDependencies:
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:
resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1787,9 +1654,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
sass@1.102.0:
resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==}
engines: {node: '>=20.19.0'}
@@ -1850,13 +1714,6 @@ packages:
tslib@2.8.1:
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:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
@@ -1899,19 +1756,6 @@ packages:
resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==}
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:
resolution: {integrity: sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==}
engines: {node: '>=20.19.0'}
@@ -1922,10 +1766,6 @@ packages:
'@nuxt/kit':
optional: true
unplugin@1.16.1:
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
engines: {node: '>=14.0.0'}
unplugin@3.3.0:
resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2077,9 +1917,6 @@ packages:
snapshots:
'@antfu/utils@0.7.10':
optional: true
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -2302,43 +2139,6 @@ snapshots:
- '@vue/composition-api'
- 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':
dependencies:
'@swc/helpers': 0.5.23
@@ -2381,21 +2181,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@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': {}
'@oxfmt/binding-android-arm-eabi@0.62.0':
@@ -2617,15 +2402,6 @@ snapshots:
'@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':
optional: true
@@ -2774,8 +2550,6 @@ snapshots:
'@types/pako@2.0.3': {}
'@types/web-bluetooth@0.0.15': {}
'@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))':
@@ -2951,31 +2725,12 @@ snapshots:
'@vueuse/shared': 14.4.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))':
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@9.0.0': {}
'@vueuse/shared@14.4.0(vue@3.5.40(typescript@6.0.3))':
dependencies:
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.18.0: {}
@@ -2984,12 +2739,6 @@ snapshots:
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:
dependencies:
tslib: 2.8.1
@@ -3015,19 +2764,8 @@ snapshots:
transitivePeerDependencies:
- debug
balanced-match@1.0.2:
optional: true
binary-extensions@2.3.0:
optional: true
birpc@2.9.0: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
optional: true
braces@3.0.3:
dependencies:
fill-range: 7.1.1
@@ -3047,19 +2785,6 @@ snapshots:
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:
dependencies:
readdirp: 5.0.0
@@ -3086,8 +2811,6 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
csstype@3.1.3: {}
csstype@3.2.3: {}
debug@4.4.0:
@@ -3096,8 +2819,6 @@ snapshots:
decode-uri-component@0.4.1: {}
defu@6.1.4: {}
defu@6.1.7: {}
delayed-stream@1.0.0: {}
@@ -3151,20 +2872,6 @@ snapshots:
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):
optionalDependencies:
picomatch: 4.0.5
@@ -3210,11 +2917,6 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
optional: true
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@@ -3229,30 +2931,10 @@ snapshots:
dependencies:
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: {}
immutable@5.1.9: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
optional: true
is-extglob@2.1.1:
optional: true
@@ -3378,12 +3060,6 @@ snapshots:
lightningcss-win32-arm64-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:
dependencies:
mlly: 1.7.4
@@ -3398,11 +3074,6 @@ snapshots:
dependencies:
magic-string: 0.30.21
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
optional: true
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3413,13 +3084,8 @@ snapshots:
math-intrinsics@1.1.0: {}
maybe-types@0.2.0: {}
memorystream@0.3.1: {}
merge2@1.4.1:
optional: true
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -3432,11 +3098,6 @@ snapshots:
dependencies:
mime-db: 1.52.0
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
optional: true
mlly@1.7.4:
dependencies:
acorn: 8.14.1
@@ -3462,9 +3123,6 @@ snapshots:
node-releases@2.0.19: {}
normalize-path@3.0.0:
optional: true
nostics@1.2.0: {}
npm-normalize-package-bin@6.0.0: {}
@@ -3587,19 +3245,11 @@ snapshots:
filter-obj: 5.1.0
split-on-first: 3.0.0
queue-microtask@1.2.3:
optional: true
read-package-json-fast@6.0.0:
dependencies:
json-parse-even-better-errors: 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: {}
reka-ui@2.10.1(vue@3.5.40(typescript@6.0.3)):
@@ -3618,9 +3268,6 @@ snapshots:
transitivePeerDependencies:
- '@vue/composition-api'
reusify@1.1.0:
optional: true
rolldown@1.2.2:
dependencies:
'@oxc-project/types': 0.142.0
@@ -3667,11 +3314,6 @@ snapshots:
fsevents: 2.3.3
optional: true
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
optional: true
sass@1.102.0:
dependencies:
chokidar: 5.0.0
@@ -3720,12 +3362,6 @@ snapshots:
tslib@2.8.1: {}
tslx@0.1.1:
dependencies:
csstype: 3.1.3
type-fest@4.38.0: {}
typescript@6.0.3: {}
ufo@1.5.4: {}
@@ -3789,26 +3425,6 @@ snapshots:
pathe: 2.0.3
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)):
dependencies:
chokidar: 5.0.0
@@ -3832,12 +3448,6 @@ snapshots:
- vite
- 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)):
dependencies:
'@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>
<div class="pos-relative w-300px h-full">
<div class="avatar-list">
<template v-for="part in infoStore.avatarParts" :key="part">
<div
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)"
>
<div class="avatar-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
{{ partName(part) }}
</div>
</template>
@@ -66,4 +62,26 @@ const partName = (part: string) => {
}
</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>
<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">
<div
class="pos-absolute text-white/50 flex items-center justify-center rounded-50%"
:style="infoStyle(part)"
@click="choosePart(part)"
>
<div class="emblem-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
<img
class="w-25px h-25px rounded-50%"
class="emblem-list__rarity"
style="object-fit: none; object-position: hue; mix-blend-mode: screen"
:src="rarityIcon(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>
</template>
</div>
@@ -102,7 +103,6 @@ const lvStyle = computed(() => {
const infoStyle = computed(() => {
return (part: string) => {
console.log(`url(${rarityIcon.value(part)}),url(${partIcon.value(part)})`)
return {
left: `${position[part as keyof typeof position].x}px`,
top: `${position[part as keyof typeof position].y}px`,
@@ -129,6 +129,35 @@ const choosePart = (part: string) => {
</script>
<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 {
background-image: url(../Images/head.png);
}
@@ -1,18 +1,23 @@
<template>
<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
class="w-28px h-28px"
class="equipment-icon__image"
:style="{
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>
</template>
<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>
@@ -47,38 +52,43 @@ const getLocalImageURL = (rarity: string) => {
return new URL(`./rarity/${rarity}.png`, import.meta.url).href
}
</script>
``
<style lang="scss" scoped>
// @keyframes primeval {
// @for $i from 0 through 29 {
// #{percentage($i / 29)} {
// background-image: url("./effect/primeval/#{$i}.png");
// }
// }
// }
.equipment-icon {
position: relative;
width: var(--ui-equipment-icon-size, var(--ui-item-image-size));
height: var(--ui-equipment-icon-size, var(--ui-item-image-size));
flex: 0 0 auto;
// .primeval_effect {
// width: 28px;
// height: 28px;
// background-size: cover;
// animation: primeval 2s infinite;
// // 设置动画名称,持续时间,和循环次数
// mix-blend-mode: color-dodge;
// }
&__image {
position: relative;
width: 100%;
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.inactive {
&::after {
content: '';
&__rarity-frame {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 28px;
height: 28px;
background-color: #00000090;
// filter: grayscale(100%);
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
&--empty {
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>
@@ -1,51 +1,37 @@
<template>
<div class="bg-hex-222222">
<div
: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="h-30px flex items-left my-2px">
<div style="position: relative" class="w-28px mr-2px">
<div class="equipment-info">
<div class="equipment-info__panel">
<div class="equipment-info__header">
<div class="equipment-info__icon">
<Icon :equipment="props.equipment"></Icon>
</div>
<div style="line-height: 15px">
<div :class="[rarity, 'pl-5px']">{{ props.equipment?.name }}</div>
<div class="pl-5px text-#3ea74e">{{ props.equipment?.categorize }}</div>
<div class="equipment-info__identity">
<div :class="[rarity, 'equipment-info__name']">{{ props.equipment?.name }}</div>
<div class="equipment-info__category">{{ props.equipment?.categorize }}</div>
</div>
</div>
<!-- 装备图标 装备名称 -->
<div class="divide"></div>
<div class="flex flex-col items-end my-1px" style="line-height: 15px">
<div class="equipment-info__divider"></div>
<div class="equipment-info__section equipment-info__section--end">
<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 class="divide"></div>
<div
class="flex flex-col items-start my-1px"
style="white-space: pre-wrap; line-height: 15px"
>
<div class="text-#908150 py-4px" v-html="mainAttr"></div>
<div class="equipment-info__divider"></div>
<div class="equipment-info__section">
<div class="equipment-info__attributes" v-html="mainAttr"></div>
</div>
<!-- 装备属性 -->
<div class="divide"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
<div class="text-#DED29A py-4px whitespace-pre-wrap" v-html="detail"></div>
<div class="equipment-info__divider"></div>
<div class="equipment-info__section">
<div class="equipment-info__detail" v-html="detail"></div>
</div>
<template v-if="bufferDetail">
<div class="divide"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
<div class="text-#A48B2E mb-10px pt-4px">辅助职业专属属性</div>
<div class="text-#DED29A pb-10px whitespace-pre-wrap" v-html="bufferDetail"></div>
<div class="equipment-info__divider"></div>
<div class="equipment-info__section">
<div class="equipment-info__buffer-heading">辅助职业专属属性</div>
<div class="equipment-info__buffer-detail" v-html="bufferDetail"></div>
</div>
</template>
</div>
@@ -93,7 +79,7 @@ const mainAttr = computed(() => {
const detail = computed(() =>
props.equipment?.detail?.replace(
/\[(?![^\]]*技能)[^[\]]+\]/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
return props.equipment?.bufferDetail.replace(
/\[(?![^\]]*技能)[^[\]]+\]/g,
(match) => `<span style="color:#3d9147">${match}</span>`,
(match) => `<span class="equipment-info__markup">${match}</span>`,
)
})
</script>
<style lang="scss" scoped>
.divide {
border-top: 1px solid rgba(243, 244, 246, 0.3);
margin: 2px 0;
.equipment-info {
display: flow-root;
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>
@@ -1,14 +1,10 @@
<template>
<div
class="pos-relative w-264px h-full"
class="equipment-list"
:style="{ backgroundImage: withBg ? `url(${getImageURL('/common/head.png')})` : 'none' }"
>
<template v-for="part in parts" :key="part">
<div
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)"
>
<div class="equipment-list__slot" :style="infoStyle(part)" @click="choosePart(part)">
<template v-if="detail?.[part]?.id">
<EquipmentIcon :equipment="equ(detail[part].id)" />
</template>
@@ -16,14 +12,10 @@
{{ partName(part) }}
</template>
</div>
<!-- <div v-if="detail?.[part]?.fusion" class="pos-absolute" :style="fusionStyle(part)">
<EquipmentIcon :equipment="oaths(detail[part].fusion)" />
</div> -->
</template>
<div
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"
style="left: 150px; top: 5px; z-index: 8"
class="equipment-list__slot equipment-list__slot--subweapon"
@click="choosePart('副武器')"
>
<template v-if="detail?.['副武器']?.id">
@@ -31,12 +23,7 @@
</template>
<template v-else> 副武 </template>
</div>
<img
v-if="props.avatar"
:src="props.avatar"
alt="Avatar"
class="pos-absolute w-70% left-1/2 translate-x--1/2 -bottom-10px"
/>
<img v-if="props.avatar" :src="props.avatar" alt="Avatar" class="equipment-list__avatar" />
</div>
</template>
@@ -108,13 +95,10 @@ const infoStyle = (part: string) => {
x += (index % 2) * 39
y += Math.floor(index / 2) * 34
const style = {
left: `${x}px`,
top: `${y}px`,
zIndex: 6,
// fontWeight: 900,
return {
'--ui-equipment-slot-left': `${x}px`,
'--ui-equipment-slot-top': `${y}px`,
}
return style
}
// const fusionStyle = (part: string) => {
@@ -152,4 +136,50 @@ const partName = (part: string) => {
}
</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>
<div
class="pos-relative w-264px h-full"
class="equipment-oath"
:style="{ backgroundImage: withBg ? `url(${getImageURL('/common/oath/head.png')})` : 'none' }"
>
<template v-for="part in parts" :key="part">
<div
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)"
>
<div class="equipment-oath__slot" :style="infoStyle(part)" @click="choosePart(part)">
<template v-if="detail?.[part]?.id">
<EquipmentIcon :equipment="equ(detail[part].id, part)" useDisplayUrl />
</template>
@@ -16,16 +12,7 @@
{{ part != '11' ? '星蕴' : '誓约' }}
</template>
</div>
<!-- <div v-if="detail?.[part]?.fusion" class="pos-absolute" :style="fusionStyle(part)">
<EquipmentIcon :equipment="oaths(detail[part].fusion)" />
</div> -->
</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>
</template>
@@ -158,20 +145,12 @@ const positionSimple = [
]
const infoStyle = (part: string) => {
if (props.withBg)
return {
left: `${position[parseInt(part)].x}px`,
top: `${position[parseInt(part)].y}px`,
zIndex: 6,
// fontWeight: 900,
}
else
return {
left: `${positionSimple[parseInt(part)].x}px`,
top: `${positionSimple[parseInt(part)].y}px`,
zIndex: 6,
// fontWeight: 900,
}
const coordinates = (props.withBg ? position : positionSimple)[Number.parseInt(part)]
return {
'--ui-equipment-slot-left': `${coordinates.x}px`,
'--ui-equipment-slot-top': `${coordinates.y}px`,
}
}
const choosePart = (part: string) => {
@@ -179,4 +158,32 @@ const choosePart = (part: string) => {
}
</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>
<calc-cascader
class="!h-5 flex-1"
<UiTreeSelect
class="part-select"
v-model="selected"
:items="items"
select-parents
placeholder="请输入名称搜索"
></calc-cascader>
label="装备部位"
@select="selectPart"
/>
</template>
<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 { syncRef, useVModel } from '@vueuse/core'
import { useVModel } from '@vueuse/core'
const props = defineProps({
partList: {
@@ -27,7 +30,9 @@ const special_parts = ['辅助装备', '魔法石', '耳环']
const else_parts = ['称号', '宠物']
const emit = defineEmits([])
const emit = defineEmits<{
'update:partList': [value: string[]]
}>()
const choose_part = useVModel(props, 'partList', emit, { passive: true })
@@ -35,95 +40,92 @@ const selected = ref('')
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 [
{
label: '全部类别',
value: '',
onSelect() {
choose_part.value = []
},
},
{
label: '武器',
value: '武器',
onSelect() {
choose_part.value = infoStore.infos?.weapons ?? []
},
children: infoStore.infos?.weapons.map((r) => {
return {
label: r,
value: r,
onSelect() {
choose_part.value = [r]
},
}
}),
},
{
label: '防具',
value: '防具',
onSelect() {
choose_part.value = [...armor_parts]
},
children: armor_parts.map((part) => {
return {
label: part,
value: part,
onSelect() {
choose_part.value = [part]
},
}
}),
},
{
label: '首饰',
value: '首饰',
onSelect() {
choose_part.value = [...jewel_parts]
},
children: jewel_parts.map((part) => {
return {
label: part,
value: part,
onSelect() {
choose_part.value = [part]
},
}
}),
},
{
label: '特殊装备',
value: '特殊装备',
onSelect() {
choose_part.value = [...special_parts]
},
children: special_parts.map((part) => {
return {
label: part,
value: part,
onSelect() {
choose_part.value = [part]
},
}
}),
},
{
label: '其它',
value: '其它',
onSelect() {
choose_part.value = [...else_parts]
},
children: else_parts.map((part) => {
return {
label: part,
value: part,
onSelect() {
choose_part.value = [part]
},
}
}),
},
]
})
</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>
<div class="flex gap-8px">
<template v-for="(rarity, _) in rarities" :key="_">
<div class="equipment-rarity-select">
<template v-for="rarity in rarities" :key="rarity.value">
<div
class="w-14px h-14px shadow-[0px_0px_3px_#ffffff_inset,0px_0px_1px_#ffffff40]"
:style="{ background: rarity.background }"
class="equipment-rarity-select__swatch"
:class="rarity.className"
@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>
</template>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({})
const value = defineModel<string>()
const rarities = [
{
value: '稀有',
background: '#9879da',
className: 'equipment-rarity-select__swatch--rare',
},
{
value: '神器',
background: '#ef28c1',
className: 'equipment-rarity-select__swatch--artifact',
},
{
value: '传说',
background: '#fc8711',
className: 'equipment-rarity-select__swatch--legend',
},
{
value: '史诗',
background: '#fcb600',
className: 'equipment-rarity-select__swatch--epic',
},
{
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>
<style lang="scss" scoped>
.active {
background: url('@/assets/img/control/item_checked.png') no-repeat center;
background-size: 12px 10px;
.equipment-rarity-select {
display: flex;
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>
@@ -1,21 +1,11 @@
<template>
<div class="bg-hex-222222">
<div
: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="equipment-suit-info">
<div class="equipment-suit-info__panel">
<!-- 装备图标 装备名称 -->
<div class="flex w-full items-center justify-center gap-5px">
<div>
<div class="equipment-suit-info__header">
<div class="equipment-suit-info__icon">
<img
class="equipment-suit-info__icon-image"
:src="getImageURL('equipment' + props.suit?.imageUrl.replace('/suit/', '/suit/icon/'))"
alt="装备图标"
/>
@@ -23,25 +13,22 @@
<div :class="rarity">{{ props.suit?.name }}</div>
<div :class="rarity">{{ props.suit?.point }}点</div>
</div>
<div class="divide"></div>
<div class="equipment-suit-info__divider"></div>
<!-- 装备基础信息 -->
<div
class="flex flex-col items-start my-1px"
style="white-space: pre-wrap; line-height: 15px"
>
<div class="text-#908150" v-html="mainAttr"></div>
<div class="equipment-suit-info__section">
<div class="equipment-suit-info__attributes" v-html="mainAttr"></div>
</div>
<!-- 装备属性 -->
<div class="divide"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
<div class="text-#DED29A py-4px whitespace-pre-wrap" v-html="detail"></div>
<div class="equipment-suit-info__divider"></div>
<div class="equipment-suit-info__section">
<div class="equipment-suit-info__detail" v-html="detail"></div>
</div>
<template v-if="bufferDetail">
<div class="divide"></div>
<div class="flex flex-col items-start my-1px whitespace-pre-wrap leading-15px">
<div class="text-#A48B2E mb-10px pt-4px">辅助职业专属属性</div>
<div class="text-#DED29A pb-10px whitespace-pre-wrap" v-html="bufferDetail"></div>
<div class="equipment-suit-info__divider"></div>
<div class="equipment-suit-info__section">
<div class="equipment-suit-info__buffer-heading">辅助职业专属属性</div>
<div class="equipment-suit-info__buffer-detail" v-html="bufferDetail"></div>
</div>
</template>
</div>
@@ -76,7 +63,7 @@ const mainAttr = computed(() => {
const detail = computed(() =>
props.suit?.value?.replace(
/\[(?![^\]]*技能)[^[\]]+\]/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
return props.suit?.bufferValue?.replace(
/\[(?![^\]]*技能)[^[\]]+\]/g,
(match) => `<span style="color:#3d9147">${match}</span>`,
(match) => `<span class="equipment-suit-info__markup">${match}</span>`,
)
})
</script>
<style lang="scss" scoped>
.divide {
border-top: 1px solid rgba(243, 244, 246, 0.3);
margin: 2px 0;
.equipment-suit-info {
display: flow-root;
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>
+9 -4
View File
@@ -1,6 +1,6 @@
import { h, reactive, renderList } from 'vue'
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'
export type ConfirmDialogOption = Omit<ShowDialogOption, 'okButton' | 'cancelButton'>
@@ -87,14 +87,14 @@ export const useDialog = createSharedComposable(() => {
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)
if (!dialog) {
return
}
dialog.resolve({
id,
status: 'ok',
status,
get isOk() {
return status == 'ok'
},
@@ -119,6 +119,10 @@ export const useDialog = createSharedComposable(() => {
}
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) => {
const {
id = randomId(),
@@ -145,7 +149,7 @@ export const useDialog = createSharedComposable(() => {
resolve,
render(visible = true) {
return h(
ActionDialog,
UiActionDialog,
{
header,
okButton,
@@ -154,6 +158,7 @@ export const useDialog = createSharedComposable(() => {
visible,
onClose,
mask,
returnFocusElement,
scale,
},
{
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ButtonHTMLAttributes } from 'vue'
import type { AsTag } from 'reka-ui'
import { Primitive } from 'reka-ui'
import { buttonVariants, type ButtonVariants } from './buttonVariants'
@@ -12,22 +13,34 @@ withDefaults(
asChild?: boolean
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
disabled?: boolean
type?: ButtonHTMLAttributes['type']
title?: string
}>(),
{
as: 'button',
asChild: false,
variant: 'primary',
size: 'default',
type: 'button',
},
)
const emits = defineEmits<{
click: [event: MouseEvent]
}>()
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:disabled="disabled"
:type="type"
:title="title"
:class="cn(buttonVariants({ variant, size }), $attrs.class ?? '')"
v-bind="$attrs"
@click="emits('click', $event)"
>
<slot />
</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">
import type { HTMLAttributes } from 'vue'
import type { CSSProperties, HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
export interface UiDataTableColumn {
@@ -7,21 +7,38 @@ export interface UiDataTableColumn {
label: string
sortable?: boolean
width?: string
align?: 'start' | 'center' | 'end'
}
export interface UiDataTableRow {
id: string
[key: string]: string
kind?: 'data' | 'note'
selectable?: boolean
disabled?: boolean
[key: string]: unknown
}
const props = defineProps<{
class?: HTMLAttributes['class']
columns: UiDataTableColumn[]
rows: UiDataTableRow[]
selectedId?: string
sortKey?: string
sortDirection?: 'ascending' | 'descending'
}>()
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
columns: UiDataTableColumn[]
rows: UiDataTableRow[]
selectedId?: string
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<{
select: [row: UiDataTableRow]
@@ -29,10 +46,32 @@ const emits = defineEmits<{
}>()
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>
<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
class="ui-data-table__row ui-data-table__header"
role="row"
@@ -49,25 +88,76 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
>
{{ column.label }}<i aria-hidden="true" />
</button>
<div v-else role="columnheader">{{ column.label }}</div>
<div v-else role="columnheader" :class="`is-align-${column.align ?? 'start'}`">
{{ column.label }}
</div>
</template>
</div>
<button
v-for="row in rows"
:key="row.id"
type="button"
<div class="ui-data-table__body" role="rowgroup" :style="bodyStyle">
<template v-if="rows.length">
<template v-for="row in rows" :key="row.id">
<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"
:class="['ui-data-table__row', { 'is-selected': row.id === selectedId }]"
:style="{ gridTemplateColumns: gridTemplate }"
@click="emits('select', row)"
>
<span v-for="column in columns" :key="column.key" role="cell">
<slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
{{ row[column.key] }}
</slot>
</span>
</button>
<slot name="summary" :columns="columns" />
</div>
<div v-if="$slots.footer" class="ui-data-table__footer">
<slot name="footer" />
</div>
</div>
</template>
@@ -81,6 +171,17 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
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 {
display: grid;
width: 100%;
@@ -97,26 +198,31 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ?
min-width: var(--ui-table-scroll-inline-size);
}
button.ui-data-table__row {
.ui-data-table__data-row.is-selectable {
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-offset: -2px;
}
button.ui-data-table__row:nth-child(odd) {
.ui-data-table__data-row:nth-child(odd) {
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);
outline: 1px solid var(--ui-table-row-hover-outline);
outline-offset: -1px;
}
button.ui-data-table__row.is-selected {
background: var(--ui-table-row-selected);
outline: 1px solid var(--ui-table-row-selected-outline);
.ui-data-table__data-row.is-selected {
background: var(--ui-table-selected-background);
outline: 1px solid var(--ui-table-selected-outline);
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 > * {
@@ -132,6 +238,16 @@ button.ui-data-table__row.is-selected {
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 {
min-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 {
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>
@@ -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 & {
class?: HTMLAttributes['class']
draggable?: boolean
returnFocusElement?: HTMLElement | null
}
>(),
{ draggable: true },
)
const emits = defineEmits<DialogContentEmits>()
const dragOffset = ref({ x: 0, y: 0 })
const capturedReturnFocusElement = shallowRef<HTMLElement | null>(null)
let activePointerId: number | undefined
let dragOrigin = { clientX: 0, clientY: 0, x: 0, y: 0 }
let dragBounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 }
const delegatedProps = computed(() => {
const { class: _class, draggable: _draggable, ...delegated } = props
const {
class: _class,
draggable: _draggable,
returnFocusElement: _returnFocusElement,
...delegated
} = props
return delegated
})
@@ -51,6 +58,36 @@ function resetPosition() {
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) {
if (activePointerId !== event.pointerId) return
const deltaX = event.clientX - dragOrigin.clientX
@@ -106,7 +143,8 @@ onBeforeUnmount(() => {
:style="dragStyle"
:data-draggable="draggable ? '' : undefined"
@pointerdown="handlePointerDown"
@open-auto-focus="resetPosition"
@open-auto-focus="handleOpenAutoFocus"
@close-auto-focus="handleCloseAutoFocus"
:class="
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]',
@@ -120,8 +158,12 @@ onBeforeUnmount(() => {
<style scoped>
.ui-dialog-content {
width: min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size));
max-height: calc(100dvh - 16px);
width: var(
--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;
border: 1px solid var(--ui-dialog-border);
border-radius: 0;
@@ -131,9 +173,10 @@ onBeforeUnmount(() => {
inset 0 0 0 1px var(--ui-dialog-inset);
color: var(--ui-surface-foreground);
transform: translate(
calc(-50% + var(--ui-dialog-drag-x, 0px)),
calc(-50% + var(--ui-dialog-drag-y, 0px))
);
calc(-50% + var(--ui-dialog-drag-x, 0px)),
calc(-50% + var(--ui-dialog-drag-y, 0px))
)
scale(var(--ui-dialog-scale, 1));
will-change: transform;
}
@@ -1,4 +1,6 @@
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 UiDialogDescription } from './UiDialogDescription.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'
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 },
)
const delegatedProps = computed(() => {
const { class: _class, marker: _marker, ...delegated } = props
const { class: _class, marker: _marker, markers: _markers, ...delegated } = props
return delegated
})
const percentage = computed(() =>
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>
<template>
<ProgressRoot v-bind="delegatedProps" :class="cn('ui-progress', props.class)">
<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>
</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
label?: string
disabled?: boolean
selectParents?: boolean
}>(),
{ placeholder: '请选择', label: '树形选择' },
{ placeholder: '请选择', label: '树形选择', selectParents: false },
)
const emits = defineEmits<{
select: [item: UiTreeSelectItem]
}>()
const model = defineModel<string>()
const open = ref(false)
const expanded = ref<string[]>([])
@@ -80,13 +85,14 @@ watch(
)
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
open.value = false
emits('select', item)
if (!item.children?.length) open.value = false
}
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 {
@@ -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 { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { UiButton } from '@/components/ui/button'
import { UiInput } from '@/components/ui/input'
export default defineComponent({
setup() {
@@ -48,20 +50,28 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-30 w-80 py-2 px-4 text-#e9c556">
<div class="flex justify-around items-center">
助手社区ID
<calc-input class="!w-60" v-model={uid.value} />
<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)]">
<label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
<span>助手社区ID</span>
<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>
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p>
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={isLoading.value}>
<div class="flex items-center justify-center gap-3">
<UiButton onClick={() => execute()} disabled={isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</UiButton>
<UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div>
</div>
</>
@@ -6,6 +6,7 @@ import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { type IHelperRoleInfo } from '@/api/info/type'
import { UiButton } from '@/components/ui/button'
export default defineComponent({
props: {
@@ -54,22 +55,30 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-45 w-80 py-2 px-4 text-#e9c556">
<div class="flex justify-around items-center flex-col gap-1">
<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 flex-col items-center justify-center gap-1 text-center">
<div>检测到以下信息,是否导入助手数据?</div>
<div class="text-#DED29A">{props.alter?.roleName}</div>
<div class="text-#908150">{props.alter?.roleJob}</div>
<div class="text-#3ea74e">名望:{props.alter?.fame}</div>
<div class="text-[color:var(--ui-primary-foreground)]">{props.alter?.roleName}</div>
<div class="text-[color:var(--ui-muted-foreground)]">{props.alter?.roleJob}</div>
<div class="text-[color:var(--ui-success)]">名望:{props.alter?.fame}</div>
</div>
<div>
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p>
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
<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 class="flex justify-around items-center">
<calc-button onClick={execute} disabled={isLoading.value}>
<div class="flex items-center justify-center gap-3">
<UiButton onClick={() => execute()} disabled={isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</UiButton>
<UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div>
</div>
</>
@@ -5,6 +5,8 @@ import api from '@/api'
import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { UiButton } from '@/components/ui/button'
import { UiInput } from '@/components/ui/input'
export default defineComponent({
setup() {
@@ -44,23 +46,28 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-35 w-80 py-2 px-4 text-#e9c556">
<div class="flex justify-around items-center">
技能加点代码
<calc-input class="!w-60" v-model={skillCode.value} />
</div>
<div>
<p class="text-#e9c556/80">
<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)]">
<label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
<span>技能加点代码</span>
<UiInput class="min-w-0" v-model={skillCode.value} />
</label>
<div class="leading-5">
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
点击导入后会根据规则解析出技能加点、技能进化、技能强化
</p>
<p class="text-#e9c556/80">* 部分技能可能存在无法识别的情况,需要手动调整</p>
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分技能可能存在无法识别的情况,需要手动调整
</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={!skillCode.value || isLoading.value}>
<div class="flex items-center justify-center gap-3">
<UiButton onClick={() => execute()} disabled={!skillCode.value || isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</UiButton>
<UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</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>
<div class="flex flex-col gap-2 w-250px p-2">
<div v-for="option in info.options" :key="option.id" class="flex items-center gap-2">
<div class="w-60px text-end">{{ option.name }}</div>
<calc-select
class="flex-1 !h-20px"
:appendTo="dialogRef"
v-model="config.config.options[option.id.toString()]"
>
<calc-option v-for="(item, index) in option.options" :key="index" :value="index">
{{ item }}
</calc-option>
</calc-select>
</div>
</div>
<form class="equipment-options" @submit.prevent>
<label v-for="option in infoStore.options" :key="option.id" class="equipment-options__row">
<span>{{ option.name }}</span>
<SelectRoot v-model="configStore.config.options[option.id.toString()]">
<UiSelectTrigger :aria-label="option.name">
<SelectValue placeholder="请选择" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="(item, index) in option.options" :key="index" :value="index">
{{ item }}
</UiSelectItem>
</UiSelectContent>
</SelectRoot>
</label>
</form>
</template>
<script lang="ts" setup>
import { useConfigStore, useInfoStore } from '@/stores'
const dialogRef = inject('dialogRef', ref(null))
const config = useConfigStore()
const info = useInfoStore()
</script>
<style scoped>
.equipment-options {
display: grid;
width: 100%;
min-width: 0;
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,
},
{
name: `${curSuit} 套装`,
name: curSuit ? `${curSuit} 套装` : '套装',
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))
return [
{
name: `${curSuit} 套装`,
name: curSuit ? `${curSuit} 套装` : '套装',
equs: suits,
},
{
@@ -315,11 +315,11 @@ export const formatOath = (
}
return [
{
name: `${curSuit?.name} 誓约`,
name: curSuit?.name ? `${curSuit.name} 誓约` : '誓约',
equs: oath,
},
{
name: `${curSuit?.name} 星蕴石`,
name: curSuit?.name ? `${curSuit.name} 星蕴石` : '星蕴石',
equs: primer,
},
]

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