style: 更新UI样式和字体设置

This commit is contained in:
2026-03-20 21:13:55 +08:00
parent 1e198af3d3
commit 9586c67a46
21 changed files with 2859 additions and 3606 deletions
+221 -121
View File
@@ -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>