1. 加工费模块重构
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
2. 快递费模块重构 3. 订单导出功能优化 4. 商品订单的折扣功能 5. 修复了一些已知BUG
This commit is contained in:
133
apps/web-antd/src/components/video-player/XgVideoPlayer.vue
Normal file
133
apps/web-antd/src/components/video-player/XgVideoPlayer.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
import Player from 'xgplayer';
|
||||
|
||||
import 'xgplayer/dist/index.min.css';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
autoplay?: boolean;
|
||||
controls?: boolean;
|
||||
thumbnail?: boolean;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
poster?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoplay: false,
|
||||
controls: true,
|
||||
thumbnail: false,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
poster: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: [player: Player];
|
||||
error: [error: any];
|
||||
ended: [];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
const player = shallowRef<Player | null>(null);
|
||||
|
||||
function createPlayer() {
|
||||
if (!containerRef.value || !props.src) return;
|
||||
|
||||
player.value?.destroy();
|
||||
player.value = null;
|
||||
|
||||
const ignores = props.thumbnail
|
||||
? [
|
||||
'cssfullscreen',
|
||||
'screenshot',
|
||||
'pip',
|
||||
'miniscreen',
|
||||
'keyboard',
|
||||
'download',
|
||||
'playbackrate',
|
||||
'time',
|
||||
'definition',
|
||||
'fullscreen',
|
||||
'loading',
|
||||
'mobile',
|
||||
'pc',
|
||||
'play',
|
||||
'poster',
|
||||
'progress',
|
||||
'replay',
|
||||
'start',
|
||||
'volume',
|
||||
]
|
||||
: [];
|
||||
|
||||
const p = new Player({
|
||||
el: containerRef.value,
|
||||
url: props.src,
|
||||
autoplay: props.autoplay,
|
||||
controls: props.thumbnail ? false : props.controls,
|
||||
closeVideoClick: props.thumbnail,
|
||||
closeVideoDblclick: props.thumbnail,
|
||||
ignores,
|
||||
fluid: true,
|
||||
fitVideoSize: 'fixWidth',
|
||||
videoInit: true,
|
||||
poster: props.poster,
|
||||
playsinline: true,
|
||||
enableContextmenu: !props.thumbnail,
|
||||
});
|
||||
|
||||
p.on('ready', () => {
|
||||
emit('ready', p);
|
||||
});
|
||||
|
||||
p.on('error', (e: any) => {
|
||||
emit('error', e);
|
||||
});
|
||||
|
||||
p.on('ended', () => {
|
||||
emit('ended');
|
||||
});
|
||||
|
||||
player.value = p;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createPlayer();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
player.value?.destroy();
|
||||
player.value = null;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
createPlayer();
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
getPlayer: () => player.value,
|
||||
play: () => player.value?.play(),
|
||||
pause: () => player.value?.pause(),
|
||||
destroy: () => {
|
||||
player.value?.destroy();
|
||||
player.value = null;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="xg-video-player"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.xg-video-player {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
}
|
||||
</style>
|
||||
1
apps/web-antd/src/components/video-player/index.ts
Normal file
1
apps/web-antd/src/components/video-player/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as XgVideoPlayer } from './XgVideoPlayer.vue';
|
||||
@@ -11,11 +11,11 @@
|
||||
alt="预览"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<video
|
||||
<XgVideoPlayer
|
||||
v-else-if="uploadPreview.type === 'video'"
|
||||
:src="uploadPreview.url"
|
||||
class="w-full h-full object-cover"
|
||||
preload="metadata"
|
||||
thumbnail
|
||||
/>
|
||||
<FileOutlined v-else class="text-2xl text-gray-400"/>
|
||||
</div>
|
||||
@@ -61,6 +61,7 @@
|
||||
<script setup>
|
||||
import {inject} from 'vue';
|
||||
import {FileOutlined, SendOutlined, CloseOutlined} from '@ant-design/icons-vue';
|
||||
import { XgVideoPlayer } from '#/components/video-player';
|
||||
|
||||
const uploadPreview = inject('uploadPreview');
|
||||
const sendFile = inject('sendFile');
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
import { Image } from 'ant-design-vue';
|
||||
|
||||
import { XgVideoPlayer } from '#/components/video-player';
|
||||
import { useMediaPreview } from '../composables/useMediaPreview.ts';
|
||||
// import VueEasyLightbox from 'vue-easy-lightbox';
|
||||
import { useThemeStore } from '../stores/theme.ts';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { visible, mediaData, hidePreview, previewType } = useMediaPreview();
|
||||
|
||||
// 视频相关状态
|
||||
const videoPlayer = ref(null);
|
||||
const isPlaying = ref(false);
|
||||
const isMuted = ref(false);
|
||||
const volume = ref(1);
|
||||
const currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
const showControls = ref(true);
|
||||
const isPortrait = ref(false);
|
||||
const controlsTimeout = ref(null);
|
||||
|
||||
// 计算属性
|
||||
const showImagePreview = computed(
|
||||
() => visible.value && previewType.value === 'image',
|
||||
);
|
||||
@@ -29,340 +17,46 @@ const showVideoPreview = computed(
|
||||
() => visible.value && previewType.value === 'video',
|
||||
);
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (duration.value === 0) return 0;
|
||||
return (currentTime.value / duration.value) * 100;
|
||||
});
|
||||
|
||||
// 视频加载完成
|
||||
const handleVideoLoad = () => {
|
||||
if (videoPlayer.value) {
|
||||
duration.value = videoPlayer.value.duration;
|
||||
|
||||
// 检测视频方向
|
||||
const video = videoPlayer.value;
|
||||
isPortrait.value = video.videoHeight > video.videoWidth;
|
||||
|
||||
// 监听时间更新
|
||||
video.addEventListener('timeupdate', updateTime);
|
||||
}
|
||||
};
|
||||
|
||||
// 图片加载完成
|
||||
const handleImageLoad = () => {
|
||||
// 可以在这里添加图片加载完成的逻辑
|
||||
};
|
||||
|
||||
// 更新播放时间
|
||||
const updateTime = () => {
|
||||
if (videoPlayer.value) {
|
||||
currentTime.value = videoPlayer.value.currentTime;
|
||||
}
|
||||
};
|
||||
|
||||
// 视频播放/暂停事件
|
||||
const handleVideoPlay = () => {
|
||||
isPlaying.value = true;
|
||||
hideControlsAfterDelay();
|
||||
};
|
||||
|
||||
const handleVideoPause = () => {
|
||||
isPlaying.value = false;
|
||||
showControls.value = true;
|
||||
clearTimeout(controlsTimeout.value);
|
||||
};
|
||||
|
||||
// 控制功能
|
||||
const togglePlay = () => {
|
||||
if (videoPlayer.value) {
|
||||
if (isPlaying.value) {
|
||||
videoPlayer.value.pause();
|
||||
} else {
|
||||
videoPlayer.value.play();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.muted = !videoPlayer.value.muted;
|
||||
isMuted.value = videoPlayer.value.muted;
|
||||
}
|
||||
};
|
||||
|
||||
const updateVolume = () => {
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.volume = volume.value;
|
||||
isMuted.value = volume.value === 0;
|
||||
}
|
||||
};
|
||||
|
||||
const seekTo = (event) => {
|
||||
if (videoPlayer.value) {
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const percent = (event.clientX - rect.left) / rect.width;
|
||||
videoPlayer.value.currentTime = percent * duration.value;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (videoPlayer.value) {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
videoPlayer.value.requestFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const togglePictureInPicture = async () => {
|
||||
if (videoPlayer.value) {
|
||||
try {
|
||||
await (document.pictureInPictureElement
|
||||
? document.exitPictureInPicture()
|
||||
: videoPlayer.value.requestPictureInPicture());
|
||||
} catch (error) {
|
||||
console.error('画中画功能不支持:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏控制栏
|
||||
const hideControlsAfterDelay = () => {
|
||||
clearTimeout(controlsTimeout.value);
|
||||
controlsTimeout.value = setTimeout(() => {
|
||||
if (isPlaying.value) {
|
||||
showControls.value = false;
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 显示控制栏
|
||||
const showControlsTemporarily = () => {
|
||||
showControls.value = true;
|
||||
if (isPlaying.value) {
|
||||
hideControlsAfterDelay();
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatDuration = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 下载媒体
|
||||
const downloadMedia = () => {
|
||||
if (mediaData.value?.url) {
|
||||
const link = document.createElement('a');
|
||||
link.href = mediaData.value.url;
|
||||
link.download = `media_${Date.now()}`;
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
||||
// 分享媒体
|
||||
const shareMedia = async () => {
|
||||
if (navigator.share && mediaData.value?.url) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: '分享媒体',
|
||||
url: mediaData.value.url,
|
||||
});
|
||||
} catch {
|
||||
console.log('分享取消或失败');
|
||||
}
|
||||
} else {
|
||||
// 复制链接到剪贴板
|
||||
navigator.clipboard.writeText(mediaData.value?.url || '');
|
||||
alert('链接已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件处理
|
||||
const handleKeydown = (event) => {
|
||||
if (!visible.value) return;
|
||||
|
||||
switch (event.code) {
|
||||
case 'ArrowLeft': {
|
||||
if (showVideoPreview.value && videoPlayer.value) {
|
||||
event.preventDefault();
|
||||
videoPlayer.value.currentTime -= 10;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (showVideoPreview.value && videoPlayer.value) {
|
||||
event.preventDefault();
|
||||
videoPlayer.value.currentTime += 10;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
hidePreview();
|
||||
break;
|
||||
}
|
||||
case 'Space': {
|
||||
if (showVideoPreview.value) {
|
||||
event.preventDefault();
|
||||
togglePlay();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (event.code === 'Escape') {
|
||||
hidePreview();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听媒体数据变化
|
||||
watch(mediaData, () => {
|
||||
if (previewType.value === 'video') {
|
||||
isPlaying.value = false;
|
||||
currentTime.value = 0;
|
||||
duration.value = 0;
|
||||
showControls.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
clearTimeout(controlsTimeout.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 图片预览组件 -->
|
||||
<!-- <div
|
||||
v-if="showImagePreview"
|
||||
class="media-preview-overlay"
|
||||
:class="{ 'dark': themeStore.isDarkMode }"
|
||||
@click.self="hidePreview"
|
||||
>
|
||||
<div class="preview-container">
|
||||
<button class="preview-close-btn" @click="hidePreview">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<div class="image-preview-content">
|
||||
<img
|
||||
:src="mediaData"
|
||||
:alt="'图片预览'"
|
||||
class="preview-image"
|
||||
@load="handleImageLoad"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preview-actions">
|
||||
<button class="action-btn" @click="downloadMedia" title="下载">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button class="action-btn" @click="shareMedia" title="分享">
|
||||
<i class="fas fa-share"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
<Image
|
||||
:imgs="[{ src: mediaData }]"
|
||||
:visible="showImagePreview"
|
||||
@hide="hidePreview"
|
||||
/>
|
||||
|
||||
<!-- 视频预览组件 -->
|
||||
<div
|
||||
v-if="showVideoPreview"
|
||||
:class="{ dark: themeStore.isDarkMode }"
|
||||
class="media-preview-overlay video-preview"
|
||||
class="media-preview-overlay"
|
||||
@click.self="hidePreview"
|
||||
>
|
||||
<div :class="{ portrait: isPortrait }" class="video-preview-container">
|
||||
<div class="video-preview-container">
|
||||
<button class="preview-close-btn" @click="hidePreview">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<div class="video-content">
|
||||
<video
|
||||
ref="videoPlayer"
|
||||
:src="mediaData"
|
||||
autoplay
|
||||
class="preview-video"
|
||||
controls
|
||||
@loadedmetadata="handleVideoLoad"
|
||||
@pause="handleVideoPause"
|
||||
@play="handleVideoPlay"
|
||||
></video>
|
||||
|
||||
<!-- 自定义视频控制栏 -->
|
||||
<div v-if="showControls" class="video-controls">
|
||||
<div class="controls-top">
|
||||
<div class="video-title">视频预览</div>
|
||||
<div class="video-info">
|
||||
{{ formatDuration(currentTime) }} / {{ formatDuration(duration) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls-bottom">
|
||||
<button class="control-btn" @click="togglePlay">
|
||||
<i :class="isPlaying ? 'fa-pause' : 'fa-play'" class="fas"></i>
|
||||
</button>
|
||||
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" @click="seekTo">
|
||||
<div
|
||||
:style="{ width: `${progressPercent}%` }"
|
||||
class="progress-fill"
|
||||
></div>
|
||||
<div
|
||||
:style="{ left: `${progressPercent}%` }"
|
||||
class="progress-thumb"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="control-btn" @click="toggleMute">
|
||||
<i
|
||||
:class="isMuted ? 'fa-volume-mute' : 'fa-volume-up'"
|
||||
class="fas"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<div class="volume-container">
|
||||
<input
|
||||
v-model="volume"
|
||||
class="volume-slider"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.1"
|
||||
type="range"
|
||||
@input="updateVolume"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="control-btn" @click="toggleFullscreen">
|
||||
<i class="fas fa-expand"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-actions">
|
||||
<button class="action-btn" title="下载" @click="downloadMedia">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button class="action-btn" title="分享" @click="shareMedia">
|
||||
<i class="fas fa-share"></i>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
title="画中画"
|
||||
@click="togglePictureInPicture"
|
||||
>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<XgVideoPlayer
|
||||
:src="mediaData"
|
||||
class="preview-video"
|
||||
autoplay
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -396,15 +90,6 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-preview-container {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
@@ -417,10 +102,6 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-preview-container.portrait {
|
||||
max-width: 60vw;
|
||||
}
|
||||
|
||||
.preview-close-btn {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
@@ -445,178 +126,17 @@ onUnmounted(() => {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.image-preview-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.video-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
width: 90vw;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
color: white;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.controls-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.controls-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #4361ee;
|
||||
border-radius: 3px;
|
||||
transition: width 0.1s ease;
|
||||
}
|
||||
|
||||
.progress-thumb {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4361ee;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
transition: left 0.1s ease;
|
||||
}
|
||||
|
||||
.volume-container {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4361ee;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-actions,
|
||||
.video-actions {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 60px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.preview-container,
|
||||
.video-preview-container {
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.video-preview-container.portrait {
|
||||
max-width: 95vw;
|
||||
}
|
||||
|
||||
.preview-close-btn {
|
||||
top: -40px;
|
||||
width: 36px;
|
||||
@@ -624,35 +144,9 @@ onUnmounted(() => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.controls-bottom {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.volume-container {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.preview-actions,
|
||||
.video-actions {
|
||||
top: 16px;
|
||||
right: 50px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
.preview-video {
|
||||
width: 95vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,6 +54,7 @@ import previewMedia from '../composables/useMediaPreview.ts';
|
||||
import { useThemeStore } from '../stores/theme.ts';
|
||||
import { useUserStore as chatUseUserStore } from '../stores/user';
|
||||
import AudioMessage from './AudioMessage.vue';
|
||||
import { XgVideoPlayer } from '#/components/video-player';
|
||||
import PatientExperienceCard from '#/views/doctor/online-consultation/components/PatientExperienceCard.vue';
|
||||
import FollowUpDrugAssistantCard from './FollowUpDrugAssistantCard.vue';
|
||||
import ProductCard from '#/views/doctor/online-consultation/components/ProductCard.vue';
|
||||
@@ -472,12 +473,11 @@ const getSexText = (sex) => {
|
||||
class="video-message relative cursor-pointer"
|
||||
@click="handlePreviewMedia(parsedContent, 'video')"
|
||||
>
|
||||
<video
|
||||
<XgVideoPlayer
|
||||
:src="parsedContent"
|
||||
class="video-thumbnail max-w-full rounded-lg"
|
||||
preload="metadata"
|
||||
@error="handleVideoError"
|
||||
></video>
|
||||
thumbnail
|
||||
/>
|
||||
<div class="video-overlay">
|
||||
<div class="play-button">
|
||||
<i class="fas fa-play"></i>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { Button, Descriptions, Image, Spin } from 'ant-design-vue';
|
||||
|
||||
import MIcon from '#/components/icon/icon.vue';
|
||||
import { XgVideoPlayer } from '#/components/video-player';
|
||||
import type { FileGalleryDetail } from '#/api/core/file-gallery';
|
||||
import { getFileGalleryDetail } from '#/api/core/file-gallery';
|
||||
|
||||
@@ -70,11 +71,10 @@ defineExpose({
|
||||
<template v-if="detail">
|
||||
<div class="preview-wrap">
|
||||
<Image v-if="detail.type === 0" :src="detail.url" :width="220" />
|
||||
<video
|
||||
<XgVideoPlayer
|
||||
v-else-if="detail.type === 1"
|
||||
class="media-player"
|
||||
controls
|
||||
:src="detail.url"
|
||||
class="media-player"
|
||||
/>
|
||||
<audio
|
||||
v-else-if="detail.type === 2"
|
||||
|
||||
Reference in New Issue
Block a user