feat(community): 友圈动态支持上传短视频,可与图片同发

- 新增 /api/upload/video 独立上传接口:仅 MP4/MOV、单条 30MB、独立限流、登录鉴权

- posts 表新增 video_url / video_cover,发布接口支持视频与图片同时落库

- 小程序发布器支持选择/预览/删除视频(≤60 秒),动态卡片与详情页支持播放

- 后台动态管理新增视频预览弹窗

- 新增短视频链路与上传鉴权回归测试,125 套件/818 用例全绿
这个提交包含在:
xcbyy
2026-10-31 10:43:43 +08:00
父节点 1681d5d086
当前提交 23f500f623
共有 11 个文件被更改,包括 478 次插入 和 36 次删除

查看文件

@@ -105,8 +105,12 @@
</template>
</a-table-column>
<a-table-column title="配图" :width="160">
<a-table-column title="图片 / 视频" :width="170">
<template #cell="{ record }">
<div v-if="record.videoUrl" class="post-video-cell">
<a-tag color="arcoblue" size="small">短视频</a-tag>
<a-button type="text" size="mini" @click="openVideoPreview(record)">播放</a-button>
</div>
<a-image-preview-group v-if="record.images && record.images.length > 0">
<a-space wrap :size="4">
<a-image
@@ -120,7 +124,10 @@
/>
</a-space>
</a-image-preview-group>
<span v-else style="color: var(--color-text-4); font-size: 12px">纯文本无配图</span>
<span
v-if="!record.videoUrl && (!record.images || record.images.length === 0)"
style="color: var(--color-text-4); font-size: 12px"
>纯文本无配图</span>
</template>
</a-table-column>
@@ -214,6 +221,23 @@
</a-table>
</a-card>
<!-- 短视频预览 -->
<a-modal
v-model:visible="videoPreviewVisible"
:title="videoPreviewTitle"
:footer="false"
:width="560"
unmount-on-close
>
<video
v-if="videoPreviewUrl"
:src="videoPreviewUrl"
:poster="videoPreviewCover || undefined"
controls
style="width: 100%; max-height: 420px; background: #000; border-radius: 6px"
></video>
</a-modal>
<!-- 评论互动抽屉 (Drawer) -->
<a-drawer
v-model:visible="commentsDrawerVisible"
@@ -337,6 +361,19 @@
const loadingApplies = ref(false);
const appliesList = ref<any[]>([]);
// 短视频预览
const videoPreviewVisible = ref(false);
const videoPreviewUrl = ref('');
const videoPreviewCover = ref('');
const videoPreviewTitle = ref('动态短视频预览');
const openVideoPreview = (record: any) => {
videoPreviewUrl.value = record.videoUrl || '';
videoPreviewCover.value = record.videoCover || '';
videoPreviewTitle.value = `动态短视频 - ${record.author || ''}`;
videoPreviewVisible.value = true;
};
const fetchData = async () => {
loading.value = true;
try {