cursor agent 使用体验
前言
最近公司在考虑重构整个开发者后台,这是公司非常重要的后台项目,有着非常多的功能模块,能够控制不同游戏在不同包,不同渠道,不同大区,不同版本,不同平台中的各项配置,能最大程度的控制和管理发行的游戏包。
同时还对游戏账号,玩家实名,防沉迷,问卷调查,短信,迁移,错误日志等等各种涉及到公司业务以及第三方服务的模块都能够在后台上操作以及管理,总之,功能十分强大。
之前这个后台使用了公司的技术总监在 react 的基础上二次封装的 mo-react,本意是希望服务端人员高效快速开发,但是随着公司发展壮大以及 AI 的普及,mo-react 反而变得不是非常好用,因此决定抛弃 mo-react 直接使用 react 。拥抱开源的技术栈来更好的使用 AI 提升开发效率。
评估重构这个后台的工时和任务就交给了我。也就是这个契机下,我决定写一个页面来更好的展示评估结果。
评估难度我决定主要按照代码行数和模块文件数量来判断。个人认为是一个比较粗略的评估方式,仅供参考。
获取模块数据
基本方向
我希望有个 node.js 脚本,用来把 /pages 目录下的所有模块文件夹中的文件都遍历出来,然后获取其中文件数量,每个文件行数,以及是否 import 了 mo-react 这个模块。生成一个 json 数据,然后我再来对这里面的数据做可视化页面。
此JSON数据是我模拟的,并非真实数据。
代码实现和生成数据
- node.js 脚本
- 生成的JSON数据
const fs = require("fs");
const path = require("path");
function isFunctionComponent(content) {
// 判断是否引入了 FC
if (
/import\s+\{\s*FC\s*\}\s+from\s+['"]react['"]/.test(content) ||
/import\s+type\s+\{\s*FC\s*\}\s+from\s+['"]react['"]/.test(content) ||
/React\.FC/.test(content)
) {
return true;
}
// 其他常见函数式组件写法,包括带类型注解的 FC 写法
const functionComponentPatterns = [
/export\s+default\s+function\s+[A-Z][A-Za-z0-9_]*\s*\(/,
/function\s+[A-Z][A-Za-z0-9_]*\s*\(/,
/const\s+[A-Z][A-Za-z0-9_]*\s*=\s*\(?.*\)?\s*=>/,
/export\s+const\s+[A-Z][A-Za-z0-9_]*\s*=\s*\(?.*\)?\s*=>/,
// 新增:带类型注解的 FC 写法
/const\s+[A-Z][A-Za-z0-9_]*\s*:\s*FC(<.*?>)?\s*=\s*\(?.*\)?\s*=>/,
/export\s+const\s+[A-Z][A-Za-z0-9_]*\s*:\s*FC(<.*?>)?\s*=\s*\(?.*\)?\s*=>/,
];
return functionComponentPatterns.some((pattern) => pattern.test(content));
}
function listAllFilesWithLineCountAndMoReactAndFun(dir, prefix = "") {
const result = [];
const items = fs.readdirSync(dir, { withFileTypes: true });
items.forEach((item) => {
const relPath = prefix ? `${prefix}/${item.name}` : item.name;
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
result.push(
...listAllFilesWithLineCountAndMoReactAndFun(fullPath, relPath)
);
} else if (item.isFile()) {
const content = fs.readFileSync(fullPath, "utf-8");
const lineCount = content.split("\n").length;
const isMoReact =
/from\s+['"]mo-react['"]|require\(['"]mo-react['"]\)/.test(content);
const isFun = isFunctionComponent(content);
result.push({ file: relPath, line: lineCount, isMoReact, isFun });
}
});
return result;
}
const baseDir = path.join(__dirname, "client/src/node_modules/g-view/pages");
const folders = fs
.readdirSync(baseDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
let allfiles = {};
folders.forEach((folder) => {
const folderPath = path.join(baseDir, folder);
const files = listAllFilesWithLineCountAndMoReactAndFun(folderPath);
allfiles[folder] = files;
});
fs.writeFileSync(
path.join(__dirname, "allfiles.json"),
JSON.stringify(allfiles, null, 2),
"utf-8"
);
console.log("已生成 allfiles.json 文件");
{
"KingMonkey": [
{
"file": "index.tsx",
"line": 126,
"isMoReact": false,
"isFun": true
},
{
"file": "server.ts",
"line": 63,
"isMoReact": false,
"isFun": false
}
],
"RiverIce": [
{
"file": "index.tsx",
"line": 265,
"isMoReact": false,
"isFun": true
},
{
"file": "server.ts",
"line": 66,
"isMoReact": false,
"isFun": false
}
],
"JungleGold": [
{
"file": "index.tsx",
"line": 388,
"isMoReact": true,
"isFun": false
},
{
"file": "server.ts",
"line": 85,
"isMoReact": false,
"isFun": false
}
],
"CatHorse": [
{
"file": "index.tsx",
"line": 43,
"isMoReact": false,
"isFun": true
},
{
"file": "server.ts",
"line": 32,
"isMoReact": false,
"isFun": false
}
],
"SunDream": [
{
"file": "index.less",
"line": 70,
"isMoReact": false,
"isFun": false
},
{
"file": "index.tsx",
"line": 243,
"isMoReact": true,
"isFun": false
},
{
"file": "server.ts",
"line": 65,
"isMoReact": false,
"isFun": false
}
],
"SilverSky": [
{
"file": "detail/index.tsx",
"line": 254,
"isMoReact": true,
"isFun": false
},
{
"file": "index.less",
"line": 74,
"isMoReact": false,
"isFun": false
},
{
"file": "index.tsx",
"line": 122,
"isMoReact": true,
"isFun": false
},
{
"file": "server.ts",
"line": 159,
"isMoReact": false,
"isFun": false
}
],
"IceIce": [
{
"file": "index.tsx",
"line": 185,
"isMoReact": true,
"isFun": false
},
{
"file": "server.ts",
"line": 67,
"isMoReact": false,
"isFun": false
}
],
....
}
展示模块文件信息
基本方向
我给 json 数据做了三种数据分析
- 按照文件数量,分为:
小于3个文件
,大于3个小于5个
,5个以上
。 - 按照模块总代码长度行数分(去除 less 文件) 分为:
简单模块(≤500代码行)
,中等模块(501~1200代码行)
,复杂模块(1201~2000代码行)
,超复杂模块(>2000代码行)
。 - 最后是查看其中 mo-react 的占比 ,分为:
mo-react组件
和函数式组件
。
然后了一些简单的交互,分类展示以及侧边的评估栏。最后页面如下。
页面展示和代码
- 页面展示
- 页面代码
import { useMemo } from 'react';
import ReactECharts from 'echarts-for-react';
import allfilesData from './allfiles.json';
import 'antd/dist/reset.css';
import { Tabs, Tag, Input, Select, Button } from 'antd';
import { useState } from 'react';
import { UpOutlined, DownOutlined } from '@ant-design/icons';
import type { EChartsOption } from 'echarts';
const allfiles = allfilesData as Record<string, any[]>;
const groupKeys = (data: Record<string, any[]>) => {
const groups = {
'小于三个文件': [] as { key: string; arr: any[] }[],
'4到5个文件': [] as { key: string; arr: any[] }[],
'大于5个文件': [] as { key: string; arr: any[] }[],
};
Object.entries(data).forEach(([key, arr]) => {
if (arr.length <= 3) groups['小于三个文件'].push({ key, arr });
else if (arr.length <= 5) groups['4到5个文件'].push({ key, arr });
else groups['大于5个文件'].push({ key, arr });
});
return groups;
};
const style = `
.moreact-badge {
position: absolute;
top: -6px;
right: 4px;
background: #70bf66;
color: #fff;
font-size: 10px;
font-weight: bold;
border-radius: 3px;
padding: 0 6px;
line-height: 16px;
box-shadow: 0 1px 4px #e6e6e6;
z-index: 1;
pointer-events: none;
}
@media (max-width: 900px) {
.showrecon-main {
flex-direction: column !important;
padding: 0 4px !important;
}
.showrecon-charts {
flex-direction: column !important;
gap: 16px !important;
min-height: unset !important;
}
.showrecon-chart-item {
width: 100% !important;
min-width: 0 !important;
margin-bottom: 12px;
padding: 12px !important;
}
.showrecon-tabs {
padding: 16px !important;
min-height: unset !important;
}
.showrecon-eval {
width: 100% !important;
margin-left: 0 !important;
margin-top: 18px;
min-height: unset !important;
padding: 16px !important;
}
.showrecon-title {
font-size: 22px !important;
margin-bottom: 6px !important;
}
.showrecon-divider {
width: 50px !important;
height: 3px !important;
margin-bottom: 18px !important;
}
.showrecon-search {
width: 100% !important;
margin-bottom: 8px !important;
}
}
`;
const ShowReconstructionDemo = () => {
const groups = useMemo(() => groupKeys(allfiles), []);
const [search, setSearch] = useState('');
const [activeTabKey, setActiveTabKey] = useState('simple');
const [activeGroupKey, setActiveGroupKey] = useState<string | null>(null);
const [tabMode, setTabMode] = useState<'group' | 'complexity'>('complexity');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [tabsVisible, setTabsVisible] = useState(true);
// 统计数据
const groupStats = useMemo(() => {
// 各分组 key 数量
const groupCount = Object.entries(groups).map(([group, keys]) => ({
name: group,
value: keys.length,
}));
// 每个 key 下 isFun/isMoReact 数量
const keyFunStats = Object.entries(allfiles).map(([key, arr]) => ({
key,
isFun: arr.filter(item => item.isFun).length,
isMoReact: arr.filter(item => item.isMoReact).length,
}));
return { groupCount, keyFunStats };
}, [groups]);
// 饼图配置
const groupPieOption = useMemo((): EChartsOption => ({
title: { text: '各个主要模块下文件数量', left: 'center' },
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
name: '文件数量',
type: 'pie',
radius: '50%',
data: groupStats.groupCount,
label: { formatter: '{b}: {c} ({d}%)' },
color: ['#3788fb', '#ffe748', '#d83b48'],
},
],
}), [groupStats.groupCount]);
// 统计 isMoReact 和 isFun 文件占比(仅统计 .tsx 文件)
const pieStats = useMemo(() => {
let total = 0, moReact = 0, fun = 0;
Object.values(allfiles).forEach(arr => {
arr.forEach(item => {
if (item.file && item.file.endsWith('.tsx')) {
total += 1;
if (item.isMoReact) moReact += 1;
if (item.isFun) fun += 1;
}
});
});
return [
{ name: 'mo-react', value: moReact },
{ name: '函数式组件', value: fun },
{ name: '其他', value: total - moReact - fun }
];
}, []);
// 统计每个模块的 .tsx 文件总行数、文件数、isMoReact占比、isFun占比
const moduleStats = useMemo(() => {
return Object.entries(allfiles).map(([key, arr]) => {
const tsxFiles = arr.filter(item => item.file && item.file.endsWith('.tsx') || item.file.endsWith('.ts'));
const totalLines = tsxFiles.reduce((sum, item) => sum + (item.line || 0), 0);
const fileCount = tsxFiles.length;
const moReactCount = tsxFiles.filter(item => item.isMoReact).length;
const funCount = tsxFiles.filter(item => item.isFun).length;
return {
key,
totalLines,
fileCount,
moReactRatio: fileCount ? moReactCount / fileCount : 0,
funRatio: fileCount ? funCount / fileCount : 0,
};
});
}, []);
// 全局 isMoReact/isFun 占比饼状图配置
const moReactFunPieOption = useMemo((): EChartsOption => ({
title: { text: 'mo-react/函数式组件 文件占比', left: 'center' },
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
name: '文件占比',
type: 'pie',
radius: '50%',
data: pieStats,
label: { formatter: '{b}: {c} ({d}%)' },
color: ['#70bf66', '#3788fb', '#ffe748'],
},
],
}), [pieStats]);
// 饼图点击事件,切换Tab
const groupPieEvents = {
click: (params: any) => {
if (params && params.name) {
setActiveGroupKey(params.name);
setTabMode('group');
setActiveTabKey(params.name);
setTabsVisible(true);
}
}
};
// 先根据activeGroupKey过滤模块
let groupModuleKeys: string[] | null = null;
if (activeGroupKey && (groups as Record<string, { key: string; arr: any[] }[]>)[activeGroupKey]) {
groupModuleKeys = (groups as Record<string, { key: string; arr: any[] }[]>)[activeGroupKey].map((g: { key: string }) => g.key);
}
const filterByGroup = (arr: typeof moduleStats) => groupModuleKeys ? arr.filter(item => groupModuleKeys!.includes(item.key)) : arr;
const simpleModules = filterByGroup(moduleStats.filter(item => item.totalLines <= 500 && item.key.includes(search)));
const mediumModules = filterByGroup(moduleStats.filter(item => item.totalLines > 500 && item.totalLines <= 1200 && item.key.includes(search)));
const complexModules = filterByGroup(moduleStats.filter(item => item.totalLines > 1200 && item.key.includes(search)));
const groupModules = groupModuleKeys ? moduleStats.filter(item => groupModuleKeys!.includes(item.key) && item.key.includes(search)) : [];
// 排序字段自动切换
const sortField: 'fileCount' | 'totalLines' = tabMode === 'group' ? 'fileCount' : 'totalLines';
const sortModules = (arr: typeof moduleStats) => {
return [...arr].sort((a, b) => {
const v1 = a[sortField] ?? 0;
const v2 = b[sortField] ?? 0;
return sortOrder === 'asc' ? v1 - v2 : v2 - v1;
});
};
// Tabs渲染内容
let moduleTabs;
if (tabMode === 'group') {
moduleTabs = [
{
key: '小于三个文件',
label: '小于三个文件',
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(
((groups as Record<string, { key: string; arr: any[] }[]>)['小于三个文件'] || [])
.filter(g => g.key.includes(search))
.map(g => moduleStats.find(item => item.key === g.key)!)
.filter(Boolean)
).map(g => {
const arr = allfiles[g.key] || [];
const hasMoReact = arr.some((item: any) => item.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={g.key}>
<Tag color="blue" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{g.key}({g.fileCount}文件)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
{
key: '4到5个文件',
label: '4到5个文件',
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(
((groups as Record<string, { key: string; arr: any[] }[]>)['4到5个文件'] || [])
.filter(g => g.key.includes(search))
.map(g => moduleStats.find(item => item.key === g.key)!)
.filter(Boolean)
).map(g => {
const arr = allfiles[g.key] || [];
const hasMoReact = arr.some((item: any) => item.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={g.key}>
<Tag color="gold" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{g.key}({g.fileCount}文件)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
{
key: '大于5个文件',
label: '大于5个文件',
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(
((groups as Record<string, { key: string; arr: any[] }[]>)['大于5个文件'] || [])
.filter(g => g.key.includes(search))
.map(g => moduleStats.find(item => item.key === g.key)!)
.filter(Boolean)
).map(g => {
const arr = allfiles[g.key] || [];
const hasMoReact = arr.some((item: any) => item.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={g.key}>
<Tag color="red" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{g.key}({g.fileCount}文件)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
];
} else {
// 新增超复杂模块
const superComplexModules = filterByGroup(moduleStats.filter(item => item.totalLines > 2000 && item.key.includes(search)));
const complexModulesNew = filterByGroup(moduleStats.filter(item => item.totalLines > 1200 && item.totalLines <= 2000 && item.key.includes(search)));
moduleTabs = [
{
key: 'simple',
label: `简单模块(${simpleModules.length})`,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(simpleModules).map((item: any) => {
const arr = allfiles[item.key] || [];
const hasMoReact = arr.some((i: any) => i.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={item.key}>
<Tag color="blue" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{item.key}({item.totalLines}行)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
{
key: 'medium',
label: `中等模块(${mediumModules.length})`,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(mediumModules).map((item: any) => {
const arr = allfiles[item.key] || [];
const hasMoReact = arr.some((i: any) => i.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={item.key}>
<Tag color="gold" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{item.key}({item.totalLines}行)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
{
key: 'complex',
label: `复杂模块(${complexModules.length})`,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(complexModules).map((item: any) => {
const arr = allfiles[item.key] || [];
const hasMoReact = arr.some((i: any) => i.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={item.key}>
<Tag color="red" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{item.key}({item.totalLines}行)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
{
key: 'superComplex',
label: `超复杂模块(${superComplexModules.length})`,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
{sortModules(superComplexModules).map((item: any) => {
const arr = allfiles[item.key] || [];
const hasMoReact = arr.some((i: any) => i.isMoReact);
return (
<span style={{ position: 'relative', display: 'inline-block' }} key={item.key}>
<Tag color="#722ed1" style={{ fontSize: 14, padding: '4px 10px', marginBottom: 4 }}>
{item.key}({item.totalLines}行)
</Tag>
{hasMoReact && <span className="moreact-badge">mo</span>}
</span>
);
})}
</div>
),
},
];
}
// 复杂度分组统计
const complexityStats = useMemo(() => {
let simple = 0, medium = 0, complex = 0, superComplex = 0;
moduleStats.forEach(item => {
if (item.totalLines > 2000) superComplex += 1;
else if (item.totalLines > 1200) complex += 1;
else if (item.totalLines > 500) medium += 1;
else simple += 1;
});
return [
{ name: '简单模块(≤500代码行)', value: simple },
{ name: '中等模块(501~1200代码行)', value: medium },
{ name: '复杂模块(1201~2000代码行)', value: complex },
{ name: '超复杂模块(>2000代码行)', value: superComplex },
];
}, [moduleStats]);
// 复杂度分布饼图配置
const complexityPieOption = useMemo((): EChartsOption => ({
title: { text: '模块复杂度分布', left: 'center' },
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
name: '模块数量',
type: 'pie',
radius: '50%',
data: complexityStats,
label: { formatter: '{b}: {c} ({d}%)' },
color: ['#3788fb', '#ffe748', '#d83b48', '#722ed1'],
},
],
}), [complexityStats]);
// 饼图点击事件,切换Tab
const complexityPieEvents = {
click: (params: any) => {
if (params && params.name) {
if (params.name.startsWith('简单')) setActiveTabKey('simple');
else if (params.name.startsWith('中等')) setActiveTabKey('medium');
else if (params.name.startsWith('复杂模块')) setActiveTabKey('complex');
else if (params.name.startsWith('超复杂')) setActiveTabKey('superComplex');
setTabMode('complexity');
setActiveGroupKey(null);
setTabsVisible(true);
}
}
};
const estimateRate = 8;
const estimateRateMedium = 5;
const estimateRateComplex = 3;
const estimateRateSuperComplex = 0.5;
// 计算各类整体工时
const simpleDays = Math.ceil(simpleModules.length / estimateRate);
const mediumDays = Math.ceil(mediumModules.length / estimateRateMedium);
const complexDays = Math.ceil(complexModules.length / estimateRateComplex);
const superComplexDays = Math.ceil(moduleStats.filter(item => item.totalLines > 2000).length / estimateRateSuperComplex);
const totalDays = simpleDays + mediumDays + complexDays + superComplexDays;
return (<>
<style>{style}</style>
<div style={{ minHeight: '100vh', background: '#f5f6fa', padding: '32px 0' }}>
<h1 className="showrecon-title" style={{ fontSize: 32, fontWeight: 700, textAlign: 'center', marginBottom: 8, letterSpacing: 2 }}>开发者后台重构难度可视化</h1>
<div className="showrecon-divider" style={{ width: 80, height: 4, background: '#1677ff', borderRadius: 2, margin: '0 auto 32px auto' }} />
<div className="showrecon-main" style={{ display: 'flex', maxWidth: 1800, margin: '0 auto', padding: '0 24px' }}>
{/* 主内容区域 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ margin: '0 auto' }}>
<div className="showrecon-charts" style={{ display: 'flex', gap: 24, justifyContent: 'center', marginBottom: 40, minHeight: 440 }}>
<div className="showrecon-chart-item" style={{ flex: 1, minWidth: 0, background: '#fff', borderRadius: 12, boxShadow: '0 4px 16px #e6e6e6', padding: 24, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ReactECharts option={groupPieOption} style={{ height: 400, width: '100%' }} onEvents={groupPieEvents} />
</div>
<div className="showrecon-chart-item" style={{ flex: 1, minWidth: 0, background: '#fff', borderRadius: 12, boxShadow: '0 4px 16px #e6e6e6', padding: 24, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ReactECharts option={complexityPieOption} style={{ height: 400, width: '100%' }} onEvents={complexityPieEvents} />
</div>
<div className="showrecon-chart-item" style={{ flex: 1, minWidth: 0, background: '#fff', borderRadius: 12, boxShadow: '0 4px 16px #e6e6e6', padding: 24, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ReactECharts option={moReactFunPieOption} style={{ height: 400, width: '100%' }} />
</div>
</div>
<div style={{ marginBottom: 32 }}>
<div
className={`showrecon-tabs tabs-fade${tabsVisible ? ' show' : ''}`}
style={{
background: '#fff',
borderRadius: 12,
boxShadow: '0 4px 16px #e6e6e6',
padding: 32,
opacity: tabsVisible ? 1 : 0,
pointerEvents: tabsVisible ? 'auto' : 'none',
transition: 'opacity 0.5s',
minHeight: 120,
width: '100%',
margin: '0 auto',
}}
>
{tabsVisible && <>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 24, flexWrap: 'wrap', marginBottom: 24 }}>
<Input.Search
className="showrecon-search"
placeholder="搜索模块名"
allowClear
onChange={e => setSearch(e.target.value)}
style={{ width: 360, marginBottom: 0 }}
/>
<span style={{ fontWeight: 500 }}>排序字段:{sortField === 'fileCount' ? '文件数' : '行数'}</span>
<Button type="primary" onClick={() => setSortOrder(o => o === 'asc' ? 'desc' : 'asc')}>
{sortOrder === 'asc' ? <><UpOutlined /> 升序</> : <><DownOutlined /> 降序</>}
</Button>
</div>
<Tabs
activeKey={activeTabKey}
onChange={setActiveTabKey}
items={moduleTabs}
tabBarStyle={{ fontSize: 18, fontWeight: 500, marginBottom: 16 }}
style={{ margin: '0 auto', width: '100%' }}
/>
</>}
</div>
</div>
</div>
</div>
{/* 右侧区域 */}
<div className="showrecon-eval" style={{ width: 420, marginLeft: 32, background: '#fff', borderRadius: 12, boxShadow: '0 4px 16px #e6e6e6', padding: 32, minHeight: 650, display: 'flex', flexDirection: 'column', justifyContent: 'flex-start' }}>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 16 }}>重构评估</h2>
<div className="module-count">整体模块数量:{moduleStats.length} / 整体文件数:{moduleStats.reduce((sum, item) => sum + item.fileCount, 0)} </div>
<div style={{ fontSize: 12, marginBottom: 20 }}>所有模块开发完成都会做一轮完成的测试,确保迁移或者重构无误,不影响原有功能和模块。</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* 简单模块 */}
<div style={{ background: '#f5faff', borderRadius: 8, padding: 18, boxShadow: '0 2px 8px #e6e6e6', borderLeft: '6px solid #3788fb' }}>
<div style={{ fontWeight: 600, fontSize: 17, color: '#3788fb', marginBottom: 4 }}>简单模块({simpleModules.length})</div>
<div style={{ color: '#666', fontSize: 15, marginBottom: 8, textAlign: 'left' }}>基本是一些增删改查页面。没有特殊的业务逻辑。</div>
<div style={{ marginTop: 8, color: '#1677ff', fontSize: 15, fontWeight: 500 }}>
日均重构模块:{estimateRate} <span style={{ color: '#888', fontSize: 13 }}>(评估值)</span>
</div>
<div style={{ marginTop: 10, color: '#3788fb', fontSize: 15, fontWeight: 600 }}>整体工时:{simpleDays} 天</div>
</div>
{/* 中等模块 */}
<div style={{ background: '#fffef3', borderRadius: 8, padding: 18, boxShadow: '0 2px 8px #e6e6e6', borderLeft: '6px solid #ffe748' }}>
<div style={{ fontWeight: 600, fontSize: 17, color: '#ffe748', marginBottom: 4 }}>中等模块({mediumModules.length})</div>
<div style={{ color: '#666', fontSize: 15, marginBottom: 8, textAlign: 'left' }}>在增删改查的基础上,可能有部分特殊表单提交和特殊的操作联动,开发以及完成测试的时候需要注意,会多花一部分时间。</div>
<div style={{ marginTop: 8, color: '#bfa800', fontSize: 15, fontWeight: 500 }}>
日均重构模块:{estimateRateMedium} <span style={{ color: '#888', fontSize: 13 }}>(评估值)</span>
</div>
<div style={{ marginTop: 10, color: '#ffe748', fontSize: 15, fontWeight: 600 }}>整体工时:{mediumDays} 天</div>
</div>
{/* 复杂模块 */}
<div style={{ background: '#fff6f7', borderRadius: 8, padding: 18, boxShadow: '0 2px 8px #e6e6e6', borderLeft: '6px solid #d83b48' }}>
<div style={{ fontWeight: 600, fontSize: 17, color: '#d83b48', marginBottom: 4 }}>复杂模块({complexModules.length})</div>
<div style={{ color: '#666', fontSize: 15, marginBottom: 8, textAlign: 'left' }}>文件行数 1201~2000,结构较复杂,重构难度较大。</div>
<div style={{ marginTop: 8, color: '#d83b48', fontSize: 15, fontWeight: 500 }}>
日均重构模块:{estimateRateComplex} <span style={{ color: '#888', fontSize: 13 }}>(评估值)</span>
</div>
<div style={{ marginTop: 10, color: '#d83b48', fontSize: 15, fontWeight: 600 }}>整体工时:{complexDays} 天</div>
</div>
{/* 超复杂模块 */}
<div style={{ background: '#f7f3ff', borderRadius: 8, padding: 18, boxShadow: '0 2px 8px #e6e6e6', borderLeft: '6px solid #722ed1' }}>
<div style={{ fontWeight: 600, fontSize: 17, color: '#722ed1', marginBottom: 4 }}>超复杂模块({moduleStats.filter(item => item.totalLines > 2000).length})</div>
<div style={{ color: '#666', fontSize: 15, marginBottom: 8, textAlign: 'left' }}>文件行数 > 2000,极高复杂度,建议团队协作重构。</div>
<div style={{ marginTop: 8, color: '#722ed1', fontSize: 15, fontWeight: 500 }}>
日均重构模块:{estimateRateSuperComplex} <span style={{ color: '#888', fontSize: 13 }}>(评估值)</span>
</div>
<div style={{ marginTop: 10, color: '#722ed1', fontSize: 15, fontWeight: 600 }}>整体工时:{superComplexDays} 天</div>
</div>
{/* 总体工时 */}
<div style={{ marginTop: 24, color: '#222', fontWeight: 700, fontSize: 18, textAlign: 'center' }}>
总体工时:{totalDays} 天 <span style={{ color: '#888', fontSize: 14 }}>(按评估速率自动计算)</span>
</div>
<div style={{ marginTop: 5, color: '#ff0000', fontWeight: 700, fontSize: 12, textAlign: 'center' }}>
暂时对ai测试重构的评估较少,所以以上工时仅供参考。
</div>
</div>
</div>
</div>
</div>
</>);
}
export default ShowReconstructionDemo;
总结
之前我使用 cursor 本质上和使用 vscode + copilot 没什么差别。感觉只是升级版的 copilot。
cursor 对于各个文件之间的引用以及读取能力是远超 copilot 的,在开发中,如果 A 文件和 B 文件有关联并且我修改了 A 代码会导致 B 代码的修改,那么我可以无缝的从 A 文件一路按 Tab 到 B 文件,使用起来比 copilot 舒服非常多。
之所以一直使用 Tab 而非 Agent 是因为现在公司的代码逻辑非常多,害怕在某 些时候 AI 不小心改动到了某些地方而我没注意到最后导致线上出 bug。而当完全起一个新的项目,就不怎么担心了
我个人认为:
优秀的 AI 使用者一定有着过硬的专业能力
AI 确实可以完全按照你的需求生成代码,但是你要对这里面涉及到的知识有足够的认知,知道这个东西的概念是什么,称呼是什么,原理是什么,才能更好的使用 AI,好的程序员才会是好的 AI 使用者,这是我始终坚信的。
AI 有着非常强的抹平信息差的能力
如果对于概念不清楚,你也可以在和 AI 沟通的过程中,逐渐明白这个东西的概念。并且能够很快的了解这个内容,变学习边工作的感觉我觉得非常好,AI 能够抹平非常多的信息差。
在 AI 的辅助下,一个困难的东西能够轻松的实现,对于我的正反馈也是非常大的,就像一个非常谦逊且优秀的老师一样,你只要不停的问他,他就会孜孜不倦的给你反馈,教导你,提升你。毫无疑问当下,最好的学习方式就是和 AI 沟通。
ZoomIn 和 ZoomOut
在 ai 编程的时候,确实是仿佛雇佣了一个顶级程序员,很多时候,你只要考虑大方向上的逻辑,剩下的小细节 ai 能够自动补充,有错误再纠错就好了。我能够更好的专注在想要实现什么样的功能上面,这是非常好的感觉。
这次这个重构评估我花了 5 个多小时,因为我并不知道根据什么去评估,需要什么图表,需要哪些数据,所以一边思考,一边给 Ai 提示沟通,我大部分时间都在思考这个页面的需求以及展示效果,而不是考虑怎么遍历过滤排序去生成数据如何写逻辑去实现交互,能够更好的专注在需求功能以及页面内容而非技术实现这种感觉确实是非常的产品经理。
我认为,在 ai 时代,综合能力将会是更大的考验,整体应用大方向上交互以及需求的把控,以及小细节上技术的实现,这种抓大放小,既能有全局视野也有针对细节的能力才是更重要的。