三期pk功能开发以及积分两个页

This commit is contained in:
田岩
2025-10-24 14:53:19 +08:00
parent 7bffe0c9d7
commit 7107dade1a
70 changed files with 4521 additions and 2204 deletions
+12 -12
View File
@@ -2,32 +2,32 @@
ENV = 'development'
# 'development'
#VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
# VITE_APP_BASE_API_Url = ' http://25.18.122.66:9786'
#sit
# 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 = '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'
# dev
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# sit
# VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
# UAT
# VITE_APP_BASE_API_Url = 'http://25.18.122.78:9786'
# DEV
# app入口
# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.16.122.91:9786'
# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
# h5专用地址x2
VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
# 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://25.64.32.154:9601'
# 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://25.64.32.154:9604'
# 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'
+7
View File
@@ -0,0 +1,7 @@
# 任东专用开发环境
ENV = 'development.rd'
# 'development.rd'
VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604'
+9
View File
@@ -0,0 +1,9 @@
# 田岩开发环境
ENV = 'development.ty'
# 'development.ty'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://192.168.108.129'
+10
View File
@@ -0,0 +1,10 @@
# 杨航开发环境
ENV = 'development.yh'
# 'development.yh'
VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604'
BIN
View File
Binary file not shown.
+3
View File
@@ -4,6 +4,9 @@
"description": "",
"scripts": {
"dev:h5": "uni",
"dev:rd": "uni -p h5 --mode development.rd",
"dev:yh": "uni -p h5 --mode development.yh",
"dev:ty": "uni -p h5 --mode development.ty",
"dev:h5:sit": "uni -p h5 --mode sit",
"dev:h5:uat": "uni -p h5 --mode uat",
"dev:h5:production": "uni -p h5 --mode production",
-16
View File
@@ -9,23 +9,7 @@
});
},
onShow: function () {
// function getBrowserInfo() {
// const userAgent = navigator.userAgent;
// let browserName = '未知浏览器';
// let version = '未知版本';
// // 检测 Chrome
// if (userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Edg') === -1) {
// browserName = 'Chrome';
// version = userAgent.match(/Chrome\/(\d+\.\d+)/)[1];
// }
// return {
// browser: browserName,
// version: version,
// userAgent: userAgent
// };
// }
},
onHide: function () {
}
+85
View File
@@ -0,0 +1,85 @@
import request from '@/api/request'
const module_url = '/traexam'
// 查询竞赛列表
export const queryTraCompetitionInfoPaging = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryTraCompetitionInfoPaging',
method: 'post',
data
});
};
// 查询试卷
export const queryTestPapers = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryTestPapers',
method: 'post',
data
});
};
// 查询PK人员+试题
export const queryTraCompetitionInfoByPkUser = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryTraCompetitionInfoByPkUser',
method: 'post',
data
});
};
// 循环获取简答题结果
export const queryCompetitionPracticeAnswerResult = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryCompetitionPracticeAnswerResult',
method: 'post',
data
});
};
// 答题结束
export const competitionAnswerEnd = (data) => {
return request({
url: module_url + '/traCompetitionInfo/competitionAnswerEnd',
method: 'post',
data,
suppressErrors: true // 屏蔽报错信息
});
};
// 竞赛答题
export const competitionAnswer = (data) => {
return request({
url: module_url + '/traCompetitionInfo/competitionAnswer',
method: 'post',
data
});
};
// 查询答题结果
export const queryCompetitionAnswerResult = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryCompetitionAnswerResult',
method: 'post',
data
});
};
// 竞赛排行榜
export const queryTraCompetitionRankingList = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryTraCompetitionRankingList',
method: 'post',
data
});
};
// 全行的机构数据
export const queryOrgTree = (data) => {
return request({
url: module_url + '/traCompetitionInfo/queryOrgTree',
method: 'post',
data
});
};
+111
View File
@@ -0,0 +1,111 @@
import request from '@/api/request'
const base_url = '/traapp'
/**
* 查询签到
* userId 学员ID String
* checkInToday 今日是否签到 String
* days 连续签到天数 int
*/
export const queryCheckIn = (data) => {
return request({
url: base_url + '/traPointsInfo/queryCheckIn',
method: 'post',
data
});
};
/**
* 点击签到
*/
export const addPointsByCheckIn = (data) => {
return request({
url: base_url + '/traPointsInfo/addPointsByCheckIn',
method: 'post',
data
});
};
/**
* 每次完成任务获得弹窗奖励
* 入参:
taskId 必输项:true 类型:String
type 必输项:true 类型:String
出参:
userId 学员ID String
badgeName 徽章名称 String
badgeImg 徽章图片 String
num 次数 int
name 描述 String
pointsBadgeName 积分徽章名称 String
pointsBadgeImg 徽章图片 String
currentPoints 当前积分 int
changePoints 本次获得积分 int
*/
export const addPointsByTask = (data) => {
return request({
url: base_url + '/traPointsInfo/addPointsByTask',
method: 'post',
data
});
};
/**
* 查询积分明细
* 出参:
detailId 明细ID String
userId 学员ID String
mattr 积分来源事项 String
mattrDesc 积分来源描述 String
changeValue 积分变化值 String
currentPoints 当前积分 int
countPoints 累计积分 int
ctTime 创建时间 String
*/
export const queryPointsDetail = (data) => {
return request({
url: base_url + '/traPointsInfo/queryPointsDetail',
method: 'post',
data
});
};
/**
* 查询段位明细
* 出参:
rankId 段位ID String
rankName 段位名称 String
currentRankName 当前段位名称 String
ossAddr 段位图片 String
pointsStart 段位开始区间 int
pointsEnd 段位结束区间 int
countPoints 累计积分 int
*/
export const queryRankDetail = (data) => {
return request({
url: base_url + '/traPointsInfo/queryRankDetail',
method: 'post',
data
});
};
/**
* 查询当前状态
* 出参:
userId 学员ID String
orgId 机构ID String
countPoints 累计积分 int
currentPoints 当前积分 int
currentRankName 当前段位 String
badgeNum 徽章数 int
*/
export const queryCurrentStatus = (data) => {
return request({
url: base_url + '/traPointsInfo/queryCurrentStatus',
method: 'post',
data
});
};
+20 -2
View File
@@ -4,9 +4,27 @@ import {
import common from '@/common/common.js'
const ENV = import.meta.env
// function getBrowserInfo() {
// const userAgent = navigator.userAgent;
// let browserName = '未知浏览器';
// let version = '未知版本';
// // 检测 Chrome
// if (userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Edg') === -1) {
// browserName = 'Chrome';
// version = userAgent.match(/Chrome\/(\d+\.\d+)/)[1];
// }
// return {
// browser: browserName,
// version: version,
// userAgent: userAgent
// };
// }
export const get_base_url = (url = '') => {
if (ENV.MODE === 'development') { // 开发环境
if (['development', 'development.rd', 'development.yh', 'development.ty'].includes(ENV.MODE)) { // 开发环境
// #ifdef H5
if (url) {
let _str = (url.split('/')[0] || url.split('/')[1])?.toUpperCase()
@@ -20,7 +38,7 @@ export const get_base_url = (url = '') => {
return ENV.VITE_APP_BASE_API_Url
// #endif
} else { // 其他环境,包括生产环境,sit环境,uat环境
return ENV.VITE_APP_BASE_API_Url
// return ENV.VITE_APP_BASE_API_Url
+19 -1
View File
@@ -36,4 +36,22 @@ export const updateDfSysUserExtandInfoGender = (data) => {
data
});
};
// 设置匿名 是Y 否N
export const updateTraUserAnony = (data) => {
return request({
url: '/traapp/traUserAnony/updateTraUserAnony',
method: 'post',
data
});
};
// 查询是否匿名 是Y 否N
export const queryTraUserAnonyByUserId = (data) => {
return request({
url: '/traapp/traUserAnony/queryTraUserAnonyByUserId',
method: 'post',
data
});
};
+55 -5
View File
@@ -5,7 +5,7 @@ export const optionSuccessIcon = (color : string) : string => {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
export const optionErrorIcon = (color: string='#F5212D') : string => {
export const optionErrorIcon = (color : string = '#F5212D') : string => {
const svgXml = `
<svg t="1754619171929" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6505" width="64" height="64"><path d="M910.336 186.368l-72.704-72.704L512 439.808 186.368 113.664 113.664 186.368 439.808 512l-326.144 325.632 72.704 72.704L512 584.192l325.632 326.144 72.704-72.704L584.192 512l326.144-325.632z" fill="${color}" p-id="6506"></path></svg>
`.trim();
@@ -19,7 +19,7 @@ export const examinationSheetIcon = () : string => {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
export const markIcon = (color: string='#ABABAB') : string => {
export const markIcon = (color : string = '#ABABAB') : string => {
const svgXml = `
<svg t="1754987745092" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13983" width="64" height="64"><path d="M693.01933598 59.45166004c-24.99339629-24.99339629-65.5162717-24.99339629-90.50966799 0-24.99339629 24.99339629-24.99339629 65.5162717 0 90.50966799l22.627417 22.627417-181.19328425 181.19328425c-78.50370195-71.13564929-199.8637317-68.83684515-275.5976964 6.89711955l-33.94112549 33.94112549a8 8 0 0 0 0 11.3137085l208.59650045 208.59650045-175.36248174 175.36248174a8 8 0 0 0 0 11.3137085l56.5685425 56.56854249a8 8 0 0 0 11.3137085 0L410.88373029 682.41273427l208.59650045 208.59650045a8 8 0 0 0 11.3137085 0l33.9411255-33.9411255c76.21055466-76.21055466 78.05893179-198.62346641 5.54583848-277.07484246L851.41125497 398.86291501l22.627417 22.627417c24.99339629 24.99339629 65.5162717 24.99339629 90.50966799 0 24.99339629-24.99339629 24.99339629-65.5162717 0-90.50966799L693.01933598 59.45166004z" fill="${color}" p-id="13984"></path></svg>
`.trim();
@@ -34,7 +34,7 @@ export const feedbackIcon = () : string => {
};
// 通过考试
export const passTheExamIcon = (color: string) : string => {
export const passTheExamIcon = (color : string) : string => {
const svgXml = `
<svg t="1755670289043" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6974" width="64" height="64"><path d="M97.878 483.853c0-117.062-0.108-160.999-0.108-278.062 0-101.472 56.328-108.082 110.483-108.082h441.933c85.128 0 111.453 28.968 111.453 110.481v55.241c0 43.562-55.419 38.57-55.419 0V208.19c0-40.918-13.309-55.24-56.035-55.24H208.252c-45.746 0-55.241 6.203-55.241 55.24v552.407c0 30.278 16.572 55.241 55.241 55.241 67.004 0 43.482 0.026 110.484 0 33.77 0 36.252 55.241 0 55.241s-63.711 0.44-131.306 0.17c-39.867-0.163-72.883-23.774-84.643-60.322-3.292-10.223-4.694-21.525-4.694-32.34-0.323-116.658-0.215-178.076-0.215-294.734" fill="${color}" p-id="6975"></path><path d="M649.644 926.291c-152.344-0.027-275.666-123.537-275.666-276.122 0-152.586 123.915-276.259 276.529-276.042 152.616 0.215 275.884 124.022 275.722 276.878-0.16 152.236-123.807 275.314-276.585 275.286m0.756-496.868c-121.109-0.271-220.641 98.828-220.91 220.045-0.27 121.541 98.345 220.934 219.561 221.287 121.704 0.324 221.505-98.208 221.775-219.047 0.324-122.594-98.236-221.99-220.426-222.285m-55.455-166.362c44.02 0.027 37.978 55.979 0 55.979h-331.45c-41.431 0-39.704-55.979 0-55.979 108.756-0.027 289.155-0.027 331.45 0zM318.736 428.782c44.021 0.028 37.979 55.98 0 55.98h-55.242c-41.431 0-39.704-55.98 0-55.98 108.757-0.027 12.948-0.027 55.242 0z" fill="${color}" p-id="6976"></path><path d="M785.502 547.22c-12.496-12.498-32.758-12.498-45.254 0L602.151 685.316l-42.191-42.191c-12.498-12.496-32.758-12.496-45.256 0-12.496 12.497-12.496 32.759 0 45.256l64.818 64.818c6.249 6.248 14.438 9.372 22.628 9.372s16.379-3.124 22.628-9.372a32.424 32.424 0 0 0 2.402-2.695 32.304 32.304 0 0 0 2.689-2.397l155.633-155.633c12.497-12.496 12.497-32.758 0-45.254z" fill="${color}" p-id="6977"></path></svg>
`.trim();
@@ -42,7 +42,7 @@ export const passTheExamIcon = (color: string) : string => {
};
// 完成练习
export const completeTheExercisesIcon = (color: string) : string => {
export const completeTheExercisesIcon = (color : string) : string => {
const svgXml = `
<svg t="1755670357132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7127" width="64" height="64"><path d="M832 704a42.666667 42.666667 0 0 0-42.666667 42.666667v106.666666H170.666667V170.666667h618.666666v226.133333a42.666667 42.666667 0 0 0 85.333334 0V166.4A83.2 83.2 0 0 0 789.333333 85.333333H170.666667a83.2 83.2 0 0 0-85.333334 81.066667v691.2A83.413333 83.413333 0 0 0 170.666667 938.666667h618.666666a83.413333 83.413333 0 0 0 85.333334-81.066667V746.666667a42.666667 42.666667 0 0 0-42.666667-42.666667z" fill="${color}" p-id="7128"></path><path d="M682.666667 341.333333a42.666667 42.666667 0 0 0-42.666667-42.666666H298.666667a42.666667 42.666667 0 0 0 0 85.333333h341.333333a42.666667 42.666667 0 0 0 42.666667-42.666667zM298.666667 512a42.666667 42.666667 0 0 0 0 85.333333h213.333333a42.666667 42.666667 0 0 0 0-85.333333zM921.386667 491.733333a42.666667 42.666667 0 0 0-60.373334 0L649.813333 704l-60.16-60.373333A42.666667 42.666667 0 0 0 529.28 704l90.453333 90.453333a42.666667 42.666667 0 0 0 60.373334 0l241.28-242.346666a42.666667 42.666667 0 0 0 0-60.373334z" fill="${color}" p-id="7129"></path></svg>
`.trim();
@@ -50,7 +50,7 @@ export const completeTheExercisesIcon = (color: string) : string => {
};
// 锁定
export const lockIcon = (color: string) : string => {
export const lockIcon = (color : string) : string => {
const svgXml = `
<svg t="1755683025230" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6630" width="64" height="64"><path d="M787.136 395.466H373.113c0.036-0.579 0.173-1.128 0.173-1.716V267.63c0-81.153 72.891-147.174 162.483-147.174 86.561 0 162.488 64.414 162.488 137.851 0 15.446 12.523 27.969 27.969 27.969 15.446 0 27.969-12.523 27.969-27.969 0-105.043-100.027-193.789-218.426-193.789-120.439 0-218.422 91.118-218.422 203.112v126.12c0 0.588 0.137 1.138 0.173 1.716h-14.467c-82.25 0-149.168 66.918-149.168 149.168v284.402c0 71.971 58.551 130.522 130.522 130.522h502.728c71.971 0 130.522-58.551 130.522-130.522V525.988c0.001-71.971-58.55-130.522-130.521-130.522z m74.584 433.57c0 41.125-33.459 74.584-74.584 74.584H284.408c-41.125 0-74.584-33.459-74.584-74.584V544.634c0-51.409 41.822-93.23 93.23-93.23h484.082c41.125 0 74.584 33.459 74.584 74.584v303.048z" fill="${color}" p-id="6631"></path><path d="M613.108 649.541H458.436c-15.446 0-27.969 12.523-27.969 27.969 0 15.446 12.523 27.969 27.969 27.969h154.672c15.446 0 27.969-12.523 27.969-27.969 0-15.446-12.523-27.969-27.969-27.969z" fill="${color}" p-id="6632"></path></svg>
`.trim();
@@ -66,3 +66,53 @@ export const practiceExitIcon = () : string => {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// Pk页规则图标
export const pkRules = () : string => {
const svgXml = `
<svg t="1760676829470" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7500" width="64" height="64"><path d="M512 69.963c248.05 0 445.217 197.167 445.217 445.217S760.05 960.398 512 960.398 66.783 763.23 66.783 515.18 263.95 69.963 512 69.963m0-63.603C232.15 6.36 3.18 235.33 3.18 515.18 3.18 795.031 232.15 1024 512 1024s508.82-228.969 508.82-508.82C1020.82 235.33 791.85 6.36 512 6.36z" fill="#ffffff" p-id="7501"></path><path d="M512 432.497c-38.161 0-63.602 25.44-63.602 57.242v273.49c0 31.802 25.44 57.243 63.602 57.243 38.161 0 63.602-25.44 63.602-57.242V489.74c0-31.802-25.44-57.243-63.602-57.243z m0-95.404c38.161 0 63.602-25.44 63.602-63.602 0-38.162-25.44-63.603-63.602-63.603-38.161 0-63.602 25.441-63.602 63.603 0 38.161 25.44 63.602 63.602 63.602z" fill="#ffffff" p-id="7502"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// Pk页排行榜图标
export const pkRankingList = () : string => {
const svgXml = `
<svg t="1760676897154" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7659" width="64" height="64"><path d="M323.9 326.9H215.8c-14.9 0-27 12.1-27 27v459.2c0 14.9 12.1 27 27 27h108.1c14.9 0 27-12.1 27-27V353.9c0-14.9-12.1-27-27-27zM567 488.9H459c-14.9 0-27 12.1-27 27v297.2c0 14.9 12.1 27 27 27h108c14.9 0 27-12.1 27-27V515.9c0-14.9-12-27-27-27zM810.2 191.8H702.1c-14.9 0-27 12.1-27 27v594.3c0 14.9 12.1 27 27 27h108.1c14.9 0 27-12.1 27-27V218.8c0-14.9-12.1-27-27-27z" p-id="7660" fill="#ffffff"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// Pk页匿名关
export const pkAnonymousClose = () : string => {
const svgXml = `
<svg t="1760677024029" class="icon" viewBox="0 0 1292 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8345" width="64" height="64"><path d="M1188.53945615 539.42278643a46.73094873 46.73094873 0 0 0 0-55.20152813C979.91575068 197.74436416 724.94179737 84.96193203 470.63406904 166.33659834a486.48725859 486.48725859 0 0 0-86.32376894 35.69064258L256.91849844 75.92030263a48.25374961 48.25374961 0 0 0-67.81222178 0 47.06406123 47.06406123 0 0 0 0 67.09840841L300.03279541 252.47001641c-17.22668379 12.37275615-34.31060508 24.7455123-51.15658887 39.02177021A1053.25466924 1053.25466924 0 0 0 71.1843919 484.2212583a46.68336123 46.68336123 0 0 0 0 55.20152813c163.55832011 224.61311338 356.90643018 343.58192315 558.67753212 343.58192402a542.49777422 542.49777422 0 0 0 245.64679893-60.91203076l127.10627666 125.63106328a47.68269873 47.68269873 0 0 0 67.81222177-67.09840928l-110.40305536-108.97542949a992.43781348 992.43781348 0 0 0 228.51529013-232.22711777z m-558.48718212 248.40687568c-161.79758174 0-320.21644922-95.17504834-460.07618204-276.00763974a952.03600576 952.03600576 0 0 1 140.66872119-147.99719971 675.74284102 675.74284102 0 0 1 58.58024239-42.8287711l87.46586894 86.60929307a197.8213377 197.8213377 0 0 0-30.31325332 104.21667774 202.86561533 202.86561533 0 0 0 203.67460284 201.77110195 200.20071357 200.20071357 0 0 0 105.5491286-30.45601495l68.00257179 67.57428428a440.51770987 440.51770987 0 0 1-173.55170039 37.11826846z m101.69453935-242.22049717l-135.67203106-134.19681767a100.74278847 100.74278847 0 0 1 33.97749171-6.18637852 105.35877862 105.35877862 0 0 1 101.55177685 140.38319619z m-67.76463427 67.0984084a114.210058 114.210058 0 0 1-33.97749258 5.71050351 107.119517 107.119517 0 0 1-107.690567-106.59605448 101.69453848 101.69453848 0 0 1 5.99602852-33.3112667z m226.61178925 89.9404207l-87.27551894-86.60929394a196.53647432 196.53647432 0 0 0 30.31325243-104.21667773 202.96079033 202.96079033 0 0 0-309.31890556-171.31508614L456.31022451 272.93265195c2.47455088-0.95175088 4.75875263-2.37937588 7.37606602-3.33112676 12.08723115-4.75875263 24.26963731-9.5175044 36.40445595-13.32450615 203.34149033-65.19490752 411.9176083 25.69726318 589.75218545 255.54500333a847.91450303 847.91450303 0 0 1-199.43931357 190.82597167z" fill="#ffffff" p-id="8346"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// Pk页匿名开
export const pkAnonymousOpen = () : string => {
const svgXml = `
<svg t="1760676973291" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7945" width="64" height="64"><path d="M900.4 424.87c19.47 0 37.03-11.73 44.49-29.73 7.46-17.98 3.33-38.7-10.43-52.48L713.97 122.19c-7.3-7.3-19.12-7.3-26.42 0l-41.69 41.69c-7.3 7.3-7.3 19.13 0 26.42l138.28 138.27H86.32c-10.19 0-18.46 8.26-18.46 18.46v59.39c0 10.19 8.26 18.46 18.46 18.46H900.4zM937.65 598.72H123.8c-19.47 0-37.03 11.73-44.49 29.73-7.46 17.98-3.33 38.7 10.43 52.48l220.49 220.48c7.3 7.3 19.12 7.3 26.42 0l41.69-41.69c7.3-7.3 7.3-19.13 0-26.42L240.06 695.02h697.59c10.32 0 18.68-8.37 18.68-18.68v-58.93c0-10.32-8.36-18.69-18.68-18.69z" p-id="7946" fill="#ffffff"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// 重置页图标
export const resetIcon = () : string => {
const svgXml = `
<svg t="1760952234942" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7688" width="64" height="64"><path d="M931.07 384.75a368 368 0 0 0-704 95.25H64l192 192 192-192H288.91C312 333.51 439.12 221.13 592 221.13c169.21 0 306.87 137.66 306.87 306.87S761.21 834.87 592 834.87a307.37 307.37 0 0 1-194.56-69.55 30.57 30.57 0 0 0-38.79 47.25 368.1 368.1 0 0 0 572.42-427.82z" fill="#333333" p-id="7689"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// pk页切换图标
export const switchIcon = () : string => {
const svgXml = `
<svg t="1761037923256" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7561" width="32" height="32"><path d="M900.4 424.87c19.47 0 37.03-11.73 44.49-29.73 7.46-17.98 3.33-38.7-10.43-52.48L713.97 122.19c-7.3-7.3-19.12-7.3-26.42 0l-41.69 41.69c-7.3 7.3-7.3 19.13 0 26.42l138.28 138.27H86.32c-10.19 0-18.46 8.26-18.46 18.46v59.39c0 10.19 8.26 18.46 18.46 18.46H900.4zM937.65 598.72H123.8c-19.47 0-37.03 11.73-44.49 29.73-7.46 17.98-3.33 38.7 10.43 52.48l220.49 220.48c7.3 7.3 19.12 7.3 26.42 0l41.69-41.69c7.3-7.3 7.3-19.13 0-26.42L240.06 695.02h697.59c10.32 0 18.68-8.37 18.68-18.68v-58.93c0-10.32-8.36-18.69-18.68-18.69z" p-id="7562" fill="#e6e6e6"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
@@ -0,0 +1,60 @@
<template>
<image class="img_100" v-if="showLocalImage" :src="imagePath"></image>
<image-preview v-else-if="showNetworkImage" class="img_100" :src="imageAddr"></image-preview>
<image class="img_100" v-else :src="defaultAvatar"></image>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue';
import { defaultAvatar } from '@/enum.js';
import { getUserInfo } from '@/common/common';
const imagePath = ref('');
const imageAddr = ref('');
// 状态标识
const showLocalImage = ref(false);
const showNetworkImage = ref(false);
// 初始化用户信息
const initUserInfo = () => {
const info = getUserInfo();
imagePath.value = info.imagePath || '';
imageAddr.value = info.imageAddr || '';
validateAvatarSources();
};
// 验证头像来源
const validateAvatarSources = () => {
if (imagePath.value) {
showLocalImage.value = true;
showNetworkImage.value = false;
} else if (imageAddr.value) {
showLocalImage.value = false;
showNetworkImage.value = true;
} else {
showLocalImage.value = false;
showNetworkImage.value = false;
}
};
// 强制刷新方法 - 重新获取用户信息并刷新头像
const forceRefreshAvatar = () => {
// 清空当前图片信息
imagePath.value = '';
imageAddr.value = '';
// 重新获取用户信息
initUserInfo();
};
onMounted(() => {
initUserInfo();
});
// 暴露方法给父组件
defineExpose({
forceRefreshAvatar
});
</script>
<style scoped lang="scss"></style>
+7 -8
View File
@@ -2,10 +2,11 @@
<view class="item">
<view class="top">
<view class="type">{{ subject_type_text }}</view>
<view class="score" v-if="['settlement', 'examination'].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">
<!-- 反馈中心 -->
<slot name="title-right"></slot>
<view class="mark_and_feedback_div" v-if="['examination', 'practice'].includes(props.mode)">
<image
v-if="props.mode === 'examination'"
@@ -23,12 +24,10 @@
<template v-if="['mistake'].includes(props.mode)">
<view class="expand_button_div" @click="toggleExpand()">
<!-- <view class="right_text">{{animationState?'收起':'展开'}}</view> -->
<view class="toggle-container">
<view class="toggle-text" :class="{ active: !animationState }">展开</view>
<view class="toggle-text" :class="{ active: animationState }">收起</view>
</view>
<uv-icon
class="click_text_icon"
name="arrow-right"
@@ -52,7 +51,7 @@
:isPreview="props.isPreview"
:activeVoiceId="subject_data.qnsId"
@buutton_click="questionButtonClick"
:button_complete_show="props.mode === 'practice'"
:button_complete_show="['practice', 'pk_in'].includes(props.mode)"
></questionVue>
</template>
<template v-else>
@@ -67,7 +66,7 @@
</view>
<view
class="option_div_button"
v-if="qnsTyp === 'MU' && ['practice', 'study'].includes(props.mode) && !props.isPreview && clearResult"
v-if="qnsTyp === 'MU' && ['practice', 'study', 'pk_in'].includes(props.mode) && !props.isPreview && clearResult"
>
<uv-button
type="primary"
@@ -85,7 +84,7 @@
class="analysis_div"
v-if="
!clearResult &&
['study', 'mistake', 'settlement', 'practice', 'practice_record'].includes(props.mode)
['study', 'mistake', 'settlement', 'practice', 'practice_record', 'pk_in'].includes(props.mode)
"
>
<view class="analysis_title" v-if="qnsTyp !== 'ES'">
@@ -153,8 +152,8 @@
// 错题本: mistake
// 学习: study
// 考试结算页 settlement
// 练习记录页 practice_record
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record'].includes(value);
// 练习记录页 practice_record, 'practice_record'
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record', 'pk_in'].includes(value);
}
},
// 是否预览模式
@@ -1,5 +1,5 @@
<template>
<view>
<view style="line-height: 0;">
<image :src="imgUrlShow" :mode="mode" @click="showDetail" style="width: 100%; height: 100%"></image>
<uni-popup ref="PopupRef" class="popup" mask-background-color="rgba(0,0,0,.2)" background-color="rgba(0,0,0,.2)" @change="changeShow">
<view class="overlay-box" @click="closePopup" @touchmove.stop>
@@ -167,6 +167,7 @@
</script>
<style lang="scss" scoped>
.overlay-box {
position: fixed;
left: 0;
@@ -182,6 +183,7 @@
width: 100vw;
max-height: 100%;
transform: translate(-50%, -50%);
}
}
</style>
@@ -0,0 +1,197 @@
<template>
<uni-popup ref="popupRef" class="popup">
<view class="tree-selector-div">
<view class="title">
<view class="title-text">机构选择</view>
<view class="back_div" @click="openDialog(false)">
<image :src="optionErrorIcon('#000000')" class="img"></image>
</view>
<uv-tabs
class="uvTabs"
:list="selectedPaths"
:lineWidth="40"
lineColor="#0066FF"
:activeStyle="{ color: '#0066FF', fontSize: '30rpx' }"
keyName="orgName"
:current="selectedPaths.length - 1"
@click="tabsClick"
></uv-tabs>
</view>
<scroll-view
ref="scrollRef"
scroll-with-animation
:scroll-into-view="scrollIntoId"
class="tree-selector-scroll"
scroll-x="true"
:show-scrollbar="false"
>
<treeSelector :node="orgTree"></treeSelector>
</scroll-view>
<view class="button-div">
<view class="button">
<image class="img" :src="resetIcon()"></image>
<view class="" @click="click_button('reset')">重置</view>
</view>
<view class="button" @click="click_button('all')">全部</view>
<view class="button" @click="click_button('confirm')">确定</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, reactive, provide, nextTick } from 'vue';
import common, { getUserInfo } from '@/common/common';
import { resetIcon, optionErrorIcon } from '@/common/imgSvg';
import treeSelector from './treeSelector';
const popupRef = ref(null);
// 滚动容器的 ref
const scrollRef = ref(null);
// 控制滚动位置的属性(scroll-top)
const scrollIntoId = ref('');
const openDialog = (status) => {
if (status) {
popupRef.value.open('bottom');
} else {
popupRef.value.close();
}
};
const tabsClick = (item) => {
if (item.index === selectedPaths.value.length - 1) {
return;
}
updateSelectedPaths(
{
orgName: item.orgName,
orgId: item.orgId,
parentId: item.parentId
},
item.index
);
};
const orgTreeInit = common.getValue('orgTree');
// 初始选中的
const initSelect = {
orgName: orgTreeInit[0].orgName,
orgId: orgTreeInit[0].orgId,
parentId: orgTreeInit[0].parentId
};
const orgTree = ref(orgTreeInit);
// 把全行信息插入
const selectedPaths = ref([initSelect]);
const updateSelectedPaths = (node, level) => {
console.log('node', node);
const newPaths = [...selectedPaths.value];
newPaths.splice(level);
newPaths.push(
reactive({
orgName: node['orgName'],
orgId: node['orgId'],
parentId: node['parentId']
})
);
selectedPaths.value = newPaths;
nextTick(() => {
scrollIntoId.value = `item-${selectedPaths.value.length}`;
});
};
const click_button = (type) => {
if (type === 'reset') {
updateSelectedPaths(initSelect, 0);
} else if (type === 'confirm') {
emit('submit', JSON.parse(JSON.stringify(selectedPaths.value)));
} else if (type === 'all') {
emit('submit', [{ orgName: '全部', orgId: 'A', parentId: '' }]);
}
};
provide('treeContextKey', {
selectedPaths,
updateSelectedPaths
});
const emit = defineEmits(['submit']);
defineExpose({
open: () => openDialog(true),
close: () => openDialog(false)
});
</script>
<style lang="scss" scoped>
.tree-selector-div {
display: flex;
flex-direction: column;
width: 100%;
background-color: #fff;
border-radius: 16rpx;
.title {
font-weight: 600;
font-size: 34rpx;
color: #333333;
text-align: center;
padding: 40rpx 0 0 0;
border-bottom: 4rpx solid #efefef;
.back_div {
width: 70rpx;
height: 70rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
display: flex;
align-items: center;
justify-content: center;
.img {
width: 30rpx;
height: 30rpx;
}
}
.uvTabs {
margin-bottom: -6rpx;
:deep(.uv-tabs__wrapper__nav__item) {
padding: 0 44rpx 0 1rem;
}
}
}
.button-div {
display: flex;
justify-content: space-between;
background: #ffffff;
// box-shadow: 0px -6 15px 0px rgba(0,0,0,0.05);
.button {
flex: 1;
border: none;
display: flex;
height: 124rpx;
justify-content: center;
align-items: center;
font-weight: 600;
font-size: 30rpx;
.img {
width: 38rpx;
height: 38rpx;
}
&:first-child {
color: #333333;
}
&:last-child {
color: #0066ff;
}
&:active {
background: #eaeaea;
// transform: scale(0.98); /* 按下时轻微缩小,增强反馈 */
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); /* 内阴影模拟按下凹陷感 */
}
}
}
}
.tree-selector-scroll {
white-space: nowrap;
width: 100%;
overflow: hidden;
background: #f0f0f0;
}
</style>
@@ -0,0 +1,134 @@
<template>
<view class="tree-selector" :id="`item-${props.level}`">
<view class="tree-child-selector" :class="{ fl1: props.level === 0}">
<view class="level" v-for="item in props.node" @click="click_node(item)">
<view class="node" :class="classList(item.orgId)">
{{ item.orgName }}
</view>
<view class="fl1"></view>
</view>
</view>
<treeSelector v-if="childNode && childNode.length" :node="childNode" :level="props.level + 1"></treeSelector>
</view>
</template>
<script setup>
import { defineOptions, ref, inject, reactive } from 'vue';
import { computed } from 'vue';
const treeContextKey = 'treeContextKey';
const { selectedPaths, updateSelectedPaths } = inject(treeContextKey);
const props = defineProps({
// 当前节点数据
node: {
type: Array, // 改为数组类型
required: true,
// 验证数组中的每个元素是否符合结构要求
validator: (value) => {
// 先判断是否为数组
if (!Array.isArray(value)) return false;
// 验证数组中的每个对象
return value.every((item) => {
// 每个元素必须是对象
if (typeof item !== 'object' || item === null) return false;
// 必须包含 orgId(字符串类型)
if (typeof item.orgId !== 'string') return false;
// 必须包含 orgName(字符串类型)
if (typeof item.orgName !== 'string') return false;
return true;
});
}
},
level: {
type: Number,
default: 0
}
});
// 把选择的层级树处理成id列表
const selectedPathsOrgCodeList = computed(() => selectedPaths.value.map((res) => res.orgId));
// 设置子列表的数据
const childNode = computed(() => {
const lastItem = selectedPaths.value.at(-1);
const selectItem = props.node.find((res) => selectedPathsOrgCodeList.value.includes(res.orgId));
if (selectItem) return selectItem['children'] || [];
return [];
});
// 判断需要的样式class
const classList = (orgId) => {
let a = [];
if (selectedPathsOrgCodeList.value.includes(orgId) || props.level === selectedPathsOrgCodeList.value.length) {
a.push('select-bg');
}
if (selectedPathsOrgCodeList.value.at(-1) === orgId) {
a.push('select-text');
}
return a;
};
const emit = defineEmits(['update:selectedKeys']);
// 处理当前节点被点击
const click_node = (node) => {
updateSelectedPaths(node, props.level);
};
defineOptions({
name: 'treeSelector'
});
</script>
<style scoped lang="scss">
.tree-selector {
border-left: 1rpx solid #eaeaea;
border-right: 1rpx solid #eaeaea;
display: flex;
min-height: 600rpx;
max-height: 800rpx;
display: flex;
transition: all 0.3s ease 2s; /* 关键配置 */
}
.tree-child-selector {
display: inline-block;
// 每一级树形容器
min-width: 6rem;
overflow-y: scroll;
scrollbar-width: none; /* Firefox 特有:直接隐藏滚动条 */
-ms-overflow-style: none; /* IE 和 Edge 特有:隐藏滚动条 */
&::-webkit-scrollbar {
display: none; /* 隐藏滚动条 */
}
.level {
display: flex;
flex-direction: column;
}
.node {
padding: 0 1rem;
background: #f7f7f7;
font-weight: 700;
font-size: 30rpx;
color: #333333;
text-align: left;
height: 80rpx;
line-height: 80rpx;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 超出部分显示省略号 */
&.select-bg {
background: #fff;
}
&.select-text {
color: #0066ff;
}
}
}
</style>
+2 -1
View File
@@ -43,7 +43,8 @@ export default function useZpaging({
}
// 搜索
const search = (value, state) => {
const search = (value, state=true) => {
console.log('搜索', value, state);
searchText.value = value;
if (state) {
reload()
+17 -2
View File
@@ -13,7 +13,7 @@ export const GENDER = [{
value: "f",
label: "女性",
}
]
]
// 试题题型
export const SUBJECT_TYPE = [{
value: "SN",
@@ -32,4 +32,19 @@ export const SUBJECT_TYPE = [{
label: "问答题",
}
]
export const defaultAvatar = '/static/images/me/default_user_avatar.png'
export const defaultAvatar = '/static/images/me/default_user_avatar.png'
export const defaultAnonymousAvatar = '/static/images/me/default_user_avatar.png'
export const styleButton = {
style_button_1: {
color: '#FFFFFF',
fontSize: '30rpx',
backgroundColor: '#0066FF',
borderRadius: '8rpx'
},
style_button_2: {
color: '#0066FF',
fontSize: '30rpx',
backgroundColor: '#E2EDFF',
borderRadius: '8rpx'
}
}
+4 -2
View File
@@ -48,11 +48,13 @@
]
},
/* ios */
"ios" : {},
"ios" : {
"dSYMs" : false
},
/* SDK */
"sdkConfigs" : {},
"splashscreen" : {
"androidStyle" : "default",
"androidStyle" : "common",
"android" : {
"hdpi" : "src/static/images/login/open.9.png",
"xhdpi" : "src/static/images/login/open.9.png",
+64
View File
@@ -311,6 +311,70 @@
}
}
},
{
"path": "pages/competition/index",
"style": {
"navigationBarTitleText": "竞赛列表页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/matching",
"style": {
"navigationBarTitleText": "匹配页面",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/pk",
"style": {
"navigationBarTitleText": "pk页面",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/result",
"style": {
"navigationBarTitleText": "pk结果页面",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/ranking",
"style": {
"navigationBarTitleText": "pk排行榜",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/pointsAndRank/points",
"style": {
"navigationBarTitleText": "积分变动明细",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/pointsAndRank/rank",
"style": {
"navigationBarTitleText": "段位列表",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/course/index",
"style": {
+193
View File
@@ -0,0 +1,193 @@
<template>
<z-paging
ref="pagingRef"
v-model="data_list"
@query="queryList"
class="scroll_div"
bg-color="#F1F5FA"
empty-view-img="/static/images/competition/list_empty.png"
empty-view-text="竞赛正在筹备中敬请期待~"
:empty-view-super-style="{ paddingTop: '10%' }"
:empty-view-img-style="{ height: '520rpx', width: '494rpx', marginBottom: '40rpx' }"
>
<template #top>
<view class="main_div">
<view class="body-title">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view>竞赛练习</view>
</view>
<view class="input-view">
<search-input placeholder="请输入竞赛名" @search="search"></search-input>
</view>
</view>
</template>
<view class="scroll-Y">
<view class="item" v-for="(item, index) in data_list" @click="goto_competition(item)">
<view class="cover">
<image-preview class="img" :src="item.ossAddr" mode="aspectFill"></image-preview>
</view>
<view class="title ellipsis-text">
{{ item.comName }}
</view>
</view>
</view>
</z-paging>
</template>
<script setup>
// 竞赛列表
import { ref, reactive, watch, onMounted } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
import useZpaging from '@/composables/useZpaging.js';
import common from '@/common/common';
import { queryTraUserAnonyByUserId } from '@/api/user.js';
import { queryTraCompetitionInfoPaging, queryTestPapers, queryOrgTree } from '@/api/competition.js';
const { pagingRef, queryList, data_list, search } = useZpaging({
searchKey: 'comName',
fetchFunction: queryTraCompetitionInfoPaging
});
const goto_competition = (item) => {
common.loading();
const result = Promise.all([queryTestPapers({ comId: item.comId }), queryTraUserAnonyByUserId(), queryOrgTree()]);
result
.then((res) => {
const papersId = res[0].body;
const comId = item.comId;
common.setPageCache('competition_list', {
papersId: papersId, // 试卷id
isAnony: res[1].body.isAnony, // 是否匿名 Y是N否
ruleDesc: item.ruleDesc, // 规则说明
comName: item.comName, // 标题
comId: comId
});
common.setValue('orgTree', res[2].body ?? []);
common.hideLoading();
common.navigateTo(`/pages/competition/matching?papersId=${papersId}&comId=${comId}`);
})
.catch(() => {
common.msg('网络繁忙,请稍后再试!');
});
};
onLoad(() => {});
onUnload(() => {});
</script>
<style scoped lang="scss">
.item {
margin: 28rpx;
padding: 22rpx;
border-radius: 16rpx;
overflow: hidden;
background-color: #fff;
.cover {
overflow: hidden;
border-radius: 16rpx;
width: calc(100vw - 100rpx);
padding-top: calc(9 / 16 * 100%);
position: relative;
.img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
.title {
font-weight: 700;
font-size: 30rpx;
color: #333333;
margin-top: 20rpx;
}
}
.scroll-Y {
overflow: hidden;
background-color: #f1f5fa;
}
.main_div {
display: flex;
flex-direction: column;
align-items: center;
padding-top: var(--status-bar-height);
overflow: hidden;
background-color: #fff;
}
.body-title {
width: 100%;
height: 70rpx;
font-size: 33rpx;
padding-top: 10rpx;
font-weight: 500;
color: #000;
position: relative;
text-align: center;
.delete_div {
position: absolute;
right: 20rpx;
top: 17rpx;
width: 50rpx;
height: 50rpx;
.img {
width: 34rpx;
height: 34rpx;
}
}
.back_div {
position: absolute;
left: 26rpx;
height: 34rpx;
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 #2b2c2e;
border-left: 4rpx solid #2b2c2e;
transform: rotate(-45deg);
}
}
}
.input-view {
width: 90%;
margin: 20rpx auto;
height: 78rpx;
position: relative;
.search_div {
position: absolute;
top: 0%;
right: 0;
height: 100%;
width: 70rpx;
}
}
</style>
+64
View File
@@ -0,0 +1,64 @@
/**
* 生成随机匹配时间
* 特点:同一时间段生成的时间会比较接近,时间范围可配置
*/
export const MatchTimeGenerator = (() => {
// 可配置的时间范围(毫秒)
const MIN_TIME = 2000; // 最小时间
const MAX_TIME = 6000; // 最大时间
// 时间窗口:100秒内生成的时间会基于同一基准(可根据需要调整)
const TIME_WINDOW = 100000;
// 同一窗口内的最大偏差(±1.5秒,可根据需要调整)
const MAX_DEVIATION = 1500;
// 记录上一次生成的基准时间和时间戳
let lastBaseTime = 0;
let lastTimestamp = 0;
return {
/**
* 生成匹配时间(毫秒)
* @returns {number} 介于MIN_TIME和MAX_TIME之间的随机数(毫秒)
*/
generate() : number {
const now = Date.now();
let baseTime : number;
// 如果超过时间窗口或首次生成,重新计算基准时间
if (now - lastTimestamp > TIME_WINDOW || lastBaseTime === 0) {
// 生成MIN_TIME到MAX_TIME之间的基准时间(毫秒)
const timeRange = MAX_TIME - MIN_TIME;
baseTime = MIN_TIME + Math.floor(Math.random() * timeRange);
lastBaseTime = baseTime;
lastTimestamp = now;
} else {
// 在同一时间窗口内,基于上次基准时间生成偏差
baseTime = lastBaseTime;
}
// 生成偏差值(-MAX_DEVIATION 到 MAX_DEVIATION
const deviation = Math.floor(Math.random() * (MAX_DEVIATION * 2 + 1)) - MAX_DEVIATION;
let matchTime = baseTime + deviation;
// 确保结果在配置的范围内
matchTime = Math.max(MIN_TIME, Math.min(MAX_TIME, matchTime));
return matchTime;
},
/**
* 生成匹配时间(秒)
* @returns {number} 介于MIN_TIME/1000和MAX_TIME/1000之间的随机数(秒,保留一位小数)
*/
generateInSeconds() : number {
return Math.round(this.generate() / 100) / 10;
}
};
})();
export const formatTime = (seconds : number) => {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return [m.toString().padStart(2, '0'), s.toString().padStart(2, '0')].join(':');
};
+69
View File
@@ -0,0 +1,69 @@
import AvatarW1 from '@/static/images/competition/avatar/f_1.jpg'
import AvatarW2 from '@/static/images/competition/avatar/f_2.jpg'
import AvatarW3 from '@/static/images/competition/avatar/f_3.jpg'
import AvatarW4 from '@/static/images/competition/avatar/f_4.jpg'
import AvatarM1 from '@/static/images/competition/avatar/m_1.jpg'
import AvatarM2 from '@/static/images/competition/avatar/m_2.jpg'
import AvatarM3 from '@/static/images/competition/avatar/m_3.jpg'
import AvatarM4 from '@/static/images/competition/avatar/m_4.jpg'
import AvatarS1 from '@/static/images/competition/avatar/s_1.jpg'
import AvatarS2 from '@/static/images/competition/avatar/s_2.jpg'
import AvatarS3 from '@/static/images/competition/avatar/s_3.jpg'
import AvatarS4 from '@/static/images/competition/avatar/s_4.jpg'
// 定义头像类型
type AvatarType = 'f' | 'm' | 's';
// 按类型分组
const avatarsByType = {
f: [AvatarW1, AvatarW2, AvatarW3, AvatarW4],
m: [AvatarM1, AvatarM2, AvatarM3, AvatarM4],
s: [AvatarS1, AvatarS2, AvatarS3, AvatarS4]
};
/**
* 随机获取头像
* @param type1 第一个头像类型(可选)
* @param type2 第二个头像类型(可选,传入则返回2个头像)
* @returns 单个头像路径(当只传type1或都不传时)或两个头像路径的数组(当传入type2时)
*/
export default function getRandomAvatars(
type1 ?: AvatarType,
type2 ?: AvatarType
) {
// 随机获取指定类型的头像
const getRandomAvatar = (type ?: AvatarType) => {
// 如果指定了类型且有效,则从该类型中选择
if (type && avatarsByType[type]) {
const avatars = avatarsByType[type];
return avatars[Math.floor(Math.random() * avatars.length)];
}
// 否则从所有头像中随机选择
const allAvatars = [
...avatarsByType.f,
...avatarsByType.m,
...avatarsByType.s
];
return allAvatars[Math.floor(Math.random() * allAvatars.length)];
};
// 只传type1或都不传时,返回1个头像
if (type2 === undefined) {
return getRandomAvatar(type1);
}
// 传入type2时,返回2个头像
const avatar1 = getRandomAvatar(type1);
let avatar2;
// 当两个类型相同时,确保头像不重复
if (type1 && type1 === type2) {
const sameTypeAvatars = avatarsByType[type1].filter(avatar => avatar !== avatar1);
avatar2 = sameTypeAvatars[Math.floor(Math.random() * sameTypeAvatars.length)];
} else {
avatar2 = getRandomAvatar(type2);
}
return [avatar1, avatar2];
}
+20
View File
@@ -0,0 +1,20 @@
export function formatSecondsToMS(seconds) {
// 尝试将输入转换为数字(处理字符串类型的数字)
const num = Number(seconds);
// 处理无效输入(转换失败、非数字、负数)
if (isNaN(num) || num < 0) {
return '0-秒';
}
// 计算分钟(不进位,直接取整)
const minutes = Math.floor(num / 60);
// 计算剩余秒数(取整)
const remainingSeconds = Math.floor(num % 60);
if (minutes === 0) {
return `${remainingSeconds}`;
}
// 拼接结果
return `${minutes}${remainingSeconds}`;
}
+337
View File
@@ -0,0 +1,337 @@
<template>
<view class="main_div max_page">
<template v-if="!isMatchingSuccess">
<!-- 顶部一些按钮 -->
<view class="top-button-div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view class="fl1"></view>
<view class="top-icon-div" @click="click_top_icon(1)">
<image class="img1" :src="data_info.isAnony === 'N' ? pkAnonymousClose() : pkAnonymousOpen()"></image>
<view class="text">{{ data_info.isAnony === 'N' ? '不匿名' : '匿名' }}</view>
</view>
<view class="top-icon-div" @click="click_top_icon(2)">
<image class="img2" :src="pkRules()"></image>
<view class="text">规则说明</view>
</view>
<view class="top-icon-div" @click="click_top_icon(3)">
<image class="img3" :src="pkRankingList()"></image>
<view class="text">排行榜</view>
</view>
</view>
<view class="title-div" :class="{ hidden: challengeLoading }">
{{ data_info.comName }}
</view>
<view class="match-msg-div" :class="{ hidden: !challengeLoading }">匹配中...</view>
<view class="avatar-div">
<view class="avatar-surround-div">
<view class="avatar-img-div" :class="{ hidden: challengeLoading }">
<cached-avatar v-show="data_info.isAnony === 'N'" class="img"></cached-avatar>
<image v-show="data_info.isAnony === 'Y'" class="img" :src="defaultAnonymousAvatar"></image>
</view>
</view>
<view class="avatar-name-div">{{ data_info.isAnony === 'N' ? data_info.userName : '匿名' }}</view>
</view>
<view class="select-div" :class="{ hidden: challengeLoading }" @click="selectionInstitutionsRef.open(true)">
<view class="select-institution-div">
{{ data_info.orgName }}
<image class="img" :src="switchIcon()"></image>
</view>
<view class="select-institution-msg-div">*选择机构后会根据机构进行人员随机匹配挑战</view>
</view>
<view class="fl1"></view>
<!-- 匹配时间 -->
<view class="match-time-div" :class="{ hidden: !challengeLoading }">
{{ formatTime(practiceTime) }}
</view>
<view class="button-div" v-show="challengeButtonStatus">
<view
:class="{ start_button: !challengeLoading, cancel_button: challengeLoading, 'button-transition': true }"
@click="start_challenge"
>
{{ challengeLoading ? '取消匹配' : '开始挑战' }}
</view>
</view>
<view class="fl1"></view>
<view class="fl1"></view>
<view class="fl1"></view>
</template>
<template v-else>
<view class="matching-success-msg">匹配成功</view>
<view class="matching-success-animation">
<view class="left animate-left">
<image class="img" src="@/static/images/competition/matching-success-left.png" mode="widthFix"></image>
<view class="information-div-left">
<view class="avatar-img-div">
<cached-avatar v-show="data_info.isAnony === 'N'" class="img"></cached-avatar>
<image v-show="data_info.isAnony === 'Y'" class="img" :src="defaultAnonymousAvatar"></image>
</view>
<view class="name">
{{ data_info.isAnony === 'N' ? data_info.userName : '匿名' }}
</view>
<view class="org">
{{ data_info.isAnony === 'N' ? data_info.orgIdName : '机构不显示' }}
</view>
</view>
</view>
<view class="right animate-right">
<image class="img" src="@/static/images/competition/matching-success-right.png" mode="widthFix"></image>
<view class="information-div-right">
<view class="avatar-img-div">
<image-preview
v-if="data_info.pkIsAnony === 'N'"
class="img"
:src="data_info.pkimgAddr"
></image-preview>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name">
{{ data_info.pkIsAnony === 'N' ? data_info.pkUserName : '匿名' }}
</view>
<view class="org">
{{ data_info.pkIsAnony === 'N' ? data_info.pkOrgIdName : '机构不显示' }}
</view>
</view>
</view>
</view>
<view class="matching-success-text">准备</view>
</template>
<uni-popup ref="popupRef" class="popup">
<view class="sheet_popup_div">
<view class="title_div">双人对战规则说明</view>
<view class="note_div">
{{ data_info.ruleDesc }}
</view>
</view>
</uni-popup>
<selectionInstitutions ref="selectionInstitutionsRef" @submit="selectionInstitutionsSubmit"></selectionInstitutions>
<uv-picker ref="anonyPickerRef" :columns="[anonyOptions]" keyName="label" @confirm="anonyConfirm"></uv-picker>
</view>
</template>
<script setup lang="ts">
import {ref, onMounted, reactive} from 'vue';
import common, {getUserInfo} from '@/common/common';
import {MatchTimeGenerator, formatTime} from './js/matching';
import {queryTraCompetitionInfoByPkUser} from '@/api/competition';
import {onLoad} from '@dcloudio/uni-app';
import {defaultAnonymousAvatar} from '@/enum';
import {pkAnonymousClose, pkAnonymousOpen, pkRankingList, pkRules, switchIcon} from '@/common/imgSvg';
import {queryTraUserAnonyByUserId, updateTraUserAnony} from '@/api/user';
import selectionInstitutions from '@/components/selection-institutions/index.vue';
const data_info = reactive({
userId: '',
userName: '',
orgName: '全部', // 机构名称
orgId: 'A', // 机构的ID,全行是A
papersId: '', // 试卷id
isAnony: '', // 是否匿名 Y是N否
ruleDesc: '', // 规则说明
comName: '', // 标题
comId: '',
orgIdName: '',
pkIsAnony: '',
pkimgAddr: '',
pkUserName: '',
pkOrgIdName: ''
});
const popupRef = ref(null);
const anonyPickerRef = ref(null);
const selectionInstitutionsRef = ref(null);
const challengeLoading = ref(false);
const isMatchingSuccess = ref(false); // 匹配成功状态
const challengeButtonStatus = ref(true); // 匹配按钮显示
const practiceTime = ref(0); // 匹配时间
let practiceTimeTimer: number | null = null; // 匹配时间计时器
let matchTimer: number | null = null; // 匹配成功定时器
const anonyOptions = [
{
value: 'Y',
label: '匿名'
},
{
value: 'N',
label: '不匿名'
}
];
// 开始挑战
const start_challenge = () => {
if (!challengeLoading.value) {
challengeLoading.value = true;
// 清除旧计时器
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
}
// 开始计时
const practiceStartTime = new Date().getTime();
practiceTimeTimer = setInterval(() => {
const currentTime = Date.now();
practiceTime.value = Math.floor((currentTime - practiceStartTime) / 1000);
}, 1000);
// 生成匹配延迟时间
// const delayTime = 0;
const delayTime = MatchTimeGenerator.generateInSeconds();
// 匹配定时器
matchTimer = setTimeout(async () => {
try {
challengeButtonStatus.value = false;
const orgId = data_info.orgId
const papersId = data_info.papersId
const comId = data_info.comId
const pkDataBody = await queryTraCompetitionInfoByPkUser({
// orgId:510121,
// papersId:'COMPETITION_PAPERS0250640321542025101615442400000003107',
// comId:'TRA_COMPETITION_INFO0250640321542025101615442400000003099'
orgId, papersId, comId
});
const pkData = pkDataBody.body;
// 下面两行是给后端擦屁股,后端传回的数值为空
pkData.orgId = orgId
pkData.papersId = papersId
common.setPageCache('competition_pk', pkData);
// 1. 显示匹配成功状态
isMatchingSuccess.value = true;
uni.vibrateLong({
success: function () {
}
});
// 2. 执行匹配成功动画(等待1.5秒)
await new Promise((resolve) => setTimeout(resolve, 600));
// 3. 清除计时器
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
practiceTimeTimer = null;
}
// 5. 跳转页面
common.navigateTo('/pages/competition/pk');
challengeLoading.value = true;
} catch (error) {
common.msg('队列繁忙,请稍后再试!');
// 出错处理
} finally {
resetChallengeState();
}
}, delayTime * 1000);
} else {
// 取消挑战
resetChallengeState();
}
};
interface InstitutionItem {
orgName: string; // 机构名称,类型为字符串
orgId: string; // 机构ID,类型为字符串
parentId: string; // 父级ID,类型为字符串
children?: InstitutionItem[];
}
const selectionInstitutionsSubmit = (options: [InstitutionItem, ...InstitutionItem[]]) => {
// 缓存起来
common.setValue('cache_selection_institutions' + data_info.userId, options);
const lastItem = options.at(-1);
data_info.orgName = lastItem.orgName;
data_info.orgId = lastItem.orgId;
selectionInstitutionsRef.value.close();
};
const anonyConfirm = (e) => {
const originalIsAnony = data_info.isAnony;
const isAnony = e.value[0].value;
if (isAnony === data_info.isAnony) return;
data_info.isAnony = isAnony;
common.loading('修改中');
updateTraUserAnony({isAnony})
.then(() => {
setTimeout(() => common.msg('切换成功'), 400);
})
.catch(() => {
// 4. 请求失败:用初始值回滚
data_info.isAnony = originalIsAnony;
common.msg('切换失败,请重试'); // 可选:提示失败
})
.finally(() => {
common.hideLoading();
});
};
const click_top_icon = (type: number) => {
// 匿名切换
if (type === 1) {
anonyPickerRef.value.setIndexs([data_info.isAnony === 'Y' ? 0 : 1], true);
anonyPickerRef.value.open();
} else if (type === 2) {
// 打开规则说明
popupRef.value.open('bottom');
} else if (type === 3) {
// 去排行榜
common.navigateTo(`/pages/competition/ranking?papersId=${data_info.papersId}&orgId=${data_info.orgId}`)
}
};
// 统一重置状态的函数
const resetChallengeState = () => {
if (matchTimer) {
clearTimeout(matchTimer);
matchTimer = null;
}
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
practiceTimeTimer = null;
}
challengeButtonStatus.value = true;
challengeLoading.value = false;
isMatchingSuccess.value = false;
practiceTime.value = 0;
};
onLoad((e) => {
const lastData = common.getPageCache('competition_list');
console.log('lastData', lastData);
const userInfo = getUserInfo();
Object.assign(data_info, {
...lastData,
userId: userInfo.userId,
userName: userInfo.userName,
gender: userInfo.gender,
papersId: e.papersId,
comId: e.comId
});
const selectOptions = common.getValue('cache_selection_institutions' + data_info.userId);
if (Array.isArray(selectOptions) && selectOptions.length > 0) {
const lastItem = selectOptions.at(-1);
data_info.orgName = lastItem.orgName;
data_info.orgId = lastItem.orgId;
// const orgTreeInit = common.getValue('orgTree');
// if (Array.isArray(orgTreeInit) && orgTreeInit.length > 0) {
// console.log('selectOptions', selectOptions);
// }
}
});
</script>
<style scoped lang="scss">
// 匹配成功样式动画
@import './style/matching-success.scss';
@import './style/matching-loading.scss';
@import './style/matching-prelude.scss';
/* 容器样式 */
.max_page {
width: 100%;
min-height: 100vh;
background-image: url('@/static/images/competition/bg.png');
background-color: #000; /* 图片高度不足时的背景色 */
background-size: 100% auto; /* 宽度填满,高度自适应 */
background-position: center top; /* 图片顶部居中显示 */
background-repeat: no-repeat; /* 不重复平铺 */
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
+803
View File
@@ -0,0 +1,803 @@
<template>
<view class="main_div max_page">
<nav-bar :is_seat="true"></nav-bar>
<view class="nav-div">
<view class="title ellipsis-text">PK中</view>
<view class="back_div" @click="exit_practice">
<view class="back-icon"></view>
</view>
</view>
<!-- 对决信息框 -->
<view class="duel-msg-div">
<view class="personnel-div">
<view class="avatar-div">
<cached-avatar v-if="pkData.isAnony === 'N'" class="img"></cached-avatar>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name-div ellipsis-text">{{ pkData.isAnony === 'N' ? pkData.userName : '匿名' }}</view>
</view>
<view class="fraction-div left">
<view class="fraction-title">分数</view>
<view class="fraction-value">{{ ourScore }}</view>
</view>
<view class="fraction-div right">
<view class="fraction-title">分数</view>
<view class="fraction-value">{{ opponentScore }}</view>
</view>
<view class="personnel-div">
<view class="avatar-div">
<image-preview v-if="pkData.pkIsAnony === 'N'" class="img" :src="pkData.pkImgAddr"></image-preview>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name-div">{{ pkData.pkIsAnony === 'N' ? pkData.pkUserName : '匿名' }}</view>
</view>
</view>
<view class="body-area-div">
<view class="countdown-and-question-number-div">
<view class="top">
<view class="countdown">
<text class="bold">{{ questionNumber + 1 }}</text>
<text class="grey">/{{ topicList.length }}</text>
</view>
<view class="question">
<view class="countdown_time">
{{ formatSeconds(practiceRemainingTime) }}
</view>
</view>
</view>
<view class="progress">
<uv-line-progress
:percentage="progress"
:showText="false"
activeColor="#0066FF"
inactiveColor="#D9DEE8"
:height="8"
></uv-line-progress>
</view>
</view>
<view class="scroll_div">
<swiper
class="swiper"
:indicator-dots="false"
:current="questionNumber"
:disable-touch="true"
easing-function="linear"
>
<swiper-item v-for="(item, index) in topicList">
<view class="content">
<view class="expose_shadow"></view>
<scroll-view :scroll-y="true" class="scroll-Y" :show-scrollbar="false" :refresher-threshold="0">
<Subject
:item="item"
:mode="mode"
:isPreview="item.isPreview"
:clearResult="item.clearResult"
@subject_click="subject_click"
v-model="item.my_answer"
>
<template #title-right>
<text class="success-sun-div">
已答对
<text class="number">{{ correctQuestions.length }}</text>
</text>
</template>
</Subject>
</scroll-view>
</view>
</swiper-item>
</swiper>
</view>
<view class="fixed-box font_pf" :style="{ marginBottom: popup_input_bottom }">
<view class="touch-btn-div" :class="{ show: showTouchBtn }">
<TouchBtn
ref="touchBtnRef"
class="talk-btns"
@submit="touchBtnSubmit"
disabledText="只能进行一条回答"
:isDisabled="touchIsDisabled"
/>
</view>
</view>
</view>
<!-- 反馈组件 -->
<Feedback ref="feedbackRef"></Feedback>
<!-- 结尾弹出层 -->
<Dialog ref="dialogRef" @button_click="result_click"></Dialog>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted, nextTick, onUnmounted } from 'vue';
import { onLoad, onHide, onShow, onUnload, onBackPress, onReady } from '@dcloudio/uni-app';
import { defaultAnonymousAvatar, styleButton } from '@/enum';
import Feedback from '@/pages/examination/components/feedback.vue';
import Dialog from '@/pages/examination/components/dialog.vue';
import Subject from '@/components/examination/subject.vue';
import CharactersSubject from '@/components/examination/characters-subject.vue';
import TouchBtn from '@/components/examination/touchBtn.vue';
import common from '@/common/common';
import { setPageCache, formatSeconds, getPageCache, setupKeyboardHeightListener } from '@/common/common';
import { commonUploadVoiceFile } from '@/api/common.js';
import { optionErrorIcon, examinationSheetIcon, practiceExitIcon } from '@/common/imgSvg';
import { practiceCommit, queryCrsPracticeAnswerResult } from '@/api/practice.js';
import { competitionAnswer, queryCompetitionPracticeAnswerResult, competitionAnswerEnd } from '@/api/competition.js';
import { startExamByCrs } from '@/api/examination.js';
import { startPracticeByCrs } from '@/api/practice.js';
const feedbackRef = ref(null);
const dialogRef = ref(null);
const touchBtnRef = ref(null);
const countDownRef = ref(null);
const practiceTime = ref(0); // 当前Pk时长
const practiceRemainingTime = ref(0); // 当前Pk剩余时长
let practiceStartTime = 0; // Pk开始时间
let practiceTimeTimer = null; // Pk计时器
let leaveStartTime = 0; // 离开开始时间
const mode = ref('pk_in'); // PK中 pk_in
// 计算对方分数
const opponentScore = computed(() => {
// 筛选出所有 asrTm <= 当前时长的项
const validItems = pkData.traCompetitionPkTimeRecordVos.filter((item) => item.asrTm <= practiceTime.value);
if (validItems.length > 0) {
return validItems[validItems.length - 1].singleGrade;
}
return 0;
});
const pkData = reactive({
comLimit: 0,
crsId: '',
name: '', // 名称
orgId: '',
papersId: '',
pkImgAddr: '',
traCompetitionPkTimeRecordVos:[]
}); // 试卷信息
const topicList = reactive([]); // 问题列表
const popup_input_bottom = ref('0px');
const questionNumber = ref(0); // 题号
// 答对的题目数组
const correctQuestions = computed(() => topicList.filter((res) => res.judgeResult === 'P'));
// 我方分数
const ourScore = computed(() => correctQuestions.value.reduce((sum, item) => sum + item.tgtGrade, 0));
const topic = computed({
get: () => topicList[questionNumber.value],
set: (val) => (topicList[questionNumber.value] = val)
});
const showTouchBtn = computed(() => {
return topic.value?.qnsTyp === 'ES' && !topic.value?.my_answer.anserResult;
});
// 下面语音组件的禁用状态判断
const touchIsDisabled = computed(() => {
return topic.value?.es_status !== '' && topic.value?.es_status !== undefined;
});
const progress = computed(() => {
// 计算进度百分比(保留两位小数)
const total = topicList.length;
if (total === 0) return 0;
const completed = questionNumber.value + 1;
return Math.min(Math.round((completed / total) * 10000) / 100, 100);
});
const subject_click = (type, params) => {
if (type === 'answer') {
//多选点击按钮答题
if (params.qnsTyp === 'MU') {
submitProblem({
anserResult: params.answer.join(',')
});
}
} else if (type === 'choose') {
//单选判断点击选项答题
if (['JD', 'SN'].includes(params.qnsTyp)) {
submitProblem({ anserResult: params.answer.join(',') });
}
} else if (type === 'complete') {
// 问答题点击完成
submitProblem(params.answer);
} else if (type === 'again') {
// 问答题点击重答
topic.value.my_answer = {};
topic.value.isPreview = false;
topic.value.es_status = ''; // 取消转圈
}
};
// 单题回答
const submitProblem = (answer) => {
common.loading('答题中');
// execId 执行ID 必输项:true 类型:String
// papersId 竞赛试卷ID 必输项:true 类型:String
// comId 竞赛ID 必输项:true 类型:String
// qnsId 试题ID 必输项:true 类型:String
// ossKey 对象存储 必输项:false 类型:String
// ossAddr 对象存储 必输项:false 类型:String
// showOrder 顺序号 必输项:false 类型:int
// anserResult 作答结果 必输项:false 类型:String
// asrStartTm 答题开始时间 必输项:false 类型:String
competitionAnswer({
execId: pkData.execId,
papersId: pkData.papersId,
comId: pkData.comId,
qnsId: topic.value.qnsId,
ossKey: '',
showOrder: questionNumber.value + 1,
limit: practiceTime.value, // 当前答题的秒
...answer
})
.then(async (res) => {
let traQnsInfoVo;
// 是问答题
if (topic.value.qnsTyp === 'ES') {
try {
const result = await queryWithRetry(res.body.execLogId);
traQnsInfoVo = result.body.traQnsInfoVo;
} catch (e) {
common.msg('当前服务器繁忙,请重新作答');
common.hideLoading();
topic.value.isPreview = false;
return;
}
} else {
traQnsInfoVo = res.body.traQnsInfoVo;
}
topic.value.isPreview = true; // 不能在点击了,打开预览模式
console.log('给出来的traQnsInfoVo', traQnsInfoVo);
common.hideLoading();
topic.value.clearResult = false; // 不清除正确答案
topic.value.analyContent = traQnsInfoVo.analyContent; // 答案解析
// 更新选项正确项目
topic.value.itemVos = traQnsInfoVo.itemVos; // 答案选项(todo应该不这样替换)
topic.value.anserResult = traQnsInfoVo.anserResult; // 正确答案字符串分割
topic.value.judgeResult = traQnsInfoVo.judgeResult; // 正确答案字符串分割
if (questionNumber.value >= topicList.length - 1) {
console.log('pk页答完了,带走的数据', pkData);
common.setPageCache('competition_pk_to_resulf', pkData);
nextTick(() => {
common.redirectTo(
`/pages/competition/result?&execId=${pkData.execId}&orgId=${pkData.orgId}&papersId=${pkData.papersId}`
);
});
} else {
nextTick(() => {
setTimeout(() => {
questionNumber.value++;
}, 400);
});
}
})
.catch(() => {
// 单选和判断需要清除已点击的按钮
if (['JD', 'SN'].includes(topic.value.qnsTyp)) {
topic.value.my_answer = [];
}
common.hideLoading();
});
};
// 问答题轮询结果方法
const queryWithRetry = (execLogId, maxRetries = 20) => {
// 递归终止条件:达到最大重试次数
console.log('递归终止条件:达到最大重试次数', maxRetries);
if (maxRetries <= 0) {
console.log('超过最大重试次数,查询失败');
const error = new Error('超过最大重试次数,查询失败');
error.isMaxRetry = true; // 标记为"超过最大重试次数"错误
return Promise.reject(error);
}
return queryCompetitionPracticeAnswerResult({ execLogId })
.then((res) => {
console.log('queryCompetitionPracticeAnswerResult', maxRetries, res.body);
const isWait = res.body.isWait;
if (isWait === 'Y') {
// 如果是Y,延迟2秒后重试,重试次数减1
return new Promise((resolve) => {
setTimeout(() => {
resolve(queryWithRetry(execLogId, maxRetries - 1));
}, 2000);
});
} else {
// 不是Y,直接返回结果
return res;
}
})
.catch((error) => {
console.log('error', error);
// 对接口调用错误进行重试
if (error.isMaxRetry) {
return Promise.reject(error);
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(queryWithRetry(execLogId, maxRetries - 1));
}, 2000);
});
});
};
// 录音组件提交
const touchBtnSubmit = (type, submitObj) => {
const topic = topicList[questionNumber.value];
touchBtnRef.value?.clearinputText(); // 清除发送框数据
if (type === 'voice') {
if (topic.es_status === '00') return;
topic.es_status = '00'; // 00转圈
const voicePath = submitObj;
commonUploadVoiceFile(voicePath, '/traask/traAskchat/voiceTrans', {})
.then((_res) => {
const _content = _res.body.transContent.trim();
if (_content === '') {
topic.es_status = ''; // 取消转圈
return common.msg('未识别到文字');
} else {
topic.es_status = '02';
topic.my_answer = {
anserResult: _res.body.transContent,
ossKey: _res.body.fileId,
ossAddr: _res.body.ossFileAddr,
voicePath: voicePath
};
}
})
.catch((error) => {
topic.es_status = ''; // 取消转圈
if (error === 'Other') {
common.msg('系统异常,请联系管理员');
}
});
} else if (type === 'text') {
topic.es_status = '01';
topic.my_answer = {
anserResult: submitObj // 正确答案文字内容
};
}
};
const examStat = ref('');
const result_click = async ({ key }) => {
if (key === 'exit') {
// 中途退出
dialogRef.value.close();
try {
// 考试结束
await competitionAnswerEnd({ execId: pkData.execId, comGrade: ourScore.value });
} catch (e) {}
common.navigateBack();
} else if (key === 'revert') {
// 返回
dialogRef.value.close();
}
};
// 弹出框配置
const dialogConfiguration = reactive({});
// 中途退出
const exit_practice = () => {
dialogRef.value.open({
title: '提示',
msg: '中途退出视为结束竞赛,确定要退出吗?',
buttonList: [
{
key: 'exit',
name: '确定退出',
style: styleButton.style_button_2
},
{
key: 'revert',
name: '继续竞赛',
style: styleButton.style_button_1
}
]
});
};
// 启动练习时长定时器
const startTimer = () => {
if (practiceTimeTimer) {
clearInterval(practiceTimeTimer);
}
practiceTimeTimer = setInterval(() => {
const currentTime = Date.now();
const elapsedSeconds = Math.floor((currentTime - practiceStartTime) / 1000);
practiceTime.value = elapsedSeconds;
practiceRemainingTime.value = pkData.comLimit - practiceTime.value;
// console.log('倒计时时间', practiceRemainingTime.value);
// Pk时间到
if (practiceRemainingTime.value <= 0) {
}
}, 1000);
};
// 初始化时检查
onLoad((e) => {
const pkDataParams = getPageCache('competition_pk');
if (!pkData) {
common.navigateBack();
return;
}
Object.assign(pkData, pkDataParams);
console.log('pkData', pkData);
pkDataParams.qnsInfoVos.forEach((res) => {
topicList.push({
...res,
isPreview: false,
clearResult: true,
es_status: '', // 预设一下问答题的状态,'' 空为没有 00 转圈 01 播放,其他暂定
my_answer: []
});
});
// 记录练习起始时间
practiceStartTime = new Date().getTime();
practiceRemainingTime.value = pkData.comLimit; // 设置倒计时初始值
setupKeyboardHeightListener((height) => {
popup_input_bottom.value = `${height}px`;
console.log('需要卸载吗');
});
});
onMounted(() => {
startTimer(); // 启动/恢复定时器
});
onBackPress((res) => res.from === 'backbutton');
</script>
<style scoped lang="scss">
$bg-margin-left: 30rpx;
.popup_title_text {
color: red;
}
.popup {
//答题卡
z-index: 1800;
padding: 0 38rpx;
.sheet_popup_div {
width: 100%;
height: 100%;
background-color: #ffffff;
padding: 10rpx 34rpx;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 16rpx 16rpx 0px 0px;
position: relative;
.title_div {
width: 100%;
text-align: center;
padding: 30rpx 0;
font-family: PingFangSC;
font-weight: 600;
font-size: 34rpx;
color: #000000;
text-align: center;
font-style: normal;
border-bottom: 2rpx solid #efefef;
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 30rpx;
right: 30rpx;
display: flex;
align-items: center;
justify-content: center;
.img {
width: 30rpx;
height: 30rpx;
}
}
.sheet_table {
margin-top: 48rpx;
margin-bottom: 48rpx;
display: flex;
flex-wrap: wrap;
width: calc(100vw - 108rpx);
align-content: space-between;
.sheet_table_item {
width: calc((100vw - 108rpx) / 5 - 16rpx);
height: calc((100vw - 108rpx) / 5 - 16rpx);
margin: 8rpx;
border: 2rpx solid #e5e5e5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #999999;
}
.current {
background-color: rgba(0, 102, 255, 0.6);
border-color: #0066ff;
color: #ffffff;
}
.completed {
// 已完成
background-color: rgba(0, 102, 255, 0.1);
border-color: #0066ff;
color: #0066ff;
}
.mark,
.completed.mark {
background-color: rgba(232, 181, 49, 0.1);
color: #e8b531;
border-color: #e8b531;
}
.current.mark {
// 已完成
background-color: rgba(0, 102, 255, 0.6);
color: #ffffff;
border-color: #e8b531;
}
}
.sheet_button {
width: 100%;
}
}
}
.fixed-box {
position: fixed;
width: 100%;
left: 0;
bottom: 0;
border-radius: 16rpx 16rpx 0px 0px;
border: 2rpx solid #e5e5e5;
background: #ffffff;
}
.touch-btn-div {
height: 0;
overflow: hidden;
transition: height 0.2s ease-out;
}
.touch-btn-div.show {
height: 150rpx;
}
.scroll-Y {
max-height: calc(100vh - var(--status-bar-height) - 70rpx - 210rpx - 70rpx - 40rpx - 160rpx);
min-height: calc(100vh - var(--status-bar-height) - 70rpx - 210rpx - 70rpx - 40rpx - 380rpx);
}
.swiper {
// 状态栏 var(--status-bar-height)
// 标题栏目 70rpx
// PK栏 170rpx + 上间距 40rpx
// 进度时间栏 70rpx
// 进度时间栏上下间距 40rpx = 20rpx + 20rpx
// height: calc(100vh - var(--status-bar-height) - 70rpx - 70rpx - 40rpx - 64rpx);
height: calc(100vh - var(--status-bar-height) - 70rpx - 210rpx - 70rpx - 40rpx);
.no_questions {
}
}
.content {
position: relative;
margin: 32rpx $bg-margin-left;
border-radius: 16rpx;
padding: 34rpx 30rpx 34rpx 36rpx;
background-color: #fff;
.expose_shadow {
margin-left: 36rpx;
position: absolute;
bottom: -26rpx;
left: 0%;
width: calc(100% - 66rpx);
height: 28rpx;
background-color: #ffffff;
border-radius: 0 0 14rpx 14rpx;
opacity: 0.4;
z-index: 0;
// background-color: red;
}
}
// 正确总数
.success-sun-div {
font-weight: 500;
font-size: 30rpx;
color: #333333;
.number {
font-weight: 600;
color: #0066ff;
}
}
.body-area-div {
margin-top: -2rpx;
background: linear-gradient(185deg, #eff4ff 0%, #e9ebf9 100%);
}
.countdown-and-question-number-div {
margin: 20rpx $bg-margin-left;
height: 70rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: 10;
.top {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
color: #0066ff;
text-align: left;
font-style: normal;
}
.question {
display: flex;
align-items: center;
justify-content: center;
width: 216rpx;
flex-wrap: nowrap;
height: 42rpx;
background-color: #b2d1ff;
border-radius: 20px;
color: #044ebb;
font-weight: bold;
font-size: 30rpx;
.countdown_time {
white-space: nowrap;
}
}
.grey {
color: #8cabeb;
white-space: nowrap;
}
.bold {
font-weight: 600;
}
}
.duel-msg-div {
width: 100%;
height: 172rpx;
background-image: url('@/static/images/competition/pk_bg_icon.png');
background-repeat: no-repeat;
background-size: 100% 100%; /* 让背景图填满盒子,与宽高比匹配 */
margin-top: 40rpx;
border-radius: 22rpx 22rpx 0 0;
display: flex;
align-items: center;
justify-content: space-around;
.personnel-div {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.name-div {
font-weight: 500;
font-size: 30rpx;
color: #ffffff;
text-align: center;
margin-top: 10rpx;
max-width: 5rem;
}
.avatar-div {
width: 78rpx;
height: 78rpx;
border: 2px solid #ffffff;
border-radius: 50%;
overflow: hidden;
.img {
width: 100%;
height: 100%;
}
}
}
.fraction-div {
&.left {
margin: 0 74rpx 0 30rpx;
}
&.right {
margin: 0 30rpx 0 74rpx;
}
display: flex;
flex-direction: column;
align-items: center;
.fraction-title {
font-weight: 400;
font-size: 24rpx;
color: #fbe7d9;
line-height: 34rpx;
text-align: center;
font-style: normal;
}
.fraction-value {
margin-top: 10rpx;
font-family: Arial, Arial;
font-weight: 700;
font-size: 50rpx;
color: #ffffff;
line-height: 60rpx;
text-align: center;
font-style: italic;
}
}
}
.nav-div {
position: relative;
.title {
margin: 0 calc($bg-margin-left + 70rpx);
height: 70rpx;
font-family: PingFangSC;
font-weight: 600;
font-size: 34rpx;
color: #ffffff;
text-align: center;
font-style: normal;
line-height: 70rpx;
}
.back_div {
position: absolute;
left: $bg-margin-left;
top: 0;
width: 40rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: flex-end;
.img {
width: 32rpx;
height: 32rpx;
}
.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: 4rpx solid #ffffff;
border-left: 4rpx solid #ffffff;
transform: rotate(-45deg);
}
}
}
.main_div {
display: flex;
flex-direction: column;
background: #000;
/* 确保背景占满整个视口高度 */
min-height: 100vh;
margin: 0;
}
</style>
+399
View File
@@ -0,0 +1,399 @@
<template>
<view class="exam-ranking">
<image src="@/static/images/competition/ranking/ranking_bg.png" mode="widthFix" class="bg-box"></image>
<view class="ranking-box">
<view class="nav-div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
</view>
<view class="title-view">
<!-- <view class="title-info">考试排行榜</view> -->
<image
class="title-image"
src="@/static/images/competition/ranking/ranking_title.png"
mode="aspectFit"
></image>
<view class="title-desc">欲穷千里目更上一层楼</view>
<image class="cup-image" src="@/static/images/examRanking/cup.png" mode="aspectFit"></image>
</view>
<view class="ranking-body">
<view class="list-header list-item">
<view class="rank-column">排名</view>
<view class="user-column">用户名/所属机构</view>
<view class="score-column">最高分</view>
<view class="time-column">用时</view>
</view>
<scroll-view :scroll-y="true" class="scroll-items-view">
<view v-for="(item, index) in userList" :class="['list-item', 'item-' + (index + 1)]">
<view class="rank-column">
{{ item.ranking }}
</view>
<view class="user-column">
<view class="avatar-box">
<image-preview
v-if="item.ossAddr"
class="avatar-image"
:src="item.ossAddr"
mode="aspectFit"
></image-preview>
<image v-else class="avatar-image" :src="defaultAvatarSrc"></image>
</view>
<view class="user-name">{{ item.userName }}</view>
<view class="user-org">{{ item.orgName }}</view>
</view>
<view class="score-column">{{ item.examGrade }}</view>
<view class="time-column">{{ item.time ?? 0 }}s</view>
</view>
<list-no-data v-if="userList.length == 0">暂无数据</list-no-data>
</scroll-view>
</view>
<view class="ranking-self">
<view class="list-header list-item">
<view class="rank-column">我的排名</view>
</view>
<view class="list-item item-me">
<view class="rank-column">
{{ userRankObj.ranking }}
</view>
<view class="user-column">
<view class="avatar-box">
<image-preview
v-if="userRankObj.ossAddr"
class="avatar-image"
:src="userRankObj.ossAddr"
mode="aspectFit"
></image-preview>
<image class="avatar-image" v-else :src="defaultAvatarSrc"></image>
</view>
<view class="user-name">{{ userRankObj.userName }}</view>
<view class="user-org">{{ userRankObj.orgName }}</view>
</view>
<view class="score-column">{{ userRankObj.examGrade }}</view>
<view class="time-column">{{ userRankObj.time }}s</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {queryTraCompetitionRankingList} from '@/api/competition.js';
import {defaultAvatar} from '@/enum.js';
import common from '@/common/common';
import {onLoad} from '@dcloudio/uni-app';
import {formatSecondsToMS} from './js/tool';
import {ref} from 'vue';
const papersId = ref('');
const orgId = ref('');
const userList = ref([]);
const userRankObj = ref({});
onLoad((e) => {
papersId.value = e.papersId;
orgId.value = e.orgId;
getList();
});
const getList = () => {
queryTraCompetitionRankingList({
papersId: papersId.value,
orgId: orgId.value
}).then((res) => {
userList.value = res.body.rankingListVos || [];
userRankObj.value = res.body;
});
};
</script>
<style scoped lang="scss">
$top-height: var(--status-bar-height);
.exam-ranking {
position: relative;
height: 100vh;
width: 100vw;
overflow: hidden;
box-sizing: border-box;
padding-top: $top-height;
// background:url(@/static/images/examRanking/bg.png) no-repeat top center;
.bg-box {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 412rpx;
z-index: 1;
}
.ranking-box {
height: calc(100% - $top-height);
overflow: hidden;
position: absolute;
top: $top-height;
width: 100%;
z-index: 2;
}
.nav-div {
position: relative;
overflow: hidden;
height: 70rpx;
.back_div {
margin-right: 10rpx;
position: absolute;
left: 25rpx;
top: 0;
padding: 15rpx;
.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: 4rpx solid #000;
border-left: 4rpx solid #000;
transform: rotate(-45deg);
}
}
}
.title-view {
height: 180rpx;
position: relative;
padding-left: 79rpx;
padding-top: 20rpx;
.cup-image {
position: absolute;
right: 90rpx;
bottom: -5rpx;
width: 178rpx;
height: 159rpx;
}
.title-desc {
line-height: 34rpx;
font-size: 25rpx;
color: #ffffff;
margin-top: -6rpx;
}
.title-image {
width: 354rpx;
height: 88rpx;
margin-left: -20rpx;
}
}
.ranking-body {
height: calc(100% - 170rpx - 180rpx - 70rpx);
overflow: hidden;
background-color: #fff;
border-radius: 31rpx 31rpx 0 0;
padding-top: 20rpx;
box-sizing: border-box;
z-index: 100;
.scroll-items-view {
height: calc(100% - 54rpx);
}
}
.list-item {
height: 108rpx;
border-bottom: 2rpx solid #efefef;
overflow: hidden;
&.list-header {
height: 54rpx;
line-height: 54rpx;
color: #999;
font-size: 25rpx;
border-bottom: 0;
.rank-column {
height: 54rpx;
line-height: 54rpx;
color: #999;
font-weight: normal;
font-size: 25rpx;
font-style: initial;
text-indent: 0;
}
.user-column {
padding: 0;
}
.score-column {
line-height: 54rpx;
color: #999;
font-weight: normal;
font-size: 25rpx;
}
.time-column {
line-height: 54rpx;
color: #999;
font-weight: normal;
font-size: 25rpx;
}
}
&.item-1 {
border-bottom: 0;
margin-bottom: 8rpx;
background: linear-gradient(90deg, #fbf9f5 0%, #fffffd 100%);
.rank-column {
color: #ffffff00;
background: url(@/static/images/examRanking/no-1.png) no-repeat;
background-position: 40rpx center;
background-size: 44rpx 66rpx;
text-indent: 8rpx;
}
}
&.item-2 {
border-bottom: 0;
margin-bottom: 8rpx;
background: linear-gradient(90deg, #f4f8f9 0%, #f7fbfe 100%);
.rank-column {
color: #ffffff00;
background: url(@/static/images/examRanking/no-2.png) no-repeat;
background-position: 40rpx center;
background-size: 44rpx 66rpx;
text-indent: 8rpx;
}
}
&.item-3 {
border-bottom: 0;
margin-bottom: 8rpx;
background: linear-gradient(90deg, #faf6f5 0%, #fcfbfc 100%);
.rank-column {
color: #ffffff00;
background: url(@/static/images/examRanking/no-3.png) no-repeat;
background-position: 40rpx center;
background-size: 44rpx 66rpx;
text-indent: 8rpx;
}
}
&.item-me {
border-bottom: 0;
.rank-column {
color: #06f;
}
}
.rank-column {
width: 170rpx;
padding-left: 40rpx;
float: left;
font-weight: 600;
line-height: 92rpx;
height: 108rpx;
color: #333;
font-size: 38rpx;
font-style: italic;
text-indent: 8rpx;
}
.user-column {
width: 300rpx;
float: left;
padding: 15rpx 0;
.avatar-box {
width: 77rpx;
height: 77rpx;
overflow: hidden;
float: left;
background-color: #eee;
border-radius: 50%;
.avatar-image {
width: 77rpx;
height: 77rpx;
}
}
.user-name {
float: left;
width: calc(100% - 77rpx);
padding-left: 18rpx;
padding-right: 18rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 40rpx;
line-height: 40rpx;
font-size: 29rpx;
color: #000;
font-weight: 500;
}
.user-org {
float: left;
width: calc(100% - 77rpx);
padding-left: 18rpx;
padding-right: 18rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 34rpx;
line-height: 34rpx;
font-size: 25rpx;
color: #999;
}
}
.score-column {
width: 126rpx;
text-align: right;
float: left;
font-weight: 600;
line-height: 108rpx;
color: #333;
font-size: 31rpx;
}
.time-column {
width: 100rpx;
text-align: right;
float: left;
font-weight: 600;
line-height: 108rpx;
color: #333;
font-size: 31rpx;
}
}
.ranking-self {
height: 170rpx;
overflow: hidden;
background-color: #f5f9ff;
padding-top: 4rpx;
}
}
</style>
+586
View File
@@ -0,0 +1,586 @@
<template>
<view class="main_div max_page">
<!-- 顶部一些按钮 -->
<view class="top-button-div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view class="fl1"></view>
</view>
<view class="fl1"></view>
<view class="show-main-div" :class="action">
<!-- 伸展出的吉祥物 -->
<view class="result-container-div" :class="{ victory: jieguo === 'Y' }">
<image class="img" src="@/static/images/competition/result_shengli.png"></image>
</view>
<view class="result-container-div" :class="{ defeat: jieguo === 'N' }">
<image class="img defeat" src="@/static/images/competition/result_shibai.png"></image>
</view>
<view class="mascot-div">
<image class="img" src="@/static/images/competition/result_loading_mascot.png"></image>
</view>
<view class="mascot-msg-div">
<image class="img" src="@/static/images/competition/result_loading_msg.png"></image>
</view>
<view class="bg-div">
<!-- 下方的pk结算信息 -->
<view class="pk-msg-div">
<view class="win-icon" :class="jieguo">
<image class="img_100" src="@/static/images/competition/result_huosheng.png"></image>
</view>
<view class="pk-msg-left-div">
<view class="avatar-div">
<cached-avatar v-if="pkData.isAnony === 'N'" class="img"></cached-avatar>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name-div">{{ pkData.userName }}</view>
<view class="title">正确/总题数</view>
<view class="topic-div number">
<text class="loadingNoText font-size-54">{{ pkData.correctNub }}</text>
<text class="loadingNoText">/{{ pkData.qnsNub }}</text>
<text class="loadingText">结算中</text>
</view>
<view class="title">总分</view>
<view class="score-div number">
<text class="font-size-54 loadingNoText">{{ pkData.totalScore }}</text>
<text class="loadingText">结算中</text>
</view>
<view class="title">用时</view>
<view class="time-div number">
<text class="loadingNoText">{{ formatSecondsToMS(pkData.timeTaken) }}</text>
<text class="loadingText">结算中</text>
</view>
</view>
<view class="pk-msg-right-div">
<view class="avatar-div">
<image-preview v-if="pkData.isAnonyPk === 'N'" class="img" :src="pkData.imgPk"></image-preview>
<image v-else class="img" :src="defaultAnonymousAvatar" mode="aspectFill"></image>
</view>
<view class="name-div">{{ pkData.userNamePk }}</view>
<view class="title">正确/总题数</view>
<view class="topic-div number">
<text class="loadingNoText font-size-54">{{ pkData.correctNubPk }}</text>
<text class="loadingNoText">/{{ pkData.qnsNubPk }}</text>
<text class="loadingText">结算中</text>
</view>
<view class="title">总分</view>
<view class="score-div number">
<text class="font-size-54 loadingNoText">{{ pkData.totalScorePk }}</text>
<text class="loadingText">结算中</text>
</view>
<view class="title">用时</view>
<view class="time-div number">
<text class="loadingNoText">{{ formatSecondsToMS(pkData.timeTakenPk) }}</text>
<text class="loadingText">结算中</text>
</view>
</view>
</view>
<!-- 查看排行榜按钮 -->
<view class="ranking-list-div" @click="buttonClick('toRanking')">查看排行榜</view>
<!-- 下方的两个按钮 -->
<view class="fl1"></view>
<view class="bottom-button-div">
<uv-button class="button-1" text="退出挑战" shape="circle" @click="buttonClick('exit')"></uv-button>
<uv-button class="button-2" text="继续挑战" shape="circle" @click="buttonClick('continue')"></uv-button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, computed, nextTick } from 'vue';
import common, { getUserInfo } from '@/common/common';
import { MatchTimeGenerator, formatTime } from './js/matching';
import { queryCompetitionAnswerResult } from '@/api/competition';
import { onLoad } from '@dcloudio/uni-app';
import { defaultAnonymousAvatar } from '@/enum';
import { pkAnonymousClose, pkAnonymousOpen, pkRankingList, pkRules } from '@/common/imgSvg';
import { queryTraUserAnonyByUserId, updateTraUserAnony } from '@/api/user';
import { formatSecondsToMS } from './js/tool';
const action = ref('');
const pkData = reactive({
// 公共的
execId: '',
papersId: '',
orgId: '',
answerResult: '', // 答题结果 Y代表我赢,N代表我输
timeDifference: 30, // 时差 int 单位 s
// 自己的
userName: '',
isAnony: 'Y', // 匿名 String
correctNub: 0, // 正确数 int
qnsNub: 0, // 总数 int
timeTaken: 0, // 用时 int
totalScore: 0, // 我方总分 int
// 对方的
userIdPk: '', // 用户Id String
userNamePk: '', // 用户姓名 String
imgPk: '', // 对方头像 String
isAnonyPk: 'Y', // 匿名 String
correctNubPk: 0, // 正确数 int
qnsNubPk: 0, // 总数 int
timeTakenPk: 0, // 用时 int
totalScorePk: 0 // 对方总分 int
});
const jieguo = computed(() => pkData.answerResult);
const buttonClick = (type: String) => {
if (type === 'exit') {
common.navigateBack(2);
} else if (type === 'continue') {
common.navigateBack();
} else if (type === 'toRanking') {
common.navigateTo(`/pages/competition/ranking?papersId=${pkData.papersId}&orgId=${pkData.orgId}`);
}
};
onLoad((e) => {
pkData.execId = e.execId || '';
pkData.papersId = e.papersId || '';
pkData.orgId = e.orgId || '';
const pkDataInit = common.getPageCache('competition_pk_to_resulf');
Object.assign(pkData, {
userName: pkDataInit.userName,
isAnony: pkDataInit.isAnony
});
});
onMounted(() => {
action.value = 'loading';
setTimeout(() => {
queryCompetitionAnswerResult({ execId: pkData.execId }).then(({ body }) => {
const userResult = body.userResult;
pkData.answerResult = body.answerResult; // 答题结果 Y代表我赢,N代表我输
pkData.timeDifference = body.timeDifference; // 时差 int 单位 s
Object.assign(pkData, {
// 自己的
isAnony: userResult.isAnony, // 匿名 String
correctNub: userResult.correctNub, // 正确数 int
qnsNub: userResult.qnsNub, // 总数 int
timeTaken: userResult.timeTaken, // 用时 int
totalScore: userResult.totalScore || 0, // 我方总分
// 对方的
userIdPk: userResult.userIdPk, // 用户Id String
userNamePk: userResult.userNamePk, // 用户姓名 String
imgPk: userResult.imgPk, // 用户姓名 String
pkIsAnony: userResult.pkIsAnony, // 匿名 String
correctNubPk: userResult.correctNubPk, // 正确数 int
qnsNubPk: userResult.qnsNubPk || 0, // 总数 int
timeTakenPk: userResult.timeTakenPk || 0, // 用时 int
totalScorePk: userResult.totalScorePk || 0 // 对方总分
});
nextTick(() => {
action.value = 'result';
});
});
}, 2000);
});
</script>
<style scoped lang="scss">
.pk-msg-div {
width: 570rpx;
height: 480rpx;
background-image: url('@/static/images/competition/result_pk_bg.png');
background-size: 100% auto; /* 宽度填满,高度自适应 */
background-position: center top; /* 图片顶部居中显示 */
background-repeat: no-repeat; /* 不重复平铺 */
position: relative;
display: flex;
justify-content: space-around;
font-size: 26rpx;
.pk-msg-left-div > .avatar-div {
border: 3rpx solid #2b7af1;
}
.pk-msg-right-div > .avatar-div {
border: 3rpx solid #f63c13;
}
.avatar-div {
width: 78rpx;
height: 78rpx;
border-radius: 50%;
overflow: hidden;
margin-top: 30rpx;
flex-shrink: 0;
.img {
width: 100%;
height: 100%;
}
}
.name-div {
margin-top: 10rpx;
font-weight: 700;
font-size: 30rpx;
color: #000000;
height: 42rpx;
}
.topic-div {
font-family: Arial, Arial;
font-style: italic;
height: 62rpx;
line-height: 62rpx;
}
.score-div {
font-family: Arial, Arial;
height: 62rpx;
line-height: 62rpx;
}
.win-icon {
width: 140rpx;
height: 130rpx;
position: absolute;
display: none;
&.Y {
display: block;
left: -20rpx;
top: -46rpx;
}
&.N {
display: block;
right: -30rpx;
top: -46rpx;
}
}
.pk-msg-left-div,
.pk-msg-right-div {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
.title {
font-weight: 400;
margin-top: 10rpx;
}
}
.pk-msg-left-div {
.title {
color: #6a8db1;
}
.number {
color: #2b7af1;
}
}
.pk-msg-right-div {
.title {
color: #b16f6a;
}
.number {
color: #f63c13;
}
}
.font-size-54 {
font-size: 54rpx;
font-weight: 700;
}
}
.show-main-div {
width: 100%;
min-height: 500rpx;
background-color: #fff; /* 关键:背景图未覆盖的区域填充白色 */
position: relative;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 31px 31px 0px 0px;
@keyframes victoryEnter {
0% {
transform: translateY(50rpx) scale(0.8);
opacity: 0;
}
30% {
transform: translateY(-10rpx) scale(1.05);
}
100% {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@keyframes defeatEnter {
0% {
transform: translateY(-50rpx) scale(1.2);
opacity: 0;
}
70% {
transform: translateY(10rpx) scale(0.95);
}
100% {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@keyframes defeatRotateSway {
0% {
transform: rotate(0deg); // 初始角度
}
25% {
transform: rotate(-2deg); // 向左微转2度
}
75% {
transform: rotate(2deg); // 向右微转2度
}
100% {
transform: rotate(0deg); // 回到初始角度
}
}
@keyframes victoryFloat {
0%,
100% {
transform: translateY(0);
// transform: translateY(0);
}
50% {
transform: translateY(-10rpx);
// transform: translateY(-10rpx);
}
}
.loadingText {
display: none;
}
// loading 状态样式(保持不变)
&.loading {
.mascot-div {
left: 50%;
transform: translateX(-50%);
}
.mascot-msg-div {
opacity: 1;
pointer-events: auto;
}
.topic-div,
.score-div,
.time-div {
position: relative;
&::after {
content: '';
position: absolute;
top: 4rpx;
left: calc(100% + 3rpx);
animation: loading 2.5s infinite;
}
@keyframes loading {
0% {
content: '';
}
25% {
content: '.';
}
50% {
content: '..';
}
75% {
content: '...';
}
100% {
content: '';
}
}
}
.loadingText {
display: inline;
}
.loadingNoText {
display: none;
}
}
// 当 action=result 时,触发结果动画
&.result {
// 胜利状态:入场 + 浮动 + 闪光
.result-container-div.victory {
animation: victoryEnter 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards,
victoryFloat 2s ease-in-out infinite 0.8s;
}
// 失败状态:入场 + 抖动
.result-container-div.defeat {
// defeatShake 1s ease-in-out infinite 0.6s
animation: defeatEnter 0.6s ease-out forwards, defeatRotateSway 1s ease-in-out 0.6s 1; // 入场后开始晃动,2秒结束,仅1次
}
}
// 结果容器基础样式
.result-container-div {
position: absolute;
top: -300rpx; // 初始位置(可根据需求调整)
z-index: 20;
opacity: 0; // 初始隐藏,动画中变为1
.img {
width: 512rpx;
height: 408rpx;
display: block; // 避免图片底部间隙
}
}
.mascot-div {
position: absolute;
top: -300rpx;
margin-left: 50rpx;
transition: top 0.6s ease-out;
z-index: 5;
left: 100%;
transform: translateX(0);
transition: left 0.5s ease-out, transform 0.5s ease-out;
.img {
width: 284rpx;
height: 400rpx;
}
}
.mascot-msg-div {
position: absolute;
top: -300rpx;
transition: top 0.6s ease-out;
z-index: 20;
margin-top: 240rpx;
transition: top 0.6s ease-out, opacity 0.5s ease-out; /* 新增透明度过渡 */
z-index: 20;
opacity: 0; /* 初始隐藏 */
pointer-events: none; /* 隐藏时不响应交互 */
.img {
width: 326rpx;
height: 198rpx;
}
}
.bg-div {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 20rpx 20rpx 0 0;
padding-top: 140rpx;
position: relative;
width: 100%;
height: 100%;
.img {
// width: 100%;
// height: 100%;
}
z-index: 10;
background: linear-gradient(to bottom, transparent 0%, #ffffff 200rpx, #ffffff 100%),
linear-gradient(to right, #bfebff 0%, #fef1de 100%);
}
.ranking-list-div {
margin-top: 48rpx;
margin-bottom: 24rpx;
font-weight: 400;
font-size: 30rpx;
color: #0066ff;
text-align: center;
font-style: normal;
}
.bottom-button-div {
display: flex;
margin-top: 50rpx;
margin-bottom: 60rpx;
:deep(.uv-button) {
/* 以下是所有 .uv-button 共用的公共样式 */
margin: 16rpx;
width: 246rpx;
height: 92rpx;
border-radius: 58px;
font-weight: 500;
text-align: center;
.uv-button__text {
font-size: 30rpx !important;
}
}
.button-1 :deep(.uv-button) {
background-color: #fff;
}
.button-2 :deep(.uv-button) {
background: linear-gradient(0deg, #009fff 0%, #0066ff 100%) !important;
color: #ffffff;
}
}
}
.top-button-div {
display: flex;
justify-content: space-between;
width: 100%;
height: 70rpx;
padding-top: 10rpx;
font-weight: 500;
position: relative;
margin-right: 10rpx;
.top-icon-div {
display: flex;
align-items: center;
margin-right: 30rpx;
font-weight: 400;
font-size: 26rpx;
color: #ffffff;
text-align: left;
.img1,
.img2,
.img3 {
margin-right: 6rpx;
}
.img1,
.img3 {
width: 40rpx;
height: 40rpx;
}
.img2 {
width: 30rpx;
height: 30rpx;
}
}
.back_div {
position: absolute;
left: 26rpx;
height: 34rpx;
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 #ffffff;
border-left: 4rpx solid #ffffff;
transform: rotate(-45deg);
}
}
}
/* 容器样式 */
.max_page {
width: 100%;
min-height: 100vh;
background-image: url('@/static/images/competition/bg.png');
background-color: #000; /* 图片高度不足时的背景色 */
background-size: 100% auto; /* 宽度填满,高度自适应 */
background-position: center top; /* 图片顶部居中显示 */
background-repeat: no-repeat; /* 不重复平铺 */
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
@@ -0,0 +1,37 @@
.match-time-div {
font-weight: 400;
font-size: 34rpx;
color: #ffffff;
line-height: 34rpx;
font-style: normal;
margin-bottom: 20rpx;
transition: opacity 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
opacity: 1; /* 初始显示状态 */
transform: translateY(0); /* 初始位置 */
&.hidden {
opacity: 0; /* 完全透明 */
transform: translateY(40rpx); /* 轻微上移增强淡出效果 */
pointer-events: none; /* 隐藏后不响应事件 */
height: 0; /* 可选:完全隐藏高度 */
overflow: hidden; /* 可选:隐藏溢出内容 */
}
}
.match-msg-div {
width: 100%;
height: 8vh;
font-weight: normal;
font-size: 72rpx;
color: #ffffff;
display: flex;
justify-content: center;
align-items: flex-end;
transition: opacity 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
opacity: 1; /* 初始显示状态 */
transform: translateY(0); /* 初始位置 */
&.hidden {
opacity: 0; /* 完全透明 */
transform: translateY(-40rpx); /* 轻微上移增强淡出效果 */
pointer-events: none; /* 隐藏后不响应事件 */
overflow: hidden; /* 可选:隐藏溢出内容 */
}
}
@@ -0,0 +1,334 @@
.select-div {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
transition: opacity 0.3s ease-out;
opacity: 1; /* 初始显示状态 */
&.hidden {
opacity: 0; /* 完全透明 */
pointer-events: none; /* 隐藏后不响应事件 */
height: 0; /* 可选:完全隐藏高度 */
overflow: hidden; /* 可选:隐藏溢出内容 */
}
}
.select-institution-msg-div {
width: calc(100% - 228rpx);
font-weight: 400;
font-size: 22rpx;
color: #ffffff;
line-height: 32rpx;
text-align: left;
font-style: normal;
opacity: 0.8;
padding-top: 18rpx;
padding-bottom: 18rpx;
}
.select-institution-div {
width: calc(100% - 228rpx);
height: 86rpx;
border: 1rpx solid transparent;
position: relative;
font-weight: 400;
font-size: 32rpx;
line-height: 86rpx;
text-align: center;
color: #ffffff;
background-color: rgba(0, 0, 0, 0.4);
z-index: 1;
overflow: hidden;
padding: 0 60rpx 0 20rpx;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
.img {
// background-color: red;
padding: 16rpx;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 10rpx;
width: 30rpx;
height: 30rpx;
}
}
/* 边框高光效果 */
.select-institution-div::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 4rpx solid transparent;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent) border-box;
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
/* 初始位置在左侧外部 */
background-size: 200% 100%;
/* 动画:高光从左到右流动 */
animation: highlightFlow 4s linear infinite;
}
/* 定义高光流动动画 */
@keyframes highlightFlow {
0% {
background-position: -100% 0;
}
100% {
background-position: 100% 0;
}
}
/* 环绕版本带呼吸效果 */
.select-institution-div.full-flow::before {
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.8),
transparent,
transparent,
transparent,
transparent,
transparent,
transparent
);
background-size: 300% 300%;
animation: fullBorderFlow 2s linear infinite, breathing 1s ease-in-out infinite;
}
@keyframes fullBorderFlow {
0% {
background-position: 0 0;
}
25% {
background-position: 100% 0;
}
50% {
background-position: 100% 100%;
}
75% {
background-position: 0 100%;
}
100% {
background-position: 0 0;
}
}
.title-div {
width: calc(100% - 80rpx);
margin: 40rpx;
font-size: 32rpx;
font-weight: 500;
color: #ffffff;
line-height: 44rpx;
text-shadow: 0px 0px 19px rgba(0, 0, 0, 0.11);
text-align: center;
font-family: none;
min-height: 88rpx;
margin: calc(var(--status-bar-height) + 40rpx);
/* 添加过渡动画 */
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
opacity: 1; /* 初始显示状态 */
transform: translateY(0); /* 初始位置 */
&.hidden {
opacity: 0; /* 完全透明 */
transform: translateY(-10rpx); /* 轻微上移增强淡出效果 */
pointer-events: none; /* 隐藏后不响应事件 */
height: 0; /* 可选:完全隐藏高度 */
overflow: hidden; /* 可选:隐藏溢出内容 */
}
}
.avatar-div {
margin-top: 20rpx;
.avatar-img-div {
width: 204rpx;
height: 204rpx;
border: 4rpx solid #ffffff;
border-radius: 50%;
position: relative;
overflow: visible;
/* 修复伪元素显示问题 */
&.hidden::before {
content: '';
position: absolute;
/* 使用固定尺寸确保计算正确 */
width: 230rpx;
height: 230rpx;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 确保在z轴上处于正确层级 */
z-index: 1;
/* 背景图设置 */
background-image: url('@/static/images/competition/avatar_surround.png');
background-size: contain; /* 改用contain确保完整显示 */
background-repeat: no-repeat;
background-position: center;
/* 旋转动画 */
animation: variableRotate 2s linear infinite;
}
.img {
width: 100%;
height: 100%;
border-radius: 50%;
position: relative;
z-index: 2;
overflow: hidden;
}
}
@keyframes variableRotate {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.avatar-name-div {
font-weight: 600;
font-size: 46rpx;
color: #ffffff;
line-height: 64rpx;
text-align: center;
font-style: normal;
padding-top: 16rpx;
padding-bottom: 16rpx;
}
}
.button-div {
.start_button {
width: 569rpx;
height: 92rpx;
background: linear-gradient(#009fff 0%, #0066ff 100%);
border-radius: 58rpx;
font-weight: 500;
font-size: 30rpx;
color: #ffffff;
line-height: 92rpx;
text-align: center;
margin-top: 250rpx;
}
.cancel_button {
width: 246rpx;
height: 92rpx;
border-radius: 58rpx;
font-weight: 500;
font-size: 30rpx;
color: #ffffff;
line-height: 92rpx;
text-align: center;
background: rgba(0, 0, 0, 0.5);
background: linear-gradient(rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%);
border-radius: 58rpx;
border: 2rpx solid #dbdbdb;
margin-bottom: 250rpx; /* 向上移动50rpx,可根据需要调整 */
}
.button-transition {
/* 添加过渡动画属性 */
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
/* 确保边框变化也有过渡效果 */
box-sizing: border-box;
}
}
.top-button-div {
display: flex;
justify-content: space-between;
width: 100%;
height: 70rpx;
padding-top: calc(16rpx + var(--status-bar-height));
font-weight: 500;
position: relative;
margin-right: 10rpx;
.top-icon-div {
display: flex;
align-items: center;
margin-right: 30rpx;
font-weight: 400;
font-size: 26rpx;
color: #ffffff;
text-align: left;
.img1,
.img3 {
width: 40rpx;
height: 40rpx;
margin-right: 6rpx;
}
.img2 {
width: 30rpx;
height: 30rpx;
margin-right: 6rpx;
}
}
.back_div {
position: absolute;
left: 26rpx;
height: 34rpx;
top: calc(10rpx + var(--status-bar-height));
.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 #ffffff;
border-left: 4rpx solid #ffffff;
transform: rotate(-45deg);
}
}
}
.popup {
z-index: 1800;
padding: 0 38rpx;
.sheet_popup_div {
width: 100%;
height: 100%;
background-color: #ffffff;
padding: 10rpx 34rpx;
border-radius: 34rpx 34rpx 0px 0px;
position: relative;
.title_div {
width: 100%;
padding: 30rpx 0;
font-family: PingFangSC;
font-weight: 600;
font-size: 30rpx;
color: #000000;
font-style: normal;
border-bottom: 2rpx solid #efefef;
}
.note_div {
padding: 20rpx 0 80rpx 0;
}
}
}
@@ -0,0 +1,194 @@
// 坠落动画配置变量 - 方便调整效果
$fall-duration: 0.26s; // 动画总时长
$initial-scale: 2.8; // 初始大小倍数
$impact-scale: 0.9; // 撞击瞬间的压缩倍数
$final-scale: 1; // 最终稳定大小
$primary-color: #ffe271; // 文字颜色
.matching-success-text {
margin-top: 10%;
padding-left: 1rem;
// 基础样式
font-size: 74rpx;
font-weight: bold;
color: #ffe271;
text-align: center;
// 动画属性
animation: fallDown $fall-duration ease-out forwards;
// 坠落动画关键帧
@keyframes fallDown {
0% {
// 初始状态:高处坠落的起点
transform: scale($initial-scale);
opacity: 0.8;
}
80% {
// 撞击瞬间:略微压缩,增强冲击力
transform: scale($impact-scale);
opacity: 1;
}
100% {
// 稳定状态:恢复正常大小
transform: scale($final-scale);
opacity: 1;
}
}
// 增强版:带震动效果的撞击(可选)
&.with-vibration {
animation: fallWithVibration $fall-duration cubic-bezier(0.21, 0.98, 0.6, 0.99) forwards;
@keyframes fallWithVibration {
0% {
transform: scale($initial-scale) rotate(0deg);
opacity: 0.8;
}
70% {
transform: scale($impact-scale) rotate(2deg);
opacity: 1;
}
85% {
transform: scale($final-scale * 1.05) rotate(-1deg);
opacity: 1;
}
100% {
transform: scale($final-scale) rotate(0deg);
opacity: 1;
}
}
}
}
// 可通过修改变量快速调整效果
// 示例:更重的坠落效果
.heavy-impact {
$fall-duration: 0.3s;
$initial-scale: 2.5;
$impact-scale: 0.8;
animation: fallDown $fall-duration ease-out forwards;
}
.matching-success-msg {
margin-top: 20%;
font-family: Alibaba-PuHuiTi, Alibaba-PuHuiTi;
font-weight: normal;
font-size: 74rpx;
color: #ffffff;
font-weight: 700;
margin-bottom: 10%;
}
/**匹配成功样式*/
.matching-success-animation {
display: flex;
width: 100%;
justify-content: space-between;
height: 540rpx;
position: relative;
// overflow: hidden;
.left {
position: absolute;
width: calc(59vw);
left: -10rpx;
top: 0%;
z-index: 3;
}
.right {
position: absolute;
right: -10rpx;
top: 18vw;
width: calc(59vw);
z-index: 2;
}
.img {
width: 100%;
}
// 左侧元素回弹动画
.animate-left {
animation: leftCollision 0.24s ease-out forwards;
}
@keyframes leftCollision {
0% {
transform: translateX(-100%);
} // 初始位置
90% {
transform: translateX(10rpx);
} // 超过目标位置10rpx(冲刺)
100% {
transform: translateX(0);
} // 回到目标位置(回弹)
}
// 右侧元素回弹动画
.animate-right {
animation: rightCollision 0.24s ease-out forwards;
}
@keyframes rightCollision {
0% {
transform: translateX(100%);
} // 初始位置
90% {
transform: translateX(-10rpx);
} // 超过目标位置10rpx(冲刺)
100% {
transform: translateX(0);
} // 回到目标位置(回弹)
}
// 上面的双方头像
.information-div-left,
.information-div-right {
display: flex;
flex-direction: column;
align-items: center;
.avatar-img-div {
width: 120rpx;
height: 120rpx;
border: 4rpx solid #ffffff;
border-radius: 50%;
overflow: hidden;
.img {
width: 100%;
height: 100%;
}
}
.name {
font-size: 30rpx;
margin-top: 20rpx;
margin-bottom: 100rpx;
font-weight: 600;
font-size: 36rpx;
color: #ffffff;
}
.org {
font-weight: 400;
font-size: 30rpx;
color: #ffffff;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 160rpx;
}
}
.information-div-left {
position: absolute;
top: 0%;
left: 0%;
transform: translate(24%, -20%);
}
.information-div-right {
position: absolute;
right: 0%;
top: 0%;
transform: translate(-24%, -20%);
}
}
+9 -3
View File
@@ -9,6 +9,9 @@
<!-- 3. 最后显示默认标题 -->
<template v-else>{{ title }}</template>
</view>
<view v-if="msg" class="msg_div">
{{ msg }}
</view>
<view class="button_div">
<view class="button_item" v-for="item in buttonList" :style="item?.style" @click="click_button(item)">
{{ item?.name || '' }}
@@ -25,12 +28,14 @@
dialogConfiguration: {
type: Object,
required: false
}
},
});
const emit = defineEmits(['button_click']);
const title = ref('');
const html = ref('');
const msg = ref('');
const buttonList = ref([]);
const PopupRef = ref(null);
// 点击按钮
@@ -41,9 +46,10 @@
const close = () => PopupRef.value?.close();
const open = (dialogConfiguration) => {
// 解构赋值更清晰
const { title: t, html: h, buttonList: bl = [] } = dialogConfiguration || props.dialogConfiguration || {};
const { title: t, html: h, msg: m, buttonList: bl = [] } = dialogConfiguration || props.dialogConfiguration || {};
title.value = t || '';
html.value = h || '';
msg.value = m || '';
buttonList.value = Array.isArray(bl) ? bl : [];
// 确保PopupRef存在再调用open
@@ -62,7 +68,7 @@
z-index: 1810;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
background-color: rgba(0, 0, 0, 0.5);
}
.popup_div {
@@ -143,7 +143,7 @@
onUnmounted(() => {
removeListener();
});
const open = (qnsId) => {
feedbackInfo.qnsId = qnsId;
openFeedback(true);
+1 -1
View File
@@ -10,7 +10,7 @@
<view class="talking-cont">
<MarkdownPreview :mdString="mdText" />
</view>
<view class="talking-link" v-show="dataInfo.asrTime > 0">
<view class="talking-link">
<view class="link-cont-desc">语音转文字的耗时{{ dataInfo.asrTime }}ms</view>
<!-- <view class="link-cont-desc">请求的文字{{ dataInfo.reqContent }}</view> -->
<view class="link-cont-desc">请求内容的字符长度{{ dataInfo.reqContSize }}</view>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -138,7 +138,8 @@
<image class="icon" src="@/static/images/index/navigation_examination.png"></image>
<view class="title">考试中心</view>
</view>
<view class="item">
<view class="item" @click="common.navigateTo('/pages/competition/index')">
<image class="icon" src="@/static/images/index/navigation_practice.png"></image>
<view class="title">竞赛练习</view>
</view>
+10 -5
View File
@@ -38,7 +38,7 @@
<view class="fl1"></view>
</view>
<!-- 显示当前选中的徽章 -->
<!-- <view class="dan" @click="changeBadge">
<view class="dan" @click="changeBadge">
<view class="dan_img">
<image
class="img_100"
@@ -48,18 +48,18 @@
></image>
</view>
<view class="dan_text">{{ badgeList[currentIndex].name }}</view>
</view> -->
</view>
</view>
<!-- 用户学习时间信息 -->
<view class="study_information">
<view class="item">
<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>
</view>
</view>
<view class="item-interval"></view>
<view class="item">
<view class="item" @click="common.navigateTo('/pages/pointsAndRank/rank')">
<view class="top">徽章墙</view>
<view class="bottom">
<uv-count-to :startVal="0" :endVal="0" :useEasing="true" ref="accmTaskCntRef"></uv-count-to>
@@ -101,7 +101,12 @@
</view>
<view class="cell_div">
<uv-cell-group :border="false">
<uv-cell title="问答演示" v-if="user_info.loginName === 'ADMIN'" :isLink="true" @click="common.navigateTo('/pages/example/dialog')"></uv-cell>
<uv-cell
title="问答演示"
v-if="user_info.loginName === 'ADMIN'"
:isLink="true"
@click="common.navigateTo('/pages/example/dialog')"
></uv-cell>
<uv-cell
title="消息通知"
:isLink="true"
+265
View File
@@ -0,0 +1,265 @@
<template>
<z-paging
ref="pagingRef"
v-model="data_list"
@query="queryList"
class="scroll_div"
empty-view-text="暂无积分记录"
paging-class="z-paging-class"
>
<template #top>
<view class="top-main-div">
<view class="content">
<view class="body-title">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view>积分明细</view>
</view>
<view class="fl1"></view>
<view class="my-points">
<view class="msg">我的积分</view>
<view class="score">
<uv-count-to
:startVal="startVal"
:endVal="endVal"
:useEasing="true"
ref="countToRef"
></uv-count-to>
</view>
</view>
</view>
</view>
</template>
<view class="scroll-Y">
<view class="item_th">
<view class="title">事项明细</view>
<view class="time">时间</view>
<view class="value">积分变化</view>
</view>
<view class="item" v-for="(item, index) in data_list">
<view class="title ellipsis-text">
{{ item.mattrDesc }}
</view>
<view class="time">
{{ item.ctTime.substr(0, 16) }}
</view>
<view class="value" :class="Number(item.changeValue) >= 0 ? 'up' : 'down'">
{{ item.changeValue }}
</view>
</view>
</view>
</z-paging>
</template>
<script setup>
//
import { ref, reactive, watch, onMounted } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
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();
});
return queryPointsDetail({ ...params });
};
const { pagingRef, queryList, data_list, search } = useZpaging({
fetchFunction
});
onLoad(() => {});
onUnload(() => {});
</script>
<style lang="scss" scoped>
.z-paging-class {
background-color: #e0e3ef;
::v-deep .zp-view-super {
background-color: #ffffff;
margin: 0 24rpx;
height: 100%;
border-radius: 30rpx 30rpx 0 0;
}
}
.top-main-div {
/* 宽度占满父容器 */
width: 100%;
/* 隐藏超出容器的背景图部分 */
overflow: hidden;
/* 使用 padding-top 实现 78:35 的宽高比(计算公式:35/78*100% ≈ 44.87% */
padding-top: 44.87%;
/* 背景图设置,确保图片覆盖且不重复 */
background-image: url('@/static/images/pointsAndRank/bg.png');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
/* 若盒子内有内容,需用相对定位让内容脱离文档流 */
position: relative;
margin-bottom: -44rpx;
.content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.my-points {
padding-bottom: 84rpx;
padding-left: 46rpx;
display: flex;
align-items: flex-end;
.msg {
font-weight: 400;
font-size: 26rpx;
color: #ffffff;
text-align: center;
padding-bottom: 4rpx;
}
.score {
margin-left: 16rpx;
width: 112px;
::v-deep .uv-count-num {
font-family: DINAlternate, DINAlternate !important;
font-weight: bold !important;
font-size: 77rpx !important;
color: #ffffff !important;
line-height: 77rpx !important;
}
}
}
}
.item_th {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 4rpx solid #efefef;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 26rpx;
color: #999999;
text-align: left;
padding: 32rpx 38rpx 32rpx 26rpx;
.title {
flex: 1;
}
.time {
min-width: max(150rpx, 42%);
}
.value {
text-align: right;
min-width: max(80rpx, 20%);
}
}
.item {
background: #ffffff;
border-bottom: 2rpx solid #efefef;
overflow: hidden;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 38rpx 40rpx 26rpx;
.title {
font-weight: 700;
font-size: 30rpx;
color: #333333;
flex: 1;
text-align: left;
}
.time {
font-family: ArialMT;
font-size: 30rpx;
color: #000000;
text-align: left;
min-width: max(150rpx, 42%);
}
.value {
font-family: Arial, Arial;
font-weight: normal;
font-size: 30rpx;
text-align: right;
font-weight: 600;
min-width: max(80rpx, 20%);
}
.up {
color: #ff8a2d;
}
.down {
color: #48ae80;
}
}
.scroll-Y {
overflow: hidden;
}
.body-title {
width: 100%;
height: 70rpx;
font-size: 33rpx;
font-weight: 500;
color: #ffffff;
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 #ffffff;
border-left: 4rpx solid #ffffff;
transform: rotate(-45deg);
}
}
}
</style>
+257
View File
@@ -0,0 +1,257 @@
<template>
<z-paging
ref="pagingRef"
v-model="data_list"
@query="queryList"
class="scroll_div"
:layout-only="true"
empty-view-text="暂无积分记录"
paging-class="z-paging-class"
>
<template #top>
<view class="top-main-div">
<view class="content">
<view class="body-title">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view>积分段位说明</view>
</view>
<view class="fl1"></view>
<view class="my-rank">
<view class="left">
<view class="fl1"></view>
<view class="note">当前段位{{dataInfo.currentRankName}}</view>
<view class="note">
我的累计积分
<text class="score">{{dataInfo.currentPoints}}/{{dataInfo.countPoints}}</text>
</view>
</view>
<view class="fl1"></view>
<view class="right">
<image-preview class="img_100" :src="dataInfo.ossAddr" mode="widthFix"></image-preview>
</view>
</view>
</view>
</view>
</template>
<view class="scroll-Y">
<view class="item_th">
<view class="title">段位</view>
<view class="score">积分范围</view>
<view class="value">标识</view>
</view>
<view class="item" v-for="(item, index) in data_list">
<view class="title ellipsis-text">
{{ item.rankName }}
</view>
<view class="score">{{ item.pointsStart }}~{{ item.pointsEnd }}</view>
<view class="icon" :class="Number(item.changeValue) >= 0 ? 'up' : 'down'">
<image-preview class="img_100" :src="item.ossAddr" mode="heightFix"></image-preview>
</view>
</view>
</view>
</z-paging>
</template>
<script setup>
//
import { ref, reactive, watch, onMounted } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
import useZpaging from '@/composables/useZpaging.js';
import common from '@/common/common';
import { queryRankDetail, queryCurrentStatus } from '@/api/pointsAndRank.js';
const dataInfo = reactive({
countPoints: 0, // int
currentPoints: 0, // int
currentRankName: '', // String
ossAddr: '', //
badgeNum: 0 // int
});
const fetchFunction = (params) => {
queryCurrentStatus().then(({ body }) => {
const { countPoints, currentPoints, currentRankName, badgeNum, ossAddr } = body;
Object.assign(dataInfo, {
countPoints,
currentPoints,
currentRankName,
badgeNum,
ossAddr,
});
});
return queryRankDetail({ ...params });
};
const { pagingRef, queryList, data_list, search } = useZpaging({
fetchFunction
});
onMounted(() => {
pagingRef.value.reload();
});
onUnload(() => {});
</script>
<style lang="scss" scoped>
.z-paging-class {
background-color: #e0e3ef;
::v-deep .zp-view-super {
background-color: #ffffff;
// margin: 0 24rpx;
height: 100%;
border-radius: 30rpx 30rpx 0 0;
}
}
.top-main-div {
width: 100%;
overflow: hidden;
padding-top: calc(44.87% + var(--status-bar-height));
background: linear-gradient(to right, #78238d, #5b25a0, #3b28b6);
position: relative;
margin-bottom: -44rpx;
.content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.my-rank {
padding-bottom: 84rpx;
padding-left: 46rpx;
display: flex;
align-items: flex-end;
.left {
.score {
font-family: DINAlternate, DINAlternate;
font-weight: bold;
font-size: 33rpx;
color: #ffffff;
line-height: 38rpx;
text-align: left;
}
.note {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 25rpx;
color: #ffffff;
line-height: 35rpx;
text-align: left;
}
}
.right {
width: 124rpx;
margin-right: 60rpx;
}
}
}
.item_th {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 4rpx solid #efefef;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 26rpx;
color: #999999;
text-align: left;
padding: 30rpx 38rpx 30rpx 26rpx;
.title {
text-align: left;
min-width: max(150rpx, 42%);
}
.score {
flex: 1;
// min-width: max(150rpx, 42%);/
}
.icon {
// min-width: max(80rpx, 20%);
}
}
.item {
background: #ffffff;
border-bottom: 2rpx solid #efefef;
overflow: hidden;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 26rpx 38rpx 26rpx 26rpx;
.title {
font-weight: 700;
font-size: 30rpx;
color: #333333;
text-align: left;
min-width: max(150rpx, 42%);
}
.score {
flex: 1;
font-family: ArialMT;
font-size: 30rpx;
color: #000000;
}
.icon {
height: 44rpx;
}
}
.scroll-Y {
overflow: hidden;
}
.body-title {
width: 100%;
height: 70rpx;
font-size: 33rpx;
font-weight: 500;
color: #ffffff;
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 #ffffff;
border-left: 4rpx solid #ffffff;
transform: rotate(-45deg);
}
}
}
</style>
+2 -2
View File
@@ -114,7 +114,7 @@
const storageTopicList = ref([]); //
const popup_input_bottom = ref('0px');
const questionNumber = ref(0); //
const systemInfo = uni.getSystemInfoSync();
const topic = computed({
get: () => topicList[questionNumber.value],
@@ -747,7 +747,7 @@
}
}
}
.main_div {
display: flex;
flex-direction: column;
+1 -10
View File
@@ -67,16 +67,7 @@
const mascotName = computed(() => userInfo.value.mascotInfo?.mascotName || '');
const sexOptions = GENDER;
const anonymousOptions = [
{
value: 'y',
label: '是'
},
{
value: 'n',
label: '否'
}
]; //
//
const gotoMascot = () => {
+65 -2
View File
@@ -123,7 +123,7 @@
"style": {
"navigationBarTitleText": "错题本",
"app-plus": {
"titleNView": false
"titleNView": false
}
}
},
@@ -246,7 +246,70 @@
"titleNView": false
}
}
},
{
"path": "pages/competition/index",
"style": {
"navigationBarTitleText": "竞赛列表页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/matching",
"style": {
"navigationBarTitleText": "匹配页面",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/pk",
"style": {
"navigationBarTitleText": "pk页面",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/result",
"style": {
"navigationBarTitleText": "pk结果页面",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competition/ranking",
"style": {
"navigationBarTitleText": "pk排行榜",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/pointsAndRank/points",
"style": {
"navigationBarTitleText": "积分变动明细",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/pointsAndRank/rank",
"style": {
"navigationBarTitleText": "段位列表",
"app-plus": {
"titleNView": false
}
}
}
]
}
+5 -5
View File
@@ -3,11 +3,11 @@
{
"path": "pages/index/dialog",
"style": {
"navigationBarTitleText": "AI问答",
"app-plus": {
"titleNView": false,
"animationType": "fade-in"
}
"navigationBarTitleText": "AI问答",
"app-plus": {
"titleNView": false,
"animationType": "fade-in"
}
}
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+20
View File
@@ -0,0 +1,20 @@
// 声明图片文件模块
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
// Vue
"moduleResolution": "NodeNext", // "Node"
"module": "NodeNext", // moduleResolution
"target": "ES2022",
"jsx": "preserve",
"lib": ["ES2022", "DOM"],
"baseUrl": ".", // tsconfig.json
"types": ["vue"], // vue
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+12 -6
View File
@@ -1,8 +1,14 @@
import { defineConfig } from 'vite'
import {
defineConfig
} from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
// https://vitejs.dev/config/
import path from 'path'
export default defineConfig({
plugins: [
uni(),
],
})
plugins: [uni()],
// 新增服务器配置,指定启动端口
server: {
port: 5174,
strictPort: true, // 可选:端口被占用时直接报错(避免自动切换端口)
open: false // 可选:启动后自动打开浏览器(按需开启)
}
})