人人都会AI编程

29.4 通用功能:文件上传、拖拽排序、富文本编辑器、无限滚动

更新时间:2026-07-09

在大部分后台管理系统或 C 端应用中,文件上传、列表拖拽排序、富文本编辑和无限滚动几乎是必选项。这些功能都有成熟的第三方库支持,但在 Vue 项目中做好封装,需要考虑组件边界、状态管理和性能优化。下面给出每个功能的实用实现思路和关键代码。


文件上传

核心需求:选择文件、上传进度、错误重试、图片预览、限制类型/大小。

推荐方案<input type="file"> + Axios 封装 + 组件库(Element Plus 的 el-upload 或自定义)。

关键实现(自定义上传组件):

<template>
  <div>
    <input
      ref="fileInput"
      type="file"
      accept="image/*"
      @change="handleFileChange"
      style="display: none"
    />
    <button @click="triggerSelect">选择图片</button>
    <div v-if="previewUrl">
      <img :src="previewUrl" width="100" />
    </div>
    <div v-if="uploading">上传中:{{ percent }}%</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const fileInput = ref(null)
const previewUrl = ref('')
const uploading = ref(false)
const percent = ref(0)

function triggerSelect() {
  fileInput.value.click()
}

async function handleFileChange(e) {
  const file = e.target.files[0]
  if (!file) return

  // 本地预览
  previewUrl.value = URL.createObjectURL(file)

  // 上传
  const formData = new FormData()
  formData.append('file', file)
  try {
    uploading.value = true
    const res = await axios.post('/upload', formData, {
      onUploadProgress: (e) => {
        percent.value = Math.round((e.loaded / e.total) * 100)
      }
    })
    // 拿到返回的远程地址
    console.log('上传成功', res.data.url)
  } catch (err) {
    console.error('上传失败', err)
  } finally {
    uploading.value = false
  }
}
</script>

实用建议

  • 如果需要多文件上传、拖拽区域、剪贴板粘贴,可使用成熟组件库(推荐 Element Plus 的 el-upload),它们已处理好这些交互。
  • 大文件分片上传、断点续传一般需要额外封装,可以借助 spark-md5 生成文件指纹,结合切片逻辑实现。

拖拽排序

核心需求:列表项可通过拖拽调整顺序,操作后更新数据。

推荐方案:使用 vuedraggable(Sortable.js 的 Vue 封装),它是事实标准。

安装npm install vuedraggable@next

基本用法

<template>
  <draggable v-model="list" item-key="id" handle=".handle" @end="onDragEnd">
    <template #item="{ element }">
      <div class="item">
        <span class="handle">☰</span>
        {{ element.name }}
      </div>
    </template>
  </draggable>
</template>

<script setup>
import { ref } from 'vue'
import draggable from 'vuedraggable'

const list = ref([
  { id: 1, name: '事项A' },
  { id: 2, name: '事项B' },
])

function onDragEnd() {
  // v-model 已同步顺序,可在此调用接口持久化
  console.log('新顺序', list.value.map(item => item.id))
}
</script>

实用建议

  • 通过 handle 限制只有特定图标才能拖动,避免误触。
  • 如果列表很大,开启 animation 过渡让移动更平滑。
  • 多列表互拖(如看板列)、拖拽克隆等高级场景,直接查阅 vuedraggable 文档即可。

富文本编辑器

核心需求:图文混排、样式设置、图片上传、内容回显(HTML)。

推荐方案Tiptap(基于 ProseMirror,与 Vue 3 结合极佳)或 quill

Tiptap 集成示例

npm install @tiptap/vue-3 @tiptap/starter-kit @tiptap/extension-image
<template>
  <div class="editor">
    <editor-content :editor="editor" />
  </div>
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'

const editor = useEditor({
  extensions: [
    StarterKit,
    Image,
  ],
  content: '<p>默认内容</p>',
  onUpdate({ editor }) {
    console.log(editor.getHTML()) // 获取 HTML 字符串
  }
})
</script>

实用建议

  • 图片上传需要自定义扩展:在工具栏中触发选择文件,上传拿到 URL 后通过 editor.chain().focus().setImage({ src: url }).run() 插入。
  • 安全性:务必在展示富文本内容的页面进行 XSS 过滤(如使用 DOMPurify),不要直接 v-html 原始 HTML。
  • 轻量需求可直接用 Quill,封装一个 <QuillEditor v-model="content" /> 组件。

无限滚动

核心需求:滚动到列表底部时自动加载下一页数据,直到所有数据加载完毕。

推荐方案:基于 Intersection Observer 封装指令或使用 vue-infinite-scroll 插件。

自实现指令(轻量、可控)

// v-infinite-scroll 指令
export default {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        binding.value() // 执行加载更多回调
      }
    })
    el.__observer__ = observer
    observer.observe(el)
  },
  unmounted(el) {
    el.__observer__?.disconnect()
  }
}

使用

<template>
  <div>
    <div v-for="item in list" :key="item.id">{{ item.text }}</div>
    <div v-infinite-scroll="loadMore" v-if="!finished">加载中...</div>
    <div v-else>没有更多了</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const page = ref(1)
const list = ref([])
const finished = ref(false)

async function loadMore() {
  const res = await getList(page.value)
  list.value.push(...res.data)
  page.value++
  if (res.data.length < 20) finished.value = true
}
</script>

实用建议

  • 必须给滚动容器设置固定高度且 overflow-y: auto,否则无法触发滚动。
  • 为避免重复请求,可以增加 loading 锁,在 loadMore 开始时设为 true,结束时置回 false,并用 v-if="loading" 控制触发元素展示。
  • 列表项过多时会产生性能问题,建议结合 虚拟列表(如 vue-virtual-scroller)来只渲染可视区域。

以上四个通用功能在封装后都可以沉淀为团队内部的公共组件或 Hooks(如 useUploaduseInfiniteScroll),大幅减少重复开发时间。核心原则是:借助成熟库解决底层交互,用 Vue 的组合式 API 做好业务逻辑和状态的封装