发现一个有趣的便签墙网站

站长可乐
1天前发布

发现一个不错的网站,这个网站作者是:https://github.com/uninto/notes,纯html、css、js实现。
有人说这个类似之前的许愿墙,我觉得也可以把这个元素加进去,毕竟我群里很多宅男腐女,最喜欢这个玩意儿了,晚些我直接上线到我的工具箱里
mhgb3msq.png
因为这个只有一个前端页面,代码如下,我就准备做个后端出来,到时候重新开个帖子,供大家使用。

<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>便签墙</title>
        <style>
            * {
                box-sizing: border-box;
                margin: 0;
                padding: 0;
            }

            body {
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                background-image: linear-gradient(0deg, #eee 1px, transparent 0),
                    linear-gradient(90deg, #eee 1px, transparent 0);
                background-size: 30px 30px;
                color: #333;
                min-height: 100dvh;
                overflow: hidden;
            }

            body.has-maximized-card {
                overflow: hidden;
            }

            body.is-mobile {
                overflow-y: auto;
            }

            #board {
                position: relative;
                width: 100vw;
                height: 100dvh;
                overflow: hidden;
            }

            body.is-mobile #board {
                height: auto;
                min-height: 100dvh;
            }

            .card {
                position: absolute;
                width: 220px;
                border-radius: 12px;
                box-shadow: 0 16px 35px rgba(0, 0, 0, 0.2);
                background: #fff;
                border: 1px solid rgba(0, 0, 0, 0.08);
                overflow: hidden;
                opacity: 0;
                transform-origin: center;
                transition: transform 0.35s ease, opacity 0.35s ease, left 0.35s ease,
                    top 0.35s ease, width 0.35s ease, height 0.35s ease,
                    border-radius 0.35s ease;
            }

            .card.dragging {
                transition: none;
                box-shadow: 0 22px 45px rgba(0, 0, 0, 0.35);
            }

            .card.maximized {
                position: fixed;
                inset: 0;
                width: 100vw;
                height: 100vh;
                height: 100dvh;
                border-radius: 0;
                box-shadow: 0 28px 60px rgba(0, 0, 0, 0.4);
            }

            .card-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 10px 12px;
                background: rgba(255, 255, 255, 0.7);
                cursor: grab;
                user-select: none;
                touch-action: pan-y;
            }

            .card-header.dragging {
                cursor: grabbing;
            }

            .window-controls {
                display: flex;
                align-items: center;
                gap: 6px;
            }

            .window-controls .control {
                position: relative;
                width: 12px;
                height: 12px;
                border-radius: 50%;
                border: 1px solid rgba(0, 0, 0, 0.08);
                background: #ccc;
                cursor: pointer;
                outline: none;
                padding: 0;
                display: inline-flex;
                align-items: center;
                justify-content: center;
            }

            .window-controls .control.close {
                background: #ff5f57;
                border-color: #e0443e;
            }

            .window-controls .control.minimize {
                background: #febb2e;
                border-color: #dea123;
            }

            .window-controls .control.maximize {
                background: #28c840;
                border-color: #1aab2c;
            }

            .window-controls .control::after {
                content: '';
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                opacity: 0;
                transition: opacity 0.2s ease;
            }

            .card-header:hover .window-controls .control::after {
                opacity: 0.8;
            }

            .window-controls .control.close::after {
                content: '×';
                width: auto;
                height: auto;
                background: none;
                font-size: 10px;
                line-height: 1;
                font-weight: 700;
                color: rgba(0, 0, 0, 0.7);
            }

            .window-controls .control.minimize::after {
                width: 6px;
                height: 2px;
                background: rgba(0, 0, 0, 0.6);
            }

            .window-controls .control.maximize::after {
                width: 6px;
                height: 6px;
                background: linear-gradient(
                    45deg,
                    rgba(0, 0, 0, 0.6) 0%,
                    rgba(0, 0, 0, 0.6) 45%,
                    transparent 45%,
                    transparent 55%,
                    rgba(0, 0, 0, 0.6) 55%,
                    rgba(0, 0, 0, 0.6) 100%
                );
            }

            .card-title {
                font-size: 13px;
                font-weight: 600;
                color: rgba(0, 0, 0, 0.55);
                padding-left: 10px;
                flex: 1;
            }

            .card-body {
                padding: 16px;
                font-size: 16px;
                line-height: 1.4;
                font-weight: 600;
                color: rgba(0, 0, 0, 0.72);
                word-break: break-word;
                overflow-wrap: anywhere;
                white-space: normal;
            }

            .card.maximized {
                display: flex;
                flex-direction: column;
            }

            .card.maximized .card-title {
                display: none;
            }

            .card.maximized .card-body {
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
                width: 100%;
                height: 100%;
                text-align: center;
                padding: clamp(32px, min(10vw, 10vh), 128px);
                padding-top: clamp(72px, min(14vw, 14vh), 192px);
                font-size: clamp(48px, min(18vw, 18vh), 200px);
                line-height: 1.05;
            }

            @media (max-width: 768px) {
                .card {
                    width: 180px;
                    border-radius: 10px;
                }

                .card-body {
                    padding: 14px;
                    font-size: 14px;
                }

                .card-title {
                    font-size: 12px;
                }
            }
        </style>
    </head>
    <body>
        <div id="board"></div>

        <script>
            const board = document.getElementById('board')
            const messages = [
                '保持好心情',
                '多喝水哦',
                '今天辛苦啦',
                '早点休息',
                '记得吃水果',
                '加油,你可以的',
                '祝你顺利',
                '保持微笑呀',
                '愿所有烦恼都消失',
                '期待下一次见面',
                '梦想总会实现',
                '天气冷了,多穿衣服',
                '记得给自己放松',
                '每天都要元气满满',
                '今天也要好好爱自己',
                '适当休息一下'
            ]

            const colors = [
                '#ffe0e3',
                '#c7f0ff',
                '#ffd8a8',
                '#d9f2d9',
                '#e5d7ff',
                '#f9f7d9',
                '#d2f0f8',
                '#ffd4f5'
            ]

            const cardStates = new WeakMap()
            // Reserve a very high层级给全屏卡片,避免被后续元素覆盖
            const MAXIMIZED_LAYER = 1000000
            let activeMaximizedCard = null
            const pointerMediaQuery = window.matchMedia('(pointer: coarse)')
            let isMobile =
                pointerMediaQuery.matches || window.innerWidth <= 768
            let maxCards = isMobile ? 120 : 180 // 限制 DOM 节点数量,减轻移动端压力
            const initialCardCount = isMobile ? 18 : 30
            let spawnInterval = isMobile ? 700 : 400
            let zIndexCursor = 200
            let spawnTimer = null

            document.body.classList.toggle('is-mobile', isMobile)

            function randomFrom(array) {
                return array[Math.floor(Math.random() * array.length)]
            }

            function clamp(value, min, max) {
                return Math.min(Math.max(value, min), max)
            }

            function applyTransform(card, state) {
                const scale = state.scale ?? 1
                const angle = state.angle ?? 0
                card.style.transform = `scale(${scale}) rotate(${angle}deg)`
            }

            function bringToFront(card) {
                if (card === activeMaximizedCard) {
                    card.style.zIndex = MAXIMIZED_LAYER
                    return
                }

                zIndexCursor += 1
                if (activeMaximizedCard && zIndexCursor >= MAXIMIZED_LAYER) {
                    zIndexCursor = MAXIMIZED_LAYER - 1
                }

                card.style.zIndex = zIndexCursor
            }

            function updateBodyMaximizedState() {
                document.body.classList.toggle(
                    'has-maximized-card',
                    Boolean(activeMaximizedCard)
                )
            }

            function scheduleNextSpawn() {
                clearTimeout(spawnTimer)
                spawnTimer = setTimeout(() => {
                    if (!document.hidden) {
                        createCard()
                    }
                    scheduleNextSpawn()
                }, spawnInterval)
            }

            function syncMobileMode() {
                const nextIsMobile =
                    pointerMediaQuery.matches || window.innerWidth <= 768
                if (nextIsMobile === isMobile) return

                isMobile = nextIsMobile
                maxCards = isMobile ? 120 : 180
                spawnInterval = isMobile ? 700 : 400
                document.body.classList.toggle('is-mobile', isMobile)
                scheduleNextSpawn()
            }

            function handleBoardClick(event) {
                const control = event.target.closest('.control')
                if (!control) return

                const card = control.closest('.card')
                if (!card || !board.contains(card)) return

                event.preventDefault()

                if (control.classList.contains('close')) {
                    closeCard(card)
                } else if (control.classList.contains('minimize')) {
                    minimizeCard(card)
                } else if (control.classList.contains('maximize')) {
                    toggleMaximize(card)
                }
            }

            function handleBoardPointerDown(event) {
                const card = event.target.closest('.card')
                if (!card || !board.contains(card)) return

                const control = event.target.closest('.control')
                const header = event.target.closest('.card-header')
                const pointerType = event.pointerType || 'mouse'
                const isPrimaryPointer = event.isPrimary !== false

                if (
                    header &&
                    !control &&
                    pointerType !== 'touch' &&
                    isPrimaryPointer
                ) {
                    startDrag(event, card)
                    return
                }

                bringToFront(card)
            }

            function handleBoardDoubleClick(event) {
                const header = event.target.closest('.card-header')
                if (!header || event.target.closest('.control')) return

                const card = header.closest('.card')
                if (!card || !board.contains(card)) return

                toggleMaximize(card)
            }

            board.addEventListener('click', handleBoardClick)
            board.addEventListener('pointerdown', handleBoardPointerDown)
            board.addEventListener('dblclick', handleBoardDoubleClick)

            function closeCard(card) {
                const state = cardStates.get(card)
                if (!state || state.closing) return
                if (card === activeMaximizedCard) {
                    activeMaximizedCard = null
                    updateBodyMaximizedState()
                }
                state.closing = true
                state.scale = 0.1
                card.style.opacity = '0'
                applyTransform(card, state)

                const handleTransitionEnd = event => {
                    if (event.propertyName === 'opacity') {
                        card.removeEventListener('transitionend', handleTransitionEnd)
                        card.remove()
                    }
                }

                card.addEventListener('transitionend', handleTransitionEnd)
            }

            function minimizeCard(card) {
                const state = cardStates.get(card)
                if (!state || state.closing) return

                // 最小化动画:缩小并淡出到底部,结束时移除节点释放内存
                const runMinimize = () => {
                    state.closing = true
                    bringToFront(card)
                    const bottom = Math.max(window.innerHeight - 24, 0)
                    const targetLeft = clamp(
                        state.left,
                        16,
                        Math.max(window.innerWidth - card.offsetWidth - 16, 16)
                    )

                    state.left = targetLeft
                    state.top = bottom
                    state.scale = 0.1
                    state.angle = 0
                    card.style.left = `${targetLeft}px`
                    card.style.top = `${bottom}px`
                    card.style.opacity = '0.35'
                    applyTransform(card, state)

                    const handleTransitionEnd = event => {
                        if (event.propertyName === 'transform') {
                            card.removeEventListener('transitionend', handleTransitionEnd)
                            card.remove()
                        }
                    }

                    card.addEventListener('transitionend', handleTransitionEnd)
                }

                if (state.maximized) {
                    restoreFromMaximize(card, state)
                    requestAnimationFrame(() => {
                        requestAnimationFrame(runMinimize)
                    })
                    return
                }

                runMinimize()
            }

            function toggleMaximize(card) {
                const state = cardStates.get(card)
                if (!state || state.closing) return

                if (state.maximized) {
                    restoreFromMaximize(card, state)
                } else {
                    maximizeCard(card, state)
                }
            }

            function maximizeCard(card, state) {
                if (activeMaximizedCard && activeMaximizedCard !== card) {
                    const activeState = cardStates.get(activeMaximizedCard)
                    if (activeState) {
                        restoreFromMaximize(activeMaximizedCard, activeState)
                    }
                }

                state.beforeMaximize = {
                    left: state.left,
                    top: state.top,
                    scale: state.scale ?? 1,
                    angle: state.angle ?? 0,
                    width: card.offsetWidth,
                    height: card.offsetHeight,
                    inlinePosition: card.style.position
                }

                card.classList.add('maximized')
                card.style.position = 'fixed'
                card.style.left = '0px'
                card.style.top = '0px'
                card.style.width = '100vw'
                card.style.height = '100dvh'
                card.style.borderRadius = '0'

                state.left = 0
                state.top = 0
                state.scale = 1
                state.angle = 0
                applyTransform(card, state)
                activeMaximizedCard = card
                bringToFront(card)
                state.maximized = true
                updateBodyMaximizedState()
            }

            function restoreFromMaximize(card, state) {
                const previous = state.beforeMaximize
                if (!previous) return

                card.classList.remove('maximized')
                card.style.position = previous.inlinePosition || 'absolute'
                card.style.left = `${previous.left}px`
                card.style.top = `${previous.top}px`
                card.style.width = `${previous.width}px`
                card.style.height = `${previous.height}px`
                card.style.borderRadius = '12px'

                state.left = previous.left
                state.top = previous.top
                state.scale = previous.scale ?? 1
                state.angle = previous.angle ?? state.angle ?? 0
                applyTransform(card, state)
                state.maximized = false
                if (activeMaximizedCard === card) {
                    activeMaximizedCard = null
                    updateBodyMaximizedState()
                }
                bringToFront(card)
                setTimeout(() => {
                    if (!state.maximized) {
                        card.style.width = ''
                        card.style.height = ''
                        card.style.borderRadius = ''
                        if (previous.inlinePosition) {
                            card.style.position = previous.inlinePosition
                        } else {
                            card.style.position = ''
                        }
                        state.beforeMaximize = null
                    }
                }, 360)
            }

            function startDrag(event, card) {
                const control = event.target.closest('.control')
                if (control) return

                const state = cardStates.get(card)
                if (!state || state.closing || state.maximized) return

                // 鼠标拖拽使用 rAF 节流,避免频繁触发布局计算
                event.preventDefault()
                bringToFront(card)

                const header = card.querySelector('.card-header')
                card.classList.add('dragging')
                header.classList.add('dragging')

                state.dragging = true
                state.dragOffsetX = event.clientX - state.left
                state.dragOffsetY = event.clientY - state.top

                let dragFrame = null
                let pendingLeft = state.left
                let pendingTop = state.top

                const commitDrag = () => {
                    dragFrame = null
                    const maxLeft = Math.max(window.innerWidth - card.offsetWidth, 0)
                    const maxTop = Math.max(window.innerHeight - card.offsetHeight, 0)
                    state.left = clamp(pendingLeft, -card.offsetWidth * 0.4, maxLeft)
                    state.top = clamp(pendingTop, -card.offsetHeight * 0.4, maxTop)
                    card.style.left = `${state.left}px`
                    card.style.top = `${state.top}px`
                }

                const handlePointerMove = moveEvent => {
                    if (!state.dragging) return

                    pendingLeft = moveEvent.clientX - state.dragOffsetX
                    pendingTop = moveEvent.clientY - state.dragOffsetY
                    if (dragFrame === null) {
                        dragFrame = requestAnimationFrame(commitDrag)
                    }
                }

                const handlePointerUp = () => {
                    state.dragging = false
                    card.classList.remove('dragging')
                    header.classList.remove('dragging')
                    if (dragFrame !== null) {
                        cancelAnimationFrame(dragFrame)
                        commitDrag()
                    }
                    document.removeEventListener('pointermove', handlePointerMove)
                    document.removeEventListener('pointerup', handlePointerUp)
                }

                document.addEventListener('pointermove', handlePointerMove)
                document.addEventListener('pointerup', handlePointerUp)
            }

            function createCard() {
                const card = document.createElement('div')
                card.className = 'card'

                const color = randomFrom(colors)
                const angleRange = isMobile ? 6 : 10
                const angle = (Math.random() - 0.5) * angleRange
                const entryScale = isMobile ? 0.8 : 0.65
                const cardWidth = isMobile ? 180 : 220
                const cardHeight = isMobile ? 130 : 140
                const horizontalMargin = isMobile ? 12 : 16
                const verticalMargin = isMobile ? 12 : 20
                const left =
                    horizontalMargin +
                    Math.random() *
                        Math.max(window.innerWidth - cardWidth - horizontalMargin * 2, 0)
                const top =
                    verticalMargin +
                    Math.random() *
                        Math.max(window.innerHeight - cardHeight - verticalMargin * 2, 0)

                card.style.background = color
                card.style.left = `${left}px`
                card.style.top = `${top}px`
                card.style.opacity = '0'
                if (activeMaximizedCard && zIndexCursor >= MAXIMIZED_LAYER - 2) {
                    zIndexCursor = MAXIMIZED_LAYER - 2
                }
                card.style.zIndex = ++zIndexCursor

                card.innerHTML = `
                    <div class="card-header">
                        <div class="window-controls">
                            <button class="control close" type="button" aria-label="关闭"></button>
                            <button class="control minimize" type="button" aria-label="最小化"></button>
                            <button class="control maximize" type="button" aria-label="最大化"></button>
                        </div>
                        <div class="card-title">温馨提示</div>
                    </div>
                    <div class="card-body">${randomFrom(messages)}</div>
                `

                const state = {
                    angle,
                    scale: entryScale,
                    left,
                    top,
                    maximized: false,
                    closing: false
                }

                cardStates.set(card, state)
                applyTransform(card, state)
                board.appendChild(card)

                requestAnimationFrame(() => {
                    requestAnimationFrame(() => {
                        state.scale = 1
                        applyTransform(card, state)
                        card.style.opacity = '1'
                    })
                })

                if (board.children.length > maxCards) {
                    const oldest = board.firstElementChild
                    if (oldest && oldest !== card) {
                        oldest.remove()
                    }
                }
            }

            for (let i = 0; i < initialCardCount; i++) {
                setTimeout(createCard, i * (isMobile ? 60 : 40))
            }

            scheduleNextSpawn()

            document.addEventListener('visibilitychange', () => {
                if (!document.hidden) {
                    scheduleNextSpawn()
                }
            })

            if (typeof pointerMediaQuery.addEventListener === 'function') {
                pointerMediaQuery.addEventListener('change', syncMobileMode)
            } else if (typeof pointerMediaQuery.addListener === 'function') {
                pointerMediaQuery.addListener(syncMobileMode)
            }

            window.addEventListener('resize', syncMobileMode)
        </script>
    </body>
</html>
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
OωO
取消