This commit is contained in:
2026-08-09 00:33:21 +08:00
parent 6be5ebc8f7
commit 9ea606b175
133 changed files with 7300 additions and 6289 deletions
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { DialogPortal, DialogRoot } from 'reka-ui'
import {
UiDialogContent,
UiDialogDescription,
UiDialogHeader,
UiDialogOverlay,
UiDialogTitle,
} from '@/components/ui/dialog'
import { provideShow } from '@/components/hooks/show'
withDefaults(
defineProps<{
title: string
}>(),
{
title: '导入',
},
)
const open = defineModel<boolean>('open', { default: false })
let returnFocusElement: HTMLElement | null = null
// The existing import forms reset themselves through this injection whenever their dialog opens.
provideShow(open)
watch(open, (value) => {
if (value && document.activeElement instanceof HTMLElement) {
returnFocusElement = document.activeElement
}
})
function keepOpenOnOutsideInteraction(event: Event) {
event.preventDefault()
}
function restoreTriggerFocus(event: Event) {
event.preventDefault()
nextTick(() => returnFocusElement?.focus())
}
</script>
<template>
<DialogRoot v-model:open="open" modal>
<DialogPortal>
<UiDialogOverlay />
<UiDialogContent
class="character-import-dialog"
@interact-outside="keepOpenOnOutsideInteraction"
@pointer-down-outside="keepOpenOnOutsideInteraction"
@close-auto-focus="restoreTriggerFocus"
>
<UiDialogHeader>
<UiDialogTitle>{{ title }}</UiDialogTitle>
</UiDialogHeader>
<UiDialogDescription class="character-import-dialog__description">
{{ title }}窗口
</UiDialogDescription>
<div class="character-import-dialog__body">
<slot />
</div>
</UiDialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style scoped lang="scss">
.character-import-dialog {
--ui-dialog-viewport-inline-size: calc(100vw - 32px);
--ui-dialog-max-inline-size: 360px;
grid-template-rows: auto minmax(0, 1fr);
}
.character-import-dialog__description {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.character-import-dialog__body {
box-sizing: border-box;
display: flex;
min-width: 0;
min-height: 128px;
max-height: calc(100dvh - var(--ui-dialog-header-block-size) - 32px);
align-items: center;
justify-content: center;
padding: 8px;
overflow: auto;
}
</style>
@@ -5,6 +5,8 @@ import api from '@/api'
import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { UiButton } from '@/components/ui/button'
import { UiInput } from '@/components/ui/input'
export default defineComponent({
setup() {
@@ -48,20 +50,28 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-30 w-80 py-2 px-4 text-#e9c556">
<div class="flex justify-around items-center">
助手社区ID
<calc-input class="!w-60" v-model={uid.value} />
<div class="box-border flex min-h-30 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
<label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
<span>助手社区ID</span>
<UiInput class="min-w-0" v-model={uid.value} />
</label>
<div class="leading-5">
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
点击导入后会从助手通过文字识别获取打造
</p>
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分打造无法获取,此处仅做快捷导入
</p>
</div>
<div>
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p>
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={isLoading.value}>
<div class="flex items-center justify-center gap-3">
<UiButton onClick={() => execute()} disabled={isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</UiButton>
<UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div>
</div>
</>
@@ -6,6 +6,7 @@ import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { type IHelperRoleInfo } from '@/api/info/type'
import { UiButton } from '@/components/ui/button'
export default defineComponent({
props: {
@@ -54,22 +55,30 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-45 w-80 py-2 px-4 text-#e9c556">
<div class="flex justify-around items-center flex-col gap-1">
<div class="box-border flex min-h-45 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
<div class="flex flex-col items-center justify-center gap-1 text-center">
<div>检测到以下信息,是否导入助手数据?</div>
<div class="text-#DED29A">{props.alter?.roleName}</div>
<div class="text-#908150">{props.alter?.roleJob}</div>
<div class="text-#3ea74e">名望:{props.alter?.fame}</div>
<div class="text-[color:var(--ui-primary-foreground)]">{props.alter?.roleName}</div>
<div class="text-[color:var(--ui-muted-foreground)]">{props.alter?.roleJob}</div>
<div class="text-[color:var(--ui-success)]">名望:{props.alter?.fame}</div>
</div>
<div>
<p class="text-#e9c556/80">* 点击导入后会从助手通过文字识别获取打造</p>
<p class="text-#e9c556/80">* 部分打造无法获取,此处仅做快捷导入</p>
<div class="leading-5">
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
点击导入后会从助手通过文字识别获取打造
</p>
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分打造无法获取,此处仅做快捷导入
</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={isLoading.value}>
<div class="flex items-center justify-center gap-3">
<UiButton onClick={() => execute()} disabled={isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</UiButton>
<UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div>
</div>
</>
@@ -5,6 +5,8 @@ import api from '@/api'
import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { UiButton } from '@/components/ui/button'
import { UiInput } from '@/components/ui/input'
export default defineComponent({
setup() {
@@ -44,23 +46,28 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-35 w-80 py-2 px-4 text-#e9c556">
<div class="flex justify-around items-center">
技能加点代码
<calc-input class="!w-60" v-model={skillCode.value} />
</div>
<div>
<p class="text-#e9c556/80">
<div class="box-border flex min-h-35 w-full max-w-80 flex-col justify-center gap-2 px-4 py-2 text-[color:var(--ui-primary-foreground)]">
<label class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-2">
<span>技能加点代码</span>
<UiInput class="min-w-0" v-model={skillCode.value} />
</label>
<div class="leading-5">
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
点击导入后会根据规则解析出技能加点、技能进化、技能强化
</p>
<p class="text-#e9c556/80">* 部分技能可能存在无法识别的情况,需要手动调整</p>
<p class="m-0 text-[color:var(--ui-muted-foreground)]">
*
部分技能可能存在无法识别的情况,需要手动调整
</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={!skillCode.value || isLoading.value}>
<div class="flex items-center justify-center gap-3">
<UiButton onClick={() => execute()} disabled={!skillCode.value || isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</UiButton>
<UiButton variant="outline" onClick={() => (visible.value = false)}>
取消
</UiButton>
</div>
</div>
</>
@@ -1,49 +0,0 @@
<template>
<div class="mx-10px">
<calc-collapse title="武器/称号/宠物/秘宝">
<calc-tabs v-model="anotherTab" class="!mb-2">
<calc-tab :value="0">武器</calc-tab>
<calc-tab :value="1">称号/宠物/秘宝</calc-tab>
<calc-tab :value="2" v-if="infoStore.infos?.alter.includes('vegabond')">副武器</calc-tab>
</calc-tabs>
<Weapon v-show="anotherTab === 0"></Weapon>
<Else v-show="anotherTab === 1"></Else>
<SubWeapon v-show="anotherTab === 2"></SubWeapon>
</calc-collapse>
<calc-collapse title="套装/通宝">
<calc-tabs v-model="equTab" class="!mb-2">
<calc-tab :value="0">套装</calc-tab>
<calc-tab :value="1">通宝</calc-tab>
</calc-tabs>
<Suit v-show="equTab === 0"></Suit>
<Universal v-show="equTab === 1"></Universal>
</calc-collapse>
<calc-collapse title="誓约">
<calc-tabs v-model="stoneTab" class="!mb-2">
<calc-tab :value="0">套装</calc-tab>
<calc-tab :value="1">通宝</calc-tab>
<calc-tab :value="2">其他</calc-tab>
</calc-tabs>
<Stone v-show="stoneTab == 0"></Stone>
<StoneUniversal v-show="stoneTab == 1"></StoneUniversal>
<StoneOld v-show="stoneTab == 2"></StoneOld>
</calc-collapse>
</div>
</template>
<script lang="ts" setup name="Index">
import Suit from './components/Suit.vue'
import Weapon from './components/Weapon.vue'
import Stone from './components/Stone.vue'
import Universal from './components/Universal.vue'
import StoneUniversal from './components/StoneUniversal.vue'
import StoneOld from './components/StoneOld.vue'
import Else from './components/Else.vue'
import { useInfoStore } from '@/stores'
import SubWeapon from './components/SubWeapon.vue'
const infoStore = useInfoStore()
const equTab = ref(0)
const stoneTab = ref(0)
const anotherTab = ref(0)
</script>
@@ -1,49 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equ, _) in equs" :key="_">
<EquipmentIcon
:equipment="equ"
:inactive="configStore.config.equips[equ?.itemDetailType ?? '']?.id != equ?.id"
@click="configStore.chooseEqu(equ)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const equs = computed(() => {
const items = infoStore.equips.filter((equip) => ['称号', '宠物'].includes(equip.itemDetailType))
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const equList = items.filter((item) => item.itemDetailType === type)
equList.sort((a, b) => {
if (a.itemType !== b.itemType) {
return b.itemType.localeCompare(a.itemType)
}
const rarityOrder = ['传说', '神器', '稀有']
return rarityOrder.indexOf(a.rarity) - rarityOrder.indexOf(b.rarity)
})
res.push(...equList)
const remind = rowCount - (res.length % rowCount)
res.push(...Array(remind + rowCount).fill(undefined))
})
const treasure = infoStore.equips.filter((equip) => equip.categorize.includes('秘宝'))
res.push(...treasure, ...Array(rowCount - treasure.length).fill(undefined))
return res
})
</script>
@@ -1,97 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div>
<div class="flex flex-col gap-10px pb-10px">
<!-- 套装选择tab -->
<div class="flex flex-wrap w-100%">
<template v-for="(suit, _) in suits" :key="_">
<div class="flex items-center justify-center">
<img
class="w-35px h-35px object-contain"
:src="getImageURL('equipment' + suit.imageUrl)"
:alt="suit.name"
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
@click="curSuit = suit.value"
@dblclick="handleDoubleClick"
/>
</div>
</template>
</div>
<EquipmentRaritySelect v-model="curRarity" />
</div>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in suitEquipments" :key="_">
<template v-if="equip?.rarity == curRarity || !curRarity">
<EquipmentIcon
:equipment="equip"
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.fusion != equip?.id"
@click="configStore.chooseEqu(equip, true)"
/>
</template>
<template v-else>
<EquipmentIcon />
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
const infoStore = useInfoStore()
const configStore = useConfigStore()
const curSuit = ref(16201)
const curRarity = ref()
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const suits = computed(() => {
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
infoStore.suits.forEach((a) => {
if (!uniqueSuits.has(a.suitId)) {
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
}
})
return Array.from(uniqueSuits.values())
})
const suitEquipments = computed(() => {
const equs = infoStore.stones.filter(
(a) => a.suit.includes(curSuit.value.toString()) && a.suit.length === 1,
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器', '传说', '史诗']
raritiyList.forEach((rarity) => {
const equ: (IEquipment | undefined)[] = equs.filter((a) => a.rarity === rarity)
equ.splice(5, 0, undefined)
equ.splice(9, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
return res
})
const handleDoubleClick = () => {
if (!curRarity.value) return
if (curRarity.value == '太初') return
suitEquipments.value
.filter((a) => !!a && a.rarity == curRarity.value)
.splice(0, 11)
.forEach((a) => {
configStore.chooseEqu(a, true)
})
}
</script>
@@ -1,45 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in equs" :key="_">
<EquipmentIcon
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.fusion != equip?.id"
:equipment="equip"
@click="configStore.chooseEqu(equip, true)"
/>
</template>
</div>
</template>
<script lang="ts" setup StoneUniversal>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const equs = computed(() => {
const items = infoStore.stones.filter((equip) => equip.categorize.includes('通宝'))
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器', '史诗']
const categorize = Array.from(new Set(items.map((item) => item.categorize)))
raritiyList.forEach((rarity) => {
categorize.forEach((cat) => {
const equ: (IEquipment | undefined)[] = items.filter(
(a) => a.rarity === rarity && a.categorize === cat,
)
equ.splice(5, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
})
return res
})
</script>
@@ -1,47 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(weapon, _) in weapons" :key="_">
<EquipmentIcon
:equipment="weapon"
:inactive="configStore.config.equips['副武器']?.id != weapon?.id"
@click="configStore.chooseEqu(weapon, false, true)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const weapons = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.itemType === '武器' && equip.itemDetailType == '光剑',
)
// console.log(items)
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const weapon = items.filter((item) => item.itemDetailType === type)
res.push(
...weapon.filter((item) => item.categorize === '制式武器'),
undefined,
...weapon.filter((item) => item.categorize === '传世武器'),
)
const remind = rowCount - (res.length % rowCount)
res.push(...Array(remind).fill(undefined))
})
return res
})
</script>
@@ -1,111 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div>
<div class="flex flex-col gap-10px pb-10px">
<!-- 套装选择tab -->
<div class="flex flex-wrap w-100%">
<template v-for="(suit, _) in suits" :key="_">
<div class="flex items-center justify-center">
<img
class="w-35px h-35px object-contain"
:src="getImageURL('equipment' + suit.imageUrl)"
:alt="suit.name"
:style="{ filter: curSuit === suit.value ? '' : 'grayscale(100%)' }"
@click="curSuit = suit.value"
@dblclick="handleDoubleClick"
/>
</div>
</template>
</div>
<EquipmentRaritySelect v-model="curRarity" />
</div>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in suitEquipments" :key="_">
<template v-if="equip?.rarity == curRarity || !curRarity">
<EquipmentIcon
:equipment="equip"
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.id != equip?.id"
@click="configStore.chooseEqu(equip)"
/>
</template>
<template v-else>
<EquipmentIcon />
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
const infoStore = useInfoStore()
const configStore = useConfigStore()
const curSuit = ref(16201)
const curRarity = ref()
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const suits = computed(() => {
const uniqueSuits = new Map<number, { name: string; value: number; imageUrl: string }>()
infoStore.suits.forEach((a) => {
if (!uniqueSuits.has(a.suitId)) {
uniqueSuits.set(a.suitId, { name: a.name, value: a.suitId, imageUrl: a.imageUrl })
}
})
return Array.from(uniqueSuits.values())
})
const suitEquipments = computed(() => {
const equs = infoStore.equips.filter(
(a) => a.suit.includes(curSuit.value.toString()) && a.suit.length === 1,
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['神器', '传说', '史诗']
raritiyList.forEach((rarity) => {
const equ: (IEquipment | undefined)[] = equs.filter(
(a) => a.rarity === rarity && !a.name.includes('黑牙'),
)
equ.splice(5, 0, undefined)
equ.splice(9, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
const remind = equs
.filter((a) => a.rarity == '太初' || a.name.includes('黑牙'))
.sort((a, b) => (a.rarity == '太初' ? 1 : 0) - (b.rarity == '太初' ? 1 : 0))
res.push(
undefined,
undefined,
...remind.filter((a) => a.itemDetailType == '项链'),
undefined,
...remind.filter((a) => a.itemDetailType == '手镯'),
undefined,
...remind.filter((a) => a.itemDetailType == '戒指'),
...Array(Math.max(0, rowCount - remind.length - 4)).fill(undefined),
)
return res
})
const handleDoubleClick = () => {
if (!curRarity.value) return
if (curRarity.value == '太初') return
suitEquipments.value
.filter((a) => !!a && a.rarity == curRarity.value)
.splice(0, 11)
.forEach((a) => {
configStore.chooseEqu(a)
})
}
</script>
@@ -1,43 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(equip, _) in equs" :key="_">
<EquipmentIcon
:inactive="configStore.config.equips[equip?.itemDetailType ?? '']?.id != equip?.id"
:equipment="equip"
@click="configStore.chooseEqu(equip)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const equs = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.categorize === '通宝装备' || equip.categorize === '稀有装备 套装',
)
const res: (IEquipment | undefined)[] = []
const raritiyList = ['稀有', '神器', '传说', '史诗']
raritiyList.forEach((rarity) => {
const equ: (IEquipment | undefined)[] = items.filter((a) => a.rarity === rarity)
equ.splice(5, 0, undefined)
equ.splice(9, 0, undefined)
res.push(...equ, ...Array(Math.max(0, rowCount - equ.length)).fill(undefined))
})
return res
})
</script>
@@ -1,45 +0,0 @@
<!-- 每排总计6+4+4,共计14个排列 -->
<template>
<div class="flex flex-wrap gap-4px content-start" :style="{ width: `${rowCount * 32}px` }">
<template v-for="(weapon, _) in weapons" :key="_">
<EquipmentIcon
:equipment="weapon"
:inactive="configStore.config.equips['武器']?.id != weapon?.id"
@click="configStore.chooseEqu(weapon)"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import type { IEquipment } from '@/api/info/type'
import { useConfigStore, useInfoStore } from '@/stores'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
const props = defineProps<{
rowCount?: number
}>()
const rowCount = Math.max(props.rowCount ?? 0, 14)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const weapons = computed(() => {
const items = infoStore.equips.filter(
(equip) => equip.itemType === '武器' && infoStore.infos?.weapons.includes(equip.itemDetailType),
)
const res: (IEquipment | undefined)[] = []
Array.from(new Set(items.map((item) => item.itemDetailType))).forEach((type) => {
const weapon = items.filter((item) => item.itemDetailType === type)
res.push(
...weapon.filter((item) => item.categorize === '制式武器'),
undefined,
...weapon.filter((item) => item.categorize === '传世武器'),
)
const remind = rowCount - (res.length % rowCount)
res.push(...Array(remind).fill(undefined))
})
return res
})
</script>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { computed } from 'vue'
import { UiButton } from '@/components/ui/button'
import { rarityClass } from '@/utils'
import increaseIcon from '@/assets/img/icon/increase.png'
import maxIcon from '@/assets/img/icon/max.png'
import minIcon from '@/assets/img/icon/min.png'
import reduceIcon from '@/assets/img/icon/reduce.png'
export type EquipmentLevelChange = 'preRarity' | 'nextRarity' | 'preLv' | 'nextLv'
const props = defineProps<{
rarity: string
level: number
rarityList: string[]
}>()
const emit = defineEmits<{
change: [direction: EquipmentLevelChange]
}>()
const rarityIndex = computed(() => props.rarityList.findIndex((rarity) => rarity === props.rarity))
const atFirstRarity = computed(() => rarityIndex.value <= 0)
const atLastRarity = computed(() => rarityIndex.value >= props.rarityList.length - 1)
function formatLevel(level: number, rarity: string) {
if (rarity === '太初') return ''
const greekNumerals = ['Ι', 'ΙΙ', 'ΙΙΙ', 'ΙV', 'V', 'VΙ', 'VΙΙ', 'VΙΙΙ', 'ΙX', 'X']
return greekNumerals[level - 1] || level.toString()
}
</script>
<template>
<div class="equipment-level-controls" aria-label="装备等级">
<UiButton
size="icon"
title="降低品质"
aria-label="降低品质"
:disabled="atFirstRarity"
@click="emit('change', 'preRarity')"
>
<img data-icon class="equipment-level-controls__icon" :src="minIcon" alt="" />
</UiButton>
<UiButton
size="icon"
title="降低等级"
aria-label="降低等级"
:disabled="atFirstRarity && level <= 1"
@click="emit('change', 'preLv')"
>
<img data-icon class="equipment-level-controls__icon" :src="reduceIcon" alt="" />
</UiButton>
<output class="equipment-level-controls__value" :class="rarityClass(rarity)">
<span>{{ rarity }}</span>
<span>{{ formatLevel(level, rarity) }}</span>
</output>
<UiButton
size="icon"
title="提升等级"
aria-label="提升等级"
:disabled="atLastRarity"
@click="emit('change', 'nextLv')"
>
<img data-icon class="equipment-level-controls__icon" :src="increaseIcon" alt="" />
</UiButton>
<UiButton
size="icon"
title="提升品质"
aria-label="提升品质"
:disabled="atLastRarity"
@click="emit('change', 'nextRarity')"
>
<img data-icon class="equipment-level-controls__icon" :src="maxIcon" alt="" />
</UiButton>
</div>
</template>
<style scoped>
.equipment-level-controls {
box-sizing: border-box;
display: flex;
min-width: 0;
align-items: center;
justify-content: center;
gap: var(--ui-autocomplete-trigger-gap);
padding: var(--ui-disclosure-content-padding-block);
background: var(--ui-settings-row-alt);
}
.equipment-level-controls__value {
display: flex;
min-width: 0;
min-height: var(--ui-control-height-default);
flex: 1;
align-items: center;
justify-content: center;
gap: var(--ui-control-gap);
padding-inline: var(--ui-control-padding-inline-compact);
border: 1px solid var(--ui-disclosure-border);
background: var(--ui-field-background);
line-height: var(--ui-control-line-height);
}
.equipment-level-controls__icon {
width: var(--ui-badge-height);
height: var(--ui-badge-height);
object-fit: contain;
}
:deep(.ui-button:disabled) .equipment-level-controls__icon {
filter: grayscale(1);
opacity: var(--ui-control-disabled-opacity, var(--ui-settings-dropdown-disabled-opacity));
}
</style>
@@ -1,23 +1,53 @@
<script lang="ts" setup>
import {
SelectRoot,
SelectValue,
UiSelectContent,
UiSelectItem,
UiSelectTrigger,
} from '@/components/ui/select'
import { useConfigStore, useInfoStore } from '@/stores'
const configStore = useConfigStore()
const infoStore = useInfoStore()
</script>
<template>
<div class="flex flex-col gap-2 w-250px p-2">
<div v-for="option in info.options" :key="option.id" class="flex items-center gap-2">
<div class="w-60px text-end">{{ option.name }}</div>
<calc-select
class="flex-1 !h-20px"
:appendTo="dialogRef"
v-model="config.config.options[option.id.toString()]"
>
<calc-option v-for="(item, index) in option.options" :key="index" :value="index">
{{ item }}
</calc-option>
</calc-select>
</div>
</div>
<form class="equipment-options" @submit.prevent>
<label v-for="option in infoStore.options" :key="option.id" class="equipment-options__row">
<span>{{ option.name }}</span>
<SelectRoot v-model="configStore.config.options[option.id.toString()]">
<UiSelectTrigger :aria-label="option.name">
<SelectValue placeholder="请选择" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="(item, index) in option.options" :key="index" :value="index">
{{ item }}
</UiSelectItem>
</UiSelectContent>
</SelectRoot>
</label>
</form>
</template>
<script lang="ts" setup>
import { useConfigStore, useInfoStore } from '@/stores'
const dialogRef = inject('dialogRef', ref(null))
const config = useConfigStore()
const info = useInfoStore()
</script>
<style scoped>
.equipment-options {
display: grid;
width: 100%;
min-width: 0;
gap: var(--ui-control-gap);
}
.equipment-options__row {
display: grid;
min-width: 0;
grid-template-columns: minmax(var(--ui-settings-row-min-block-size), auto) minmax(0, 1fr);
align-items: center;
gap: var(--ui-control-gap);
color: var(--ui-settings-row-foreground);
}
.equipment-options__row > span {
text-align: right;
}
</style>
@@ -164,7 +164,7 @@ export const formatEqu = (total: IEquipment[], subweapons?: string[], curSuit?:
equs: treasures,
},
{
name: `${curSuit} 套装`,
name: curSuit ? `${curSuit} 套装` : '套装',
equs: suits,
},
{
@@ -270,7 +270,7 @@ export const formatStone = (total: IEquipment[], curSuit?: string) => {
// rzs.push(...temp, ...Array(temp.length == 0 ? rowCount-remind : remind).fill(undefined))
return [
{
name: `${curSuit} 套装`,
name: curSuit ? `${curSuit} 套装` : '套装',
equs: suits,
},
{
@@ -315,11 +315,11 @@ export const formatOath = (
}
return [
{
name: `${curSuit?.name} 誓约`,
name: curSuit?.name ? `${curSuit.name} 誓约` : '誓约',
equs: oath,
},
{
name: `${curSuit?.name} 星蕴石`,
name: curSuit?.name ? `${curSuit.name} 星蕴石` : '星蕴石',
equs: primer,
},
]
@@ -1,8 +1,9 @@
<script lang="tsx">
import { computed, defineComponent, ref, renderList } from 'vue'
import { computed, defineComponent, ref } from 'vue'
import type { IEnchantingInfo } from '@/api/info/type'
import { useInfoStore, useConfigStore } from '@/stores'
import ClothList from '@/components/dnf/Avatar/List/index.vue'
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
export default defineComponent({
name: 'Clothes',
setup() {
@@ -73,6 +74,25 @@ export default defineComponent({
// 属性
const option = currentInfo<string | number>('option')
const enchantOptions = computed<ForgeSelectOption[]>(() => [
{ label: '无', value: 0 },
...(enchant_list.value ?? []).map((item) => ({ label: item.detail, value: item.id })),
])
const avatarGradeOptions = computed<ForgeSelectOption[]>(() =>
(basicInfoStore.avatars[part.value] ?? []).map((item) => ({
label: item.suit ?? '',
value: item.id,
})),
)
const avatarPropertyOptions = computed<ForgeSelectOption[]>(() => [
{ label: '无', value: 0 },
...(
basicInfoStore.avatars[part.value]?.find((avatar) => avatar.id == id.value)?.options ?? []
).map((item) => ({ label: item, value: item })),
])
/**
* 同步徽章1到徽章2
* @param val
@@ -84,47 +104,41 @@ export default defineComponent({
return () => {
return (
<>
<div class="h-120px flex justify-center items-center">
<div class="forge-preview">
<ClothList v-model:part={part.value} />
</div>
<div class="flex flex-wrap equ-profile">
<div class="equ-profile-item">
<div class="mr-10px row-name">当前部位</div>
<div class="row-name">当前部位</div>
<span> {part.value}</span>
</div>
{has_enchant.value ? (
<div class="equ-profile-item">
<div class="row-name">{part.value == '宠物' ? '附魔' : '属性'}</div>
<calc-select v-model={enchant.value} class="flex-1 !h-20px">
<calc-option value={0}>无</calc-option>
{renderList(enchant_list.value ?? [], (item) => (
<calc-option value={item.id}>{item.detail}</calc-option>
))}
</calc-select>
<ForgeSelect
v-model={enchant.value}
label={part.value == '宠物' ? '宠物附魔' : '装扮属性'}
options={enchantOptions.value}
/>
</div>
) : (
<>
<div class="equ-profile-item">
<div class="row-name">品级</div>
<calc-select v-model={id.value} class="flex-1 !h-20px">
{renderList(basicInfoStore.avatars[part.value] ?? [], (item) => (
<calc-option value={item.id}>{item.suit}</calc-option>
))}
</calc-select>
<ForgeSelect
v-model={id.value}
label="装扮品级"
options={avatarGradeOptions.value}
/>
</div>
<div class="equ-profile-item">
<div class="row-name">属性</div>
<calc-select v-model={option.value} class="flex-1 !h-20px">
<calc-option value={0}>无</calc-option>
{renderList(
basicInfoStore.avatars[part.value].find((a) => a.id == id.value)?.options ??
[],
(item) => (
<calc-option value={item}>{item}</calc-option>
),
)}
</calc-select>
<ForgeSelect
v-model={option.value}
label="装扮属性"
options={avatarPropertyOptions.value}
/>
</div>
</>
)}
@@ -1,34 +1,25 @@
<template>
<div class="h-179px flex justify-center items-center">
<div class="forge-preview">
<EmblemList :withBg="true" v-model:part="part" :detail="configStore.config.emblems" />
</div>
<div class="flex flex-wrap equ-profile">
<div class="equ-profile-item">
<div class="mr-10px row-name">当前槽位</div>
<div class="row-name">当前槽位</div>
<span>{{ part }}</span>
</div>
<div class="equ-profile-item" v-if="part != '白金'">
<div class="row-name">等级</div>
<calc-select v-model="lv" class="flex-1 !h-20px">
<template v-for="(i, index) in lvs" :key="index">
<calc-option :value="index">{{ i }}</calc-option>
</template>
</calc-select>
<ForgeSelect v-model="lv" label="徽章等级" :options="levelOptions" />
</div>
<div class="flex w-full">
<div class="flex-basis-50px text-center mr-10px">徽章</div>
<div class="flex-1 grid gap-4px" :style="emblemGridStyle">
<div
v-for="(emblem, index) in emblemMaxCount"
:style="emblemItemStyle(index)"
class="flex"
:key="index"
>
<calc-select v-model="emblems[index]" class="min-w-0 flex-1 !h-20px">
<template v-for="(i, index) in emblemOptions" :key="index">
<calc-option :value="i.id">[{{ i.rarity }}]{{ i.value }}</calc-option>
</template>
</calc-select>
<div class="equ-profile-item forge-emblem-row">
<div class="row-name">徽章</div>
<div class="forge-emblem-grid">
<div v-for="(_, index) in emblemMaxCount" :style="emblemItemStyle(index)" :key="index">
<ForgeSelect
v-model="emblems[index]"
:label="`徽章槽位 ${index + 1}`"
:options="emblemSelectOptions"
/>
</div>
</div>
</div>
@@ -38,6 +29,7 @@
<script lang="ts" setup name="Emblems">
import EmblemList from '@/components/dnf/Emblem/List/index.vue'
import { useConfigStore, useInfoStore } from '@/stores'
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
const part = ref('彩色')
@@ -63,6 +55,8 @@ const lvs = [
'史诗Lv.1',
]
const levelOptions: ForgeSelectOption[] = lvs.map((label, value) => ({ label, value }))
const emblemOptions = computed(() => {
if (part.value == '彩色') {
return infoStore.emblems.filter((a) => a.type != '白金').sort((a, b) => b.fame - a.fame)
@@ -81,6 +75,13 @@ const emblemOptions = computed(() => {
.sort((a, b) => b.fame - a.fame)
})
const emblemSelectOptions = computed<ForgeSelectOption[]>(() =>
emblemOptions.value.map((emblem) => ({
label: `[${emblem.rarity}]${emblem.value}`,
value: emblem.id,
})),
)
const emblemMaxCount = computed(() => {
if (part.value == '彩色') {
return 6
@@ -91,12 +92,6 @@ const emblemMaxCount = computed(() => {
return 4
})
const emblemGridStyle = computed(() => {
return {
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
}
})
const emblemItemStyle = (index: number) => {
const count = emblemMaxCount.value
const remainder = count % 2
@@ -1,9 +1,11 @@
<script lang="tsx">
import { computed, defineComponent, ref, renderList } from 'vue'
import { computed, defineComponent, ref } from 'vue'
import type { IEnchantingInfo } from '@/api/info/type'
import { useInfoStore, useConfigStore } from '@/stores'
import EquList from '@/components/dnf/Equipment/List/index.vue'
import { getImageURL } from '@/utils/images'
import { UiCheckbox } from '@/components/ui/checkbox'
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
import ForgeSlider from './ForgeSlider.vue'
export default defineComponent({
name: 'Equip',
setup() {
@@ -121,11 +123,32 @@ export default defineComponent({
//精度
const precision = currentInfo<number>('precision', 0)
const reinforceTypeOptions: ForgeSelectOption[] = [
{ label: '强化', value: 0 },
{ label: '增幅', value: 1 },
]
const reinforceOptions: ForgeSelectOption[] = Array.from({ length: 31 }, (_, value) => ({
label: String(value),
value,
}))
const refineOptions: ForgeSelectOption[] = Array.from({ length: 9 }, (_, value) => ({
label: `锻造+ ${value}`,
value,
}))
const adaptationOptions: ForgeSelectOption[] = Array.from({ length: 4 }, (_, value) => ({
label: String(value),
value,
}))
const enchantOptions = computed<ForgeSelectOption[]>(() => [
{ label: '无', value: '' },
...(enchant_list.value ?? []).map((item) => ({ label: item.detail, value: item.id })),
])
// 武器融合
return () => {
return (
<>
<div class="h-150px flex justify-center items-center">
<div class="forge-preview">
<EquList
v-model:part={part.value}
withBg={false}
@@ -136,108 +159,86 @@ export default defineComponent({
<div class="flex flex-wrap equ-profile">
<div class="equ-profile-item">
<div class="mr-10px row-name">当前部位</div>
<div class="row-name">当前部位</div>
<span> {part.value}</span>
{can_upgrade.value ? (
<calc-checkbox
style="margin-left:auto"
v-model={global_change.value}
label="全局修改"
></calc-checkbox>
) : (
<div></div>
)}
<label class="forge-checkbox" style="margin-left:auto">
<UiCheckbox v-model={global_change.value} aria-label="全局修改" />
<span>全局修改</span>
</label>
) : null}
</div>
{part.value == '副武器' ? (
<>
<div class="equ-profile-item">
<div class="row-name">增幅</div>
<calc-select v-model={reinforce_type.value} class="flex-1 !h-20px">
<calc-option value={0}>强化</calc-option>
<calc-option value={1}>增幅</calc-option>
</calc-select>
<calc-select v-model={reinforce.value} class="flex-1 !h-20px">
{renderList(31, (i) => (
<calc-option value={i - 1}>{i - 1}</calc-option>
))}
</calc-select>
</div>
</>
<div class="equ-profile-item">
<div class="row-name">增幅</div>
<ForgeSelect
v-model={reinforce_type.value}
label="副武器强化类型"
options={reinforceTypeOptions}
/>
<ForgeSelect
v-model={reinforce.value}
label="副武器强化等级"
options={reinforceOptions}
/>
</div>
) : (
<>
{can_upgrade.value ? (
<div class="equ-profile-item">
<div class="row-name">增幅</div>
<calc-select v-model={reinforce_type.value} class="flex-1 !h-20px">
<calc-option value={0}>强化</calc-option>
<calc-option value={1}>增幅</calc-option>
</calc-select>
<calc-select v-model={reinforce.value} class="flex-1 !h-20px">
{renderList(31, (i) => (
<calc-option value={i - 1}>{i - 1}</calc-option>
))}
</calc-select>
<ForgeSelect
v-model={reinforce_type.value}
label={`${part.value}强化类型`}
options={reinforceTypeOptions}
/>
<ForgeSelect
v-model={reinforce.value}
label={`${part.value}强化等级`}
options={reinforceOptions}
/>
{part.value == '武器' && (
<calc-select v-model={refine.value} class="flex-1 !h-20px">
{renderList(9, (i) => (
<calc-option value={i - 1}>锻造+ {i - 1}</calc-option>
))}
</calc-select>
<ForgeSelect
v-model={refine.value}
label="武器锻造等级"
options={refineOptions}
/>
)}
</div>
) : (
<div></div>
)}
) : null}
<div class="equ-profile-item">
<div class="row-name">附魔</div>
<calc-select v-model={enchant.value} class="flex-1 !h-20px">
<calc-option value={''}>无</calc-option>
{renderList(enchant_list.value ?? [], (item) => (
<calc-option value={item.id}>{item.detail}</calc-option>
))}
</calc-select>
<ForgeSelect
v-model={enchant.value}
label={`${part.value}附魔`}
options={enchantOptions.value}
/>
</div>
{can_upgrade.value ? (
<div class="equ-profile-item">
<div class="row-name">调适</div>
{/* <calc-slider
showValue={true}
showPercent={false}
v-model={adaptation.value}
min={0}
max={3}
step={1}
class="flex-1 !h-20px"
/> */}
<calc-select v-model={adaptation.value} class="flex-1 !h-20px">
{renderList(4, (i) => (
<calc-option value={i - 1}>{i - 1}</calc-option>
))}
</calc-select>
<ForgeSelect
v-model={adaptation.value}
label={`${part.value}调适`}
options={adaptationOptions}
/>
</div>
) : (
<div></div>
)}
) : null}
{can_set_secret.value ? (
<>
<div class="equ-profile-item">
<div class="row-name">精度</div>
<calc-slider
showPercent={true}
showValue={false}
v-model={precision.value}
min={0}
max={100}
step={1}
class="flex-1 !h-20px"
/>
</div>
</>
) : (
<div></div>
)}
<div class="equ-profile-item">
<div class="row-name">精度</div>
<ForgeSlider
modelValue={precision.value}
onUpdate:modelValue={(value) => (precision.value = value)}
label={`${part.value}秘宝精度`}
min={0}
max={100}
step={1}
/>
</div>
) : null}
</>
)}
</div>
@@ -0,0 +1,81 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import {
SelectRoot,
SelectValue,
UiSelectContent,
UiSelectItem,
UiSelectTrigger,
} from '@/components/ui/select'
import { cn } from '@/lib/utils'
export type ForgeSelectValue = string | number
export interface ForgeSelectOption {
disabled?: boolean
label: string
value: ForgeSelectValue
}
// Reka reserves the empty string for clearing a Select. Forge uses it as a real "none" value,
// so the adapter keeps that business value outside the primitive and maps it back on update.
const emptyValueSentinel = '__dcalc_forge_empty_value__'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
disabled?: boolean
label: string
options?: ForgeSelectOption[]
placeholder?: string
}>(),
{
disabled: false,
options: () => [],
placeholder: '请选择',
},
)
const model = defineModel<ForgeSelectValue>()
const selectModel = computed<ForgeSelectValue | undefined>({
get: () => (model.value === '' ? emptyValueSentinel : model.value),
set: (value) => {
model.value = value === emptyValueSentinel ? '' : value
},
})
const selectedLabel = computed(
() => props.options.find((option) => Object.is(option.value, model.value))?.label,
)
function primitiveValue(value: ForgeSelectValue) {
return value === '' ? emptyValueSentinel : value
}
</script>
<template>
<div :class="cn('forge-select', props.class)">
<SelectRoot v-model="selectModel" :disabled="disabled">
<UiSelectTrigger :aria-label="label">
<SelectValue :placeholder="selectedLabel ?? placeholder" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="option in options"
:key="`${typeof option.value}:${String(option.value)}`"
:value="primitiveValue(option.value)"
:disabled="option.disabled"
>
{{ option.label }}
</UiSelectItem>
</UiSelectContent>
</SelectRoot>
</div>
</template>
<style scoped>
.forge-select {
min-width: 0;
flex: 1 1 0;
}
</style>
@@ -0,0 +1,84 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { UiSlider } from '@/components/ui/slider'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
disabled?: boolean
formatValue?: (value: number) => string
label: string
max?: number
min?: number
showValue?: boolean
step?: number
}>(),
{
disabled: false,
max: 100,
min: 0,
showValue: true,
step: 1,
},
)
const emits = defineEmits<{ change: [value: number] }>()
const model = defineModel<number | string>({ required: true })
const numericValue = computed(() => {
const value = Number(model.value)
return Number.isFinite(value) ? Math.min(props.max, Math.max(props.min, value)) : props.min
})
const sliderValue = computed(() => [numericValue.value])
const displayValue = computed(() => {
if (props.formatValue) return props.formatValue(numericValue.value)
const range = props.max - props.min
const percentage = range === 0 ? 0 : ((numericValue.value - props.min) / range) * 100
return `${percentage.toFixed(0)}%`
})
function updateValue(values: number[] | undefined) {
const value = values?.[0]
if (value === undefined || !Number.isFinite(value)) return
model.value = value
emits('change', value)
}
</script>
<template>
<div :class="cn('forge-slider', props.class)">
<UiSlider
:model-value="sliderValue"
:disabled="disabled"
:label="label"
:max="max"
:min="min"
:show-value="false"
:step="step"
@update:model-value="updateValue"
/>
<output v-if="showValue" class="forge-slider__value">{{ displayValue }}</output>
</div>
</template>
<style scoped>
.forge-slider {
display: flex;
min-width: 0;
flex: 1 1 0;
align-items: center;
gap: var(--ui-control-gap);
}
.forge-slider__value {
min-width: var(--ui-slider-value-min-inline-size);
color: var(--ui-primary-foreground);
font-size: var(--ui-control-font-size-default);
line-height: var(--ui-control-line-height);
text-align: right;
white-space: nowrap;
}
</style>
@@ -1,29 +1,17 @@
<template>
<div>
<div class="forge-paired-grid">
<template v-for="key in ['0', '1', '2', '3']" :key="key">
<div class="flex gap-2 mt-5px">
<calc-select class="flex-1 !h-20px" v-model="configStore.config.jades[key].id">
<calc-option :value="0">无</calc-option>
<calc-option v-for="item in infoStore.jades" :key="item.id" :value="item.id">
{{ item.name }}
</calc-option>
</calc-select>
<!-- <calc-slider
v-model="configStore.config.jades[key].value"
:formatValue="formatValue(configStore.config.jades[key].id)"
:min="Number(currentJade(configStore.config.jades[key].id)?.min)"
:max="Number(currentJade(configStore.config.jades[key].id)?.max)"
:step="Number(currentJade(configStore.config.jades[key].id)?.pre)"
></calc-slider> -->
<calc-select class="flex-1 !h-20px" v-model="configStore.config.jades[key].value">
<calc-option
v-for="item in valueList(configStore.config.jades[key].id)"
:key="item.id"
:value="item.id"
>
{{ item.value }}
</calc-option>
</calc-select>
<div class="forge-paired-row">
<ForgeSelect
v-model="configStore.config.jades[key].id"
:label="`辟邪玉 ${Number(key) + 1}`"
:options="jadeOptions"
/>
<ForgeSelect
v-model="configStore.config.jades[key].value"
:label="`辟邪玉 ${Number(key) + 1} 数值`"
:options="valueOptions(configStore.config.jades[key].id)"
/>
</div>
</template>
</div>
@@ -34,59 +22,39 @@ import type { IJade } from '@/api/info/type'
import { useConfigStore } from '@/stores'
import { useInfoStore } from '@/stores'
import { getFloat } from '@/utils'
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
const configStore = useConfigStore()
const infoStore = useInfoStore()
// const currentJade = computed(() => {
// return function (id: number = -1) {
// if (id == -1) {
// return undefined
// } else {
// const cur = infoStore.jades?.find((e) => e.id == id) as IJade
// return cur
// }
// }
// })
const jadeOptions = computed<ForgeSelectOption[]>(() => [
{ label: '无', value: 0 },
...(infoStore.jades ?? []).map((item) => ({ label: item.name, value: item.id })),
])
// const formatValue = (id: number) => {
// return (value: number) => {
// if (currentJade.value(id)?.unit.includes('等级')) {
// return `$等级 +${value}`
// }
// return `${getFloat(value, 1)}${currentJade.value(id)?.unit}`
// }
// }
function valueOptions(id: number): ForgeSelectOption[] {
if (id === -1) return []
const valueList = computed(() => {
return function (id: number = -1) {
if (id == -1) {
return undefined
} else {
const res = []
const cur = infoStore.jades?.find((e) => e.id == id) as IJade
if (cur) {
const { max, min, unit, pre } = cur
//
if (unit.includes('等级')) {
res.push({
id: 1,
value: unit,
})
} else {
for (let i = Number(max) as number; i >= Number(min);) {
if (i != 0) {
res.push({
id: i,
value: (i > 0 ? '+' : '') + getFloat(i, 1) + unit,
})
}
i = Number((i - Number(pre)).toFixed(1))
}
}
return res
}
}
const result: ForgeSelectOption[] = []
const current = infoStore.jades?.find((item) => item.id === id) as IJade | undefined
if (!current) return result
const { max, min, unit, pre } = current
if (unit.includes('等级')) {
result.push({ label: unit, value: 1 })
return result
}
})
for (let value = Number(max); value >= Number(min);) {
if (value !== 0) {
result.push({
label: `${value > 0 ? '+' : ''}${getFloat(value, 1)}${unit}`,
value,
})
}
value = Number((value - Number(pre)).toFixed(1))
}
return result
}
</script>
@@ -1,38 +1,29 @@
<template>
<div class="h-150px flex justify-center items-center">
<div class="forge-preview">
<Oath v-model:part="part" :withBg="false" />
</div>
<div class="flex flex-wrap equ-profile">
<div class="equ-profile-item">
<div class="mr-10px row-name">当前部位</div>
<div class="row-name">当前部位</div>
<span>{{ part == '11' ? '誓约' : `星蕴石-${parseInt(part) + 1}` }}</span>
<calc-checkbox
style="margin-left: auto"
v-model="global_change"
label="全局修改"
v-if="part !== '11'"
></calc-checkbox>
<label v-if="part !== '11'" class="forge-checkbox" style="margin-left: auto">
<UiCheckbox v-model="global_change" aria-label="全局修改" />
<span>全局修改</span>
</label>
</div>
<div class="equ-profile-item" v-if="part !== '11'">
<div class="row-name">调适</div>
<calc-select v-model="adaptation" class="flex-1 !h-20px">
<template v-for="i in Array.from({ length: 4 }, (_, i) => i)" :key="i">
<calc-option :value="i">{{ i }}</calc-option>
</template>
</calc-select>
<ForgeSelect v-model="adaptation" label="誓约调适" :options="adaptationOptions" />
</div>
<div class="equ-profile-item" v-else>
<div class="equ-profile-item equ-profile-item--wide" v-else>
<div class="row-name">誓约升级</div>
<template v-for="i in Array.from({ length: 4 }, (_, i) => i)" :key="i">
<calc-select
<ForgeSelect
:model-value="getOathUpgradeValue(i)"
@update:model-value="setOathUpgradeValue(i, $event)"
class="flex-1 !h-20px"
>
<calc-option :value="0">无</calc-option>
<calc-option :value="1">技攻 +4%</calc-option>
<calc-option :value="2">CD -8%</calc-option>
</calc-select>
:label="`誓约升级 ${i + 1}`"
:options="oathUpgradeOptions"
@update:model-value="setOathUpgradeValue(i, Number($event))"
/>
</template>
</div>
</div>
@@ -40,7 +31,9 @@
<script lang="ts" setup name="Oaths">
import Oath from '@/components/dnf/Equipment/Oath/index.vue'
import { UiCheckbox } from '@/components/ui/checkbox'
import { useConfigStore, useInfoStore } from '@/stores'
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
const part = ref('0')
@@ -50,6 +43,17 @@ const configStore = useConfigStore()
const infoStore = useInfoStore()
const adaptationOptions: ForgeSelectOption[] = Array.from({ length: 4 }, (_, value) => ({
label: String(value),
value,
}))
const oathUpgradeOptions: ForgeSelectOption[] = [
{ label: '无', value: 0 },
{ label: '技攻 +4%', value: 1 },
{ label: 'CD -8%', value: 2 },
]
const currentInfo = function <T>(name: string, defaultValue?: T) {
return computed<T>({
get() {
@@ -2,101 +2,69 @@
<div class="flex flex-wrap equ-profile">
<div class="equ-profile-item">
<div class="row-name">迷雾系统</div>
<calc-slider
:showPercent="true"
:formatValue="(lv: number) => `${lv} 级`"
<ForgeSlider
v-model="sundryData['fog']"
label="迷雾系统等级"
:format-value="formatLevel"
:min="1"
:max="100"
:step="1"
class="flex-1 !h-20px"
/>
</div>
<!-- <div class="equ-profile-item">
<div class="row-name">勋章</div>
<calc-select
v-for="(key, index) in ['medal_rarity', 'medal_reinforce']"
:key="index"
v-model="sundryData[key as keyof typeof sundryData]"
class="flex-1 !h-20px"
>
<calc-option v-for="(item, idx) in list(key)" :key="idx" :value="item.value">
{{ item.name }}
</calc-option>
</calc-select>
</div>
<div class="equ-profile-item">
<div class="row-name">宝石</div>
<calc-select
v-for="(key, index) in ['medal_gem_0', 'medal_gem_1', 'medal_gem_2', 'medal_gem_3']"
:key="index"
v-model="sundryData[key as keyof typeof sundryData]"
class="flex-1 !h-20px !min-w-10"
>
<calc-option v-for="(item, idx) in list(key)" :key="idx" :value="item.value">
{{ item.name }}
</calc-option>
</calc-select>
</div> -->
<div class="equ-profile-item">
<div class="row-name">冒险团</div>
<calc-slider
:showPercent="true"
:formatValue="(lv: number) => `${lv} 级`"
<ForgeSlider
v-model="sundryData['adventure']"
label="冒险团等级"
:format-value="formatLevel"
:min="1"
:max="40"
:step="1"
class="flex-1 !h-20px"
/>
</div>
<div class="equ-profile-item">
<div class="row-name">结婚系统</div>
<calc-select
<ForgeSelect
v-for="(key, index) in ['marriage_house', 'marriage_ring']"
:key="index"
v-model="sundryData[key as keyof typeof sundryData]"
class="flex-1 !h-20px !min-w-10"
>
<calc-option v-for="(item, idx) in list(key)" :key="idx" :value="item.value">
{{ item.name }}
</calc-option>
</calc-select>
:label="index === 0 ? '婚房等级' : '婚戒等级'"
:options="sundryOptions(key)"
/>
</div>
<div class="equ-profile-item">
<div class="row-name">晶体契约</div>
<calc-select v-model="sundryData['contract']" class="flex-1 !h-20px">
<calc-option v-for="(item, idx) in list('contract')" :key="idx" :value="item.value">
{{ item.name }}
</calc-option>
</calc-select>
<ForgeSelect
v-model="sundryData['contract']"
label="晶体契约"
:options="sundryOptions('contract')"
/>
</div>
<div class="equ-profile-item">
<div class="row-name">收集箱</div>
<calc-select
<ForgeSelect
v-for="(key, index) in ['collection_type', 'collection_num_0', 'collection_num_1']"
:key="index"
v-model="sundryData[key as keyof typeof sundryData]"
class="flex-1 !h-20px !min-w-10"
>
<calc-option v-for="(item, idx) in list(key)" :key="idx" :value="item.value">
{{ item.name }}
</calc-option>
</calc-select>
:label="`收集箱选项 ${index + 1}`"
:options="sundryOptions(key)"
/>
</div>
<div class="equ-profile-item">
<div class="row-name">名称装扮</div>
<calc-select v-model="sundryData['costume_card']" class="flex-1 !h-20px !min-w-10">
<calc-option v-for="(item, idx) in list('costume_card')" :key="idx" :value="item.value">
{{ item.name }}
</calc-option>
</calc-select>
<ForgeSelect
v-model="sundryData['costume_card']"
label="名称装扮"
:options="sundryOptions('costume_card')"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useConfigStore, useInfoStore } from '@/stores'
import ForgeSelect, { type ForgeSelectOption } from './ForgeSelect.vue'
import ForgeSlider from './ForgeSlider.vue'
const configStore = useConfigStore()
const infoStore = useInfoStore()
@@ -114,11 +82,14 @@ const currentInfo = function <T>(name: string, defaultValue?: T) {
})
}
const list = computed(() => {
return (key: string) => {
return infoStore.sundries[key].options
}
})
const formatLevel = (level: number) => `${level} 级`
function sundryOptions(key: string): ForgeSelectOption[] {
return infoStore.sundries[key].options.map((item) => ({
label: item.name,
value: item.value,
}))
}
const sundryData = reactive({
medal_rarity: currentInfo('medal_rarity', 5),
+120 -38
View File
@@ -1,57 +1,139 @@
<template>
<div class="w-480px pr-10px overflow-auto">
<calc-collapse class="w-100%" title="装备/时装/徽章打造(点击部位切换)">
<calc-tabs v-model="partTab" class="!mb-2">
<calc-tab :value="0">装备</calc-tab>
<calc-tab :value="2">誓约</calc-tab>
<calc-tab :value="1">时装</calc-tab>
<calc-tab :value="3">徽章</calc-tab>
</calc-tabs>
<equipment v-if="partTab === 0"></equipment>
<clothes v-if="partTab === 1"></clothes>
<oaths v-if="partTab === 2"></oaths>
<emblems v-if="partTab === 3"></emblems>
</calc-collapse>
<calc-collapse class="w-100%" title="辟邪玉">
<jade></jade>
</calc-collapse>
<calc-collapse class="w-100%" title="其它">
<sundry></sundry>
</calc-collapse>
</div>
<main class="forge-page">
<UiDataSection title="装备/时装/徽章打造(点击部位切换)">
<TabsRoot v-model="partTab">
<UiTabsList class="forge-tabs" aria-label="打造类型">
<UiTabsTrigger :value="0">装备</UiTabsTrigger>
<UiTabsTrigger :value="2">誓约</UiTabsTrigger>
<UiTabsTrigger :value="1">时装</UiTabsTrigger>
<UiTabsTrigger :value="3">徽章</UiTabsTrigger>
</UiTabsList>
</TabsRoot>
<Equipment v-if="partTab === 0" />
<Avatars v-else-if="partTab === 1" />
<Oaths v-else-if="partTab === 2" />
<Emblems v-else />
</UiDataSection>
<UiDataSection title="辟邪玉">
<Jades />
</UiDataSection>
<UiDataSection title="其它">
<Sundry />
</UiDataSection>
</main>
</template>
<script setup lang="ts">
import equipment from './components/Equipments.vue'
import clothes from './components/Avatars.vue'
import emblems from './components/Emblems.vue'
import oaths from './components/Oaths.vue'
import jade from './components/Jades.vue'
import sundry from './components/Sundry.vue'
import { UiDataSection } from '@/components/ui/data-section'
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
import Avatars from './components/Avatars.vue'
import Emblems from './components/Emblems.vue'
import Equipment from './components/Equipments.vue'
import Jades from './components/Jades.vue'
import Oaths from './components/Oaths.vue'
import Sundry from './components/Sundry.vue'
const partTab = ref(0)
</script>
<style lang="scss">
.equ-profile {
.forge-page {
box-sizing: border-box;
width: 100%;
.equ-profile-item {
width: 100%;
height: 20px;
max-width: 760px;
min-width: 0;
padding-right: clamp(4px, 0.8vw, 12px);
display: grid;
align-content: start;
gap: clamp(6px, 0.8vw, 12px);
overflow: auto;
container-type: inline-size;
.forge-tabs {
margin-bottom: clamp(8px, 1.2cqi, 14px);
}
.forge-preview {
display: flex;
margin-top: 5px;
margin-bottom: 5px;
min-height: clamp(120px, 24cqi, 180px);
align-items: center;
justify-content: center;
overflow: auto hidden;
}
.forge-checkbox {
display: inline-flex;
min-width: max-content;
align-items: center;
gap: var(--ui-control-gap);
color: var(--ui-secondary-foreground);
line-height: var(--ui-control-line-height);
cursor: pointer;
}
.forge-paired-grid,
.equ-profile {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: minmax(0, 1fr);
gap: clamp(6px, 1cqi, 10px);
}
.forge-paired-row,
.equ-profile-item {
box-sizing: border-box;
display: flex;
min-width: 0;
min-height: var(--ui-control-height-default);
align-items: center;
gap: var(--ui-control-gap);
margin: 0;
padding-block: 2px;
.row-name {
flex: 0 0 50px;
text-align: center;
min-width: 0;
flex: 0 0 clamp(64px, 18cqi, 92px);
color: var(--ui-secondary-foreground);
line-height: var(--ui-control-line-height);
text-align: left;
}
:not(:last-child) {
margin-right: 10px !important;
> * {
margin-right: 0 !important;
}
.i-select-label {
margin-right: 0px !important;
> span:not(.row-name) {
display: inline-flex;
min-width: 0;
align-items: center;
line-height: var(--ui-control-line-height);
}
}
.forge-emblem-row {
align-items: flex-start;
}
.forge-emblem-grid {
display: grid;
min-width: 0;
flex: 1;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--ui-control-gap);
}
@container (min-width: 640px) {
.forge-paired-grid,
.equ-profile {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.equ-profile-item--wide,
.forge-emblem-row {
grid-column: 1 / -1;
}
}
}
+205 -63
View File
@@ -1,47 +1,59 @@
<template>
<div class="w-100% h-100% main flex" :class="infoStore.infos?.alter">
<div class="header">
<calc-tabs route :query="route.query">
<calc-tab :value="`/character/equipment/${props.alter}`">装备设置</calc-tab>
<calc-tab :value="`/character/skill/${props.alter}`">技能设置</calc-tab>
<calc-tab :value="`/character/forge/${props.alter}`">打造设置</calc-tab>
</calc-tabs>
</div>
<div class="content flex gap-1">
<div
class="flex h-auto p-1 overflow-y-hidden flex-1 box-border"
v-if="!!infoStore.infos?.alter"
<div
class="main character-shell"
:class="[infoStore.infos?.alter, `character-shell--${String(route.name ?? 'unknown')}`]"
>
<header class="character-shell__header">
<TabsRoot
class="character-shell__navigation"
:model-value="route.path"
@update:model-value="navigateCharacterTab"
>
<UiTabsList aria-label="角色配置导航">
<UiTabsTrigger :value="`/character/equipment/${props.alter}`">装备设置</UiTabsTrigger>
<UiTabsTrigger :value="`/character/skill/${props.alter}`">技能设置</UiTabsTrigger>
<UiTabsTrigger :value="`/character/forge/${props.alter}`">打造设置</UiTabsTrigger>
</UiTabsList>
</TabsRoot>
</header>
<div class="character-shell__workspace">
<section v-if="!!infoStore.infos?.alter" class="character-shell__config">
<RouterView></RouterView>
</div>
<div class="m-1 ml-auto h-auto z-2 box-border">
</section>
<div class="character-shell__stage" aria-hidden="true"></div>
<aside class="character-shell__result" aria-label="计算结果">
<Result></Result>
</div>
</div>
<div class="footer flex">
<calc-checkbox round v-model="configStore.config.DSB">系统奶</calc-checkbox>
<calc-checkbox round v-model="configStore.config.BUFF">组队</calc-checkbox>
<calc-button title="打造导入" @click="showImport" class="ml-1" icon="import"></calc-button>
<calc-button
title="技能导入"
@click="showImportSkill"
class="ml-1"
icon="exchange"
></calc-button>
</aside>
</div>
<footer class="character-shell__footer">
<label class="character-toggle">
<UiSwitch v-model="configStore.config.DSB" />
<span>系统奶</span>
</label>
<label class="character-toggle">
<UiSwitch v-model="configStore.config.BUFF" />
<span>组队</span>
</label>
<UiButton size="icon" title="打造导入" aria-label="打造导入" @click="showImport">
<img data-icon class="character-toolbar-icon" :src="importIcon" alt="" />
</UiButton>
<UiButton size="icon" title="技能导入" aria-label="技能导入" @click="showImportSkill">
<img data-icon class="character-toolbar-icon" :src="exchangeIcon" alt="" />
</UiButton>
</footer>
</div>
<calc-dialog lazy header="打造导入" v-model:visible="importVisible">
<ImportDialog v-model:open="importVisible" title="打造导入">
<ImportVue></ImportVue>
</calc-dialog>
</ImportDialog>
<calc-dialog lazy header="技能导入" v-model:visible="importSkillVisible">
<ImportDialog v-model:open="importSkillVisible" title="技能导入">
<ImportSkillVue></ImportSkillVue>
</calc-dialog>
</ImportDialog>
<calc-dialog lazy header="助手打造导入" v-model:visible="helperImport">
<ImportDialog v-model:open="helperImport" title="助手打造导入">
<ImportDnfhelper :alter="toImport" :uid="helperStore.helperInfo?.id"></ImportDnfhelper>
</calc-dialog>
</ImportDialog>
</template>
<script lang="ts" setup name="Index">
@@ -50,9 +62,15 @@ import { useConfigStore } from '@/stores'
import ImportVue from './Config/import.vue'
import ImportSkillVue from './Config/importSkill.vue'
import ImportDnfhelper from './Config/importDnfhelper.vue'
import ImportDialog from './Config/ImportDialog.vue'
import Result from './Result/Index.vue'
import { useDnfHelperStore } from '@/stores/dnfhelper.ts'
import { type IHelperRoleInfo } from '@/api/info/type'
import { UiButton } from '@/components/ui/button'
import { UiSwitch } from '@/components/ui/switch'
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
import importIcon from '@/assets/img/icon/import.png'
import exchangeIcon from '@/assets/img/icon/exchange.png'
const props = defineProps<{
alter: string
}>()
@@ -93,6 +111,11 @@ const showImportSkill = () => {
}
const route = useRoute()
const router = useRouter()
const navigateCharacterTab = (value: string | number) => {
void router.push({ path: String(value), query: route.query })
}
const saveConfigBeforeUnload = () => {
configStore.saveConfig()
@@ -118,49 +141,168 @@ onUnmounted(() => {
</script>
<style lang="scss">
.main {
.character-shell {
--character-font-size: clamp(12px, 0.625vw, 13px);
--app-font-size: var(--character-font-size);
--ui-control-font-size-compact: calc(var(--character-font-size) * 0.92);
--ui-control-font-size-default: var(--character-font-size);
--ui-control-font-size-large: calc(var(--character-font-size) * 1.18);
--ui-control-height-compact: clamp(24px, 1.25vw, 26px);
--ui-control-height-default: clamp(28px, 1.5vw, 32px);
--ui-control-height-large: clamp(42px, 2.3vw, 48px);
--ui-tabs-height: clamp(40px, 2.05vw, 44px);
--ui-tab-height: clamp(30px, 1.5vw, 32px);
--ui-control-gap: clamp(5px, 0.4vw, 8px);
--ui-item-slot-size: clamp(30px, 1.6vw, 34px);
--ui-item-image-size: clamp(28px, 1.45vw, 30px);
--ui-character-config-inline-size: clamp(340px, 21vw, 480px);
--ui-character-footer-min-block-size: clamp(38px, 2vw, 42px);
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
background-repeat: no-repeat;
background-size: cover;
background-color: black;
background-position: center;
background-color: var(--ui-background);
color: var(--ui-primary-foreground);
}
color: #e9c558;
// font-size: 12px;
.character-shell::before {
content: '';
position: absolute;
z-index: 0;
inset: 0;
background: var(--ui-character-backdrop);
pointer-events: none;
}
.header {
display: flex;
align-items: flex-end;
z-index: 2;
height: 26px;
}
.character-shell__header,
.character-shell__footer {
position: relative;
z-index: 3;
flex: 0 0 auto;
border-color: var(--ui-character-chrome-border);
background: var(--ui-character-chrome-background);
box-shadow: var(--ui-character-chrome-shadow);
}
.footer {
display: flex;
align-items: center;
z-index: 2;
height: 40px;
gap: 10px;
}
.character-shell__header {
display: flex;
min-height: var(--ui-tabs-height);
align-items: center;
border-bottom-width: var(--ui-component-stroke-size);
border-bottom-style: solid;
}
.content {
flex: 1;
overflow-y: hidden;
overflow-x: hidden;
z-index: 2;
width: 100%;
.character-shell__navigation {
width: 100%;
}
.character-shell__workspace {
position: relative;
z-index: 2;
display: grid;
min-width: 0;
min-height: 0;
flex: 1;
grid-template-columns:
minmax(0, var(--ui-character-config-inline-size))
minmax(0, 1fr)
max-content;
gap: var(--ui-character-workspace-gap);
padding: var(--ui-character-workspace-padding);
overflow: hidden;
}
.character-shell__config,
.character-shell__result {
min-width: 0;
min-height: 0;
}
.character-shell__config {
container: character-config / inline-size;
overflow: visible;
}
.character-shell__config > * {
max-height: 100%;
}
.character-shell__stage {
position: relative;
min-width: 0;
pointer-events: none;
}
.character-shell__stage::before {
content: '';
position: absolute;
inset: 7% -12% 4%;
background: var(--ui-character-stage-glow);
filter: blur(var(--ui-character-stage-glow-blur));
opacity: var(--ui-character-stage-glow-opacity);
}
.character-shell__result {
position: relative;
z-index: 2;
overflow: visible;
}
.character-shell__footer {
box-sizing: border-box;
display: flex;
min-height: var(--ui-character-footer-min-block-size);
align-items: center;
flex-wrap: wrap;
gap: var(--ui-control-gap);
padding: var(--ui-character-footer-padding);
border-top-width: var(--ui-component-stroke-size);
border-top-style: solid;
}
.character-shell--skill,
.character-shell--forge {
--ui-character-config-inline-size: clamp(400px, 36vw, 760px);
}
@media (min-width: 1600px) {
.character-shell__workspace:has(.equipment-dictionary.has-details) {
// Only expand the configuration rail when a wide desktop can keep equipment details resident.
--ui-character-config-inline-size: clamp(600px, 34vw, 720px);
}
}
.main::after {
content: '';
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(50, 50, 50, 0.75);
.character-toggle {
display: inline-flex;
min-height: var(--ui-control-height-default);
align-items: center;
gap: 6px;
cursor: pointer;
}
.character-toolbar-icon {
width: var(--ui-character-toolbar-icon-size);
height: var(--ui-character-toolbar-icon-size);
object-fit: contain;
}
@media (max-width: 1179px) {
.character-shell {
--ui-character-config-inline-size: clamp(332px, 34vw, 360px);
--ui-character-workspace-gap: 4px;
--ui-character-workspace-padding: 4px;
}
.character-shell--skill,
.character-shell--forge {
--ui-character-config-inline-size: clamp(380px, 40vw, 420px);
}
}
</style>
@@ -1,28 +1,24 @@
<template>
<div class="flex gap-1 h-full" v-if="configStore.result.state">
<div class="flex flex-col justify-between">
<div class="flex gap-5 items-center">
<calc-button class="flex-1" @click="setStandard">设为基准</calc-button>
<calc-button class="flex-1" @click="clearStandard">清空基准</calc-button>
</div>
<div class="flex bg-#101012 h-40px text-#6C5E4A font-bold z-10">
<div
class="flex-1 text-15px flex items-center justify-center"
@click="chooseCategory('equ')"
:class="{ active: type === 'equ' }"
>
装备
</div>
<div class="w-auto text-18px flex items-center justify-center !font-normal">|</div>
<div
class="flex-1 text-15px flex items-center justify-center"
@click="chooseCategory('oath')"
:class="{ active: type === 'oath' }"
>
誓约
</div>
<section
v-if="configStore.result.state"
class="result-shell"
:class="{ 'result-shell--buffer': configStore.result.state.buffer }"
>
<div class="result-overview">
<div class="result-overview__actions">
<UiButton class="flex-1" @click="setStandard">设为基准</UiButton>
<UiButton class="flex-1" variant="outline" @click="clearStandard">清空基准</UiButton>
</div>
<TabsRoot v-model="type" class="result-overview__tabs">
<UiTabsList aria-label="结果详情类别">
<UiTabsTrigger value="equ">装备</UiTabsTrigger>
<UiTabsTrigger value="oath">誓约</UiTabsTrigger>
</UiTabsList>
</TabsRoot>
<Info
class="result-overview__info"
:suits="configStore.result.state.suits"
:info="configStore.result.state.info"
:equs="configStore.config.equips"
@@ -43,7 +39,7 @@
:stadard="infoStore.standard"
:standard="infoStore.standard"
></BufferSkill>
</div>
</section>
</template>
<script setup lang="ts">
@@ -51,6 +47,8 @@ import { useConfigStore, useInfoStore } from '@/stores'
import Info from './components/Info.vue'
import Skill from './components/Skill.vue'
import BufferSkill from './components/BufferSkill.vue'
import { UiButton } from '@/components/ui/button'
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
const configStore = useConfigStore()
const infoStore = useInfoStore()
@@ -64,45 +62,63 @@ const setStandard = () => {
const clearStandard = () => {
infoStore.setStandard(undefined)
}
const chooseCategory = (cat: 'equ' | 'oath') => {
type.value = cat
// console.log(type.value)
}
</script>
<style lang="scss" scoped>
.active {
color: #ccc088;
.result-shell {
position: relative;
&::before {
content: '';
position: absolute;
width: 60px;
height: 15px;
opacity: 0.35;
background: #bf8d20;
filter: blur(7px);
z-index: 2;
}
&::after {
content: '';
position: absolute;
background: url('@/assets/img/active.png') no-repeat;
width: 148px;
height: 16px;
bottom: 2px;
transform: translate(0, 50%);
z-index: 4;
}
display: grid;
width: calc(
var(--ui-character-result-info-inline-size) + var(--ui-character-result-summary-inline-size) +
var(--ui-character-result-toggle-inline-size)
);
min-width: 0;
height: 100%;
min-height: 0;
grid-template-columns:
var(--ui-character-result-info-inline-size)
var(--ui-character-result-summary-inline-size);
gap: var(--ui-character-result-toggle-inline-size);
overflow: visible;
}
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
text-align: center;
.result-overview {
display: grid;
min-width: 0;
min-height: 0;
grid-template-rows: auto auto minmax(0, 1fr);
padding: var(--ui-character-result-panel-padding);
border: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
background: var(--ui-result-panel-background);
box-shadow: var(--ui-result-panel-shadow);
}
.result-overview__actions {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: var(--ui-control-gap);
margin-bottom: var(--ui-control-gap);
}
.result-overview__tabs {
min-width: 0;
}
.result-overview__info {
min-width: 0;
min-height: 0;
}
@media (min-width: 1600px) {
.result-shell {
width: calc(
var(--ui-character-result-info-inline-size) + var(--ui-character-result-full-inline-size) +
var(--ui-character-workspace-gap)
);
grid-template-columns:
var(--ui-character-result-info-inline-size)
var(--ui-character-result-full-inline-size);
gap: var(--ui-character-workspace-gap);
}
}
</style>
@@ -1,80 +1,96 @@
<template>
<div
class="bg-hex-000000/60 p-2 border-solid border-#FFFF/15 overflow-hidden flex"
v-if="data && data.length > 0"
>
<div
class="bg-hex-000000/40 text-white border-solid border-#FFFF/15 border-t-0 overflow-hidden flex-1 flex flex-col"
>
<div class="flex px-1px gap-1px">
<div
v-for="(header, key) in headers"
:key="key"
class="item-head"
:style="{ width: `${header.width}px` }"
@click="header.sortKey && sortValue(header.sortKey)"
>
{{ header.label }}
<template v-if="header.sortKey && sort === header.sortKey">
<span>{{ order === 'desc' ? '△' : '▽' }}</span>
<section v-if="rows.length > 0" class="buffer-skill-table" aria-label="辅助职业技能详情">
<div class="buffer-skill-table__surface">
<div class="buffer-skill-table__viewport">
<div class="buffer-skill-table__header buffer-skill-table__grid">
<template v-for="header in headers" :key="header.label">
<button
v-if="header.sortKey"
type="button"
class="buffer-skill-table__heading"
:aria-label="sortLabel(header.label, header.sortKey)"
:title="sortLabel(header.label, header.sortKey)"
@click="sortValue(header.sortKey)"
>
<span class="buffer-skill-table__heading-label">{{ header.label }}</span>
<span
v-if="sort === header.sortKey"
class="buffer-skill-table__sort-indicator"
aria-hidden="true"
>
{{ order === 'desc' ? '▼' : '▲' }}
</span>
</button>
<div v-else class="buffer-skill-table__heading" :title="header.label">
<span class="buffer-skill-table__heading-label">{{ header.label }}</span>
</div>
</template>
</div>
</div>
<div class="flex-1 overflow-hidden flex">
<div class="overflow-y-auto flex-1">
<div class="flex px-1px gap-1px h-8.5" v-for="(skill, _) in data" :key="_">
<div class="flex w-180px items-center gap-10px">
<div class="w-28px h-28px relative">
<img
:src="getImageURL(skill.icon)"
alt=""
:class="['h-28px w-28px', skill.type === 'passive' ? 'passive' : '']"
/>
<div v-if="skill.lv" class="skill-lv-min" :data-text="`${skill.lv}`">
{{ skill.lv }}
</div>
</div>
<div>{{ skill.name }}</div>
</div>
<div class="w-50px flex items-center justify-center">
{{ typeof skill.cd === 'number' ? `${skill.cd.toFixed(1)}s` : '-' }}
</div>
<component :is="formatDamage(skill)"></component>
<div
v-for="row in rows"
:key="row.key"
class="buffer-skill-table__row buffer-skill-table__grid"
>
<div class="buffer-skill-table__skill-cell">
<span class="buffer-skill-table__icon-wrap">
<img
:src="getImageURL(row.skill.icon)"
:alt="row.skill.name"
:class="{ 'is-passive': row.skill.type === 'passive' }"
/>
<span
v-if="row.skill.lv"
class="buffer-skill-table__level"
:data-text="`${row.skill.lv}`"
>
{{ row.skill.lv }}
</span>
</span>
<span class="buffer-skill-table__skill-name" :title="row.skill.name">
{{ row.skill.name }}
</span>
</div>
<div class="buffer-skill-table__cell" :title="row.cd">{{ row.cd }}</div>
<div
v-for="value in row.values"
:key="value.key"
class="buffer-skill-table__cell"
:class="toneClass(value.tone)"
:title="value.text"
>
{{ value.text }}
</div>
</div>
</div>
<!-- <div
v-if="!totalStandard"
class="h-40px flex items-center justify-center text-18px border-t-1px border-t-solid border-t-#FFFF/15"
>
{{ showTotal.toFixed(0) }}
</div>
<div
v-else
class="h-40px flex items-center justify-center text-18px border-t-1px border-t-solid border-t-#FFFF/15"
:style="{ color: showTotal >= 0 ? '#37fa38' : '#ff0000' }"
>
{{ showTotal.toFixed(2) }}%
</div> -->
</div>
</div>
</section>
</template>
<script setup lang="ts" name="Skill">
<script setup lang="ts" name="BufferSkill">
import type { IResultSkill, IResultSkillCount } from '@/api/calc/type'
import { useConfigStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import { h } from 'vue'
const headers: Array<{ label: string; width: number; sortKey?: string }> = [
{ label: '技能', width: 180, sortKey: 'name' },
{ label: 'CD', width: 50 },
{ label: '适用加成', width: 80, sortKey: 'MAIN' },
{ label: '三攻', width: 80, sortKey: 'ATK' },
{ label: '力智', width: 80, sortKey: 'STRINT' },
{ label: '额外增伤', width: 80, sortKey: 'ratio' },
type BuffIndex = 0 | 1 | 2 | 3
type SortKey = 'name' | 'MAIN' | 'ATK' | 'STRINT' | 'ratio'
type Tone = 'positive' | 'negative' | undefined
const headers: ReadonlyArray<{ label: string; sortKey?: SortKey }> = [
{ label: '技能', sortKey: 'name' },
{ label: 'CD' },
{ label: '适用加成', sortKey: 'MAIN' },
{ label: '三攻', sortKey: 'ATK' },
{ label: '力智', sortKey: 'STRINT' },
{ label: '额外增伤', sortKey: 'ratio' },
]
const buffIndexes = {
MAIN: 0,
ATK: 1,
STRINT: 2,
ratio: 3,
} as const satisfies Record<Exclude<SortKey, 'name'>, BuffIndex>
const props = defineProps<{
data: IResultSkill[]
standard?: {
@@ -83,165 +99,287 @@ const props = defineProps<{
}
}>()
const sort = ref<string>('STRINT')
const sort = ref<SortKey>('STRINT')
const order = ref<'asc' | 'desc'>('desc')
const config = useConfigStore()
function buffValue(skill: IResultSkill, index: BuffIndex) {
return Number(skill.buff?.[index] ?? 0)
}
const data = computed(() => {
return [...props.data].sort((a, b) => {
let aValue = 0
let bValue = 0
if (['cd', 'name'].includes(sort.value)) {
// @ts-ignore
aValue = a[sort.value]
// @ts-ignore
bValue = b[sort.value]
const sortedSkills = computed(() =>
[...props.data].sort((left, right) => {
let result: number
if (sort.value === 'name') {
result = left.name.localeCompare(right.name, 'zh-CN', {
numeric: true,
sensitivity: 'base',
})
} else {
const index = buffIndexes[sort.value]
result = buffValue(left, index) - buffValue(right, index)
}
const key = ['MAIN', 'ATK', 'STRINT', 'ratio'].findIndex((a) => sort.value == a)
aValue = a.buff?.[key]
bValue = b.buff?.[key]
return order.value === 'asc' ? result : -result
}),
)
return order.value == 'asc' ? aValue - bValue : bValue - aValue
})
})
function comparisonTone(value: number): Tone {
return value >= 0 ? 'positive' : 'negative'
}
const sortValue = (key: string) => {
if (sort.value == key) {
order.value = order.value == 'asc' ? 'desc' : 'asc'
} else {
function formatValue(value: number, index: BuffIndex, showSign: boolean) {
const rounded = Math.round(value).toLocaleString()
const sign = showSign && value >= 0 ? '+' : ''
return `${sign}${rounded}${index === 3 ? '%' : ''}`
}
const rows = computed(() =>
sortedSkills.value.map((skill, skillIndex) => {
const standardSkill = props.standard?.skills.find((item) => item.name === skill.name)
const values = ([0, 1, 2, 3] as const).map((index) => {
const current = buffValue(skill, index)
const value = standardSkill ? current - buffValue(standardSkill, index) : current
return {
key: `${skill.name}-${skill.mode ?? ''}-${index}`,
text: formatValue(value, index, Boolean(standardSkill)),
tone: standardSkill ? comparisonTone(value) : undefined,
}
})
return {
key: `${skill.name}-${skill.mode ?? ''}-${skillIndex}`,
skill,
cd:
typeof skill.cd === 'number' && Number.isFinite(skill.cd) ? `${skill.cd.toFixed(1)}s` : '-',
values,
}
}),
)
function sortValue(key: SortKey) {
if (sort.value === key) order.value = order.value === 'asc' ? 'desc' : 'asc'
else {
sort.value = key
order.value = 'desc'
}
sort.value = key
}
const skillCount = (skill: IResultSkill) => {
return computed({
get() {
let countConfig = config.skillCountConfig.find(
(a) => a.name == skill.name && a.mode == skill.mode,
)
if (!countConfig) {
countConfig = { name: skill.name, mode: skill.mode, count: 1 }
config.skillCountConfig.push(countConfig)
}
return countConfig.count
},
set(val) {
const index = config.skillCountConfig.findIndex(
(a) => a.name == skill.name && a.mode == skill.mode,
)
if (index > -1) {
config.skillCountConfig[index].count = val
} else {
config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count: val })
}
},
})
function sortLabel(label: string, key: SortKey) {
if (sort.value !== key) return `${label},点击排序`
return `${label},当前${order.value === 'desc' ? '降序' : '升序'},点击切换`
}
const total = computed(() => {
return [0, 1, 2, 3].map((index) =>
props.data.reduce((acc, skill) => {
return acc + skill.buff?.[index]
}, 0),
)
})
const totalStandard = computed(() => {
if (!props.standard) return undefined
return [0, 1, 2, 3].map((index) =>
props.standard?.skills.reduce((acc, skill) => {
return acc + skill.buff?.[index]
}, 0),
)
})
const formatDamage = (skill: IResultSkill) => {
const standardSkill = props.standard?.skills?.find((a) => a.name == skill.name)
if (standardSkill) {
return h(
'div',
{
class: 'flex items-center justify-center',
},
[0, 1, 2, 3].map((index) => {
const value = skill.buff?.[index] - standardSkill.buff?.[index]
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '80px',
color: value >= 0 ? '#37fa38' : '#ff0000',
},
},
`${value >= 0 ? '+' : ''}${
index == 3
? `${Math.round(value).toLocaleString()}%`
: Math.round(value).toLocaleString()
}`,
)
}),
)
} else {
return h(
'div',
{
class: 'flex items-center justify-center',
},
[0, 1, 2, 3].map((index) => {
const value = skill.buff?.[index]
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '80px',
},
},
index == 3
? `${Math.round(value).toLocaleString()}%`
: Math.round(value).toLocaleString(),
)
}),
)
function toneClass(tone: Tone) {
return {
'is-positive': tone === 'positive',
'is-negative': tone === 'negative',
}
}
</script>
<style scoped lang="scss">
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
.buffer-skill-table {
/* Layout token: all six tracks can shrink with the Result summary column without overflow. */
--buffer-skill-grid-columns: minmax(0, 1.5fr) minmax(0, 0.72fr) repeat(4, minmax(0, 0.95fr));
/* Spacing token: derive compact cell padding from the shared control rhythm. */
--buffer-skill-cell-padding-inline: calc(var(--ui-control-gap) / 2);
container-type: inline-size;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
}
.buffer-skill-table__surface {
display: flex;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
flex-direction: column;
padding: var(--ui-character-result-panel-padding);
overflow: hidden;
border: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
background: var(--ui-result-panel-background);
box-shadow: var(--ui-result-panel-shadow);
}
.buffer-skill-table__viewport {
min-width: 0;
min-height: 0;
flex: 1;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.buffer-skill-table__grid {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: var(--buffer-skill-grid-columns);
> * + * {
border-inline-start: var(--ui-component-stroke-size) solid var(--ui-result-divider);
}
}
.buffer-skill-table__header {
position: sticky;
z-index: 2;
top: 0;
}
.buffer-skill-table__heading {
appearance: none;
display: flex;
min-width: 0;
min-height: var(--ui-table-header-block-size);
align-items: center;
justify-content: center;
gap: var(--ui-component-stroke-size);
padding-inline: var(--buffer-skill-cell-padding-inline);
overflow: hidden;
border-top: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
border-right: 0;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
border-left: 0;
background: var(--ui-result-header-background);
color: var(--ui-result-header-foreground);
font: inherit;
text-align: center;
}
.skill-lv-min {
position: absolute;
z-index: 0;
top: -3px;
right: -4px;
font-size: 12px;
transform: scale(0.7);
color: black;
text-shadow: none;
font-weight: bolder;
font-family: Arial;
button.buffer-skill-table__heading {
cursor: pointer;
&:hover,
&:focus-visible {
color: var(--ui-primary-foreground);
filter: brightness(1.12);
}
&:focus-visible {
outline: var(--ui-focus-outline-size) solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset);
}
}
.skill-lv-min::before {
content: attr(data-text);
.buffer-skill-table__heading-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.buffer-skill-table__sort-indicator {
flex: 0 0 auto;
color: currentColor;
font-size: 0.72em;
line-height: 1;
}
.buffer-skill-table__row {
min-height: var(--ui-character-result-row-block-size);
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-result-row-background);
color: var(--ui-foreground);
&:nth-child(odd) {
background: var(--ui-result-row-alt-background);
}
&:hover {
background: var(--ui-result-row-hover-background);
}
}
.buffer-skill-table__skill-cell,
.buffer-skill-table__cell {
display: flex;
min-width: 0;
align-items: center;
padding-block: var(--ui-component-stroke-size);
padding-inline: var(--buffer-skill-cell-padding-inline);
overflow: hidden;
}
.buffer-skill-table__skill-cell {
gap: var(--ui-control-gap);
}
.buffer-skill-table__cell {
justify-content: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.buffer-skill-table__icon-wrap {
position: relative;
width: var(--ui-character-result-icon-size);
height: var(--ui-character-result-icon-size);
flex: 0 0 var(--ui-character-result-icon-size);
img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
image-rendering: pixelated;
&.is-passive {
filter: grayscale(100%) invert(3%) sepia(14%) saturate(874%) hue-rotate(65deg)
brightness(105%) contrast(102%);
}
}
}
.buffer-skill-table__level {
position: absolute;
-webkit-text-stroke: 2.5px #37fa38;
font-family: Arial;
z-index: 1;
inset-block-start: calc(var(--ui-component-stroke-size) * -4);
inset-inline-end: calc(var(--ui-component-stroke-size) * -5);
color: var(--ui-background);
font:
700 calc(var(--character-font-size, 12px) * 0.75) / 1 Arial,
sans-serif;
text-shadow: none;
z-index: -1;
&::before {
content: attr(data-text);
position: absolute;
z-index: -1;
inset: 0;
color: inherit;
font: inherit;
-webkit-text-stroke: calc(var(--ui-component-stroke-size) * 2) var(--ui-success);
text-shadow: none;
}
}
.buffer-skill-table__skill-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.buffer-skill-table__cell.is-positive {
color: var(--ui-success);
}
.buffer-skill-table__cell.is-negative {
color: var(--ui-destructive);
}
@container (max-width: 330px) {
.buffer-skill-table__surface {
--buffer-skill-grid-columns: minmax(0, 1.25fr) minmax(0, 0.68fr) repeat(4, minmax(0, 1fr));
--buffer-skill-cell-padding-inline: var(--ui-component-stroke-size);
}
}
</style>
@@ -1,156 +1,116 @@
<template>
<div class="bg-#000000bf w-266px flex-1 overflow-hidden flex flex-col">
<div class="h-177px flex-shrink-0">
<section class="result-info" aria-label="角色结果概览">
<div class="result-info__preview">
<EquList
v-show="display === 'equ'"
:detail="props.equs"
:with-sub-weapon="(infoStore.infos?.subweapons?.length ?? 0) > 0"
:avatar="props.avatar"
v-show="display == 'equ'"
></EquList>
<Oath :detail="props.oaths" v-show="display == 'oath'"></Oath>
/>
<Oath v-show="display === 'oath'" :detail="props.oaths" />
</div>
<div class="flex flex-wrap border-y-1px my-1px border-y-solid border-#FFFF/10 gap-1% px-1%">
<template v-for="info in props.info" :key="info.id">
<div
class="flex box-border mt-2px"
:style="{ width: info.value.toString().includes('/') ? '100%' : '49%' }"
<div class="result-info__stats">
<div
v-for="infoItem in props.info"
:key="infoItem.id"
class="result-info__stat"
:class="{ 'is-wide': infoItem.value.toString().includes('/') }"
>
<span>{{ infoItem.name }}</span>
<strong>{{ infoItem.value }}</strong>
</div>
</div>
<div v-if="suit || oathSuit || oathSkills.length" class="result-info__set-summary">
<div v-if="suit" class="result-info__set-row">
<img :src="getImageURL(`/equipment${suit.imageUrl}`)" alt="" />
<span class="result-info__set-name" :class="rarityClass(suit.rarity)">
{{ suit.name }} 套装
</span>
<span :class="rarityClass(suit.rarity)">
{{ suit.rarity }} {{ lvFormat(suit.level, suit.rarity) }}
</span>
<span :class="rarityClass(suit.rarity)">{{ suit.point }}点</span>
</div>
<div v-if="oathSuit" class="result-info__set-row">
<img :src="getImageURL(`/equipment${oathSuit.imageUrl}`)" alt="" />
<span class="result-info__set-name" :class="rarityClass(oathSuit.rarity)">
{{ oathSuit.name }} 誓约
</span>
<span :class="rarityClass(oathSuit.rarity)">
{{ oathSuit.rarity }} {{ lvFormat(oathSuit.level, oathSuit.rarity) }}
</span>
<span :class="rarityClass(oathSuit.rarity)">{{ oathSuit.point }}点</span>
</div>
<div v-if="oathSkills.length" class="result-info__oath-skills">
<button
v-for="skill in oathSkills"
:key="skill.id"
type="button"
class="result-info__oath-skill"
:class="{ 'is-active': skill.active }"
:disabled="!skill.enable"
:aria-label="skill.name"
:aria-pressed="skill.active"
@click="changeSkillId(skill)"
>
<div class="w-60px text-#836832">{{ info.name }}</div>
<div class="ml-auto text-#3ea74e">{{ info.value }}</div>
</div>
</template>
<span
:style="{ backgroundImage: `url(${getImageURL(skill.imageUrl)})` }"
aria-hidden="true"
></span>
</button>
</div>
</div>
<div class="border-y-1px border-y-solid my-1px border-#FFFF/10 bg-black">
<div class="flex items-center px-5px" v-if="suit">
<img
:src="getImageURL(`/equipment${suit.imageUrl}`)"
alt=""
class="w-20px h-20px object-contain"
/>
<div class="flex px-5px flex-1" :class="rarityClass(suit.rarity)">
<span class="flex-1 mr-auto block">{{ suit.name }} 套装</span>
<span class="w-50px text-right block">
{{ suit.rarity }} {{ lvFormat(suit.level, suit.rarity) }}
</span>
<span class="w-50px text-right block">{{ suit.point }}点</span>
</div>
</div>
<!-- <div class="flex justify-end mr-10px text-#836832" v-if="correction > 0">
补正:{{ correction }}点
</div> -->
<div class="flex items-center px-5px" v-if="oathSuit">
<img
:src="getImageURL(`/equipment${oathSuit.imageUrl}`)"
alt=""
class="w-20px h-20px object-contain"
/>
<div class="flex px-5px flex-1" :class="rarityClass(oathSuit.rarity)">
<span class="flex-1 mr-auto block">{{ oathSuit.name }} 誓约</span>
<span class="w-50px text-right block">
{{ oathSuit.rarity }} {{ lvFormat(oathSuit.level, oathSuit.rarity) }}
</span>
<span class="w-50px text-right block">{{ oathSuit.point }}点</span>
</div>
</div>
<div v-if="oathSkills" class="flex justify-around my-5px oath-skill">
<!-- :class="['flex-1', skill.active ? 'bg-#3ea74e text-white' : 'bg-#000000bf text-#836832']" -->
<template v-for="skill in oathSkills" :key="skill.id">
<div
class="flex items-center px-5px flex-col skill-item"
:class="{ active: skill.active, disabled: !skill.enable }"
@click="changeSkillId(skill)"
>
<div
:style="{
backgroundImage: `url(${getImageURL(`${skill.imageUrl}`)})`,
backgroundSize: 'contain',
}"
class="w-28px h-28px skill-icon"
<section class="result-info__equipped">
<h3>穿戴{{ display === 'equ' ? '装备' : '誓约' }}信息</h3>
<div class="result-info__equipped-list">
<template v-if="display === 'equ'">
<div v-for="part in partOrder" :key="part" class="result-info__equipped-row">
<img :src="getImageURL(`/equipment/part/${part}.png`)" alt="" />
<span v-if="adaptation(part) > 0" class="result-info__adaptation">
<img :src="getImageURL(`/equipment/adaptation/${adaptation(part)}.png`)" alt="" />
</span>
<span
v-if="props.equs[part]?.reinforce > 0"
:class="props.equs[part]?.reinforceType === 1 ? 'artifact' : 'advanced'"
>
+{{ props.equs[part]?.reinforce }}
</span>
<span v-if="props.equs[part]?.refine > 0" class="rare">
({{ props.equs[part]?.refine }})
</span>
<span
class="result-info__equipped-name"
:class="rarityClass(equ(props.equs?.[part]?.id)?.rarity ?? '')"
>
{{ equ(props.equs?.[part]?.id)?.name }}
</span>
</div>
</template>
<template v-else>
<div v-for="part in oathOrder" :key="part" class="result-info__equipped-row">
<img
:src="getImageURL(`/equipment/part/${part === '11' ? '誓约' : '星蕴石'}.png`)"
alt=""
/>
<!-- <span class="pt-4px">{{ skill.name }}</span> -->
<span v-if="oathAdaptation(part) > 0" class="result-info__adaptation">
<img :src="getImageURL(`/equipment/adaptation/${oathAdaptation(part)}.png`)" alt="" />
</span>
<span
class="result-info__equipped-name"
:class="rarityClass(equ(props.oaths?.[part]?.id)?.rarity ?? '')"
>
{{ equ(props.oaths?.[part]?.id)?.name }}
</span>
</div>
</template>
</div>
</div>
<div
class="border-y-1px border-y-solid my-1px border-#FFFF/10 flex-1 flex flex-col overflow-hidden"
>
<div class="flex">
<div class="flex-1 flex items-center justify-center bg-#221A08 text-#9A7E4A">
穿戴{{ display == 'equ' ? '装备' : '誓约' }}信息
</div>
</div>
<div class="flex-1 overflow-y-auto">
<template v-if="display == 'equ'">
<template v-for="part in partOrder" :key="part">
<div class="flex p-2px partItem gap-5px">
<img
:src="getImageURL(`/equipment/part/${part}.png`)"
alt=""
class="w-20px h-20px object-contain"
/>
<div class="flex items-center justify-center" v-if="adaptation(part) > 0">
<img
:src="getImageURL(`/equipment/adaptation/${adaptation(part)}.png`)"
alt=""
class="w-11px h-10px object-contain"
/>
</div>
<div
class="flex items-center"
v-if="props.equs[part]?.reinforce > 0"
:class="props.equs[part]?.reinforceType == 1 ? 'artifact' : 'advanced'"
>
+{{ props.equs[part]?.reinforce }}
</div>
<div class="rare" v-if="props.equs[part]?.refine > 0">
({{ props.equs[part]?.refine }})
</div>
<div :class="rarityClass(equ(props.equs?.[part]?.id)?.rarity ?? '')">
{{ equ(props.equs?.[part]?.id)?.name }}
</div>
</div>
</template>
</template>
<template v-else>
<template v-for="part in oathOrder" :key="part">
<div class="flex p-2px partItem gap-5px">
<img
:src="getImageURL(`/equipment/part/${part == '11' ? '誓约' : '星蕴石'}.png`)"
alt=""
class="w-20px h-20px object-contain"
/>
<div class="flex items-center justify-center" v-if="oathAdaptation(part) > 0">
<img
:src="getImageURL(`/equipment/adaptation/${oathAdaptation(part)}.png`)"
alt=""
class="w-11px h-10px object-contain"
/>
</div>
<!-- <div
class="flex items-center"
v-if="props.equs[part]?.reinforce > 0"
:class="props.equs[part]?.reinforceType == 1 ? 'artifact' : 'advanced'"
>
+{{ props.equs[part]?.reinforce }}
</div> -->
<!-- <div class="rare" v-if="props.equs[part]?.refine > 0">
({{ props.equs[part]?.refine }})
</div> -->
{{ equ(props.equs?.[part]?.id) }}
<div :class="rarityClass(equ(props.oaths?.[part]?.id)?.rarity ?? '')">
{{ equ(props.oaths?.[part]?.id)?.name }}
</div>
</div>
</template>
</template>
</div>
</div>
<!-- <div class="h-40px w-full"></div> -->
</div>
</section>
</section>
</template>
<script setup lang="ts" name="Info">
@@ -234,7 +194,8 @@ const oathSkills = computed(() => {
}))
})
const equ = (id: string) => {
const equ = (id?: string) => {
if (!id) return undefined
if (props.display == 'oath') {
// console.log(props.oaths, id)
return infoStore.oaths.find((e) => e.id == id)
@@ -276,50 +237,222 @@ const lvFormat = (lv: number, rarity: string) => {
</script>
<style scoped lang="scss">
.partItem {
.result-info {
display: flex;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
flex-direction: column;
overflow: hidden;
border-top: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
background: var(--ui-result-row-background);
color: var(--ui-foreground);
}
.result-info__preview {
display: flex;
min-height: var(--ui-equipment-preview-block-size);
flex: 0 0 var(--ui-equipment-preview-block-size);
align-items: center;
justify-content: center;
overflow: hidden;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-result-row-alt-background);
}
.partItem:nth-child(2n + 1) {
background-color: #0b090b;
.result-info__stats {
display: grid;
flex: 0 0 auto;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 3px var(--ui-control-gap);
padding: 5px 7px;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
}
.partItem:nth-child(2n) {
background-color: #000100;
}
.result-info__stat {
display: grid;
min-width: 0;
grid-template-columns: minmax(52px, auto) minmax(0, 1fr);
align-items: center;
gap: 5px;
.oath-skill {
.skill-item {
&.active {
}
&.is-wide {
grid-column: 1 / -1;
}
&:not(.active) {
filter: grayscale(100%);
}
span {
color: var(--ui-result-total-label);
}
.skill-icon {
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
&.disabled .skill-icon {
position: relative;
filter: grayscale(100%);
&::after {
content: ' ';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background-image: url('@/assets/img/oath/disabled.png');
background-size: cover;
background-position: center;
}
}
strong {
min-width: 0;
overflow: hidden;
color: var(--ui-success);
font-weight: 400;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.result-info__set-summary {
flex: 0 0 auto;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-equipment-info-background);
}
.result-info__set-row {
display: grid;
min-height: 24px;
grid-template-columns: 20px minmax(0, 1fr) auto auto;
align-items: center;
gap: 5px;
padding: 2px 5px;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
> img {
width: 20px;
height: 20px;
object-fit: contain;
image-rendering: pixelated;
}
> span:not(.result-info__set-name) {
white-space: nowrap;
}
}
.result-info__set-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-info__oath-skills {
display: flex;
align-items: center;
justify-content: space-evenly;
gap: var(--ui-control-gap);
padding: 5px;
}
.result-info__oath-skill {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px;
border: var(--ui-component-stroke-size) solid transparent;
border-radius: 50%;
outline: none;
background: transparent;
filter: grayscale(1);
cursor: pointer;
> span {
display: block;
width: var(--ui-character-result-icon-size);
height: var(--ui-character-result-icon-size);
border: var(--ui-component-stroke-size) solid var(--ui-equipment-slot-border);
border-radius: 50%;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
border-color: var(--ui-ring);
}
&.is-active {
border-color: var(--ui-selected);
filter: none;
}
&:disabled {
opacity: var(--ui-control-disabled-opacity);
cursor: default;
}
}
.result-info__equipped {
display: flex;
min-width: 0;
min-height: 0;
flex: 1;
flex-direction: column;
overflow: hidden;
h3 {
display: flex;
min-height: var(--ui-control-height-compact);
flex: 0 0 auto;
align-items: center;
justify-content: center;
margin: 0;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-result-header-background);
color: var(--ui-result-header-foreground);
font-size: inherit;
font-weight: 400;
}
}
.result-info__equipped-list {
min-width: 0;
min-height: 0;
flex: 1;
overflow-y: auto;
overscroll-behavior: contain;
}
.result-info__equipped-row {
display: flex;
min-width: 0;
min-height: 24px;
align-items: center;
gap: 5px;
padding: 2px 5px;
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-result-row-background);
&:nth-child(odd) {
background: var(--ui-result-row-alt-background);
}
> img {
width: 20px;
height: 20px;
flex: 0 0 20px;
object-fit: contain;
image-rendering: pixelated;
}
&:hover {
background: var(--ui-result-row-hover-background);
}
}
.result-info__adaptation {
display: inline-flex;
align-items: center;
justify-content: center;
img {
width: 11px;
height: 10px;
object-fit: contain;
image-rendering: pixelated;
}
}
.result-info__equipped-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -1,81 +1,138 @@
<template>
<div
class="bg-hex-000000/60 p-2 border-solid border-#FFFF/15 overflow-hidden flex"
v-if="data && data.length > 0"
<section
v-if="rows.length > 0"
class="damage-panel"
:class="{ 'is-expanded': expanded }"
aria-label="技能伤害详情"
>
<div
class="bg-hex-000000/40 text-white border-solid border-#FFFF/15 border-t-0 overflow-hidden flex-1 flex flex-col"
>
<div class="flex px-1px gap-1px">
<div
v-for="(header, key) in headers"
:key="key"
class="item-head"
:style="{ width: `${header.width}px` }"
@click="header.sortKey && sortValue(header.sortKey)"
<div class="damage-panel__surface">
<button
type="button"
class="damage-panel__toggle"
:aria-expanded="expanded"
:aria-label="expanded ? '收起完整伤害详情' : '展开完整伤害详情'"
:title="expanded ? '收起完整伤害详情' : '展开完整伤害详情'"
@click="expanded = !expanded"
>
<svg viewBox="0 0 16 28" aria-hidden="true">
<path :d="expanded ? 'M5 3l7 11-7 11' : 'M11 3L4 14l7 11'" />
</svg>
</button>
<div class="damage-panel__header damage-panel__grid">
<button
type="button"
class="damage-panel__heading damage-panel__skill-heading"
@click="sortValue('learnLv')"
>
{{ header.label }}
<template v-if="header.sortKey && sort === header.sortKey">
<span>{{ order === 'desc' ? '△' : '▽' }}</span>
</template>
</div>
<div class="item-head w-60px">占比</div>
技能
<span
v-if="sort === 'learnLv'"
class="damage-panel__sort-indicator"
:class="{ 'is-ascending': order === 'asc' }"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="damage-panel__heading damage-panel__full-column"
@click="sortValue('cd')"
>
CD
<span
v-if="sort === 'cd'"
class="damage-panel__sort-indicator"
:class="{ 'is-ascending': order === 'asc' }"
aria-hidden="true"
></span>
</button>
<div class="damage-panel__heading">次数</div>
<button
type="button"
class="damage-panel__heading damage-panel__full-column"
@click="sortValue('dps')"
>
秒伤
<span
v-if="sort === 'dps'"
class="damage-panel__sort-indicator"
:class="{ 'is-ascending': order === 'asc' }"
aria-hidden="true"
></span>
</button>
<button
type="button"
class="damage-panel__heading damage-panel__full-column"
@click="sortValue('total')"
>
总伤害
<span
v-if="sort === 'total'"
class="damage-panel__sort-indicator"
:class="{ 'is-ascending': order === 'asc' }"
aria-hidden="true"
></span>
</button>
<div class="damage-panel__heading">占比</div>
</div>
<div class="flex-1 overflow-hidden flex">
<div class="overflow-y-auto flex-1">
<div class="flex px-1px gap-1px h-8.5" v-for="(skill, _) in data" :key="_">
<div class="flex w-180px items-center gap-10px">
<div class="w-28px h-28px relative">
<img :src="getImageURL(skill.icon)" alt="" class="w-28px h-28px" />
<div v-if="skill.lv" class="skill-lv-min" :data-text="`Lv${skill.lv}`">
Lv{{ skill.lv }}
</div>
</div>
<div>{{ skill.name }} {{ !skill.mode ? '' : `[${skill.mode}]` }}</div>
</div>
<div class="w-50px flex items-center justify-center">{{ skill.cd.toFixed(1) }}s</div>
<div class="w-50px flex items-center justify-center">
<calc-input
type="number"
class="!w-40px !min-w-40px !pl-0 flex items-center justify-center"
v-model="skillCount(skill).value"
></calc-input>
</div>
<component :is="formatDamage(skill)"></component>
<div class="damage-panel__body">
<div v-for="row in rows" :key="row.key" class="damage-panel__row damage-panel__grid">
<div class="damage-panel__skill-cell">
<span class="damage-panel__icon-wrap">
<img :src="getImageURL(row.skill.icon)" :alt="row.skill.name" />
<span
v-if="row.skill.lv"
class="damage-panel__level"
:data-text="`Lv${row.skill.lv}`"
>
Lv{{ row.skill.lv }}
</span>
</span>
<span class="damage-panel__skill-name" :title="row.displayName">
{{ row.displayName }}
</span>
</div>
<div class="damage-panel__cell damage-panel__full-column">{{ row.cd }}</div>
<div class="damage-panel__cell damage-panel__count-cell">
<UiNumberField
:model-value="row.count"
:min="0"
:step="1"
:show-fast-step="false"
class="damage-panel__count-field"
@update:model-value="setSkillCount(row.skill, $event)"
/>
</div>
<div class="damage-panel__cell damage-panel__full-column" :class="toneClass(row.dpsTone)">
{{ row.dps }}
</div>
<div
class="damage-panel__cell damage-panel__full-column"
:class="toneClass(row.totalTone)"
>
{{ row.total }}
</div>
<div class="damage-panel__cell damage-panel__share">{{ row.share }}</div>
</div>
</div>
<div
v-if="!totalStandard"
class="h-40px flex items-center justify-center text-18px border-t-1px border-t-solid border-t-#FFFF/15"
>
{{ showTotal.toFixed(0) }}
</div>
<div
v-else
class="h-40px flex items-center justify-center text-18px border-t-1px border-t-solid border-t-#FFFF/15"
:style="{ color: showTotal >= 0 ? '#37fa38' : '#ff0000' }"
>
{{ showTotal.toFixed(2) }}%
</div>
<footer class="damage-panel__total" :class="toneClass(totalTone)">
<span>{{ totalStandard ? '相对基准' : '总伤害' }}</span>
<strong>{{ showTotalText }}</strong>
</footer>
</div>
</div>
</section>
</template>
<script setup lang="ts" name="Skill">
import type { IResultSkill, IResultSkillCount } from '@/api/calc/type'
import { UiNumberField } from '@/components/ui/number-field'
import { useConfigStore } from '@/stores'
import { getImageURL } from '@/utils/images'
import { h } from 'vue'
const headers: Array<{ label: string; width: number; sortKey?: string }> = [
{ label: '技能', width: 180, sortKey: 'learnLv' },
{ label: 'CD', width: 50, sortKey: 'cd' },
{ label: '次数', width: 50 },
{ label: '秒伤', width: 100, sortKey: 'dps' },
{ label: '总伤害', width: 110, sortKey: 'total' },
]
type SortKey = 'learnLv' | 'cd' | 'dps' | 'total'
type Tone = 'positive' | 'negative' | undefined
const props = defineProps<{
data: IResultSkill[]
@@ -85,215 +142,459 @@ const props = defineProps<{
}
}>()
const sort = ref<string>('learnLv')
const expanded = ref(false)
const sort = ref<SortKey>('learnLv')
const order = ref<'asc' | 'desc'>('desc')
const config = useConfigStore()
const data = computed(() => {
return props.data.sort((a, b) => {
let aValue = 0
let bValue = 0
if (['damage', 'cd', 'learnLv'].includes(sort.value)) {
// @ts-ignore
aValue = a[sort.value]
// @ts-ignore
bValue = b[sort.value]
}
if (sort.value == 'total') {
aValue = a.damage * skillCount(a).value
bValue = b.damage * skillCount(b).value
}
if (sort.value == 'dps') {
aValue = a.damage / a.cd
bValue = b.damage / b.cd
}
if (sort.value == 'name') {
// @ts-ignore
return order.value == 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue)
}
return order.value == 'asc' ? aValue - bValue : bValue - aValue
watchEffect(() => {
props.data.forEach((skill) => {
const exists = config.skillCountConfig.some(
(item) => item.name === skill.name && item.mode === skill.mode,
)
if (!exists) config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count: 1 })
})
})
const sortValue = (key: string) => {
if (sort.value == key) {
order.value = order.value == 'asc' ? 'desc' : 'asc'
} else {
order.value = 'desc'
}
sort.value = key
function skillCountValue(skill: IResultSkill) {
return (
config.skillCountConfig.find((item) => item.name === skill.name && item.mode === skill.mode)
?.count ?? 1
)
}
const skillCount = (skill: IResultSkill) => {
return computed({
get() {
let countConfig = config.skillCountConfig.find(
(a) => a.name == skill.name && a.mode == skill.mode,
)
if (!countConfig) {
countConfig = { name: skill.name, mode: skill.mode, count: 1 }
config.skillCountConfig.push(countConfig)
}
return countConfig.count
},
set(val) {
const index = config.skillCountConfig.findIndex(
(a) => a.name == skill.name && a.mode == skill.mode,
)
if (index > -1) {
config.skillCountConfig[index].count = val
} else {
config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count: val })
}
},
})
function setSkillCount(skill: IResultSkill, value: number | undefined) {
const count = typeof value === 'number' && Number.isFinite(value) ? value : 0
const target = config.skillCountConfig.find(
(item) => item.name === skill.name && item.mode === skill.mode,
)
if (target) target.count = count
else config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count })
}
const totalDamage = computed(() => {
return props.data.reduce((acc, skill) => {
return acc + skill.damage * skillCount(skill).value
}, 0)
})
const totalDamage = computed(() =>
props.data.reduce((sum, skill) => sum + skill.damage * skillCountValue(skill), 0),
)
const totalStandard = computed(() => {
if (!props.standard) return undefined
return props.standard.skills.reduce((acc, skill) => {
const standardSkillCount = props.standard?.skillCount.find(
(a) => a.name == skill.name && a.mode == skill.mode,
return props.standard.skills.reduce((sum, skill) => {
const count = props.standard?.skillCount.find(
(item) => item.name === skill.name && item.mode === skill.mode,
)?.count
return acc + skill.damage * (standardSkillCount || 0)
return sum + skill.damage * (count ?? 0)
}, 0)
})
function standardValues(skill: IResultSkill) {
const standardSkill = props.standard?.skills.find(
(item) => item.name === skill.name && item.mode === skill.mode,
)
const standardCount = props.standard?.skillCount.find(
(item) => item.name === skill.name && item.mode === skill.mode,
)?.count
if (!standardSkill || standardSkill.damage <= 0 || standardSkill.cd <= 0 || !standardCount) {
return undefined
}
return { skill: standardSkill, count: standardCount }
}
function comparisonTone(value: number): Tone {
return value >= 0 ? 'positive' : 'negative'
}
function formatPercent(value: number) {
return `${(value * 100).toFixed(2)}%`
}
function sortMetric(skill: IResultSkill, key: SortKey) {
if (key === 'total') return skill.damage * skillCountValue(skill)
if (key === 'dps') return skill.cd > 0 ? skill.damage / skill.cd : 0
return Number(skill[key] ?? 0)
}
const sortedSkills = computed(() =>
[...props.data].sort((left, right) => {
const leftValue = sortMetric(left, sort.value)
const rightValue = sortMetric(right, sort.value)
return order.value === 'asc' ? leftValue - rightValue : rightValue - leftValue
}),
)
const rows = computed(() =>
sortedSkills.value.map((skill) => {
const count = skillCountValue(skill)
const currentTotal = skill.damage * count
const currentDps = skill.cd > 0 ? skill.damage / skill.cd : 0
const standard = standardValues(skill)
let dps = Math.round(currentDps).toLocaleString()
let total = Math.round(currentTotal).toLocaleString()
let dpsTone: Tone
let totalTone: Tone
if (standard) {
const standardDps = standard.skill.damage / standard.skill.cd
const standardTotal = standard.skill.damage * standard.count
const dpsDelta = standardDps === 0 ? 0 : (currentDps - standardDps) / standardDps
const totalDelta = standardTotal === 0 ? 0 : (currentTotal - standardTotal) / standardTotal
dps = formatPercent(dpsDelta)
total = formatPercent(totalDelta)
dpsTone = comparisonTone(dpsDelta)
totalTone = comparisonTone(totalDelta)
}
const share = totalDamage.value === 0 ? 0 : currentTotal / totalDamage.value
return {
key: `${skill.name}-${skill.mode ?? ''}`,
skill,
displayName: `${skill.name}${skill.mode ? ` [${skill.mode}]` : ''}`,
count,
cd: `${skill.cd.toFixed(1)}s`,
dps,
total,
dpsTone,
totalTone,
share: formatPercent(share),
}
}),
)
const showTotal = computed(() => {
if (!totalStandard.value || totalStandard.value == 0) return totalDamage.value
if (!totalStandard.value) return totalDamage.value
return ((totalDamage.value - totalStandard.value) / totalStandard.value) * 100
})
const formatDamage = (skill: IResultSkill) => {
const standardSkill = props.standard?.skills?.find(
(a) => a.name == skill.name && a.mode == skill.mode,
)
const standardSkillCount = props.standard?.skillCount.find(
(a) => a.name == skill.name && a.mode == skill.mode,
)?.count
const rate = h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '60px',
},
},
(((skill.damage * skillCount(skill).value) / totalDamage.value) * 100).toFixed(2) + '%',
)
if (standardSkill && standardSkill.damage > 0 && standardSkill.cd > 0 && !!standardSkillCount) {
const currentSkillCount = skillCount(skill).value
const damageValue =
(skill.damage * currentSkillCount - standardSkill.damage * standardSkillCount) /
(standardSkill.damage * standardSkillCount)
const damage = h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '110px',
color: damageValue >= 0 ? '#37fa38' : '#ff0000',
},
},
(damageValue * 100).toFixed(2) + '%',
)
const dpsValue =
(skill.damage / skill.cd - standardSkill.damage / standardSkill.cd) /
(standardSkill.damage / standardSkill.cd)
const dps = h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '110px',
color: dpsValue >= 0 ? '#37fa38' : '#ff0000',
},
},
(dpsValue * 100).toFixed(2) + '%',
)
return h(
'div',
{
class: 'flex items-center justify-center',
},
[dps, damage, rate],
)
} else {
const damage = h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '110px',
},
},
Math.round(skill.damage * skillCount(skill).value).toLocaleString(),
)
const dps = h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '110px',
},
},
Math.round(skill.damage / skill.cd).toLocaleString(),
)
return h(
'div',
{
class: 'flex items-center justify-center',
},
[dps, damage, rate],
)
const showTotalText = computed(() =>
totalStandard.value
? `${showTotal.value.toFixed(2)}%`
: Math.round(showTotal.value).toLocaleString(),
)
const totalTone = computed<Tone>(() =>
totalStandard.value ? comparisonTone(showTotal.value) : undefined,
)
function toneClass(tone: Tone) {
return {
'is-positive': tone === 'positive',
'is-negative': tone === 'negative',
}
}
function sortValue(key: SortKey) {
if (sort.value === key) order.value = order.value === 'asc' ? 'desc' : 'asc'
else {
sort.value = key
order.value = 'desc'
}
}
</script>
<style scoped lang="scss">
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
.damage-panel {
position: relative;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
overflow: visible;
}
.damage-panel__surface {
position: relative;
display: flex;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
flex-direction: column;
padding: var(--ui-character-result-panel-padding);
border: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
background: var(--ui-result-panel-background);
box-shadow: var(--ui-result-panel-shadow);
}
.damage-panel__toggle {
position: absolute;
z-index: 4;
top: 50%;
left: calc(var(--ui-character-result-toggle-inline-size) * -1);
display: inline-flex;
width: var(--ui-character-result-toggle-inline-size);
height: var(--ui-character-result-toggle-block-size);
align-items: center;
justify-content: center;
padding: 0;
border: var(--ui-component-stroke-size) solid var(--ui-result-toggle-border);
border-right: 0;
background: var(--ui-result-toggle-background);
box-shadow: var(--ui-result-toggle-shadow);
color: var(--ui-result-toggle-foreground);
transform: translateY(-50%);
cursor: pointer;
svg {
width: 12px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-linecap: square;
stroke-linejoin: miter;
stroke-width: 2;
}
&:focus-visible {
outline: var(--ui-focus-outline-size) solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset);
}
&:active svg {
transform: translateY(var(--ui-pressed-offset));
}
}
.damage-panel__grid {
display: grid;
min-width: 0;
grid-template-columns:
minmax(0, 1fr)
var(--ui-character-result-count-inline-size)
var(--ui-character-result-share-inline-size);
column-gap: var(--ui-component-stroke-size);
}
.damage-panel__header {
flex: 0 0 auto;
padding-right: var(--ui-character-result-scrollbar-inline-size);
}
.damage-panel__heading {
appearance: none;
display: flex;
min-width: 0;
min-height: var(--ui-table-header-block-size);
align-items: center;
justify-content: center;
gap: 4px;
padding: 0 4px;
border: 0;
border-top: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-result-header-background);
color: var(--ui-result-header-foreground);
font: inherit;
text-align: center;
}
.skill-lv-min {
position: absolute;
z-index: 0;
top: -3px;
right: -4px;
font-size: 12px;
transform: scale(0.7);
color: black;
text-shadow: none;
font-weight: bolder;
font-family: Arial;
button.damage-panel__heading {
cursor: pointer;
&:hover,
&:focus-visible {
color: var(--ui-primary-foreground);
filter: brightness(1.12);
}
&:focus-visible {
outline: var(--ui-focus-outline-size) solid var(--ui-ring);
outline-offset: var(--ui-focus-outline-offset);
}
}
.skill-lv-min::before {
content: attr(data-text);
.damage-panel__sort-indicator {
width: 0;
height: 0;
border-right: 4px solid transparent;
border-bottom: 6px solid currentColor;
border-left: 4px solid transparent;
&.is-ascending {
border-top: 6px solid currentColor;
border-bottom: 0;
}
}
.damage-panel__body {
min-width: 0;
min-height: 0;
flex: 1;
overflow-y: scroll;
overscroll-behavior: contain;
}
.damage-panel__row {
min-height: var(--ui-character-result-row-block-size);
border-bottom: var(--ui-component-stroke-size) solid var(--ui-result-divider);
background: var(--ui-result-row-background);
color: var(--ui-foreground);
&:nth-child(even) {
background: var(--ui-result-row-alt-background);
}
&:hover {
background: var(--ui-result-row-hover-background);
}
}
.damage-panel__skill-cell,
.damage-panel__cell {
display: flex;
min-width: 0;
align-items: center;
}
.damage-panel__skill-cell {
gap: var(--ui-control-gap);
padding: 2px 5px;
}
.damage-panel__cell {
justify-content: center;
padding: 2px 4px;
border-left: var(--ui-component-stroke-size) solid var(--ui-result-divider);
white-space: nowrap;
}
// Keep compact rows to the requested skill/count/share contract. This must follow the generic
// cell display rule so hidden detail columns cannot create a second implicit grid row.
.damage-panel__full-column {
display: none;
}
.damage-panel__icon-wrap {
position: relative;
width: var(--ui-character-result-icon-size);
height: var(--ui-character-result-icon-size);
flex: 0 0 var(--ui-character-result-icon-size);
img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
image-rendering: pixelated;
}
}
.damage-panel__level {
position: absolute;
-webkit-text-stroke: 2.5px #37fa38;
font-family: Arial;
z-index: 1;
top: -4px;
right: -5px;
color: var(--ui-background);
font:
700 9px/1 Arial,
sans-serif;
text-shadow: none;
z-index: -1;
&::before {
content: attr(data-text);
position: absolute;
z-index: -1;
inset: 0;
color: inherit;
font: inherit;
-webkit-text-stroke: 2px var(--dcalc-quality-primeval);
text-shadow: none;
}
}
.damage-panel__skill-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.damage-panel__count-field {
--ui-number-field-width: 100%;
--ui-field-padding-inline: 2px;
text-align: center;
}
.damage-panel__share {
color: var(--ui-muted-foreground);
}
.damage-panel__total {
display: flex;
min-height: var(--ui-character-result-footer-block-size);
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
gap: var(--ui-control-gap);
padding: 0 clamp(10px, 0.8vw, 16px);
border-top: var(--ui-component-stroke-size) solid var(--ui-result-panel-border);
background: var(--ui-result-total-background);
color: var(--ui-result-total-label);
strong {
color: var(--ui-result-total-value);
font-size: calc(var(--character-font-size, 12px) * 1.22);
font-weight: 500;
}
}
.is-positive,
.is-positive strong {
color: var(--ui-success);
}
.is-negative,
.is-negative strong {
color: var(--ui-destructive);
}
@media (max-width: 1599px) {
.damage-panel.is-expanded .damage-panel__surface {
position: absolute;
z-index: 12;
top: 0;
right: 0;
bottom: 0;
width: var(--ui-character-result-full-inline-size);
}
.damage-panel.is-expanded .damage-panel__grid {
grid-template-columns:
minmax(180px, 1fr)
var(--ui-character-result-cd-inline-size)
var(--ui-character-result-count-inline-size)
minmax(74px, 0.9fr)
minmax(74px, 1fr)
var(--ui-character-result-share-inline-size);
}
.damage-panel.is-expanded .damage-panel__full-column {
display: flex;
}
}
@media (min-width: 1600px) {
.damage-panel__toggle {
display: none;
}
.damage-panel__grid {
grid-template-columns:
minmax(180px, 1fr)
var(--ui-character-result-cd-inline-size)
var(--ui-character-result-count-inline-size)
minmax(var(--ui-character-result-value-min-inline-size), 0.9fr)
minmax(var(--ui-character-result-value-min-inline-size), 1fr)
var(--ui-character-result-share-inline-size);
}
.damage-panel__full-column {
display: flex;
}
}
@media (prefers-reduced-motion: reduce) {
.damage-panel__toggle svg {
transition: none;
}
}
</style>
@@ -1,8 +1,7 @@
import { asyncComputed } from '@vueuse/core'
import api from '@/api'
import './style.scss'
import CalcTabs from '@/components/calc/tabs/index.vue'
import CalcTab from '@/components/calc/tab/index.vue'
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
export default defineComponent({
name: 'SkillDetail',
@@ -88,11 +87,11 @@ export default defineComponent({
const renderSkillDetail = () => (
<>
<div class="skill_title">基本信息</div>
<div class="flex flex-col gap-2px">
<div class="skill-detail__stack">
{basic.value.map(
(item) =>
!!item.value && (
<div class="flex justify-between item">
<div class="item skill-detail__row">
<span class="label">{item.label}</span>
<span class="value">{item.value}</span>
</div>
@@ -100,25 +99,23 @@ export default defineComponent({
)}
</div>
<div class="skill_title">技能描述</div>
<div class="h-auto max-h-200px">
<div class="h-full overflow-y-auto bg-#161816 p-2px">
<div class="skill-detail__description-frame">
<div class="skill-detail__scroll-content">
{desc.value?.map((item) => (
// item.startsWith('[') || item.startsWith('-') || item.startsWith('<') ? (
// <div class="min-h-10px">{item}</div>
// ) :
<div class="min-h-10px" style="white-space:pre-wrap;">
{item}
</div>
<div class="skill-detail__description-line">{item}</div>
))}
</div>
</div>
{detail.value.length > 0 && (
<>
<div class="skill_title">技能属性</div>
<div class="flex-1 overflow-y-auto">
<div class=" flex flex-col gap-2px">
<div class="skill-detail__flex-scroll">
<div class="skill-detail__stack">
{detail.value.map((item) => (
<div class="flex justify-between item items-center" v-html={item}></div>
<div class="item skill-detail__row skill-detail__row--center" v-html={item}></div>
))}
</div>
</div>
@@ -129,17 +126,15 @@ export default defineComponent({
const renderSkillVp = () => (
<>
<div class="h-auto">
<div class="h-full flex flex-col gap-5px">
<div class="skill-detail__evolution">
<div class="skill-detail__evolution-list">
{skillInfo.value?.evolution?.map((item) => (
<>
<div class="flex flex-col gap-2px">
<div class="skill-detail__stack">
<div class="skill_vp_title">{item.name}</div>
<div class="skill_title">技能描述</div>
<div class="h-auto bg-#161816 max-h-300px overflow-y-auto">
<div class="h-auto p-2px" style="white-space: pre-wrap;">
{item.descDetail}
</div>
<div class="skill-detail__evolution-scroll">
<div class="skill-detail__evolution-copy">{item.descDetail}</div>
</div>
</div>
</>
@@ -158,21 +153,17 @@ export default defineComponent({
return () => (
<>
<div class="flex flex-col gap-2px mx-2px text-#907B54 h-full">
<div class="skill_name h-20px">{skillInfo.value?.name}</div>
<div class="skill-detail">
<div class="skill_name">{skillInfo.value?.name}</div>
<div class="relative">
<CalcTabs v-model={curTab.value}>
<CalcTab class="flex-1 flex justify-center" value={0}>
技能信息
</CalcTab>
<CalcTab
class="flex-1 flex justify-center"
value={1}
disabled={!skillInfo.value?.evolution?.length}
>
技能进化
</CalcTab>
</CalcTabs>
<TabsRoot v-model={curTab.value}>
<UiTabsList>
<UiTabsTrigger value={0}>技能信息</UiTabsTrigger>
<UiTabsTrigger value={1} disabled={!skillInfo.value?.evolution?.length}>
技能进化
</UiTabsTrigger>
</UiTabsList>
</TabsRoot>
</div>
{curTab.value === 0 && renderSkillDetail()}
{curTab.value === 1 && renderSkillVp()}
@@ -1,3 +1,73 @@
.skill-detail {
display: flex;
height: 100%;
margin-inline: 2px;
flex-direction: column;
gap: 2px;
color: #907b54;
}
.skill-detail__stack {
display: flex;
flex-direction: column;
gap: 2px;
}
.skill-detail__row {
display: flex;
justify-content: space-between;
&--center {
align-items: center;
}
}
.skill-detail__description-frame {
height: auto;
max-height: 200px;
}
.skill-detail__scroll-content,
.skill-detail__evolution-scroll {
height: 100%;
padding: 2px;
overflow-y: auto;
background: #161816;
}
.skill-detail__description-line {
min-height: 10px;
white-space: pre-wrap;
}
.skill-detail__flex-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
}
.skill-detail__evolution {
height: auto;
}
.skill-detail__evolution-list {
display: flex;
height: 100%;
flex-direction: column;
gap: 5px;
}
.skill-detail__evolution-scroll {
height: auto;
max-height: 300px;
}
.skill-detail__evolution-copy {
height: auto;
padding: 2px;
white-space: pre-wrap;
}
.skill_name {
border-width: 1px;
border-style: solid;
@@ -1,7 +1,11 @@
import { getImageURL } from '@/utils/images'
import type { ISkill } from '@/api/info/type'
import { useVModel } from '@vueuse/core'
import CalcButton from '@/components/calc/button/index.vue'
import { UiButton } from '@/components/ui/button'
import addIcon from './img/add.svg'
import maxIcon from './img/max.svg'
import minIcon from './img/min.svg'
import subIcon from './img/sub.svg'
import './style.scss'
export default defineComponent({
@@ -101,17 +105,14 @@ export default defineComponent({
if (info?.lv > skill.maxLearnLv) color = '#58E0FC'
return (
<>
<div class="relative w-35px h-55px">
<div class="flex w-full items-center justify-center gap-2px absolute top--18px left-1px">
<div class="skill-tree__slot">
<div class="skill-tree__enhancements">
{info?.vp > 0 && <div class={`vp_${info.vp}`}></div>}
{info?.up > 0 && <div class={`up_${info.up}`}></div>}
</div>
{activeSkill.value == skill.id && renderSkillAction(skill)}
<div
class={[
'w-36px absolute h-48px flex flex-col box-border items-center py-3px z-2',
skill.learnLv == 100 && skill.bind ? '' : 'skill',
]}
class={['skill-tree__skill', skill.learnLv == 100 && skill.bind ? '' : 'skill']}
onClick={(e) => {
e.stopPropagation()
chooseSkill(skill)
@@ -124,42 +125,30 @@ export default defineComponent({
key={index}
>
<img
class={['h-28px w-28px', skill.type === 'passive' ? 'passive' : '']}
class={['skill-tree__icon', skill.type === 'passive' ? 'passive' : '']}
src={getImageURL(skill.icon)}
alt={skill.name}
></img>
<div class="w-100% flex flex-1 justify-center" style={{ color }}>
<div class="skill-tree__level" style={{ color }}>
{info?.lv ?? 0}
</div>
</div>
{skill.learnLv == 100 && skill.bind && (
<>
<div
class={`pos-absolute w-196px h-54px box-border translate-x--50% left-18px top--3px flex justify-between items-center py-2px px-3px awake_${bindAwake.value}`}
>
<div
class="w-36px flex flex-col box-border items-center py-2px z-2"
onClick={() => chooseBindAwake(50)}
>
<div class={`skill-tree__awake awake_${bindAwake.value}`}>
<div class="skill-tree__awake-option" onClick={() => chooseBindAwake(50)}>
<img
class="h-28px w-28px mt-2px"
class="skill-tree__icon skill-tree__awake-icon"
src={getImageURL(awakeSkill_1.value?.icon ?? '')}
></img>
<div class="w-100% mt-1px flex items-center justify-center text-#ffc701">
1次
</div>
<div class="skill-tree__awake-label">1次</div>
</div>
<div
class="w-36px flex flex-col box-border items-center py-2px z-2"
onClick={() => chooseBindAwake(85)}
>
<div class="skill-tree__awake-option" onClick={() => chooseBindAwake(85)}>
<img
class="h-28px w-28px mt-1px"
class="skill-tree__icon skill-tree__awake-icon"
src={getImageURL(awakeSkill_2.value?.icon ?? '')}
></img>
<div class="w-100% mt-1px flex items-center justify-center text-#ffc701">
2次
</div>
<div class="skill-tree__awake-label">2次</div>
</div>
</div>
</>
@@ -172,39 +161,55 @@ export default defineComponent({
const renderSkillAction = (skill: ISkill) => (
<>
<>
<div class="absolute skill-active w-88px h-78px translate-x--50% left-16px top--1px z-5">
<div class="flex justify-between items-center px-5px relative top-54px">
<CalcButton
icon="min"
<div class="skill-active skill-tree__actions">
<div class="skill-tree__action-list">
<UiButton
size="icon"
class="skill-action-button"
aria-label="技能等级归零"
disabled={lvInfo.value[skill.id.toString()]?.lv == 0}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'subMax')
}}
></CalcButton>
<CalcButton
icon="reduce"
>
<img data-icon src={minIcon} alt="" />
</UiButton>
<UiButton
size="icon"
class="skill-action-button"
aria-label="技能等级减一"
disabled={lvInfo.value[skill.id.toString()]?.lv == 0}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'sub')
}}
></CalcButton>
<CalcButton
icon="increase"
>
<img data-icon src={subIcon} alt="" />
</UiButton>
<UiButton
size="icon"
class="skill-action-button"
aria-label="技能等级加一"
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'add')
}}
></CalcButton>
<CalcButton
icon="max"
>
<img data-icon src={addIcon} alt="" />
</UiButton>
<UiButton
size="icon"
class="skill-action-button"
aria-label="技能等级加满"
disabled={lvInfo.value[skill.id.toString()]?.lv >= skill.maxLearnLv}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'addMax')
}}
></CalcButton>
>
<img data-icon src={maxIcon} alt="" />
</UiButton>
{/* <img
src={`${new URL('./img/min.svg', import.meta.url).href}`}
onClick={(e: Event) => {
@@ -241,11 +246,11 @@ export default defineComponent({
return () => (
<>
<div onClick={() => (activeSkill.value = -1)}>
<div class="skill-tree" onClick={() => (activeSkill.value = -1)}>
{((getSkill(0, 15, -1) ?? []) as ISkill[]).length > 0 && (
<>
<div class="skill-tree-line flex items-center gap-8px px-5px">
<div class="w-34px h-34px flex items-center justify-center text-white">
<div class="skill-tree-line">
<div class="skill-tree__row-label">
其<br />余
</div>
{((getSkill(0, 15, -1) ?? []) as ISkill[])?.map((skill, index) =>
@@ -257,17 +262,21 @@ export default defineComponent({
{lvList.map((lv, index) =>
((getSkill(lv, lvList[index + 1] ?? 150, -1) as ISkill[])?.length ?? 0) > 0 ? (
<div class="skill-tree-line flex items-center gap-8px px-5px" key={index}>
<div class="skill-tree-line" key={index}>
{lv == 1 || lv % 10 == 0 ? (
<div class="w-34px h-34px flex items-center justify-center text-white">{lv}</div>
<div class="skill-tree__row-label">{lv}</div>
) : (
<div class="w-34px h-34px"></div>
<div class="skill-tree__placeholder"></div>
)}
{columns.map((column) => {
const skill = getSkill(lv, lvList[index + 1] ?? 150, column) as ISkill
return skill ? renderSkill(skill, index) : <div class="w-34px h-34px"></div>
return skill ? (
renderSkill(skill, index)
) : (
<div class="skill-tree__placeholder"></div>
)
})}
<div class="w-5px h-34px" onClick={() => (activeSkill.value = -1)}></div>
<div class="skill-tree__row-end" onClick={() => (activeSkill.value = -1)}></div>
</div>
) : (
<></>
@@ -1,6 +1,16 @@
.skill-tree {
/* Preserve the original 11-column tree rhythm while allowing its host to scroll on narrow screens. */
min-width: 550px;
}
.skill-tree-line {
display: flex;
height: 80px;
width: auto;
align-items: center;
gap: 8px;
padding-inline: 5px;
&:nth-child(n + 1) {
background-color: #000000;
}
@@ -9,6 +19,130 @@
}
}
.skill-tree__slot {
position: relative;
width: 35px;
height: 55px;
flex: 0 0 35px;
}
.skill-tree__enhancements {
position: absolute;
top: -18px;
left: 1px;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
gap: 2px;
}
.skill-tree__skill {
position: absolute;
z-index: 2;
display: flex;
box-sizing: border-box;
width: 36px;
height: 48px;
padding-block: 3px;
align-items: center;
flex-direction: column;
cursor: pointer;
}
.skill-tree__icon {
display: block;
width: 28px;
height: 28px;
object-fit: contain;
}
.skill-tree__level {
display: flex;
width: 100%;
min-height: 0;
flex: 1;
justify-content: center;
}
.skill-tree__awake {
position: absolute;
top: -3px;
left: 18px;
display: flex;
box-sizing: border-box;
width: 196px;
height: 54px;
padding: 2px 3px;
align-items: center;
justify-content: space-between;
transform: translateX(-50%);
}
.skill-tree__awake-option {
position: relative;
z-index: 2;
display: flex;
box-sizing: border-box;
width: 36px;
padding-block: 2px;
align-items: center;
flex-direction: column;
cursor: pointer;
}
.skill-tree__awake-icon {
margin-top: 1px;
}
.skill-tree__awake-label {
display: flex;
width: 100%;
margin-top: 1px;
align-items: center;
justify-content: center;
color: #ffc701;
}
.skill-tree__actions {
position: absolute;
z-index: 5;
top: -1px;
left: 16px;
width: 88px;
height: 78px;
transform: translateX(-50%);
}
.skill-tree__action-list {
position: relative;
top: 54px;
display: flex;
padding-inline: 5px;
align-items: center;
justify-content: space-between;
}
.skill-tree__row-label,
.skill-tree__placeholder {
display: flex;
width: 34px;
height: 34px;
flex: 0 0 34px;
align-items: center;
justify-content: center;
}
.skill-tree__row-label {
color: #fff;
}
.skill-tree__row-end {
width: 5px;
height: 34px;
flex: 0 0 5px;
}
.passive {
filter: grayscale(100%) invert(3%) sepia(14%) saturate(874%) hue-rotate(65deg) brightness(105%)
contrast(102%);
@@ -24,6 +158,18 @@
background-size: 100% 100%;
}
.skill-action-button {
--ui-control-height-default: 17px;
border-color: transparent;
img {
width: 11px;
height: 11px;
object-fit: contain;
}
}
.awake_50 {
background-image: url('./img/awake_50.png');
background-size: 100% 100%;
@@ -0,0 +1,161 @@
<script setup lang="ts">
import { computed, nextTick, ref, type CSSProperties } from 'vue'
import { UiButton } from '@/components/ui/button'
import { UiItemSlot } from '@/components/ui/item-slot'
import { PopoverRoot, PopoverTrigger, UiPopoverContent } from '@/components/ui/popover'
export interface SkillUpIconOption {
label: string
src?: string
value: string
}
const props = withDefaults(
defineProps<{
columnCount?: number
disabled?: boolean
label: string
options?: SkillUpIconOption[]
}>(),
{
columnCount: 4,
disabled: false,
options: () => [],
},
)
const model = defineModel<string>()
const open = ref(false)
const gridElement = ref<HTMLElement>()
const selectedOption = computed(() => props.options.find((option) => option.value === model.value))
const gridStyle = computed(
() =>
({
'--skill-up-icon-columns': Math.max(1, props.columnCount),
}) as CSSProperties,
)
function optionButtons() {
return Array.from(
gridElement.value?.querySelectorAll<HTMLButtonElement>('[data-skill-up-option]') ?? [],
).filter((button) => !button.disabled)
}
function focusInitialOption(event: Event) {
event.preventDefault()
nextTick(() => {
const buttons = optionButtons()
const selected = buttons.find((button) => button.dataset.selected === 'true')
;(selected ?? buttons[0])?.focus()
})
}
function handleGridKeydown(event: KeyboardEvent) {
const buttons = optionButtons()
const currentIndex = buttons.findIndex((button) => button === document.activeElement)
if (currentIndex < 0) return
let nextIndex = currentIndex
if (event.key === 'ArrowRight') nextIndex += 1
else if (event.key === 'ArrowLeft') nextIndex -= 1
else if (event.key === 'ArrowDown') nextIndex += props.columnCount
else if (event.key === 'ArrowUp') nextIndex -= props.columnCount
else if (event.key === 'Home') nextIndex = 0
else if (event.key === 'End') nextIndex = buttons.length - 1
else return
event.preventDefault()
buttons[Math.min(buttons.length - 1, Math.max(0, nextIndex))]?.focus()
}
function selectOption(value: string) {
model.value = value
open.value = false
}
</script>
<template>
<PopoverRoot v-model:open="open">
<PopoverTrigger as-child>
<UiButton
class="skill-up-icon-select__trigger"
size="icon"
variant="ghost"
:disabled="disabled"
:aria-label="`${label}:${selectedOption?.label ?? '未选择'}`"
:title="selectedOption?.label ?? label"
>
<UiItemSlot :src="selectedOption?.src" :label="selectedOption?.label ?? `${label}未选择`" />
</UiButton>
</PopoverTrigger>
<UiPopoverContent
class="skill-up-icon-select__content"
align="start"
side="bottom"
@open-auto-focus="focusInitialOption"
>
<div
ref="gridElement"
class="skill-up-icon-select__grid"
role="grid"
:aria-label="`${label}候选技能`"
:style="gridStyle"
@keydown="handleGridKeydown"
>
<UiButton
v-for="option in options"
:key="option.value"
class="skill-up-icon-select__option"
size="icon"
variant="ghost"
role="gridcell"
data-skill-up-option
:data-selected="option.value === model ? 'true' : 'false'"
:aria-label="option.label"
:aria-pressed="option.value === model"
:title="option.label"
@click="selectOption(option.value)"
>
<UiItemSlot
:src="option.src"
:label="option.label"
:emphasized="option.value === model"
/>
</UiButton>
</div>
</UiPopoverContent>
</PopoverRoot>
</template>
<style scoped>
.skill-up-icon-select__trigger,
.skill-up-icon-select__option {
--ui-control-height-default: var(--ui-item-slot-size);
}
.skill-up-icon-select__trigger {
display: inline-flex;
}
.skill-up-icon-select__content {
width: max-content;
max-width: min(var(--ui-popover-inline-size), calc(100vw - 16px));
}
.skill-up-icon-select__grid {
display: grid;
max-height: var(--ui-autocomplete-max-block-size);
grid-template-columns: repeat(var(--skill-up-icon-columns), var(--ui-item-slot-size));
gap: var(--ui-control-gap);
overflow: auto;
}
.skill-up-icon-select__option[data-selected='true'] {
outline: 1px solid var(--ui-ring);
outline-offset: 1px;
background: var(--ui-option-highlight-background);
}
</style>
@@ -1,12 +1,14 @@
import { getImageURL } from '@/utils/images'
import type { ISkill } from '@/api/info/type'
import { computed, defineComponent, type PropType } from 'vue'
import { useVModel } from '@vueuse/core'
import CalcButton from '@/components/calc/button/index.vue'
import CaclIconSelect from '@/components/calc/iconselect/index.vue'
import CalcOption from '@/components/calc/option/index.vue'
import { renderList } from 'vue'
import type { ISkill } from '@/api/info/type'
import { UiButton } from '@/components/ui/button'
import { UiItemSlot } from '@/components/ui/item-slot'
import { getImageURL } from '@/utils/images'
import SkillUpIconSelect, { type SkillUpIconOption } from './SkillUpIconSelect.vue'
import './style.scss'
type SkillLevelInfo = Record<string, { lv: number; up: number }>
export default defineComponent({
name: 'SkillTree',
props: {
@@ -15,7 +17,7 @@ export default defineComponent({
default: () => [],
},
lvInfo: {
type: Object as PropType<Record<string, { lv: number; up: number }>>,
type: Object as PropType<SkillLevelInfo>,
default: () => ({}),
},
buffer: {
@@ -25,283 +27,190 @@ export default defineComponent({
},
setup(props) {
const lvInfo = useVModel(props, 'lvInfo')
const selectedSkillIds = computed(() =>
Object.keys(lvInfo.value).filter(
(skillId) => !!lvInfo.value[skillId].up && lvInfo.value[skillId].up > 0,
),
)
const skills = (index: number = 0) => {
index -= 1
const upList = Object.keys(lvInfo.value).filter(
(a) => !!lvInfo.value[a].up && lvInfo.value[a].up > 0,
)
const currentUPSkill = index >= 0 ? upList[index] : undefined
const rowIndex = index - 1
const currentUPSkill = rowIndex >= 0 ? selectedSkillIds.value[rowIndex] : undefined
return computed(() =>
props.skills
.filter(
(a) =>
a.id.toString() === currentUPSkill ||
(a.hasUP &&
(!lvInfo.value[a.id]?.up || lvInfo.value[a.id]?.up == 0) &&
lvInfo.value[a.id].lv > 0),
(skill) =>
skill.id.toString() === currentUPSkill ||
(skill.hasUP &&
(!lvInfo.value[skill.id]?.up || lvInfo.value[skill.id]?.up === 0) &&
lvInfo.value[skill.id].lv > 0),
)
.sort((a, b) => a.learnLv - b.learnLv),
.sort((left, right) => left.learnLv - right.learnLv),
)
}
const itemChecked = (index: number, up: number) => {
index -= 1
const rowIndex = index - 1
return computed<string | undefined>({
get() {
const upList = Object.keys(lvInfo.value).filter(
(a) => !!lvInfo.value[a].up && lvInfo.value[a].up > 0,
)
const currentUPSkill = upList[index]
const currentUPSkill = selectedSkillIds.value[rowIndex]
const currentUP = lvInfo.value[currentUPSkill]?.up
return currentUP == up ? currentUPSkill : undefined
return currentUP === up ? currentUPSkill : undefined
},
set(value: string | undefined) {
set(value) {
if (!value) return
const upList = Object.keys(lvInfo.value).filter(
(a) => !!lvInfo.value[a].up && lvInfo.value[a].up > 0,
)
const currentUPSkill = upList[index]
if (currentUPSkill) {
lvInfo.value[currentUPSkill].up = 0
}
lvInfo.value[value].up = up
const currentUPSkill = selectedSkillIds.value[rowIndex]
if (currentUPSkill) lvInfo.value[currentUPSkill].up = 0
if (value === '-1') return
const target = lvInfo.value[value]
if (target) target.up = up
},
})
}
const exchange = (index: number) => {
const skill = itemChecked(index, 1).value || itemChecked(index, 2).value
if (!skill) return
lvInfo.value[skill].up = lvInfo.value[skill].up == 1 ? 2 : 1
const iconOptions = (index: number, clearable = false): SkillUpIconOption[] => {
const options = skills(index).value.map((skill) => ({
label: skill.name,
src: getImageURL(skill.icon),
value: skill.id.toString(),
}))
return clearable ? [{ label: '清空技能', value: '-1' }, ...options] : options
}
return () => (
<>
{lvInfo.value && Object.keys(lvInfo.value).length > 0 ? (
<div class="flex bg-black">
<div class="skill-up flex justify-between">
<div class="left">
<div class="w-full">
<div class="flex justify-between">
<div class="up_head_1">
<div class="icon"></div>
<div class="text-14px font-bold text-center">技能攻击力增加</div>
</div>
<div class="up_head_2">
<div class="icon"></div>
<div class="text-14px font-bold text-center">
技能冷却时间减少
<br />
技能攻击力增加
</div>
</div>
</div>
<div class="flex flex-col gap-5px px-20px py-10px">
{renderList(3, (index) => (
<>
<div class="flex items-center w-full justify-between">
<div class={`up_${!itemChecked(index, 1).value ? index : 'active'}`}>
<CaclIconSelect
v-model:modelValue={itemChecked(index, 1).value}
class="!bg-white/0 !border-white/0"
columnNum={4}
>
{
<CalcOption value={'-1'}>
<div class="w-28px h-28px"></div>
</CalcOption>
}
{renderList(skills(index).value, (item) => (
<>
<CalcOption value={item.id.toString()}>
<img
class="mt-2px"
src={getImageURL(item.icon)}
alt={item.name}
/>
</CalcOption>
</>
))}
</CaclIconSelect>
</div>
<CalcButton icon="exchange" onClick={() => exchange(index)}></CalcButton>
<div class={`up_${!itemChecked(index, 2).value ? index : 'active'}`}>
<CaclIconSelect
v-model:modelValue={itemChecked(index, 2).value}
class=" !bg-white/0 !border-white/0"
columnNum={4}
>
{renderList(skills(index).value, (item) => (
<>
<CalcOption value={item.id.toString()}>
<img
class="mt-2px"
src={getImageURL(item.icon)}
alt={item.name}
/>
</CalcOption>
</>
))}
</CaclIconSelect>
</div>
</div>
</>
))}
const exchange = (index: number) => {
const skillId = itemChecked(index, 1).value || itemChecked(index, 2).value
if (!skillId) return
lvInfo.value[skillId].up = lvInfo.value[skillId].up === 1 ? 2 : 1
}
const renderUpgradeSlot = (index: number, up: number, clearable = false) => {
const selection = itemChecked(index, up)
return (
<div class={selection.value ? 'up_active' : `up_${index}`}>
<SkillUpIconSelect
modelValue={selection.value}
onUpdate:modelValue={(value) => (selection.value = value)}
label={`第 ${index} 组${up === 1 ? '技能攻击力' : '冷却与攻击力'}技能`}
options={iconOptions(index, clearable)}
columnCount={4}
/>
</div>
)
}
const skillDescription = (skill: ISkill) => {
let description = ''
const up = lvInfo.value[skill.id.toString()].up
if (up === 1) {
description =
skill.upType === 'damage'
? `技能攻击力 +${skill.learnLv < 35 ? '60' : 55}%`
: '生命值恢复量 +20%'
} else if (up === 2) {
description =
skill.upType === 'damage'
? `技能冷却时间 -15%<br/>技能攻击力 +${skill.learnLv < 35 ? '43' : 38}%`
: '技能冷却时间 -10%<br/>生命值恢复量 +5%'
}
return props.buffer ? `${description} / 主属性 +40` : description
}
return () => {
if (!lvInfo.value || Object.keys(lvInfo.value).length === 0) return null
const targetSkills = skills().value
const emptyTargetCount = Math.max(0, 36 - targetSkills.length)
return (
<div class="skill-up-shell">
<div class="skill-up">
<section class="skill-up__left" aria-label="技能突破互换">
<div class="skill-up__heads">
<div class="up_head_1">
<div class="icon" aria-hidden="true"></div>
<div class="skill-up__head-label">技能攻击力增加</div>
</div>
<div class="up_head_2">
<div class="icon" aria-hidden="true"></div>
<div class="skill-up__head-label">
技能冷却时间减少
<br />
技能攻击力增加
</div>
</div>
</div>
<div class="right">
<div class="title text-16px text-center leading-48px">目标技能</div>
<div class="flex flex-wrap gap-8px content-start h-110px overflow-y-auto mb-8px w-245px m-auto">
{renderList(skills().value, (item) => (
<div
key={item.id}
class="w-[28px] h-[28px] rounded-[1px] border-[1px] border-solid border-[#6d4e2b] bg-[linear-gradient(180deg,_#191919_0%,_#000000_100%)] [box-shadow:0px_4px_4px_#00000040_inset] rounded-4px"
<div class="skill-up__swap-list">
{[1, 2, 3].map((index) => (
<div class="skill-up__swap-row" key={index}>
{renderUpgradeSlot(index, 1, true)}
<UiButton
size="icon"
variant="ghost"
aria-label={`互换第 ${index} 组技能效果`}
title={`互换第 ${index} 组技能效果`}
onClick={() => exchange(index)}
>
<img src={getImageURL(item.icon)} alt={item.name} />
</div>
))}
{renderList(36 - skills().value.length, (item) => (
<div
key={item}
class="w-[28px] h-[28px] rounded-[1px] border-[1px] border-solid border-[#6d4e2b] bg-[linear-gradient(180deg,_#191919_0%,_#000000_100%)] [box-shadow:0px_4px_4px_#00000040_inset] rounded-4px"
></div>
))}
</div>
<div class="flex flex-col gap-10px">
{renderList(
Object.keys(lvInfo.value).filter(
(a) => !!lvInfo.value[a].up && lvInfo.value[a].up > 0,
),
(index) => {
const skill = props.skills.find((a) => a.id.toString() === index)
let tooltip = ''
if (skill) {
if (lvInfo.value[skill.id.toString()].up == 1) {
if (skill.upType == 'damage') {
tooltip = `技能攻击力 +${skill.learnLv < 35 ? '60' : 55}%`
} else {
tooltip = `生命值恢复量 +20%`
}
}
if (lvInfo.value[skill.id.toString()].up == 2) {
if (skill.upType == 'damage') {
tooltip = `技能冷却时间 -15%<br/>技能攻击力 +${skill.learnLv < 35 ? '43' : 38}%`
} else {
tooltip = `技能冷却时间 -10%<br/>生命值恢复量 +5%`
}
}
}
if (props.buffer) {
tooltip += ` / 主属性 +40`
}
return skill ? (
<div class="flex gap-10px items-center bg-[linear-gradient(180deg,_#1b1c1c_0%,_#000000_100%)] rounded-4px p-5px border-1px border-solid border-#2E2E2E">
<div class="up_1 !w-38px !h-38px">
<img src={getImageURL(skill?.icon)} alt={skill.name} />
</div>
<div class="text-#947E4E" v-html={tooltip}></div>
</div>
) : (
<></>
)
},
)}
</div>
</div>
{/* <div class="absolute left-76px top-137px w-250px flex flex-col gap-21.5px">
{renderList(3, (index) => (
<>
<div class="w-169px h-36px flex items-center justify-between">
<div class="m-4px w-28px h-28px">
<CaclIconSelect
v-model:modelValue={itemChecked(index, 1).value}
class="!bg-white/0 !border-white/0 m-4px"
columnNum={4}
>
{renderList(skills(index).value, (item) => (
<>
<CalcOption value={item.id.toString()}>
<img src={getImageURL(item.icon)} alt={item.name} />
</CalcOption>
</>
))}
</CaclIconSelect>
</div>
<CalcButton icon="exchange" onClick={() => exchange(index)}></CalcButton>
<div class="m-4px w-28px h-28px">
<CaclIconSelect
v-model:modelValue={itemChecked(index, 2).value}
class=" !bg-white/0 !border-white/0"
columnNum={4}
>
{renderList(skills(index).value, (item) => (
<>
<CalcOption value={item.id.toString()}>
<img src={getImageURL(item.icon)} alt={item.name} />
</CalcOption>
</>
))}
</CaclIconSelect>
</div>
</div>
</>
<span class="icon-exchange" data-icon aria-hidden="true"></span>
</UiButton>
{renderUpgradeSlot(index, 2)}
</div>
))}
</div>
<div class="absolute top-70px left-310px w-320px h-270px py-5px px-30px box-border flex flex-col">
<div class="flex flex-wrap gap-10px content-start h-120px overflow-y-auto mb-10px">
{renderList(skills().value, (item) => (
<div
key={item.id}
class="w-[28px] h-[28px] rounded-[1px] border-[1px] border-solid border-[#6d4e2b] bg-[linear-gradient(180deg,_#191919_0%,_#000000_100%)] [box-shadow:0px_4px_4px_#00000040_inset] rounded-4px"
>
<img src={getImageURL(item.icon)} alt={item.name} />
</div>
))}
{renderList(36 - skills().value.length, (item) => (
<div
key={item}
class="w-[28px] h-[28px] rounded-[1px] border-[1px] border-solid border-[#6d4e2b] bg-[linear-gradient(180deg,_#191919_0%,_#000000_100%)] [box-shadow:0px_4px_4px_#00000040_inset] rounded-4px"
></div>
))}
</div>
<div class="flex flex-col gap-10px">
{renderList(
Object.keys(lvInfo.value).filter(
(a) => !!lvInfo.value[a].up && lvInfo.value[a].up > 0,
),
(index) => {
const skill = props.skills.find((a) => a.id.toString() === index)
let tooltip = ''
if (skill) {
if (lvInfo.value[skill.id.toString()].up == 1) {
tooltip = `技能攻击力 +${skill.learnLv < 35 ? '60' : 55}%`
}
if (lvInfo.value[skill.id.toString()].up == 2) {
tooltip = `技能攻击力 +${skill.learnLv < 35 ? '43' : 38}%<br/>技能冷却时间 -15%`
}
}
</section>
return skill ? (
<div class="flex gap-5px items-center">
<div class="w-[28px] h-[28px] rounded-[1px] border-[1px] border-solid border-[#6d4e2b] bg-[linear-gradient(180deg,_#191919_0%,_#000000_100%)] [box-shadow:0px_4px_4px_#00000040_inset] rounded-4px">
<img src={getImageURL(skill?.icon)} alt={skill.name} />
</div>
<div class="text-#947E4E" v-html={tooltip}></div>
</div>
) : (
<></>
)
},
)}
</div>
</div> */}
</div>
<section class="skill-up__right" aria-label="目标技能与当前突破">
<h3 class="skill-up__title">目标技能</h3>
<div class="skill-up__target-grid" aria-label="可突破技能">
{targetSkills.map((skill) => (
<UiItemSlot
key={skill.id}
class="skill-up__target-slot"
src={getImageURL(skill.icon)}
label={skill.name}
/>
))}
{Array.from({ length: emptyTargetCount }, (_, index) => (
<UiItemSlot
key={`empty-${index}`}
class="skill-up__target-slot"
label="空技能槽位"
/>
))}
</div>
<div class="skill-up__summary-list">
{selectedSkillIds.value.map((skillId) => {
const skill = props.skills.find(
(candidate) => candidate.id.toString() === skillId,
)
if (!skill) return null
return (
<div class="skill-up__summary-row" key={skillId}>
<div class="up_1 skill-up__summary-frame">
<UiItemSlot
class="skill-up__summary-icon"
src={getImageURL(skill.icon)}
label={skill.name}
/>
</div>
<div class="skill-up__summary-text" v-html={skillDescription(skill)}></div>
</div>
)
})}
</div>
</section>
</div>
) : (
<></>
)}
</>
)
</div>
)
}
},
})
@@ -1,75 +1,209 @@
.skill-up-shell {
box-sizing: border-box;
width: 100%;
min-width: 0;
overflow: auto;
background: var(--ui-background);
container-type: inline-size;
}
.skill-up {
width: 540px;
// height: 334px;
// background-image: url('./img/bg.png');
// background-size: 100% 100%;
padding: 5px 10px;
padding-bottom: 10px;
background-color: black;
box-sizing: border-box;
display: grid;
width: 100%;
min-width: 500px;
grid-template-columns: minmax(228px, 0.92fr) minmax(250px, 1.08fr);
align-items: stretch;
gap: clamp(8px, 1.8cqi, 16px);
padding: clamp(6px, 1.4cqi, 12px);
background: var(--ui-background);
.left {
width: 240px;
height: 325px;
border-radius: 2px;
border: 1px solid #2e2e2e;
background: linear-gradient(to bottom, #1c1c1c 0%, #0a0d0a 100%);
.skill-up__left,
.skill-up__right {
box-sizing: border-box;
min-width: 0;
}
.skill-up__left {
display: flex;
padding: 5px 10px;
min-height: 325px;
flex-direction: column;
padding: clamp(6px, 1.4cqi, 10px);
border: 1px solid var(--ui-settings-border);
background: var(--ui-panel-background);
}
.up_head_1,
.up_head_2 {
display: flex;
flex-direction: column;
align-items: center;
.skill-up__heads {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--ui-control-gap);
}
.icon {
width: 80px;
height: 80px;
background-position: center center;
}
}
.up_head_1,
.up_head_2 {
display: flex;
min-width: 0;
flex: 1 1 0;
flex-direction: column;
align-items: center;
color: var(--ui-secondary-foreground);
.up_head_1 .icon {
background-image: url('./img/attack.png');
}
.up_head_2 .icon {
background-image: url('./img/cd.png');
.icon {
width: clamp(64px, 14cqi, 80px);
aspect-ratio: 1;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
}
.right {
.title {
width: 261px;
height: 48px;
background: url('./img/title.png') no-repeat center center;
}
.up_head_1 .icon {
background-image: url('./img/attack.png');
}
.up_head_2 .icon {
background-image: url('./img/cd.png');
}
.skill-up__head-label {
min-height: calc(var(--ui-control-line-height) * 2em);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--ui-control-font-size-default);
font-weight: 700;
line-height: var(--ui-control-line-height);
text-align: center;
}
.skill-up__swap-list {
display: grid;
margin-top: auto;
gap: clamp(5px, 1cqi, 9px);
padding: clamp(6px, 1.4cqi, 10px) clamp(4px, 2.6cqi, 20px) 0;
}
.skill-up__swap-row {
display: grid;
grid-template-columns: 54px auto 54px;
align-items: center;
justify-content: space-between;
gap: var(--ui-control-gap);
}
.skill-up__right {
display: flex;
flex-direction: column;
}
.skill-up__title {
box-sizing: border-box;
display: flex;
width: 100%;
min-height: 48px;
align-items: center;
justify-content: center;
margin: 0;
background: url('./img/title.png') no-repeat center / 100% 100%;
color: var(--ui-primary-foreground);
font-size: var(--ui-control-font-size-large);
font-weight: 400;
line-height: var(--ui-control-line-height);
}
.skill-up__target-grid {
box-sizing: border-box;
display: grid;
min-height: 110px;
max-height: clamp(110px, 24cqi, 180px);
grid-template-columns: repeat(auto-fill, 30px);
align-content: start;
justify-content: center;
gap: var(--ui-control-gap);
margin: 0 auto clamp(6px, 1.4cqi, 10px);
padding: var(--ui-control-gap);
overflow-y: auto;
}
.skill-up__target-slot {
--ui-item-slot-size: 30px;
--ui-item-image-size: 28px;
--ui-item-frame-inset: 1px;
border: 1px solid var(--ui-table-border);
}
.skill-up__summary-list {
display: grid;
min-height: 0;
gap: clamp(6px, 1.2cqi, 10px);
overflow-y: auto;
}
.skill-up__summary-row {
box-sizing: border-box;
display: flex;
min-height: 48px;
align-items: center;
gap: var(--ui-control-gap);
padding: 5px;
border: 1px solid var(--ui-shop-frame-rest-outer);
background: var(--ui-shop-frame-rest-surface);
}
.skill-up__summary-frame {
width: 40px;
height: 40px;
flex: 0 0 40px;
background-size: 100% 100%;
}
.skill-up__summary-icon {
--ui-item-slot-size: 28px;
--ui-item-image-size: 28px;
--ui-item-frame-inset: 0;
--ui-item-slot-background: transparent;
}
.skill-up__summary-text {
min-width: 0;
color: var(--ui-secondary-foreground);
font-size: var(--ui-control-font-size-default);
line-height: 1.45;
}
.up_1,
.up_2,
.up_3,
.up_active {
display: flex;
width: 54px;
height: 54px;
display: flex;
align-items: center;
justify-content: center;
background-position: center;
background-repeat: no-repeat;
}
.up_1 {
background: url('./img/up_1.png') no-repeat center center;
background-image: url('./img/up_1.png');
}
.up_2 {
background: url('./img/up_2.png') no-repeat center center;
background-image: url('./img/up_2.png');
}
.up_3 {
background: url('./img/up_3.png') no-repeat center center;
background-image: url('./img/up_3.png');
}
.up_active {
background: url('./img/up_active.png') no-repeat center center;
background-image: url('./img/up_active.png');
}
}
@container (min-width: 720px) {
.skill-up {
grid-template-columns: minmax(270px, 0.85fr) minmax(330px, 1.15fr);
}
}
@@ -1,7 +1,7 @@
import { getImageURL } from '@/utils/images'
import type { ISkill } from '@/api/info/type'
import { useVModel } from '@vueuse/core'
import CalcCheckbox from '@/components/calc/checkbox/index.vue'
import { UiCheckbox } from '@/components/ui/checkbox'
import { renderList } from 'vue'
import './style.scss'
export default defineComponent({
@@ -54,22 +54,19 @@ export default defineComponent({
return () => (
<>
<div class="h-auto py-2 flex flex-col bg-#080808 box-content">
<div class="skill-vp-list">
{renderList(skills.value, (item) => (
<div class="h-140px flex items-center vp pl-26px pr-16px box-content">
<div class="w-30px text-center text-#816f4c">{item.learnLv}</div>
<div class="w-150px flex gap-5px items-center">
<div class="border-1px border-solid border-#3F382E rounded-4px w-28px h-28px">
<img class="w-28px h-28px" src={getImageURL(item.icon)} alt={item.name} />
<div class="vp skill-vp-list__row">
<div class="skill-vp-list__learn-level">{item.learnLv}</div>
<div class="skill-vp-list__identity">
<div class="skill-vp-list__icon-frame">
<img src={getImageURL(item.icon)} alt={item.name} />
</div>
<div class="skill-name">{item.name}</div>
</div>
{renderList(2, (index) => (
<div
class={[
'flex items-center flex-col vp-item py-10px',
index == 1 ? 'ml-auto' : 'ml-2px',
]}
class={['vp-item', index == 1 ? 'vp-item--first' : '']}
onClick={() => {
emit('chooseSkill', item.id)
itemChecked(item.id.toString(), index).value = !itemChecked(
@@ -81,43 +78,38 @@ export default defineComponent({
{' '}
<div
class={[
'box-border w-full h-full pt-5px m-2px',
'vp-item__content',
itemChecked(item.id.toString(), index).value ? 'selected' : '',
]}
>
<div class="flex justify-between w-full h-30px px-10px box-border">
<div class="flex items-start">
<CalcCheckbox
<div class="vp-item__header">
<div class="vp-item__checkbox">
<UiCheckbox
modelValue={itemChecked(item.id.toString(), index).value}
disabled={
currentUsedVP.value >= 5 && lvInfo.value[item.id.toString()]?.vp == 0
}
/>
</div>
<div class="relative inline-block border-1px border-solid border-#3F382E rounded-4px w-28px h-28px">
<div
class="bg-#a128f4 w-26px h-26px top-1px left-1px absolute z-5"
style="mix-blend-mode: hue;"
></div>
<div class="vp-item__icon-frame">
<div class="vp-item__icon-tint" style="mix-blend-mode: hue;"></div>
<img
style="filter:brightness(1) contrast(1) saturate(1.5)"
class="w-28px h-28px"
class="vp-item__icon"
src={getImageURL(item.icon)}
alt={item.name}
/>
<div class={`vp_${index} absolute bottom--8px left-5px z-10`}></div>
<div class={`vp_${index} vp-item__badge`}></div>
</div>
<div class="w-16px"></div>
</div>
<div class="text-13px mt-5px text-center selected-name">
{item.vps[index - 1]?.name}
<div class="vp-item__balance"></div>
</div>
<div class="vp-item__name selected-name">{item.vps[index - 1]?.name}</div>
{renderList(
item.vps[index - 1]?.desc
? item.vps[index - 1].desc.replace(/<br\/>/g, '\n').split('\n')
: [],
(desc) => (
<div class="text-12px text-#816f4c text-center leading-none">{desc}</div>
<div class="vp-item__description">{desc}</div>
),
)}
</div>
@@ -1,3 +1,53 @@
.skill-vp-list {
display: flex;
box-sizing: content-box;
width: max-content;
min-width: 100%;
height: auto;
padding-block: 2px;
flex-direction: column;
background: #080808;
}
.skill-vp-list__row {
display: flex;
box-sizing: content-box;
min-width: 540px;
height: 140px;
padding-right: 16px;
padding-left: 26px;
align-items: center;
}
.skill-vp-list__learn-level {
width: 30px;
color: #816f4c;
text-align: center;
}
.skill-vp-list__identity {
display: flex;
width: 150px;
align-items: center;
gap: 5px;
}
.skill-vp-list__icon-frame,
.vp-item__icon-frame {
box-sizing: border-box;
width: 28px;
height: 28px;
flex: 0 0 28px;
border: 1px solid #3f382e;
border-radius: 4px;
img {
display: block;
width: 28px;
height: 28px;
}
}
.vp {
&:nth-child(n + 1) {
background-color: #000000;
@@ -16,6 +66,10 @@
background-image: url(./img/bg.png);
// padding:5px 10px ;
box-sizing: border-box;
&--first {
margin-left: auto;
}
}
.selected {
@@ -45,3 +99,64 @@
background-image: url(../img/vp_2.png);
}
}
.vp-item__content {
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 2px;
padding-top: 5px;
}
.vp-item__header {
display: flex;
box-sizing: border-box;
width: 100%;
height: 30px;
padding-inline: 10px;
justify-content: space-between;
}
.vp-item__checkbox {
display: flex;
align-items: flex-start;
}
.vp-item__icon-frame {
position: relative;
display: inline-block;
}
.vp-item__icon-tint {
position: absolute;
z-index: 5;
top: 1px;
left: 1px;
width: 26px;
height: 26px;
background: #a128f4;
}
.vp-item__badge {
position: absolute;
z-index: 10;
bottom: -8px;
left: 5px;
}
.vp-item__balance {
width: 16px;
}
.vp-item__name {
margin-top: 5px;
font-size: 13px;
text-align: center;
}
.vp-item__description {
color: #816f4c;
font-size: 12px;
line-height: 1;
text-align: center;
}
+145 -31
View File
@@ -1,25 +1,13 @@
<template>
<div class="flex relative" ref="skillTreeRef">
<div class="flex flex-col">
<div class="flex bg-#101012 h-40px text-#6C5E4A font-bold">
<div
class="flex-1 text-15px flex items-center justify-center"
@click="tab = 0"
:class="{ active: tab === 0 }"
>
技能加点
</div>
<div class="w-auto text-18px flex items-center justify-center !font-normal">|</div>
<div
class="flex-1 text-15px flex items-center justify-center"
@click="tab = 1"
:class="{ active: tab === 1 }"
>
技能定制
</div>
<div ref="skillTreeRef" class="skill-page">
<div class="skill-page__panel">
<div class="skill-page__tabs">
<div class="skill-page__tab" @click="tab = 0" :class="{ active: tab === 0 }">技能加点</div>
<div class="skill-page__tab-divider">|</div>
<div class="skill-page__tab" @click="tab = 1" :class="{ active: tab === 1 }">技能定制</div>
</div>
<div class="flex-1 w-550px overflow-x-hidden bg-#101012">
<div class="overflow-y-auto h-full box-border" v-if="tab === 0">
<div class="skill-page__viewport">
<div v-if="tab === 0" class="skill-page__tree-scroll">
<SkillTree
:skills="infoStore.skills"
v-model:lvInfo="configStore.config.skills"
@@ -27,17 +15,17 @@
@chooseSkill="setCurrentSkill"
></SkillTree>
</div>
<div v-if="tab === 1" class="flex flex-col overflow-y-hidden h-full">
<div v-if="tab === 1" class="skill-page__customize">
<div class="item-head" 角色等级>技能突破</div>
<SkillUP
class="flex-1"
class="skill-page__customize-section"
:skills="infoStore.skills"
v-model:lvInfo="configStore.config.skills"
v-model:bindAwake="configStore.config.bindAwake"
:buffer="infoStore.infos?.buffer"
></SkillUP>
<div class="item-head" 角色等级>技能进化</div>
<div class="flex-1 overflow-y-auto w-full">
<div class="skill-page__customize-section skill-page__vp-scroll">
<SkillVP
:skills="infoStore.skills"
v-model:lvInfo="configStore.config.skills"
@@ -50,19 +38,17 @@
</div>
<div
class="absolute right--65px"
class="skill-page__detail-launcher"
v-if="curCurrentSkill.skillId && curCurrentSkill.level > 0 && !showSkillDetail"
>
<div class="text-#AE8D5A p-1px box-border flex pl-2 w-auto">
<div
class="border-1px border-solid border-#322E20 border-l-none flex p-1px box-border pl-0"
>
<div class="skill-page__detail-launcher-frame">
<div class="skill-page__detail-launcher-border">
<div
class="border-1px border-#514531 border-solid rounded-2px flex flex-col items-center justify-center p-5px"
class="skill-page__detail-launcher-button"
style="background: linear-gradient(to bottom, #313330 0%, #0f110f 100%)"
@click="showSkillDetail = true"
>
<img class="w-30px h-30px" src="./components/img/detail.png" />
<img src="./components/img/detail.png" alt="" />
技能详情
</div>
</div>
@@ -70,7 +56,7 @@
</div>
</div>
<div
class="bg-#101012 w-300px h-full absolute"
class="skill-page__detail"
:style="style"
ref="SkillDetailRef"
v-if="curCurrentSkill.skillId && curCurrentSkill.level > 0 && showSkillDetail"
@@ -133,6 +119,134 @@ onClickOutside(
</script>
<style lang="scss" scoped>
.skill-page {
position: relative;
display: flex;
width: 100%;
min-width: 0;
height: 100%;
}
.skill-page__panel {
display: flex;
width: 100%;
min-width: 0;
height: 100%;
flex-direction: column;
background: var(--ui-skill-page-background, #101012);
}
.skill-page__tabs {
display: flex;
min-height: 40px;
flex: 0 0 40px;
color: var(--ui-skill-page-muted, #6c5e4a);
font-weight: 700;
}
.skill-page__tab,
.skill-page__tab-divider {
display: flex;
align-items: center;
justify-content: center;
}
.skill-page__tab {
position: relative;
min-width: 0;
flex: 1;
font-size: 15px;
cursor: pointer;
}
.skill-page__tab-divider {
flex: 0 0 auto;
font-size: 18px;
font-weight: 400;
}
.skill-page__viewport,
.skill-page__tree-scroll,
.skill-page__customize {
min-width: 0;
min-height: 0;
flex: 1;
}
.skill-page__viewport {
overflow: hidden;
}
.skill-page__tree-scroll {
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: auto;
}
.skill-page__customize {
display: flex;
height: 100%;
flex-direction: column;
overflow: hidden;
}
.skill-page__customize-section {
min-height: 0;
flex: 1;
}
.skill-page__vp-scroll {
width: 100%;
overflow: auto;
}
.skill-page__detail-launcher {
position: absolute;
z-index: 20;
top: 40px;
right: -64px;
}
.skill-page__detail-launcher-frame {
box-sizing: border-box;
display: flex;
padding: 1px 1px 1px 2px;
color: var(--ui-skill-page-accent, #ae8d5a);
}
.skill-page__detail-launcher-border {
box-sizing: border-box;
display: flex;
padding: 1px 1px 1px 0;
border: 1px solid #322e20;
border-left: 0;
}
.skill-page__detail-launcher-button {
display: flex;
padding: 5px;
align-items: center;
flex-direction: column;
justify-content: center;
border: 1px solid #514531;
border-radius: 2px;
background: linear-gradient(to bottom, #313330 0%, #0f110f 100%);
cursor: pointer;
img {
width: 30px;
height: 30px;
}
}
.skill-page__detail {
position: absolute;
width: min(300px, 45%);
height: 100%;
background: var(--ui-skill-page-background, #101012);
}
.active {
color: #ccc088;
position: relative;
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
import { UiAutocomplete, type UiAutocompleteOption } from '@/components/ui/autocomplete'
import { UiBadge } from '@/components/ui/badge'
import { buttonVariants, UiButton } from '@/components/ui/button'
import { UiCommandInput, type UiCommandInputOption } from '@/components/ui/command-input'
import { UiCheckbox } from '@/components/ui/checkbox'
import { UiCascader, type UiCascaderGroup } from '@/components/ui/cascader'
import { UiDataRow, UiDataSection } from '@/components/ui/data-section'
@@ -31,6 +32,8 @@ import {
} from '@/components/ui/dropdown-menu'
import { UiInput } from '@/components/ui/input'
import { UiItemSlot } from '@/components/ui/item-slot'
import { UiFavoriteDrawer, type UiFavoriteDrawerItem } from '@/components/ui/favorite-drawer'
import { UiLevelBadge, UiNoticeBadge } from '@/components/ui/notice-badge'
import { UiNumberField } from '@/components/ui/number-field'
import { UiNavigation, type UiNavigationItem } from '@/components/ui/navigation'
import { UiPanel } from '@/components/ui/panel'
@@ -40,6 +43,7 @@ import { UiProgress } from '@/components/ui/progress'
import { UiRadioGroup, UiRadioGroupItem } from '@/components/ui/radio-group'
import { UiRating } from '@/components/ui/rating'
import { UiScrollArea } from '@/components/ui/scroll-area'
import { UiSearchTree, type UiSearchTreeNode } from '@/components/ui/search-tree'
import {
SelectRoot,
SelectValue,
@@ -49,10 +53,12 @@ import {
} from '@/components/ui/select'
import { UiSlider } from '@/components/ui/slider'
import { UiSettingRow, UiSettingsGroup, UiSettingsSection } from '@/components/ui/settings'
import { UiSettingsDropdown, UiSettingsDropdownItem } from '@/components/ui/settings-dropdown'
import { UiShopItemList, type UiShopItem } from '@/components/ui/shop-item-row'
import { UiSwitch } from '@/components/ui/switch'
import { TabsRoot, UiTabsContent, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
import { UiTreeSelect, type UiTreeSelectItem } from '@/components/ui/tree-select'
import { UiUtilitySheet } from '@/components/ui/utility-sheet'
import {
TooltipProvider,
TooltipRoot,
@@ -94,6 +100,10 @@ const sortDirection = ref<'ascending' | 'descending'>('ascending')
const treeValue = ref('rare')
const cascaderValue = ref(['celestial', 'silent-city'])
const currentPage = ref(1)
const commandValue = ref('equip search')
const searchTreeValue = ref('sword')
const searchTreeQuery = ref('')
const favoriteOpen = ref(true)
const progress = computed(() => Math.min(100, 18 + clickCount.value * 14))
@@ -126,8 +136,46 @@ const componentNames = [
'Cascader',
'Pagination',
'Popover',
'CommandInput',
'SearchTree',
'FavoriteDrawer',
'NoticeBadge',
'LevelBadge',
'SettingsDropdown',
'UtilitySheet',
]
const commandOptions: UiCommandInputOption[] = [
{ value: 'equip search', label: '搜索当前装备', prefix: '/装备', tone: 'success' },
{ value: 'skill compare', label: '比较技能伤害', prefix: '/技能' },
{ value: 'preset save', label: '保存当前预设', prefix: '/预设', tone: 'warning' },
]
const searchTreeItems: UiSearchTreeNode[] = [
{
value: 'equipment-tree',
label: '装备目录',
children: [
{ value: 'sword', label: '魔剑共鸣核心', icon: itemRare },
{ value: 'soul', label: '史诗灵魂结晶', icon: itemEpic },
],
},
{
value: 'skill-tree',
label: '技能目录',
children: [
{ value: 'awakening', label: '觉醒技能' },
{ value: 'passive', label: '被动技能' },
],
},
]
const favoriteItems = ref<UiFavoriteDrawerItem[]>([
{ value: 'favorite-sword', label: '魔剑共鸣核心', icon: itemRare },
{ value: 'favorite-soul', label: '史诗灵魂结晶', icon: itemEpic },
{ value: 'favorite-preset', label: '当前伤害预设' },
])
const treeSelectItems: UiTreeSelectItem[] = [
{
value: 'rarity',
@@ -256,6 +304,11 @@ const tableRows: UiDataTableRow[] = [
status: '购买失败',
region: '西北 1',
},
{
id: 'table-note',
kind: 'note',
label: '※ 数据更新时间:今日 08:00;说明行不参与选择。',
},
]
function handleSort(key: string) {
@@ -266,6 +319,11 @@ function handleSort(key: string) {
sortDirection.value = 'ascending'
}
}
function removeFavorite(item: UiFavoriteDrawerItem) {
favoriteItems.value = favoriteItems.value.filter((favorite) => favorite.value !== item.value)
lastAction.value = `已移除 ${item.label}`
}
</script>
<template>
@@ -531,6 +589,8 @@ function handleSort(key: string) {
:selected-id="selectedRow"
:sort-key="sortKey"
:sort-direction="sortDirection"
selection-tone="blue"
max-body-height="190px"
@sort="handleSort"
@select="selectedRow = $event.id"
>
@@ -547,9 +607,20 @@ function handleSort(key: string) {
value
}}</span></template
>
<template #summary>
<span>汇总</span><span>3 条装备</span><span>+22</span><span>2 / 1</span
><span>全区</span>
</template>
<template #footer>
<UiButton size="sm" variant="outline" @click="lastAction = '已刷新表格数据'"
>刷新数据</UiButton
>
</template>
</UiDataTable>
<div class="progress-line">
<UiProgress :model-value="progress" :marker="50" /><span>{{ progress }}%</span>
<UiProgress :model-value="progress" :markers="[25, 50, 75]" /><span
>{{ progress }}%</span
>
</div>
</UiPanel>
</div>
@@ -592,6 +663,94 @@ function handleSort(key: string) {
</UiPanel>
</div>
<div class="two-column advanced-row">
<UiPanel
title="CommandInput / SearchTree"
note="Command suggestions · searchable hierarchy"
>
<div class="command-search-demo">
<label class="field-stack">
<span>CommandInput · prefix + keyboard navigation</span>
<UiCommandInput
v-model="commandValue"
prefix="/命令"
:suggestions="commandOptions"
open-on-focus
filter-suggestions
placeholder="输入命令或操作名称"
@select="lastAction = `已选择 ${$event.label}`"
/>
</label>
<UiSearchTree
v-model="searchTreeValue"
v-model:query="searchTreeQuery"
:items="searchTreeItems"
filter
@select="lastAction = `已定位 ${$event.label}`"
@refresh="lastAction = `已刷新搜索:${$event || '全部'}`"
/>
</div>
</UiPanel>
<UiPanel
title="FavoriteDrawer / Badges / Desktop overlays"
note="Removable favorites · separate menu and sheet anatomy"
>
<div class="drawer-overlay-demo">
<UiFavoriteDrawer
v-model:open="favoriteOpen"
:items="favoriteItems"
title="常用配置"
:max-visible-items="3"
@select="lastAction = `已打开 ${$event.label}`"
@remove="removeFavorite"
/>
<div class="badge-overlay-actions">
<UiNoticeBadge label="有一条新提醒">
<UiButton size="icon" aria-label="通知中心">!</UiButton>
</UiNoticeBadge>
<UiLevelBadge :level="110" />
<UiSettingsDropdown title="系统设置">
<template #trigger><UiButton>设置菜单</UiButton></template>
<UiSettingsDropdownItem
:icon-src="navHome"
@select="lastAction = '已打开基础设置'"
>基础设置</UiSettingsDropdownItem
>
<UiSettingsDropdownItem
:icon-src="navEffect"
@select="lastAction = '已打开特效设置'"
>特效设置</UiSettingsDropdownItem
>
<UiSettingsDropdownItem disabled>云同步(不可用)</UiSettingsDropdownItem>
</UiSettingsDropdown>
<UiUtilitySheet title="快捷工具" size="default">
<template #trigger
><UiButton variant="outline">打开 UtilitySheet</UiButton></template
>
<template #body>
<UiSettingsGroup>
<UiSettingsSection>当前工具</UiSettingsSection>
<UiSettingRow label="自动同步">
<label class="ui-control-row"
><UiSwitch v-model="syncEnabled" />开启</label
>
</UiSettingRow>
<UiSettingRow label="透明度" alt>
<UiSlider v-model="volume" label="工具透明度" />
</UiSettingRow>
</UiSettingsGroup>
</template>
<template #footer>
<DialogClose :class="buttonVariants()">完成</DialogClose>
</template>
</UiUtilitySheet>
</div>
<output>{{ lastAction }}</output>
</div>
</UiPanel>
</div>
<UiPanel title="ShopItemRow / ScrollArea" note="Source frame layers · real scrolling">
<div class="shop-scroll-demo">
<UiShopItemList
@@ -1059,6 +1218,24 @@ label > span,
display: grid;
gap: 12px;
}
.command-search-demo,
.drawer-overlay-demo {
display: grid;
min-width: 0;
gap: 12px;
}
.badge-overlay-actions {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: center;
gap: var(--ui-control-gap);
}
.drawer-overlay-demo output {
min-width: 0;
overflow-wrap: anywhere;
color: var(--ui-muted-foreground);
}
.navigation-settings-demo {
display: grid;
grid-template-columns: minmax(150px, 0.42fr) minmax(0, 1fr);
+18 -14
View File
@@ -1,32 +1,36 @@
<template>
<div class="w-full h-full flex flex-col items-center justify-center gap-4">
<div class="flex flex-col items-center">
<h1 class="text-2xl font-bold">绑定助手社区ID</h1>
<h3>输入助手社区ID后可同步角色列表与基础打造信息</h3>
<calc-input
<div class="box-border flex h-full w-full flex-col items-center justify-center gap-4 p-4">
<div class="flex w-full max-w-xl flex-col items-center text-center">
<h1 class="text-2xl font-bold leading-tight">绑定助手社区ID</h1>
<h3 class="mt-2">输入助手社区ID后可同步角色列表与基础打造信息</h3>
<UiInput
v-model="helperId"
placeholder="请输入助手社区ID"
class="w-64 h-8"
class="mt-3 w-full max-w-80"
@keyup.enter="onSubmit"
/>
<div class="flex justify-between w-64 mt-4">
<calc-button class="w-20 !h-8" @click="onSubmit" :disabled="!isChecking">绑定</calc-button>
<calc-button class="w-20 !h-8" @click="onSeletRole">选择角色</calc-button>
<div class="mt-4 flex w-full max-w-80 items-center justify-center gap-4">
<UiButton class="flex-1" @click="onSubmit" :disabled="!isChecking">绑定</UiButton>
<UiButton class="flex-1" @click="onSeletRole">选择角色</UiButton>
</div>
<span class="text-white/50 mt-4"
<span class="mt-4 max-w-lg text-white/50"
>助手识别基于后台模拟器图片识别,存在一定误差,导入后建议进行手动校对。</span
>
<calc-checkbox v-model="isChecking" class="mt-2 w-64"
><span>
<label class="mt-3 flex w-full max-w-lg items-center justify-center gap-2 text-left">
<UiCheckbox v-model="isChecking" />
<span>
我已同意计算器通过截图的方式<br />从DNF助手截图识别到助手ID公开的职业列表、角色打造信息
</span></calc-checkbox
>
</span>
</label>
</div>
</div>
</template>
<script lang="ts" name="dnfhelper-bind" setup>
import router from '@/router'
import { useDnfHelperStore } from '@/stores/dnfhelper'
import { UiButton } from '@/components/ui/button'
import { UiCheckbox } from '@/components/ui/checkbox'
import { UiInput } from '@/components/ui/input'
const helperId = ref('')
const helperStore = useDnfHelperStore()
+44 -9
View File
@@ -26,15 +26,13 @@
</template>
</div>
</div>
<div class="bg-white/10 h-15 mt-auto w-full flex items-center">
<div class="flex-1"></div>
<calc-button class="h-10 w-30" @click="importRole">导入角色</calc-button>
<div class="flex-1 flex justify-end px-10 gap-5">
<calc-button @click="navToHome">选择职业</calc-button>
<calc-button @click="helperStore.refreshHelperInfo(helperStore.helperInfo?.id)">
刷新
</calc-button>
<calc-button @click="helperStore.logout">取消绑定</calc-button>
<div class="helper-actions mt-auto w-full bg-white/10">
<span class="helper-actions__spacer" aria-hidden="true"></span>
<UiButton size="lg" class="min-w-30" @click="importRole">导入角色</UiButton>
<div class="helper-actions__secondary">
<UiButton @click="navToHome">选择职业</UiButton>
<UiButton @click="helperStore.refreshHelperInfo(helperStore.helperInfo?.id)"> 刷新 </UiButton>
<UiButton @click="helperStore.logout">取消绑定</UiButton>
</div>
</div>
</template>
@@ -44,6 +42,7 @@ import { useDialog } from '@/components/hooks/dialog'
import router from '@/router'
import { useDnfHelperStore } from '@/stores/dnfhelper'
import { computed } from 'vue'
import { UiButton } from '@/components/ui/button'
const props = defineProps<{
list: {
@@ -93,3 +92,39 @@ const importRole = async () => {
}
}
</script>
<style scoped lang="scss">
.helper-actions {
box-sizing: border-box;
display: grid;
min-height: 60px;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 12px;
padding: 8px 16px;
}
.helper-actions__secondary {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
@media (max-width: 1023px) {
.helper-actions {
grid-template-columns: 1fr;
justify-items: center;
}
.helper-actions__spacer {
display: none;
}
.helper-actions__secondary {
justify-content: center;
}
}
</style>
+3 -2
View File
@@ -5,6 +5,7 @@ import type { IAlterInfo } from '@/api/info/type'
import { getImageURL } from '@/utils/images'
import router from '@/router'
import { useDialog } from '@/components/hooks/dialog'
import { UiButton } from '@/components/ui/button'
function job_icon(child: IAlterInfo) {
return {
@@ -38,7 +39,7 @@ export default defineComponent(async () => {
return (
<div class="flex items-center justify-around flex-wrap" style="width:200px">
{renderList(options, (option) => (
<calc-button
<UiButton
class="!w-40% !mb-2"
onClick={() => {
window.open('/character/' + (option.value || option.name), '_blank')
@@ -46,7 +47,7 @@ export default defineComponent(async () => {
}}
>
{option.title}
</calc-button>
</UiButton>
))}
</div>
)