画廊-瀑布流实现
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@ -32,4 +32,7 @@ declare module 'vue' {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vImagePreview: typeof import('vue-devui/image-preview/index.es.js')['ImagePreviewDirective']
|
||||
}
|
||||
}
|
||||
|
4
env.d.ts
vendored
4
env.d.ts
vendored
@ -5,13 +5,15 @@ declare global {
|
||||
let $http: any;
|
||||
let $cookies: any;
|
||||
let $msg: any;
|
||||
let $store:any
|
||||
let $store:any;
|
||||
let $modal :any
|
||||
interface Window {
|
||||
// 扩展Windows环境下的全局$http对象
|
||||
$http: any;
|
||||
$cookies: any;
|
||||
$msg: any;
|
||||
$store:any
|
||||
$modal:any
|
||||
}
|
||||
}
|
||||
|
||||
|
36
src/App.vue
36
src/App.vue
@ -4,15 +4,51 @@
|
||||
<router-view></router-view>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
|
||||
<d-modal class="!w-80" v-model="modal.visible" :title="modal.title">
|
||||
{{ modal.content }}
|
||||
<div class="mt-10 w-full flex justify-between">
|
||||
<d-button @click="modal.handdleCancel" variant="text"
|
||||
class="w-[49%] hover:bg-[#8a6684] hover:!text-white">取消</d-button>
|
||||
<span class="text-[20px]"> | </span>
|
||||
<d-button @click="modal.handdleSubmit" variant="text" class="w-[49%] hover:bg-[#5c866a] hover:!text-white"
|
||||
color="primary">确定</d-button>
|
||||
</div>
|
||||
</d-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 空白项目入口
|
||||
const modal = reactive<any>({
|
||||
visible: false,
|
||||
title: "",
|
||||
content: "",
|
||||
handdleCancel: () => { modal.visible = false },
|
||||
handdleSubmit: () => { },
|
||||
})
|
||||
|
||||
function comemodal(mv: any) {
|
||||
modal.visible = true
|
||||
modal.title = mv.title
|
||||
modal.content = mv.content
|
||||
if (mv.handdleCancel) modal.handdleCancel = mv.handdleCancel
|
||||
modal.handdleSubmit = () => {
|
||||
mv.handdleSubmit()
|
||||
modal.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
window.$modal = comemodal
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ps {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,10 @@
|
||||
import request from "@/util/request";
|
||||
|
||||
//getFileList
|
||||
export function getFileList(params: Record<string, string>) {
|
||||
return request({
|
||||
url: "/files/search",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
@ -10,3 +10,9 @@
|
||||
.ps__thumb-y {
|
||||
background-color: #f6cbe7 !important;
|
||||
}
|
||||
.devui-image-preview {
|
||||
background-color: #00000090 !important;
|
||||
img {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
@ -39,8 +39,8 @@
|
||||
<template #content>
|
||||
<div class="py-1" v-for="i in bdNews" :key="i.id">
|
||||
<a class="devui-link flex justify-between" :href="i.url" target="_blank">
|
||||
<span>{{ i.index }}. {{ i.title }}
|
||||
<icon-hot v-show="i.index < 4" class="w-4 text-[#ec66ab] inline-block"></icon-hot>
|
||||
<span class="flex items-center">{{ i.index }}. {{ i.title }}
|
||||
<icon-hot v-show="i.index < 4" class="ml-2 w-4 text-[red] inline-block"></icon-hot>
|
||||
</span>
|
||||
<span class="text-primary">{{ i.hot }}</span>
|
||||
</a>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import router from "@/router";
|
||||
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
||||
import axios from "axios";
|
||||
import { useCookies } from "vue3-cookies";
|
||||
@ -43,13 +44,21 @@ request.interceptors.response.use(
|
||||
// 对响应错误做点什么
|
||||
console.log("Response error", error);
|
||||
|
||||
// if (error.response?.status === 401) {
|
||||
// window.$msg.warning("无效的token");
|
||||
// cookies.remove("token");
|
||||
// cookies.remove("userinfo");
|
||||
// router.replace("/login");
|
||||
// return "Unauthorized";
|
||||
// }
|
||||
if (error.response?.status === 401) {
|
||||
// window.$msg.warning("无效的token");
|
||||
cookies.remove("token");
|
||||
cookies.remove("userinfo");
|
||||
window.$modal({
|
||||
title: "无效的token",
|
||||
content: "token已失效,需要登录,请登录 =>",
|
||||
handdleSubmit: () => {
|
||||
router.replace("/login");
|
||||
},
|
||||
});
|
||||
|
||||
// router.replace("/login");
|
||||
return "Unauthorized";
|
||||
}
|
||||
|
||||
return error.message || "Response error";
|
||||
}
|
||||
|
@ -1,20 +1,190 @@
|
||||
<template>
|
||||
<div class="gallery-page">
|
||||
<h1>画廊页</h1>
|
||||
<div class="test w-100 ml-20 mb-20">
|
||||
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
|
||||
<PerfectScrollbar ref="scrollbar" @ps-scroll-y="handleScroll">
|
||||
<div class="gallery-page py-5 px-[10%]">
|
||||
<d-tabs v-model="tid" type="slider">
|
||||
<d-tab id="share" title="画廊">
|
||||
<div class="gallery-container w-full box-border">
|
||||
<!-- 瀑布流容器 -->
|
||||
<div ref="waterfallContainer" v-image-preview
|
||||
class="waterfall-container flex justify-between flex-nowrap w-full overflow-hidden">
|
||||
<!-- 动态生成的列 -->
|
||||
<div v-for="(column, index) in columns" :key="index" class="waterfall-column flex flex-col w-[240px]">
|
||||
<div v-for="item in column" :key="item.id"
|
||||
class="gallery-item my-[10px] rounded-lg overflow-hidden transition-transform duration-300 box-border hover:-translate-y-1.5">
|
||||
<img :src="item.filepath" alt="" class="gallery-image block w-full h-auto object-cover rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载中指示器 -->
|
||||
<div v-if="loading" class="loading-indicator text-center p-5 text-gray-600">加载中...</div>
|
||||
</div>
|
||||
</d-tab>
|
||||
<d-tab id="my" title="我的">我的</d-tab>
|
||||
</d-tabs>
|
||||
</div>
|
||||
<!-- 画廊内容 -->
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
// 画廊页逻辑
|
||||
const tid = ref('share');
|
||||
const fileList = ref<any[]>([]);
|
||||
const pn = ref(1);
|
||||
const ps = ref(40);
|
||||
const loading = ref(false);
|
||||
const waterfallContainer = ref<HTMLDivElement | null>(null);
|
||||
const columns = ref<Array<Array<any>>>([]);
|
||||
const columnHeights = ref<number[]>([]);
|
||||
const columnCount = ref(4); // 默认列数
|
||||
const imageHeights = ref<Record<string, number>>({}); // 存储每张图片的实际高度
|
||||
const imagesLoaded = ref(0);
|
||||
const itemWidth = ref(240); // 图片宽度固定为220px
|
||||
const scrollbar = ref<any>(null);
|
||||
|
||||
|
||||
// 计算列数 based on 屏幕宽度
|
||||
function calculateColumnCount() {
|
||||
if (!waterfallContainer.value) return;
|
||||
const containerWidth = waterfallContainer.value.clientWidth;
|
||||
const newColumnCount = Math.max(1, Math.floor(containerWidth / itemWidth.value));
|
||||
if (newColumnCount !== columnCount.value) {
|
||||
columnCount.value = newColumnCount;
|
||||
resetWaterfall();
|
||||
}
|
||||
}
|
||||
|
||||
// 重置瀑布流
|
||||
function resetWaterfall() {
|
||||
columns.value = Array(columnCount.value).fill(0).map(() => []);
|
||||
columnHeights.value = Array(columnCount.value).fill(0);
|
||||
// 重新分配图片
|
||||
fileList.value.forEach(item => addToWaterfall(item));
|
||||
}
|
||||
|
||||
// 添加图片到瀑布流
|
||||
function addToWaterfall(item: any) {
|
||||
if (columns.value.length === 0) return;
|
||||
// 找到高度最小的列
|
||||
let minHeight = Math.min(...columnHeights.value);
|
||||
let minIndex = columnHeights.value.indexOf(minHeight);
|
||||
// 添加到该列
|
||||
columns.value[minIndex].push(item);
|
||||
// 估算列高 - 实际高度会在图片加载后更新
|
||||
const estimatedHeight = itemWidth.value; // 假设1:1比例
|
||||
columnHeights.value[minIndex] += estimatedHeight + 24; // 加上padding和margin
|
||||
}
|
||||
|
||||
// 图片加载完成后更新瀑布流
|
||||
function onImageLoad(id: string) {
|
||||
// 等待DOM更新
|
||||
nextTick(() => {
|
||||
if (!waterfallContainer.value) return;
|
||||
const imgElements = waterfallContainer.value.querySelectorAll(`.gallery-image[src*="${id}"]`);
|
||||
if (imgElements.length > 0) {
|
||||
const img = imgElements[0] as HTMLImageElement;
|
||||
imageHeights.value[id] = img.height;
|
||||
updateColumnHeights();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新所有列的高度
|
||||
function updateColumnHeights() {
|
||||
if (!waterfallContainer.value) return;
|
||||
const columnElements = waterfallContainer.value.querySelectorAll('.waterfall-column');
|
||||
columnHeights.value = Array.from(columnElements).map(el => el.clientHeight);
|
||||
}
|
||||
|
||||
// 获取文件列表
|
||||
async function getFileList() {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await $http.file.getFileList({
|
||||
page_num: pn.value,
|
||||
page_size: ps.value,
|
||||
});
|
||||
console.log('>>> --> getFileList --> res:', res);
|
||||
|
||||
if (pn.value === 1) {
|
||||
fileList.value = res.data;
|
||||
resetWaterfall();
|
||||
} else {
|
||||
// 追加新数据
|
||||
res.data.forEach((item: any) => {
|
||||
fileList.value.push(item);
|
||||
addToWaterfall(item);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动事件
|
||||
function handleScroll(e: any) {
|
||||
// console.log('>>> --> handleScroll --> loading:', e)
|
||||
if (loading.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 * 0.8) {
|
||||
console.log('>>> --> handleScroll --> 加载更多')
|
||||
pn.value++;
|
||||
getFileList();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getFileList();
|
||||
// 计算初始列数
|
||||
calculateColumnCount();
|
||||
// 添加窗口大小变化监听
|
||||
window.addEventListener('resize', calculateColumnCount);
|
||||
// 添加滚动监听
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除监听
|
||||
window.removeEventListener('resize', calculateColumnCount);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 画廊页样式 */
|
||||
.test {
|
||||
box-shadow: 0 2px 12px 0 @primary !important;
|
||||
:deep(.devui-tabs__nav) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100% !important;
|
||||
// padding: 0 10%;
|
||||
|
||||
li {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
li a span {
|
||||
font-size: 18px !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.ps {
|
||||
height: calc(100vh - 65px);
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -85,8 +85,8 @@ function login() {
|
||||
$msg.error('登录失败')
|
||||
return
|
||||
}
|
||||
$cookies.set('token',res.data.token,'7d')
|
||||
$cookies.set('userinfo',res.data.userinfo,'7d')
|
||||
$cookies.set('token', res.data.token, '1d')
|
||||
$cookies.set('userinfo', res.data.userinfo, '1d')
|
||||
$msg.success(res.msg)
|
||||
router.push({ path: "/" })
|
||||
}
|
||||
|
Reference in New Issue
Block a user