功能需求:实现视频交互性格测试,根据用户的选择展示对应的性格
整体思路:在视频上方嵌套遮罩层,视频结束展示遮罩层用户点击选项记录选项,播放下一条视频,当播放最后一条带有选择项的视频,用户选择完后根据用户的选择进行展示不同的图片
主页面 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>