@ -220,7 +220,8 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
|
||||
<!-- Card List -->
|
||||
<template x-for="it in items" :key="it.id">
|
||||
<a class="showscard" :href="itemHref(it)" :aria-label="`${it.brand_name} ${it.title}`">
|
||||
<div class="showscard-wrap relative group">
|
||||
<a class="showscard" :href="itemHref(it)" :aria-label="`${it.brand_name} ${it.title}`">
|
||||
<div class="showscard-left">
|
||||
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||
@ -241,7 +242,11 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
<button type="button" class="fav-toggle" :class="{ 'is-on': favState(it.id) }" @click.prevent="toggleFav(it)" :aria-pressed="favState(it.id)" :title="favState(it.id) ? 'Saved' : 'Save'">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21C12 21 4 14.5 4 8.8C4 6.1 6.1 4 8.7 4C10.3 4 11.5 4.8 12 6C12.5 4.8 13.7 4 15.3 4C17.9 4 20 6.1 20 8.8C20 14.5 12 21 12 21Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@ -298,6 +303,8 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
import { getArticles, getBrands, toAbs, getUser, login, type ArticleItem, type BrandEntry } from "@/lib/api"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/looks-grid"
|
||||
import { toggleFavorite, checkFavorited, getFavorites } from "@/lib/favorites"
|
||||
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("showsPage", () => {
|
||||
@ -334,6 +341,9 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
lastPage: 1,
|
||||
reqToken: 0,
|
||||
|
||||
// 收藏态:响应式 id 映射(卡片爱心从此读取;按当前页服务端校验,与收藏总量解耦)
|
||||
favMap: {} as Record<string, boolean>,
|
||||
|
||||
// 登录门禁弹窗状态(硬门禁:未登录点筛选/翻页时弹出内联登录表单)
|
||||
showLoginModal: false,
|
||||
loginModalAccount: "",
|
||||
@ -348,7 +358,9 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
const ALLOWED = ['rtw', 'menswear', 'womenswear', 'couture', 'resort', 'pre_fall', 'bridal', 'swimwear']
|
||||
if (ct && ALLOWED.includes(ct)) this.selectedCt = ct
|
||||
// 首页(含筛选后的首页)公开:未登录也直接加载首屏,不弹登录框
|
||||
// 收藏态在 loadPage 内按当前页校正(markFavs),与收藏总量解耦,10万也常数级
|
||||
this.loadPage(1)
|
||||
// 浏览历史:改为「点开文章详情」时记录(见 item 详情页),列表页不再记。
|
||||
},
|
||||
|
||||
// 骨架屏数量与首屏每页条数一致(后端固定 ArticleFixedSize=24),消除加载抖动
|
||||
@ -533,6 +545,7 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
this.lastPage = json.last_page
|
||||
this.page = page
|
||||
this.items = rows
|
||||
this.markFavs() // 当前页可见 id 走服务端校验收藏态(与收藏总量解耦)
|
||||
} catch {
|
||||
if (myToken !== this.reqToken) return
|
||||
this.items = []
|
||||
@ -577,7 +590,40 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
return makeCardSlots(it.gallery || [], 5, {
|
||||
moreLabel: () => (total > 30 ? "30+" : `${total}+`),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 收藏(图片收藏入口)=====
|
||||
// 按当前页可见 id 拉服务端收藏态:本地先即时填充,登录态异步用服务端校正。
|
||||
// 复杂度仅与「一屏多少张」相关,与用户收藏总量解耦,即便 10 万收藏也常数级。
|
||||
async markFavs() {
|
||||
const ids = this.items.map((i: any) => String(i.id))
|
||||
if (!ids.length) return
|
||||
// 本地即时填充(同设备秒显)
|
||||
const m: Record<string, boolean> = { ...this.favMap }
|
||||
for (const f of getFavorites()) {
|
||||
const id = String(f.id)
|
||||
if (!(id in m)) m[id] = true
|
||||
}
|
||||
this.favMap = m
|
||||
if (!getUser()) return
|
||||
try {
|
||||
const set = await checkFavorited(ids)
|
||||
const m2: Record<string, boolean> = { ...this.favMap }
|
||||
for (const id of ids) m2[id] = set.has(id)
|
||||
this.favMap = m2
|
||||
} catch {
|
||||
/* 失败维持本地态 */
|
||||
}
|
||||
},
|
||||
favState(id: string) { return !!this.favMap[String(id)] },
|
||||
toggleFav(it: any) {
|
||||
if (!getUser()) { this.showLoginModal = true; return false }
|
||||
const id = String(it.id)
|
||||
const now = !this.favState(id)
|
||||
toggleFavorite({ id, type: 'runway', title: it.title || '', cover: it.cover || '', brand: it.brand_name || '' })
|
||||
this.favMap = { ...this.favMap, [id]: now }
|
||||
return now
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -180,7 +180,8 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
<template x-if="!loading && items.length > 0">
|
||||
<div class="snapsgrid">
|
||||
<template x-for="it in items" :key="it.id">
|
||||
<a class="snapscard" :href="itemHref(it)" :aria-label="cardTitle(it)">
|
||||
<div class="showscard-wrap relative group">
|
||||
<a class="snapscard" :href="itemHref(it)" :aria-label="cardTitle(it)">
|
||||
<div class="snapscard-left">
|
||||
<div class="snapscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||
<span class="snapsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||
@ -201,7 +202,11 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
<button type="button" class="fav-toggle" :class="{ 'is-on': favState(it.id) }" @click.prevent="toggleFav(it)" :aria-pressed="favState(it.id)" :title="favState(it.id) ? 'Saved' : 'Save'">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21C12 21 4 14.5 4 8.8C4 6.1 6.1 4 8.7 4C10.3 4 11.5 4.8 12 6C12.5 4.8 13.7 4 15.3 4C17.9 4 20 6.1 20 8.8C20 14.5 12 21 12 21Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -256,6 +261,8 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
import { getStreetSnaps, toAbs, getUser, login, type StreetSnapItem } from "@/lib/api"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/looks-grid"
|
||||
import { toggleFavorite, checkFavorited, getFavorites } from "@/lib/favorites"
|
||||
|
||||
|
||||
const DEFAULT_SORT = "image_count"
|
||||
|
||||
@ -290,6 +297,9 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
loginModalError: "",
|
||||
loginModalLoading: false,
|
||||
|
||||
// 收藏态:响应式 id 映射(卡片爱心从此读取;按当前页服务端校验,与收藏总量解耦)
|
||||
favMap: {} as Record<string, boolean>,
|
||||
|
||||
init() {
|
||||
this.locale = this.$el.dataset.locale || 'en'
|
||||
// 支持从 header 下拉深链 ?city=xxx 直接进入对应地区(与侧栏 City 筛选一致)
|
||||
@ -297,7 +307,9 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
const allowed = (this.cityOptions || []).map((c: any) => c.value)
|
||||
if (city && allowed.includes(city)) this.selectedCity = city
|
||||
// 首页公开:未登录也直接加载首屏,不弹登录框
|
||||
// 收藏态在 loadPage 内按当前页校正(markFavs),与收藏总量解耦,10万也常数级
|
||||
this.loadPage(1)
|
||||
// 浏览历史:改为「点开文章详情」时记录(见 item 详情页),列表页不再记。
|
||||
},
|
||||
|
||||
// ===== 侧栏筛选 =====
|
||||
@ -390,6 +402,7 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
this.lastPage = json.last_page
|
||||
this.page = page
|
||||
this.items = rows
|
||||
this.markFavs() // 当前页可见 id 走服务端校验收藏态(与收藏总量解耦)
|
||||
} catch {
|
||||
if (myToken !== this.reqToken) return
|
||||
this.items = []
|
||||
@ -426,6 +439,39 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
cardTitle(it: StreetSnapItem): string {
|
||||
return (it.title || "").replace(/\t/g, " · ")
|
||||
},
|
||||
|
||||
// ===== 收藏(图片收藏入口)=====
|
||||
// 按当前页可见 id 拉服务端收藏态:本地先即时填充,登录态异步用服务端校正。
|
||||
// 复杂度仅与「一屏多少张」相关,与用户收藏总量解耦,即便 10 万收藏也常数级。
|
||||
async markFavs() {
|
||||
const ids = this.items.map((i: any) => String(i.id))
|
||||
if (!ids.length) return
|
||||
// 本地即时填充(同设备秒显)
|
||||
const m: Record<string, boolean> = { ...this.favMap }
|
||||
for (const f of getFavorites()) {
|
||||
const id = String(f.id)
|
||||
if (!(id in m)) m[id] = true
|
||||
}
|
||||
this.favMap = m
|
||||
if (!getUser()) return
|
||||
try {
|
||||
const set = await checkFavorited(ids)
|
||||
const m2: Record<string, boolean> = { ...this.favMap }
|
||||
for (const id of ids) m2[id] = set.has(id)
|
||||
this.favMap = m2
|
||||
} catch {
|
||||
/* 失败维持本地态 */
|
||||
}
|
||||
},
|
||||
favState(id: string) { return !!this.favMap[String(id)] },
|
||||
toggleFav(it: any) {
|
||||
if (!getUser()) { this.showLoginModal = true; return false }
|
||||
const id = String(it.id)
|
||||
const now = !this.favState(id)
|
||||
toggleFavorite({ id, type: 'street', title: it.title || '', cover: it.cover || '', brand: (it as any).brand || '' })
|
||||
this.favMap = { ...this.favMap, [id]: now }
|
||||
return now
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user