This commit is contained in:
2026-08-04 17:57:01 +08:00
parent 768fd4a3a9
commit f77034edd7
641 changed files with 26137 additions and 0 deletions
@@ -0,0 +1,72 @@
<script lang="tsx">
import { useAsyncState } from '@vueuse/core'
import { defineComponent, ref } from 'vue'
import api from '@/api'
import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
export default defineComponent({
setup() {
const configStore = useConfigStore()
const uid = ref('')
const { state, execute, isLoading } = useAsyncState(
async () => {
const res = await api.getConfigFromHelper(uid.value)
configStore.config.equips = res.equips
configStore.config.avatar = res.avatars
configStore.config.avatarLink = res.avatarLink
configStore.config.emblems = res.emblems
configStore.config.sundry.fog = res.fogOathLv
configStore.config.oaths = res.oaths
const { alert } = useDialog()
visible.value = false
await alert({
content: '导入成功',
})
return res
},
undefined,
{
resetOnExecute: false,
immediate: false,
onError: Promise.reject,
},
)
onShow(
() => {
uid.value = ''
state.value = undefined
},
{ immediate: true },
)
const visible = isShow()
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>
<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}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</div>
</div>
</>
)
}
},
})
</script>
@@ -0,0 +1,80 @@
<script lang="tsx">
import { useAsyncState } from '@vueuse/core'
import { defineComponent, ref } from 'vue'
import api from '@/api'
import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
import { type IHelperRoleInfo } from '@/api/info/type'
export default defineComponent({
props: {
alter: {
type: Object as () => IHelperRoleInfo,
},
uid: {
type: String,
},
},
setup(props) {
const configStore = useConfigStore()
const { state, execute, isLoading } = useAsyncState(
async () => {
const res = await api.getConfigFromHelperAlter(props.uid || '', props.alter?.roleId || '')
configStore.config.equips = res.equips
configStore.config.avatar = res.avatars
configStore.config.avatarLink = res.avatarLink
configStore.config.emblems = res.emblems
configStore.config.sundry.fog = res.fogOathLv
configStore.config.oaths = res.oaths
const { alert } = useDialog()
visible.value = false
await alert({
content: '导入成功',
})
return res
},
undefined,
{
resetOnExecute: false,
immediate: false,
onError: Promise.reject,
},
)
onShow(
() => {
state.value = undefined
},
{ immediate: true },
)
const visible = isShow()
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>检测到以下信息,是否导入助手数据?</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>
<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}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</div>
</div>
</>
)
}
},
})
</script>
@@ -0,0 +1,71 @@
<script lang="tsx">
import { useAsyncState } from '@vueuse/core'
import { defineComponent, ref } from 'vue'
import api from '@/api'
import { useDialog } from '@/components/hooks/dialog'
import { isShow, onShow } from '@/components/hooks/show'
import { useConfigStore } from '@/stores'
export default defineComponent({
setup() {
const configStore = useConfigStore()
const skillCode = ref('')
const { state, execute, isLoading } = useAsyncState(
async () => {
const res = await api.getSkillFromCode(skillCode.value)
// console.log(res)
configStore.config.skills = res.skills
const { alert } = useDialog()
visible.value = false
await alert({
content: '导入成功',
})
return res
},
undefined,
{
resetOnExecute: false,
immediate: false,
onError: Promise.reject,
},
)
onShow(
() => {
skillCode.value = ''
state.value = undefined
},
{ immediate: true },
)
const visible = isShow()
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">
*
点击导入后会根据规则解析出技能加点、技能进化、技能强化
</p>
<p class="text-#e9c556/80">* 部分技能可能存在无法识别的情况,需要手动调整</p>
</div>
<div class="flex justify-around items-center">
<calc-button onClick={execute} disabled={!skillCode.value || isLoading.value}>
导入
</calc-button>
<calc-button onClick={() => (visible.value = false)}>取消</calc-button>
</div>
</div>
</>
)
}
},
})
</script>
@@ -0,0 +1,49 @@
<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>
@@ -0,0 +1,49 @@
<!-- 每排总计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>
@@ -0,0 +1,97 @@
<!-- 每排总计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>
@@ -0,0 +1,45 @@
<!-- 每排总计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>
@@ -0,0 +1,47 @@
<!-- 每排总计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>
@@ -0,0 +1,111 @@
<!-- 每排总计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>
@@ -0,0 +1,43 @@
<!-- 每排总计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>
@@ -0,0 +1,45 @@
<!-- 每排总计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>
@@ -0,0 +1,496 @@
<template>
<div class="w-340px min-w-340px shrink-0 bg-black flex flex-col">
<calc-dialog lazy header="装备条件" v-model:visible="triggerVisible">
<options />
</calc-dialog>
<!-- 头部 选择是装备还是融合石 -->
<div class="flex bg-#101012 h-40px text-#6C5E4A font-bold">
<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>
</div>
<div
class="w-full h-1px bg-gradient-to-r from-[rgba(52,43,27,0.05)] via-[rgba(52,43,27,1)] to-[rgba(52,43,27,0.05)]"
></div>
<div class="p-1 space-y-1 mt-10px">
<div class="flex w-full">
<div class="flex flex-1 items-center">
部位:<part-select class="!max-w-25 !w-25" v-model:partList="choose_part"></part-select>
</div>
<div class="flex flex-1 items-center">
品质:<EquipmentRaritySelect v-model="curRarity" />
</div>
</div>
<div class="flex flex-1 items-center justify-between">
<div class="flex items-center">
套装:
<div class="flex items-center justify-center gap-2px">
<template v-for="(suit, _) in suitList" :key="_">
<img
class="w-auto h-15px"
:src="getImageURL('equipment' + suit.imageUrl)"
:alt="suit.name"
:style="{
filter: curSuit == suit.value ? '' : 'grayscale(100%)',
}"
@click="curSuit = suit.value"
/>
</template>
</div>
</div>
<calc-button
icon="set"
title="装备触发条件"
@click="triggerVisible = !triggerVisible"
></calc-button>
</div>
</div>
<div class="overflow-y-auto gap-1 p-1 flex flex-col items-center flex-1 h-0">
<template v-for="item in equs" :key="item.name">
<template v-if="item.equs.length > 0">
<div class="item-head w-full">{{ item.name }}</div>
<div class="grid grid-cols-10 gap-4px w-316px">
<template
v-for="(equ, index) in item.equs"
:key="equ ? `${equ.id}-${equ.oathPos ?? 'equ'}-${index}` : `empty-${index}`"
>
<EquipmentIcon :inactive="!isActive(equ)" :equipment="equ" @click="chooseEqu(equ)" />
</template>
</div>
</template>
</template>
</div>
</div>
<div class="px-5px flex flex-col">
<!-- 基础信息 -->
<div
class="flex flex-col overflow-hidden bg-black"
:class="!hideBase ? '' : 'mb-4px'"
v-if="curEquInfo"
>
<div class="item-head" @click="hideBase = !hideBase">
基础信息{{ hideBase ? ' (点击显示)' : '(点击隐藏)' }}
</div>
<div v-show="!hideBase" class="overflow-y-auto overflow-x-hidden bg-black w-240px">
<Info v-if="curEquInfo" :equipment="curEquInfo" />
</div>
</div>
<!-- 套装信息 -->
<div
class="flex flex-col overflow-hidden bg-black"
:class="!hideSuit ? 'flex-1' : 'mb-4px'"
v-if="curSuitInfo"
>
<div class="item-head" @click="hideSuit = !hideSuit">
套装信息{{ hideSuit ? ' (点击显示)' : '(点击隐藏)' }}
</div>
<template v-if="!hideSuit">
<div class="flex bg-#191918 items-center justify-center gap-2px p-5px">
<calc-button
:disabled="raritiyList.findIndex((rarity) => rarity === curSuitLv.rarity) <= 0"
icon="min-little"
@click="changeSuitLv('preRarity')"
></calc-button>
<calc-button
icon="reduce-little"
:disabled="
raritiyList.findIndex((rarity) => rarity === curSuitLv.rarity) <= 0 &&
curSuitLv.lv <= 1
"
@click="changeSuitLv('preLv')"
></calc-button>
<div class="flex px-20px gap-x-10px" :class="rarityClass(curSuitLv.rarity)">
<span>{{ curSuitLv.rarity }}</span>
<span>{{ formatLv(curSuitLv.lv, curSuitLv.rarity) }}</span>
</div>
<calc-button
:disabled="
raritiyList.findIndex((rarity) => rarity === curSuitLv.rarity) >=
raritiyList.length - 1
"
@click="changeSuitLv('nextLv')"
icon="increase-little"
></calc-button>
<calc-button
:disabled="
raritiyList.findIndex((rarity) => rarity === curSuitLv.rarity) >=
raritiyList.length - 1
"
@click="changeSuitLv('nextRarity')"
icon="max-little"
></calc-button>
</div>
<div
class="w-80% m-auto h-1px bg-gradient-to-r from-[rgba(52,43,27,0.05)] via-[rgba(52,43,27,1)] to-[rgba(52,43,27,0.05)]"
></div>
<div class="flex-1 overflow-y-auto overflow-x-hidden bg-black w-240px">
<SuitInfo v-if="curSuitInfo" :suit="curSuitInfo" />
</div>
</template>
</div>
<div class="flex flex-col flex-1 overflow-hidden bg-black" v-if="curOathInfo">
<div class="item-head">誓约信息</div>
<div class="flex bg-#191918 items-center justify-center gap-2px p-5px">
<calc-button
:disabled="raritiyList.findIndex((rarity) => rarity === curOathSkillLv.rarity) <= 0"
icon="min-little"
@click="changeOathSkillLv('preRarity')"
></calc-button>
<calc-button
icon="reduce-little"
:disabled="
raritiyList.findIndex((rarity) => rarity === curOathSkillLv.rarity) <= 0 &&
curOathSkillLv.lv <= 1
"
@click="changeOathSkillLv('preLv')"
></calc-button>
<div class="flex px-20px gap-x-10px" :class="rarityClass(curOathSkillLv.rarity)">
<span>{{ curOathSkillLv.rarity }}</span>
<span>{{ formatLv(curOathSkillLv.lv, curOathSkillLv.rarity) }}</span>
</div>
<calc-button
:disabled="
raritiyList.findIndex((rarity) => rarity === curOathSkillLv.rarity) >=
raritiyList.length - 1
"
@click="changeOathSkillLv('nextLv')"
icon="increase-little"
></calc-button>
<calc-button
:disabled="
raritiyList.findIndex((rarity) => rarity === curOathSkillLv.rarity) >=
raritiyList.length - 1
"
@click="changeOathSkillLv('nextRarity')"
icon="max-little"
></calc-button>
</div>
<!-- 技能信息 -->
<div class="flex items-center justify-center py-5px">
<template v-for="skill in curOathInfo.skills" :key="skill.name">
<div
class="flex-1 flex flex-col items-center justify-center"
:class="{ unActive: skill.skillId != oathSkillId }"
@click="oathSkillId = skill.skillId"
>
<img class="w-40px h-40px" :src="getImageURL(skill.icon)" :alt="skill.name" />
<div class="text-#DED29A">{{ skill.name }}</div>
</div>
</template>
</div>
<div
class="w-80% m-auto h-1px bg-gradient-to-r from-[rgba(52,43,27,0.05)] via-[rgba(52,43,27,1)] to-[rgba(52,43,27,0.05)]"
></div>
<div class="flex-1 overflow-y-auto overflow-x-hidden bg-black w-240px">
<SuitInfo v-if="curOathSkillInfo" :suit="curOathSkillInfo" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useConfigStore, useInfoStore } from '@/stores'
import PartSelect from '@/components/dnf/Equipment/PartSelect/index.vue'
import EquipmentRaritySelect from '@/components/dnf/Equipment/RaritySelect/index.vue'
import { getImageURL } from '@/utils/images'
import { rarityClass } from '@/utils'
import type { IEquipment } from '@/api/info/type'
import EquipmentIcon from '@/components/dnf/Equipment/Icon/index.vue'
import Info from '@/components/dnf/Equipment/Info/index.vue'
import SuitInfo from '@/components/dnf/Equipment/SuitInfo/index.vue'
import { formatEqu, formatOath } from './formatData'
import options from './components/options.vue'
// const props = defineProps<{
// alter: string
// }>()
const infoStore = useInfoStore()
const type = ref('equ')
const curRarity = ref('')
const curSuit = ref(16203)
const curEquId = ref()
const curSuitLv = ref({
rarity: '太初',
lv: 1,
})
const curOathSkillLv = ref({
rarity: '史诗',
lv: 1,
})
const triggerVisible = ref(false)
const hideBase = ref(false)
const hideSuit = ref(false)
const oathSkillId = ref('1')
const raritiyList = ['稀有', '神器', '传说', '史诗', '太初']
const formatLv = (number: number, raritiy?: string) => {
if (raritiy == '太初') return ''
const greekNumerals = ['Ι', 'ΙΙ', 'ΙΙΙ', 'ΙV', 'V', 'VΙ', 'VΙΙ', 'VΙΙΙ', 'ΙX', 'X']
return greekNumerals[number - 1] || number.toString()
}
const changeSuitLv = (
type: 'preRarity' | 'nextRarity' | 'preLv' | 'nextLv',
resetLv: boolean = true,
) => {
if (type == 'preRarity') {
const index = raritiyList.findIndex((a) => a == curSuitLv.value.rarity)
if (index > 0) {
curSuitLv.value.rarity = raritiyList[index - 1]
curSuitLv.value.lv = resetLv ? 1 : 5
return
}
} else if (type == 'nextRarity') {
const index = raritiyList.findIndex((a) => a == curSuitLv.value.rarity)
if (index < raritiyList.length - 1) {
curSuitLv.value.rarity = raritiyList[index + 1]
curSuitLv.value.lv = 1
return
}
} else if (type == 'preLv') {
if (curSuitLv.value.lv > 1) {
curSuitLv.value.lv--
return
} else {
changeSuitLv('preRarity', false)
}
} else if (type == 'nextLv') {
if (curSuitLv.value.lv < 5) {
curSuitLv.value.lv++
return
} else {
changeSuitLv('nextRarity')
}
}
}
const changeOathSkillLv = (
type: 'preRarity' | 'nextRarity' | 'preLv' | 'nextLv',
resetLv: boolean = true,
) => {
if (type == 'preRarity') {
const index = raritiyList.findIndex((a) => a == curOathSkillLv.value.rarity)
if (index > 0) {
curOathSkillLv.value.rarity = raritiyList[index - 1]
curOathSkillLv.value.lv = resetLv ? 1 : 5
return
}
} else if (type == 'nextRarity') {
const index = raritiyList.findIndex((a) => a == curOathSkillLv.value.rarity)
if (index < raritiyList.length - 1) {
curOathSkillLv.value.rarity = raritiyList[index + 1]
curOathSkillLv.value.lv = 1
return
}
} else if (type == 'preLv') {
if (curOathSkillLv.value.lv > 1) {
curOathSkillLv.value.lv--
return
} else {
changeOathSkillLv('preRarity', false)
}
} else if (type == 'nextLv') {
if (curOathSkillLv.value.lv < 5) {
curOathSkillLv.value.lv++
return
} else {
changeOathSkillLv('nextRarity')
}
}
}
const choose_part = ref<string[]>([])
const curEquInfo = computed(() => {
if (type.value == 'equ') {
return infoStore.equips.find((a) => a.id == curEquId.value)
} else {
return infoStore.oaths.find((a) => a.id == curEquId.value)
}
})
const curSuitInfo = computed(() => {
if (curEquInfo.value?.suit.length != 1) {
return undefined
}
return infoStore.suits.find(
(a) =>
a.suitId.toString() == curEquInfo.value?.suit[0] &&
a.rarity == curSuitLv.value.rarity &&
a.level == curSuitLv.value.lv,
)
})
const curOathInfo = computed(() => {
if (curEquInfo.value?.suit.length != 1 || curEquInfo.value?.oathPos == undefined) {
return undefined
}
return infoStore.oathSkills?.[curEquInfo.value?.suit[0]]
})
const curOathSkillInfo = asyncComputed(async () => {
if (!curOathInfo.value || !curEquInfo.value?.suit[0]) {
return undefined
}
const res = await infoStore.getOathInfo(
curEquInfo.value?.suit[0],
oathSkillId.value,
curOathSkillLv.value.rarity,
curOathSkillLv.value.lv,
)
return res
})
// const curOathSkillInfo = computed(() => {
// const res = curOathSkillLvs.value
// if (!res) {
// return undefined
// }
// // console.log(curEquInfo.value?.suit[0])
// return {
// ...res,
// SkillAttack: parseFloat(res.SkillAttack.toString().split(',')[curOathSkillLv.value.lv - 1]),
// Buffer: parseFloat(res.Buffer.toString().split(',')[curOathSkillLv.value.lv - 1]),
// point: parseFloat(res.point.toString().split(',')[curOathSkillLv.value.lv - 1]),
// }
// })
const chooseCategory = (cat: string) => {
type.value = cat
if (type.value == 'oath' && curSuit.value == -1) {
curSuit.value = 16203
}
}
const configStore = useConfigStore()
const suitList = 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.suitName,
value: a.suitId,
imageUrl: a.imageUrl.replace('/suit/', '/suit/icon/'),
})
}
})
const res = Array.from(uniqueSuits.values())
if (type.value == 'equ') res.push({ name: '通宝', value: -1, imageUrl: '/suit/icon/-1.png' })
return res
})
const equs = computed(() => {
// 筛选出装备
const total = (type.value == 'equ' ? infoStore.equips : infoStore.oaths).filter(
(a) =>
(!curRarity.value || a.rarity === curRarity.value) &&
(choose_part.value.length === 0 ||
choose_part.value.includes(a.itemDetailType || a.itemType)) &&
(a.suit.length == 0 ||
(curSuit.value == -1 && a.suit.length > 1 && type.value == 'equ') ||
(a.suit.length > 1 && type.value == 'oath') ||
(a.suit.includes(curSuit.value.toString()) && a.suit.length === 1)),
)
if (type.value == 'equ') {
return formatEqu(
total,
infoStore.infos?.subweapons,
suitList.value.find((a) => a.value == curSuit.value)?.name,
)
} else {
return formatOath(
total as unknown as IEquipment[],
suitList.value.find((a) => a.value == curSuit.value),
)
}
})
const isActive = computed(() => {
return (item?: IEquipment) => {
if (!item) return false
if (type.value == 'equ') {
if (item.itemType.includes('武器')) {
return configStore.config.equips[item.itemType].id == item.id
} else {
return configStore.config.equips[item.itemDetailType].id == item.id
}
} else {
return configStore.config.oaths[item.oathPos].id == item.id
}
}
})
const chooseEqu = (item?: IEquipment) => {
if (!item) return
curEquId.value = item.id
const target =
type.value === 'equ'
? configStore.config.equips[
item.itemType.includes('武器') ? item.itemType : item.itemDetailType
]
: configStore.config.oaths[item.oathPos]
if (isActive.value(item) && type.value == 'oath') {
// 誓约和星蕴石 有默认穿戴
target['id'] = item.oathPos == '11' ? '2000500' : '2000101'
} else {
target['id'] = isActive.value(item) ? '' : item.id
}
}
</script>
<style lang="scss" scoped>
.active {
color: #ccc088;
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%);
}
}
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
text-align: center;
}
.unActive {
filter: grayscale(100%);
}
</style>
@@ -0,0 +1,23 @@
<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>
</template>
<script lang="ts" setup>
import { useConfigStore, useInfoStore } from '@/stores'
const dialogRef = inject('dialogRef', ref(null))
const config = useConfigStore()
const info = useInfoStore()
</script>
@@ -0,0 +1,326 @@
import type { IEquipment } from '@/api/info/type'
export const rowCount = 10
export const formatEqu = (total: IEquipment[], subweapons?: string[], curSuit?: string) => {
// 武器
const item_weapon = total.filter((a) => a.itemType == '武器') ?? []
const weapons: (IEquipment | undefined)[] = []
if (item_weapon.length > 0) {
Array.from(new Set(item_weapon.map((item) => item.rarity)))
.filter((a) => !['稀有', '神器'].includes(a))
.forEach((rarity) => {
const weapon = item_weapon
.filter((item) => item.rarity === rarity)
.sort((a, b) => {
if (a.categorize == b.categorize) {
return a.itemDetailType.localeCompare(b.itemDetailType)
}
return b.categorize.localeCompare(a.categorize)
})
weapons.push(
...weapon.filter((a) => !['传世武器升级', '狄瑞吉传世武器'].includes(a.categorize)),
)
const remind = weapons.length % rowCount
if (remind > 0) {
weapons.push(...Array(rowCount - remind).fill(undefined))
}
})
if (item_weapon.some((a) => a.categorize === '传世武器升级')) {
const length = item_weapon.filter((a) => a.categorize === '传世武器升级').length
weapons.push(...Array(length).fill(undefined))
weapons.push(
...item_weapon
.filter((a) => a.categorize === '传世武器升级')
.sort((a, b) => {
return a.itemDetailType.localeCompare(b.itemDetailType)
}),
)
}
let remind = weapons.length % rowCount
if (remind > 0) {
weapons.push(...Array(rowCount - remind).fill(undefined))
}
if (item_weapon.some((a) => a.categorize === '狄瑞吉传世武器')) {
const length = item_weapon.filter((a) => a.categorize === '狄瑞吉传世武器').length
weapons.push(...Array(length).fill(undefined))
weapons.push(
...item_weapon
.filter((a) => a.categorize === '狄瑞吉传世武器')
.sort((a, b) => {
return a.itemDetailType.localeCompare(b.itemDetailType)
}),
)
}
remind = weapons.length % rowCount
if (remind > 0) {
weapons.push(...Array(rowCount - remind).fill(undefined))
}
}
// # region 副武器
const subWeapons: (IEquipment | undefined)[] = []
const item_sub_weapon = (total.filter((a) => subweapons?.includes(a.itemDetailType)) ?? []).sort(
(a, b) => {
if (a.categorize == b.categorize) {
return a.itemDetailType.localeCompare(b.itemDetailType)
}
return b.categorize.localeCompare(a.categorize)
},
)
if (item_sub_weapon.length > 0) {
subWeapons.push(
...item_sub_weapon.map((a) => ({ ...a, itemType: '副武器' })),
...Array(rowCount - (item_sub_weapon.length % rowCount)).fill(undefined),
)
}
// # endregion
// 秘宝
const item_treasure = total.filter((equip) => equip.categorize.includes('秘宝'))
const treasures: (IEquipment | undefined)[] = []
if (item_treasure.length > 0) {
treasures.push(
...item_treasure,
...Array(rowCount - (item_treasure.length % rowCount)).fill(undefined),
)
}
// 套装
const item_suits = total.filter(
(equip) => equip.categorize.includes('套装') || equip.categorize.includes('通宝'),
)
const suits: (IEquipment | undefined)[] = []
const raritiyList = Array.from(new Set(item_suits.map((item) => item.rarity))).filter(
(a) => !['稀有', '神器'].includes(a),
)
raritiyList.forEach((rarity) => {
const armor = item_suits.filter(
(item) => item.rarity === rarity && item.itemType === '防具' && !item.name.includes('侵蚀'),
)
if (armor.length > 0) {
suits.push(...armor, undefined)
}
const special = item_suits.filter(
(item) => item.rarity === rarity && item.itemType === '特殊装备',
)
if (special.length > 0) {
suits.push(...special)
if (suits.length % rowCount > 0) {
suits.push(...Array(rowCount - (suits.length % rowCount)).fill(undefined))
}
}
const jewelry = item_suits.filter((item) => item.rarity === rarity && item.itemType === '首饰')
const jewelryNormal = jewelry.filter((a) => !a.name.includes('黑牙')) ?? []
const jewelryHY = jewelry.filter((a) => a.name.includes('黑牙')) ?? []
if (jewelry.length > 0) {
suits.push(
...Array(3 - jewelryHY.length + 2).fill(undefined),
...jewelryHY,
undefined,
...jewelryNormal,
)
suits.push(...Array(rowCount - (suits.length % rowCount)).fill(undefined))
}
const armorUpgrade = item_suits.filter(
(item) => item.rarity === rarity && item.itemType === '防具' && item.name.includes('侵蚀'),
)
if (armorUpgrade.length > 0) {
suits.push(...armorUpgrade)
}
if (suits.length % rowCount > 0) {
suits.push(...Array(rowCount - (suits.length % rowCount)).fill(undefined))
}
})
// 称号
const item_title = total.filter((equip) => equip.itemDetailType == '称号')
const title: (IEquipment | undefined)[] = []
if (item_title.length > 0) {
title.push(...item_title, ...Array(rowCount - (item_title.length % rowCount)).fill(undefined))
}
// 宠物
const item_pets = total.filter((equip) => equip.itemDetailType == '宠物')
const pet: (IEquipment | undefined)[] = []
if (item_pets.length > 0) {
pet.push(...item_pets, ...Array(rowCount - (item_pets.length % rowCount)).fill(undefined))
}
// 称号
return [
{
name: '武器',
equs: weapons,
},
{
name: '副武器',
equs: subWeapons,
},
{
name: '秘宝',
equs: treasures,
},
{
name: `${curSuit} 套装`,
equs: suits,
},
{
name: '称号',
equs: title,
},
{
name: '宠物',
equs: pet,
},
]
}
export const formatStone = (total: IEquipment[], curSuit?: string) => {
// 套装
const item_suits = total.filter((equip) => equip.suit.length == 1)
const suits: (IEquipment | undefined)[] = []
const raritiyList = Array.from(new Set(item_suits.map((item) => item.rarity)))
raritiyList.forEach((rarity) => {
const armor = item_suits.filter((item) => item.rarity === rarity && item.itemType === '防具')
if (armor.length > 0) {
suits.push(...armor, undefined)
}
const special = item_suits.filter(
(item) => item.rarity === rarity && item.itemType === '特殊装备',
)
if (special.length > 0) {
suits.push(...special)
if (suits.length % rowCount > 0) {
suits.push(...Array(rowCount - (suits.length % rowCount)).fill(undefined))
}
}
const jewelry = item_suits.filter((item) => item.rarity === rarity && item.itemType === '首饰')
const jewelryNormal = jewelry.filter((a) => !a.name.includes('黑牙')) ?? []
const jewelryHY = jewelry.filter((a) => a.name.includes('黑牙')) ?? []
if (jewelry.length > 0) {
suits.push(
...Array(3 - jewelryHY.length + 2).fill(undefined),
...jewelryHY,
undefined,
...jewelryNormal,
)
suits.push(...Array(rowCount - (suits.length % rowCount)).fill(undefined))
}
if (suits.length % rowCount > 0) {
suits.push(...Array(rowCount - (suits.length % rowCount)).fill(undefined))
}
})
const item_suits_wns = total.filter((equip) => equip.categorize.includes('维纳斯通宝'))
const wns: (IEquipment | undefined)[] = []
const categorizeList = Array.from(new Set(item_suits_wns.map((item) => item.categorize)))
categorizeList.forEach((categorize) => {
;['神器', '史诗'].forEach((raritiy) => {
const temp = item_suits_wns.filter(
(item) => item.categorize === categorize && item.rarity === raritiy,
)
const remind = temp.length % (rowCount / 2)
wns.push(...temp, ...Array(temp.length == 0 ? rowCount / 2 - remind : remind).fill(undefined))
})
})
const item_suits_nbe = total.filter((equip) => equip.categorize.includes('纳波尔通宝'))
const nbes: (IEquipment | undefined)[] = []
const nbeList = Array.from(new Set(item_suits_nbe.map((item) => item.categorize)))
nbeList.forEach((categorize) => {
;['神器', '史诗'].forEach((raritiy) => {
const temp = item_suits_nbe.filter(
(item) => item.categorize === categorize && item.rarity === raritiy,
)
const remind = temp.length % (rowCount / 2)
nbes.push(
...temp,
...Array(temp.length == 0 ? rowCount / 2 - remind : remind - 1).fill(undefined),
)
})
})
const items_suits_rzs = total.filter((equip) => equip.categorize.includes('人造神'))
const rzs: (IEquipment | undefined)[] = []
const partList = Array.from(new Set(items_suits_rzs.map((item) => item.itemDetailType)))
// const raritiy = Array.from(new Set(items_suits_rzs.map((item) => item.rarity)))
partList.forEach((part) => {
raritiyList.forEach((rarity) => {
const temp = items_suits_rzs.filter(
(item) => item.itemDetailType === part && item.rarity === rarity,
)
if (temp.length > 0) {
rzs.push(...temp, ...Array(rowCount - temp.length).fill(undefined))
}
})
// const temp = items_suits_rzs.filter((item) => item.itemDetailType === part)
// const remind = temp.length % rowCount
// rzs.push(...temp, ...Array(temp.length == 0 ? rowCount-remind : remind).fill(undefined))
})
// const items_sj = total.filter((equip) => equip.categorize.includes('神界'))
// const sj: (IEquipment | undefined)[] = []
// sj.push(...items_sj, ...Array(rowCount * 2 - (items_sj.length % rowCount)).fill(undefined))
// const temp = items_suits_rzs.filter((item) => item.itemDetailType === part)
// const remind = temp.length % rowCount
// rzs.push(...temp, ...Array(temp.length == 0 ? rowCount-remind : remind).fill(undefined))
return [
{
name: `${curSuit} 套装`,
equs: suits,
},
{
name: '维纳斯通宝',
equs: wns,
},
{
name: '纳波尔通宝',
equs: nbes,
},
{
name: '人造神',
equs: rzs,
},
// {
// name: '神界',
// equs: sj,
// },
]
}
export const formatOath = (
total: IEquipment[],
curSuit?: { name: string; value: number; imageUrl: string },
) => {
// oath
const oath = total.filter(
(equip) => equip.itemDetailType == '誓约' && equip.categorize != '通宝装备',
)
oath.push(...Array(rowCount - (oath.length % rowCount)).fill(undefined))
// primer
const primer = []
const primerItem = total.filter(
(equip) => equip.itemDetailType == '星蕴石' && equip.rarity != '稀有',
)
for (let i = 0; i < 11; i++) {
const temp = primerItem.filter((a) => a.oathPos == i.toString())
primer.push(
...temp,
...Array(temp.length == 0 ? rowCount : rowCount - (temp.length % rowCount)).fill(undefined),
)
}
return [
{
name: `${curSuit?.name} 誓约`,
equs: oath,
},
{
name: `${curSuit?.name} 星蕴石`,
equs: primer,
},
]
}
@@ -0,0 +1,139 @@
<script lang="tsx">
import { computed, defineComponent, ref, renderList } from 'vue'
import type { IEnchantingInfo } from '@/api/info/type'
import { useInfoStore, useConfigStore } from '@/stores'
import ClothList from '@/components/dnf/Avatar/List/index.vue'
export default defineComponent({
name: 'Clothes',
setup() {
const configStore = useConfigStore()
const part = ref('武器装扮')
// const globalStore = useGlobalStore()
const basicInfoStore = useInfoStore()
const has_enchant = computed(() => {
return !['头部', '帽子', '脸部', '胸部', '上衣', '腰带', '下装', '鞋'].includes(
part.value as string,
)
})
const enchant_list = computed<IEnchantingInfo[] | undefined>(() => {
return basicInfoStore.enchants
?.filter((item) => item.position.includes(part.value))
.sort((a, b) => (b.fame ?? 0) - (a.fame ?? 0))
})
// const emblem_list = computed<IEnchantingInfo[] | undefined>(() => {
// return basicInfoStore.emblems.filter((item) => item.position.includes(part.value))
// })
const currentInfo = function <T>(name: string, defaultValue?: T) {
return computed<string | number>({
get() {
let key: 'avatar' | 'equips' = 'avatar'
if (part.value == '宠物') key = 'equips'
return (
(configStore.config[key]?.[part.value] as Record<string, string | number>)?.[name] ??
defaultValue ??
0
)
},
set(val) {
if (val == undefined) {
return
}
let key: 'avatar' | 'equips' = 'avatar'
if (part.value == '宠物') key = 'equips'
if (!configStore.config[key][part.value]) {
configStore.config[key][part.value] =
key === 'avatar'
? { ...configStore.defaultAvatarConfig }
: { ...configStore.defaultEqusConfig }
}
if (configStore.config[key][part.value]) {
;(configStore.config[key][part.value] as Record<string, string | number>)[name] = val
}
},
})
}
// 附魔
const enchant = currentInfo<string | number>('enchant')
// // 镶嵌栏1
// const emblem_0 = currentInfo<string | number>('emblem_0', 0)
// // 镶嵌栏2
// const emblem_1 = currentInfo<string | number>('emblem_1')
// 品级
const id = currentInfo<string | number>('id')
// 属性
const option = currentInfo<string | number>('option')
/**
* 同步徽章1到徽章2
* @param val
*/
// function changeSocket(val: number) {
// emblem_1.value = val
// }
return () => {
return (
<>
<div class="h-120px flex justify-center items-center">
<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>
<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>
</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>
</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>
</div>
</>
)}
</div>
</>
)
}
},
})
</script>
<style lang="scss"></style>
@@ -0,0 +1,185 @@
<template>
<div class="h-179px flex justify-center items-center">
<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>
<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>
</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>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="Emblems">
import EmblemList from '@/components/dnf/Emblem/List/index.vue'
import { useConfigStore, useInfoStore } from '@/stores'
const part = ref('彩色')
const configStore = useConfigStore()
const infoStore = useInfoStore()
const lvs = [
'稀有Lv.1',
'稀有Lv.2',
'稀有Lv.3',
'稀有Lv.4',
'稀有Lv.5',
'神器Lv.1',
'神器Lv.2',
'神器Lv.3',
'神器Lv.4',
'神器Lv.5',
'传说Lv.1',
'传说Lv.2',
'传说Lv.3',
'传说Lv.4',
'传说Lv.5',
'史诗Lv.1',
]
const emblemOptions = computed(() => {
if (part.value == '彩色') {
return infoStore.emblems.filter((a) => a.type != '白金').sort((a, b) => b.fame - a.fame)
}
return infoStore.emblems
.filter((a) => {
const color = part.value[0]
// if (part.value == '红色' || part.value == '绿色') {
// return a.type == part.value || a.type == '红绿' || a.type == '彩色'
// }
// if (part.value == '黄色' || part.value == '蓝色') {
// return a.type == part.value || a.type == '蓝黄' || a.type == '彩色'
// }
return a.type == part.value || a.type == '彩色' || a.type.includes(color)
})
.sort((a, b) => b.fame - a.fame)
})
const emblemMaxCount = computed(() => {
if (part.value == '彩色') {
return 6
}
if (part.value == '白金') {
return 2
}
return 4
})
const emblemGridStyle = computed(() => {
return {
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
}
})
const emblemItemStyle = (index: number) => {
const count = emblemMaxCount.value
const remainder = count % 2
if (remainder === 1 && index === count - 1) {
return {
gridColumn: 'span 2 / span 2',
}
}
return {
gridColumn: 'span 1 / span 1',
}
}
const ensurePartConfig = (partName: string) => {
if (!configStore.config.emblems[partName]) {
configStore.config.emblems[partName] = configStore.createDefaultEmblemConfig()
}
const target = configStore.config.emblems[partName]
if (!Array.isArray(target.items)) {
target.items = ['', '', '', '', '', '']
}
if (target.items.length < 6) {
target.items = [...target.items, ...Array.from({ length: 6 - target.items.length }, () => '')]
}
}
watch(
part,
(newPart) => {
ensurePartConfig(newPart)
},
{ immediate: true },
)
const currentInfo = function <T>(name: string, defaultValue?: T) {
return computed<string | number>({
get() {
ensurePartConfig(part.value)
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(configStore.config.emblems?.[part.value] as Record<string, any>)?.[name] ??
defaultValue ??
0
)
},
set(val) {
if (val == undefined) {
return
}
// if (name == 'emblem_1' && !has_emblem_1.value) {
// // configStore.setForge(part.value, name, 0)
// return
// }
ensurePartConfig(part.value)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(configStore.config.emblems[part.value] as Record<string, any>)[name] = val
if (part.value == '彩色' && name == 'lv') {
// 同步其他部位的等级
const lv = val as number
for (const p of ['红色', '蓝色', '黄色', '绿色']) {
ensurePartConfig(p)
configStore.config.emblems[p]['lv'] = Math.max(
configStore.config.emblems[p]['lv'] || 0,
lv,
)
}
} else if (part.value != '彩色' && name == 'lv') {
// 同步彩色部位的等级
const lv = val as number
ensurePartConfig('彩色')
configStore.config.emblems['彩色']['lv'] = Math.min(
configStore.config.emblems['彩色']['lv'] || 0,
lv,
)
}
},
})
}
const lv = currentInfo('lv', 0)
const emblems = currentInfo<string[]>('items', ['', '', '', '', '', '']) as unknown as Ref<string[]>
</script>
@@ -0,0 +1,251 @@
<script lang="tsx">
import { computed, defineComponent, ref, renderList } 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'
export default defineComponent({
name: 'Equip',
setup() {
const configStore = useConfigStore()
const part = ref('头肩')
const can_upgrade = computed(() => {
return !['称号', '宠物'].includes(part.value as string)
})
// const globalStore = useGlobalStore()
/** 是否可以设置秘宝精度 */
const can_set_secret = computed(() => {
return ['武器', '辅助装备', '魔法石', '耳环'].includes(part.value as string)
})
const basicInfoStore = useInfoStore()
const enchant_list = computed<IEnchantingInfo[] | undefined>(() => {
return basicInfoStore.enchants
?.filter(
(item) =>
item.position.includes(part.value) &&
!item.position.includes('武器装扮') &&
!item.position.includes('宠物装备'),
)
.sort((a, b) => (b.fame ?? 0) - (a.fame ?? 0))
})
// const emblem_list = computed<IEnchantingInfo[] | undefined>(() => {
// return basicInfoStore.emblems.filter((item) => item.position.includes(part.value))
// })
const global_change = ref(false)
const currentInfo = function <T>(name: string, defaultValue?: T) {
return computed<string | number>({
get() {
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(configStore.config.equips?.[part.value] as Record<string, any>)?.[name] ??
defaultValue ??
0
)
},
set(val) {
if (val == undefined && name !== 'weaponFusion') {
return
}
// if (name == 'emblem_1' && !has_emblem_1.value) {
// // configStore.setForge(part.value, name, 0)
// return
// }
let parts: string[] = []
if (global_change.value && name !== 'weaponFusion') {
if (name === 'enchant') {
const enchant = basicInfoStore?.enchants?.find((item) => item.id == val) as
| IEnchantingInfo
| undefined
if (enchant?.position) {
parts = enchant.position
}
}
// if (['emblem_0', 'emblem_1'].includes(name)) {
// const emblem = basicInfoStore?.emblems?.find(
// (item) => item.id.toString() == val.toString(),
// ) as IEnchantingInfo | undefined
// if (emblem?.position) {
// parts = emblem.position.filter(
// (item) => !['皮肤', '武器装扮', '光环'].includes(item),
// )
// }
// }
if (['reinforceType', 'reinforce', 'adaptation'].includes(name)) {
parts = basicInfoStore.parts.filter((e) => !['称号', '宠物'].includes(e))
}
} else {
parts = [part.value]
}
for (const part of parts) {
if (!configStore.config.equips[part]) {
configStore.config.equips[part] = { ...configStore.defaultEqusConfig }
}
if (configStore.config.equips[part]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(configStore.config.equips[part] as Record<string, any>)[name] = val
}
}
},
})
}
// 附魔
const enchant = currentInfo<string | number>('enchant')
// // 镶嵌栏1
// const emblem_0 = currentInfo<string | number>('emblem_0', 0)
// // 镶嵌栏2
// const emblem_1 = currentInfo<string | number>('emblem_1')
// 增幅
const reinforce_type = currentInfo<string | number>('reinforceType', 1)
// 增幅数值
const reinforce = currentInfo<string | number>('reinforce')
// 锻造数值
const refine = currentInfo<string | number>('refine')
// 调适
const adaptation = currentInfo<string | number>('adaptation')
//精度
const precision = currentInfo<number>('precision', 0)
// 武器融合
return () => {
return (
<>
<div class="h-150px flex justify-center items-center">
<EquList
v-model:part={part.value}
withBg={false}
withPet={false}
with-sub-weapon={useInfoStore().infos?.alter.includes('vegabond')}
/>
</div>
<div class="flex flex-wrap equ-profile">
<div class="equ-profile-item">
<div class="mr-10px 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>
)}
</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>
</>
) : (
<>
{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>
{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>
)}
</div>
) : (
<div></div>
)}
<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>
</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>
</div>
) : (
<div></div>
)}
{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>
</>
)
}
},
})
</script>
<style lang="scss"></style>
@@ -0,0 +1,92 @@
<template>
<div>
<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>
</template>
</div>
</template>
<script setup lang="ts">
import type { IJade } from '@/api/info/type'
import { useConfigStore } from '@/stores'
import { useInfoStore } from '@/stores'
import { getFloat } from '@/utils'
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 formatValue = (id: number) => {
// return (value: number) => {
// if (currentJade.value(id)?.unit.includes('等级')) {
// return `$等级 +${value}`
// }
// return `${getFloat(value, 1)}${currentJade.value(id)?.unit}`
// }
// }
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
}
}
}
})
</script>
@@ -0,0 +1,101 @@
<template>
<div class="h-150px flex justify-center items-center">
<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>
<span>{{ part == '11' ? '誓约' : `星蕴石-${parseInt(part) + 1}` }}</span>
<calc-checkbox
style="margin-left: auto"
v-model="global_change"
label="全局修改"
v-if="part !== '11'"
></calc-checkbox>
</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>
</div>
<div class="equ-profile-item" v-else>
<div class="row-name">誓约升级</div>
<template v-for="i in Array.from({ length: 4 }, (_, i) => i)" :key="i">
<calc-select
: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>
</template>
</div>
</div>
</template>
<script lang="ts" setup name="Oaths">
import Oath from '@/components/dnf/Equipment/Oath/index.vue'
import { useConfigStore, useInfoStore } from '@/stores'
const part = ref('0')
const global_change = ref(false)
const configStore = useConfigStore()
const infoStore = useInfoStore()
const currentInfo = function <T>(name: string, defaultValue?: T) {
return computed<T>({
get() {
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(configStore.config.oaths?.[part.value] as Record<string, any>)?.[name] ?? defaultValue ?? 0
)
},
set(val) {
if (val == undefined) {
return
}
// if (name == 'emblem_1' && !has_emblem_1.value) {
// // configStore.setForge(part.value, name, 0)
// return
// }
let parts: string[] = []
if (global_change.value) {
parts = infoStore.oathParts
} else {
parts = [part.value]
}
for (const part of parts) {
if (!configStore.config.oaths[part]) {
configStore.config.oaths[part] = configStore.createDefaultOathConfig()
}
if (configStore.config.oaths[part]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(configStore.config.oaths[part] as Record<string, any>)[name] = val
}
}
},
})
}
// 调适
const adaptation = currentInfo<string | number>('adaptation')
// 誓约升级
const oath_upgrade = currentInfo<number[]>('upgrade', [0, 0, 0, 0])
const getOathUpgradeValue = (index: number) => oath_upgrade.value[index] ?? 0
const setOathUpgradeValue = (index: number, val: number) => {
const nextUpgrade = [...oath_upgrade.value]
nextUpgrade[index] = val
oath_upgrade.value = nextUpgrade
}
</script>
@@ -0,0 +1,140 @@
<template>
<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} 级`"
v-model="sundryData['fog']"
: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} 级`"
v-model="sundryData['adventure']"
:min="1"
:max="40"
: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 ['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>
</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>
</div>
<div class="equ-profile-item">
<div class="row-name">收集箱</div>
<calc-select
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>
</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>
</div>
</div>
</template>
<script setup lang="ts">
import { useConfigStore, useInfoStore } from '@/stores'
const configStore = useConfigStore()
const infoStore = useInfoStore()
const currentInfo = function <T>(name: string, defaultValue?: T) {
return computed<string | number>({
get() {
return (configStore.config.sundry?.[name] ?? defaultValue ?? 0) as string | number
},
set(val) {
if (val == undefined) {
return
}
configStore.config.sundry[name] = val
},
})
}
const list = computed(() => {
return (key: string) => {
return infoStore.sundries[key].options
}
})
const sundryData = reactive({
medal_rarity: currentInfo('medal_rarity', 5),
medal_reinforce: currentInfo('medal_reinforce', 12),
medal_gem_0: currentInfo('medal_gem_0', 4),
medal_gem_1: currentInfo('medal_gem_1', 4),
medal_gem_2: currentInfo('medal_gem_2', 4),
medal_gem_3: currentInfo('medal_gem_3', 4),
adventure: currentInfo('adventure', 30),
fog: currentInfo('fog', 10),
marriage_house: currentInfo('marriage_house', 4),
marriage_ring: currentInfo('marriage_ring', 2),
contract: currentInfo('contract', 1),
collection_type: currentInfo('collection_type', 1),
collection_num_0: currentInfo('collection_num_0', 5),
collection_num_1: currentInfo('collection_num_1', 5),
costume_card: currentInfo('costume_card', 1),
})
</script>
@@ -0,0 +1,58 @@
<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>
</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'
const partTab = ref(0)
</script>
<style lang="scss">
.equ-profile {
width: 100%;
.equ-profile-item {
width: 100%;
height: 20px;
display: flex;
margin-top: 5px;
margin-bottom: 5px;
align-items: center;
.row-name {
flex: 0 0 50px;
text-align: center;
}
:not(:last-child) {
margin-right: 10px !important;
}
.i-select-label {
margin-right: 0px !important;
}
}
}
</style>
+166
View File
@@ -0,0 +1,166 @@
<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"
>
<RouterView></RouterView>
</div>
<div class="m-1 ml-auto h-auto z-2 box-border">
<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>
</div>
</div>
<calc-dialog lazy header="打造导入" v-model:visible="importVisible">
<ImportVue></ImportVue>
</calc-dialog>
<calc-dialog lazy header="技能导入" v-model:visible="importSkillVisible">
<ImportSkillVue></ImportSkillVue>
</calc-dialog>
<calc-dialog lazy header="助手打造导入" v-model:visible="helperImport">
<ImportDnfhelper :alter="toImport" :uid="helperStore.helperInfo?.id"></ImportDnfhelper>
</calc-dialog>
</template>
<script lang="ts" setup name="Index">
import { useInfoStore } from '@/stores/info'
import { useConfigStore } from '@/stores'
import ImportVue from './Config/import.vue'
import ImportSkillVue from './Config/importSkill.vue'
import ImportDnfhelper from './Config/importDnfhelper.vue'
import Result from './Result/Index.vue'
import { useDnfHelperStore } from '@/stores/dnfhelper.ts'
import { type IHelperRoleInfo } from '@/api/info/type'
const props = defineProps<{
alter: string
}>()
const infoStore = useInfoStore()
const configStore = useConfigStore()
const helperImport = ref(false)
const importVisible = ref(false)
const importSkillVisible = ref(false)
const helperStore = useDnfHelperStore()
const toImport = ref<IHelperRoleInfo>()
// const shouldWatch = ref(true)
const isExecuting = ref(false)
const stopWatch = watch(
() => {
return JSON.stringify(configStore.config)
},
async () => {
if (isExecuting.value) return
isExecuting.value = true
try {
;(await configStore.result).execute()
} finally {
isExecuting.value = false
}
},
)
const showImport = () => {
importVisible.value = true
}
const showImportSkill = () => {
importSkillVisible.value = true
}
const route = useRoute()
const saveConfigBeforeUnload = () => {
configStore.saveConfig()
}
onMounted(async () => {
await infoStore.createCharacter(props.alter, '0')
window.addEventListener('beforeunload', saveConfigBeforeUnload)
// 检测router的传参
const { roleId } = route.query
if (roleId) {
toImport.value = helperStore.helperInfo?.roleList.find((a) => a.roleId === roleId)
if (toImport.value) {
helperImport.value = true
}
}
})
onUnmounted(() => {
stopWatch()
window.removeEventListener('beforeunload', saveConfigBeforeUnload)
})
</script>
<style lang="scss">
.main {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-repeat: no-repeat;
background-size: cover;
background-color: black;
color: #e9c558;
// font-size: 12px;
.header {
display: flex;
align-items: flex-end;
z-index: 2;
height: 26px;
}
.footer {
display: flex;
align-items: center;
z-index: 2;
height: 40px;
gap: 10px;
}
.content {
flex: 1;
overflow-y: hidden;
overflow-x: hidden;
z-index: 2;
width: 100%;
}
}
.main::after {
content: '';
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(50, 50, 50, 0.75);
}
</style>
@@ -0,0 +1,108 @@
<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>
</div>
<Info
:suits="configStore.result.state.suits"
:info="configStore.result.state.info"
:equs="configStore.config.equips"
:avatar="configStore.config.avatarLink"
:oaths="configStore.config.oaths"
:display="type"
v-model:active-skill-id="configStore.config.oathSkill"
></Info>
</div>
<Skill
v-if="!configStore.result.state.buffer"
:data="configStore.result.state.skills"
:standard="infoStore.standard"
></Skill>
<BufferSkill
v-else
:data="configStore.result.state.skills"
:stadard="infoStore.standard"
:standard="infoStore.standard"
></BufferSkill>
</div>
</template>
<script setup lang="ts">
import { useConfigStore, useInfoStore } from '@/stores'
import Info from './components/Info.vue'
import Skill from './components/Skill.vue'
import BufferSkill from './components/BufferSkill.vue'
const configStore = useConfigStore()
const infoStore = useInfoStore()
const type = ref<'equ' | 'oath'>('equ')
const setStandard = () => {
if (!configStore?.result?.state?.uuid) return
infoStore.setStandard(configStore.result.state, configStore.skillCountConfig)
}
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;
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;
}
}
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
text-align: center;
}
</style>
@@ -0,0 +1,247 @@
<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>
</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>
</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>
</template>
<script setup lang="ts" name="Skill">
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' },
]
const props = defineProps<{
data: IResultSkill[]
standard?: {
skills: IResultSkill[]
skillCount: IResultSkillCount[]
}
}>()
const sort = ref<string>('STRINT')
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 (['cd', 'name'].includes(sort.value)) {
// @ts-ignore
aValue = a[sort.value]
// @ts-ignore
bValue = b[sort.value]
}
const key = ['MAIN', 'ATK', 'STRINT', 'ratio'].findIndex((a) => sort.value == a)
aValue = a.buff?.[key]
bValue = b.buff?.[key]
return order.value == 'asc' ? aValue - bValue : bValue - aValue
})
})
const sortValue = (key: string) => {
if (sort.value == key) {
order.value = order.value == 'asc' ? 'desc' : 'asc'
} else {
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 })
}
},
})
}
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(),
)
}),
)
}
}
</script>
<style scoped lang="scss">
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
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;
}
.skill-lv-min::before {
content: attr(data-text);
position: absolute;
-webkit-text-stroke: 2.5px #37fa38;
font-family: Arial;
text-shadow: none;
z-index: -1;
}
</style>
@@ -0,0 +1,325 @@
<template>
<div class="bg-#000000bf w-266px flex-1 overflow-hidden flex flex-col">
<div class="h-177px flex-shrink-0">
<EquList
: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>
</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="w-60px text-#836832">{{ info.name }}</div>
<div class="ml-auto text-#3ea74e">{{ info.value }}</div>
</div>
</template>
</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"
/>
<!-- <span class="pt-4px">{{ skill.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>
</template>
<script setup lang="ts" name="Info">
import { getImageURL } from '@/utils/images'
import { rarityClass } from '@/utils'
import type { IResultUserInfo, IResultSuit } from '@/api/calc/type'
import EquList from '@/components/dnf/Equipment/List/index.vue'
import Oath from '@/components/dnf/Equipment/Oath/index.vue'
import type { IConfigEquip, IConfigOath } from '@/stores/config'
import { useInfoStore } from '@/stores'
const props = defineProps<{
info: IResultUserInfo[]
suits: {
suit?: IResultSuit
oath?: IResultSuit
correction?: number
}
equs: Record<string, IConfigEquip>
oaths: Record<string, IConfigOath>
avatar: string
display: 'oath' | 'equ'
}>()
const partOrder = [
'武器',
'上衣',
'下装',
'头肩',
'腰带',
'鞋',
'项链',
'手镯',
'戒指',
'辅助装备',
'魔法石',
'耳环',
]
const oathOrder = Array.from({ length: 12 }, (_, i) => (11 - i).toString())
const infoStore = useInfoStore()
const activeSkillId = defineModel<string>('activeSkillId', { default: '0' })
const suit = computed(() => {
return props.suits.suit
})
const oathSuit = computed(() => {
return props.suits.oath
})
// const correction = computed(() => {
// return props.suits.correction ?? 0
// })
const changeSkillId = (skill: { skillId: string; enable: boolean }) => {
// console.log(skill)
if (!skill.enable) return
if (activeSkillId.value !== skill.skillId) {
activeSkillId.value = skill.skillId
}
}
const oathSkills = computed(() => {
const skillOrder = ['2', '1', '3']
const res = [...(props.suits.oath?.skills ?? [])].sort((left, right) => {
const leftIndex = skillOrder.indexOf(left.skillId)
const rightIndex = skillOrder.indexOf(right.skillId)
const normalizedLeftIndex = leftIndex === -1 ? skillOrder.length : leftIndex
const normalizedRightIndex = rightIndex === -1 ? skillOrder.length : rightIndex
return normalizedLeftIndex - normalizedRightIndex
})
return res?.map((a) => ({
...a,
active:
(a.enable && a.id == props.suits.oath?.id) || (a.enable && a.skillId == activeSkillId.value),
}))
})
const equ = (id: string) => {
if (props.display == 'oath') {
// console.log(props.oaths, id)
return infoStore.oaths.find((e) => e.id == id)
}
return infoStore.equips.find((e) => e.id == id)
}
const adaptation = (part: string) => {
return Math.min(
equ(props.equs?.[part]?.id)?.max_adaptation ?? 0,
props.equs[part]?.adaptation ?? 0,
)
}
const oathAdaptation = (part: string) => {
return Math.min(
equ(props.oaths?.[part]?.id)?.max_adaptation ?? 0,
props.oaths[part]?.adaptation ?? 0,
)
}
const lvFormat = (lv: number, rarity: string) => {
if (rarity == '太初') return ''
switch (lv) {
case 1:
return 'I'
case 2:
return 'II'
case 3:
return 'III'
case 4:
return 'IV'
case 5:
return 'V'
default:
return ''
}
}
</script>
<style scoped lang="scss">
.partItem {
display: flex;
align-items: center;
}
.partItem:nth-child(2n + 1) {
background-color: #0b090b;
}
.partItem:nth-child(2n) {
background-color: #000100;
}
.oath-skill {
.skill-item {
&.active {
}
&:not(.active) {
filter: grayscale(100%);
}
.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;
}
}
}
}
</style>
@@ -0,0 +1,299 @@
<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>
</template>
</div>
<div class="item-head w-60px">占比</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>
</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>
</template>
<script setup lang="ts" name="Skill">
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: 'learnLv' },
{ label: 'CD', width: 50, sortKey: 'cd' },
{ label: '次数', width: 50 },
{ label: '秒伤', width: 100, sortKey: 'dps' },
{ label: '总伤害', width: 110, sortKey: 'total' },
]
const props = defineProps<{
data: IResultSkill[]
standard?: {
skills: IResultSkill[]
skillCount: IResultSkillCount[]
}
}>()
const sort = ref<string>('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
})
})
const sortValue = (key: string) => {
if (sort.value == key) {
order.value = order.value == 'asc' ? 'desc' : 'asc'
} else {
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 })
}
},
})
}
const totalDamage = computed(() => {
return props.data.reduce((acc, skill) => {
return acc + skill.damage * skillCount(skill).value
}, 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,
)?.count
return acc + skill.damage * (standardSkillCount || 0)
}, 0)
})
const showTotal = computed(() => {
if (!totalStandard.value || totalStandard.value == 0) 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],
)
}
}
</script>
<style scoped lang="scss">
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
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;
}
.skill-lv-min::before {
content: attr(data-text);
position: absolute;
-webkit-text-stroke: 2.5px #37fa38;
font-family: Arial;
text-shadow: none;
z-index: -1;
}
</style>
@@ -0,0 +1,183 @@
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'
export default defineComponent({
name: 'SkillDetail',
props: {
skillId: {
type: String,
required: true,
},
level: {
type: Number,
default: 1,
},
},
setup(props) {
const currentSkill = computed(() => props.skillId)
const currentLevel = computed(() => props.level)
const skillInfo = asyncComputed(async () => {
if (!currentSkill.value && !currentLevel.value) return undefined
const res = await api.skillDetail(currentSkill.value, currentLevel.value)
return res
})
const curTab = ref(0)
const basic = computed(() =>
!skillInfo.value
? []
: [
{ label: '技能等级', value: skillInfo.value?.attribute?.level ?? props.level },
{ label: '学习等级', value: skillInfo.value?.requiredLevel },
{ label: '等级上限', value: skillInfo.value?.maxLevel },
{
label: '冷却时间',
value: skillInfo.value?.attribute?.coolTime
? `${skillInfo.value?.attribute?.coolTime.toFixed(1)}秒`
: undefined,
},
{
label: '施放时间',
value: !skillInfo.value?.attribute?.castingTime
? '瞬发'
: `${skillInfo.value?.attribute?.castingTime.toFixed(1)}秒`,
},
{ label: '魔法值', value: skillInfo.value?.attribute?.consumeMp },
{
label: '消耗无色小晶块',
value: skillInfo.value?.consumeItem?.value
? `${skillInfo.value?.consumeItem?.value}个`
: undefined,
},
],
)
const desc = computed(() => skillInfo.value?.descDetail.split('\n'))
const detail = computed(() => {
const items =
skillInfo.value?.attribute?.optionDesc?.split('\n').filter((item) => !!item) ?? []
const details = items?.map((a) => {
const beforeReplace = a.match(/^(.+?)\s*(\{value\d+\}.*)$/)
if (beforeReplace) {
a = `${beforeReplace[1]}<span class="skill_info_value">${beforeReplace[2]}</span>`
}
const matches = a.match(/{([^}]+)}/g)
if (matches) {
matches.forEach((match) => {
const key = match.replace(/[{}]/g, '')
// Only access if key matches the expected pattern
if (/^value.+/.test(key)) {
const value = skillInfo.value?.attribute?.optionValue[key as `value${string}`] ?? '-'
if (value) {
a = a.replace(match, `<span style="padding:0px 2px;color:#4AA256">${value}</span>`)
}
}
})
}
return a
})
return details || []
})
const renderSkillDetail = () => (
<>
<div class="skill_title">基本信息</div>
<div class="flex flex-col gap-2px">
{basic.value.map(
(item) =>
!!item.value && (
<div class="flex justify-between item">
<span class="label">{item.label}</span>
<span class="value">{item.value}</span>
</div>
),
)}
</div>
<div class="skill_title">技能描述</div>
<div class="h-auto max-h-200px">
<div class="h-full overflow-y-auto bg-#161816 p-2px">
{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>
</div>
{detail.value.length > 0 && (
<>
<div class="skill_title">技能属性</div>
<div class="flex-1 overflow-y-auto">
<div class=" flex flex-col gap-2px">
{detail.value.map((item) => (
<div class="flex justify-between item items-center" v-html={item}></div>
))}
</div>
</div>
</>
)}
</>
)
const renderSkillVp = () => (
<>
<div class="h-auto">
<div class="h-full flex flex-col gap-5px">
{skillInfo.value?.evolution?.map((item) => (
<>
<div class="flex flex-col gap-2px">
<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>
</div>
</>
))}
</div>
</div>
</>
)
onMounted(() => {
watchEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
;(skillInfo.value?.evolution?.length ?? 0) == 0 && (curTab.value = 0)
})
})
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="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>
</div>
{curTab.value === 0 && renderSkillDetail()}
{curTab.value === 1 && renderSkillVp()}
</div>
</>
)
},
})
@@ -0,0 +1,97 @@
.skill_name {
border-width: 1px;
border-style: solid;
border-image-source: linear-gradient(90deg, #7d5f07 0%, #2e302e 100%);
border-image-slice: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 20px;
z-index: 5;
transform-style: preserve-3d;
color: #c1a238;
&::before {
width: 100%;
position: absolute;
content: '';
height: 50%;
bottom: 0;
background: linear-gradient(90deg, #281e06 0%, #121212 100%);
transform: translateZ(-1px);
}
&::after {
width: 100%;
position: absolute;
content: '';
height: 50%;
top: 0;
background: linear-gradient(90deg, #563e02 0%, #121212 100%);
transform: translateZ(-1px);
}
}
.skill_vp_title {
border-width: 1px;
border-style: solid;
border-image-source: linear-gradient(90deg, #691b86 0%, #2e302e 100%);
border-image-slice: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 20px;
z-index: 5;
transform-style: preserve-3d;
color: #9443be;
&::before {
width: 100%;
position: absolute;
content: '';
height: 50%;
bottom: 0;
background: linear-gradient(90deg, #3a0f47 0%, #121212 100%);
transform: translateZ(-1px);
}
&::after {
width: 100%;
position: absolute;
content: '';
height: 50%;
top: 0;
background: linear-gradient(90deg, #1e0b24 0%, #171717 100%);
transform: translateZ(-1px);
}
}
.skill_title {
border: 1px solid #3e4040;
color: #c1a238;
background: linear-gradient(180deg, #2b292b 0%, #0b0b0b 100%);
height: 20px;
display: flex;
align-items: center;
padding: 0px 5px;
}
.item {
background-color: #171616;
padding: 0px 5px;
.label {
color: #907b54;
}
.value {
color: #4aa256;
}
}
.skill_info_value {
white-space: nowrap;
display: flex;
align-items: center;
margin-left: auto;
margin-top: auto;
color: #4aa256;
}
@@ -0,0 +1,26 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_43_35)">
<rect x="1" y="1" width="16" height="16" fill="url(#paint0_linear_43_35)"/>
</g>
<rect x="0.5" y="0.5" width="17" height="17" stroke="#5D4F30"/>
<rect x="8" y="4" width="2" height="10" fill="#B7975F"/>
<rect x="14" y="8" width="2" height="10" transform="rotate(90 14 8)" fill="#B7975F"/>
<defs>
<filter id="filter0_i_43_35" x="0" y="0" width="18" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect1_innerShadow_43_35"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_43_35"/>
</filter>
<linearGradient id="paint0_linear_43_35" x1="9" y1="1" x2="9" y2="17" gradientUnits="userSpaceOnUse">
<stop stop-color="#451A1F"/>
<stop offset="0.407132" stop-color="#391619"/>
<stop offset="1" stop-color="#271111"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,26 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_43_36)">
<rect x="1" y="1" width="16" height="16" fill="url(#paint0_linear_43_36)"/>
</g>
<rect x="0.5" y="0.5" width="17" height="17" stroke="#5D4F30"/>
<path d="M9 9L3.75 14.1962V3.80385L9 9Z" fill="#B7975F"/>
<path d="M15 9L9.75 14.1962V3.80385L15 9Z" fill="#B7975F"/>
<defs>
<filter id="filter0_i_43_36" x="0" y="0" width="18" height="19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect1_innerShadow_43_36"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_43_36"/>
</filter>
<linearGradient id="paint0_linear_43_36" x1="9" y1="1" x2="9" y2="17" gradientUnits="userSpaceOnUse">
<stop stop-color="#451A1F"/>
<stop offset="0.407132" stop-color="#391619"/>
<stop offset="1" stop-color="#271111"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,25 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_43_33)">
<rect x="1" y="1" width="16" height="16" fill="url(#paint0_linear_43_33)"/>
</g>
<rect x="0.5" y="0.5" width="17" height="17" stroke="#5D4F30"/>
<path d="M9 9L14.25 3.80385V14.1962L9 9Z" fill="#B7975F"/>
<path d="M3 9L8.25 3.80385V14.1962L3 9Z" fill="#B7975F"/>
<defs>
<filter id="filter0_i_43_33" x="0" y="0" width="18" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect1_innerShadow_43_33"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_43_33"/>
</filter>
<linearGradient id="paint0_linear_43_33" x1="9" y1="1" x2="9" y2="17" gradientUnits="userSpaceOnUse">
<stop stop-color="#103562"/>
<stop offset="1" stop-color="#092343"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,24 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_i_43_34)">
<rect x="1" y="1" width="16" height="16" fill="url(#paint0_linear_43_34)"/>
</g>
<rect x="0.5" y="0.5" width="17" height="17" stroke="#5D4F30"/>
<rect x="14" y="8" width="2" height="10" transform="rotate(90 14 8)" fill="#B7975F"/>
<defs>
<filter id="filter0_i_43_34" x="0" y="0" width="18" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect1_innerShadow_43_34"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_43_34"/>
</filter>
<linearGradient id="paint0_linear_43_34" x1="9" y1="1" x2="9" y2="17" gradientUnits="userSpaceOnUse">
<stop stop-color="#103562"/>
<stop offset="1" stop-color="#092343"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,280 @@
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 './style.scss'
export default defineComponent({
name: 'SkillTree',
props: {
skills: {
type: Array as PropType<ISkill[]>,
default: () => [],
},
lvInfo: {
type: Object as PropType<Record<string, { lv: number; vp: number; up: number }>>,
default: () => ({}),
},
bindAwake: {
type: Number,
default: () => 50,
},
},
emits: ['chooseSkill'],
setup(props, { emit }) {
const skills = computed(() => props.skills)
const lvInfo = useVModel(props, 'lvInfo')
const bindAwake = useVModel(props, 'bindAwake')
// const emit = defineEmits(['chooseSkill'])
const getSkill = (lvStart: number, lvEnd: number, position: number) => {
if (position >= 0)
return skills.value.find(
(item) =>
item.line > lvStart - 5 &&
item.line <= Math.max(lvEnd - 5, 1) &&
item.position == position,
)
else
return skills.value.filter(
(item) => item.line > lvStart - 5 && item.line <= Math.max(lvEnd - 5, 1),
)
}
const lvList = [15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100]
const columns = Array.from({ length: 11 }, (_, index) => index)
const activeSkill = ref<number>(-1)
const chooseSkill = (skill?: ISkill, operation: string = 'add') => {
if (!skill) {
activeSkill.value = -1
return
}
if (activeSkill.value == skill.id) actionSkillLv(skill, operation)
else activeSkill.value = skill.id
emit('chooseSkill', skill.id)
}
const awakeSkill_1 = computed(() =>
skills.value.find((item) => item.learnLv == 50 && item.type == 'active' && item.bind),
)
const awakeSkill_2 = computed(() =>
skills.value.find((item) => item.learnLv == 85 && item.type == 'active' && item.bind),
)
const actionSkillLv = (skill: ISkill, operation: string) => {
if (operation == 'add') {
lvInfo.value[skill.id.toString()].lv = Math.min(
lvInfo.value[skill.id.toString()]?.lv + 1,
skill.maxLv,
)
return
}
if (operation == 'addMax') {
lvInfo.value[skill.id.toString()].lv = skill.maxLearnLv
return
}
if (operation == 'sub') {
lvInfo.value[skill.id.toString()].lv = Math.max(
lvInfo.value[skill.id.toString()]?.lv - 1,
0,
)
return
}
if (operation == 'subMax') {
lvInfo.value[skill.id.toString()] = { lv: 0, vp: 0, up: 0 }
return
}
emit('chooseSkill', skill.id)
}
const chooseBindAwake = (lv: number) => {
bindAwake.value = lv
}
const renderSkill = (skill: ISkill, index: number) => {
const info = lvInfo.value?.[skill.id.toString()]
let color = 'white'
if (info?.lv < skill.maxLearnLv) color = '#32E432'
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">
{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',
]}
onClick={(e) => {
e.stopPropagation()
chooseSkill(skill)
}}
onContextmenu={(e) => {
e.stopPropagation()
e.preventDefault()
chooseSkill(skill, 'sub')
}}
key={index}
>
<img
class={['h-28px w-28px', skill.type === 'passive' ? 'passive' : '']}
src={getImageURL(skill.icon)}
alt={skill.name}
></img>
<div class="w-100% flex flex-1 justify-center" 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)}
>
<img
class="h-28px w-28px mt-2px"
src={getImageURL(awakeSkill_1.value?.icon ?? '')}
></img>
<div class="w-100% mt-1px flex items-center justify-center text-#ffc701">
1次
</div>
</div>
<div
class="w-36px flex flex-col box-border items-center py-2px z-2"
onClick={() => chooseBindAwake(85)}
>
<img
class="h-28px w-28px mt-1px"
src={getImageURL(awakeSkill_2.value?.icon ?? '')}
></img>
<div class="w-100% mt-1px flex items-center justify-center text-#ffc701">
2次
</div>
</div>
</div>
</>
)}
</div>
</>
)
}
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"
disabled={lvInfo.value[skill.id.toString()]?.lv == 0}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'subMax')
}}
></CalcButton>
<CalcButton
icon="reduce"
disabled={lvInfo.value[skill.id.toString()]?.lv == 0}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'sub')
}}
></CalcButton>
<CalcButton
icon="increase"
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'add')
}}
></CalcButton>
<CalcButton
icon="max"
disabled={lvInfo.value[skill.id.toString()]?.lv >= skill.maxLearnLv}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'addMax')
}}
></CalcButton>
{/* <img
src={`${new URL('./img/min.svg', import.meta.url).href}`}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'subMax')
}}
></img>
<img
src={`${new URL('./img/sub.svg', import.meta.url).href}`}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'sub')
}}
></img>
<img
src={`${new URL('./img/add.svg', import.meta.url).href}`}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'add')
}}
></img>
<img
src={`${new URL('./img/max.svg', import.meta.url).href}`}
onClick={(e: Event) => {
e.stopPropagation()
actionSkillLv(skill, 'addMax')
}}
></img> */}
</div>
</div>
</>
</>
)
return () => (
<>
<div 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">
其<br />余
</div>
{((getSkill(0, 15, -1) ?? []) as ISkill[])?.map((skill, index) =>
renderSkill(skill, index),
)}
</div>
</>
)}
{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}>
{lv == 1 || lv % 10 == 0 ? (
<div class="w-34px h-34px flex items-center justify-center text-white">{lv}</div>
) : (
<div class="w-34px h-34px"></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>
})}
<div class="w-5px h-34px" onClick={() => (activeSkill.value = -1)}></div>
</div>
) : (
<></>
),
)}
</div>
</>
)
},
})
@@ -0,0 +1,66 @@
.skill-tree-line {
height: 80px;
width: auto;
&:nth-child(n + 1) {
background-color: #000000;
}
&:nth-child(2n + 1) {
background-color: #202020;
}
}
.passive {
filter: grayscale(100%) invert(3%) sepia(14%) saturate(874%) hue-rotate(65deg) brightness(105%)
contrast(102%);
}
.skill {
background-image: url('./img/skill.png');
background-size: 100% 100%;
}
.skill-active {
background-image: url('./img/skillActive.png');
background-size: 100% 100%;
}
.awake_50 {
background-image: url('./img/awake_50.png');
background-size: 100% 100%;
}
.awake_85 {
background-image: url('./img/awake_85.png');
background-size: 100% 100%;
}
.vp_1 {
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../img/vp_1.png);
}
.vp_2 {
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../img/vp_2.png);
}
.up_1 {
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../img/up_1.png);
}
.up_2 {
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../img/up_2.png);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,307 @@
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 CaclIconSelect from '@/components/calc/iconselect/index.vue'
import CalcOption from '@/components/calc/option/index.vue'
import { renderList } from 'vue'
import './style.scss'
export default defineComponent({
name: 'SkillTree',
props: {
skills: {
type: Array as PropType<ISkill[]>,
default: () => [],
},
lvInfo: {
type: Object as PropType<Record<string, { lv: number; up: number }>>,
default: () => ({}),
},
buffer: {
type: Boolean,
default: false,
},
},
setup(props) {
const lvInfo = useVModel(props, 'lvInfo')
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
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),
)
.sort((a, b) => a.learnLv - b.learnLv),
)
}
const itemChecked = (index: number, up: number) => {
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 currentUP = lvInfo.value[currentUPSkill]?.up
return currentUP == up ? currentUPSkill : undefined
},
set(value: string | undefined) {
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 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
}
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>
</>
))}
</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"
>
<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>
</>
))}
</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%`
}
}
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>
</div>
) : (
<></>
)}
</>
)
},
})
@@ -0,0 +1,75 @@
.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;
.left {
width: 240px;
height: 325px;
border-radius: 2px;
border: 1px solid #2e2e2e;
background: linear-gradient(to bottom, #1c1c1c 0%, #0a0d0a 100%);
display: flex;
padding: 5px 10px;
.up_head_1,
.up_head_2 {
display: flex;
flex-direction: column;
align-items: center;
.icon {
width: 80px;
height: 80px;
background-position: center center;
}
}
.up_head_1 .icon {
background-image: url('./img/attack.png');
}
.up_head_2 .icon {
background-image: url('./img/cd.png');
}
}
.right {
.title {
width: 261px;
height: 48px;
background: url('./img/title.png') no-repeat center center;
}
}
.up_1,
.up_2,
.up_3,
.up_active {
width: 54px;
height: 54px;
display: flex;
align-items: center;
justify-content: center;
}
.up_1 {
background: url('./img/up_1.png') no-repeat center center;
}
.up_2 {
background: url('./img/up_2.png') no-repeat center center;
}
.up_3 {
background: url('./img/up_3.png') no-repeat center center;
}
.up_active {
background: url('./img/up_active.png') no-repeat center center;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

@@ -0,0 +1,132 @@
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 { renderList } from 'vue'
import './style.scss'
export default defineComponent({
name: 'SkillVP',
props: {
skills: {
type: Array as PropType<ISkill[]>,
default: () => [],
},
lvInfo: {
type: Object as PropType<Record<string, { lv: number; vp: number }>>,
default: () => ({}),
},
},
emits: ['chooseSkill'],
setup(props, { emit }) {
const lvInfo = useVModel(props, 'lvInfo')
const skills = computed(() =>
props.skills.filter((a) => a.hasVP && lvInfo).sort((a, b) => a.learnLv - b.learnLv),
)
const currentUsedVP = computed(() => {
return Object.keys(lvInfo.value)
.filter((a) => skills.value.find((skill) => skill.id.toString() === a))
.map((a) => lvInfo.value[a].vp)
.filter((a) => !!a && a > 0).length
})
const itemChecked = (skillID: string, vp: number) => {
return computed<boolean>({
get() {
if (!lvInfo.value[skillID].vp) {
lvInfo.value[skillID].vp = 0
}
return lvInfo.value[skillID]?.vp == vp
},
set(value: boolean) {
// 选中当前VP形态 并且当前已选VP大于5 并且当前技能没有VP
if (value && currentUsedVP.value >= 5 && lvInfo.value[skillID]?.vp == 0) {
return
}
if (value) {
lvInfo.value[skillID].vp = vp
} else {
lvInfo.value[skillID].vp = 0
}
},
})
}
return () => (
<>
<div class="h-auto py-2 flex flex-col bg-#080808 box-content">
{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>
<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',
]}
onClick={() => {
emit('chooseSkill', item.id)
itemChecked(item.id.toString(), index).value = !itemChecked(
item.id.toString(),
index,
).value
}}
>
{' '}
<div
class={[
'box-border w-full h-full pt-5px m-2px',
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
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>
<img
style="filter:brightness(1) contrast(1) saturate(1.5)"
class="w-28px h-28px"
src={getImageURL(item.icon)}
alt={item.name}
/>
<div class={`vp_${index} absolute bottom--8px left-5px z-10`}></div>
</div>
<div class="w-16px"></div>
</div>
<div class="text-13px mt-5px text-center 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>
</div>
))}
</div>
))}
</div>
</>
)
},
})
@@ -0,0 +1,47 @@
.vp {
&:nth-child(n + 1) {
background-color: #000000;
}
&:nth-child(2n + 1) {
background-color: #080808;
}
.vp-item {
height: 120px;
width: 148px;
padding: 1px;
display: flex;
align-items: center;
justify-content: center;
background-image: url(./img/bg.png);
// padding:5px 10px ;
box-sizing: border-box;
}
.selected {
background-image: url(./img/selected.png);
background-repeat: no-repeat;
background-position: center center;
background-size: calc(100% - 2.5px) calc(100% + 2.5px);
.selected-name {
color: #cf57ff !important;
}
}
.vp_1 {
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../img/vp_1.png);
}
.vp_2 {
width: 18px;
height: 18px;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../img/vp_2.png);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

@@ -0,0 +1,172 @@
<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>
<div class="flex-1 w-550px overflow-x-hidden bg-#101012">
<div class="overflow-y-auto h-full box-border" v-if="tab === 0">
<SkillTree
:skills="infoStore.skills"
v-model:lvInfo="configStore.config.skills"
v-model:bindAwake="configStore.config.bindAwake"
@chooseSkill="setCurrentSkill"
></SkillTree>
</div>
<div v-if="tab === 1" class="flex flex-col overflow-y-hidden h-full">
<div class="item-head" 角色等级>技能突破</div>
<SkillUP
class="flex-1"
: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">
<SkillVP
:skills="infoStore.skills"
v-model:lvInfo="configStore.config.skills"
v-model:bindAwake="configStore.config.bindAwake"
@chooseSkill="setCurrentSkill"
></SkillVP>
</div>
</div>
</div>
</div>
<div
class="absolute right--65px"
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="border-1px border-#514531 border-solid rounded-2px flex flex-col items-center justify-center p-5px"
style="background: linear-gradient(to bottom, #313330 0%, #0f110f 100%)"
@click="showSkillDetail = true"
>
<img class="w-30px h-30px" src="./components/img/detail.png" />
技能详情
</div>
</div>
</div>
</div>
</div>
<div
class="bg-#101012 w-300px h-full absolute"
:style="style"
ref="SkillDetailRef"
v-if="curCurrentSkill.skillId && curCurrentSkill.level > 0 && showSkillDetail"
>
<SkillDetail :skillId="curCurrentSkill.skillId" :level="curCurrentSkill.level"></SkillDetail>
</div>
</template>
<script lang="ts" setup name="Index">
import { useInfoStore } from '@/stores/info'
import SkillTree from './components/SkillTree'
import SkillVP from './components/SkillVP'
import SkillUP from './components/SkillUP'
import { useConfigStore } from '@/stores'
import SkillDetail from './components/SkillDetail'
import { onClickOutside } from '@vueuse/core'
const props = defineProps<{
alter: string
}>()
const tab = ref(0)
const infoStore = useInfoStore()
const configStore = useConfigStore()
const curSkillId = ref()
const curCurrentSkill = computed(() => {
const skill = infoStore.skills.find((skill) => skill.uuid === curSkillId.value)
return {
skillId: skill?.uuid ?? '',
level: configStore.config.skills[skill?.id || 0]?.lv || 0,
}
})
const showSkillDetail = ref(false)
const skillTreeRef = ref<HTMLHtmlElement>()
const SkillDetailRef = ref<HTMLHtmlElement>()
const style = computed(() => {
return {
zIndex: 99,
height: `${skillTreeRef.value?.clientHeight ?? 0}px`,
left: `${(skillTreeRef.value?.clientWidth ?? 0) + 5}px`,
}
})
const setCurrentSkill = (skillId: number) => {
curSkillId.value = infoStore.skills.find((skill) => skill.id === skillId)?.uuid || ''
}
onClickOutside(
SkillDetailRef,
(event) => {
showSkillDetail.value = false
},
{
ignore: [skillTreeRef],
},
)
</script>
<style lang="scss" scoped>
.active {
color: #ccc088;
position: relative;
&::before {
content: '';
position: absolute;
width: 80px;
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%);
}
}
.item-head {
background: linear-gradient(#2b2817, #171407);
border-top: 1px solid #423d2c;
border-bottom: 1px solid #211d15;
text-align: center;
// 以下后面注释掉
height: 20px;
line-height: 20px;
font-weight: 14px;
}
</style>