Files
frontend_v2/src/components/views/StreetSnaps.astro
toom1996 a8fcafac10 update
2026-09-22 21:39:45 +08:00

154 lines
6.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import CollectionListView from "@/components/CollectionListView.astro"
import { getI18n } from '@/lib/i18n'
import { buildYearOptions } from "@/lib/filters"
const { t } = getI18n(Astro)
// 年份筛选:近 10 年(含当年),末项兜底(与走秀共用 lib/filters.ts)
const yearOptions = buildYearOptions()
// 排序选项:image_count 默认(按图片数降序)/ year_desc / year_asc
const sortOptions: { value: string; label: string }[] = [
{ value: "image_count", label: t('Most photos') },
{ value: "year_desc", label: t('Newest year') },
{ value: "year_asc", label: t('Oldest year') },
]
// 地区筛选:值即后端 street_snap.city 列存储的规范英文名(与 backfill_city 词典保持一致)。
// 标签用 t(city) —— 英文环境回退显示英文(巴黎→Paris),中文环境显示中文(Paris→巴黎)。
const cityOptions: { value: string; label: string }[] = [
{ value: "Paris", label: t('Paris') },
{ value: "New York", label: t('New York') },
{ value: "Milan", label: t('Milan') },
{ value: "London", label: t('London') },
{ value: "Copenhagen", label: t('Copenhagen') },
{ value: "Florence", label: t('Florence') },
{ value: "Berlin", label: t('Berlin') },
{ value: "Tokyo", label: t('Tokyo') },
{ value: "Seoul", label: t('Seoul') },
{ value: "Shanghai", label: t('Shanghai') },
{ value: "Beijing", label: t('Beijing') },
{ value: "Rome", label: t('Rome') },
{ value: "Madrid", label: t('Madrid') },
{ value: "Amsterdam", label: t('Amsterdam') },
{ value: "Antwerp", label: t('Antwerp') },
{ value: "Los Angeles", label: t('Los Angeles') },
{ value: "Moscow", label: t('Moscow') },
{ value: "Mumbai", label: t('Mumbai') },
]
// 通用筛选分组(year/sort/city)
const streetGroups = [
{ key: "year", title: t('Year'), options: yearOptions, selected: "selectedYear" },
{ key: "sort", title: t('Sort'), options: sortOptions, selected: "selectedSort" },
{ key: "city", title: t('City'), options: cityOptions, selected: "selectedCity" },
]
---
<CollectionListView
prefix="snaps"
alpineName="streetSnapsPage"
title={t('All Street Snaps')}
groups={streetGroups}
skeletonLines={2}
emptyText="No street snaps match the current filters"
/>
<script>
import { getStreetSnaps, type StreetSnapItem } from "@/lib/api"
import { makeCardSlots } from "@/lib/ui"
import { createCollectionPage } from "@/lib/collection-page"
const DEFAULT_SORT = "image_count"
document.addEventListener("alpine:init", () => {
Alpine.data("streetSnapsPage", createCollectionPage({
favType: "street",
skeletonCount: 4,
scrollSelector: ".snapsmain",
fetch: (self, page) =>
getStreetSnaps({
page,
year: self.selectedYear || undefined,
city: self.selectedCity || undefined,
sort: self.selectedSort,
}),
onInit: (self) => {
// 支持从 header 下拉深链 ?city=xxx 直接进入对应地区(与侧栏 City 筛选一致)
const city = new URLSearchParams(window.location.search).get("city")
const allowed = (self.cityOptions || []).map((c: any) => c.value)
if (city && allowed.includes(city)) self.selectedCity = city
},
extra: {
yearOptions: [] as { value: number | string; label: string }[],
sortOptions: [] as { value: string; label: string }[],
cityOptions: [] as { value: string; label: string }[],
selectedYear: "" as string,
selectedSort: DEFAULT_SORT as string,
selectedCity: "" as string,
// 侧栏分组折叠状态:默认全部收起(不做持久化,每次进页都是关着的)
collapsedGroups: { year: true, sort: true, city: true } as Record<string, boolean>,
clearAll() {
this.selectedYear = ""
this.selectedSort = DEFAULT_SORT
this.selectedCity = ""
this.loadPage(1)
},
// 已激活筛选 chips(顶部 pills + 侧栏 Clear all 的可见性都依赖它)
// 必须是方法:createCollectionPage 用对象展开返回,getter 会在展开时被求值冻结成静态值,模板里写 pills()
pills() {
const out: { label: string; clear: () => void }[] = []
if (this.selectedYear) {
const hit = this.yearOptions.find((o: any) => String(o.value) === String(this.selectedYear))
out.push({
label: "YEAR · " + (hit ? hit.label : this.selectedYear),
clear: () => { this.selectedYear = ""; this.loadPage(1) },
})
}
if (this.selectedSort !== DEFAULT_SORT) {
const hit = this.sortOptions.find((o: any) => o.value === this.selectedSort)
out.push({
label: "SORT · " + (hit ? hit.label : this.selectedSort),
clear: () => { this.selectedSort = DEFAULT_SORT; this.loadPage(1) },
})
}
if (this.selectedCity) {
const hit = this.cityOptions.find((o: any) => o.value === this.selectedCity)
out.push({
label: "CITY · " + (hit ? hit.label : this.selectedCity),
clear: () => { this.selectedCity = ""; this.loadPage(1) },
})
}
return out
},
// 卡片右侧缩略图条带:固定 5 个槽位,缺图留 placeholder
// 街拍 images 是 { image, name }[](与 runway 的 string[] gallery 不同),先抽成纯 URL 数组
getCardSlots(it: StreetSnapItem) {
// 缩略图条带跳过主图(it.cover 即 images[0]),从第二张起,避免首张缩略图与主图重复。
const imgs = (it.images || []).map((im: any) => (im && (im.image || im)) || "")
return makeCardSlots(imgs)
},
// theimpression 标题含字面 \t:地点 \t 场次,拆成 [location, event]
titleParts(it: StreetSnapItem): string[] {
const parts = (it.title || "").split("\t")
return [parts[0] || it.title || "", parts.slice(1).join(" ").trim()]
},
// 街拍:标题行取地点,副标题行取场次
cardTitle(it: StreetSnapItem): string {
return this.titleParts(it)[0]
},
cardMeta(it: StreetSnapItem): string {
return this.titleParts(it)[1]
},
},
}))
})
</script>