perf(listings): 优化详情页图片加载链路支持公开变体与分级秒开
This commit is contained in:
@@ -60,7 +60,17 @@ func (h *Handler) Screenshot(c *gin.Context) {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
}
|
||||
h.writePublicObject(c, filemodule.ImageVariantFallbackKeys(key, filemodule.ImageVariantMedium)...)
|
||||
variant := c.Query("variant")
|
||||
var fallbackKeys []string
|
||||
switch variant {
|
||||
case "thumb":
|
||||
fallbackKeys = filemodule.ImageVariantFallbackKeys(key, filemodule.ImageVariantThumb)
|
||||
case "raw":
|
||||
fallbackKeys = []string{key}
|
||||
default:
|
||||
fallbackKeys = filemodule.ImageVariantFallbackKeys(key, filemodule.ImageVariantMedium)
|
||||
}
|
||||
h.writePublicObject(c, fallbackKeys...)
|
||||
}
|
||||
|
||||
func (h *Handler) writePublicObject(c *gin.Context, keys ...string) {
|
||||
|
||||
@@ -65,15 +65,71 @@ func sellerListings(items []ListingDTO) []ListingDTO {
|
||||
}
|
||||
|
||||
func applyPublicListingURLs(item *ListingDTO) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
item.SourceChannel = ""
|
||||
item.IsExternalUpload = false
|
||||
rawScreenshotURLs := item.ScreenshotURLS
|
||||
item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus)
|
||||
if item.AssetSummary != nil {
|
||||
delete(item.AssetSummary, "import_meta")
|
||||
delete(item.AssetSummary, "price_breakdown")
|
||||
applyPublicScreenshotGroups(item.ID, item.AssetSummary, rawScreenshotURLs, item.ScreenshotURLS)
|
||||
}
|
||||
}
|
||||
|
||||
func applyPublicScreenshotGroups(listingID uint64, assetSummary map[string]any, rawURLs []string, publicURLs []string) {
|
||||
rawGroups, ok := assetSummary["screenshot_groups"].(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
urlMap := make(map[string]string, len(rawURLs))
|
||||
keyMap := make(map[string]string, len(rawURLs))
|
||||
for i, rawURL := range rawURLs {
|
||||
if i < len(publicURLs) {
|
||||
urlMap[rawURL] = publicURLs[i]
|
||||
if key := extractListingObjectKey(rawURL); key != "" {
|
||||
keyMap[key] = publicURLs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
newGroups := make(map[string]any, len(rawGroups))
|
||||
for groupKey, groupVal := range rawGroups {
|
||||
switch list := groupVal.(type) {
|
||||
case []string:
|
||||
converted := make([]string, len(list))
|
||||
for i, u := range list {
|
||||
if pub, found := urlMap[u]; found {
|
||||
converted[i] = pub
|
||||
} else if key := extractListingObjectKey(u); key != "" && keyMap[key] != "" {
|
||||
converted[i] = keyMap[key]
|
||||
} else {
|
||||
converted[i] = u
|
||||
}
|
||||
}
|
||||
newGroups[groupKey] = converted
|
||||
case []any:
|
||||
converted := make([]string, len(list))
|
||||
for i, v := range list {
|
||||
if s, ok := v.(string); ok {
|
||||
if pub, found := urlMap[s]; found {
|
||||
converted[i] = pub
|
||||
} else if key := extractListingObjectKey(s); key != "" && keyMap[key] != "" {
|
||||
converted[i] = keyMap[key]
|
||||
} else {
|
||||
converted[i] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
newGroups[groupKey] = converted
|
||||
default:
|
||||
newGroups[groupKey] = groupVal
|
||||
}
|
||||
}
|
||||
assetSummary["screenshot_groups"] = newGroups
|
||||
}
|
||||
|
||||
func applySellerListingPrice(item *ListingDTO) {
|
||||
if item == nil {
|
||||
return
|
||||
|
||||
@@ -672,3 +672,30 @@ func containsString(values []string, target string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestApplyPublicScreenshotGroups(t *testing.T) {
|
||||
rawURLs := []string{
|
||||
"/api/files/object?key=listing/coin.png",
|
||||
"/api/files/object?key=listing/skin1.png",
|
||||
}
|
||||
publicURLs := []string{
|
||||
"/api/listings/88/screenshots/0",
|
||||
"/api/listings/88/screenshots/1",
|
||||
}
|
||||
summary := map[string]any{
|
||||
"screenshot_groups": map[string]any{
|
||||
"coin": []any{"/api/files/object?key=listing/coin.png"},
|
||||
"skin": []string{"/api/files/object?key=listing/skin1.png"},
|
||||
},
|
||||
}
|
||||
applyPublicScreenshotGroups(88, summary, rawURLs, publicURLs)
|
||||
groups := summary["screenshot_groups"].(map[string]any)
|
||||
coinList := groups["coin"].([]string)
|
||||
if len(coinList) != 1 || coinList[0] != "/api/listings/88/screenshots/0" {
|
||||
t.Fatalf("expected /api/listings/88/screenshots/0, got %#v", coinList)
|
||||
}
|
||||
skinList := groups["skin"].([]string)
|
||||
if len(skinList) != 1 || skinList[0] != "/api/listings/88/screenshots/1" {
|
||||
t.Fatalf("expected /api/listings/88/screenshots/1, got %#v", skinList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ import {
|
||||
readAssetString,
|
||||
getRatioValue,
|
||||
isTopTierInsurance,
|
||||
getScreenshotThumbURL,
|
||||
getScreenshotMediumURL,
|
||||
getScreenshotRawURL,
|
||||
type ListingDisplayResource,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
|
||||
@@ -165,7 +168,9 @@ const detailScreenshots = computed(() => {
|
||||
url,
|
||||
}))
|
||||
})
|
||||
const detailScreenshotUrls = computed(() => detailScreenshots.value.map(shot => shot.url))
|
||||
const detailScreenshotRawUrls = computed(() =>
|
||||
detailScreenshots.value.map(shot => getScreenshotRawURL(shot.url))
|
||||
)
|
||||
|
||||
const activeScreenshot = computed(() => {
|
||||
if (detailScreenshots.value.length === 0) return null
|
||||
@@ -533,12 +538,12 @@ function goBack() {
|
||||
<div class="gallery-stage">
|
||||
<div v-if="activeScreenshot" class="stage-viewer">
|
||||
<AuthImage
|
||||
:source="activeScreenshot.url"
|
||||
:source="getScreenshotMediumURL(activeScreenshot.url)"
|
||||
:alt="activeScreenshot.label"
|
||||
image-class="stage-image"
|
||||
fallback-class="stage-image"
|
||||
fit="contain"
|
||||
:preview-src-list="detailScreenshotUrls"
|
||||
:preview-src-list="detailScreenshotRawUrls"
|
||||
:preview-initial-index="activeImageIndex"
|
||||
/>
|
||||
<!-- 左上角分类角标 -->
|
||||
@@ -566,7 +571,7 @@ function goBack() {
|
||||
@mouseenter="activeImageIndex = idx"
|
||||
>
|
||||
<AuthImage
|
||||
:source="shot.url"
|
||||
:source="getScreenshotThumbURL(shot.url)"
|
||||
:alt="shot.label"
|
||||
image-class="thumb-img"
|
||||
fallback-class="thumb-img"
|
||||
|
||||
@@ -37,6 +37,9 @@ import {
|
||||
readAssetString,
|
||||
getRatioValue,
|
||||
isTopTierInsurance,
|
||||
getScreenshotThumbURL,
|
||||
getScreenshotMediumURL,
|
||||
getScreenshotRawURL,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -164,7 +167,9 @@ const detailScreenshots = computed(() => {
|
||||
url,
|
||||
}))
|
||||
})
|
||||
const detailScreenshotUrls = computed(() => detailScreenshots.value.map(shot => shot.url))
|
||||
const detailScreenshotRawUrls = computed(() =>
|
||||
detailScreenshots.value.map(shot => getScreenshotRawURL(shot.url))
|
||||
)
|
||||
|
||||
const activeScreenshot = computed(() => {
|
||||
if (detailScreenshots.value.length === 0) return null
|
||||
@@ -505,12 +510,12 @@ function handleToggleFavorite() {
|
||||
<div class="gallery-stage">
|
||||
<div v-if="activeScreenshot" class="stage-viewer">
|
||||
<AuthImage
|
||||
:source="activeScreenshot.url"
|
||||
:source="getScreenshotMediumURL(activeScreenshot.url)"
|
||||
:alt="activeScreenshot.label"
|
||||
image-class="stage-image"
|
||||
fallback-class="stage-image"
|
||||
fit="contain"
|
||||
:preview-src-list="detailScreenshotUrls"
|
||||
:preview-src-list="detailScreenshotRawUrls"
|
||||
:preview-initial-index="activeImageIndex"
|
||||
/>
|
||||
<span class="stage-tag">{{ activeScreenshot.label }}</span>
|
||||
@@ -535,7 +540,7 @@ function handleToggleFavorite() {
|
||||
@click="activeImageIndex = idx"
|
||||
>
|
||||
<AuthImage
|
||||
:source="shot.url"
|
||||
:source="getScreenshotThumbURL(shot.url)"
|
||||
:alt="shot.label"
|
||||
image-class="thumb-img"
|
||||
fallback-class="thumb-img"
|
||||
|
||||
@@ -115,9 +115,27 @@ async function loadImage() {
|
||||
const sourceURL = await resolveURL(props.source)
|
||||
imageURL.value = sourceURL
|
||||
if (props.previewSrcList?.length) {
|
||||
previewURLs.value = await Promise.all(
|
||||
props.previewSrcList.map(url => (url === props.source ? sourceURL : resolveURL(url)))
|
||||
// 先填充公开或当前已解析的主图 URL,让预览组件立即可用
|
||||
const initialPreviews = props.previewSrcList.map(url =>
|
||||
url === props.source ? sourceURL : url
|
||||
)
|
||||
previewURLs.value = initialPreviews
|
||||
|
||||
// 若包含需要鉴权转 Blob 的私有 URL,在后台异步静默解析,不阻塞主图呈现
|
||||
const hasAuthPreviews = props.previewSrcList.some(
|
||||
url => url !== props.source && shouldFetchWithAuth(url)
|
||||
)
|
||||
if (hasAuthPreviews) {
|
||||
Promise.all(
|
||||
props.previewSrcList.map(url => (url === props.source ? sourceURL : resolveURL(url)))
|
||||
)
|
||||
.then(urls => {
|
||||
previewURLs.value = urls
|
||||
})
|
||||
.catch(() => {
|
||||
// 预览图静默降级,不破坏主图正常显示
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
failed.value = true
|
||||
|
||||
@@ -416,3 +416,39 @@ export function isTopTierInsurance(value: string | undefined | null): boolean {
|
||||
export function getCoinScreenshotURL(item: Listing): string {
|
||||
return item.cover_url || item.screenshot_urls?.[0] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换截图为缩略图 URL(优先使用 16KB 极速缩略图,秒级加载)
|
||||
*/
|
||||
export function getScreenshotThumbURL(url: string | undefined | null): string {
|
||||
if (!url) return ''
|
||||
if (url.includes('/screenshots/')) {
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
return `${url}${separator}variant=thumb`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换截图为中等图 URL(1280px,100KB左右,清晰秒开)
|
||||
*/
|
||||
export function getScreenshotMediumURL(url: string | undefined | null): string {
|
||||
if (!url) return ''
|
||||
if (url.includes('/screenshots/')) {
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
return `${url}${separator}variant=medium`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换截图为原始高清原图 URL(用于全屏放大查看细节)
|
||||
*/
|
||||
export function getScreenshotRawURL(url: string | undefined | null): string {
|
||||
if (!url) return ''
|
||||
if (url.includes('/screenshots/')) {
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
return `${url}${separator}variant=raw`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user