update
This commit is contained in:
@@ -108,6 +108,8 @@ export interface ISkill {
|
||||
name: string
|
||||
desc: string
|
||||
}[]
|
||||
/** 后端下发的技能树坐标:[列, 行]。缔造者与黑暗武士使用扩展网格。 */
|
||||
cellPosition: number[]
|
||||
uuid: string
|
||||
upType: 'damage' | 'heal'
|
||||
}
|
||||
|
||||
@@ -47,6 +47,6 @@ const props = withDefaults(
|
||||
}
|
||||
|
||||
.ui-data-row.is-disabled {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
color: var(--ui-unavailable-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,8 +12,17 @@ const props = withDefaults(
|
||||
disabled?: boolean
|
||||
expandedLabel?: string
|
||||
collapsedLabel?: string
|
||||
compact?: boolean
|
||||
showState?: boolean
|
||||
indicatorPosition?: 'start' | 'end'
|
||||
}>(),
|
||||
{ expandedLabel: '已展开', collapsedLabel: '未展开' },
|
||||
{
|
||||
expandedLabel: '已展开',
|
||||
collapsedLabel: '未展开',
|
||||
compact: false,
|
||||
showState: true,
|
||||
indicatorPosition: 'start',
|
||||
},
|
||||
)
|
||||
|
||||
const open = defineModel<boolean>('open', { default: true })
|
||||
@@ -23,12 +32,21 @@ const open = defineModel<boolean>('open', { default: true })
|
||||
<CollapsibleRoot
|
||||
v-model:open="open"
|
||||
:disabled="disabled"
|
||||
:class="cn('ui-data-section', props.class)"
|
||||
:class="
|
||||
cn(
|
||||
'ui-data-section',
|
||||
compact && 'ui-data-section--compact',
|
||||
indicatorPosition === 'end' && 'ui-data-section--indicator-end',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<CollapsibleTrigger class="ui-data-section__trigger">
|
||||
<img :src="open ? expandedIcon : collapsedIcon" alt="" aria-hidden="true" />
|
||||
<span class="ui-data-section__title">{{ title }}</span>
|
||||
<span class="ui-data-section__state">{{ open ? expandedLabel : collapsedLabel }}</span>
|
||||
<span v-if="showState" class="ui-data-section__state">
|
||||
{{ open ? expandedLabel : collapsedLabel }}
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent class="ui-data-section__content">
|
||||
<slot />
|
||||
@@ -99,4 +117,12 @@ const open = defineModel<boolean>('open', { default: true })
|
||||
.ui-data-section__content {
|
||||
padding: var(--ui-disclosure-content-padding-block) var(--ui-field-padding-inline);
|
||||
}
|
||||
|
||||
.ui-data-section--compact .ui-data-section__trigger {
|
||||
min-height: var(--ui-control-height-compact);
|
||||
}
|
||||
|
||||
.ui-data-section--indicator-end .ui-data-section__trigger > img {
|
||||
order: 3;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -207,4 +207,11 @@ function fastChange(direction: 1 | -1) {
|
||||
.ui-number-field__fast-step button + button span {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.ui-number-field {
|
||||
height: 50px;
|
||||
grid-template-columns: minmax(0, 1fr) 25px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,7 +66,7 @@ const page = defineModel<number>('page', { default: 1 })
|
||||
border: 1px solid var(--ui-pagination-border);
|
||||
outline: none;
|
||||
background: var(--ui-pagination-background);
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
color: var(--ui-muted-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ const page = defineModel<number>('page', { default: 1 })
|
||||
}
|
||||
|
||||
.ui-pagination__button:disabled {
|
||||
color: var(--ui-control-disabled-foreground);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ const visibleMarkers = computed(() => {
|
||||
|
||||
<template>
|
||||
<ProgressRoot v-bind="delegatedProps" :class="cn('ui-progress', props.class)">
|
||||
<ProgressIndicator class="ui-progress__indicator" :style="{ width: `${percentage}%` }" />
|
||||
<ProgressIndicator
|
||||
class="ui-progress__indicator"
|
||||
:style="{ transform: `scaleX(${percentage / 100})` }"
|
||||
/>
|
||||
<span
|
||||
v-for="milestone in visibleMarkers"
|
||||
:key="milestone"
|
||||
@@ -52,10 +55,12 @@ const visibleMarkers = computed(() => {
|
||||
}
|
||||
|
||||
.ui-progress__indicator {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--ui-progress-fill);
|
||||
box-shadow: inset 0 0 0 1px var(--ui-progress-fill-inset);
|
||||
transition: width 180ms ease-out;
|
||||
transform-origin: left center;
|
||||
transition: transform 180ms ease-out;
|
||||
}
|
||||
|
||||
.ui-progress__marker {
|
||||
@@ -68,4 +73,10 @@ const visibleMarkers = computed(() => {
|
||||
border-left: 4px solid transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ui-progress__indicator {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
<template>
|
||||
<RatingRoot v-bind="forwarded" :class="cn('ui-rating', props.class)" :aria-label="props.label">
|
||||
<RatingItem v-for="item in props.length" :key="item" :item="item" class="ui-rating__item">
|
||||
<RatingItem
|
||||
v-for="item in props.length"
|
||||
:key="item"
|
||||
:item="item"
|
||||
class="ui-rating__item"
|
||||
:aria-label="`${props.label}:${item} / ${props.length}`"
|
||||
>
|
||||
<img :src="emptyStar" alt="" />
|
||||
<RatingItemIndicator :step="item" class="ui-rating__indicator">
|
||||
<img :src="activeStar" alt="" />
|
||||
|
||||
@@ -31,7 +31,7 @@ function selectRow(row: UiShopItem) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('ui-shop-list', props.class)" role="listbox" aria-label="物品列表">
|
||||
<div :class="cn('ui-shop-list', props.class)" role="grid" aria-label="物品列表">
|
||||
<div class="ui-shop-list__surface">
|
||||
<div
|
||||
v-for="(row, index) in rows"
|
||||
@@ -42,7 +42,7 @@ function selectRow(row: UiShopItem) {
|
||||
'is-selected': selected === row.id,
|
||||
'is-disabled': row.disabled,
|
||||
}"
|
||||
role="option"
|
||||
role="row"
|
||||
:aria-selected="selected === row.id"
|
||||
:aria-disabled="row.disabled || undefined"
|
||||
:tabindex="row.disabled ? -1 : 0"
|
||||
@@ -51,11 +51,11 @@ function selectRow(row: UiShopItem) {
|
||||
@keydown.space.prevent="selectRow(row)"
|
||||
>
|
||||
<i class="ui-shop-row__frame" aria-hidden="true" />
|
||||
<div class="ui-shop-row__icon"><img :src="row.image" alt="" /></div>
|
||||
<div>{{ row.reinforce }}</div>
|
||||
<div>{{ row.amplify }}</div>
|
||||
<div :class="row.rarity && `quality-${row.rarity}`">{{ row.name }}</div>
|
||||
<div>
|
||||
<div class="ui-shop-row__icon" role="gridcell"><img :src="row.image" alt="" /></div>
|
||||
<div role="gridcell">{{ row.reinforce }}</div>
|
||||
<div role="gridcell">{{ row.amplify }}</div>
|
||||
<div role="gridcell" :class="row.rarity && `quality-${row.rarity}`">{{ row.name }}</div>
|
||||
<div role="gridcell">
|
||||
<button
|
||||
type="button"
|
||||
class="ui-shop-row__inspect"
|
||||
@@ -65,8 +65,10 @@ function selectRow(row: UiShopItem) {
|
||||
<img :src="inspectIcon" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<div :class="row.statusTone && `status-${row.statusTone}`">{{ row.status }}</div>
|
||||
<div class="ui-shop-row__owner">{{ row.owner }}</div>
|
||||
<div role="gridcell" :class="row.statusTone && `status-${row.statusTone}`">
|
||||
{{ row.status }}
|
||||
</div>
|
||||
<div class="ui-shop-row__owner" role="gridcell">{{ row.owner }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,6 +307,6 @@ function selectRow(row: UiShopItem) {
|
||||
color: var(--ui-success);
|
||||
}
|
||||
.status-danger {
|
||||
color: var(--ui-destructive);
|
||||
color: var(--ui-danger-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<TabsListProps & { class?: HTMLAttributes['class']; marker?: 'arrow' | 'line' }>(),
|
||||
{ marker: 'arrow' },
|
||||
{ marker: 'line' },
|
||||
)
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _class, marker: _marker, ...delegated } = props
|
||||
@@ -22,6 +22,12 @@ const delegatedProps = computed(() => {
|
||||
const listElement = ref<HTMLElement | ComponentPublicInstance>()
|
||||
const markerLeft = ref('0px')
|
||||
const markerReady = ref(false)
|
||||
const itemWidth = ref('auto')
|
||||
const groupWidth = ref('max-content')
|
||||
const geometryStyle = computed(() => ({
|
||||
'--ui-tab-item-width': itemWidth.value,
|
||||
'--ui-tabs-group-width': groupWidth.value,
|
||||
}))
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let mutationObserver: MutationObserver | undefined
|
||||
|
||||
@@ -42,19 +48,60 @@ function positionMarker(animate = true) {
|
||||
requestAnimationFrame(() => (markerReady.value = true))
|
||||
}
|
||||
|
||||
function equalizeItems() {
|
||||
const list = getListElement()
|
||||
const triggers = list ? [...list.querySelectorAll<HTMLElement>('[role="tab"]')] : []
|
||||
if (!list || !triggers.length) return
|
||||
|
||||
const labels = triggers.map(
|
||||
(trigger) => trigger.querySelector<HTMLElement>('.ui-tabs-trigger__label') ?? trigger,
|
||||
)
|
||||
const widestLabel = Math.max(
|
||||
0,
|
||||
...labels.map((label) => Math.max(label.scrollWidth, label.getBoundingClientRect().width)),
|
||||
)
|
||||
const listStyle = getComputedStyle(list)
|
||||
const triggerStyle = getComputedStyle(triggers[0])
|
||||
const groupPadding =
|
||||
Number.parseFloat(listStyle.paddingLeft) + Number.parseFloat(listStyle.paddingRight)
|
||||
const itemPadding =
|
||||
Number.parseFloat(triggerStyle.paddingLeft) + Number.parseFloat(triggerStyle.paddingRight)
|
||||
const parentWidth = list.parentElement?.clientWidth ?? list.clientWidth
|
||||
const availableWidth = Math.max(0, parentWidth - groupPadding)
|
||||
if (!availableWidth) return
|
||||
|
||||
const equalizedItemWidth = Math.min(
|
||||
Math.ceil(widestLabel + itemPadding),
|
||||
availableWidth / triggers.length,
|
||||
)
|
||||
itemWidth.value = `${equalizedItemWidth}px`
|
||||
groupWidth.value = `${equalizedItemWidth * triggers.length + groupPadding}px`
|
||||
}
|
||||
|
||||
async function updateGeometry(animate = true) {
|
||||
equalizeItems()
|
||||
await nextTick()
|
||||
positionMarker(animate)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
positionMarker(false)
|
||||
document.fonts?.ready.then(() => positionMarker(false))
|
||||
resizeObserver = new ResizeObserver(() => positionMarker(false))
|
||||
await updateGeometry(false)
|
||||
document.fonts?.ready.then(() => updateGeometry(false))
|
||||
resizeObserver = new ResizeObserver(() => updateGeometry(false))
|
||||
const list = getListElement()
|
||||
if (list) resizeObserver.observe(list)
|
||||
mutationObserver = new MutationObserver(() => positionMarker())
|
||||
if (list) {
|
||||
resizeObserver.observe(list)
|
||||
if (list.parentElement) resizeObserver.observe(list.parentElement)
|
||||
}
|
||||
mutationObserver = new MutationObserver(() => updateGeometry())
|
||||
if (list) {
|
||||
mutationObserver.observe(list, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['data-state'],
|
||||
childList: true,
|
||||
characterData: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -66,7 +113,12 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsList ref="listElement" v-bind="delegatedProps" :class="cn('ui-tabs-list', props.class)">
|
||||
<TabsList
|
||||
ref="listElement"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('ui-tabs-list', props.class)"
|
||||
:style="geometryStyle"
|
||||
>
|
||||
<slot />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -84,19 +136,22 @@ onBeforeUnmount(() => {
|
||||
.ui-tabs-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
width: var(--ui-tabs-group-width, max-content);
|
||||
max-width: 100%;
|
||||
height: var(--ui-tabs-height);
|
||||
align-items: center;
|
||||
padding: 5px 7px;
|
||||
justify-content: flex-start;
|
||||
padding: 4px 6px;
|
||||
background: var(--ui-tab-list-background);
|
||||
}
|
||||
|
||||
.ui-tabs-list::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 35px;
|
||||
left: 7px;
|
||||
right: 6px;
|
||||
top: 28px;
|
||||
left: 6px;
|
||||
height: 1px;
|
||||
background: var(--ui-tab-divider);
|
||||
}
|
||||
|
||||
@@ -23,18 +23,20 @@ const delegatedProps = computed(() => {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
width: var(--ui-tab-item-width, auto);
|
||||
height: var(--ui-tab-height);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
padding: 0 12px;
|
||||
flex: 0 0 var(--ui-tab-item-width, auto);
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: var(--ui-tab-background);
|
||||
color: var(--ui-tab-foreground);
|
||||
font:
|
||||
16px / var(--ui-control-line-height) 'DCalc',
|
||||
14px / var(--ui-control-line-height) 'DCalc',
|
||||
SimSun,
|
||||
sans-serif;
|
||||
text-shadow: 1px 1px 0 var(--ui-dialog-titlebar-text-shadow);
|
||||
@@ -44,10 +46,10 @@ const delegatedProps = computed(() => {
|
||||
.ui-tabs-trigger:not(:last-of-type)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
top: 4px;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 19px;
|
||||
height: 15px;
|
||||
background: linear-gradient(180deg, transparent, var(--ui-tab-separator) 22% 78%, transparent);
|
||||
}
|
||||
|
||||
@@ -72,7 +74,11 @@ const delegatedProps = computed(() => {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
line-height: var(--ui-control-line-height);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transform: translateY(var(--ui-control-text-offset-y));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,8 +16,20 @@ export default defineComponent({
|
||||
const { state, execute, isLoading } = useAsyncState(
|
||||
async () => {
|
||||
const res = await api.getSkillFromCode(skillCode.value)
|
||||
// console.log(res)
|
||||
configStore.config.skills = res.skills
|
||||
const shortcuts = (res.shortcuts ?? []) as { skill_id: number; slot_index: number }[]
|
||||
const combos = (res.combos ?? []) as {
|
||||
slot_index: number
|
||||
skills: { skill_id: number }[]
|
||||
}[]
|
||||
const importedShortcuts = res.job?.name === 'dark_knight' ? shortcuts.slice(6) : shortcuts
|
||||
configStore.config.skillShortcuts = {
|
||||
shortcuts: importedShortcuts.map((item) => item.skill_id?.toString() ?? ''),
|
||||
combos:
|
||||
res.job?.name === 'dark_knight'
|
||||
? combos.map((item) => item.skills.map((skill) => skill.skill_id?.toString() ?? ''))
|
||||
: [],
|
||||
}
|
||||
const { alert } = useDialog()
|
||||
visible.value = false
|
||||
await alert({
|
||||
|
||||
@@ -58,8 +58,12 @@
|
||||
|
||||
<div class="equipment-groups">
|
||||
<template v-for="group in equs" :key="group.name">
|
||||
<section v-if="group.equs.length" class="equipment-group">
|
||||
<h2 class="equipment-group__heading">{{ group.name }}</h2>
|
||||
<UiDataSection
|
||||
v-if="group.equs.length"
|
||||
class="equipment-group"
|
||||
:title="group.name"
|
||||
compact
|
||||
>
|
||||
<div class="equipment-grid">
|
||||
<button
|
||||
v-for="(equipment, index) in group.equs"
|
||||
@@ -82,7 +86,7 @@
|
||||
<EquipmentIcon :inactive="!isActive(equipment)" :equipment="equipment" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</UiDataSection>
|
||||
</template>
|
||||
|
||||
<div v-if="!hasEquipmentResults" class="equipment-empty">没有符合当前条件的装备</div>
|
||||
@@ -109,6 +113,7 @@
|
||||
v-model:open="baseOpen"
|
||||
class="equipment-detail-section"
|
||||
title="基础信息"
|
||||
compact
|
||||
>
|
||||
<div class="equipment-detail-content">
|
||||
<Info :equipment="curEquInfo" />
|
||||
@@ -120,6 +125,7 @@
|
||||
v-model:open="suitOpen"
|
||||
class="equipment-detail-section"
|
||||
title="套装信息"
|
||||
compact
|
||||
>
|
||||
<LevelControls
|
||||
:rarity="curSuitLv.rarity"
|
||||
@@ -137,6 +143,7 @@
|
||||
v-model:open="oathOpen"
|
||||
class="equipment-detail-section"
|
||||
title="誓约信息"
|
||||
compact
|
||||
>
|
||||
<LevelControls
|
||||
:rarity="curOathSkillLv.rarity"
|
||||
@@ -531,21 +538,8 @@ function chooseEqu(equipment?: IEquipment) {
|
||||
margin-top: var(--ui-control-gap);
|
||||
}
|
||||
|
||||
.equipment-group__heading {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-height: var(--ui-disclosure-header-min-block-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 0 var(--ui-autocomplete-trigger-gap);
|
||||
padding-inline: var(--ui-field-padding-inline);
|
||||
border-top: 1px solid var(--ui-disclosure-border);
|
||||
border-bottom: 1px solid var(--ui-disclosure-divider);
|
||||
background: var(--ui-disclosure-header-background);
|
||||
color: var(--ui-settings-section-foreground);
|
||||
font-size: var(--ui-control-font-size-default);
|
||||
font-weight: 400;
|
||||
line-height: var(--ui-control-line-height);
|
||||
.equipment-group :deep(.ui-data-section__content) {
|
||||
padding: var(--ui-autocomplete-trigger-gap);
|
||||
}
|
||||
|
||||
.equipment-grid {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap equ-profile">
|
||||
<div class="equ-profile-item">
|
||||
<div class="equ-profile-item equ-profile-item--wide">
|
||||
<div class="row-name">迷雾系统</div>
|
||||
<ForgeSlider
|
||||
v-model="sundryData['fog']"
|
||||
@@ -11,7 +11,7 @@
|
||||
:step="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="equ-profile-item">
|
||||
<div class="equ-profile-item equ-profile-item--wide">
|
||||
<div class="row-name">冒险团</div>
|
||||
<ForgeSlider
|
||||
v-model="sundryData['adventure']"
|
||||
@@ -22,7 +22,7 @@
|
||||
:step="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="equ-profile-item">
|
||||
<div class="equ-profile-item equ-profile-item--wide">
|
||||
<div class="row-name">结婚系统</div>
|
||||
<ForgeSelect
|
||||
v-for="(key, index) in ['marriage_house', 'marriage_ring']"
|
||||
@@ -40,7 +40,7 @@
|
||||
:options="sundryOptions('contract')"
|
||||
/>
|
||||
</div>
|
||||
<div class="equ-profile-item">
|
||||
<div class="equ-profile-item equ-profile-item--wide">
|
||||
<div class="row-name">收集箱</div>
|
||||
<ForgeSelect
|
||||
v-for="(key, index) in ['collection_type', 'collection_num_0', 'collection_num_1']"
|
||||
|
||||
@@ -1,33 +1,59 @@
|
||||
<template>
|
||||
<main class="forge-page">
|
||||
<UiDataSection title="装备/时装/徽章打造(点击部位切换)">
|
||||
<TabsRoot v-model="partTab">
|
||||
<UiTabsList class="forge-tabs" aria-label="打造类型">
|
||||
<UiTabsTrigger :value="0">装备</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="2">誓约</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="1">时装</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="3">徽章</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
</TabsRoot>
|
||||
<Equipment v-if="partTab === 0" />
|
||||
<Avatars v-else-if="partTab === 1" />
|
||||
<Oaths v-else-if="partTab === 2" />
|
||||
<Emblems v-else />
|
||||
</UiDataSection>
|
||||
<div class="forge-page__editor">
|
||||
<UiDataSection title="装备/时装/徽章打造(点击部位切换)">
|
||||
<TabsRoot v-model="partTab">
|
||||
<UiTabsList class="forge-tabs" aria-label="打造类型">
|
||||
<UiTabsTrigger :value="0">装备</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="2">誓约</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="1">时装</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="3">徽章</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
</TabsRoot>
|
||||
<Equipment v-if="partTab === 0" />
|
||||
<Avatars v-else-if="partTab === 1" />
|
||||
<Oaths v-else-if="partTab === 2" />
|
||||
<Emblems v-else />
|
||||
</UiDataSection>
|
||||
|
||||
<UiDataSection title="辟邪玉">
|
||||
<Jades />
|
||||
</UiDataSection>
|
||||
<UiDataSection title="辟邪玉">
|
||||
<Jades />
|
||||
</UiDataSection>
|
||||
|
||||
<UiDataSection title="其它">
|
||||
<Sundry />
|
||||
</UiDataSection>
|
||||
<UiDataSection title="其它">
|
||||
<Sundry />
|
||||
</UiDataSection>
|
||||
</div>
|
||||
|
||||
<aside class="forge-page__summary" aria-label="打造摘要">
|
||||
<header>打造摘要</header>
|
||||
<UiDataRow
|
||||
><span>已穿戴装备</span><strong>{{ equippedCount }}</strong></UiDataRow
|
||||
>
|
||||
<UiDataRow
|
||||
><span>已配置增幅</span><strong>{{ reinforcedCount }}</strong></UiDataRow
|
||||
>
|
||||
<UiDataRow
|
||||
><span>已配置附魔</span><strong>{{ enchantedCount }}</strong></UiDataRow
|
||||
>
|
||||
<UiDataRow
|
||||
><span>已配置融合</span><strong>{{ fusionCount }}</strong></UiDataRow
|
||||
>
|
||||
<UiDataRow
|
||||
><span>已配置时装</span><strong>{{ avatarCount }}</strong></UiDataRow
|
||||
>
|
||||
<UiDataRow
|
||||
><span>辟邪玉槽位</span><strong>{{ jadeCount }}</strong></UiDataRow
|
||||
>
|
||||
<p>左侧继续使用现有打造组件;摘要只读取当前配置,不改变计算数据。</p>
|
||||
</aside>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UiDataSection } from '@/components/ui/data-section'
|
||||
import { UiDataRow, UiDataSection } from '@/components/ui/data-section'
|
||||
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
|
||||
import { useConfigStore } from '@/stores'
|
||||
import Avatars from './components/Avatars.vue'
|
||||
import Emblems from './components/Emblems.vue'
|
||||
import Equipment from './components/Equipments.vue'
|
||||
@@ -36,21 +62,90 @@ import Oaths from './components/Oaths.vue'
|
||||
import Sundry from './components/Sundry.vue'
|
||||
|
||||
const partTab = ref(0)
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const equipmentValues = computed(() => Object.values(configStore.config.equips))
|
||||
const equippedCount = computed(() => equipmentValues.value.filter((item) => item.id).length)
|
||||
const reinforcedCount = computed(
|
||||
() => equipmentValues.value.filter((item) => item.reinforce > 0 || item.refine > 0).length,
|
||||
)
|
||||
const enchantedCount = computed(
|
||||
() => equipmentValues.value.filter((item) => Boolean(item.enchant)).length,
|
||||
)
|
||||
const fusionCount = computed(
|
||||
() =>
|
||||
equipmentValues.value.filter((item) => Boolean(item.fusion) || item.weaponFusion > 0).length,
|
||||
)
|
||||
const avatarCount = computed(
|
||||
() => Object.values(configStore.config.avatar).filter((item) => Boolean(item.id)).length,
|
||||
)
|
||||
const jadeCount = computed(
|
||||
() => Object.values(configStore.config.jades).filter((item) => item.id >= 0).length,
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.forge-page {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
max-width: 840px;
|
||||
min-width: 0;
|
||||
padding-right: clamp(4px, 0.8vw, 12px);
|
||||
display: grid;
|
||||
align-content: start;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(170px, 0.3fr);
|
||||
gap: clamp(6px, 0.8vw, 12px);
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
container-type: inline-size;
|
||||
|
||||
.forge-page__editor,
|
||||
.forge-page__summary {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.forge-page__editor {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: clamp(6px, 0.8vw, 12px);
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.forge-page__summary {
|
||||
border: 1px solid var(--ui-composition-border);
|
||||
background: var(--ui-composition-background);
|
||||
overflow: auto;
|
||||
|
||||
> header {
|
||||
display: flex;
|
||||
min-height: var(--ui-disclosure-header-min-block-size);
|
||||
align-items: center;
|
||||
padding-inline: var(--ui-field-padding-inline);
|
||||
border-bottom: 1px solid var(--ui-disclosure-divider);
|
||||
background: var(--ui-disclosure-header-background);
|
||||
color: var(--ui-settings-section-foreground);
|
||||
}
|
||||
|
||||
.ui-data-row {
|
||||
justify-content: space-between;
|
||||
padding-inline: var(--ui-field-padding-inline);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--ui-disclosure-row-selected-foreground);
|
||||
font:
|
||||
400 12px Consolas,
|
||||
monospace;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: var(--ui-control-padding-inline-default);
|
||||
color: var(--ui-muted-foreground);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.forge-tabs {
|
||||
margin-bottom: clamp(8px, 1.2cqi, 14px);
|
||||
}
|
||||
@@ -136,5 +231,13 @@ const partTab = ref(0)
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 650px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
.forge-page__summary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,37 +10,37 @@
|
||||
@update:model-value="navigateCharacterTab"
|
||||
>
|
||||
<UiTabsList aria-label="角色配置导航">
|
||||
<UiTabsTrigger :value="`/character/equipment/${props.alter}`">装备设置</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="`/character/skill/${props.alter}`">技能设置</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="`/character/forge/${props.alter}`">打造设置</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="`/character/equipment/${props.alter}`">装备</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="`/character/skill/${props.alter}`">技能</UiTabsTrigger>
|
||||
<UiTabsTrigger :value="`/character/forge/${props.alter}`">打造</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
</TabsRoot>
|
||||
<div class="character-shell__tools" aria-label="计算工具">
|
||||
<label class="character-toggle">
|
||||
<UiSwitch v-model="configStore.config.DSB" />
|
||||
<span>系统奶</span>
|
||||
</label>
|
||||
<label class="character-toggle">
|
||||
<UiSwitch v-model="configStore.config.BUFF" />
|
||||
<span>组队</span>
|
||||
</label>
|
||||
<UiButton size="icon" title="打造导入" aria-label="打造导入" @click="showImport">
|
||||
<img data-icon class="character-toolbar-icon" :src="importIcon" alt="" />
|
||||
</UiButton>
|
||||
<UiButton size="icon" title="技能导入" aria-label="技能导入" @click="showImportSkill">
|
||||
<img data-icon class="character-toolbar-icon" :src="exchangeIcon" alt="" />
|
||||
</UiButton>
|
||||
<UiButton :disabled="isExecuting" @click="recalculate">开始计算</UiButton>
|
||||
</div>
|
||||
</header>
|
||||
<div class="character-shell__workspace">
|
||||
<section v-if="!!infoStore.infos?.alter" class="character-shell__config">
|
||||
<RouterView></RouterView>
|
||||
</section>
|
||||
<div class="character-shell__stage" aria-hidden="true"></div>
|
||||
<aside class="character-shell__result" aria-label="计算结果">
|
||||
<Result></Result>
|
||||
<Result :show-overview="route.name === 'equipment'" />
|
||||
</aside>
|
||||
</div>
|
||||
<footer class="character-shell__footer">
|
||||
<label class="character-toggle">
|
||||
<UiSwitch v-model="configStore.config.DSB" />
|
||||
<span>系统奶</span>
|
||||
</label>
|
||||
<label class="character-toggle">
|
||||
<UiSwitch v-model="configStore.config.BUFF" />
|
||||
<span>组队</span>
|
||||
</label>
|
||||
<UiButton size="icon" title="打造导入" aria-label="打造导入" @click="showImport">
|
||||
<img data-icon class="character-toolbar-icon" :src="importIcon" alt="" />
|
||||
</UiButton>
|
||||
<UiButton size="icon" title="技能导入" aria-label="技能导入" @click="showImportSkill">
|
||||
<img data-icon class="character-toolbar-icon" :src="exchangeIcon" alt="" />
|
||||
</UiButton>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<ImportDialog v-model:open="importVisible" title="打造导入">
|
||||
@@ -110,6 +110,11 @@ const showImportSkill = () => {
|
||||
importSkillVisible.value = true
|
||||
}
|
||||
|
||||
const recalculate = () => {
|
||||
if (isExecuting.value) return
|
||||
void configStore.result.execute()
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -150,13 +155,12 @@ onUnmounted(() => {
|
||||
--ui-control-height-compact: clamp(24px, 1.25vw, 26px);
|
||||
--ui-control-height-default: clamp(28px, 1.5vw, 32px);
|
||||
--ui-control-height-large: clamp(42px, 2.3vw, 48px);
|
||||
--ui-tabs-height: clamp(40px, 2.05vw, 44px);
|
||||
--ui-tab-height: clamp(30px, 1.5vw, 32px);
|
||||
--ui-tabs-height: 32px;
|
||||
--ui-tab-height: 24px;
|
||||
--ui-control-gap: clamp(5px, 0.4vw, 8px);
|
||||
--ui-item-slot-size: clamp(30px, 1.6vw, 34px);
|
||||
--ui-item-image-size: clamp(28px, 1.45vw, 30px);
|
||||
--ui-character-config-inline-size: clamp(340px, 21vw, 480px);
|
||||
--ui-character-footer-min-block-size: clamp(38px, 2vw, 42px);
|
||||
--ui-character-config-inline-size: clamp(360px, 28vw, 520px);
|
||||
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
@@ -181,8 +185,7 @@ onUnmounted(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.character-shell__header,
|
||||
.character-shell__footer {
|
||||
.character-shell__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
flex: 0 0 auto;
|
||||
@@ -195,12 +198,24 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
min-height: var(--ui-tabs-height);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ui-control-gap);
|
||||
padding-inline-end: var(--ui-character-workspace-padding);
|
||||
border-bottom-width: var(--ui-component-stroke-size);
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.character-shell__navigation {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
}
|
||||
|
||||
.character-shell__tools {
|
||||
display: flex;
|
||||
min-width: max-content;
|
||||
align-items: center;
|
||||
gap: var(--ui-control-gap);
|
||||
}
|
||||
|
||||
.character-shell__workspace {
|
||||
@@ -212,8 +227,7 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
grid-template-columns:
|
||||
minmax(0, var(--ui-character-config-inline-size))
|
||||
minmax(0, 1fr)
|
||||
max-content;
|
||||
minmax(0, 1fr);
|
||||
gap: var(--ui-character-workspace-gap);
|
||||
padding: var(--ui-character-workspace-padding);
|
||||
overflow: hidden;
|
||||
@@ -234,42 +248,15 @@ onUnmounted(() => {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.character-shell__stage {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.character-shell__stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 7% -12% 4%;
|
||||
background: var(--ui-character-stage-glow);
|
||||
filter: blur(var(--ui-character-stage-glow-blur));
|
||||
opacity: var(--ui-character-stage-glow-opacity);
|
||||
}
|
||||
|
||||
.character-shell__result {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.character-shell__footer {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
min-height: var(--ui-character-footer-min-block-size);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ui-control-gap);
|
||||
padding: var(--ui-character-footer-padding);
|
||||
border-top-width: var(--ui-component-stroke-size);
|
||||
border-top-style: solid;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.character-shell--skill,
|
||||
.character-shell--forge {
|
||||
--ui-character-config-inline-size: clamp(400px, 36vw, 760px);
|
||||
--ui-character-config-inline-size: clamp(700px, 54vw, 840px);
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
@@ -295,14 +282,58 @@ onUnmounted(() => {
|
||||
|
||||
@media (max-width: 1179px) {
|
||||
.character-shell {
|
||||
--ui-character-config-inline-size: clamp(332px, 34vw, 360px);
|
||||
--ui-character-config-inline-size: clamp(332px, 34vw, 380px);
|
||||
--ui-character-workspace-gap: 4px;
|
||||
--ui-character-workspace-padding: 4px;
|
||||
}
|
||||
|
||||
.character-shell--skill,
|
||||
.character-shell--forge {
|
||||
--ui-character-config-inline-size: clamp(380px, 40vw, 420px);
|
||||
--ui-character-config-inline-size: clamp(620px, 58vw, 700px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.character-shell {
|
||||
--ui-character-config-inline-size: min(100%, 520px);
|
||||
}
|
||||
|
||||
.character-shell--skill,
|
||||
.character-shell--forge {
|
||||
--ui-character-config-inline-size: min(100%, 760px);
|
||||
}
|
||||
|
||||
.character-shell__header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.character-shell__tools {
|
||||
min-height: var(--ui-control-height-large);
|
||||
justify-content: flex-end;
|
||||
padding-inline: var(--ui-character-workspace-padding);
|
||||
border-top: var(--ui-component-stroke-size) solid var(--ui-character-chrome-border);
|
||||
}
|
||||
|
||||
.character-shell__workspace {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(560px, calc(100dvh - 96px)) minmax(1124px, auto);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.character-shell__config,
|
||||
.character-shell__result {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.character-shell__config > * {
|
||||
max-width: min(100%, var(--ui-character-config-inline-size));
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
<section
|
||||
v-if="configStore.result.state"
|
||||
class="result-shell"
|
||||
:class="{ 'result-shell--buffer': configStore.result.state.buffer }"
|
||||
:class="{
|
||||
'result-shell--buffer': configStore.result.state.buffer,
|
||||
'result-shell--without-overview': !showOverview,
|
||||
}"
|
||||
>
|
||||
<div class="result-overview">
|
||||
<div v-if="showOverview" class="result-overview">
|
||||
<div class="result-overview__actions">
|
||||
<UiButton class="flex-1" @click="setStandard">设为基准</UiButton>
|
||||
<UiButton class="flex-1" variant="outline" @click="clearStandard">清空基准</UiButton>
|
||||
@@ -54,6 +57,13 @@ const configStore = useConfigStore()
|
||||
const infoStore = useInfoStore()
|
||||
const type = ref<'equ' | 'oath'>('equ')
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
showOverview?: boolean
|
||||
}>(),
|
||||
{ showOverview: true },
|
||||
)
|
||||
|
||||
const setStandard = () => {
|
||||
if (!configStore?.result?.state?.uuid) return
|
||||
infoStore.setStandard(configStore.result.state, configStore.skillCountConfig)
|
||||
@@ -68,20 +78,22 @@ const clearStandard = () => {
|
||||
.result-shell {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: calc(
|
||||
var(--ui-character-result-info-inline-size) + var(--ui-character-result-summary-inline-size) +
|
||||
var(--ui-character-result-toggle-inline-size)
|
||||
);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
grid-template-columns:
|
||||
var(--ui-character-result-info-inline-size)
|
||||
var(--ui-character-result-summary-inline-size);
|
||||
minmax(0, 1fr);
|
||||
gap: var(--ui-character-result-toggle-inline-size);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.result-shell--without-overview {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.result-overview {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
@@ -109,15 +121,18 @@ const clearStandard = () => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
@media (min-width: 1180px) {
|
||||
.result-shell {
|
||||
width: calc(
|
||||
var(--ui-character-result-info-inline-size) + var(--ui-character-result-full-inline-size) +
|
||||
var(--ui-character-workspace-gap)
|
||||
);
|
||||
grid-template-columns:
|
||||
var(--ui-character-result-info-inline-size)
|
||||
var(--ui-character-result-full-inline-size);
|
||||
gap: var(--ui-character-workspace-gap);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.result-shell {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: repeat(2, minmax(560px, 1fr));
|
||||
gap: var(--ui-character-workspace-gap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="damage-panel__total" :class="toneClass(totalTone)">
|
||||
<span>{{ totalStandard ? "相对基准" : "总伤害" }}</span>
|
||||
<span>{{ totalStandard ? '相对基准' : '总伤害' }}</span>
|
||||
<strong>{{ showTotalText }}</strong>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -126,166 +126,166 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Skill">
|
||||
import type { IResultSkill, IResultSkillCount } from "@/api/calc/type";
|
||||
import { UiNumberField } from "@/components/ui/number-field";
|
||||
import { useConfigStore } from "@/stores";
|
||||
import { getImageURL } from "@/utils/images";
|
||||
import type { IResultSkill, IResultSkillCount } from '@/api/calc/type'
|
||||
import { UiNumberField } from '@/components/ui/number-field'
|
||||
import { useConfigStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
|
||||
type SortKey = "learnLv" | "cd" | "dps" | "total";
|
||||
type Tone = "positive" | "negative" | undefined;
|
||||
type SortKey = 'learnLv' | 'cd' | 'dps' | 'total'
|
||||
type Tone = 'positive' | 'negative' | undefined
|
||||
|
||||
const props = defineProps<{
|
||||
data: IResultSkill[];
|
||||
data: IResultSkill[]
|
||||
standard?: {
|
||||
skills: IResultSkill[];
|
||||
skillCount: IResultSkillCount[];
|
||||
};
|
||||
}>();
|
||||
skills: IResultSkill[]
|
||||
skillCount: IResultSkillCount[]
|
||||
}
|
||||
}>()
|
||||
|
||||
const expanded = ref(false);
|
||||
const sort = ref<SortKey>("learnLv");
|
||||
const order = ref<"asc" | "desc">("desc");
|
||||
const config = useConfigStore();
|
||||
const expanded = ref(false)
|
||||
const sort = ref<SortKey>('learnLv')
|
||||
const order = ref<'asc' | 'desc'>('desc')
|
||||
const config = useConfigStore()
|
||||
|
||||
watchEffect(() => {
|
||||
props.data.forEach((skill) => {
|
||||
const exists = config.skillCountConfig.some(
|
||||
(item) => item.name === skill.name && item.mode === skill.mode,
|
||||
);
|
||||
if (!exists) config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count: 1 });
|
||||
});
|
||||
});
|
||||
)
|
||||
if (!exists) config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
function skillCountValue(skill: IResultSkill) {
|
||||
return (
|
||||
config.skillCountConfig.find((item) => item.name === skill.name && item.mode === skill.mode)
|
||||
?.count ?? 1
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function setSkillCount(skill: IResultSkill, value: number | undefined) {
|
||||
const count = typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
const count = typeof value === 'number' && Number.isFinite(value) ? value : 0
|
||||
const target = config.skillCountConfig.find(
|
||||
(item) => item.name === skill.name && item.mode === skill.mode,
|
||||
);
|
||||
if (target) target.count = count;
|
||||
else config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count });
|
||||
)
|
||||
if (target) target.count = count
|
||||
else config.skillCountConfig.push({ name: skill.name, mode: skill.mode, count })
|
||||
}
|
||||
|
||||
const totalDamage = computed(() =>
|
||||
props.data.reduce((sum, skill) => sum + skill.damage * skillCountValue(skill), 0),
|
||||
);
|
||||
)
|
||||
|
||||
const totalStandard = computed(() => {
|
||||
if (!props.standard) return undefined;
|
||||
if (!props.standard) return undefined
|
||||
return props.standard.skills.reduce((sum, skill) => {
|
||||
const count = props.standard?.skillCount.find(
|
||||
(item) => item.name === skill.name && item.mode === skill.mode,
|
||||
)?.count;
|
||||
return sum + skill.damage * (count ?? 0);
|
||||
}, 0);
|
||||
});
|
||||
)?.count
|
||||
return sum + skill.damage * (count ?? 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
function standardValues(skill: IResultSkill) {
|
||||
const standardSkill = props.standard?.skills.find(
|
||||
(item) => item.name === skill.name && item.mode === skill.mode,
|
||||
);
|
||||
)
|
||||
const standardCount = props.standard?.skillCount.find(
|
||||
(item) => item.name === skill.name && item.mode === skill.mode,
|
||||
)?.count;
|
||||
)?.count
|
||||
if (!standardSkill || standardSkill.damage <= 0 || standardSkill.cd <= 0 || !standardCount) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return { skill: standardSkill, count: standardCount };
|
||||
return { skill: standardSkill, count: standardCount }
|
||||
}
|
||||
|
||||
function comparisonTone(value: number): Tone {
|
||||
return value >= 0 ? "positive" : "negative";
|
||||
return value >= 0 ? 'positive' : 'negative'
|
||||
}
|
||||
|
||||
function formatPercent(value: number) {
|
||||
return `${(value * 100).toFixed(2)}%`;
|
||||
return `${(value * 100).toFixed(2)}%`
|
||||
}
|
||||
|
||||
function sortMetric(skill: IResultSkill, key: SortKey) {
|
||||
if (key === "total") return skill.damage * skillCountValue(skill);
|
||||
if (key === "dps") return skill.cd > 0 ? skill.damage / skill.cd : 0;
|
||||
return Number(skill[key] ?? 0);
|
||||
if (key === 'total') return skill.damage * skillCountValue(skill)
|
||||
if (key === 'dps') return skill.cd > 0 ? skill.damage / skill.cd : 0
|
||||
return Number(skill[key] ?? 0)
|
||||
}
|
||||
|
||||
const sortedSkills = computed(() =>
|
||||
[...props.data].sort((left, right) => {
|
||||
const leftValue = sortMetric(left, sort.value);
|
||||
const rightValue = sortMetric(right, sort.value);
|
||||
return order.value === "asc" ? leftValue - rightValue : rightValue - leftValue;
|
||||
const leftValue = sortMetric(left, sort.value)
|
||||
const rightValue = sortMetric(right, sort.value)
|
||||
return order.value === 'asc' ? leftValue - rightValue : rightValue - leftValue
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
const rows = computed(() =>
|
||||
sortedSkills.value.map((skill) => {
|
||||
const count = skillCountValue(skill);
|
||||
const currentTotal = skill.damage * count;
|
||||
const currentDps = skill.cd > 0 ? skill.damage / skill.cd : 0;
|
||||
const standard = standardValues(skill);
|
||||
let dps = Math.round(currentDps).toLocaleString();
|
||||
let total = Math.round(currentTotal).toLocaleString();
|
||||
let dpsTone: Tone;
|
||||
let totalTone: Tone;
|
||||
const count = skillCountValue(skill)
|
||||
const currentTotal = skill.damage * count
|
||||
const currentDps = skill.cd > 0 ? skill.damage / skill.cd : 0
|
||||
const standard = standardValues(skill)
|
||||
let dps = Math.round(currentDps).toLocaleString()
|
||||
let total = Math.round(currentTotal).toLocaleString()
|
||||
let dpsTone: Tone
|
||||
let totalTone: Tone
|
||||
|
||||
if (standard) {
|
||||
const standardDps = standard.skill.damage / standard.skill.cd;
|
||||
const standardTotal = standard.skill.damage * standard.count;
|
||||
const dpsDelta = standardDps === 0 ? 0 : (currentDps - standardDps) / standardDps;
|
||||
const totalDelta = standardTotal === 0 ? 0 : (currentTotal - standardTotal) / standardTotal;
|
||||
dps = formatPercent(dpsDelta);
|
||||
total = formatPercent(totalDelta);
|
||||
dpsTone = comparisonTone(dpsDelta);
|
||||
totalTone = comparisonTone(totalDelta);
|
||||
const standardDps = standard.skill.damage / standard.skill.cd
|
||||
const standardTotal = standard.skill.damage * standard.count
|
||||
const dpsDelta = standardDps === 0 ? 0 : (currentDps - standardDps) / standardDps
|
||||
const totalDelta = standardTotal === 0 ? 0 : (currentTotal - standardTotal) / standardTotal
|
||||
dps = formatPercent(dpsDelta)
|
||||
total = formatPercent(totalDelta)
|
||||
dpsTone = comparisonTone(dpsDelta)
|
||||
totalTone = comparisonTone(totalDelta)
|
||||
}
|
||||
|
||||
const share = totalDamage.value === 0 ? 0 : currentTotal / totalDamage.value;
|
||||
const share = totalDamage.value === 0 ? 0 : currentTotal / totalDamage.value
|
||||
return {
|
||||
key: `${skill.name}-${skill.mode ?? ""}`,
|
||||
key: `${skill.name}-${skill.mode ?? ''}`,
|
||||
skill,
|
||||
displayName: `${skill.name}${skill.mode ? ` [${skill.mode}]` : ""}`,
|
||||
displayName: `${skill.name}${skill.mode ? ` [${skill.mode}]` : ''}`,
|
||||
count,
|
||||
cd: typeof skill.cd === "number" ? `${skill.cd.toFixed(1)}s` : "-",
|
||||
cd: typeof skill.cd === 'number' ? `${skill.cd.toFixed(1)}s` : '-',
|
||||
dps,
|
||||
total,
|
||||
dpsTone,
|
||||
totalTone,
|
||||
share: formatPercent(share),
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
)
|
||||
|
||||
const showTotal = computed(() => {
|
||||
if (!totalStandard.value) return totalDamage.value;
|
||||
return ((totalDamage.value - totalStandard.value) / totalStandard.value) * 100;
|
||||
});
|
||||
if (!totalStandard.value) return totalDamage.value
|
||||
return ((totalDamage.value - totalStandard.value) / totalStandard.value) * 100
|
||||
})
|
||||
|
||||
const showTotalText = computed(() =>
|
||||
totalStandard.value
|
||||
? `${showTotal.value.toFixed(2)}%`
|
||||
: Math.round(showTotal.value).toLocaleString(),
|
||||
);
|
||||
)
|
||||
|
||||
const totalTone = computed<Tone>(() =>
|
||||
totalStandard.value ? comparisonTone(showTotal.value) : undefined,
|
||||
);
|
||||
)
|
||||
|
||||
function toneClass(tone: Tone) {
|
||||
return {
|
||||
"is-positive": tone === "positive",
|
||||
"is-negative": tone === "negative",
|
||||
};
|
||||
'is-positive': tone === 'positive',
|
||||
'is-negative': tone === 'negative',
|
||||
}
|
||||
}
|
||||
|
||||
function sortValue(key: SortKey) {
|
||||
if (sort.value === key) order.value = order.value === "asc" ? "desc" : "asc";
|
||||
if (sort.value === key) order.value = order.value === 'asc' ? 'desc' : 'asc'
|
||||
else {
|
||||
sort.value = key;
|
||||
order.value = "desc";
|
||||
sort.value = key
|
||||
order.value = 'desc'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -293,6 +293,7 @@ function sortValue(key: SortKey) {
|
||||
<style scoped lang="scss">
|
||||
.damage-panel {
|
||||
position: relative;
|
||||
container: damage-result / inline-size;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
@@ -572,6 +573,38 @@ button.damage-panel__heading {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.damage-panel__toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container damage-result (min-width: 620px) {
|
||||
.damage-panel__toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.damage-panel.is-expanded .damage-panel__surface {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.damage-panel__grid {
|
||||
grid-template-columns:
|
||||
minmax(180px, 1fr)
|
||||
var(--ui-character-result-cd-inline-size)
|
||||
var(--ui-character-result-count-inline-size)
|
||||
minmax(var(--ui-character-result-value-min-inline-size), 0.9fr)
|
||||
minmax(var(--ui-character-result-value-min-inline-size), 1fr)
|
||||
var(--ui-character-result-share-inline-size);
|
||||
}
|
||||
|
||||
.damage-panel__full-column {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.damage-panel__toggle {
|
||||
display: none;
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<aside ref="settingRef" class="dark-knight-setting" :class="{ 'is-collapsed': collapsed }">
|
||||
<UiButton
|
||||
v-if="collapsed"
|
||||
size="icon"
|
||||
class="dark-knight-setting__launcher"
|
||||
title="展开技能排列"
|
||||
aria-label="展开技能排列"
|
||||
@click="collapsed = false"
|
||||
>
|
||||
<img data-icon :src="shortcutIcon" alt="" />
|
||||
</UiButton>
|
||||
|
||||
<template v-else>
|
||||
<header class="dark-knight-setting__header">
|
||||
<span>技能排列</span>
|
||||
<UiButton size="icon" variant="ghost" aria-label="收起技能排列" @click="collapse">
|
||||
−
|
||||
</UiButton>
|
||||
</header>
|
||||
|
||||
<section class="dark-knight-setting__section">
|
||||
<h3>快捷栏</h3>
|
||||
<div class="shortcut-grid">
|
||||
<button
|
||||
v-for="(key, index) in shortcutKeys"
|
||||
:key="key"
|
||||
type="button"
|
||||
class="skill-slot"
|
||||
:class="{ 'is-selected': isEditing('shortcut', index) }"
|
||||
:title="getSkill(arrangement.shortcuts[index])?.name || `快捷键 ${key}`"
|
||||
@click="openPicker('shortcut', index)"
|
||||
>
|
||||
<img
|
||||
v-if="getSkill(arrangement.shortcuts[index])"
|
||||
:src="getImageURL(getSkill(arrangement.shortcuts[index])!.icon)"
|
||||
:alt="getSkill(arrangement.shortcuts[index])!.name"
|
||||
/>
|
||||
<span v-else>{{ key }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dark-knight-setting__section dark-knight-setting__combos">
|
||||
<h3>连续技</h3>
|
||||
<div class="combo-grid">
|
||||
<div v-for="(key, column) in comboKeys" :key="key" class="combo-column">
|
||||
<template v-for="stage in stageCount" :key="stage">
|
||||
<button
|
||||
type="button"
|
||||
class="skill-slot combo-slot"
|
||||
:class="{
|
||||
'is-selected': isEditing('combo', column, stage - 1),
|
||||
'is-disabled': isStageDisabled(column, stage - 1),
|
||||
}"
|
||||
:disabled="isStageDisabled(column, stage - 1)"
|
||||
:title="comboTitle(column, stage - 1, key)"
|
||||
@click="openPicker('combo', column, stage - 1)"
|
||||
>
|
||||
<img
|
||||
v-if="getSkill(arrangement.combos[column][stage - 1])"
|
||||
:src="getImageURL(getSkill(arrangement.combos[column][stage - 1])!.icon)"
|
||||
:alt="getSkill(arrangement.combos[column][stage - 1])!.name"
|
||||
/>
|
||||
<span v-else>{{ stage }}</span>
|
||||
</button>
|
||||
<span
|
||||
v-if="stage < stageCount"
|
||||
class="stage-arrow"
|
||||
:class="{ 'is-connected': isStageConnected(column, stage - 1) }"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</template>
|
||||
<UiButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
:aria-label="`重置 ${key} 连续技`"
|
||||
@click="resetCombo(column)"
|
||||
>
|
||||
↻
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="editingSlot" class="skill-picker" aria-label="选择主动技能">
|
||||
<header>
|
||||
<span>选择主动技能</span>
|
||||
<div>
|
||||
<UiButton size="sm" variant="outline" :disabled="!currentValue" @click="selectSkill('')"
|
||||
>清空</UiButton
|
||||
>
|
||||
<UiButton
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="关闭技能选择"
|
||||
@click="editingSlot = null"
|
||||
>×</UiButton
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
<div class="skill-picker__grid">
|
||||
<button
|
||||
v-for="skill in selectableSkills"
|
||||
:key="skill.id"
|
||||
type="button"
|
||||
class="skill-picker__item"
|
||||
:class="{ 'is-active': currentValue === skill.id.toString() }"
|
||||
:disabled="isSkillUsed(skill.id.toString())"
|
||||
:title="skill.name"
|
||||
@click="selectSkill(skill.id.toString())"
|
||||
>
|
||||
<img :src="getImageURL(skill.icon)" :alt="skill.name" />
|
||||
<span>{{ skill.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ISkill } from '@/api/info/type'
|
||||
import { UiButton } from '@/components/ui/button'
|
||||
import type { ISkillShortcuts } from '@/stores/config'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import { onClickOutside, useEventListener } from '@vueuse/core'
|
||||
import shortcutIcon from '../img/shortcut.png'
|
||||
|
||||
type EditingSlot =
|
||||
| { type: 'shortcut'; index: number }
|
||||
| { type: 'combo'; column: number; stage: number }
|
||||
|
||||
const shortcutKeys = ['Q', 'W', 'E', 'R', 'T', 'Y']
|
||||
const comboKeys = ['A', 'S', 'D', 'F', 'G', 'H']
|
||||
const stageCount = 5
|
||||
const excludedSkillNames = new Set(['后跳', '受身蹲伏', '跃翔', '黑暗释放'])
|
||||
const excludedAwakeLevels = new Set([85, 100])
|
||||
|
||||
const props = defineProps<{
|
||||
skills: ISkill[]
|
||||
modelValue?: ISkillShortcuts
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ISkillShortcuts]
|
||||
}>()
|
||||
|
||||
const settingRef = ref<HTMLElement>()
|
||||
const editingSlot = ref<EditingSlot | null>(null)
|
||||
const collapsed = ref(false)
|
||||
|
||||
const arrangement = computed<ISkillShortcuts>(() => ({
|
||||
shortcuts: Array.from(
|
||||
{ length: shortcutKeys.length },
|
||||
(_, index) => props.modelValue?.shortcuts?.[index] ?? '',
|
||||
),
|
||||
combos: Array.from({ length: comboKeys.length }, (_, column) =>
|
||||
Array.from(
|
||||
{ length: stageCount },
|
||||
(_, stage) => props.modelValue?.combos?.[column]?.[stage] ?? '',
|
||||
),
|
||||
),
|
||||
}))
|
||||
|
||||
const skillMap = computed(() => new Map(props.skills.map((skill) => [skill.id.toString(), skill])))
|
||||
const selectableSkills = computed(() =>
|
||||
[...props.skills]
|
||||
.filter(
|
||||
(skill) =>
|
||||
skill.type === 'active' &&
|
||||
!excludedSkillNames.has(skill.name) &&
|
||||
!excludedAwakeLevels.has(skill.learnLv),
|
||||
)
|
||||
.sort((left, right) => {
|
||||
const [leftColumn = -1, leftRow = -1] = left.cellPosition ?? []
|
||||
const [rightColumn = -1, rightRow = -1] = right.cellPosition ?? []
|
||||
return leftRow - rightRow || leftColumn - rightColumn || left.learnLv - right.learnLv
|
||||
}),
|
||||
)
|
||||
const selectedSkillIds = computed(
|
||||
() =>
|
||||
new Set([...arrangement.value.shortcuts, ...arrangement.value.combos.flat()].filter(Boolean)),
|
||||
)
|
||||
const currentValue = computed(() => {
|
||||
const slot = editingSlot.value
|
||||
if (!slot) return ''
|
||||
return slot.type === 'shortcut'
|
||||
? arrangement.value.shortcuts[slot.index]
|
||||
: arrangement.value.combos[slot.column][slot.stage]
|
||||
})
|
||||
|
||||
const getSkill = (skillId: string) => skillMap.value.get(skillId)
|
||||
const isEditing = (type: EditingSlot['type'], index: number, stage?: number) => {
|
||||
const slot = editingSlot.value
|
||||
if (!slot) return false
|
||||
return type === 'shortcut'
|
||||
? slot.type === 'shortcut' && slot.index === index
|
||||
: slot.type === 'combo' && slot.column === index && slot.stage === stage
|
||||
}
|
||||
const isStageDisabled = (column: number, stage: number) =>
|
||||
stage > 0 && !arrangement.value.combos[column][stage - 1]
|
||||
const isStageConnected = (column: number, stage: number) =>
|
||||
Boolean(arrangement.value.combos[column][stage] && arrangement.value.combos[column][stage + 1])
|
||||
const comboTitle = (column: number, stage: number, key: string) =>
|
||||
isStageDisabled(column, stage)
|
||||
? `请先设置 ${stage} 阶段`
|
||||
: getSkill(arrangement.value.combos[column][stage])?.name || `${stage + 1}阶段 ${key}`
|
||||
|
||||
const openPicker = (type: EditingSlot['type'], index: number, stage = 0) => {
|
||||
if (type === 'combo' && isStageDisabled(index, stage)) return
|
||||
editingSlot.value = type === 'shortcut' ? { type, index } : { type, column: index, stage }
|
||||
}
|
||||
const isSkillUsed = (skillId: string) =>
|
||||
selectedSkillIds.value.has(skillId) && currentValue.value !== skillId
|
||||
|
||||
const selectSkill = (skillId: string) => {
|
||||
const slot = editingSlot.value
|
||||
if (!slot || isSkillUsed(skillId)) return
|
||||
const next: ISkillShortcuts = {
|
||||
shortcuts: [...arrangement.value.shortcuts],
|
||||
combos: arrangement.value.combos.map((column) => [...column]),
|
||||
}
|
||||
if (slot.type === 'shortcut') next.shortcuts[slot.index] = skillId
|
||||
else {
|
||||
next.combos[slot.column][slot.stage] = skillId
|
||||
if (!skillId) next.combos[slot.column].fill('', slot.stage)
|
||||
}
|
||||
emit('update:modelValue', next)
|
||||
if (slot.type === 'combo' && skillId) {
|
||||
editingSlot.value = slot.stage < stageCount - 1 ? { ...slot, stage: slot.stage + 1 } : null
|
||||
}
|
||||
}
|
||||
|
||||
const resetCombo = (column: number) => {
|
||||
emit('update:modelValue', {
|
||||
shortcuts: [...arrangement.value.shortcuts],
|
||||
combos: arrangement.value.combos.map((items, index) =>
|
||||
index === column ? Array.from({ length: stageCount }, () => '') : [...items],
|
||||
),
|
||||
})
|
||||
if (editingSlot.value?.type === 'combo' && editingSlot.value.column === column) {
|
||||
editingSlot.value = { type: 'combo', column, stage: 0 }
|
||||
}
|
||||
}
|
||||
const collapse = () => {
|
||||
editingSlot.value = null
|
||||
collapsed.value = true
|
||||
}
|
||||
|
||||
onClickOutside(settingRef, () => (editingSlot.value = null))
|
||||
useEventListener(window, 'blur', () => (editingSlot.value = null))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dark-knight-setting {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--ui-disclosure-border);
|
||||
background: var(--ui-disclosure-background);
|
||||
color: var(--ui-disclosure-state);
|
||||
|
||||
&.is-collapsed {
|
||||
width: var(--ui-control-height-large);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-knight-setting__launcher img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.dark-knight-setting__header,
|
||||
.skill-picker header {
|
||||
display: flex;
|
||||
min-height: var(--ui-disclosure-header-min-block-size);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-inline: var(--ui-field-padding-inline);
|
||||
border-bottom: 1px solid var(--ui-disclosure-divider);
|
||||
background: var(--ui-disclosure-header-background);
|
||||
}
|
||||
.dark-knight-setting__header .ui-button {
|
||||
--ui-control-height-default: 20px;
|
||||
}
|
||||
.dark-knight-setting__section {
|
||||
padding: var(--ui-control-gap);
|
||||
}
|
||||
.dark-knight-setting__section h3 {
|
||||
margin: 0 0 var(--ui-control-gap);
|
||||
color: var(--ui-settings-section-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.shortcut-grid,
|
||||
.combo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 28px);
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.combo-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.skill-slot {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 1px solid var(--ui-border);
|
||||
outline: 0;
|
||||
background: var(--ui-item-slot-background);
|
||||
color: var(--ui-muted-foreground);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 0 0 1px #000;
|
||||
|
||||
img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
object-fit: contain;
|
||||
}
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&.is-selected {
|
||||
border-color: var(--ui-ring);
|
||||
}
|
||||
&.is-disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.stage-arrow {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 11px;
|
||||
}
|
||||
.stage-arrow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 7px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--ui-muted);
|
||||
}
|
||||
.stage-arrow::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 3px;
|
||||
border-top: 6px solid var(--ui-muted);
|
||||
border-right: 7px solid transparent;
|
||||
border-left: 7px solid transparent;
|
||||
}
|
||||
.stage-arrow.is-connected::before {
|
||||
background: var(--ui-primary);
|
||||
}
|
||||
.stage-arrow.is-connected::after {
|
||||
border-top-color: var(--ui-primary);
|
||||
}
|
||||
.combo-column > .ui-button {
|
||||
--ui-control-height-compact: 18px;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
margin-top: 4px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
.skill-picker {
|
||||
position: absolute;
|
||||
z-index: var(--ui-popup-z-index);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
width: min(232px, 100%);
|
||||
border: 1px solid var(--ui-popover-border);
|
||||
background: var(--ui-popover-background);
|
||||
box-shadow: 0 5px 18px var(--ui-popover-shadow);
|
||||
}
|
||||
.skill-picker header > div {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.skill-picker__grid {
|
||||
display: grid;
|
||||
max-height: 390px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
padding: 5px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.skill-picker__item {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
padding: 3px 2px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border: 1px solid var(--ui-option-divider);
|
||||
outline: 0;
|
||||
background: var(--ui-popover-background);
|
||||
color: var(--ui-popover-foreground);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
span {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible,
|
||||
&.is-active {
|
||||
border-color: var(--ui-ring);
|
||||
color: var(--ui-option-highlight-foreground);
|
||||
}
|
||||
&:disabled {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asyncComputed } from '@vueuse/core'
|
||||
import api from '@/api'
|
||||
import './style.scss'
|
||||
import { UiDataSection } from '@/components/ui/data-section'
|
||||
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
export default defineComponent({
|
||||
@@ -86,32 +87,48 @@ export default defineComponent({
|
||||
|
||||
const renderSkillDetail = () => (
|
||||
<>
|
||||
<div class="skill_title">基本信息</div>
|
||||
<div class="skill-detail__stack">
|
||||
{basic.value.map(
|
||||
(item) =>
|
||||
!!item.value && (
|
||||
<div class="item skill-detail__row">
|
||||
<span class="label">{item.label}</span>
|
||||
<span class="value">{item.value}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div class="skill_title">技能描述</div>
|
||||
<div class="skill-detail__description-frame">
|
||||
<div class="skill-detail__scroll-content">
|
||||
{desc.value?.map((item) => (
|
||||
// item.startsWith('[') || item.startsWith('-') || item.startsWith('<') ? (
|
||||
// <div class="min-h-10px">{item}</div>
|
||||
// ) :
|
||||
<div class="skill-detail__description-line">{item}</div>
|
||||
))}
|
||||
<UiDataSection
|
||||
class="skill-detail__section"
|
||||
title="基本信息"
|
||||
compact
|
||||
showState={false}
|
||||
indicatorPosition="end"
|
||||
>
|
||||
<div class="skill-detail__stack">
|
||||
{basic.value.map(
|
||||
(item) =>
|
||||
!!item.value && (
|
||||
<div class="item skill-detail__row">
|
||||
<span class="label">{item.label}</span>
|
||||
<span class="value">{item.value}</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</UiDataSection>
|
||||
<UiDataSection
|
||||
class="skill-detail__section"
|
||||
title="技能描述"
|
||||
compact
|
||||
showState={false}
|
||||
indicatorPosition="end"
|
||||
>
|
||||
<div class="skill-detail__description-frame">
|
||||
<div class="skill-detail__scroll-content">
|
||||
{desc.value?.map((item) => (
|
||||
<div class="skill-detail__description-line">{item}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</UiDataSection>
|
||||
{detail.value.length > 0 && (
|
||||
<>
|
||||
<div class="skill_title">技能属性</div>
|
||||
<UiDataSection
|
||||
class="skill-detail__section skill-detail__section--grow"
|
||||
title="技能属性"
|
||||
compact
|
||||
showState={false}
|
||||
indicatorPosition="end"
|
||||
>
|
||||
<div class="skill-detail__flex-scroll">
|
||||
<div class="skill-detail__stack">
|
||||
{detail.value.map((item) => (
|
||||
@@ -119,7 +136,7 @@ export default defineComponent({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</UiDataSection>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -132,10 +149,17 @@ export default defineComponent({
|
||||
<>
|
||||
<div class="skill-detail__stack">
|
||||
<div class="skill_vp_title">{item.name}</div>
|
||||
<div class="skill_title">技能描述</div>
|
||||
<div class="skill-detail__evolution-scroll">
|
||||
<div class="skill-detail__evolution-copy">{item.descDetail}</div>
|
||||
</div>
|
||||
<UiDataSection
|
||||
class="skill-detail__section"
|
||||
title="技能描述"
|
||||
compact
|
||||
showState={false}
|
||||
indicatorPosition="end"
|
||||
>
|
||||
<div class="skill-detail__evolution-scroll">
|
||||
<div class="skill-detail__evolution-copy">{item.descDetail}</div>
|
||||
</div>
|
||||
</UiDataSection>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -46,6 +46,29 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.skill-detail__section {
|
||||
flex: 0 0 auto;
|
||||
border-color: #3e4040;
|
||||
}
|
||||
|
||||
.skill-detail__section--grow {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skill-detail__section .ui-data-section__content {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.skill-detail__section--grow .ui-data-section__content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skill-detail__evolution {
|
||||
height: auto;
|
||||
}
|
||||
@@ -136,16 +159,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,13 +1,17 @@
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import type { ISkill } from '@/api/info/type'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { UiButton } from '@/components/ui/button'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import addIcon from './img/add.svg'
|
||||
import maxIcon from './img/max.svg'
|
||||
import minIcon from './img/min.svg'
|
||||
import subIcon from './img/sub.svg'
|
||||
import './style.scss'
|
||||
|
||||
const rowPrefixes = ['通用', 1, '', 10, '', 20, '', 30, '', 40, 50, 60, 70, 80, 90, 100]
|
||||
const defaultColumnCount = 11
|
||||
const altersWithoutRowPrefix = new Set(['creator', 'dark_knight'])
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SkillTree',
|
||||
props: {
|
||||
@@ -21,269 +25,259 @@ export default defineComponent({
|
||||
},
|
||||
bindAwake: {
|
||||
type: Number,
|
||||
default: () => 50,
|
||||
default: 50,
|
||||
},
|
||||
alter: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['chooseSkill'],
|
||||
emits: ['chooseSkill', 'update:lvInfo', 'update:bindAwake'],
|
||||
setup(props, { emit }) {
|
||||
const skills = computed(() => props.skills)
|
||||
const lvInfo = useVModel(props, 'lvInfo')
|
||||
const bindAwake = useVModel(props, 'bindAwake')
|
||||
// const emit = defineEmits(['chooseSkill'])
|
||||
const activeSkill = ref(-1)
|
||||
|
||||
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 skillGrid = computed(() => {
|
||||
const grid = new Map<string, ISkill>()
|
||||
const occupiedRows = new Set<number>()
|
||||
let maxColumn = defaultColumnCount - 1
|
||||
|
||||
props.skills.forEach((skill) => {
|
||||
const [column, row] = skill.cellPosition ?? []
|
||||
if (!Number.isInteger(column) || !Number.isInteger(row) || column < 0 || row < 0) return
|
||||
grid.set(`${row}:${column}`, skill)
|
||||
occupiedRows.add(row)
|
||||
maxColumn = Math.max(maxColumn, column)
|
||||
})
|
||||
|
||||
return {
|
||||
grid,
|
||||
columns: Array.from({ length: maxColumn + 1 }, (_, index) => index),
|
||||
rows: Array.from(occupiedRows).sort((left, right) => left - right),
|
||||
}
|
||||
})
|
||||
|
||||
const showRowPrefix = computed(() => !altersWithoutRowPrefix.has(props.alter))
|
||||
const isExtraAlter = computed(() => altersWithoutRowPrefix.has(props.alter))
|
||||
|
||||
const getRowPrefix = (row: number) => {
|
||||
const lastRow = skillGrid.value.rows.at(-1) ?? -1
|
||||
if (lastRow >= rowPrefixes.length && row >= rowPrefixes.length - 1) {
|
||||
return row === lastRow ? rowPrefixes.at(-1) : ''
|
||||
}
|
||||
return rowPrefixes[row]
|
||||
}
|
||||
|
||||
const lvList = [15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100]
|
||||
const getLastPositionedSkill = (learnLv: number) =>
|
||||
props.skills
|
||||
.filter((skill) => skill.learnLv === learnLv && skill.type === 'active' && skill.bind)
|
||||
.sort((left, right) => {
|
||||
const [leftColumn = -1, leftRow = -1] = left.cellPosition ?? []
|
||||
const [rightColumn = -1, rightRow = -1] = right.cellPosition ?? []
|
||||
return leftRow - rightRow || leftColumn - rightColumn
|
||||
})
|
||||
.at(-1)
|
||||
|
||||
const columns = Array.from({ length: 11 }, (_, index) => index)
|
||||
const awakeSkill1 = computed(() =>
|
||||
props.skills.find((skill) => skill.learnLv === 50 && skill.type === 'active' && skill.bind),
|
||||
)
|
||||
const awakeSkill2 = computed(() =>
|
||||
props.skills.find((skill) => skill.learnLv === 85 && skill.type === 'active' && skill.bind),
|
||||
)
|
||||
const extraAwakeSkill2 = computed(() =>
|
||||
props.alter === 'creator'
|
||||
? props.skills.find((skill) => skill.learnLv === 85 && skill.name === '生命之树')
|
||||
: getLastPositionedSkill(85),
|
||||
)
|
||||
const extraAwakeSkill3 = computed(() => getLastPositionedSkill(100))
|
||||
|
||||
const activeSkill = ref<number>(-1)
|
||||
watchEffect(() => {
|
||||
if (isExtraAlter.value && bindAwake.value !== 85) bindAwake.value = 85
|
||||
})
|
||||
|
||||
const chooseSkill = (skill?: ISkill, operation: string = 'add') => {
|
||||
const actionSkillLevel = (skill: ISkill, operation: string) => {
|
||||
const key = skill.id.toString()
|
||||
const current = lvInfo.value[key] ?? { lv: 0, vp: 0, up: 0 }
|
||||
if (!lvInfo.value[key]) lvInfo.value[key] = current
|
||||
|
||||
if (operation === 'add') current.lv = Math.min(current.lv + 1, skill.maxLv)
|
||||
if (operation === 'addMax') current.lv = skill.maxLearnLv
|
||||
if (operation === 'sub') current.lv = Math.max(current.lv - 1, 0)
|
||||
if (operation === 'subMax') lvInfo.value[key] = { lv: 0, vp: 0, up: 0 }
|
||||
}
|
||||
|
||||
const chooseSkill = (skill?: ISkill, operation = 'add') => {
|
||||
if (!skill) {
|
||||
activeSkill.value = -1
|
||||
return
|
||||
}
|
||||
if (activeSkill.value == skill.id) actionSkillLv(skill, operation)
|
||||
if (activeSkill.value === skill.id) actionSkillLevel(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 hasAwakeBinding = (skill: ISkill) =>
|
||||
isExtraAlter.value
|
||||
? skill.id === extraAwakeSkill3.value?.id
|
||||
: skill.learnLv === 100 && skill.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
|
||||
const renderSkillAction = (skill: ISkill) => {
|
||||
const level = lvInfo.value[skill.id.toString()]?.lv ?? 0
|
||||
const action = (event: Event, operation: string) => {
|
||||
event.stopPropagation()
|
||||
actionSkillLevel(skill, operation)
|
||||
}
|
||||
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="skill-tree__slot">
|
||||
<div class="skill-tree__enhancements">
|
||||
{info?.vp > 0 && <div class={`vp_${info.vp}`}></div>}
|
||||
{info?.up > 0 && <div class={`up_${info.up}`}></div>}
|
||||
</div>
|
||||
{activeSkill.value == skill.id && renderSkillAction(skill)}
|
||||
<div
|
||||
class={['skill-tree__skill', skill.learnLv == 100 && skill.bind ? '' : 'skill']}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
chooseSkill(skill)
|
||||
}}
|
||||
onContextmenu={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
chooseSkill(skill, 'sub')
|
||||
}}
|
||||
key={index}
|
||||
<div class="skill-active skill-tree__actions">
|
||||
<div class="skill-tree__action-list">
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级归零"
|
||||
disabled={level === 0}
|
||||
onClick={(event: Event) => action(event, 'subMax')}
|
||||
>
|
||||
<img
|
||||
class={['skill-tree__icon', skill.type === 'passive' ? 'passive' : '']}
|
||||
src={getImageURL(skill.icon)}
|
||||
alt={skill.name}
|
||||
></img>
|
||||
<div class="skill-tree__level" style={{ color }}>
|
||||
{info?.lv ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
{skill.learnLv == 100 && skill.bind && (
|
||||
<>
|
||||
<div class={`skill-tree__awake awake_${bindAwake.value}`}>
|
||||
<div class="skill-tree__awake-option" onClick={() => chooseBindAwake(50)}>
|
||||
<img
|
||||
class="skill-tree__icon skill-tree__awake-icon"
|
||||
src={getImageURL(awakeSkill_1.value?.icon ?? '')}
|
||||
></img>
|
||||
<div class="skill-tree__awake-label">1次</div>
|
||||
</div>
|
||||
<div class="skill-tree__awake-option" onClick={() => chooseBindAwake(85)}>
|
||||
<img
|
||||
class="skill-tree__icon skill-tree__awake-icon"
|
||||
src={getImageURL(awakeSkill_2.value?.icon ?? '')}
|
||||
></img>
|
||||
<div class="skill-tree__awake-label">2次</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<img data-icon src={minIcon} alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级减一"
|
||||
disabled={level === 0}
|
||||
onClick={(event: Event) => action(event, 'sub')}
|
||||
>
|
||||
<img data-icon src={subIcon} alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级加一"
|
||||
onClick={(event: Event) => action(event, 'add')}
|
||||
>
|
||||
<img data-icon src={addIcon} alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级加满"
|
||||
disabled={level >= skill.maxLearnLv}
|
||||
onClick={(event: Event) => action(event, 'addMax')}
|
||||
>
|
||||
<img data-icon src={maxIcon} alt="" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSkillAction = (skill: ISkill) => (
|
||||
<>
|
||||
<>
|
||||
<div class="skill-active skill-tree__actions">
|
||||
<div class="skill-tree__action-list">
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级归零"
|
||||
disabled={lvInfo.value[skill.id.toString()]?.lv == 0}
|
||||
onClick={(e: Event) => {
|
||||
e.stopPropagation()
|
||||
actionSkillLv(skill, 'subMax')
|
||||
}}
|
||||
>
|
||||
<img data-icon src={minIcon} alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级减一"
|
||||
disabled={lvInfo.value[skill.id.toString()]?.lv == 0}
|
||||
onClick={(e: Event) => {
|
||||
e.stopPropagation()
|
||||
actionSkillLv(skill, 'sub')
|
||||
}}
|
||||
>
|
||||
<img data-icon src={subIcon} alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级加一"
|
||||
onClick={(e: Event) => {
|
||||
e.stopPropagation()
|
||||
actionSkillLv(skill, 'add')
|
||||
}}
|
||||
>
|
||||
<img data-icon src={addIcon} alt="" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
size="icon"
|
||||
class="skill-action-button"
|
||||
aria-label="技能等级加满"
|
||||
disabled={lvInfo.value[skill.id.toString()]?.lv >= skill.maxLearnLv}
|
||||
onClick={(e: Event) => {
|
||||
e.stopPropagation()
|
||||
actionSkillLv(skill, 'addMax')
|
||||
}}
|
||||
>
|
||||
<img data-icon src={maxIcon} alt="" />
|
||||
</UiButton>
|
||||
{/* <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>
|
||||
</>
|
||||
</>
|
||||
const renderNormalAwakeBinding = () => (
|
||||
<div class={`skill-tree__awake awake_${bindAwake.value}`}>
|
||||
<button
|
||||
type="button"
|
||||
class="skill-tree__awake-option"
|
||||
aria-pressed={bindAwake.value === 50}
|
||||
onClick={() => (bindAwake.value = 50)}
|
||||
>
|
||||
<img
|
||||
class="skill-tree__icon skill-tree__awake-icon"
|
||||
src={getImageURL(awakeSkill1.value?.icon ?? '')}
|
||||
alt={awakeSkill1.value?.name ?? '一觉'}
|
||||
/>
|
||||
<span class="skill-tree__awake-label">1次</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="skill-tree__awake-option"
|
||||
aria-pressed={bindAwake.value === 85}
|
||||
onClick={() => (bindAwake.value = 85)}
|
||||
>
|
||||
<img
|
||||
class="skill-tree__icon skill-tree__awake-icon"
|
||||
src={getImageURL(awakeSkill2.value?.icon ?? '')}
|
||||
alt={awakeSkill2.value?.name ?? '二觉'}
|
||||
/>
|
||||
<span class="skill-tree__awake-label">2次</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<div class="skill-tree" onClick={() => (activeSkill.value = -1)}>
|
||||
{((getSkill(0, 15, -1) ?? []) as ISkill[]).length > 0 && (
|
||||
<>
|
||||
<div class="skill-tree-line">
|
||||
<div class="skill-tree__row-label">
|
||||
其<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" key={index}>
|
||||
{lv == 1 || lv % 10 == 0 ? (
|
||||
<div class="skill-tree__row-label">{lv}</div>
|
||||
) : (
|
||||
<div class="skill-tree__placeholder"></div>
|
||||
)}
|
||||
{columns.map((column) => {
|
||||
const skill = getSkill(lv, lvList[index + 1] ?? 150, column) as ISkill
|
||||
return skill ? (
|
||||
renderSkill(skill, index)
|
||||
) : (
|
||||
<div class="skill-tree__placeholder"></div>
|
||||
)
|
||||
})}
|
||||
<div class="skill-tree__row-end" onClick={() => (activeSkill.value = -1)}></div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
)}
|
||||
const renderExtraAwakeBinding = () => (
|
||||
<div class="skill-tree__extra-awake">
|
||||
<div class="skill-tree__extra-awake-option">
|
||||
<img
|
||||
class="skill-tree__icon"
|
||||
src={getImageURL(extraAwakeSkill2.value?.icon ?? '')}
|
||||
alt={extraAwakeSkill2.value?.name ?? '二觉'}
|
||||
/>
|
||||
<span>2次</span>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderSkill = (skill: ISkill) => {
|
||||
const info = lvInfo.value[skill.id.toString()]
|
||||
const color =
|
||||
(info?.lv ?? 0) < skill.maxLearnLv
|
||||
? '#32e432'
|
||||
: (info?.lv ?? 0) > skill.maxLearnLv
|
||||
? '#58e0fc'
|
||||
: 'white'
|
||||
|
||||
return (
|
||||
<div class="skill-tree__slot" key={skill.id}>
|
||||
<div class="skill-tree__enhancements">
|
||||
{(info?.vp ?? 0) > 0 && <span class={`vp_${info.vp}`}></span>}
|
||||
{(info?.up ?? 0) > 0 && <span class={`up_${info.up}`}></span>}
|
||||
</div>
|
||||
{activeSkill.value === skill.id && renderSkillAction(skill)}
|
||||
<button
|
||||
type="button"
|
||||
class={['skill-tree__skill', hasAwakeBinding(skill) ? '' : 'skill']}
|
||||
aria-label={`${skill.name},当前等级 ${info?.lv ?? 0}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
chooseSkill(skill)
|
||||
}}
|
||||
onContextmenu={(event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
chooseSkill(skill, 'sub')
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class={['skill-tree__icon', skill.type === 'passive' ? 'passive' : '']}
|
||||
src={getImageURL(skill.icon)}
|
||||
alt=""
|
||||
/>
|
||||
<span class="skill-tree__level" style={{ color }}>
|
||||
{info?.lv ?? 0}
|
||||
</span>
|
||||
</button>
|
||||
{hasAwakeBinding(skill) &&
|
||||
(isExtraAlter.value ? renderExtraAwakeBinding() : renderNormalAwakeBinding())}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class="skill-tree" onClick={() => (activeSkill.value = -1)}>
|
||||
{skillGrid.value.rows.map((row) => (
|
||||
<div class="skill-tree-line" key={row}>
|
||||
<div class="skill-tree__row-label">{showRowPrefix.value ? getRowPrefix(row) : ''}</div>
|
||||
{skillGrid.value.columns.map((column) => {
|
||||
const skill = skillGrid.value.grid.get(`${row}:${column}`)
|
||||
return skill ? (
|
||||
renderSkill(skill)
|
||||
) : (
|
||||
<div class="skill-tree__placeholder" key={column} />
|
||||
)
|
||||
})}
|
||||
<div class="skill-tree__row-end" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.skill-tree {
|
||||
/* Preserve the original 11-column tree rhythm while allowing its host to scroll on narrow screens. */
|
||||
min-width: 550px;
|
||||
/* Fill the standard tree viewport; expanded special-job grids keep their intrinsic width and scroll. */
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.skill-tree-line {
|
||||
@@ -48,6 +49,10 @@
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.skill-tree__icon {
|
||||
@@ -89,6 +94,38 @@
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.skill-tree__extra-awake {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 0;
|
||||
width: 117px;
|
||||
height: 54px;
|
||||
background: url('./img/extra_awake_85.png') 0 0 / 100% 100% no-repeat;
|
||||
}
|
||||
|
||||
.skill-tree__extra-awake-option {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 3px;
|
||||
left: 78px;
|
||||
display: flex;
|
||||
width: 36px;
|
||||
height: 48px;
|
||||
box-sizing: border-box;
|
||||
padding-block: 3px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
color: #ffc701;
|
||||
|
||||
span {
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-tree__awake-icon {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -1,168 +1,145 @@
|
||||
<template>
|
||||
<div ref="skillTreeRef" class="skill-page">
|
||||
<div class="skill-page__panel">
|
||||
<div class="skill-page__tabs">
|
||||
<div class="skill-page__tab" @click="tab = 0" :class="{ active: tab === 0 }">技能加点</div>
|
||||
<div class="skill-page__tab-divider">|</div>
|
||||
<div class="skill-page__tab" @click="tab = 1" :class="{ active: tab === 1 }">技能定制</div>
|
||||
</div>
|
||||
<main class="skill-page" :class="{ 'skill-page--dark-knight': showDarkKnightSkillSetting }">
|
||||
<section class="skill-page__panel" aria-label="技能配置">
|
||||
<TabsRoot v-model="tab" class="skill-page__tabs">
|
||||
<UiTabsList aria-label="技能配置类别">
|
||||
<UiTabsTrigger value="tree">技能加点</UiTabsTrigger>
|
||||
<UiTabsTrigger value="customize">技能定制</UiTabsTrigger>
|
||||
</UiTabsList>
|
||||
</TabsRoot>
|
||||
|
||||
<div class="skill-page__viewport">
|
||||
<div v-if="tab === 0" class="skill-page__tree-scroll">
|
||||
<div v-if="tab === 'tree'" class="skill-page__tree-scroll">
|
||||
<SkillTree
|
||||
:skills="infoStore.skills"
|
||||
v-model:lvInfo="configStore.config.skills"
|
||||
v-model:bindAwake="configStore.config.bindAwake"
|
||||
@chooseSkill="setCurrentSkill"
|
||||
></SkillTree>
|
||||
:alter="infoStore.infos?.alter"
|
||||
v-model:lv-info="configStore.config.skills"
|
||||
v-model:bind-awake="configStore.config.bindAwake"
|
||||
@choose-skill="setCurrentSkill"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="tab === 1" class="skill-page__customize">
|
||||
<div class="item-head" 角色等级>技能突破</div>
|
||||
<SkillUP
|
||||
class="skill-page__customize-section"
|
||||
:skills="infoStore.skills"
|
||||
v-model:lvInfo="configStore.config.skills"
|
||||
v-model:bindAwake="configStore.config.bindAwake"
|
||||
:buffer="infoStore.infos?.buffer"
|
||||
></SkillUP>
|
||||
<div class="item-head" 角色等级>技能进化</div>
|
||||
<div class="skill-page__customize-section skill-page__vp-scroll">
|
||||
<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="skill-page__detail-launcher"
|
||||
v-if="curCurrentSkill.skillId && curCurrentSkill.level > 0 && !showSkillDetail"
|
||||
>
|
||||
<div class="skill-page__detail-launcher-frame">
|
||||
<div class="skill-page__detail-launcher-border">
|
||||
<div
|
||||
class="skill-page__detail-launcher-button"
|
||||
style="background: linear-gradient(to bottom, #313330 0%, #0f110f 100%)"
|
||||
@click="showSkillDetail = true"
|
||||
>
|
||||
<img src="./components/img/detail.png" alt="" />
|
||||
技能详情
|
||||
</div>
|
||||
<div v-else class="skill-page__customize">
|
||||
<UiDataSection title="技能突破" class="skill-page__customize-section">
|
||||
<SkillUP
|
||||
:skills="infoStore.skills"
|
||||
v-model:lv-info="configStore.config.skills"
|
||||
v-model:bind-awake="configStore.config.bindAwake"
|
||||
:buffer="infoStore.infos?.buffer"
|
||||
/>
|
||||
</UiDataSection>
|
||||
<UiDataSection title="技能进化" class="skill-page__customize-section">
|
||||
<div class="skill-page__vp-scroll">
|
||||
<SkillVP
|
||||
:skills="infoStore.skills"
|
||||
v-model:lv-info="configStore.config.skills"
|
||||
v-model:bind-awake="configStore.config.bindAwake"
|
||||
@choose-skill="setCurrentSkill"
|
||||
/>
|
||||
</div>
|
||||
</UiDataSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="skill-page__detail"
|
||||
:style="style"
|
||||
ref="SkillDetailRef"
|
||||
v-if="curCurrentSkill.skillId && curCurrentSkill.level > 0 && showSkillDetail"
|
||||
>
|
||||
<SkillDetail :skillId="curCurrentSkill.skillId" :level="curCurrentSkill.level"></SkillDetail>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="skill-page__context" aria-label="技能详情">
|
||||
<header class="skill-page__context-header">
|
||||
<span>技能详情</span>
|
||||
<span v-if="currentSkill" class="skill-page__context-level">
|
||||
Lv{{ currentSkillLevel }} / {{ currentSkill.maxLearnLv }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div v-if="currentSkill && currentSkillLevel > 0" class="skill-page__detail">
|
||||
<div class="skill-page__selected-skill">
|
||||
<img :src="getImageURL(currentSkill.icon)" :alt="currentSkill.name" />
|
||||
<div>
|
||||
<strong>{{ currentSkill.name }}</strong>
|
||||
<span>{{ currentSkill.type === 'passive' ? '被动技能' : '主动技能' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-page__detail-scroll">
|
||||
<SkillDetail :skill-id="currentSkill.uuid" :level="currentSkillLevel" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="skill-page__empty">
|
||||
<img src="./components/img/detail.png" alt="" />
|
||||
<span>在技能树中选择技能查看详情</span>
|
||||
</div>
|
||||
|
||||
<DarkKnightSkillSetting
|
||||
v-if="showDarkKnightSkillSetting"
|
||||
:skills="infoStore.skills"
|
||||
v-model="configStore.config.skillShortcuts"
|
||||
class="skill-page__dark-knight-setting"
|
||||
/>
|
||||
</aside>
|
||||
</main>
|
||||
</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'
|
||||
<script setup lang="ts">
|
||||
import { UiDataSection } from '@/components/ui/data-section'
|
||||
import { TabsRoot, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
|
||||
import { useConfigStore, useInfoStore } from '@/stores'
|
||||
import { getImageURL } from '@/utils/images'
|
||||
import DarkKnightSkillSetting from './components/DarkKnightSkillSetting/index.vue'
|
||||
import SkillDetail from './components/SkillDetail'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
const props = defineProps<{
|
||||
import SkillTree from './components/SkillTree'
|
||||
import SkillUP from './components/SkillUP'
|
||||
import SkillVP from './components/SkillVP'
|
||||
|
||||
defineProps<{
|
||||
alter: string
|
||||
}>()
|
||||
const tab = ref(0)
|
||||
|
||||
const tab = ref<'tree' | 'customize'>('tree')
|
||||
const infoStore = useInfoStore()
|
||||
const configStore = useConfigStore()
|
||||
const currentSkillId = ref<number>()
|
||||
|
||||
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 showDarkKnightSkillSetting = computed(() => infoStore.infos?.alter === 'dark_knight')
|
||||
const currentSkill = computed(() =>
|
||||
infoStore.skills.find((skill) => skill.id === currentSkillId.value),
|
||||
)
|
||||
const currentSkillLevel = computed(() =>
|
||||
currentSkill.value ? (configStore.config.skills[currentSkill.value.id]?.lv ?? 0) : 0,
|
||||
)
|
||||
|
||||
const setCurrentSkill = (skillId: number) => {
|
||||
curSkillId.value = infoStore.skills.find((skill) => skill.id === skillId)?.uuid || ''
|
||||
currentSkillId.value = skillId
|
||||
}
|
||||
|
||||
onClickOutside(
|
||||
SkillDetailRef,
|
||||
(event) => {
|
||||
showSkillDetail.value = false
|
||||
},
|
||||
{
|
||||
ignore: [skillTreeRef],
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped lang="scss">
|
||||
.skill-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(210px, 0.38fr);
|
||||
gap: var(--ui-character-workspace-gap);
|
||||
overflow: hidden;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.skill-page__panel,
|
||||
.skill-page__context {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--ui-composition-border);
|
||||
background: var(--ui-skill-page-background, var(--ui-composition-background));
|
||||
}
|
||||
|
||||
.skill-page__panel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
background: var(--ui-skill-page-background, #101012);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skill-page__tabs {
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
flex: 0 0 40px;
|
||||
color: var(--ui-skill-page-muted, #6c5e4a);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.skill-page__tab,
|
||||
.skill-page__tab-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.skill-page__tab {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.skill-page__tab-divider {
|
||||
flex: 0 0 auto;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--ui-disclosure-divider);
|
||||
}
|
||||
|
||||
.skill-page__viewport,
|
||||
@@ -174,6 +151,7 @@ onClickOutside(
|
||||
}
|
||||
|
||||
.skill-page__viewport {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -182,105 +160,137 @@ onClickOutside(
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.skill-page__customize {
|
||||
display: flex;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--ui-control-gap);
|
||||
padding: var(--ui-control-gap);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skill-page__customize-section {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skill-page__customize-section :deep(.ui-data-section__content),
|
||||
.skill-page__vp-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.skill-page__detail-launcher {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 40px;
|
||||
right: -64px;
|
||||
}
|
||||
|
||||
.skill-page__detail-launcher-frame {
|
||||
box-sizing: border-box;
|
||||
.skill-page__context {
|
||||
display: flex;
|
||||
padding: 1px 1px 1px 2px;
|
||||
color: var(--ui-skill-page-accent, #ae8d5a);
|
||||
}
|
||||
|
||||
.skill-page__detail-launcher-border {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 1px 1px 1px 0;
|
||||
border: 1px solid #322e20;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.skill-page__detail-launcher-button {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 1px solid #514531;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to bottom, #313330 0%, #0f110f 100%);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.skill-page__context-header {
|
||||
display: flex;
|
||||
min-height: var(--ui-disclosure-header-min-block-size);
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ui-control-gap);
|
||||
padding-inline: var(--ui-field-padding-inline);
|
||||
border-bottom: 1px solid var(--ui-disclosure-divider);
|
||||
background: var(--ui-disclosure-header-background);
|
||||
color: var(--ui-settings-section-foreground);
|
||||
}
|
||||
|
||||
.skill-page__context-level {
|
||||
color: var(--ui-disclosure-state);
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.skill-page__detail {
|
||||
position: absolute;
|
||||
width: min(300px, 45%);
|
||||
height: 100%;
|
||||
background: var(--ui-skill-page-background, #101012);
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #ccc088;
|
||||
position: relative;
|
||||
.skill-page__selected-skill {
|
||||
display: grid;
|
||||
grid-template-columns: var(--ui-control-height-large) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: var(--ui-control-gap);
|
||||
padding: var(--ui-control-padding-inline-default);
|
||||
border-bottom: 1px solid var(--ui-disclosure-divider);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 15px;
|
||||
opacity: 0.35;
|
||||
background: #bf8d20;
|
||||
filter: blur(7px);
|
||||
z-index: 2;
|
||||
img {
|
||||
width: var(--ui-control-height-large);
|
||||
height: var(--ui-control-height-large);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: url('@/assets/img/active.png') no-repeat;
|
||||
width: 148px;
|
||||
height: 16px;
|
||||
bottom: 2px;
|
||||
transform: translate(0, 50%);
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
strong {
|
||||
overflow: hidden;
|
||||
color: var(--ui-settings-section-foreground);
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: 4px;
|
||||
color: var(--ui-muted-foreground);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-head {
|
||||
background: linear-gradient(#2b2817, #171407);
|
||||
border-top: 1px solid #423d2c;
|
||||
border-bottom: 1px solid #211d15;
|
||||
.skill-page__detail-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.skill-page__empty {
|
||||
display: flex;
|
||||
min-height: 150px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: var(--ui-control-gap);
|
||||
padding: var(--ui-control-padding-inline-default);
|
||||
color: var(--ui-muted-foreground);
|
||||
text-align: center;
|
||||
// 以下后面注释掉
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-weight: 14px;
|
||||
|
||||
img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: contain;
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-page__dark-knight-setting {
|
||||
max-height: 58%;
|
||||
flex: 0 0 auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@container (max-width: 650px) {
|
||||
.skill-page {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.skill-page__context {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -538,8 +538,8 @@ function removeFavorite(item: UiFavoriteDrawerItem) {
|
||||
<UiTabsList aria-label="物品目录"
|
||||
><UiTabsTrigger value="equipment">装备</UiTabsTrigger
|
||||
><UiTabsTrigger value="consumable">消耗品</UiTabsTrigger
|
||||
><UiTabsTrigger value="material">材料</UiTabsTrigger
|
||||
><UiTabsTrigger value="quest">任务</UiTabsTrigger></UiTabsList
|
||||
><UiTabsTrigger value="material">制作材料</UiTabsTrigger
|
||||
><UiTabsTrigger value="quest">任务物品</UiTabsTrigger></UiTabsList
|
||||
>
|
||||
<UiTabsContent value="equipment">展示武器、防具与首饰词条。</UiTabsContent>
|
||||
<UiTabsContent value="consumable">展示药剂、宝珠与特殊消耗品。</UiTabsContent>
|
||||
@@ -990,7 +990,7 @@ footer {
|
||||
background: #101111;
|
||||
color: var(--ui-muted-foreground);
|
||||
font:
|
||||
9px/1.4 Consolas,
|
||||
11px/1.4 Consolas,
|
||||
monospace;
|
||||
}
|
||||
.catalog-content {
|
||||
@@ -1197,7 +1197,7 @@ label > span,
|
||||
color: var(--dcalc-success);
|
||||
}
|
||||
.status-danger {
|
||||
color: var(--dcalc-danger);
|
||||
color: var(--ui-danger-foreground);
|
||||
}
|
||||
.table-icon {
|
||||
display: block;
|
||||
@@ -1319,9 +1319,9 @@ footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 14px;
|
||||
color: #81755f;
|
||||
color: var(--ui-muted-foreground);
|
||||
font:
|
||||
9px/1 Consolas,
|
||||
11px/1 Consolas,
|
||||
monospace;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useInfoStore } from './info'
|
||||
import api from '@/api'
|
||||
import type { IEquipment } from '@/api/info/type'
|
||||
import type { IEquipment, ISkill } from '@/api/info/type'
|
||||
import type { IResultSkillCount } from '@/api/calc/type'
|
||||
// | '上衣'
|
||||
// | '头肩'
|
||||
@@ -42,6 +42,11 @@ export interface IConfigEmblem {
|
||||
items: string[]
|
||||
}
|
||||
|
||||
export interface ISkillShortcuts {
|
||||
shortcuts: string[]
|
||||
combos: string[][]
|
||||
}
|
||||
|
||||
export interface IConfig {
|
||||
skills: Record<string, { lv: number; vp: number; up: number }>
|
||||
equips: Record<string, IConfigEquip>
|
||||
@@ -73,6 +78,7 @@ export interface IConfig {
|
||||
options: Record<string, number>
|
||||
avatarLink: string
|
||||
emblems: Record<string, IConfigEmblem>
|
||||
skillShortcuts: ISkillShortcuts
|
||||
}
|
||||
|
||||
const defaultEqusConfig = {
|
||||
@@ -113,6 +119,39 @@ const createDefaultEmblemConfig = (): IConfigEmblem => ({
|
||||
|
||||
const defaultEmblemConfig = createDefaultEmblemConfig()
|
||||
|
||||
const createDefaultSkillShortcuts = (): ISkillShortcuts => ({
|
||||
shortcuts: Array.from({ length: 6 }, () => ''),
|
||||
combos: Array.from({ length: 6 }, () => Array.from({ length: 5 }, () => '')),
|
||||
})
|
||||
|
||||
const normalizeSkillShortcuts = (
|
||||
arrangement?: Partial<ISkillShortcuts>,
|
||||
skills: ISkill[] = [],
|
||||
): ISkillShortcuts => {
|
||||
const validSkillIds = new Set(skills.map((skill) => skill.id.toString()))
|
||||
const skillIdByUuid = new Map(skills.map((skill) => [skill.uuid, skill.id.toString()]))
|
||||
const normalizeSkillId = (value: unknown) => {
|
||||
const key =
|
||||
typeof value === 'number' ? value.toString() : typeof value === 'string' ? value : ''
|
||||
if (!key) return ''
|
||||
return validSkillIds.has(key) ? key : (skillIdByUuid.get(key) ?? '')
|
||||
}
|
||||
|
||||
return {
|
||||
shortcuts: Array.from({ length: 6 }, (_, index) =>
|
||||
normalizeSkillId(arrangement?.shortcuts?.[index]),
|
||||
),
|
||||
combos: Array.from({ length: 6 }, (_, column) => {
|
||||
const combo = Array.from({ length: 5 }, (_, stage) =>
|
||||
normalizeSkillId(arrangement?.combos?.[column]?.[stage]),
|
||||
)
|
||||
const firstEmptyStage = combo.findIndex((skillId) => !skillId)
|
||||
if (firstEmptyStage >= 0) combo.fill('', firstEmptyStage)
|
||||
return combo
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const useConfigStore = defineStore('configStore', () => {
|
||||
const config = ref<IConfig>({
|
||||
skills: {},
|
||||
@@ -128,6 +167,7 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
options: {},
|
||||
avatarLink: '',
|
||||
emblems: {},
|
||||
skillShortcuts: createDefaultSkillShortcuts(),
|
||||
})
|
||||
|
||||
const skillCountConfig = ref<IResultSkillCount[]>([])
|
||||
@@ -161,6 +201,7 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
options: {},
|
||||
avatarLink: '',
|
||||
emblems: {},
|
||||
skillShortcuts: createDefaultSkillShortcuts(),
|
||||
}
|
||||
}
|
||||
if (!config.value?.equips) config.value.equips = {}
|
||||
@@ -172,6 +213,10 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
if (!config.value?.options) config.value.options = {}
|
||||
if (!config.value?.oaths) config.value.oaths = {}
|
||||
if (!config.value?.emblems) config.value.emblems = {}
|
||||
config.value.skillShortcuts = normalizeSkillShortcuts(
|
||||
config.value.skillShortcuts,
|
||||
infoStore.skills,
|
||||
)
|
||||
|
||||
const part = [...infoStore.parts, '宠物', '副武器'].filter(
|
||||
(a) => !config.value?.equips.hasOwnProperty(a),
|
||||
@@ -291,5 +336,6 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
createDefaultOathConfig,
|
||||
defaultEmblemConfig,
|
||||
createDefaultEmblemConfig,
|
||||
createDefaultSkillShortcuts,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
--ui-accent-foreground: #fee97d;
|
||||
--ui-destructive: var(--dcalc-danger);
|
||||
--ui-destructive-foreground: #fff4f4;
|
||||
--ui-danger-foreground: #ef8883;
|
||||
--ui-unavailable-foreground: #8f8b83;
|
||||
--ui-border: var(--dcalc-brass-dark);
|
||||
--ui-input: #5b472a;
|
||||
--ui-ring: var(--dcalc-focus);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
--ui-accent-foreground: #ffffff;
|
||||
--ui-destructive: #a73f45;
|
||||
--ui-destructive-foreground: #fff4f4;
|
||||
--ui-danger-foreground: #f08b94;
|
||||
--ui-unavailable-foreground: #8da0bc;
|
||||
--ui-border: #40577a;
|
||||
--ui-input: #354b6e;
|
||||
--ui-ring: #6fb5e6;
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
--ui-popup-z-index: 80;
|
||||
--ui-field-padding-inline: 9px;
|
||||
--ui-option-height: 27px;
|
||||
--ui-tabs-height: 40px;
|
||||
--ui-tab-height: 30px;
|
||||
--ui-tabs-height: 32px;
|
||||
--ui-tab-height: 24px;
|
||||
|
||||
/*
|
||||
* Component geometry defaults. Every value is consumed through var(), so a component instance
|
||||
@@ -132,10 +132,10 @@
|
||||
100dvh - 16px - var(--ui-dialog-header-block-size) - var(--ui-dialog-footer-min-block-size)
|
||||
);
|
||||
--ui-dialog-body-padding: 12px;
|
||||
--ui-tab-marker-inline-size: 39px;
|
||||
--ui-tab-marker-block-size: 5px;
|
||||
--ui-tab-marker-arrow-inline-size: 9px;
|
||||
--ui-tab-marker-arrow-block-size: 4px;
|
||||
--ui-tab-marker-inline-size: 33px;
|
||||
--ui-tab-marker-block-size: 4px;
|
||||
--ui-tab-marker-arrow-inline-size: 7px;
|
||||
--ui-tab-marker-arrow-block-size: 3px;
|
||||
--ui-number-fast-inline-size: 26px;
|
||||
--ui-number-fast-block-size: 28px;
|
||||
--ui-number-fast-step-block-size: 14px;
|
||||
@@ -300,3 +300,14 @@
|
||||
--ui-shop-action-block-size: 24px;
|
||||
--ui-shop-action-icon-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
:root {
|
||||
/* Preserve pixel artwork while giving standalone touch controls a 24px minimum hit box. */
|
||||
--ui-slider-control-size: 24px;
|
||||
--ui-select-icon-inline-size: 24px;
|
||||
--ui-select-icon-block-size: 24px;
|
||||
--ui-number-fast-block-size: 48px;
|
||||
--ui-number-fast-step-block-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user