Browse Source

feat(课程评论): 添加课程评分功能并优化评论界面

refactor(导航菜单): 简化导航菜单结构
style(国际化): 更新课程阶段和评论相关文案
feat(工作流): 添加步骤进度条和滚动定位功能
ext.zhangbin71 1 week ago
parent
commit
e259826d08

+ 8 - 8
src/App.vue

@@ -390,20 +390,20 @@ const navigation = ref([
     name: 'common.gongzuoliu',
     name: 'common.gongzuoliu',
     href: '#',
     href: '#',
     path: '/',
     path: '/',
-    children: [
-      { path: '/', icon: '🔍', label: t('common.gongzuoliu_search'), desc: t('common.gongzuoliu_search_desc') },
-      { path: '/workflow-trade', icon: '💼', label: t('common.gongzuoliu_trade'), desc: t('common.gongzuoliu_trade_desc') },
-    ]
+    // children: [
+    //   { path: '/', icon: '🔍', label: t('common.gongzuoliu_search'), desc: t('common.gongzuoliu_search_desc') },
+    //   { path: '/workflow-trade', icon: '💼', label: t('common.gongzuoliu_trade'), desc: t('common.gongzuoliu_trade_desc') },
+    // ]
   },
   },
   { name: 'common.gongzuoliu_trade', href: '#', path: '/workflow-trade' },
   { name: 'common.gongzuoliu_trade', href: '#', path: '/workflow-trade' },
   {
   {
     name: 'route.learning_system',
     name: 'route.learning_system',
     href: '#',
     href: '#',
     path: '/learning-system',
     path: '/learning-system',
-    children: [
-      { path: '/learning-system', icon: '🎓', label: t('common.learning_course'), desc: t('common.learning_course_desc') },
-      { path: '/learn-note', icon: '📝', label: t('common.learning_note'), desc: t('common.learning_note_desc') },
-    ]
+    // children: [
+    //   { path: '/learning-system', icon: '🎓', label: t('common.learning_course'), desc: t('common.learning_course_desc') },
+    //   { path: '/learn-note', icon: '📝', label: t('common.learning_note'), desc: t('common.learning_note_desc') },
+    // ]
   },
   },
   { name: 'common.xuxibiji', href: '#', path: '/learn-note' },
   { name: 'common.xuxibiji', href: '#', path: '/learn-note' },
   { name: 'route.mibiShop', href: '#', path: '/mibi-shop' },
   { name: 'route.mibiShop', href: '#', path: '/mibi-shop' },

+ 4 - 0
src/api/course.js

@@ -20,4 +20,8 @@ export function getChaptersList(data = {}) {
 // 新增删除收藏记录(未被收藏新增,否则删除)
 // 新增删除收藏记录(未被收藏新增,否则删除)
 export function collect(data = {}) {
 export function collect(data = {}) {
   return request.post('/collect',data)
   return request.post('/collect',data)
+}
+// 课程数据
+export function getCourseSummary(data = {}) {
+  return request.get('/index/courseSummary',data)
 }
 }

+ 4 - 2
src/locales/en.js

@@ -136,6 +136,7 @@ export default {
     search:"Search",
     search:"Search",
     mibiShopSubtitle:"Exchange exquisite gifts, enrich learning experience",
     mibiShopSubtitle:"Exchange exquisite gifts, enrich learning experience",
     learnNoteSubtitle:"Record learning moments, accumulate knowledge essence",
     learnNoteSubtitle:"Record learning moments, accumulate knowledge essence",
+    courseRating:"Course Rating",
   },
   },
   login: {
   login: {
     smsLogin: 'Captcha Login',
     smsLogin: 'Captcha Login',
@@ -206,14 +207,15 @@ export default {
     hotLabel1: 'Popular Course',
     hotLabel1: 'Popular Course',
     hotLabel2: 'Hot Recommendation',
     hotLabel2: 'Hot Recommendation',
     hotLabel3: 'New Course',
     hotLabel3: 'New Course',
-    stageBasic: 'Beginner',
+    stageBasic: 'Basic',
     stageIntermediate: 'Intermediate',
     stageIntermediate: 'Intermediate',
     stageAdvanced: 'Advanced',
     stageAdvanced: 'Advanced',
     stageAll: 'All Levels',
     stageAll: 'All Levels',
     previewCourse1: 'n8n Automation Intro',
     previewCourse1: 'n8n Automation Intro',
     previewCourse2: 'Coze Bot Development',
     previewCourse2: 'Coze Bot Development',
     previewCourse3: 'Dify RAG Application',
     previewCourse3: 'Dify RAG Application',
-    reviewCount: 'reviews',
+    reviewCount: 'comments',
+    noRating: 'No Rating',
     chapterCount: 'chapters',
     chapterCount: 'chapters',
     courseDetail: 'Course Detail',
     courseDetail: 'Course Detail',
     tenThousand: 'k'
     tenThousand: 'k'

+ 5 - 3
src/locales/zh-CN.js

@@ -141,6 +141,7 @@ export default {
     search:"搜索",
     search:"搜索",
     mibiShopSubtitle:"兑换精美礼品,丰富学习体验",
     mibiShopSubtitle:"兑换精美礼品,丰富学习体验",
     learnNoteSubtitle:"记录学习点滴,沉淀知识精华",
     learnNoteSubtitle:"记录学习点滴,沉淀知识精华",
+    courseRating:"课程评分",
   },
   },
   login: {
   login: {
     smsLogin: '验证码登录',
     smsLogin: '验证码登录',
@@ -208,14 +209,15 @@ export default {
     hotLabel1: '爆款课程',
     hotLabel1: '爆款课程',
     hotLabel2: '热门推荐',
     hotLabel2: '热门推荐',
     hotLabel3: '新课上线',
     hotLabel3: '新课上线',
-    stageBasic: '入门级',
-    stageIntermediate: '进阶级',
+    stageBasic: '级',
+    stageIntermediate: '级',
     stageAdvanced: '高级',
     stageAdvanced: '高级',
     stageAll: '全阶段',
     stageAll: '全阶段',
     previewCourse1: 'n8n 自动化入门',
     previewCourse1: 'n8n 自动化入门',
     previewCourse2: 'Coze Bot 开发',
     previewCourse2: 'Coze Bot 开发',
     previewCourse3: 'Dify RAG 应用',
     previewCourse3: 'Dify RAG 应用',
-    reviewCount: '人评价',
+    reviewCount: '条评论',
+    noRating: '暂无评分',
     chapterCount: '节课程',
     chapterCount: '节课程',
     courseDetail: '课程详情',
     courseDetail: '课程详情',
     tenThousand: '万'
     tenThousand: '万'

File diff suppressed because it is too large
+ 1289 - 451
src/pages/LearningSystem/LearningSystem.vue


+ 329 - 6
src/pages/LearningSystem/components/pinglun.vue

@@ -9,8 +9,43 @@
       <div class="avatar-wrapper">
       <div class="avatar-wrapper">
         <el-avatar :size="40" :src="appStore.avatarDefault" class="user-avatar" />
         <el-avatar :size="40" :src="appStore.avatarDefault" class="user-avatar" />
       </div>
       </div>
-      
+          
       <div class="input-container">
       <div class="input-container">
+        <!-- 评分选择器 -->
+        <div class="rating-selector">
+          <span class="rating-label">{{$t('common.courseRating')}}</span>
+          <div class="stars-container">
+            <div 
+              v-for="star in 5" 
+              :key="star"
+              class="star-wrapper"
+              @mousemove="handleStarMouseMove($event, star)"
+              @mouseleave="handleStarLeave"
+              @click="handleStarClick(star)"
+            >
+              <!-- 背景空星 -->
+              <svg class="star-icon star-empty" viewBox="0 0 24 24" fill="currentColor">
+                <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
+              </svg>
+              
+              <!-- 前景实星 - 通过 clip-path 控制显示比例 -->
+              <svg 
+                class="star-icon star-filled"
+                :class="{ 
+                  'star-half': getStarFillRatio(star) === 0.5,
+                  'star-full': getStarFillRatio(star) === 1
+                }"
+                viewBox="0 0 24 24" 
+                fill="currentColor"
+                :style="{ clipPath: getStarClipPath(star) }"
+              >
+                <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
+              </svg>
+            </div>
+          </div>
+          <span v-if="ratingScore > 0" class="rating-text">{{ ratingScore }} 星</span>
+        </div>
+            
         <div class="input-wrapper">
         <div class="input-wrapper">
           <el-input 
           <el-input 
             type="textarea" 
             type="textarea" 
@@ -29,8 +64,8 @@
             @click="handleSend" 
             @click="handleSend" 
             class="submit-btn gradient"
             class="submit-btn gradient"
           >
           >
-            <span v-if="!isSubmiting">{{$t('common.send')}}</span>
-            <span v-else>{{$t('common.sending')}}</span>
+            <span v-if="!isSubmiting">{{ $t('common.send') }}</span>
+            <span v-else>{{ $t('common.sending') }}</span>
           </el-button>
           </el-button>
         </div>
         </div>
       </div>
       </div>
@@ -48,8 +83,38 @@
         
         
         <div class="comment-content">
         <div class="comment-content">
           <div class="comment-header">
           <div class="comment-header">
-            <span class="username">{{item.nickName}}</span>
-            <span class="comment-time">{{item.createTime}}</span>
+            <div class="header-left">
+              <span class="username">{{ item.nickName }}</span>
+              <!-- 显示评分星级 -->
+              <div v-if="item.rating && item.rating > 0" class="rating-display">
+                <div 
+                  v-for="star in 5" 
+                  :key="star"
+                  class="rating-star-wrapper"
+                >
+                  <!-- 背景空星 -->
+                  <svg class="rating-star rating-star-empty" viewBox="0 0 24 24" fill="currentColor">
+                    <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
+                  </svg>
+                  
+                  <!-- 前景实星 - 通过 clip-path 控制显示比例 -->
+                  <svg 
+                    class="rating-star rating-star-filled"
+                    :class="{ 
+                      'rating-star-half': getDisplayStarFillRatio(item.rating, star) === 0.5,
+                      'rating-star-full': getDisplayStarFillRatio(item.rating, star) === 1
+                    }"
+                    viewBox="0 0 24 24" 
+                    fill="currentColor"
+                    :style="{ clipPath: getDisplayStarClipPath(item.rating, star) }"
+                  >
+                    <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
+                  </svg>
+                </div>
+                <span class="rating-value">{{ item.rating }}</span>
+              </div>
+            </div>
+            <span class="comment-time">{{ item.createTime }}</span>
           </div>
           </div>
           
           
           <div class="comment-text">
           <div class="comment-text">
@@ -101,6 +166,9 @@ const props = defineProps({
 })
 })
 // 防止重复提交的加载状态
 // 防止重复提交的加载状态
 const isSubmiting = ref(false)
 const isSubmiting = ref(false)
+// 评分相关状态
+const ratingScore = ref(0) // 当前选中的评分 (0-5, 支持 0.5 步进)
+const hoverRating = ref(0) // 鼠标悬停时的评分 (支持 0.5 步进)
 
 
 //监听props.info.courseId变化
 //监听props.info.courseId变化
 watch(() => props.info.courseId, (newVal, oldVal) => {
 watch(() => props.info.courseId, (newVal, oldVal) => {
@@ -151,15 +219,26 @@ const handleSend = async () => {
     DGTMessage.warning(t('common.pleaseInputCommentContent'))
     DGTMessage.warning(t('common.pleaseInputCommentContent'))
     return
     return
   }
   }
+  
+  // 校验:如果要求必须评分,则检查是否已选择星级
+  // 如果需要强制评分,取消下面的注释
+  // if(ratingScore.value === 0){
+  //   DGTMessage.warning('请选择评分星级')
+  //   return
+  // }
+  
   // 设置提交状态为true,禁用按钮
   // 设置提交状态为true,禁用按钮
   isSubmiting.value = true
   isSubmiting.value = true
   const res = await commentAdd({
   const res = await commentAdd({
     courseId: props.info.courseId,
     courseId: props.info.courseId,
-    content: comments.value
+    content: comments.value,
+    rating: ratingScore.value // 添加评分参数(需后端接口支持)
   })
   })
   if(res.code === 200){
   if(res.code === 200){
     DGTMessage.success(t('common.commentSuccess'))
     DGTMessage.success(t('common.commentSuccess'))
     comments.value = ''
     comments.value = ''
+    ratingScore.value = 0 // 重置评分
+    hoverRating.value = 0
     getList();
     getList();
   }
   }
   // 提交完成后,将提交状态设置为false,启用按钮
   // 提交完成后,将提交状态设置为false,启用按钮
@@ -168,6 +247,90 @@ const handleSend = async () => {
   }, 1000)
   }, 1000)
 };
 };
 
 
+// 评分相关方法
+/**
+ * 计算输入星星的填充比例
+ * @param {number} starIndex - 星星索引 (1-5)
+ * @returns {number} 填充比例 (0, 0.5, 1)
+ */
+const getStarFillRatio = (starIndex) => {
+  const currentRating = hoverRating.value > 0 ? hoverRating.value : ratingScore.value
+  
+  if (currentRating >= starIndex) {
+    return 1 // 全星
+  } else if (currentRating >= starIndex - 0.5) {
+    return 0.5 // 半星
+  }
+  return 0 // 空星
+}
+
+/**
+ * 获取星星的 clip-path 样式
+ * @param {number} starIndex - 星星索引 (1-5)
+ * @returns {string} clip-path CSS 值
+ */
+const getStarClipPath = (starIndex) => {
+  const ratio = getStarFillRatio(starIndex)
+  if (ratio === 0) return 'inset(0 100% 0 0)' // 完全隐藏
+  if (ratio === 0.5) return 'inset(0 50% 0 0)' // 显示左半部分
+  return 'none' // 完全显示
+}
+
+/**
+ * 处理星星鼠标移动事件(检测半星)
+ * @param {MouseEvent} event - 鼠标事件
+ * @param {number} starIndex - 星星索引 (1-5)
+ */
+const handleStarMouseMove = (event, starIndex) => {
+  const rect = event.currentTarget.getBoundingClientRect()
+  const x = event.clientX - rect.left
+  const width = rect.width
+  
+  // 如果鼠标在左半部分,设置为半星;否则为全星
+  if (x < width / 2) {
+    hoverRating.value = starIndex - 0.5
+  } else {
+    hoverRating.value = starIndex
+  }
+}
+
+const handleStarLeave = () => {
+  hoverRating.value = 0
+}
+
+const handleStarClick = (starIndex) => {
+  // 点击时使用当前的 hoverRating(已经通过 mousemove 确定了是半星还是全星)
+  ratingScore.value = hoverRating.value > 0 ? hoverRating.value : starIndex
+}
+
+/**
+ * 计算显示星星的填充比例(用于评论列表)
+ * @param {number} rating - 评分值
+ * @param {number} starIndex - 星星索引 (1-5)
+ * @returns {number} 填充比例 (0, 0.5, 1)
+ */
+const getDisplayStarFillRatio = (rating, starIndex) => {
+  if (rating >= starIndex) {
+    return 1 // 全星
+  } else if (rating >= starIndex - 0.5) {
+    return 0.5 // 半星
+  }
+  return 0 // 空星
+}
+
+/**
+ * 获取显示星星的 clip-path 样式
+ * @param {number} rating - 评分值
+ * @param {number} starIndex - 星星索引 (1-5)
+ * @returns {string} clip-path CSS 值
+ */
+const getDisplayStarClipPath = (rating, starIndex) => {
+  const ratio = getDisplayStarFillRatio(rating, starIndex)
+  if (ratio === 0) return 'inset(0 100% 0 0)' // 完全隐藏
+  if (ratio === 0.5) return 'inset(0 50% 0 0)' // 显示左半部分
+  return 'none' // 完全显示
+}
+
 
 
 
 
 </script>
 </script>
@@ -241,6 +404,72 @@ const handleSend = async () => {
     .input-container {
     .input-container {
       flex: 1;
       flex: 1;
       min-width: 0;
       min-width: 0;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      
+      // 评分选择器样式
+      .rating-selector {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        padding: 8px 0;
+        
+        .rating-label {
+          font-size: 14px;
+          font-weight: 600;
+          color: #374151;
+          white-space: nowrap;
+        }
+        
+        .stars-container {
+          display: flex;
+          gap: 4px;
+          
+          .star-wrapper {
+            position: relative;
+            width: 24px;
+            height: 24px;
+            cursor: pointer;
+            
+            .star-icon {
+              position: absolute;
+              top: 0;
+              left: 0;
+              width: 100%;
+              height: 100%;
+              transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+              
+              &.star-empty {
+                color: #d1d5db;
+                z-index: 1;
+              }
+              
+              &.star-filled {
+                color: #fbbf24;
+                z-index: 2;
+                filter: drop-shadow(0 2px 4px rgba(251, 191, 36, 0.3));
+                
+                &:hover {
+                  transform: scale(1.15);
+                  filter: drop-shadow(0 3px 6px rgba(251, 191, 36, 0.4));
+                }
+                
+                &.star-half {
+                  color: #f59e0b;
+                }
+              }
+            }
+          }
+        }
+        
+        .rating-text {
+          font-size: 13px;
+          color: #6b7280;
+          font-weight: 500;
+          margin-left: 4px;
+        }
+      }
       
       
       .input-wrapper {
       .input-wrapper {
         display: flex;
         display: flex;
@@ -388,6 +617,66 @@ const handleSend = async () => {
         margin-bottom: 10px;
         margin-bottom: 10px;
         gap: 12px;
         gap: 12px;
         
         
+        .header-left {
+          display: flex;
+          align-items: center;
+          gap: 10px;
+          flex: 1;
+          min-width: 0;
+          
+          .username {
+            font-size: 15px;
+            font-weight: 600;
+            color: #374151;
+            transition: color 0.3s ease;
+            white-space: nowrap;
+          }
+          
+          // 评分显示样式
+          .rating-display {
+            display: flex;
+            align-items: center;
+            gap: 3px;
+            
+            .rating-star-wrapper {
+              position: relative;
+              width: 14px;
+              height: 14px;
+              
+              .rating-star {
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                transition: all 0.2s ease;
+                
+                &.rating-star-empty {
+                  color: #e5e7eb;
+                  z-index: 1;
+                }
+                
+                &.rating-star-filled {
+                  color: #fbbf24;
+                  z-index: 2;
+                  filter: drop-shadow(0 1px 2px rgba(251, 191, 36, 0.25));
+                  
+                  &.rating-star-half {
+                    color: #f59e0b;
+                  }
+                }
+              }
+            }
+            
+            .rating-value {
+              font-size: 12px;
+              color: #6b7280;
+              font-weight: 600;
+              margin-left: 2px;
+            }
+          }
+        }
+        
         .username {
         .username {
           font-size: 15px;
           font-size: 15px;
           font-weight: 600;
           font-weight: 600;
@@ -503,6 +792,23 @@ const handleSend = async () => {
       }
       }
       
       
       .input-container {
       .input-container {
+        .rating-selector {
+          .stars-container {
+            .star-wrapper {
+              width: 22px;
+              height: 22px;
+            }
+          }
+          
+          .rating-label {
+            font-size: 13px;
+          }
+          
+          .rating-text {
+            font-size: 12px;
+          }
+        }
+        
         .input-wrapper {
         .input-wrapper {
           flex-direction: column;
           flex-direction: column;
           gap: 10px;
           gap: 10px;
@@ -537,6 +843,23 @@ const handleSend = async () => {
       
       
       .comment-content {
       .comment-content {
         .comment-header {
         .comment-header {
+          .header-left {
+            .username {
+              font-size: 14px;
+            }
+            
+            .rating-display {
+              .rating-star-wrapper {
+                width: 12px;
+                height: 12px;
+              }
+              
+              .rating-value {
+                font-size: 11px;
+              }
+            }
+          }
+          
           .username {
           .username {
             font-size: 14px;
             font-size: 14px;
           }
           }

+ 217 - 8
src/pages/WorkflowAdd.vue

@@ -1,10 +1,35 @@
 <template>
 <template>
-  <div class="workflow-add container-height">
+  <div class="workflow-add container-height container">
+    <!-- ══════════════════════════════════════
+         步骤进度条(sticky sentinel + 步骤条)
+    ══════════════════════════════════════ -->
+    <div ref="stepsSentinelRef" class="wta-steps-sentinel"></div>
+    <div class="wta-steps-wrap" :class="{ 'wta-steps-wrap--sticky': isSticky }">
+      <div class="container">
+        <div class="wta-steps">
+          <div v-for="(step, idx) in steps" :key="idx"
+               class="wta-step"
+               :class="{ 'wta-step--active': idx === currentStep, 'wta-step--done': idx < currentStep }"
+               style="cursor: pointer"
+               @click="scrollToSection(idx)">
+            <div class="wta-step__circle">
+              <svg v-if="idx < currentStep" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
+              <span v-else>{{ idx + 1 }}</span>
+            </div>
+            <span class="wta-step__label">{{ step }}</span>
+            <div v-if="idx < steps.length - 1" class="wta-step__line"
+                 :class="{ 'wta-step__line--done': idx < currentStep }"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <Breadcrumb />
     <Breadcrumb />
+
     <div class="flex-between mt10">
     <div class="flex-between mt10">
       <div class="flex_1 mr20">
       <div class="flex_1 mr20">
         <el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-position="top" class="page-add">
         <el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-position="top" class="page-add">
-          <div class="padding16 bg_color_fff border_radius_10 box_shadow_card"  v-show="!isFullscreen">
+          <div class="padding16 bg_color_fff border_radius_10 box_shadow_card" id="section-upload" v-show="!isFullscreen">
             <div class="gap10">
             <div class="gap10">
               <div class="line_vertical"></div>
               <div class="line_vertical"></div>
               <div class="font_size20 bold"><span class="color_required font_size16">*</span>{{$t('workflowTrade.fileUpload')}}</div>
               <div class="font_size20 bold"><span class="color_required font_size16">*</span>{{$t('workflowTrade.fileUpload')}}</div>
@@ -26,7 +51,7 @@
               </el-form-item>
               </el-form-item>
             </div>
             </div>
           </div>
           </div>
-          <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card">
+          <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card" id="section-basic">
             <div class="gap10">
             <div class="gap10">
               <div class="line_vertical"></div>
               <div class="line_vertical"></div>
               <div class="font_size20 bold">{{$t('common.basicInfo')}}</div>
               <div class="font_size20 bold">{{$t('common.basicInfo')}}</div>
@@ -129,7 +154,7 @@
               </el-form-item>
               </el-form-item>
             </div>
             </div>
           </div>
           </div>
-          <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card" v-show="!isFullscreen">
+          <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card" id="section-price" v-show="!isFullscreen">
             <div class="gap10 mb20">
             <div class="gap10 mb20">
               <div class="line_vertical"></div>
               <div class="line_vertical"></div>
               <div class="font_size20 bold">{{$t('workflowTrade.priceSetting')}}</div>
               <div class="font_size20 bold">{{$t('workflowTrade.priceSetting')}}</div>
@@ -189,7 +214,7 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, onMounted, reactive, watchEffect, nextTick } from 'vue'
+import { ref, onMounted, reactive, watchEffect, nextTick, onUnmounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useRoute, useRouter } from 'vue-router'
 import FileUploader from '@/components/FileUploader.vue'
 import FileUploader from '@/components/FileUploader.vue'
 import DGTMessage from '@/utils/message'
 import DGTMessage from '@/utils/message'
@@ -201,9 +226,87 @@ import { getCategoryListTree } from '@/api/category.js'
 import { getAgreementType } from '@/api/common.js'
 import { getAgreementType } from '@/api/common.js'
 import { publishAdd, getPublishDetail,publishEdit } from '@/api/publish.js'
 import { publishAdd, getPublishDetail,publishEdit } from '@/api/publish.js'
 
 
-import { useI18n } from 'vue-i18n' 
+import { useI18n } from 'vue-i18n'
 const { t } = useI18n()
 const { t } = useI18n()
 
 
+// ══════════════════════════════════════
+// 步骤条状态管理
+// ══════════════════════════════════════
+const currentStep = ref(0)
+const isSticky = ref(false)
+const stepsSentinelRef = ref(null)
+
+const sectionRefs = {
+  0: 'section-upload',
+  1: 'section-basic',
+  2: 'section-price'
+}
+
+const steps = [
+  t('workflowTrade.fileUpload'),
+  t('common.basicInfo'),
+  t('workflowTrade.priceSetting')
+]
+
+let scrollObserver = null
+let stickyObserver = null
+
+const scrollToSection = (stepIndex) => {
+  if (stepIndex === currentStep.value) return
+
+  const sectionId = sectionRefs[stepIndex]
+  const element = document.getElementById(sectionId)
+
+  if (element) {
+    const offsetTop = element.offsetTop - 120
+    window.scrollTo({
+      top: offsetTop,
+      behavior: 'smooth'
+    })
+    currentStep.value = stepIndex
+  }
+}
+
+const initScrollObserver = () => {
+  const sections = Object.values(sectionRefs)
+    .map(id => document.getElementById(id))
+    .filter(Boolean)
+
+  if (sections.length === 0) return
+
+  scrollObserver = new IntersectionObserver((entries) => {
+    entries.forEach(entry => {
+      if (entry.isIntersecting) {
+        const sectionId = entry.target.id
+        const stepIndex = Object.values(sectionRefs).indexOf(sectionId)
+
+        if (stepIndex !== -1 && stepIndex !== currentStep.value) {
+          currentStep.value = stepIndex
+        }
+      }
+    })
+  }, {
+    rootMargin: '-100px 0px -50% 0px',
+    threshold: 0.15
+  })
+
+  sections.forEach(section => scrollObserver.observe(section))
+}
+
+const initStickyObserver = () => {
+  const sentinel = stepsSentinelRef.value
+  if (!sentinel) return
+
+  stickyObserver = new IntersectionObserver(([entry]) => {
+    isSticky.value = !entry.isIntersecting
+  }, {
+    threshold: 0,
+    rootMargin: '-60px 0px 0px 0px'
+  })
+
+  stickyObserver.observe(sentinel)
+}
+
 // 防止重复提交的加载状态
 // 防止重复提交的加载状态
 const isSubmiting = ref(false)
 const isSubmiting = ref(false)
 // 发布规则
 // 发布规则
@@ -295,8 +398,27 @@ onMounted(() => {
     getDetail();
     getDetail();
   }
   }
   getAgreementTypeFn();
   getAgreementTypeFn();
+
+  nextTick(() => {
+    initScrollObserver()
+    initStickyObserver()
+  })
 });
 });
 
 
+onUnmounted(() => {
+  window.removeEventListener('keydown', handleKeyDown)
+
+  if (scrollObserver) {
+    scrollObserver.disconnect()
+    scrollObserver = null
+  }
+
+  if (stickyObserver) {
+    stickyObserver.disconnect()
+    stickyObserver = null
+  }
+})
+
 // 提交表单
 // 提交表单
 const submitForm = async () => {
 const submitForm = async () => {
  
  
@@ -424,13 +546,92 @@ const getAgreementTypeFn = () => {
   })
   })
 };
 };
 </script>
 </script>
-<style lang="scss">
+<style lang="scss" scoped>
+
+/* ══════════════════════════════════════
+   步骤进度条
+══════════════════════════════════════ */
+.wta-steps-sentinel { height: 1px; }
+.wta-steps-wrap {
+  background: #fff;
+  // border-bottom: 1px solid #e8eaf6;
+  // box-shadow: 0 2px 12px rgba(79,70,229,0.06);
+  transition: box-shadow 0.3s;
+}
+.wta-steps-wrap--sticky {
+  position: fixed;
+  top: 60px;
+  left: 0;
+  right: 0;
+  z-index: 100;
+  box-shadow: 0 4px 24px rgba(79,70,229,0.13);
+  backdrop-filter: blur(16px);
+  background: rgba(255,255,255,0.95);
+}
+.wta-steps {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 16px 0;
+  overflow-x: auto;
+}
+.wta-step {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+.wta-step__circle {
+  width: 28px; height: 28px;
+  border-radius: 50%;
+  border: 2px solid #e0e0f0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: 700;
+  color: #9ca3af;
+  background: #fff;
+  transition: all 0.3s;
+  flex-shrink: 0;
+}
+.wta-step__label {
+  font-size: 13px;
+  color: #9ca3af;
+  font-weight: 500;
+  white-space: nowrap;
+  transition: color 0.3s;
+}
+.wta-step__line {
+  width: 40px;
+  height: 2px;
+  background: #e0e0f0;
+  margin: 0 4px;
+  border-radius: 2px;
+  transition: background 0.3s;
+}
+.wta-step__line--done { background: linear-gradient(90deg, #6366f1, #a855f7); }
+.wta-step--active .wta-step__circle {
+  border-color: #6366f1;
+  background: linear-gradient(135deg, #6366f1, #a855f7);
+  color: #fff;
+  box-shadow: 0 0 0 4px rgba(99,102,241,0.15);
+}
+.wta-step--active .wta-step__label { color: #4f46e5; font-weight: 700; }
+.wta-step--done .wta-step__circle {
+  border-color: #6366f1;
+  background: linear-gradient(135deg, #6366f1, #a855f7);
+  color: #fff;
+}
+.wta-step--done .wta-step__label { color: #6366f1; }
+
 .workflow-add{
 .workflow-add{
   .payType{
   .payType{
     background: #EAF0FF;
     background: #EAF0FF;
     border-radius: 8px 8px 8px 8px;
     border-radius: 8px 8px 8px 8px;
     border: 1px solid transparent;
     border: 1px solid transparent;
     padding: 10px 16px;
     padding: 10px 16px;
+    cursor: pointer;
     &.active{
     &.active{
       background: #EAF0FF;
       background: #EAF0FF;
       border-color: $primary-color;
       border-color: $primary-color;
@@ -448,7 +649,7 @@ const getAgreementTypeFn = () => {
   }
   }
 }
 }
 </style>
 </style>
-<style lang="scss">
+<style lang="scss" scoped>
 .editor-container {
 .editor-container {
   position: relative;
   position: relative;
   width: 100%;
   width: 100%;
@@ -507,4 +708,12 @@ const getAgreementTypeFn = () => {
     }
     }
   }
   }
 }
 }
+
+.container{
+  min-width: auto;
+}
+
+.line_vertical{
+  background: linear-gradient(135deg, rgb(0, 85, 254), rgb(200, 50, 250));
+}
 </style>
 </style>