init
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user