ソースを参照

feat: 添加最后一天签到弹窗

huangziyang 5 日 前
コミット
86db30312f

+ 27 - 6
components/Topic/Questions.vue

@@ -5,9 +5,15 @@
       'question-item-selected':
         question.checked && !props.showResult && styleCount !== 6,
       'question-item-correct':
-        question.checked && props.showResult && question.isRight && styleCount !== 6,
+        question.checked &&
+        props.showResult &&
+        question.isRight &&
+        styleCount !== 6,
       'question-item-wrong':
-        question.checked && props.showResult && !question.isRight && styleCount !== 6,
+        question.checked &&
+        props.showResult &&
+        !question.isRight &&
+        styleCount !== 6,
     }"
     :style="{
       border: styleCount !== 6 ? '1px solid #ccc' : 0,
@@ -34,7 +40,10 @@
     <view
       class="option-icon"
       v-if="
-        question.checked && props.showResult && !question.isRight && styleCount !== 6
+        question.checked &&
+        props.showResult &&
+        !question.isRight &&
+        styleCount !== 6
       "
     >
       <uni-icons type="closeempty" color="#f00"></uni-icons>
@@ -42,7 +51,10 @@
     <view
       class="option-icon"
       v-if="
-        question.checked && props.showResult && question.isRight && styleCount !== 6
+        question.checked &&
+        props.showResult &&
+        question.isRight &&
+        styleCount !== 6
       "
     >
       <uni-icons type="checkmarkempty" color="#00be00"></uni-icons>
@@ -86,8 +98,13 @@ const props = defineProps({
 // Emits 定义
 const emit = defineEmits(["select"]);
 
-const replaceImageDimensions = (str) =>
-  str
+const replaceImageDimensions = (str) => {
+  // 将图片宽度设为100%
+  str = str
+    .replace(
+      /<img([^>]*?)src="([^"]*?)"([^>]*?)\/?>/g,
+      '<img$1src="$2"$3 style="width:100%;height:100%;"/>'
+    )
     .replace(
       /<img([^>]*?)style="([^"]*?)width:\s*[^;]+;\s*height:\s*[^;]+;([^"]*?)"([^>]*?)\/?>/g,
       '<img$1style="$2width:100%;height:100%;$3"$4/>'
@@ -96,6 +113,10 @@ const replaceImageDimensions = (str) =>
       /<img([^>]*?)width="[^"]*"([^>]*?)height="[^"]*"([^>]*?)\/?>/g,
       '<img$1width="100%"$2height="100%"$3/>'
     );
+  console.log(str);
+
+  return str;
+};
 
 // 方法
 const handleClick = () => {

+ 4 - 0
components/Topic/QuestionsItemSelect.vue

@@ -78,6 +78,10 @@ const emit = defineEmits(["select"]);
 
 const replaceImageDimensions = (str) =>
   str
+    .replace(
+      /<img([^>]*?)src="([^"]*?)"([^>]*?)\/?>/g,
+      '<img$1src="$2"$3 style="width:100%;height:100%;"/>'
+    )
     .replace(
       /<img([^>]*?)style="([^"]*?)width:\s*[^;]+;\s*height:\s*[^;]+;([^"]*?)"([^>]*?)\/?>/g,
       '<img$1style="$2width:100%;height:100%;$3"$4/>'

+ 33 - 14
pages.json

@@ -182,20 +182,39 @@
       }
     },
     {
-    	"path" : "pages/success/paysuccess",
-    	"style" : 
-    	{
-			"navigationStyle": "custom",
-    		"navigationBarTitleText" : "支付成功"
-    	}
-    },
-    {
-    	"path" : "pages/success/error",
-    	"style" : 
-    	{
-			"navigationStyle": "custom",
-    		"navigationBarTitleText" : "支付失败"
-    	}
+      "path": "pages/success/paysuccess",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "支付成功"
+      }
+    },
+    {
+      "path": "pages/success/error",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "支付失败"
+      }
+    },
+    {
+      "path": "pages/punch/index",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "签到打卡"
+      }
+    },
+    {
+      "path": "pages/learn/dayPractice",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "每日一练"
+      }
+    },
+    {
+      "path": "pages/index/yearExam",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "历史真题"
+      }
     }
   ],
   "globalStyle": {

+ 136 - 83
pages/index/index.vue

@@ -48,56 +48,61 @@
             <view class="notice-more">{{ "更多 >" }}</view>
           </navigator>
         </view>
+        <div class="qiandao">
+          <div class="left">
+            <span class="text-16" style="font-weight: bold">每日一练</span>
+            <span class="text-12 text-t9"
+              >今日已有<span class="text-primary">48</span>人打卡</span
+            >
+          </div>
+          <div
+            class="bt"
+            :style="{
+              backgroundColor: !success ? '#FF6666' : '#3FCCFF',
+            }"
+            @click="onClickToPunch"
+          >
+            {{ !success ? "今日未打卡" : "今日已打卡" }}
+          </div>
+        </div>
         <!-- 2025新大纲 -->
         <view class="new_outline">
-          <uni-section title="基础用法" type="line">
-            <view class="p-20">
-              <uni-segmented-control
-                :flex="false"
-                :current="current"
-                :values="list.map((item) => item.name)"
-                style-type="text"
-                @clickItem="(e) => (current = e.currentIndex)"
-              />
-              <!-- 执业药师 -->
-              <template v-for="(item, index) in list" :key="item.id">
-                <view v-if="current === index" class="grid">
-                  <view
-                    v-for="i in item?.children"
-                    :key="i.id"
-                    class="flex"
-                    @click="clickClass(i)"
-                  >
-                    <image :src="i.chapter_image_url" class="img_small"></image>
-                    <view>{{ i.name }}</view>
-                  </view>
-                </view>
-              </template>
-            </view>
-          </uni-section>
-        </view>
-        <view class="p-20">
-          <uni-section title="往年真题" type="line">
-            <!-- 往年真题 -->
-            <view class="grid-3">
-              <view
-                v-for="item in rightList"
-                :key="item.originName"
-                class="flex"
-                @click="
-                  toReal({
-                    title: item.name + '真题',
-                    origin: item.originName,
-                  })
-                "
-              >
-                <view class="bg-red">
-                  <view class="text">{{ item.name }}真题</view>
+          <uni-section title="执业药师章节练习" type="line">
+            <template v-for="(item, index) in list" :key="item.id">
+              <view v-if="current === index" class="grid">
+                <view
+                  v-for="i in item?.children"
+                  :key="i.id"
+                  class="flex"
+                  @click="clickClass(i)"
+                >
+                  <image :src="i.chapter_image_url" class="img_small"></image>
+                  <view>{{ i.name }}</view>
                 </view>
               </view>
-            </view>
+            </template>
           </uni-section>
         </view>
+        <div class="flex-box">
+          <div
+            class="children lishi"
+            @click="
+              router.push({
+                url: '/pages/index/yearExam',
+              })
+            "
+          >
+            <div>往年真题</div>
+            <div class="te">10年真题卷</div>
+            <div class="cricle">{{ ">" }}</div>
+          </div>
+          <div class="children gaopin">
+            <div>高频考点</div>
+            <div class="te">经典拿分题</div>
+            <div class="cricle">{{ ">" }}</div>
+          </div>
+        </div>
+
         <!-- #ifdef  MP-WEIXIN -->
         <official-account @load="onload"></official-account>
         <!-- #endif -->
@@ -144,9 +149,17 @@ const list = ref([]);
 const banner_list = ref([]);
 const notice_list = ref([]);
 const imgHeight = ref("auto"); // 初始高度
-const rightList = ref([]);
 const showModal = ref(false);
+const success = ref(false);
 
+const onClickToPunch = () => {
+  router.push({
+    url: "/pages/learn/dayPractice",
+    params: {
+      title: "每日一练",
+    },
+  });
+};
 const days = ref("000"); // 默认值设为100,确保有3位数
 let timer = null;
 const exam_time = ref(""); //考试时间
@@ -195,13 +208,6 @@ const clickClass = ({ id, name }) => {
   });
 };
 
-const toReal = (item) => {
-  router.push({
-    url: "/pages/real/index",
-    params: item,
-  });
-};
-
 const onImageLoad = (e, item, index) => {
   const { width, height } = e.detail;
   const ratio = height / width;
@@ -258,37 +264,6 @@ onMounted(async () => {
   exam_time.value =
     examination_res != "" ? examination_res.data.content_detail : "";
 
-  // 获取历年真题
-  const rightListRes = await request(
-    "api/question_bank/question_reception/real_catalogue/get_year",
-    {},
-    "post"
-  );
-  rightList.value = rightListRes.data.map((i) => {
-    const newItem = {
-      name: i + "年",
-      originName: i,
-    };
-    if (i.includes("(")) {
-      // 2022(1)变成 2022年(一)
-      newItem.name = i.replace(/\((\d+)\)/g, (text, number) => {
-        const chineseNumber = [
-          "一",
-          "二",
-          "三",
-          "四",
-          "五",
-          "六",
-          "七",
-          "八",
-          "九",
-        ];
-        return `年(${chineseNumber[number - 1]})`;
-      });
-    }
-    return newItem;
-  });
-
   if (!isLogin()) {
     showModal.value = true;
   }
@@ -305,7 +280,85 @@ onMounted(async () => {
 </script>
 
 <style scoped lang="scss">
+// https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/kPFuCa2RDyoUXbiCzj0MOwcCm7XowJcWqNqs18oB.png 往年真题
+// https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/WsEF9w5bs7Gknnf3vUV3EMR0WxqleESpYSVxZtkn.png 高频考点
 @import "@/uni.scss";
+
+.flex-box {
+  display: flex;
+  align-items: center;
+  gap: 10rpx;
+  .children {
+    flex: 1;
+    height: 173rpx;
+    width: 100%;
+    padding: 24rpx 12rpx;
+    box-sizing: border-box;
+    color: #333333;
+  }
+
+  @mixin text($bg) {
+    .te {
+      color: $bg;
+      font-weight: 400;
+      font-size: 24rpx;
+    }
+
+    .cricle {
+      width: 32rpx;
+      height: 32rpx;
+      background: $bg;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #fff;
+      border-radius: 50%;
+      font-size: 20rpx;
+      margin: 14rpx 0 0 8rpx;
+    }
+  }
+
+  .lishi {
+    background: url(https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/kPFuCa2RDyoUXbiCzj0MOwcCm7XowJcWqNqs18oB.png);
+    background-size: 100% 100%;
+    @include text(#00a3ff);
+  }
+  .gaopin {
+    background: url(https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/WsEF9w5bs7Gknnf3vUV3EMR0WxqleESpYSVxZtkn.png);
+    background-size: 100% 100%;
+    @include text(#ff6a00);
+  }
+}
+.qiandao {
+  background: url(https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/hASs5yhNTWUMTAGdod21hDIcibFNFSDTbiGpEMd0.png)
+    no-repeat;
+  background-size: 100% 100%;
+  width: 100%;
+  height: 104rpx;
+  display: flex;
+  justify-content: space-between;
+  box-sizing: border-box;
+  padding: 10rpx 16rpx;
+  align-items: center;
+  .left {
+    display: flex;
+    flex-direction: column;
+    gap: 4rpx;
+  }
+
+  .bt {
+    width: 188rpx;
+    height: 55rpx;
+    background: #ff6666;
+    border-radius: 200rpx 200rpx 200rpx 200rpx;
+    font-weight: 400;
+    font-size: 28rpx;
+    color: #ffffff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
 .mo {
   display: flex;
   flex-direction: column;

+ 134 - 0
pages/index/yearExam.vue

@@ -0,0 +1,134 @@
+<script setup>
+import Container from "../../components/Container/Container.vue";
+import { ref } from "vue";
+import { request } from "../../utils/request";
+import { router } from "../../utils/router";
+import { onShow } from "@dcloudio/uni-app";
+
+const rightList = ref([]);
+onShow(async () => {
+  // 获取历年真题
+  const rightListRes = await request(
+    "api/question_bank/question_reception/real_catalogue/get_year",
+    {},
+    "post"
+  );
+  rightList.value = rightListRes.data.map((i) => {
+    const newItem = {
+      name: i + "年",
+      originName: i,
+    };
+    if (i.includes("(")) {
+      // 2022(1)变成 2022年(一)
+      newItem.name = i.replace(/\((\d+)\)/g, (text, number) => {
+        const chineseNumber = [
+          "一",
+          "二",
+          "三",
+          "四",
+          "五",
+          "六",
+          "七",
+          "八",
+          "九",
+        ];
+        return `年(${chineseNumber[number - 1]})`;
+      });
+    }
+    return newItem;
+  });
+});
+
+const toReal = (item) => {
+  router.push({
+    url: "/pages/real/index",
+    params: item,
+  });
+};
+</script>
+<template>
+  <Container title="往年真题">
+    <view class="p-20">
+      <view class="grid-3">
+        <view
+          v-for="item in rightList"
+          :key="item.originName"
+          class="flex"
+          @click="
+            toReal({
+              title: item.name + '真题',
+              origin: item.originName,
+            })
+          "
+        >
+          <view class="bg-red">
+            <view class="text">{{ item.name }}真题</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </Container>
+</template>
+<style scoped lang="scss">
+// https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/kPFuCa2RDyoUXbiCzj0MOwcCm7XowJcWqNqs18oB.png 往年真题
+// https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/WsEF9w5bs7Gknnf3vUV3EMR0WxqleESpYSVxZtkn.png 高频考点
+@import "@/uni.scss";
+
+.p-20 {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  background-color: #ffffff;
+  padding: 24rpx;
+  gap: 16rpx;
+  border-radius: 16rpx;
+}
+
+.grid-3 {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  background-color: #ffffff;
+  padding: 24rpx;
+  gap: 16rpx;
+  border-radius: 16rpx;
+  height: 100%;
+  margin-bottom: 20rpx;
+}
+
+.flex {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+  align-items: center;
+  justify-content: center;
+}
+
+.bg-red {
+  width: 191.07rpx;
+  height: 179.61rpx;
+  background: url("https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/WmhlbORF2q8A62Ytg1RVac8AYSGPkf7F2pEY6jQP.png")
+    no-repeat;
+  background-size: cover;
+  display: flex;
+  justify-content: center;
+
+  .text {
+    font-family: jiangxizhuokai, jiangxizhuokai;
+    font-weight: bold;
+    font-size: 27rpx;
+    color: #3f75ff;
+    text-shadow: 0px 2px 4px #bdcfff;
+    text-align: left;
+    font-style: normal;
+    text-transform: none;
+    margin-top: 16rpx;
+    width: 95rpx;
+    height: 70rpx;
+  }
+}
+</style>

+ 333 - 0
pages/learn/dayPractice.vue

@@ -0,0 +1,333 @@
+<script setup name="Exam">
+import TopicPractice from "../../components/Topic/TopicPractice.vue";
+import { ref, onMounted } from "vue";
+import { getRoute, router } from "../../utils/router";
+import { request } from "../../utils/request";
+import { useTimeStore } from "@/store/time";
+import Container from "../../components/Container/Container.vue";
+import ShareTemplate from "../../components/ShareTemplate/ShareTemplate.vue";
+
+const Time = useTimeStore();
+
+const TopicMapList = ["A", "B", "C", "D", "E"];
+
+const title = ref("");
+const total = ref(100000);
+const correct = ref({
+  rate: 0, // 正确率
+  error: 0, // 错误数
+  right: 0, // 正确数
+  not: 0, // 未答题
+  total: 0, // 总题数
+});
+
+const showReport = ref(false);
+const showShare = ref(false);
+const submitter = ref({
+  closeText: "直接退出",
+  context: "您本次答题还未提交, 确定要退出答题吗?",
+  isEndQuestion: false,
+  text: "提交查看",
+  totalTime: {
+    formatTime: "00:00",
+    totalTime: 0,
+  },
+});
+
+const pageParams = ref({
+  page: 1,
+  limit: 999,
+});
+const data = ref([]);
+const user_exercise_paper_id = ref(0);
+const getList = async (params) => {
+  if (pageParams.value.page * pageParams.value.limit >= total.value) return;
+  const res = await request(
+    "api/question_bank/question_reception/topic/day_practice",
+    {
+      ...params,
+    }
+  );
+  total.value = res.data.total;
+  data.value.push(
+    ...res.data.data.map((item, ind) => {
+      let questions = [];
+      const ans = item.correct_answer.split(",");
+      const ansList = [];
+      for (let i = 0; i < item.question_count; i++) {
+        const current = TopicMapList[i];
+        if (ans.includes(current)) {
+          ansList.push({
+            label: current,
+            value: i,
+          });
+        }
+        const v =
+          item[`select_${current.toLowerCase()}`].replace(/<br\s*\/?>/g, "") ||
+          "";
+        questions.push({
+          label: v,
+          value: current,
+          checked: false,
+          index: i,
+        });
+      }
+
+      return {
+        ...item,
+        questions, // 题目
+        ansList, // 正确答案
+        selectAns: [], // 选择的答案
+        showResult: false, // 是否展示答案
+        isRight: false, // 是否正确
+        isImage: item.title.includes("<img"),
+        ind,
+      };
+    })
+  );
+  pageParams.value.page++;
+};
+
+const nextPage = (e) => {
+  if (pageParams.value.page * pageParams.value.limit - e.index - 1 !== 1)
+    return;
+  getList(pageParams.value);
+};
+
+const onStar = (item) =>
+  new Promise((resolve) => {
+    request(
+      !item.is_favorite
+        ? "api/question_bank/question_reception/topic/set_favorite"
+        : "api/question_bank/question_reception/topic/cancel_favorite",
+      {
+        id: item.id,
+        chapter_id: item.chapter_id,
+      }
+    ).then(() => {
+      item.is_favorite = item.is_favorite ? 0 : 1;
+      resolve(item.is_favorite);
+    });
+  });
+
+const lookReport = async (d, s) => {
+  data.value = d;
+  const totalTime = Time.end();
+  showReport.value = true;
+  submitter.value = {
+    ...s,
+    totalTime,
+  };
+  const r = data.value.filter((item) => item.isRight).length;
+  const n = data.value.filter((item) => !item.selectAns.length).length;
+  correct.value = {
+    rate: (r / total.value) * 100,
+    right: r,
+    error: total.value - r - n,
+    not: n,
+    total: total.value,
+  };
+};
+
+onMounted(() => {
+  Time.start();
+  const params = getRoute().params;
+  title.value = params.title;
+  getList(pageParams.value);
+});
+</script>
+
+<template>
+  <TopicPractice
+    :title="title"
+    :total="total"
+    mode="practice"
+    :topics="data"
+    :user_exercise_paper_id="user_exercise_paper_id"
+    @nextPage="nextPage"
+    @lookReport="lookReport"
+    :onStar="onStar"
+    :empty="!data.length"
+    text="加载中···"
+    border
+    v-if="!showReport && !showShare"
+  />
+  <Container
+    bgColor="url('https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/7i6ROn34tZGe8QR4gaWDP1ZzyPYjqlvPgLLFRmIe.png')"
+    bottomBorder
+    v-else-if="!showShare && showReport"
+    themeColor="#fff"
+    bottomBgColor="#fff"
+    title="练习报告"
+  >
+    <view class="reslut">
+      <div class="title-r">恭 喜 完 成 考 试</div>
+      <view class="card">
+        <view class="card-time">
+          <view>{{ submitter.totalTime?.formatTime }}</view>
+          <view>学习时长</view>
+        </view>
+        <view class="card-time">
+          <view class="red">{{ correct.rate.toFixed(2) }}%</view>
+          <view>正确率</view>
+        </view>
+      </view>
+      <view class="template">
+        <view class="header">
+          <view>答题卡</view>
+          <view class="right-error">
+            <view class="right">答对({{ correct.right }})</view>
+            <view class="error">答错({{ correct.error }})</view>
+          </view>
+        </view>
+        <view class="group">
+          <view
+            class="item"
+            :class="{
+              right: it.isRight && it.showResult,
+              error: !it.isRight && it.showResult && it.selectAns.length,
+            }"
+            v-for="(it, index) in data"
+            :key="it.id"
+            >{{ index + 1 }}</view
+          >
+        </view>
+      </view>
+    </view>
+    <template #footer>
+      <view class="footer">
+        <view class="button plain" @click="showReport = false">答题解析</view>
+        <view class="button" @click="showShare = true">炫耀一下</view>
+      </view>
+    </template>
+  </Container>
+  <ShareTemplate
+    :onClose="() => (showShare = false)"
+    :correct="correct"
+    bg="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/ix4wGk9h7Fb4c3d8hjkYjJP6VsPNbZgG3uDTUzxp.png"
+    v-else
+  />
+</template>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+.template {
+  padding: 15rpx;
+  border-radius: 24rpx;
+  background-color: #f8f9fb;
+}
+.red {
+  color: #ff4444;
+}
+
+.title-r {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 500;
+  font-size: 50rpx;
+  color: #002fa7;
+}
+.card {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 63rpx;
+  padding: 40rpx 50rpx;
+  background-color: #f8f9fb;
+  border-radius: 24rpx;
+  &-time {
+    font-family: PingFang SC, PingFang SC;
+    font-weight: 500;
+    font-size: 28rpx;
+    color: #999999;
+    display: flex;
+    flex-direction: column;
+    gap: 10rpx;
+    align-items: center;
+  }
+}
+
+.reslut {
+  margin: 0 32rpx;
+  padding: 24rpx;
+  font-family: PingFang SC, PingFang SC;
+  font-weight: 500;
+  font-size: 28rpx;
+  color: #333333;
+  background: #fff;
+  border-radius: 24rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .right-error {
+      display: flex;
+      gap: 20rpx;
+      font-weight: 400;
+      font-size: 20rpx;
+      color: #333333;
+      @mixin type($color) {
+        display: flex;
+        align-items: center;
+        gap: 10rpx;
+        &::before {
+          content: "";
+          width: 16rpx;
+          height: 16rpx;
+          border-radius: 50%;
+          display: block;
+          background: $color;
+        }
+      }
+      .right {
+        @include type($success);
+      }
+      .error {
+        @include type($error);
+      }
+      .not {
+        @include type($default);
+      }
+    }
+  }
+
+  .group {
+    display: grid;
+    grid-template-columns: repeat(6, 1fr);
+    gap: 20rpx;
+    margin-top: 20rpx;
+
+    .item {
+      width: 72rpx;
+      height: 72rpx;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: $default;
+      font-family: PingFang SC, PingFang SC;
+      font-weight: 500;
+      font-size: 28rpx;
+      color: #ffffff;
+    }
+    .item.right {
+      background: $success;
+    }
+    .item.error {
+      background: $error;
+    }
+  }
+}
+
+.footer {
+  display: flex;
+  gap: 20rpx;
+  align-items: center;
+  padding-top: 20rpx;
+}
+</style>

+ 335 - 0
pages/punch/index.vue

@@ -0,0 +1,335 @@
+<script setup>
+import Container from "../../components/Container/Container.vue";
+import LDialer from "../../uni_modules/lime-dialer/components/l-dialer/l-dialer.vue";
+import Modal from "../../components/Modal/Modal.vue";
+import { ref } from "vue";
+import { request } from "../../utils/request";
+import { onShow } from "@dcloudio/uni-app";
+const prizeList = ref([
+  {
+    id: "coupon88",
+    name: "8.8折",
+    img: "https://img11.360buyimg.com/pop/jfs/t1/175718/35/12595/5477/60b660c6Eb850717b/a1cfe750dcdb5b78.png",
+  },
+  {
+    id: "coupon900",
+    name: "900",
+    img: "https://img11.360buyimg.com/pop/jfs/t1/190845/9/6092/4489/60b65fe8Ebb8f8284/955da889f6d1c13e.png",
+  },
+]);
+
+const data = ref([
+  {
+    name: "一",
+  },
+  {
+    name: "二",
+  },
+  {
+    name: "三",
+  },
+  {
+    name: "四",
+    isEnd: true,
+  },
+]);
+const dialer = ref(null);
+const onClick = async () => {
+  if (lottery.value == 0) {
+    uni.showToast({
+      title: "暂无抽奖次数",
+      icon: "none",
+    });
+    return;
+  }
+  const reward = await request(
+    "api/question_bank/question_reception/lottery_sign_in/get_reward"
+  );
+  dialer.value.run(reward.data.reward_index);
+};
+const lottery = ref(0);
+const day = ref(0);
+
+const onDone = (_, origin) => {
+  uni.showModal({
+    title: "恭喜你获得",
+    content: origin.name,
+  });
+};
+
+const getList = async () => {
+  const getlottery = await request(
+    "api/question_bank/question_reception/lottery_usable/detail"
+  );
+  lottery.value = getlottery.data.number;
+  const getData = await request(
+    "api/question_bank/question_reception/lottery_sign_in/list"
+  );
+  prizeList.value = getData.data.reward_list.map((item) => {
+    if (item.status) {
+      day.value++;
+    }
+    return {
+      ...item,
+      name: item.reward_name,
+    };
+  });
+};
+const showModal = ref({
+  show: false,
+});
+const qiandao = async () => {
+  const res = await request(
+    "api/question_bank/question_reception/check_in/set_user_record",
+    {
+      activity_id: 1,
+    }
+  );
+  if (res.code === "error") return;
+  if (day.value === data.value.length) {
+    const reward = await request(
+      "api/question_bank/question_reception/lottery_sign_in/get_reward"
+    );
+    showModal.value = {
+      show: true,
+      ...reward.data,
+    };
+  }
+};
+
+onShow(getList);
+</script>
+<template>
+  <Container title="签到打卡" themeColor="#fff">
+    <template #bg>
+      <img
+        src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/Au9JYRsneh9TmtnDw0ya2xctIvItn4SvpKgUl5Kh.png"
+        width="750rpx"
+        height="2059rpx"
+        style="width: 750rpx; height: 2059rpx"
+        alt=""
+      />
+    </template>
+    <div class="items">
+      <div class="gifs">
+        <div
+          v-for="item in data"
+          class="gif-item"
+          :style="{
+            width: item.isEnd ? '100%' : '180rpx',
+          }"
+          :key="item.name"
+        >
+          <div class="mask" v-if="item.status">
+            <img
+              src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/L3NICP0cpYtUe4RFI5s1vyYTFCYmjjbqfc5L8LiJ.png"
+              style="width: 118rpx; height: 118rpx"
+              alt=""
+            />
+          </div>
+          <template v-if="!item.isEnd">
+            <div class="name">第{{ item.name }}天</div>
+            <img
+              style="
+                width: 118rpx;
+                height: 117.81rpx;
+                background-size: 100% 100%;
+              "
+              src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/lfczc5uWYEfK4XP4GtlTRlmCtFHVapryiR7AgWLE.png"
+              alt=""
+            />
+          </template>
+          <template v-else>
+            <div class="name">第{{ item.name }}天</div>
+            <div style="display: flex; align-items: center">
+              <span class="text-error">神秘</span>
+              <img
+                src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/7Is5IMCcauPZYS5MZJjKEThHaHAPFllI2pPf0AKI.png"
+                style="width: 130rpx; height: 137rpx"
+                alt=""
+              />
+              <span class="text-error">大奖</span>
+            </div>
+          </template>
+        </div>
+      </div>
+      <div class="button b" @click="qiandao">参与打卡签到</div>
+      <span class="text-t9 text-12"
+        >需完成每日一练/完成一次题库练习<span class="text-warning"
+          >并分享</span
+        ></span
+      >
+    </div>
+    <div class="dialer">
+      <l-dialer
+        :prizeList="prizeList"
+        @click="onClick"
+        @done="onDone"
+        ref="dialer"
+        size="230"
+      />
+      <img
+        @click="onClick"
+        class="btn"
+        src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/tpWgvlnwA5G7A3PYZ4Op535n1M1uiYrNbftl7B4m.png"
+        alt=""
+      />
+      <div style="color: #fff">剩余 {{ lottery }} 次</div>
+    </div>
+  </Container>
+  <Modal v-model:open="showModal.show">
+    <template #custom>
+      <div class="css">
+        <div class="bac">
+          <img
+            class="header"
+            src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/qXKNJDt0XxL1f1VBzaQaavmGuAdyVBSopAt3xmq9.png"
+            alt=""
+          />
+          <div class="middle">
+            <img class="img" v-if="showModal.img" :src="showModal.img" alt="" />
+            <div>{{ showModal.name }}</div>
+          </div>
+          <div class="t">开心收下</div>
+        </div>
+        <img @click="showModal = { show: false }" src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/v9DO9q0OYMwZ58Cb5YHO3z7ceXXEMx5Jf59bPT2w.png" class="close"></img>
+      </div>
+    </template>
+  </Modal>
+</template>
+<style lang="scss" scoped>
+.b {
+  background: linear-gradient(90deg, #ff8f18 0%, #fe6002 100%);
+  border-radius: 200rpx 200rpx 200rpx 200rpx;
+  margin: 32rpx 0 10rpx;
+}
+.bac {
+  background: url(https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/2z6PwP2ooQkSOXVeydIewjpVvL0h0hgDD2srfUqK.png);
+  background-size: 100% 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-direction: column;
+  width: 548rpx;
+  height: 658rpx;
+  box-sizing: border-box;
+  padding: 40rpx 0 56rpx;
+
+  .middle {
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+    gap: 9rpx;
+    .img {
+      width: 208rpx;
+      height: 208rpx;
+      background-size: 100% 100%;
+    }
+  }
+
+  .header {
+    width: 400rpx;
+    height: 88rpx;
+    background-size: 100% 100%;
+  }
+
+  .t {
+    width: 272rpx;
+    height: 77rpx;
+    font-weight: 500;
+    font-size: 32rpx;
+    color: #ffffff;
+    background: linear-gradient(180deg, #ff211a 0%, #ffb883 100%);
+    box-shadow: inset 0rpx -2rpx 1rpx 0rpx #fff6b6,
+      inset 0rpx 4rpx 6rpx 0rpx rgba(255, 255, 255, 0.6);
+    border-radius: 50rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+.css {
+  display: flex;
+  flex-direction: column;
+  gap: 32rpx;
+  align-items: center;
+  .close {
+    width: 48rpx;
+    height: 48rpx;
+  }
+}
+
+
+.items {
+  background: url(https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/9rtXotBySN0yiKmoyT0d41QPox63L3F8Gpdvyt00.png)
+    no-repeat;
+  background-size: 100% 100%;
+  width: 654rpx;
+  height: 740rpx;
+  margin: 165rpx auto 60rpx;
+  padding: 23rpx 33rpx;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  align-items: center;
+
+  .gifs {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 25rpx;
+    width: 100%;
+
+    .gif-item {
+      background: #ffeedd;
+      border-radius: 16rpx;
+      height: 194rpx;
+      font-weight: bold;
+      font-size: 28rpx;
+      color: #e9782b;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      position: relative;
+
+      .mask {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background-color: rgba($color: #000000, $alpha: 0.4);
+        position: absolute;
+        border-radius: 16rpx;
+      }
+    }
+
+    .end {
+      grid-column-start: 3;
+    }
+  }
+}
+
+.dialer {
+  background-size: 100% 100%;
+  width: 534rpx;
+  height: 758rpx;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-sizing: border-box;
+
+  background: url(https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/06/JDNYxtJNVLcDvwyAxDRvx0SOmL2Eca42zJRNko2v.png)
+    no-repeat;
+  background-size: 100% 100%;
+  padding-top: 34rpx;
+  .btn {
+    margin: 106rpx 0 0;
+    width: 300rpx;
+    height: 103rpx;
+  }
+}
+</style>

+ 16 - 0
uni_modules/lime-dialer/changelog.md

@@ -0,0 +1,16 @@
+## 0.2.5(2024-11-11)
+- fix: 优化styleOpt样式
+## 0.2.4(2024-07-19)
+- chore: 更新文档,增加背景框
+## 0.2.3(2024-05-27)
+- fix: 修复只有2项时无法显示的问题
+## 0.2.2(2024-05-07)
+- chore: stylus 改成 scss
+- fix: 修复vue3点击多触发问题
+## 0.2.1(2023-05-08)
+- chore: 增加示例
+## 0.2.0(2021-07-09)
+- chore: 统一命名规范,无须主动引入组件
+## 0.1.0(2021-06-16)
+- 首次上传
+- 纯CSS实现的抽奖转盘

+ 129 - 0
uni_modules/lime-dialer/components/l-dialer/index.scss

@@ -0,0 +1,129 @@
+@mixin theme($property, $variable) {
+  $theme: (
+    "dialer_text_color": #ffb400,
+    "dialer_prize_font_size": 12px,
+    "dialer_prize_name_padding": 8px,
+    "dialer_prize_inner_padding": 8px,
+    "dialer_prize_image_size": 36px,
+  );
+
+  $value: map-get($theme, $variable);
+  #{$property}: $value;
+
+  /* #ifndef APP-IOS || APP-ANDROID */
+  $css-variable: var(--#{$variable}, #{$value});
+  #{$property}: #{$css-variable};
+  /* #endif */
+}
+
+.l-dialer {
+  position: relative;
+
+  &__inner {
+    width: 100%;
+    height: 100%;
+    flex: 1;
+    position: relative;
+    @include theme("color", "dialer_text_color");
+    background-repeat: no-repeat;
+    background-size: cover;
+    box-sizing: border-box;
+    border-radius: 999px;
+    overflow: hidden;
+    // transition: transform 3s ease;
+    // transform-origin: 50% 50%;
+    transition-property: transform;
+    transition-timing-function: cubic-bezier(0.25, 0.46, 0.455, 0.995);
+    &-border {
+      position: absolute;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      right: 0;
+    }
+
+    &-wrap {
+      position: relative;
+      z-index: 1;
+      width: 100%;
+      height: 100%;
+      border-radius: 999px;
+      overflow: hidden;
+      box-sizing: border-box;
+      // background: red;
+      /* #ifndef APP-ANDROID */
+      &::after {
+        position: absolute;
+        left: 0;
+        top: 0;
+        bottom: 0;
+        right: 0;
+        content: "";
+        border-radius: 50%;
+        box-shadow: 0 0 20rpx currentColor inset;
+      }
+      /* #endif */
+    }
+
+    &-item {
+      overflow: hidden;
+      position: absolute;
+      top: -50%;
+      left: 50%;
+      width: 100%;
+      height: 100%;
+      transform-origin: 0 100%;
+    }
+
+    &-content {
+      position: absolute;
+      @include theme("padding-top", "dialer_prize_inner_padding");
+      box-sizing: border-box;
+      width: 100%;
+      height: 100%;
+      left: -50%;
+      bottom: -50%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+    }
+
+    &-name {
+      @include theme("padding-top", "dialer_prize_name_padding");
+      @include theme("padding-bottom", "dialer_prize_name_padding");
+      @include theme("font-size", "dialer_prize_font_size");
+      @include theme("color", "dialer_text_color");
+    }
+
+    &-img {
+      @include theme("width", "dialer_prize_image_size");
+      @include theme("height", "dialer_prize_image_size");
+    }
+  }
+
+  &__pointer {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 1;
+  }
+}
+
+/* #ifndef APP-IOS || APP-ANDROID */
+.heart {
+  animation: heart 1s infinite;
+}
+
+@keyframes heart {
+  0% {
+    transform: scale(1);
+  }
+  25% {
+    transform: scale(1.03);
+  }
+  to {
+    transform: scale(1);
+  }
+}
+/* #endif */

+ 111 - 0
uni_modules/lime-dialer/components/l-dialer/index.styl

@@ -0,0 +1,111 @@
+replace2(val)
+	r = match('\$[^() ]+', val, 'gi')
+	re = val
+	for v, i in r
+		k = split(v, re)
+		j = s('%s', convert(v))
+		re = join(j, k)
+	unquote(re)
+
+theme($property, $imp)
+	a = replace('(\$[^() ]+)', '$1', $imp)
+	// #ifndef APP-NVUE
+	b = replace('(\$)([^() ]+)', 'var(--$2, $1$2)', $imp)
+	// #endif
+	{$property} replace2(a)
+	// #ifndef APP-NVUE
+	{$property} replace2(b)
+	// #endif
+		
+$dialer_text_color ?= #ffb400
+$dialer_prize_font_size ?= 12px
+$dialer_prize_name_padding ?= 8px
+$dialer_prize_inner_padding ?= 8px
+$dialer_prize_image_size ?= 36px
+
+.l-dialer
+	position relative
+	&__inner
+		width 100%
+		height 100%
+		position relative
+		// color $dialer_text_color
+		theme('color', '$dialer_text_color')
+		background-repeat no-repeat
+		background-size cover
+		box-sizing border-box
+		&-border
+			position absolute
+			left 0
+			top 0
+			bottom 0
+			right 0
+		&-wrap
+			position relative
+			z-index 1
+			// flex 1
+			width 100%
+			height 100%
+			border-radius 50%
+			overflow hidden
+			box-sizing border-box
+			
+			&::after
+				position absolute
+				left 0
+				top 0
+				bottom 0
+				right 0
+				content ''
+				border-radius 50%
+				box-shadow 0 0 20rpx currentColor inset 
+		&-item
+			overflow hidden
+			position absolute
+			top -50%
+			left 50%
+			width 100%
+			height 100%
+			transform-origin 0 100%
+		&-content
+			position absolute
+			theme('padding-top', '$dialer_prize_inner_padding')
+			// padding-top $dialer_prize_inner_padding
+			box-sizing border-box
+			width 100%
+			height 100%
+			left -50%
+			bottom -50%
+			display flex
+			flex-direction column
+			align-items center
+		&-name
+			theme('padding-top', '$dialer_prize_name_padding')
+			theme('padding-bottom', '$dialer_prize_name_padding')
+			theme('font-size', '$dialer_prize_font_size')
+			theme('color', '$dialer_text_color')
+			// padding-top $dialer_prize_name_padding
+			// padding-bottom $dialer_prize_name_padding
+			// font-size $dialer_prize_font_size
+			// color $dialer_text_color
+		&-img
+			// margin-top 24rpx
+			theme('width', '$dialer_prize_image_size')
+			theme('height', '$dialer_prize_image_size')
+			// width $dialer_prize_image_size
+			// height $dialer_prize_image_size
+	&__pointer
+		position absolute
+		left 50%
+		top 50%
+		transform translate(-50%, -50%)
+		z-index 1
+.heart
+	animation heart 1s infinite
+@keyframes heart
+	0%
+		transform scale(1)
+	25%
+		transform scale(1.03)
+	to
+		transform scale(1)

+ 260 - 0
uni_modules/lime-dialer/components/l-dialer/l-dialer.uvue

@@ -0,0 +1,260 @@
+<template>
+	<view class="l-dialer" :style="rootStyles">
+		<view class="l-dialer__inner" :style="innerStyle">
+			<view class="l-dialer__inner-border" v-if="$slots['border'] != null">
+				<slot name="border" />
+			</view>
+			<view class="l-dialer__inner-wrap" ref="drawbleRef" :style="wrapStyle">
+				<view class="l-dialer__inner-item" v-for="(item, index) in prizeList" :key="index"
+					:style="itemStyle(index)">
+					<view class="l-dialer__inner-content" :style="contentStyle(index)">
+						<slot v-if="$slots['prize'] != null" name="prize" :item="item" :even="index % 2"></slot>
+						<template v-else>
+							<text class="l-dialer__inner-name" :style="nameStyle">{{ item['name'] }}</text>
+							<image class="l-dialer__inner-img" :src="item['img']"></image>
+						</template>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="l-dialer__pointer" :style="pointerStyle">
+			<slot v-if="$slots['pointer'] != null" name="pointer" />
+			<image v-else :class="!isTurnIng ? 'heart' : ''" src="/uni_modules/lime-dialer/static/turnable_btn.png"
+				style="width: 100%" mode="widthFix" @tap="$emit('click')" />
+		</view>
+	</view>
+</template>
+<script lang="uts" setup>
+	const emits = defineEmits(['click', 'done'])
+	const slots = defineSlots<{
+		prize : {
+			item : UTSJSONObject,
+			even : number
+		}
+	}>()
+	const props = defineProps({
+		size: {
+			// #ifdef APP-ANDROID
+			type: Object,
+			//  #endif
+			// #ifndef APP-ANDROID
+			type: [String, Number],
+			//  #endif
+			default: 300
+		},
+		prizeList: {
+			type: Array as PropType<UTSJSONObject[]>,
+			default: () : UTSJSONObject[] => []
+		},
+		turns: {
+			type: Number,
+			default: 10
+		},
+		duration: {
+			type: Number,
+			default: 3
+		},
+		styleOpt: {
+			type: Object as PropType<UTSJSONObject>,
+			default: () : UTSJSONObject => ({
+				// 每一块扇形的背景色,默认值,可通过父组件来改变
+				prizeBgColors: ['#fff0a3', '#fffce6'],
+				// 每一块扇形的外边框颜色,默认值,可通过父组件来改变
+				borderColor: '#ffd752',
+			} as UTSJSONObject)
+		},
+		customStyle: {
+			type: String,
+		},
+		dialStyle: {
+			type: String,
+		},
+		pointerStyle: {
+			type: String,
+			default: `width: 30%`
+		}
+	})
+
+	const drawbleRef = ref<UniElement | null>(null)
+	const startRotateDegree = ref(0)
+	const rotateAngle = ref('rotate(0deg)')
+	const rotateTransition = ref('')
+	const isTurnIng = ref(false)
+
+
+	const getStyleOpt = computed(() : UTSJSONObject => {
+		const style = {
+			// 每一块扇形的背景色,默认值,可通过父组件来改变
+			prizeBgColors: ['#fff0a3', '#fffce6'],
+			// 每一块扇形的外边框颜色,默认值,可通过父组件来改变
+			borderColor: '#ffd752',
+		}
+		return UTSJSONObject.assign(style, props.styleOpt)
+	})
+	const rootStyles = computed(() : string => {
+		const size = /\d$/.test(`${props.size}`) ? `${props.size}px` : props.size;
+		return `width: ${size}; height: ${size}; ${props.customStyle}`
+	})
+
+	const innerStyle = computed(() : string => {
+		// const style = new Map<string, string>()
+		let style = ''
+		const padding = getStyleOpt.value['padding'] ?? 0
+
+		style += `padding: ${padding};`
+		style += `transform: ${rotateAngle.value};`
+		style += `${rotateTransition.value};`//`transition: ${rotateTransition.value};`
+		style += `${props.dialStyle};`
+
+		return style
+	})
+
+	const wrapStyle = computed(() : string => {
+		const borderColor = getStyleOpt.value['borderColor']
+		if (borderColor != null) {
+			return `border: 1rpx solid ${borderColor}`
+		}
+		return ''
+	})
+
+	const itemStyle = computed(() : ((index : number) => Map<string, any>) => {
+		return (index : number) : Map<string, any> => {
+			const length = props.prizeList.length;
+			const prizeBgColors : string[] = (getStyleOpt.value['prizeBgColors'] ?? [] as string[]) as string[]
+			const prizeBgColorsLength = prizeBgColors.length;
+			const borderColor = getStyleOpt.value['borderColor']
+			const style = new Map<string, any>();
+			// #ifndef APP
+			if (length == 2) {
+				// style['transform'] = index == 0 ? 0 : `rotate(270deg)` 
+				style.set('transform', index == 0 ? `rotate(0deg)` : `rotate(270deg)`)
+				style.set('top', 0)
+			} else {
+				style.set('transform', `rotate(${(360 / length) * index}deg) skewX(0deg) skewY(${360 / length - 90}deg)`);
+			}
+			if (prizeBgColorsLength > 0) {
+				style.set('backgroundColor', `${prizeBgColors[index % prizeBgColorsLength]}`)
+			}
+			if (borderColor != null) {
+				style.set('border', `1rpx solid ${borderColor}`)
+			}
+			// #endif
+			// #ifdef APP
+			if (length == 2) {
+				style.set('backgroundColor', `${prizeBgColors[index % prizeBgColorsLength]}`)
+				style.set('transform', index == 0 ? `rotate(0deg)` : `rotate(270deg)`);
+				style.set('top', 0)
+				if (borderColor != null) {
+					style.set('border', `1rpx solid ${borderColor}`)
+				}
+			} else {
+				style.set('transform', `rotate(${(360 / length) * index}deg)`);
+			}
+
+			// #endif
+			return style
+		}
+	})
+
+	const contentStyle = computed(() : ((index : number) => string) => {
+		return (index : number) : string => {
+			// #ifndef APP
+			if (props.prizeList.length != 2) {
+				return `transform: skewY(${90 - 360 / props.prizeList.length}deg) skewX(0deg) rotate(${180 / props.prizeList.length}deg)`
+			} else {
+				return index == 0
+					? `transform: rotate(90deg); bottom: 0`
+					: `transform: rotate(0deg); bottom: -50%; left: 0`
+			}
+
+			// #endif
+			// #ifdef APP
+			if (props.prizeList.length != 2) {
+				return `transform: rotate(${180 / props.prizeList.length}deg)`
+			} else {
+				return index == 0
+					? `transform: rotate(90deg); bottom: 0`
+					: `transform: rotate(0deg); bottom: -50%; left: 0`
+			}
+
+			// #endif
+		}
+
+	})
+
+	const nameStyle = computed(() : Map<string, any> => {
+		const fontSize = getStyleOpt.value['fontSize']
+		const color = getStyleOpt.value['color']
+		const style = new Map<string, any>()
+
+		if (fontSize != null) {
+			style.set('fontSize', fontSize)
+		}
+		if (color != null) {
+			style.set('color', color)
+		}
+		return style
+	})
+
+
+	const run = (index : number) => {
+		if (isTurnIng.value) return
+		const duration = props.duration;
+		const length = props.prizeList.length;
+
+		const _rotateAngle = startRotateDegree.value + props.turns * 360 + 360 - (180 / length + (360 / length) * index) - (startRotateDegree.value % 360);
+		startRotateDegree.value = _rotateAngle;
+		rotateAngle.value = `rotate(${_rotateAngle}deg)`;
+		rotateTransition.value = `transition-duration: ${duration}s`;
+		isTurnIng.value = true
+		setTimeout(() => {
+			emits('done', index);
+			isTurnIng.value = false
+		}, duration * 1000 + 500);
+	}
+	// #ifdef APP
+	onMounted(() => {
+		nextTick(() => {
+			if (drawbleRef.value == null) return;
+			const ctx = drawbleRef.value!.getDrawableContext()!;
+			const size = drawbleRef.value!.offsetWidth;
+			watch(props.prizeList, () => {
+				ctx.reset()
+				const length = props.prizeList.length;
+				if (length == 2) return
+				const prizeBgColors : string[] = (getStyleOpt.value['prizeBgColors'] ?? [] as string[]) as string[]
+				const prizeBgColorsLength = prizeBgColors.length;
+				const borderColor = getStyleOpt.value['borderColor'] as string | null
+
+				const centerX = size / 2;
+				const centerY = size / 2;
+				const radius = size / 2;
+
+				const angle = (2 * Math.PI) / length;
+
+				for (let i = 0; i < length; i++) {
+					ctx.beginPath();
+					ctx.moveTo(centerX, centerY);
+					ctx.arc(centerX, centerY, radius, i * angle, (i + 1) * angle);
+					ctx.lineTo(centerX, centerY);
+					ctx.closePath();
+					ctx.fillStyle = prizeBgColors[i % prizeBgColorsLength];
+					if (borderColor != null) {
+						ctx.lineWidth = 2
+						ctx.strokeStyle = borderColor;
+						ctx.stroke()
+					}
+					ctx.fill();
+				}
+				ctx.update()
+			}, { immediate: true })
+		})
+	})
+	// #endif
+	defineExpose({
+		run
+	})
+</script>
+<style lang="scss" scoped>
+	@import './index';
+</style>

+ 176 - 0
uni_modules/lime-dialer/components/l-dialer/l-dialer.vue

@@ -0,0 +1,176 @@
+<template>
+	<view class="l-dialer" :style="getStyle">
+		<view class="l-dialer__inner" :style="getDialStyle">
+			<view class="l-dialer__inner-border" v-if="$slots.border">
+				<slot name="border"/>
+			</view>
+			<view class="l-dialer__inner-wrap" :style="styleOpt.borderColor && (' border: 1rpx solid ' + styleOpt.borderColor)">
+				<view class="l-dialer__inner-item" v-for="(item, index) in prizeList" :key="index" :style="[getRotateAngle(index)]">
+					<view class="l-dialer__inner-content" :style="[getCorrectAngle(index)]">
+						<slot v-if="$slots.prize" name="prize" :item="item" :even="index % 2"></slot>
+						<block v-else>
+							<view class="l-dialer__inner-name" :style="[{fontSize: styleOpt.fontSize, color: styleOpt.color}]">{{ item.name }}</view>
+							<image class="l-dialer__inner-img" :src="item.img"></image>
+						</block>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="l-dialer__pointer" :style="pointerStyle" >
+			<slot v-if="$slots && $slots.pointer" name="pointer"/>
+			<image
+				v-else
+				:class="!isTurnIng ? 'heart': ''"
+				src="/uni_modules/lime-dialer/static/turnable_btn.png" 
+				style="width: 100%"
+				mode="widthFix" 
+				@tap="$emit('click')"
+			/>
+		</view>
+	</view>
+</template>
+<script>
+// import {addUnit} from '@/uni_modules/lime-shared/addUnit'	
+// import {sleep} from '@/uni_modules/lime-shared/sleep'	
+export default {
+	name: 'l-dialer',
+	emits: ['click', 'done'],
+	props: {
+		size: {
+			type: [String, Number],
+			default: 300
+		},
+		prizeList: {
+			type: Array
+		},
+		turns: {
+			type: Number,
+			default: 10
+		},
+		duration: {
+			type: Number,
+		    default: 3
+		},
+		styleOpt: {
+			type: Object,
+			default: () => ({
+				// 每一块扇形的背景色,默认值,可通过父组件来改变
+				// $primary-1 $primary-2 
+				prizeBgColors: ['#fff0a3', '#fffce6'],
+				// 每一块扇形的外边框颜色,默认值,可通过父组件来改变
+				// primary-4
+				borderColor: '#ffd752',
+			})
+		},
+		customStyle: {
+			type: String,
+		},
+		dialStyle: {
+			type: String,
+		},
+		pointerStyle: {
+			type: String,
+			default: `width: 30%`
+		}
+	},
+	data() {
+		return {
+			// 开始转动的角度
+			startRotateDegree: 0,
+			// 设置指针默认指向的位置,现在是默认指向2个扇形之间的边线上
+			rotateAngle: 0,
+			rotateTransition: '',
+			isTurnIng: false,
+		};
+	},
+	computed: {
+		getStyleOpt() {
+			const style = {
+				// 每一块扇形的背景色,默认值,可通过父组件来改变
+				prizeBgColors: ['#fff0a3', '#fffce6'],
+				// 每一块扇形的外边框颜色,默认值,可通过父组件来改变
+				borderColor: '#ffd752',
+			}
+			return Object.assign(style, this.styleOpt)
+		},
+		getRotateAngle() {
+			return index => {
+				const style = {
+					transform: `rotate(${(360 / this.prizeList.length) * index}deg) skewX(0deg) skewY(${360 / this.prizeList.length - 90}deg)`,
+					backgroundColor: `${this.getStyleOpt.prizeBgColors[index % this.getStyleOpt.prizeBgColors.length]}`,
+					border: `${this.getStyleOpt.borderColor && '1rpx solid ' + this.getStyleOpt.borderColor }`
+				}
+				if(this.prizeList.length == 2) {
+					style['transform'] = index == 0 ? 0 : `rotate(270deg)` //`rotate(${(360 / this.prizeList.length) * index}deg)`
+					style['top']  = 0
+				} 
+				return style
+				// return {
+				// 	transform: `rotate(${(360 / this.prizeList.length) * index}deg) skewX(0deg) skewY(${360 / this.prizeList.length - 90}deg)`,
+				// 	backgroundColor: `${this.styleOpt.prizeBgColors[index % this.styleOpt.prizeBgColors.length]}`,
+				// 	border: `${this.styleOpt.borderColor && '1rpx solid ' + this.styleOpt.borderColor }`
+				// }
+			};
+		},
+		getCorrectAngle() {
+			return index => {
+				const style = {
+					transform: `skewY(${90 - 360 / this.prizeList.length}deg) skewX(0deg) rotate(${180 / this.prizeList.length}deg)`
+				}
+				if(this.prizeList.length == 2){
+					if(index == 0) {
+						style['transform'] = `rotate(90deg)` 
+						style['bottom'] = 0
+					} else {
+						style['transform'] = `rotate(0deg)` 
+						style['left'] = 0
+						style['bottom'] = '-50%'
+					}
+					
+				}
+				return style
+			};
+		},
+		getStyle() {
+			let { size, customStyle } = this;
+			//addUnit(size)//
+			size = /\d$/.test(size) ? size + 'px' : size;
+			return `width: ${size}; height: ${size}; ${customStyle}`;
+		},
+		getDialStyle() {
+			return `
+				padding: ${this.getStyleOpt.padding};
+				transform: ${this.rotateAngle};
+				transition: ${this.rotateTransition};
+				${this.dialStyle}
+			`;
+		}
+		
+	},
+	methods: {
+		// 转动起来
+		run(index) {
+			if(this.isTurnIng) return
+			const duration = this.duration;
+			const length = this.prizeList.length
+			
+			const rotateAngle = this.startRotateDegree + this.turns * 360 + 360 - (180 / length + (360 / length) * index) - (this.startRotateDegree % 360);
+			this.startRotateDegree = rotateAngle;
+			this.rotateAngle = `rotate(${rotateAngle}deg)`;
+			this.rotateTransition = `transform ${duration}s cubic-bezier(0.250, 0.460, 0.455, 0.995)`;
+			this.isTurnIng = true
+			// sleep(duration * 1000 + 500).then(() => {
+			// 	this.$emit('done', index);
+			// 	this.isTurnIng = false
+			// })
+			setTimeout(() => {
+				this.$emit('done', index, this.prizeList[index]);
+				this.isTurnIng = false
+			}, duration * 1000 + 500);
+		}
+	}
+};
+</script>
+<style lang="scss" scoped>
+@import './index';
+</style>

ファイルの差分が大きいため隠しています
+ 2 - 0
uni_modules/lime-dialer/components/lime-dialer/lime-dialer.uvue


ファイルの差分が大きいため隠しています
+ 2 - 0
uni_modules/lime-dialer/components/lime-dialer/lime-dialer.vue


+ 82 - 0
uni_modules/lime-dialer/package.json

@@ -0,0 +1,82 @@
+{
+	"id": "lime-dialer",
+	"displayName": "幸运转盘",
+	"version": "0.2.5",
+	"description": "幸运转盘 抽奖 抽奖转盘,兼容uniapp/uniappX(h5,ios,安卓)",
+	"keywords": [
+        "转盘",
+        "抽奖",
+        "抽奖转盘"
+    ],
+	"repository": "",
+	"engines": {
+		"HBuilderX": "^3.4.12"
+	},
+	"dcloudext": {
+		"sale": {
+			"regular": {
+				"price": "0.00"
+			},
+			"sourcecode": {
+				"price": "0.00"
+			}
+		},
+		"contact": {
+			"qq": ""
+		},
+		"declaration": {
+			"ads": "无",
+			"data": "无",
+			"permissions": "无"
+		},
+		"npmurl": "",
+		"type": "component-vue"
+	},
+	"uni_modules": {
+		"dependencies": [],
+		"encrypt": [],
+		"platforms": {
+			"cloud": {
+				"tcb": "y",
+                "aliyun": "y",
+                "alipay": "n"
+			},
+			"client": {
+				"Vue": {
+					"vue2": "y",
+					"vue3": "y"
+				},
+				"App": {
+                    "app-vue": "y",
+                    "app-nvue": "n",
+                    "app-uvue": "y",
+                    "app-harmony": "u"
+                },
+				"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"
+				}
+			}
+		}
+	}
+}

+ 176 - 0
uni_modules/lime-dialer/readme.md

@@ -0,0 +1,176 @@
+# Dialer 转盘抽奖 
+> 营销活动类组件  
+> [查看更多](https://limeui.qcoon.cn/#/dialer) <br>
+
+
+
+### 平台兼容
+|	H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ小程序 | App |
+|-----------|-----------|-----------|-----------|-----------|-----------|-----------|
+| √	| √ |	√	| √ |	√	| √ |	√ |
+
+
+### 代码演示
+#### 基础用法
+
+```html
+<l-dialer :prizeList="prizeList" @click="onClick" @done="onDone" ref="dialer" />
+```
+```js
+export default {
+    data() {
+        return {
+			// 奖品列表,
+            prizeList: [
+            	{
+            		id: 'coupon88',
+            		name: '8.8折',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/175718/35/12595/5477/60b660c6Eb850717b/a1cfe750dcdb5b78.png',
+            	},
+            	{
+            		id: 'coupon900',
+            		Color: 'rgb(251, 219, 216)',
+            		name: '900',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/190845/9/6092/4489/60b65fe8Ebb8f8284/955da889f6d1c13e.png',
+            	},
+            	{
+            		id: 'coupon1',
+            		name: '1元',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/189927/14/6092/4174/60b66173E23c472ea/44af15a151defca1.png',
+            	},
+            	{
+            		id: 'apple',
+            		Color: 'rgba(246, 142, 46, 0.5)',
+            		name: '苹果手机',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/177670/26/4591/2514/60a25874Ee0e5332a/99c7bdfede732ae4.png'
+            	},
+            	{
+            		id: 'coupon210',
+            		name: '210元',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/124578/12/20170/4429/60b635d8E7089ebb0/7a47d76a2a260cc0.png'
+            	},
+            	{
+            		id: 'jd100',
+            		name: '100京豆',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/162790/37/15087/28046/6062a49aE8f2c10f2/5591ff0ff38a45e2.png',
+            	},
+            	{
+            		id: 'coupon400',
+            		name: '400元',
+            		img: 'https://img11.360buyimg.com/pop/jfs/t1/177090/2/7001/4535/60b6607aEe9c1db2a/76c67675f547db3f.png'
+            	},
+            	{
+            		id: 'thanks',
+            		name: '谢谢参与',
+            		img: 'https://storage.jd.com/cdn-upload/dialTemplateHeart.png',
+            	}
+            ]
+        };
+    },
+    methods: {
+		onDone(index) {
+			const prize = this.prizeList[index]
+			uni.showModal({
+				title: prize.id == 'thanks' ? '很遗憾': '恭喜您',
+				content: (prize.id !== 'thanks' ? `获得`:'') + prize.name
+			})
+		},
+		onClick() {
+			// 奖品的索引
+			this.$refs.dialer.run(5)
+		}
+	}
+}
+```
+
+#### 表框
+- 1、通过设置`dial-style`设置背景的方式设置,必须是网络图片
+- 2、通过插槽`border`设置
+
+```html
+// 方式1
+<l-dialer dial-style="color: rgba(60,48,158,0.7); padding: 32rpx;background-image: url(http://a.cn/static/dialer/lottery-bg.png)"/>
+
+// 方式2
+<l-dialer>
+	<image slot="border" src="static/dialer/lottery-bg.png"/>
+</l-dialer>
+```
+
+#### 指针
+- 1、可通过`pointer-style`设置背景的方式设置,但必须为网络图片
+- 2、可通过插槽`pointer`设置
+- 3、因为插件是能过获取内部方法实现抽奖,只要获取方法,任何元素都是指针按钮
+
+```html
+// 方式1
+<l-dialer pointer-style="background-image: url(http://a.cn/static/dialer/bg.png)"/>
+// 方式2
+<l-dialer>
+	<image slot="pointer" src="static/dialer/lottery-bg.png" />
+</l-dialer>
+```
+
+#### 奖品插槽
+- 默认会按奖品列表渲染,但想更个性可通过插槽`prize`设置
+- 微信小程序最好使用HX3.7.12+并且在`manifest.json`设置`slotMultipleInstance`
+```json
+"mp-weixin" : {
+    "slotMultipleInstance" : true
+}
+```
+
+```html
+<l-dialer>
+	<template #prize="{item}">
+		<image style="width: 72rpx; height: 72rpx;" :src="item.img" />
+	</template>
+</l-dialer>
+```
+
+
+### 查看示例
+- 导入后直接使用这个标签查看演示效果
+
+```html
+<!-- // 代码位于 uni_modules/lime-dialer/compoents/lime-dialer -->
+<lime-dialer />
+```
+
+
+### 插件标签
+- 默认 l-dialer 为 component
+- 默认 lime-dialer 为 demo
+
+
+
+### API
+#### Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| size | 转盘直径,默认单位为 `rpx` | Number | `300` | - |
+| prizeList | 奖品列表  | Array | [] | - |
+| turns | 旋转圈数 | Number | `10` | - |
+| duration | 旋转过程时间,单位为 `s` | Number | `3` | - |
+| styleOpt | 转盘中的样式,包括每个扇区的背景颜色(在每条数据中页可单独设置prizeColor),扇区的边框颜色 | Object | {prizeBgColors: [],borderColor: ''} | - |
+| customStyle | 外容器的自定义样式 | String |  | - |
+| dialStyle | 转盘自定义样式 | String |  | - |
+| pointerStyle | 指针自定义样式 | String | `width:30%` | - |
+
+#### Event
+
+| 名称 | 说明                                                       |
+| ---- | ---------------------------------------------------------- |
+| run(index)   | 旋转到指定索引,该事件是通过`ref`获取插件实例的内部方法 |
+| done   | 旋转结束,该事件是通过标签接收的方法 |
+| click   | 点击指针,该事件是通过标签接收的方法 |
+
+
+#### Slots
+
+| 名称 | 说明                                                       |
+| ---- | ---------------------------------------------------------- |
+| border   | 边框插槽 |
+| prize   | 奖品插槽 |
+| pointer   | 指针插槽 |

BIN
uni_modules/lime-dialer/static/turnable_btn.png


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません