神重現,去照片飄新。
今天給大家帶來一個老照片修復的輕量級版本。輕量級之前發了一個版本修復,雖然效果很好,但是運行太卡了,16G以下顯存基本用不了。看一下修復效果。
這個工作流加了一個去Al化的節點,也就是會把照片做的比較真實。看一下整體效果,不管是質感還是效果都很不錯。并且這個工作流對電腦要求比較低,基本6G以上的顯卡就可以跑。
簡單介紹一下工作流的使用。首先使用也做的很簡單,這里輸入中文提示詞,然后把照片給拖上來。注意因為是輕量版的,這個流無法修復劃痕。如果大家有PS基礎,建議還是先用PS把一些比較明顯的劃痕修復,之后再用這工作流來處理。如果有劃痕不會用的小伙伴,就建議還是用威力加強V2版本。到時候這兩個版本我都會貼在簡介里邊。
描述一下男孩短發黑色背景,直接出圖看一下效果。經過差不多一分鐘左右等待,因為我是第一次運行,所以加載一些模型用了一些時間。2分鐘,第一次生成的AI化還是比較嚴重的。第二次生成,看這皮膚質感,就把Al畫能給去除一些,看起來更像真實的照片。這里提示詞也是根據提示詞再來一張試一下。
換一個美女的描述,比如女孩20歲,黑色這是長款的黑色頭發,簡單背景,配一個白色綠色。第二次運行基本也就1分鐘,一分鐘就生成了整套流程了,因為模型已經不需要加載了。看一下效果。這是原來照片,這是第一次生成的,皮膚質感還是比較差的。經過磨皮修復之后,看一下效果,是不是直接很完美的效果。
輕量化的工作流簡單說一下,需要的大模型Lola,還有VE,包括control模型,我都會貼到資料區,到時候大家下載一下,替換一下位置就行。
這個工作流和之前工作流最大的對比,之前工作流效果挺好,并且帶自動劃痕修復,但是得16G以上的顯存才能跑比較慢。如果大家需要之前的工作流,到時候也貼在評論區,大家看一下,效果也是很好,但是它并且能自動修復劃痕,大家也可以考慮一下,電腦配置高的可以自行選擇。
我現在工作流量基本做了20多個,基本都是常用工作流,并且都很實用,有需要的可以下載一下,謝謝大家支持。
還記得那是周五的一個晚上,那天正在寢室用筆記本玩賽博朋克2077 happy 中,突然女朋友給我發了一個小紅書的超鏈接,我像往常一樣無視發送的內容直奔鏈接準備看完趕緊應付幾句話繼續游戲
打開鏈接以為跟其他類似產品一樣都會讓你直接選擇下載 App,沒想到小紅書的 PC 端驚到我了,首頁出乎意料的好看
那時候年輕的我還不知道瀑布流是什么,只是覺得這個布局配合卡片的一些點擊交互很有感覺,嘖嘖嘖,我一個大男的看一些女性的推薦都被吸引到了,不過吸引我的不是內容,而是它的布局和交互
這段時間全在忙學校實訓不寫文章手都癢了,今天就來研究一下小紅書的瀑布流布局
其實掘金上已經有很多篇文章都講過小紅書瀑布流的實現,但是如果仔細觀察小紅書使用的是瀑布流虛擬列表,關于這一點我幾乎沒有見到一篇文章有著重講解的,都是簡單講講瀑布流實現了事
但我認為小紅書首頁布局最大的亮點就在瀑布流和虛擬列表的結合,所以這段時間就來深入講解一下這塊實現的原理
本文屬于瀑布流虛擬列表的前置篇 ,因為瀑布流虛擬列表牽扯的概念比較多所以需要拆分講解,這次就先科普一下基礎瀑布流的實現,用 Vue3 + TS 封裝一個瀑布流組件
上手寫代碼之前我們簡單介紹一下瀑布流布局的實現思路
瀑布流布局的應用場景主要在于圖片或再搭配一點文字形成單獨的一張卡片,由于圖片尺寸不同會造成卡片大小不一,按照常規的盒子布局思路無外乎只有兩種:
獨占一行不用講肯定不符合我們想要實現的效果,而緊挨著排列由于卡片大小問題會出現這樣的情況:
當前行高度最大的盒子決定了下一行盒子擺布的起始位置,如果卡片之間高度差距過大就會出現大量的留白
很顯然常規布局并不能很好的利用空間,給人帶來的視覺效果也較為混亂
而使用瀑布流布局很好的解決了這一點,我們打破常規布局的方案,使用定位或者位移來控制每張卡片的位置,最大化彌補卡片之間的留白情況
所以瀑布流布局的核心實現思想:
如果按照這樣的思想我們改造上面圖中卡片擺放的順序:
①②③ 按照順序緊挨著排布
④ 準備排布時找到最小高度列是第三列,所以會排布在 ③ 下面
⑤ 準備排布時找到最小高度列是第二列,所以會排布在 ② 下面
⑥ 準備排布時找到最小高度列是第一列,所以會排布在 ① 下面
可以看到這種布局方式解決了第一行和第二行中間留白的情況,布局時卡片再帶一點間距視覺效果會更好,同理剩下圖片卡片擺放也是按照這樣的思路
關于圖片相關的數據我就不自己準備了,有現成的數據接口那當然拿來用啦
我們直接使用小紅書的數據接口把數據粘下來保存到本地即可:
不過稍微動點腦子就知道像這樣的網站針對于圖片一定會加上防盜鏈的,所以我也就不費勁繞開處理了,主要是要提取它的尺寸信息,至于圖片就先隨便給個顏色占位了
在開始寫代碼之前還有這一個問題需要討論:一般情況下瀑布流布局后端返回的數據不止有圖片的鏈接還有圖片的寬高信息(比如小紅書中針對于單個卡片就有 width 和 height 字段)
有了這些信息前端使用時無需再單獨獲取 img DOM 就能夠快速計算卡片縮放后的寬高以及后續的位置信息
但如果后端沒有返回這些信息只給了圖片鏈接那就只能全部交給前端來處理,因為圖片尺寸信息在瀑布流實現中是必須要獲取到的,這里就需要用到圖片預加載技術
簡單描述一下就先提前訪問圖片鏈接進行加載操作但不展示在視圖上,后續使用該鏈接后圖片會從緩存中加載而不是向服務器請求,因此被稱之為預加載
而在瀑布流當中我們就是提前訪問圖片鏈接來獲取其尺寸信息,我們封裝為一個工具函數:
function preLoadImage(link) {
return new Promise((resolve, reject)=> {
const img=new Image();
img.src=link;
img.onload=()=> {
// load 事件代表圖片已經加載完畢,通過該回調才訪問到圖片真正的尺寸信息
resolve({ width: img.width, height: img.height });
};
img.onerror=(err)=> {
reject(err);
};
});
}
所以假設如果有很多圖片,那么就必須要保證所有圖片全部加載完畢獲取到尺寸信息后才能開始瀑布流布局流程,如果一旦有一張圖片加載失敗就會導致瀑布流布局出現問題
好處就是用戶看到的圖片是直接從緩存進行加載的速度很快,壞處就是剛開始等待所有圖片加載會很慢
而如果后端返回圖片尺寸信息我們就無需考慮圖片是否加載完成,直接根據其尺寸信息先進行布局,之后利用圖片懶加載技術即可,所以真實業務場景肯定還是后端攜帶信息更好
前面鋪墊了這么多終于要開始寫代碼了,我們還是按照以前的老規矩,先看看整個 DOM 結構是什么樣:
其實和虛擬列表差不多,只需要一個容器 container、列表 list 以及數據項 item
只不過封裝組件后 item 后續會使用 v-for 遍歷出來,同時可以定義插槽讓父組件展示圖片,這些后續再說
<div class="fs-waterfall-container">
<div class="fs-waterfall-list">
<div class="fs-waterfall-item"></div>
</div>
</div>
container 作為整個瀑布流的容器它是需要展示滾動條的, list 作為 item 的容器可以開啟相對定位,而 item 開啟絕對定位,由于我會通過 translate 來控制每張卡片的位置,所以每張卡片定位統一放到左上角即可:
.fs-waterfall {
&-container {
width: 100%;
height: 100%;
overflow-y: scroll; // 注意需要提前設置展示滾動條,如果等數據展示再出現滾動造成計算偏差
overflow-x: hidden;
}
&-list {
width: 100%;
position: relative;
}
&-item {
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
}
}
既然封裝成組件必然少不了 props 傳遞來進行配置,針對于瀑布流其實只需要這幾個屬性
而對于單個數據項我們只需要圖片的信息,其他的信息都不重要
小紅書的瀑布流還有 title 以及 author 信息影響整個卡片的高度,這塊放到最后實現,我們先只展示圖片
export interface IWaterFallProps {
gap: number; // 卡片間隔
column: number; // 瀑布流列數
bottom: number; // 距底距離(觸底加載更多)
pageSize: number;
request: (page: number, pageSize: number)=> Promise<ICardItem[]>;
}
export interface ICardItem {
id: string | number;
url: string;
width: number;
height: number;
[key: string]: any;
}
// 單個卡片計算的位置信息,設置樣式
export interface ICardPos {
width: number;
height: number;
x: number;
y: number;
}
接下來我們定義組件內部狀態:
const containerRef=ref<HTMLDivElement | null>(null); // 綁定 template 上的 container,需要容器寬度
const state=reactive({
isFinish: false, // 判斷是否已經沒有數據,后續不再發送請求
page: 1,
cardWidth: 0, // // 容器內卡片寬度
cardList: [] as ICardItem[], // 卡片數據源
cardPos: [] as ICardPos[], // 卡片擺放位置信息
cloumnHeight: new Array(props.column).fill(0) as number[], // 存儲每列的高度,進行初始化操作
});
初始化操作只有兩個工作:計算卡片寬度 、發送請求獲取數據
初始化時最重要的就是先計算出該瀑布流布局中卡片的寬度是多少,即 state.cardWidth,每列的寬度都是固定的
其實計算方法很簡單,直接來看下圖就知道怎么計算了:
const containerWidth=containerRef.value.clientWidth;
state.cardWidth=(containerWidth - props.gap * (props.column - 1)) / props.column;
注意使用 clientWidth 作為容器的寬度(clientWidth 不會計算滾動條的寬度)
之后就需要封裝一個發送請求獲取數據的函數了,需要注意的就是獲取數據后要判斷是否為空來決定后續是否還發送請求:
const getCardList=async (page: number, pageSize: number)=> {
if (state.isFinish) return;
const list=await props.request(page, pageSize);
state.page++;
if (!list.length) {
state.isFinish=true;
return;
}
state.cardList=[...state.cardList, ...list];
computedCardPos(list); // key:根據請求的數據計算卡片位置
};
我們整合到 init 方法中,在 onMounted 里進行調用:
const init=()=> {
if (containerRef.value) {
const containerWidth=containerRef.value.clientWidth;
state.cardWidth=(containerWidth - props.gap * (props.column - 1)) / props.column;
getCardList(state.page, props.pageSize);
}
};
onMounted(()=> {
init();
});
下面就到瀑布流核心實現環節了,我們在實現思路中談到每當后續卡片進行布局時都需要計算最小列高度將其擺放至下面,很顯然計算最小列高度方法是被頻繁使用的,關鍵在于獲取最小列以及最小列高度,這里可以直接使用計算屬性實現:
因為還要獲取下標,所以沒法直接 Math.min 了,直接遍歷比較出最小值即可:
const minColumn=computed(()=> {
let minIndex=-1,
minHeight=Infinity;
state.columnHeight.forEach((item, index)=> {
if (item < minHeight) {
minHeight=item;
minIndex=index;
}
});
return {
minIndex,
minHeight,
};
});
在上面一小節的發送請求函數中的末尾有一個 computedCardPos 方法我們沒有實現,它就是每當獲取到新的數據后計算新數據卡片的位置信息,將其保存至 state.cardPos 中
我們來看它的實現步驟:
下面就直接粘代碼了:
const computedCardPos=(list: ICardItem[])=> {
list.forEach((item, index)=> {
const cardHeight=Math.floor((item.height * state.cardWidth) / item.width);
if (index < props.column) {
state.cardPos.push({
width: state.cardWidth,
height: cardHeight,
x: index % props.column !==0 ? index * (state.cardWidth + props.gap) : 0,
y: 0,
});
state.columnHeight[index]=cardHeight + props.gap;
} else {
const { minIndex, minHeight }=minColumn.value;
state.cardPos.push({
width: state.cardWidth,
height: cardHeight,
x: minIndex % props.column !==0 ? minIndex * (state.cardWidth + props.gap) : 0,
y: minHeight,
});
state.columnHeight[minIndex] +=cardHeight + props.gap;
}
});
};
這里的計算可能會有一些疑問,簡單做下解答吧:
有了 state.cardPos 位置信息就可以修改 template 模板了,我們遍歷數據設置位置樣式即可:
<template>
<div class="fs-waterfall-container" ref="containerRef">
<div class="fs-waterfall-list">
<div
class="fs-waterfall-item"
v-for="(item, index) in state.cardList"
:key="item.id"
:style="{
width: `${state.cardPos[index].width}px`,
height: `${state.cardPos[index].height}px`,
transform: `translate3d(${state.cardPos[index].x}px, ${state.cardPos[index].y}px, 0)`,
}"
>
<slot name="item" :item="item"></slot>
</div>
</div>
</div>
</template>
到此我們的瀑布流已經可以看到效果了,我們在父組件里使用一下看看
這里就不再解釋了,稍微寫點結構和樣式,導入最早扒來的小紅書數據,按照規定屬性傳入即可,只不過我們不能使用圖片鏈接(防盜鏈問題),就稍微寫一個帶顏色的盒子吧:
<template>
<div class="app">
<div class="container">
<fs-waterfall :bottom="20" :column="4" :gap="10" :page-size="20" :request="getData">
<template #item="{ item, index }">
<div
class="card-box"
:style="{
background: colorArr[index % (colorArr.length - 1)],
}"
>
<!-- <img :src="item.url" /> -->
</div>
</template>
</fs-waterfall>
</div>
</div>
</template>
<script setup lang="ts">
import data1 from "./config/data1.json";
import data2 from "./config/data2.json";
import FsWaterfall from "./components/FsWaterfall.vue";
import { ICardItem } from "./components/type";
const colorArr=["#409eff", "#67c23a", "#e6a23c", "#f56c6c", "#909399"];
const list1: ICardItem[]=data1.data.items.map((i)=> ({
id: i.id,
url: i.note_card.cover.url_pre,
width: i.note_card.cover.width,
height: i.note_card.cover.height,
}));
const list2: ICardItem[]=data2.data.items.map((i)=> ({
id: i.id,
url: i.note_card.cover.url_pre,
width: i.note_card.cover.width,
height: i.note_card.cover.height,
}));
const list=[...list1, ...list2];
console.log(list.length);
const getData=(page: number, pageSize: number)=> {
return new Promise<ICardItem[]>((resolve)=> {
setTimeout(()=> {
resolve(list.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize));
}, 1000);
});
};
</script>
<style scoped lang="scss">
.app {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.container {
width: 700px;
height: 600px;
border: 1px solid red;
}
.card-box {
position: relative;
width: 100%;
height: 100%;
border-radius: 10px;
}
}
</style>
效果還是不錯的,但是觸底加載更多我們還沒有實現,下一步就來實現它
這其實也很好實現,我們只需給 container 添加滾動事件即可,按照以往的判斷觸底套路,再用上我們前面封裝的獲取數據函數即可
不過需要注意兩個問題:
const state=reactive({
// ...
loading: false,
});
const getCardList=async (page: number, pageSize: number)=> {
// ...
state.loading=true;
const list=await props.request(page, pageSize);
// ...
state.loading=false;
};
const computedCardPos=(list: ICardItem[])=> {
list.forEach((item, index)=> {
// 增加另外條件,cardList <=pageSize 說明是第一次獲取數據,第一行緊挨排布
if (index < props.column && state.cardList.length <=props.pageSize) {
// ...
} else {
// ...
}
});
};
const handleScroll=rafThrottle(()=> {
const { scrollTop, clientHeight, scrollHeight }=containerRef.value!;
const bottom=scrollHeight - clientHeight - scrollTop;
if (bottom <=props.bottom) {
!state.loading && getCardList(state.page, props.pageSize);
}
});
<template>
<!-- 綁定 scroll 事件 -->
<div class="fs-waterfall-container" ref="containerRef" @scroll="handleScroll">
<!-- ... -->
</div>
</template>
嗯,效果不錯,至于 loading 蒙層以及圖片的懶加載效果我就不做了,留給大伙自行拓展吧
到此一個基礎的圖片瀑布流組件已經封裝完成了,接下來我們來深入研究一下小紅書的瀑布流
拋開小紅書中虛擬列表的實現先不談,還有一點就是展示的卡片不僅有圖片信息,還有文字信息:
這些文字信息你會發現它還是不定高的,這就比較麻煩了,無法確定單個卡片的高度會導致瀑布流布局計算出現問題
不過如果仔細分析的話,你會發現它只有兩種情況: title 文本是單行或者雙行,這點直接從 css 就可以看得出來:
后來發現還有一種情況是連 title 都沒有,但是出現的情況比較少,這點先不考慮了
不僅如此,小紅書的瀑布流還是響應式的,如果你去改變視口寬度,可能會出現一種情況:單行文本由于卡片寬度的壓縮變成了雙行
而關于 author 我發現它的高度是定死的 20px:
所以總結下來最大的兩個問題:
廢話不多說,我們先封裝一個小紅書卡片組件出來,當然只實現最基本的樣式效果,圖片依舊直接純色占位:
<template>
<div class="fs-book-card-container">
<div class="fs-book-card-image">
<!-- <img :src="props.detail.url" /> -->
</div>
<div class="fs-book-card-footer">
<div class="title">{{ props.detail.title }}</div>
<div class="author">
<div class="author-info">
<div class="avatar" />
<!-- <img :src="props.detail.avatar" class="avatar" /> -->
<span class="name">{{ props.detail.author }}</span>
</div>
<div class="like">100</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface ICardDetail {
bgColor: string;
title: string;
author: string;
imageHeight: number;
[key: string]: any;
}
const props=defineProps<{
detail: ICardDetail;
}>();
</script>
<style scoped lang="scss">
.fs-book-card {
&-container {
width: 100%;
height: 100%;
background-color: #fff;
}
&-image {
width: 100%;
height: v-bind("`${props.detail.imageHeight}px`");
border: 1px solid #eee;
border-radius: 20px;
background-color: v-bind("props.detail.bgColor");
}
&-footer {
padding: 12px;
font-size: 14px;
.title {
margin-bottom: 8px;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
color: rgba(51, 51, 51, 0.8);
}
.author {
font-size: 13px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
.author-info {
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.avatar {
margin-right: 6px;
width: 20px;
height: 20px;
border-radius: 20px;
border: 1px solid rgba(0, 0, 0, 0.08);
background-color: v-bind("props.detail.bgColor");
}
.name {
width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: rgba(51, 51, 51, 0.8);
}
}
}
}
}
</style>
</style>
emm ,大差不差吧~ 無非是沒有圖片罷了
下面就要大改之前實現的瀑布流組件了,思路很簡單,現在最大的問題就是卡片的高度不固定了,我們需要自己獲取 DOM 來計算
也就是說之前我們實現 computedCardPos 方法要分兩步了:
首先我們改造之前存儲卡片位置信息的數據結構,現在高度分為:卡片高度、卡片內圖片高度:
export interface IBookCardPos {
width: number;
imageHeight: number; // 圖片高度
cardHeight: number; // 卡片高度
x: number;
y: number;
}
在獲取到數據信息后我們增加一個計算卡片圖片高度的方法,并將其添加到記錄卡片位置信息的數組中,而卡片高度和位置信息統一為 0,等下一步 DOM 更新后獲取計算:
const computedImageHeight=(list: ICardItem[])=> {
list.forEach((item)=> {
const imageHeight=Math.floor((item.height * state.cardWidth) / item.width);
state.cardPos.push({
width: state.cardWidth,
imageHeight: imageHeight,
cardHeight: 0,
x: 0,
y: 0,
});
});
};
添加完成之后我們需要等待一次 nextTick,保證上面的位置信息 DOM 已經進行了掛載(但是還沒有渲染到界面上)
nextTick 之后我們就需要計算真正卡片的高度以及其位置了,我們獲取 list DOM 并在內部獲取其 children 遍歷獲取其高度,位置計算和之前一樣:
const listRef=ref<HTMLDivElement | null>(null);
const getCardList=async (page: number, pageSize: number)=> {
// ...
computedCardPos(list);
// ...
};
const computedCardPos=async (list: ICardItem[])=> {
computedImageHeight(list);
await nextTick();
computedRealDomPos(list);
};
const computedRealDomPos=(list: ICardItem[])=> {
const children=listRef.value!.children;
list.forEach((_, index)=> {
const nextIndex=state.preLen + index;
const cardHeight=children[nextIndex].getBoundingClientRect().height;
if (index < props.column && state.cardList.length <=props.pageSize) {
state.cardPos[nextIndex]={
...state.cardPos[nextIndex],
cardHeight: cardHeight,
x: nextIndex % props.column !==0 ? nextIndex * (state.cardWidth + props.gap) : 0,
y: 0,
};
state.columnHeight[nextIndex]=cardHeight + props.gap;
} else {
const { minIndex, minHeight }=minColumn.value;
state.cardPos[nextIndex]={
...state.cardPos[nextIndex],
cardHeight: cardHeight,
x: minIndex % props.column !==0 ? minIndex * (state.cardWidth + props.gap) : 0,
y: minHeight,
};
state.columnHeight[minIndex] +=cardHeight + props.gap;
}
});
state.preLen=state.cardPos.length;
};
注意這里的 nextIndex 計算,這是因為要考慮到觸底加載更多的情況,我們在狀態中增加了 preLen 屬性用來保存當前已經計算過的卡片位置數組長度,等觸底加載更多數據再重復走計算邏輯時它的索引就應該從 preLen 開始往后計算
同樣在 template 模板中,我們使用插槽把卡片里圖片高度拋出,正好可以讓我們封裝的小紅書卡片使用:
<template>
<div class="fs-book-waterfall-container" ref="containerRef" @scroll="handleScroll">
<!-- 獲取 list DOM ref -->
<div class="fs-book-waterfall-list" ref="listRef">
<div
class="fs-book-waterfall-item"
v-for="(item, index) in state.cardList"
:key="item.id"
:style="{
width: `${state.cardWidth}px`,
transform: `translate3d(${state.cardPos[index].x}px, ${state.cardPos[index].y}px, 0)`,
}"
>
<!-- 傳遞 imageHeight 給小紅書卡片組件 -->
<slot name="item" :item="item" :index="index" :imageHeight="state.cardPos[index].imageHeight"></slot>
</div>
</div>
</div>
</template>
這時候父組件使用瀑布流組件時就可以這樣用了:
<template>
<div class="app">
<div class="container">
<fs-book-waterfall :bottom="20" :column="4" :gap="10" :page-size="20" :request="getData">
<template #item="{ item, index, imageHeight }">
<fs-book-card
:detail="{
imageHeight,
title: item.title,
author: item.author,
bgColor: colorArr[index % (colorArr.length - 1)],
}"
/>
</template>
</fs-book-waterfall>
</div>
</div>
</template>
<script setup lang="ts">
import data1 from "./config/data1.json";
import data2 from "./config/data2.json";
import FsBookWaterfall from "./components/FsBookWaterfall.vue";
import FsBookCard from "./components/FsBookCard.vue";
import { ICardItem } from "./components/type";
const colorArr=["#409eff", "#67c23a", "#e6a23c", "#f56c6c", "#909399"];
const list1: ICardItem[]=data1.data.items.map((i)=> ({
id: i.id,
url: i.note_card.cover.url_pre,
width: i.note_card.cover.width,
height: i.note_card.cover.height,
title: i.note_card.display_title,
author: i.note_card.user.nickname,
}));
const list2: ICardItem[]=data2.data.items.map((i)=> ({
id: i.id,
url: i.note_card.cover.url_pre,
width: i.note_card.cover.width,
height: i.note_card.cover.height,
title: i.note_card.display_title,
author: i.note_card.user.nickname,
}));
const list=[...list1, ...list2];
const getData=(page: number, pageSize: number)=> {
return new Promise<ICardItem[]>((resolve)=> {
setTimeout(()=> {
resolve(list.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize));
}, 1000);
});
};
</script>
<style scoped lang="scss">
.app {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.container {
width: 700px;
height: 600px;
border: 1px solid red;
}
.box {
width: 250px;
}
}
</style>
效果還不錯,就是這樣做性能就比不上定高的實現了,畢竟現在計算位置信息都需要進行 DOM 操作
小紅書的瀑布流響應式一共實現了兩點:
第一點很好實現,我們可以監聽容器 DOM 尺寸改變后重置數據并走一遍所有的計算邏輯即可,只不過這是一個頻繁回流的過程,建議上個防抖更好:
這里監聽 DOM 尺寸變化推薦使用 ResizeObserver,我就不再封裝直接使用了,至于怎么使用看 MDN 文檔
ResizeObserver - Web API 接口參考 | MDN (mozilla.org)
// 創建監聽對象
const resizeObserver=new ResizeObserver(()=> {
handleResize();
});
// 重置計算卡片寬度以及之前所有的位置信息
const handleResize=debounce(()=> {
const containerWidth=containerRef.value!.clientWidth;
state.cardWidth=(containerWidth - props.gap * (props.column - 1)) / props.column;
state.columnHeight=new Array(props.column).fill(0);
state.cardPos=[];
state.preLen=0;
computedCardPos(state.cardList);
});
const init=()=> {
if (containerRef.value) {
//...
resizeObserver.observe(containerRef.value);
}
};
// 掛載時監聽 container 尺寸變化
onMounted(()=> {
init();
});
// 卸載取消監聽
onUnmounted(()=> {
containerRef.value && resizeObserver.unobserve(containerRef.value);
});
可以再給 item 上添加一個過渡,顯得更自然一些:
.fs-book-waterfall {
&-item {
// ...
transition: all 0.3s;
}
}
可以可以,這樣就好看多了
接下來我們看斷點響應式的實現,它的實現其實有兩種:
為了兼容我們之前的實現就使用第二種方式,我們隨便在父組件設幾個斷點然后監聽外部 container 元素的寬度修改 column 即可:
const fContainerRef=ref<HTMLDivElement | null>(null);
const column=ref(5);
const fContainerObserver=new ResizeObserver((entries)=> {
changeColumn(entries[0].target.clientWidth);
});
const changeColumn=(width: number)=> {
if (width > 960) {
column.value=5;
} else if (width >=690 && width < 960) {
column.value=4;
} else if (width >=500 && width < 690) {
column.value=3;
} else {
column.value=2;
}
};
onMounted(()=> {
fContainerRef.value && fContainerObserver.observe(fContainerRef.value);
});
onUnmounted(()=> {
fContainerRef.value && fContainerObserver.unobserve(fContainerRef.value);
});
而在瀑布流組件中我們使用 watch 監聽 column 變化,發生變化就進行回流重新計算布局:
watch(
()=> props.column,
()=> {
handleResize();
}
);
完美!這才是完整的瀑布流!
最后源碼奉上,有圖片版的瀑布流以及小紅書版的瀑布流,沒有怎么組織,但功能反正實現了:
DrssXpro/waterfall-demo: Vue3 + TS:模仿小紅書封裝瀑布流組件 (github.com):https://github.com/DrssXpro/waterfall-demo
終于把瀑布流基礎篇講完了,下一篇就直接來瀑布流虛擬列表組件了,這次實現一個完完整整的小紅書版瀑布流!
作者:討厭吃香菜
鏈接:https://juejin.cn/post/7322655035699396660
去攝影師是怎樣將照片視頻存入NAS的?
您也是這樣操作的嗎?
其實可以進一步簡化這個工作流,你甚至不需要電腦!直接就能拷貝進威聯通的NAS中。并且通過Qfiling軟件設置可以自動實現照片和視頻的分開存放。
這一切都不需要復雜的操作,只需要機器上按一鍵即可。
按此鍵即可一鍵拷貝到NAS
存入后,通過威聯通的Qfile軟件,或者電腦與NAS共享連接,攝影師可以自由選擇在手機或電腦上來編輯這些照片。
最重要的是,這些基礎的實用功能全部都是免費的!下面我來教大家如何使用。
詳細攻略
本攻略給大家實操演示,我們模擬了一個相機存儲卡文件夾,將相機存儲卡插入NAS后,會顯示如下文件夾,其中DCIM是大部分相機照片和視頻的存放文件夾,作為我們的樣例。
首先進入APP Center 下載 HBS3文件備份同步中心 。
拷貝文件之前,將插了存儲卡的讀卡器插入到威聯通NAS前置USB口即可,目前主流威聯通產品都是USB3.2 Gen2,上限能達到恐怖的10Gbps傳輸速度。
進入HBS3 點擊【服務】,接著點擊【USB一鍵復制】,可看到分為三種操作模式:智能導入、USB一鍵備份、外部存儲驅動器。
其中智能導入可以認為是一鍵傻瓜導入功能,USB一鍵備份則是可以自定義的高級導入功能。
智能導入
智能導入設置完成后,按下“”一鍵復制按鈕“”連接的U盤中的數據會全部導入NAS中。
操作步驟
首先選擇智能導入,點擊設置。
如果你什么都不設置,文件會默認存儲到Multimedia 的子文件夾Smartlmport文件中。
我們則是在File Station中新建一個【影像存儲】文件夾,用來單獨存放文件,我也建議大家這么做。
導入保存文件夾設置完成后,點擊應用。
按下NAS的一鍵復制按鈕,NAS右上角的界面會顯示導入的進度。
建議你在【系統】中設置警告音,按鍵和備份完成后會發出“滴”的聲音。(一共兩聲,第一聲開始備份,備份完畢再響一聲)
備份成功后,存儲卡中所有數據會導入到【影像存儲】文件夾下Smartlmport的子文件中,備份好數據會以備份的日期命名文件夾。
USB一鍵備份
此功能有點繞,請大家仔細閱讀體會。
【USB一鍵備份】備份模式有兩種:備份到NAS和備份到連接U盤,我們用備份到NAS就好,另一個功能是從NAS備份到USB移動硬盤使用的,切勿搞錯。
備份到NAS
【備份到NAS】有三種備份操作:增加目錄、復制和同步。
1、增加目錄
分別選擇NAS和U盤對應的文件。
左側選擇你要存放到哪個NAS中的文件夾,右邊存儲卡中直接選擇照片和視頻的文件夾DCIM。
應用后,按下一鍵復制按鈕,U盤中指定文件被復制到NAS中指定文件的子文件夾中,該文件以“復制的日期”為文件名。
2、復制
備份操作模式選擇:復制,設置好對應的文件夾,照片和視頻會復制到該文件夾下。
和第一種增加目錄的區別在于,不會建立任何文件夾,直接拷貝文件過去,如果你拷貝過多天的數據會混在一起哦。
應用后,按下NAS的一鍵復制按鈕,U盤中數據復制到NAS指定文件夾。可以看到直接拷貝的文件,沒有出現第一種方法的日期文件夾。
3、同步
在同步模式下,U盤中數據增加或者刪除,NAS中備份的數據也會增加刪除。
存儲卡/U盤文件夾內容=NAS文件夾內容
所以如果有一天你換了一張新卡備份,新的文件將會覆蓋NAS里之前拷貝進去的文件,選擇同步要慎重。
設置好對應文件夾,應用后,按下一鍵復制按鈕,U盤數據同步到NAS。我們之前建立的文件夾可見都沒了,該文件夾目前和U盤中的文件一摸一樣。
備份到NAS選擇同步要慎重。如果想使用同步,擔心文件同步刪除,在Qfiling自動歸檔時,設置個臨時文件可以避免同步刪除數據,下文我們會介紹。
文件歸類
攝影的朋友,可能會把照片和視頻都放在一個文件中,如果有海量照片和視頻,手動把照片和視頻分開會很麻煩,但是可以通過Qfiling 實現自動歸類,輕松將它們分類。
打開Qfiling,點擊右上角【創建自動歸檔任務】。
選擇源文件夾,這里新建一個臨時文件夾,U盤備份的數據可以備份在該文件夾,臨時文件夾可以做為中轉,執行完歸檔任務后,源文件刪除,歸檔后的文件存放在新的文件夾下。如果備份到NAS模式選擇同步,U盤數據變動就不會影響NAS里的文件。
執行任務選擇實時任務,只要源文件數據有變化,Qfiling會自動執行分類任務。
接著需要設置好目標路徑、目標文件結構、目標文件處理。
設置好目標路徑(歸類后文件存儲的地方)。
重點介紹下目標文件夾結構,這里選擇根據指定條件創建文件夾結構,分類結果以右邊文件夾為參考。
接著設置目標文件處理,最后設置完成后點擊應用。
備份的數據會將文件歸類好分別存放在【Picture】、【Video】文件夾中。
總結:
一鍵備份結合Qfiling,實現自動備份及歸檔流程:
U盤數據被復制(按一鍵復制)→NAS臨時文件夾→數據通過Qfiling歸檔到新的文件夾(照片視頻分開存放)。
關于Qfiling簡單去重、文件歸檔這里有更詳細的教程。簡單去重技巧,利用威聯通文件歸檔實現照片文件去重教程
在這個過程中擔心數據會出現意外情況,還可以開啟快照,保證數據安全。用容量換安全,憑什么?因為能把搞勒索病毒給氣哭!