dev
This commit is contained in:
@@ -2,6 +2,28 @@
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Migration Status (frontend -> frontendNew)
|
||||
|
||||
- UI library baseline: shadcn-vue + Tailwind v4
|
||||
- Routing migrated:
|
||||
- `/home`
|
||||
- `/dcalc/dnfhelper`
|
||||
- `/character/:alter/(equipment|skill|forge)`
|
||||
- Migrated pages:
|
||||
- `src/pages/Home/Index.vue`
|
||||
- `src/pages/Dcalc/DnfHelper/*`
|
||||
- `src/pages/Character/Index.vue`
|
||||
- `src/pages/Character/EquipmentDictionary/Index.vue`
|
||||
- `src/pages/Character/Skill/index.vue`
|
||||
- `src/pages/Character/Forge/index.vue`
|
||||
|
||||
## Next Migration Steps
|
||||
|
||||
- Migrate `frontend/src/pages/Character/Equipment/components/*` into shadcn-vue filter + picker panels.
|
||||
- Migrate `frontend/src/pages/Character/Result/*` with Card/Item-based result layout and baseline compare blocks.
|
||||
- Migrate `frontend/src/pages/Character/Config/import*.vue` into dialog/sheet flows in `frontendNew`.
|
||||
- Replace placeholder summary blocks in Forge with interactive editors for avatar, emblem, jade, oath.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DNF Tools</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"pako": "^2.1.0",
|
||||
"pinia": "^3.0.4",
|
||||
"reka-ui": "^2.9.6",
|
||||
"shadcn-vue": "^2.6.2",
|
||||
@@ -34,6 +35,7 @@
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/pako": "^2.0.4",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
|
||||
Generated
+16
-30
@@ -26,6 +26,9 @@ importers:
|
||||
lucide-vue-next:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(vue@3.5.33(typescript@6.0.3))
|
||||
pako:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
||||
@@ -60,6 +63,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^24.12.2
|
||||
version: 24.12.2
|
||||
'@types/pako':
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.4))(vue@3.5.33(typescript@6.0.3))
|
||||
@@ -474,56 +480,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.45.0':
|
||||
resolution: {integrity: sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.45.0':
|
||||
resolution: {integrity: sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.45.0':
|
||||
resolution: {integrity: sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.45.0':
|
||||
resolution: {integrity: sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.45.0':
|
||||
resolution: {integrity: sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg==}
|
||||
@@ -596,56 +594,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.60.0':
|
||||
resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==}
|
||||
@@ -709,42 +699,36 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
|
||||
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
|
||||
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
|
||||
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
|
||||
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
|
||||
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
|
||||
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
|
||||
@@ -819,28 +803,24 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.4':
|
||||
resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.4':
|
||||
resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.4':
|
||||
resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.4':
|
||||
resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
|
||||
@@ -904,6 +884,9 @@ packages:
|
||||
'@types/node@24.12.2':
|
||||
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
|
||||
|
||||
'@types/pako@2.0.4':
|
||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||
|
||||
'@types/web-bluetooth@0.0.21':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
|
||||
@@ -2051,28 +2034,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
@@ -2336,6 +2315,9 @@ packages:
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -3702,6 +3684,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/pako@2.0.4': {}
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)':
|
||||
@@ -5188,6 +5172,8 @@ snapshots:
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
Regular → Executable
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AxiosInstance, AxiosResponse } from "axios";
|
||||
import axios from "axios";
|
||||
import { h } from "vue";
|
||||
// import { gzip, ungzip } from 'pako'
|
||||
import { ungzip } from "pako";
|
||||
import { useInfoStore } from "@/stores";
|
||||
// import { useDialog } from '@/components/hooks/dialog'
|
||||
// import { useAppStore, useConfigStore } from '@/store'
|
||||
|
||||
@@ -33,6 +33,7 @@ export default defineRequest((request) => {
|
||||
},
|
||||
async getHelperList(id: string) {
|
||||
const res = await request.get<IHelperInfo>(`/dnfhelper/${id}/list`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { computed } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const navs = [
|
||||
{ name: "首页", path: "/dcalc/home" },
|
||||
{ name: "助手导入", path: "/dcalc/dnfhelper" },
|
||||
{ name: "装备词典", path: "/dcalc/character/swordman_male/equipment" },
|
||||
{ name: "技能面板", path: "/dcalc/character/swordman_male/skill" },
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const activePath = computed(() => {
|
||||
// 只匹配主路径
|
||||
const found = navs.find((n) => route.path.startsWith(n.path));
|
||||
return found ? found.path : "";
|
||||
});
|
||||
|
||||
function go(path: string) {
|
||||
if (route.path !== path) router.push(path);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="w-full flex items-center gap-2 px-4 py-2 border-b bg-background/80 sticky top-0 z-30">
|
||||
<template v-for="nav in navs" :key="nav.path">
|
||||
<Button
|
||||
variant="ghost"
|
||||
:class="activePath === nav.path ? 'text-primary font-bold' : 'text-muted-foreground'"
|
||||
@click="go(nav.path)"
|
||||
>
|
||||
{{ nav.name }}
|
||||
</Button>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,6 @@ import type { SidebarProps } from "@/components/ui/sidebar";
|
||||
import {
|
||||
BookOpen,
|
||||
Bot,
|
||||
Command,
|
||||
// Frame,
|
||||
LifeBuoy,
|
||||
// Map,
|
||||
@@ -58,78 +57,19 @@ const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: "计算器",
|
||||
url: "#",
|
||||
url: "/dcalc/dnfhelper",
|
||||
icon: Bot,
|
||||
isActive: true,
|
||||
onClick: () => {
|
||||
console.log("Playground clicked");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "名望数据",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Genesis",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Explorer",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Quantum",
|
||||
// url: "#",
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
{
|
||||
title: "职业数据",
|
||||
url: "#",
|
||||
title: "技能数据",
|
||||
url: "/dcalc/character/swordman_male/skill",
|
||||
icon: BookOpen,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Introduction",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Get Started",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Tutorials",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Changelog",
|
||||
// url: "#",
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
// {
|
||||
// title: "Settings",
|
||||
// url: "#",
|
||||
// icon: Settings2,
|
||||
// items: [
|
||||
// {
|
||||
// title: "General",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Team",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Billing",
|
||||
// url: "#",
|
||||
// },
|
||||
// {
|
||||
// title: "Limits",
|
||||
// url: "#",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
title: "装备词典",
|
||||
url: "/dcalc/character/swordman_male/equipment",
|
||||
icon: SquareTerminal,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
@@ -171,11 +111,7 @@ const data = {
|
||||
<div class="flex items-center gap-2 group-data-[collapsible=icon]:justify-center">
|
||||
<SidebarMenuButton size="lg" as-child class="group-data-[collapsible=icon]:hidden">
|
||||
<a href="#">
|
||||
<div
|
||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||
>
|
||||
<Command class="size-4" />
|
||||
</div>
|
||||
<img src="/favicon.ico" alt="DNF Tools" class="size-10 rounded-sm object-cover" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-medium">DNF Tools</span>
|
||||
<span class="truncate text-xs">DNF工具集</span>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import Breadcrumb from "@/components/ui/breadcrumb/Breadcrumb.vue";
|
||||
import BreadcrumbList from "@/components/ui/breadcrumb/BreadcrumbList.vue";
|
||||
import BreadcrumbItem from "@/components/ui/breadcrumb/BreadcrumbItem.vue";
|
||||
import BreadcrumbLink from "@/components/ui/breadcrumb/BreadcrumbLink.vue";
|
||||
import BreadcrumbSeparator from "@/components/ui/breadcrumb/BreadcrumbSeparator.vue";
|
||||
import BreadcrumbEllipsis from "@/components/ui/breadcrumb/BreadcrumbEllipsis.vue";
|
||||
import BreadcrumbPage from "@/components/ui/breadcrumb/BreadcrumbPage.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
type PathCrumb = { label: string; to: string };
|
||||
type EllipsisCrumb = { type: "ellipsis" };
|
||||
type DisplayCrumb = PathCrumb | EllipsisCrumb;
|
||||
|
||||
const routeTrail = ref<PathCrumb[]>([]);
|
||||
|
||||
function labelFromPath(path: string): string {
|
||||
if (path === "/dcalc" || path === "/dcalc/home") return "首页";
|
||||
if (path.startsWith("/dcalc/dnfhelper")) return "助手导入";
|
||||
if (path.endsWith("/equipment")) return "装备词典";
|
||||
if (path.endsWith("/skill")) return "技能面板";
|
||||
if (path.endsWith("/forge")) return "打造";
|
||||
if (path.includes("/character/")) {
|
||||
const alter = path.split("/")[3];
|
||||
return alter ? `角色 ${alter}` : "角色";
|
||||
}
|
||||
return "页面";
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(fullPath) => {
|
||||
if (!fullPath.startsWith("/dcalc")) {
|
||||
routeTrail.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const existedIndex = routeTrail.value.findIndex((item) => item.to === fullPath);
|
||||
if (existedIndex >= 0) {
|
||||
routeTrail.value = routeTrail.value.slice(0, existedIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
routeTrail.value = [
|
||||
...routeTrail.value,
|
||||
{
|
||||
label: labelFromPath(fullPath),
|
||||
to: fullPath,
|
||||
},
|
||||
];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const crumbs = computed(() => routeTrail.value);
|
||||
|
||||
const maxCrumbs = 4; // 超过则折叠
|
||||
const showEllipsis = computed(() => crumbs.value.length > maxCrumbs);
|
||||
const visibleCrumbs = computed(() => {
|
||||
if (!showEllipsis.value) return crumbs.value;
|
||||
// 只显示第一个、...、最后两个
|
||||
return [crumbs.value[0], { type: "ellipsis" }, ...crumbs.value.slice(-2)] as DisplayCrumb[];
|
||||
});
|
||||
|
||||
function go(to: string) {
|
||||
if (to && typeof to === "string" && to !== route.fullPath) router.push(to);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
v-if="crumbs.length"
|
||||
class="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-6 py-3 flex items-center min-h-[56px]">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<template
|
||||
v-for="(crumb, idx) in visibleCrumbs"
|
||||
:key="'type' in crumb ? 'ellipsis' + idx : crumb.to"
|
||||
>
|
||||
<BreadcrumbSeparator v-if="idx !== 0" />
|
||||
<template v-if="'type' in crumb">
|
||||
<BreadcrumbEllipsis />
|
||||
</template>
|
||||
<template v-else>
|
||||
<BreadcrumbItem>
|
||||
<template v-if="idx !== visibleCrumbs.length - 1">
|
||||
<BreadcrumbLink
|
||||
as="a"
|
||||
@click.prevent="go(crumb.to)"
|
||||
class="cursor-pointer font-normal text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||
>{{ crumb.label }}</BreadcrumbLink
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<BreadcrumbPage class="font-semibold text-gray-900 dark:text-gray-100">{{
|
||||
crumb.label
|
||||
}}</BreadcrumbPage>
|
||||
</template>
|
||||
</BreadcrumbItem>
|
||||
</template>
|
||||
</template>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { LucideIcon } from "lucide-vue-next";
|
||||
import { ChevronRight } from "lucide-vue-next";
|
||||
import { RouterLink } from "vue-router";
|
||||
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
||||
@@ -30,6 +31,8 @@ defineProps<{
|
||||
}[];
|
||||
}[];
|
||||
}>();
|
||||
|
||||
const isExternal = (url: string) => /^https?:\/\//.test(url);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,10 +42,20 @@ defineProps<{
|
||||
<Collapsible v-for="item in items" :key="item.title" as-child :default-open="item.isActive">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton as-child :tooltip="item.title">
|
||||
<a :href="item.url" @click="item.onClick">
|
||||
<a
|
||||
v-if="isExternal(item.url)"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click="item.onClick"
|
||||
>
|
||||
<component :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
</a>
|
||||
<RouterLink v-else :to="item.url" @click="item.onClick">
|
||||
<component :is="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
</RouterLink>
|
||||
</SidebarMenuButton>
|
||||
<template v-if="item.items?.length">
|
||||
<CollapsibleTrigger as-child>
|
||||
@@ -55,9 +68,18 @@ defineProps<{
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
||||
<SidebarMenuSubButton as-child>
|
||||
<a :href="subItem.url" @click="subItem.onClick">
|
||||
<a
|
||||
v-if="isExternal(subItem.url)"
|
||||
:href="subItem.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click="subItem.onClick"
|
||||
>
|
||||
<span>{{ subItem.title }}</span>
|
||||
</a>
|
||||
<RouterLink v-else :to="subItem.url" @click="subItem.onClick">
|
||||
<span>{{ subItem.title }}</span>
|
||||
</RouterLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
|
||||
@@ -19,7 +19,7 @@ const props = withDefaults(
|
||||
:data-size="size"
|
||||
:class="
|
||||
cn(
|
||||
'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col transition-all duration-200 hover:shadow-lg hover:ring-foreground/20',
|
||||
'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col transition-all duration-200',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
export const tabsContextKey = Symbol("tabs") as InjectionKey<TabsContextValue>;
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide } from "vue";
|
||||
import type { InjectionKey, Ref } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TabsContextValue {
|
||||
modelValue: Ref<string>;
|
||||
setValue: (value: string) => void;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<string>({ required: true });
|
||||
|
||||
const setValue = (value: string) => {
|
||||
modelValue.value = value;
|
||||
};
|
||||
|
||||
provide(tabsContextKey, {
|
||||
modelValue,
|
||||
setValue,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="tabs" :class="cn('flex flex-col gap-3', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="tabs-list"
|
||||
:class="
|
||||
cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center rounded-lg p-1',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { tabsContextKey } from "./Tabs.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
}>();
|
||||
|
||||
const tabs = inject(tabsContextKey);
|
||||
|
||||
if (!tabs) {
|
||||
throw new Error("TabsTrigger must be used within Tabs");
|
||||
}
|
||||
|
||||
const isActive = computed(() => tabs.modelValue.value === props.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="tabs-trigger"
|
||||
:disabled="disabled"
|
||||
:data-state="isActive ? 'active' : 'inactive'"
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex h-8 items-center justify-center rounded-md px-3 text-sm font-medium whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50',
|
||||
'data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
'hover:text-foreground',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
@click="tabs.setValue(value)"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
@@ -3,9 +3,9 @@ import { pinia } from "@/stores";
|
||||
import { loadingPlugin } from "@/directives/loading";
|
||||
|
||||
import router from "./router";
|
||||
import App from "@/pages/index.vue";
|
||||
import App from "@/App.vue";
|
||||
import "./style.css";
|
||||
|
||||
//
|
||||
const appContainer = document.createElement("div");
|
||||
appContainer.setAttribute("style", "text-rendering: geometricPrecision;letter-spacing: -0.1px;");
|
||||
document.body.appendChild(appContainer);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+77
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { IEquipment } from "@/api/info/type";
|
||||
import { getImageURL } from "@/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
equipment?: IEquipment;
|
||||
inactive?: boolean;
|
||||
selected?: boolean;
|
||||
useDisplayUrl?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
click: [];
|
||||
preview: [];
|
||||
}>();
|
||||
|
||||
const rarityClass = computed(() => {
|
||||
switch (props.equipment?.rarity) {
|
||||
case "稀有":
|
||||
return "border-violet-400";
|
||||
case "神器":
|
||||
return "border-fuchsia-500";
|
||||
case "传说":
|
||||
return "border-orange-500";
|
||||
case "史诗":
|
||||
return "border-amber-400";
|
||||
case "太初":
|
||||
return "border-emerald-400";
|
||||
default:
|
||||
return "border-muted";
|
||||
}
|
||||
});
|
||||
|
||||
const imageUrl = computed(() => {
|
||||
if (!props.equipment) return "";
|
||||
return getImageURL(
|
||||
props.useDisplayUrl
|
||||
? props.equipment.displayUrl || props.equipment.imageUrl
|
||||
: props.equipment.imageUrl,
|
||||
);
|
||||
});
|
||||
|
||||
const cellStyle = {
|
||||
width: "var(--equip-cell-size, 1.75rem)",
|
||||
height: "var(--equip-cell-size, 1.75rem)",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-if="equipment"
|
||||
type="button"
|
||||
:class="
|
||||
cn(
|
||||
'relative shrink-0 overflow-hidden rounded-sm border bg-background p-0 leading-none transition-colors',
|
||||
rarityClass,
|
||||
selected
|
||||
? 'after:pointer-events-none after:absolute after:inset-0 after:rounded-[3px] after:shadow-[inset_0_0_0_2px_hsl(var(--primary)),0_0_0_1px_hsl(var(--background))]'
|
||||
: 'hover:border-primary/60',
|
||||
inactive ? 'opacity-60' : '',
|
||||
)
|
||||
"
|
||||
:style="cellStyle"
|
||||
@click="$emit('click')"
|
||||
@mouseenter="$emit('preview')"
|
||||
>
|
||||
<img :src="imageUrl" :alt="equipment.name" class="size-full object-contain" />
|
||||
<div v-if="inactive" class="absolute inset-0 bg-black/45" />
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="shrink-0 rounded-sm border border-dashed border-muted-foreground/30 bg-muted/15"
|
||||
:style="cellStyle"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { Hammer, ShieldCheck, Sparkles } from "lucide-vue-next";
|
||||
import { useConfigStore, useInfoStore } from "@/stores";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const infoStore = useInfoStore();
|
||||
|
||||
const summary = computed(() => {
|
||||
const equipCount = Object.values(configStore.config.equips || {}).filter(
|
||||
(item) => !!item.id,
|
||||
).length;
|
||||
const avatarCount = Object.values(configStore.config.avatar || {}).filter(
|
||||
(item) => !!item.id,
|
||||
).length;
|
||||
const oathCount = Object.values(configStore.config.oaths || {}).filter(
|
||||
(item) => !!item.id,
|
||||
).length;
|
||||
|
||||
return {
|
||||
equipCount,
|
||||
avatarCount,
|
||||
oathCount,
|
||||
emblemSlots: infoStore.emblemParts.length,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Hammer class="size-4" />
|
||||
装备打造
|
||||
</CardTitle>
|
||||
<CardDescription>已迁移基础统计,后续补齐增幅/强化/锻造编辑器。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2 text-sm text-muted-foreground">
|
||||
<div>已配置装备: {{ summary.equipCount }}</div>
|
||||
<div>誓约槽位: {{ summary.oathCount }}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Sparkles class="size-4" />
|
||||
时装与徽章
|
||||
</CardTitle>
|
||||
<CardDescription>保留旧版数据结构,逐步替换为 shadcn-vue 控件。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2 text-sm text-muted-foreground">
|
||||
<div>已配置时装: {{ summary.avatarCount }}</div>
|
||||
<div>徽章位类型: {{ summary.emblemSlots }}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<ShieldCheck class="size-4" />
|
||||
助手导入
|
||||
</CardTitle>
|
||||
<CardDescription>沿用助手识别能力快速回填打造数据。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<router-link to="/dcalc/dnfhelper">
|
||||
<Button variant="outline">前往助手导入</Button>
|
||||
</router-link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { LoaderCircle, RefreshCcw } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import Tabs from "@/components/ui/tabs/Tabs.vue";
|
||||
import TabsList from "@/components/ui/tabs/TabsList.vue";
|
||||
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
|
||||
import { useInfoStore } from "@/stores";
|
||||
const props = defineProps<{
|
||||
alter: string;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const infoStore = useInfoStore();
|
||||
const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
|
||||
const tabs = [
|
||||
{ label: "装备", key: "equipment" },
|
||||
{ label: "技能", key: "skill" },
|
||||
{ label: "打造", key: "forge" },
|
||||
] as const;
|
||||
|
||||
const activeTab = computed(() => {
|
||||
const path = route.path;
|
||||
if (path.endsWith("/skill")) return "skill";
|
||||
if (path.endsWith("/forge")) return "forge";
|
||||
return "equipment";
|
||||
});
|
||||
|
||||
const loadCharacter = async () => {
|
||||
loading.value = true;
|
||||
loadError.value = "";
|
||||
try {
|
||||
await infoStore.createCharacter(props.alter);
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error ? error.message : "角色数据加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const switchTab = (tab: (typeof tabs)[number]["key"]) => {
|
||||
router.push(`/dcalc/character/${props.alter}/${tab}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadCharacter();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.alter,
|
||||
() => {
|
||||
loadCharacter();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-8">
|
||||
<Card class="flex-none">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center justify-between">
|
||||
<span>角色配置 · {{ alter }}</span>
|
||||
<Button variant="outline" size="sm" :disabled="loading" @click="loadCharacter">
|
||||
<RefreshCcw :class="{ 'animate-spin': loading }" />
|
||||
刷新
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
>此页面承接旧版 character 模块,逐步迁移装备/技能/打造功能。</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<Tabs :model-value="activeTab" class="gap-0">
|
||||
<TabsList>
|
||||
<TabsTrigger
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:value="tab.key"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<span v-if="loading" class="inline-flex items-center gap-2">
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
正在同步角色数据...
|
||||
</span>
|
||||
<span v-else-if="loadError">{{ loadError }}</span>
|
||||
<span v-else>数据源已就绪,可继续编辑配置。</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<router-view />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { Search } from "lucide-vue-next";
|
||||
import { useInfoStore } from "@/stores";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const infoStore = useInfoStore();
|
||||
const keyword = ref("");
|
||||
|
||||
const skills = computed(() => {
|
||||
const source = infoStore.skills ?? [];
|
||||
const text = keyword.value.trim().toLowerCase();
|
||||
if (!text) return source;
|
||||
return source.filter((skill) => {
|
||||
return skill.name.toLowerCase().includes(text) || skill.type.toLowerCase().includes(text);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>技能面板</CardTitle>
|
||||
<CardDescription>与旧版技能页等价的基础浏览能力,后续补齐 VP/符文配置。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="relative">
|
||||
<Search class="text-muted-foreground absolute left-2.5 top-2.5 size-4" />
|
||||
<Input v-model="keyword" class="pl-8" placeholder="搜索技能名 / 类型" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="skill in skills" :key="skill.id" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">{{ skill.name }}</CardTitle>
|
||||
<CardDescription>{{ skill.type }} · 最高等级 {{ skill.maxLv }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex items-center gap-3 pt-0">
|
||||
<img :src="skill.icon" :alt="skill.name" class="size-11 rounded-md object-cover" />
|
||||
<div class="text-xs text-muted-foreground">
|
||||
<div>可学等级: {{ skill.maxLearnLv }}</div>
|
||||
<div>学习等级: {{ skill.learnLv }}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div v-if="skills.length === 0" class="py-10 text-center text-sm text-muted-foreground">
|
||||
当前筛选条件没有匹配技能。
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { LoaderCircle, RefreshCcw } from "lucide-vue-next";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../../../components/ui/card";
|
||||
import { useInfoStore } from "../../../../stores";
|
||||
const props = defineProps<{
|
||||
alter: string;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const infoStore = useInfoStore();
|
||||
const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
|
||||
const tabs = [
|
||||
{ label: "装备", key: "equipment" },
|
||||
{ label: "技能", key: "skill" },
|
||||
{ label: "打造", key: "forge" },
|
||||
] as const;
|
||||
|
||||
const activeTab = computed(() => {
|
||||
const path = route.path;
|
||||
if (path.endsWith("/skill")) return "skill";
|
||||
if (path.endsWith("/forge")) return "forge";
|
||||
return "equipment";
|
||||
});
|
||||
|
||||
const loadCharacter = async () => {
|
||||
loading.value = true;
|
||||
loadError.value = "";
|
||||
try {
|
||||
await infoStore.createCharacter(props.alter);
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error ? error.message : "角色数据加载失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const switchTab = (tab: (typeof tabs)[number]["key"]) => {
|
||||
router.push(`/dcalc/character/${props.alter}/${tab}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadCharacter();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.alter,
|
||||
() => {
|
||||
loadCharacter();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center justify-between">
|
||||
<span>角色配置 · {{ alter }}</span>
|
||||
<Button variant="outline" size="sm" :disabled="loading" @click="loadCharacter">
|
||||
<RefreshCcw :class="{ 'animate-spin': loading }" />
|
||||
刷新
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
>此页面承接旧版 character 模块,逐步迁移装备/技能/打造功能。</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div class="inline-flex gap-2">
|
||||
<Button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:variant="activeTab === tab.key ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<span v-if="loading" class="inline-flex items-center gap-2">
|
||||
<LoaderCircle class="size-4 animate-spin" />
|
||||
正在同步角色数据...
|
||||
</span>
|
||||
<span v-else-if="loadError">{{ loadError }}</span>
|
||||
<span v-else>数据源已就绪,可继续编辑配置。</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<router-view />
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,10 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
import { GalleryVerticalEnd, Shuffle, Grip } from "lucide-vue-next";
|
||||
import { GalleryVerticalEnd, Shuffle } from "lucide-vue-next";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -21,56 +30,74 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const helperId = ref("");
|
||||
const helperStore = useDnfHelperStore();
|
||||
|
||||
const onSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
await useDnfHelperStore().refleshHelperInfo(helperId.value);
|
||||
await helperStore.refreshHelperInfo(helperId.value);
|
||||
};
|
||||
|
||||
const onRandom = () => {
|
||||
const seed = `${Math.floor(1000000 + Math.random() * 8999999)}`;
|
||||
helperId.value = seed;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-6', props.class)">
|
||||
<form @submit="onSubmit">
|
||||
<FieldGroup>
|
||||
<div class="flex flex-col items-center gap-2 text-center">
|
||||
<a href="#" class="flex flex-col items-center gap-2 font-medium">
|
||||
<div class="flex size-8 items-center justify-center rounded-md">
|
||||
<GalleryVerticalEnd class="size-6" />
|
||||
</div>
|
||||
<span class="sr-only">DNF计算器</span>
|
||||
</a>
|
||||
<!-- <h1 class="text-xl font-bold">输入助手ID</h1> -->
|
||||
<!-- <FieldDescription>请输入助手的社区ID </FieldDescription> -->
|
||||
</div>
|
||||
<Field class="flex flex-col items-center">
|
||||
<FieldLabel for="helperId" class="sr-only"> 助手ID </FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="helperId"
|
||||
type="number"
|
||||
placeholder="助手社区ID"
|
||||
required
|
||||
v-model="helperId"
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<Shuffle />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>Enter</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<!-- <FieldDescription class="text-center">
|
||||
Didn't receive the code? <a href="#">Resend</a>
|
||||
</FieldDescription> -->
|
||||
</Field>
|
||||
<Field>
|
||||
<Button class="self-center w-sm!" type="submit"> 确认 </Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<FieldSeparator class="w-sm self-center">没有助手ID</FieldSeparator>
|
||||
<FieldDescription class="px-6 text-center">
|
||||
<Button size="sm" variant="outline"> <Grip /> 选择职业 </Button>
|
||||
<div :class="cn('flex flex-col gap-6 self-center', props.class)">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-lg">
|
||||
<GalleryVerticalEnd class="size-5" />
|
||||
绑定助手社区ID
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
输入助手社区ID后可同步角色列表与基础打造信息。该流程与旧版导入逻辑保持一致。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form class="space-y-4" @submit="onSubmit">
|
||||
<FieldGroup>
|
||||
<Field class="flex flex-col gap-2">
|
||||
<FieldLabel for="helperId">助手ID</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="helperId"
|
||||
v-model="helperId"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
placeholder="例如: 12345678"
|
||||
required
|
||||
/>
|
||||
<InputGroupAddon as-child>
|
||||
<button type="button" class="inline-flex items-center" @click="onRandom">
|
||||
<Shuffle class="size-4" />
|
||||
</button>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>Enter</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>点击回车或确认按钮即可触发同步。</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<CardFooter class="grid grid-cols-1 gap-2 px-0 sm:grid-cols-2">
|
||||
<Button type="submit" :disabled="helperStore.loading">
|
||||
{{ helperStore.loading ? "同步中..." : "确认并同步" }}
|
||||
</Button>
|
||||
<Button variant="outline" type="button" @click="$router.push('/dcalc/home')">
|
||||
选择职业
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<FieldSeparator>提示</FieldSeparator>
|
||||
<FieldDescription class="px-1 text-sm">
|
||||
助手识别基于OCR,存在一定误差,导入后建议在“装备/打造”页复核关键词条。
|
||||
</FieldDescription>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useDnfHelperStore } from "@/stores/dnfhelper";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import Bind from "./bind.vue";
|
||||
import List from "./list.vue";
|
||||
import Processing from "./processing.vue";
|
||||
|
||||
const helperStore = useDnfHelperStore();
|
||||
|
||||
@@ -9,10 +11,17 @@ const listInfo = computed(() => helperStore.helperInfo?.roleList ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="listInfo.length == 0">
|
||||
<Bind class="self-center justify-center w-fit !max-w-sm h-full" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<List :list="listInfo" />
|
||||
</template>
|
||||
<div class="mx-auto flex h-full w-full flex-col gap-4 flex-1">
|
||||
<Card v-if="helperStore.errorMessage" class="border-destructive/50 bg-destructive/5">
|
||||
<CardContent class="py-3 text-sm text-destructive">{{
|
||||
helperStore.errorMessage
|
||||
}}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Processing v-if="helperStore.loading" />
|
||||
|
||||
<Bind v-else-if="listInfo.length === 0" class="mx-auto my-auto" />
|
||||
|
||||
<List v-else :list="listInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import Button from "@/components/ui/button/Button.vue";
|
||||
import { computed } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
// CardDescription,
|
||||
CardFooter,
|
||||
CardDescription,
|
||||
// CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useDnfHelperStore } from "@/stores/dnfhelper";
|
||||
import { RefreshCcw, Grip } from "lucide-vue-next";
|
||||
import { Grip, RefreshCcw } from "lucide-vue-next";
|
||||
|
||||
const props = defineProps<{
|
||||
list: {
|
||||
@@ -24,41 +26,58 @@ const props = defineProps<{
|
||||
const { list } = toRefs(props);
|
||||
|
||||
const helperStore = useDnfHelperStore();
|
||||
|
||||
const sortedList = computed(() => {
|
||||
return [...list.value].sort((left, right) => right.fame - left.fame);
|
||||
});
|
||||
|
||||
const formatFame = (fame: number) => {
|
||||
return new Intl.NumberFormat("zh-CN").format(fame);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="bg-background sticky top-0 flex h-16 shrink-0 items-center gap-2 border-b px-4 justify-between"
|
||||
class="sticky top-0 z-10 flex shrink-0 flex-col gap-3 border-b bg-background/95 px-1 py-2 backdrop-blur md:flex-row md:items-center md:justify-between px-6"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold">角色列表</h1>
|
||||
<p class="text-sm text-muted-foreground">共 {{ list.length }} 个角色</p>
|
||||
<p class="text-sm text-muted-foreground">共 {{ sortedList.length }} 个角色</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Button variant="outline"> <Grip /> 其他职业 </Button>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" @click="$router.push('/dcalc/home')"> <Grip /> 选择职业 </Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Submit"
|
||||
@click="helperStore.refleshHelperInfo(helperStore.helperInfo?.id)"
|
||||
:disabled="helperStore.loading"
|
||||
@click="helperStore.refreshHelperInfo(helperStore.helperInfo?.id)"
|
||||
>
|
||||
<RefreshCcw />
|
||||
<RefreshCcw :class="{ 'animate-spin': helperStore.loading }" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="secondary" @click="helperStore.logout">取消绑定</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-5 box-border">
|
||||
<template v-for="item in list" :key="item.roleId">
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 py-4 px-6 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<template v-for="item in sortedList" :key="item.roleId">
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ item.roleJob }}</CardTitle>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-base flex">
|
||||
{{ item.roleName }}
|
||||
<Button class="ml-auto" size="sm" variant="outline" @click="$router.push('/dcalc/home')"
|
||||
>导入</Button
|
||||
>
|
||||
</CardTitle>
|
||||
<CardDescription class="flex justify-between">
|
||||
<span>
|
||||
{{ item.roleJob }}
|
||||
</span>
|
||||
<span>名望: {{ formatFame(item.fame) }}</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="flex items-center justify-center h-sm">
|
||||
<img :src="item.avatarLink" alt="avatar" class="w-auto h-sm rounded-full object-cover" />
|
||||
<CardContent class="flex items-center justify-center">
|
||||
<img :src="item.avatarLink" :alt="item.roleName" class="h-40 w-auto rounded" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-between">
|
||||
<span>{{ item.roleName }}</span>
|
||||
<span>{{ item.fame }} </span>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { ArrowRight, Search } from "lucide-vue-next";
|
||||
import api from "@/api";
|
||||
import type { IAdventureInfo, IAlterInfo } from "@/api/info/type";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
// CardDescription,
|
||||
// CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getImageURL } from "@/utils";
|
||||
const router = useRouter();
|
||||
const adventures = ref<IAdventureInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const helperId = ref("");
|
||||
|
||||
const loadAdventures = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
adventures.value = await api.adventures();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openAlter = (alter: IAlterInfo) => {
|
||||
if (alter.comment === "首页") {
|
||||
window.open("https://dnftools.com", "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
if (alter.name === "sponsor") {
|
||||
if (alter.url) {
|
||||
window.open(alter.url, "_blank", "noopener");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!alter.open) {
|
||||
return;
|
||||
}
|
||||
const target = alter.value || alter.name;
|
||||
router.push(`/dcalc/character/${target}/equipment`);
|
||||
};
|
||||
|
||||
const gotoHelper = () => {
|
||||
if (!helperId.value.trim()) {
|
||||
router.push("/dcalc/dnfhelper");
|
||||
return;
|
||||
}
|
||||
router.push("/dcalc/dnfhelper");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadAdventures();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-8">
|
||||
<Card class="h-auto flex-none">
|
||||
<CardContent class="flex min-h-14 flex-col gap-3 justify-center items-center">
|
||||
<img src="/favicon.ico" alt="DNF Tools" class="size-30 rounded-sm object-cover" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
点击下方职业卡片进入配置,或使用助手导入快速同步角色。
|
||||
</div>
|
||||
|
||||
<Button type="submit" variant="outline" @click="gotoHelper">
|
||||
<Search />
|
||||
助手导入
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div v-if="loading" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="idx in 6" :key="idx">
|
||||
<CardHeader>
|
||||
<Skeleton class="h-5 w-40" />
|
||||
<Skeleton class="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Skeleton class="h-8 w-full" v-for="row in 4" :key="row" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<Card v-for="adventure in adventures" :key="adventure.name">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ adventure.title }}</CardTitle>
|
||||
<!-- <CardDescription>{{ adventure.children.length }} 个分支职业</CardDescription> -->
|
||||
</CardHeader>
|
||||
<CardContent class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
v-for="child in adventure.children.filter((item) => item.title)"
|
||||
:key="child.name"
|
||||
variant="outline"
|
||||
class="justify-between p-1.5 !h-auto"
|
||||
:disabled="!child.open"
|
||||
@click="openAlter(child)"
|
||||
>
|
||||
<item class="flex items-center gap-2">
|
||||
<img
|
||||
:src="
|
||||
getImageURL(`characters/${adventure.name}/${child.name}/material/Seicing_3.png`)
|
||||
"
|
||||
:alt="child.title"
|
||||
class="size-8 rounded-sm object-cover"
|
||||
/>
|
||||
<span class="truncate">{{ child.title }}</span>
|
||||
</item>
|
||||
<ArrowRight class="size-3" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
<!-- <CardFooter class="text-xs text-muted-foreground">
|
||||
{{ adventure.name }}
|
||||
</CardFooter> -->
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-xs text-muted-foreground">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">
|
||||
赣ICP备2023003492号
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -1,16 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import AppSidebar from "@/components/AppSidebar.vue";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
// import DcalcBreadcrumb from "@/components/DcalcBreadcrumb.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarProvider>
|
||||
<div class="flex w-full">
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<router-view />
|
||||
<!-- router-view -->
|
||||
</SidebarInset>
|
||||
<div class="flex h-svh w-full overflow-hidden">
|
||||
<div class="flex w-full min-h-0">
|
||||
<AppSidebar />
|
||||
<SidebarInset class="flex min-w-0 flex-1 min-h-0 flex-col overflow-hidden">
|
||||
<!-- <DcalcBreadcrumb /> -->
|
||||
<div class="flex items-center border-b px-2 py-2 md:hidden">
|
||||
<SidebarTrigger class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="flex flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<router-view />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
@@ -5,20 +5,61 @@ const router = createRouter({
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("@/pages/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/dcalc",
|
||||
name: "dcalc",
|
||||
component: () => import("@/pages/index.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "/dcalc/dnfhelper",
|
||||
path: "",
|
||||
redirect: "/dcalc/home",
|
||||
},
|
||||
{
|
||||
path: "home",
|
||||
name: "home",
|
||||
component: () => import("@/pages/Dcalc/Home/Index.vue"),
|
||||
},
|
||||
{
|
||||
path: "dnfhelper",
|
||||
name: "dnfhelper",
|
||||
component: () => import("@/pages/Dcalc/DnfHelper/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "character/:alter",
|
||||
name: "character",
|
||||
props: true,
|
||||
component: () => import("@/pages/Dcalc/Character/Index.vue"),
|
||||
redirect: (to) => {
|
||||
return {
|
||||
name: "character-equipment",
|
||||
params: { alter: to.params.alter },
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "equipment",
|
||||
name: "character-equipment",
|
||||
component: () => import("@/pages/Dcalc/Character/EquipmentDictionary/Index.vue"),
|
||||
},
|
||||
{
|
||||
path: "skill",
|
||||
name: "character-skill",
|
||||
component: () => import("@/pages/Dcalc/Character/Skill/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "forge",
|
||||
name: "character-forge",
|
||||
component: () => import("@/pages/Dcalc/Character/Forge/index.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
redirect: "/dcalc/home",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -10,22 +10,42 @@ export const useDnfHelperStore = defineStore("dnfHelperStore", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
const refleshHelperInfo = async (uid?: string) => {
|
||||
if (!uid) return;
|
||||
const data = await api.getHelperList(uid);
|
||||
if (data) {
|
||||
helperInfo.value = data;
|
||||
console.log(helperInfo.value);
|
||||
loading.value = true;
|
||||
errorMessage.value = "";
|
||||
try {
|
||||
const data = await api.getHelperList(uid);
|
||||
if (data) {
|
||||
helperInfo.value = data;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : "同步助手数据失败";
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshHelperInfo = async (uid?: string) => {
|
||||
await refleshHelperInfo(uid);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
helperInfo.value = null;
|
||||
errorMessage.value = "";
|
||||
};
|
||||
|
||||
return {
|
||||
helperInfo,
|
||||
loading,
|
||||
errorMessage,
|
||||
refleshHelperInfo,
|
||||
refreshHelperInfo,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
let baseURL = "https://image.dnftools.com";
|
||||
|
||||
// 避免远程开发的时候冲突
|
||||
if (import.meta.env.MODE == "development") {
|
||||
baseURL = "http://127.0.0.1:3000";
|
||||
}
|
||||
|
||||
if (import.meta.env.MODE == "web" || import.meta.env.MODE == "show") {
|
||||
baseURL = "https://image.dnftools.com";
|
||||
}
|
||||
|
||||
export const getImageURL = (image: string) => {
|
||||
image = image.startsWith("/") ? image : `/${image}`;
|
||||
return `${baseURL}${image}`;
|
||||
};
|
||||
|
||||
export const getLocalImageURL = (url: string) => {
|
||||
return new URL(url, import.meta.url).href;
|
||||
};
|
||||
Reference in New Issue
Block a user