A:添加虚拟列表类型文件
U:调整时间线组件列表展示
This commit is contained in:
1 parent
96049909a3
commit
fbfd77ec93
9 files changed
+655
-7
No files matched your search
@@ -1,2 +1,2 @@
|
|||||||
VITE_MODE='development'
|
VITE_MODE='development'
|
||||||
VITE_APP_API_URL='http://localhost:8080/api'
|
VITE_APP_API_URL='http://127.0.0.1:9484/api'
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"justified-layout": "^4.1.0",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
|
|||||||
Generated
+8
@@ -14,6 +14,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
version: 1.9.0
|
version: 1.9.0
|
||||||
|
justified-layout:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
|
version: 3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
|
||||||
@@ -1640,6 +1643,9 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==, tarball: https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==, tarball: https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz}
|
||||||
|
|
||||||
|
justified-layout@4.1.0:
|
||||||
|
resolution: {integrity: sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==, tarball: https://registry.npmmirror.com/justified-layout/-/justified-layout-4.1.0.tgz}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, tarball: https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, tarball: https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz}
|
||||||
|
|
||||||
@@ -3983,6 +3989,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
justified-layout@4.1.0: {}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { HttpConfig } from '@/types/http.ts'
|
|||||||
// 2. 配置管理器 (config/httpConfig.ts)
|
// 2. 配置管理器 (config/httpConfig.ts)
|
||||||
class HttpConfigManager {
|
class HttpConfigManager {
|
||||||
private config: HttpConfig = {
|
private config: HttpConfig = {
|
||||||
baseURL: import.meta.env.VITE_APP_API_URL || 'http://localhost:8726',
|
baseURL: import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:9484',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/photos',
|
path: '/photos',
|
||||||
name: 'photos',
|
name: 'photos',
|
||||||
component: () => import('@/views/PhotosView.vue'),
|
component: () => import('@/views/PhotosView2.vue'),
|
||||||
meta: { title: '照片' }
|
meta: { title: '照片' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
declare module 'vue-virtual-scroller' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
|
||||||
|
export const RecycleScroller: DefineComponent<{
|
||||||
|
items: any[]
|
||||||
|
itemSize: number | string
|
||||||
|
keyField?: string
|
||||||
|
direction?: 'vertical' | 'horizontal'
|
||||||
|
listTag?: string
|
||||||
|
itemTag?: string
|
||||||
|
listClass?: string | object | any[]
|
||||||
|
itemClass?: string | object | any[]
|
||||||
|
gridItems?: number
|
||||||
|
sizeField?: string
|
||||||
|
typeField?: string
|
||||||
|
buffer?: number
|
||||||
|
pageMode?: boolean
|
||||||
|
prerender?: number
|
||||||
|
emitUpdate?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const DynamicScroller: DefineComponent<{
|
||||||
|
items: any[]
|
||||||
|
minItemSize: number | string
|
||||||
|
keyField?: string
|
||||||
|
direction?: 'vertical' | 'horizontal'
|
||||||
|
listTag?: string
|
||||||
|
itemTag?: string
|
||||||
|
listClass?: string | object | any[]
|
||||||
|
itemClass?: string | object | any[]
|
||||||
|
buffer?: number
|
||||||
|
pageMode?: boolean
|
||||||
|
prerender?: number
|
||||||
|
emitUpdate?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const DynamicScrollerItem: DefineComponent<{
|
||||||
|
item: any
|
||||||
|
index: number
|
||||||
|
active: boolean
|
||||||
|
sizeDependencies?: any[]
|
||||||
|
watchData?: boolean
|
||||||
|
tag?: string
|
||||||
|
emitResize?: boolean
|
||||||
|
onResize?: () => void
|
||||||
|
}>
|
||||||
|
|
||||||
|
const plugin: {
|
||||||
|
install: (app: any, options?: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
||||||
|
}
|
||||||
@@ -550,10 +550,6 @@ onUnmounted(() => {
|
|||||||
display: none; /* Webkit browsers */
|
display: none; /* Webkit browsers */
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-period {
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.period-header {
|
.period-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline-container">
|
||||||
|
<RecycleScroller
|
||||||
|
:items="blocks"
|
||||||
|
:key-field="'id'"
|
||||||
|
:min-item-size="100"
|
||||||
|
class="scroller"
|
||||||
|
v-slot="{ item }"
|
||||||
|
>
|
||||||
|
<!-- 日期标题 -->
|
||||||
|
<div v-if="item.type === 'header'" class="header">
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 照片行 -->
|
||||||
|
<div v-else-if="item.type === 'photoRow'" class="photo-row" :style="{ height: item.rowHeight + 'px' }">
|
||||||
|
<div
|
||||||
|
v-for="photo in item.photos"
|
||||||
|
:key="photo.hash"
|
||||||
|
class="photo"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: photo.color,
|
||||||
|
width: photo.displayWidth + 'px',
|
||||||
|
height: item.rowHeight + 'px'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未来可能出现的卡片 -->
|
||||||
|
<div v-else-if="item.type === 'card'" class="card">
|
||||||
|
{{ item.text }}
|
||||||
|
</div>
|
||||||
|
</RecycleScroller>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||||
|
|
||||||
|
// 模拟接口返回的数据结构
|
||||||
|
const photoApiData = {
|
||||||
|
hash: [
|
||||||
|
"9df97ccc44529d1d377b36f50f79cd5ab9c0920dd58352630412e4b3f8eceaf5",
|
||||||
|
"70886b5e8bf59eeb3cc5fbb1f714c3da5c371e1a0c83ac5ad9c20621c703ff4f"
|
||||||
|
],
|
||||||
|
isImage: [true, true],
|
||||||
|
takenAt: ["2025-08-02T14:16:36", "2025-08-02T14:16:36"],
|
||||||
|
ratio: [1, 0.46153846]
|
||||||
|
}
|
||||||
|
|
||||||
|
const timelineApiData = [
|
||||||
|
{ date: "2024-10-05", count: 1202 },
|
||||||
|
{ date: "2024-11-01", count: 1 },
|
||||||
|
{ date: "2024-11-04", count: 4 },
|
||||||
|
{ date: "2025-08-02", count: 3 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 随机颜色生成
|
||||||
|
function randomColor() {
|
||||||
|
const colors = ['#f87171', '#fbbf24', '#34d399', '#60a5fa', '#a78bfa', '#f472b6']
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成模拟照片
|
||||||
|
function generatePhotos(count) {
|
||||||
|
const photos = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const ratio = (Math.random() * 1.5 + 0.5).toFixed(2) // 比例 0.5 ~ 2.0
|
||||||
|
photos.push({
|
||||||
|
hash: Math.random().toString(36).substring(2, 15),
|
||||||
|
color: randomColor(),
|
||||||
|
ratio: parseFloat(ratio)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return photos
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将照片按行布局,每行总宽度接近 containerWidth
|
||||||
|
function buildPhotoRows(photos, containerWidth, rowHeight) {
|
||||||
|
const rows = []
|
||||||
|
let currentRow = []
|
||||||
|
let currentRatioSum = 0
|
||||||
|
|
||||||
|
photos.forEach(photo => {
|
||||||
|
currentRow.push(photo)
|
||||||
|
currentRatioSum += photo.ratio
|
||||||
|
|
||||||
|
// 如果行里照片太多,就换行
|
||||||
|
if (currentRatioSum >= containerWidth / rowHeight) {
|
||||||
|
const scale = containerWidth / (currentRatioSum * rowHeight)
|
||||||
|
rows.push({ photos: currentRow, scale })
|
||||||
|
currentRow = []
|
||||||
|
currentRatioSum = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (currentRow.length > 0) {
|
||||||
|
const scale = containerWidth / (currentRatioSum * rowHeight)
|
||||||
|
rows.push({ photos: currentRow, scale })
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成完整 blocks
|
||||||
|
function generateBlocks() {
|
||||||
|
const blocks = []
|
||||||
|
let idCounter = 0
|
||||||
|
const containerWidth = 800 // 假设容器宽度 800px
|
||||||
|
const rowHeight = 200 // 每行高度 200px
|
||||||
|
|
||||||
|
timelineApiData.forEach(entry => {
|
||||||
|
// 日期标题
|
||||||
|
blocks.push({ id: idCounter++, type: 'header', label: entry.date })
|
||||||
|
|
||||||
|
// 生成该日期对应的照片
|
||||||
|
const photos = generatePhotos(entry.count)
|
||||||
|
const rows = buildPhotoRows(photos, containerWidth, rowHeight)
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const scaledHeight = rowHeight * row.scale
|
||||||
|
const scaledPhotos = row.photos.map(p => ({
|
||||||
|
...p,
|
||||||
|
displayWidth: p.ratio * scaledHeight
|
||||||
|
}))
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
id: idCounter++,
|
||||||
|
type: 'photoRow',
|
||||||
|
rowHeight: scaledHeight,
|
||||||
|
photos: scaledPhotos
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = generateBlocks()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||||
|
|
||||||
|
.timeline-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller {
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 12px 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
<template>
|
||||||
|
<div class="photos-timeline">
|
||||||
|
<DynamicScroller
|
||||||
|
ref="scrollerRef"
|
||||||
|
:items="timelineItems"
|
||||||
|
:min-item-size="100"
|
||||||
|
key-field="id"
|
||||||
|
class="scroller"
|
||||||
|
:buffer="200"
|
||||||
|
>
|
||||||
|
<template #default="{ item, index, active }">
|
||||||
|
<DynamicScrollerItem
|
||||||
|
:item="item"
|
||||||
|
:index="index"
|
||||||
|
:active="active"
|
||||||
|
:size-dependencies="[containerWidth]"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<!-- 月份标题 -->
|
||||||
|
<div v-if="item.type === 'month'" class="month-header">
|
||||||
|
<h2 class="month-title">{{ item.title }}</h2>
|
||||||
|
<div class="month-subtitle">{{ item.subtitle }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回忆卡片 -->
|
||||||
|
<div v-else-if="item.type === 'memory'" class="memory-card">
|
||||||
|
<div class="memory-icon">📸</div>
|
||||||
|
<div class="memory-content">
|
||||||
|
<div class="memory-title">{{ item.title }}</div>
|
||||||
|
<div class="memory-subtitle">{{ item.subtitle }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间卡片 -->
|
||||||
|
<div v-else-if="item.type === 'date'" class="date-card">
|
||||||
|
<div class="date-label">{{ item.title }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 照片网格 -->
|
||||||
|
<div v-else-if="item.type === 'photos'" class="photos-grid" :style="{ height: item.height + 'px' }">
|
||||||
|
<div
|
||||||
|
v-for="photo in item.photos"
|
||||||
|
:key="photo.id"
|
||||||
|
class="photo-item"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: photo.box.top + 'px',
|
||||||
|
left: photo.box.left + 'px',
|
||||||
|
width: photo.box.width + 'px',
|
||||||
|
height: photo.box.height + 'px',
|
||||||
|
backgroundColor: photo.color
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="photo-placeholder">
|
||||||
|
<span class="photo-ratio">{{ photo.ratio.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DynamicScrollerItem>
|
||||||
|
</template>
|
||||||
|
</DynamicScroller>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||||
|
import justifiedLayout from 'justified-layout'
|
||||||
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||||
|
|
||||||
|
interface Photo {
|
||||||
|
id: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
ratio: number
|
||||||
|
color: string
|
||||||
|
box: {
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimelineItem {
|
||||||
|
id: string
|
||||||
|
type: 'month' | 'memory' | 'date' | 'photos'
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
height?: number
|
||||||
|
photos?: Photo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollerRef = ref()
|
||||||
|
const containerWidth = ref(1200)
|
||||||
|
|
||||||
|
// 生成随机颜色
|
||||||
|
function generateRandomColor(): string {
|
||||||
|
const colors = [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||||
|
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||||
|
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2'
|
||||||
|
]
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机照片数据
|
||||||
|
function generateRandomPhotos(count: number): Photo[] {
|
||||||
|
const photos: Photo[] = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const width = Math.floor(Math.random() * 400) + 200 // 200-600px
|
||||||
|
const height = Math.floor(Math.random() * 400) + 200 // 200-600px
|
||||||
|
const ratio = width / height
|
||||||
|
|
||||||
|
photos.push({
|
||||||
|
id: `photo-${i}`,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
ratio,
|
||||||
|
color: generateRandomColor(),
|
||||||
|
box: { top: 0, left: 0, width: 0, height: 0 } // 先设置默认值,后面会计算
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return photos
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 justified-layout 计算照片布局
|
||||||
|
function calculatePhotosLayout(photos: Photo[]): { photos: Photo[], height: number } {
|
||||||
|
const photoInputs = photos.map(photo => ({
|
||||||
|
width: photo.width,
|
||||||
|
height: photo.height
|
||||||
|
}))
|
||||||
|
|
||||||
|
const geometry = justifiedLayout(photoInputs, {
|
||||||
|
containerWidth: containerWidth.value - 40, // 减去左右边距
|
||||||
|
targetRowHeight: 220,
|
||||||
|
targetRowHeightTolerance: 0.25,
|
||||||
|
boxSpacing: 4,
|
||||||
|
containerPadding: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const layoutPhotos = photos.map((photo, index) => ({
|
||||||
|
...photo,
|
||||||
|
box: geometry.boxes[index]
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
photos: layoutPhotos,
|
||||||
|
height: geometry.containerHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成时间线数据
|
||||||
|
function generateTimelineData(): TimelineItem[] {
|
||||||
|
const items: TimelineItem[] = []
|
||||||
|
const months = [
|
||||||
|
{ title: '2024年1月', subtitle: '128张照片' },
|
||||||
|
{ title: '2023年12月', subtitle: '95张照片' },
|
||||||
|
{ title: '2023年11月', subtitle: '156张照片' }
|
||||||
|
]
|
||||||
|
|
||||||
|
months.forEach((month, monthIndex) => {
|
||||||
|
// 添加月份标题
|
||||||
|
items.push({
|
||||||
|
id: `month-${monthIndex}`,
|
||||||
|
type: 'month',
|
||||||
|
title: month.title,
|
||||||
|
subtitle: month.subtitle
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加回忆卡片 (随机)
|
||||||
|
if (Math.random() > 0.6) {
|
||||||
|
items.push({
|
||||||
|
id: `memory-${monthIndex}`,
|
||||||
|
type: 'memory',
|
||||||
|
title: '回忆',
|
||||||
|
subtitle: '一年前的今天'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加日期和照片组
|
||||||
|
const daysInMonth = Math.floor(Math.random() * 8) + 3 // 3-10天
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dayId = `day-${monthIndex}-${day}`
|
||||||
|
|
||||||
|
// 添加日期卡片
|
||||||
|
items.push({
|
||||||
|
id: `date-${dayId}`,
|
||||||
|
type: 'date',
|
||||||
|
title: `${month.title.slice(0, 7)}${day}日`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加照片组
|
||||||
|
const photoCount = Math.floor(Math.random() * 20) + 5 // 5-24张照片
|
||||||
|
const photos = generateRandomPhotos(photoCount)
|
||||||
|
const layout = calculatePhotosLayout(photos)
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: `photos-${dayId}`,
|
||||||
|
type: 'photos',
|
||||||
|
height: layout.height,
|
||||||
|
photos: layout.photos
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const timelineItems = ref<TimelineItem[]>([])
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 获取容器宽度
|
||||||
|
const container = document.querySelector('.photos-timeline')
|
||||||
|
if (container) {
|
||||||
|
containerWidth.value = container.clientWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成时间线数据
|
||||||
|
timelineItems.value = generateTimelineData()
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
if (container) {
|
||||||
|
const newWidth = container.clientWidth
|
||||||
|
if (Math.abs(newWidth - containerWidth.value) > 50) { // 只有明显变化时才重新计算
|
||||||
|
containerWidth.value = newWidth
|
||||||
|
// 重新计算所有照片布局
|
||||||
|
timelineItems.value = timelineItems.value.map(item => {
|
||||||
|
if (item.type === 'photos' && item.photos) {
|
||||||
|
const layout = calculatePhotosLayout(item.photos)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
height: layout.height,
|
||||||
|
photos: layout.photos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
// 清理事件监听器
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.photos-timeline {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 月份标题样式 */
|
||||||
|
.month-header {
|
||||||
|
padding: 40px 20px 20px 20px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 回忆卡片样式 */
|
||||||
|
.memory-card {
|
||||||
|
margin: 16px 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日期卡片样式 */
|
||||||
|
.date-card {
|
||||||
|
padding: 16px 20px 12px 20px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 照片网格样式 */
|
||||||
|
.photos-grid {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 20px 24px 20px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-ratio {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色主题适配 */
|
||||||
|
.dark .month-header {
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .date-card {
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .photos-grid {
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.month-header {
|
||||||
|
padding: 32px 16px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-card {
|
||||||
|
margin: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-card {
|
||||||
|
padding: 12px 16px 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photos-grid {
|
||||||
|
margin: 0 16px 20px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user