三阶段签到页面

This commit is contained in:
田岩
2025-10-29 14:14:00 +08:00
parent 7107dade1a
commit 1e9cc30c1b
26 changed files with 1043 additions and 180 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ ENV = 'development'
# 'development'
# 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 = 'http://192.168.247.200'
# VITE_APP_BASE_API_Url = 'http://aitscdn.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = 'http://192.168.108.129'
@@ -27,7 +27,7 @@ VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://192.168.247.200'
#VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://192.168.247.200'
# VITE_APP_BASE_H5_API_Url_TRASTUDY = 'http://25.64.32.154:9602'
# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://192.168.247.200'
#VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://192.168.247.200'
# VITE_APP_BASE_H5_API_Url_TRAASK = 'http://25.64.32.154:9605'
+2 -1
View File
@@ -2,6 +2,7 @@
ENV = 'development.rd'
# 'development.rd'
VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604'
+107 -105
View File
@@ -4,6 +4,10 @@ import {
queryCurrentValidMascotInfo
} from "@/api/mascot.js"
import {
queryCurrentStatus
} from '@/api/pointsAndRank.js';
import {
setToken,
setUserInfo,
@@ -24,15 +28,26 @@ export const useAccountLoginApp = (data) => request({
}) => {
if (rtnCode === '0000') {
setToken(body)
const res = await Promise.all([queryCurrentUser(), queryCurrentValidMascotInfo()])
const res = await Promise.all([queryCurrentUser(), queryCurrentValidMascotInfo()
// queryCurrentStatus()
])
const userInfo = getUserInfo()
const mascotInfo = res[1]
setUserInfo({
...userInfo,
'mascotInfo': mascotInfo
})
// 积分状态
// const pointsStatus = res[2].body
common.setValue('points_data', {
badgeNum: pointsStatus['badgeNum'] ?? 0, // 徽章数
currentPoints: pointsStatus['currentPoints'] ?? 0, // 当前积分
currentRankAddr: pointsStatus['ossAddr'] ?? '', // 当前段位图片
currentRankName: pointsStatus['currentRankName']?? '未知', // 当前段位名称
})
return {
mascotState:!mascotInfo.mascotId
mascotState: !mascotInfo.mascotId
}
}
return Promise.reject({
@@ -40,22 +55,6 @@ export const useAccountLoginApp = (data) => request({
body
})
}).catch(res => {
/**
error: "Service Unavailable"
path: "/traapp/access/userLogin"
requestId: "74d60800-104"
status: 503
timestamp: "2025-06-24T00:53:35.293+00:00"
*/
/**
body: null
channelNo: "FF6944B69A99745C009F4D0B3C3B22B8E81B22F587D68455E354BABBABCCB6246C54ABD28F3C55AAD34224A16CE7E24E"
head: null
message: "系统异常"
rtnCode: "9999"
rtnTime: "2025-06-24 08:54:52"
*/
return Promise.reject(res)
})
@@ -72,7 +71,7 @@ export const queryCurrentUser = (data) => request({
method: 'post',
data
}).then(res => {
// deptId: ""
// deptName: null
// loginName: "ADMIN" // todo id
@@ -92,107 +91,109 @@ export const queryCurrentUser = (data) => request({
// #ifdef APP-PLUS
// 更新用户信息中的头像路径
const updateUserAvatar = (imagePath) => {
const _userInfo = getUserInfo() ?? {}
setUserInfo({
..._userInfo,
...res.body,
'imagePath': imagePath
})
const _userInfo = getUserInfo() ?? {}
setUserInfo({
..._userInfo,
...res.body,
'imagePath': imagePath
})
}
// 确保目录存在
const ensureDirectoryExists = (dirPath) => {
return new Promise((resolve) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getDirectory(dirPath, { create: true }, resolve, resolve)
})
})
return new Promise((resolve) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getDirectory(dirPath, {
create: true
}, resolve, resolve)
})
})
}
// 生成唯一文件名
const generateUniqueFilename = (pathName) => {
if (!pathName || typeof pathName !== 'string') {
throw new Error('无效的路径');
}
// 直接使用原始文件名(带扩展名)
const filename = pathName.split('/').pop();
if (!filename) {
throw new Error('无法从路径中提取文件名');
}
// 确保文件扩展名为.png (可选,根据实际需求)
const ext = filename.split('.').pop().toLowerCase();
if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp') {
return filename;
}
// 如果没有有效扩展名,添加.png
return `${filename}.png`;
if (!pathName || typeof pathName !== 'string') {
throw new Error('无效的路径');
}
// 直接使用原始文件名(带扩展名)
const filename = pathName.split('/').pop();
if (!filename) {
throw new Error('无法从路径中提取文件名');
}
// 确保文件扩展名为.png (可选,根据实际需求)
const ext = filename.split('.').pop().toLowerCase();
if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp') {
return filename;
}
// 如果没有有效扩展名,添加.png
return `${filename}.png`;
};
// 下载并保存头像
const downloadAndSaveAvatar = async (fileUrl, localFilePath) => {
try {
await ensureDirectoryExists('avatar')
return new Promise((resolve, reject) => {
console.log('开始下载头像:', fileUrl)
const dtask = plus.downloader.createDownload(fileUrl, {
filename: localFilePath
}, (d, status) => {
if (status === 200) {
console.log("保存路径:", d.filename)
resolve(d.filename)
} else {
console.error("文件下载失败:", status)
reject(new Error(`下载失败,状态码: ${status}`))
}
})
dtask.start()
})
} catch (error) {
console.error('创建目录失败:', error)
throw error
}
try {
await ensureDirectoryExists('avatar')
return new Promise((resolve, reject) => {
console.log('开始下载头像:', fileUrl)
const dtask = plus.downloader.createDownload(fileUrl, {
filename: localFilePath
}, (d, status) => {
if (status === 200) {
console.log("保存路径:", d.filename)
resolve(d.filename)
} else {
console.error("文件下载失败:", status)
reject(new Error(`下载失败,状态码: ${status}`))
}
})
dtask.start()
})
} catch (error) {
console.error('创建目录失败:', error)
throw error
}
}
// 检查文件是否存在
const checkFileExists = (filePath) => {
return new Promise((resolve) => {
uni.getFileInfo({
filePath,
success: () => resolve(true),
fail: () => resolve(false)
})
})
return new Promise((resolve) => {
uni.getFileInfo({
filePath,
success: () => resolve(true),
fail: () => resolve(false)
})
})
}
// 主逻辑
(async () => {
try {
const pathName = res.body.imageAddr
const fileUrl = base_url + pathName
// 生成保存路径
const filename = generateUniqueFilename(pathName)
const localFilePath = '_doc/avatar/' + filename
// 检查文件是否存在
const exists = await checkFileExists(localFilePath)
if (exists) {
// console.log('头像已存在,无需下载')
updateUserAvatar(localFilePath)
} else {
const savedPath = await downloadAndSaveAvatar(fileUrl, localFilePath)
updateUserAvatar(savedPath)
}
} catch (error) {
console.error('头像预加载过程发生错误:', error)
}
try {
const pathName = res.body.imageAddr
const fileUrl = base_url + pathName
// 生成保存路径
const filename = generateUniqueFilename(pathName)
const localFilePath = '_doc/avatar/' + filename
// 检查文件是否存在
const exists = await checkFileExists(localFilePath)
if (exists) {
// console.log('头像已存在,无需下载')
updateUserAvatar(localFilePath)
} else {
const savedPath = await downloadAndSaveAvatar(fileUrl, localFilePath)
updateUserAvatar(savedPath)
}
} catch (error) {
console.error('头像预加载过程发生错误:', error)
}
})()
// #endif
@@ -230,4 +231,5 @@ export const loginOut = (data) => request({
setUserInfo(null)
common.setValue('me_statistics_data', null) // 我的页统计数据缓存
common.setValue('index_statistics_data', null) // 首页统计数据缓存
common.setValue('points_data', null) // 首页统计数据缓存
})
+13 -1
View File
@@ -108,4 +108,16 @@ export const queryCurrentStatus = (data) => {
method: 'post',
data
});
};
};
/**
* 徽章墙
*/
export const queryRankWall = (data) => {
return request({
url: base_url + '/traPointsInfo/queryRankWall',
method: 'post',
data
});
};
+2 -2
View File
@@ -233,8 +233,8 @@ function setValue(key, value) {
uni.setStorageSync(key, value);
}
function getValue(key) {
return uni.getStorageSync(key)
function getValue(key, defaultValue=null) {
return uni.getStorageSync(key) ?? defaultValue
}
function isLogin() {
+1 -1
View File
@@ -19,7 +19,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed, toRaw, watch } from 'vue';
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
const emit = defineEmits(['click_option']);
+17
View File
@@ -0,0 +1,17 @@
<template>
<view class="">
<uni-popup ref="popup" border-radius="10px 10px 0 0">
</uni-popup>
</view>
</template>
<script setup>
const popup = ref(null)
const open = () => {
}
defineExpose({open})
</script>
<style></style>
@@ -0,0 +1,417 @@
<template>
<view class="">
<uni-popup ref="popup">
<view class="popup-content" @touchmove.stop>
<view class="fl1"></view>
<view class="max-div">
<view class="popup_back_div" @click="close">
<image :src="optionErrorIcon('#C8BBB7')" class="img"></image>
</view>
<view class="title_div">
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-left.png"></image>
<view class="title">每日签到</view>
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-right.png"></image>
</view>
<view class="continuous-check-in-div">
<image class="img" src="@/static/images/pointsAndRank/badge-icon.png"></image>
<text class="text">
您已连续签到
<text class="number">27</text>
</text>
</view>
<scroll-view scroll-y="true" class="scroll-Y" :show-scrollbar="false">
<view class="calendar-div">
<view class="calendar-line" v-for="(line, lineIndex) in dateGroups">
<view
class="calendar-day"
v-for="(day, dayIndex) in line.days"
:class="{ hide: day.index > 30, expand: expand }"
>
<view class="calendar-day-bg" :class="[day.type, isSigned ? 'signed' : 'no-signed']">
<image
v-if="day.type === 'past' || (day.type === 'today' && isSigned)"
:src="optionSuccessIcon('#FFFFFF')"
class="img"
></image>
<text v-else>+{{ day.text }}</text>
<view class="check-in-tip"></view>
</view>
<view class="calendar-day-title">
{{ day.date }}
</view>
</view>
</view>
</view>
</scroll-view>
<view class="switch-div" @click="switchStatus()">
<view class="back_div">
<view class="back-icon"></view>
</view>
</view>
<view class="button-div">
<uv-button
class="button"
text="签到"
:custom-style="customStyle"
:color="buttonColor"
@click="expand = !expand"
></uv-button>
</view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
const popup = ref(null);
const buttonColor = 'linear-gradient( 270deg, #E0914A 0%, #EDBE7F 100%)';
const customStyle = {
borderRadius: '40rpx' //圆角
};
const dateGroups = ref([]);
const expand = ref(true); // 展开状态
const isSigned = ref(false); // 今日签到状态
// 切换显示状态
const switchStatus = () => {
}
// 退出签到
const close = () => {
popup.value.close();
}
/**
* 生成35天日期列表(分5组,每组7天),包含类型标记和text字段
* 以"前X天"为第一天,第1天显示1,每7天显示5,第30天显示30
* @param {number} X - 前X天的天数(0-35之间的整数,总天数=35)
* @returns {Array<{
* groupIndex: number, // 组索引(0-4
* days: Array<{
* date: string, // mm-dd格式日期
* type: 'past' | 'today' | 'future', // 日期类型
* index: number, // 相对今天的索引(负数为过去,正数为未来)
* text: string // 显示文本(1/5/30
* }>
* }>} 分组后的日期列表
*/
function generateGroupedDateList(X) {
if (X < 0 || X > 35 || !Number.isInteger(X)) {
throw new Error('X必须是0到35之间的整数');
}
const allDays = [];
const today = new Date();
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
// 生成前X天到后(35-X)天的所有日期(共35天)
for (let i = -X; i <= 35 - X - 1; i++) {
const currentDate = new Date(today);
currentDate.setDate(today.getDate() + i);
const pureDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
// 确定日期类型
let type;
if (pureDate < todayDate) type = 'past';
else if (pureDate > todayDate) type = 'future';
else type = 'today';
// 格式化日期
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
// 计算当前是第几天(以"前X天"为第1天)
const dayNumber = i - -X + 1; // 核心计算:转换为从1开始的序列
// 计算text字段
let text = '1'; // 默认显示1
if (dayNumber === 30) {
text = '30'; // 第30天显示30
} else if ((dayNumber % 30) % 7 === 0) {
text = '5'; // 每7天(第7、14、21、28、35天)显示5
}
allDays.push({
date: `${month}.${day}`,
type,
index: i - -X + 1,
text,
i
});
}
// 每7天分为一组,共5组
const groupedList = [];
for (let i = 0; i < 5; i++) {
const start = i * 7;
const end = start + 7;
groupedList.push({
groupIndex: i,
days: allDays.slice(start, end)
});
}
return groupedList;
}
// 示例:X=10时,以"10天前"为第1天,生成35天列表
try {
const X = 10;
dateGroups.value = generateGroupedDateList(X);
console.log(`${X}天前为第1天的35天分组日期:`);
dateGroups.value.forEach((group) => {
console.log(`\n第${group.groupIndex + 1}组:`);
console.log(
group.days.map((day) => ({
日期: day.date,
天数序号: day.index, // 显示当前是第几天
text: day.text,
类型: day.type
}))
);
});
} catch (err) {
console.error(err.message);
}
const open = () => {
console.log('111111111');
popup.value.open();
};
defineExpose({ open, close });
</script>
<style scoped lang="scss">
// 日期
.calendar-day {
text-align: center;
// 隐藏展开时超过30天的样式
&.hide.expand {
.calendar-day-title {
width: 70rpx;
color: #fff;
}
.calendar-day-bg {
display: none;
}
}
.calendar-day-bg {
width: 70rpx;
height: 84rpx;
border-radius: 8rpx;
// 前面日期,代表已签到
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-family: Arial, Arial;
font-size: 32rpx;
.img {
width: 32rpx;
margin-bottom: 6rpx;
}
&.past {
background-color: #ffd07c;
}
// 未来日期,代表未签到
&.future {
color: #d88f50;
font-weight: bold;
background: #ffe6b8;
border: 4rpx solid #ffd58a;
}
// 当前已签到和未签到
&.signed.today {
background-color: #ffd07c;
}
&.no-signed.today {
color: #d88f50;
font-weight: bold;
background: #ffe6b8;
border: 4rpx solid #ffd58a;
}
// 当天的签到小图标
.check-in-tip {
display: none;
}
&.today > .check-in-tip {
display: block;
position: absolute;
top: 0;
right: 0;
width: 30rpx;
height: 30rpx;
font-size: 20rpx;
border-radius: 50%;
border: 2rpx solid #fff;
box-shadow: 0 0 4rpx #fef5e6;
color: #fff;
font-weight: 400;
background-color: #f8831c;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
/* 2. 解决文字被边框挤压:清除默认内边距,重置行高 */
padding: 0; /* 清除默认内边距(部分元素默认有padding) */
line-height: 1; /* 行高与字体大小一致,避免垂直方向多余空间 */
/* 3. 旋转30度(transform 旋转属性) */
transform: translate(30%, -40%) rotate(-30deg);
}
}
.calendar-day-title {
margin: 8rpx 0;
font-family: ArialMT;
font-size: 22rpx;
color: #999999;
line-height: 28rpx;
text-align: center;
font-style: normal;
}
}
.scroll-Y {
/**
* 26rpx 主盒子内间距
* 68rpx 标题高度
* 128rpx 日历高度
* 16rpx 日历下外间距
* 92rpx 下方按钮盒子高度
* 80rpx 切换盒子高度
*/
height: calc(100% - 26rpx - 68rpx - 128rpx - 16rpx - 92rpx - 80rpx - 8rpx );
}
.switch-div {
display: flex;
justify-content: center;
align-items: center;
height: 60rpx;
margin: 10rpx 30%;
.back_div {
.back-icon {
position: relative;
width: 40rpx;
height: 40rpx;
cursor: pointer;
}
.back-icon::before,
.back-icon::after {
content: '';
position: absolute;
transition: all 0.3s ease;
}
.back-icon::after {
top: 25%;
left: 25%;
width: 40%;
height: 40%;
border-top: 2rpx solid #bfbfbf;
border-left: 2rpx solid #bfbfbf;
transform: rotate(45deg);
}
}
}
.calendar-div {
display: flex;
flex-direction: column;
.calendar-line {
display: flex;
justify-content: space-between;
}
}
.button-div {
height: 92rpx;
width: 442rpx;
margin: 0 auto;
.button {
width: 100%;
height: 100%;
}
}
.continuous-check-in-div {
display: flex;
border-bottom: 2rpx solid #efefef;
margin-bottom: 16rpx;
padding-top: 8rpx;
height: 128rpx;
.img {
width: 112rpx;
height: 102rpx;
}
.text {
margin-left: 16rpx;
font-weight: 400;
font-size: 30rpx;
height: 82rpx;
color: #333333;
display: flex;
align-items: flex-end;
}
.number {
font-weight: 600;
color: #0066ff;
font-family: Arial Black;
}
}
.title_div {
display: flex;
align-items: center;
justify-content: center;
margin-top: 34rpx;
height: 68rpx;
.img {
width: 26rpx;
height: 44rpx;
}
.title {
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 46rpx;
color: #d88f50;
margin: 0rpx 30rpx;
text-align: center;
}
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
.img {
margin: 10rpx;
width: 40rpx;
height: 40rpx;
}
}
.popup-content {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
}
.max-div {
width: 80vw;
background-color: #fefbfa;
border-radius: 38rpx;
position: relative;
padding: 26rpx;
overflow: hidden;
margin-top: var(--status-bar-height);
margin-bottom: 120rpx;
}
</style>
@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>
+1 -1
View File
@@ -33,7 +33,7 @@ export const SUBJECT_TYPE = [{
}
]
export const defaultAvatar = '/static/images/me/default_user_avatar.png'
export const defaultAnonymousAvatar = '/static/images/me/default_user_avatar.png'
export const defaultAnonymousAvatar = '/static/images/competition/competition_default_avatar.png'
export const styleButton = {
style_button_1: {
color: '#FFFFFF',
+9
View File
@@ -375,6 +375,15 @@
}
}
},
{
"path": "pages/pointsAndRank/badge",
"style": {
"navigationBarTitleText": "徽章列表",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/course/index",
"style": {
@@ -1,5 +1,5 @@
<template>
<view class="item" :class="getStatusClass(item.isFinish)" @click="click_item(item)">
<view class="item" :class="getStatusClass(item)" @click="click_item(item)">
<view class="img_div">
<image-preview class="img" :src="item.ossAddr"></image-preview>
<view class="subscript" v-if="item.isFinish === '02'">已完成</view>
@@ -10,15 +10,19 @@
{{ item.taskName }}
</view>
<view class="time_div" v-if="item.limitTyp === '02'">
起止时间<text class="blod_color">{{ item.startTm?.substr(0, 10) }}{{ item.endTm?.substr(0, 10) }}</text>
起止时间
<text class="blod_color">{{ item.startTm?.substr(0, 10) }}{{ item.endTm?.substr(0, 10) }}</text>
</view>
<view class="people_progress_div">
<view class="mr-10">
完成人数<text class="">{{ item.taskFinishSums ?? 0 }}/{{ item.taskSums ?? 0 }}</text>
完成人数
<text class="">{{ item.taskFinishSums ?? 0 }}/{{ item.taskSums ?? 0 }}</text>
</view>
<view>
进度
<text class="">{{ item.finishCrs ?? item.learnedNumber ?? 0 }}/{{ item.sumCrs ?? item.taskNumber ?? 0 }}门课程</text>
<text class="">
{{ item.finishCrs ?? item.learnedNumber ?? 0 }}/{{ item.sumCrs ?? item.taskNumber ?? 0 }}门课程
</text>
</view>
</view>
</view>
@@ -37,26 +41,27 @@
});
const emit = defineEmits(['click_item']);
const item = computed(() => props.item);
const getStatusClass = (isFinish) => {
const getStatusClass = (item) => {
// 01进行中、02已完成、03已超时
// if (isFinish === '00') return 'in_progress';
// if (isFinish === '01') return 'completed';
// return 'expired';
if (isFinish === '02') return 'completed';
if (item.isFinish === '02') return 'completed';
if (item.isTimeOut === '02') return 'expired';
return 'expired';
};
// 跳转到详情
const click_item = (item) => {
if(item.limitTyp === '02') {
if(item.isTimeOut === '01') {
if (item.limitTyp === '02') {
if (item.isTimeOut === '01') {
return common.msg('任务未开始');
} else if(item.isTimeOut === '02') {
} else if (item.isTimeOut === '02') {
return common.msg('任务已结束');
}
}
setPageCache('learning_tasks_details', item);
common.navigateTo('/pages/learningTasks/details?taskId='+item.taskId);
common.navigateTo('/pages/learningTasks/details?taskId=' + item.taskId);
};
</script>
@@ -67,7 +72,6 @@
color: #0066ff;
background-color: #daf0ff;
}
.blod_color {
color: #0066ff;
}
@@ -89,11 +93,11 @@
// 已超时
.subscript {
color: #ffffff;
background-color: #ef5705;
background-color: #999 !important;
}
.blod_color {
color: #d70c18;
color: #333333;
}
}
.item {
+134 -44
View File
@@ -32,37 +32,47 @@
src="/static/images/me/man.png"
></image>
</view>
<!-- 显示当前选中的徽章 -->
<view class="dan" @click="common.navigateTo('/pages/pointsAndRank/rank')">
<view class="dan_img">
<image
class="img_100"
:src="`/static/images/me/${badgeList[currentIndex].png}`"
mode="heightFix"
alt="当前徽章"
></image>
</view>
</view>
<view class="fl1"></view>
</view>
<view class="work_number_div">工号{{ user_info.loginName }}</view>
<view class="fl1"></view>
</view>
<!-- 显示当前选中的徽章 -->
<view class="dan" @click="changeBadge">
<view class="dan_img">
<image
class="img_100"
:src="`/static/images/me/${badgeList[currentIndex].png}`"
mode="heightFix"
alt="当前徽章"
></image>
</view>
<view class="dan_text">{{ badgeList[currentIndex].name }}</view>
</view>
<view class="sign-in-div" @click="checkInFun">签到</view>
</view>
<!-- 用户学习时间信息 -->
<view class="study_information">
<view class="item" @click="common.navigateTo('/pages/pointsAndRank/points')">
<view class="top">我的积分</view>
<view class="bottom">
<uv-count-to :startVal="0" :endVal="0" :useEasing="true" ref="accmTaskCntRef"></uv-count-to>
<uv-count-to
:startVal="statistics_data['startCurrentPoints']"
:endVal="statistics_data['currentPoints']"
:useEasing="true"
ref="currentPointsRef"
></uv-count-to>
</view>
</view>
<view class="item-interval"></view>
<view class="item" @click="common.navigateTo('/pages/pointsAndRank/rank')">
<view class="item" @click="common.navigateTo('/pages/pointsAndRank/badge')">
<view class="top">徽章墙</view>
<view class="bottom">
<uv-count-to :startVal="0" :endVal="0" :useEasing="true" ref="accmTaskCntRef"></uv-count-to>
<uv-count-to
:startVal="statistics_data['startBadgeNum']"
:endVal="statistics_data['badgeNum']"
:useEasing="true"
ref="badgeNumRef"
></uv-count-to>
</view>
</view>
</view>
@@ -81,13 +91,13 @@
<view class="scroll-view-item" @click="common.navigateTo('/pages/examinationRecord/index')">
<view class="item">
<image class="icon" src="@/static/images/me/report.png"></image>
<image class="icon" src="@/static/images/me/todo.png"></image>
<view class="title">考试记录</view>
</view>
</view>
<view class="scroll-view-item">
<view class="item">
<image class="icon" src="@/static/images/me/todo.png"></image>
<image class="icon" src="@/static/images/me/report.png"></image>
<view class="title">AI陪练记录</view>
</view>
</view>
@@ -146,12 +156,13 @@
@cancel="avatarOnCancel"
@confirm="avatarOnConfirm"
></avatar-cropper>
<checkIn ref="checkInRef"></checkIn>
</view>
</template>
<script setup>
import { ref, computed, reactive, nextTick } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import common from '@/common/common';
import { getUserInfo } from '@/common/common';
import { onPullDownRefresh } from '@dcloudio/uni-app';
@@ -164,12 +175,15 @@
import { queryCurrentUser } from '@/api/login.js';
import { defaultAvatar } from '@/enum.js';
import useUpdateAvatar from '@/composables/useUpdateAvatar';
import { queryCurrentStatus } from '@/api/pointsAndRank.js';
import checkIn from '@/components/points-and-badge/check-in';
const { avatarCropperUrl, click_update_avatar, avatarCropperStatus, avatarOnCancel, avatarOnConfirm } = useUpdateAvatar({
success: (userData) => {
user_info.imageAddr = userData.imageAddr;
user_info.imagePath = '';
}
});
const badgeList = [
{
name: '黑铁',
@@ -201,14 +215,12 @@
}
];
const currentIndex = ref(0);
const changeBadge = () => {
// 计算下一个索引,如果当前是最后一个则回到0
currentIndex.value = (currentIndex.value + 1) % badgeList.length;
};
const socketStore = useSocketStore();
const accmTaskCntRef = ref(null);
const accmPointRef = ref(null);
const stdtTmLenRef = ref(null);
const badgeNumRef = ref(null);
const checkInRef = ref(null);
const currentPointsRef = ref(null);
const user_info = reactive({
userName: '',
@@ -220,7 +232,9 @@
console.log('goTest');
// common.navigateTo('/pages/test/index');
};
const checkInFun = () => {
checkInRef.value.open();
};
const click_loginout = () => {
common.show('确定退出登录?').then(async (res) => {
if (!res) return;
@@ -234,9 +248,80 @@
common.reLaunch('/pages/login/login');
});
};
const statistics_data = reactive({
badgeNum: 0, // 徽章数
currentPoints: 0, // 当前积分
currentRankAddr: '', // 当前段位名称
currentRankName: '', // 当前段位名称
startBadgeNum: 0, // 起始 徽章数
startCurrentPoints: 0, // 起始 积分
request_time: 0, // 上次请求时间
init: function () {
// 初始化统计数据
const points_data = common.getValue('points_data');
if (points_data) {
this.badgeNum = points_data.badgeNum; // 徽章数
this.currentPoints = points_data.currentPoints; // 当前积分
this.currentRankAddr = points_data.currentRankAddr;
this.currentRankName = points_data.currentRankName;
}
},
get_statistics_data: async function () {
try {
const time_now = Date.now();
let points_data = common.getValue('points_data') || {};
if (time_now - this.request_time > 300) {
this.request_time = time_now;
// 获取最新数据
const res = await queryCurrentStatus();
if (res && res.body) {
// 合并新数据与缓存数据,确保只更新有效字段
points_data = {
badgeNum: res.body['badgeNum'], // 徽章数
currentPoints: res.body['currentPoints'], // 当前积分
currentRankAddr: res.body['ossAddr'], // 当前段位图片
currentRankName: res.body['currentRankName'] // 当前段位名称
};
common.setValue('points_data', points_data);
this.currentRankAddr = points_data.currentRankAddr;
this.currentRankName = points_data.currentRankName;
}
}
// 解构赋值获取最新值
const { badgeNum = 0, currentPoints = 0 } = points_data;
// 更新数据并触发动画
if (badgeNum !== this.badgeNum) {
this.startBadgeNum = this.badgeNum;
this.badgeNum = badgeNum;
badgeNumRef.value.start();
}
if (currentPoints !== this.currentPoints) {
this.startCurrentPoints = this.currentPoints;
this.currentPoints = currentPoints;
console.log('this.startCurrentPointsx', this.startCurrentPoints, this.currentPoints);
currentPointsRef.value.start();
}
} catch (error) {
console.error('获取统计数据失败:', error);
// 可以在这里添加更多的错误处理逻辑,比如显示错误提示
}
}
});
onLoad(() => {
statistics_data['init'](); // 初始化数据
});
onShow(() => {
setMascot();
Object.assign(user_info, getUserInfo());
statistics_data['get_statistics_data'](); // 获取统计数据
});
onHide(() => {
checkInRef.value && checkInRef.value.close();
});
onPullDownRefresh(() => {
uni.stopPullDownRefresh();
@@ -251,7 +336,7 @@
margin: 30rpx $bg-margin-left 0 $bg-margin-left;
background: #ffffff;
border-radius: 15rpx;
z-index: 101;
z-index: 90;
overflow: hidden;
// 为了让线缩进来24单位
@@ -268,15 +353,12 @@
// 快捷导航栏
.navigation_div {
margin: 30rpx $bg-margin-left 0 $bg-margin-left;
// padding: 24rpx 0 0 20rpx;
padding: 24rpx 10rpx;
background-color: #ffffff;
border-radius: 23rpx;
display: flex;
// flex-direction: column;
justify-content: space-around;
z-index: 101;
z-index: 90;
.scroll-view {
white-space: nowrap;
width: 100%;
@@ -375,6 +457,14 @@
margin-left: 10rpx;
width: 30rpx;
height: 30rpx;
flex-shrink: 0;
}
.dan {
font-size: 16rpx;
padding-left: 20rpx;
.dan_img {
height: 46rpx;
}
}
}
@@ -390,19 +480,19 @@
text-overflow: ellipsis;
}
}
}
.dan {
display: flex;
flex-direction: column;
font-size: 16rpx;
align-items: center;
.dan_img {
height: 50rpx;
}
.dan_text {
.sign-in-div {
margin-top: 10rpx;
width: 102rpx;
height: 48rpx;
font-weight: 500;
font-size: 26rpx;
line-height: 46rpx;
text-align: center;
color: #ffffff;
background-image: url('@/static/images/me/sign-in-button-bg.png');
background-size: 100%; /* 图片自适应元素大小(避免拉伸变形) */
background-repeat: no-repeat; /* 禁止重复平铺 */
background-position: center; /* 图片居中显示 */
}
}
+289
View File
@@ -0,0 +1,289 @@
<template>
<z-paging
ref="pagingRef"
v-model="data_list"
@query="queryList"
class="scroll_div"
:loading-more-enabled="false"
:layout-only="true"
empty-view-text="暂无积分记录"
paging-class="z-paging-class"
>
<template #top>
<view class="body-title">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view>我的徽章</view>
</view>
<view class="personal_information">
<view class="profile">
<cached-avatar class="img"></cached-avatar>
</view>
<view class="personal_information_data">
<view class="name_sex_div">
<view class="name">
{{ user_info.userName }}
</view>
<view class="fl1"></view>
</view>
<view class="work_number_div">工号{{ user_info.loginName }}</view>
<view class="fl1"></view>
</view>
</view>
</template>
<view class="main-div">
<view class="item-array" v-for="(itemList, index) in data_list">
<view class="title-div">
<image class="img" src="@/static/images/pointsAndRank/badge-title-left.png" mode=""></image>
<view class="title">
{{ itemList.name }}
</view>
<image class="img" src="@/static/images/pointsAndRank/badge-title-right.png" mode=""></image>
</view>
<view class="item-list-container">
<view class="item" v-for="item in itemList.child">
<view class="badge-img">
<image-preview
class="img"
:src="item.isOpen === 'Y' ? item.ossAddr : item.ossAddrOff"
></image-preview>
</view>
<view class="badge-img-title light-up">
{{ item.badgeName }}
</view>
</view>
</view>
</view>
</view>
<!-- </view> -->
</z-paging>
</template>
<script setup>
// 竞赛列表
import { ref, reactive, watch, onMounted } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
import common from '@/common/common';
import useZpaging from '@/composables/useZpaging.js';
import { queryRankWall } from '@/api/pointsAndRank.js';
import { getUserInfo } from '@/common/common';
const user_info = reactive({
userName: '',
userId: ''
});
const fetchFunction = (params) => {
return new Promise((resolve, reject) => {
queryRankWall().then(({ body }) => {
console.log('body', body);
let originalData = body;
// 处理逻辑:构建二维数组(外层分类列表 + 内层徽章列表)
const twoDimensionalArray = [];
// 遍历每个分类,生成二维数组的每一项
Object.entries(originalData).forEach(([categoryKey, badgeArray]) => {
// 构建外层列表项:包含 name 和子列表 badgeList
const categoryItem = {
name: categoryKey, // 第一维度的 name 是原始键
child: badgeArray // 子列表:直接放入当前分类的所有徽章数据
};
// 将分类项加入二维数组
twoDimensionalArray.push(categoryItem);
});
console.log('resultList', twoDimensionalArray);
resolve({
body: twoDimensionalArray
});
});
});
};
const { pagingRef, queryList, data_list, search } = useZpaging({
fetchFunction
});
onMounted(() => {
Object.assign(user_info, getUserInfo());
pagingRef.value.reload();
});
// onUnload(() => {});
</script>
<style lang="scss" scoped>
.personal_information {
padding: 40rpx 26rpx 0 6vw;
display: flex;
// 个人头像框框
.profile {
border-radius: 100%;
// border: 2px solid #ffffff;
width: 108rpx;
height: 108rpx;
overflow: hidden;
border: 2rpx solid #b39e81;
}
// 头像右边的信息
.personal_information_data {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 26rpx;
overflow: hidden;
// 上边的姓名性别盒子
.name_sex_div {
display: flex;
margin-top: 6rpx;
justify-content: space-between;
.name {
font-family: PingFangSC;
font-weight: 600;
font-size: 28rpx;
color: #000000;
line-height: 40rpx;
}
}
.work_number_div {
font-family: PingFangSC;
font-weight: 400;
font-size: 25rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
font-style: normal;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.title-div {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
.title {
margin: 0 16rpx;
font-weight: 600;
font-size: 34rpx;
color: #ae723b;
text-align: center;
font-style: normal;
}
.img {
width: 242rpx;
height: 40rpx;
}
}
.item-list-container {
display: flex;
flex-wrap: wrap;
justify-content: flex-start; /* 改为居中对齐 */
gap: 40rpx; /* 调大间距(根据需要调整数值) */
padding: 6vw;
box-sizing: border-box;
.item {
/* 计算宽度:总宽度减去两倍间距后三等分(保证三列) */
width: calc((100% - 80rpx) / 3); /* 间距40rpx时,左右两边间距总和为80rpx */
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
.badge-img {
width: calc(27vw - 14rpx); /* 基于父元素宽度计算,更适配 */
height: calc(27vw - 14rpx);
.img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.badge-img-title {
margin-top: 10rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
font-size: 30rpx;
color: #ae723b;
line-height: 30px;
text-align: center;
font-style: normal;
}
.light-up {
color: #8f4628;
}
}
}
.body-title {
width: 100%;
height: 70rpx;
font-size: 33rpx;
font-weight: 500;
color: #000;
position: relative;
text-align: center;
margin-top: var(--status-bar-height);
.delete_div {
position: absolute;
right: 20rpx;
top: 17rpx;
width: 50rpx;
height: 50rpx;
.img {
width: 34rpx;
height: 34rpx;
}
}
.back_div {
position: absolute;
left: 26rpx;
top: 17rpx;
.back-icon {
position: relative;
width: 50rpx;
height: 50rpx;
cursor: pointer;
display: flex;
align-items: center;
}
.back-icon::before,
.back-icon::after {
content: '';
position: absolute;
transition: all 0.3s ease;
}
.back-icon::after {
top: 25%;
left: 25%;
width: 40%;
height: 40%;
border-top: 4rpx solid #000;
border-left: 4rpx solid #000;
transform: rotate(-45deg);
}
}
}
.z-paging-class {
min-height: 100vh;
background-image: url('@/static/images/pointsAndRank/badge-bg.png');
background-color: #000; /* 图片高度不足时的背景色 */
background-size: 100% auto; /* 宽度填满,高度自适应 */
background-position: center top; /* 图片顶部居中显示 */
background-repeat: no-repeat; /* 不重复平铺 */
background-color: #fef1dd;
}
</style>
+13 -8
View File
@@ -59,16 +59,17 @@
import useZpaging from '@/composables/useZpaging.js';
import common from '@/common/common';
import { queryPointsDetail, queryCurrentStatus } from '@/api/pointsAndRank.js';
const startVal = ref(0);
const endVal = ref(0);
const countToRef = ref(null);
const fetchFunction = (params) => {
queryCurrentStatus().then(({ body }) => {
const { currentPoints } = body;
console.log('recurrentPointss', currentPoints);
startVal.value = 9999;
endVal.value = 99999;
stdtTmLenRef.value.start();
if (currentPoints !== endVal.value) {
endVal.value = currentPoints;
countToRef.value.start();
}
});
return queryPointsDetail({ ...params });
};
@@ -76,13 +77,17 @@
fetchFunction
});
onLoad(() => {});
onLoad(() => {
const points_data = common.getValue('points_data');
if (points_data) {
startVal.value = points_data.currentPoints; // 徽章数
endVal.value = points_data.currentPoints; // 徽章数
}
});
onUnload(() => {});
</script>
<style lang="scss" scoped>
.z-paging-class {
background-color: #e0e3ef;
::v-deep .zp-view-super {
@@ -230,7 +235,7 @@
height: 34rpx;
}
}
.back_div {
position: absolute;
left: 26rpx;
+9
View File
@@ -310,6 +310,15 @@
"titleNView": false
}
}
},
{
"path": "pages/pointsAndRank/badge",
"style": {
"navigationBarTitleText": "徽章墙",
"app-plus": {
"titleNView": false
}
}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB