mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-02 03:55:58 +00:00
feat: 新增itemGroup
This commit is contained in:
187
frontend_nuxt/components/BaseItemGroup.vue
Normal file
187
frontend_nuxt/components/BaseItemGroup.vue
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="groupRef"
|
||||||
|
class="base-item-group"
|
||||||
|
:class="groupClass"
|
||||||
|
:style="groupStyle"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
@focusin="onFocusIn"
|
||||||
|
@focusout="onFocusOut"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in normalizedItems"
|
||||||
|
:key="resolveKey(item, index)"
|
||||||
|
class="base-item-group-item"
|
||||||
|
:style="{ zIndex: getZIndex(index) }"
|
||||||
|
>
|
||||||
|
<slot name="item" :item="item" :index="index"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="after"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
itemKey: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
overlap: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 12,
|
||||||
|
},
|
||||||
|
expandedGap: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 8,
|
||||||
|
},
|
||||||
|
direction: {
|
||||||
|
type: String,
|
||||||
|
default: 'horizontal',
|
||||||
|
validator: (value) => ['horizontal', 'vertical'].includes(value),
|
||||||
|
},
|
||||||
|
reverse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
animationDuration: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 200,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupRef = ref(null)
|
||||||
|
const state = reactive({
|
||||||
|
hovering: false,
|
||||||
|
focused: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizedItems = computed(() => props.items || [])
|
||||||
|
|
||||||
|
const sanitizedOverlap = computed(() => Math.max(0, Number(props.overlap) || 0))
|
||||||
|
const sanitizedExpandedGap = computed(() => Math.max(0, Number(props.expandedGap) || 0))
|
||||||
|
const sanitizedAnimationDuration = computed(() => Math.max(0, Number(props.animationDuration) || 0))
|
||||||
|
|
||||||
|
const groupClass = computed(() => [
|
||||||
|
`base-item-group--${props.direction}`,
|
||||||
|
{
|
||||||
|
'is-expanded': isExpanded.value,
|
||||||
|
'is-reversed': props.reverse,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const groupStyle = computed(() => ({
|
||||||
|
'--base-item-group-overlap': `${sanitizedOverlap.value}px`,
|
||||||
|
'--base-item-group-expanded-gap': `${sanitizedExpandedGap.value}px`,
|
||||||
|
'--base-item-group-transition-duration': `${sanitizedAnimationDuration.value}ms`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isExpanded = computed(() => state.hovering || state.focused)
|
||||||
|
|
||||||
|
function onMouseEnter() {
|
||||||
|
state.hovering = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeave() {
|
||||||
|
state.hovering = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocusIn() {
|
||||||
|
state.focused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocusOut(event) {
|
||||||
|
const nextTarget = event.relatedTarget
|
||||||
|
if (!groupRef.value) {
|
||||||
|
state.focused = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!nextTarget || !groupRef.value.contains(nextTarget)) {
|
||||||
|
state.focused = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveKey(item, index) {
|
||||||
|
if (typeof props.itemKey === 'function') {
|
||||||
|
return props.itemKey(item, index)
|
||||||
|
}
|
||||||
|
if (props.itemKey && item && Object.prototype.hasOwnProperty.call(item, props.itemKey)) {
|
||||||
|
return item[props.itemKey]
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
function getZIndex(index) {
|
||||||
|
if (props.reverse) {
|
||||||
|
return index + 1
|
||||||
|
}
|
||||||
|
return normalizedItems.value.length - index
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.base-item-group {
|
||||||
|
--base-item-group-overlap: 12px;
|
||||||
|
--base-item-group-expanded-gap: 8px;
|
||||||
|
--base-item-group-transition-duration: 200ms;
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group:focus-within {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--horizontal.is-reversed {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--vertical.is-reversed {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group-item {
|
||||||
|
transition:
|
||||||
|
margin var(--base-item-group-transition-duration) ease,
|
||||||
|
transform var(--base-item-group-transition-duration) ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--horizontal:not(.is-expanded) .base-item-group-item:not(:first-child) {
|
||||||
|
margin-left: calc(var(--base-item-group-overlap) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--horizontal.is-expanded .base-item-group-item:not(:first-child) {
|
||||||
|
margin-left: var(--base-item-group-expanded-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--vertical:not(.is-expanded) .base-item-group-item:not(:first-child) {
|
||||||
|
margin-top: calc(var(--base-item-group-overlap) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group--vertical.is-expanded .base-item-group-item:not(:first-child) {
|
||||||
|
margin-top: var(--base-item-group-expanded-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-item-group.is-expanded .base-item-group-item {
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
frontend_nuxt/components/DonateGroup.vue
Normal file
126
frontend_nuxt/components/DonateGroup.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="donate-container">
|
||||||
|
<div class="donate-viewer">
|
||||||
|
<div class="donate-viewer-item-container" @mouseenter="cancelHide" @mouseleave="scheduleHide">
|
||||||
|
<BaseItemGroup :items="donateItems" :overlap="10" :expanded-gap="2" :direction="vertical">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<BaseUserAvatar
|
||||||
|
:user-id="1"
|
||||||
|
:src="avatar"
|
||||||
|
alt="avatar"
|
||||||
|
:width="25"
|
||||||
|
:disable-link="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BaseItemGroup>
|
||||||
|
<div class="donate-counts-text">100</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="panelVisible"
|
||||||
|
class="donate-panel"
|
||||||
|
ref="reactionsPanelRef"
|
||||||
|
:style="panelInlineStyle"
|
||||||
|
@mouseenter="cancelHide"
|
||||||
|
@mouseleave="scheduleHide"
|
||||||
|
>
|
||||||
|
<div class="donate-option">
|
||||||
|
<financing />
|
||||||
|
<div class="donate-counts-text">100</div>
|
||||||
|
</div>
|
||||||
|
<div class="donate-option">
|
||||||
|
<financing />
|
||||||
|
<div class="donate-counts-text">300</div>
|
||||||
|
</div>
|
||||||
|
<div class="donate-option">
|
||||||
|
<financing />
|
||||||
|
<div class="donate-counts-text">500</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({})
|
||||||
|
|
||||||
|
const avatar = ref('')
|
||||||
|
const panelVisible = ref(false)
|
||||||
|
const reactionsPanelRef = ref(null)
|
||||||
|
const panelInlineStyle = ref({})
|
||||||
|
const donateItems = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
count: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
count: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
count: 500,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
let hideTimer = null
|
||||||
|
|
||||||
|
const scheduleHide = () => {
|
||||||
|
clearTimeout(hideTimer)
|
||||||
|
hideTimer = setTimeout(() => {
|
||||||
|
panelVisible.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
const cancelHide = () => {
|
||||||
|
clearTimeout(hideTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePanelInlineStyle = () => {
|
||||||
|
if (!panelVisible.value) return
|
||||||
|
const panelEl = reactionsPanelRef.value
|
||||||
|
if (!panelEl) return
|
||||||
|
const parentEl = panelEl.closest('.reactions-container')?.parentElement
|
||||||
|
if (!parentEl) return
|
||||||
|
const parentWidth = parentEl.clientWidth - 20
|
||||||
|
panelInlineStyle.value = {
|
||||||
|
width: 'max-content',
|
||||||
|
maxWidth: `${parentWidth}px`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(panelVisible, async (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
await nextTick()
|
||||||
|
updatePanelInlineStyle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener('resize', updatePanelInlineStyle)
|
||||||
|
|
||||||
|
const updateAvatar = async () => {
|
||||||
|
if (authState.loggedIn) {
|
||||||
|
const user = await loadCurrentUser()
|
||||||
|
if (user && user.avatar) {
|
||||||
|
avatar.value = user.avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await updateAvatar()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updatePanelInlineStyle)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.donate-viewer-item-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -164,7 +164,7 @@ const updatePanelInlineStyle = () => {
|
|||||||
if (!panelVisible.value) return
|
if (!panelVisible.value) return
|
||||||
const panelEl = reactionsPanelRef.value
|
const panelEl = reactionsPanelRef.value
|
||||||
if (!panelEl) return
|
if (!panelEl) return
|
||||||
const parentEl = panelEl.closest('.reactions-container')?.parentElement
|
const parentEl = panelEl.closest('.reactions-container')?.parentElement?.parentElement
|
||||||
if (!parentEl) return
|
if (!parentEl) return
|
||||||
const parentWidth = parentEl.clientWidth - 20
|
const parentWidth = parentEl.clientWidth - 20
|
||||||
panelInlineStyle.value = {
|
panelInlineStyle.value = {
|
||||||
@@ -357,7 +357,6 @@ onBeforeUnmount(() => {
|
|||||||
border: 1px solid var(--normal-border-color);
|
border: 1px solid var(--normal-border-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -92,12 +92,15 @@
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="article-footer-container">
|
<div class="article-footer-container">
|
||||||
<ReactionsGroup
|
<div class="option-container">
|
||||||
ref="postReactionsGroupRef"
|
<ReactionsGroup
|
||||||
v-model="postReactions"
|
ref="postReactionsGroupRef"
|
||||||
content-type="post"
|
v-model="postReactions"
|
||||||
:content-id="postId"
|
content-type="post"
|
||||||
/>
|
:content-id="postId"
|
||||||
|
/>
|
||||||
|
<DonateGroup />
|
||||||
|
</div>
|
||||||
<div class="article-footer-actions">
|
<div class="article-footer-actions">
|
||||||
<div
|
<div
|
||||||
class="reaction-action like-action"
|
class="reaction-action like-action"
|
||||||
@@ -211,6 +214,7 @@ import PostChangeLogItem from '~/components/PostChangeLogItem.vue'
|
|||||||
import ArticleTags from '~/components/ArticleTags.vue'
|
import ArticleTags from '~/components/ArticleTags.vue'
|
||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||||
|
import DonateGroup from '~/components/DonateGroup.vue'
|
||||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||||
import PostLottery from '~/components/PostLottery.vue'
|
import PostLottery from '~/components/PostLottery.vue'
|
||||||
import PostPoll from '~/components/PostPoll.vue'
|
import PostPoll from '~/components/PostPoll.vue'
|
||||||
@@ -1278,6 +1282,14 @@ onMounted(async () => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.option-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.article-footer-actions {
|
.article-footer-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import {
|
|||||||
Dislike,
|
Dislike,
|
||||||
CheckOne,
|
CheckOne,
|
||||||
Share,
|
Share,
|
||||||
|
Financing,
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
@@ -163,4 +164,5 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
nuxtApp.vueApp.component('Dislike', Dislike)
|
nuxtApp.vueApp.component('Dislike', Dislike)
|
||||||
nuxtApp.vueApp.component('CheckOne', CheckOne)
|
nuxtApp.vueApp.component('CheckOne', CheckOne)
|
||||||
nuxtApp.vueApp.component('Share', Share)
|
nuxtApp.vueApp.component('Share', Share)
|
||||||
|
nuxtApp.vueApp.component('Financing', Financing)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user