Vue项目组件分析报告

165 阅读4分钟

一、安装依赖

webpack-stats-plugin是一个用于生成 Webpack 构建统计信息的插件,通过输出 JSON 文件帮助开发者分析模块依赖、文件大小和构建性能。

npm install --save-dev webpack-stats-plugin glob
# 或
yarn add --dev webpack-stats-plugin glob

二、基础配置

// rsbuild.config.ts
import { StatsWriterPlugin } from 'webpack-stats-plugin'
import { pluginComponentSimilarity } from './component-similarity-plugin'

module.exports = {
  // 其他 Webpack 配置
  plugins: [
    pluginComponentSimilarity(), // 组件相似度分析插件
  ],
  tools: {
    rspack: {
      plugins: [
        new StatsWriterPlugin({
          filename: 'stats.json', // 输出文件名
          transform: (data) => {
            const cleanedData = {
              modules: data.modules.map((module) => ({
                ...module,
                name: module.name.replace(/ \+ \d+ modules$/, '')
              }))
            }
            return JSON.stringify(cleanedData, null, 2)
          },
          fields: null
        })
      ]
    }
  }
}
// component-similarity-plugin.ts
import type { RsbuildPlugin } from '@rsbuild/core'
import { glob } from 'glob'
import path from 'node:path'
import fs from 'fs/promises'

interface ComponentInfo {
  path: string
  template: string
  script: string
  style: string
}

// 代码分词器(将代码拆分为有意义的标记)
const tokenize = (content: string): string[] => {
  if (!content) return []

  // 移除注释和字符串常量
  const cleanCode = content
    .replace(/\/\/.*|\/\*[\s\S]*?\*\/|(['"])(?:\\.|.)*?\1/g, '')
    .replace(/\s+/g, ' ')

  // 提取关键符号和关键字
  return Array.from(
    new Set(
      cleanCode.match(
        /[a-zA-Z_$][\w$]*|=>|\.\.\.|\?\?=|\|\||&&|[{}()[\];,<>:=+-\/*%]/g
      ) || []
    )
  )
}

// 改进的相似度计算(余弦相似度 + 权重)
const calculateSimilarity = (
  tokensA: string[],
  tokensB: string[],
  weights: { [token: string]: number } = {}
): number => {
  if (tokensA.length === 0 && tokensB.length === 0) return 1
  if (tokensA.length === 0 || tokensB.length === 0) return 0

  const allTokens = new Set([...tokensA, ...tokensB])
  const vecA: number[] = []
  const vecB: number[] = []

  // 构建带权重的向量
  allTokens.forEach((token) => {
    const weight = weights[token] || 1
    vecA.push(tokensA.includes(token) ? weight : 0)
    vecB.push(tokensB.includes(token) ? weight : 0)
  })

  // 计算余弦相似度
  let dotProduct = 0
  let magA = 0
  let magB = 0

  for (let i = 0; i < vecA.length; i++) {
    dotProduct += vecA[i] * vecB[i]
    magA += vecA[i] * vecA[i]
    magB += vecB[i] * vecB[i]
  }

  magA = Math.sqrt(magA)
  magB = Math.sqrt(magB)

  return magA * magB ? dotProduct / (magA * magB) : 0
}

export const pluginComponentSimilarity = (): RsbuildPlugin => ({
  name: 'component-similarity-plugin',
  setup(api) {
    api.onBeforeBuild(async () => {
      const { rootPath } = api.context
      const reportPath = path.resolve(rootPath, 'component-similarity.json')
      const componentPath = path.resolve(rootPath, 'componentPaths.json')

      // 1. 组件收集:扫描 Vue 文件
      const vueFiles = glob.sync('src/**/*.vue', {
        cwd: rootPath,
        ignore: '**/node_modules/**'
      })

      const components: ComponentInfo[] = []
      const componentPaths: string[] = []

      // 2. 内容提取与预处理
      for (const filePath of vueFiles) {
        const fullPath = path.resolve(rootPath, filePath)
        const content = await fs.readFile(fullPath, 'utf-8')

        // 提取各区块内容(支持带lang属性的区块)
        const template =
          content.match(/<template[\s\S]*?>([\s\S]*?)<\/template>/i)?.[1] || ''
        const script =
          content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/i)?.[1] || ''
        const style =
          content.match(/<style[\s\S]*?>([\s\S]*?)<\/style>/i)?.[1] || ''

        components.push({
          path: filePath,
          template: template.trim(),
          script: script.trim(),
          style: style.trim()
        })

        componentPaths.push(filePath)
      }

      // 3. 相似度计算
      const report: any = []
      const componentTokens = components.map((comp) => ({
        path: comp.path,
        template: tokenize(comp.template),
        script: tokenize(comp.script)
      }))

      // 关键元素权重(根据业务调整)
      const weights = {
        'v-if': 3,
        'v-for': 3,
        'v-model': 3, // 重要指令
        template: 2,
        script: 2,
        style: 2, // 区块标签
        computed: 2,
        watch: 2,
        methods: 2 // Vue选项
      }

      for (let i = 0; i < componentTokens.length; i++) {
        for (let j = i + 1; j < componentTokens.length; j++) {
          const compA = componentTokens[i]
          const compB = componentTokens[j]

          // 计算各区块相似度(带权重)
          const templateSim = calculateSimilarity(
            compA.template,
            compB.template,
            weights
          )

          const scriptSim = calculateSimilarity(
            compA.script,
            compB.script,
            weights
          )

          // 加权综合相似度(可调整权重因子)
          const totalSimilarity = templateSim * 0.6 + scriptSim * 0.4

          if (totalSimilarity > 0.3) {
            // 过滤低相似度项
            report.push({
              comp1: compA.path,
              comp2: compB.path,
              templateSimilarity: templateSim,
              scriptSimilarity: scriptSim,
              totalSimilarity
            })
          }
        }
      }

      // 4. 生成报告
      const sortedReport = report.sort(
        (a, b) => b.totalSimilarity - a.totalSimilarity
      )

      await fs.writeFile(reportPath, JSON.stringify(sortedReport, null, 2))
      await fs.writeFile(componentPath, JSON.stringify(componentPaths, null, 2))
    })
  }
})

运行构建后会在输出目录生成 stats.json文件 和 根目录下生成 component-similarity.json 文件 和 componentPaths.json

三、分析 stats.json 文件信息

stats.json 的核心结构通常包含:

modules:所有构建模块的列表,每个模块包含 name(组件路径)和 reasons(引用来源)。

在 Webpack 生成的 stats.json 文件中,modules 数组项的 name 字段中出现类似 + N modules 的标识,本质上是 Webpack 对模块依赖关系的聚合显示优化,主要目的是简化统计信息的可读性

image.png

所以这就是为什么在webpack的配置中 要进行module.name.replace(/ \+ \d+ modules$/, '')的必要性,为了统计数据更准确。

四、提取stats.jsoneslint-report.json中的数据信息到 module.js

// analyze.js
const stats = require('./dist/stats.json')
const componentPaths = require('./componentPaths.json')
const similarity = require('./component-similarity.json')

const fs = require('fs')
const vueComponents = {}
stats.modules.forEach((module) => {
  if (module.name.match(/\.vue$/)) {
    const refs = new Set()

    if (module.reasons) {
      module.reasons.forEach((reason) => {
        if (reason.moduleName) refs.add(reason.moduleName)
      })
    }
    vueComponents[module.name] = refs.size
  }
})

function addBeforeSrc(str) {
  return './' + str // 若未找到"/src"则返回原字符串
}

// 获取src下全部vue组件
const allComponents = componentPaths.reduce((result, path) => {
  result.push(addBeforeSrc(path))
  return result
}, [])

const outputLineJson = []
Object.entries(vueComponents)
  .sort((a, b) => b[1] - a[1])
  .forEach(([comp, count]) => {
    outputLineJson.push({ path: comp, count })
  })

allComponents.forEach((comp) => {
  if (!vueComponents[comp]) {
    outputLineJson.push({ path: comp, count: 0 })
  }
})

// 获取 totalSimilarity 大于 0.7 的数据
const highSimilarityData = similarity.filter(
  (item) => item.totalSimilarity > 0.7
)

fs.writeFileSync(
  'module.js',
  `const componentData = ${JSON.stringify(outputLineJson, null, 2)}
   const highSimilarityData = ${JSON.stringify(highSimilarityData, null, 2)}
  `
)

执行node analyze.js 生成 module.js 文件

五、html文件引用生成的module.js

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件分析报告</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
      :root {
        --primary: #3498db;
        --secondary: #2ecc71;
        --accent: #9b59b6;
        --warning: #f39c12;
        --text: #2c3e50;
        --light-text: #7f8c8d;
        --bg: #f8f9fa;
        --card-bg: #ffffff;
        --border: #e1e4e8;
        --success: #27ae60;
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
        line-height: 1.6;
        color: var(--text);
        background-color: var(--bg);
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }

      header {
        text-align: center;
        margin-bottom: 30px;
        padding-bottom: 20px;
        border-bottom: 2px solid var(--border);
      }

      h1 {
        color: var(--primary);
        margin-bottom: 10px;
        font-size: 2.2rem;
      }

      .subtitle {
        color: var(--light-text);
        font-size: 1.1rem;
      }

      /* 标签页样式 */
      .tabs {
        display: flex;
        margin-bottom: 20px;
        border-bottom: 1px solid var(--border);
      }

      .tab {
        padding: 10px 20px;
        cursor: pointer;
        background-color: var(--card-bg);
        border: 1px solid var(--border);
        border-bottom: none;
        border-radius: 5px 5px 0 0;
        margin-right: 5px;
      }

      .tab.active {
        background-color: var(--primary);
        color: white;
        font-weight: bold;
      }

      .tab-content {
        display: none;
      }

      .tab-content.active {
        display: block;
      }

      /* 复用度分析样式 */
      .metrics {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
        gap: 15px;
        margin-bottom: 30px;
      }

      .metric-card {
        background: var(--card-bg);
        border-radius: 10px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
        padding: 20px;
        text-align: center;
        transition: transform 0.3s;
      }

      .metric-card:hover {
        transform: translateY(-5px);
        box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
      }

      .metric-value {
        font-size: 2.5rem;
        font-weight: bold;
        margin: 10px 0;
      }

      .metric-high {
        color: var(--accent);
      }

      .metric-med {
        color: var(--primary);
      }

      .metric-low {
        color: var(--warning);
      }

      .controls {
        display: flex;
        justify-content: space-between;
        margin: 20px 0;
        flex-wrap: wrap;
        gap: 15px;
      }

      .search-box {
        flex: 1;
        min-width: 250px;
        position: relative;
      }

      .search-box input {
        width: 100%;
        padding: 10px 15px;
        border: 1px solid var(--border);
        border-radius: 4px;
        font-size: 1rem;
      }

      .filter-options {
        display: flex;
        gap: 15px;
        flex-wrap: wrap;
      }

      .filter-btn {
        padding: 8px 15px;
        background: var(--card-bg);
        border: 1px solid var(--border);
        border-radius: 4px;
        cursor: pointer;
        transition: all 0.3s;
      }

      .filter-btn.active {
        background: var(--primary);
        color: white;
        border-color: var(--primary);
      }

      .reuse-list {
        background: var(--card-bg);
        border-radius: 8px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
        overflow: hidden;
        margin-bottom: 20px;
      }

      .component-card {
        padding: 15px;
        border-bottom: 1px solid var(--border);
        transition: background 0.2s;
      }

      .component-card:hover {
        background: #f9f9f9;
      }

      .component-card.high-reuse {
        border-left: 4px solid var(--accent);
      }

      .component-card.med-reuse {
        border-left: 4px solid var(--primary);
      }

      .component-card.low-reuse {
        border-left: 4px solid var(--warning);
      }

      .path {
        font-size: 0.85rem;
        color: var(--light-text);
        word-break: break-word;
        margin: 5px 0;
      }

      .count {
        font-weight: bold;
        font-size: 1.1rem;
        display: inline-block;
        padding: 2px 8px;
        border-radius: 12px;
        background: #f0f7ff;
      }

      .count.high {
        background: #f0e6ff;
        color: var(--accent);
      }

      .usage-bar {
        height: 8px;
        background-color: #e0e0e0;
        border-radius: 4px;
        margin: 10px 0;
        overflow: hidden;
      }

      .usage-fill {
        height: 100%;
        border-radius: 4px;
      }

      .usage-high {
        background: linear-gradient(to right, var(--accent), #8e44ad);
      }

      .usage-med {
        background: linear-gradient(to right, var(--primary), #2980b9);
      }

      .usage-low {
        background: linear-gradient(to right, var(--warning), #e67e22);
      }

      .footer {
        margin-top: 40px;
        padding-top: 20px;
        border-top: 1px solid var(--border);
      }

      .chart-container {
        position: relative;
        height: 400px;
        margin: 40px 0;
      }

      .pagination {
        display: flex;
        justify-content: center;
        margin: 20px 0;
        gap: 10px;
      }

      .analysis-section {
        background: var(--card-bg);
        border-radius: 8px;
        padding: 25px;
        margin: 30px 0;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
      }

      .badge {
        display: inline-block;
        padding: 3px 8px;
        border-radius: 4px;
        font-size: 0.8rem;
        font-weight: bold;
        margin-left: 10px;
        margin-top: -5px;
      }

      .badge.high {
        background: #e8d6ff;
        color: var(--accent);
      }

      .badge.med {
        background: #d6eaff;
        color: var(--primary);
      }

      .badge.low {
        background: #ffedcc;
        color: #e67e22;
      }

      .recommendation {
        padding: 15px;
        background: #e8f4ff;
        border-left: 4px solid var(--primary);
        margin: 15px 0;
        border-radius: 0 4px 4px 0;
      }

      /* 相似度分析样式 */
      .similarity-table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 20px;
        background: var(--card-bg);
        border-radius: 8px;
        overflow: hidden;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
      }

      .similarity-table th,
      .similarity-table td {
        padding: 12px 15px;
        text-align: left;
        border-bottom: 1px solid var(--border);
      }

      .similarity-table th {
        background-color: var(--primary);
        color: white;
        font-weight: bold;
        text-transform: uppercase;
        letter-spacing: 0.5px;
      }

      .similarity-table tr:last-child td {
        border-bottom: none;
      }

      .similarity-table tr:hover {
        background-color: #f5f5f5;
      }

      .similarity-bar {
        height: 8px;
        background-color: #e0e0e0;
        border-radius: 4px;
        margin: 5px 0;
        overflow: hidden;
      }

      .similarity-fill {
        height: 100%;
        border-radius: 4px;
      }

      .high {
        background-color: var(--accent);
      }

      .med {
        background-color: var(--primary);
      }

      .low {
        background-color: var(--warning);
      }

      .similarity-value {
        font-weight: bold;
        font-size: 1.1rem;
        display: inline-block;
        padding: 2px 8px;
        border-radius: 12px;
        background: #f0f7ff;
      }

      .similarity-value.high {
        background: #f0e6ff;
        color: var(--accent);
      }

      .similarity-value.med {
        background: #f0f7ff;
        color: var(--primary);
      }

      .similarity-value.low {
        background: #fff5e6;
        color: var(--warning);
      }

      /* 分页控件样式 */
      .pagination {
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 20px 0;
        gap: 10px;
      }

      .pagination button {
        padding: 8px 12px;
        background: var(--card-bg);
        border: 1px solid var(--border);
        border-radius: 4px;
        cursor: pointer;
        transition: all 0.3s;
      }

      .pagination button:hover:not(:disabled) {
        background: var(--primary);
        color: white;
        border-color: var(--primary);
      }

      .pagination button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      .pagination .page-info {
        margin: 0 15px;
        font-size: 1rem;
      }

      .pagination select {
        padding: 8px;
        border: 1px solid var(--border);
        border-radius: 4px;
      }

      @media (max-width: 768px) {
        .metrics {
          grid-template-columns: 1fr;
        }

        .controls {
          flex-direction: column;
        }
      }
    </style>
  </head>
  <body>
    <header>
      <h1>组件分析报告</h1>
      <p class="subtitle">组件复用度与相似度分析结果</p>
    </header>

    <!-- 标签页 -->
    <div class="tabs">
      <div class="tab active" data-tab="reuse">组件复用度分析</div>
      <div class="tab" data-tab="similarity">组件相似度分析</div>
      <div class="tab" data-tab="recommendation">优化建议</div>
    </div>

    <!-- 组件复用度分析 -->
    <div id="reuse-tab" class="tab-content active">
      <section class="metrics">
        <div class="metric-card">
          <h2>总的组件数</h2>
          <div class="metric-value metric-high" id="totalComponents">500</div>
          <p>项目中所有的vue组件数量</p>
        </div>
        <div class="metric-card">
          <h2>使用组件数</h2>
          <div class="metric-value metric-med" id="useComponents">500</div>
          <p>项目中使用组件数量</p>
        </div>
        <div class="metric-card">
          <h2>未使用组件数</h2>
          <div class="metric-value metric-low" id="noUseComponents">35</div>
          <p>项目中未使用组件数量</p>
        </div>
        <div class="metric-card">
          <h2>复用次数>1组件数</h2>
          <div class="metric-value metric-high" id="moreThanOnceComponents">
            500
          </div>
          <p>项目内组件复用(大于1次)</p>
        </div>
        <div class="metric-card">
          <h2>最高复用次数</h2>
          <div class="metric-value metric-med" id="maxCount">35</div>
          <p id="maxComponentName">loading-animation 组件</p>
        </div>
        <div class="metric-card">
          <h2>平均复用率</h2>
          <div class="metric-value metric-low" id="avgRate">9.7</div>
          <p>所有组件平均复用次数</p>
        </div>
      </section>

      <section class="controls">
        <div class="search-box">
          <input
            type="text"
            id="searchInput"
            placeholder="搜索组件路径或名称..."
          />
        </div>
        <div class="filter-options">
          <div class="filter-btn active" data-filter="all">全部组件</div>
          <div class="filter-btn" data-filter="high">高频 (>10次)</div>
          <div class="filter-btn" data-filter="med">中频 (5-10次)</div>
          <div class="filter-btn" data-filter="low">低频 (<5次)</div>
          <div class="filter-btn" data-filter="zero">未使用 (0次)</div>
        </div>
      </section>

      <section>
        <h2>
          组件复用排行榜 <span class="badge high" id="visibleCount">10</span>
        </h2>
        <div class="reuse-list" id="reuseList">
          <!-- 组件列表将通过JavaScript动态生成 -->
        </div>
        <div class="pagination">
          <button id="prevPage">上一页</button>
          <span id="pageInfo">第 1 页 / 共 50 页</span>
          <button id="nextPage">下一页</button>
          <select id="reusePageSize">
            <option value="10" selected>10条/页</option>
            <option value="20">20条/页</option>
            <option value="50">50条/页</option>
            <option value="100">100条/页</option>
          </select>
        </div>
      </section>

      <section class="chart-container">
        <canvas id="reuseChart"></canvas>
      </section>
    </div>

    <!-- 组件相似度分析 -->
    <div id="similarity-tab" class="tab-content">
      <!-- 相似度分析统计卡片 -->
      <section class="metrics">
        <div class="metric-card">
          <h2>组件对总数</h2>
          <div class="metric-value metric-high" id="totalPairs">-</div>
          <p>相似度分析的组件对数量</p>
        </div>
        <div class="metric-card">
          <h2>高相似度对</h2>
          <div class="metric-value metric-med" id="highSimilarityPairs">-</div>
          <p>相似度≥90%的组件对</p>
        </div>
        <div class="metric-card">
          <h2>中相似度对</h2>
          <div class="metric-value metric-low" id="medSimilarityPairs">-</div>
          <p>相似度70%-90%的组件对</p>
        </div>
        <div class="metric-card">
          <h2>平均相似度</h2>
          <div class="metric-value metric-high" id="avgSimilarity">-</div>
          <p>所有组件对的平均相似度</p>
        </div>
      </section>

      <!-- 相似度分析搜索和筛选 -->
      <section class="controls">
        <div class="search-box">
          <input
            type="text"
            id="similaritySearchInput"
            placeholder="搜索组件路径或名称..."
          />
        </div>
        <div class="filter-options">
          <div class="filter-btn active" data-filter="all">全部</div>
          <div class="filter-btn" data-filter="high">高相似度 (≥90%)</div>
          <div class="filter-btn" data-filter="med">中相似度 (70%-90%)</div>
          <!-- <div class="filter-btn" data-filter="low">低相似度 (<70%)</div> -->
        </div>
      </section>
      <table class="similarity-table">
        <thead>
          <tr>
            <th>组件1</th>
            <th>组件2</th>
            <th>模板相似度</th>
            <th>脚本相似度</th>
            <th>总相似度</th>
          </tr>
        </thead>
        <tbody id="similarityTableBody">
          <!-- 数据将通过JavaScript动态填充 -->
        </tbody>
      </table>

      <!-- 分页控件 -->
      <div class="pagination" id="pagination">
        <button id="firstPage">首页</button>
        <button id="prevPage2">上一页</button>
        <span class="page-info"><span id="currentPage">1</span> 页,共
          <span id="totalPages">1</span></span>
        <button id="nextPage2">下一页</button>
        <button id="lastPage">末页</button>
        <select id="pageSize">
          <option value="10">10条/页</option>
          <option value="20" selected>20条/页</option>
          <option value="50">50条/页</option>
          <option value="100">100条/页</option>
        </select>
      </div>
    </div>

    <!-- 优化建议 -->
    <div id="recommendation-tab" class="tab-content">
      <section class="analysis-section">
        <h2>优化建议</h2>
        <p>基于组件复用度和相似度分析结果,我们为您提供以下优化建议:</p>

        <div class="recommendation">
          <h3>📌 复用度优化建议</h3>
          <ul>
            <li>
              清理未使用的组件:发现
              <span id="unusedCount">35</span>
              个组件未被使用,建议评估后进行清理以减小项目体积
            </li>
            <li>
              推广高复用组件:识别出
              <span id="highReuseCount">500</span>
              个复用次数大于10的组件,建议在合适场景中进一步推广使用
            </li>
            <li>
              封装通用功能:对于中等复用频率的组件,建议提取通用功能为独立组件或工具函数
            </li>
          </ul>
        </div>

        <div class="recommendation">
          <h3>📌 相似度优化建议</h3>
          <ul>
            <li>
              组件合并:发现
              <span id="highSimilarityCount">-</span>
              对高相似度(≥90%)组件,建议评估是否可以合并为一个通用组件
            </li>
            <li>
              代码重构:对于中等相似度(70%-90%)的
              <span id="medSimilarityCount">-</span>
              对组件,建议进行代码重构以提高一致性
            </li>
            <li>
              建立组件规范:针对相似度较高的组件,建议制定组件开发规范以减少重复开发
            </li>
          </ul>
        </div>

        <div class="recommendation">
          <h3>📌 综合优化建议</h3>
          <ul>
            <li>
              组件文档化:为高复用和高相似度组件编写详细文档,方便团队成员理解和使用
            </li>
            <li>组件库建设:将高复用组件整理为内部组件库,提升开发效率</li>
            <li>定期审查:建议每季度进行一次组件分析,持续优化组件结构</li>
          </ul>
        </div>
      </section>
    </div>

    <footer class="footer">
      <p>
        © 2025 前端架构组 | 组件化规范 v2.1 | 数据更新时间:
        <span id="updateTime"></span>
      </p>
    </footer>

    <script src="./module.js"></script>
    <script>
      // 标签页切换功能
      document.querySelectorAll('.tab').forEach((tab) => {
        tab.addEventListener('click', () => {
          // 移除所有活动状态
          document
            .querySelectorAll('.tab')
            .forEach((t) => t.classList.remove('active'))
          document
            .querySelectorAll('.tab-content')
            .forEach((c) => c.classList.remove('active'))

          // 添加活动状态到当前标签
          tab.classList.add('active')

          // 显示对应的内容
          const tabId = tab.getAttribute('data-tab')
          document.getElementById(`${tabId}-tab`).classList.add('active')

          // 初始化优化建议标签页
          if (tabId === 'recommendation') {
            initRecommendationTab()
          }
        })
      })

      // 初始化优化建议标签页
      function initRecommendationTab() {
        // 获取复用度分析数据
        const noUseComponents = componentData.filter(
          (item) => item.count === 0
        ).length
        const highReuseComponents = componentData.filter(
          (item) => item.count > 10
        ).length

        // 获取相似度分析数据
        const highSimilarityPairs = highSimilarityData.filter(
          (item) => item.totalSimilarity >= 0.9
        ).length
        const medSimilarityPairs = highSimilarityData.filter(
          (item) => item.totalSimilarity >= 0.7 && item.totalSimilarity < 0.9
        ).length

        // 更新优化建议中的数据
        document.getElementById('unusedCount').textContent = noUseComponents
        document.getElementById('highReuseCount').textContent =
          highReuseComponents
        document.getElementById('highSimilarityCount').textContent =
          highSimilarityPairs
        document.getElementById('medSimilarityCount').textContent =
          medSimilarityPairs
      }

      // 复用度分析相关代码
      // 页面初始化
      document.addEventListener('DOMContentLoaded', () => {
        // 设置时间信息
        const now = new Date()
        document.getElementById('updateTime').textContent = now.toLocaleString()

        // 只在复用度分析标签页激活时执行初始化
        if (document.getElementById('reuse-tab').classList.contains('active')) {
          initReuseAnalysis()
        }
      })

      // 初始化复用度分析
      function initReuseAnalysis() {
        // 设置时间信息
        const now = new Date()
        document.getElementById('updateTime').textContent = now
          .toISOString()
          .split('T')[0]

        // 计算统计指标
        const totalComponents = componentData.length
        const useComponents = componentData.filter(
          (item) => item.count > 0
        ).length
        const noUseComponents = componentData.filter(
          (item) => item.count === 0
        ).length
        const moreThanOnceComponents = componentData.filter(
          (item) => item.count > 1
        ).length
        const maxComponent = componentData[0]
        const totalCount = componentData.reduce(
          (sum, item) => sum + item.count,
          0
        )
        const avgRate = (totalCount / totalComponents).toFixed(1)

        // 更新指标
        document.getElementById('totalComponents').textContent = totalComponents
        document.getElementById('useComponents').textContent = useComponents
        document.getElementById('noUseComponents').textContent = noUseComponents
        document.getElementById('moreThanOnceComponents').textContent =
          moreThanOnceComponents
        document.getElementById('maxCount').textContent = maxComponent.count
        document.getElementById('maxComponentName').textContent =
          getNameFromPath(maxComponent.path)
        document.getElementById('avgRate').textContent = avgRate

        // 初始化分页
        initPagination(componentData)

        // 初始化图表
        initChart()

        // 初始化搜索和筛选
        initSearchFilter()
      }

      // 从路径提取组件名
      function getNameFromPath(path) {
        const parts = path.split('/')
        const fileName =
          parts[parts.length - 1] === 'index.vue'
            ? parts[parts.length - 2]
            : parts[parts.length - 1].replace('.vue', '')
        return fileName || path
      }

      // 分页实现
      function initPagination(data) {
        let itemsPerPage = 10 // 默认每页显示10条
        let currentPage = 1
        let filteredData = [...data]
        const totalPages = Math.ceil(filteredData.length / itemsPerPage)

        // 更新分页信息
        const updatePageInfo = () => {
          document.getElementById('pageInfo').textContent =
            `第 ${currentPage} 页 / 共 ${totalPages} 页`
          document.getElementById('prevPage').disabled = currentPage === 1
          document.getElementById('nextPage').disabled =
            currentPage === totalPages
          document.getElementById('visibleCount').textContent = itemsPerPage
        }

        // 渲染当前页
        const renderCurrentPage = () => {
          const startIndex = (currentPage - 1) * itemsPerPage
          const endIndex = Math.min(
            startIndex + itemsPerPage,
            filteredData.length
          )
          const pageData = filteredData.slice(startIndex, endIndex)

          const listContainer = document.getElementById('reuseList')
          listContainer.innerHTML = ''

          pageData.forEach((item) => {
            const card = createComponentCard(item)
            listContainer.appendChild(card)
          })

          updatePageInfo()
        }

        // 创建组件卡片
        const createComponentCard = (item) => {
          const card = document.createElement('div')
          card.className = 'component-card'

          // 分类复用级别
          let reuseClass = 'low-reuse'
          let usageClass = 'usage-low'
          if (item.count > 10) {
            reuseClass = 'high-reuse'
            usageClass = 'usage-high'
          } else if (item.count > 5) {
            reuseClass = 'med-reuse'
            usageClass = 'usage-med'
          }

          card.classList.add(reuseClass)

          // 计算使用条形的宽度比例
          const percentage = Math.min(100, (item.count / 35) * 100)

          // 提取组件名称
          const name = getNameFromPath(item.path)

          card.innerHTML = `
                <h3>${name} <span class="count ${item.count > 10 ? 'high' : item.count > 5 ? 'med' : ''}">${item.count}次</span></h3>
                <div class="path">${item.path}</div>
                <div class="usage-bar">
                    <div class="usage-fill ${usageClass}" style="width: ${percentage}%"></div>
                </div>
            `

          return card
        }

        // 分页按钮事件
        document.getElementById('prevPage').addEventListener('click', () => {
          if (currentPage > 1) {
            currentPage--
            renderCurrentPage()
          }
        })

        document.getElementById('nextPage').addEventListener('click', () => {
          if (currentPage < totalPages) {
            currentPage++
            renderCurrentPage()
          }
        })

        // 每页显示条数选择器事件
        document
          .getElementById('reusePageSize')
          .addEventListener('change', (e) => {
            itemsPerPage = parseInt(e.target.value)
            currentPage = 1 // 重置到第一页
            renderCurrentPage()
          })

        // 初始渲染
        renderCurrentPage()
      }

      // 图表初始化
      function initChart() {
        const ctx = document.getElementById('reuseChart').getContext('2d')

        // 按复用次数分组统计
        const reuseLevels = {
          high: componentData.filter((item) => item.count > 10).length,
          medium: componentData.filter(
            (item) => item.count > 5 && item.count <= 10
          ).length,
          low: componentData.filter((item) => item.count <= 5).length
        }

        // 取复用次数TOP20组件
        const topComponents = [...componentData].slice(0, 20)

        new Chart(ctx, {
          type: 'bar',
          data: {
            labels: topComponents.map((item) => getNameFromPath(item.path)),
            datasets: [
              {
                label: '组件复用次数',
                data: topComponents.map((item) => item.count),
                backgroundColor: [
                  'rgba(155, 89, 182, 0.7)', // 高频
                  'rgba(52, 152, 219, 0.7)', // 中频
                  'rgba(243, 156, 18, 0.7)' // 低频
                ],
                borderWidth: 1
              }
            ]
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            scales: {
              y: {
                beginAtZero: true,
                title: {
                  display: true,
                  text: '复用次数'
                }
              },
              x: {
                title: {
                  display: true,
                  text: '组件名称'
                },
                ticks: {
                  autoSkip: false,
                  maxRotation: 45,
                  minRotation: 45
                }
              }
            },
            plugins: {
              legend: {
                position: 'top'
              },
              title: {
                display: true,
                text: '复用率TOP20组件分布',
                font: {
                  size: 16
                }
              },
              tooltip: {
                callbacks: {
                  footer: (tooltipItems) => {
                    const item = topComponents[tooltipItems[0].dataIndex]
                    return `路径: ${item.path}`
                  }
                }
              }
            }
          }
        })
      }

      // 搜索和筛选功能
      function initSearchFilter() {
        const searchInput = document.getElementById('searchInput')
        const filterButtons = document.querySelectorAll('.filter-btn')
        let currentFilter = 'all'

        // 搜索功能
        searchInput.addEventListener('input', (e) => {
          filterComponents(e.target.value, currentFilter)
        })

        // 筛选按钮
        filterButtons.forEach((btn) => {
          btn.addEventListener('click', () => {
            filterButtons.forEach((b) => b.classList.remove('active'))
            btn.classList.add('active')
            currentFilter = btn.dataset.filter
            filterComponents(searchInput.value, currentFilter)
          })
        })
      }

      // 组件筛选逻辑
      function filterComponents(searchTerm = '', filter = 'all') {
        const filteredData = componentData.filter((item) => {
          // 搜索过滤
          const matchSearch =
            item.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
            getNameFromPath(item.path)
              .toLowerCase()
              .includes(searchTerm.toLowerCase())

          // 复用频率过滤
          let matchFilter = true
          switch (filter) {
            case 'high':
              matchFilter = item.count > 10
              break
            case 'med':
              matchFilter = item.count > 5 && item.count <= 10
              break
            case 'low':
              matchFilter = item.count <= 5
              break
            case 'zero':
              matchFilter = item.count === 0
              break
          }

          return matchSearch && matchFilter
        })

        // 更新分页数据
        initPagination(filteredData)
      }

      // 相似度分析相关代码
      // 分页相关变量
      let currentPage2 = 1
      let pageSize = 20
      let currentData = []
      let similarityData = []
      let currentSimilarityFilter = 'all'

      // 相似度分析标签页激活时初始化
      document
        .querySelector('[data-tab="similarity"]')
        .addEventListener('click', () => {
          // 延迟加载数据,确保DOM完全渲染
          setTimeout(() => {
            initSimilarityAnalysis()
          }, 100)
        })

      // 初始化相似度分析
      function initSimilarityAnalysis() {
        // 设置时间信息
        const now = new Date()
        document.getElementById('updateTime').textContent = now
          .toISOString()
          .split('T')[0]

        // 加载数据
        loadSimilarityData()

        // 初始化搜索和筛选
        initSimilaritySearchFilter()
      }

      // 从component-similarity.json加载数据并渲染表格
      async function loadSimilarityData() {
        try {
          const data = highSimilarityData

          // 保存数据
          similarityData = data
          currentData = [...data]

          // 计算统计指标
          calculateSimilarityMetrics(data)

          // 渲染第一页
          renderTable()
          setupPagination()
        } catch (error) {
          console.error('加载相似度数据失败:', error)
        }
      }

      // 计算相似度分析统计指标
      function calculateSimilarityMetrics(data) {
        // 组件对总数
        const totalPairs = data.length

        // 高相似度对 (≥90%)
        const highSimilarityPairs = data.filter(
          (item) => item.totalSimilarity >= 0.9
        ).length

        // 中相似度对 (70%-90%)
        const medSimilarityPairs = data.filter(
          (item) => item.totalSimilarity >= 0.7 && item.totalSimilarity < 0.9
        ).length

        // 平均相似度
        const avgSimilarity =
          data.reduce((sum, item) => sum + item.totalSimilarity, 0) /
          data.length

        // 更新指标
        document.getElementById('totalPairs').textContent = totalPairs
        document.getElementById('highSimilarityPairs').textContent =
          highSimilarityPairs
        document.getElementById('medSimilarityPairs').textContent =
          medSimilarityPairs
        document.getElementById('avgSimilarity').textContent =
          (avgSimilarity * 100).toFixed(2) + '%'
      }

      // 渲染表格数据
      function renderTable() {
        const tableBody = document.getElementById('similarityTableBody')
        tableBody.innerHTML = ''

        // 计算当前页的数据
        const startIndex = (currentPage2 - 1) * pageSize
        const endIndex = Math.min(startIndex + pageSize, currentData.length)
        const displayData = currentData.slice(startIndex, endIndex)

        displayData.forEach((pair) => {
          const row = document.createElement('tr')

          // 组件1
          const comp1Cell = document.createElement('td')
          comp1Cell.innerHTML = `<div class="path">${pair.comp1}</div>`
          row.appendChild(comp1Cell)

          // 组件2
          const comp2Cell = document.createElement('td')
          comp2Cell.innerHTML = `<div class="path">${pair.comp2}</div>`
          row.appendChild(comp2Cell)

          // 模板相似度
          const templateCell = document.createElement('td')
          templateCell.innerHTML = `
            <div class="similarity-value ${getSimilarityClass(pair.templateSimilarity)}">
              ${(pair.templateSimilarity * 100).toFixed(2)}%
            </div>
            <div class="similarity-bar">
              <div 
                class="similarity-fill ${getSimilarityClass(pair.templateSimilarity)}" 
                style="width: ${(pair.templateSimilarity * 100).toFixed(2)}%"
              ></div>
            </div>
          `
          row.appendChild(templateCell)

          // 脚本相似度
          const scriptCell = document.createElement('td')
          scriptCell.innerHTML = `
            <div class="similarity-value ${getSimilarityClass(pair.scriptSimilarity)}">
              ${(pair.scriptSimilarity * 100).toFixed(2)}%
            </div>
            <div class="similarity-bar">
              <div 
                class="similarity-fill ${getSimilarityClass(pair.scriptSimilarity)}" 
                style="width: ${(pair.scriptSimilarity * 100).toFixed(2)}%"
              ></div>
            </div>
          `
          row.appendChild(scriptCell)

          // 总相似度
          const totalCell = document.createElement('td')
          totalCell.innerHTML = `
            <div class="similarity-value ${getSimilarityClass(pair.totalSimilarity)}">
              ${(pair.totalSimilarity * 100).toFixed(2)}%
            </div>
            <div class="similarity-bar">
              <div 
                class="similarity-fill ${getSimilarityClass(pair.totalSimilarity)}" 
                style="width: ${(pair.totalSimilarity * 100).toFixed(2)}%"
              ></div>
            </div>
          `
          row.appendChild(totalCell)

          tableBody.appendChild(row)
        })

        // 更新分页信息
        updatePaginationInfo()
      }

      // 设置分页控件事件监听器
      function setupPagination() {
        // 首页按钮
        document.getElementById('firstPage').addEventListener('click', () => {
          currentPage2 = 1
          renderTable()
        })

        // 上一页按钮
        document.getElementById('prevPage2').addEventListener('click', () => {
          if (currentPage2 > 1) {
            currentPage2--
            renderTable()
          }
        })

        // 下一页按钮
        document.getElementById('nextPage2').addEventListener('click', () => {
          const totalPages = Math.ceil(currentData.length / pageSize)
          if (currentPage2 < totalPages) {
            currentPage2++
            renderTable()
          }
        })

        // 末页按钮
        document.getElementById('lastPage').addEventListener('click', () => {
          currentPage2 = Math.ceil(currentData.length / pageSize)
          renderTable()
        })

        // 每页显示数量选择器
        document.getElementById('pageSize').addEventListener('change', (e) => {
          pageSize = parseInt(e.target.value)
          currentPage2 = 1 // 重置到第一页
          renderTable()
        })
      }

      // 更新分页信息显示
      function updatePaginationInfo() {
        const totalPages = Math.ceil(currentData.length / pageSize)
        document.getElementById('currentPage').textContent = currentPage2
        document.getElementById('totalPages').textContent = totalPages

        // 更新按钮状态
        document.getElementById('firstPage').disabled = currentPage2 === 1
        document.getElementById('prevPage2').disabled = currentPage2 === 1
        document.getElementById('nextPage2').disabled =
          currentPage2 === totalPages
        document.getElementById('lastPage').disabled =
          currentPage2 === totalPages
      }

      // 根据相似度值返回相应的CSS类
      function getSimilarityClass(similarity) {
        if (similarity >= 0.9) {
          return 'high'
        } else if (similarity >= 0.7) {
          return 'med'
        } else {
          return 'low'
        }
      }

      // 初始化相似度分析搜索和筛选功能
      function initSimilaritySearchFilter() {
        const searchInput = document.getElementById('similaritySearchInput')
        const filterButtons = document.querySelectorAll(
          '#similarity-tab .filter-btn'
        )
        currentSimilarityFilter = 'all'

        // 搜索功能
        searchInput.addEventListener('input', (e) => {
          filterSimilarityData(e.target.value, currentSimilarityFilter)
        })

        // 筛选按钮
        filterButtons.forEach((btn) => {
          btn.addEventListener('click', () => {
            filterButtons.forEach((b) => b.classList.remove('active'))
            btn.classList.add('active')
            currentSimilarityFilter = btn.dataset.filter
            filterSimilarityData(searchInput.value, currentSimilarityFilter)
          })
        })
      }

      // 相似度数据分析筛选逻辑
      function filterSimilarityData(searchTerm = '', filter = 'all') {
        const filteredData = similarityData.filter((item) => {
          // 搜索过滤
          const matchSearch =
            item.comp1.toLowerCase().includes(searchTerm.toLowerCase()) ||
            item.comp2.toLowerCase().includes(searchTerm.toLowerCase())

          // 相似度过滤
          let matchFilter = true
          switch (filter) {
            case 'high':
              matchFilter = item.totalSimilarity >= 0.9
              break
            case 'med':
              matchFilter =
                item.totalSimilarity >= 0.7 && item.totalSimilarity < 0.9
              break
            case 'low':
              matchFilter = item.totalSimilarity < 0.7
              break
          }

          return matchSearch && matchFilter
        })

        // 更新当前数据和分页
        currentData = filteredData
        currentPage2 = 1
        renderTable()
      }
    </script>
  </body>
</html>