在大部分后台管理系统或 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(如 useUpload、useInfiniteScroll),大幅减少重复开发时间。核心原则是:借助成熟库解决底层交互,用 Vue 的组合式 API 做好业务逻辑和状态的封装。