Vue3+Video.js实现视频选择互动

功能需求:实现视频交互性格测试,根据用户的选择展示对应的性格

整体思路:在视频上方嵌套遮罩层,视频结束展示遮罩层用户点击选项记录选项,播放下一条视频,当播放最后一条带有选择项的视频,用户选择完后根据用户的选择进行展示不同的图片

主页面 index.js

  • 此页面引入了视频组件,问题列表组件
  • 判断初始状态展示开始测试
  • 判断结束状态展示性格图片
<template>
    <div class="video-container" ref="parent">
        <div class="start" @click="getVideo" v-if="showStart">开始测试</div>
        <VideoComp ref="player" @end="onEnd"/>
        <QuestionDialog  v-model:show="showQuestion" :stage="stage" @video-start="onVideoStart" :question-list="questionList" />
        <div class="img" v-if="stage === 13">
            <img :src="require(`@/assets/character/${traitImg}.jpg`)" alt="" class="arrow">
        </div>
    </div>
</template>

<script setup>

import { onMounted, ref, computed } from 'vue'
import VideoComp from './components/Video'
import QuestionDialog from './components/QuestionDialog'
import { flexible, handlerRem, handleRemResize } from './flexible'

import {videoQuestionList} from './components/data/video'

const parent = ref()
onMounted(() => {
    handlerRem()
    window.addEventListener('resize', handleRemResize)
    flexible({
      mode: 'landscape'
    })
})
// 定义视频组件
const player = ref()
// 展示选项弹窗
const showQuestion = ref(false)
// 当前第几阶段, 值为0、1、2、3、4其中之一。0表示是在“开视频”阶段
const stage = ref(0)

// 视频播放结束,开始答题
function onEnd() {
    // 没题目,直接跳到下一阶段
    if (Object.keys(questionList.value).length === 0) {
        stage.value += 1
        console.log('没有题目,结束测试展示结果')
    } else {
        showQuestion.value = true
    }
}
// 获取题目
const soureObj = videoQuestionList
const questionList = computed(() => soureObj[stage.value]?.question || {})
const showStart = ref(true)

// 获取视频
function getVideo() {
    showStart.value = false
    handleStep()
}
// 播放视频
function handleStep() {
    player.value.changeUrl(soureObj[stage.value].url)
    player.value.play()
}

// 最高得分的性格
const traitImg = ref('')
// 性格得分
let traitScores = ref({});

// 选择视频选项
function onVideoStart(item) {
    // 定义一个空对象来存储性格得分
    const selectedTraits = item.scores;
    // 将性格得分添加到 traitScores 对象中
    for (let trait in selectedTraits) {
        if (traitScores[trait]) {
            traitScores[trait] += selectedTraits[trait];
        } else {
            traitScores[trait] = selectedTraits[trait];
        }
    }
    // 最后一条带有选项的视频,展示最高得分的性格
    if (stage.value >= 11) {
        const highestTrait = getHighestTrait();
        if (highestTrait) {
            console.log(`最高得分的性格是:${highestTrait}`);
            // 在此处添加显示性格相关图片或信息的逻辑
            traitImg.value = highestTrait
            stage.value += 1
            showQuestion.value = false
            handleStep()
        } else {
            console.log("没有性格得分");
        }
    }else{
        // 进入下一阶段
        stage.value += 1
        showQuestion.value = false
        handleStep()
    }
}

// 获取最高得分的性格
function getHighestTrait() {
    let maxTrait = null;
    let maxScore = 0;
    for (let trait in traitScores) {
        if (traitScores[trait] > maxScore) {
            maxScore = traitScores[trait];
            maxTrait = trait;
        }
    }
    return maxTrait;
}

</script>

<style lang="less" scoped>
.start {
    background: #fff;
    position: absolute;
    z-index: 100;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.video-container {
    width: 100%;
    height: 100%;
    position: relative;
}
.img{
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
    display: flex;
    justify-content: center; /* 水平居中 */
    align-items: center; /* 垂直居中 */
    height: 100%;
    img{
        max-width: 100%;
        max-height: 100%;
        object-fit: contain; /* 保持图片比例 */
    }
}
</style>

问题列表数据video.js

let videoQuestionList = [
    {
        // "stage": 0,
        "url": require('@/assets/video/01.mp4'),
        "question": {
            "title": "你的年龄是?",
            "questionLocation": "center",
            "options": [
                {
                    "option": "A",
                    "description": "18岁以下",
                    "scores": {}
                },
                {
                    "option": "B",
                    "description": "18岁-35岁",
                    "scores": {}
                },
                {
                    "option": "C",
                    "description": "35岁-45岁",
                    "scores": {}
                },
                {
                    "option": "D",
                    "description": "45岁以上",
                    "scores": {}
                }
            ]
        }
    },
    {
        // // "stage": 1,
        "url": require('@/assets/video/02.mp4'),
        "question": {
            "title": "你的性别是?",
            "questionLocation": "center",
            "options": [
                {
                    "option": "A",
                    "description": "男生",
                    "scores": {}
                },
                {
                    "option": "B",
                    "description": "女生",
                    "scores": {}
                }
            ]
        }
    },
    {
        // "stage": 2,
        "url": require('@/assets/video/03.mp4'),
        "question": {
            "title": "在团队项目中,你更倾向于:",
            "questionLocation": "right",
            "options": [
                {
                    "option": "A",
                    "description": "主动与团队成员交流,分享想法",
                    "scores": {
                        "外向": 4,
                        "友好": 4,
                        "合作型": 4
                    }
                },
                {
                    "option": "B",
                    "description": "默默工作,独自完成任务",
                    "scores": {
                        "内敛": 4,
                        "独立": 4,
                        "保守": 2
                    }
                },
                {
                    "option": "C",
                    "description": "竞争成为团队中的佼佼者",
                    "scores": {
                        "竞争型": 4,
                        "个人成长": 3,
                        "勇敢": 2
                    }
                },
                {
                    "option": "D",
                    "description": "乐于协助他人,确保团队整体成功",
                    "scores": {
                        "付出型": 4,
                        "合作型": 4,
                        "关心他人": 2
                    }
                }
            ]
        }
    },
    {
        // "stage": 3,
        "url": require('@/assets/video/04.mp4'),
        "question": {
            "title": "面对新同事,你的态度是:",
            "questionLocation": "left",
            "options": [
                {
                    "option": "A",
                    "description": "热情打招呼,主动介绍自己",
                    "scores": {
                        "外向": 4,
                        "友好": 4
                    }
                },
                {
                    "option": "B",
                    "description": "保持距离,先观察再行动",
                    "scores": {
                        "内敛": 4,
                        "适应性强": 3
                    }
                },
                {
                    "option": "C",
                    "description": "立即寻找共同话题,建立友好关系",
                    "scores": {
                        "友好": 4
                    }
                },
                {
                    "option": "D",
                    "description": "不太关心,专注于自己的工作",
                    "scores": {
                        "冷漠": 4,
                        "个人成长": 2
                    }
                }
            ]
        }
    },
    {
        // "stage": 4,
        "url": require('@/assets/video/05.mp4'),
        "question": {
            "title": "在工作中遇到难题时,你会:",
            "questionLocation": "right",
            "options": [
                {
                    "option": "A",
                    "description": "勇敢面对,寻找解决方案",
                    "scores": {
                        "勇敢": 4,
                        "发展导向": 4
                    }
                },
                {
                    "option": "B",
                    "description": "放松心态,相信问题总会解决",
                    "scores": {
                        "放松": 4,
                        "适应性强": 3
                    }
                },
                {
                    "option": "C",
                    "description": "保守处理,避免冒险",
                    "scores": {
                        "保守": 4
                    }
                },
                {
                    "option": "D",
                    "description": "寻求他人帮助,共同解决",
                    "scores": {
                        "乐于助人": 3,
                        "合作型": 4
                    }
                }
            ]
        }
    },
    {
        // "stage": 5,
        "url": require('@/assets/video/06.mp4'),
        "question": {
            "title": "对于新技能或知识的学习,你的态度是:",
            "questionLocation": "left",
            "options": [
                {
                    "option": "A",
                    "description": "充满热情,积极学习",
                    "scores": {
                        "发展导向": 4
                    }
                },
                {
                    "option": "B",
                    "description": "根据需要学习,不主动拓展",
                    "scores": {
                        "适应性强": 3,
                        "保守": 3
                    }
                },
                {
                    "option": "C",
                    "description": "认为这不是自己的职责范围",
                    "scores": {
                        "冷漠": 4,
                        "个人成长": 1
                    }
                },
                {
                    "option": "D",
                    "description": "乐于分享自己的知识和经验",
                    "scores": {
                        "乐于助人": 4,
                        "合作型": 3
                    }
                }
            ]
        }
    },
    {
        // "stage": 6,
        "url": require('@/assets/video/07.mp4'),
        "question": {
            "title": "在工作中遇到难题时,你会:",
            "questionLocation": "left",
            "options": [
                {
                    "option": "A",
                    "description": "提出创新观点,引领讨论",
                    "scores": {
                        "创新思维": 4,
                        "发展导向": 4
                    }
                },
                {
                    "option": "B",
                    "description": "倾听他人意见,避免冲突",
                    "scores": {
                        "友好": 4,
                        "合作型": 4
                    }
                },
                {
                    "option": "C",
                    "description": "坚持自己的观点,说服他人",
                    "scores": {
                        "竞争型": 4,
                        "个人成长": 3
                    }
                },
                {
                    "option": "D",
                    "description": "不发表意见,保持沉默",
                    "scores": {
                        "内敛": 4,
                        "冷漠": 2
                    }
                }
            ]
        }
    },
    {
        // "stage": 7,
        "url": require('@/assets/video/08.mp4'),
        "question": {
            "title": "对于社会责任,你的看法是:",
            "questionLocation": "right",
            "options": [
                {
                    "option": "A",
                    "description": "每个人都应该承担社会责任",
                    "scores": {
                        "社会责任": 4,
                        "关心他人": 4
                    }
                },
                {
                    "option": "B",
                    "description": "只在能力范围内承担",
                    "scores": {
                        "适应性强": 3,
                        "保守": 3
                    }
                },
                {
                    "option": "C",
                    "description": "这不是我关心的事情",
                    "scores": {
                        "冷漠": 4,
                        "个人成长": 1
                    }
                },
                {
                    "option": "D",
                    "description": "通过工作实现社会价值",
                    "scores": {
                        "发展导向": 4,
                        "社会责任": 2
                    }
                }
            ]
        }
    },
    {
        // "stage": 8,
        "url": require('@/assets/video/09.mp4'),
        "question": {
            "title": "在面对工作压力时,你通常会:",
            "questionLocation": "left",
            "options": [
                {
                    "option": "A",
                    "description": "寻找放松方式,调整心态",
                    "scores": {
                        "放松": 4,
                        "适应性强": 4
                    }
                },
                {
                    "option": "B",
                    "description": "勇敢面对,积极解决",
                    "scores": {
                        "勇敢": 4,
                        "发展导向": 4
                    }
                },
                {
                    "option": "C",
                    "description": "保守处理,避免冒险",
                    "scores": {
                        "保守": 4
                    }
                },
                {
                    "option": "D",
                    "description": "寻求团队支持,共同应对",
                    "scores": {
                        "合作型": 4,
                        "团队合作": 4
                    }
                }
            ]
        }
    },
    {
        // "stage": 9,
        "url": require('@/assets/video/10.mp4'),
        "question": {
            "title": "对于文艺活动,你的态度是:",
            "questionLocation": "right",
            "options": [
                {
                    "option": "A",
                    "description": "热爱参与,享受其中",
                    "scores": {
                        "文艺爱好者": 4,
                        "友好": 3
                    }
                },
                {
                    "option": "B",
                    "description": "有时参与,视情况而定",
                    "scores": {
                        "适应性强": 3,
                        "内敛": 3
                    }
                },
                {
                    "option": "C",
                    "description": "不太感兴趣,更注重实际工作",
                    "scores": {
                        "个人成长": 3,
                        "发展导向": 4
                    }
                },
                {
                    "option": "D",
                    "description": "认为这与职业发展无关",
                    "scores": {
                        "冷漠": 4,
                        "保守": 2
                    }
                }
            ]
        }
    },
    {
        // "stage": 10,
        "url": require('@/assets/video/11.mp4'),
        "question": {
            "title": "在解决问题时,你更倾向于:",
            "questionLocation": "left",
            "options": [
                {
                    "option": "A",
                    "description": "创新方法,寻求突破",
                    "scores": {
                        "创新思维": 4,
                        "发展导向": 4
                    }
                },
                {
                    "option": "B",
                    "description": "沿用旧方法,确保稳定",
                    "scores": {
                        "保守": 4
                    }
                },
                {
                    "option": "C",
                    "description": "寻求他人帮助,共同解决",
                    "scores": {
                        "合作型": 4,
                        "付出型": 3
                    }
                },
                {
                    "option": "D",
                    "description": "独自思考,找出答案",
                    "scores": {
                        "独立": 4,
                        "个人成长": 3
                    }
                }
            ]
        }
    },
    {
        // "stage": 11,
        "url": require('@/assets/video/12.mp4'),
        "question": {
            "title": "对干新环境或新任务,你的适应能力是:",
            "questionLocation": "right",
            "options": [
                {
                    "option": "A",
                    "description": "迅速适应,积极面对",
                    "scores": {
                        "适应性强": 4,
                        "发展导向": 4
                    }
                },
                {
                    "option": "B",
                    "description": "需要一定时间适应",
                    "scores": {
                        "适应性强": 3,
                        "内敛": 3
                    }
                },
                {
                    "option": "C",
                    "description": "害怕改变,希望保持现状",
                    "scores": {
                        "保守": 4,
                        "冷漠": 2
                    }
                },
                {
                    "option": "D",
                    "description": "主动寻求挑战,促进成长",
                    "scores": {
                        "勇敢": 4,
                        "个人成长": 4
                    }
                }
            ]
        }
    },
    {
        // "stage": 12,
        "url": require('@/assets/video/13.mp4'),
        "question": {}
    }
]




export {
    videoQuestionList,
}

封装的video组件;定义了视频的开始,暂停,结束等方法

<template>

    <div class="video-content">
        <div v-show="showPlay" class="play-icon" @click="handlePlay">
            <svg xmlns="http://www.w3.org/2000/svg" class="play" width="28" height="40" viewBox="3 -4 28 40">
                <path fill="#fff" transform="scale(0.0320625 0.0320625)"
                    d="M576,363L810,512L576,661zM342,214L576,363L576,661L342,810z"></path>
            </svg>
        </div>
        <video class="video-js video-item" id="question-video" playsinline></video>
    </div>
</template>

<script setup>

import 'video.js/dist/video-js.css'
import videojs from 'video.js'
import { onMounted, defineExpose, defineEmits, ref, onUnmounted } from 'vue'

const emit = defineEmits(['end'])
let player = null
let supposedCurrentTime = 0
const showPlay = ref(false)

onMounted(() => {
    // console.log('video')
    player = videojs('question-video', {}, () => {
        // player.src('https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4')
        // player.src('https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/hls/xgplayer-demo.m3u8')
        // player.src('http://192.168.1.3:8080/video/c.m3u8')
        // player.play()
        player.on('timeupdate', () => {
            if (!player.seeking()) {
                supposedCurrentTime = player.currentTime()
            }
            // console.log('timeupdate', e, player)
        })
        player.on('seeking', () => {
            // console.error("Seeking is disabled")
            const delta = player.currentTime() - supposedCurrentTime
            if (Math.abs(delta) > 0.01) {
                player.currentTime(supposedCurrentTime)
            }
        })
        player.on('play', () => {
            // console.log('play', player)
            showPlay.value = false
        })
        player.on('pause', () => {
            // console.log('pause', player)
            showPlay.value = true
        })
        player.on('ended', () => {
            // console.log('ended')
            supposedCurrentTime = 0
            showPlay.value = false
            emit('end')
        })
        player.on('click', handleClick)
        player.on('touchstart', handleClick)
        player.on('error', () => {
            // console.log('123')
            showPlay.value = false
        })

        function handleClick() {
            // 暂停的时候点击了视频,那么要开始播放
            if (player.paused()) {
                player.play()
                showPlay.value = false
            } else {
                player.pause()
                showPlay.value = false
            }
        }
    })


})

onUnmounted(() => {
    if (player) {
        player.dispose()
        player = null
    }
})

function handlePlay() {
    showPlay.value = false
    player && player.play()
}

function play() {
    player && player.play()
    supposedCurrentTime = 0
    showPlay.value = false
}

function changeUrl(url) {
    // console.log('changeUrl',url)
    player && url && player.src(url)
}

function currentTime(time) {
    player && player.currentTime(time)
}

defineExpose({
    play,
    changeUrl,
    currentTime
})

</script>

<style lang="less" scoped>
.video-content {
    width: 100%;
    height: 100%;
    position: relative;
}

.play-icon {
    position: absolute;
    width: 50px;
    height: 50px;
    background-color: rgba(0, 0, 0, 0.5);
    border-radius: 100%;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
    margin: auto;
    font-size: 0;
    line-height: 50px;
    text-align: center;

    .play {
        width: 40px;
        height: 40px;
        vertical-align: middle;
    }
}

.video-item {
    width: 100%;
    height: 100%;
}
</style>

问题列表组件

  • 问题的展示效果有三种 ,所以根据不同的题目设置了不同的展示效果

居中靠下

右侧

左侧

<template>
    <div class="question-container" v-show="showDialog">
        <div class="question-bg">
            <!-- 选项中间布局-年龄-性别 -->
            <div class="option-container" v-if="questionList.questionLocation == 'center'">
                <div class="option" v-for="(item,index) in questionItem" :key="item.id">
                    <div class="option-text" @click="handleClick(item, index)">{{item.description}}</div>
                </div>
            </div>
            <!-- 题目 -->
            <div class='title-left'  v-if="questionList.questionLocation == 'left'"><span>{{questionList.title}}</span></div>
            <div class='title-right'  v-if="questionList.questionLocation == 'right'"><span>{{questionList.title}}</span></div>
            <!-- 选项右侧布局 -->
            <div class="option-container-right" v-if="questionList.questionLocation == 'right'">
                <div class="option-right" v-for="(item,index) in questionItem" :key="item.id">
                    <div class="option-text-right" @click="handleClick(item, index)">{{item.description}}</div>
                </div>
            </div>
            <!-- 选项左侧布局 -->
            <div class="option-container-left" v-if="questionList.questionLocation == 'left'">
                <div class="option-left" v-for="(item,index) in questionItem" :key="item.id">
                    <div class="option-text-left" @click="handleClick(item, index)">{{item.description}}</div>
                </div>
            </div>

        </div>
    </div>
</template>

<script setup>

import { ref, onUnmounted, defineProps, computed, defineEmits, watch} from 'vue'
// import { ref, onUnmounted, defineProps, computed, defineEmits, watch, nextTick} from 'vue'
// import { saveViewingRecords } from '@/api/video'

const props = defineProps({
    questionList: {
        type: Object,
        required: true
    },
    stage: {
        type: Number,
        required: true
    },
    show: {
        type: Boolean,
        default: false
    },
})

const emit = defineEmits(['update:modelValue', 'update:index', 'next', 'video-start'])

watch(() => props.show, () => {
    // console.log(props.index, props)
})

const showDialog = computed({
    get() {
        return props.show
    },
    set(flag) {
        emit('update:show', flag)
    }
})


const questionItem = computed(() => props.questionList.options || {})

// 当前点击的索引
// const currentIndex = ref(-1)
let timer = null
onUnmounted(() => {
    if (timer) {
        clearTimeout(timer)
        timer = null
    }
})

// 记录选中选项
const checkedOptions = ref([])

function handleClick(item, index) {
    // emit("video-start");
    checkedOptions.value.push(item)
    emit("video-start",item);
    console.log('选中选项',checkedOptions.value,item,index)
}


</script>

<style lang="less" scoped>
.question-container {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0);
    z-index: 100;
    .question-bg {
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        margin: auto;
        width: 88.3978%;
        height: 86.9333%;
        box-sizing: border-box;
        padding: 32px 42px 19px;
        .option-container {
            width: 100%;
            display: flex;
            justify-content: space-evenly;
            flex-wrap: wrap;
            position: absolute;
            bottom: 0;
            left: 0;
            .option {
                position: relative;
                margin-top: 20px;
                width: 180px;
                height: 50px;
                line-height: 50px;
                border-radius: 10px;
                border: 1px solid #000;
                background: #FFF;
                flex-basis: 35%;
                .option-text {
                    font-size: 24px;
                    font-weight: bold;
                }
            }
        }
            .title-left {
                width: 80%;
                left: 5%;
                position: absolute;
                text-align: left;
                span{
                    font-size: 20px;
                    font-weight: bold;
                    line-height: 20px;
                    background: #FFF;
                    padding: 5px 30px;
                    border-radius: 20px;
                    border: 1px solid #000;
                }
            }
            .title-right {
                width: 80%;
                right: 5%;
                position: absolute;
                text-align: right;
                span{
                    font-size: 20px;
                    font-weight: bold;
                    line-height: 20px;
                    background: #FFF;
                    padding: 5px 30px;
                    border-radius: 20px;
                    border: 1px solid #000;
                }
            }
        .option-container-right {
            width: 50%;
            position: absolute;
            bottom: 0;
            right: 5%;
            .option-right {
                position: relative;
                margin-top: 20px;
                height: 40px;
                line-height: 40px;
                border-radius: 10px;
                border: 1px solid #000;
                background: #FFF;
                flex-basis: 50%;
                .option-text-right {
                    font-size: 16px;
                    font-weight: bold;
                    text-align: center;
                }
            }
        }

        .option-container-left {
            width: 50%;
            position: absolute;
            bottom: 0;
            left: 5%;
            .option-left {
                position: relative;
                margin-top: 20px;
                height: 40px;
                line-height: 40px;
                border-radius: 10px;
                border: 1px solid #000;
                background: #FFF;
                flex-basis: 50%;
                .option-text-left {
                    font-size: 16px;
                    font-weight: bold;
                    text-align: center;
                }
            }
        }
    }
}
</style>

另外创建了一个匹配设备的js文件;将视频横向展示

const defaultConfig = {
  pageWidth: 375,
  pageHeight: 724,
  pageFontSize: 75,
  mode: 'portrait' // 默认竖屏模式
}

const flexible = (config = defaultConfig) => {
  const {
    pageWidth = defaultConfig.pageWidth,
    pageHeight = defaultConfig.pageHeight,
    pageFontSize = defaultConfig.pageFontSize,
    mode = defaultConfig.mode
  } = config
  const pageAspectRatio = defaultConfig.pageAspectRatio || (pageWidth / pageHeight)
  // 根据屏幕大小及dpi调整缩放和大小
  function onResize () {
    let clientWidth = document.documentElement.clientWidth
    let clientHeight = document.documentElement.clientHeight

    // 该页面需要强制横屏
    if (mode === 'landscape') {
      if (clientWidth < clientHeight) {
        [clientWidth, clientHeight] = [clientHeight, clientWidth]
      }
    }

    const aspectRatio = clientWidth / clientHeight

    // 根元素字体
    let e = 16
    if (clientWidth > pageWidth) {
      // 认为是ipad/pc
      console.log('认为是ipad/pc')
      e = pageFontSize * (clientHeight / pageHeight)
    } else if (aspectRatio > pageAspectRatio) {
      // 宽屏移动端
      console.log('宽屏移动端')
      e = pageFontSize * (clientHeight / pageHeight)
    } else {
      // 正常移动端
      console.log('正常移动端')
      e = pageFontSize * (clientWidth / pageWidth)
    }

    e = parseFloat(e.toFixed(3))

    document.documentElement.style.fontSize = `${e}px`
    const realitySize = parseFloat(window.getComputedStyle(document.documentElement).fontSize)
    if (e !== realitySize) {
      e = e * e / realitySize
      document.documentElement.style.fontSize = `${e}px`
    }
  }

  const handleResize = () => {
    onResize()
  }

  window.addEventListener('resize', handleResize)
  onResize()
  return (defaultSize) => {
    window.removeEventListener('resize', handleResize)
    if (defaultSize) {
      if (typeof defaultSize === 'string') {
        document.documentElement.style.fontSize = defaultSize
      } else if (typeof defaultSize === 'number') {
        document.documentElement.style.fontSize = `${defaultSize}px`
      }
    }
  }
}

const handlerRem = (id = '.video-container') => {
  const width = document.documentElement.clientWidth
  const height = document.documentElement.clientHeight
  const targetDom = document.querySelector(id)
  if (!targetDom) return

  // 如果宽度比高度大,则认为处于横屏状态
  // 也可以获取 window.orientation 方向来判断屏幕状态
  if (width > height) {
    targetDom.style.position = 'absolute'
    targetDom.style.width = `${width}px`
    targetDom.style.height = `${height}px`
    targetDom.style.left = `${0}px`
    targetDom.style.top = `${0}px`
    targetDom.style.transform = 'none'
    targetDom.style.transformOrigin = '50% 50%'
  } else {
    targetDom.style.position = 'absolute'
    targetDom.style.width = `${height}px`
    targetDom.style.height = `${width}px`
    targetDom.style.left = `${0 - (height - width) / 2}px`
    targetDom.style.top = `${(height - width) / 2}px`
    targetDom.style.transform = 'rotate(90deg)'
    targetDom.style.transformOrigin = '50% 50%'
  }
}

let timer = null
const handleRemResize = () => {
  if (timer) {
    clearTimeout(timer)
    timer = null
  }
  timer = setTimeout(() => {
    handlerRem()
  }, 100)
}

export {
  flexible,
  handlerRem,
  handleRemResize
}

后期更改,新增登录页,测评展示由图片更换为视频

登录页没什么说的

前端本地验证码自测插件

新建vue组件components/identify/identify.vue
<template>
  <div class="s-canvas">
    <canvas id="s-canvas" :width="contentWidth" :height="contentHeight"></canvas>
  </div>
</template>
<script>
export default {
  name: 'SIdentify',
  props: {
    identifyCode: {
      type: String,
      default: '1234'
    },
    fontSizeMin: {
      type: Number,
      default: 28
    },
    fontSizeMax: {
      type: Number,
      default: 40
    },
    backgroundColorMin: {
      type: Number,
      default: 180
    },
    backgroundColorMax: {
      type: Number,
      default: 240
    },
    colorMin: {
      type: Number,
      default: 50
    },
    colorMax: {
      type: Number,
      default: 160
    },
    lineColorMin: {
      type: Number,
      default: 40
    },
    lineColorMax: {
      type: Number,
      default: 180
    },
    dotColorMin: {
      type: Number,
      default: 0
    },
    dotColorMax: {
      type: Number,
      default: 255
    },
    contentWidth: {
      type: Number,
      default: 112
    },
    contentHeight: {
      type: Number,
      default: 40
    }
  },
  methods: {
    // 生成一个随机数
    randomNum (min, max) {
      return Math.floor(Math.random() * (max - min) + min)
    },
    // 生成一个随机的颜色
    randomColor (min, max) {
      var r = this.randomNum(min, max)
      var g = this.randomNum(min, max)
      var b = this.randomNum(min, max)
      return 'rgb(' + r + ',' + g + ',' + b + ')'
    },
    drawPic () {
      var canvas = document.getElementById('s-canvas')
      var ctx = canvas.getContext('2d')
      ctx.textBaseline = 'bottom'
      // 绘制背景
      ctx.fillStyle = this.randomColor(
        this.backgroundColorMin,
        this.backgroundColorMax
      )
      ctx.fillRect(0, 0, this.contentWidth, this.contentHeight)
      // 绘制文字
      for (let i = 0; i < this.identifyCode.length; i++) {
        this.drawText(ctx, this.identifyCode[i], i)
      }
      this.drawLine(ctx)
      this.drawDot(ctx)
    },
    drawText (ctx, txt, i) {
      ctx.fillStyle = this.randomColor(this.colorMin, this.colorMax)
      ctx.font =
        this.randomNum(this.fontSizeMin, this.fontSizeMax) + 'px SimHei'
      var x = (i + 1) * (this.contentWidth / (this.identifyCode.length + 1))
      var y = this.randomNum(this.fontSizeMax, this.contentHeight - 5)
      var deg = this.randomNum(-30, 30)
      // 修改坐标原点和旋转角度
      ctx.translate(x, y)
      ctx.rotate(deg * Math.PI / 270)
      ctx.fillText(txt, 0, 0)
      // 恢复坐标原点和旋转角度
      ctx.rotate(-deg * Math.PI / 270)
      ctx.translate(-x, -y)
    },
    drawLine (ctx) {
      // 绘制干扰线
      for (let i = 0; i < 2; i++) {
        ctx.strokeStyle = this.randomColor(
          this.lineColorMin,
          this.lineColorMax
        )
        ctx.beginPath()
        ctx.moveTo(
          this.randomNum(0, this.contentWidth),
          this.randomNum(0, this.contentHeight)
        )
        ctx.lineTo(
          this.randomNum(0, this.contentWidth),
          this.randomNum(0, this.contentHeight)
        )
        ctx.stroke()
      }
    },
    drawDot (ctx) {
      // 绘制干扰点
      for (let i = 0; i < 20; i++) {
        ctx.fillStyle = this.randomColor(0, 255)
        ctx.beginPath()
        ctx.arc(
          this.randomNum(0, this.contentWidth),
          this.randomNum(0, this.contentHeight),
          1,
          0,
          2 * Math.PI
        )
        ctx.fill()
      }
    }
  },
  watch: {
    identifyCode () {
      this.drawPic()
    }
  },
  mounted () {
    this.drawPic()
  }
}
</script>
<style lang='less' scoped>
.s-canvas {
    height: 38px;
}
.s-canvas canvas{
    margin-top: 1px;
    margin-left: 8px;
}
</style>
其他页面使用
<template>
 <span @click="refreshCode" style="cursor: pointer;">
  <s-identify :identifyCode="identifyCode" ></s-identify>
 </span>
</template>

<script>
// 引入图片验证码组件
import SIdentify from '@/components/identify'
export default {
 components: { SIdentify },
 data() {
  return {
   // 图片验证码
   identifyCode: '',
   // 验证码规则
   identifyCodes: '3456789ABCDEFGHGKMNPQRSTUVWXY',
  }
 },
 methods: {
  // 切换验证码
     refreshCode() {
   this.identifyCode = ''
   this.makeCode(this.identifyCodes, 4)
     },
  // 生成随机验证码
  makeCode(o, l) {
    for (let i = 0; i < l; i++) {
      this.identifyCode += this.identifyCodes[
        Math.floor(Math.random() * (this.identifyCodes.length - 0) + 0)
      ]
    }
  },
 }
}
</script>

结尾效果展示

因为是纯本地开发,只需要改下边几个地方就好

  • 视频播放结束判断是否为最后一条视频

  • 判断最后一条带有选项的视频,选完后添加对应的视频结尾展示push在数组中

<template>
    <div class="video-container" ref="parent">
        <div class="start" @click="getVideo" v-if="showStart">开始测评</div>
        <VideoComp ref="player" @end="onEnd"/>
        <QuestionDialog  v-model:show="showQuestion" :stage="stage" @video-start="onVideoStart" :question-list="questionList" />
        <!-- <div class="img" v-if="stage === 13">
            <img :src="require(`@/assets/character/${traitImg}.jpg`)" alt="" class="arrow">
        </div> -->
        <div class="start" v-if="endShow">完成测评</div>
        
    </div>
</template>

<script setup>

import { onMounted, ref, computed } from 'vue'
import VideoComp from './components/Video'
import QuestionDialog from './components/QuestionDialog'
import { flexible, handlerRem, handleRemResize } from '@/utils/flexible'

import {videoQuestionList} from './components/data/video'

const parent = ref()
onMounted(() => {
    handlerRem()
    window.addEventListener('resize', handleRemResize)
    flexible({
      mode: 'landscape'
    })
})
// 定义视频组件
const player = ref()
// 展示选项弹窗
const showQuestion = ref(false)
// 当前第几阶段, 值为0、1、2、3、4其中之一。0表示是在“开视频”阶段
const stage = ref(0)
// 测试是否结束
const endShow = ref(false)

// 视频播放结束,开始答题
function onEnd() {
    console.log('stage.value',stage.value)
    // 没题目,直接跳到下一阶段  stage.value判断是否为最后一条视频
    if (Object.keys(questionList.value).length === 0 && stage.value < 13) {
        stage.value += 1
        showQuestion.value = false
        handleStep()
    } else if(stage.value === 13){   //如果是最后一条视频,直接结束
        setTimeout(() => {
            endShow.value = true
        },1000)
        console.log('结束测试展示结果')
    } else {
        showQuestion.value = true
    }
}
// 获取题目
const soureObj = videoQuestionList
const questionList = computed(() => soureObj[stage.value]?.question || {})
console.log('questionList',questionList.value)
const showStart = ref(true)

// 获取视频
function getVideo() {
    showStart.value = false
    handleStep()
}
// 播放视频
function handleStep() {
    player.value.changeUrl(soureObj[stage.value].url)
    player.value.play() 
}

// 最高得分的性格
const traitImg = ref('')
// 性格得分
let traitScores = ref({});

// 选择视频选项
function onVideoStart(item) {
    // 定义一个空对象来存储性格得分
    const selectedTraits = item.scores;
    // 将性格得分添加到 traitScores 对象中
    for (let trait in selectedTraits) {
        if (traitScores[trait]) {
            traitScores[trait] += selectedTraits[trait];
        } else {
            traitScores[trait] = selectedTraits[trait];
        }
    }
    // stage.value 判断最后一条带有选项的视频,展示最高得分的性格
    if (stage.value >= 11) {
        const highestTrait = getHighestTrait();
        if (highestTrait) {
            console.log(`最高得分的性格是:${highestTrait}`);
            // 在此处添加显示性格相关图片或信息的逻辑
            traitImg.value = highestTrait
            stage.value += 1
            showQuestion.value = false
            soureObj.push(
                {
                    "url": require(`@/assets/character/${ traitImg.value}.mp4`),
                    "question": {}
                }
            )
            handleStep()
        } else {
            console.log("没有性格得分");
        }
    }else{
        // 进入下一阶段
        stage.value += 1
        showQuestion.value = false
        handleStep()
    }
}

// 获取最高得分的性格
function getHighestTrait() {
    let maxTrait = null;
    let maxScore = 0;
    for (let trait in traitScores) {
        if (traitScores[trait] > maxScore) {
            maxScore = traitScores[trait];
            maxTrait = trait;
        }
    }
    return maxTrait;
}

</script>

<style lang="less" scoped>
.start {
    background: #fff;
    position: absolute;
    z-index: 100;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.video-container {
    width: 100%;
    height: 100%;
    position: relative;
}
.img{
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
    display: flex;
    justify-content: center; /* 水平居中 */
    align-items: center; /* 垂直居中 */
    height: 100%;
    img{
        max-width: 100%;
        max-height: 100%;
        object-fit: contain; /* 保持图片比例 */
    }
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值