mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加音频元素
This commit is contained in:
parent
405fd8cd57
commit
162aa32d59
@ -6,6 +6,7 @@ export const ELEMENT_TYPE_ZH = {
|
|||||||
chart: '图表',
|
chart: '图表',
|
||||||
table: '表格',
|
table: '表格',
|
||||||
video: '视频',
|
video: '视频',
|
||||||
|
audio: '音频',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MIN_SIZE = {
|
export const MIN_SIZE = {
|
||||||
@ -15,4 +16,5 @@ export const MIN_SIZE = {
|
|||||||
chart: 200,
|
chart: 200,
|
||||||
table: 20,
|
table: 20,
|
||||||
video: 250,
|
video: 250,
|
||||||
|
audio: 20,
|
||||||
}
|
}
|
@ -262,6 +262,27 @@ export default () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建音频元素
|
||||||
|
* @param src 音频地址
|
||||||
|
*/
|
||||||
|
const createAudioElement = (src: string) => {
|
||||||
|
createElement({
|
||||||
|
type: 'audio',
|
||||||
|
id: createRandomCode(),
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
rotate: 0,
|
||||||
|
left: (VIEWPORT_SIZE - 50) / 2,
|
||||||
|
top: (VIEWPORT_SIZE * viewportRatio.value - 50) / 2,
|
||||||
|
loop: false,
|
||||||
|
autoplay: false,
|
||||||
|
fixedRatio: true,
|
||||||
|
color: theme.value.themeColor,
|
||||||
|
src,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createImageElement,
|
createImageElement,
|
||||||
createChartElement,
|
createChartElement,
|
||||||
@ -271,5 +292,6 @@ export default () => {
|
|||||||
createLineElement,
|
createLineElement,
|
||||||
createLatexElement,
|
createLatexElement,
|
||||||
createVideoElement,
|
createVideoElement,
|
||||||
|
createAudioElement,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,7 @@ export const enum ElementTypes {
|
|||||||
TABLE = 'table',
|
TABLE = 'table',
|
||||||
LATEX = 'latex',
|
LATEX = 'latex',
|
||||||
VIDEO = 'video',
|
VIDEO = 'video',
|
||||||
|
AUDIO = 'audio',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -511,8 +512,32 @@ export interface PPTVideoElement extends PPTBaseElement {
|
|||||||
poster?: string;
|
poster?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频元素
|
||||||
|
*
|
||||||
|
* type: 元素类型(audio)
|
||||||
|
*
|
||||||
|
* fixedRatio: 固定图标宽高比例
|
||||||
|
*
|
||||||
|
* color: 图标颜色
|
||||||
|
*
|
||||||
|
* loop: 循环播放
|
||||||
|
*
|
||||||
|
* autoplay: 自动播放
|
||||||
|
*
|
||||||
|
* src: 音频地址
|
||||||
|
*/
|
||||||
|
export interface PPTAudioElement extends PPTBaseElement {
|
||||||
|
type: 'audio';
|
||||||
|
fixedRatio: boolean;
|
||||||
|
color: string,
|
||||||
|
loop: boolean,
|
||||||
|
autoplay: boolean,
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement
|
|
||||||
|
export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement | PPTAudioElement
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,6 +39,7 @@ import ChartElement from '@/views/components/element/ChartElement/index.vue'
|
|||||||
import TableElement from '@/views/components/element/TableElement/index.vue'
|
import TableElement from '@/views/components/element/TableElement/index.vue'
|
||||||
import LatexElement from '@/views/components/element/LatexElement/index.vue'
|
import LatexElement from '@/views/components/element/LatexElement/index.vue'
|
||||||
import VideoElement from '@/views/components/element/VideoElement/index.vue'
|
import VideoElement from '@/views/components/element/VideoElement/index.vue'
|
||||||
|
import AudioElement from '@/views/components/element/AudioElement/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'editable-element',
|
name: 'editable-element',
|
||||||
@ -75,6 +76,7 @@ export default defineComponent({
|
|||||||
[ElementTypes.TABLE]: TableElement,
|
[ElementTypes.TABLE]: TableElement,
|
||||||
[ElementTypes.LATEX]: LatexElement,
|
[ElementTypes.LATEX]: LatexElement,
|
||||||
[ElementTypes.VIDEO]: VideoElement,
|
[ElementTypes.VIDEO]: VideoElement,
|
||||||
|
[ElementTypes.AUDIO]: AudioElement,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
/>
|
/>
|
||||||
<RotateHandler
|
<RotateHandler
|
||||||
class="operate-rotate-handler"
|
class="operate-rotate-handler"
|
||||||
|
v-if="!cannotRotate"
|
||||||
:style="{ left: scaleWidth / 2 + 'px' }"
|
:style="{ left: scaleWidth / 2 + 'px' }"
|
||||||
@mousedown.stop="rotateElement(elementInfo)"
|
@mousedown.stop="rotateElement(elementInfo)"
|
||||||
/>
|
/>
|
||||||
@ -30,7 +31,7 @@
|
|||||||
import { computed, defineComponent, PropType } from 'vue'
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useMainStore } from '@/store'
|
import { useMainStore } from '@/store'
|
||||||
import { PPTShapeElement, PPTVideoElement, PPTLatexElement } from '@/types/slides'
|
import { PPTShapeElement, PPTVideoElement, PPTLatexElement, PPTAudioElement } from '@/types/slides'
|
||||||
import { OperateResizeHandler } from '@/types/edit'
|
import { OperateResizeHandler } from '@/types/edit'
|
||||||
import useCommonOperate from '../hooks/useCommonOperate'
|
import useCommonOperate from '../hooks/useCommonOperate'
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ import RotateHandler from './RotateHandler.vue'
|
|||||||
import ResizeHandler from './ResizeHandler.vue'
|
import ResizeHandler from './ResizeHandler.vue'
|
||||||
import BorderLine from './BorderLine.vue'
|
import BorderLine from './BorderLine.vue'
|
||||||
|
|
||||||
type PPTElement = PPTShapeElement | PPTVideoElement | PPTLatexElement
|
type PPTElement = PPTShapeElement | PPTVideoElement | PPTLatexElement | PPTAudioElement
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'common-element-operate',
|
name: 'common-element-operate',
|
||||||
@ -73,10 +74,13 @@ export default defineComponent({
|
|||||||
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
||||||
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
||||||
|
|
||||||
|
const cannotRotate = computed(() => ['video', 'audio'].includes(props.elementInfo.type))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scaleWidth,
|
scaleWidth,
|
||||||
resizeHandlers,
|
resizeHandlers,
|
||||||
borderLines,
|
borderLines,
|
||||||
|
cannotRotate,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -107,6 +107,7 @@ export default defineComponent({
|
|||||||
[ElementTypes.CHART]: CommonElementOperate,
|
[ElementTypes.CHART]: CommonElementOperate,
|
||||||
[ElementTypes.LATEX]: CommonElementOperate,
|
[ElementTypes.LATEX]: CommonElementOperate,
|
||||||
[ElementTypes.VIDEO]: CommonElementOperate,
|
[ElementTypes.VIDEO]: CommonElementOperate,
|
||||||
|
[ElementTypes.AUDIO]: CommonElementOperate,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
98
src/views/Editor/CanvasTool/MediaInput.vue
Normal file
98
src/views/Editor/CanvasTool/MediaInput.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div class="media-input">
|
||||||
|
<div class="tabs">
|
||||||
|
<div
|
||||||
|
class="tab"
|
||||||
|
:class="{ 'active': type === tab.key }"
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
@click="type = tab.key"
|
||||||
|
>{{tab.label}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="type === 'video'">
|
||||||
|
<Input v-model:value="videoSrc" placeholder="请输入视频地址,e.g. https://xxx.mp4"></Input>
|
||||||
|
<div class="btns">
|
||||||
|
<Button @click="close()" style="margin-right: 10px;">取消</Button>
|
||||||
|
<Button type="primary" @click="insertVideo()">确认</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="type === 'audio'">
|
||||||
|
<Input v-model:value="audioSrc" placeholder="请输入音频地址,e.g. https://xxx.mp3"></Input>
|
||||||
|
<div class="btns">
|
||||||
|
<Button @click="close()" style="margin-right: 10px;">取消</Button>
|
||||||
|
<Button type="primary" @click="insertAudio()">确认</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'media-input',
|
||||||
|
emits: ['insertVideo', 'insertAudio', 'close'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const type = ref<'video' | 'audio'>('video')
|
||||||
|
|
||||||
|
const videoSrc = ref('https://www.w3school.com.cn/i/movie.ogg')
|
||||||
|
const audioSrc = ref('https://www.w3school.com.cn/i/horse.ogg')
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'video', label: '视频' },
|
||||||
|
{ key: 'audio', label: '音频' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const insertVideo = () => {
|
||||||
|
if (!videoSrc.value) return message.error('请先输入正确的视频地址')
|
||||||
|
emit('insertVideo', videoSrc.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAudio = () => {
|
||||||
|
if (!audioSrc.value) return message.error('请先输入正确的音频地址')
|
||||||
|
emit('insertAudio', audioSrc.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => emit('close')
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
videoSrc,
|
||||||
|
audioSrc,
|
||||||
|
tabs,
|
||||||
|
insertVideo,
|
||||||
|
insertAudio,
|
||||||
|
close,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.media-input {
|
||||||
|
width: 480px;
|
||||||
|
}
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid $borderColor;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
padding: 0 10px 8px;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-bottom: 2px solid $themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btns {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,45 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="video-input">
|
|
||||||
<Input v-model:value="src" placeholder="请输入视频地址,e.g. https://xxx.mp4"></Input>
|
|
||||||
<div class="btns">
|
|
||||||
<Button @click="close()" style="margin-right: 10px;">取消</Button>
|
|
||||||
<Button type="primary" @click="intsertVideo()">确认</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'video-input',
|
|
||||||
emits: ['insert', 'close'],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const src = ref('https://www.w3school.com.cn/i/movie.ogg')
|
|
||||||
|
|
||||||
const intsertVideo = () => {
|
|
||||||
if (!src.value) return message.error('请先输入正确的视频地址')
|
|
||||||
emit('insert', src.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => emit('close')
|
|
||||||
|
|
||||||
return {
|
|
||||||
src,
|
|
||||||
intsertVideo,
|
|
||||||
close,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.video-input {
|
|
||||||
width: 480px;
|
|
||||||
}
|
|
||||||
.btns {
|
|
||||||
margin-top: 10px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -56,14 +56,15 @@
|
|||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入公式">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入公式">
|
||||||
<IconFormula class="handler-item" @click="latexEditorVisible = true" />
|
<IconFormula class="handler-item" @click="latexEditorVisible = true" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover trigger="click" v-model:visible="videoInputVisible">
|
<Popover trigger="click" v-model:visible="mediaInputVisible">
|
||||||
<template #content>
|
<template #content>
|
||||||
<VideoInput
|
<MediaInput
|
||||||
@close="videoInputVisible = false"
|
@close="mediaInputVisible = false"
|
||||||
@insert="src => { createVideoElement(src); videoInputVisible = false }"
|
@insertVideo="src => { createVideoElement(src); mediaInputVisible = false }"
|
||||||
|
@insertAudio="src => { createAudioElement(src); mediaInputVisible = false }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入视频">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入音视频">
|
||||||
<IconVideoTwo class="handler-item" />
|
<IconVideoTwo class="handler-item" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -108,7 +109,7 @@ import ShapePool from './ShapePool.vue'
|
|||||||
import LinePool from './LinePool.vue'
|
import LinePool from './LinePool.vue'
|
||||||
import ChartPool from './ChartPool.vue'
|
import ChartPool from './ChartPool.vue'
|
||||||
import TableGenerator from './TableGenerator.vue'
|
import TableGenerator from './TableGenerator.vue'
|
||||||
import VideoInput from './VideoInput.vue'
|
import MediaInput from './MediaInput.vue'
|
||||||
import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
|
import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -118,7 +119,7 @@ export default defineComponent({
|
|||||||
LinePool,
|
LinePool,
|
||||||
ChartPool,
|
ChartPool,
|
||||||
TableGenerator,
|
TableGenerator,
|
||||||
VideoInput,
|
MediaInput,
|
||||||
LaTeXEditor,
|
LaTeXEditor,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
@ -131,7 +132,14 @@ export default defineComponent({
|
|||||||
const { scaleCanvas, setCanvasPercentage } = useScaleCanvas()
|
const { scaleCanvas, setCanvasPercentage } = useScaleCanvas()
|
||||||
const { redo, undo } = useHistorySnapshot()
|
const { redo, undo } = useHistorySnapshot()
|
||||||
|
|
||||||
const { createImageElement, createChartElement, createTableElement, createLatexElement, createVideoElement } = useCreateElement()
|
const {
|
||||||
|
createImageElement,
|
||||||
|
createChartElement,
|
||||||
|
createTableElement,
|
||||||
|
createLatexElement,
|
||||||
|
createVideoElement,
|
||||||
|
createAudioElement,
|
||||||
|
} = useCreateElement()
|
||||||
|
|
||||||
const insertImageElement = (files: File[]) => {
|
const insertImageElement = (files: File[]) => {
|
||||||
const imageFile = files[0]
|
const imageFile = files[0]
|
||||||
@ -143,7 +151,7 @@ export default defineComponent({
|
|||||||
const linePoolVisible = ref(false)
|
const linePoolVisible = ref(false)
|
||||||
const chartPoolVisible = ref(false)
|
const chartPoolVisible = ref(false)
|
||||||
const tableGeneratorVisible = ref(false)
|
const tableGeneratorVisible = ref(false)
|
||||||
const videoInputVisible = ref(false)
|
const mediaInputVisible = ref(false)
|
||||||
const latexEditorVisible = ref(false)
|
const latexEditorVisible = ref(false)
|
||||||
|
|
||||||
// 绘制文字范围
|
// 绘制文字范围
|
||||||
@ -184,7 +192,7 @@ export default defineComponent({
|
|||||||
linePoolVisible,
|
linePoolVisible,
|
||||||
chartPoolVisible,
|
chartPoolVisible,
|
||||||
tableGeneratorVisible,
|
tableGeneratorVisible,
|
||||||
videoInputVisible,
|
mediaInputVisible,
|
||||||
latexEditorVisible,
|
latexEditorVisible,
|
||||||
drawText,
|
drawText,
|
||||||
drawShape,
|
drawShape,
|
||||||
@ -193,6 +201,7 @@ export default defineComponent({
|
|||||||
createTableElement,
|
createTableElement,
|
||||||
createLatexElement,
|
createLatexElement,
|
||||||
createVideoElement,
|
createVideoElement,
|
||||||
|
createAudioElement,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, provide, ref } from 'vue'
|
import { computed, defineComponent, ref } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
|
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
|
||||||
import { fillDigit } from '@/utils/common'
|
import { fillDigit } from '@/utils/common'
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
@change="value => updateWidth(value)"
|
@change="value => updateWidth(value)"
|
||||||
style="flex: 4;"
|
style="flex: 4;"
|
||||||
/>
|
/>
|
||||||
<template v-if="['image', 'shape'].includes(handleElement.type)">
|
<template v-if="['image', 'shape', 'audio'].includes(handleElement.type)">
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="解除宽高比锁定" v-if="fixedRatio">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="解除宽高比锁定" v-if="fixedRatio">
|
||||||
<IconLock style="flex: 1;" class="icon-btn" @click="updateFixedRatio(false)" />
|
<IconLock style="flex: 1;" class="icon-btn" @click="updateFixedRatio(false)" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -99,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="handleElement.type !== 'line'">
|
<template v-if="!['line', 'video', 'audio'].includes(handleElement.type)">
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="audio-style-panel">
|
||||||
|
<div class="row">
|
||||||
|
<div style="flex: 2;">图标颜色:</div>
|
||||||
|
<Popover trigger="click">
|
||||||
|
<template #content>
|
||||||
|
<ColorPicker
|
||||||
|
:modelValue="handleElement.color"
|
||||||
|
@update:modelValue="value => updateAudio({ color: value })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<ColorButton :color="handleElement.color" style="flex: 3;" />
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row switch-row">
|
||||||
|
<div style="flex: 2;">自动播放:</div>
|
||||||
|
<div class="switch-wrapper" style="flex: 3;">
|
||||||
|
<Switch
|
||||||
|
:checked="handleElement.autoplay"
|
||||||
|
@change="checked => updateAudio({ autoplay: checked })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row switch-row">
|
||||||
|
<div style="flex: 2;">循环播放:</div>
|
||||||
|
<div class="switch-wrapper" style="flex: 3;">
|
||||||
|
<Switch
|
||||||
|
:checked="handleElement.loop"
|
||||||
|
@change="checked => updateAudio({ loop: checked })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
|
import { PPTAudioElement } from '@/types/slides'
|
||||||
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
|
||||||
|
import ColorButton from '../common/ColorButton.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'audio-style-panel',
|
||||||
|
components: {
|
||||||
|
ColorButton,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const slidesStore = useSlidesStore()
|
||||||
|
const { handleElement } = storeToRefs(useMainStore())
|
||||||
|
|
||||||
|
const { addHistorySnapshot } = useHistorySnapshot()
|
||||||
|
|
||||||
|
const updateAudio = (props: Partial<PPTAudioElement>) => {
|
||||||
|
if (!handleElement.value) return
|
||||||
|
slidesStore.updateElement({ id: handleElement.value.id, props })
|
||||||
|
addHistorySnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleElement,
|
||||||
|
updateAudio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.switch-row {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.switch-wrapper {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
@ -21,6 +21,7 @@ import ChartStylePanel from './ChartStylePanel/index.vue'
|
|||||||
import TableStylePanel from './TableStylePanel.vue'
|
import TableStylePanel from './TableStylePanel.vue'
|
||||||
import LatexStylePanel from './LatexStylePanel.vue'
|
import LatexStylePanel from './LatexStylePanel.vue'
|
||||||
import VideoStylePanel from './VideoStylePanel.vue'
|
import VideoStylePanel from './VideoStylePanel.vue'
|
||||||
|
import AudioStylePanel from './AudioStylePanel.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'element-style-panel',
|
name: 'element-style-panel',
|
||||||
@ -39,6 +40,7 @@ export default defineComponent({
|
|||||||
[ElementTypes.TABLE]: TableStylePanel,
|
[ElementTypes.TABLE]: TableStylePanel,
|
||||||
[ElementTypes.LATEX]: LatexStylePanel,
|
[ElementTypes.LATEX]: LatexStylePanel,
|
||||||
[ElementTypes.VIDEO]: VideoStylePanel,
|
[ElementTypes.VIDEO]: VideoStylePanel,
|
||||||
|
[ElementTypes.AUDIO]: AudioStylePanel,
|
||||||
}
|
}
|
||||||
return panelMap[handleElement.value.type] || null
|
return panelMap[handleElement.value.type] || null
|
||||||
})
|
})
|
||||||
|
@ -329,6 +329,7 @@ export default defineComponent({
|
|||||||
el.gridColor = fontColor
|
el.gridColor = fontColor
|
||||||
}
|
}
|
||||||
else if (el.type === 'latex') el.color = fontColor
|
else if (el.type === 'latex') el.color = fontColor
|
||||||
|
else if (el.type === 'audio') el.color = themeColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slidesStore.setSlides(newSlides)
|
slidesStore.setSlides(newSlides)
|
||||||
|
@ -33,6 +33,7 @@ import ScreenChartElement from '@/views/components/element/ChartElement/ScreenCh
|
|||||||
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
||||||
import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexElement.vue'
|
import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexElement.vue'
|
||||||
import ScreenVideoElement from '@/views/components/element/VideoElement/ScreenVideoElement.vue'
|
import ScreenVideoElement from '@/views/components/element/VideoElement/ScreenVideoElement.vue'
|
||||||
|
import ScreenAudioElement from '@/views/components/element/AudioElement/ScreenAudioElement.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'screen-element',
|
name: 'screen-element',
|
||||||
@ -65,6 +66,7 @@ export default defineComponent({
|
|||||||
[ElementTypes.TABLE]: BaseTableElement,
|
[ElementTypes.TABLE]: BaseTableElement,
|
||||||
[ElementTypes.LATEX]: BaseLatexElement,
|
[ElementTypes.LATEX]: BaseLatexElement,
|
||||||
[ElementTypes.VIDEO]: ScreenVideoElement,
|
[ElementTypes.VIDEO]: ScreenVideoElement,
|
||||||
|
[ElementTypes.AUDIO]: ScreenAudioElement,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, PropType, defineComponent } from 'vue'
|
import { computed, PropType, defineComponent, provide } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useSlidesStore } from '@/store'
|
import { useSlidesStore } from '@/store'
|
||||||
import { Slide } from '@/types/slides'
|
import { Slide } from '@/types/slides'
|
||||||
@ -58,6 +58,9 @@ export default defineComponent({
|
|||||||
const background = computed(() => props.slide.background)
|
const background = computed(() => props.slide.background)
|
||||||
const { backgroundStyle } = useSlideBackgroundStyle(background)
|
const { backgroundStyle } = useSlideBackgroundStyle(background)
|
||||||
|
|
||||||
|
const slideId = computed(() => props.slide.id)
|
||||||
|
provide('slideId', slideId)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundStyle,
|
backgroundStyle,
|
||||||
VIEWPORT_SIZE,
|
VIEWPORT_SIZE,
|
||||||
|
@ -26,6 +26,7 @@ import BaseChartElement from '@/views/components/element/ChartElement/BaseChartE
|
|||||||
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
||||||
import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexElement.vue'
|
import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexElement.vue'
|
||||||
import BaseVideoElement from '@/views/components/element/VideoElement/BaseVideoElement.vue'
|
import BaseVideoElement from '@/views/components/element/VideoElement/BaseVideoElement.vue'
|
||||||
|
import BaseAudioElement from '@/views/components/element/AudioElement/BaseAudioElement.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'base-element',
|
name: 'base-element',
|
||||||
@ -50,6 +51,7 @@ export default defineComponent({
|
|||||||
[ElementTypes.TABLE]: BaseTableElement,
|
[ElementTypes.TABLE]: BaseTableElement,
|
||||||
[ElementTypes.LATEX]: BaseLatexElement,
|
[ElementTypes.LATEX]: BaseLatexElement,
|
||||||
[ElementTypes.VIDEO]: BaseVideoElement,
|
[ElementTypes.VIDEO]: BaseVideoElement,
|
||||||
|
[ElementTypes.AUDIO]: BaseAudioElement,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
543
src/views/components/element/AudioElement/AudioPlayer.vue
Normal file
543
src/views/components/element/AudioElement/AudioPlayer.vue
Normal file
@ -0,0 +1,543 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="audio-player"
|
||||||
|
:style="{ transform: `scale(${1 / scale})` }"
|
||||||
|
>
|
||||||
|
<audio
|
||||||
|
class="audio"
|
||||||
|
ref="audioRef"
|
||||||
|
:src="src"
|
||||||
|
:autoplay="autoplay"
|
||||||
|
@durationchange="handleDurationchange()"
|
||||||
|
@timeupdate="handleTimeupdate()"
|
||||||
|
@play="handlePlayed()"
|
||||||
|
@ended="handleEnded()"
|
||||||
|
@progress="handleProgress()"
|
||||||
|
@error="handleError()"
|
||||||
|
></audio>
|
||||||
|
|
||||||
|
<div class="controller">
|
||||||
|
<div class="icons">
|
||||||
|
<div class="icon play-icon" @click="toggle()">
|
||||||
|
<span class="icon-content">
|
||||||
|
<IconPlayOne v-if="paused" />
|
||||||
|
<IconPause v-else />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="volume">
|
||||||
|
<div class="icon volume-icon" @click="toggleVolume()">
|
||||||
|
<span class="icon-content">
|
||||||
|
<IconVolumeMute v-if="volume === 0" />
|
||||||
|
<IconVolumeNotice v-else-if="volume === 1" />
|
||||||
|
<IconVolumeSmall v-else />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="volume-bar-wrap"
|
||||||
|
@mousedown="$event => handleMousedownVolumeBar($event)"
|
||||||
|
@touchstart="$event => handleMousedownVolumeBar($event)"
|
||||||
|
@click="$event => handleClickVolumeBar($event)"
|
||||||
|
>
|
||||||
|
<div class="volume-bar" ref="volumeBarRef">
|
||||||
|
<div class="volume-bar-inner" :style="{ width: volumeBarWidth }">
|
||||||
|
<span class="thumb"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="time">
|
||||||
|
<span class="ptime">{{ptime}}</span> / <span class="dtime">{{dtime}}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bar-wrap"
|
||||||
|
ref="playBarWrap"
|
||||||
|
@mousedown="$event => handleMousedownPlayBar($event)"
|
||||||
|
@touchstart="$event => handleMousedownPlayBar($event)"
|
||||||
|
@mousemove="$event => handleMousemovePlayBar($event)"
|
||||||
|
@mouseenter="playBarTimeVisible = true"
|
||||||
|
@mouseleave="playBarTimeVisible = false"
|
||||||
|
>
|
||||||
|
<div class="bar-time" :class="{ 'hidden': !playBarTimeVisible }" :style="{ left: playBarTimeLeft }">{{playBarTime}}</div>
|
||||||
|
<div class="bar">
|
||||||
|
<div class="loaded" :style="{ width: loadedBarWidth }"></div>
|
||||||
|
<div class="played" :style="{ width: playedBarWidth }">
|
||||||
|
<span class="thumb"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const secondToTime = (second = 0) => {
|
||||||
|
if (second === 0 || isNaN(second)) return '00:00'
|
||||||
|
|
||||||
|
const add0 = (num: number) => (num < 10 ? '0' + num : '' + num)
|
||||||
|
const hour = Math.floor(second / 3600)
|
||||||
|
const min = Math.floor((second - hour * 3600) / 60)
|
||||||
|
const sec = Math.floor(second - hour * 3600 - min * 60)
|
||||||
|
return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBoundingClientRectViewLeft = (element: HTMLElement) => {
|
||||||
|
return element.getBoundingClientRect().left
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'audio-player',
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
loop: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
autoplay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const audioRef = ref<HTMLAudioElement>()
|
||||||
|
const playBarWrap = ref<HTMLElement>()
|
||||||
|
const volumeBarRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const volume = ref(0.5)
|
||||||
|
const paused = ref(true)
|
||||||
|
const currentTime = ref(0)
|
||||||
|
const duration = ref(0)
|
||||||
|
const loaded = ref(0)
|
||||||
|
|
||||||
|
const playBarTimeVisible = ref(false)
|
||||||
|
const playBarTime = ref('00:00')
|
||||||
|
const playBarTimeLeft = ref('0')
|
||||||
|
|
||||||
|
const ptime = computed(() => secondToTime(currentTime.value))
|
||||||
|
const dtime = computed(() => secondToTime(duration.value))
|
||||||
|
const playedBarWidth = computed(() => currentTime.value / duration.value * 100 + '%')
|
||||||
|
const loadedBarWidth = computed(() => loaded.value / duration.value * 100 + '%')
|
||||||
|
const volumeBarWidth = computed(() => volume.value * 100 + '%')
|
||||||
|
|
||||||
|
const seek = (time: number) => {
|
||||||
|
if (!audioRef.value) return
|
||||||
|
|
||||||
|
time = Math.max(time, 0)
|
||||||
|
time = Math.min(time, duration.value)
|
||||||
|
|
||||||
|
audioRef.value.currentTime = time
|
||||||
|
currentTime.value = time
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
if (!audioRef.value) return
|
||||||
|
|
||||||
|
paused.value = false
|
||||||
|
audioRef.value.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
|
if (!audioRef.value) return
|
||||||
|
|
||||||
|
paused.value = true
|
||||||
|
audioRef.value.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (paused.value) play()
|
||||||
|
else pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setVolume = (percentage: number) => {
|
||||||
|
if (!audioRef.value) return
|
||||||
|
|
||||||
|
percentage = Math.max(percentage, 0)
|
||||||
|
percentage = Math.min(percentage, 1)
|
||||||
|
|
||||||
|
audioRef.value.volume = percentage
|
||||||
|
volume.value = percentage
|
||||||
|
if (audioRef.value.muted && percentage !== 0) audioRef.value.muted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDurationchange = () => {
|
||||||
|
duration.value = audioRef.value?.duration || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTimeupdate = () => {
|
||||||
|
currentTime.value = audioRef.value?.currentTime || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlayed = () => {
|
||||||
|
paused.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
if (!props.loop) pause()
|
||||||
|
else {
|
||||||
|
seek(0)
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProgress = () => {
|
||||||
|
loaded.value = audioRef.value?.buffered.length ? audioRef.value.buffered.end(audioRef.value.buffered.length - 1) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = () => message.error('视频加载失败')
|
||||||
|
|
||||||
|
const thumbMove = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!audioRef.value || !playBarWrap.value) return
|
||||||
|
const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX
|
||||||
|
let percentage = (clientX - getBoundingClientRectViewLeft(playBarWrap.value)) / playBarWrap.value.clientWidth
|
||||||
|
percentage = Math.max(percentage, 0)
|
||||||
|
percentage = Math.min(percentage, 1)
|
||||||
|
const time = percentage * duration.value
|
||||||
|
|
||||||
|
audioRef.value.currentTime = time
|
||||||
|
currentTime.value = time
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbUp = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!audioRef.value || !playBarWrap.value) return
|
||||||
|
|
||||||
|
const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX
|
||||||
|
let percentage = (clientX - getBoundingClientRectViewLeft(playBarWrap.value)) / playBarWrap.value.clientWidth
|
||||||
|
percentage = Math.max(percentage, 0)
|
||||||
|
percentage = Math.min(percentage, 1)
|
||||||
|
const time = percentage * duration.value
|
||||||
|
|
||||||
|
audioRef.value.currentTime = time
|
||||||
|
currentTime.value = time
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', thumbMove)
|
||||||
|
document.removeEventListener('touchmove', thumbMove)
|
||||||
|
document.removeEventListener('mouseup', thumbUp)
|
||||||
|
document.removeEventListener('touchend', thumbUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMousedownPlayBar = () => {
|
||||||
|
document.addEventListener('mousemove', thumbMove)
|
||||||
|
document.addEventListener('touchmove', thumbMove)
|
||||||
|
document.addEventListener('mouseup', thumbUp)
|
||||||
|
document.addEventListener('touchend', thumbUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeMove = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!volumeBarRef.value) return
|
||||||
|
const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX
|
||||||
|
const percentage = (clientX - getBoundingClientRectViewLeft(volumeBarRef.value) - 5.5) / 35
|
||||||
|
setVolume(percentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeUp = () => {
|
||||||
|
document.removeEventListener('mousemove', volumeMove)
|
||||||
|
document.removeEventListener('touchmove', volumeMove)
|
||||||
|
document.removeEventListener('mouseup', volumeUp)
|
||||||
|
document.removeEventListener('touchend', volumeUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMousedownVolumeBar = () => {
|
||||||
|
document.addEventListener('mousemove', volumeMove)
|
||||||
|
document.addEventListener('touchmove', volumeMove)
|
||||||
|
document.addEventListener('mouseup', volumeUp)
|
||||||
|
document.addEventListener('touchend', volumeUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickVolumeBar = (e: MouseEvent) => {
|
||||||
|
if (!volumeBarRef.value) return
|
||||||
|
const percentage = (e.clientX - getBoundingClientRectViewLeft(volumeBarRef.value) - 5.5) / 35
|
||||||
|
setVolume(percentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMousemovePlayBar = (e: MouseEvent) => {
|
||||||
|
if (duration.value && playBarWrap.value) {
|
||||||
|
const px = playBarWrap.value.getBoundingClientRect().left
|
||||||
|
const tx = e.clientX - px
|
||||||
|
if (tx < 0 || tx > playBarWrap.value.offsetWidth) return
|
||||||
|
|
||||||
|
const time = duration.value * (tx / playBarWrap.value.offsetWidth)
|
||||||
|
playBarTimeLeft.value = `${tx - (time >= 3600 ? 25 : 20)}px`
|
||||||
|
playBarTime.value = secondToTime(time)
|
||||||
|
playBarTimeVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleVolume = () => {
|
||||||
|
if (!audioRef.value) return
|
||||||
|
|
||||||
|
if (audioRef.value.muted) {
|
||||||
|
audioRef.value.muted = false
|
||||||
|
setVolume(0.5)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
audioRef.value.muted = true
|
||||||
|
setVolume(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
audioRef,
|
||||||
|
playBarWrap,
|
||||||
|
volumeBarRef,
|
||||||
|
volume,
|
||||||
|
paused,
|
||||||
|
ptime,
|
||||||
|
dtime,
|
||||||
|
playBarTime,
|
||||||
|
playBarTimeVisible,
|
||||||
|
playBarTimeLeft,
|
||||||
|
playedBarWidth,
|
||||||
|
loadedBarWidth,
|
||||||
|
volumeBarWidth,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
toggle,
|
||||||
|
setVolume,
|
||||||
|
handleDurationchange,
|
||||||
|
handleTimeupdate,
|
||||||
|
handlePlayed,
|
||||||
|
handleEnded,
|
||||||
|
handleProgress,
|
||||||
|
handleError,
|
||||||
|
handleMousedownPlayBar,
|
||||||
|
handleMousedownVolumeBar,
|
||||||
|
handleClickVolumeBar,
|
||||||
|
handleMousemovePlayBar,
|
||||||
|
toggleVolume,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
width: 280px;
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
line-height: 1;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controller {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 41px;
|
||||||
|
padding: 0 20px;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
.bar-wrap {
|
||||||
|
padding: 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 35px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: 3px;
|
||||||
|
|
||||||
|
&:hover .bar .played .thumb {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-time {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: -20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 7px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.62);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.1s ease-in-out;
|
||||||
|
word-wrap: normal;
|
||||||
|
word-break: normal;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
position: relative;
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.loaded {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
height: 3px;
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
will-change: width;
|
||||||
|
}
|
||||||
|
.played {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 3px;
|
||||||
|
will-change: width;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 5px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-right: -10px;
|
||||||
|
height: 11px;
|
||||||
|
width: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
transform: scale(0);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icons {
|
||||||
|
height: 38px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
&.play-icon {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-content {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active .icon-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:hover .icon-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.volume {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.volume-bar-wrap .volume-bar {
|
||||||
|
width: 45px;
|
||||||
|
}
|
||||||
|
.volume-bar-wrap .volume-bar .volume-bar-inner .thumb {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.volume-active {
|
||||||
|
.volume-bar-wrap .volume-bar {
|
||||||
|
width: 45px;
|
||||||
|
}
|
||||||
|
.volume-bar-wrap .volume-bar .volume-bar-inner .thumb {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.volume-bar-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 15px 0 -5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.volume-bar {
|
||||||
|
position: relative;
|
||||||
|
top: 17px;
|
||||||
|
width: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: #aaa;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
.volume-bar-inner {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
will-change: width;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 5px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-right: -10px;
|
||||||
|
height: 11px;
|
||||||
|
width: 11px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
transform: scale(0);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
height: 38px;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 38px;
|
||||||
|
color: #eee;
|
||||||
|
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
.ptime {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
.dtime {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="base-element-audio"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top + 'px',
|
||||||
|
left: elementInfo.left + 'px',
|
||||||
|
width: elementInfo.width + 'px',
|
||||||
|
height: elementInfo.height + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rotate-wrapper"
|
||||||
|
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
|
||||||
|
>
|
||||||
|
<div class="element-content">
|
||||||
|
<IconVolumeNotice
|
||||||
|
class="audio-icon"
|
||||||
|
:style="{
|
||||||
|
fontSize: audioIconSize,
|
||||||
|
color: elementInfo.color,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { PPTAudioElement } from '@/types/slides'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'base-element-audio',
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTAudioElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const audioIconSize = computed(() => {
|
||||||
|
return Math.min(props.elementInfo.width, props.elementInfo.height) + 'px'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
audioIconSize,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.base-element-audio {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.rotate-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.element-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.audio-icon {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
</style>
|
139
src/views/components/element/AudioElement/ScreenAudioElement.vue
Normal file
139
src/views/components/element/AudioElement/ScreenAudioElement.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="screen-element-audio"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top + 'px',
|
||||||
|
left: elementInfo.left + 'px',
|
||||||
|
width: elementInfo.width + 'px',
|
||||||
|
height: elementInfo.height + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rotate-wrapper"
|
||||||
|
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
|
||||||
|
>
|
||||||
|
<div class="element-content">
|
||||||
|
<IconVolumeNotice
|
||||||
|
class="audio-icon"
|
||||||
|
:style="{
|
||||||
|
fontSize: audioIconSize,
|
||||||
|
color: elementInfo.color,
|
||||||
|
}"
|
||||||
|
@click="toggle()"
|
||||||
|
/>
|
||||||
|
<AudioPlayer
|
||||||
|
class="audio-player"
|
||||||
|
ref="audioPlayerRef"
|
||||||
|
v-if="inCurrentSlide"
|
||||||
|
:style="{ ...audioPlayerPosition }"
|
||||||
|
:src="elementInfo.src"
|
||||||
|
:loop="elementInfo.loop"
|
||||||
|
:autoplay="elementInfo.autoplay"
|
||||||
|
:scale="scale"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, inject, PropType, ref, Ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
|
import { PPTAudioElement } from '@/types/slides'
|
||||||
|
import { VIEWPORT_SIZE } from '@/configs/canvas'
|
||||||
|
|
||||||
|
import AudioPlayer from './AudioPlayer.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'screen-element-audio',
|
||||||
|
components: {
|
||||||
|
AudioPlayer,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTAudioElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
|
||||||
|
|
||||||
|
const scale: Ref<number> = inject('slideScale') || ref(1)
|
||||||
|
const slideId: Ref<string> = inject('slideId') || ref('')
|
||||||
|
|
||||||
|
const inCurrentSlide = computed(() => currentSlide.value.id === slideId.value)
|
||||||
|
|
||||||
|
const audioIconSize = computed(() => {
|
||||||
|
return Math.min(props.elementInfo.width, props.elementInfo.height) + 'px'
|
||||||
|
})
|
||||||
|
const audioPlayerPosition = computed(() => {
|
||||||
|
const canvasWidth = VIEWPORT_SIZE
|
||||||
|
const canvasHeight = VIEWPORT_SIZE * viewportRatio.value
|
||||||
|
|
||||||
|
const audioWidth = 280 / scale.value
|
||||||
|
const audioHeight = 50 / scale.value
|
||||||
|
|
||||||
|
const elWidth = props.elementInfo.width
|
||||||
|
const elHeight = props.elementInfo.height
|
||||||
|
const elLeft = props.elementInfo.left
|
||||||
|
const elTop = props.elementInfo.top
|
||||||
|
|
||||||
|
let left = 0
|
||||||
|
let top = elHeight
|
||||||
|
|
||||||
|
if (elLeft + audioWidth >= canvasWidth) left = elWidth - audioWidth
|
||||||
|
if (elTop + elHeight + audioHeight >= canvasHeight) top = -audioHeight
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + 'px',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const audioPlayerRef = ref()
|
||||||
|
const toggle = () => {
|
||||||
|
if (!audioPlayerRef.value) return
|
||||||
|
audioPlayerRef.value.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scale,
|
||||||
|
inCurrentSlide,
|
||||||
|
audioIconSize,
|
||||||
|
audioPlayerPosition,
|
||||||
|
audioPlayerRef,
|
||||||
|
toggle,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.screen-element-audio {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.rotate-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.element-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.audio-player {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.audio-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.audio-player {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
143
src/views/components/element/AudioElement/index.vue
Normal file
143
src/views/components/element/AudioElement/index.vue
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="editable-element-audio"
|
||||||
|
:class="{ 'lock': elementInfo.lock }"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top + 'px',
|
||||||
|
left: elementInfo.left + 'px',
|
||||||
|
width: elementInfo.width + 'px',
|
||||||
|
height: elementInfo.height + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rotate-wrapper"
|
||||||
|
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="element-content"
|
||||||
|
v-contextmenu="contextmenus"
|
||||||
|
@mousedown="$event => handleSelectElement($event)"
|
||||||
|
>
|
||||||
|
<IconVolumeNotice
|
||||||
|
class="audio-icon"
|
||||||
|
:style="{
|
||||||
|
fontSize: audioIconSize,
|
||||||
|
color: elementInfo.color,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<AudioPlayer
|
||||||
|
class="audio-player"
|
||||||
|
v-if="handleElementId === elementInfo.id"
|
||||||
|
:style="{ ...audioPlayerPosition }"
|
||||||
|
:src="elementInfo.src"
|
||||||
|
:loop="elementInfo.loop"
|
||||||
|
:scale="canvasScale"
|
||||||
|
@mousedown.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
|
import { PPTAudioElement } from '@/types/slides'
|
||||||
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
import { VIEWPORT_SIZE } from '@/configs/canvas'
|
||||||
|
|
||||||
|
import AudioPlayer from './AudioPlayer.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'editable-element-audio',
|
||||||
|
components: {
|
||||||
|
AudioPlayer,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTAudioElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTAudioElement, canMove?: boolean) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
contextmenus: {
|
||||||
|
type: Function as PropType<() => ContextmenuItem[]>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { canvasScale, handleElementId } = storeToRefs(useMainStore())
|
||||||
|
const { viewportRatio } = storeToRefs(useSlidesStore())
|
||||||
|
|
||||||
|
const audioIconSize = computed(() => {
|
||||||
|
return Math.min(props.elementInfo.width, props.elementInfo.height) + 'px'
|
||||||
|
})
|
||||||
|
const audioPlayerPosition = computed(() => {
|
||||||
|
const canvasWidth = VIEWPORT_SIZE
|
||||||
|
const canvasHeight = VIEWPORT_SIZE * viewportRatio.value
|
||||||
|
|
||||||
|
const audioWidth = 280 / canvasScale.value
|
||||||
|
const audioHeight = 50 / canvasScale.value
|
||||||
|
|
||||||
|
const elWidth = props.elementInfo.width
|
||||||
|
const elHeight = props.elementInfo.height
|
||||||
|
const elLeft = props.elementInfo.left
|
||||||
|
const elTop = props.elementInfo.top
|
||||||
|
|
||||||
|
let left = 0
|
||||||
|
let top = elHeight
|
||||||
|
|
||||||
|
if (elLeft + audioWidth >= canvasWidth) left = elWidth - audioWidth
|
||||||
|
if (elTop + elHeight + audioHeight >= canvasHeight) top = -audioHeight
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + 'px',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelectElement = (e: MouseEvent) => {
|
||||||
|
if (props.elementInfo.lock) return
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
props.selectElement(e, props.elementInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canvasScale,
|
||||||
|
handleElementId,
|
||||||
|
audioIconSize,
|
||||||
|
audioPlayerPosition,
|
||||||
|
handleSelectElement,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editable-element-audio {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&.lock .audio-icon {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rotate-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.element-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.audio-icon {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.audio-player {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
@ -13,6 +13,7 @@
|
|||||||
>
|
>
|
||||||
<div class="element-content">
|
<div class="element-content">
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
|
v-if="inCurrentSlide"
|
||||||
:width="elementInfo.width"
|
:width="elementInfo.width"
|
||||||
:height="elementInfo.height"
|
:height="elementInfo.height"
|
||||||
:src="elementInfo.src"
|
:src="elementInfo.src"
|
||||||
@ -25,7 +26,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, inject, PropType, Ref, ref } from 'vue'
|
import { computed, defineComponent, inject, PropType, Ref, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
import { PPTVideoElement } from '@/types/slides'
|
import { PPTVideoElement } from '@/types/slides'
|
||||||
|
|
||||||
import VideoPlayer from './VideoPlayer/index.vue'
|
import VideoPlayer from './VideoPlayer/index.vue'
|
||||||
@ -42,10 +45,16 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
const { currentSlide } = storeToRefs(useSlidesStore())
|
||||||
|
|
||||||
const scale: Ref<number> = inject('slideScale') || ref(1)
|
const scale: Ref<number> = inject('slideScale') || ref(1)
|
||||||
|
const slideId: Ref<string> = inject('slideId') || ref('')
|
||||||
|
|
||||||
|
const inCurrentSlide = computed(() => currentSlide.value.id === slideId.value)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scale,
|
scale,
|
||||||
|
inCurrentSlide,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="player"
|
class="video-player"
|
||||||
:class="{ 'hide-controller': hideController }"
|
:class="{ 'hide-controller': hideController }"
|
||||||
:style="{
|
:style="{
|
||||||
width: width * scale + 'px',
|
width: width * scale + 'px',
|
||||||
height: height * scale + 'px',
|
height: height * scale + 'px',
|
||||||
transform: `scale(${1 / scale})`,
|
transform: `scale(${1 / scale})`,
|
||||||
}"
|
}"
|
||||||
ref="containerRef"
|
|
||||||
@mousemove="autoHideController()"
|
@mousemove="autoHideController()"
|
||||||
@click="autoHideController()"
|
@click="autoHideController()"
|
||||||
>
|
>
|
||||||
@ -133,7 +132,7 @@ const getBoundingClientRectViewLeft = (element: HTMLElement) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Player',
|
name: 'video-player',
|
||||||
props: {
|
props: {
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@ -157,7 +156,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const containerRef = ref<HTMLElement>()
|
|
||||||
const videoRef = ref<HTMLVideoElement>()
|
const videoRef = ref<HTMLVideoElement>()
|
||||||
const playBarWrap = ref<HTMLElement>()
|
const playBarWrap = ref<HTMLElement>()
|
||||||
const volumeBarRef = ref<HTMLElement>()
|
const volumeBarRef = ref<HTMLElement>()
|
||||||
@ -367,7 +365,6 @@ export default defineComponent({
|
|||||||
useMSE(props.src, videoRef)
|
useMSE(props.src, videoRef)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containerRef,
|
|
||||||
videoRef,
|
videoRef,
|
||||||
playBarWrap,
|
playBarWrap,
|
||||||
volumeBarRef,
|
volumeBarRef,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
// player
|
.video-player {
|
||||||
.player {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user