Vue3可编辑字段组件的演进之路:从繁琐到优雅

前言

在开发管理系统时,我们经常会遇到需要原地编辑的场景:用户点击某个字段,该字段变成可编辑状态,编辑完成后保存。这看似是个简单的需求,但要做好却不容易。本文将分享一个真实的开发案例,看看如何从最初的繁琐实现,一步步优化到最终的优雅解决方案。

1. 最初的实现

作为开发者,我们往往会选择最直接的实现方式:为每个可编辑字段添加独立的编辑状态和处理逻辑。点击时修改编辑状态,让文本变成可编辑状态,可能是input,也可能是多行文本,还可能是下拉选择。

html 复制代码
<template>
  <div class="indicator-detail">
    <!-- 指标名称 -->
    <div class="detail-item">
      <span class="label">指标名称:</span>
      <template v-if="isEditingName">
        <input 
          v-model="indicator.name"
          @blur="handleNameSave"
          ref="nameInput"
        />
      </template>
      <span v-else @click="startEditName">
        {{ indicator.name }}
      </span>
    </div>

    <!-- 指标类型 -->
    <div class="detail-item">
      <span class="label">指标类型:</span>
      <template v-if="isEditingType">
        <select 
          v-model="indicator.type"
          @change="handleTypeSave"
          ref="typeSelect"
        >
          <option value="1">定量指标</option>
          <option value="2">定性指标</option>
        </select>
      </template>
      <span v-else @click="startEditType">
        {{ getTypeName(indicator.type) }}
      </span>
    </div>

    <!-- 指标权重 -->
    <div class="detail-item">
      <span class="label">指标权重:</span>
      <template v-if="isEditingWeight">
        <input 
          type="number"
          v-model="indicator.weight"
          @blur="handleWeightSave"
          ref="weightInput"
        />
      </template>
      <span v-else @click="startEditWeight">
        {{ indicator.weight }}%
      </span>
    </div>
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';

// 编辑状态
const isEditingName = ref(false);
const isEditingType = ref(false);
const isEditingWeight = ref(false);

// 输入框引用
const nameInput = ref(null);
const typeSelect = ref(null);
const weightInput = ref(null);

// 开启编辑
const startEditName = async () => {
  isEditingName.value = true;
  await nextTick();
  nameInput.value?.focus();
};

const startEditType = async () => {
  isEditingType.value = true;
  await nextTick();
  typeSelect.value?.focus();
};

const startEditWeight = async () => {
  isEditingWeight.value = true;
  await nextTick();
  weightInput.value?.focus();
};

// 保存处理
const handleNameSave = async () => {
  try {
    await saveIndicatorName(indicator.name);
    isEditingName.value = false;
    //这个是t-design的提示
    MessagePlugin.success('保存成功');
  } catch (error) {
    MessagePlugin.error('保存失败');
  }
};

const handleTypeSave = async () => {
  try {
    await saveIndicatorType(indicator.type);
    isEditingType.value = false;
    MessagePlugin.success('保存成功');
  } catch (error) {
    MessagePlugin.error('保存失败');
  }
};

const handleWeightSave = async () => {
  try {
    await saveIndicatorWeight(indicator.weight);
    isEditingWeight.value = false;
    MessagePlugin.success('保存成功');
  } catch (error) {
    MessagePlugin.error('保存失败');
  }
};
</script>

<style scoped>
.detail-item {
  margin-bottom: 16px;
}

.label {
  font-weight: bold;
  margin-right: 8px;
}
</style>

2.1 存在的问题

刚开始的时候就想着多个字段就多复制几次代码,后面越复制越不对劲,重复的东西太多了。:

代码重复

  • 每个字段都需要独立的编辑状态变量
  • 每个字段都需要独立的开启编辑方法
  • 每个字段都需要独立的保存方法
  • 每个字段都需要独立的模板结构

维护困难

  • 如果要修改编辑交互(比如从点击改为双击),需要修改多处代码
  • 样式修改需要确保所有地方都同步更新
  • 错误处理逻辑分散,难以统一管理

扩展性差

  • 添加新的可编辑字段需要复制大量代码
  • 难以统一添加新功能(如校验、格式化等)
  • 代码越写越长,越来越难维护

3. 第一次优化:状态集中管理

为了减少重复代码,我使用对象来统一管理编辑状态,核心设计原理: 统一字段配置

  • 使用 fields 数组统一管理所有字段的配置信息
  • 每个字段包含:key(字段名)、label(显示标签)、editComponent(编辑组件类型)等

编辑状态管理

  • 使用 reactive 对象 editingStatus 统一管理所有字段的编辑状态
  • 每个字段对应一个布尔值,表示是否处于编辑状态

动态组件渲染

  • 使用 component 动态组件根据配置渲染不同的编辑组件
  • 支持 input、select 等不同类型的编辑组件

统一的事件处理

  • startEdit: 开启编辑状态
  • handleSave: 统一的保存处理逻辑,包含成功/失败提示
  • formatValue: 统一的值格式化处理
vue 复制代码
<template>
  <div class="indicator-detail">
    <div v-for="field in fields" :key="field.key" class="detail-item">
      <span class="label">{{ field.label }}:</span>
      <template v-if="editingStatus[field.key]">
        <component 
          :is="field.editComponent"
          v-model="indicator[field.key]"
          @blur="handleSave(field.key)"
          ref="inputs"
        />
      </template>
      <span v-else @click="startEdit(field.key)">
        {{ formatValue(field.key, indicator[field.key]) }}
      </span>
    </div>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

// 字段配置
const fields = [
  {
    key: 'name',
    label: '指标名称',
    editComponent: 'input'
  },
  {
    key: 'type',
    label: '指标类型',
    editComponent: 'select',
    options: [
      { value: '1', label: '定量指标' },
      { value: '2', label: '定性指标' }
    ]
  },
  {
    key: 'weight',
    label: '指标权重',
    editComponent: 'input',
    type: 'number'
  }
];

// 统一管理编辑状态
const editingStatus = reactive({
  name: false,
  type: false,
  weight: false
});

// 统一的编辑和保存处理
const startEdit = (field) => {
  editingStatus[field] = true;
};

const handleSave = async (field) => {
  try {
    await saveIndicator(field, indicator[field]);
    editingStatus[field] = false;
    MessagePlugin.success('保存成功');
  } catch (error) {
    MessagePlugin.error('保存失败');
  }
};

// 值格式化
const formatValue = (field, value) => {
  if (field === 'type') {
    return getTypeName(value);
  }
  if (field === 'weight') {
    return `${value}%`;
  }
  return value;
};
</script>

这次优化确实减少了一些代码量,但仍然存在问题:

  1. 模板结构依然复杂
  2. 编辑组件的处理不够灵活
  3. 这些东西都是放在这个组件里面,如果其他组件有这个需求的话,就需要写很多重复代码了。

4. 灵光乍现:组件化重构

经过上述优化,我们发现每个可编辑字段其实都遵循相同的模式:

  1. 显示态与编辑态的切换
  2. 值的展示与编辑
  3. 保存操作
  4. 错误处理

这不正是组件化的最佳场景吗?

4.1 第一版组件

让我们尝试将这些共同的逻辑抽取成一个组件:

vue 复制代码
<template>
  <div class="editable-field" @click="startEdit">
    <span v-if="label" class="label">{{ label }}:</span>
    <template v-if="isEditing">
      <input 
        v-model="editValue"
        @blur="handleSave"
        ref="input"
      />
    </template>
    <span v-else class="value">
      {{ value }}
    </span>
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue';

const props = defineProps({
  label: String,
  value: [String, Number],
});

const emit = defineEmits(['update']);

const isEditing = ref(false);
const editValue = ref(props.value);
const input = ref(null);

const startEdit = async () => {
  isEditing.value = true;
  await nextTick();
  input.value?.focus();
};

const handleSave = () => {
  if (editValue.value !== props.value) {
    emit('update', editValue.value);
  }
  isEditing.value = false;
};
</script>

4.2 组件优化

基础功能有了,接下来我们不断完善这个组件:

  1. 支持不同的编辑组件类型
  2. 添加值格式化功能,比如像权重的话显示格式就是val%,这种的。
  3. 优化交互(改为双击编辑)
  4. 添加保存回调和错误处理
  5. 使用t-design(腾讯的)的组件库

最终,我们得到了一个功能完整的可编辑字段组件:

html 复制代码
<template>
  <div class="detail-item" @dblclick="startEdit">
    <span v-if="label" class="label" style="width: fit-content; white-space: nowrap;">{{ label }}:</span>
    <div class="value-container" style="width: 100%; cursor: pointer;">
      <template v-if="isEditing">
        <t-select
          v-if="type === 'select'"
          ref="inputRef"
          v-model="editValue"
          :style="{ width: '120px' }"
          @change="handleSave"
          @blur="handleSave"
        >
          <t-option 
            v-for="option in options" 
            :key="option.value" 
            :value="option.value" 
            :label="option.label" 
          />
        </t-select>
        <t-textarea
          v-else
          ref="inputRef"
          v-model="editValue"
          :style="{ flex: '1',width: '100%' }"
          :autosize="{ minRows: 1, maxRows: 5 }"
          @blur="handleSave"
        />
      </template>
      <span v-else class="value" :class="valueClass">
        {{ displayValue }}
      </span>
      <slot name="suffix"></slot>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, nextTick } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';

const props = defineProps({
  label: String,
  value: [String, Number],
  type: {
    type: String,
    default: 'text' // 类型可自己加,文本,下拉框,日期
  },
  options: {
    type: Array,
    default: () => []
  },
  valueClass: String,
  displayFormatter: {
    type: Function,
    default: val => val
  },
  saveHandler: {
    type: Function,
    required: true
  }
});

const isEditing = ref(false);
const editValue = ref(props.value);
const inputRef = ref(null);

const displayValue = computed(() => {
  return props.displayFormatter(props.value);
});

const startEdit = () => {
  editValue.value = props.value;
  isEditing.value = true;
  nextTick(() => {
    inputRef.value?.focus();
  });
};

const handleSave = async () => {
  if (editValue.value !== props.value) {
    try {
      await props.saveHandler(editValue.value);
      MessagePlugin.success('修改成功');
    } catch (error) {
      MessagePlugin.error('修改失败');
      editValue.value = props.value;
    }
  }
  isEditing.value = false;
};
</script>

5. 使用示例

有了这个组件后,我们的业务代码变得非常简洁:

vue 复制代码
<template>
  <div class="indicator-detail">
    <editable-field
      label="指标名称"
      :value="indicator.name"
      :save-handler="handleNameSave"
    />
    
    <editable-field
      label="指标类型"
      :value="indicator.type"
      type="select"
      :options="typeOptions"
      :save-handler="handleTypeSave"
      :display-formatter="formatType"
    />
    
    <editable-field
      label="指标权重"
      :value="indicator.weight"
      :save-handler="handleWeightSave"
      :display-formatter="value => `${value}%`"
    />
  </div>
</template>

<script setup>
const handleNameSave = async (value) => {
  await saveIndicatorName(value);
};

const handleTypeSave = async (value) => {
  await saveIndicatorType(value);
};

const handleWeightSave = async (value) => {
  await saveIndicatorWeight(value);
};
</script>

6. 总结与思考

开发这个可编辑字段组件时,我真切感受到代码是需要慢慢 "长" 出来的。一开始没多想,直接给每个字段单独写编辑逻辑,功能倒是实现了,可代码越堆越多,复制粘贴的重复操作越来越频繁 ------ 比如不同字段的编辑状态切换、输入校验这些逻辑,几乎每个都得重写一遍。直到看着满屏相似的代码块,才意识到必须得做点什么改变。

后面边写边琢磨,发现很多字段的差异其实只在表现层:有的是输入框,有的是下拉选,有的需要格式化显示。但核心的编辑流程 ------ 进入编辑态、处理用户输入、校验数据、保存回调 ------ 其实都是一套逻辑。于是试着把这些共性抽出来,把不同的部分做成可配置的参数,比如用一个 type 属性区分编辑类型,用 format 函数处理显示格式,把保存逻辑留给外部通过回调处理。这么一拆,原本零散的代码块就拼成了一个通用组件。

这个过程让我特别有感触:好的组件从来不是一开始就设计出来的,而是在解决具体问题时慢慢 "磨" 出来的。就像手里的工具,刚开始能把眼前的活干完就行,用着用着发现哪里不顺手,就打磨哪里 ------ 加个扩展接口,留个配置项,慢慢就变成了一把 "万能工具"。现在这个组件里,用户想换编辑类型?改改 type 就行。显示格式要调整?传个 format 函数进去。业务逻辑有变化?保存时的回调里随便写 ------ 既没让组件变得复杂,又给后续扩展留了余地。

回头看整个过程,最大的感触是:别一开始就想着搞个 "完美设计"。刚上手时对需求的理解其实很浅层,只有真正在代码里踩过坑,遇到过重复劳动的痛苦,才能知道哪些是真正需要抽象的核心逻辑,哪些只是表面差异。组件化不是为了炫技,本质是让代码能 "可持续发展"------ 以后再遇到类似需求,不用从零开始,而是像搭积木一样快速组合,这才是真正的复用价值。

参考资料

相关推荐
海上彼尚19 分钟前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
工业互联网专业21 分钟前
基于JavaWeb的花店销售系统设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计·花店销售系统
阿虎儿26 分钟前
MCP
前端
layman052838 分钟前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝39 分钟前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML39 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia31139 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生1 小时前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇1 小时前
一文搞定CSS Grid布局
前端
0xHashlet1 小时前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端