JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_除了语音转文字都开发完成

This commit is contained in:
田岩
2026-01-08 09:46:01 +08:00
parent ce1626810e
commit 3d2f7b3a30
52 changed files with 1397 additions and 217 deletions
+3 -5
View File
@@ -4,13 +4,11 @@ ENV = 'development'
VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
#VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
@@ -46,7 +44,7 @@ VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.16.130:9601'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.150:9601'
# VITE_APP_BASE_H5_API_Url_TRASTUDY = 'http://25.64.16.130:9602'
# VITE_APP_BASE_H5_API_Url_TRAASK = 'http://25.64.16.144:9605'
@@ -6,9 +6,11 @@ rm -rf dist
rm -rf node_modules
echo "-----删除成功-----"
ls -a
export TLS_MAX_MEM=8388608
npm install @dcloudio/uts-linux-x64-gnu --registry=http://25.12.10.69:8081/repository/aliyun-npm/ --force
#下载依赖
npm install --unsafe-perm=true --allow-root --registry=http://25.12.10.69:8081/repository/aliyun-npm/
# npm install --unsafe-perm=true --allow-root --registry=http://25.12.10.69:8081/repository/aliyun-npm/
npm install --registry=http://25.12.10.69:8081/repository/aliyun-npm/
#打包成静态资源
npm run build:app-plus:${ENV_NAME}
echo "打包成功"
+23
View File
@@ -127,3 +127,26 @@ export const queryRankWall = (data) => {
data
});
};
/**
* 查询当前时间是否存在假日积分奖励
*/
export const queryHolidayReward = (data) => {
return request({
url: base_url + '/traPointsInfo/queryHolidayReward',
method: 'post',
data
});
};
/**
* 随机假日积分获得
*/
export const obtainRewardRandom = (data) => {
return request({
url: base_url + '/traPointsInfo/obtainRewardRandom',
method: 'post',
data
});
};
+74 -6
View File
@@ -3,16 +3,23 @@
<view class="line">
<text class="tr">维度评分</text>
<text class="td">{{ dimension_all_score }}</text>
<view class="td_success_error_img">
<image :src="select_status_icon" class="img"></image>
</view>
</view>
<view class="line" v-for="item in evalSettingVos">
<view class="dimension-line" v-for="item in evalSettingVos">
<text class="tr">{{ item.gradeDimens }}</text>
<text class="td">{{ item.anlyGrade }}</text>
<view class="your_answer" v-if="item.dimensCode === '01'">
<rich-text :nodes="highlightedText" class="content" />
<view class="mt-10" v-if="missingKeywords.length">
<text class="">未命中</text>
<text v-for="(keyword, index) in missingKeywords" :key="index" class="miss-keyword-item">
<!-- 关键词红色 -->
<text class="keyword-text">{{ keyword }}</text>
<text class="separator" v-if="index < missingKeywords.length - 1"></text>
</text>
</view>
</view>
</view>
</view>
@@ -57,7 +64,7 @@
const highlightedText = computed(() => {
// 如果没有关键词,直接返回原文本
if (!keywords.value.length) return '你的答案' + props.anserResult;
if (!keywords.value.length) return '命中' + props.anserResult;
// 构建正则表达式,匹配所有关键词(不区分大小写)
// 对关键词进行转义,避免特殊字符影响正则
const escapedKeywords = keywords.value.map((keyword) => keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
@@ -66,7 +73,7 @@
const regex = new RegExp(`(${escapedKeywords.join('|')})`, 'gi');
// 替换匹配到的关键词,用span包裹并添加高亮样式
return '你的答案' + props.anserResult.replace(regex, (match) => `<span class="highlight">${match}</span>`);
return '命中' + props.anserResult.replace(regex, (match) => `<span class="highlight">${match}</span>`);
});
const dimension_all_score = computed(() => {
return (
@@ -77,13 +84,51 @@
.reduce((total, current) => total + current, 0)
);
});
// 找出答案中不存在的关键词
const missingKeywords = computed(() => {
const relKeyword = props.value.relKeyword;
// 容错:答案为空时返回所有关键词
if (!props.anserResult) {
return relKeyword.split('、');
}
// 步骤1:按顿号拆分关键词组
const keywordGroups = relKeyword.split('、');
const missing = [];
// 步骤2:遍历每个关键词组,检查是否存在任意一种说法
keywordGroups.forEach((group) => {
// 拆分同关键词的不同说法(/ 分隔)
const keywordVariants = group.split('/');
let isFound = false;
// 检查答案中是否包含任意一种说法
for (const variant of keywordVariants) {
// 可选:增加 trim() 忽略首尾空格,提升匹配容错
if (props.anserResult.trim().includes(variant.trim())) {
isFound = true;
break;
}
}
// 未找到则加入缺失列表
if (!isFound) {
missing.push(group);
}
});
return missing;
});
const click_option = () => {
emit('click_option', props.analysisVo);
};
</script>
<style scoped lang="scss">
.mt-10 {
margin-top: 10rpx;
}
.success {
background-color: #e7f8ed;
.td {
@@ -102,21 +147,44 @@
color: #f5212d;
}
}
.your_answer {
:deep(.highlight) {
text-decoration: underline;
}
}
.keyword-text {
color: #d70c18;
text-decoration: underline;
}
.separator {
}
.analysis_div {
margin-top: 30rpx;
border-radius: 16rpx;
padding: 22rpx 15rpx 15rpx 34rpx;
.line {
margin: 12rpx 0;
position: relative;
}
.dimension-line {
margin: 12rpx 0;
position: relative;
padding-left: 1.5em;
}
.dimension-line::before {
content: '';
position: absolute;
left: 0.5em; /* 对齐padding-left的起始位置 */
top: 1em; /* 垂直居中 */
transform: translateY(-50%);
width: 8px; /* 圆点大小(默认是4px左右,调大到8px) */
height: 8px; /* 和宽度一致,保证圆形 */
border-radius: 50%; /* 圆形 */
background-color: #333; /* 圆点颜色,可自定义 */
}
.tr {
font-weight: 600;
font-size: 30rpx;
@@ -0,0 +1,66 @@
<template>
<view class="reference_answer">
<view class="reference_answer_title">参考答案</view>
<rich-text :nodes="highlightedText" class="content" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
const emit = defineEmits(['click_option']);
const props = defineProps({
value: {
type: Object,
default: () => {},
required: false
},
anserResult: {
type: String,
required: true
}
});
const keywords = computed(() => {
if (!props.value.relKeyword) return [];
return props.value.relKeyword.split(/[,、/]/).map((key) => key.trim());
});
const highlightedText = computed(() => {
// 如果没有关键词,直接返回原文本
if (!keywords.value.length) return props.anserResult;
// 构建正则表达式,匹配所有关键词(不区分大小写)
// 对关键词进行转义,避免特殊字符影响正则
const escapedKeywords = keywords.value.map((keyword) => keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
// 创建正则表达式,g表示全局匹配,i表示不区分大小写
const regex = new RegExp(`(${escapedKeywords.join('|')})`, 'gi');
// 替换匹配到的关键词,用span包裹并添加高亮样式
return props.anserResult.replace(regex, (match) => `<span class="highlight">${match}</span>`);
});
</script>
<style scoped lang="scss">
.reference_answer {
.reference_answer_title {
font-weight: 700;
font-size: 30rpx;
color: #333333;
text-align: left;
line-height: 30rpx;
margin: 30rpx 0;
}
font-weight: 400;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
:deep(.highlight) {
color: #15b859;
text-decoration: underline;
}
}
</style>
+20 -7
View File
@@ -2,7 +2,9 @@
<view class="item">
<view class="top">
<view class="type">{{ subject_type_text }}</view>
<view class="score" v-if="['settlement', 'examination', 'pk_in'].includes(props.mode)">{{ item.tgtGrade }}</view>
<view class="score" v-if="['settlement', 'examination', 'pk_in'].includes(props.mode)">
{{ item.tgtGrade }}
</view>
<view class="fl1"></view>
<view class="right">
<!-- 反馈中心 -->
@@ -66,7 +68,12 @@
</view>
<view
class="option_div_button"
v-if="qnsTyp === 'MU' && ['practice', 'study', 'pk_in'].includes(props.mode) && !props.isPreview && clearResult"
v-if="
qnsTyp === 'MU' &&
['practice', 'study', 'pk_in'].includes(props.mode) &&
!props.isPreview &&
clearResult
"
>
<uv-button
type="primary"
@@ -106,11 +113,14 @@
></esAnalysis>
<!-- 问答题专属参考答案 -->
<view class="reference_answer" v-if="qnsTyp === 'ES'">
<view class="reference_answer_title">参考答案:</view>
{{ subject_data.itemVos[0]?.itemAnswer ?? '' }}
<referenceAnswer
v-if="qnsTyp === 'ES'"
:value="subject_data.itemVos[0]"
:anserResult="subject_data.anserResult"
></referenceAnswer>
<view class="analysis_note" v-if="props.mode !== 'pk_in'">
试题解析:{{ subject_data?.analyContent }}
</view>
<view class="analysis_note" v-if="props.mode !== 'pk_in'">试题解析:{{ subject_data?.analyContent }}</view>
</view>
</view>
</view>
@@ -120,6 +130,7 @@
import { ref, reactive, onMounted, nextTick, computed, getCurrentInstance, watch } from 'vue';
import radioSubjectItem from './radio-subject-item.vue';
import esAnalysis from './es-analysis.vue';
import referenceAnswer from './reference-answer.vue';
import questionVue from './question.vue';
import { markIcon, feedbackIcon } from '@/common/imgSvg';
import { SUBJECT_TYPE } from '@/enum.js';
@@ -153,7 +164,9 @@
// 学习: study
// 考试结算页 settlement
// 练习记录页 practice_record, 'practice_record'
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record', 'pk_in'].includes(value);
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record', 'pk_in'].includes(
value
);
}
},
// 是否预览模式
@@ -0,0 +1,412 @@
<template>
<uni-popup ref="popup" mask-background-color="rgba(0,0,0,0.9)">
<view class="popup-content" @touchmove.stop catchmove="true" @touchmove.stop.prevent="handleTouchMove">
<view class="fl1"></view>
<view class="max-div">
<view class="festival-text-pic-div">
<image
class="festival-text-img"
mode="widthFix"
src="@/static/images/pointsAndRank/festival-text.png"
></image>
</view>
<image
class="festival-bg-up-div"
mode="widthFix"
src="@/static/images/pointsAndRank/festival-bg-up.png"
></image>
<view class="max-area-div">
<!-- 右上角百禄图标 -->
<view class="festival-special-mascot">
<image-preview
class="festival-special-mascot-img"
mode="widthFix"
:src="holidayInfo.ossAddr"
></image-preview>
</view>
<!-- 几个特殊节日的特殊背景 -->
<view class="festival-special-bg" v-if="holidayInfo.backgroundOssAddr">
<image-preview
class="festival-special-bg-img"
mode="widthFix"
:src="holidayInfo.backgroundOssAddr"
></image-preview>
</view>
<!-- 顶部的字随机还是大额 -->
<view class="festival-text-div">{{ holidayInfo.holidayDesc }}</view>
<view class="festival-main-div">
<image
class="festival-main-img"
mode="widthFix"
src="@/static/images/pointsAndRank/festival-main.png"
></image>
<view class="festival-main-text">
<view class="festival-main-title">
{{ done.tipText }}
</view>
<view class="festival-main-number-div">
<text class="festival-main-number">
{{ score }}
</text>
<text class="festival-main-number-text">积分</text>
</view>
</view>
</view>
<view class="festival-button-div" @click="startLottery()">{{ done.buttonText }}</view>
</view>
<view class="close-div" @click="close(false)"></view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</template>
<script setup lang="ts">
import type { HolidayInfo, CompleteCallback, StartLotteryCallback } from './types/holiday';
import { RewardType, AnimationStatus } from './types/holiday';
import { ref, computed, reactive } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { obtainRewardRandom } from '@/api/pointsAndRank';
import common from '@/common/common';
const countToRef = ref(null);
const popup = ref(null);
const animationLoading = ref<AnimationStatus>(AnimationStatus.UNSTARTED);
const done = computed(() =>
holidayInfo.rewardType === RewardType.RANDOM_POINT
? {
tipText: '最高可获',
buttonText: animationLoading.value === AnimationStatus.COMPLETED ? '开心收下' : '立即抽取'
}
: { tipText: '恭喜获得', buttonText: '开心收下' }
);
// 显示的分数
const score = ref(10);
let lotteryTimer: number | null = null;
const emits = defineEmits(['checkInFun']);
// 开始抽奖
const startLottery = async () => {
// 固定积分,或者随机积分但是已经抽全完毕,再次点击按钮就是关闭
if (holidayInfo.rewardType === RewardType.FIXED_POINT || animationLoading.value === AnimationStatus.COMPLETED) {
close(true);
return;
}
// 正在抽取中防止重复点击
if (animationLoading.value === AnimationStatus.LOADING) return;
animationLoading.value = AnimationStatus.LOADING;
// 访问接口获取结果
if (lotteryTimer) clearInterval(lotteryTimer);
startLotteryCallback({
holidayCode: holidayInfo.holidayCode,
successCb: (body) => {
// const { body } = await obtainRewardRandom({ holidayCode: holidayInfo.holidayCode });
const finalScore = body.val;
// 记录已完成的循环圈数
let completedLoops = 0;
// 1. 数值范围(可自定义)
const LOTTERY_MIN = body.start; // 最小值
const LOTTERY_MAX = body.end; // 最大值
// 2. 循环圈数
const LOOP_TIMES = finalScore - LOTTERY_MIN > 10 ? 1 : 2;
// 3. 跳动速度(ms/次,越大越慢)
const JUMP_SPEED = 80;
// 3. 核心:循环指定圈数后,最后一轮停在目标数字
lotteryTimer = setInterval(() => {
// 先判断:是否已完成指定圈数 + 当前轮次能精准到目标数字
if (completedLoops >= LOOP_TIMES) {
if (score.value === finalScore) {
clearInterval(lotteryTimer!);
lotteryTimer = null;
animationLoading.value = AnimationStatus.COMPLETED;
return;
}
// 最后一轮:继续递增(不重置)
score.value += 1;
} else {
// 未完成指定圈数:正常循环(到max重置,计数+1)
score.value += 1;
// 当数值到max时,代表完成1圈
if (score.value >= LOTTERY_MAX) {
completedLoops += 1;
score.value = LOTTERY_MIN; // 重置为最小值,开始下一圈
}
}
}, JUMP_SPEED);
},
failCb: (res) => {
console.log('报错');
}
});
};
const handleTouchMove = (e) => {
e.stopPropagation();
e.preventDefault();
};
// 节日奖励信息响应式对象
const holidayInfo = reactive<HolidayInfo>({
// 假日编码
holidayCode: '',
// 是否存在节日奖励(true:有奖励 false:无奖励)
rewardFlag: false,
// 奖励类型(枚举限定:1-固定积分 2-随机积分)
rewardType: RewardType.RANDOM_POINT,
// 文案
holidayDesc: '',
// 奖励分数(字符串类型,兼容不同格式的积分值)
rewardPoints: 0,
// 可以获得的最高分
rangeEnd: 0,
// 可以获得的最低分
rangeStart: 0,
// 奖励获取标识(true:已获取 false:未获取)
obtainFlag: false,
// 文案图片的OSS存储key(用于图片上传/读取标识)
ossKey: '',
// 文案图片的OSS访问地址(图片完整URL)
ossAddr: '',
// 背景图片的OSS存储key(用于图片上传/读取标识)
backgroundOssKey: '',
// 背景图片的OSS访问地址(图片完整URL)
backgroundOssAddr: ''
});
let completeCallback: CompleteCallback | undefined; // 关闭弹窗的回调
let startLotteryCallback: StartLotteryCallback | undefined; // 抽奖的回调
// 退出
const close = (status: boolean): void => {
uni.showTabBar({
success() {
popup.value.close();
console.log('点击退出,执行回调函数');
typeof completeCallback === 'function' &&
completeCallback({
status
});
}
});
};
const open = (
holidayBody: any,
lotteryCb: StartLotteryCallback = () => {},
completeCb: CompleteCallback = () => {}
): void => {
startLotteryCallback = lotteryCb; // 抽奖的回调
completeCallback = completeCb; // 关闭弹窗的回调
Object.assign(holidayInfo, holidayBody);
// if (!holidayInfo.rewardFlag || holidayInfo.obtainFlag) {
if (!holidayInfo.rewardFlag) {
// 如果当前没有节日窗口,立刻返回回调函数
completeCallback({ status: false });
return;
}
// 设置初始得分
if (holidayInfo.rewardType === RewardType.RANDOM_POINT) {
score.value = holidayInfo.rangeEnd;
} else {
score.value = holidayInfo.rewardPoints;
}
animationLoading.value = AnimationStatus.UNSTARTED;
uni.hideTabBar({
success() {
popup.value.open();
}
});
};
defineExpose({ open, close });
</script>
<style scoped lang="scss">
.max-area-div {
width: 520rpx;
height: 542rpx;
background-image: url('@/static/images/pointsAndRank/festival-bg.png');
background-size: 100%;
border-radius: 0 0 60rpx 60rpx;
position: relative;
// .festival-tip-div {
// z-index: 20;
// position: relative;
// margin-left: 16rpx;
// margin-bottom: -16rpx;
// .festival-tip-img {
// margin-top: 30rpx;
// height: 56rpx;
// }
// }
.festival-text-div {
position: relative;
margin-left: 25rpx;
z-index: 60;
margin-top: 28rpx;
color: #ffffff;
font-style: normal;
font-weight: 600;
font-size: 58rpx;
color: #fffae8;
letter-spacing: 0.05em;
line-height: 77rpx;
text-shadow: 0px 4px 8px #d6061b;
text-align: left;
font-style: italic;
transform: skewX(2deg);
}
.festival-main-div {
// margin-top: 8rpx;
z-index: 20;
position: relative;
width: 530rpx;
height: 270rpx;
.festival-main-img {
margin-left: 16rpx;
width: 530rpx;
position: absolute;
left: 0;
top: 0;
}
.festival-main-text {
position: absolute;
left: 40rpx;
top: 50rpx;
width: 150rpx;
// background-color:red;
.festival-main-title {
font-weight: 600;
font-size: 34rpx;
color: #b24325;
line-height: 46rpx;
text-align: right;
}
.festival-main-number-div {
margin-top: 10rpx;
font-family: DINAlternate, DINAlternate;
color: #b24325;
line-height: 64rpx;
margin-right: 8rpx;
text-align: right;
white-space: nowrap;
.festival-main-number {
font-size: 64rpx;
font-weight: bold;
}
.festival-main-number-text {
font-size: 26rpx;
white-space: nowrap;
}
}
}
}
.festival-button-div {
position: relative;
z-index: 20;
width: 392rpx;
height: 108rpx;
margin: 0 auto;
background-image: url('@/static/images/pointsAndRank/festival-button.png');
background-size: 100%;
background-repeat: no-repeat;
font-weight: 600;
font-size: 38rpx;
color: #b24325;
line-height: 108rpx;
text-align: center;
}
.festival-special-bg {
width: 520rpx;
height: 542rpx;
overflow: hidden;
position: absolute;
left: 0;
bottom: 0;
border-radius: 0 0 60rpx 60rpx;
z-index: 10;
.festival-special-bg-img {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.festival-special-mascot {
z-index: 50;
position: absolute;
right: -36rpx;
top: 70rpx;
transform-origin: left bottom;
transform: translateY(-100%);
.festival-special-mascot-img {
width: 180rpx;
}
}
}
.festival-text-pic-div {
width: 100%;
.festival-text-img {
width: 400rpx;
}
}
.festival-bg-up-div {
width: 580rpx;
margin-bottom: -14rpx;
z-index: 5;
}
.popup-content {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: calc(10rpx + 2%) 0;
padding: 20rpx 0;
}
.max-div {
width: 600rpx;
border-radius: 38rpx;
position: relative;
padding: 26rpx;
margin-top: var(--status-bar-height);
margin-bottom: calc(180rpx + 15%);
display: flex;
flex-direction: column;
align-items: center;
}
.close-div {
// 基础样式:设置按钮大小、圆形、边框等
margin-top: 50rpx;
position: relative; // 为伪类定位做准备
width: 62rpx; // 圆圈直径,可根据需求调整
height: 62rpx;
border: 4rpx solid #b9b9b9; // 圆圈边框
border-radius: 50%; // 变成正圆
box-sizing: border-box; // 保证边框不影响整体大小
// 伪类通用样式:十字的基础线条
&::before,
&::after {
content: ''; // 伪类必须有content
position: absolute; // 绝对定位居中
top: 50%;
left: 50%;
width: 24rpx; // 十字线条长度,可调整
height: 4rpx; // 十字线条粗细,可调整
background-color: #b9b9b9; // 十字颜色
transform: translate(-50%, -50%); // 居中对齐
}
// 第一条线旋转45度
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
// 第二条线旋转-45度,组合成×
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
</style>
+412
View File
@@ -0,0 +1,412 @@
<template>
<uni-popup ref="popup" mask-background-color="rgba(0,0,0,0.9)">
<view class="popup-content" @touchmove.stop catchmove="true" @touchmove.stop.prevent="handleTouchMove">
<view class="fl1"></view>
<view class="max-div">
<view class="festival-text-pic-div">
<image
class="festival-text-img"
mode="widthFix"
src="@/static/images/pointsAndRank/festival-text.png"
></image>
</view>
<image
class="festival-bg-up-div"
mode="widthFix"
src="@/static/images/pointsAndRank/festival-bg-up.png"
></image>
<view class="max-area-div">
<!-- 右上角百禄图标 -->
<view class="festival-special-mascot">
<image-preview
class="festival-special-mascot-img"
mode="widthFix"
:src="holidayInfo.ossAddr"
></image-preview>
</view>
<!-- 几个特殊节日的特殊背景 -->
<view class="festival-special-bg" v-if="holidayInfo.backgroundOssAddr">
<image-preview
class="festival-special-bg-img"
mode="widthFix"
:src="holidayInfo.backgroundOssAddr"
></image-preview>
</view>
<!-- 顶部的字随机还是大额 -->
<view class="festival-text-div">{{ holidayInfo.holidayDesc }}</view>
<view class="festival-main-div">
<image
class="festival-main-img"
mode="widthFix"
src="@/static/images/pointsAndRank/festival-main.png"
></image>
<view class="festival-main-text">
<view class="festival-main-title">
{{ done.tipText }}
</view>
<view class="festival-main-number-div">
<text class="festival-main-number">
{{ score }}
</text>
<text class="festival-main-number-text">积分</text>
</view>
</view>
</view>
<view class="festival-button-div" @click="startLottery()">{{ done.buttonText }}</view>
</view>
<view class="close-div" @click="close(false)"></view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</template>
<script setup lang="ts">
import type { HolidayInfo, CompleteCallback, StartLotteryCallback } from './types/holiday';
import { RewardType, AnimationStatus } from './types/holiday';
import { ref, computed, reactive } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { obtainRewardRandom } from '@/api/pointsAndRank';
import common from '@/common/common';
const countToRef = ref(null);
const popup = ref(null);
const animationLoading = ref<AnimationStatus>(AnimationStatus.UNSTARTED);
const done = computed(() =>
holidayInfo.rewardType === RewardType.RANDOM_POINT
? {
tipText: '最高可获',
buttonText: animationLoading.value === AnimationStatus.COMPLETED ? '开心收下' : '立即抽取'
}
: { tipText: '恭喜获得', buttonText: '开心收下' }
);
// 显示的分数
const score = ref(10);
let lotteryTimer: number | null = null;
const emits = defineEmits(['checkInFun']);
// 开始抽奖
const startLottery = async () => {
// 固定积分,或者随机积分但是已经抽全完毕,再次点击按钮就是关闭
if (holidayInfo.rewardType === RewardType.FIXED_POINT || animationLoading.value === AnimationStatus.COMPLETED) {
close(true);
return;
}
// 正在抽取中防止重复点击
if (animationLoading.value === AnimationStatus.LOADING) return;
animationLoading.value = AnimationStatus.LOADING;
// 访问接口获取结果
if (lotteryTimer) clearInterval(lotteryTimer);
startLotteryCallback({
holidayCode: holidayInfo.holidayCode,
successCb: (body) => {
// const { body } = await obtainRewardRandom({ holidayCode: holidayInfo.holidayCode });
const finalScore = body.val;
// 记录已完成的循环圈数
let completedLoops = 0;
// 1. 数值范围(可自定义)
const LOTTERY_MIN = body.start; // 最小值
const LOTTERY_MAX = body.end; // 最大值
// 2. 循环圈数
const LOOP_TIMES = finalScore - LOTTERY_MIN > 10 ? 1 : 2;
// 3. 跳动速度(ms/次,越大越慢)
const JUMP_SPEED = 80;
// 3. 核心:循环指定圈数后,最后一轮停在目标数字
lotteryTimer = setInterval(() => {
// 先判断:是否已完成指定圈数 + 当前轮次能精准到目标数字
if (completedLoops >= LOOP_TIMES) {
if (score.value === finalScore) {
clearInterval(lotteryTimer!);
lotteryTimer = null;
animationLoading.value = AnimationStatus.COMPLETED;
return;
}
// 最后一轮:继续递增(不重置)
score.value += 1;
} else {
// 未完成指定圈数:正常循环(到max重置,计数+1)
score.value += 1;
// 当数值到max时,代表完成1圈
if (score.value >= LOTTERY_MAX) {
completedLoops += 1;
score.value = LOTTERY_MIN; // 重置为最小值,开始下一圈
}
}
}, JUMP_SPEED);
},
failCb: (res) => {
console.log('报错');
}
});
};
const handleTouchMove = (e) => {
e.stopPropagation();
e.preventDefault();
};
// 节日奖励信息响应式对象
const holidayInfo = reactive<HolidayInfo>({
// 假日编码
holidayCode: '',
// 是否存在节日奖励(true:有奖励 false:无奖励)
rewardFlag: false,
// 奖励类型(枚举限定:1-固定积分 2-随机积分)
rewardType: RewardType.RANDOM_POINT,
// 文案
holidayDesc: '',
// 奖励分数(字符串类型,兼容不同格式的积分值)
rewardPoints: 0,
// 可以获得的最高分
rangeEnd: 0,
// 可以获得的最低分
rangeStart: 0,
// 奖励获取标识(true:已获取 false:未获取)
obtainFlag: false,
// 文案图片的OSS存储key(用于图片上传/读取标识)
ossKey: '',
// 文案图片的OSS访问地址(图片完整URL)
ossAddr: '',
// 背景图片的OSS存储key(用于图片上传/读取标识)
backgroundOssKey: '',
// 背景图片的OSS访问地址(图片完整URL)
backgroundOssAddr: ''
});
let completeCallback: CompleteCallback | undefined; // 关闭弹窗的回调
let startLotteryCallback: StartLotteryCallback | undefined; // 抽奖的回调
// 退出
const close = (status: boolean): void => {
uni.showTabBar({
success() {
popup.value.close();
console.log('点击退出,执行回调函数');
typeof completeCallback === 'function' &&
completeCallback({
status
});
}
});
};
const open = (
holidayBody: any,
lotteryCb: StartLotteryCallback = () => {},
completeCb: CompleteCallback = () => {}
): void => {
startLotteryCallback = lotteryCb; // 抽奖的回调
completeCallback = completeCb; // 关闭弹窗的回调
Object.assign(holidayInfo, holidayBody);
// if (!holidayInfo.rewardFlag || holidayInfo.obtainFlag) {
if (!holidayInfo.rewardFlag) {
// 如果当前没有节日窗口,立刻返回回调函数
completeCallback({ status: false });
return;
}
// 设置初始得分
if (holidayInfo.rewardType === RewardType.RANDOM_POINT) {
score.value = holidayInfo.rangeEnd;
} else {
score.value = holidayInfo.rewardPoints;
}
animationLoading.value = AnimationStatus.UNSTARTED;
uni.hideTabBar({
success() {
popup.value.open();
}
});
};
defineExpose({ open, close });
</script>
<style scoped lang="scss">
.max-area-div {
width: 520rpx;
height: 542rpx;
background-image: url('@/static/images/pointsAndRank/festival-bg.png');
background-size: 100%;
border-radius: 0 0 60rpx 60rpx;
position: relative;
// .festival-tip-div {
// z-index: 20;
// position: relative;
// margin-left: 16rpx;
// margin-bottom: -16rpx;
// .festival-tip-img {
// margin-top: 30rpx;
// height: 56rpx;
// }
// }
.festival-text-div {
position: relative;
margin-left: 25rpx;
z-index: 60;
margin-top: 28rpx;
color: #ffffff;
font-style: normal;
font-weight: 600;
font-size: 58rpx;
color: #fffae8;
letter-spacing: 0.05em;
line-height: 77rpx;
text-shadow: 0px 4px 8px #d6061b;
text-align: left;
font-style: italic;
transform: skewX(2deg);
}
.festival-main-div {
// margin-top: 8rpx;
z-index: 20;
position: relative;
width: 530rpx;
height: 270rpx;
.festival-main-img {
margin-left: 16rpx;
width: 530rpx;
position: absolute;
left: 0;
top: 0;
}
.festival-main-text {
position: absolute;
left: 40rpx;
top: 50rpx;
width: 150rpx;
// background-color:red;
.festival-main-title {
font-weight: 600;
font-size: 34rpx;
color: #b24325;
line-height: 46rpx;
text-align: right;
}
.festival-main-number-div {
margin-top: 10rpx;
font-family: DINAlternate, DINAlternate;
color: #b24325;
line-height: 64rpx;
margin-right: 8rpx;
text-align: right;
white-space: nowrap;
.festival-main-number {
font-size: 64rpx;
font-weight: bold;
}
.festival-main-number-text {
font-size: 26rpx;
white-space: nowrap;
}
}
}
}
.festival-button-div {
position: relative;
z-index: 20;
width: 360rpx;
height: 80rpx;
margin: 0 auto;
background-image: url('@/static/images/pointsAndRank/festival-button.png');
background-size: 100%;
background-repeat: no-repeat;
font-weight: 600;
text-align: center;
font-size: 32rpx;
color: #B24325;
line-height: 80rpx;
}
.festival-special-bg {
width: 520rpx;
height: 542rpx;
overflow: hidden;
position: absolute;
left: 0;
bottom: 0;
border-radius: 0 0 60rpx 60rpx;
z-index: 10;
.festival-special-bg-img {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.festival-special-mascot {
z-index: 50;
position: absolute;
right: -36rpx;
top: 70rpx;
transform-origin: left bottom;
transform: translateY(-100%);
.festival-special-mascot-img {
width: 180rpx;
}
}
}
.festival-text-pic-div {
width: 100%;
.festival-text-img {
width: 400rpx;
}
}
.festival-bg-up-div {
width: 580rpx;
margin-bottom: -14rpx;
z-index: 5;
}
.popup-content {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: calc(10rpx + 2%) 0;
padding: 20rpx 0;
}
.max-div {
width: 600rpx;
border-radius: 38rpx;
position: relative;
padding: 26rpx;
margin-top: var(--status-bar-height);
margin-bottom: calc(180rpx + 15%);
display: flex;
flex-direction: column;
align-items: center;
}
.close-div {
// 基础样式:设置按钮大小、圆形、边框等
margin-top: 50rpx;
position: relative; // 为伪类定位做准备
width: 62rpx; // 圆圈直径,可根据需求调整
height: 62rpx;
border: 4rpx solid #b9b9b9; // 圆圈边框
border-radius: 50%; // 变成正圆
box-sizing: border-box; // 保证边框不影响整体大小
// 伪类通用样式:十字的基础线条
&::before,
&::after {
content: ''; // 伪类必须有content
position: absolute; // 绝对定位居中
top: 50%;
left: 50%;
width: 24rpx; // 十字线条长度,可调整
height: 4rpx; // 十字线条粗细,可调整
background-color: #b9b9b9; // 十字颜色
transform: translate(-50%, -50%); // 居中对齐
}
// 第一条线旋转45度
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
// 第二条线旋转-45度,组合成×
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
</style>
+1 -1
View File
@@ -188,6 +188,6 @@
overflow: hidden;
margin-top: var(--status-bar-height);
margin-bottom: 100rpx;
overflow: auto;
overflow-y: auto;
}
</style>
@@ -0,0 +1,68 @@
// 奖励类型枚举(和接口相关的枚举也放一起)
export enum RewardType {
FIXED_POINT = '1', // 固定积分
RANDOM_POINT = '2' // 随机积分
}
// 定义抽奖动画状态枚举(码值)
export enum AnimationStatus {
// 未开始(初始状态)
UNSTARTED = 'unstarted',
// 动画进行中
LOADING = 'loading',
// 动画已完成
COMPLETED = 'completed'
}
// 节日奖励信息接口
export interface HolidayInfo {
holidayCode : string;
rewardFlag : boolean;
rewardType : RewardType;
rewardPoints : number;
holidayDesc : string;
rangeEnd : number;
rangeStart : number;
obtainFlag : boolean;
ossKey : string;
ossAddr : string;
backgroundOssKey : string;
backgroundOssAddr : string;
}
/**
* 节日弹窗关闭后的回调函数接口
* @param params - 回调参数
* @param params.status - 签到状态(false 表示关闭/未完成)
*/
export interface CompleteCallback {
(params : {
status : boolean;
}) : void;
}
/** 抽取积分成功的回调类型 */
export interface LotterySuccessCb {
// val 为抽取到的积分,start积分的最小数,end为积分的最大值
(data : { val : number; start : number; end : number }) : void;
}
/** 抽取积分失败的回调类型 */
export interface LotteryFailCb {
// 失败回调接收错误信息
(error : any) : void;
}
/**
* 需要随机抽取积分的节日,随机抽取积分的回调函数
* @param params - 回调参数
*/
export interface StartLotteryCallback {
(params : {
holidayCode : string;
successCb : LotterySuccessCb;
failCb : LotteryFailCb;
}) : void;
}
+133 -68
View File
@@ -1,6 +1,8 @@
import {
queryCheckIn,
addPointsByCheckIn
addPointsByCheckIn,
queryHolidayReward,
obtainRewardRandom
} from '@/api/pointsAndRank.js';
import common from '@/common/common';
import {
@@ -11,6 +13,7 @@ import {
} from 'vue';
export default function useCheckIn() {
const checkInRef = ref(null)
const holidayRef = ref(null)
// ========== 核心工具方法 ==========
/**
* 获取当前用户的签到存储key(区分不同用户)
@@ -45,78 +48,96 @@ export default function useCheckIn() {
PopupDay: ''
};
try {
// 读取本地存储的签到信息(兼容JSON格式)
// 读取本地存储的签到信息
const rawData = common.getValue(key);
if (!rawData) return {
signDate: '',
checkInDays: 0,
popupDay: ''
popupDay: '',
rewardFlag: null,
obtainFlag: null,
};
// 兼容存储的是字符串或JSON对象的情况
const signStatus = rawData;
return {
signDate: signStatus.signDate || '',
checkInDays: signStatus.checkInDays || 0,
popupDay: signStatus.popupDay || '',
rewardFlag: signStatus.rewardFlag || null,
obtainFlag: signStatus.obtainFlag || null,
};
} catch (e) {
console.error('读取本地签到状态失败', e);
return {
signDate: '',
checkInDays: 0,
popupDay: ''
popupDay: '',
rewardFlag: null,
obtainFlag: null,
};
}
};
/**
* 更新本地签到状态(支持按需更新字段)
* @param {boolean|undefined} isSigned - 是否签到(可选,传入则更新)
* @param {number|undefined} checkInDays - 签到天数(可选,传入则更新)
* @param {string|undefined} popupDay - 弹窗关闭日期(可选,传入则更新)
*/
const setLocalSignStatus = (isSigned, checkInDays, popupDay) => {
const key = getKey();
if (!key) return;
const setLocalSignStatus = (params) => {
const {
isSigned,
checkInDays,
popupDay,
rewardFlag,
obtainFlag
} = params;
const key = getKey();
if (!key) return;
// 1. 读取本地原有数据(若无则初始化空对象)
let oldSignStatus = {};
try {
oldSignStatus = common.getValue(key) || {}; // 兼容common.getValue返回null/undefined的情况
} catch (e) {
console.warn('读取原有签到状态失败,使用空对象初始化', e);
oldSignStatus = {};
}
// 1. 读取本地原有数据(若无则初始化空对象)
let oldSignStatus = {};
try {
oldSignStatus = common.getValue(key) || {}; // 兼容common.getValue返回null/undefined的情况
} catch (e) {
console.warn('读取原有签到状态失败,使用空对象初始化', e);
oldSignStatus = {};
}
// 2. 初始化新状态:先继承原有数据
const newSignStatus = { ...oldSignStatus };
// 2. 初始化新状态:先继承原有数据
const newSignStatus = {
...oldSignStatus
};
// 3. 按需更新字段(仅当参数传入时覆盖)
// 处理isSigned:传入则更新,且为true时自动设置signDate
if (isSigned !== undefined) {
// 保持原有逻辑:isSigned为true时,signDate设为今日
newSignStatus.signDate = isSigned ? getTodayDate() : oldSignStatus.signDate;
}
// 3. 按需更新字段(仅当参数传入时覆盖)
// 处理isSigned:传入则更新,且为true时自动设置signDate
if (isSigned !== undefined) {
// 保持原有逻辑:isSigned为true时,signDate设为今日
newSignStatus.signDate = isSigned ? getTodayDate() : oldSignStatus.signDate;
}
// 处理checkInDays:传入则更新,未传入保留原有值
if (checkInDays !== undefined) {
newSignStatus.checkInDays = checkInDays;
}
// 处理checkInDays:传入则更新,未传入保留原有值
if (checkInDays !== undefined) {
newSignStatus.checkInDays = checkInDays;
}
// 处理popupDay:传入非空则更新,未传入/空值保留原有值
if (popupDay !== undefined) {
newSignStatus.popupDay = popupDay? getTodayDate() : oldSignStatus.popupDay;
}
// 处理popupDay:传入非空则更新,未传入/空值保留原有值
if (popupDay !== undefined) {
newSignStatus.popupDay = popupDay ? getTodayDate() : oldSignStatus.popupDay;
}
// 4. 重新存入本地(覆盖原有数据)
try {
console.log('存入的数据(原有+更新)', newSignStatus);
common.setValue(key, newSignStatus);
} catch (e) {
console.error('更新本地签到状态失败', e);
}
if (rewardFlag !== undefined) {
newSignStatus.rewardFlag = rewardFlag;
}
if (obtainFlag !== undefined) {
newSignStatus.obtainFlag = obtainFlag;
}
// 4. 重新存入本地
try {
console.log('存入的数据(原有+更新)', newSignStatus);
common.setValue(key, newSignStatus);
} catch (e) {
console.error('更新本地签到状态失败', e);
}
};
@@ -129,32 +150,46 @@ export default function useCheckIn() {
const initCheckInStatus = async (complete = () => {}) => {
const today = getTodayDate();
const localStatus = getLocalSignStatus();
console.log('localStatus', localStatus);
// 1. 本地已有今日签到记录 → 直接返回本地状态
if (localStatus.signDate === today) {
complete(true, localStatus.checkInDays, localStatus.popupDay === today)
const checkInInfo = {
isSigned: true,
checkInDays: localStatus.checkInDays,
popUpStatus: localStatus.popupDay === today
}
const holidayBody = {
rewardFlag: localStatus.rewardFlag,
obtainFlag: localStatus.obtainFlag
}
complete(checkInInfo, holidayBody)
return true;
}
// 2. 本地记录过期/无记录 → 请求接口获取最新状态
try {
const res = await queryCheckIn(); // 调用查询签到状态接口
const {
body
} = res || {};
console.log('bodbodyy', body);
// // 接口返回格式适配(根据实际后端返回调整)
const [{
body = {}
}, {
body: holidayBody = {}
}] = await Promise.all([
queryCheckIn(), // 第一个接口:查询签到状态
queryHolidayReward() // 第二个接口:查询节日奖励
]);
const isSigned = body?.checkInToday === 'Y';
const checkInDays = body?.days || 0;
setLocalSignStatus(isSigned, checkInDays);
// if (!isSigned) { // 没签到执行签到弹窗
// console.log('没签到执行签到弹窗');
// //如果今日点击过x那么久不弹了
// // if(localStatus.popupDay === today) return;
// complete(isSigned, checkInDays, localStatus.popupDay === today)
// }
complete(isSigned, checkInDays, localStatus.popupDay === today)
setLocalSignStatus({
isSigned,
checkInDays,
rewardFlag: holidayBody?.rewardFlag,
obtainFlag: holidayBody?.obtainFlag,
});
const checkInInfo = {
isSigned,
checkInDays,
popUpStatus: localStatus.popupDay === today
}
complete(checkInInfo, holidayBody)
} catch (e) {
console.error('请求签到状态接口失败', e);
}
@@ -181,14 +216,15 @@ export default function useCheckIn() {
*/
const checkInFun = async ({
status,
days=0,
successCb=() =>{},
failCb=() =>{}
days = 0,
successCb = () => {},
failCb = () => {}
}) => {
if(!status) { // 点击了关闭
if (!status) { // 点击了关闭
console.log('点击了关闭');
setLocalSignStatus(undefined, undefined, true);
setLocalSignStatus({
popupDay: true,
});
return;
}
console.log('请求接口', days);
@@ -197,7 +233,10 @@ export default function useCheckIn() {
})
.then((res) => {
console.log('接口成功回调', res);
setLocalSignStatus(true, days + 1);
setLocalSignStatus({
isSigned: true,
checkInDays: days + 1,
});
successCb?.({
days: days + 1,
res
@@ -207,6 +246,30 @@ export default function useCheckIn() {
})
};
/**
* 执行抽取节日积分操作
* @param {string} holidayCode - 节日积分
* @param {Function} successCb - 抽取积分成功回调
* @param {Function} failCb - 抽取积分失败回调
* @returns {Promise<boolean>} 签到是否成功
*/
const startLotteryFun = async ({
holidayCode,
successCb = () => {},
failCb = () => {}
}) => {
obtainRewardRandom({
holidayCode
}).then(({body}) => {
console.log('抽取成功回调', body);
// 把缓存中的记录设置为已经抽取积分
setLocalSignStatus({ obtainFlag: true});
successCb?.(body);
}).catch((error) => {
failCb?.(error)
})
};
/**
* 重置签到状态(可选,用于特殊场景)
*/
@@ -217,9 +280,11 @@ export default function useCheckIn() {
// ========== 返回对外暴露的方法 ==========
return {
holidayRef,
checkInRef,
initCheckInStatus, // 初始化签到状态(页面加载时调用)
checkInFun, // 执行签到
startLotteryFun, // 抽取随机积分
getLocalSignStatus, // 获取签到缓存信息
getTodayCheckInStatus // 获取今天签到状态
};
+76 -54
View File
@@ -1,74 +1,99 @@
<template>
<view class="point-item mb-30" v-for="(item, index) in modelValue?.pgrphData || []" :key="item.index"
@click="toDetail(item)" :class="item.pgrphStdyStateName == '已学习' ? 'learn' : 'unlearn'">
<view class="top mb-30">
<view class="">
<view class="knowledge-tip-div">前序知识点学完后方可解锁下一知识点学习</view>
<view
class="point-item mb-30"
v-for="(item, index) in modelValue?.pgrphData || []"
:key="item.index"
@click="toDetail(item)"
:class="item.pgrphStdyStateName == '已学习' ? 'learn' : 'unlearn'"
>
<view class="blue-ball">
<view class="ball"></view>
<image :src="item.pgrphStdyStateName == '已学习'?knowledgeComplete:knowledgeUnfinished" class="img"></image>
</view>
<view class="fz-30 font_pf title">知识点{{ item.pgrphNm }}</view>
<view class="fl1">
<view class="top mb-30">
<view class="fz-30 font_pf title">知识点{{ item.pgrphNm }}</view>
<view class="fl1"></view>
</view>
<view class="font_pf fz-24 study-status" >
{{ item.pgrphStdyStateName }}
<view class="bottom">
<view class="fz-30 font_pf text ellipsis-text">{{ item.pgrphTitle || item.pgrphContent }}</view>
</view>
</view>
<view class="bottom">
<view class="fz-30 font_pf text ellipsis-text">{{ item.pgrphTitle || item.pgrphContent }}</view>
</view>
</view>
</template>
<script setup>
import {computed} from "vue"
import { computed } from 'vue';
import common from '@/common/common';
const emit = defineEmits(['toStudyPrgh'])
import knowledgeComplete from '@/static/images/courseDetail/knowledgeComplete.png'
import knowledgeUnfinished from '@/static/images/courseDetail/knowledgeUnfinished.png'
const emit = defineEmits(['toStudyPrgh']);
const props = defineProps({
modelValue: Object
})
});
const min_no_success_id = computed(() => {
const find_item = props.modelValue?.pgrphData.find(res => (res.pgrphStdyStateName === '未学习' || res.pgrphStdyStateName === '学习中'))
return find_item?.pgrphId || ''
})
const find_item = props.modelValue?.pgrphData.find(
(res) => res.pgrphStdyStateName === '未学习' || res.pgrphStdyStateName === '学习中'
);
return find_item?.pgrphId || '';
});
const toDetail = (item) => {
if (item.pgrphStdyStateName === '已学习' || item.pgrphStdyStateName === '学习中' || min_no_success_id.value === item.pgrphId) {
emit('toStudyPrgh', item)
if (
item.pgrphStdyStateName === '已学习' ||
item.pgrphStdyStateName === '学习中' ||
min_no_success_id.value === item.pgrphId
) {
emit('toStudyPrgh', item);
} else {
common.msg('请按顺序学习知识点')
common.msg('请按顺序学习知识点');
}
}
};
</script>
<style lang="scss" scoped>
.learn{
.study-status{
color: #1DD17A;
}
.title,.text{
color: #000;
font-weight: 600;
}
}
.unlearn{
.study-status{
color: #666666;
}
color: #666;
}
.point-item {
background-color: #fff;
// display: flex;
.knowledge-tip-div {
padding-top: 24rpx;
padding-left: 36rpx;
padding-right: 52rpx;
padding-bottom: 24rpx;
font-weight: 500;
font-size: 30rpx;
color: #0066ff;
line-height: 40rpx;
text-align: center;
background: #e4f2ff;
border-radius: 22rpx;
border: 4rpx solid #96c0ff;
margin-bottom: 26rpx;
margin-top: 6rpx;
}
.learn {
.study-status {
color: #1dd17a;
}
.title,
.text {
color: #000;
font-weight: 600;
}
}
.unlearn {
.study-status {
color: #999999;
}
color: #999999;
}
.point-item {
background-color: #fff;
padding-top: 24rpx;
padding-left: 88rpx;
padding-right: 52rpx;
padding-bottom: 36rpx;
border-radius: 22rpx;
// justify-content: space-between;
// margin-left: 16rpx;
// margin-right: 16rpx;
position: relative;
overflow: hidden;
.top {
display: flex;
justify-content: space-between;
@@ -77,16 +102,13 @@
.bottom {
padding-left: 12rpx;
}
.blue-ball {
padding-top: 16rpx;
margin-right: 12rpx;
.ball {
width: 12rpx;
height: 12rpx;
background-color: #0066ff;
border-radius: 50%;
position: absolute;
left: 0%;
top: 0%;
.img{
width: 75rpx;
height: 73rpx;
}
}
}
+70 -20
View File
@@ -24,33 +24,36 @@
<image
class="title-icon"
src="/src/static/images/courseDetail/zhishidian.png"
mode="heightFix"
mode="widthFix"
></image>
<view class="mr-10">知识点</view>
<view class="c-000">{{ form.pgrphNum || 0 }}</view>
<view class="">知识点</view>
</view>
<view class="num-item">
<image
class="title-icon"
src="/src/static/images/courseDetail/yixue.png"
mode="heightFix"
></image>
<view class="mr-10">已学</view>
<view class="c-000">{{ form.styParaGraphNum || 0 }}</view>
<view class="line-progress-div">
<view class="text-1">{{ form.styParaGraphNum || 0 }}</view>
<uv-line-progress
:percentage="progress"
:showText="false"
activeColor="#0066FF"
inactiveColor="#D9DEE8"
:height="8"
></uv-line-progress>
<view class="text-2">{{ form.pgrphNum || 0 }}</view>
</view>
<view
class="num-item examination"
@click="common.navigateTo(`/pages/examRanking/index?crsId=${crsId}`)"
>
<image
class="title-icon"
class="ranking-icon"
src="/src/static/images/courseDetail/charts.png"
mode="heightFix"
mode="aspectFill"
></image>
<view>考试排行榜</view>
<view class="back-icon"></view>
</view>
<view class="fl1"></view>
<view class="num-item" @click="collect_click(form.collectId)">
<image
class="collect-icon"
@@ -64,14 +67,15 @@
<uv-sticky :customNavHeight="statusBarHeight" class="sticky">
<view class="body-tab">
<uv-tabs
class="font_pf"
class="font_pf my_tabs"
:current="tabAct"
:list="tabList"
@change="tabSwitch"
:scrollable="false"
line-color="#0066FF"
:active-style="{ color: '#0066FF', 'font-weight': 600, 'font-size': '30rpx' }"
:inactive-style="{ color: '#666666', 'font-size': '30rpx' }"
:inactive-style="{ color: '#333333', fontSize: '30rpx', fontWeight: '500' }"
:activeStyle="{ color: '#0066FF', fontSize: '30rpx', fontWeight: '600' }"
:lineColor="`url(${LineBg}) 10% 10%`"
></uv-tabs>
</view>
</uv-sticky>
@@ -110,6 +114,7 @@
</template>
<script setup>
import { LineBg } from '@/enum.js';
import { ref, computed, onMounted } from 'vue';
import { onShow, onLoad, onUnload } from '@dcloudio/uni-app';
import { getCourseDetailApi } from '@/api/courseDetail.js';
@@ -135,7 +140,13 @@
const scrollTop = ref(0);
const practiceButtonDisabled = computed(() => isPreview.value === 'preview' || form.value.examStat === '01');
const examinationButtonDisabled = computed(() => isPreview.value === 'preview' || form.value.examStat !== '00');
const progress = computed(() => {
// 计算进度百分比(保留两位小数)
const total = form.value.pgrphNum;
if (total === 0) return 0;
const completed = form.value.styParaGraphNum;
return Math.min(Math.round((completed / total) * 10000) / 100, 100);
});
const tabSwitch = (item) => {
tabAct.value = item.index;
};
@@ -298,6 +309,16 @@
</script>
<style lang="scss" scoped>
.my_tabs {
// 解决这个圆角的图片,开穿透
:deep(.uv-tabs__wrapper__nav__line) {
height: 20rpx !important;
left: 60rpx;
background-size: 26rpx !important;
background-repeat: no-repeat !important;
}
}
// 1124新改样式 ↓ ↓ ↓ ↓ ↓ ↓
.btn-box {
:deep(.uv-button--disabled) {
@@ -407,8 +428,14 @@
.title-icon {
height: 30rpx;
width: 30rpx;
margin-right: 10rpx;
}
.ranking-icon {
height: 40rpx;
width: 40rpx;
margin-right: 6rpx;
}
.collect-icon {
width: 40rpx;
@@ -419,6 +446,7 @@
.title-num {
display: flex;
flex-wrap: nowrap;
align-items: center;
.examination {
color: #0066ff;
@@ -510,11 +538,26 @@
display: flex;
align-items: center;
font-size: 30rpx;
margin-right: 26rpx;
margin-right: 16rpx;
color: #666666;
height: 30rpx;
}
.line-progress-div {
display: flex;
align-items: center;
flex: 1;
margin-right: 20rpx;
.text-1 {
color: #0066ff;
font-size: 30rpx;
margin: 0 16rpx;
}
.text-2 {
color: #999999;
font-size: 30rpx;
margin: 0 16rpx;
}
}
.learn {
color: #1dd17a;
}
@@ -527,4 +570,11 @@
font-weight: 600;
color: #000000;
}
.c-000 {
font-weight: 600;
color: #000000;
}
.c-0066FF {
color: #0066ff;
}
</style>
+16 -38
View File
@@ -156,43 +156,13 @@
<classScroll :ref="(el) => (classScrollRef[0] = el)" name="hot_class"></classScroll>
<classScroll :ref="(el) => (classScrollRef[1] = el)" name="recommend_class"></classScroll>
<classScroll :ref="(el) => (classScrollRef[2] = el)" name="new_class"></classScroll>
<!-- <view class="hot_class_div">
<scroll-view class="scroll-view" :scroll-x="true" :show-scrollbar="false">
<view class="scroll-view-item" v-for="(item, index) in hot_class_list" :key="index">
<view class="item_div">
<uv-skeleton :loading="!item.crsNum" :rows="3" avatar avatar-shape="square" :title="false">
<view class="item" @click="goto_course_detail(item.crsId)">
<view class="left">
<image-preview
class="img-box img"
:imgId="item.imgId"
:isCache="true"
:width="240"
:height="240"
></image-preview>
</view>
<view class="right">
<view class="title ellipsis-text">{{ item.crsName }}</view>
<view class="note ellipsis-text">更新{{ (item.mtTime || '').substr(0, 10) }}</view>
<view class="button_div">
<view class="note ellipsis-text">已学{{ item.accmStdyCnt }}人次</view>
<view class="button">去学习</view>
</view>
</view>
</view>
</uv-skeleton>
</view>
</view>
</scroll-view>
</view> -->
<view class="bottom_line">
<uv-divider text="我是有底线的" lineColor="#9cB0DC" :hairline="true"></uv-divider>
</view>
</view>
<tabbar-shadow></tabbar-shadow>
<checkIn ref="checkInRef"></checkIn>
<holiday ref="holidayRef"></holiday>
<badge ref="badgeRef"></badge>
<points ref="pointsRef"></points>
</view>
@@ -210,10 +180,11 @@
import classScroll from './components/class_scroll.vue';
import useCheckIn from '@/composables/useCheckIn';
import checkIn from '@/components/points-and-badge/check-in';
import holiday from '@/components/points-and-badge/holiday';
import badge from '@/components/points-and-badge/badge';
import points from '@/components/points-and-badge/points';
import usePointsAndBadge from '@/components/points-and-badge/usePointsAndBadge';
const { initCheckInStatus, checkInRef, checkInFun } = useCheckIn();
const { initCheckInStatus, checkInRef, checkInFun, startLotteryFun, holidayRef } = useCheckIn();
const { handlePointsBadgePopup, badgeRef, pointsRef } = usePointsAndBadge();
const scrollTop = ref(0);
const no_read_message_num = ref(0);
@@ -339,12 +310,19 @@
return;
}
initCheckInStatus((isSigned, checkInDays, popUpStatus) => {
if(!isSigned && !popUpStatus) {
checkInRef.value.open(isSigned, checkInDays, checkInFun, (status, days, pointsList = [], badgesList = []) => {
handlePointsBadgePopup(pointsList, badgesList);
});
}
initCheckInStatus((checkInInfo, holidayBody) => {
// 先判断是否有节日活动
const handleCheckInComplete = (params) => {
const {isSigned, checkInDays, popUpStatus} = checkInInfo
setTimeout(() => {
if(!isSigned && !popUpStatus) {
checkInRef.value.open(isSigned, checkInDays, checkInFun, (status, days, pointsList = [], badgesList = []) => {
handlePointsBadgePopup(pointsList, badgesList);
});
}
}, 140)
};
holidayRef.value?.open(holidayBody, startLotteryFun, handleCheckInComplete)
});
const userInfo = getUserInfo();
if (userInfo) {
+4 -1
View File
@@ -314,10 +314,13 @@
Object.assign(user_info, getUserInfo());
statistics_data['get_statistics_data'](); // 获取统计数据
// 获取今日是否签到消息
initCheckInStatus((isSigned, checkInDays, popUpStatus) => {
initCheckInStatus(({isSigned, checkInDays, popUpStatus}) => {
user_info.checkInToday = isSigned ? 1 : 2;
user_info.checkInDays = checkInDays;
});
});
onHide(() => {
checkInRef.value && checkInRef.value.close();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.