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_APP_API_URL='http://localhost:8080/api'
|
||||
VITE_APP_API_URL='http://127.0.0.1:9484/api'
|
||||
@@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.9.0",
|
||||
"justified-layout": "^4.1.0",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
|
||||
Generated
+8
@@ -14,6 +14,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
justified-layout:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
pinia:
|
||||
specifier: ^3.0.1
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
justified-layout@4.1.0: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { HttpConfig } from '@/types/http.ts'
|
||||
// 2. 配置管理器 (config/httpConfig.ts)
|
||||
class HttpConfigManager {
|
||||
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,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -10,7 +10,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/photos',
|
||||
name: 'photos',
|
||||
component: () => import('@/views/PhotosView.vue'),
|
||||
component: () => import('@/views/PhotosView2.vue'),
|
||||
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 */
|
||||
}
|
||||
|
||||
.photo-period {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.period-header {
|
||||
display: flex;
|
||||
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