mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-24 07:00:49 +08:00
Compare commits
4 Commits
codex/refa
...
codex/crea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e58a5741 | ||
|
|
a68c925c68 | ||
|
|
4f248e8a71 | ||
|
|
277883f9d9 |
157
frontend_nuxt/components/BaseItemGroup.vue
Normal file
157
frontend_nuxt/components/BaseItemGroup.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-item-group"
|
||||
:style="{
|
||||
width: `${containerWidth}px`,
|
||||
height: `${itemSize}px`,
|
||||
'--base-item-group-duration': `${animationDuration}ms`,
|
||||
}"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="itemKey(item, index)"
|
||||
class="base-item-group__item"
|
||||
:style="{
|
||||
width: `${itemSize}px`,
|
||||
height: `${itemSize}px`,
|
||||
transform: `translateX(${index * activeGap}px)`,
|
||||
zIndex: items.length - index,
|
||||
}"
|
||||
>
|
||||
<slot :item="item" :index="index">
|
||||
<BaseImage
|
||||
v-if="item && (item.src || typeof item === 'string')"
|
||||
class="base-item-group__image"
|
||||
:src="typeof item === 'string' ? item : item.src"
|
||||
:alt="itemAlt(item, index)"
|
||||
/>
|
||||
<div v-else class="base-item-group__placeholder">{{ placeholderText(item) }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import BaseImage from './BaseImage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
itemSize: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
collapsedGap: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
expandedGap: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
animationDuration: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
itemKeyField: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
})
|
||||
|
||||
const isHovered = ref(false)
|
||||
|
||||
const onMouseEnter = () => {
|
||||
isHovered.value = true
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
isHovered.value = false
|
||||
}
|
||||
|
||||
const effectiveExpandedGap = computed(() =>
|
||||
props.expandedGap == null ? props.itemSize : props.expandedGap,
|
||||
)
|
||||
|
||||
const activeGap = computed(() =>
|
||||
isHovered.value ? effectiveExpandedGap.value : props.collapsedGap,
|
||||
)
|
||||
|
||||
const containerWidth = computed(() =>
|
||||
props.items.length ? props.itemSize + (props.items.length - 1) * activeGap.value : props.itemSize,
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
if (effectiveExpandedGap.value < props.collapsedGap) {
|
||||
console.warn('[BaseItemGroup] `expandedGap` should be greater than or equal to `collapsedGap`.')
|
||||
}
|
||||
})
|
||||
|
||||
const itemKey = (item, index) => {
|
||||
if (item && typeof item === 'object' && props.itemKeyField in item) {
|
||||
return item[props.itemKeyField]
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
const itemAlt = (item, index) => {
|
||||
if (item && typeof item === 'object') {
|
||||
return item.alt || `item-${index}`
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return `item-${index}`
|
||||
}
|
||||
return 'item'
|
||||
}
|
||||
|
||||
const placeholderText = (item) => {
|
||||
if (item == null) return ''
|
||||
if (typeof item === 'object' && 'text' in item) return item.text
|
||||
return String(item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.base-item-group {
|
||||
display: flex;
|
||||
position: relative;
|
||||
transition: width var(--base-item-group-duration) ease;
|
||||
}
|
||||
|
||||
.base-item-group__item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-neutral-100, #f0f2f5);
|
||||
transition: transform var(--base-item-group-duration) ease;
|
||||
box-shadow: 0 0 0 2px var(--color-surface, #fff);
|
||||
}
|
||||
|
||||
.base-item-group__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.base-item-group__placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-500, #666);
|
||||
background-color: var(--color-neutral-200, #e5e7eb);
|
||||
}
|
||||
</style>
|
||||
@@ -392,15 +392,6 @@ const handleContentClick = (e) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-footer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.comment-reaction-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
<div
|
||||
v-if="panelVisible"
|
||||
class="reactions-panel"
|
||||
ref="reactionsPanelRef"
|
||||
:style="panelInlineStyle"
|
||||
@mouseenter="cancelHide"
|
||||
@mouseleave="scheduleHide"
|
||||
>
|
||||
@@ -57,7 +59,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { toast } from '~/main'
|
||||
import { authState, getToken } from '~/utils/auth'
|
||||
import { reactionEmojiMap } from '~/utils/reactions'
|
||||
@@ -141,6 +143,8 @@ const displayedReactions = computed(() => {
|
||||
const panelTypes = computed(() => sortedReactionTypes.value)
|
||||
|
||||
const panelVisible = ref(false)
|
||||
const reactionsPanelRef = ref(null)
|
||||
const panelInlineStyle = ref({})
|
||||
let hideTimer = null
|
||||
const openPanel = () => {
|
||||
clearTimeout(hideTimer)
|
||||
@@ -156,6 +160,33 @@ 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()
|
||||
}
|
||||
})
|
||||
|
||||
watch(panelTypes, async () => {
|
||||
if (panelVisible.value) {
|
||||
await nextTick()
|
||||
updatePanelInlineStyle()
|
||||
}
|
||||
})
|
||||
|
||||
const toggleReaction = async (type) => {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
@@ -231,11 +262,16 @@ const toggleReaction = async (type) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
window.addEventListener('resize', updatePanelInlineStyle)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
toggleReaction,
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updatePanelInlineStyle)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -288,7 +324,7 @@ defineExpose({
|
||||
|
||||
.reactions-panel {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
bottom: 40px;
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid var(--normal-border-color);
|
||||
border-radius: 20px;
|
||||
|
||||
@@ -756,7 +756,7 @@ function goBack() {
|
||||
.message-reaction-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -1272,7 +1272,7 @@ onMounted(async () => {
|
||||
.article-footer-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: 0px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user