前端必看!我把一段"能跑不敢动"的报表代码用 AI 重构成了组件(附完整 Prompt)

关键词:Vue2、组件化、AI 辅助编程、代码重构

一、背景:一段没人敢动的报表代码

我们系统里有一张资金报表表格,需要同时展示银行A / 银行B 两家对手方的数据,而且每个对手方又分左面板 / 右面板。功能一直正常,但代码没人敢动------改一个样式要同步改四个地方,漏一个就出 bug。

直到前两天我用 AI 把它重构了一遍。虽然总行数没少多少,但结构清爽了,同事终于敢在上面改需求了

下面把完整过程和 Prompt 公开,供大家参考。

二、重构前:问题到底在哪

先看一下重构前单个单元格的模板(已脱敏,仅展示其中一处):

vue 复制代码
<!-- 重构前:四套几乎一样的 :class 内联在模板里,这里只贴一处 -->
<div class="data-cell"
  v-for="(cell, cellIndex) in row"
  :key="cellIndex"
  :class="{
    'hiddle-cell': [7, 8].includes(cellIndex),
    'no-right-border': cellIndex === 6,
    'fixed-width': cellIndex === 0,
    'link-style link-style--secondary': cellIndex === 1 && !isSumArr.includes(row[0]),
    'link-style link-style--black-underline': cellIndex === 0 && (cell == 'Fund Transfer' || cell == 'TR Loan' || cell == 'Margin Call' || cell == 'IA' || cell == 'Client' || cell == 'Others'),
    'data-cell--gery-bg': row[0] == 'Closing Balance' && cellIndex != 0,
    'text-width': cellIndex != 0,
    'italic-text': cellIndex == 0 && getCellCalculateFlag(cell, 'A', 'left'),
    'cash-all-gray-cell': isOpeningBalanceRow(row) && cellIndex !== 0 && isALL,
    'bg-totalMovement': cell === 'Total Movement' && cellIndex === 0,
    'bg-closingBalance': leftAmountArr.includes(cell) && cellIndex === 0,
    'bg-other-hightLight': cell && cellIndex === 8,
    'bg-mc-hightLight': row[0] === 'Margin Call' && row[8] && cellIndex !== 0,
    'bg-zero-hightLight': row[0] === 'Opening Balance' && !row[1] && cellIndex !== 0,
    'copy-amount': isSumArr.includes(row[0]) && cellIndex > 0,
  }"
>
  <ui-tooltip :content="cell" placement="top" :disabled="cell?.length < 18 || cellIndex == 0">
    <span :class="{ 'font-700': isSumArr.includes(row[0]) && cellIndex > 0 }"
      @click="showLeftDialog(row, cellIndex, rowIndex, data)">{{ cell }}</span>
  </ui-tooltip>
</div>

三个真实痛点

  1. 同一段 :class 对象复制粘贴了 4 遍(银行A左、银行A右、银行B左、银行B右),每套 14 个条件,只差几个字符。
  2. 左/右、银行A/银行B 结构完全镜像 ,整个块在 v-for 里又重复一遍,零抽象。
  3. 行内逻辑爆炸:编辑态、tooltip、权限指令全堆在模板里,单文件已无法维护。

三、我写给 AI 的 Prompt(已脱敏)

下面是一段 Vue2 报表表格模板,银行A/银行B、左右两面板共四套几乎一样的 :class 内联在模板里,难维护。请帮我重构:

  1. 把重复 class 计算抽成方法;
  2. 把左右/银行结构抽成可复用组件;
  3. 保持原有样式类和数据逻辑不变。 注意:对手方名称属公司信息,请脱敏为银行A/银行B;内部 UI 组件库前缀统一替换为 ui-

四、AI 给的初版:抽成 getCellClass 方法

AI 第一版很聪明地把重复的 class 计算抽成了方法,模板从 14 行 :class 变成 1 行:

html 复制代码
<div class="data-cell"
  v-for="(cell, ci) in row"
  :key="ci"
  :class="getCellClass(cell, ci, row, 'A', 'left')"
  :data-amount="sumArr.includes(row[0]) && ci > 0 ? String(cell).replace(/,/g,'') : undefined"
>
js 复制代码
methods: {
  getCellClass(cell, ci, row, bankType, side) {
    const isSum = this.sumArr.includes(row[0])
    const isOpening = this.isOpeningBalanceRow(row)
    const linkRows = ['Fund Transfer','TR Loan','Margin Call','IA','Client','Others','Closing调整明细']
    const cls = {
      'fixed-width': ci === 0,
      'link-style link-style--secondary': ci === 1 && !isSum,
      'link-style link-style--black-underline': ci === 0 && linkRows.includes(cell),
      'data-cell--gery-bg': row[0] === 'Closing Balance' && ci !== 0,
      'italic-text': ci === 0 && this.getCellCalculateFlag(cell, bankType, side),
      'bg-totalMovement': cell === 'Total Movement' && ci === 0,
      'bg-closingBalance': this.leftAmountArr.includes(cell) && ci === 0,
      'bg-other-hightLight': !!cell && ci === 8,
      'bg-mc-hightLight': row[0] === 'Margin Call' && row[8] && ci !== 0,
      'bg-zero-hightLight': row[0] === 'Opening Balance' && !row[1] && ci !== 0,
      'copy-amount': isSum && ci > 0,
    }
    if (side === 'left') {
      cls['no-right-border'] = ci === 6
      cls['hiddle-cell'] = [7, 8].includes(ci)
      cls['text-width'] = ci !== 0
      cls['cash-all-gray-cell'] = isOpening && ci !== 0 && this.isALL
    } else {
      cls['text-right-width'] = ci !== 0
      cls['hiddle-cell'] = ci === 7
      cls['cash-all-gray-cell'] = ci !== 0 && this.isALL
    }
    return cls
  }
}

五、我的调整:为什么还要抽组件

AI 第一版只做了"抽方法",但左右面板、银行A/银行B 的表格结构块还是重复的------它没进一步抽象。

我的判断是:左右只是 side 不同、银行只是 bankType / boxColor 不同,本质是一个组件。于是我推了 AI 一步,抽出 ReportTable.vue,父组件用 v-for + props 把四个块收敛成 4 次 <report-table> 调用。

ReportTable.vue(自包含组件)

vue 复制代码
<template>
  <div class="left-content">
    <div class="scb-box" :style="boxStyle">{{ bankLabel }}</div>
    <div class="table-content">
      <div class="header-row">
        <div v-for="(title, i) in titles" :key="'h'+i"
          class="header-cell" :class="headerClass(title, i)">{{ title }}</div>
      </div>
      <div v-for="(row, ri) in rows" :key="'r'+ri" class="data-row">
        <div v-for="(cell, ci) in row" :key="'c'+ci"
          class="data-cell" :class="cellClass(cell, ci, row)"
          :data-amount="isSumRow(row) && ci>0 ? String(cell).replace(/,/g,'') : undefined">
          <slot name="cell" :cell="cell" :ci="ci" :row="row" :ri="ri">
            <ui-tooltip :content="cell" placement="top"
              :disabled="cell?.length < 18 || ci === 0">
              <span :class="{ 'font-700': isSumRow(row) && ci>0 }"
                @click="$emit('cell-click', { cell, ci, ri, row })">{{ cell }}</span>
            </ui-tooltip>
          </slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ReportTable',
  props: {
    titles: { type: Array, required: true },
    rows: { type: Array, required: true },
    bankType: { type: String, default: 'A' },   // 'A' | 'B'
    side: { type: String, default: 'left' },     // 'left' | 'right'
    bankLabel: { type: String, default: '' },
    boxColor: { type: String, default: '' },
    sumArr: { type: Array, default: () => [] },
    leftAmountArr: { type: Array, default: () => [] },
    isAll: { type: Boolean, default: false },
  },
  computed: {
    boxStyle() { return this.boxColor ? { backgroundColor: this.boxColor } : {} },
  },
  methods: {
    isSumRow(row) { return this.sumArr.includes(row[0]) },
    isOpeningBalanceRow(row) { return row[0] === 'Opening Balance' },
    // 原项目逻辑:按 bankType/side 判断计算字段是否斜体,接入你真实的数据源即可
    getCellCalculateFlag(cell, bankType, side) {
      return this.calcFlagMap[`${bankType}-${side}`]?.includes(cell) || false
    },
    headerClass(title, i) {
      return {
        'bg-gray': i === 0, 'bg-green': i === 1, 'fixed-width': i === 0,
        'text-width': i !== 0 && this.side === 'left',
        'text-right-width': i !== 0 && this.side === 'right',
        'no-right-border': i === 6 && this.side === 'left',
        'hiddle-cell':
          (this.side === 'left' && ['TR','Others'].includes(title)) ||
          (this.side === 'right' && title === 'TR'),
      }
    },
    cellClass(cell, ci, row) { return this.getCellClass(cell, ci, row, this.bankType, this.side) },
    getCellClass(cell, ci, row, bankType, side) {
      const isSum = this.isSumRow(row)
      const isOpening = this.isOpeningBalanceRow(row)
      const linkRows = ['Fund Transfer','TR Loan','Margin Call','IA','Client','Others','Closing调整明细']
      const cls = {
        'fixed-width': ci === 0,
        'link-style link-style--secondary': ci === 1 && !isSum,
        'link-style link-style--black-underline': ci === 0 && linkRows.includes(cell),
        'data-cell--gery-bg': row[0] === 'Closing Balance' && ci !== 0,
        'italic-text': ci === 0 && this.getCellCalculateFlag(cell, bankType, side),
        'bg-totalMovement': cell === 'Total Movement' && ci === 0,
        'bg-closingBalance': this.leftAmountArr.includes(cell) && ci === 0,
        'bg-other-hightLight': !!cell && ci === 8,
        'bg-mc-hightLight': row[0] === 'Margin Call' && row[8] && ci !== 0,
        'bg-zero-hightLight': row[0] === 'Opening Balance' && !row[1] && ci !== 0,
        'copy-amount': isSum && ci > 0,
      }
      if (side === 'left') {
        cls['no-right-border'] = ci === 6
        cls['hiddle-cell'] = [7, 8].includes(ci)
        cls['text-width'] = ci !== 0
        cls['cash-all-gray-cell'] = isOpening && ci !== 0 && this.isAll
      } else {
        cls['text-right-width'] = ci !== 0
        cls['hiddle-cell'] = ci === 7
        cls['cash-all-gray-cell'] = ci !== 0 && this.isAll
      }
      return cls
    },
  },
}
</script>

父组件调用(左/右、银行A/银行B 全收敛)

vue 复制代码
<template>
  <!-- 银行A -->
  <div class="table-box">
    <div style="flex:1">
      <report-table :titles="bankALeftTitles" :rows="bankAReport.leftData"
        bank-type="A" side="left" bank-label="银行A"
        :sum-arr="sumArr" :left-amount-arr="leftAmountArr" :is-all="isAll"
        @cell-click="onCellClick" />
    </div>
    <div style="flex:1" class="ml10">
      <report-table :titles="bankARightTitles" :rows="bankAReport.rightData"
        bank-type="A" side="right" bank-label="银行A"
        :sum-arr="sumArr" :left-amount-arr="leftAmountArr" :is-all="isAll"
        @cell-click="onCellClick" />
    </div>
  </div>

  <!-- 银行B(多账户循环) -->
  <div v-for="report in bankBReportList" :key="report.bankAccountId" class="report-container">
    <div class="table-box">
      <div style="flex:1">
        <report-table :titles="bankBLeftTitles" :rows="report.leftData"
          bank-type="B" side="left" bank-label="银行B" box-color="#5a9d8f"
          :sum-arr="sumArr" :left-amount-arr="leftAmountArr" :is-all="isAll"
          @cell-click="(e) => onCellClick(e, report.bankAccountId)" />
      </div>
      <div style="flex:1" class="ml10">
        <report-table :titles="bankBRightTitles" :rows="report.rightData"
          bank-type="B" side="right" bank-label="银行B" box-color="#5a9d8f"
          :sum-arr="sumArr" :left-amount-arr="leftAmountArr" :is-all="isAll"
          @cell-click="(e) => onCellClick(e, report.bankAccountId)" />
      </div>
    </div>
  </div>
</template>

提示:编辑态(期初余额编辑)通过父组件的 #cell 插槽或 cell-click 事件接回原逻辑,原有 ui-input-number 编辑 UI 不受影响。

六、重构前后对比

指标 重构前 重构后
重复 :class 对象 4 套 × 14 行 1 个方法
表格结构块 银行A×2 + 银行B×N×2 <report-table> × N
改一处样式的成本 改 4 处 改 1 处
单文件可维护性 不敢动 敢改

七、一点思考

AI 能很快帮你把"重复代码"抽成方法,但"这段代码到底是不是一个组件"这种抽象判断,目前还得靠人。我的经验是:先让 AI 做低风险的抽取(抽方法、抽变量),再自己决定是否值得进一步组件化------别一上来就让 AI 大改结构,容易改出回归。

八、互动

你们项目里有这种"能跑不敢动"的代码吗?是怎么处理的?欢迎评论区聊聊 👋

如果觉得有用,点赞 + 收藏 不迷路,后续我会写更多「AI 辅助前端重构」的实战。


相关推荐
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十三):前后端数据验证
后端·go·ai编程
hunterandroid1 小时前
[鸿蒙从零到一] ArkUI 动画与转场实战:状态驱动、组件过渡与页面衔接
前端
何时梦醒1 小时前
React + TypeScript + Vite 实战:从零构建 Color Picker 应用
前端·javascript·架构
谁在黄金彼岸1 小时前
Nuxt.js 详解(一):Vue 开发者为什么要关注 Nuxt
前端
英勇无比的消炎药1 小时前
TinyRobot v0.5.0 深度解读(三):Layout 组件——如何组织复杂工作区的页面布局
前端·vue.js
默_笙1 小时前
😋 我让 DeepSeek-R1 在浏览器本地跑了起来,后端同事说"你认真的?"
前端·javascript
英勇无比的消炎药1 小时前
TinyRobot v0.5.0 深度解读(四):CLI 脚手架——从零搭建 AI 应用的工程化实践
前端·vue.js·github
程序员黑豆2 小时前
鸿蒙应用开发之双向绑定实战:从 V1 到 V2 的完整迁移指南
前端·harmonyos