加载中...
返回

读书摘抄站点构建(页面篇|之一)

终于下定决心构建一个属于自己的读书摘抄站点了!

此前的读书摘抄一直都是写在一个Markdown文件里,然后通过OneDrive同步。这种原始的办法问题不大,但很低效,也谈不上视觉效果;面对偶发的要把某些极好的片段分享给好友的需求更是无能为力,只能截个图勉强作数。

从现在开始呢,我希望一步步构建一下属于自己的读书摘抄站点;此前买的好几个月的腾讯云服务器一直闲置,近期可能就有他的用武之地了。

开工大吉! ☀️

总体规划

这次要做的肯定是一个动态网站(In fact, 人生中第一个动态网站 /(ㄒoㄒ)/~~),大概的实施要点是:

  • 前后端分离(Maybe)
  • 不设置账号了,因为使用范围比较私人;发布文章的接口是暴露的,但是发布摘抄之前要进行身份认证
  • 加密压缩存储(看后期技术能力)
  • 数据库定期备份
  • HTTPS
  • 实现分享功能
  • 前端尽量适应多设备
  • 动态加载+加载时的页面动画

已经物色好了后端开发的框架: drogonframework/drogon: Drogon: A C++14/17/20 based HTTP web application framework running on Linux/macOS/Unix/Windows (github.com) ,感觉用C++开发后端非常酷炫,本身需要实现的功能也不是很多 ,大有可为! 😼

Step01 主体框架

在我的构思中,读书摘抄页面的主体应该是一个很简单的结构:

  • 摘抄列表类似于我个人博客的文章列表就行;
  • 点击某个文段之后,触发缩放效果,文段成为页面中心,同时背景虚化。

正在我犹豫迷茫的时候,发现了 Steven 大佬的 【CSS】App Store 卡片展开效果 视频!Amazing!

我很快决定页面主体就基于他的这个视频了。下面的内容假设你已经实现了Steven在视频中介绍的所有效果。

我照着视频实现的总的代码为:

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <title>Test</title>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Long+Cang&family=Ma+Shan+Zheng&family=Noto+Sans+SC:wght@300&family=Noto+Serif+SC:wght@300&family=Zhi+Mang+Xing&display=swap" rel="stylesheet">
        <style>
            :root {
                font-size: 15px;
                font-family: 'Zhi Mang Xing', cursive;
                --body-width: 100%;
                --card-width: 420px;
                --card-height: 280px;
                --img-height: 226px;
                --img-height-expended: 320px;
                background-color: #333;
            }
            body {
                width: var(--body-width);
                background-color: #eee;
                margin: auto;
                display: flex;
                flex-direction: column;
                align-items: center;
                min-height: 100vh;
                padding: 1rem 0;
            }
            body.noscroll {
                overflow: hidden;
            }
            .card {
                width: var(--card-width);
                height: var(--card-height);
                background-color: #fff;
                border-radius: 1rem;
                box-shadow: 0 .2rem 2rem rgba(0, 0, 0, .1);
                margin: 1rem 0;
                transition: .3s all cubic-bezier(0, 1, 0.95, 1.05);
            }
            .card img {
                display: block;
                width: 100%;
                height: var(--img-height);
                object-fit: cover;
                border-top-left-radius: 1rem;
                border-top-right-radius: 1rem;
            }
            .card h4 {
                margin: 0;
                font-size: 1.5rem;
                font-weight: bold;
                padding: .8rem 1.2rem;
                background-color: #fff;
                line-height: 2rem;
                letter-spacing: -.5px;
                padding-bottom: 0;
            }
            .card .content-wrapper {
                height: 0;
                overflow: hidden;
                transition: .3s all ease-out;
                opacity: .8;
            }
            .card .content-wrapper .content {
                padding: 0 1.2rem;
                background-color: #fff;
                overflow: auto;
            }
            .card p {
                font-size: 1.2rem;
                line-height: 1.5rem;

            }

            /* active classes */
            .card.active {
                transform: translateY(var(--data-offset-top)) scale(calc(480/420));
                transform-origin: 50% 0;
                border-radius: 0;
            }
            .card.active h4 {
                padding-bottom: .8rem;
            }
            .card.active img {
                border-top-left-radius: 0;
                border-top-right-radius: 0;
                height: var(--img-height-expended);
            }
            .card.active .content-wrapper {
                height: 100vh;
                transition: .3s all ease-in;
                opacity: 1;
            }


            ::-webkit-scrollbar {
                width: 20px;
            }

            ::-webkit-scrollbar-track {
                background-color: transparent;
            }

            ::-webkit-scrollbar-thumb {
                background-color: #d6dee1;
                border-radius: 20px;
                border: 6px solid transparent;
                background-clip: content-box;
            }

            ::-webkit-scrollbar-thumb:hover {
                background-color: #a8bbbf;
            }

        </style>

        <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
        <script type="text/javascript">
            window.onload = function() {
                $('.card').on('click', function (e) {
                    let card = $(e.currentTarget);
                    let card_offset_scrolltop = $(card).offset().top - $(window).scrollTop();

                    $(card).css('--data-offset-top', card_offset_scrolltop * -1 + 'px');

                    $(card).toggleClass('active');

                    let height = $(window).height();
                    let ratio = 480/420;
                    height -= $(card).find('img').outerHeight() * ratio;
                    height -= $(card).find('h4').outerHeight() * ratio;
                    height /= ratio;

                    $(card).find('.content').css('height', height);

                    if ($(card).hasClass('active')) {
                        $('body').addClass('noscroll');
                    } else {
                        $('body').removeClass('noscroll');
                    }
                });
            }
        </script>
    </head>

    <body>
        <div class="card">
            <img src="https://source.unsplash.com/900x600/?nature,water,1" />
            <h4>Title1</h4>
            <div class="content-wrapper">
                <div class="content">
                    <p></p>
                    <p></p>
                </div>
            </div>
        </div>

        <div class="card">
            <img src="https://source.unsplash.com/900x600/?nature,water,2" />
            <h4>Title2</h4>
            <div class="content-wrapper">
                <div class="content">
                    <p></p>
                    <p></p>
                </div>
            </div>
        </div>

        <div class="card">
            <img src="https://source.unsplash.com/900x600/?nature,water,3" />
            <h4>Title3</h4>
            <div class="content-wrapper">
                <div class="content">
                    <p></p>
                    <p></p>
                </div>
            </div>
        </div>

        <div class="card">
            <img src="https://source.unsplash.com/900x600/?nature,water,4" />
            <h4>Title4</h4>
            <div class="content-wrapper">
                <div class="content">
                    <p></p>
                    <p></p>
                </div>
            </div>
        </div>
    </body>
</html>

Step02 移除图片&样式调整

Steven大佬做出来的效果,对 img 的操作占了比较大的部分;不过我的需求是文字摘抄,并不需要这么多的图片,因此决定将这些图片移除掉。同时,把CSS中关于图片的样式也全部移除。

此外,在demo中限定了 --card-width: 420px ,这是因为作者假设了使用场景是小屏幕;我的使用场景不是小屏幕,因此将这个数值改大一些: --card-width: 80%

现有的布局结构是一个 card 下有一个标题 h4 和内容部分 content-wrapper ,我决定将他们全部保留,不过移除了图片之后,样式变得有点奇怪,需要进行一些调整。

  • 卡片高度要缩小,因为已经没有图片了;
  • 内容展示的时候不能展示完全,只呈现出三行,超过部分用省略号替代;同时,点开内容之后要把这些限制去掉;
  • 保留圆角(调了蛮久才搞出来,感觉最终的代码并不是很简洁);
  • 滚动条美化(老技能了)。

调整之后的CSS变为:

:root {
    font-size: 15px;
    font-family: 'Zhi Mang Xing', cursive;
    --body-width: 100%;
    --card-width: 80%;
    --card-height: 150px;
    background-color: #333;
}
::-webkit-scrollbar {
    width: 20px;
    background-color: #eee;
}
::-webkit-scrollbar-track {
    background-color: transparent;
}
::-webkit-scrollbar-thumb {
    background-color: #d6dee1;
    border-radius: 20px;
    border: 6px solid transparent;
    background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
    background-color: #a8bbbf;
}

body {
    width: var(--body-width);
    background-color: #eee;
    margin: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    padding: 1rem 0;
}
body.noscroll {
    overflow: hidden;
}

.card {
    width: var(--card-width);
    height: var(--card-height);
    background-color: #fff;
    border-radius: 1rem;
    box-shadow: 0 .2rem 2rem rgba(0, 0, 0, .1);
    margin: 1rem 0;
    transition: .3s all cubic-bezier(0, 1, 0.95, 1.05);
}
.card h4 {
    margin: 0;
    font-size: 1.8rem;
    font-weight: bold;
    padding: .8rem 1.2rem;
    background-color: #fff;
    line-height: 2rem;
    letter-spacing: -.5px;
    padding-bottom: 0;
    border-radius: 1rem;
}
.card .content-wrapper {
    transition: .3s all ease-out;
    opacity: .8;
}
.card .content-wrapper .content {
    padding: 0 1.2rem;
    background-color: #fff;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    border-radius: 1rem;
}
.card p {
    font-size: 1.5rem;
    line-height: 2rem;
}

/* active classes */
.card.active {
    transform: translateY(var(--data-offset-top)) scale(calc(480/420));
    transform-origin: 50% 0;
    border-radius: 0;
    z-index: 99;
}
.card.active h4 {
    padding-bottom: .8rem;
}
.card.active .content-wrapper {
    height: 100vh;
    transition: .3s all ease-in;
    opacity: 1;
}
.card.active .content-wrapper .content {
    display: block;
    height: 80%;
}

完成了以上的微调之后,需要进行细节部分的大改动了,我将分为几个细节来进行处理。

滚动条美化

滚动条美化是此前对个人博客进行优化时接触过的东西,总体来说就是使用CSS一系列选择器来选中滚动条的对应部分。在上面的CSS中,我将滚动条背景颜色设置为了 #eee ,即正常情况下的背景颜色,而这导致了一个问题:当我点开一个具体的摘抄,其中文字内容需要进行滚动时,滚动条的颜色跟卡片颜色有点不协调,很出戏。

优化的思路很简单,使用一个CSS变量来设置颜色;配合此前已经有了的jQuery对点击事件的判断,改变这个背景颜色即可。

既然已经提到了使用变量,我就 统一将各个背景颜色全部使用变量进行控制 ,作出的相应修改在此不表。

使用 --scrollbar-bg-color: #eee; 作为滚动条默认背景颜色(与 body 背景颜色一致),在jQuery判断点击的那部分代码中加上对这个颜色变量的控制:

if ($(card).hasClass('active')) {
    $('body').addClass('noscroll');
    $(':root').css('--scrollbar-bg-color', '#fff');
} else {
    $('body').removeClass('noscroll');
    $(':root').css('--scrollbar-bg-color', '#eee');
}

搞定!

卡片缩放美化

Steven大佬的卡片缩放,由于有一张尺寸较大的图,使得原本“顶天立地”的缩放效果就显得很出色。在我的使用场景中,已经将图片去掉了,如果继续“顶天立地”下去,就会造成卡片下端的大量空白。

毫无疑问,这样的效果是很不好的,即便我后续将要对卡片的结构进行一些调整,读书摘抄这种有限的内容仍然很难铺满一个浏览器窗口。

我的理想放大效果是:内容基本位于窗口中心,上下左右都留出一些空白。

基于原有的代码,要实现内容居中较为容易。原本的代码会计算 --data-offset-top ,然后对卡片进行一个 translateY 的操作,将卡片移到页面顶部;我不希望卡片“头顶天”,因此将这个 --data-offset-top 的值修改一下即可。

具体的操作是:先获取 $(window).height() 即窗口高度,然后把 --data-offset-top 加上窗口高度的一定比例。

实现出来的效果类似于为卡片加上了 margin-top

而为了不让卡片“脚着地”,只需要把卡片的 height 减小即可。在原本的代码中,这个高度通过 .card.active .content-wrapper 来控制,我将其改为一个变量 var(--content-wrapper-height-expended) ,在卡片展开的时候,设置这个变量的值为窗口高度的一定比例。

同时,为了不让 contentcontent-wrapper 撑大,要为这个元素设置 height ,使它至多跟父元素同样高。

实现出来的效果类似于为卡片加上了 margin-bottom

对卡片展开的比例进行调整之后,大小是合理了,不过出现了新的问题:

这个问题实际上花了我一段时间,最后的解决方案是:新增一个全屏的“遮罩”元素,背景颜色设置好透明度;遮罩在正常情况下 z-index 较小且 displaynone ,卡片展开之后把 z-index 抬升到 展开的卡片之下、未展开的卡片之上 的位置,然后 display 呈现出来。

注意这边展开之后看起来右边没有间隙,实际上是GIF录制过程中的范围有限所致。

点击事件优化

原本的卡片缩放实现方案是:在 click 事件中进行 toggleClass 的操作来添加/移除 active 这个类,逻辑很简单,但并不完全实用。

其中很关键的一点是:当我选中一段文字时,也算作触发 click 事件;而毫无疑问,当读者选中某些文字的时候希望进行的是复制之类的操作,假如触发了 click 事件,卡片就会缩回去,选中的文字也就丢失了。

修复它的做法比较简单:在 click 事件中判断一下当前是否选中了文本,若存在,则说明卡片缩放的步骤不应该执行,否则再按照原有的流程执行卡片缩放。

let txt = '';
if (window.getSelection) {
    txt = window.getSelection().toString();
} else {
    txt = document.selection.createRange().text;
}
if (txt == '') {
    // 原有的卡片缩放步骤
}

缩放事件优化

卡片展开之后缩回,存在一个不到位的细节是:内容没有回到首行。

这使得体验很割裂,尤其当我添加了 书籍名称发布日期 等元素之后,出现了重叠。

解决的办法是:在卡片回弹的函数中,选中 content-wrapper 并将使用 scrollTop(0) 将它的偏移量重置为 0

Step03 添加伸缩侧边栏

在一个读书摘抄的站点中,根据书籍进行分类应该是最基本的需求。

我的构思是添加一个可伸缩的侧边栏,侧边栏上呈现出所有的书籍,点击时进行异步通信,后台根据书籍名称筛选读书摘抄。

实现这个侧边栏需要对页面结构进行如下的调整:

  • 在原有的若干卡片外层再添加一个 div 包裹起来,实现完整性;
  • 添加一个 position: fixed 的元素,放置在页面右侧;
  • 添加一个可点击的元素,点击之后触发侧边栏伸缩。

侧边栏元素的添加比较简单:假定侧边栏的宽度是 10% ,由于使用了 fixed 属性使得宽度比例的计算按照外层窗体进行,因此 body 要为侧边栏让出 10% 的空间,使用 padding-right: 10% 实现;

按照相同办法添加一个 fixed 元素充当按钮,注意计算按钮位置即可。按钮点击时判断侧边栏是否有宽度,若有,说明应该将侧边栏收缩,修改侧边栏宽度和 bodypadding-right 为零;若侧边栏无宽度,说明应该展开,将二者改回原有的 10% 即可。

侧边栏中要放置书籍名称,使用类似于主体部分的卡片的布局进行实现;书籍名称的文字部分使用 writing-mode: vertical-rl;text-align: center; 来使文字居中垂直排列。

Step04 侧边栏进一步完善

原本以为上一步添加的侧边栏就很够用了,可惜需求总是随着人类的异想天开而不断增加。

这一步骤主要完善两点功能:

  • 站点需要对用户的登录状态进行判定,因此决定将用户的登录状态和登录接口放置在侧边栏上;
  • 原有的侧边栏卡片有一些小问题,修复。

第一步相当于在侧边栏上又新增一个元素了。在若干个书籍卡片 之前 新增一个 <div id="authentication"> ,我的设想是 用户未登录之前显示登录入口、用户登录之后显示一个头像图片 ,当然,目前肯定是不支持更换头像的,因此图片按我的喜好来 😸

未登录状态就使用 未登录 三个字来表示即可,用一个 <a> 标签把它做成链接;设置这个标签的 position: relative ,然后使用 ::before 伪元素来实现鼠标 hover 的效果:

登录状态使用一张正方形图片,然后 border-radius: 100% 改为圆形,不表。注意正常情况下图片总是能吸引用户点击,因此我希望将这个图片作为发布读书摘抄的入口。

接着是一个极其重要的改动:侧边栏滚动的启用。此前的布局看起来没有什么毛病,但当书籍多起来之后,就会:

卡片被压缩了!这不是我的本意,我希望书籍多起来之后侧边栏可以向下滚动,不要挤在这么一个小空间里。

修改的办法是:

  1. 在卡片样式中(即 #right-sidebar .book-card 选择器)加上 flex-shrink: 0;
  2. 侧边栏样式中(即 #right-sidebar 选择器)加上 overflow: scroll;
  3. 侧边栏滚动条隐藏(使用 #right-sidebar::-webkit-scrollbar 选择器设置 display: none )。

至此先告一段落啦!

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy