Просмотр исходного кода

refactor(beautify): E批 图片 lazy-load 治理(长列表页 39 处)

覆盖目录:discover / article-center / shop / activity / growth / promotion
组件:ArticleBookshelf / DimensionActivities / DimensionProducts / RecommendedFeed
- 所有含 v-for 长列表的 <image> 补 lazy-load(跳过已有 lazy-load 的标签,自闭合与成对标签均处理)
- 避免滚动卡顿,降低长列表图片渲染开销
liaoxg 4 дней назад
Родитель
Сommit
e0c1f8c7e4
28 измененных файлов с 134 добавлено и 39 удалено
  1. 1 1
      cfc-frontend/components/ArticleBookshelf.vue
  2. 1 1
      cfc-frontend/components/DimensionActivities.vue
  3. 1 1
      cfc-frontend/components/DimensionProducts.vue
  4. 2 2
      cfc-frontend/components/RecommendedFeed.vue
  5. 2 2
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue
  6. 1 1
      cfc-frontend/pages/activity/create.vue
  7. 4 4
      cfc-frontend/pages/activity/index.vue
  8. 2 2
      cfc-frontend/pages/article-center/article-detail.vue
  9. 1 1
      cfc-frontend/pages/article-center/article-edit.vue
  10. 2 2
      cfc-frontend/pages/article-center/index.vue
  11. 1 1
      cfc-frontend/pages/article-center/my-posts.vue
  12. 2 2
      cfc-frontend/pages/discover/circle-feed.vue
  13. 1 1
      cfc-frontend/pages/discover/circles.vue
  14. 1 1
      cfc-frontend/pages/discover/index.vue
  15. 1 1
      cfc-frontend/pages/growth/diet-checkin.vue
  16. 1 1
      cfc-frontend/pages/growth/exercise-checkin.vue
  17. 1 1
      cfc-frontend/pages/growth/upload.vue
  18. 1 1
      cfc-frontend/pages/promotion/index.vue
  19. 4 4
      cfc-frontend/pages/promotion/leaderboard.vue
  20. 1 1
      cfc-frontend/pages/promotion/material.vue
  21. 1 1
      cfc-frontend/pages/promotion/team.vue
  22. 1 1
      cfc-frontend/pages/shop/after-sales-detail/after-sales-detail.vue
  23. 1 1
      cfc-frontend/pages/shop/cart/cart.vue
  24. 1 1
      cfc-frontend/pages/shop/checkout/checkout.vue
  25. 2 2
      cfc-frontend/pages/shop/index/index.vue
  26. 1 1
      cfc-frontend/pages/shop/order-list/order-list.vue
  27. 1 1
      cfc-frontend/pages/shop/search/search.vue
  28. 95 0
      cfc-frontend/scripts/add-lazy-load.js

+ 1 - 1
cfc-frontend/components/ArticleBookshelf.vue

@@ -34,7 +34,7 @@
             class="bs-cover-img"
             :src="article.coverSrc"
             mode="aspectFill"
-          />
+           lazy-load />
 
           <view v-else class="bs-cover-text">
             <text

+ 1 - 1
cfc-frontend/components/DimensionActivities.vue

@@ -39,7 +39,7 @@
               class="card-cover-img"
               :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <!-- 右侧信息区 -->
             <view class="card-info">
               <text class="card-title line-clamp-1">{{ act.title }}</text>

+ 1 - 1
cfc-frontend/components/DimensionProducts.vue

@@ -5,7 +5,7 @@
     </view>
     <view v-else :class="['product-list', layout === 'mall' ? 'product-list-mall' : '']">
       <view :class="['product-card', layout === 'mall' ? 'product-card-mall' : '']" v-for="prod in displayProducts" :key="prod.id" @click="onProductClick(prod)">
-        <image class="product-img" :class="layout === 'mall' ? 'product-img-mall' : ''" :src="getImageUrl(prod.coverImage) || '/static/default-product.png'" mode="aspectFill" />
+        <image class="product-img" :class="layout === 'mall' ? 'product-img-mall' : ''" :src="getImageUrl(prod.coverImage) || '/static/default-product.png'" mode="aspectFill"  lazy-load />
         <view class="product-body" :class="layout === 'mall' ? 'product-body-mall' : ''">
           <text class="product-name" :class="layout === 'mall' ? 'product-name-mall' : ''">{{ prod.name }}</text>
           <view class="price-row">

+ 2 - 2
cfc-frontend/components/RecommendedFeed.vue

@@ -25,7 +25,7 @@
         >
           <!-- 商品卡片 -->
           <template v-if="item.type === 'product'">
-            <image class="feed-img" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
+            <image class="feed-img" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill"  lazy-load />
             <view class="feed-info">
               <view class="feed-tag-row">
                 <text class="feed-type-badge product-badge">🛍️ 商品</text>
@@ -38,7 +38,7 @@
           </template>
           <!-- 活动卡片 -->
           <template v-else-if="item.type === 'activity'">
-            <image class="feed-img" :src="getImageUrl(item.coverImage) || '/static/default-activity.png'" mode="aspectFill" />
+            <image class="feed-img" :src="getImageUrl(item.coverImage) || '/static/default-activity.png'" mode="aspectFill"  lazy-load />
             <view class="feed-info">
               <view class="feed-tag-row">
                 <text class="feed-type-badge activity-badge">🔥 活动</text>

+ 2 - 2
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -23,8 +23,8 @@
         class="cover"
         :src="getImageUrl(activity.coverImage)"
         mode="widthFix"
-      />
-      <image v-else class="cover placeholder-cover" src="/static/default-activity.png" mode="aspectFill" />
+       lazy-load />
+      <image v-else class="cover placeholder-cover" src="/static/default-activity.png" mode="aspectFill"  lazy-load />
 
       <view class="info-section">
         <text class="activity-title">{{ activity.title }}</text>

+ 1 - 1
cfc-frontend/pages/activity/create.vue

@@ -92,7 +92,7 @@
       <view class="form-item">
         <text class="form-label">封面图片</text>
         <view class="cover-upload" @click="chooseImage">
-          <image v-if="form.coverImage" class="preview-img" :src="getCoverUrl(form.coverImage)" mode="aspectFill" @click.stop />
+          <image v-if="form.coverImage" class="preview-img" :src="getCoverUrl(form.coverImage)" mode="aspectFill" @click.stop  lazy-load />
           <view v-else class="upload-placeholder">
             <text class="upload-icon">📷</text>
             <text class="upload-text">点击上传封面图</text>

+ 4 - 4
cfc-frontend/pages/activity/index.vue

@@ -53,7 +53,7 @@
               class="card-cover"
               :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-meta" v-if="act.registrantName">报名人:{{ act.registrantName }}</text>
@@ -127,7 +127,7 @@
               class="card-cover"
               :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
@@ -192,7 +192,7 @@
               class="card-cover"
               :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
@@ -270,7 +270,7 @@
               class="card-cover"
               :src="getImageUrl(act.coverImage) || '/static/default-activity.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-meta">{{ act.startTime }}</text>

+ 2 - 2
cfc-frontend/pages/article-center/article-detail.vue

@@ -13,7 +13,7 @@
     </view>
     <template v-else-if="article">
       <scroll-view scroll-y class="content-scroll" @scrolltolower="onScrollToBottom">
-        <image v-if="article.coverImage" class="detail-cover" :src="article.coverImage" mode="widthFix" />
+        <image v-if="article.coverImage" class="detail-cover" :src="article.coverImage" mode="widthFix"  lazy-load />
         <text class="detail-title">{{ article.title }}</text>
         <view class="detail-meta">
           <text class="meta-author">{{ article.author || '浠艾福' }}</text>
@@ -55,7 +55,7 @@
               hover-class="ra-item-pressed"
               @click="goRelatedArticle(item)"
             >
-              <image class="ra-cover" :src="getImageUrl(item.coverImage) || '/static/default-article.png'" mode="aspectFill" />
+              <image class="ra-cover" :src="getImageUrl(item.coverImage) || '/static/default-article.png'" mode="aspectFill"  lazy-load />
               <view class="ra-body">
                 <text class="ra-title">{{ item.title }}</text>
                 <text class="ra-summary" v-if="item.summary">{{ item.summary }}</text>

+ 1 - 1
cfc-frontend/pages/article-center/article-edit.vue

@@ -15,7 +15,7 @@
     <scroll-view scroll-y class="edit-form">
       <!-- 封面图 -->
       <view class="form-cover" @click="chooseCover">
-        <image v-if="coverImage" :src="coverImage" mode="aspectFill" class="cover-preview" />
+        <image v-if="coverImage" :src="coverImage" mode="aspectFill" class="cover-preview"  lazy-load />
         <view v-else class="cover-placeholder">
           <text class="cover-icon">📷</text>
           <text class="cover-hint">添加封面</text>

+ 2 - 2
cfc-frontend/pages/article-center/index.vue

@@ -69,7 +69,7 @@
 
       <!-- 空状态 -->
       <view v-else-if="articles.length === 0" class="ac-empty">
-        <image class="ac-empty-img" src="/static/empty-article.png" mode="aspectFit"></image>
+        <image class="ac-empty-img" src="/static/empty-article.png" mode="aspectFit" lazy-load></image>
         <text class="ac-empty-text">暂无文章</text>
       </view>
 
@@ -87,7 +87,7 @@
               class="ac-card-cover"
               :src="getImageUrl(item.coverImage) || '/static/default-article.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <view class="ac-card-dimension-tags" v-if="item.relatedDimensions">
               <view
                 v-for="dim in parseDimensions(item.relatedDimensions)"

+ 1 - 1
cfc-frontend/pages/article-center/my-posts.vue

@@ -18,7 +18,7 @@
             class="post-cover"
             :src="getImageUrl(item.coverImage)"
             mode="aspectFill"
-          />
+           lazy-load />
           <view class="post-body">
             <text class="post-title">{{ item.title }}</text>
             <view class="post-meta">

+ 2 - 2
cfc-frontend/pages/discover/circle-feed.vue

@@ -56,7 +56,7 @@
             mode="aspectFill"
             class="feed-img"
             @tap="previewImage(post.imageUrls, idx)"
-          />
+           lazy-load />
         </view>
 
         <!-- 操作栏 -->
@@ -82,7 +82,7 @@
         <!-- 评论列表 -->
         <view class="feed-comments" v-if="post.comments && post.comments.length > 0">
           <view class="comment-item" v-for="(c, idx) in getVisibleComments(post)" :key="getCommentKey(c, idx)">
-            <image class="c-avatar" v-if="getCommentAvatar(c)" :src="getCommentAvatar(c)" mode="aspectFill" />
+            <image class="c-avatar" v-if="getCommentAvatar(c)" :src="getCommentAvatar(c)" mode="aspectFill"  lazy-load />
             <text class="c-name">{{ getCommentName(c) }}</text>
             <text class="c-text">{{ c.content }}</text>
           </view>

+ 1 - 1
cfc-frontend/pages/discover/circles.vue

@@ -57,7 +57,7 @@
             <image
               class="pending-avatar"
               :src="pm.applicantAvatar || defaultAvatar"
-              mode="aspectFill" />
+              mode="aspectFill"  lazy-load />
             <view class="pending-info">
               <view class="pending-name">
                 <text class="pending-name-text">{{ pm.applicantName || '圈友' }}</text>

+ 1 - 1
cfc-frontend/pages/discover/index.vue

@@ -34,7 +34,7 @@
           class="product-card"
           @click="goDetail(item.id)"
         >
-          <image class="product-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
+          <image class="product-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill"  lazy-load />
           <view class="product-info">
             <text class="product-name">{{ item.name }}</text>
             <view class="product-price-row">

+ 1 - 1
cfc-frontend/pages/growth/diet-checkin.vue

@@ -73,7 +73,7 @@
           <!-- 照片缩略图 -->
           <view class="photo-strip" v-if="form.photos.length > 0">
             <view class="photo-item" v-for="(img, idx) in form.photos" :key="getItemKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-img"/>
+              <image :src="img" mode="aspectFill" class="photo-img" lazy-load />
               <view class="photo-delete-overlay">
                 <text class="delete-icon">×</text>
               </view>

+ 1 - 1
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -91,7 +91,7 @@
           <!-- 照片缩略图 -->
           <view class="photo-strip" v-if="form.photos.length > 0">
             <view class="photo-item" v-for="(img, idx) in form.photos" :key="getItemKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-img"/>
+              <image :src="img" mode="aspectFill" class="photo-img" lazy-load />
               <view class="photo-delete-overlay">
                 <text class="delete-icon">×</text>
               </view>

+ 1 - 1
cfc-frontend/pages/growth/upload.vue

@@ -28,7 +28,7 @@
             :key="pi"
             @click="goToProduct(item.id)"
           >
-            <image class="purchase-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
+            <image class="purchase-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill"  lazy-load />
             <text class="purchase-name">{{ item.name }}</text>
             <text class="purchase-price">{{ formatPrice(item.price) }}</text>
             <view class="purchase-btn">

+ 1 - 1
cfc-frontend/pages/promotion/index.vue

@@ -49,7 +49,7 @@
             <text class="rank-num" v-if="idx < 3 && !showAllRanks">🏅</text>
             <text class="rank-num rank-plain" v-else>{{ idx + 1 }}</text>
           </view>
-          <image v-if="m.avatar" class="member-avatar" :src="m.avatar" mode="aspectFill"></image>
+          <image v-if="m.avatar" class="member-avatar" :src="m.avatar" mode="aspectFill" lazy-load></image>
           <text v-else class="member-avatar-placeholder">{{ m.nickname ? m.nickname.charAt(0) : '?' }}</text>
           <view class="member-info">
             <text class="member-name">{{ m.nickname || '用户' }}</text>

+ 4 - 4
cfc-frontend/pages/promotion/leaderboard.vue

@@ -23,7 +23,7 @@
       <view class="podium-item podium-silver" v-if="topList.length >= 2">
         <view class="podium-rank-badge silver-badge">2</view>
         <view class="podium-avatar-wrap silver-border">
-          <image v-if="topList[1] && topList[1].avatarUrl" class="podium-avatar" :src="topList[1].avatarUrl" mode="aspectFill"></image>
+          <image v-if="topList[1] && topList[1].avatarUrl" class="podium-avatar" :src="topList[1].avatarUrl" mode="aspectFill" lazy-load></image>
           <text v-else class="podium-avatar-icon">👤</text>
         </view>
         <text class="podium-name">{{ topList[1] && topList[1].nickname ? topList[1].nickname : '微信用户' }}</text>
@@ -37,7 +37,7 @@
         </view>
         <view class="podium-rank-badge gold-badge">1</view>
         <view class="podium-avatar-wrap gold-border gold-size">
-          <image v-if="topList[0] && topList[0].avatarUrl" class="podium-avatar" :src="topList[0].avatarUrl" mode="aspectFill"></image>
+          <image v-if="topList[0] && topList[0].avatarUrl" class="podium-avatar" :src="topList[0].avatarUrl" mode="aspectFill" lazy-load></image>
           <text v-else class="podium-avatar-icon">👤</text>
         </view>
         <text class="podium-name gold-name">{{ topList[0] && topList[0].nickname ? topList[0].nickname : '微信用户' }}</text>
@@ -48,7 +48,7 @@
       <view class="podium-item podium-bronze" v-if="topList.length >= 3">
         <view class="podium-rank-badge bronze-badge">3</view>
         <view class="podium-avatar-wrap bronze-border">
-          <image v-if="topList[2] && topList[2].avatarUrl" class="podium-avatar" :src="topList[2].avatarUrl" mode="aspectFill"></image>
+          <image v-if="topList[2] && topList[2].avatarUrl" class="podium-avatar" :src="topList[2].avatarUrl" mode="aspectFill" lazy-load></image>
           <text v-else class="podium-avatar-icon">👤</text>
         </view>
         <text class="podium-name">{{ topList[2] && topList[2].nickname ? topList[2].nickname : '微信用户' }}</text>
@@ -63,7 +63,7 @@
           <text class="rank-num">{{ item.rank }}</text>
         </view>
         <view class="rank-avatar-wrap">
-          <image v-if="item.avatarUrl" class="rank-avatar" :src="item.avatarUrl" mode="aspectFill"></image>
+          <image v-if="item.avatarUrl" class="rank-avatar" :src="item.avatarUrl" mode="aspectFill" lazy-load></image>
           <text v-else class="rank-avatar-icon">👤</text>
         </view>
         <text class="rank-name">{{ item.nickname || '微信用户' }}</text>

+ 1 - 1
cfc-frontend/pages/promotion/material.vue

@@ -38,7 +38,7 @@
           :key="item.id"
           @click="goDetail(item)"
         >
-          <image class="material-cover" :src="getImageUrl(item.coverUrl) || '/static/default-cover.png'" mode="aspectFill" />
+          <image class="material-cover" :src="getImageUrl(item.coverUrl) || '/static/default-cover.png'" mode="aspectFill"  lazy-load />
           <view class="material-info">
             <text class="material-title">{{ item.title }}</text>
             <text class="material-tags" v-if="item.tags">{{ item.tags }}</text>

+ 1 - 1
cfc-frontend/pages/promotion/team.vue

@@ -39,7 +39,7 @@
       </view>
       <view class="member-item" v-for="item in list" :key="item.userId">
         <view class="member-avatar-wrap">
-          <image v-if="item.avatarUrl" class="member-avatar-img" :src="item.avatarUrl" mode="aspectFill"></image>
+          <image v-if="item.avatarUrl" class="member-avatar-img" :src="item.avatarUrl" mode="aspectFill" lazy-load></image>
           <text v-else class="member-avatar-icon">👤</text>
         </view>
         <view class="member-info">

+ 1 - 1
cfc-frontend/pages/shop/after-sales-detail/after-sales-detail.vue

@@ -49,7 +49,7 @@
       <view v-if="imageList.length > 0" class="section">
         <text class="section-title">凭证图片</text>
         <view class="image-list">
-          <image v-for="(img, idx) in imageList" :key="idx" class="proof-image" :src="img" mode="aspectFill" @click="previewImage(idx)" />
+          <image v-for="(img, idx) in imageList" :key="idx" class="proof-image" :src="img" mode="aspectFill" @click="previewImage(idx)"  lazy-load />
         </view>
       </view>
 

+ 1 - 1
cfc-frontend/pages/shop/cart/cart.vue

@@ -33,7 +33,7 @@
               class="item-image"
               :src="getImageUrl(item.coverImage) || '/static/default-product.png'"
               mode="aspectFill"
-            />
+             lazy-load />
           </view>
           <view class="item-right">
             <view class="item-header">

+ 1 - 1
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -153,7 +153,7 @@
             class="item-image"
             :src="getImageUrl(item.coverImage) || '/static/default-product.png'"
             mode="aspectFill"
-          />
+           lazy-load />
           <view class="item-info">
             <text class="item-name">{{ item.productName }}</text>
             <text v-if="item.specDesc" class="item-spec-desc">{{ item.specDesc }}</text>

+ 2 - 2
cfc-frontend/pages/shop/index/index.vue

@@ -88,7 +88,7 @@
               class="recommend-img"
               :src="getImageUrl(item.image || item.coverImage) || '/static/default-product.png'"
               mode="aspectFill"
-            />
+             lazy-load />
             <text class="recommend-name">{{ item.name }}</text>
             <text class="recommend-price">{{ formatPriceWithSymbol(item.price) }}</text>
           </view>
@@ -121,7 +121,7 @@
             class="cover"
             :src="getImageUrl(item.image || item.coverImage) || '/static/default-product.png'"
             mode="aspectFill"
-          />
+           lazy-load />
           <view class="card-body">
             <text class="product-name">{{ item.name }}</text>
             <view class="price-row">

+ 1 - 1
cfc-frontend/pages/shop/order-list/order-list.vue

@@ -59,7 +59,7 @@
               :src="getImageUrl(item.coverImage) || defaultCover(item.productType)"
               mode="aspectFill"
               @error="onImgError($event, item)"
-            />
+             lazy-load />
             <view class="product-section">
               <view class="product-header">
                 <text class="product-name">{{ item.productName }}</text>

+ 1 - 1
cfc-frontend/pages/shop/search/search.vue

@@ -52,7 +52,7 @@
             class="cover"
             :src="item.image || item.coverImage || '/static/default-product.png'"
             mode="aspectFill"
-          />
+           lazy-load />
           <view class="card-body">
             <text class="product-name">{{ item.name }}</text>
             <view class="price-row">

+ 95 - 0
cfc-frontend/scripts/add-lazy-load.js

@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+/**
+ * E 批:图片 lazy-load 治理
+ * 在长列表页的 <image> 标签上补 lazy-load(若尚未有 lazy-load 属性)
+ *
+ * 策略:
+ * - 对 <image ...> 不含 lazy-load 的标签补上 lazy-load
+ * - 跳过已有 lazy-load 的标签
+ * - 仅处理已识别的长列表页(discover / article-center / shop / activity / growth 等)
+ */
+const fs = require('fs')
+const path = require('path')
+
+const ROOT = path.resolve(__dirname, '..')
+const EXCLUDE_DIRS = new Set(['node_modules', 'dist', 'unpackage', '.git', 'docs'])
+
+// 需要治理的长列表页面/组件(按父目录分组,walk 时过滤)
+const TARGET_PATTERNS = [
+  'pages/discover/',
+  'pages/article-center/',
+  'pages/shop/',
+  'pages/activity/',
+  'pages/growth/',
+  'pages/promotion/',
+  'components/DimensionActivities.vue',
+  'components/DimensionProducts.vue',
+  'components/RecommendedFeed.vue',
+  'components/ArticleBookshelf.vue',
+  'components/FamilyFeed.vue',
+  'components/daily-task-card.vue',
+]
+
+// 模式:找到 <image ...> 中不含 lazy-load 的,加上 lazy-load
+// 匹配:<image ...attrs...>(不含 lazy-load),在后面某个位置补
+const IMG_TAG_RE = /<image\s([^>]*)>/gi
+
+function processFile(filePath) {
+  const src = fs.readFileSync(filePath, 'utf8')
+  // 只处理包含长列表 image 的文件(启发式:有 v-for 或 list/data 字样的文件优先)
+  const hasLoop = /v-for\s*=/.test(src) || /v_loop/.test(src)
+  if (!hasLoop) return { changed: false, count: 0 }
+
+  let result = src
+  let count = 0
+  result = result.replace(IMG_TAG_RE, (fullMatch, attrs) => {
+    if (/lazy-load/.test(attrs)) return fullMatch // 已有 lazy-load,跳过
+    // 在标签闭合 `>`(可能为 `/>`)前插入 lazy-load
+    if (fullMatch.endsWith('/>')) {
+      return fullMatch.slice(0, -2) + ' lazy-load />'
+    }
+    return fullMatch.slice(0, -1) + ' lazy-load>'
+  })
+
+  count = (result.match(/lazy-load/g) || []).length - (src.match(/lazy-load/g) || []).length
+  if (count > 0) {
+    fs.writeFileSync(filePath, result, 'utf8')
+  }
+  return { changed: count > 0, count }
+}
+
+function walk(dir, out = []) {
+  for (const name of fs.readdirSync(dir)) {
+    if (EXCLUDE_DIRS.has(name)) continue
+    const p = path.join(dir, name)
+    const st = fs.statSync(p)
+    if (st.isDirectory()) walk(p, out)
+    else if (name.endsWith('.vue')) out.push(p)
+  }
+  return out
+}
+
+function main() {
+  const allFiles = walk(ROOT)
+  const targets = allFiles.filter(f => {
+    const rel = path.relative(ROOT, f).split(path.sep).join('/')
+    return TARGET_PATTERNS.some(p => rel.startsWith(p))
+  })
+
+  let grandTotal = 0
+  const log = []
+  for (const f of targets) {
+    const rel = path.relative(ROOT, f)
+    const r = processFile(f)
+    if (r.changed) {
+      grandTotal += r.count
+      log.push(`${r.count.toString().padStart(3)}  ${rel}`)
+    }
+  }
+  console.log(`\n=== E 批 lazy-load 补全完成 ===`)
+  console.log(`新增 lazy-load: ${grandTotal} 处`)
+  log.sort((a, b) => parseInt(b.split(' ')[0]) - parseInt(a.split(' ')[0]))
+  for (const line of log.slice(0, 30)) console.log(line)
+}
+
+main()