Ver código fonte

feat: 分享有礼功能完成

huangziyang 1 semana atrás
pai
commit
221744ed36

+ 16 - 3
components/Container/Container.vue

@@ -61,6 +61,18 @@ const props = defineProps({
     type: Number,
     default: 0,
   },
+  headerColor: {
+    type: String,
+    default: "transparent",
+  },
+  bottomBgColor: {
+    type: String,
+    default: "transparent",
+  },
+  bottomBorder: {
+    type: Boolean,
+    default: false,
+  }
 });
 
 const emit = defineEmits(["onSafeAreaChange", "onScroll"]);
@@ -150,7 +162,7 @@ defineExpose({
       class="title"
       :style="{
         paddingTop: `${safeArea.top}px`,
-        background: 'transparent',
+        background: headerColor,
       }"
       v-if="showTitle"
     >
@@ -160,7 +172,7 @@ defineExpose({
         :border="border"
         :title="title"
         fixed
-        :backgroundColor="bgColor"
+        :backgroundColor="headerColor"
       />
     </view>
     <scroll-view
@@ -202,7 +214,8 @@ defineExpose({
       class="bottom-button"
       :style="{
         width: `${safeArea.width}px`,
-        background: 'transparent',
+        background: bottomBgColor,
+        borderTop: bottomBorder? '1px solid #eee' : 'none',
       }"
     >
       <slot name="footer" :footer-height="footerHeight" />

+ 9 - 1
components/CustomerService/CustomerService.vue

@@ -1,7 +1,7 @@
 <script setup></script>
 <template>
   <button open-type="contact" session-from class="contact">
-    <slot>联系客服</slot>
+    <image class="contact-img"></image>
   </button>
 </template>
 
@@ -17,5 +17,13 @@
   &::after {
     border: 0; // 或者 border: none;
   }
+
+  .contact-img {
+    background: url("https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/jWjoCzJjk3DqjcGpHoJXYAUU5MXykAyWjF2UGgGO.png")
+      no-repeat center center;
+    background-size: cover;
+    width: 144rpx;
+    height: 150rpx;
+  }
 }
 </style>

+ 9 - 2
components/Empty/Empty.vue

@@ -1,8 +1,15 @@
-<script setup></script>
+<script setup name="Empty">
+defineProps({
+  text: {
+    type: String,
+    default: "敬请期待",
+  },
+});
+</script>
 
 <template>
   <view class="empty">
-    <view>敬请期待</view>
+    <view>{{ text }}</view>
   </view>
 </template>
 

+ 7 - 0
pages.json

@@ -35,6 +35,13 @@
         "navigationBarTitleText": "分享有礼"
       }
     },
+    {
+      "path": "pages/user/invited",
+      "style": {
+        "navigationStyle": "custom",
+        "navigationBarTitleText": "分享有礼"
+      }
+    },
     {
       "path": "pages/regulations/index",
       "style": {

+ 1 - 1
pages/learn/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <Container title="首页"></Container>
+  <Container title="学习本" empty></Container>
 </template>
 
 <script setup>

+ 61 - 29
pages/real/exam.vue

@@ -26,8 +26,8 @@ const submitter = ref({
   isEndQuestion: false,
   text: "提交查看",
   totalTime: {
-    formatTime: "00:03",
-    totalTime: 2892,
+    formatTime: "00:00",
+    totalTime: 0,
   },
 });
 
@@ -131,39 +131,50 @@ onMounted(() => {
     border
     v-if="!showReport"
   />
-  <Container v-else title="练习报告">
-    <uni-card>
+  <Container
+    bgColor="url('https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/7i6ROn34tZGe8QR4gaWDP1ZzyPYjqlvPgLLFRmIe.png')"
+    bottomBorder
+    v-else
+    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>{{ correct.rate.toFixed(2) }}%</view>
+          <view class="red">{{ correct.rate.toFixed(2) }}%</view>
           <view>正确率</view>
         </view>
-      </view>
-    </uni-card>
-    <view class="reslut">
-      <view class="header">
-        <view>答题卡</view>
-        <view class="right-error">
-          <view class="right">答对({{ correct.right }})</view>
-          <view class="error">答错({{ correct.error }})</view>
-          <view class="not">未作({{ correct.not }})</view>
+        <view class="card-time">
+          <view class="red">{{ correct.right }}</view>
+          <view>获得分数</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 class="template">
+        <view class="header">
+          <view>答题卡</view>
+          <view class="right-error">
+            <view class="right">答对({{ correct.right }})</view>
+            <view class="error">答错({{ correct.error }})</view>
+            <view class="not">未作({{ correct.not }})</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>
@@ -173,7 +184,7 @@ onMounted(() => {
           class="button"
           @click="
             router.push({
-              url: '/pages/real/shareExam',
+              url: '/pages/real/sharePractice',
               params: {
                 correct,
               },
@@ -188,12 +199,30 @@ onMounted(() => {
 
 <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: space-between;
   align-items: center;
   padding: 40rpx 50rpx;
+  background-color: #f8f9fb;
+  border-radius: 24rpx;
   &-time {
     font-family: PingFang SC, PingFang SC;
     font-weight: 500;
@@ -207,14 +236,17 @@ onMounted(() => {
 }
 
 .reslut {
-  margin: 0 30rpx;
-  padding: 20rpx 50rpx;
+  margin: 0 32rpx;
+  padding: 24rpx;
   font-family: PingFang SC, PingFang SC;
   font-weight: 500;
   font-size: 28rpx;
   color: #333333;
   background: #fff;
-  border-radius: 10rpx;
+  border-radius: 24rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
   .header {
     display: flex;
     justify-content: space-between;

+ 2 - 1
pages/regulations/index.vue

@@ -36,6 +36,7 @@
             height: `${maskStyle.height}px`,
             width: `${maskStyle.width}px`,
           }"
+          v-if="maskStyle.height > 0"
         >
           <uni-icons type="locked" color="#fff" size="35"></uni-icons>
           <view class="modal-wrapper" @click="onClickMask">邀请好友可解锁</view>
@@ -141,7 +142,7 @@ const resolveHeight = () =>
       },
     });
     // 动画300需要等待
-  }, 500);
+  }, 800);
 
 const onSafeAreaChange = (s) => {
   safeArea.value = s;

+ 78 - 36
pages/regulations/practice.vue

@@ -1,7 +1,7 @@
-<script setup name="practice">
+<script setup name="Exam">
 import TopicPractice from "../../components/Topic/TopicPractice.vue";
 import { ref, onMounted } from "vue";
-import { getRoute } from "../../utils/router";
+import { getRoute, router } from "../../utils/router";
 import { request } from "../../utils/request";
 import { useTimeStore } from "@/store/time";
 import Container from "../../components/Container/Container.vue";
@@ -26,8 +26,8 @@ const submitter = ref({
   isEndQuestion: false,
   text: "提交查看",
   totalTime: {
-    formatTime: "00:03",
-    totalTime: 2892,
+    formatTime: "00:00",
+    totalTime: 0,
   },
 });
 
@@ -43,12 +43,13 @@ const getList = async (params) => {
     "api/question_bank/question_reception/topic/get_chapter_topic",
     {
       ...params,
-      id: getRoute().params.id,
+      // id: getRoute().params.id,
+      id: 66,
     }
   );
   total.value = res.data.total;
   data.value.push(
-    ...res.data.data.map((item) => {
+    ...res.data.data.map((item, ind) => {
       let questions = [];
       const ans = item.correct_answer.split(",");
       const ansList = [];
@@ -79,6 +80,7 @@ const getList = async (params) => {
         showResult: false, // 是否展示答案
         isRight: false, // 是否正确
         isImage: item.title.includes("<img"),
+        ind,
       };
     })
   );
@@ -100,7 +102,7 @@ const lookReport = (d, s) => {
     totalTime,
   };
   const r = data.value.filter((item) => item.isRight).length;
-  const n = data.value.filter((item) => !item.showResult).length;
+  const n = data.value.filter((item) => !item.selectAns.length).length;
   correct.value = {
     rate: (r / total.value) * 100,
     right: r,
@@ -125,48 +127,66 @@ onMounted(() => {
     :topics="data"
     @nextPage="nextPage"
     @lookReport="lookReport"
+    :empty="!data.length"
     border
     v-if="!showReport"
   />
-  <Container v-else title="练习报告">
-    <uni-card>
+  <Container
+    bgColor="url('https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/7i6ROn34tZGe8QR4gaWDP1ZzyPYjqlvPgLLFRmIe.png')"
+    bottomBorder
+    v-else
+    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>{{ correct.rate.toFixed(2) }}%</view>
+          <view class="red">{{ correct.rate.toFixed(2) }}%</view>
           <view>正确率</view>
         </view>
       </view>
-    </uni-card>
-    <view class="reslut">
-      <view class="header">
-        <view>答题卡</view>
-        <view class="right-error">
-          <view class="right">答对({{ correct.right }})</view>
-          <view class="error">答错({{ correct.error }})</view>
-          <view class="not">未作({{ correct.not }})</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 class="group">
-        <view
-          class="item"
-          :class="{
-            right: it.isRight && it.showResult,
-            error: !it.isRight && it.showResult,
-          }"
-          v-for="(it, index) in data"
-          :key="it.id"
-          >{{ index + 1 }}</view
-        >
       </view>
     </view>
     <template #footer>
       <view class="footer">
-        <view class="button plain">答题解析</view>
-        <view class="button">炫耀一下</view>
+        <view class="button plain" @click="showReport = false">答题解析</view>
+        <view
+          class="button"
+          @click="
+            router.push({
+              url: '/pages/real/shareExam',
+              params: {
+                correct,
+              },
+            })
+          "
+          >炫耀一下</view
+        >
       </view>
     </template>
   </Container>
@@ -174,12 +194,31 @@ onMounted(() => {
 
 <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: space-between;
+  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;
@@ -193,14 +232,17 @@ onMounted(() => {
 }
 
 .reslut {
-  margin: 0 30rpx;
-  padding: 20rpx 50rpx;
+  margin: 0 32rpx;
+  padding: 24rpx;
   font-family: PingFang SC, PingFang SC;
   font-weight: 500;
   font-size: 28rpx;
   color: #333333;
   background: #fff;
-  border-radius: 10rpx;
+  border-radius: 24rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
   .header {
     display: flex;
     justify-content: space-between;

+ 2 - 2
pages/user/index.vue

@@ -49,8 +49,8 @@
             ><uni-icons type="right" size="20"></uni-icons
           ></view>
         </navigator>
-        <navigator class="navigator_item" url="/">
-          <view class="navigator_title">邀请有礼</view>
+        <navigator class="navigator_item" url="/pages/user/invited">
+          <view class="navigator_title">邀请记录</view>
           <view class="navigator_title_ico"
             ><uni-icons type="right" size="20"></uni-icons
           ></view>

+ 163 - 0
pages/user/invited.vue

@@ -0,0 +1,163 @@
+<script setup>
+import Container from "../../components/Container/Container.vue";
+import { ref, onMounted } from "vue";
+import { request } from "../../utils/request";
+import utils from "../../utils/common";
+import Empty from "../../components/Empty/Empty.vue";
+const list = ref([]);
+
+const titleList = ref([
+  {
+    label: "已邀请",
+    value: 0,
+    key: "number_of_invitations",
+  },
+  {
+    label: "可免费解锁科目",
+    value: 0,
+    key: "unlockable_subjects",
+  },
+  {
+    label: "已免费解锁科目",
+    value: 0,
+    key: "unlocked_subject",
+  },
+]);
+
+onMounted(() => {
+  request("api/question_bank/question_reception/invite_user_form/list").then(
+    (res) => {
+      if (!res.data) return;
+      list.value = res.data.data;
+    }
+  );
+
+  request("api/question_bank/question_reception/user_share_forms/detail").then(
+    (res) => {
+      if (!res.data) return;
+      titleList.value.forEach((item) => {
+        item.value = res.data[item.key] || 0;
+      });
+    }
+  );
+});
+</script>
+<template>
+  <Container title="邀请记录" headerColor="#fff" bgColor="#f8f8f8">
+    <view class="contariner">
+      <view class="title">
+        <view v-for="item in titleList" class="item" :key="item.label">
+          <view class="label">{{ item.label }}</view>
+          <view class="value">{{ item.value }}</view>
+        </view>
+      </view>
+      <view class="list" v-if="list.length">
+        <scroll-view scroll-y class="card" v-for="item in list" :key="item.id">
+          <view class="left">
+            <image
+              class="avatar"
+              :src="item.invited_user_info?.userpic"
+              mode="scaleToFill"
+            />
+            <div class="info">
+              <div class="name">{{ item.invited_user_info?.username }}</div>
+              <div class="num">答题数: {{ item.number_of_answers }}题</div>
+            </div>
+          </view>
+          <div class="right">
+            {{ utils.timestampToString(item.insert_time) }}
+          </div>
+        </scroll-view>
+      </view>
+      <Empty text="暂无数据" v-else />
+    </view>
+  </Container>
+</template>
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.contariner {
+  display: flex;
+  flex-direction: column;
+  gap: 24rpx;
+
+  .title {
+    background: url("https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/mSSwWMeK16KulBkYTchvlCXuJ2DaFceeuhS6MLz6.png")
+      no-repeat center center;
+    background-size: cover;
+    height: 181rpx;
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+
+    .item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      gap: 16rpx;
+      font-family: PingFang SC, PingFang SC;
+      font-weight: 400;
+      font-size: 24rpx;
+      color: #666666;
+
+      .value {
+        font-size: 48rpx;
+        color: #000000;
+      }
+    }
+  }
+
+  .list {
+    display: flex;
+    flex-direction: column;
+    gap: 16rpx;
+    height: 100%;
+    .card {
+      background-color: #fff;
+      padding: 24rpx 16rpx;
+      display: flex;
+      justify-content: space-between;
+      border-radius: 16rpx;
+      flex: 1;
+
+      .left {
+        display: flex;
+        gap: 24rpx;
+        flex: 1;
+        font-family: PingFang SC, PingFang SC;
+        font-weight: 500;
+        font-size: 24rpx;
+        color: #000000;
+
+        .avatar {
+          width: 56rpx;
+          height: 56rpx;
+          border-radius: 50%;
+        }
+
+        .info {
+          display: flex;
+          flex-direction: column;
+          gap: 24rpx;
+          justify-content: center;
+          margin-bottom: 7rpx;
+
+          .name {
+            font-weight: bold;
+          }
+          .num {
+            font-size: 20rpx;
+          }
+        }
+      }
+
+      .right {
+        font-family: PingFang SC, PingFang SC;
+        font-weight: 500;
+        font-size: 24rpx;
+        color: #999999;
+      }
+    }
+  }
+}
+</style>

+ 126 - 7
pages/user/share.vue

@@ -1,8 +1,35 @@
 <script setup>
 import Container from "../../components/Container/Container.vue";
+import lPainter from "@/uni_modules/lime-painter/components/l-painter/l-painter.vue";
+import lPainterView from "@/uni_modules/lime-painter/components/l-painter-view/l-painter-view.vue";
+import lPainterImage from "@/uni_modules/lime-painter/components/l-painter-image/l-painter-image.vue";
+
 import { ref, onMounted } from "vue";
 import { request } from "../../utils/request";
 const qrCode = ref("");
+const painter = ref(null);
+const showShare = ref(false);
+
+const onSaveImage = () => {
+  painter.value?.canvasToTempFilePathSync({
+    fileType: "jpg",
+    pathType: "url",
+    quality: 1,
+    success: (res) => {
+      // 非H5 保存到相册
+      uni.saveImageToPhotosAlbum({
+        filePath: res.tempFilePath,
+        success: function () {
+          uni.showToast({
+            title: "保存成功",
+            icon: "success",
+          });
+          showShare.value = false;
+        },
+      });
+    },
+  });
+};
 
 onMounted(() => {
   // 生成二维码
@@ -10,7 +37,7 @@ onMounted(() => {
     page_url: "/pages/index/index",
   }).then((res) => {
     console.log(res);
-    qrCode.value = res.data
+    qrCode.value = res.data;
   });
 });
 </script>
@@ -20,15 +47,53 @@ onMounted(() => {
       padding: 0,
     }"
     title="分享有礼"
+    v-if="!showShare"
   >
     <view class="bg">
-      <image
-        class="qr-code"
-        :src="qrCode"
-        mode="scaleToFill"
-      />
+      <image class="qr-code" :src="qrCode" mode="scaleToFill" />
     </view>
+    <template #footer>
+      <button @click="showShare = true">立即分享</button>
+    </template>
   </Container>
+  <view class="share-template" v-else>
+    <view class="share-img">
+      <l-painter ref="painter">
+        <l-painter-view
+          css="width: 686rpx; height: 888rpx; position: relative;"
+        >
+          <l-painter-image
+            src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/12nSpSIh19rrhQBm0q3jvmT6dgXeN77q4el6cdXX.png"
+            css="width: 686rpx; height: 888rpx; position: absolute;top: 0;"
+          />
+          <l-painter-image
+            :src="qrCode"
+            css="width: 448rpx; height: 448rpx; position: absolute;top: 268rpx;left: 119rpx;"
+          />
+        </l-painter-view>
+      </l-painter>
+    </view>
+    <view class="footer">
+      <div class="title">
+        <view>分享更多好友</view>
+        <uni-icons
+          type="closeempty"
+          class="closeempty"
+          @click="showShare = false"
+        />
+      </div>
+      <view class="items">
+        <view class="save-img" @click="onSaveImage">
+          <image
+            src="https://openwork-oss.oss-cn-shenzhen.aliyuncs.com/uploads/question/2025/05/zGBXaE27pgRjEA6NoMLC75CSdMs3BKjh1vnpGKAc.png"
+            mode="scaleToFill"
+            class="save"
+          />
+          <text>保存图片</text>
+        </view>
+      </view>
+    </view>
+  </view>
 </template>
 <style scoped lang="scss">
 @import "@/uni.scss";
@@ -43,11 +108,65 @@ onMounted(() => {
     width: 448rpx;
     height: 448rpx;
     border-radius: 24rpx;
-    background-color: red;
     border: 16rpx solid $primary;
     left: 50%;
     top: 37%;
     transform: translateX(-50%);
   }
 }
+
+.share-template {
+  height: 100vh;
+  width: 100%;
+  background: #999999;
+  position: relative;
+  .share-img {
+    position: absolute;
+    top: 368rpx;
+    left: 32rpx;
+  }
+
+  .footer {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: calc(68rpx + 80rpx + 140rpx);
+    background-color: #fff;
+    border-radius: 40rpx 40rpx 0rpx 0rpx;
+
+    .title {
+      display: flex;
+      height: 80rpx;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+
+      .closeempty {
+        position: absolute;
+        right: 32rpx;
+      }
+    }
+
+    .items {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 140rpx;
+      .save-img {
+        display: flex;
+        align-items: center;
+        gap: 12rpx;
+        font-weight: 400;
+        font-size: 28rpx;
+        color: #000000;
+
+        .save {
+          width: 60rpx;
+          height: 60rpx;
+        }
+      }
+    }
+  }
+}
 </style>