소스 검색

feat: 新手引导初始化

longyang 1 개월 전
부모
커밋
041b3dd222
4개의 변경된 파일849개의 추가작업 그리고 1개의 파일을 삭제
  1. 221 0
      src/components/NewUserGuide/index.vue
  2. 276 0
      src/components/NewUserGuide/steps/Step1.vue
  3. 335 0
      src/components/NewUserGuide/steps/Step2.vue
  4. 17 1
      src/views/HomeView.vue

+ 221 - 0
src/components/NewUserGuide/index.vue

@@ -0,0 +1,221 @@
+<template>
+  <transition name="guide-fade">
+    <div class="new-user-guide-mask" v-if="visible">
+      <!-- 跳过按钮 -->
+      <div class="guide-skip" @click="handleSkip">跳过</div>
+
+      <!-- 步骤指示器(动态自适应已注册步骤数) -->
+      <div class="guide-steps-indicator">
+        <span
+          v-for="i in totalSteps"
+          :key="i"
+          class="step-dot"
+          :class="{ active: i === currentStep, done: i < currentStep }"
+          @click="handleDotClick(i)"
+          :title="'第 ' + i + ' 步'"
+        ></span>
+      </div>
+
+      <!-- 步骤内容区域 -->
+      <div class="guide-content-wrapper" @click.self="handleMaskClick">
+        <transition name="guide-step" mode="out-in">
+          <Step1
+            v-if="currentStep === 1"
+            key="step1"
+            :isLast="isLastStep"
+            @next="goNext"
+          />
+          <Step2
+            v-if="currentStep === 2"
+            key="step2"
+            :isLast="isLastStep"
+            @next="goNext"
+          />
+          <!-- 动态注册更多步骤:Step3 ~ StepN 在此追加 -->
+        </transition>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script>
+import Step1 from './steps/Step1.vue'
+import Step2 from './steps/Step2.vue'
+
+// 步骤组件注册表 —— 新增步骤只需在此数组末尾追加即可
+const stepComponents = [Step1, Step2]
+
+export default {
+  name: 'NewUserGuide',
+  components: { Step1, Step2 },
+  data() {
+    return {
+      visible: false,
+      currentStep: 1,
+      totalSteps: stepComponents.length, // 动态自适应
+    }
+  },
+  computed: {
+    /** 当前是否为最后一步 */
+    isLastStep() {
+      return this.currentStep === this.totalSteps
+    },
+  },
+  methods: {
+    /** 显示引导(可由外部调用) */
+    show() {
+      this.visible = true
+      this.currentStep = 1
+      this.totalSteps = stepComponents.length // 刷新步骤数
+    },
+
+    /** 下一步 */
+    goNext() {
+      if (this.currentStep < this.totalSteps) {
+        this.currentStep++
+      } else {
+        this.handleFinish()
+      }
+    },
+
+    /** 点击步骤圆点跳转 */
+    handleDotClick(step) {
+      // 只允许跳转到已完成的步骤或当前步骤的下一步
+      if (step < this.currentStep || step === this.currentStep + 1) {
+        this.currentStep = step
+      }
+    },
+
+    /** 点击蒙层(Step1 可忽略,其他步骤弹确认) */
+    handleMaskClick() {
+      if (this.currentStep === 1) return
+      this.handleSkip()
+    },
+
+    /** 跳过引导 */
+    handleSkip() {
+      // 临时降低遮罩层 z-index,让 Element UI MessageBox 显示在上方
+      const mask = this.$el
+      if (mask) mask.style.zIndex = '1000'
+      this.$confirm(
+        '跳过引导将错过新手积分奖励,确定跳过?',
+        '跳过引导',
+        {
+          confirmButtonText: '确定跳过',
+          cancelButtonText: '继续引导',
+          type: 'warning',
+          customClass: 'guide-skip-confirm',
+        }
+      )
+        .then(() => {
+          if (mask) mask.style.zIndex = ''
+          this.handleFinish()
+        })
+        .catch(() => {
+          if (mask) mask.style.zIndex = ''
+          // 用户选择继续引导
+        })
+    },
+
+    /** 完成引导 */
+    handleFinish() {
+      this.visible = false
+      this.$emit('finish')
+    },
+  },
+}
+</script>
+
+<style scoped>
+.new-user-guide-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 3000;
+  background-color: rgba(0, 0, 0, 0.55);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 跳过按钮 */
+.guide-skip {
+  position: absolute;
+  top: 24px;
+  right: 32px;
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.75);
+  cursor: pointer;
+  padding: 6px 16px;
+  border-radius: 20px;
+  border: 1px solid rgba(255, 255, 255, 0.3);
+  transition: all 0.25s;
+  z-index: 3001;
+}
+.guide-skip:hover {
+  color: #fff;
+  border-color: rgba(255, 255, 255, 0.7);
+  background: rgba(255, 255, 255, 0.1);
+}
+
+/* 步骤指示器 */
+.guide-steps-indicator {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 32px;
+}
+.step-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  background-color: rgba(255, 255, 255, 0.3);
+  transition: all 0.3s;
+  cursor: default;
+}
+.step-dot.done {
+  background-color: rgba(255, 255, 255, 0.75);
+  cursor: pointer;
+}
+.step-dot.done:hover {
+  background-color: #fff;
+  transform: scale(1.2);
+}
+.step-dot.active {
+  width: 28px;
+  border-radius: 5px;
+  background-color: #2C6FBF;
+}
+
+/* 内容区域 */
+.guide-content-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* ---- 动画 ---- */
+.guide-fade-enter-active,
+.guide-fade-leave-active {
+  transition: opacity 0.35s ease;
+}
+.guide-fade-enter,
+.guide-fade-leave-to {
+  opacity: 0;
+}
+
+.guide-step-enter-active,
+.guide-step-leave-active {
+  transition: all 0.35s ease;
+}
+.guide-step-enter {
+  opacity: 0;
+  transform: translateX(40px);
+}
+.guide-step-leave-to {
+  opacity: 0;
+  transform: translateX(-40px);
+}
+</style>

+ 276 - 0
src/components/NewUserGuide/steps/Step1.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="step1-card">
+    <!-- 顶部插画区域 -->
+    <div class="step1-illustration">
+      <div class="illustration-bubble bubble-1">🎁</div>
+      <div class="illustration-bubble bubble-2">⭐</div>
+      <div class="illustration-bubble bubble-3">🏆</div>
+      <div class="illustration-icon-wrap">
+        <div class="illustration-icon">🎉</div>
+        <div class="illustration-ring ring-1"></div>
+        <div class="illustration-ring ring-2"></div>
+      </div>
+    </div>
+
+    <!-- 文字内容 -->
+    <div class="step1-body">
+      <h2 class="step1-title">欢迎来到誉商城!</h2>
+      <p class="step1-desc">
+        这里是你的<strong>专属积分奖励平台</strong><br />
+        完成任务赚积分,积分兑换好礼!
+      </p>
+
+      <!-- 特性展示 -->
+      <div class="step1-features">
+        <div class="feature-item">
+          <span class="feature-icon">📌</span>
+          <span class="feature-text">每日签到赚积分</span>
+        </div>
+        <div class="feature-item">
+          <span class="feature-icon">🛒</span>
+          <span class="feature-text">积分商城兑好礼</span>
+        </div>
+        <div class="feature-item">
+          <span class="feature-icon">🎊</span>
+          <span class="feature-text">参与活动赢大奖</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="step1-footer">
+      <button class="btn-start" @click="$emit('next')">
+        {{ isLast ? '完成新手教程' : '开始探索' }}
+        <span class="btn-arrow" v-if="!isLast">→</span>
+        <span class="btn-check" v-else>✓</span>
+      </button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Step1',
+  props: {
+    isLast: {
+      type: Boolean,
+      default: false,
+    },
+  },
+}
+</script>
+
+<style scoped>
+.step1-card {
+  width: 440px;
+  max-width: 90vw;
+  background: #fff;
+  border-radius: 20px;
+  overflow: hidden;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
+  animation: cardFloat 0.5s ease-out;
+}
+
+@keyframes cardFloat {
+  from {
+    opacity: 0;
+    transform: translateY(30px) scale(0.95);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+}
+
+/* ---- 插画区域 ---- */
+.step1-illustration {
+  position: relative;
+  height: 180px;
+  background: linear-gradient(135deg, #2C6FBF 0%, #5B9FE6 50%, #87CEEB 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+
+.illustration-icon-wrap {
+  position: relative;
+  z-index: 2;
+}
+
+.illustration-icon {
+  width: 80px;
+  height: 80px;
+  line-height: 80px;
+  text-align: center;
+  font-size: 52px;
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 50%;
+  backdrop-filter: blur(4px);
+}
+
+.illustration-ring {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  border: 2px solid rgba(255, 255, 255, 0.2);
+  border-radius: 50%;
+  animation: ringPulse 3s ease-in-out infinite;
+}
+
+.ring-1 {
+  width: 120px;
+  height: 120px;
+  margin-top: -60px;
+  margin-left: -60px;
+  animation-delay: 0s;
+}
+
+.ring-2 {
+  width: 160px;
+  height: 160px;
+  margin-top: -80px;
+  margin-left: -80px;
+  animation-delay: 1.5s;
+}
+
+@keyframes ringPulse {
+  0%, 100% {
+    transform: scale(1);
+    opacity: 0.3;
+  }
+  50% {
+    transform: scale(1.1);
+    opacity: 0.6;
+  }
+}
+
+.illustration-bubble {
+  position: absolute;
+  font-size: 24px;
+  z-index: 1;
+  animation: bubbleFloat 4s ease-in-out infinite;
+}
+
+.bubble-1 {
+  top: 20px;
+  left: 30px;
+  animation-delay: 0s;
+}
+.bubble-2 {
+  top: 30px;
+  right: 40px;
+  animation-delay: 1.3s;
+}
+.bubble-3 {
+  bottom: 25px;
+  left: 50%;
+  transform: translateX(-50%);
+  animation-delay: 2.6s;
+}
+
+@keyframes bubbleFloat {
+  0%, 100% { transform: translateY(0); }
+  50% { transform: translateY(-10px); }
+}
+
+/* ---- 内容区域 ---- */
+.step1-body {
+  padding: 28px 32px 20px;
+  text-align: center;
+}
+
+.step1-title {
+  font-size: 22px;
+  font-weight: 700;
+  color: #1d2129;
+  margin: 0 0 12px;
+}
+
+.step1-desc {
+  font-size: 14px;
+  line-height: 1.8;
+  color: #86909c;
+  margin: 0 0 24px;
+}
+
+.step1-desc strong {
+  color: #2C6FBF;
+}
+
+/* ---- 特性卡片 ---- */
+.step1-features {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+}
+
+.feature-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  flex: 1;
+  padding: 14px 8px;
+  background: #f7f9fc;
+  border-radius: 12px;
+  transition: all 0.25s;
+}
+
+.feature-item:hover {
+  background: #eaf2ff;
+  transform: translateY(-2px);
+}
+
+.feature-icon {
+  font-size: 22px;
+}
+
+.feature-text {
+  font-size: 12px;
+  color: #4e5969;
+  font-weight: 500;
+}
+
+/* ---- 底部按钮 ---- */
+.step1-footer {
+  padding: 0 32px 28px;
+  text-align: center;
+}
+
+.btn-start {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  width: 100%;
+  height: 48px;
+  font-size: 16px;
+  font-weight: 600;
+  color: #fff;
+  background: linear-gradient(135deg, #2C6FBF, #5B9FE6);
+  border: none;
+  border-radius: 24px;
+  cursor: pointer;
+  transition: all 0.3s;
+  box-shadow: 0 4px 16px rgba(44, 111, 191, 0.35);
+}
+
+.btn-start:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 6px 20px rgba(44, 111, 191, 0.5);
+}
+
+.btn-start:active {
+  transform: translateY(0);
+}
+
+.btn-arrow {
+  font-size: 18px;
+  transition: transform 0.25s;
+}
+
+.btn-start:hover .btn-arrow {
+  transform: translateX(4px);
+}
+</style>

+ 335 - 0
src/components/NewUserGuide/steps/Step2.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="step2-card">
+    <!-- 左侧:说明区域 -->
+    <div class="step2-info">
+      <div class="step2-badge">Step 2</div>
+      <h2 class="step2-title">每日签到,积分到手</h2>
+      <p class="step2-desc">
+        每天来签到,轻松赚取积分奖励!<br />
+        <strong>连续签到</strong>还能获得额外加成 🎁
+      </p>
+
+      <!-- 签到日历示意 -->
+      <div class="checkin-preview">
+        <div class="preview-header">
+          <span class="preview-weekday" v-for="d in weekDays" :key="d">{{ d }}</span>
+        </div>
+        <div class="preview-row">
+          <div
+            v-for="(day, idx) in checkinDays"
+            :key="idx"
+            class="preview-day"
+            :class="{
+              checked: day.checked,
+              today: day.isToday,
+              reward: day.isReward,
+            }"
+          >
+            <span class="day-num">{{ day.num }}</span>
+            <span v-if="day.isReward" class="day-bonus">+{{ day.bonus }}</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="step2-tip">
+        <span class="tip-icon">💡</span>
+        <span>连续签到 7 天可领取额外积分大礼包</span>
+      </div>
+    </div>
+
+    <!-- 右侧:操作指引 -->
+    <div class="step2-action">
+      <div class="action-arrow-area">
+        <div class="arrow-text">点击左侧菜单</div>
+        <div class="arrow-icon">「每日签到」</div>
+        <div class="arrow-down">
+          <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
+            <path d="M20 8 L20 30" stroke="#2C6FBF" stroke-width="2.5" stroke-linecap="round" />
+            <path d="M12 22 L20 30 L28 22" stroke="#2C6FBF" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
+          </svg>
+        </div>
+      </div>
+
+      <button class="btn-checkin" @click="handleGoSignIn">
+        <span class="btn-icon">✅</span>
+        去签到
+      </button>
+      <button class="btn-next" @click="$emit('next')">
+        {{ isLast ? '完成新手教程 ✓' : '下一步' }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Step2',
+  props: {
+    isLast: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      weekDays: ['一', '二', '三', '四', '五', '六', '日'],
+      checkinDays: [
+        { num: 1, checked: true, isToday: false, isReward: false, bonus: 0 },
+        { num: 2, checked: true, isToday: false, isReward: false, bonus: 0 },
+        { num: 3, checked: true, isToday: false, isReward: false, bonus: 0 },
+        { num: 4, checked: true, isToday: true, isReward: true, bonus: 10 },
+        { num: 5, checked: false, isToday: false, isReward: false, bonus: 0 },
+        { num: 6, checked: false, isToday: false, isReward: false, bonus: 0 },
+        { num: 7, checked: false, isToday: false, isReward: true, bonus: 30 },
+      ],
+    }
+  },
+  methods: {
+    handleGoSignIn() {
+      // 跳转到签到页
+      this.$router.push('/home/signIn')
+      // 延迟后自动进入下一步
+      setTimeout(() => {
+        this.$emit('next')
+      }, 500)
+    },
+  },
+}
+</script>
+
+<style scoped>
+.step2-card {
+  width: 560px;
+  max-width: 92vw;
+  background: #fff;
+  border-radius: 20px;
+  overflow: hidden;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
+  display: flex;
+  animation: cardFloat 0.5s ease-out;
+}
+
+@keyframes cardFloat {
+  from {
+    opacity: 0;
+    transform: translateY(30px) scale(0.95);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+}
+
+/* ---- 左侧信息区 ---- */
+.step2-info {
+  flex: 1;
+  padding: 32px 28px 28px;
+}
+
+.step2-badge {
+  display: inline-block;
+  padding: 3px 12px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #2C6FBF;
+  background: #eaf2ff;
+  border-radius: 12px;
+  margin-bottom: 16px;
+}
+
+.step2-title {
+  font-size: 20px;
+  font-weight: 700;
+  color: #1d2129;
+  margin: 0 0 10px;
+}
+
+.step2-desc {
+  font-size: 14px;
+  line-height: 1.7;
+  color: #86909c;
+  margin: 0 0 20px;
+}
+
+.step2-desc strong {
+  color: #2C6FBF;
+}
+
+/* ---- 签到日历预览 ---- */
+.checkin-preview {
+  background: #f7f9fc;
+  border-radius: 12px;
+  padding: 12px;
+  margin-bottom: 16px;
+}
+
+.preview-header {
+  display: flex;
+  margin-bottom: 8px;
+}
+
+.preview-weekday {
+  flex: 1;
+  text-align: center;
+  font-size: 11px;
+  color: #86909c;
+  font-weight: 500;
+}
+
+.preview-row {
+  display: flex;
+  gap: 4px;
+}
+
+.preview-day {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 2px;
+  padding: 6px 0;
+  border-radius: 8px;
+  transition: all 0.25s;
+  position: relative;
+}
+
+.preview-day.checked {
+  background: #eaf2ff;
+}
+
+.preview-day.today {
+  background: #2C6FBF;
+  box-shadow: 0 2px 8px rgba(44, 111, 191, 0.3);
+}
+
+.preview-day.today .day-num {
+  color: #fff;
+  font-weight: 700;
+}
+
+.preview-day.reward:not(.checked) {
+  background: linear-gradient(135deg, #fff5e6, #fff0d6);
+  border: 1px dashed #f5a623;
+}
+
+.day-num {
+  font-size: 14px;
+  font-weight: 600;
+  color: #4e5969;
+}
+
+.day-bonus {
+  font-size: 9px;
+  color: #f5a623;
+  font-weight: 700;
+}
+
+/* ---- 提示条 ---- */
+.step2-tip {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 14px;
+  background: linear-gradient(135deg, #fff9e6, #fff3cc);
+  border-radius: 8px;
+  font-size: 12px;
+  color: #b8860b;
+}
+
+.tip-icon {
+  font-size: 16px;
+}
+
+/* ---- 右侧操作区 ---- */
+.step2-action {
+  width: 180px;
+  background: linear-gradient(180deg, #f0f6ff 0%, #e4eeff 100%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 28px 16px;
+  border-left: 1px solid #e5eaf2;
+}
+
+.action-arrow-area {
+  text-align: center;
+  margin-bottom: 24px;
+}
+
+.arrow-text {
+  font-size: 13px;
+  color: #86909c;
+  margin-bottom: 6px;
+}
+
+.arrow-icon {
+  font-size: 18px;
+  font-weight: 700;
+  color: #2C6FBF;
+  margin-bottom: 12px;
+}
+
+.arrow-down {
+  animation: arrowBounce 1.5s ease-in-out infinite;
+}
+
+@keyframes arrowBounce {
+  0%, 100% { transform: translateY(0); }
+  50% { transform: translateY(6px); }
+}
+
+.btn-checkin {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 28px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #fff;
+  background: linear-gradient(135deg, #2C6FBF, #5B9FE6);
+  border: none;
+  border-radius: 24px;
+  cursor: pointer;
+  transition: all 0.3s;
+  box-shadow: 0 4px 12px rgba(44, 111, 191, 0.3);
+  white-space: nowrap;
+}
+
+.btn-checkin:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 6px 18px rgba(44, 111, 191, 0.45);
+}
+
+.btn-checkin:active {
+  transform: translateY(0);
+}
+
+.btn-icon {
+  font-size: 18px;
+}
+
+.btn-next {
+  margin-top: 12px;
+  width: 100%;
+  padding: 10px 20px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #2C6FBF;
+  background: #fff;
+  border: 1.5px solid #2C6FBF;
+  border-radius: 20px;
+  cursor: pointer;
+  transition: all 0.25s;
+  white-space: nowrap;
+}
+
+.btn-next:hover {
+  background: #eaf2ff;
+  transform: translateY(-1px);
+}
+
+.btn-next:active {
+  transform: translateY(0);
+}
+</style>

+ 17 - 1
src/views/HomeView.vue

@@ -16,16 +16,20 @@
         <el-button style="width:50%;font-size: 24px;" type="danger" :disabled="checked?false:true" @click="getUnlock">确认激活</el-button>
       </span>
     </el-dialog>
+    <!-- 新手指引 -->
+    <NewUserGuide ref="guide" @finish="onGuideFinish" />
   </div>
 </template>
 
 <script>
 import AppSidebar from '@/components/AppSidebar.vue'
+import NewUserGuide from '@/components/NewUserGuide/index.vue'
 import { lockStatus,unlock } from "@/api/allApi";
 export default {
   name: 'HomeView',
   components: {
-    AppSidebar
+    AppSidebar,
+    NewUserGuide
   },
   data() {
     return {
@@ -36,6 +40,14 @@ export default {
   },
   created() {
     this.getHtmldata();
+    // 延迟显示新手指引,等系统规则弹窗检查完成
+    this.$nextTick(() => {
+      setTimeout(() => {
+        if (this.$refs.guide) {
+          this.$refs.guide.show();
+        }
+      }, 800);
+    });
   },
   methods:{
     getHtmldata(){
@@ -51,6 +63,10 @@ export default {
       unlock().then(response=>{
           this.dialogVisible = false;
       })
+    },
+    onGuideFinish() {
+      // 引导完成后的回调(可扩展:标记用户已完成引导等)
+      console.log('新手指引已完成');
     }
   }
 }