Commit ca761014 by lijiabin

【需求 20331】 feat: 完成关于首次上线的活动相关的功能

parent dffcec96
<template>
<Teleport to="body">
<Transition name="cg-overlay">
<div
v-if="activityStore.showCgGuide || activityStore.showCommonGuide"
class="fixed inset-0 z-[10000] flex items-center justify-center bg-black/90"
>
<!-- 阶段1: 播放CG动画 -->
<div
v-if="phase === 'video' && activityStore.showCgGuide"
class="relative w-full h-full flex flex-col items-center justify-center"
>
<!-- CG视频区域 -->
<div
class="relative w-full max-w-4xl aspect-video rounded-2xl overflow-hidden shadow-2xl mx-4"
>
<video
ref="videoRef"
class="w-full h-full object-cover"
:src="cgVideoUrl"
playsinline
preload="metadata"
@ended="onVideoEnd"
@error="onVideoEnd"
@play="isPlaying = true"
@pause="isPlaying = false"
/>
<!-- 自定义播放按钮:未播放时显示,点击后开始播放 -->
<button
v-if="!isPlaying"
type="button"
class="absolute inset-0 flex items-center justify-center bg-black/30 hover:bg-black/40 transition-colors cursor-pointer"
@click="playVideo"
>
<span
class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center text-white/90"
>
<svg class="w-8 h-8 sm:w-10 sm:h-10 ml-1" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
</span>
</button>
<!-- 控制栏:播放 / 暂停 + 时间 + 进度条 -->
<div
class="absolute bottom-4 left-3 right-3 flex items-center gap-3 text-white pointer-events-none"
>
<!-- 播放 / 暂停按钮 -->
<button
type="button"
class="w-9 h-9 sm:w-10 sm:h-10 rounded-full bg-black/50 hover:bg-black/80 flex items-center justify-center text-white shadow-md border border-white/20 cursor-pointer transition-colors pointer-events-auto"
@click="isPlaying ? pauseVideo() : playVideo()"
>
<svg
v-if="!isPlaying"
class="w-4 h-4 sm:w-5 sm:h-5 ml-0.5"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M8 5v14l11-7z" />
</svg>
<svg v-else class="w-4 h-4 sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
</svg>
</button>
<!-- 时间 + 进度条 -->
<div
class="flex-1 flex flex-col gap-1 rounded-lg bg-black/50 backdrop-blur-sm px-3 py-2 pointer-events-auto"
>
<div class="flex items-center justify-end text-[11px] sm:text-xs font-mono">
<span class="text-white/90">{{ formattedCurrentTime }}</span>
<span class="text-white/60 mx-1">/</span>
<span class="text-white/80">{{ formattedDuration }}</span>
</div>
<div class="h-1.5 rounded-full bg-white/15 overflow-hidden">
<div
class="h-full bg-gradient-to-r from-indigo-300 via-indigo-400 to-purple-400 transition-all duration-300"
:style="{ width: `${videoProgress}%` }"
/>
</div>
</div>
</div>
</div>
<!-- 跳过按钮:视频播放满 1 分钟后才显示 -->
<button
v-if="canSkip"
class="absolute top-6 right-6 px-5 py-2 rounded-full bg-white/15 hover:bg-white/25 text-white/80 hover:text-white text-sm border border-white/20 transition-all duration-200 cursor-pointer"
@click="skipVideo"
>
跳过
</button>
</div>
<!-- 阶段2: 奖励动画 -->
<div
v-else-if="phase === 'reward' && activityStore.showCgGuide"
class="flex flex-col items-center gap-6"
>
<div class="text-white/60 text-sm mb-2">恭喜获得观看奖励</div>
<!-- 奖励卡片 -->
<div class="flex gap-6">
<div class="reward-card reward-card-1">
<div
class="flex flex-col items-center gap-3 px-8 py-6 rounded-2xl bg-gradient-to-br from-indigo-500/90 to-purple-600/90 border border-white/20 shadow-[0_8px_32px_rgba(99,102,241,0.4)]"
>
<span class="text-4xl">🧩</span>
<span class="text-white text-2xl font-bold">碎片 +1</span>
</div>
</div>
<div class="reward-card reward-card-2">
<div
class="flex flex-col items-center gap-3 px-8 py-6 rounded-2xl bg-gradient-to-br from-amber-400/90 to-orange-500/90 border border-white/20 shadow-[0_8px_32px_rgba(245,158,11,0.4)]"
>
<span class="text-4xl">💰</span>
<span class="text-white text-2xl font-bold">亚币 +2</span>
</div>
</div>
</div>
<!-- 光效 -->
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<div
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 rounded-full bg-amber-400/10 blur-3xl animate-pulse"
/>
</div>
</div>
<!-- 阶段3: 指引按钮 -->
<div
v-else-if="phase === 'guide' && activityStore.showCommonGuide"
class="flex flex-col items-center gap-8"
>
<div class="text-center">
<h2 class="text-white text-2xl font-bold mb-2">欢迎来到 YAYA 文化岛 🏝️</h2>
<p class="text-white/60 text-sm">选择你接下来想要探索的内容</p>
</div>
<div class="flex gap-5">
<button
class="group flex flex-col items-center gap-3 px-8 py-6 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 hover:from-indigo-400 hover:to-purple-500 shadow-lg hover:shadow-[0_8px_32px_rgba(99,102,241,0.5)] border border-white/20 transition-all duration-300 hover:-translate-y-1 cursor-pointer"
@click="goToActivity"
>
<span class="text-3xl group-hover:scale-110 transition-transform duration-300"
>🎯</span
>
<span class="text-white font-semibold text-base">直达活动页面</span>
<span class="text-white/60 text-xs">查看精选推文</span>
</button>
<button
class="group flex flex-col items-center gap-3 px-8 py-6 rounded-2xl bg-white/10 hover:bg-white/20 shadow-lg hover:shadow-[0_8px_32px_rgba(255,255,255,0.15)] border border-white/20 transition-all duration-300 hover:-translate-y-1 cursor-pointer"
@click="goHome"
>
<span class="text-3xl group-hover:scale-110 transition-transform duration-300"
>🏠</span
>
<span class="text-white font-semibold text-base">返回首页</span>
<span class="text-white/60 text-xs">开始自由浏览</span>
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { useActivityStore } from '@/stores'
const activityStore = useActivityStore()
const router = useRouter()
type Phase = 'video' | 'reward' | 'guide'
const phase = ref<Phase>('video')
const videoRef = ref<HTMLVideoElement>()
const videoProgress = ref(0)
const videoCurrentTime = ref(0)
const videoDuration = ref(0)
const isPlaying = ref(false)
// 是否允许跳过:视频播放满 60s 后变为 true
const canSkip = ref(false)
const cgVideoUrl = ref(
'https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/readName/mp4/2026/03/10/Common/1773129191249.mp4',
)
const formatTime = (seconds: number) => {
if (Number.isNaN(seconds) || seconds < 0) return '0:00'
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${m}:${s.toString().padStart(2, '0')}`
}
const formattedCurrentTime = computed(() => formatTime(videoCurrentTime.value))
const formattedDuration = computed(() => formatTime(videoDuration.value))
let progressTimer: ReturnType<typeof setInterval> | null = null
const startTimers = () => {
progressTimer = setInterval(() => {
if (videoRef.value) {
const { currentTime, duration } = videoRef.value
videoCurrentTime.value = currentTime
videoDuration.value = duration
if (duration && Number.isFinite(duration)) {
videoProgress.value = (currentTime / duration) * 100
if (!canSkip.value && currentTime >= 60) canSkip.value = true
}
}
}, 200)
}
const playVideo = () => {
videoRef.value?.play()
}
const pauseVideo = () => {
videoRef.value?.pause()
}
const clearTimers = () => {
if (progressTimer) clearInterval(progressTimer)
}
const onVideoEnd = () => {
clearTimers()
transitionToReward()
}
const skipVideo = () => {
if (videoRef.value) videoRef.value.pause()
clearTimers()
transitionToReward()
}
const transitionToReward = async () => {
phase.value = 'reward'
// 标记用户已经观看过cg动画
await activityStore.markUserGuideWatched('isView')
setTimeout(() => {
// 第二天上线打开这个 去掉下一行注释
// phase.value = 'guide'
close()
}, 2500)
}
const close = () => {
activityStore.showCgGuide = false
activityStore.showCommonGuide = false
phase.value = 'video'
videoProgress.value = 0
videoCurrentTime.value = 0
videoDuration.value = 0
isPlaying.value = false
canSkip.value = false
}
const goToActivity = async () => {
await activityStore.markUserGuideWatched('isViewGuide')
close()
router.push('/articleDetail/289')
}
const goHome = async () => {
activityStore.markUserGuideWatched('isViewGuide')
close()
router.push('/homePage/homeTab')
}
watch(
() => [activityStore.showCgGuide, activityStore.showCommonGuide],
(val) => {
if (val[0]) {
phase.value = 'video'
nextTick(() => startTimers())
} else if (val[1]) {
// 第二天上线打开这个 去掉下一行注释
// phase.value = 'guide'
close()
} else {
clearTimers()
}
},
)
onUnmounted(() => {
clearTimers()
})
</script>
<style scoped>
.cg-overlay-enter-active {
transition: opacity 0.5s ease;
}
.cg-overlay-leave-active {
transition: opacity 0.4s ease;
}
.cg-overlay-enter-from,
.cg-overlay-leave-to {
opacity: 0;
}
.reward-card {
opacity: 0;
animation: rewardPop 0.7s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.reward-card-1 {
animation-delay: 0.2s;
}
.reward-card-2 {
animation-delay: 0.5s;
}
@keyframes rewardPop {
0% {
opacity: 0;
transform: scale(0.5) translateY(20px);
}
60% {
opacity: 1;
transform: scale(1.08) translateY(-5px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
</style>
<template>
<span @click="handleClick">
<slot></slot>
</span>
</template>
<script setup lang="ts">
import { useActivityStore } from '@/stores/activity'
import { RewardButtonEnum } from '@/constants'
const { pageKey } = defineProps<{
pageKey: RewardButtonEnum
}>()
const activityStore = useActivityStore()
// 调用接口让他跳出动画
const handleClick = () => {
console.log('点击了')
activityStore.triggerPageReward(pageKey)
}
</script>
<template>
<Teleport to="body">
<Transition name="reward-toast">
<div
v-if="activityStore.showRewardAnimation"
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"
>碎片 +{{ activityStore.rewardText.fragment }}</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"
>亚币 +{{ activityStore.rewardText.yabi }}</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 { useActivityStore } from '@/stores'
const activityStore = useActivityStore()
const getParticleStyle = (index: number) => {
const angle = (index / 8) * 360
const delay = index * 0.1
return {
'--angle': `${angle}deg`,
'--delay': `${delay}s`,
}
}
</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>
<template>
<el-dialog
v-model="activityStore.showSurvey"
title=""
width="480px"
:close-on-click-modal="false"
class="satisfaction-survey-dialog"
@close="handleClose"
>
<div class="flex flex-col gap-5">
<!-- 头部 -->
<div class="text-center">
<div class="text-2xl mb-2">📋</div>
<h3 class="text-lg font-bold text-gray-800 mb-1">使用体验反馈</h3>
<p class="text-xs text-gray-400">您的反馈对我们非常重要,仅需1分钟</p>
</div>
<!-- Q1: 满意度评分 -->
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-gray-700">整体满意度</label>
<div class="flex items-center justify-center gap-2">
<button
v-for="star in 5"
:key="star"
class="text-2xl transition-all duration-200 cursor-pointer hover:scale-125 border-none bg-transparent p-1"
:class="star <= form.rating ? 'grayscale-0' : 'grayscale opacity-30'"
@click="form.rating = star"
>
</button>
</div>
<div class="text-center text-xs text-gray-400">
{{ ratingLabels[form.rating] || '请点击星星评分' }}
</div>
</div>
<!-- Q2: 最喜欢的功能 -->
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-gray-700">最喜欢的功能(可多选)</label>
<div class="flex flex-wrap gap-2">
<button
v-for="feat in featureOptions"
:key="feat.value"
class="px-3 py-1.5 rounded-full text-xs border transition-all duration-200 cursor-pointer"
:class="
form.favoriteFeatures.includes(feat.value)
? 'bg-indigo-50 border-indigo-300 text-indigo-600'
: 'bg-white border-gray-200 text-gray-500 hover:border-gray-300'
"
@click="toggleFeature(feat.value)"
>
{{ feat.label }}
</button>
</div>
</div>
<!-- Q3: 改进建议 -->
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-gray-700">改进建议(选填)</label>
<el-input
v-model="form.suggestion"
type="textarea"
:rows="3"
placeholder="说说您的想法,帮助我们做得更好..."
maxlength="300"
show-word-limit
/>
</div>
<!-- 提交按钮 -->
<div class="flex gap-3 mt-1">
<el-button class="flex-1" @click="handleClose">下次再说</el-button>
<el-button
type="primary"
class="flex-1 bg-gradient-to-r from-indigo-500 to-purple-500! border-none!"
:disabled="form.rating === 0"
@click="handleSubmit"
>
提交反馈
</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { useActivityStore } from '@/stores'
import { submitSurvey } from '@/api'
import { push } from 'notivue'
const activityStore = useActivityStore()
const form = reactive({
rating: 0,
favoriteFeatures: [] as string[],
suggestion: '',
})
const ratingLabels: Record<number, string> = {
1: '非常不满意',
2: '不太满意',
3: '一般',
4: '比较满意',
5: '非常满意',
}
const featureOptions = [
{ value: 'post', label: '帖子/实践' },
{ value: 'video', label: '视频' },
{ value: 'question', label: '问吧' },
{ value: 'sign', label: '签到/任务' },
{ value: 'store', label: '积分商城' },
{ value: 'auction', label: '限时竞拍' },
{ value: 'lottery', label: '每日抽奖' },
{ value: 'column', label: '专栏/专访' },
]
const toggleFeature = (value: string) => {
const idx = form.favoriteFeatures.indexOf(value)
if (idx >= 0) {
form.favoriteFeatures.splice(idx, 1)
} else {
form.favoriteFeatures.push(value)
}
}
const handleSubmit = async () => {
if (form.rating === 0) return
try {
await submitSurvey({
rating: form.rating,
favoriteFeatures: form.favoriteFeatures,
suggestion: form.suggestion,
})
push.success('感谢您的反馈!')
activityStore.markSurveySubmitted()
} catch {
push.success('感谢您的反馈!')
activityStore.markSurveySubmitted()
}
}
const handleClose = () => {
activityStore.showSurvey = false
}
</script>
<style>
.satisfaction-survey-dialog .el-dialog__header {
display: none;
}
.satisfaction-survey-dialog .el-dialog__body {
padding: 24px;
}
</style>
import type { SetupContext } from 'vue'
import RewardButton from '@/components/common/RewardButton/index.vue'
import { RewardButtonEnum } from '@/constants'
type TypeOfValue = string | number
interface TabsProps<T> {
tabs: {
label: string
value: T
pageKey?: RewardButtonEnum
}[]
modelValue: T
}
......@@ -25,21 +28,41 @@ export default function Tabs<T extends TypeOfValue>(
{ tabs, modelValue }: TabsProps<T>,
{ emit }: SetupContext<TabsEmits<T>>,
) {
return tabs.map((tab) => (
<div
key={tab.value}
class={[
BASE_TAB_CALASSES,
{
[ACTIVE_TAB_CLASSES]: modelValue === tab.value,
},
]}
onClick={() => {
emit('update:modelValue', tab.value)
emit('change', tab.value)
}}
>
{tab.label}
</div>
))
return tabs.map((tab) =>
tab.pageKey ? (
<RewardButton pageKey={tab.pageKey}>
<div
key={tab.value}
class={[
BASE_TAB_CALASSES,
{
[ACTIVE_TAB_CLASSES]: modelValue === tab.value,
},
]}
onClick={() => {
emit('update:modelValue', tab.value)
emit('change', tab.value)
}}
>
{tab.label}
</div>
</RewardButton>
) : (
<div
key={tab.value}
class={[
BASE_TAB_CALASSES,
{
[ACTIVE_TAB_CLASSES]: modelValue === tab.value,
},
]}
onClick={() => {
emit('update:modelValue', tab.value)
emit('change', tab.value)
}}
>
{tab.label}
</div>
),
)
}
<template>
<Draggable
v-show="showOnlineTime"
:initial-value="{ x: x, y: y }"
storage-key="vueuse-draggable"
storage-type="session"
class="fixed z-2"
>
<div
class="group flex items-center gap-2.5 py-2.5 pr-4 pl-3 bg-#fff rounded-xl shadow-[inset_0_2px_6px_rgba(0,0,0,0.08)] cursor-pointer transition-all duration-250 relative shadow-[inset_0_2px_8px_rgba(0,0,0,0.12)]"
<transition name="online-time">
<Draggable
v-show="showOnlineTime"
:initial-value="{ x: x, y: y }"
storage-key="vueuse-draggable"
storage-type="session"
class="fixed z-2"
>
<!-- 在线指示灯 -->
<div
class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0 animate-pulse shadow-[0_0_6px_rgba(34,197,94,0.4)]"
></div>
class="group flex items-center gap-2.5 py-2.5 pr-4 pl-3 bg-#fff rounded-xl shadow-[inset_0_2px_6px_rgba(0,0,0,0.08)] cursor-pointer transition-all duration-250 relative shadow-[inset_0_2px_8px_rgba(0,0,0,0.12)]"
>
<!-- 在线指示灯 -->
<div
class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0 animate-pulse shadow-[0_0_6px_rgba(34,197,94,0.4)]"
></div>
<!-- 时长内容 -->
<div class="flex flex-col leading-tight">
<span class="text-base text-gray-400 tracking-wider font-medium">今日在线时长</span>
<span class="text-base font-bold text-blue-600 tabular-nums tracking-wider">
{{ formatSeconds }}
</span>
</div>
<!-- 时长内容 -->
<div class="flex flex-col leading-tight">
<span class="text-base text-gray-400 tracking-wider font-medium">今日在线时长</span>
<span class="text-base font-bold text-blue-600 tabular-nums tracking-wider">
{{ formatSeconds }}
</span>
</div>
<!-- 关闭按钮 -->
<div
class="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-white text-gray-400 shadow-sm flex items-center justify-center opacity-0 scale-60 transition-all duration-200 cursor-pointer group-hover:opacity-100 group-hover:scale-100 hover:!bg-red-500 hover:!text-white"
title="关闭"
@click.stop="showOnlineTime = false"
>
<svg viewBox="0 0 12 12" class="w-2.5 h-2.5">
<path
d="M1.5.4L6 4.9 10.5.4l1.1 1.1L7.1 6l4.5 4.5-1.1 1.1L6 7.1 1.5 11.6.4 10.5 4.9 6 .4 1.5z"
fill="currentColor"
/>
</svg>
<!-- 关闭按钮 -->
<div
class="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-white text-gray-400 shadow-sm flex items-center justify-center opacity-0 scale-60 transition-all duration-200 cursor-pointer group-hover:opacity-100 group-hover:scale-100 hover:!bg-red-500 hover:!text-white"
title="关闭"
@click.stop="showOnlineTime = false"
>
<svg viewBox="0 0 12 12" class="w-2.5 h-2.5">
<path
d="M1.5.4L6 4.9 10.5.4l1.1 1.1L7.1 6l4.5 4.5-1.1 1.1L6 7.1 1.5 11.6.4 10.5 4.9 6 .4 1.5z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
</Draggable>
</Draggable>
</transition>
</template>
<script setup lang="ts">
......@@ -86,3 +88,15 @@ onUnmounted(() => {
clearInterval(timer2)
})
</script>
<style lang="scss" scoped>
.online-time-enter-from,
.online-time-leave-to {
opacity: 0;
transform: translateY(10px);
}
.online-time-enter-active,
.online-time-leave-active {
transition: all 0.3s ease;
}
</style>
......@@ -36,22 +36,25 @@
</div>
<!-- 右侧菜单 -->
<div class="flex items-center gap-1 flex-shrink-0 sm:gap-2 lg:gap-4">
<div
class="group h-80% flex items-center cursor-pointer px-2 py-1 sm:px-3 sm:py-2 rounded-lg transition-all duration-200 hover:shadow-lg hover:bg-white/60"
@click="router.push('/userPage')"
>
<el-avatar
:size="30"
:src="userInfo.hiddenAvatar"
class="border-3 border-blue-200 shadow-lg flex-shrink-0 transition-transform duration-200 group-hover:scale-105"
/>
<span
class="ml-2 text-sm whitespace-nowrap hidden lg:inline transition-all duration-300 ease-out group-hover:translate-y--0.5 text-gray-700"
<RewardButton :pageKey="RewardButtonEnum.USER_PAGE">
<div
class="group h-80% flex items-center cursor-pointer px-2 py-1 sm:px-3 sm:py-2 rounded-lg transition-all duration-200 hover:shadow-lg hover:bg-white/60"
@click="router.push('/userPage')"
>
个人中心
</span>
</div>
<el-avatar
:size="30"
:src="userInfo.hiddenAvatar"
class="border-3 border-blue-200 shadow-lg flex-shrink-0 transition-transform duration-200 group-hover:scale-105"
/>
<span
class="ml-2 text-sm whitespace-nowrap hidden lg:inline transition-all duration-300 ease-out group-hover:translate-y--0.5 text-gray-700"
>
个人中心
</span>
</div>
</RewardButton>
<div
class="group flex items-center cursor-pointer px-2 py-1 sm:px-3 sm:py-2 rounded-lg transition-all duration-200 hover:shadow-lg hover:bg-white/60"
@click="router.push('/auction')"
......@@ -89,20 +92,42 @@
</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="ArticleTypeEnum.POST">帖子</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.PRACTICE">实践</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.VIDEO">视频</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.QUESTION">问吧</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.POST">
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_LONG_ARTICLE">
帖子
</RewardButton>
</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.PRACTICE">
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_LONG_ARTICLE">
实践
</RewardButton>
</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.VIDEO">
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_LONG_ARTICLE">
视频
</RewardButton>
</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.QUESTION">
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_LONG_ARTICLE">
问吧
</RewardButton>
</el-dropdown-item>
<el-dropdown-item
v-if="userInfo.isOfficialAccount || userInfo.isAdmin"
:command="ArticleTypeEnum.COLUMN"
>专栏</el-dropdown-item
>
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_LONG_ARTICLE">
专栏
</RewardButton>
</el-dropdown-item>
<el-dropdown-item
v-if="userInfo.isOfficialAccount || userInfo.isAdmin"
:command="ArticleTypeEnum.INTERVIEW"
>专访</el-dropdown-item
>
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_LONG_ARTICLE">
专访
</RewardButton>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
......@@ -110,7 +135,10 @@
</div>
</div>
<div class="flex-1 w-full flex items-center justify-center">
<div class="container max-h-none transition-all duration-300 min-h-[calc(100vh-96px)]">
<div
class="container max-h-none transition-all duration-300 min-h-[calc(100vh-96px)]"
:class="{ 'px-10!': !isHomePage }"
>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<!-- 注释不能放到keep-alive下面 route是最终的路由信息 Component是当前n级路由的组件 二级路由 homePage videoDetail -->
......@@ -123,27 +151,37 @@
</div>
</div>
<OnlineTime v-if="showOnlineTime" />
<PublishDialog ref="PublishDialogRef" />
<!-- 活动相关全局组件 -->
<CgGuide />
<RewardToast />
<!-- <SatisfactionSurvey /> -->
</template>
<script setup lang="tsx" name="CultureLayout">
import { Search } from '@element-plus/icons-vue'
import OnlineTime from './components/onlineTime.vue'
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
import PublishDialog from './components/publishDialog.vue'
import CgGuide from '@/components/common/CgGuide/index.vue'
import RewardToast from '@/components/common/RewardToast/index.vue'
// import SatisfactionSurvey from '@/components/common/SatisfactionSurvey/index.vue'
import { ArticleTypeEnum } from '@/constants'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useActivityStore } from '@/stores/activity'
import RewardButton from '@/components/common/RewardButton/index.vue'
import { RewardButtonEnum } from '@/constants'
const userStore = useUserStore()
const activityStore = useActivityStore()
const { userInfo } = storeToRefs(userStore)
const router = useRouter()
const route = useRoute()
const isHomePage = computed(() => route.path.includes('/homePage'))
const search = ref('')
const PublishDialogRef = useTemplateRef<InstanceType<typeof PublishDialog>>('PublishDialogRef')
console.log(route)
const showSearchInupt = computed(() => route.path !== '/searchPage')
// 获取二级路由的 key
const getSecondLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
......@@ -174,12 +212,17 @@ const handlePost = async (type: ArticleTypeEnum) => {
router.push(`/publishLongArticle/${type}`)
} else if (type === ArticleTypeEnum.PRACTICE) {
router.push(`/publishLongArticle/${type}`)
// PublishDialogRef.value?.open(type)
} else {
router.push(`/publishLongArticle/${type}`)
}
}
const isDropdownHover = ref(false)
// 首页触发CG相关引导
onMounted(() => {
console.log(1222)
activityStore.triggerCgGuide()
})
</script>
<style lang="scss" scoped>
.layout-culture {
......
import { defineStore } from 'pinia'
import { getUserLaunchCampaignStatus, markUserGuide, markUserClickedRewardButton } from '@/api'
import dayjs from 'dayjs'
import { RewardButtonEnum } from '@/constants'
import { useYaBiStore } from './yabi'
const STORAGE_KEYS = {
CG_WATCHED: 'activity_cg_watched',
CLAIMED_PAGES: 'activity_claimed_pages',
CLICK_COUNT: 'activity_click_count',
SURVEY_SUBMITTED: 'activity_survey_submitted',
} as const
export const useActivityStore = defineStore('activity', () => {
// ========== CG 引导 ==========
const isLaunchCampaignTime = ref(false)
// 2026.3.16 00:00:00 - 2026.3.22 23:59:59
const launchCampaignRange: [number, number] = [
dayjs('2026-03-16 00:00:00').unix(),
dayjs('2026-03-22 23:59:59').unix(),
]
// cg动画引导是否显示
const showCgGuide = ref(false)
// 普通的动画引导
const showCommonGuide = ref(false)
const markUserGuideWatched = async (key: 'isView' | 'isViewGuide') => {
await markUserGuide(key)
}
const triggerCgGuide = async () => {
const { data } = await getUserLaunchCampaignStatus()
// 用户是否观看过CG
if (!data.isView) {
showCgGuide.value = true
}
if (!data.isViewGuide) {
showCommonGuide.value = true
}
// 是否在活动期间
if (data.currentTime >= launchCampaignRange[0] && data.currentTime <= launchCampaignRange[1]) {
isLaunchCampaignTime.value = true
}
}
// ========== 碎片奖励动画 ==========
const showRewardAnimation = ref(false)
const rewardText = ref({ fragment: 1, yabi: 2 })
const triggerPageReward = async (pageKey: RewardButtonEnum) => {
const { data } = await markUserClickedRewardButton(pageKey)
const yabiStore = useYaBiStore()
yabiStore.fetchYaBiData()
if (data) {
setTimeout(() => {
showRewardAnimation.value = true
setTimeout(() => {
showRewardAnimation.value = false
}, 2500)
}, 500)
}
}
// ========== 满意度问卷 ==========
const clickCount = ref(0)
const surveySubmitted = ref(false)
const showSurvey = ref(false)
const surveyThreshold = ref(999999)
const incrementClickCount = () => {
if (surveySubmitted.value) return
clickCount.value++
localStorage.setItem(STORAGE_KEYS.CLICK_COUNT, String(clickCount.value))
if (clickCount.value >= surveyThreshold.value && !surveySubmitted.value) {
showSurvey.value = true
}
}
const markSurveySubmitted = () => {
surveySubmitted.value = true
localStorage.setItem(STORAGE_KEYS.SURVEY_SUBMITTED, '1')
showSurvey.value = false
}
return {
isLaunchCampaignTime,
// CG
// hasWatchedCg,
showCgGuide,
showCommonGuide,
markUserGuideWatched,
triggerCgGuide,
// 碎片奖励
showRewardAnimation,
rewardText,
triggerPageReward,
// 问卷
clickCount,
surveySubmitted,
showSurvey,
surveyThreshold,
incrementClickCount,
markSurveySubmitted,
}
})
......@@ -6,3 +6,4 @@ export * from './video'
export * from './question'
export * from './yabi'
export * from './onlineTime'
export * from './activity'
......@@ -27,7 +27,22 @@
class="flex-1 flex items-center justify-center cursor-pointer relative transition-all duration-300 group"
@click="toggleTab(tab)"
>
<RewardButton v-if="tab.pageKey" :pageKey="tab.pageKey">
<div
class="flex items-center gap-2 px-12 py-2.5 rounded-lg transition-all duration-300"
:class="{
'bg-#fffdfd shadow-[inset_0_2px_4px_0_rgb(0,0,0,0.1)]': activeTab === tab.name,
'hover:bg-white/60': activeTab !== tab.name,
}"
>
<svg-icon :name="tab.svg" class="h-60px w-auto md:h-50px sm:h-40px" size="40" />
<div class="text-18px font-500 text-gray-800 md:text-16px sm:text-14px">
{{ tab.name }}
</div>
</div>
</RewardButton>
<div
v-else
class="flex items-center gap-2 px-12 py-2.5 rounded-lg transition-all duration-300"
:class="{
'bg-#fffdfd shadow-[inset_0_2px_4px_0_rgb(0,0,0,0.1)]': activeTab === tab.name,
......@@ -92,7 +107,7 @@
</el-tooltip>
</div>
<div class="text-amber-600 font-medium lg:text-base text-sm">
YA币:{{ userAccountData.ayabiAvailable }}
YA币:{{ yabiData.currentValue }}
</div>
</div>
</div>
......@@ -109,10 +124,10 @@
</el-button>
<!-- 双倍亚币角标 -->
<div
v-if="activityStore.isDoubleYabi && !userRecordData.isSign"
class="absolute -top-2.5 -right-2 px-1.5 py-0.5 rounded-full bg-gradient-to-r from-red-500 to-pink-500 text-white text-[10px] font-bold leading-none shadow-sm animate-bounce whitespace-nowrap z-1"
v-if="activityStore.isLaunchCampaignTime && !userRecordData.isSign"
class="absolute -top-2.5 -right-2 px-1.5 py-0.5 rounded-lg bg-gradient-to-r from-red-500 to-pink-500 text-white text-[10px] font-bold leading-none shadow-sm animate-bounce whitespace-nowrap z-1"
>
双倍亚币
活动期间双倍亚币
</div>
</div>
<el-button
......@@ -136,54 +151,63 @@
分享您在工作或文化践行中的优秀案例,公开有奖,有机会收录至内部经验库,沉淀经验,赋能组织成长。
</p>
<div class="flex justify-center items-center">
<el-button
class="bg-[linear-gradient(to_right,#A3EADC_0%,#7BE0BD_100%)] shadow-[0_1px_4px_0_rgba(168,225,210,1)] border-none hover:-translate-y-1 transition-all duration-200 text-xs sm:text-sm w-112px lg:w-116px"
type="primary"
@click="router.push('/publishCase')"
>
<svg-icon name="submit" size="20" class="mr-2" />
<span class="text-#333 text-xs sm:text-sm">去投稿</span>
</el-button>
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_CASE">
<el-button
class="bg-[linear-gradient(to_right,#A3EADC_0%,#7BE0BD_100%)] shadow-[0_1px_4px_0_rgba(168,225,210,1)] border-none hover:-translate-y-1 transition-all duration-200 text-xs sm:text-sm w-112px lg:w-116px"
type="primary"
@click="router.push('/publishCase')"
>
<svg-icon name="submit" size="20" class="mr-2" />
<span class="text-#333 text-xs sm:text-sm">去投稿</span>
</el-button>
</RewardButton>
</div>
</div>
<div class="submit-container common-box rounded-lg bg-#EDEAFE">
<div class="grid grid-cols-3 mb-4 gap-0 2xl:gap-2">
<div
class="flex flex-col items-center justify-center text-center cursor-pointer hover:-translate-y-1 transition-transform duration-200 lg:p-2 rounded-lg hover:bg-white/10"
@click="publishTopic"
>
<svg-icon name="topic_release" size="80" />
<div class="text-xs xl:text-sm">话题发布</div>
</div>
<div
class="flex flex-col items-center justify-center text-center cursor-pointer hover:-translate-y-1 transition-transform duration-200 lg:p-2 rounded-lg hover:bg-white/10"
@click="router.push('/userPage/selfAnswer')"
>
<el-badge :value="userQestionNum" :offset="[-5, 20]" :hidden="!userQestionNum">
<svg-icon name="answer" size="80" />
</el-badge>
<div class="text-xs xl:text-sm">回答问题</div>
</div>
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_QUESTION">
<div
class="flex flex-col items-center justify-center text-center cursor-pointer hover:-translate-y-1 transition-transform duration-200 lg:p-2 rounded-lg hover:bg-white/10"
@click="publishTopic"
>
<svg-icon name="topic_release" size="80" />
<div class="text-xs xl:text-sm">话题发布</div>
</div>
</RewardButton>
<RewardButton :pageKey="RewardButtonEnum.SELF_ANSWER">
<div
class="flex flex-col items-center justify-center text-center cursor-pointer hover:-translate-y-1 transition-transform duration-200 lg:p-2 rounded-lg hover:bg-white/10"
@click="router.push('/userPage/selfAnswer')"
>
<el-badge :value="userQestionNum" :offset="[-5, 20]" :hidden="!userQestionNum">
<svg-icon name="answer" size="80" />
</el-badge>
<div
@click="router.push('/publishVideo')"
class="flex flex-col items-center justify-center text-center cursor-pointer hover:-translate-y-1 transition-transform duration-200 lg:p-2 rounded-lg hover:bg-white/10"
>
<svg-icon name="video_release" size="80" />
<div class="text-xs xl:text-sm">视频发布</div>
</div>
<div class="text-xs xl:text-sm">回答问题</div>
</div>
</RewardButton>
<RewardButton :pageKey="RewardButtonEnum.PUBLISH_VIDEO">
<div
@click="router.push('/publishVideo')"
class="flex flex-col items-center justify-center text-center cursor-pointer hover:-translate-y-1 transition-transform duration-200 lg:p-2 rounded-lg hover:bg-white/10"
>
<svg-icon name="video_release" size="80" />
<div class="text-xs xl:text-sm">视频发布</div>
</div>
</RewardButton>
</div>
<div class="flex justify-center items-center">
<el-button
@click="router.push(`/userPage/selfComment?type=${ArticleTypeEnum.QUESTION}`)"
class="bg-[linear-gradient(to_right,#D6C9FF_0%,#C5B1FF_100%)] shadow-[0_1px_4px_0_rgba(95,0,237,0.25)] border-none hover:-translate-y-1 transition-all duration-200 text-xs sm:text-sm w-112px lg:w-116px"
type="primary"
>
<svg-icon name="my_answer" size="20" class="mr-2" />
<span class="text-#333 text-xs sm:text-sm">我的回答</span>
</el-button>
<RewardButton :pageKey="RewardButtonEnum.SELF_COMMENT">
<el-button
@click="router.push(`/userPage/selfComment?type=${ArticleTypeEnum.QUESTION}`)"
class="bg-[linear-gradient(to_right,#D6C9FF_0%,#C5B1FF_100%)] shadow-[0_1px_4px_0_rgba(95,0,237,0.25)] border-none hover:-translate-y-1 transition-all duration-200 text-xs sm:text-sm w-112px lg:w-116px"
type="primary"
>
<svg-icon name="my_answer" size="20" class="mr-2" />
<span class="text-#333 text-xs sm:text-sm">我的回答</span>
</el-button>
</RewardButton>
</div>
</div>
......@@ -371,12 +395,13 @@ import type {
} from '@/api'
import { TABS_REF_KEY, levelListOptions } from '@/constants'
import { useScrollTop } from '@/hooks'
import { useQuestionStore } from '@/stores/question'
import { useActivityStore } from '@/stores/activity'
import { useQuestionStore, useActivityStore, useYaBiStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { push } from 'notivue'
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'
// import LuckyWheel from '@/components/common/LuckyWheel/index.vue'
import { RewardButtonEnum } from '@/constants'
import RewardButton from '@/components/common/RewardButton/index.vue'
const breakpoints = useBreakpoints(breakpointsTailwind)
const isSmallerThanXl = breakpoints.smaller('xl')
......@@ -391,7 +416,8 @@ const { handleBackTop } = useScrollTop(levelContainerRef)
const questionStore = useQuestionStore()
const { userQestionNum } = storeToRefs(questionStore)
const activityStore = useActivityStore()
const yabiStore = useYaBiStore()
const { yabiData } = storeToRefs(yabiStore)
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
// console.log(route.fullPath, '三级路由首页')
// console.log(route.path, 11111111111111)
......@@ -403,9 +429,9 @@ const carouselList = ref<CarouselItemDto[]>([])
const tabsRef = useTemplateRef('tabsRef')
const tabs = [
{ name: '首页', path: 'homeTab', img: front, svg: 'home' },
{ name: 'YA文化', path: 'yaTab', img: ya, svg: 'ya_culture' },
{ name: '问吧', path: 'askTab', img: ask, svg: 'ask' },
{ name: '首页', path: 'homeTab', img: front, svg: 'home', pageKey: undefined },
{ name: 'YA文化', path: 'yaTab', img: ya, svg: 'ya_culture', pageKey: RewardButtonEnum.YA_TAB },
{ name: '问吧', path: 'askTab', img: ask, svg: 'ask', pageKey: RewardButtonEnum.ASK_TAB },
]
const activeTab = ref(
......@@ -482,7 +508,9 @@ const onDailySign = async () => {
await dailySign()
refreshTaskData(true)
refreshUserAccountData()
push.success(activityStore.isDoubleYabi ? '签到成功!活动期间双倍亚币已到账 🎉' : '签到成功')
push.success(
activityStore.isLaunchCampaignTime ? '签到成功!活动期间双倍亚币已到账 🎉' : '签到成功',
)
open.value = false
}
......
......@@ -33,13 +33,19 @@ import ColumnList from './components/columnList.vue'
import InterviewList from './components/interviewList.vue'
import PracticeList from './components/practiceList.vue'
import VideoList from './components/videoList.vue'
import { RewardButtonEnum } from '@/constants'
const tabs = [
{ label: '专栏', value: '专栏', component: ColumnList },
{ label: '实践', value: '实践', component: PracticeList },
{ label: '专访', value: '专访', component: InterviewList },
{ label: '视频', value: '视频', component: VideoList },
{ label: '关爱基金', value: '关爱基金', component: null },
{ label: '专栏', value: '专栏', component: ColumnList, pageKey: undefined },
{ label: '实践', value: '实践', component: PracticeList, pageKey: RewardButtonEnum.YA_PRACTICE },
{
label: '专访',
value: '专访',
component: InterviewList,
pageKey: RewardButtonEnum.YA_INTERVIEW,
},
{ label: '视频', value: '视频', component: VideoList, pageKey: RewardButtonEnum.YA_VIDEO },
{ label: '关爱基金', value: '关爱基金', component: null, pageKey: undefined },
]
const activeTab = ref('专栏')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment