Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
corporate-culture-qd
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
王立鹏
corporate-culture-qd
Commits
78d2d837
Commit
78d2d837
authored
Mar 19, 2026
by
lijiabin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【需求 21096】 feat: 完成第一版视频奖励碎片亚币等
parent
c46ff158
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
291 additions
and
6 deletions
+291
-6
rewardFullSetToast.vue
src/views/videoDetail/components/rewardFullSetToast.vue
+0
-0
rewardToast.vue
src/views/videoDetail/components/rewardToast.vue
+146
-0
index.vue
src/views/videoDetail/index.vue
+145
-6
No files found.
src/views/videoDetail/components/rewardFullSetToast.vue
0 → 100644
View file @
78d2d837
This diff is collapsed.
Click to expand it.
src/views/videoDetail/components/rewardToast.vue
0 → 100644
View file @
78d2d837
<
template
>
<Teleport
to=
"body"
>
<Transition
name=
"reward-toast"
>
<div
v-if=
"show"
class=
"fixed inset-0 z-9999 flex items-center justify-center pointer-events-none"
>
<div
class=
"relative flex flex-col items-center gap-4"
>
<!-- 光晕背景 -->
<div
class=
"absolute inset--20 rounded-full bg-amber-400/10 blur-3xl animate-pulse"
/>
<!-- 碎片 +1 -->
<div
class=
"reward-item reward-item-1"
>
<div
class=
"flex items-center gap-2.5 px-6 py-3 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 shadow-[0_4px_24px_rgba(99,102,241,0.5)]"
>
<span
class=
"text-2xl"
>
🧩
</span>
<span
class=
"text-white text-lg font-bold tracking-wide"
>
{{
rewardText
}}
+ 1
</span>
</div>
</div>
<!-- 亚币 +2 -->
<div
class=
"reward-item reward-item-2"
>
<div
class=
"flex items-center gap-2.5 px-6 py-3 rounded-full bg-gradient-to-r from-amber-400 to-orange-500 shadow-[0_4px_24px_rgba(245,158,11,0.5)]"
>
<span
class=
"text-2xl"
>
💰
</span>
<span
class=
"text-white text-lg font-bold tracking-wide"
>
亚币 + 5
</span>
</div>
</div>
<!-- 星星粒子效果 -->
<div
class=
"absolute inset--10"
>
<span
v-for=
"i in 8"
:key=
"i"
class=
"particle"
:style=
"getParticleStyle(i)"
/>
</div>
</div>
</div>
</Transition>
</Teleport>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
SpecificVideoRewardEnum
}
from
'@/constants'
const
{
rewardVideoType
}
=
defineProps
<
{
rewardVideoType
:
SpecificVideoRewardEnum
}
>
()
const
rewardText
=
computed
(()
=>
{
switch
(
rewardVideoType
)
{
case
SpecificVideoRewardEnum
.
QA_CHIP
:
return
'QA碎片'
case
SpecificVideoRewardEnum
.
ORIGINAL_CHIP
:
return
'初心碎片'
case
SpecificVideoRewardEnum
.
MODEST_CHIP
:
return
'本分&包容碎片'
case
SpecificVideoRewardEnum
.
INNOVATION_CHIP
:
return
'创新碎片'
}
})
const
show
=
ref
(
false
)
const
getParticleStyle
=
(
index
:
number
)
=>
{
const
angle
=
(
index
/
8
)
*
360
const
delay
=
index
*
0.1
return
{
'--angle'
:
`
${
angle
}
deg`
,
'--delay'
:
`
${
delay
}
s`
,
}
}
const
showRewardToast
=
async
()
=>
{
show
.
value
=
true
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
2500
))
show
.
value
=
false
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
3000
))
}
defineExpose
({
showRewardToast
,
})
</
script
>
<
style
scoped
>
.reward-toast-enter-active
{
transition
:
opacity
0.3s
ease
;
}
.reward-toast-leave-active
{
transition
:
opacity
0.5s
ease
1.8s
;
}
.reward-toast-enter-from
,
.reward-toast-leave-to
{
opacity
:
0
;
}
.reward-item
{
opacity
:
0
;
animation
:
rewardSlideUp
0.6s
cubic-bezier
(
0.34
,
1.56
,
0.64
,
1
)
forwards
;
}
.reward-item-1
{
animation-delay
:
0.1s
;
}
.reward-item-2
{
animation-delay
:
0.35s
;
}
@keyframes
rewardSlideUp
{
0
%
{
opacity
:
0
;
transform
:
translateY
(
30px
)
scale
(
0.7
);
}
60
%
{
opacity
:
1
;
transform
:
translateY
(
-8px
)
scale
(
1.05
);
}
100
%
{
opacity
:
1
;
transform
:
translateY
(
0
)
scale
(
1
);
}
}
.particle
{
position
:
absolute
;
top
:
50%
;
left
:
50%
;
width
:
6px
;
height
:
6px
;
border-radius
:
50%
;
background
:
linear-gradient
(
135deg
,
#fbbf24
,
#a78bfa
);
opacity
:
0
;
animation
:
particleBurst
1.2s
ease-out
var
(
--delay
)
forwards
;
}
@keyframes
particleBurst
{
0
%
{
opacity
:
1
;
transform
:
translate
(
-50%
,
-50%
)
rotate
(
var
(
--angle
))
translateX
(
0
)
scale
(
1
);
}
70
%
{
opacity
:
0.8
;
transform
:
translate
(
-50%
,
-50%
)
rotate
(
var
(
--angle
))
translateX
(
80px
)
scale
(
0.8
);
}
100
%
{
opacity
:
0
;
transform
:
translate
(
-50%
,
-50%
)
rotate
(
var
(
--angle
))
translateX
(
120px
)
scale
(
0
);
}
}
</
style
>
src/views/videoDetail/index.vue
View file @
78d2d837
...
...
@@ -90,6 +90,7 @@
</div>
<!-- 视频 16/ 9 -->
<!-- 监听每秒播放进度 -->
<div
class=
"mx-4"
>
<video
ref=
"videoRef"
...
...
@@ -98,6 +99,10 @@
controls
@
play=
"handlePlay"
@
pause=
"handlePause"
@
timeupdate=
"handleTimeUpdate"
@
seeking=
"handleSeeking"
@
seeked=
"handleSeeked"
@
loadedmetadata=
"handleLoadedMetadata"
></video>
</div>
<div
class=
"bg-white rounded-xl shadow-sm p-4"
>
...
...
@@ -305,27 +310,36 @@
class=
"mt-5"
/>
<RewardDialog
ref=
"rewardDialogRef"
v-model:rewardNum=
"videoDetail.rewardNum"
/>
<RewardToast
ref=
"rewardToastRef"
:rewardVideoType=
"rewardVideoType"
/>
<RewardFullSetToast
ref=
"rewardFullSetToastRef"
@
hided=
"videoRef?.play()"
/>
</div>
</template>
<
script
lang=
"ts"
setup
>
import
dayjs
from
'dayjs'
import
{
getArticleDetail
,
addOrCanceArticlelCollect
,
addOrCanceArticlelLike
,
addVideoPlayCount
,
getSpecificVideoWatchReward
,
}
from
'@/api'
import
type
{
ArticleItemDto
}
from
'@/api/article/types'
import
Comment
from
'@/components/common/Comment/index.vue'
import
RewardDialog
from
'./components/rewardDialog.vue'
import
RewardToast
from
'./components/rewardToast.vue'
import
RewardFullSetToast
from
'./components/rewardFullSetToast.vue'
import
ActionMore
from
'@/components/common/ActionMore/index.vue'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
{
useNavigation
}
from
'@/hooks'
import
{
ArticleTypeEnum
}
from
'@/constants'
import
{
ArticleTypeEnum
,
SpecificVideoRewardEnum
,
specificVideoRewardListOptions
,
}
from
'@/constants'
import
{
push
}
from
'notivue'
import
{
useStorage
}
from
'@vueuse/core'
const
route
=
useRoute
()
const
videoId
=
Number
(
route
.
params
.
id
)
const
{
jumpToUserHomePage
}
=
useNavigation
()
...
...
@@ -333,8 +347,126 @@ const { jumpToUserHomePage } = useNavigation()
const
videoDetail
=
ref
({}
as
ArticleItemDto
)
const
loading
=
computed
(()
=>
!
videoDetail
.
value
.
title
)
const
videoRef
=
useTemplateRef
<
HTMLVideoElement
|
null
>
(
'videoRef'
)
const
rewardFullSetToastRef
=
useTemplateRef
<
InstanceType
<
typeof
RewardFullSetToast
>
|
null
>
(
'rewardFullSetToastRef'
,
)
const
commentRef
=
useTemplateRef
<
InstanceType
<
typeof
Comment
>
|
null
>
(
'commentRef'
)
// 关于视频跳出奖励相关的逻辑
const
watchedSecondsObj
=
useStorage
(
'watched-seconds-obj'
,
{
[
SpecificVideoRewardEnum
.
QA_CHIP
]:
{
watchedSeconds
:
0
,
hasReward
:
false
,
},
[
SpecificVideoRewardEnum
.
ORIGINAL_CHIP
]:
{
watchedSeconds
:
0
,
hasReward
:
false
,
},
[
SpecificVideoRewardEnum
.
MODEST_CHIP
]:
{
watchedSeconds
:
0
,
hasReward
:
false
,
},
[
SpecificVideoRewardEnum
.
INNOVATION_CHIP
]:
{
watchedSeconds
:
0
,
hasReward
:
false
,
},
})
const
rewardDialogRef
=
useTemplateRef
<
InstanceType
<
typeof
RewardDialog
>
|
null
>
(
'rewardDialogRef'
)
const
rewardVideoLimitDuration
=
ref
(
0
)
const
specificVideoRewardItem
=
computed
(()
=>
{
return
specificVideoRewardListOptions
.
find
((
item
)
=>
videoDetail
.
value
.
title
?.
includes
(
item
.
title
),
)
})
const
showRewardToastComp
=
computed
(()
=>
{
return
!!
specificVideoRewardItem
.
value
})
const
rewardVideoType
=
computed
(()
=>
{
return
specificVideoRewardItem
.
value
?.
value
??
SpecificVideoRewardEnum
.
QA_CHIP
})
const
rewardToastRef
=
useTemplateRef
<
InstanceType
<
typeof
RewardToast
>
|
null
>
(
'rewardToastRef'
)
// 真实观看时长(只统计连续播放,不算暂停和拖拽跳跃)
const
isPlaying
=
ref
(
false
)
const
isSeeking
=
ref
(
false
)
let
lastVideoTime
:
number
|
null
=
null
const
resetWatchCursor
=
(
t
:
number
)
=>
{
lastVideoTime
=
Number
.
isFinite
(
t
)
?
t
:
null
}
const
accumulateWatchTime
=
async
(
video
:
HTMLVideoElement
)
=>
{
if
(
!
isPlaying
.
value
||
isSeeking
.
value
)
return
const
t
=
video
.
currentTime
if
(
!
Number
.
isFinite
(
t
))
return
if
(
lastVideoTime
==
null
)
{
resetWatchCursor
(
t
)
return
}
const
delta
=
t
-
lastVideoTime
resetWatchCursor
(
t
)
// 只统计“自然播放”的连续前进:拖拽/跳跃(大步长)或倒退都不算
// timeupdate 通常 250ms~1s 一次,这里给一点冗余避免误伤
if
(
delta
>
0
&&
delta
<=
1.5
)
{
watchedSecondsObj
.
value
[
rewardVideoType
.
value
].
watchedSeconds
+=
delta
if
(
watchedSecondsObj
.
value
[
rewardVideoType
.
value
].
watchedSeconds
>=
rewardVideoLimitDuration
.
value
&&
!
watchedSecondsObj
.
value
[
rewardVideoType
.
value
].
hasReward
)
{
// 调用后端接口
await
getSpecificVideoWatchReward
(
rewardVideoType
.
value
)
const
rewardToastPromise
=
rewardToastRef
.
value
?.
showRewardToast
()
watchedSecondsObj
.
value
[
rewardVideoType
.
value
].
hasReward
=
true
// 如果四个都奖励了,则显示全套奖励toast
if
(
Object
.
values
(
watchedSecondsObj
.
value
).
every
((
item
)
=>
item
.
hasReward
))
{
// 暂停视频
await
rewardToastPromise
videoRef
.
value
?.
pause
()
rewardFullSetToastRef
.
value
?.
showFullSetToast
()
watchedSecondsObj
.
value
=
null
}
}
}
}
const
handleTimeUpdate
=
(
event
:
Event
)
=>
{
if
(
showRewardToastComp
.
value
)
{
const
video
=
event
.
target
as
HTMLVideoElement
accumulateWatchTime
(
video
)
}
}
const
handleSeeking
=
(
event
:
Event
)
=>
{
if
(
showRewardToastComp
.
value
)
{
const
video
=
event
.
target
as
HTMLVideoElement
isSeeking
.
value
=
true
resetWatchCursor
(
video
.
currentTime
)
}
}
const
handleSeeked
=
(
event
:
Event
)
=>
{
if
(
showRewardToastComp
.
value
)
{
const
video
=
event
.
target
as
HTMLVideoElement
isSeeking
.
value
=
false
resetWatchCursor
(
video
.
currentTime
)
}
}
const
handleLoadedMetadata
=
()
=>
{
if
(
videoRef
.
value
&&
showRewardToastComp
.
value
)
{
// 四舍五入
rewardVideoLimitDuration
.
value
=
Math
.
round
(
videoRef
.
value
.
duration
/
2
)
}
}
// 格式化数字
const
formatNumber
=
(
num
:
number
)
=>
{
...
...
@@ -347,12 +479,21 @@ const formatNumber = (num: number) => {
// 播放 记录播放量 + 1
const
handlePlay
=
async
()
=>
{
if
(
showRewardToastComp
.
value
)
{
isPlaying
.
value
=
true
isSeeking
.
value
=
false
}
resetWatchCursor
(
videoRef
.
value
?.
currentTime
??
0
)
await
addVideoPlayCount
(
videoDetail
.
value
.
id
)
videoDetail
.
value
.
playCount
=
videoDetail
.
value
.
playCount
+
1
}
const
handlePause
=
()
=>
{
// 记录暂停
if
(
showRewardToastComp
.
value
)
{
isPlaying
.
value
=
false
resetWatchCursor
(
videoRef
.
value
?.
currentTime
??
0
)
}
}
// 点赞
...
...
@@ -375,10 +516,8 @@ const handleCollect = async (item: ArticleItemDto) => {
const
handleReward
=
()
=>
{
rewardDialogRef
.
value
?.
open
(
videoDetail
.
value
.
id
)
}
onMounted
(
async
()
=>
{
const
{
data
}
=
await
getArticleDetail
(
videoId
)
console
.
log
(
data
)
videoDetail
.
value
=
data
})
</
script
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment