This commit is contained in:
2026-08-04 17:57:01 +08:00
parent 768fd4a3a9
commit f77034edd7
641 changed files with 26137 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"semi": false,
"singleQuote": true,
"ignorePatterns": ["dist", "src/types/auto-imports.d.ts", "components.d.ts"]
}
+49
View File
@@ -0,0 +1,49 @@
# DCalc frontend-ui
`frontend-ui` is the progressive UI refactor of the current `frontend` application. The original
project remains available as the behavior baseline.
## Foundation
- Vue 3, Vue Router, Pinia, Vite and TypeScript track the current stable release line.
- Reka UI provides accessible interaction primitives.
- Tailwind CSS 4 provides layout utilities without Preflight during the migration period.
- CSS variables define the stable semantic contract between components and themes.
- SCSS/CSS remains available for game textures, image borders and state effects.
- `class-variance-authority` defines component variants in the local, source-owned UI layer.
- Oxc (`oxlint` and `oxfmt`) handles linting and formatting.
## Theme model
Components consume `--ui-*` semantic variables. Each skin maps its own palette and effects to that
contract under `[data-theme]`. Add a theme stylesheet and register it in `useTheme.ts`; component
code should not need to change.
The initial themes are:
- `dcalc`: tokens captured from `design/src-web`.
- `midnight`: a second skin used to verify that components are not coupled to the classic palette.
## Commands
```sh
pnpm install
pnpm dev
pnpm check
pnpm build
```
Open `/components` for the complete catalog of currently introduced components and their live
interactions. `/ui-preview` remains the smaller foundation smoke-test page.
See [`docs/ui-migration-standard.md`](docs/ui-migration-standard.md) for the reusable component
migration, theming, responsive implementation and browser QA standard.
The current catalog covers Button, Badge, Panel, Input, NumberField, Checkbox, RadioGroup, Switch,
Rating, Select, Slider, Progress, Tabs, ItemSlot, DataTable, Tooltip, DropdownMenu and Dialog.
## Migration rule
New UI components live under `src/components/ui`. Route changes, keyboard shortcuts and DNF data
mapping belong to pages, composables or domain components instead of generic primitives. Existing
`calc-*` components remain only until their business call sites are migrated.
+56
View File
@@ -0,0 +1,56 @@
# Component fidelity QA
- Source visual truth: `/Users/xigua/.codex/visualizations/2026/08/04/019fcbb3-69c4-7363-81ba-fa53cab1424e/design-board-2-1920.png`
- Implementation: `/Users/xigua/.codex/visualizations/2026/08/04/019fcbb3-69c4-7363-81ba-fa53cab1424e/catalog-fidelity-audit-final-1920.png`
- Viewport: 1920 × 1200 CSS px
- Source pixels: 1920 × 1200
- Implementation pixels: 1920 × 1200
- Density normalization: devicePixelRatio 1; no resampling required
- State: default catalog with Button state matrix; interactive Dialog state captured separately at `catalog-audit-dialog-animated.png`
## Full-view comparison
The implementation keeps the source board's full-width dark surface, 46 px panel headings, brass dividers, DCalc typography, square corners, compact 30 px controls, and dense state matrices. The catalog intentionally adds a component index, theme controls, and implementation-specific copy; these are catalog navigation rather than component anatomy changes.
## Focused comparisons
- Button: three five-column state rows now match the source's Rest, Hover, Pressed, Disabled, and tone-family organization. Panel height is 281.5 px versus the 292 px source module target; the difference is the catalog's smaller bottom spacing and is visually non-material.
- NumberField: 30 px field, 23 px spinner, visible up/down arrows, and two 14 px `+10`/`-10` fast-step buttons match the source anatomy.
- Tabs: 40 px list, 30 px triggers, glow layers, divider line, animated 39 × 5 px arrow marker, and line-marker variant are present. Marker movement follows Reka selection state and respects reduced motion.
- Dialog: the titlebar uses the exact 45 raster frames from `windowheadereffect.img`, advances every 66 ms while focused, and hides the effect in the unfocused/reduced-motion state.
- Responsive: document overflow is absent at 1920, 1280, 768, and 390 widths. At 390 px, DataTable keeps a 328 px viewport with an intentional 560 px internal scroll surface.
## Required fidelity surfaces
- Fonts and typography: DCalc/SimSun control typography and Segoe UI/Chinese fallbacks retain the source hierarchy and compact line-height.
- Spacing and layout rhythm: page gutters use `clamp()`, panels use container-relative padding, grids collapse at 1280/900/520 breakpoints, and atomic game-control sizes stay pixel-aligned.
- Colors and tokens: blue action surfaces, brass edges, gold selection, blue focus, disabled gray, semantic status colors, and rarity colors map to DCalc CSS variables.
- Image quality and assets: stars, rarity frames, item icons, select arrow, dialog close icon, and all 45 title-effect frames use original raster assets with zero broken images.
- Copy and content: catalog copy differs only where it documents the Reka implementation or exposes live state; component labels and source examples remain representative.
## Interaction verification
- NumberField fast step: 1 → 11.
- Tabs: line marker moved to `来源`; selected state became active.
- Checkbox: unchecked → checked.
- Switch: checked → unchecked.
- Rating: 1 → 3.
- Select: 装备 → 材料.
- DropdownMenu: `导出结果` updated live status.
- Dialog: title frame changed from `4.PNG` to `5.PNG`; cancel restored focus to `打开 WindowDialog`.
## Comparison history
- P1: NumberField step affordances were too small and FastStep was missing. Fixed with source-sized arrows and `+10`/`-10` controls.
- P1: Tabs used a trigger-local pseudo marker and could not reproduce source movement. Fixed with a list-owned marker driven by active Reka tab geometry.
- P1: Dialog titlebar had a static gradient only. Fixed with the exact focused 45-frame animation.
- P2: Button showcase omitted source state rows. Fixed with the complete three-row state matrix.
- P2: shadcn-like radii, generic focus rings, menu surface, and tooltip transitions drifted from DNF styling. Replaced with source-specific square surfaces and state treatments.
- P2: layout relied on fixed page spacing. Replaced page-level spacing and grids with fluid/container-responsive rules while preserving atomic raster dimensions.
## Remaining intentional deviations
- The catalog header includes theme switching and a component index that are not present on Design board 2.
- Components not yet migrated (CommandInput, TreeSelect, Cascader, navigation, ScrollArea, and complex product dialogs) are out of this comparison and must be covered in later migration batches.
final result: passed
+257
View File
@@ -0,0 +1,257 @@
# DCalc UI 统一迁移与实现标准
本文档用于在其他 Vue 项目中复用当前 `frontend-ui` 的组件库迁移方法。目标不是机械复制页面,而是在保持业务功能的前提下,统一交互底座、视觉语言、主题能力、响应式策略和验收流程。
## 1. 标准目标
- 以现有业务实现为功能基线,允许调整业务代码以适配新的组件组合方式。
- 以 `design` 的组件结构、状态、动效、素材和交互为视觉事实来源。
- 使用 Reka UI 承担无障碍、键盘、焦点、受控状态和 Portal 等交互语义。
- 使用 Tailwind CSS 处理通用布局,关闭 Preflight,避免迁移期全局样式突变。
- 使用 CSS Variables 建立 DCalc Token 和语义 Token,组件只消费语义 Token。
- 使用 CSS/SCSS 实现游戏质感、像素素材、复杂渐变、边框和状态效果。
- 参考 shadcn-vue 的目录、组合、表单和 variants 规范,不要求引入整套默认视觉。
- 使用 Oxc 统一格式化和静态检查。
## 2. 事实来源与优先级
发生冲突时按以下顺序判断:
1. 业务功能、数据边界和用户可完成的操作不能丢失。
2. `design` 中的组件结构、状态矩阵、动效和真实素材决定视觉结果。
3. Reka UI 的可访问性与交互语义决定组件的基础 DOM 和状态模型。
4. 组件 API、目录和组合方式遵循本标准。
5. 仅在不影响以上约束时使用通用 Tailwind 工具类简化布局。
不要以静态截图代替业务与交互核对。设计中未展示的状态仍需覆盖 hover、pressed、focus-visible、disabled、open/closed、checked/unchecked 和 reduced-motion。
## 3. 推荐技术栈
| 层级 | 标准职责 |
| ------------------------ | ----------------------------------------------------------- |
| Vue 3 + TypeScript | 使用 Composition API、`<script setup>` 和类型化 Props/Emits |
| Vue Router + Pinia | 保留现有路由和业务状态边界 |
| Reka UI | Primitive、焦点管理、键盘交互、受控状态、Portal |
| Tailwind CSS | 栅格、Flex、尺寸约束和简单响应式布局 |
| CSS Variables | Primitive Token、Semantic Token、主题映射 |
| CSS/SCSS | 游戏纹理、分层渐变、图片边框、复杂动效 |
| class-variance-authority | Button、Badge 等离散 variants |
| clsx + tailwind-merge | 条件 class 合并,统一由 `cn()` 暴露 |
| oxlint + oxfmt | Lint、格式化和 CI 检查 |
## 4. 推荐目录
```text
src/
├── components/
│ ├── ui/ # 无业务语义的基础组件
│ │ └── button/
│ │ ├── UiButton.vue
│ │ ├── buttonVariants.ts
│ │ └── index.ts
│ ├── domain/ # 领域组合组件
│ └── hooks/ # Dialog 等跨组件编排
├── composables/
│ └── useTheme.ts
├── lib/
│ └── utils.ts # cn()
├── pages/
│ └── ComponentCatalog/ # 全组件状态与交互目录
└── style/
├── tokens/primitives.css
├── themes/dcalc.css
├── themes/<skin>.css
├── components/
├── tailwind.css
└── index.scss
```
基础组件不得直接请求接口、读取业务 Store 或绑定具体页面路由。业务行为放在页面、领域组件、Store 或 composable 中。
## 5. Token 与换肤标准
Token 分成两层:
1. Primitive Token 保存 DCalc 的原始色值、品质颜色和基础素材色。
2. Semantic Token 表达组件用途,如 `--ui-background`、`--ui-primary`、`--ui-border`、`--ui-ring`。
组件只使用 `--ui-*` 语义变量;主题文件在 `[data-theme='<name>']` 下完成映射。新增皮肤时:
1. 新建 `style/themes/<name>.css`。
2. 完整实现现有 `--ui-*` 契约。
3. 在 Tailwind 入口中导入主题文件。
4. 在 `useTheme.ts` 注册主题名。
5. 使用组件目录页逐状态验证,不修改组件内部代码。
禁止在组件中以 `data-theme` 分支写特定皮肤逻辑。确需固定的源素材颜色时,保留物理色值,并在代码注释或 QA 文档中注明它属于素材还原而非主题语义。
## 6. 单组件迁移流程
### 6.1 盘点功能
- 找出旧组件的所有调用点、Props、Emits、插槽、v-model、快捷键和禁用条件。
- 沿调用链确认业务副作用,避免只复制组件外观。
- 记录必须保持的功能和允许调整的旧 API。
### 6.2 拆解 design
- 对照组件结构、真实尺寸、边框层、渐变、素材、文本和间距。
- 建立完整状态矩阵:rest、hover、pressed、focus、disabled、selected/open。
- 动效需记录帧数、间隔、缓动、焦点条件和 reduced-motion 行为。
- 优先复用原始 PNG、字体、图标和 rarity frame,不用近似图标替代。
### 6.3 选择 Reka Primitive
- 先使用 Reka 已有 Primitive,再包装视觉层。
- 使用 Reka 提供的 `data-state`、`data-disabled`、ARIA 和键盘模型。
- 不重复实现已有的焦点陷阱、Escape 关闭、方向键选择和焦点恢复。
- 只有 design 明确增加的行为才在包装层补充,例如 NumberField 快速步进和 Tabs 移动标记。
### 6.4 实现包装组件
标准模式:
```ts
const props = defineProps<RekaProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<RekaEmits>()
const delegatedProps = computed(() => {
const { class: _class, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
```
- 从转发对象中排除视觉包装专属 Props。
- 通过 `cn(baseClass, props.class)` 合并调用方 class。
- variants 较少且稳定时使用 CVA;动态几何或连续数值使用 computed/CSS Variable。
- 保留清晰的 `Ui<Component>` 命名和目录级 `index.ts` 导出。
### 6.5 迁移业务调用
- 将旧组件 API 映射到新组件的受控状态和事件。
- 允许调整页面或业务组件,使其符合 Reka 的组合方式。
- 每次迁移一个可验证的业务切片,旧实现只在调用点迁完后删除。
- 不在通用组件中加入仅为兼容单个旧页面的隐式逻辑。
## 7. 关键实现规则
### 7.1 Portal、Teleport 与层级
Reka 的 Select、Tooltip、DropdownMenu、Popover 等通常经过 Portal 和 Popper 包装层。Vue scoped CSS 可能只落到包装节点,而没有落到实际 Content 节点。
- 对 Portal Content 使用全局唯一的 BEM class,并采用非 scoped 样式或 `:global()`。
- 同时给 Content 和 `[data-reka-popper-content-wrapper]:has(> .<content-class>)` 设置明确层级。
- 配置 `collisionPadding`,验证视口四边的避让。
- 不能只看 DOM 中存在 listbox/tooltip;必须检查背景、边框、尺寸和最上层命中元素。
推荐层级:普通浮层 80,Tooltip 90,Dialog/Overlay 按页面统一规划,禁止无上限叠加 z-index。
### 7.2 Slider 坐标一致性
`SliderRoot` 的宽度就是 Reka 计算 Range 和 Thumb 的坐标系。数值标签、单位和标题不得放进这个坐标容器并参与其宽度。
```text
.slider-shell
├── SliderRoot.slider-control
│ ├── SliderTrack
│ │ └── SliderRange
│ └── SliderThumb
└── .slider-value
```
验收时检查 `rangeWidth / trackWidth` 是否等于当前百分比,并确认 Thumb 中心与 Range 终点一致。
### 7.3 Tabs 动态标记
- 标记归 TabsList 所有,不绑定在某个 Trigger 的伪元素上。
- 根据 active Trigger 与 List 的实际矩形计算中心位置。
- 监听 `data-state`、容器 Resize 和字体加载完成事件。
- 初次定位不播放位移动画,后续选择变化才动画。
- 在 `prefers-reduced-motion: reduce` 下移除过渡。
### 7.4 Dialog 标题动效
- 焦点态使用 design 提供的完整帧序列,不用单个渐变假装动画。
- 使用确定的数字顺序加载帧,避免字符串排序造成 `10` 排在 `2` 前。
- 仅在 focused 状态推进和展示动效;unfocused 状态使用静态标题栏。
- 组件卸载时清理计时器,reduced-motion 下隐藏或静态化动效。
### 7.5 NumberField 扩展操作
- 基础加减由 Reka NumberField 处理。
- 快速步进作为包装层显式按钮,遵守 min/max 边界并发出标准 `update:modelValue`。
- 所有图形按钮必须有可读的 `aria-label`。
### 7.6 页面滚动契约
- 从应用根到页面滚动容器的 Flex/Grid 链路必须包含 `min-height: 0`。
- 页面只能有一个主要纵向滚动所有者。
- 若 `html/body` 被旧应用设为 `overflow:hidden`,路由视图容器必须明确 `height:100%`、`min-height:0` 和 `overflow-y:auto`。
- Portal 不得渲染到会裁剪内容的局部 `overflow:hidden` 容器。
## 8. 响应式标准
区分“必须像素对齐的组件原子”和“必须流式变化的页面布局”:
- 保留 px:1px 边框、位图原始尺寸、30px 控件高度、像素箭头、帧动画和精细纹理。
- 使用流式单位:页面 gutter、列宽、面板间距、内容最大宽度和字号层级。
- 推荐 `clamp()`、`minmax(0, 1fr)`、`auto-fit`、Flex wrap 和少量语义断点。
- 宽表格和完整状态矩阵允许组件内部横向滚动,但页面本身不得产生横向溢出。
- 至少验证 1920、1280、768、390 CSS px;同时检查 200% 缩放与键盘访问。
不要为了“去除 px”而破坏游戏素材的像素精度。响应式的重点是容器、组合与内容流,而不是把所有物理尺寸换成 rem。
## 9. 组件目录页要求
每引入一个组件,都必须同步加入 `/components` 或等价目录页,并展示:
- 默认、hover、pressed、focus、disabled 等状态。
- 受控值变化和可见的结果反馈。
- 键盘操作、Escape、焦点恢复和 Portal 展示。
- 主题切换后的同一状态。
- 窄屏组合和必要的内部滚动。
- 真实业务素材,而非临时占位图。
目录页既是文档,也是运行态回归面。静态状态展示不能替代真实可交互实例。
## 10. 验证流程
### 10.1 静态检查
```sh
pnpm run format:check
pnpm run lint
pnpm run type-check
pnpm run build-only
git diff --check
```
### 10.2 浏览器检查
逐个组件验证:
1. 默认视觉和 design 对比。
2. 鼠标 hover/pressed 和键盘 focus-visible。
3. v-model、禁用态和边界值。
4. Select、Tooltip、DropdownMenu、Dialog 的层级、避让和关闭行为。
5. Slider、Tabs 等动态几何的实际端点。
6. 页面顶部到底部可滚动,且无意外横向滚动。
7. 主题切换后组件无需修改即可工作。
8. 浏览器控制台无新增 error/warning。
构建通过不是浏览器交互通过。涉及滚动、焦点、Portal、拖拽、动效或响应式的问题必须保留运行态证据。
## 11. 完成定义
一个组件只有同时满足以下条件才算迁移完成:
- 旧调用点所需功能已覆盖,业务链路可完成。
- design 的结构、状态、动效和素材已经逐项核对。
- Reka 的键盘、ARIA、焦点和受控状态没有被视觉包装破坏。
- 组件只消费语义 Token,并通过至少两个主题验证。
- 组件目录页包含真实交互和状态矩阵。
- 桌面、平板、手机宽度无页面级溢出。
- format、lint、type-check、build 和浏览器 QA 均通过。
- 尚未迁移或有意保留的差异在 QA 文档中明确记录。
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.PNG' {
const src: string
export default src
}
+54
View File
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Expires" content="0" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="Cache" content="no-cache" />
<meta name="baidu-site-verification" content="codeva-hGziQg28WN" />
<title>DNF计算器</title>
<script
defer
src="https://umami.dnftools.com/script.js"
data-website-id="f30ce285-0a71-4992-b26c-894f0eab34ac"
></script>
<script>
var _hmt = _hmt || []
;(function () {
var hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?472775d2edd6fb31b00828eef60ba388'
var s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(hm, s)
})()
</script>
<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-J0NPDEYQJS"></script>
<script>
// 百度统计
// eslint-disable-next-line vars-on-top, no-var
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?472775d2edd6fb31b00828eef60ba388";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
// Google 分析
window.dataLayer = window.dataLayer || []
function gtag(...args) {
window.dataLayer.push(args)
}
gtag('js', new Date())
gtag('config', 'G-J0NPDEYQJS')
</script> -->
</head>
<body>
<!-- <script>
window.flag
= navigator.userAgent.match(/(phone|pad|pod|iphone|ipod|ios|ipad|android|mobile|blackberry|iemobile|mqqbrowser|juc|fennec|wosbrowser|browserng|webos|symbian|windows phone)/i)
</script> -->
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
{
"name": "frontend-ui",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"check": "run-s format:check lint type-check",
"lint": "oxlint --deny-warnings .",
"lint:fix": "oxlint --fix .",
"format": "oxfmt --write .",
"format:check": "oxfmt --check ."
},
"dependencies": {
"@vueuse/core": "^14.4.0",
"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",
"reka-ui": "^2.10.1",
"tailwind-merge": "^3.6.0",
"vue": "^3.5.40",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"@tsconfig/node24": "^24.0.4",
"@types/node": "^26.1.2",
"@types/pako": "^2.0.3",
"@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue-jsx": "^5.1.6",
"@vue/tsconfig": "^0.9.1",
"jiti": "^2.7.0",
"npm-run-all2": "^9.0.3",
"oxfmt": "^0.62.0",
"oxlint": "^1.77.0",
"sass": "^1.102.0",
"tailwindcss": "^4.3.3",
"typescript": "~6.0.3",
"unplugin-auto-import": "^21.1.0",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.2.0",
"vue-tsc": "^3.3.9"
},
"engines": {
"node": ">=22.12.0"
},
"packageManager": "pnpm@10.25.0"
}
+3940
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

+12
View File
@@ -0,0 +1,12 @@
import type { IConfig } from '@/stores'
import { defineRequest } from '../common'
import type { IResult } from './type'
export default defineRequest((request) => {
return {
async calc(config: IConfig) {
const res = await request.post<IResult>('/calc', config)
return res.data
},
}
})
// Compare this snippet from frontend/src/api/common/index.ts:
+56
View File
@@ -0,0 +1,56 @@
export interface IResult {
uuid: string
skills: IResultSkill[]
info: IResultUserInfo[]
suits: {
suit?: IResultSuit
oath?: IResultSuit
correction?: number
}
buffer: boolean
}
export interface IResultSkill {
name: string
icon: string
lv: number
data: number
ratio: number
cd: number
damage: number
mode: string
buff: number[]
learnLv: number
type: string
}
export interface IResultUserInfo {
id: number
name: string
value: string | number
}
export interface IResultSuit {
id: string
count: number
imageUrl: string
level: number
name: string
point: number
rarity: string
value: string
suitId: string
skills: {
id: string
skillId: string
name: string
imageUrl: string
enable: boolean
}[]
}
export interface IResultSkillCount {
count: number
mode: string
name: string
}
+118
View File
@@ -0,0 +1,118 @@
import type { AxiosInstance, AxiosResponse } from 'axios'
import axios from 'axios'
import { h } from 'vue'
import { ungzip } from 'pako'
import { useInfoStore } from '@/stores'
import { useDialog } from '@/components/hooks/dialog'
// import { useAppStore, useConfigStore } from '@/store'
// import { useDialog } from '@/components/hooks/dialog'
export interface HttpResponse<T = unknown> extends AxiosResponse {
code?: number
message: string
data: T
state?: number
}
let instance: AxiosInstance | null = null
let baseURL = '/api'
// 避免远程开发的时候冲突
if (process.env.NODE_ENV == 'development') {
baseURL = '/api'
}
if (import.meta.env.MODE == 'web' || import.meta.env.MODE == 'show') {
baseURL = 'https://api.dnftools.com/'
}
if (import.meta.env.MODE == 'test') {
baseURL = 'https://testapi.dnftools.com/'
}
// 解压
function unzip(key: string) {
// 将二进制字符串转换为字符数组
const charData = key.split('').map((x) => {
return x?.charCodeAt(0)
})
// 将数字数组转换成字节数组
const binData = new Uint8Array(charData)
// 解压
const data = ungzip(binData)
const decoder = new TextDecoder()
const str = decoder.decode(data)
// unescape(str) --->解压后解码,防止中午乱码
return str
}
export function defineRequest<T>(fn: (ax: AxiosInstance) => T) {
if (!instance) {
instance = axios.create({
baseURL,
})
instance.interceptors.request.use(
(request) => {
// const configStore = useConfigStore()
// const token = configStore.token
// const uid = useAppStore().uid
const token = useInfoStore().alter_token
if (token) {
request.headers['Alter-Token'] = token
}
return request
},
(error) => {
// do something
return Promise.reject(error)
},
)
instance.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
// instance.defaults.headers["Accept-Encoding"] = "gzip"
// instance.defaults.headers["Content-Encoding"] = "gzip"
// add response interceptors
instance.interceptors.response.use(
async (response: AxiosResponse<HttpResponse>) => {
if (response.data.code === 500) {
const { alert } = useDialog()
await alert({
content: () => {
return h(
'div',
{
class: 'justify-center text-#d4d6b6 text-center',
style: 'white-space:pre-wrap;width:250px;margin:10px',
},
`出现错误,可加群反馈:183439472\n${response.data.message}`,
)
},
})
}
// 解密数据
if (typeof response.data.data == 'string') {
try {
const data = unzip(atob(response.data.data as string))
response.data.data = JSON.parse(data)
} catch {}
}
return response.data
},
async (error: Error) => {
return Promise.reject(error)
},
)
}
return fn(Object.create(instance))
}
+4
View File
@@ -0,0 +1,4 @@
import info from './info'
import calc from './calc'
export default { ...info, ...calc }
+48
View File
@@ -0,0 +1,48 @@
import { defineRequest } from '../common'
import type { IAdventureInfo, IAlterInfo, ICharacterInfo, IHelperInfo, ISkillInfo } from './type'
export default defineRequest((request) => {
return {
async adventures() {
const res = await request.get<IAdventureInfo[]>('/adventure')
return res.data
},
async alterByName(name: string) {
const res = await request.get<IAlterInfo>('/adventure', { params: { name } })
return res.data
},
async createToken(alter: string, equVersion?: string) {
const res = await request.get<string>(`/token/get/${alter}`, { params: { equVersion } })
return res.data
},
async characterInfo() {
const res = await request.get<ICharacterInfo>('/character')
return res.data
},
async skillDetail(skillId: string, level: number) {
const res = await request.get<ISkillInfo>(`/skill/${skillId}/${level}`)
return res.data
},
async getConfigFromHelper(uid: string) {
const res = await request.get(`/dnfhelper/${uid}/`)
return res.data
},
async getConfigFromHelperAlter(uid: string, alter: string) {
const res = await request.get(`/dnfhelper/${uid}/${alter}/`)
return res.data
},
async getSkillFromCode(code: string) {
const res = await request.post(`/skillTree/`, { code })
return res.data
},
async getOathInfo(suitId: string, skillId: string, rarity: string, lv: number) {
const res = await request.get(`/info/oath/${suitId}/${skillId}`, { params: { rarity, lv } })
return res.data
},
async getHelperList(id: string) {
const res = await request.get<IHelperInfo>(`/dnfhelper/${id}/list`)
await new Promise((resolve) => setTimeout(resolve, 30000))
return res.data
},
}
})
+232
View File
@@ -0,0 +1,232 @@
export interface IAdventureInfo {
title: string
name: string
children: IAlterInfo[]
}
export interface IAlterOption {
name: string
title: string
value?: string
}
export interface IAlterInfo {
name: string
url?: string
title: string
default_value: string
options?: IAlterOption[]
open?: boolean
comment?: string
value?: string
}
export interface ICharacterInfo {
role: string
alter: string
name: string
buffer: boolean
equVersion: string
equips: IEquipment[]
properties: string[]
stones: IEquipment[]
enchants: IEnchantingInfo[]
emblems: IEmblemInfo[]
oaths: IEquipment[]
weapons: string[]
subweapons: string[]
skills: ISkill[]
suits: ISuit[]
avatar: Record<string, IAvatarInfo[]>
jades: IJade[]
sundry: Record<string, ISundry>
options: { id: number; name: string; options: string[] }[]
oaths_skills: Record<
string,
{
suitInfo: {
name: string
rarity: string
SkillAttack: number
Buffer: number
Attack: number
}[]
skills: { skillId: string; name: string; icon: string }[]
}
>
}
export interface ISundry {
id: number
options: {
name: string
value: number
}[]
}
export interface IEquipment {
id: string
imageUrl: string
itemType: string
itemDetailType: string
name: string
categorize: string
suit: string[]
rarity: string
max_adaptation: number
STR: number[]
INT: number[]
Vitality: number[]
Spirit: number[]
AtkP: number[]
AtkM: number[]
AtkI: number[]
SkillAttack: number[]
Attack: number[]
Buffer: number[]
detail: string
bufferDetail: string
position: string[]
wearUrl: string[]
oathPos: string
displayUrl?: string
}
export interface ISkill {
icon: string
name: string
id: number
learnLv: number
position: number
line: number
type: string
maxLearnLv: number
maxLv: number
bind: boolean
hasVP: boolean
hasUP: boolean
vps: {
name: string
desc: string
}[]
uuid: string
upType: 'damage' | 'heal'
}
export interface ISuit {
id: number
name: string
imageUrl: string
suitId: number
suitName: string
point: number
rarity: string
level: number
SkillAttack: number
Buffer: number
value: string
bufferValue: string
}
export interface IEnchantingInfo {
id: string | number
fame: number | undefined
position: string[]
detail: string
categorize: string[]
rarity?: string
type?: string
}
export interface IEmblemInfo {
fame: number
id: string
rarity: string
type: string
value: string
}
export interface IAvatarInfo {
id: number
options: string[]
part: string
rarity: string
suit?: string
name: string
}
export interface IJade {
name: string
id: number
max: number
min: number
pre: number
unit: string
}
export interface ISkillInfo {
name: string
type: string
desc: string
descDetail: string
consumeItem: ConsumeItem
descSpecial: string[]
maxLevel: number
requiredLevel: number
requiredLevelRange: number
preRequiredSkill: any
jobId: string
jobName: string
jobGrowLevel: any[]
evolution: Evolution[]
enhancement: Enhancement[]
attribute: Attribute
}
export interface ConsumeItem {
itemId: string
itemName: string
value: number
}
export interface Evolution {
type: number
name: string
desc: string
descDetail: string
skills?: string[]
}
export interface Enhancement {
type: number
status: Status[]
}
export interface Status {
name: string
value: string
}
export interface Attribute {
optionDesc: string
level: number
consumeMp: number
coolTime: number
castingTime?: number
optionValue: Record<`value${string}`, string>
detail: string
}
export interface IHelperRoleInfo {
roleId: string
roleName: string
roleJob: string
avatarLink: string
fame: number
}
export interface IHelperInfo {
id: string
avatar: string
name: string
roleList: IHelperRoleInfo[]
}
+6
View File
@@ -0,0 +1,6 @@
export interface IDataType<T = any> {
code?: number
message: string
data: T
state?: number
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

@@ -0,0 +1,39 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_135_176)">
<rect x="1" y="1" width="20" height="18" fill="url(#paint0_linear_135_176)"/>
</g>
<rect x="0.5" y="0.5" width="21" height="19" stroke="#49453F"/>
<g filter="url(#filter1_d_135_176)">
<path d="M11.1514 14.3975V12H6V11H16L11.1514 14.3975ZM10.8486 8.39746H16V9.39746H6L10.8486 6V8.39746Z" fill="url(#paint1_linear_135_176)"/>
</g>
<defs>
<filter id="filter0_i_135_176" x="0" y="0" width="22" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_135_176"/>
</filter>
<filter id="filter1_d_135_176" x="2" y="2" width="18" height="16.3975" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.44 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_135_176"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_135_176" result="shape"/>
</filter>
<linearGradient id="paint0_linear_135_176" x1="11" y1="1" x2="11" y2="19" gradientUnits="userSpaceOnUse">
<stop stop-color="#0D3969"/>
<stop offset="1" stop-color="#0A2244"/>
</linearGradient>
<linearGradient id="paint1_linear_135_176" x1="11" y1="6" x2="11" y2="14.3975" gradientUnits="userSpaceOnUse">
<stop stop-color="#D6B17D"/>
<stop offset="1" stop-color="#96763A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

@@ -0,0 +1,39 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_135_173)">
<rect x="1" y="1" width="20" height="16" fill="url(#paint0_linear_135_173)"/>
</g>
<rect x="0.5" y="0.5" width="21" height="17" stroke="#49453F"/>
<g filter="url(#filter1_d_135_173)">
<path d="M11.1514 12.3975V10H6V9H16L11.1514 12.3975ZM10.8486 6.39746H16V7.39746H6L10.8486 4V6.39746Z" fill="url(#paint1_linear_135_173)"/>
</g>
<defs>
<filter id="filter0_i_135_173" x="0" y="0" width="22" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_135_173"/>
</filter>
<filter id="filter1_d_135_173" x="2" y="0" width="18" height="16.3975" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.44 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_135_173"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_135_173" result="shape"/>
</filter>
<linearGradient id="paint0_linear_135_173" x1="11" y1="1" x2="11" y2="17" gradientUnits="userSpaceOnUse">
<stop stop-color="#306EB3"/>
<stop offset="1" stop-color="#0A2244"/>
</linearGradient>
<linearGradient id="paint1_linear_135_173" x1="11" y1="4" x2="11" y2="12.3975" gradientUnits="userSpaceOnUse">
<stop stop-color="#D6B17D"/>
<stop offset="1" stop-color="#96763A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

@@ -0,0 +1,39 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_135_172)">
<rect x="1" y="1" width="20" height="18" fill="url(#paint0_linear_135_172)"/>
</g>
<rect x="0.5" y="0.5" width="21" height="19" stroke="#49453F"/>
<g filter="url(#filter1_d_135_172)">
<path d="M11.1514 14.3975V12H6V11H16L11.1514 14.3975ZM10.8486 8.39746H16V9.39746H6L10.8486 6V8.39746Z" fill="url(#paint1_linear_135_172)"/>
</g>
<defs>
<filter id="filter0_i_135_172" x="0" y="0" width="22" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_135_172"/>
</filter>
<filter id="filter1_d_135_172" x="2" y="2" width="18" height="16.3975" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.44 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_135_172"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_135_172" result="shape"/>
</filter>
<linearGradient id="paint0_linear_135_172" x1="11" y1="1" x2="11" y2="19" gradientUnits="userSpaceOnUse">
<stop stop-color="#626262"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_135_172" x1="11" y1="6" x2="11" y2="14.3975" gradientUnits="userSpaceOnUse">
<stop stop-color="#D1D1D1"/>
<stop offset="1" stop-color="#989898"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

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