Pārlūkot izejas kodu

添加问题纠错功能

shmily1213 3 nedēļas atpakaļ
vecāks
revīzija
da1e84ef7e

+ 10 - 0
src/api/modules/study.ts

@@ -187,3 +187,13 @@ export function getSimulationExamSubjects() {
 export function beginExaminee(examineeId: number) {
   return flyio.get('/front/exam/beginExaminee', { examineeId }) as Promise<ApiResponse<Examinee>>;
 } 
+
+
+/**
+ * 纠错
+ * @param params 
+ * @returns 
+ */
+export function correctQuestion(params: { questionid: number, remark: string }) {
+  return flyio.post('/front/adjustWrong/correctQuestion', params) as Promise<ApiResponse<any>>;
+}

+ 1 - 1
src/components/mx-question/components/mx-question-correct-popup.vue

@@ -39,7 +39,7 @@ const close = () => {
 const handleSubmit = async () => {
     await form.value.validate()
     await correctQuestion(model.value)
-    toast('保存成功,等待工作人员处理。')
+    toast('提交成功,等待工作人员处理')
     model.value.remark = '' // clean if success commit.
     close()
 }

+ 1 - 1
src/components/mx-question/components/mx-question-parse.vue

@@ -46,7 +46,7 @@ watch(() => props.question, () => updateMathJaxIfNeeded())
 
 const updateMathJaxIfNeeded = () => {
     if (disabledMathJax) return
-    updateMathJax(parseEl.value?.$el)
+    // updateMathJax(parseEl.value?.$el)
 }
 </script>
 

+ 2 - 1
src/components/mx-question/useQuestionTranslate.js

@@ -26,7 +26,8 @@ export function useQuestionTranslate(question) {
         questionId: question.id || question.questionId,
         type: question.qtpye || question.type,
         options: makeOptions(),
-        answers: [question.answer1, question.answer2],
+        // answers: [question.answer1, question.answer2],
+        answers: question.answers,
         knowledge: question.knownledgeName || question.knowledges || question.knowledge,
         _raw: question
     }

+ 1 - 1
src/pagesOther/pages/topic-center/wrong-book/components/wrong-book-item.vue

@@ -1,7 +1,7 @@
 <template>
     <view class="bg-white mx-card">
         <view class="p-30">
-            <mx-question-content v-model="question.answer" :question="question" :sys-answer="question.answer1"
+            <mx-question-content v-model="question.answer" :question="question" :sys-answer="question.answer"
                                  disabled readonly/>
             <mx-question-parse :question="question" class="mt-40"/>
         </view>

+ 92 - 0
src/pagesStudy/pages/start-exam/components/question-correct-popup.vue

@@ -0,0 +1,92 @@
+<template>
+  <!-- #ifdef H5 -->
+  <teleport to="body">
+    <!-- #endif -->
+    <!-- #ifdef MP-WEIXIN -->
+    <root-portal externalClass="theme-ie">
+      <!-- #endif -->
+      <uv-popup ref="popupRef" mode="center" :close-on-click-overlay="false" :closeable="false" :round="12">
+        <view class="theme-ie popup-content box-border bg-white">
+          <view class="popup-header">
+            <view class="popup-header-title">
+              <text>问题纠错</text>
+            </view>
+          </view>
+          <view class="px-30">
+            <view class="h-60 flex items-center justify-between border-0 border-b border-solid border-[#efefef]">
+              <view class="flex items-center gap-x-10">
+                <text>问题编号</text>
+                <ie-image src="/static/image/icon-lock.png" custom-class="w-26 h-26" mode="aspectFit" />
+              </view>
+              <text class="text-fore-light">{{ questionId }}</text>
+            </view>
+            <view class="mt-30">
+              <uv-textarea v-model="remark" placeholder="在这里填写问题描述" count :height="100" :maxlength="200" />
+            </view>
+          </view>
+          <view class="mt-50 mx-30 mb-30 flex items-center gap-x-30">
+            <view class="flex-1">
+              <uv-button type="error" plain shape="circle" @click="close">取消</uv-button>
+            </view>
+            <view class="flex-1">
+              <uv-button type="primary" shape="circle" :disabled="submitBtnDisabled"
+                @click="handleSubmit">提交</uv-button>
+            </view>
+          </view>
+        </view>
+      </uv-popup>
+      <!-- #ifdef MP-WEIXIN -->
+    </root-portal>
+    <!-- #endif -->
+    <!-- #ifdef H5 -->
+  </teleport>
+  <!-- #endif -->
+</template>
+
+<script lang="ts" setup>
+import { correctQuestion } from '@/api/modules/study';
+const popupRef = ref();
+const questionId = ref(0);
+const remark = ref('');
+const loading = ref(false);
+const submitBtnDisabled = computed(() => !remark.value?.trim() || loading.value);
+const open = (id: number) => {
+  if (id !== questionId.value) {
+    remark.value = '';
+  }
+  questionId.value = id;
+  popupRef.value.open();
+}
+const close = () => {
+  popupRef.value.close();
+}
+const handleSubmit = () => {
+  loading.value = true;
+  correctQuestion({ questionid: questionId.value, remark: remark.value.trim() }).then((res) => {
+    loading.value = false;
+    uni.$ie.showToast('提交成功,等待工作人员处理');
+    close();
+  }).catch((err) => {
+    console.log(err)
+    loading.value = false;
+  })
+}
+defineExpose({
+  open,
+  close
+});
+</script>
+
+<style lang="scss" scoped>
+.popup-content {
+  @apply w-[88vw];
+}
+
+.popup-header {
+  @apply px-30 h-100 flex items-center justify-center;
+}
+
+.popup-header-title {
+  @apply flex items-center text-30 text-fore-title font-bold;
+}
+</style>

+ 46 - 6
src/pagesStudy/pages/start-exam/start-exam.vue

@@ -25,16 +25,19 @@
     </view>
     <ie-safe-toolbar v-if="isReady" :height="64" :shadow="false">
       <view class="px-18 h-full flex items-center justify-around border-0 border-t border-solid border-[#EFEFEF]">
-        <view class="w-48 h-48 flex items-center justify-center" @click="handleFavorite">
+        <view class="w-48 h-48 flex items-center justify-center" id="question-correct-btn" @click="handleCorrect">
+          <uv-icon name="info-circle" size="24" />
+        </view>
+        <view class="w-48 h-48 flex items-center justify-center" id="question-favorite-btn" @click="handleFavorite">
           <uv-icon v-if="currentQuestion.isFavorite" name="star-fill" color="#FF9A18" size="27" />
           <uv-icon v-else name="star" size="27" />
         </view>
-        <view class="w-48 h-48 flex items-center justify-center" @click="handleMark">
+        <view class="w-48 h-48 flex items-center justify-center" id="question-mark-btn" @click="handleMark">
           <ie-image
             :src="currentQuestion.isMark ? '/pagesStudy/static/image/icon-mark-active.png' : '/pagesStudy/static/image/icon-mark.png'"
             custom-class="w-38 h-38" mode="aspectFill" />
         </view>
-        <view class="w-48 h-48 flex items-center justify-center" @click="handleCalendar">
+        <view class="w-48 h-48 flex items-center justify-center" id="question-calendar-btn" @click="handleCalendar">
           <uv-icon name="calendar" size="28" />
         </view>
       </view>
@@ -84,19 +87,22 @@
       </view>
     </view>
   </question-stats-popup>
+  <question-correct-popup ref="questionCorrectPopupRef" />
+  <fast-guide v-model:show="guideShow" :list="guideList" v-model:index="guideIndex"></fast-guide>
 </template>
 
 <script lang="ts" setup>
-import QuestionItem from './components/question-item.vue';
 import QuestionWrap from './components/question-wrap.vue';
 import QuestionStatsPopup from './components/question-stats-popup.vue';
 import QuestionProgress from './components/question-progress.vue';
+import QuestionCorrectPopup from './components/question-correct-popup.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
 import { EnumPaperType } from '@/common/enum';
 import { getOpenExaminee, getPaper, commitExamineePaper, collectQuestion, cancelCollectQuestion, beginExaminee } from '@/api/modules/study';
 import { useExam } from '@/composables/useExam';
 import { Study } from '@/types';
 import { NEXT_QUESTION, PREV_QUESTION, NEXT_QUESTION_QUICKLY, PREV_QUESTION_QUICKLY } from '@/types/injectionSymbols';
+import { number } from 'echarts';
 // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
 const { prevData, transferBack } = useTransferPage();
 const { setQuestionList, questionList, groupedQuestionList, stateQuestionList, questionTypeDesc, favoriteList, notKnowList, markList, currentIndex,
@@ -108,7 +114,32 @@ provide(NEXT_QUESTION, nextQuestion);
 provide(PREV_QUESTION, prevQuestion);
 provide(NEXT_QUESTION_QUICKLY, nextQuestionQuickly);
 provide(PREV_QUESTION_QUICKLY, prevQuestionQuickly);
-
+//
+const guideShow = ref(false);
+const guideList = ref([
+  {
+    target: '#question-correct-btn',
+    position: 'top',
+    msg: '这是第一步'
+  },
+  {
+    target: '#question-favorite-btn',
+    position: 'top',
+    msg: '这是第二步'
+  },
+  {
+    target: '#question-mark-btn',
+    position: 'top',
+    msg: '这是第三步'
+  },
+  {
+    target: '#question-calendar-btn',
+    position: 'top',
+    msg: '这是第四步'
+  }
+]);
+const guideIndex = ref(0);
+// 
 const isAnimationFinish = ref(false);
 const transitionStartX = ref(null);
 const transitionEndX = ref(null);
@@ -162,6 +193,11 @@ const currentQuestion = computed(() => {
 const hanadleNavigate = (index: number) => {
   changeIndex(index);
 }
+/// 问题纠错
+const questionCorrectPopupRef = ref();
+const handleCorrect = () => {
+  questionCorrectPopupRef.value.open(currentQuestion.value.id);
+}
 const handleFavorite = async () => {
   if (!currentQuestion.value.isFavorite) {
     await collectQuestion(currentQuestion.value.id);
@@ -327,6 +363,8 @@ const loadPracticeData = async () => {
     transferBack();
     return;
   }
+  // 练习没有规定时间,设置为最大值
+  totalExamTime.value = Number.MAX_SAFE_INTEGER;
   combinePaperData(data, paperType);
 }
 const loadSimulationData = async () => {
@@ -356,7 +394,9 @@ const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperTy
     setDuration(examinee.duration || 0);
     isReady.value = true;
     startTime();
-    console.log(stateQuestionList.value)
+    setTimeout(() => {
+      guideShow.value = true;
+    }, 500);
   }
 }
 const loadData = async () => {

+ 3 - 3
src/pagesSystem/pages/bind-profile/bind-profile.vue

@@ -12,7 +12,7 @@
           <ie-picker ref="pickerRef" v-model="examTypeForm.location" :list="provinceList" placeholder="选择省份"
             :custom-style="customStyle" key-label="dictLabel" key-value="dictValue" :disabled="isProvinceDisabled">
             <template v-if="isProvinceDisabled" #right>
-              <ie-image src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              <ie-image src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </template>
           </ie-picker>
         </uv-form-item>
@@ -20,7 +20,7 @@
           <ie-picker ref="pickerRef" v-model="examTypeForm.examType" :list="examTypeList" :disabled="isExamTypeDisabled"
             placeholder="选择考生类别" :custom-style="customStyle" key-label="dictLabel" key-value="dictValue">
             <template v-if="isExamTypeDisabled" #right>
-              <ie-image src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              <ie-image src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </template>
           </ie-picker>
         </uv-form-item>
@@ -75,7 +75,7 @@
           <ie-picker ref="pickerRef" v-model="form.schoolName" disabled placeholder="请选择就读学校"
             :custom-style="customStyle" :custom-label="form.schoolName" @click="handleSchoolSelect">
             <template v-if="isSchoolDisabled" #right>
-              <ie-image src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              <ie-image src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </template>
           </ie-picker>
         </uv-form-item>

+ 9 - 9
src/pagesSystem/pages/edit-student-profile/edit-student-profile.vue

@@ -19,21 +19,21 @@
             <uv-input v-model="form.location" border="none" placeholder="" placeholderClass="text-30" font-size="30rpx"
               :custom-style="customStyle" readonly>
             </uv-input>
-            <ie-image slot="right" src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
+            <ie-image slot="right" src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
               mode="aspectFill" />
           </uv-form-item>
           <uv-form-item label="考试类别" prop="name" borderBottom>
             <view class="flex-1 pl-[26px]">
               <ie-dict :dictName="EnumDictName.EXAM_TYPE" :dictValue="form.examType" />
             </view>
-            <ie-image slot="right" src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
+            <ie-image slot="right" src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
               mode="aspectFill" />
           </uv-form-item>
           <uv-form-item label="毕业年份" prop="name">
             <uv-input v-model="form.endYear" border="none" placeholder="" placeholderClass="text-30" font-size="30rpx"
               :custom-style="customStyle" readonly>
             </uv-input>
-            <ie-image slot="right" src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
+            <ie-image slot="right" src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
               mode="aspectFill" />
           </uv-form-item>
         </content-card>
@@ -44,21 +44,21 @@
               font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
             </uv-input>
             <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-              src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
           <uv-form-item label="数学" prop="name" borderBottom>
             <uv-input v-model="scores.mathematics" border="none" placeholder="请输入" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
             </uv-input>
             <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-              src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
           <uv-form-item label="外语" prop="name" :borderBottom="form.examType === EnumExamType.OHS">
             <uv-input v-model="scores.foreign" border="none" placeholder="请输入" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
             </uv-input>
             <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-              src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+              src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
           </uv-form-item>
           <block v-if="[EnumExamType.OHS].includes(form.examType)">
             <uv-form-item label="物理" prop="name" borderBottom>
@@ -66,14 +66,14 @@
                 font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
               </uv-input>
               <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-                src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+                src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </uv-form-item>
             <uv-form-item label="政治" prop="name">
               <uv-input v-model="scores.political" border="none" placeholder="请输入" placeholderClass="text-30"
                 font-size="30rpx" :custom-style="customStyle" :readonly="form.examType === EnumExamType.OHS">
               </uv-input>
               <ie-image v-if="form.examType === EnumExamType.OHS" slot="right"
-                src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
+                src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30" mode="aspectFill" />
             </uv-form-item>
           </block>
         </content-card>
@@ -92,7 +92,7 @@
             <uv-input v-model="form.schoolName" border="none" placeholder="" placeholderClass="text-30"
               font-size="30rpx" :custom-style="customStyle" readonly>
             </uv-input>
-            <ie-image slot="right" src="/pagesSystem/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
+            <ie-image slot="right" src="/static/image/icon/icon-lock.png" custom-class="w-24 h-30"
               mode="aspectFill" />
           </uv-form-item>
           <uv-form-item label="所在班级" prop="form.name">

+ 0 - 0
src/pagesSystem/static/image/icon/icon-lock.png → src/static/image/icon-lock.png


+ 4 - 0
src/uni_modules/fast-guide/changelog.md

@@ -0,0 +1,4 @@
+## 1.0.1(2025-02-24)
+`修改` 插件发布规范
+## 1.0.0(2025-02-24)
+`新增` fast-guide组件发布

+ 526 - 0
src/uni_modules/fast-guide/components/fast-guide/fast-guide.vue

@@ -0,0 +1,526 @@
+<template>
+  <view v-if="show" @click="focusTap" @touchmove.stop.prevent :style="{ '--path': path, '--duration': duration + 'ms' }"
+    class="clip-container">
+    <view @click.stop="maskTap" class="clip-box"></view>
+    <view @click.stop class="guide-box animate__animated" :class="[contentVisible ? 'opacity-100' : 'opacity-0']" :style="[msgStyles]">
+      <view v-if="$slots.message" class="">
+        <slot name="message" :msg="list[index].msg"></slot>
+      </view>
+      <view v-else class="msg-label">
+        {{ list[index].msg }}
+      </view>
+
+      <view class="step-box">
+        <view v-if="index > 0" @click="lastStep" class="step-btn">
+          上一步
+        </view>
+        <view @click="nextStep" class="step-btn">
+          下一步
+        </view>
+        <view @click="skipStep" class="step-btn">
+          跳过
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+<script>
+const path_default = `
+		polygon(
+			0% 0%, 
+			0% 0%, 
+			100% 0%, 
+			100% 100%, 
+			0% 100%, 
+			0% 0%, 
+			0% 0%, 
+			0% 100%, 
+			100% 100%, 
+			100% 0%
+		)
+	`
+
+/**
+ * guide 引导弹窗
+ * @description 引导组件,用于教学提示、用户操作引导等内容,支持自定义组件节点自动聚焦和手动设置相对位置聚焦。组件只提供容器,内部内容由用户自定义
+ * @tutorial //git地址
+ * @property {Boolean}			show				是否展示弹窗 (默认 false )
+ * @property {Number}			index				当前步骤索引(默认 0 )
+ * @property {String | Number}	duration			动画时长
+ * @property {String}			unit				换算单位
+ * @property {Array}			list				步骤列表
+ * @property {Object}			{
+ * 								@property{String}	ref					要聚焦的子组件的ref
+ * 								@property{String}	target				当前组件/子组件中的选择器标识
+ * 								@property{String}	position			提示框展示位置'top'/'bottom'
+ * 								@property{String}	msg					提示框文字
+ * 								@property{String}	msgStyles			提示框自定义样式
+ * 								@property{String}	width				自定义聚焦范围的宽度
+ * 								@property{String}	height				自定义聚焦范围的高度
+ * 								@property{String}	left				自定义聚焦范围的左侧偏移量 left/right仅生效一个	使用自定义聚焦后ref、target属性将失效
+ * 								@property{String}	right				自定义聚焦范围的右侧偏移量 left/right仅生效一个	使用自定义聚焦后ref、target属性将失效
+ * 								@property{String}	top					自定义聚焦范围的上侧偏移量 top/bottom仅生效一个	使用自定义聚焦后ref、target属性将失效
+ * 								@property{String}	bottom				自定义聚焦范围的下侧偏移量 top/bottom仅生效一个	使用自定义聚焦后ref、target属性将失效
+                }					list步骤列表内部参数说明
+ * @event {Function} open   	弹出层打开
+ * @event {Function} close  	弹出层收起
+ * @event {Function} focus  	聚焦区域点击事件
+ * @event {Function} mask		遮罩区域点击事件
+ * @event {Function} next   	执行下一步
+ * @event {Function} last   	执行上一步
+ * @event {Function} skip   	跳过所有步骤
+ * @event {Function} finish 	结束引导
+ * @event {Function} getQuery	获取兄弟组件布局查询对象
+ * @example <popup-guide :show="guideShow" :list="guideList" :index="guideIndex" @next="guideNext" @last="guideLast" @skip="guideSkip" @finish="guideFinish" ></popup-guide>
+ */
+export default {
+  data() {
+    return {
+      path: path_default,
+      msgStyles: {},
+      contentVisible: false
+    }
+  },
+  props: {
+    // 是否展示
+    show: {
+      default: false
+    },
+    // 步骤列表
+    list: {
+      default: []
+    },
+    // 当前步骤索引
+    index: {
+      default: 0
+    },
+    // 动画时长
+    duration: {
+      default: 350
+    },
+    // 换算单位
+    unit: {
+      default: 'rpx'
+    }
+  },
+  emits: ['open', 'close', 'focus', 'mask', 'next', 'last', 'skip', 'finish', 'getQuery'],
+  watch: {
+    index: {
+      handler(newVal) {
+        if (newVal != undefined) {
+          this.getCurrentPath()
+        }
+      }
+    },
+    show: {
+      handler(newVal) {
+        if (newVal == true) {
+          this.getCurrentPath()
+          this.$emit('open', this.callBackData())
+        }
+        if (newVal == false) {
+          this.$emit('close', this.callBackData())
+        }
+      }
+    }
+  },
+  methods: {
+    pathInit() {
+      this.path = path_default
+    },
+    testNumber(value) {
+      return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
+    },
+    addUnit(value = '', unit = this.unit) {
+      value = String(value)
+      return this.testNumber(value) ? `${value}${unit}` : value
+    },
+    isEmpty(val) {
+      return val === null || val === undefined || val === ''
+    },
+    async getCurrentPath() {
+      await this.$nextTick(() => { })
+
+      let {
+        ref,
+        target,
+
+        width,
+        height,
+
+        top,
+        left,
+        right,
+        bottom,
+
+        gap,
+        position,
+        msgStyles
+
+      } = this.list[this.index]
+      let x1, x2, y1, y2
+
+      if (target) {
+        let maxWidth
+        uni.getSystemInfo({
+          success(res) {
+            // #ifndef H5
+            maxWidth = res.screenWidth
+            // #endif
+            // #ifdef H5
+            maxWidth = res.windowWidth
+            // #endif
+          }
+        })
+        // console.log(maxWidth,'maxWidth')
+
+        const targetDom = await new Promise((resolve, reject) => {
+          if (ref) {
+            this.$emit('getQuery', function () {
+              uni.createSelectorQuery()
+                .in(this.$refs[ref])
+                .select(target)
+                .boundingClientRect(data => {
+                  resolve(data)
+                }).exec()
+            })
+          } else {
+            uni.createSelectorQuery()
+              .select(target)
+              .boundingClientRect(data => {
+                resolve(data)
+              }).exec()
+          }
+        })
+        // console.log(targetDom,'targetDom')
+        width = targetDom.width
+        height = targetDom.height
+        top = targetDom.top
+        left = targetDom.left
+        right = maxWidth - targetDom.right
+        //+1像素修正元素处于屏幕正中间时的msg相对位置
+        if (left > right + 1) {
+          left = ''
+        } else {
+          right = ''
+        }
+        bottom = targetDom.bottom
+        // console.log(left,'left',right,'right')
+
+
+        x1 = !this.isEmpty(left) ? `calc(${this.addUnit(left, 'px')} + ${this.addUnit(width, 'px')})` : `calc(100% - ${this.addUnit(right, 'px')})`
+        x2 = !this.isEmpty(left) ? `${this.addUnit(left, 'px')}` : `calc(100% - ${this.addUnit(right, 'px')} - ${this.addUnit(width, 'px')})`
+
+        y1 = !this.isEmpty(top) ? `${this.addUnit(top, 'px')}` : `calc(100% - ${this.addUnit(bottom, 'px')} - ${this.addUnit(height, 'px')})`
+        y2 = !this.isEmpty(top) ? `calc(${this.addUnit(top, 'px')} + ${this.addUnit(height, 'px')})` : `calc(100% - ${this.addUnit(bottom, 'px')})`
+
+      } else {
+        x1 = !this.isEmpty(left) ? `calc(${this.addUnit(left)} + ${this.addUnit(width)})` : `calc(100% - ${this.addUnit(right)})`
+        x2 = !this.isEmpty(left) ? `${this.addUnit(left)}` : `calc(100% - ${this.addUnit(right)} - ${this.addUnit(width)})`
+
+        y1 = !this.isEmpty(top) ? `${this.addUnit(top)}` : `calc(100% - ${this.addUnit(bottom)} - ${this.addUnit(height)})`
+        y2 = !this.isEmpty(top) ? `calc(${this.addUnit(top)} + ${this.addUnit(height)})` : `calc(100% - ${this.addUnit(bottom)})`
+      }
+      // 延时,解决动画丢失的问题
+      await new Promise((resolve) => {
+        setTimeout(() => {
+          resolve()
+        }, 50)
+      })
+      this.path = `
+					polygon(
+						0% 0%,
+						0% ${y1},
+						${x1} ${y1},
+						${x1} ${y2},
+						${x2} ${y2},
+						${x2} ${y1},
+						0% ${y1},
+						0% 100%,
+						100% 100%, 
+						100% 0%
+					)
+				`
+      // console.log(this.path,'path.value')
+
+
+      //设置msg样式
+      const msgDom = await new Promise((resolve, reject) => {
+        uni.createSelectorQuery().in(this).select('.guide-box').boundingClientRect(data => {
+          resolve(data)
+        }).exec()
+      })
+
+      let animationName
+      gap = gap || 20
+      position = position || 'top'
+
+      if (target) {
+        switch (position) {
+          case 'top':
+            if (!this.isEmpty(top)) {
+              top = `calc(${this.addUnit(top, 'px')} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
+            } else {
+              top = `calc(100% - ${this.addUnit(bottom, 'px')} - ${this.addUnit(height, 'px')} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
+            }
+            animationName = 'backInDown'
+            break;
+          case 'bottom':
+            if (!this.isEmpty(top)) {
+              top = `calc(${this.addUnit(top, 'px')} + ${this.addUnit(height, 'px')} + ${this.addUnit(gap)})`
+            } else {
+              top = `calc(100% - ${this.addUnit(bottom, 'px')} + ${this.addUnit(gap)} + ${this.addUnit(msgDom.height, 'px')})`
+            }
+            animationName = 'backInUp'
+            break;
+          default:
+            break;
+        }
+
+      } else {
+        switch (position) {
+          case 'top':
+            if (top) {
+              top = `calc(${this.addUnit(top)} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
+            } else {
+              top = `calc(100% - ${this.addUnit(bottom)} - ${this.addUnit(height)} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
+            }
+            animationName = 'backInDown'
+            break;
+          case 'bottom':
+            if (top) {
+              top = `calc(${this.addUnit(top)} + ${this.addUnit(height)} + ${this.addUnit(gap)})`
+            } else {
+              top = `calc(100% - ${this.addUnit(bottom)} + ${this.addUnit(gap)} + ${this.addUnit(msgDom.height, 'px')})`
+            }
+            animationName = 'backInUp'
+            break;
+          default:
+            break;
+        }
+
+      }
+      left = target ? this.addUnit(left, 'px') : this.addUnit(left)
+      right = target ? this.addUnit(right, 'px') : this.addUnit(right)
+
+      this.msgStyles = {
+        'top': top,
+        'left': left,
+        'right': right,
+        'transition': `all ${this.duration}ms`,
+        'animation-name': animationName,
+        '-webkit-animation-name': animationName,
+
+        ...msgStyles
+      }
+      setTimeout(() => {
+        this.contentVisible = true;
+      }, 300);
+    },
+    nextStep() {
+      if (this.list[this.index + 1]) {
+        this.$emit('next', this.callBackData())
+        this.$emit('update:index', this.index + 1)
+      } else {
+        this.contentVisible = false;
+        this.pathInit()
+        setTimeout(() => {
+          this.$emit('finish', this.callBackData())
+          this.$emit('update:show', false)
+        }, this.duration)
+      }
+    },
+    lastStep() {
+      this.$emit('last', this.callBackData())
+    },
+    skipStep() {
+      this.pathInit()
+      setTimeout(() => {
+        this.$emit('skip', this.callBackData())
+      }, this.duration)
+    },
+    focusTap() {
+      this.$emit('focus', this.callBackData())
+    },
+    maskTap() {
+      this.$emit('mask', this.callBackData())
+    },
+    callBackData() {
+      return {
+        index: this.index,
+        value: this.list[this.index]
+      }
+    }
+  },
+
+
+}
+
+
+</script>
+
+<style lang="scss">
+.shadow {
+  box-shadow: 2rpx 2rpx 12rpx var(--shadow);
+}
+
+.clip-container {
+  --path: '';
+  --duration: '';
+  --color: #1676ff;
+
+  position: absolute;
+  /* #ifdef H5 */
+  width: 750rpx;
+  /* #endif */
+  /* #ifndef H5 */
+  width: 100%;
+  /* #endif */
+  height: 100vh;
+  top: 0;
+  left: 0;
+  z-index: 9996;
+}
+
+
+.clip-box {
+
+  box-sizing: border-box;
+  width: 100%;
+  height: 100vh;
+  transition: all var(--duration);
+
+  clip-path: var(--path);
+
+  background-color: rgba(0, 0, 0, 0.5);
+
+}
+
+.guide-box {
+  width: 50%;
+  transition: all var(--duration);
+  position: absolute;
+  color: var(--color);
+  background-color: #fff;
+  border-radius: 12rpx;
+  box-shadow: 2rpx 2rpx 2rpx #fff;
+  box-sizing: border-box;
+  padding: 20rpx;
+
+  display: flex;
+  flex-direction: column;
+}
+
+.msg-label {
+  font-size: 28rpx;
+}
+
+.step-box {
+  margin-top: auto;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding: 20rpx 0 0 0;
+  white-space: nowrap;
+}
+
+.step-btn {
+  margin-left: 20rpx;
+  box-shadow: 2rpx 2rpx 12rpx #ddd;
+  padding: 6rpx 16rpx;
+  border-radius: 8rpx;
+  font-size: 26rpx;
+}
+
+
+@-webkit-keyframes backInDown {
+  0% {
+    -webkit-transform: translateY(-1200px);
+    transform: translateY(-1200px);
+    opacity: 0.7;
+  }
+
+  80% {
+    -webkit-transform: translateY(0px);
+    transform: translateY(0px);
+    opacity: 0.7;
+  }
+
+  100% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+
+@keyframes backInDown {
+  0% {
+    -webkit-transform: translateY(-1200px);
+    transform: translateY(-1200px);
+    opacity: 0.7;
+  }
+
+  80% {
+    -webkit-transform: translateY(0px);
+    transform: translateY(0px);
+    opacity: 0.7;
+  }
+
+  100% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+@-webkit-keyframes backInUp {
+  0% {
+    -webkit-transform: translateY(1200px);
+    transform: translateY(1200px);
+    opacity: 0.7;
+  }
+
+  80% {
+    -webkit-transform: translateY(0px);
+    transform: translateY(0px);
+    opacity: 0.7;
+  }
+
+  100% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+@keyframes backInUp {
+  0% {
+    -webkit-transform: translateY(1200px);
+    transform: translateY(1200px);
+    opacity: 0.7;
+  }
+
+  80% {
+    -webkit-transform: translateY(0px);
+    transform: translateY(0px);
+    opacity: 0.7;
+  }
+
+  100% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1;
+  }
+}
+
+.animate__animated {
+  -webkit-animation-duration: var(--duration);
+  animation-duration: var(--duration);
+  -webkit-animation-duration: var(--duration);
+  animation-duration: var(--duration);
+  -webkit-animation-fill-mode: both;
+  animation-fill-mode: both;
+}
+</style>

+ 87 - 0
src/uni_modules/fast-guide/package.json

@@ -0,0 +1,87 @@
+{
+  "id": "fast-guide",
+  "displayName": "fast-guide 超好用的guide引导组件 制作用户操作引导页",
+  "version": "1.0.1",
+  "description": "用于制作用户操作引导",
+  "keywords": [
+    "用户引导",
+    "操作引导",
+    "引导页",
+    "guide",
+    "操作教程"
+],
+  "repository": "https://gitee.com/wwj123188/example-uniapp-components-fast-guide.git",
+"engines": {
+  },
+  "dcloudext": {
+    "type": "component-vue",
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "插件不采集任何数据",
+      "permissions": "无"
+    },
+    "npmurl": ""
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "y"
+      },
+      "client": {
+        "Vue": {
+          "vue2": "y",
+          "vue3": "y"
+        },
+        "App": {
+            "app-vue": "y",
+            "app-nvue": "u",
+            "app-uvue": "u",
+            "app-harmony": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y",
+          "钉钉": "y",
+          "快手": "y",
+          "飞书": "y",
+          "京东": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        }
+      }
+    }
+  }
+}

+ 295 - 0
src/uni_modules/fast-guide/readme.md

@@ -0,0 +1,295 @@
+# uniapp用户引导组件fast-guide
+
+#### 介绍
+简介:一个使用便捷的用户引导组件,支持自动/手动聚焦,组件聚焦,支持根据步骤聚焦引导,支持自定义聚焦提示词。
+框架兼容性:支持vue2、vue3
+平台兼容性:支持h5、小程序、app
+
+***  
+### 功能&特点
+* 【配置简单灵活】仅需维护一个数组,绑定操作方法即可使用功能。
+* 【强大的兼容性】支持vue2,vue3,支持h5、app及各家小程序。
+* 【流畅的交互体验】聚焦切换全部采用动画过渡,丝滑流畅!
+
+*** 
+
+#### 开发文档
+
+
+```vue
+/**
+ * guide 引导弹窗
+ * @description 引导组件,用于教学提示、用户操作引导等内容,支持自定义组件节点自动聚焦和手动设置相对位置聚焦。组件只提供容器,内部内容由用户自定义
+ * @tutorial //git地址
+ * @property {Boolean}			show				是否展示弹窗 (默认 false )
+ * @property {Number}			index				当前步骤索引(默认 0 )
+ * @property {String | Number}	duration			动画时长
+ * @property {String}			unit				换算单位
+ * @property {Array}			list				步骤列表
+ * @property {Object}			{
+ * 								@property{String}	ref					要聚焦的子组件的ref
+ * 								@property{String}	target				当前组件/子组件中的选择器标识
+ * 								@property{String}	position			提示框展示位置'top'/'bottom'
+ * 								@property{String}	msg					提示框文字
+ * 								@property{String}	msgStyles			提示框自定义样式
+ * 								@property{String}	width				自定义聚焦范围的宽度
+ * 								@property{String}	height				自定义聚焦范围的高度
+ * 								@property{String}	left				自定义聚焦范围的左侧偏移量 left/right仅生效一个	使用自定义聚焦后ref、target属性将失效
+ * 								@property{String}	right				自定义聚焦范围的右侧偏移量 left/right仅生效一个	使用自定义聚焦后ref、target属性将失效
+ * 								@property{String}	top					自定义聚焦范围的上侧偏移量 top/bottom仅生效一个	使用自定义聚焦后ref、target属性将失效
+ * 								@property{String}	bottom				自定义聚焦范围的下侧偏移量 top/bottom仅生效一个	使用自定义聚焦后ref、target属性将失效
+								}					list步骤列表内部参数说明
+ * @event {Function} open   	弹出层打开
+ * @event {Function} close  	弹出层收起
+ * @event {Function} focus  	聚焦区域点击事件
+ * @event {Function} mask		遮罩区域点击事件
+ * @event {Function} next   	执行下一步
+ * @event {Function} last   	执行上一步
+ * @event {Function} skip   	跳过所有步骤
+ * @event {Function} finish 	结束引导
+ * @event {Function} getQuery	获取兄弟组件布局查询对象
+ * @example <popup-guide :show="guideShow" :list="guideList" :index="guideIndex" @next="guideNext" @last="guideLast" @skip="guideSkip" @finish="guideFinish" ></popup-guide>
+ */
+```
+
+
+
+#### 示例代码
+
+```vue
+<template>
+	<view class="content" id="content">
+		<image class="logo" src="/static/logo.png"></image>
+		<view class="" style="padding-left: 20rpx;">
+			<view class="text-area">
+				<text class="title">{{title}}123</text>
+			</view>
+		</view>
+		
+		<view @click="again" class="again">
+			再来一次
+		</view>
+		<Test ref="TestNode"></Test>
+		<view class="" style="margin: 100rpx 0 0 150rpx;background-color: antiquewhite;padding: 50rpx;">
+			自定义聚焦
+		</view>
+		
+		<!-- 普通用法 -->
+		<fast-guide
+			:show="guideShow" 
+			:list="guideList" 
+			:index="guideIndex"
+			@open="guideOpen"
+			@close="guideClose"
+			@focus="guideFocus"
+			@mask="guideMask"
+			@next="guideNext" 
+			@last="guideLast" 
+			@skip="guideSkip" 
+			@finish="guideFinish"
+			@getQuery="getQuery"
+		></fast-guide>
+		
+		<!-- 作用域插槽用法 -->
+		<!-- <fast-guide 
+			:show="guideShow" 
+			:list="guideList" 
+			:index="guideIndex"
+			@open="guideOpen"
+			@close="guideClose"
+			@focus="guideFocus"
+			@mask="guideMask"
+			@next="guideNext" 
+			@last="guideLast" 
+			@skip="guideSkip" 
+			@finish="guideFinish"
+			@getQuery="getQuery"
+		>
+			<template #message="{msg}" >
+				<view class="" style="color: #333;">
+					【作用域插槽】{{msg}}
+				</view>
+			</template>
+		</fast-guide> -->
+	</view>
+</template>
+
+<script>
+	import Test from './component/test.vue'
+	
+	export default {
+		components:{
+			Test
+		},
+		data() {
+			return {
+				title: 'Hello',
+				
+				guideShow:false,
+				guideList:[
+					
+					{
+						target:'#content',
+						position:'bottom',
+						msg:'这是第一步'
+					},
+					{
+						target:'.logo',
+						msg:'这是第二步【自定义样式】',
+						position:'bottom',
+						msgStyles:{
+							'width':'60vw',
+							'color':'#ff5500'
+						}
+					},
+					{
+						target:'.text-area',
+						position:'top',
+						msg:'这是第三步'
+					},
+					{
+						// 使用ref指定组件聚焦时必须定义getQuery回调
+						ref:'TestNode',
+						target:'.children-node',
+						position:'bottom',
+						msg:'这是【子组件】聚焦,使用ref指定组件聚焦时必须定义getQuery回调'
+					},
+					{
+						// 使用ref指定组件聚焦时必须定义getQuery回调
+						ref:'TestNode',
+						target:'.deep-children-node',
+						position:'bottom',
+						msg:'这是【嵌套子组件】聚焦,使用ref指定组件聚焦时必须定义getQuery回调'
+					},
+					{
+						width:300,
+						height:180,
+						
+						right:150,
+						top:900,
+						position:'top',
+						msg:'这是自定义聚焦范围,支持响应式单位'
+					},
+					
+					{
+						target:'.again',
+						position:'bottom',
+						msg:'点击此处可以重新体验'
+					},
+					
+				],
+				guideIndex:0,
+				
+				testDom:{}
+			}
+		},
+		onLoad() {
+
+		},
+		mounted() {
+			this.guideShow = true
+		},
+		methods: {
+			guideOpen(e){
+				console.log(e,'guideOpen')
+				uni.showToast({
+					title:'打开引导',
+					icon:'none'
+				})
+			},
+			guideClose(e){
+				console.log(e,'guideClose')
+				uni.showToast({
+					title:'关闭引导',
+					icon:'none'
+				})
+			},
+			guideFocus(e){
+				console.log(e,'guideFocus')
+				uni.showToast({
+					title:'聚焦范围点击事件',
+					icon:'none'
+				})
+			},
+			guideMask(e){
+				console.log(e,'guideMask')
+				uni.showToast({
+					title:'遮罩范围点击事件',
+					icon:'none'
+				})
+			},
+			getQuery(fn){
+				//当使用ref属性进行引导时,此处传入需要聚焦的组件的父组件实例对象
+				console.log('父组件事件被触发,子组件获取兄弟组件布局查询对象')
+
+				// #ifdef VUE2
+				// vue2传入this
+				fn.call(this)
+				// #endif
+				
+				// #ifdef VUE3
+				// import { getCurrentInstance } from 'vue'
+				// vue3通过getCurrentInstance获取this
+				fn.call(getCurrentInstance().proxy)
+				// #endif
+			},
+			guideNext(e){
+				console.log(e,'guideNext')
+				this.guideIndex++
+			},
+			guideLast(e){
+				console.log(e,'guideLast')
+				this.guideIndex--
+			},
+			guideSkip(e){
+				console.log(e,'guideSkip')
+				this.guideShow = false
+			},
+			guideFinish(e){
+				console.log(e,'guideFinish')
+				this.guideShow = false
+			},
+			again(){
+				this.guideIndex = 0
+				this.guideShow = true
+			}
+		}
+	}
+</script>
+
+<style>
+	.content {
+		width: 100vw;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.logo {
+		height: 200rpx;
+		width: 200rpx;
+		margin-top: 200rpx;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 50rpx;
+	}
+
+	.text-area {
+		display: flex;
+		justify-content: center;
+	}
+
+	.title {
+		font-size: 36rpx;
+		color: #8f8f94;
+	}
+	
+	/* 深度选择修改步骤盒子样式 */
+	/* /deep/ .step-btn{
+		color: red;
+	} */
+</style>
+
+```
+