From 3c7c5d8855d5abfe3499305a7c5e8d6ccbf8423e Mon Sep 17 00:00:00 2001
From: toom1996 <23cm.cn@gmail.com>
Date: Sat, 29 Aug 2026 15:47:58 +0800
Subject: [PATCH] update
---
src/components/Index.astro | 164 ++++++++-
src/components/Item.astro | 2 +-
src/components/RunwayLooks.astro | 269 +++++++-------
src/components/StreetSnap.astro | 348 ++++++++++++++++++
src/components/StreetSnaps.astro | 490 ++++++++++++++++++++++++++
src/components/pages/article.astro | 2 +-
src/components/pages/articles.astro | 470 ------------------------
src/i18n/dictionary.ts | 14 +-
src/layouts/Layout.astro | 1 +
src/lib/api.ts | 294 +++++++++-------
src/lib/routes.ts | 4 +-
src/pages/cn/street-snaps/[id].astro | 15 +
src/pages/cn/street-snaps/index.astro | 7 +
src/pages/en/street-snaps/[id].astro | 15 +
src/pages/en/street-snaps/index.astro | 7 +
15 files changed, 1369 insertions(+), 733 deletions(-)
create mode 100644 src/components/StreetSnap.astro
create mode 100644 src/components/StreetSnaps.astro
delete mode 100644 src/components/pages/articles.astro
create mode 100644 src/pages/cn/street-snaps/[id].astro
create mode 100644 src/pages/cn/street-snaps/index.astro
create mode 100644 src/pages/en/street-snaps/[id].astro
create mode 100644 src/pages/en/street-snaps/index.astro
diff --git a/src/components/Index.astro b/src/components/Index.astro
index 456a887..175230a 100644
--- a/src/components/Index.astro
+++ b/src/components/Index.astro
@@ -5,6 +5,7 @@ import { toAbs } from "@/lib/api"
import { getI18n } from "@/i18n/utils"
import { href, ROUTES } from '@/lib/routes'
import { getSsgIndexRunway } from "@/lib/api"
+import { getSsgStreetSnapPopular } from "@/lib/api"
// 国际化:一行取当前语言 + 翻译函数
const { locale, t } = getI18n(Astro)
@@ -29,6 +30,19 @@ const galleryItems: GalleryItem[] = popular
articleId: b.article_id,
title: b.title,
}))
+
+// 首页「Street Style」热门街拍:构建期走 SSG 内部接口 /api/v1/ssg/street-snaps/popular(server-only),
+// 返回按图片数前 N 的街拍(含封面),直接烧入静态 HTML;后端不可达则静默留空(区块回退 ComingSoon)。
+const streetPopular = (await getSsgStreetSnapPopular(12)).filter((b) => b.cover && b.id)
+const streetItems: { src: string; loc: string; event: string; id: string }[] = streetPopular.map((b) => {
+ const parts = (b.title || "").split("\t")
+ return {
+ src: toAbs(b.cover || ""),
+ loc: parts[0] || b.title || "",
+ event: parts.slice(1).join(" ").trim(),
+ id: b.id,
+ }
+})
---
@@ -44,8 +58,8 @@ const galleryItems: GalleryItem[] = popular
@@ -136,9 +150,54 @@ const galleryItems: GalleryItem[] = popular
-
+
@@ -273,14 +280,13 @@ const seasonOptions: { value: string; label: string }[] = [
@@ -368,7 +374,7 @@ const seasonOptions: { value: string; label: string }[] = [
.showschecks {
- @apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin] overscroll-contain;
+ @apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
}
.showschecks li {
@apply flex items-center gap-2 py-[5px] text-[11.5px] uppercase tracking-[0.04em] text-black cursor-pointer select-none hover:bg-[#f6f6f6];
@@ -376,12 +382,14 @@ const seasonOptions: { value: string; label: string }[] = [
.showschecks li.letter-head {
@apply p-[8px_0_3px] text-[10px] tracking-[0.22em] text-[#8a8a8a] cursor-default pointer-events-none border-t border-dotted border-[#e6e6e6] first:border-t-0 first:pt-1;
}
- .showschecks input[type="checkbox"] {
- @apply appearance-none w-3 h-3 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-none inline-grid place-content-center checked:bg-black;
+ .showschecks input[type="radio"] {
+ @apply appearance-none w-3.5 h-3.5 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-full;
}
- .showschecks input[type="checkbox"]:checked::after {
- content: "";
- @apply w-[6px] h-[6px] bg-white;
+ /* 选中态由 Alpine 状态驱动(.is-checked),不依赖原生 :checked 伪类,
+ 因为 @click.prevent 会阻止原生选中,伪类不可靠;实心黑圆一眼可辨 */
+ .showschecks input[type="radio"].is-checked {
+ @apply bg-white border-black;
+ background-image: radial-gradient(circle, #000 36%, transparent 40%);
}
.showschecks .ct {
@apply text-[#999] text-[10px] ml-auto;
@@ -493,11 +501,26 @@ const seasonOptions: { value: string; label: string }[] = [
animation: hbxShim 1.6s ease infinite;
}
- .showsloadmore-wrap {
- @apply flex justify-center mt-[38px];
+ .showspager {
+ @apply flex flex-wrap items-center justify-center gap-2 mt-[38px];
}
- .showsloadmore {
- @apply border-[1.5px] border-black bg-white text-black p-[13px_36px] text-[11px] uppercase tracking-[0.22em] font-bold cursor-pointer rounded-none transition-all duration-200 hover:bg-black hover:text-white disabled:opacity-40 disabled:cursor-default;
+ .pg {
+ @apply min-w-[40px] h-[40px] px-3 border-[1.5px] border-black bg-white text-black text-[11px] uppercase tracking-[0.12em] font-bold cursor-pointer rounded-none transition-colors duration-200;
+ }
+ .pg.is-current {
+ @apply bg-black text-white cursor-default;
+ }
+ .pg:disabled {
+ @apply opacity-30 cursor-default;
+ }
+ .pg.enabled:hover {
+ @apply bg-black text-white;
+ }
+ .pg-ellipsis {
+ @apply px-1 text-[#999] select-none text-[11px];
+ }
+ .pg-wrap {
+ @apply inline-flex items-center;
}
.showsempty {
@@ -546,6 +569,9 @@ const seasonOptions: { value: string; label: string }[] = [
.showsmodal-input {
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
}
+ .showsmodal-tip {
+ @apply mt-2 text-[11px] leading-snug text-black/45 font-mono;
+ }
.showsmodal-az {
@apply flex flex-wrap gap-1 px-4 pb-3 shrink-0;
}
@@ -573,6 +599,13 @@ const seasonOptions: { value: string; label: string }[] = [
.showsmodal-row {
@apply flex items-center gap-2 w-full cursor-pointer px-3 py-2 text-[13px] transition-colors hover:bg-black/5;
}
+ .showsmodal-row input[type="radio"] {
+ @apply appearance-none w-4 h-4 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-full;
+ }
+ .showsmodal-row input[type="radio"].is-checked {
+ @apply bg-white border-black;
+ background-image: radial-gradient(circle, #000 36%, transparent 40%);
+ }
.showsmodal-name {
@apply flex-1 truncate;
}
@@ -616,10 +649,10 @@ const seasonOptions: { value: string; label: string }[] = [
season: localStorage.getItem("showsg-season") === "1",
year: localStorage.getItem("showsg-year") === "1",
},
- selectedBrands: [] as number[],
- selectedCt: [] as string[],
- selectedSeason: [] as string[],
- selectedYear: [] as string[],
+ selectedBrand: null as (string | null),
+ selectedCt: "" as string,
+ selectedSeason: "" as string,
+ selectedYear: "" as string,
// 品牌筛选弹窗状态
modalOpen: false,
@@ -627,91 +660,74 @@ const seasonOptions: { value: string; label: string }[] = [
modalLetter: "",
modalResults: [] as BrandEntry[],
modalLoading: false,
- modalSelected: [] as number[],
- get modalMaxReached() {
- return this.modalSelected.length >= 1
- },
+ modalSelected: null as (string | null),
items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
loading: false,
- loadingMore: false,
page: 1,
- size: 24,
- total: 0,
- isEnd: false,
+ lastPage: 1,
reqToken: 0,
init() {
this.locale = this.$el.dataset.locale || 'en';
- this.loadPage(1, false)
+ this.loadPage(1)
},
- // 骨架屏数量与首屏 data 数量一致,保证「加载中」与「数据完成」两阶段的网格高度相同,消除抖动
+ // 骨架屏数量与首屏每页条数一致(后端固定 ArticleFixedSize=24),消除加载抖动
get skeletonCount() {
- return this.size
+ return 24
},
// 已筛选选项 Pills 视图数据
get pills() {
const list: { label: string; clear: () => void }[] = []
- if (this.selectedBrands.length) {
+ if (this.selectedBrand) {
const findName = (id: any) => this.hotBrands.find((b) => b.id === id)?.name ?? this.modalResults.find((b) => b.id === id)?.name ?? `#${id}`
- const labels = this.selectedBrands
- .map((id) => findName(id))
- .slice(0, 4)
- const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : ""
+ const name = findName(this.selectedBrand)
list.push({
- label: "DESIGNER · " + labels.join(", ") + more,
- clear: () => { this.selectedBrands = []; this.refreshFilter(); }
+ label: "DESIGNER · " + name,
+ clear: () => { this.selectedBrand = null; this.refreshFilter(); }
})
}
- if (this.selectedCt.length) {
+ if (this.selectedCt) {
list.push({
- label: "COLLECTION · " + this.selectedCt.join("+").toUpperCase(),
- clear: () => { this.selectedCt = []; this.refreshFilter(); }
+ label: "COLLECTION · " + this.selectedCt.toUpperCase(),
+ clear: () => { this.selectedCt = ""; this.refreshFilter(); }
})
}
- if (this.selectedSeason.length) {
+ if (this.selectedSeason) {
list.push({
- label: "SEASON · " + this.selectedSeason.join("+").toUpperCase(),
- clear: () => { this.selectedSeason = []; this.refreshFilter(); }
+ label: "SEASON · " + this.selectedSeason.toUpperCase(),
+ clear: () => { this.selectedSeason = ""; this.refreshFilter(); }
})
}
- if (this.selectedYear.length) {
+ if (this.selectedYear) {
list.push({
- label: "YEAR · " + [...this.selectedYear].sort().join("+"),
- clear: () => { this.selectedYear = []; this.refreshFilter(); }
+ label: "YEAR · " + this.selectedYear,
+ clear: () => { this.selectedYear = ""; this.refreshFilter(); }
})
}
return list
},
- get loadBtnText() {
- if (this.loadingMore) return "Loading…"
- if (this.isEnd) return "— End of Archive —"
- return "Load More ↓"
- },
-
// 事件处理方法
toggleGroup(group: "designers" | "collection" | "season" | "year") {
this.collapsedGroups[group] = !this.collapsedGroups[group]
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
},
- toggleBrand(id: number) {
- const idx = this.selectedBrands.indexOf(id)
- if (idx >= 0) this.selectedBrands.splice(idx, 1)
- else this.selectedBrands.push(id)
- this.refreshFilter()
+ refreshFilter() {
+ this.loadPage(1)
},
- refreshFilter() {
- this.loadPage(1, false)
+ toggleBrand(id: string) {
+ this.selectedBrand = this.selectedBrand === id ? null : id
+ this.refreshFilter()
},
// ===== 品牌筛选弹窗方法 =====
async openBrandModal() {
- this.modalSelected = [...this.selectedBrands]
+ this.modalSelected = this.selectedBrand
this.modalKeyword = ""
this.modalLetter = "HOT"
this.modalOpen = true
@@ -730,7 +746,7 @@ const seasonOptions: { value: string; label: string }[] = [
this.modalLetter = "HOT"
this.modalLoading = true
try {
- this.modalResults = this.hotBrands.map((b) => ({ id: b.id, name: b.name, article_count: b.article_count || 0 }))
+ this.modalResults = this.hotBrands.map((b) => ({ id: b.id, name: b.name }))
} finally {
this.modalLoading = false
}
@@ -742,7 +758,7 @@ const seasonOptions: { value: string; label: string }[] = [
this.modalLoading = true
this.modalResults = []
try {
- this.modalResults = await getBrands({ keyword: kw, size: 60 })
+ this.modalResults = await getBrands({ keyword: kw })
} catch {
this.modalResults = []
} finally {
@@ -766,23 +782,15 @@ const seasonOptions: { value: string; label: string }[] = [
this.modalResults = []
try {
const letter = l === "#" ? "OTHER" : l
- this.modalResults = await getBrands({ letter, size: 200 })
+ this.modalResults = await getBrands({ letter })
} catch {
this.modalResults = []
} finally {
this.modalLoading = false
}
},
- toggleModalBrand(id: number) {
- const idx = this.modalSelected.indexOf(id)
- if (idx >= 0) this.modalSelected.splice(idx, 1)
- else {
- if (this.modalSelected.length >= 5) return
- this.modalSelected.push(id)
- }
- },
confirmBrandModal() {
- this.selectedBrands = [...this.modalSelected]
+ this.selectedBrand = this.modalSelected
this.modalOpen = false
document.documentElement.style.overflow = ""
document.documentElement.style.paddingRight = ""
@@ -790,54 +798,73 @@ const seasonOptions: { value: string; label: string }[] = [
},
clearAll() {
- this.selectedBrands = []
- this.selectedCt = []
- this.selectedSeason = []
- this.selectedYear = []
+ this.selectedBrand = null
+ this.selectedCt = ""
+ this.selectedSeason = ""
+ this.selectedYear = ""
this.refreshFilter()
},
- // API 加载
- async loadPage(page: number, append = false) {
+ // API 加载(分页:每次整页替换,不追加)
+ async loadPage(page: number) {
const myToken = ++this.reqToken
- if (append) {
- this.loadingMore = true
- } else {
- this.loading = true
- this.items = []
- }
+ this.loading = true
+ this.items = []
try {
const json = await getArticles({
page,
- size: this.size,
sort: "newest",
- brand_ids: this.selectedBrands,
- collection_type: this.selectedCt,
- season: this.selectedSeason,
- year: this.selectedYear,
- with_images: 6,
+ brandId: this.selectedBrand || undefined,
+ collection: this.selectedCt || undefined,
+ season: this.selectedSeason || undefined,
+ year: this.selectedYear || undefined,
})
if (myToken !== this.reqToken) return
const rows = json.data.map((item: ArticleItem) => ({ ...item, _imgLoaded: false }))
- this.total = json.total
+ this.lastPage = json.last_page
this.page = page
- this.items = append ? [...this.items, ...rows] : rows
-
- this.isEnd = page >= json.last_page || rows.length < this.size
+ this.items = rows
} catch {
if (myToken !== this.reqToken) return
- if (!append) this.items = []
+ this.items = []
} finally {
- if (myToken === this.reqToken) {
- this.loading = false
- this.loadingMore = false
- }
+ if (myToken === this.reqToken) this.loading = false
}
},
+ // ===== 分页导航 =====
+ // 生成窗口化的页码列表:始终含首尾页,当前页 ±1,其余用 "..." 折叠。
+ get pageList() {
+ const total = this.lastPage
+ const cur = this.page
+ if (total <= 7) {
+ return Array.from({ length: total }, (_: any, i: number) => i + 1)
+ }
+ const pages: (number | "...")[] = [1]
+ const start = Math.max(2, cur - 1)
+ const end = Math.min(total - 1, cur + 1)
+ if (start > 2) pages.push("...")
+ for (let i = start; i <= end; i++) pages.push(i)
+ if (end < total - 1) pages.push("...")
+ pages.push(total)
+ return pages
+ },
+
+ goPage(p: number) {
+ if (this.loading) return
+ if (p < 1 || p > this.lastPage || p === this.page) return
+ this.loadPage(p)
+ const main = this.$el.querySelector(".showsmain")
+ if (main) main.scrollIntoView({ behavior: "smooth", block: "start" })
+ else window.scrollTo({ top: 0, behavior: "smooth" })
+ },
+
+ prevPage() { this.goPage(this.page - 1) },
+ nextPage() { this.goPage(this.page + 1) },
+
// 辅助辅助工具函数
toAbsUrl(url: string) { return toAbs(url) },
diff --git a/src/components/StreetSnap.astro b/src/components/StreetSnap.astro
new file mode 100644
index 0000000..e3a470e
--- /dev/null
+++ b/src/components/StreetSnap.astro
@@ -0,0 +1,348 @@
+---
+import type { RawStreetSnap } from "@/lib/api"
+import Breadcrumb from "@/components/Breadcrumb.astro"
+import { getI18n } from "@/i18n/utils"
+import { href, ROUTES } from '@/lib/routes'
+import { ssrBase } from "@/lib/api"
+
+interface Props {
+ snap: RawStreetSnap | null
+ error?: "missing" | "failed" | null
+}
+
+const { snap, error = null } = Astro.props
+const { locale, t } = getI18n(Astro)
+
+const full = (u: string): string => {
+ if (!u) return ""
+ if (/^https?:\/\//i.test(u)) return u
+ return ssrBase().replace(/\/$/, "") + u
+}
+
+const images = (snap?.images ?? [])
+ .map((im) => ({
+ id: im.id,
+ url: full(im.image || ""),
+ name: im.name || "",
+ }))
+ .filter((im) => im.url)
+
+const homeHref = href(locale, ROUTES.home)
+const archiveHref = href(locale, ROUTES.streetSnaps)
+
+// theimpression 标题含字面 \t:地点 \t 场次,列表/详情统一以 " · " 呈现
+const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
+---
+
+
+ {
+ snap ? (
+
+
+ {displayTitle}
+
+
+
+ {displayTitle}
+
+
+ {images.length > 0 && (
+
+
+ {images.map((im, i) => (
+
+
+
+ {im.name && (
+
+ {im.name}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ ) : (
+
+
+ {error === "missing" ? t('No street snap ID specified') : t('Failed to load street snap. Please refresh or try later.')}
+
+
+
+ ← {t('Back to Index')}
+
+
+ )
+ }
+
+ {snap && images.length > 0 && (
+
+
+
+
+
+
+
+ {images.map((im, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+ {t('Click image to zoom · Scroll to view · Esc to close')}
+
+
+ )}
+
+
+
+
+
diff --git a/src/components/StreetSnaps.astro b/src/components/StreetSnaps.astro
new file mode 100644
index 0000000..75a00a6
--- /dev/null
+++ b/src/components/StreetSnaps.astro
@@ -0,0 +1,490 @@
+---
+import Breadcrumb from "@/components/Breadcrumb.astro"
+import { getI18n } from '@/i18n/utils'
+import { clientHref, ROUTES } from '@/lib/routes'
+import { toAbs } from '@/lib/api'
+
+const { locale, t } = getI18n(Astro)
+
+// 年份筛选:近 10 年(含当年),末项兜底
+const currentYear = new Date().getFullYear()
+const yearFloor = currentYear - 9
+const yearOptions: { value: number | string; label: string }[] = []
+for (let y = currentYear; y >= yearFloor; y--) {
+ yearOptions.push({ value: y, label: String(y) })
+}
+yearOptions.push({ value: `before${yearFloor}`, label: `${yearFloor}年前` })
+
+// 排序选项: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') },
+]
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No street snaps match the current filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/pages/article.astro b/src/components/pages/article.astro
index bbbe4b1..28994ec 100644
--- a/src/components/pages/article.astro
+++ b/src/components/pages/article.astro
@@ -1,5 +1,5 @@
---
-import type { RawArticle } from "@/lib/data"
+import type { RawArticle } from "@/lib/api"
import Breadcrumb from "@/components/Breadcrumb.astro"
import { ssrBase } from "@/lib/api"
diff --git a/src/components/pages/articles.astro b/src/components/pages/articles.astro
deleted file mode 100644
index 8f20a4d..0000000
--- a/src/components/pages/articles.astro
+++ /dev/null
@@ -1,470 +0,0 @@
----
-import { getReports, fmtDate } from "@/lib/data"
-import { href, ROUTES } from '@/lib/routes'
-import { getI18n } from "@/i18n/utils"
-
-// 文章展示创意布局页:复用你现有的 Layout 外壳。
-// 数据由数据层 getReports() 统一从后端公开接口拉取(见 src/lib/data.ts);
-// 后端不可达时回落到内置示例,非对称创意布局结构完全不变。
-
-const reports = await getReports(30)
-const fmt = fmtDate
-const { locale } = getI18n(Astro)
-const itemBase = href(locale, "/items")
----
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/i18n/dictionary.ts b/src/i18n/dictionary.ts
index 5c3994a..6e712d5 100644
--- a/src/i18n/dictionary.ts
+++ b/src/i18n/dictionary.ts
@@ -96,7 +96,19 @@ export const dict: Record
= {
'language': { cn: '语言' },
- 'runway looks': { cn: '秀场造型' }
+ 'runway looks': { cn: '秀场造型' },
+
+ // ── 街拍 / Street Snaps ──
+
+ 'street style': { cn: '街拍' },
+
+ 'street snaps': { cn: '街拍' },
+
+ 'all street snaps': { cn: '全部街拍' },
+
+ 'no street snap id specified': { cn: '未指定街拍 ID' },
+
+ 'failed to load street snap. please refresh or try later.': { cn: '加载街拍失败,请刷新或稍后再试' }
};
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index edb1434..06d7821 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -88,6 +88,7 @@ const headers = {