
基于 Spring Boot3、Vue3、Naive UI 构建,助力应用快速开发、发布、运维的低代码平台,旨在帮助使用者(包含但不限于开发人员、业务人员)快速响应业务需求
项目地址1:https://gitcode.com/app-meta/app-meta
项目地址2:https://github.com/app-meta/app-meta
需求背景
客户希望增加一个在线播放视频的功能(类似于 Markdown 文档页),同时可以查看哪些用户查看该视频。
思路
用现有的快应用就能快速解决该需求,视频文件以附件形式上传(后端已经有处理附件的接口)。
常见播放器
GitHub 收藏数据截止到 2026-02-10
| 序号 | 播放器 | GitHub ⭐Star | 最新版本 & 发布情况 | 社区/维护活跃度 | 包体积 / 说明 |
|---|---|---|---|---|---|
| 1 | mui-player | ⭐ ~550+ | 最新 v1.8.1(发布 ~3 年前)([NPM][1]) | 🟡 维护较弱(Issues 不够活跃) | 中等体积 HTML5 UI 播放器(~35 KB 核心)([jsDelivr][2]) |
| 2 | oplayer | ⭐ ~180+ | 最新 UI 组件 v1.3.x(2024 发布) | 🟡 中等(Release & PR 不算频繁) | 未官方明确说明体积(轻量级实现) |
| 3 | Video.js | ⭐ ~39.5k | v8.x 系列持续更新 | 🟢 非常活跃(成熟生态 & 大贡献者网络) | 功能丰富但体积相对较大 |
| 4 | ArtPlayer.js | ⭐ ~3.5k | v5.x 系列活跃发布 | 🟢 中高(插件生态 & 文档较完善) | ~25 KB minified + gzipped(轻量) |
ArtPlayer.js

Video.js
Video.js 明显领先,是最成熟、最受欢迎的视频播放器库,适合大型业务级应用,并提供非常全面的能力:多格式支持、插件体系、主题、事件机制等,但体积相对大。

oplayer
OPlayer 有不错的功能覆盖,但 Star 数相对较少,社区参与度一般。

mui-player
MuiPlayer 在国内生态相对有人关注,但整体社区活跃度偏弱。

集成到 Vue3
最终,选择了 ArtPlayer 播放器。
安装依赖
shell
pnpm i artplayer
渲染器
html
<template>
<n-card :title="title">
<n-grid :gutter="10">
<n-grid-item :span>
<div ref="playerDiv" :style="style"></div>
</n-grid-item>
<n-grid-item :span="24-span">
<n-card title="视频信息" size="small" :style="`height: `+height+`px`" content-style="overflow:auto">
<n-table size="small" :bordered="true" :single-line="false">
<tbody>
<tr>
<td class="text-center" width="120">大小</td>
<td>{{filesize(video.size)}}</td>
</tr>
<tr>
<td class="text-center">上传者</td>
<td>{{ video.user }}</td>
</tr>
<tr>
<td class="text-center">上传时间</td>
<td>{{ video.time? toDate(video.time) : "" }}</td>
</tr>
</tbody>
</n-table>
<div class="mt-4" v-if="video.summary">
<n-tag :bordered="false">描述信息</n-tag>
<div class="mt-1">{{ video.summary }}</div>
</div>
</n-card>
</n-grid-item>
</n-grid>
</n-card>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import ArtPlayer from 'artplayer'
import { renderProps, loadContent } from "../"
const props = defineProps(renderProps)
const height = window.innerHeight - 160
let player = null
let title = ref(props.page.name)
const video = JSON.parse(props.data)
const filesize = r=> H.filesize(r)
const toDate = d=>H.date.datetime(d)
let span = ref(24)
const playerDiv = ref()
const style = reactive({
margin: "5px auto",
height: `${height}px`,
overflow: "hidden"
})
onMounted(() => {
let { ratio = 16/9, highlight = [], path, showInfo } = video
//计算宽度
style.width = `${Math.floor(height * ratio)}px`
span.value = showInfo ? 16 : 24
let url = path ? window.SERVER+"/"+path : null
//处理高亮点
let highlights = []
if(highlight){
highlight.split("\n").forEach(line=>{
let [time, text] = line.trim().split(/\s+/)
if(time && text && !isNaN(time))
highlights.push({ time: Number(time), text })
})
}
player = new ArtPlayer({
//播放器的唯一标识,目前只用于记忆播放 autoplayback
id: `video-${props.page.id}`,
container: playerDiv.value,
url, //'https://muiplayer.js.org/media/media.mp4',
volume: 1.0, //播放器的默认音量
muted: false, //是否默认静音
autoSize: true, //播放器的尺寸默认会填充整个 container 容器尺寸,所以经常出现黑边,该值能自动调整播放器尺寸以隐藏黑边
autoPlay: false,
loop: false, //是否循环播放
playbackRate: true, //是否显示视频播放速度功能,会出现在 设置面板 和 右键菜单 里
setting: true, //是否在底部控制栏里显示 设置面板 的开关按钮
screenshot: true, //是否在底部控制栏里显示播放器 视频截图 功能
fullscreen: true, //是否在底部控制栏里显示播放器 窗口全屏 按钮
fullscreenWeb: true, //是否在底部控制栏里显示播放器 网页全屏 按钮
pip: true, //是否在底部控制栏里显示 画中画 的开关按钮
theme: window.color, //播放器主题颜色,目前用于 进度条 和 高亮元素 上
miniProgressBar: true, //迷你进度条,只在播放器失去焦点后且正在播放时出现
highlight: highlights, //在进度条上显示高亮信息,格式为:[ { time:60, text:`前方高能` }]
autoPlayback: true, //是否使用自动 回放功能
lock: true, //是否在移动端显示一个 锁定按钮 ,用于隐藏底部 控制栏
aspectRatio: true, //是否显示视频长宽比功能,会出现在 设置面板 和 右键菜单 里
})
})
onUnmounted(() => player?.destroy(false))
</script>
编辑页面
html
<!-- 视频页编辑 -->
<template>
<n-card title="视频设置" segmented size="small">
<n-form :show-feedback="false" label-placement="left" label-width="100px">
<n-space vertical>
<n-form-item label="画面宽高比">
<n-select v-model:value="bean.ratio" :options="ratios" />
</n-form-item>
<n-form-item label="显示基本信息">
<n-switch v-model:value="bean.showInfo" />
<n-text class="ml-2" depth="3">勾选后将显示视频的大小、上传者、日期等信息</n-text>
</n-form-item>
<n-form-item label="描述信息">
<n-input v-model:value="bean.summary" type="textarea" />
</n-form-item>
<n-form-item label="更新视频">
<Uploader action="/page/document-upload" :data accept=".mp4" showFileList :noticeOnOk="false" @ok="uploadDone">
<n-button type="primary" secondary>上传视频文件</n-button>
<n-text class="ml-2" depth="3">仅支持 MP4 格式,重新生成将覆盖原视频</n-text>
<div v-if="bean.id || bean.size" class="mt-1">
<n-tag size="small" :bordered="false">{{ bean.addOn? toDate(bean.addOn) : "" }}</n-tag>
已上传视频
<n-tag size="small" :bordered="false">{{ bean.file}}</n-tag> ,大小为
<n-tag size="small" :bordered="false">{{ filesize(bean.size) }}</n-tag>
</div>
</Uploader>
</n-form-item>
<n-form-item label="高亮时间点">
<n-space vertical class="w-full">
<n-input v-model:value="bean.highlight" type="textarea" placeholder="在进度条上显示高亮信息"/>
<n-text class="ml-2" depth="3">
一行一个高亮,格式为
<n-tag size="small" :bordered="false">秒数(空格)文本</n-tag>,示例:
10 前方高能
</n-text>
</n-space>
</n-form-item>
<div class="text-center">
<n-button type="primary" @click="toSave" size="large">更新视频配置信息</n-button>
</div>
</n-space>
</n-form>
</n-card>
</template>
<script setup>
import { ref, reactive, h, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import Uploader from "@C/uploader.vue"
import { pageEditor } from "../"
const r16_9 = 1.78
let ratios = [
{ label:"16:9", value: r16_9 },
{ label:"16:10", value: 1.6 },
{ label:"4:3", value: 1.33 },
{ label:"1:1", value: 1 }
]
let { id, bean, loading , updateContent } = pageEditor(
{ ratio: r16_9, summary: "", path: null, highlight: '' },
d=> JSON.parse(d),
{ padding: false }
)
const filesize = r=> H.filesize(r)
const toDate = d=>H.date.datetime(d)
const data = computed(()=> bean.value.id ? { pid: id, id: bean.value.id }:{ pid: id })
let uploadDone = ({ data:{ path, addOn, uid, id } }, { file:{ size, name }})=>{
bean.value.path = path? path.replace("\\","/") : null
bean.value.size = size
bean.value.user = uid
bean.value.addOn= Date.now()
bean.value.file = name
bean.value.id = id
M.ok(`⌈${name}⌋已上传`)
}
let toSave = () => updateContent(JSON.stringify(bean.value))
</script>
效果展示
新增视频模块

视频配置

播放页面
