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.
153 lines
7.5 KiB
153 lines
7.5 KiB
import { getCache, setCache } from "#server/utils/context";
|
|
|
|
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 typeCycleLength = typeCycle.length
|
|
const type = typeCycle[globalIdx % typeCycleLength] ?? 'text'
|
|
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]
|
|
if (!tc) return { ...base, title: '默认文本', description: undefined, aspectRatio: 0.6 + Math.random() * 0.5 }
|
|
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]
|
|
if (!set) return { ...base, images: [], description: undefined, aspectRatio: 0.85 }
|
|
const ar = 0.85 + Math.random() * 0.15
|
|
return { ...base, images: set as string[], 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,
|
|
}
|
|
default:
|
|
return { ...base, title: '默认卡片', description: undefined, aspectRatio: 0.6 }
|
|
}
|
|
}
|
|
|
|
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 getCache<{ 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 setCache(cacheKey, result, 300)
|
|
return result
|
|
})
|
|
|