154 lines
6.3 KiB
Plaintext
154 lines
6.3 KiB
Plaintext
---
|
||
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>
|