feat(shopGoods): 优化商品列表加载及封面图性能

- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL
- shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力
- shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用
- 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度
- search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求
- 新增商品封面图样式,保证图片大小固定且样式统一
This commit is contained in:
2026-08-06 17:19:15 +08:00
parent c553a71ea2
commit 3bc856fc95
8 changed files with 296 additions and 24 deletions
+21 -3
View File
@@ -76,18 +76,34 @@ function removeOssProcess(url: string): string {
* @param options 压缩选项(width/quality 不传则使用默认值)
* @returns 处理后的图片 URL
*/
// 缓存已计算的压缩 URL,避免列表重渲染时(勾选、hover、滚动)重复拼接字符串
const compressedUrlCache = new Map<string, string>();
export function getCompressedImageUrl(
url: string,
options?: ImageCompressOptions
): string {
if (!url) return '';
// 命中缓存直接返回,省去重复计算
const cacheKey = options ? `${url}|${JSON.stringify(options)}` : url;
const cached = compressedUrlCache.get(cacheKey);
if (cached !== undefined) return cached;
// 先补全为完整 URL
const fullUrl = ensureFullUrl(url);
if (!fullUrl) return '';
if (!fullUrl) {
compressedUrlCache.set(cacheKey, '');
return '';
}
const { width, quality, enabled } = { ...DEFAULT_OPTIONS, ...options };
// 未启用压缩,原样返回
if (!enabled) return fullUrl;
if (!enabled) {
compressedUrlCache.set(cacheKey, fullUrl);
return fullUrl;
}
// 优先使用调用方指定的 width/quality
// 先剥离 URL 自带的 x-oss-process,再按本函数参数重新拼接,确保以调用方为准。
@@ -96,5 +112,7 @@ export function getCompressedImageUrl(
// 已有其他 query 参数用 & 拼接,否则用 ?
const separator = baseUrl.includes('?') ? '&' : '?';
return `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
const result = `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
compressedUrlCache.set(cacheKey, result);
return result;
}
+12 -10
View File
@@ -119,7 +119,7 @@
ArrowDownOutlined,
DownOutlined
} from '@ant-design/icons-vue';
import { ref, watch } from 'vue';
import { ref, watch, onMounted } from 'vue';
import { getCount } from '@/api/shop/shopGoods';
import type { GoodsCount, ShopGoodsParam } from '@/api/shop/shopGoods/model';
import useSearch from '@/utils/use-search';
@@ -244,18 +244,20 @@
emit('search', where);
};
// 首次进入:设置商户条件并触发统计 + 列表加载。
// 用 onMounted 替代 watch 的 immediate,避免与 ele-pro-table 的自动加载(initLoad)重复发请求。
onMounted(() => {
const id = props.merchantId;
where.merchantId = Number(id) > 0 ? id : undefined;
reload();
});
watch(
() => props.merchantId,
(id) => {
if (Number(id) > 0) {
where.merchantId = id;
reload();
} else {
where.merchantId = undefined;
reload();
}
},
{ immediate: true }
where.merchantId = Number(id) > 0 ? id : undefined;
reload();
}
);
</script>
+22 -10
View File
@@ -10,6 +10,7 @@
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:init-load="false"
:scroll="{ x: 1800 }"
size="small"
v-model:selection="selection"
@@ -36,12 +37,14 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<a-space class="flex items-center cursor-pointer">
<a-image
:src="getCompressedImageUrl(record.image)"
<img
v-if="record.image"
:preview="false"
:width="50"
:height="50"
:src="getCompressedImageUrl(record.image)"
class="goods-thumb"
loading="lazy"
alt=""
width="50"
height="50"
/>
<div class="flex flex-col justify-center leading-tight">
<span class="text-gray-700 font-bold">{{ record.name }}</span>
@@ -154,7 +157,7 @@
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { createVNode, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
@@ -511,9 +514,8 @@
});
};
/* 查询 */
/* 查询:分类数据延迟到列表首屏渲染完成后再加载,不阻塞首屏 */
const query = () => {
loading.value = true;
// 加载分类数据
if (!navigationList.value) {
listShopGoodsCategory({}).then((res) => {
@@ -623,7 +625,7 @@
}
};
};
query();
onMounted(query);
</script>
<script lang="ts">
@@ -632,4 +634,14 @@
};
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.goods-thumb {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.06);
flex-shrink: 0;
display: block;
}
</style>