style: 更新UI样式和字体设置
This commit is contained in:
+221
-121
@@ -1,180 +1,280 @@
|
||||
<template>
|
||||
<n-scrollbar ref="virtualListInst" class="py-5" :style="boxStyle" @scroll="handleScroll">
|
||||
<n-input class="my-4 !w-[72%] ml-[14%]" round size="large" v-model:value="kw" @keyup.enter="onSearch"
|
||||
placeholder="请输入关键字">
|
||||
<template #suffix>
|
||||
<n-icon size="large">
|
||||
<icon-search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
<div
|
||||
class="relative overflow-hidden bg-[radial-gradient(circle_at_12%_18%,rgba(215,177,123,0.24),transparent_24%),radial-gradient(circle_at_86%_12%,rgba(120,162,182,0.18),transparent_24%),radial-gradient(circle_at_50%_100%,rgba(176,198,172,0.14),transparent_28%),linear-gradient(180deg,#efe7da_0%,#e8f0ef_44%,#f3f1e8_100%)]">
|
||||
<div
|
||||
class="pointer-events-none absolute left-[-5rem] top-6 h-80 w-80 rounded-full bg-[rgba(234,176,107,0.45)] opacity-48 blur-[88px]">
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none absolute right-[-5rem] top-32 h-[22rem] w-[22rem] rounded-full bg-[rgba(118,176,214,0.32)] opacity-48 blur-[88px]">
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 opacity-14 [background-image:linear-gradient(rgba(255,255,255,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.18)_1px,transparent_1px)] [background-size:36px_36px] [mask-image:linear-gradient(180deg,rgba(0,0,0,0.8),transparent_85%)]">
|
||||
</div>
|
||||
|
||||
<n-scrollbar ref="virtualListInst" class="relative z-[1] py-6" :style="boxStyle" @scroll="handleScroll">
|
||||
<section
|
||||
class="relative mx-auto w-[82%] rounded-[28px] border border-white/62 bg-[linear-gradient(145deg,rgba(255,251,246,0.68),rgba(237,246,244,0.56))] p-6 shadow-[0_20px_50px_rgba(110,124,112,0.11)] backdrop-blur-[20px] after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-[linear-gradient(120deg,rgba(255,255,255,0.24),transparent_45%)] after:content-['']">
|
||||
<div class="max-w-[48rem]">
|
||||
<h1 class="m-0 text-[clamp(1.8rem,2.5vw,2.8rem)] leading-[1.2] text-[#2c3a4a]">
|
||||
把喜欢的画面收进一片更柔和的光里
|
||||
</h1>
|
||||
<p class="mt-[0.9rem] max-w-[42rem] leading-[1.75] text-[#66778b]">
|
||||
输入关键词后按回车即可筛选图片。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Waterfall class="ml-[10%] !w-[80%]" ref="waterfall" :list="fileList" :gutter="gutter" :columns="column"
|
||||
img-selector="url" animation-effect="fadeIn" :animation-duration="1000" :animation-delay="300"
|
||||
backgroundColor="transparent"> >
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
class="card h-full flex items-center justify-center rounded-md shadow-lg overflow-hidden group transition-transform duration-300 box-border hover:-translate-y-1.5">
|
||||
<!-- <div class="image-wrapper"> -->
|
||||
<LazyImg class="rounded-md overflow-hidden" :Pwidth="item.width" :Pheight="item.height" :url="item.filepath" />
|
||||
<!-- </div> -->
|
||||
<div
|
||||
class="hidden truncate group-hover:block absolute rounded-md z-10 truncate top-0 text-center w-full bg-[#00000070] text-white">
|
||||
{{ item.filename }}
|
||||
class="mt-6 rounded-full border border-white/62 bg-[linear-gradient(135deg,rgba(255,249,241,0.76),rgba(236,245,243,0.72)),radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_34%)] px-[1.1rem] py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.55)]">
|
||||
<n-input class="gallery-search" round size="large" v-model:value="kw" @keyup.enter="onSearch"
|
||||
placeholder="请输入关键词,按回车开始搜索">
|
||||
<template #suffix>
|
||||
<n-icon class="cursor-pointer text-slate-500" size="large">
|
||||
<icon-search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="mx-auto mt-6 w-[82%] rounded-[30px] border border-white/62 bg-white/42 p-6 shadow-[0_18px_42px_rgba(109,126,117,0.1)] backdrop-blur-[18px]">
|
||||
<div class="mb-5 flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<div class="text-[1.6rem] font-semibold text-[#2f4050]">图片列表</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute rounded-md flex px-2 z-10 truncate bottom-0 w-full bg-[#00000070] text-white justify-between items-center">
|
||||
<div>由 <span class="text-[#f1d9db] font-600">{{ item.nickname }}</span> 分享</div>
|
||||
<div class="text-sm cursor-pointer text-[#f6cbe7] flex items-center" @click="downloadFile(item.filepath)">
|
||||
<icon-download class="w-5 h-5" />
|
||||
下载
|
||||
</div>
|
||||
class="inline-flex items-center whitespace-nowrap rounded-full border border-white/80 bg-white/68 px-4 py-[0.65rem] text-[#526376]">
|
||||
{{ galleryStats.loaded }} 张
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
</n-scrollbar>
|
||||
|
||||
<Waterfall ref="waterfall" :list="fileList" :gutter="gutter" :columns="column" class="w-full" img-selector="url"
|
||||
animation-effect="fadeIn" :animation-duration="1000" :animation-delay="300" backgroundColor="transparent">
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
class="group relative flex h-full items-center justify-center overflow-hidden rounded-[24px] border border-white/82 bg-[linear-gradient(180deg,rgba(255,252,248,0.92),rgba(239,244,241,0.78))] shadow-[0_16px_34px_rgba(116,132,124,0.14)] backdrop-blur-[14px] transition-[transform,box-shadow,border-color] duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:border-white/196 hover:shadow-[0_22px_42px_rgba(102,123,112,0.18)]">
|
||||
<LazyImg class="overflow-hidden rounded-[20px]" :Pwidth="item.width" :Pheight="item.height"
|
||||
:url="item.filepath" />
|
||||
<div
|
||||
class="absolute left-0 right-0 top-0 z-10 overflow-hidden text-ellipsis whitespace-nowrap rounded-[14px] border border-white/22 bg-[rgba(39,50,65,0.1)] px-3 py-1 items-center justify-center text-sm text-white hidden backdrop-blur-[12px] group-hover:flex">
|
||||
{{ item.filename }}
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-between gap-3 rounded-[14px] border border-white/22 bg-[rgba(39,50,65,0.5)] px-3 py-1 text-white backdrop-blur-[12px]">
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-[#eef4f8]">
|
||||
由 <span class="font-semibold text-[#ffd8cb]">{{ item.nickname }}</span> 分享
|
||||
</div>
|
||||
<div
|
||||
class="inline-flex flex-shrink-0 cursor-pointer items-center rounded-full bg-white/18 px-4 py-1 text-[0.86rem] font-semibold text-[#ffe3d3] transition-colors duration-200 hover:bg-white/24 hover:text-white"
|
||||
@click="downloadFile(item.filepath)">
|
||||
<icon-download class="mr-1 h-5 w-5" />
|
||||
下载
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
</section>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { throttle } from 'es-toolkit';
|
||||
import { LazyImg, Waterfall } from '../lib';
|
||||
import { throttle } from 'es-toolkit'
|
||||
import { LazyImg, Waterfall } from '../lib'
|
||||
|
||||
definePage({
|
||||
name: 'gallery',
|
||||
meta: {
|
||||
title: '画廊',
|
||||
}
|
||||
})
|
||||
const waterfall = ref<any>(null);
|
||||
|
||||
interface GalleryItem {
|
||||
filepath: string
|
||||
filename: string
|
||||
nickname: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface ScrollContainer extends EventTarget {
|
||||
scrollTop: number
|
||||
scrollHeight: number
|
||||
offsetHeight: number
|
||||
}
|
||||
|
||||
const GALLERY_WIDTH_RATIO = 0.82
|
||||
const PAGE_SIZE = 20
|
||||
const SCROLL_THRESHOLD = 100
|
||||
|
||||
const waterfall = ref<any>(null)
|
||||
const nav: any = $store.nav.useNavStore()
|
||||
const boxStyle: any = ref({})
|
||||
// 画廊页逻辑
|
||||
const fileList = ref<any[]>([]);
|
||||
const pn = ref(1);
|
||||
const ps = ref(20);
|
||||
const loading = ref(false);
|
||||
const kw = ref<string>('');
|
||||
const cwidth = ref<number>(240);
|
||||
const column = ref<number>(5);
|
||||
const gutter = ref<number>(18);
|
||||
const boxStyle = ref<Record<string, string>>({})
|
||||
const fileList = ref<GalleryItem[]>([])
|
||||
const pn = ref(1)
|
||||
const ps = ref(PAGE_SIZE)
|
||||
const loading = ref(false)
|
||||
const kw = ref('')
|
||||
const cwidth = ref(240)
|
||||
const column = ref(5)
|
||||
const gutter = ref(18)
|
||||
const isLoadAll = ref(false)
|
||||
|
||||
const isLoadAll = ref<boolean>(false)
|
||||
const searchKeyword = computed(() => kw.value.trim())
|
||||
|
||||
const galleryStats = computed(() => {
|
||||
let status = '浏览全部'
|
||||
|
||||
if (loading.value) {
|
||||
status = '加载中'
|
||||
} else if (isLoadAll.value && fileList.value.length > 0) {
|
||||
status = '已全部加载'
|
||||
} else if (searchKeyword.value) {
|
||||
status = '搜索结果'
|
||||
}
|
||||
|
||||
return {
|
||||
loaded: fileList.value.length,
|
||||
columns: column.value,
|
||||
status,
|
||||
keyword: searchKeyword.value ? `关键词:${searchKeyword.value}` : '关键词:全部图片',
|
||||
}
|
||||
})
|
||||
|
||||
// 计算列数
|
||||
function calculateColumns() {
|
||||
const totalWidth = window.innerWidth * 0.8; // 画廊宽度为视口宽度的80%
|
||||
const col = Math.floor((totalWidth + gutter.value) / (cwidth.value + gutter.value));
|
||||
column.value = col > 0 ? col : 1;
|
||||
function renderWaterfall() {
|
||||
waterfall.value?.renderer()
|
||||
}
|
||||
|
||||
// 获取文件列表
|
||||
function calculateColumns() {
|
||||
const totalWidth = window.innerWidth * GALLERY_WIDTH_RATIO
|
||||
const col = Math.floor((totalWidth + gutter.value) / (cwidth.value + gutter.value))
|
||||
column.value = col > 0 ? col : 1
|
||||
renderWaterfall()
|
||||
}
|
||||
|
||||
function updateBoxStyle() {
|
||||
boxStyle.value = {
|
||||
maxHeight: `calc(100vh - 5px - ${nav.navH}px)`,
|
||||
}
|
||||
}
|
||||
|
||||
function appendFileList(list: GalleryItem[]) {
|
||||
if (pn.value === 1) {
|
||||
fileList.value = list
|
||||
return
|
||||
}
|
||||
|
||||
list.forEach((item) => {
|
||||
fileList.value.push(item)
|
||||
})
|
||||
}
|
||||
|
||||
function resetGalleryState() {
|
||||
pn.value = 1
|
||||
kw.value = ''
|
||||
fileList.value = []
|
||||
isLoadAll.value = false
|
||||
}
|
||||
|
||||
async function getFileList() {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await $http.file.getFileList({
|
||||
keyword: kw.value,
|
||||
page_num: pn.value,
|
||||
page_size: ps.value,
|
||||
});
|
||||
})
|
||||
const list = res.data as GalleryItem[]
|
||||
|
||||
// console.log('>>> --> getFileList --> res:', res);
|
||||
if (res.data.length < ps.value) {
|
||||
isLoadAll.value = true;
|
||||
}
|
||||
if (pn.value === 1) {
|
||||
fileList.value = res.data;
|
||||
} else {
|
||||
// 追加新数据
|
||||
res.data.forEach((item: any) => {
|
||||
fileList.value.push(item);
|
||||
});
|
||||
if (list.length < ps.value) {
|
||||
isLoadAll.value = true
|
||||
}
|
||||
|
||||
appendFileList(list)
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
console.error('获取图片列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false;
|
||||
waterfall.value?.renderer();
|
||||
loading.value = false
|
||||
renderWaterfall()
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = throttle((e: Event) => {
|
||||
if (loading.value || isLoadAll.value) return
|
||||
|
||||
const target = e.target as ScrollContainer | null
|
||||
if (!target) return
|
||||
|
||||
const scrollTop = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.offsetHeight
|
||||
|
||||
// 监听滚动事件
|
||||
const handleScroll: any = throttle((e: any) => {
|
||||
// console.log('>>> --> handleScroll --> loading:', e)
|
||||
if (loading.value) return;
|
||||
if (isLoadAll.value) return;
|
||||
const scrollTop = e.target.scrollTop
|
||||
// console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
|
||||
const scrollHeight = e.target.scrollHeight
|
||||
// console.log('>>> --> handleScroll --> scrollHeight:', scrollHeight)
|
||||
const clientHeight = e.target.offsetHeight;
|
||||
|
||||
// 当滚动到距离底部20%时加载更多
|
||||
// console.log('>>> --> clientHeight --> clientHeight:', clientHeight)
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||
console.log('>>> --> handleScroll --> 加载更多')
|
||||
pn.value++;
|
||||
getFileList();
|
||||
if (scrollTop + clientHeight >= scrollHeight - SCROLL_THRESHOLD) {
|
||||
pn.value += 1
|
||||
getFileList()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
function onSearch() {
|
||||
pn.value = 1;
|
||||
pn.value = 1
|
||||
kw.value = kw.value.trim()
|
||||
isLoadAll.value = false
|
||||
getFileList();
|
||||
getFileList()
|
||||
}
|
||||
|
||||
function downloadFile(url: string) {
|
||||
console.log('>>> --> downloadFile --> url:', url)
|
||||
// 创建临时a标签
|
||||
const link = document.createElement('a');
|
||||
// 设置下载链接
|
||||
link.href = url;
|
||||
// 提取文件名
|
||||
const fileName = url.split('/').pop() || 'downloaded-file';
|
||||
// 设置下载属性和文件名
|
||||
link.download = fileName;
|
||||
// 设置为隐藏元素
|
||||
link.style.display = 'none';
|
||||
// 添加到文档
|
||||
document.body.appendChild(link);
|
||||
// 触发点击事件
|
||||
link.click();
|
||||
// 移除临时元素
|
||||
document.body.removeChild(link);
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
const fileName = url.split('/').pop() || 'downloaded-file'
|
||||
link.download = fileName
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calculateColumns()
|
||||
getFileList();
|
||||
console.log('>>> --> nav.NavH:', nav.navH)
|
||||
boxStyle.value = {
|
||||
maxHeight: `calc(100vh - 5px - ${nav.navH}px)`,
|
||||
}
|
||||
// 添加滚动监听
|
||||
window.addEventListener('resize', calculateColumns);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
updateBoxStyle()
|
||||
getFileList()
|
||||
window.addEventListener('resize', calculateColumns)
|
||||
window.addEventListener('resize', updateBoxStyle)
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pn.value = 1;
|
||||
kw.value = '';
|
||||
fileList.value = [];
|
||||
isLoadAll.value = false
|
||||
// 移除监听
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
resetGalleryState()
|
||||
window.removeEventListener('resize', calculateColumns)
|
||||
window.removeEventListener('resize', updateBoxStyle)
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.n-image img) {
|
||||
border-radius: 0.375rem !important;
|
||||
:deep(.gallery-search .n-input) {
|
||||
background: rgba(255, 255, 255, 0.74) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7) !important;
|
||||
box-shadow: 0 10px 28px rgba(109, 138, 132, 0.1) !important;
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
</style>
|
||||
|
||||
:deep(.gallery-search .n-input__input-el) {
|
||||
color: #314155 !important;
|
||||
}
|
||||
|
||||
:deep(.gallery-search .n-input__placeholder) {
|
||||
color: #93a2b3 !important;
|
||||
}
|
||||
|
||||
:deep(.gallery-search .n-input.n-input--focus) {
|
||||
border-color: rgba(122, 160, 148, 0.7) !important;
|
||||
box-shadow: 0 16px 38px rgba(118, 149, 139, 0.16) !important;
|
||||
}
|
||||
|
||||
:deep(.n-image img) {
|
||||
border-radius: 1rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user