You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

146 lines
7.1 KiB

type CardType = 'text' | 'image' | 'image-text' | 'portfolio' | 'project'
interface CardData {
id: number
type: CardType
image?: string
images?: string[]
title: string
description?: string
tags?: string[]
aspectRatio: number
}
const images = [
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=600&h=800&fit=crop',
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=600&h=500&fit=crop',
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=600&h=700&fit=crop',
'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=600&h=900&fit=crop',
'https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=600&h=550&fit=crop',
'https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=600&h=650&fit=crop',
'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=600&h=750&fit=crop',
'https://images.unsplash.com/photo-1518173946687-a1e4e09e68b7?w=600&h=850&fit=crop',
'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=600&h=580&fit=crop',
'https://images.unsplash.com/photo-1505144808419-1957a94ca61e?w=600&h=720&fit=crop',
'https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?w=600&h=600&fit=crop',
'https://images.unsplash.com/photo-1490730141103-6cac27aaab94?w=600&h=780&fit=crop',
]
const portfolioSets = [
[images[0], images[1], images[4], images[5]],
[images[2], images[3], images[6], images[7]],
[images[8], images[9], images[10], images[11]],
[images[1], images[3], images[8], images[10]],
[images[0], images[2], images[6], images[9]],
[images[4], images[5], images[7], images[11]],
]
const titles = [
'山间晨雾', '海岸余晖', '林深见鹿', '星河滚烫',
'秋日私语', '雪落无声', '荒漠孤烟', '江南水乡',
'云海翻涌', '古城旧梦', '樱花吹雪', '冰川纪行',
]
const descriptions = [
'清晨第一缕阳光穿透薄雾,洒在青翠的山谷间',
'海浪轻抚沙滩,余晖将天际染成珊瑚般的暖色调',
'密林深处,光影斑驳,万物有灵且美',
'浩瀚星空下,篝火噼啪作响,银河横亘天际',
'金黄落叶铺满小径,秋风携来果实成熟的芬芳',
'白雪覆盖大地,万物归于宁静与纯粹',
'大漠无垠,风沙雕刻出千年的纹路',
'小桥流水,烟雨朦胧中白墙黛瓦静静伫立',
'云层如海浪般在山巅之下翻涌奔腾',
'石板路上回响着千年前的足音,城墙斑驳诉说时光',
'粉色花瓣如雪般飘落,短暂而绚烂',
'万年冰层透着幽蓝的光,仿佛地球深处的呼吸',
]
const textContents = [
{ title: '关于灵感', description: '灵感从不拜访懒人。它像一只羞怯的鸟,只在你俯首案前、笔耕不辍时,悄然落在你的肩头。那些看似神来之笔的瞬间,不过是千百次练习后的水到渠成。' },
{ title: '时间之河', description: '时间是一条沉默的河流,我们都是河中的石头。水流磨平了棱角,却让内里的纹理愈发清晰。每一道纹路都是一个故事,每一次冲刷都是一次重生。' },
{ title: '留白之美', description: '最好的设计往往藏在未说之处。一幅山水画的力量不在墨色,而在无墨之间的云雾与山岚。界面如此,人生亦然——空间不是空缺,是呼吸的节拍。' },
{ title: '光与影', description: '没有阴影,光便失去了意义。创作的本质不是消除黑暗,而是让明暗相互成全。一幅好的摄影作品,阴影里藏着比亮处更多的故事。' },
{ title: '旅途随笔', description: '旅行不是从一个地方到另一个地方,而是从一种视角切换到另一种视角。同一轮明月,在不同的经度里,会映出不同的乡愁。' },
{ title: '匠人之心', description: '一把好刀,从锻打到淬火,每一步都不可省略。手艺人知道:没有什么捷径能绕过时间的沉淀。代码与器物,皆是时间的艺术。' },
{ title: '草木人间', description: '每一片叶子都是一个小小的宇宙——脉络如河流,叶肉如大地。阳光穿过叶片的时候,像是神在端详自己的指纹。我们不过是这绿色剧场里的观众。' },
{ title: '夜的独白', description: '凌晨三点,城市终于安静下来。窗外零星灯火像是失眠者彼此交换的暗号。在这万籁俱寂的时刻,思绪反而最清晰——或许黑夜才是思考者真正的白昼。' },
]
const projectTags = [
['Vue', 'TypeScript', 'Nuxt'],
['React', 'Next.js', 'Tailwind'],
['Node.js', 'PostgreSQL', 'Docker'],
['Figma', 'Design System', 'UI/UX'],
['Python', 'FastAPI', 'MongoDB'],
['Swift', 'SwiftUI', 'iOS'],
['Go', 'gRPC', 'Kubernetes'],
['Rust', 'WebAssembly', 'Canvas'],
['Three.js', 'WebGL', 'GLSL'],
['Svelte', 'SvelteKit', 'Prisma'],
['Astro', 'MDX', 'Contentful'],
['Flutter', 'Dart', 'Firebase'],
]
const typeCycle: CardType[] = [
'image-text', 'text', 'image-text', 'portfolio',
'image-text', 'image', 'image-text', 'project',
'text', 'image-text', 'portfolio', 'image-text',
'image', 'image-text', 'project', 'text',
'image-text', 'portfolio', 'image-text', 'image',
]
function genItem(globalIdx: number): CardData {
const type = typeCycle[globalIdx % typeCycle.length]
const idx = globalIdx % 12
const base: Pick<CardData, 'id' | 'type' | 'title' | 'aspectRatio'> = {
id: globalIdx + 1,
type,
title: titles[idx],
aspectRatio: 0.6 + Math.random() * 0.7,
}
switch (type) {
case 'text': {
const tc = textContents[globalIdx % textContents.length]
return { ...base, title: tc.title, description: tc.description, aspectRatio: 0.6 + Math.random() * 0.5 }
}
case 'image':
return { ...base, image: images[idx], description: undefined, aspectRatio: 0.65 + Math.random() * 0.55 }
case 'image-text':
return { ...base, image: images[idx], description: descriptions[idx], aspectRatio: 0.6 + Math.random() * 0.7 }
case 'portfolio': {
const set = portfolioSets[globalIdx % portfolioSets.length]
const ar = 0.85 + Math.random() * 0.15
return { ...base, images: set, description: descriptions[idx], aspectRatio: ar }
}
case 'project':
return {
...base,
image: images[idx],
description: descriptions[idx],
tags: projectTags[globalIdx % projectTags.length],
aspectRatio: 0.65 + Math.random() * 0.45,
}
}
}
export default defineWrappedResponseHandler(async (event) => {
const query = getQuery(event)
const page = Math.max(1, parseInt(String(query.page || '1')))
const pageSize = Math.min(30, Math.max(1, parseInt(String(query.pageSize || '12'))))
const cacheKey = `cards:list:${page}:${pageSize}`
const cached = await event.context.cache.get<{ items: CardData[], hasMore: boolean, page: number }>(cacheKey)
if (cached) return cached
const start = (page - 1) * pageSize
const items = Array.from({ length: pageSize }, (_, i) => genItem(start + i))
const total = 60
const hasMore = start + pageSize < total
const result = { items, hasMore, page }
await event.context.cache.set(cacheKey, result, 300)
return result
})