一、安装依赖
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 对模块依赖关系的聚合显示优化,主要目的是简化统计信息的可读性
所以这就是为什么在webpack的配置中 要进行module.name.replace(/ \+ \d+ modules$/, '')的必要性,为了统计数据更准确。
四、提取stats.json和eslint-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>