CheckinTargetList.vue 7.78 KB
<!--
 * @Date: 2025-12-16 11:44:27
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-12-17 11:57:03
 * @FilePath: /mlaj/src/components/count/CheckinTargetList.vue
 * @Description: 打卡动态对象列表组件
-->
<template>
    <div class="mb-4">
        <div class="flex justify-between items-center mb-2 mx-2">
            <div class="flex items-center gap-2">
                <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}列表</div>
                <div class="text-xs text-gray-400 font-normal scale-90 origin-left">(列表项长按可编辑)</div>
            </div>
            <van-button size="small" type="primary" plain icon="plus" @click="onAdd" class="!h-7">添加</van-button>
        </div>

        <div class="bg-gray-50 rounded-lg p-2 relative">
            <div
                ref="listContainerRef"
                class="flex flex-wrap gap-2 transition-all duration-300 overflow-hidden"
                :style="{ maxHeight: isExpanded ? 'none' : collapsedHeight }"
            >
                <template v-if="targetList.length > 0">
                    <div v-for="(item, index) in targetList" :key="index"
                        class="px-4 py-1.5 rounded-full text-sm transition-colors duration-200 border cursor-pointer select-none relative"
                        :style="[
                            selectedTargets.some(t => (t.id && item.id && t.id == item.id) || (!t.id && t.name === item.name)) ? {
                                backgroundColor: '#4caf50',
                                color: '#ffffff',
                                borderColor: '#4caf50'
                            } : {
                                backgroundColor: '#ffffff',
                                color: '#4b5563',
                                borderColor: '#e5e7eb'
                            },
                            {
                                '-webkit-touch-callout': 'none',
                                '-webkit-user-select': 'none',
                                'user-select': 'none'
                            }
                        ]"
                        @contextmenu.prevent
                        @click="onClick(item)"
                        @touchstart="onTouchStart(item)"
                        @touchend="onTouchEnd"
                        @touchmove="onTouchMove"
                        @mousedown="onMouseDown(item)"
                        @mouseup="onMouseUp"
                        @mouseleave="onMouseUp"
                    >
                        {{ item.name }}
                    </div>
                </template>
                <div v-else class="w-full text-center py-4 text-gray-400 text-sm">
                    暂无{{ dynamicFieldText }}列表,请点击上方添加按钮
                </div>
            </div>

            <!-- 展开/收起按钮 -->
            <div
                v-if="showExpandBtn"
                class="flex justify-center mt-2 cursor-pointer"
                @click="toggleExpand"
            >
                <div class="bg-white rounded-full p-1 shadow-sm border border-gray-200 flex items-center justify-center w-6 h-6 hover:bg-gray-50 active:bg-gray-100">
                    <van-icon :name="isExpanded ? 'arrow-up' : 'arrow-down'" color="#9ca3af" size="12" />
                </div>
            </div>
        </div>

        <!-- 操作菜单 -->
        <van-action-sheet
            v-model:show="showActionSheet"
            :actions="actions"
            cancel-text="取消"
            close-on-click-action
            @select="onSelectAction"
        />
    </div>
</template>

<script setup>
import { ref, onMounted, nextTick, watch } from 'vue'
import { showConfirmDialog } from 'vant'

/**
 * 计数对象列表组件
 * @description 展示可供选择的计数对象列表,支持选择和添加
 */

const props = defineProps({
    /**
     * 动态字段文本 (e.g. "感恩", "念佛")
     */
    dynamicFieldText: {
        type: String,
        default: '计数'
    },
    /**
     * 所有可用的对象列表
     */
    targetList: {
        type: Array,
        default: () => []
    },
    /**
     * 当前选中的对象列表
     */
    selectedTargets: {
        type: Array,
        default: () => []
    }
})

// 展开/收起相关
const isExpanded = ref(false)
const showExpandBtn = ref(false)
const listContainerRef = ref(null)
const collapsedHeight = ref('118px') // 默认值,会被动态计算覆盖

const checkHeight = async () => {
    await nextTick()
    const container = listContainerRef.value
    if (!container || props.targetList.length === 0) {
        showExpandBtn.value = false
        return
    }

    // 获取第一个子元素的高度和容器的行间距
    const firstItem = container.children[0]
    if (firstItem) {
        const itemHeight = firstItem.offsetHeight
        const style = window.getComputedStyle(container)
        const rowGap = parseFloat(style.rowGap) || 8 // 获取 gap 值,默认为 8px

        // 计算3行的高度: 3个元素高度 + 2个间距
        const limitHeight = (itemHeight * 3) + (rowGap * 2)
        collapsedHeight.value = `${limitHeight}px`

        // 增加一点容差(2px),避免因为像素计算误差导致刚好3行时显示按钮
        showExpandBtn.value = container.scrollHeight > (limitHeight + 2)
    }
}

const toggleExpand = () => {
    isExpanded.value = !isExpanded.value
}

// 监听列表变化,重新计算高度
watch(() => props.targetList, () => {
    checkHeight()
}, { deep: true })

onMounted(() => {
    checkHeight()
})

const emit = defineEmits(['add', 'toggle', 'edit', 'delete'])

/**
 * 点击添加按钮
 */
const onAdd = () => {
    emit('add')
}

// 长按相关逻辑
const longPressTimer = ref(null)
const isLongPress = ref(false)
const showActionSheet = ref(false)
const currentItem = ref(null)

const actions = [
    { name: '编辑', color: '#1989fa', action: 'edit' },
]

const startLongPress = (item) => {
    isLongPress.value = false
    longPressTimer.value = setTimeout(() => {
        isLongPress.value = true
        currentItem.value = item
        showActionSheet.value = true
        // 震动反馈 (如果设备支持)
        if (navigator.vibrate) {
            navigator.vibrate(50)
        }
    }, 500)
}

const clearLongPress = () => {
    if (longPressTimer.value) {
        clearTimeout(longPressTimer.value)
        longPressTimer.value = null
    }
}

// Touch events
const onTouchStart = (item) => {
    startLongPress(item)
}

const onTouchEnd = () => {
    clearLongPress()
}

const onTouchMove = () => {
    clearLongPress()
}

// Mouse events (for PC debugging)
const onMouseDown = (item) => {
    // 仅在非 PC 端(触摸设备)或明确需要测试长按时启用
    // 这里简单地禁用 PC 端鼠标长按,因为点击会触发 onClick
    // 如果需要在 PC 端支持长按,需要区分点击和长按
    // startLongPress(item)
}

const onMouseUp = () => {
    clearLongPress()
}

/**
 * 点击项
 * @param {Object} item
 */
const onClick = (item) => {
    // 如果是长按触发的结束,不执行点击
    if (isLongPress.value) {
        // 重置状态
        setTimeout(() => {
            isLongPress.value = false
        }, 0)
        return
    }
    emit('toggle', item)
}

/**
 * 选中操作
 */
const onSelectAction = (action) => {
    if (action.action === 'edit') {
        emit('edit', currentItem.value)
    } else if (action.action === 'delete') {
        confirmDelete()
    }
}

// 删除相关
const confirmDelete = () => {
    if (!currentItem.value) return

    showConfirmDialog({
        title: '确认删除',
        message: `确定要删除"${currentItem.value.name}"吗?`,
    })
        .then(() => {
            emit('delete', currentItem.value)
        })
        .catch(() => {
            // on cancel
        })
}
</script>