This commit is contained in:
田岩
2025-12-03 20:54:23 +08:00
parent f250b21b38
commit 11ce05edcf
972 changed files with 121839 additions and 824 deletions
+39
View File
@@ -0,0 +1,39 @@
# 开发环境
ENV = 'development'
# 'development'
# VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn: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_TRAAPP = 'http://25.64.32.150:9601'
# dev
#VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
# sit
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
+8
View File
@@ -0,0 +1,8 @@
# 任东专用开发环境
ENV = 'development.rd'
# 'development.rd'
#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
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'
+14
View File
@@ -0,0 +1,14 @@
# 杨航开发环境
ENV = 'development.yh'
# 'development.yh'
# 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_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604'
# VITE_APP_BASE_H5_API_Url_TRAPRACTICE = 'http://25.64.32.156:9603'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.154:9601'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.158:9601'
+5
View File
@@ -0,0 +1,5 @@
# 开发环境
ENV = 'development'
# 'development'
VITE_APP_BASE_API_Url = ''
+5
View File
@@ -0,0 +1,5 @@
# 生产环境
ENV = 'production'
# base api
VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
+5
View File
@@ -0,0 +1,5 @@
# SIT环境
ENV = 'sit'
# base api
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
+5
View File
@@ -0,0 +1,5 @@
# UAT环境
ENV = 'uat'
# base api
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7003'
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
package-lock
package-lock.json
node_modules
.DS_Store
dist
unpackage
*.local
build
release
# Editor directories and files
.idea
.svn
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+9
View File
@@ -0,0 +1,9 @@
npm install --registry=http://25.12.10.69:8081/repository/aliyun-npm/
npm 淘宝源下载 npm config set registry https://registry.npmmirror.com
npm config set registry http://25.12.10.69:8081/repository/aliyun-npm/
npm install --registry=https://registry.npmmirror.com
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
{
"name": "tra-app",
"version": "0.0.1",
"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",
"build:app-plus:dev": "uni build -p app-plus --mode development",
"build:app-plus:develop": "uni build -p app-plus --mode development",
"build:app-plus:sit": "uni build -p app-plus --mode sit",
"build:app-plus:uat": "uni build -p app-plus --mode uat",
"build:app-plus:prod": "uni build -p app-plus",
"build:h5:dev": "uni build -p h5 --mode development_h5",
"build:h5": "uni build"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4060620250520001",
"@dcloudio/uni-app-harmony": "3.0.0-4060620250520001",
"@dcloudio/uni-app-plus": "3.0.0-4060620250520001",
"@dcloudio/uni-components": "3.0.0-4060620250520001",
"@dcloudio/uni-h5": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-harmony": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-jd": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-lark": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-qq": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
"crypto-js": "^3.1.9-1",
"dompurify": "^3.3.0",
"markdown-it": "^14.1.0",
"pinia": "^2.3.1",
"terser": "^5.42.0",
"uuid": "^11.1.0",
"vue": "^3.5.11",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
"@vue/runtime-core": "^3.4.21",
"sass": "1.77.0",
"vite": "5.2.8"
}
}
+10
View File
@@ -0,0 +1,10 @@
/// <reference types='@dcloudio/types' />
import 'vue'
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {
}
}
+153
View File
@@ -0,0 +1,153 @@
<script>
export default {
onLaunch: function () {
uni.onTabBarMidButtonTap(() => {
uni.navigateTo({
url: '/pages/index/dialog',
animationType: 'slide-in-bottom'
});
});
},
onShow: function () {
},
onHide: function () {
}
};
</script>
<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
// @import "@/uni_modules/uview-ui/index.scss";
@font-face {
font-family: 'PingFangSC';
src: url('@/static/font/PingFang-SC.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
view {
box-sizing: border-box;
font-family: 'PingFangSC';
}
// 解决uview离线环境下没有图标
@font-face {
font-family: 'uicon-iconfont';
src: url('@/static/font/font_2225171_8kdcwk4po24.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
.max_page {
min-height: 100vh;
}
.mr-10 {
margin-right: 10rpx;
}
.mb-8 {
margin-bottom: 8rpx;
}
.pt-8 {
padding-top: 8rpx;
}
.mb-10 {
margin-bottom: 10rpx;
}
.mb-18 {
margin-bottom: 18rpx;
}
.mb-64 {
margin-bottom: 64rpx;
}
.mb-30 {
margin-bottom: 30rpx;
}
.mb-36 {
margin-bottom: 36rpx;
}
.mb-4 {
margin-bottom: 4rpx;
}
.fz-30 {
font-size: 30rpx;
}
.fz-28 {
font-size: 28rpx;
}
.fz-24 {
font-size: 24rpx;
}
.w_100 {
width: 100%;
}
.h_100 {
height: 100%;
}
.flex {
display: flex;
}
.flex-jcsb {
display: flex;
justify-content: space-between;
}
.flex-c-c {
display: flex;
justify-content: center;
align-items: center;
}
.tar {
text-align: right;
}
.fw-600 {
font-weight: 600;
}
.fw-400 {
font-weight: 400;
}
.img_100 {
width: 100%;
height: 100%;
}
.color-666{
color: #666666;
}
.fl1 {
flex: 1;
}
.font_pf {
font-family: 'PingFangSC';
}
.ellipsis-text {
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 超出部分显示省略号 */
}
/* 多行文本溢出 */
.multi-line-ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
/* 显示的行数 */
overflow: hidden;
}
.no-overflow {
overflow: hidden;
}
.bold {
font-weight: bold;
}
.maoh{
margin-right: 8rpx;
margin-left: 2rpx;
}
</style>
+38
View File
@@ -0,0 +1,38 @@
{
"version": "1",
"prompt": "template",
"title": "个人信息保护提示",
"message": "欢迎使用吉AI学!<br/>  请你务必审慎阅读、充分理解<a href=\"https://aits.jlbank.com.cn:7001/traapp/dfAgtInfo/queryValidDfAgtHtml\">《用户隐私协议》</a>各条款,帮助您了解我们为您提供的服务、我们如何处理个人信息以及您享有的权利。我们会严格按照相关法律法规要求,采取各种安全措施来保护您的个人信息。<br/>  如果你同意,请点击下面按钮开始接受我们的服务。<br/>1.为了保障软件的安全运行和账户安全,我们会申请手机您的设备信息、IP地址、WLAN MAC地址。<br/>2.上传或拍摄图片,需要使用您的媒体影音、图片、视频、音频、相机、等权限。<br/>3.为了实现AI问答、课程学习等APP内功能,我们需要使用您的麦克风权限。",
"buttonAccept": "同意并接受",
"buttonRefuse": "暂不同意",
"hrefLoader": "system",
"backToExit":"true",
"second": {
"title": "确认提示",
"message": "  进入应用前,你需先同意<a href=\"https://aits.jlbank.com.cn:7001/traapp/dfAgtInfo/queryValidDfAgtHtml\">《用户隐私协议》</a>,否则将退出应用。",
"buttonAccept": "同意并继续",
"buttonRefuse": "退出应用"
},
"disagreeMode":{
"support": false,
"loadNativePlugins": false,
"visitorEntry": false,
"showAlways": false
},
"styles": {
"backgroundColor": "#fff",
"borderRadius":"5px",
"title": {
"color": "#000"
},
"buttonAccept": {
"color": "#F91B59"
},
"buttonRefuse": {
"color": "#333"
},
"buttonVisitor": {
"color": "#00ffff"
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import request from '@/api/request'
// 1.本月/本年/累计 学习信息查询接口
export const queryStudyStatistics = (data) => {
return request({
url: '/traapp/traStudyAnalysis/queryStudyStatistics',
method: 'post',
data
});
};
// 课程推荐查询接口
export const queryRecmdCrsInfo = (data) => {
return request({
url: '/traapp/traStudyAnalysis/queryRecmdCrsInfo',
method: 'post',
data
});
};
// 4.维度分析查询接口
export const queryDimesionInfo = (data) => {
return request({
url: '/traapp/traStudyAnalysis/queryDimesionInfo',
method: 'post',
timeout: 60000,
data
});
};
// 5.学习习惯查询接口
export const queryHabitInfo = (data) => {
return request({
url: '/traapp/traStudyAnalysis/queryHabitInfo',
method: 'post',
timeout: 60000,
data
});
};
// 6.学习建议查询接口
export const querySuggestInfo = (data) => {
return request({
url: '/traapp/traStudyAnalysis/querySuggestInfo',
method: 'post',
timeout: 60000,
data
});
};
+90
View File
@@ -0,0 +1,90 @@
import { getToken } from '@/common/common.js'
import { get_base_url } from '@/api/request'
const base_url = get_base_url();
const token = getToken();
// 基础配置
const baseConfig = {
enableChunked: true, // 必须开启分块传输
responseType: 'arraybuffer', // 微信小程序需用arraybuffer
timeout: 30000 // 超时时间延长
}
// 流式请求核心实现
function streamRequest(options) {
const {
url,
method = 'GET',
data,
headers = {},
onChunk,
onComplete,
onError
} = options
// headers['Accept'] = 'text/event-stream'
headers['Content-Type'] = 'text/event-stream'
headers['summary'] = token
// 创建请求任务
const requestTask = uni.request({
url,
method,
data,
header: headers,
responseType: 'arraybuffer',
...baseConfig,
success: (res) => {
console.log(res.data)
onComplete?.(res)
},
fail: (err) => {
onError?.(err)
}
})
console.log(requestTask)
// 流式数据处理器
let bufferCache = ''
requestTask.onChunkReceived((res) => {
try {
// ArrayBuffer转字符串(兼容多平台)
const uint8Array = new Uint8Array(res.data)
const chunkText = bufferCache +
String.fromCharCode.apply(null, uint8Array)
// 处理SSE格式数据(data:开头)
const events = chunkText.split('\n\n')
bufferCache = events.pop() || '' // 缓存不完整数据
events.forEach(event => {
if (event.startsWith('data:')) {
onChunk?.(event.substring(5).trim())
}
})
} catch (e) {
onError?.(e)
}
})
return {
abort: () => requestTask.abort()
}
}
// 使用示例
export const demoUsage = (data) => {
const controller = streamRequest({
url: `${base_url}/trastudy/intgask/askAbout`,
data: data,
onChunk: (chunk) => {
console.log('收到数据块:', chunk)
// 实时更新UI逻辑
},
onComplete: () => console.log('传输完成'),
onError: (err) => console.error('错误:', err)
})
// 需要中断时调用
// controller.abort()
}
+10
View File
@@ -0,0 +1,10 @@
import request from '@/api/request'
// 获取list
export const leaderBoardApi = (data) => {
return request({
url: '/traapp/myLeaderBoard/leaderBoard ',
method: 'post',
data
});
};
+272
View File
@@ -0,0 +1,272 @@
import {
get_base_url,
REQUES_ERROR_CODES,
goto_login_fun
} from '@/api/request'
import {
getToken
} from '@/common/common.js'
import common from '@/common/common';
export const uploadFile = (file, data) => {
const url = '/traoss/tras3/upload';
const base_url = get_base_url(url);
const token = getToken();
const header = {};
console.log('file', file);
if (token)
header['summary'] = token
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${base_url}${url}`,
header: header,
filePath: file,
name: 'file',
formData: data,
success(result) {
try {
const res = JSON.parse(result.data);
if (res.rtnCode === '0000') {
resolve(res.body);
} else {
reject(new Error(res.msg));
}
} catch (error) {
reject(new Error('Failed to parse response'));
}
},
fail(error) {
console.log('error', error);
reject(new Error('Upload failed'));
}
});
});
};
export const getPreviewFileUrl = (id) => {
return get_base_url() + `/traoss/tras3/show/${id}`
}
// 对象存储下载 /traoss/tras3/show/{文件ID}
export function downloadFile(id) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url();
return new Promise((resolve, reject) => {
uni.downloadFile({
url: `${baseUrl}/traoss/tras3/show/${id}`,
header,
success(result) {
resolve(result)
},
fail(error) {
reject(new Error(error));
}
});
});
}
// 上传音频 通用
export const commonUploadVoiceFile = (file, url, data = {}, fileKey = 'voice') => {
const base_url = get_base_url(url);
const token = getToken();
const header = {
"Content-Type": "multipart/form-data; charset=UTF-8",
};
if (token)
header['summary'] = token
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${base_url}${url}`,
header: header,
filePath: file,
name: fileKey,
formData: data,
timeout: 60000,
success(result) {
try {
console.log(result)
const res = JSON.parse(result.data);
if (res.rtnCode === '0000') {
return resolve(res);
} else if (REQUES_ERROR_CODES['NO_LOGIN'].includes(res.rtnCode)) {
goto_login_fun()
return reject('NO_LOGIN');
} else if (res.rtnCode === '0004') {
return reject('N');
}
return reject('Other');
} catch (error) {
reject(new Error('Failed to parse response'));
}
},
fail(error) {
return reject('Other');
}
});
});
};
// 上传音频 翻译文本
export const uploadVoiceFile = (file) => {
const url = '/trastudy/traStdyInfo/audioTranscriptions';
const base_url = get_base_url(url);
const token = getToken();
const header = {
"Content-Type": "multipart/form-data; charset=UTF-8",
};
if (token)
header['summary'] = token
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${base_url}${url}`,
header: header,
filePath: file,
name: 'voice',
success(result) {
try {
const res = JSON.parse(result.data);
if (res.rtnCode === '0000') {
resolve(result);
} else {
reject(new Error(res.message));
}
} catch (error) {
reject(new Error('Failed to parse response'));
}
},
fail(error) {
reject(new Error('Upload failed'));
}
});
});
};
// 获取音频 通用
export function downloadVoiceFile(url) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url(url);
return new Promise((resolve, reject) => {
uni.downloadFile({
url: `${baseUrl}${url}`,
header,
timeout: 180000,
success(result) {
console.log(result)
resolve(result)
},
fail(error) {
console.log(error)
reject(new Error(error));
}
});
});
}
// 获取音频 通用参数
export function downloadVoiceParams(url) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url(url);
return {
url: `${baseUrl}${url}`,
header,
timeout: 180000,
}
}
export function getAudioByText(data) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url();
return new Promise((resolve, reject) => {
uni.request({
url: `${baseUrl}/trastudy/traStdyInfo/readContent`,
header,
method: "POST",
data,
responseType: "arraybuffer",
success(result) {
if (result.statusCode === 200) {
resolve(result.data)
} else {
reject(new Error(result))
}
},
fail(error) {
reject(new Error(error));
}
});
});
}
// 对象存储预览 /traoss/tras3/show/{文件ID}
export function previewFile(id, responseType = 'arraybuffer') {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url();
return new Promise((resolve, reject) => {
uni.request({
url: `${baseUrl}/traoss/tras3/show/${id}`,
header,
responseType,
success(result) {
resolve(result)
},
fail(error) {
reject(new Error(error));
}
});
});
}
// 缩略图预览 /traoss/tras3/getCacheThumbImage/{fileId}/{width}/{height}
export function previewCacheThumbImage(id, width = 30, height = 30) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url();
return new Promise((resolve, reject) => {
uni.request({
url: `${baseUrl}/traoss/tras3/getCacheThumbImage/${id}/${width}/${height}`,
header,
success(result) {
if (result.data.rtnCode === '0000') {
let base64 = `data:image/png;base64,` + result.data.body
resolve(base64)
} else {
reject({
error: result.data.message
})
}
},
fail(error) {
reject(new Error(error));
}
});
});
}
+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
});
};
+72
View File
@@ -0,0 +1,72 @@
import request from '@/api/request'
const base_url = '/traapp'
const course_base_url = '/trastudy'
// 获取课程分类树接口
export const getCourseTypeListApi = (data) => {
return request({
url: base_url + '/app/traCrsCatalogInfo/courseQueryTraCrsCatalogInfoList',
method: 'post',
data
});
};
// 获取生产线分类树接口
export const getPrdLineTypeListApi = (data) => {
return request({
url: base_url + '/app/traPrdLineInfo/courseQueryTraPrdLineInfoList',
method: 'post',
data
});
};
// 获取标签分类树 接口
export const getTraTagListApi = (data) => {
return request({
url: base_url + '/app/traTagCatalogInfo/queryTraTagCatalogInfoList',
method: 'post',
data
});
};
// 全部模块
export const queryCrsInfoByCatalogApi = (data) => {
return request({
url: course_base_url + '/crsInfo/queryCrsInfoByCatalog',
method: 'post',
data
});
};
// 生产线模块-
export const queryCrsInfoByPrdLineApi = (data) => {
return request({
url: course_base_url + '/crsInfo/queryCrsInfoByPrdLine',
method: 'post',
data
});
};
// 最新
export const queryCrsInfoByLastnewApi = (data) => {
return request({
url: course_base_url + '/crsInfo/queryCrsInfoByLastnew',
method: 'post',
data
});
};
// 最热
export const queryCrsInfoByPopularApi = (data) => {
return request({
url: course_base_url + '/crsInfo/queryCrsInfoByPopular',
method: 'post',
data
});
};
// 推荐
export const queryCrsInfoByRecmdApi = (data) => {
return request({
url: course_base_url + '/crsInfo/queryCrsInfoByRecmd',
method: 'post',
data
});
};
+72
View File
@@ -0,0 +1,72 @@
import request from '@/api/request'
const module_url = '/traapp'
// AITS-A-4006--APP课程详情查询-含知识点学习状态数据
export const getCourseDetailApi = (data) => {
return request({
url: module_url + '/crsInfo/queryCrsInfoDetail',
method: 'post',
data
});
};
// AITS-A-4007--APP课程评价列表数据查询(20250723预计废弃)
export const qryTraCrsBbsDataByCrsNum = (data) => {
return request({
url: module_url + '/traCrsBbsInfo/qryTraCrsBbsDataByCrsNum',
method: 'post',
data
});
};
//王洋新写
export const queryTraCrsBbsInfoByCrsId = (data) => {
return request({
url: module_url + '/traCrsBbsInfo/queryTraCrsBbsInfoByCrsId',
method: 'post',
data
});
};
// AITS-A-4008--APP课程评价下回复评论时刷新二级评论列表数据接口
export const qryChildTraCrsBbsInfo = (data) => {
return request({
url: module_url + '/traCrsBbsInfo/qryChildTraCrsBbsInfo',
method: 'post',
data
});
};
// AITS-A-4009--APP课程评价回复评论接口
export const replyTraCrsBbsInfo = (data) => {
return request({
url: module_url + '/traCrsBbsInfo/replyTraCrsBbsInfo',
method: 'post',
data
});
};
// AITS-A-4010--APP课程评价接口
export const publishCrsEvaluation = (data) => {
return request({
url: module_url + '/traCrsBbsInfo/publishCrsEvaluation',
method: 'post',
data
});
};
// 考试排行榜/traexam/traCrsPapers/queryTraCrsRankingList
export const queryTraCrsRankingList = (data) => {
return request({
url: '/traexam/traCrsPapers/queryTraCrsRankingList',
method: 'post',
data
});
};
+124
View File
@@ -0,0 +1,124 @@
import request from '@/api/request'
const base_url = '/traexam'
/**学习*/
// 学习列表页
export const queryStdyRecordPaging = (data) => {
return request({
url: base_url + '/traRecord/queryStdyRecordPaging',
method: 'post',
toastErrors: true,
data
});
};
// 学习单项列表 crsId
export const queryStdyBatchByCrsId = (data) => {
return request({
url: base_url + '/traRecord/queryStdyBatchByCrsId',
method: 'post',
toastErrors: true,
data
});
};
// 学习详情 通过 stdyId 查询
export const queryTraStudyExecuteRecord = (data) => {
return request({
url: base_url + '/traCrsPapers/queryTraStudyExecuteRecord',
method: 'post',
toastErrors: true,
data
});
};
/**练习*/
// 练习列表页
export const queryPracticeRecordPaging = (data) => {
return request({
url: base_url + '/traRecord/queryPracticeRecordPaging',
method: 'post',
toastErrors: true,
data
});
};
// 练习每一项列表
export const queryPracticeHis = (data) => {
return request({
url: base_url + '/traRecord/queryPracticeHis',
method: 'post',
toastErrors: true,
data
});
};
// 练习详情 通过 exrId 查询
export const queryPracticeRecordByExrId = (data) => {
return request({
url: base_url + '/traCrsPractice/queryPracticeRecordByExrId',
method: 'post',
toastErrors: true,
data
});
};
/**考试*/
export const queryCrsExamRecordPaging = (data) => {
return request({
url: base_url + '/traRecord/queryCrsExamRecordPaging',
method: 'post',
toastErrors: true,
data
});
};
// 考试每一项列表
export const queryCrsExamHisByExamId = (data) => {
return request({
url: base_url + '/traRecord/queryCrsExamHisByExamId',
method: 'post',
toastErrors: true,
data
});
};
/**考试中心考试*/
// 考试列表页
export const queryExamRecordPaging = (data) => {
return request({
url: base_url + '/traRecord/queryExamRecordPaging',
method: 'post',
toastErrors: true,
data
});
};
// 考试每一项列表
export const queryExamHisByExamId = (data) => {
return request({
url: base_url + '/traRecord/queryExamHisByExamId',
method: 'post',
toastErrors: true,
data
});
};
/**竞赛*/
// 查询竞赛记录
export const queryTraCompetitionRecordPaging = (data) => {
return request({
url: base_url + '/traCompetitionInfo/queryTraCompetitionRecordPaging',
method: 'post',
toastErrors: true,
data
});
};
// 查询竞赛答题列表
export const queryTraCompetitionRecordInfoPaging = (data) => {
return request({
url: base_url + '/traCompetitionInfo/queryTraCompetitionRecordInfoPaging',
method: 'post',
toastErrors: true,
data
});
};
+64
View File
@@ -0,0 +1,64 @@
import request from '@/api/request'
// 获取问题list
export const queryHotQuestionApi = (data) => {
return request({
// url: '/traask/traAskchat/queryHotQuestion',
url: '/traask/traAskchat/queryHotQuestions',
method: 'post',
data
});
};
// 获取问答历史
export const queryAskBatchSessionPaging = (data) => {
return request({
url: '/traask/traAskchatSession/queryAskBatchSessionPaging',
method: 'post',
data
});
};
// 问答详情 /traAskchatSession/queryAskHisByBatchIdPaging
export const queryAskHisByBatchIdPaging = (data) => {
return request({
url: '/traask/traAskchatSession/queryAskHisByBatchId',
method: 'post',
data
});
};
// 问答反馈
export const insertTraAskFeedback = (data) => {
return request({
url: '/traask/traAskFeedback/insertTraAskFeedback',
method: 'post',
data
});
};
// 取消反馈 取消踩 /traAskFeedback/updateDownvoteStat
export const updateDownvoteStat = (data) => {
return request({
url: '/traask/traAskFeedback/updateDownvoteStat',
method: 'post',
data
});
};
// 赞 /取消赞/traAskFeedback/updateDownvoteStat
export const updateLikeStat = (data) => {
return request({
url: '/traask/traAskFeedback/updateLikeStat',
method: 'post',
data
});
};
// example /traask/traTestchat/chatVoice
export const chatVoice = (data) => {
return request({
url: '/traask/traTestchat/chatVoice',
method: 'post',
timeout: 120000,
data
});
};
+90
View File
@@ -0,0 +1,90 @@
import CryptoJS from 'crypto-js'// 引入AES加密库 npm install crypto-js@3.1.9-1 -save
// import { JSEncrypt } from 'jsencrypt'; // npm install jsencrypt -save
const RsaPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZf8e1tzYTSr7KciAHH2KrE3O13ftuv3rHRRz7MztYXirMdLquocTNSJ5BAoj1H3V8fvFtfmN6BkuB6XnQrbY5heOzZZQWvO4qAa0EktwkLQUwCkYJfaQNc1Iw4wZFt4VO7U8+LLo+IO7jtxmaNIBqJm8laDc1C6zw2ETZD4bQ6wIDAQAB"
const iv = CryptoJS.enc.Utf8.parse('1234567812345678');
//定义rsa加密类
// const crypt = new JSEncrypt();
// crypt.setPublicKey(RsaPublicKey); // 设置公钥
// 生成一个随机的128位(16字节)AES密钥
const generateAESKey = () => {
return CryptoJS.lib.WordArray.random(16);
}
// 转化生成AES密钥成base64字符串
const getGenerateAESKeyBase64 = (aesKey) => aesKey.toString(CryptoJS.enc.Base64)
// 执行rsa加密
const encryptRSA = (str) => crypt.encrypt(str)
// 执行rsa解密
const decryptRSA = (str) => crypt.decrypt(str)
// 执行aes加密
export const encryptAES = (data, aesKeyBase64, iv) => {
const key = CryptoJS.enc.Utf8.parse(aesKeyBase64);
return CryptoJS.AES.encrypt(JSON.stringify(data), key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
}
// aes解密
const decryptAES = (ciphertext, aesKeyBase64, iv) => {
const key = CryptoJS.enc.Utf8.parse(aesKeyBase64);
const bytes = CryptoJS.AES.decrypt(ciphertext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
}
// 加密过程
export const encryptData = (data={}) => {
const aesKey = generateAESKey(); // 获取随机密钥key
const aesKeyBase64 = getGenerateAESKeyBase64(aesKey) // 变成base64
const key = encryptRSA(aesKeyBase64)
const ciphertext = encryptAES(data, aesKeyBase64, iv);
const decryptedData = decryptAES(ciphertext, aesKeyBase64, iv);
return {text:ciphertext, key:key, aesKeyBase64:aesKeyBase64}
}
// 解密过程
export const decryptData= (aesStr, aesKey) => {
return decryptAES(aesStr, aesKey, iv)
}
/**
* AES加密
* @param plainText 明文
* @param keyInBase64Str base64编码后的key
* @returns {string} base64编码后的密文
*/
export function encryptByAES(plainText, keyInBase64Str) {
let key = CryptoJS.enc.Base64.parse(keyInBase64Str);
let encrypted = CryptoJS.AES.encrypt(plainText, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
// 这里的encrypted不是字符串,而是一个CipherParams对象
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
/**
* AES解密
* @param cipherText 密文
* @param keyInBase64Str base64编码后的key
* @return 明文
*/
export function decryptByAES(cipherText, keyInBase64Str) {
let key = CryptoJS.enc.Base64.parse(keyInBase64Str);
// 返回的是一个Word Array Object,其实就是Java里的字节数组
let decrypted = CryptoJS.AES.decrypt(cipherText, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
+110
View File
@@ -0,0 +1,110 @@
import request from '@/api/request'
import { getToken } from '@/common/common.js'
import { get_base_url } from '@/api/request'
const base_url = '/traexam'
// 开始考试(试卷信息)
export const startTraExamPapersInfo = (data) => {
return request({
url: base_url + '/traExamPapers/startTraExamPapersInfo',
method: 'post',
toastErrors: true,
data
});
};
// 提交试卷
export const commitPaperExam = (data) => {
return request({
url: base_url + '/traExamPapers/commitPaperExam',
method: 'post',
toastErrors: true,
data
});
};
// 考试中心考试查分
export const queryPaperExamScore = (data) => {
return request({
url: base_url + '/traExamPapers/queryPaperExamScore',
method: 'post',
toastErrors: true,
data
});
};
// 根据试卷id查询详情
export const queryTraExamPapersByPapersId = (data) => {
return request({
url: base_url + '/traExamPapers/queryTraExamPapersByPapersId',
method: 'post',
toastErrors: true,
data
});
};
// ↑↑↑↑↑↑↑↑↑考试中心考试↑↑↑↑↑↑↑
// ↓↓↓↓↓↓↓↓↓课程考试↓↓↓↓↓↓↓↓
export const startExamByCrs = (data) => {
return request({
url: base_url + '/traCrsPapers/startExamByCrs',
method: 'post',
toastErrors: true,
data
});
};
// 提交课程考试
export const commitCrsExam = (data) => {
return request({
url: base_url + '/traCrsPapers/commitCrsExam',
method: 'post',
toastErrors: true,
data
});
};
// 试卷考试查分
export const queryCrsExamScore = (data) => {
return request({
url: base_url + '/traCrsPapers/queryCrsExamScore',
method: 'post',
toastErrors: true,
data
});
};
// 试卷考试 排行榜
export const queryPaperExamRankingList = (data) => {
return request({
url: base_url + '/traExamPapers/queryPaperExamRankingList',
method: 'post',
toastErrors: true,
data
});
};
// 反馈问题
export const saveTraQnsFeedback = (data) => {
return request({
url: base_url + '/traCrsPapers/saveTraQnsFeedback',
method: 'post',
toastErrors: true,
data
});
};
// 考试过程中提交结果,后端用来缓存(考试中心考试和课程考试共用这个缓存接口)
export const commitPaperCache = (data) => {
return request({
url: base_url + '/traExamPapers/commitPaperCache',
method: 'post',
toastErrors: true,
data
});
};
+49
View File
@@ -0,0 +1,49 @@
import request from '@/api/request'
const base_url = '/trastudy'
// 课程收藏分页查询
export const queryTraPersonalCollectionCrsPaging = (data) => {
return request({
url: base_url + '/traPersonalCollection/queryTraPersonalCollectionCrsPaging',
method: 'post',
toastErrors: true,
data
});
};
// 任务收藏分页查询
export const queryTraPersonalCollectionTaskPaging = (data) => {
return request({
url: base_url + '/traPersonalCollection/queryTraPersonalCollectionTaskPaging',
method: 'post',
toastErrors: true,
data
});
};
// 根据收藏ID删除收藏
export const deleteTraPersonalCollectionById = (data) => {
return request({
url: base_url + '/traPersonalCollection/deleteTraPersonalCollectionById',
method: 'post',
toastErrors: true,
data
});
};
/*
新增任务收藏
入参:
collectTyp 收藏类型 必须 String 01--课程,02--任务
collectBusId 收藏业务ID 必须 String
**/
export const insertTraPersonalCollectionTask = (data) => {
return request({
url: base_url + '/traPersonalCollection/insertTraPersonalCollectionTask',
method: 'post',
toastErrors: true,
data
});
};
+91
View File
@@ -0,0 +1,91 @@
import request from '@/api/request'
// 获取首页统计数据方法(查询个人学习统计信息 )
export const getStatisticsData = (data) => {
/**
userId 学员ID String
stdtTmLen 本年学习时长 double
accmPracticeCnt 累计训练次数 int
accmExamCnt 累计考试次数 int
**/
return request({
url: '/traapp/traStdyInfo/queryTraStdyStatisticsPerson',
method: 'post',
data
});
};
// 最新课程查询接口
export const queryCrsInfoByLastnew = (data) => {
/**
crsNum 课程编号 String
crsName 课程名称 String
tagId 课程分类 String
prdLineId 生产线分类 String
issuTm 发布日期 String
imgId 课程封面ID String
accmStdyCnt 累计学习次数 int
evalGrade 课程评分 Bigdecimal
tagCataList 课程标签集 List<Object>
tagId 标签ID String
tagName 标签名称 String
**/
return request({
url: '/traapp/mycrs/queryCrsInfoByLastnew',
method: 'post',
data
});
};
// 热门课程查询接口
export const queryCrsInfoByPopular = (data) => {
/**
tagId 标签ID(可选填),为空时查全部 必输项:false 类型:String
出参:
tagId 标签ID(可选填),为空时查全部 必输项:false 类型:String
出参:
crsId 课程ID StringcrsNum 课程编号 String
crsName 课程名称 String
tagId 课程分类 String
prdLineId 生产线分类 String
issuTm 发布日期 String
imgId 课程封面ID String
accmStdyCnt 累计学习次数 int
evalGrade 课程评分 Bigdecimal
tagCataList 课程标签集 List<Object>
tagId 标签ID String
tagName 标签名称 String
**/
return request({
url: '/traapp/mycrs/queryCrsInfoByPopular',
method: 'post',
data
});
};
// 推荐课程查询接口
export const queryCrsInfoByRecmd = (data) => {
/**
crsNum 课程编号 String
crsName 课程名称 String
tagId 课程分类 String
prdLineId 生产线分类 String
issuTm 发布日期 String
imgId 课程封面ID String
accmStdyCnt 累计学习次数 int
evalGrade 课程评分 Bigdecimal
tagCataList 课程标签集 List<Object>
tagId 标签ID String
tagName 标签名称 String
**/
return request({
url: '/traapp/mycrs/queryCrsInfoByRecmd',
method: 'post',
data
});
};
+38
View File
@@ -0,0 +1,38 @@
import request from '@/api/request'
const base_url = '/traapp'
// 学习时长
export const getLeaderBoardStdyTmApi = (data) => {
return request({
url: base_url + '/myLeaderBoard/stdyTmLen',
method: 'post',
data
});
};
// 练习次数
export const getLeaderBoardPractCntApi = (data) => {
return request({
url: base_url + '/myLeaderBoard/practCnt',
method: 'post',
data
});
};
// 考试次数
export const getLeaderBoardExamCntApi = (data) => {
return request({
url: base_url + '/myLeaderBoard/examCnt',
method: 'post',
data
});
};
// 通关课程数
export const getLeaderBoardCrsCntApi = (data) => {
return request({
url: base_url + '/myLeaderBoard/crsCnt',
method: 'post',
data
});
};
+31
View File
@@ -0,0 +1,31 @@
import request from '@/api/request'
const base_url = '/trastudy'
// 查询所有、未完成、已完成的任务(分页)
export const queryAllTraTaskInfoByUserId = (data) => {
return request({
url: base_url + '/traTaskInfo/queryAllTraTaskInfoByUserId',
method: 'post',
data
});
};
// 查询当前用户的课程完成情况
export const queryAllTraTaskCrsInfo = (data) => {
return request({
url: base_url + '/traTaskInfo/queryAllTraTaskCrsInfo',
method: 'post',
data
});
};
// 根据任务id查询这一条
export const queryTraTaskInfoByTaskId = (data) => {
return request({
url: base_url + '/traTaskInfo/queryTraTaskInfoByTaskId',
method: 'post',
data
});
};
+239
View File
@@ -0,0 +1,239 @@
import request from '@/api/request'
import common from "@/common/common";
import {
queryCurrentValidMascotInfo
} from "@/api/mascot.js"
import {
queryCurrentStatus
} from '@/api/pointsAndRank.js';
import {
setToken,
setUserInfo,
getUserInfo,
getToken
} from "@/common/common";
import {
get_base_url
} from '@/api/request.js'
// 账号登录接口
export const useAccountLoginApp = (data) => request({
url: '/traapp/access/userLogin',
method: 'post',
data
}).then(async ({
rtnCode,
body
}) => {
if (rtnCode === '0000') {
setToken(body)
const res = await Promise.all([queryCurrentUser(), queryCurrentValidMascotInfo()
// queryCurrentStatus()
])
const userInfo = getUserInfo()
const mascotInfo = res[1]
setUserInfo({
...userInfo,
'mascotInfo': mascotInfo
})
// 积分状态
// const pointsStatus = res[2].body
const pointsStatus = {}
common.setValue('points_data', {
badgeNum: pointsStatus['badgeNum'] ?? 0, // 徽章数
currentPoints: pointsStatus['currentPoints'] ?? 0, // 当前积分
currentRankAddr: pointsStatus['ossAddr'] ?? '', // 当前段位图片
currentRankName: pointsStatus['currentRankName'] ?? '未知', // 当前段位名称
})
return {
mascotState: !mascotInfo.mascotId
}
}
return Promise.reject({
rtnCode,
body
})
}).catch(res => {
return Promise.reject(res)
})
/**
* 获取个人信息
* 入参
gender f是女 m男
imageAddr 图片
loginName ID
userName 姓名
*/
export const queryCurrentUser = (data) => request({
url: '/traapp/access/queryCurrentUser',
method: 'post',
data
}).then(res => {
// deptId: ""
// deptName: null
// loginName: "ADMIN" // todo id
// orgId: "00E200"
// orgName: "金融科技部"
// userId: "USER001"
// userName: "ADMIN" / todo name
// userStatus: "01"
const userInfo = getUserInfo() ?? {}
setUserInfo({
...userInfo,
...res.body
})
// console.log('预加载头像');
const base_url = get_base_url()
const token = getToken()
// 预加载头像
// #ifdef APP-PLUS
// 更新用户信息中的头像路径
const updateUserAvatar = (imagePath) => {
const _userInfo = getUserInfo() ?? {}
setUserInfo({
..._userInfo,
...res.body,
'imagePath': imagePath
})
}
// 确保目录存在
const ensureDirectoryExists = (dirPath) => {
return new Promise((resolve) => {
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getDirectory(dirPath, {
create: true
}, resolve, resolve)
})
})
}
// 生成唯一文件名
const generateUniqueFilename = (pathName) => {
if (!pathName || typeof pathName !== 'string') {
throw new Error('无效的路径');
}
// 直接使用原始文件名(带扩展名)
const filename = pathName.split('/').pop();
if (!filename) {
throw new Error('无法从路径中提取文件名');
}
// 确保文件扩展名为.png (可选,根据实际需求)
const ext = filename.split('.').pop().toLowerCase();
if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'webp') {
return filename;
}
// 如果没有有效扩展名,添加.png
return `${filename}.png`;
};
// 下载并保存头像
const downloadAndSaveAvatar = async (fileUrl, localFilePath) => {
try {
await ensureDirectoryExists('avatar')
return new Promise((resolve, reject) => {
console.log('开始下载头像:', fileUrl)
const dtask = plus.downloader.createDownload(fileUrl, {
filename: localFilePath
}, (d, status) => {
if (status === 200) {
console.log("保存路径:", d.filename)
resolve(d.filename)
} else {
console.error("文件下载失败:", status)
reject(new Error(`下载失败,状态码: ${status}`))
}
})
dtask.start()
})
} catch (error) {
console.error('创建目录失败:', error)
throw error
}
}
// 检查文件是否存在
const checkFileExists = (filePath) => {
return new Promise((resolve) => {
uni.getFileInfo({
filePath,
success: () => resolve(true),
fail: () => resolve(false)
})
})
}
// 主逻辑
(async () => {
try {
const pathName = res.body.imageAddr
if (pathName) {
const fileUrl = base_url + pathName + '?tk=' + token
console.log('fileUrl', fileUrl);
// 生成保存路径
const filename = generateUniqueFilename(pathName)
const localFilePath = '_doc/avatar/' + filename
// 检查文件是否存在
const exists = await checkFileExists(localFilePath)
if (exists) {
// console.log('头像已存在,无需下载')
updateUserAvatar(localFilePath)
} else {
const savedPath = await downloadAndSaveAvatar(fileUrl, localFilePath)
updateUserAvatar(savedPath)
}
}
} catch (error) {
console.error('头像预加载过程发生错误:', error)
}
})()
// #endif
return res.body
})
//获取隐私协议
export const getYsxy = (data) => request({
url: '/traapp/dfAgtInfo/queryValidDfAgt',
method: 'post',
data
})
//获取服务条款
export const getFutk = (data) => request({
url: '/traapp/dfAgtInfo/queryValidDfAgtTerm',
method: 'post',
data
})
// 获取验证码(作废)
export const getLoginAppCheckCode = (data) => request({
url: '/traapp/access/checkCode',
method: 'get',
data
})
// 退出登录
export const loginOut = (data) => request({
url: '/traapp/access/loginOut',
method: 'post',
data
}).then(() => {
setToken(null)
setUserInfo(null)
common.setValue('me_statistics_data', null) // 我的页统计数据缓存
common.setValue('index_statistics_data', null) // 首页统计数据缓存
common.setValue('points_data', null) // 首页统计数据缓存
})
+112
View File
@@ -0,0 +1,112 @@
import request from '@/api/request'
import {
get_base_url
} from '@/api/request.js'
import { getToken } from '@/common/common.js'
const app_url = '/traapp'
/**
* 查询吉祥物列表
* 出参:
mascotId 吉祥物ID String
mascotName 吉祥物名称 String
mascotNameEn 吉祥物英文名称 String
mascotDesc 吉祥物简介 String
mascotNo 吉祥物编号 String
imgAddr 吉祥物图片 String
imgThumbAddr 吉祥物缩略图 String
showOrder 显示顺序 int
*/
export const queryValidTraMascotInfo = (data) => {
return request({
url: app_url + '/traMascotInfo/queryValidTraMascotInfo',
method: 'post',
toastErrors: true,
data
});
};
/**
* 设置吉祥物
* 入参:
mascotId 必输项:true 类型:String
*/
export const setTraMascotInfo = (data) => {
return request({
url: app_url + '/traMascotInfo/setTraMascotInfo',
method: 'post',
toastErrors: true,
data
});
};
// 查询当前已经选择的吉祥物
// 检查文件是否存在
const checkFileExists = (filePath) => {
return new Promise((resolve) => {
uni.getFileInfo({
filePath,
success: () => resolve(true),
fail: () => resolve(false)
})
})
}
/**
* 查询当前已经选择的吉祥物
* 出参:
mascotId 吉祥物ID String
mascotName 吉祥物名称 String
mascotNameEn 吉祥物英文名称 String
mascotDesc 吉祥物简介 String
mascotNo 吉祥物编号 String
imgAddr 吉祥物图片 String
imgThumbAddr 吉祥物缩略图 String
showOrder 显示顺序 int
*/
export const queryCurrentValidMascotInfo = (data) => {
return request({
url: app_url + '/traMascotInfo/queryCurrentValidMascotInfo',
method: 'post',
toastErrors: true,
data
}).then(({
body
}) => {
if (body.imgAddr) {
console.log('body.imgAddrmascotNo');
const base_url = get_base_url()
const token = getToken()
const pathName = body.imgAddr
const fileUrl = base_url + pathName + '?tk=' + token
const localFilePath = '_doc/mascot/' + body.mascotNo + '.png'
console.log('fileUrl', fileUrl);
// 将 Promise 回调函数声明为 async
return new Promise(async (resolve, reject) => {
// 现在可以在这里使用 await 了
const exists = await checkFileExists(localFilePath)
if (exists) {
// console.log('已存在,无需下载吉祥物')
resolve({...body, imgAddr:localFilePath})
} else {
// console.log('开始下载吉祥物:', fileUrl)
const dtask = plus.downloader.createDownload(fileUrl, {
filename: localFilePath
}, (d, status) => {
if (status === 200) {
console.log("保存路径:", d.filename)
resolve({...body, imgAddr:d.filename})
} else {
console.error("文件下载失败:", status)
reject(new Error(`下载失败,状态码: ${status}`))
}
})
dtask.start()
}
})
}
return body
})
};
+62
View File
@@ -0,0 +1,62 @@
import request from '@/api/request'
const base_url = '/trastudy'
// 查询当前用户的所有消息通知 (分页)
export const queryTraPersonalMessageNoticePaging = (data) => {
return request({
url: base_url + '/traPersonalMessageNotice/queryTraPersonalMessageNoticePaging',
method: 'post',
toastErrors: true,
data
});
};
// 读取单一消息
export const readNotice = (data) => {
return request({
url: base_url + '/traPersonalMessageNotice/readNotice',
method: 'post',
toastErrors: true,
data
});
};
// 一键读取所有消息(不展示)
export const oneTimeRead = (data) => {
return request({
url: base_url + '/traPersonalMessageNotice/oneTimeRead',
method: 'post',
toastErrors: true,
data
});
};
export const noticeMessageCount = (data) => {
return request({
url: base_url + '/traPersonalMessageNotice/noticeMessageCount',
method: 'post',
data
});
};
export const queryMessageNoticeClass = (data) => {
return request({
url: base_url + '/traPersonalMessageNotice/queryMessageNoticeClass',
method: 'post',
data
});
};
// 按照分类一键读取消息
export const oneTimeReadByNoticeTyp = (data) => {
return request({
url: base_url + '/traPersonalMessageNotice/oneTimeReadByNoticeTyp',
method: 'post',
data
});
};
+129
View File
@@ -0,0 +1,129 @@
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) => {
// base_url
return request({
url: base_url + '/traPointsInfo/addPointsByTask',
method: 'post',
data
});
// return request({
// url: '/test/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
});
};
/**
* 徽章墙
*/
export const queryRankWall = (data) => {
return request({
url: base_url + '/traPointsInfo/queryRankWall',
method: 'post',
data
});
};
+20
View File
@@ -0,0 +1,20 @@
import request from '@/api/request'
const base_url = '/traapp'
// String actId,活动id
export const queryPointsActivityDetail = (data) => {
return request({
url: base_url + '/traPointsActivity/queryPointsActivityDetail',
method: 'post',
data
});
};
export const addPointsExchange = (data) => {
return request({
url: base_url + '/traPointsActivity/addPointsExchange',
method: 'post',
data
});
};
+49
View File
@@ -0,0 +1,49 @@
import request from '@/api/request'
const base_url = '/traexam'
// 1.开始练习(课程练习)
export const startPracticeByCrs = (data) => {
return request({
url: base_url + '/traCrsPractice/startPracticeByCrs',
method: 'post',
data
});
};
// 2.课程练习答题
export const practiceAnswer = (data) => {
return request({
url: base_url + '/traCrsPractice/practiceAnswer',
method: 'post',
data
});
};
// 3.课程练习状态变更
export const practiceCommit = (data) => {
return request({
url: base_url + '/traCrsPractice/practiceCommit',
method: 'post',
suppressErrors:true,
data
});
};
// 4.课程练习记录
export const queryPracticeRecordByExrId = (data) => {
return request({
url: base_url + '/traCrsPractice/queryPracticeRecordByExrId',
method: 'post',
data
});
};
// 5.练习时候查询问答题结果
export const queryCrsPracticeAnswerResult = (data) => {
return request({
url: base_url + '/traCrsPractice/queryCrsPracticeAnswerResult',
method: 'post',
toastErrors: true,
data
});
};
+20
View File
@@ -0,0 +1,20 @@
import request from '@/api/request'
const base_url = '/trastudy'
export const queryTraStdyInfoPreviewPaging = (data) => {
return request({
url: base_url + '/traStdyInfoPreview/queryTraStdyInfoPreviewPaging',
method: 'post',
toastErrors: true,
data
});
};
export const queryTraListPreviewPaging = (data) => {
return request({
url: '/trapractice/traPartnerInfo/queryTraPartnerPreviewPaging',
method: 'post',
toastErrors: true,
data
});
};
+154
View File
@@ -0,0 +1,154 @@
import {
getToken
} from '@/common/common.js'
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 (['development', 'development.rd', 'development.yh', 'development.ty'].includes(ENV.MODE)) { // 开发环境
// #ifdef H5
if (url) {
let _str = (url.split('/')[0] || url.split('/')[1])?.toUpperCase()
return ENV[`VITE_APP_BASE_H5_API_Url_${_str}`] || ENV.VITE_APP_BASE_H5_API_Url || ENV
.VITE_APP_BASE_API_Url
} else {
return ENV.VITE_APP_BASE_H5_API_Url || ENV.VITE_APP_BASE_API_Url
}
// #endif
// #ifdef APP-PLUS
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
}
}
// 请求错误码
export const REQUES_ERROR_CODES = {
NO_LOGIN: ['QQ0005', '0005'] //登录
};
// 去登录弹窗状态
let no_login_show_modal_state = false
// 去登录方法
export const goto_login_fun = (from = 'http') => {
if (no_login_show_modal_state) return;
no_login_show_modal_state = true
let page_route = ''
const pages = getCurrentPages();
if (pages.length >= 1) {
const page = pages[pages.length - 1];
page_route = page.route
}
console.log('当前页路由', page_route);
if (page_route === 'pages/login/login') return;
common.hideLoading()
common.show('未登录', '现在去登录', false).then(() => {
// common.navigateTo('/pages/login/login')
if (from === 'socket' && page_route !== '') {
common.redirectTo('/pages/login/login?path=' + page_route)
} else {
common.redirectTo('/pages/login/login')
// common.navigateTo('/pages/login/login')
}
}).finally(() => {
no_login_show_modal_state = false
})
}
export default function request({
url,
method,
data,
meta,
isStream,
suppressErrors = false,
toastErrors = true,
header = {},
timeout = 6000,
baseUrl = ''
}) {
const base_url = get_base_url(url);
const headers_base = {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
...(header || {})
}
let token = getToken()
if (token) {
headers_base['summary'] = token // 让每个请求携带令牌
}
let that = this
return new Promise((resolve, reject) => {
uni.request({
url: `${baseUrl || base_url}${url}`,
data: data,
method: method,
header: headers_base,
timeout: timeout,
success: (response) => {
if (isStream) { // 流式传输,直接返回
return resolve(response);
}
// suppressErrors
// 模拟拦截器功能
const res = response.data
if (res?.rtnCode === '0000') {
return resolve(res)
} else if (res?.rtnCode === '1001') { // 为前后端约定不公共拦截的码值(小树20250903定)
return reject(res)
} else if (res?.rtnCode === '0002' && res?.message) {
if (toastErrors) {
common.msg(res.message)
}
return reject(response)
}
//到这里下面全是异常的处理
if (suppressErrors) { // 静默的接口,报错也不处理,因为下面有公共处理
return reject(response)
}
if (!res) {
common.msg("系统异常.")
return reject()
}
// 0005 QQ0005 登录已过期
if (REQUES_ERROR_CODES['NO_LOGIN'].includes(res.rtnCode)) {
goto_login_fun()
}
if (res?.rtnCode === '9999') {
common.msg("系统异常.")
}
return reject(res)
},
fail(res) {
console.log('请求错误', res, url, base_url);
return reject(res)
}
});
})
};
+12
View File
@@ -0,0 +1,12 @@
import request from '@/api/request'
const base_url = '/traapp'
// 搜索
export const appSearchCrsApi = (data) => {
return request({
url: base_url + '/traSearchHis/appSearchCrs',
method: 'post',
toastErrors: true,
data
});
};
+242
View File
@@ -0,0 +1,242 @@
import { goto_login_fun } from '@/api/request'
export default class WebSocketUtil {
/**
* WebSocket工具类,用于管理WebSocket连接
* @param {string} url - WebSocket服务器地址
* @param {Object} options - 配置选项
* @param {number} [options.maxReconnectCount=5] - 最大重连次数
* @param {number} [options.reconnectInterval=3000] - 重连间隔时间(ms)
* @param {number} [options.heartbeatInterval=30000] - 心跳间隔时间(ms)
* @param {string|Object} [options.heartbeatMsg='ping'] - 心跳消息
*/
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.socketTask = null; // WebSocket任务实例
this.reconnectTimer = null; // 重连计时器
this.reconnectCount = 0; // 当前重连次数
this.maxReconnectCount = options.maxReconnectCount || 5; // 最大重连次数
this.reconnectInterval = options.reconnectInterval || 3000; // 重连间隔(ms)
this.heartbeatTimer = null; // 心跳计时器
this.heartbeatInterval = options.heartbeatInterval || 30000; // 心跳间隔(ms)
this.heartbeatMsg = options.heartbeatMsg || 'ping'; // 心跳消息
this.isClose = false; // 手动关闭
this.callbacks = {
open: [], // 连接打开回调
message: [], // 消息接收回调
close: [], // 连接关闭回调
error: [] // 错误处理回调
};
this.init(true); // 初始化WebSocket连接
}
/**
* 初始化WebSocket连接
*/
init(flag = false) {
if(flag) this.reconnectCount = 0
this.isClose = false; // 手动关闭
// 创建WebSocket连接
this.socketTask = uni.connectSocket({
url: this.url,
header: this.options.header || {},
protocols: this.options.protocols || [],
success: () => {
// console.log('WebSocket连接创建成功');
},
fail: (err) => {
// console.error('WebSocket连接创建失败', err);
this.triggerEvent('error', err);
this.tryReconnect(); // 连接失败时尝试重连
}
});
// 监听WebSocket连接打开事件
this.socketTask.onOpen(() => {
// console.log('WebSocket连接已打开');
this.reconnectCount = 0; // 重置重连计数
clearInterval(this.reconnectTimer); // 清除重连计时器
// this.startHeartbeat(); // 启动心跳机制
this.triggerEvent('open'); // 触发连接打开事件
});
// 监听WebSocket消息接收事件
this.socketTask.onMessage((res) => {
if(res.data){
const { rtnCode } = JSON.parse(res.data)
// console.log('收到WebSocket消息', JSON.parse(res.data) );
if(['0005', 'QQ0005'].includes(rtnCode)) {
clearInterval(this.reconnectTimer);
this.reconnectTimer = null
this.close()
goto_login_fun('socket');
return
}
}else{
clearInterval(this.reconnectTimer); // 清除重连计时器
this.reconnectTimer = null
goto_login_fun('socket');
return
}
this.triggerEvent('message', res); // 触发消息接收事件
});
// 监听WebSocket连接关闭事件
this.socketTask.onClose((res) => {
// console.log('WebSocket连接已关闭', res);
clearInterval(this.heartbeatTimer); // 清除心跳计时器
this.triggerEvent('close', res); // 触发连接关闭事件
this.tryReconnect(); // 尝试重连
});
// 监听WebSocket错误事件
this.socketTask.onError((err) => {
console.error('WebSocket发生错误', err);
clearInterval(this.heartbeatTimer); // 清除心跳计时器
this.triggerEvent('error', err); // 触发错误事件
this.tryReconnect(); // 尝试重连
});
}
/**
* 注册事件回调
* @param {string} event - 事件名称(open/message/close/error)
* @param {Function} callback - 回调函数
* @returns {WebSocketUtil} - 返回当前实例,支持链式调用
*/
on(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event].push(callback);
}
return this;
}
/**
* 移除事件回调
* @param {string} event - 事件名称(open/message/close/error)
* @param {Function} callback - 要移除的回调函数
* @returns {WebSocketUtil} - 返回当前实例,支持链式调用
*/
off(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
}
return this;
}
/**
* 触发事件回调
* @param {string} event - 事件名称
* @param {any} [data] - 传递给回调函数的数据
*/
triggerEvent(event, data) {
if (this.callbacks[event]) {
this.callbacks[event].forEach(callback => callback(data));
}
}
/**
* 通过WebSocket发送消息
* @param {string|Object} message - 要发送的消息,可以是字符串或对象
* @returns {WebSocketUtil} - 返回当前实例,支持链式调用
*/
send(message, num = 1) {
if (this.socketTask && this.getStatus() === 1) {
// 发送消息,对象会自动转换为JSON字符串
this.socketTask.send({
data: typeof message === 'string' ? message : JSON.stringify(message),
success: () => {
// console.log('WebSocket消息发送成功');
},
fail: (err) => {
console.error('WebSocket消息发送失败', err);
this.triggerEvent('error', err);
}
});
} else {
console.error('WebSocket连接未打开,无法发送消息');
this.triggerEvent('error', new Error('WebSocket连接未打开'));
if(num === 10) return
setTimeout(()=>{
this.send(message, num + 1)
},500)
}
return this;
}
/**
* 关闭WebSocket连接
* @param {number} [code=1000] - 关闭码
* @param {string} [reason=''] - 关闭原因
* @returns {WebSocketUtil} - 返回当前实例,支持链式调用
*/
close(code = 1000, reason = '') {
this.isClose = true
if (this.socketTask) {
clearInterval(this.heartbeatTimer); // 清除心跳计时器
clearInterval(this.reconnectTimer); // 清除重连计时器
// 关闭WebSocket连接
this.socketTask.close({
code,
reason,
success: () => {
console.log('WebSocket连接正在关闭');
},
fail: (err) => {
console.error('WebSocket关闭失败', err);
this.triggerEvent('error', err);
}
});
}
return this;
}
/**
* 获取WebSocket连接状态
* @returns {number} - 连接状态:0-连接中,1-已连接,2-连接关闭中,3-已关闭,-1-未知
*/
getStatus() {
if (this.socketTask) {
try {
return this.socketTask.readyState;
} catch (e) {
console.error('获取WebSocket状态失败', e);
return -1;
}
}
return -1;
}
/**
* 尝试重新连接WebSocket
*/
tryReconnect() {
if(this.isClose) return
if (this.reconnectCount < this.maxReconnectCount) {
clearInterval(this.reconnectTimer);
this.reconnectTimer = setTimeout(() => {
this.reconnectCount++;
console.log(`尝试重新连接WebSocket (${this.reconnectCount}/${this.maxReconnectCount})`);
this.init(); // 重新初始化WebSocket连接
}, this.reconnectInterval);
} else {
console.error('达到最大重连次数,停止尝试');
this.triggerEvent('error', new Error('达到最大重连次数'));
this.reconnectTimer = null
this.close()
goto_login_fun('socket')
}
}
/**
* 启动心跳机制
*/
startHeartbeat() {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
if (this.getStatus() === 1) {
this.send(this.heartbeatMsg); // 发送心跳消息
}
}, this.heartbeatInterval);
}
}
+92
View File
@@ -0,0 +1,92 @@
import request from '@/api/request'
const base_url = '/trapractice'
// 获取分类树接口
export const queryTraPartnerCatelogList = (data) => {
return request({
url: base_url + '/traPartnerCatelog/queryTraPartnerCatelogList',
method: 'post',
data
});
};
// 获取标签分类树 接口
export const getTraTagListApi = (data) => {
return request({
url: base_url + '/traPartnerTag/queryTraTagCatalogInfoList',
method: 'post',
data
});
};
// 全部模块
export const queryCrsInfoByCatalogApi = (data) => {
return request({
url: base_url + '/traPartnerInfo/queryTraPartnerInfoPaging',
method: 'post',
data
});
};
// id 查详情/trapractice/traPartnerInfo/queryTraPartnerInfoById
export const queryTraPartnerInfoById = (data) => {
return request({
url: base_url + '/traPartnerInfo/queryTraPartnerInfoById',
method: 'post',
data
});
};
// 陪练角色列表 traId
export const queryTraPartnerCharacterInfoList = (data) => {
return request({
url: base_url + '/traPartnerCharacterInfo/queryTraPartnerCharacterInfoList',
method: 'post',
data
});
};
// 陪练角色详情 + 对话详情 /traPartnerCharacterInfo/queryTraPartnerCharacterInfoById
export const queryTraPartnerCharacterInfoById = (data) => {
return request({
url: base_url + '/traPartnerCharacterInfo/queryTraPartnerCharacterInfoById',
method: 'post',
data
});
};
// 结束陪练 /trapractice/traPartnerChatReport/partnerChatReportTrigger?execId=
export const partnerChatReportTrigger = (data) => {
return request({
url: base_url + '/traPartnerChatReport/partnerChatReportTrigger',
method: 'post',
data
});
};
// 问答提示/trapractice/traPartnerChat/partnerChatPrompt
export const partnerChatPrompt = (data) => {
return request({
url: base_url + '/traPartnerChat/partnerChatPrompt',
method: 'post',
data
});
};
// 获取报告traPartnerChatReport/partnerChatReport
export const partnerChatReport = (data) => {
return request({
url: base_url + '/traPartnerChatReport/partnerChatReport',
method: 'post',
data
});
};
// 反馈
export const insertTraPartnerFeedback = (data) => {
return request({
url: base_url + '/traPartnerFeedback/insertTraPartnerFeedback',
method: 'post',
data
});
};
// 查询是否可以进入trapractice/traPartnerInfo/checkTraPartnerInfoById
export const checkTraPartnerInfoById = (data) => {
return request({
url: base_url + '/traPartnerInfo/checkTraPartnerInfoById',
method: 'post',
data
});
};
+206
View File
@@ -0,0 +1,206 @@
import request from '@/api/request'
import { getToken } from '@/common/common.js'
import { get_base_url } from '@/api/request'
const base_url = '/trastudy'
// 获取课程学习信息接口
export const getCourseStudyInfoApi = (data) => {
return request({
url: base_url + '/traStdyInfo/queryTraStdyProgressAndTeacher',
method: 'post',
toastErrors: true,
data
});
};
// 获取课程学习 知识点 接口
export const queryTraStdyBatchWaitParagraphInfoApi = (data) => {
return request({
url: base_url + '/traStdyInfo/queryTraStdyBatchWaitParagraphInfo',
method: 'post',
toastErrors: true,
data
});
};
// 重新开始 删除学习信息
export const deleteTraStdyInfoByIdApi = (data) => {
return request({
url: base_url + '/traStdyInfo/deleteTraStdyInfoById',
method: 'post',
toastErrors: true,
data
});
};
// 重新学习
export const afreshStudyCourseApi = (data) => {
return request({
url: base_url + '/traStdyInfo/afreshStudyCourse',
method: 'post',
toastErrors: true,
data
});
};
// 学习段落完成
export const studyParagraphOverApi = (data) => {
return request({
url: base_url + '/traStdyInfo/studyParagraphOver',
method: 'post',
toastErrors: true,
data
});
};
// 回答问题 /traStdyInfo/studyAnswerTextUp
export const studyAnswerTextUpApi = (data) => {
return request({
url: base_url + '/traStdyInfo/studyAnswerTextUp',
method: 'post',
toastErrors: true,
data
});
};
// 获取问题信息/traStdyInfo/queryTraStdyBatchWaitQuestionInfo
export const queryTraStdyBatchWaitQuestionInfoApi = (data) => {
return request({
url: base_url + '/traStdyInfo/queryTraStdyBatchWaitQuestionInfo',
method: 'post',
toastErrors: true,
data
});
};
// 上传语音翻译i文本 /traStdyInfo/audioTranscriptions
export const audioTranscriptionsApi = (data) => {
return request({
url: base_url + '/traStdyInfo/audioTranscriptions',
method: 'post',
toastErrors: true,
data
});
};
// 撤回消息
export const withdrawalAnswerApi = (data) => {
return request({
url: base_url + '/traStdyInfo/withdrawalAnswer',
method: 'post',
toastErrors: true,
data
});
};
// 异步完成回答
// 入参:
// qstLogId 必输项:true 类型:String
// 出参:
// qstLogId 问题日志ID String
// qnsId 问题ID String
// processStat 状态 String
// message 处理消息 String
export const waitMakeEvaluateApi = (data) => {
return request({
url: base_url + '/traStdyInfo/waitMakeEvaluate',
method: 'post',
toastErrors: true,
data
});
};
// 查询问题评价
// /traStdyInfo/queryQuestionEvalate
export const queryQuestionEvalateApi = (data) => {
return request({
url: base_url + '/traStdyInfo/queryQuestionEvalate',
method: 'post',
toastErrors: true,
data
});
};
// /trastudy/intgask/askAbout
// 流式获取文本
export const getAskAboutRawApi = (data) => {
// return request({
// baseUrl:'http://25.18.122.76:3030',
// url: '/api/v1/prediction/a402cf58-9f64-479b-a9f8-c527300a7905',
// isStream: true,
// header: {
// 'Accept':'text/event-stream',
// 'content-type': 'application/json; charset=UTF-8',
// 'Authorization': 'Bearer PjadGPFSHDg6F8ZRekROe-QxS2fPXlWRsmvP5Mp0vxA'
// },
// method: 'post',
// toastErrors: false,
// data
// });
return request({
url: base_url + '/intgask/askAbout',
header: {
'Accept':'text/event-stream',
'content-type': 'text/event-stream; charset=UTF-8',
},
responseType: 'arrayBuffer',
method: 'get',
isStream: true,
timeout: 30000,
toastErrors: false,
data
})
};
// 学习开始
export const studyStartApi = (data) => {
return request({
url: base_url + '/traStdychat/studyStart',
method: 'post',
toastErrors: true,
data
});
};
// 学习结束
export const studyEndApi = (data) => {
return request({
url: base_url + '/traStdychat/studyEnd',
method: 'post',
toastErrors: true,
data
});
};
// 学习 对话
export const textChartApi = (data) => {
return request({
url: base_url + '/traStdychat/textChart',
method: 'post',
toastErrors: true,
data
});
};
// 预览 对话
export const textChartPreviewApi = (data) => {
return request({
url: base_url + '/traStdyInfoPreview/previewCrsInfo',
method: 'post',
toastErrors: true,
data
});
};
export const previewAfreshStudyCourse = (data) => {
return request({
url: base_url + '/traStdyInfo/previewAfreshStudyCourse',
method: 'post',
toastErrors: true,
data
});
};
// 设置学习顾问音色/traStdyTeacher/setStudyTeacher
export const setStudyTeacherApi = (data) => {
return request({
url: base_url + '/traStdyTeacher/setStudyTeacher',
method: 'post',
toastErrors: true,
data
});
};
+11
View File
@@ -0,0 +1,11 @@
import request from '@/api/request'
export const prepareApi = (data) => {
return request({
url: '/trapractice/traPartnerVoiceCall/voiceCallPrepare' ,
method: 'post',
toastErrors: true,
data,
baseUrl: 'http://25.64.32.157:9603'
});
};
+13
View File
@@ -0,0 +1,13 @@
import request from '@/api/request'
const module_url = '/traexam'
// 1.查询考试列表分页
export const queryTraExamPapersPage = (data) => {
return request({
url: module_url + '/traExamPapers/queryTraExamPapersPage',
method: 'post',
data
});
};
+58
View File
@@ -0,0 +1,58 @@
import request from '@/api/request'
const base_url = '/trapractice'
/**练习*/
// 练习列表页
export const queryPracticeRecordPaging = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecutePaging',
method: 'post',
toastErrors: true,
data:{
traType:'01',
...data
}
});
};
// 练习每一项列表
export const queryPracticeHis = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecuteHisPaging',
method: 'post',
toastErrors: true,
data
});
};
// 练习详情 通过 exrId 查询
export const queryPracticeRecordByExrId = (data) => {
return request({
url: base_url + '/traCrsPractice/queryPracticeRecordByExrId',
method: 'post',
toastErrors: true,
data
});
};
/**考试*/
export const queryCrsExamRecordPaging = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecutePaging',
method: 'post',
toastErrors: true,
data:{
traType:'02',
...data
}
});
};
// 考试每一项列表
export const queryCrsExamHisByExamId = (data) => {
return request({
url: base_url + '/traPartnerExecuteHis/queryTraPartnerExecuteHisPaging',
method: 'post',
toastErrors: true,
data
});
};
+57
View File
@@ -0,0 +1,57 @@
import request from '@/api/request'
// 我的-统计信息
export const queryTraStdyStatisticsSelf = (data) => {
/**
userId 学员ID String
stdtTmLen 累计学习时长 double
accmTaskCnt 累计完成任务 int
accmPoint 累计获得积分 int
**/
return request({
url: '/traapp/traStdyInfo/queryTraStdyStatisticsSelf',
method: 'post',
data
});
};
// 更新性别
export const updateDfSysUserExtandInfoGender = (data) => {
/**
**/
return request({
url: '/traapp/dfSysUserInfo/updateDfSysUserExtandInfoGender ',
method: 'post',
data
});
};
// 更新照片
export const updateDfSysUserExtandInfoImageAddr = (data) => {
return request({
url: '/traapp/dfSysUserInfo/updateDfSysUserExtandInfoImageAddr',
method: 'post',
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
});
};
+54
View File
@@ -0,0 +1,54 @@
import request from '@/api/request'
const base_url = '/traexam'
// 错题本列表页
export const queryTraMistakesCollectionPaging = (data) => {
return request({
url: base_url + '/traMistakesCollection/queryTraMistakesCollectionPaging',
method: 'post',
toastErrors: true,
data
});
};
// 错题订正
export const mistakesCorrect = (data) => {
return request({
url: base_url + '/traMistakesCollection/mistakesCorrect',
method: 'post',
toastErrors: true,
data
});
};
// 错题本id集合
export const queryTraMistakesCollection = (data) => {
return request({
url: base_url + '/traMistakesCollection/queryTraMistakesCollection',
method: 'post',
toastErrors: true,
data
});
};
// 根据ID查题
export const queryMistakesAnswerByMisId = (data) => {
return request({
url: base_url + '/traMistakesCollection/queryMistakesAnswerByMisId ',
method: 'post',
toastErrors: false,
data
});
};
// 根据ID查题
export const queryMistakesCorrectByMisId = (data) => {
return request({
url: base_url + '/traMistakesCollection/queryMistakesCorrectByMisId',
method: 'post',
toastErrors: false,
data
});
};
+385
View File
@@ -0,0 +1,385 @@
import {
get_base_url
} from '@/api/request';
// #ifdef APP-PLUS
import showDialog from '@/common/dialogMessage'
import common from '@/common/common'
// #endif
function msg(title, duration = 1000) {
uni.showToast({
title: title,
duration: duration,
icon: 'none',
mask: true,
});
}
function show(title, msg, showCancel = true, cancelText = '取消', confirmText = '确定') {
return new Promise((resolve, reject) => {
// 创建通用参数对象
const dialogOptions = {
title,
content: msg,
showCancel,
cancelText,
confirmText,
success: (res) => {
// 统一处理成功回调,解析为布尔值
resolve(!!res.confirm);
}
};
// #ifdef APP-PLUS
if (getPhoneEnvBool('AND')) {
// 安卓环境使用showDialog,补充cancel回调
showDialog({
...dialogOptions,
cancel: () => {
console.log('用户点击取消');
resolve(false); // 取消时也返回Promise结果
}
});
} else {
// 其他环境使用uni.showModal,补充fail回调
uni.showModal({
...dialogOptions,
fail: (res) => {
reject(res);
}
});
}
// #endif
// #ifdef H5
uni.showModal({
...dialogOptions,
fail: (res) => {
reject(res);
}
});
// #endif
});
}
const loading = (title = '加载中', mask = true) => uni.showLoading({
title: title,
mask: mask
})
const hideLoading = () => uni.hideLoading()
// 根据姓名的长度生成对应的带星号格式
export const formatName = (name) => {
// 处理空值或非字符串情况
if (!name || typeof name !== 'string') {
return '';
}
const familyName = name.charAt(0); // 取姓氏(第一个字符)
const givenNameLength = name.length - 1; // 名字部分的长度
// 根据名字长度计算星号数量,最多4个
const starCount = Math.min(givenNameLength, 3);
const stars = '*'.repeat(starCount);
return familyName + stars;
}
// 防抖工具函数
function debounce(fn, wait = 300) {
let isLocked = false; // 锁定状态标记
return function(...args) {
// 如果处于锁定状态,直接返回不执行
if (isLocked) {
return;
}
// 执行函数
fn.apply(this, args);
// 锁定,防止再次执行
isLocked = true;
// 等待指定时间后解锁
setTimeout(() => {
isLocked = false;
}, wait);
};
}
// 保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。
const navigateTo = debounce(function(url, data = {}) {
uni.navigateTo({
url: url,
...data
});
})
// 关闭所有页面,打开到应用内的某个页面。
const reLaunch = url => {
uni.reLaunch({
url: url
});
}
// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。
const switchTab = url => {
uni.switchTab({
url: url
});
}
// 关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。
const navigateBack = (delta = 1, fun = () => {}, data = {}) => {
uni.navigateBack({
delta: delta,
...data,
success() {
fun()
}
})
}
//关闭当前页面,跳转到应用内的某个页面。
const redirectTo = (url, obj = {}) => uni.redirectTo({
url: url,
...obj
});
// 时间转换时间戳
const dateToTime = (dateString) => {
let date = new Date(dateString);
let timestamp = date.getTime();
return timestamp
}
// 数字格式化
const formatDate2 = (timestamp, format = 'yyyy-MM-dd HH:mm:ss') => {
if (!timestamp)
return ''
let date
try {
if (timestamp) {
if (typeof timestamp == 'number' && timestamp <= 9999999999) {
timestamp *= 1000
}
date = new Date(timestamp)
} else {
date = new Date()
}
} catch (e) {
return ''
}
let day = date.getDate()
let month = date.getMonth() + 1
let year = date.getFullYear()
let hours = date.getHours()
let minutes = date.getMinutes()
let seconds = date.getSeconds()
// 替换日期格式中的占位符
return format
.replace('yyyy', year)
.replace('MM', month.toString().padStart(2, '0'))
.replace('dd', day.toString().padStart(2, '0'))
.replace('HH', hours.toString().padStart(2, '0'))
.replace('mm', minutes.toString().padStart(2, '0'))
.replace('ss', seconds.toString().padStart(2, '0'))
}
// 获取token
export const getToken = () => uni.getStorageSync('token')
export const setToken = data => uni.setStorageSync('token', data)
export const setUserInfo = data => uni.setStorageSync('userInfo', data)
export const getUserInfo = (key = '') => {
const userInfo = uni.getStorageSync('userInfo')
return '' === key ? userInfo : userInfo[key];
}
// 数字转换,把传入的秒转换为HH:mm:ss格式
export const formatSeconds = (seconds) => {
// 处理非数字或负数情况
if (typeof seconds !== 'number' || isNaN(seconds) || seconds < 0) {
return '00:00:00';
}
// 计算小时、分钟和剩余秒数
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60);
// 补零函数:将数字转为两位数字符串
const padZero = (num) => num.toString().padStart(2, '0');
// 拼接成00:00:00格式
return `${padZero(hours)}:${padZero(minutes)}:${padZero(remainingSeconds)}`;
};
/**
* 判断当前运行平台是否匹配指定标识
* @param {string} flag - 平台标识:'IOS' 表示苹果iOS平台,'AND' 表示安卓平台
* @returns {boolean} 如果当前平台匹配指定标识返回true,否则返回false
*/
export const getPhoneEnvBool = (flag) => {
// 获取当前设备的平台信息
const {
platform
} = uni.getSystemInfoSync();
// 平台与标识的映射关系
const platformMap = {
ios: 'IOS',
android: 'AND',
};
// 根据当前平台返回匹配结果
return platformMap[platform] === flag;
};
// 将对象和字符串相互转换
export const objToJson = obj => encodeURLComponent('page_data', obj)
export const jsonToObj = () => uni.getStorageSync('page_data')
// 将对象存入缓存
export const setPageCache = (name, obj) => {
uni.setStorageSync(name, obj)
}
// 将对象取出并且删除缓存
export const getPageCache = name => {
const cache = uni.getStorageSync(name)
uni.removeStorageSync(name)
return cache
}
function setValue(key, value) {
uni.setStorageSync(key, value);
}
function getValue(key, defaultValue = null) {
return uni.getStorageSync(key) ?? defaultValue
}
function isLogin() {
return !!getToken();
}
// 封装键盘高度变化处理
export function setupKeyboardHeightListener(updateCallback) {
// #ifndef APP-PLUS
return () => {};
// #endif
// #ifdef APP-PLUS
// 获取系统信息
const systemInfo = uni.getSystemInfoSync();
// 计算安全区域底部高度
const safeAreaBottomHeight = systemInfo.screenHeight - systemInfo.safeArea.bottom;
// 定义处理函数
const handleKeyboardHeightChange = (res) => {
let keyboardHeight = res.height;
// 仅在iOS端补偿安全区域高度
if (systemInfo.platform === 'ios') {
keyboardHeight = Math.max(0, keyboardHeight - safeAreaBottomHeight); // 避免负数
}
// 通过回调函数更新外部的响应式变量
updateCallback(keyboardHeight);
};
// 监听键盘高度变化
uni.onKeyboardHeightChange(handleKeyboardHeightChange);
// 返回取消监听的函数,方便组件卸载时清理
return () => {
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
};
// #endif
}
let isDownloading = false
export const previewFileFnc = ({
fileId,
getFileSrc,
fileName
}) => {
let userInfo = getUserInfo()
let userParams = '/' + (userInfo?.loginName ?? '')
navigateTo('/pages/index/preview?fileId=' + fileId + '&fileApiSrc=' + getFileSrc);
// if (getPhoneEnvBool('AND')) {
// navigateTo('/pages/index/preview?fileId=' + fileId + '&fileApiSrc=' + getFileSrc);
// } else {
// if (isDownloading) return
// const base_url = get_base_url(getFileSrc);
// let fileUrl = base_url + getFileSrc + fileId + userParams;
// const localFilePath = '_doc/pdf_view/' + fileName + '.pdf';
// isDownloading = true
// downloadFileFnc(fileUrl, localFilePath).then((res) => {
// uni.openDocument({
// filePath: res,
// showMenu: true,
// success: function() {
// console.log('打开文档成功');
// }
// })
// }).finally(() => {
// setTimeout(() => {
// isDownloading = false
// }, 1000)
// });
// }
}
const downloadFileFnc = async (fileUrl, localFilePath) => {
try {
//#ifdef APP-PLUS
return new Promise((resolve, reject) => {
console.log('开始下载:', fileUrl);
const dtask = plus.downloader.createDownload(
fileUrl, {
filename: localFilePath
},
(d, status) => {
if (status === 200) {
console.log('保存路径:', d.filename);
resolve(d.filename);
} else {
console.error('文件下载失败:', status);
reject(new Error(`下载失败,状态码: ${status}`));
}
}
);
dtask.start();
});
// #endif
//#ifdef H5
return new Promise((resolve, reject) => {
resolve();
});
// #endif
} catch (error) {
console.error('创建目录失败:', error);
throw error;
}
};
export default {
getPageCache,
setPageCache,
objToJson,
jsonToObj,
hideLoading,
loading,
msg,
show,
redirectTo,
navigateTo,
navigateBack,
formatDate2,
setValue,
getValue,
isLogin,
reLaunch,
switchTab,
dateToTime,
}
+225
View File
@@ -0,0 +1,225 @@
export default function showDialog(options) {
const view = new plus.nativeObj.View('customModal', {
top: '0',
left: '0',
height: '100%',
width: '100%',
backgroundColor: 'rgba(0,0,0,0.1)',
position: 'fixed'
});
const screenWidth = plus.screen.resolutionWidth;
const screenHeight = plus.screen.resolutionHeight;
const borderRadius = 10;
const contentPadding = 12; // 内容左右内边距
const lineHeight = 17; // 单行文本行高
const maxLines = 2; // 最大行数
const fontSize = 14; // 字体大小
// 判断内容和标题是否存在
const hasContent = options.content && options.content.trim() !== '';
const hasTitle = options.title && options.title.trim() !== '';
// 弹窗宽度计算
const modalWidth = Math.min(0.65 * screenWidth, 500);
const modalLeft = (screenWidth - modalWidth) / 2;
// 内容区域可用宽度
const contentAvailableWidth = modalWidth - 2 * contentPadding - 10;
// 动态计算各部分高度
const titleHeight = hasTitle ? 20 : 0;
const titleToContentSpacing = (hasTitle && hasContent) ? 10 : 0;
const contentHeight = hasContent ? (lineHeight * maxLines) : 0;
const contentToBtnSpacing = 10;
const btnHeight = 46;
// 弹窗总高度
let modalHeight = contentPadding + 6 + titleHeight + titleToContentSpacing + contentHeight + contentToBtnSpacing + btnHeight;
modalHeight = options.showCancel ? Math.min(modalHeight, 300) : Math.min(modalHeight, 250);
// 位置计算
const modalTop = (screenHeight - modalHeight) / 2;
const contentAreaTop = modalTop + contentPadding + 6;
const titleTop = hasTitle ? contentAreaTop : 0;
let contentTop;
if (hasContent) {
contentTop = hasTitle ? (titleTop + titleHeight + titleToContentSpacing) : contentAreaTop;
} else {
contentTop = hasTitle ? (titleTop + titleHeight) : contentAreaTop;
}
const btnTop = hasContent
? (contentTop + contentHeight + contentToBtnSpacing)
: (hasTitle
? (titleTop + titleHeight + contentToBtnSpacing)
: (contentAreaTop + (modalHeight - btnHeight - contentPadding))); // 既无标题也无内容时
// 绘制弹窗背景
view.draw([{
tag: 'rect',
id: 'bg',
position: { top: modalTop + 'px', left: modalLeft + 'px', width: modalWidth + 'px', height: modalHeight + 'px' },
rectStyles: { color: '#fff', radius: borderRadius + 'px' }
}]);
// 绘制标题和内容
const elements = [];
// 标题(仅当存在时)
if (hasTitle) {
elements.push({
tag: 'font',
id: 'title',
text: options.title,
position: { top: titleTop + 'px', left: modalLeft + 'px', width: modalWidth + 'px', height: titleHeight + 'px' },
textStyles: {
color: '#000',
size: '18px',
align: 'center',
fontWeight: 'bold',
verticalAlign: 'middle'
}
});
}
if (hasContent) {
const text = options.content;
const maxLines = 2; // 最大显示行数
const lineHeightPx = lineHeight;
const leftPos = modalLeft + contentPadding;
const textWidth = contentAvailableWidth;
const fontSizePx = fontSize;
// 按 \n 分割段落
const paragraphs = text.split('\n');
let lines = [];
// 遍历每个段落,计算换行
for (let para of paragraphs) {
if (lines.length >= maxLines) break;
// 估算每行能容纳的字符数(中文按 1.2 倍宽度计算)
const avgCharWidth = fontSizePx * (isChinese(para) ? 1.2 : 0.6);
const maxCharsPerLine = Math.floor(textWidth / avgCharWidth);
let currentLine = '';
for (let i = 0; i < para.length; i++) {
const char = para[i];
currentLine += char;
// 如果当前行字符数超过限制,换行
if (currentLine.length >= maxCharsPerLine) {
lines.push(currentLine);
currentLine = '';
if (lines.length >= maxLines) break;
}
}
// 添加剩余部分
if (currentLine && lines.length < maxLines) {
lines.push(currentLine);
}
}
// 如果超出最大行数,最后一行加 "..."
if (lines.length > maxLines) {
lines = lines.slice(0, maxLines);
const lastLine = lines[maxLines - 1];
lines[maxLines - 1] = lastLine.substring(0, lastLine.length - 3) + '...';
}
// 绘制每一行文本(确保行高正确)
lines.forEach((line, index) => {
const topPos = contentTop + index * lineHeightPx;
elements.push({
tag: 'font',
id: 'content_line_' + index,
text: line,
position: {
top: topPos + 'px',
left: leftPos + 'px',
width: textWidth + 'px',
height: lineHeightPx + 'px'
},
textStyles: {
color: '#111',
size: fontSizePx + 'px',
align: 'center',
verticalAlign: 'top',
lineHeight: lineHeightPx + 'px'
}
});
});
}
// 判断是否主要是中文
function isChinese(text) {
return /[\u4e00-\u9fa5]/.test(text);
}
// 分割线
elements.push({
tag: 'rect',
id: 'divider',
position: { top: btnTop + 'px', left: modalLeft + 'px', width: modalWidth + 'px', height: '0.5px' },
rectStyles: { color: '#b3b3b3' }
});
view.draw(elements);
// 绘制按钮(不变)
if (options.showCancel) {
const btnWidth = Math.floor((modalWidth - 2 * borderRadius) / 2);
view.draw([
{ tag: 'rect', id: 'cancelBtn', position: { top: (btnTop + 2) + 'px', left: (modalLeft + borderRadius) + 'px', width: btnWidth + 'px', height: (btnHeight - 3) + 'px' }, rectStyles: { color: '#fff' } },
{ tag: 'font', id: 'cancelText', text: options.cancelText || '取消', position: { top: btnTop + 'px', left: (modalLeft + borderRadius) + 'px', width: btnWidth + 'px', height: btnHeight + 'px' }, textStyles: { color: '#007AFF', size: '16px', align: 'center', verticalAlign: 'middle' } },
{ tag: 'rect', id: 'btnDivider', position: { top: btnTop + 'px', left: (modalLeft + borderRadius + btnWidth) + 'px', width: '0.5px', height: btnHeight + 'px' }, rectStyles: { color: '#b3b3b3' } },
{ tag: 'rect', id: 'confirmBtn', position: { top: (btnTop + 2) + 'px', left: (modalLeft + borderRadius + btnWidth + 1) + 'px', width: btnWidth + 'px', height: (btnHeight - 3) + 'px' }, rectStyles: { color: '#fff' } },
{ tag: 'font', id: 'confirmText', text: options.confirmText || '确定', position: { top: btnTop + 'px', left: (modalLeft + borderRadius + btnWidth + 1) + 'px', width: btnWidth + 'px', height: btnHeight + 'px' }, textStyles: { color: '#007AFF', size: '16px', align: 'center', verticalAlign: 'middle' } }
]);
} else {
view.draw([
{ tag: 'rect', id: 'confirmBtn', position: { top: (btnTop + 2) + 'px', left: (modalLeft + borderRadius) + 'px', width: (modalWidth - 2 * borderRadius) + 'px', height: (btnHeight - 3) + 'px' }, rectStyles: { color: '#fff' } },
{ tag: 'font', id: 'confirmText', text: options.confirmText || '确定', position: { top: btnTop + 'px', left: (modalLeft + borderRadius) + 'px', width: (modalWidth - 2 * borderRadius) + 'px', height: btnHeight + 'px' }, textStyles: { color: '#007AFF', size: '16px', align: 'center', verticalAlign: 'middle' } }
]);
}
// 点击事件(不变)
view.addEventListener("click", function(e) {
const x = e.clientX;
const y = e.clientY;
if (options.showCancel) {
const btnWidth = Math.floor((modalWidth - 2 * borderRadius) / 2);
const cancelBtnLeft = modalLeft + borderRadius;
if (y > btnTop && y < btnTop + btnHeight) {
if (x >= cancelBtnLeft && x < cancelBtnLeft + btnWidth) {
options.cancel?.();
view.close();
} else if (x >= cancelBtnLeft + btnWidth + 1 && x < cancelBtnLeft + 2 * btnWidth + 1) {
options.success?.({ confirm: true });
view.close();
}
}
} else {
const btnLeft = modalLeft + borderRadius;
if (y > btnTop && y < btnTop + btnHeight && x >= btnLeft && x < btnLeft + (modalWidth - 2 * borderRadius)) {
options.success?.({ confirm: true });
view.close();
}
}
}, false);
view.show();
return {
close: () => {
view.close();
}
};
}
+127
View File
@@ -0,0 +1,127 @@
export const optionSuccessIcon = (color : string) : string => {
const svgXml = `
<svg t="1754618739937" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6322" width="64" height="64"><path d="M843.693959 293.609061 425.255869 712.056362 186.145026 472.947566 66.579883 592.504522 425.255869 951.165158 963.260126 413.174204Z" fill="${color}" p-id="6323"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
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();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
export const examinationSheetIcon = () : string => {
const svgXml = `
<svg t="1754630871725" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6324" width="64" height="64"><path d="M802.107077 51.2a145.289846 145.289846 0 0 1 145.092923 145.526154v630.547692a145.289846 145.289846 0 0 1-145.092923 145.526154H221.892923a145.329231 145.329231 0 0 1-145.092923-145.526154V196.726154A145.289846 145.289846 0 0 1 221.892923 51.2h580.214154z m-3.387077 76.8H225.28a71.837538 71.837538 0 0 0-71.561846 67.741538l-0.118154 4.253539v624.009846a71.837538 71.837538 0 0 0 67.465846 71.876923l4.214154 0.118154h573.44c38.203077 0 69.356308-29.932308 71.561846-67.741538l0.118154-4.253539V200.034462a71.876923 71.876923 0 0 0-67.465846-71.876924l-4.214154-0.157538zM729.6 512a38.4 38.4 0 1 1 0 76.8h-230.4a38.4 38.4 0 1 1 0-76.8h230.4z m-384 0a38.4 38.4 0 1 1 0 76.8h-51.2a38.4 38.4 0 1 1 0-76.8h51.2z m384-204.8a38.4 38.4 0 1 1 0 76.8h-230.4a38.4 38.4 0 1 1 0-76.8h230.4z m-384 0a38.4 38.4 0 1 1 0 76.8h-51.2a38.4 38.4 0 0 1 0-76.8h51.2z" p-id="6325"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
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();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
export const feedbackIcon = () : string => {
const svgXml = `
<svg t="1754962550365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6478" width="64" height="64"><path d="M512 853.333333C323.4816 853.333333 170.666667 700.5184 170.666667 512S323.4816 170.666667 512 170.666667s341.333333 152.814933 341.333333 341.333333-152.814933 341.333333-341.333333 341.333333z m0-51.2a290.133333 290.133333 0 1 0 0-580.266666 290.133333 290.133333 0 0 0 0 580.266666z m-25.6-429.738666a25.6 25.6 0 1 1 51.2 0v248.2176a25.6 25.6 0 0 1-51.2 0v-248.251734zM512 729.1904a31.061333 31.061333 0 1 1 0-62.088533 31.061333 31.061333 0 0 1 0 62.088533z" fill="#999999" p-id="6479"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// 通过考试
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();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// 完成练习
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();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// 锁定
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();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
// 练习退出图标
export const practiceExitIcon = () : string => {
const svgXml = `
<svg t="1756432094912" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7026" width="32" height="32"><path d="M833.633836 551.096437 481.955218 551.096437c-16.207124 0-29.359678-13.019525-29.359678-29.322839 0-16.188704 13.057387-29.321816 29.359678-29.321816l359.371834 0-74.718749-74.719772c-11.435447-11.414981-11.473309-29.894867 0.057305-41.424458 11.453866-11.453866 30.124088-11.340279 41.407062-0.038886l109.329011 109.310591c5.555529 1.069355 10.881838 3.703345 15.196097 8.018627 6.681166 6.681166 9.468652 15.691377 8.399297 24.319895 1.069355 8.630564-1.718131 17.640776-8.399297 24.321942-4.314259 4.315282-9.640568 6.987135-15.196097 8.018627l-109.329011 109.309568c-11.281951 11.30037-29.953196 11.413957-41.407062-0.039909-11.530614-11.530614-11.491729-30.009478-0.057305-41.423435L833.633836 551.096437zM745.933425 238.32392l0-97.704253c0-43.2572-35.029818-78.230736-78.230736-78.230736l-508.36931 0c-43.181475 0-78.230736 35.011398-78.230736 78.230736l0 742.759641c0 43.256176 35.011398 78.230736 78.230736 78.230736l508.36931 0c43.181475 0 78.230736-35.050284 78.230736-78.230736l0-97.704253c0-16.188704-13.134135-29.321816-29.341259-29.321816-16.208147 0-29.341259 13.133112-29.341259 29.321816l0 78.230736c0 21.571295-17.600867 39.059598-38.925544 39.059598l-469.635123 0c-21.49557 0-38.943964-17.450441-38.943964-39.059598l0-703.852516c0-21.533432 17.600867-39.019689 38.943964-39.019689l469.635123 0c21.515013 0 38.925544 17.410532 38.925544 39.019689l0 78.269621c0 16.187681 13.133112 29.321816 29.341259 29.321816C732.79929 267.645736 745.933425 254.511601 745.933425 238.32392z" fill="#ffffff" p-id="7027"></path></svg>
`.trim();
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="1763361315669" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7687" width="64" height="64"><path d="M512 832c-156.448 0-296.021333-98.730667-418.410667-291.605333a52.938667 52.938667 0 0 1 0-56.789334C215.978667 290.730667 355.552 192 512 192c156.448 0 296.021333 98.730667 418.410667 291.605333a52.938667 52.938667 0 0 1 0 56.789334C808.021333 733.269333 668.448 832 512 832z m0-576c-129.514667 0-249.461333 83.850667-360.117333 256C262.538667 684.149333 382.485333 768 512 768c129.514667 0 249.461333-83.850667 360.117333-256C761.461333 339.850667 641.514667 256 512 256z m0 405.333333c-83.210667 0-150.666667-66.858667-150.666667-149.333333S428.789333 362.666667 512 362.666667s150.666667 66.858667 150.666667 149.333333S595.210667 661.333333 512 661.333333z m0-64c47.552 0 86.101333-38.208 86.101333-85.333333S559.552 426.666667 512 426.666667c-47.552 0-86.101333 38.208-86.101333 85.333333s38.549333 85.333333 86.101333 85.333333z" fill="#ffffff" p-id="7688"></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)}`;
};
// 左右切换箭头
export const arrowRightIcon = (color : string = '#F5212D') : string => {
const svgXml = `
<svg t="1762762748602" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7658" width="64" height="64" fill="${color}"><path d="M462.08 192a47.36 47.36 0 0 0 0 64l256 256-256 256a47.36 47.36 0 1 0 64 64l288.64-288.64A50.56 50.56 0 0 0 832 512a47.36 47.36 0 0 0-14.08-33.28L529.28 192a47.36 47.36 0 0 0-67.2 0z" p-id="7659"></path><path d="M206.08 192a47.36 47.36 0 0 0 0 64l256 256-256 256a47.36 47.36 0 1 0 64 64l291.84-286.08A50.56 50.56 0 0 0 576 512a47.36 47.36 0 0 0-14.08-33.28L273.28 192a47.36 47.36 0 0 0-67.2 0z" p-id="7660" fill="${color}"></path></svg>
`.trim();
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}`;
};
+28
View File
@@ -0,0 +1,28 @@
// 设置科技鹿
import {
getUserInfo
} from "@/common/common.js"
import common from "@/common/common.js"
export const setMascot = () => {
const mascotState = false
if (!mascotState) {
try {
const userInfo = getUserInfo()
const mascotInfo = userInfo?.mascotInfo
if (mascotInfo) {
uni.setTabBarStyle({
midButton: {
iconPath: "/static/images/icon/fawn-" + (mascotInfo?.mascotNo || '1') + ".png",
width: "86px",
height: "86px",
iconWidth: "90px"
},
success: () => {
common.setValue('mascotState', true)
}
})
}
} catch (e) {}
}
return mascotState
}
+272
View File
@@ -0,0 +1,272 @@
/**
* 本模块封装了Android、iOS的应用权限判断、打开应用权限设置界面、以及位置系统服务是否开启
*/
var isIos
// #ifdef APP-PLUS
isIos = (plus.os.name == "iOS")
// #endif
// 判断推送权限是否开启
function judgeIosPermissionPush() {
var result = false;
var UIApplication = plus.ios.import("UIApplication");
var app = UIApplication.sharedApplication();
var enabledTypes = 0;
if (app.currentUserNotificationSettings) {
var settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute("types");
console.log("enabledTypes1:" + enabledTypes);
if (enabledTypes == 0) {
console.log("推送权限没有开启");
} else {
result = true;
console.log("已经开启推送功能!")
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
console.log("推送权限没有开启!");
} else {
result = true;
console.log("已经开启推送功能!")
}
console.log("enabledTypes2:" + enabledTypes);
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
// 判断定位权限是否开启
function judgeIosPermissionLocation() {
var result = false;
var cllocationManger = plus.ios.import("CLLocationManager");
var status = cllocationManger.authorizationStatus();
result = (status != 2)
console.log("定位权限开启:" + result);
// 以下代码判断了手机设备的定位是否关闭,推荐另行使用方法 checkSystemEnableLocation
/* var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
console.log("enable:" + enable);
console.log("status:" + status);
if (enable && status != 2) {
result = true;
console.log("手机定位服务已开启且已授予定位权限");
} else {
console.log("手机系统的定位没有打开或未给予定位权限");
} */
plus.ios.deleteObject(cllocationManger);
return result;
}
// 判断麦克风权限是否开启
function judgeIosPermissionRecord() {
var result = false;
var avaudiosession = plus.ios.import("AVAudioSession");
var avaudio = avaudiosession.sharedInstance();
var permissionStatus = avaudio.recordPermission();
console.log("permissionStatus:" + permissionStatus);
if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {
console.log("麦克风权限没有开启");
} else {
result = true;
console.log("麦克风权限已经开启");
}
plus.ios.deleteObject(avaudiosession);
return result;
}
// 判断相机权限是否开启
function judgeIosPermissionCamera() {
var result = false;
var AVCaptureDevice = plus.ios.import("AVCaptureDevice");
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
console.log("authStatus:" + authStatus);
if (authStatus == 3) {
result = true;
console.log("相机权限已经开启");
} else {
console.log("相机权限没有开启");
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
// 判断相册权限是否开启
function judgeIosPermissionPhotoLibrary() {
var result = false;
var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
var authStatus = PHPhotoLibrary.authorizationStatus();
console.log("authStatus:" + authStatus);
if (authStatus == 3) {
result = true;
console.log("相册权限已经开启");
} else {
console.log("相册权限没有开启");
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
// 判断通讯录权限是否开启
function judgeIosPermissionContact() {
var result = false;
var CNContactStore = plus.ios.import("CNContactStore");
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus == 3) {
result = true;
console.log("通讯录权限已经开启");
} else {
console.log("通讯录权限没有开启");
}
plus.ios.deleteObject(CNContactStore);
return result;
}
// 判断日历权限是否开启
function judgeIosPermissionCalendar() {
var result = false;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = true;
console.log("日历权限已经开启");
} else {
console.log("日历权限没有开启");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// 判断备忘录权限是否开启
function judgeIosPermissionMemo() {
var result = false;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = true;
console.log("备忘录权限已经开启");
} else {
console.log("备忘录权限没有开启");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// Android权限查询
function requestAndroidPermission(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID], // 理论上支持多个权限同时查询,但实际上本函数封装只处理了一个权限的情况。有需要的可自行扩展封装
function(resultObj) {
var result = 0;
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:' + grantedPermission);
result = 1
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:' + deniedPresentPermission);
result = 0
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
result = -1
}
resolve(result);
// 若所需权限被拒绝,则打开APP设置界面,可以在APP设置界面打开相应权限
// if (result != 1) {
// gotoAppPermissionSetting()
// }
},
function(error) {
console.log('申请权限错误:' + error.code + " = " + error.message);
resolve({
code: error.code,
message: error.message
});
}
);
});
}
// 使用一个方法,根据参数判断权限
function judgeIosPermission(permissionID) {
if (permissionID == "location") {
return judgeIosPermissionLocation()
} else if (permissionID == "camera") {
return judgeIosPermissionCamera()
} else if (permissionID == "photoLibrary") {
return judgeIosPermissionPhotoLibrary()
} else if (permissionID == "record") {
return judgeIosPermissionRecord()
} else if (permissionID == "push") {
return judgeIosPermissionPush()
} else if (permissionID == "contact") {
return judgeIosPermissionContact()
} else if (permissionID == "calendar") {
return judgeIosPermissionCalendar()
} else if (permissionID == "memo") {
return judgeIosPermissionMemo()
}
return false;
}
// 跳转到**应用**的权限页面
function gotoAppPermissionSetting() {
if (isIos) {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");
var setting2 = NSURL2.URLWithString("app-settings:");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
// console.log(plus.device.vendor);
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
// 检查系统的设备服务是否开启
// var checkSystemEnableLocation = async function () {
function checkSystemEnableLocation() {
if (isIos) {
var result = false;
var cllocationManger = plus.ios.import("CLLocationManager");
var result = cllocationManger.locationServicesEnabled();
console.log("系统定位开启:" + result);
plus.ios.deleteObject(cllocationManger);
return result;
} else {
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
var result = mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER);
console.log("系统定位开启:" + result);
return result
}
}
export default ({
judgeIosPermission: judgeIosPermission,
requestAndroidPermission: requestAndroidPermission,
checkSystemEnableLocation: checkSystemEnableLocation,
gotoAppPermissionSetting: gotoAppPermissionSetting
})
+94
View File
@@ -0,0 +1,94 @@
import {
getPhoneEnvBool
} from '@/common/common.js'
import common from '@/common/common.js'
const IsAndEnv = getPhoneEnvBool('AND')
import permission from '@/common/permission.js'
/**
* 录音权限工具类
* 统一处理安卓和iOS的录音权限状态:同意、拒绝、未设置
*/
const RecordPermission = {
hasRecordPermission: false,
/**
* 获取当前录音权限状态
* @returns {Promise<'granted'|'denied'|'undetermined'>} 权限状态
*/
getPermissionState() {
// #ifdef APP-PLUS
const authSetting = uni.getAppAuthorizeSetting();
const state = authSetting.microphoneAuthorized;
switch (state) {
case 'authorized':
return 'authorized';
case 'denied':
return 'denied';
case 'not determined':
return 'undetermined';
case 'config error':
return 'undetermined';
default:
return 'undetermined';
}
// #endif
// 非APP环境默认返回以获取方便h5调试
return 'undetermined';
},
/**
* 检查并请求录音权限
* @returns {Promise<boolean>} 是否获得录音权限
*/
requestPermission() {
if (this.hasRecordPermission) {
console.log('直接通过');
return Promise.resolve('authorized');
}
return new Promise(async (resolve, reject) => {
const state = this.getPermissionState();
// 已同意权限
console.log('权限状态', state);
if (state === 'authorized') {
this.hasRecordPermission = true; // 缓存权限状态
resolve('authorized')
};
if (state === 'denied') {
const result = await common.show(
'提示信息',
'当前页面需要使用录音权限,是否前往开启?'
)
if (result) {
// 前往权限设置页面
uni.openAppAuthorizeSetting();
reject('open_app_authorize_setting')
} else {
// 用户取消,返回上一页
reject('cancel')
}
}
if (state === 'undetermined') {
// #ifdef APP-PLUS
if (IsAndEnv) {
// 安卓需要再次请求权限
const permResult = await permission.requestAndroidPermission(
'android.permission.RECORD_AUDIO');
if(permResult === 1) {
resolve('need_request');
} else {
reject('permResultx')
}
} else {
resolve('need_request');
}
// #endif
}
});
},
};
export default RecordPermission;
+174
View File
@@ -0,0 +1,174 @@
// 定义正则表达式
const regexes = {
email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/,
phone: /^(?:(?:\+|00)86)?1\d{10}$/,
url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/,
ip: /^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/,
uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
required: /^.+$/,
is_number: /^\d+(\.\d+)?$/,
is_chinese: /^[\u4e00-\u9fa5]+$/,
is_realname: /^(?:[\u4e00-\u9fa5·]{2,50})$/,
is_english: /^[A-Za-z]+$/,
is_id_card: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/,
is_bank_card: /^[1-9]\d{9,29}$/,
is_car_id: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/,
is_username: /^.{5,18}$/,
is_password: /^.{4,18}$/,
is_email: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
};
// 邮箱
export const checkEmail = (rule, sId, callback) => {
let _idRe = regexes.is_usis_emailername
if (!_idRe.test(sId)) {
callback(new Error('邮箱格式不正确'));
}
callback();
}
// 用户名
export const checkUserName = (rule, sId, callback) => {
let _idRe = regexes.is_username
if (!_idRe.test(sId)) {
callback(new Error('用户名格式不正确'));
}
callback();
}
// 密码
export const checkPassword = (rule, sId, callback) => {
let _idRe = regexes.is_password
if (!_idRe.test(sId)) {
callback(new Error('密码格式不正确'));
}
callback();
}
// 车牌号
export const checkCarId = (rule, sId, callback) => {
let _idRe = regexes.is_car_id
if (!_idRe.test(sId)) {
callback(new Error('车牌号格式不正确'));
}
callback();
}
// 银行卡
export const checkBankCard = (rule, sId, callback) => {
let _idRe = regexes.is_bank_card
if (!_idRe.test(sId)) {
callback(new Error('银行卡号格式不正确'));
}
callback();
}
// 验证手机号
export const checkPhone = (rule, sId, callback) => {
let _idRe = regexes.phone
if (!_idRe.test(sId)) {
callback(new Error('手机号不正确'));
}
callback();
}
// 验证姓名
export const checkName = (rule, sId, callback) => {
let _idRe = regexes.is_chinese
if (!_idRe.test(sId)) {
callback(new Error('姓名不正确'));
}
callback();
}
export const checkRealName = (rule, sId, callback) => {
let _idRe = regexes.is_realname
if (!_idRe.test(sId)) {
callback(new Error('姓名不正确'));
}
callback();
}
// 验证身份证号
export const checkIdCard = (rule, sId, callback) => {
if (!sId) {
return callback(new Error('身份证号不能为空'));
}
let _idRe =
/^\d{6}((((((19|20)\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|(((19|20)\d{2})(0[13578]|1[02])31)|((19|20)\d{2})02(0[1-9]|1\d|2[0-8])|((((19|20)([13579][26]|[2468][048]|0[48]))|(2000))0229))\d{3})|((((\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|((\d{2})(0[13578]|1[02])31)|((\d{2})02(0[1-9]|1\d|2[0-8]))|(([13579][26]|[2468][048]|0[048])0229))\d{2}))(\d|X|x)$/
if (!_idRe.test(sId)) {
callback(new Error('⾝份证长度或格式错误'));
}
//⾝份证城市
const aCity = {
11: "北京",
12: "天津",
13: "河北",
14: "山西",
15: "内蒙古",
21: "辽宁",
22: "吉林",
23: "黑龙江",
31: "上海",
32: "江苏",
33: "浙江",
34: "安徽",
35: "福建",
36: "江西",
37: "山东",
41: "河南",
42: "湖北",
43: "湖南",
44: "广东",
45: "广西",
46: "海南",
50: "重庆",
51: "四川",
52: "贵州",
53: "云南",
54: "西藏",
61: "陕西",
62: "甘肃",
63: "青海",
64: "宁夏",
65: "新疆",
71: "台湾",
81: "香港",
82: "澳门",
91: "国外"
};
if (!aCity[parseInt(sId.substr(0, 2))]) {
callback(new Error('身份证地区有误'));
}
// 出生期验证
const sBirthday = (sId.substr(6, 4) + "-" + Number(sId.substr(10, 2)) + "-" + Number(sId.substr(12, 2)))
.replace(/-/g, "/")
const d = new Date(sBirthday)
if (sBirthday != (d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate())) {
callback(new Error('出生日期有误'));
}
// ⾝份证号码校验
if (sId.length == 18) {
let sum = 0
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2],
codes = "10X98765432"
for (let i = 0; i < sId.length - 1; i++) {
sum += sId[i] * weights[i];
}
const last = codes[sum % 11]; //计算出来的最后⼀位⾝份证号码
if (sId[sId.length - 1] != last) {
callback(new Error('身份证号校验码有误'));
}
callback();
}
};
export const validator = {
checkName,
checkIdCard,
checkPhone,
checkCarId,
checkBankCard,
checkUserName,
checkPassword,
checkEmail
}
export default regexes;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="30px" height="30px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
.st1{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M17.1,24.2h-12c-0.2,0-0.3-0.2-0.3-0.3v-9.3c0-0.2,0.2-0.3,0.3-0.3h12c0.2,0,0.3,0.2,0.3,0.3v9.3
C17.5,24.1,17.3,24.2,17.1,24.2z"/>
<path class="st0" d="M16.6,5.4c4.8,0,8.7,3.9,8.7,8.7"/>
<polyline class="st0" points="19.3,10.1 14.9,5.6 19.3,1.2 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 791 B

@@ -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>
@@ -0,0 +1,175 @@
<template>
<view class="course-item-box" @click="clickItem">
<view class="item-title">{{itemInfo.crsName}}</view>
<view class="img-box-view">
<ImagePreview class="img-box" :imgId="itemInfo.imgId" :isCache="true" width="225" height="127">
</ImagePreview>
<view class="item-grade">{{itemInfo.evalGrade}}</view>
<!-- <image class="item-grade" mode="aspectFill"></view> -->
</view>
<view class="cont-box">
<scroll-view class="item-tags" :scroll-x="true">
<view class="item-tag-view">
<view :class="['item-tag', this.activeTag.includes(item.tagId)?'active-tag':'']" v-for="item in tagsShowList">
{{item.tagName}}
</view>
</view>
</scroll-view>
<view class="item-publish">更新{{itemInfo.mtTime}}</view>
<view class="item-num">已学{{itemInfo.accmStdyCnt}}人次</view>
</view>
</view>
</template>
<script>
import ImagePreview from '@/components/image-preview/image-preview.vue'
export default {
name: "course-item",
components: {
ImagePreview
},
props: {
itemInfo: {
type: Object,
default: () => ({
crsName: '',
tagCataList: [],
publicTime: "",
learnedNum: ''
})
},
activeTag: {
default: () => [],
type: Array
}
},
data() {
return {
};
},
watch: {
activeTag: {
handler() {},
deep: true,
immediate: true
}
},
computed: {
tagsShowList() {
if (!this.activeTag) return this.itemInfo.tagCataList || []
let _arr = (this.itemInfo.tagCataList || []).filter(t => this.activeTag.includes(t.tagId))
let _arr1 = (this.itemInfo.tagCataList || []).filter(t => !this.activeTag.includes(t.tagId))
return _arr.concat(_arr1)
}
},
methods: {
clickItem() {
this.$emit('clickItem', this.itemInfo)
}
}
}
</script>
<style lang="scss" scoped>
.course-item-box {
padding: 12rpx 15rpx 8rpx;
overflow: hidden;
width: 100%;
.img-box-view {
width: 225rpx;
height: 127rpx;
float: left;
position: relative;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 0 10rpx #c3c3c3;
.img-box {
width: 225rpx;
height: 127rpx;
}
.item-grade {
position: absolute;
right: 0;
width: 62rpx;
height: 32rpx;
background-color: #ff581c;
top: 20rpx;
border-radius: 8rpx 0 0 8rpx;
color: #fff;
text-align: center;
// padding-right: 10rpx;
font-size: 22rpx;
line-height: 32rpx;
}
}
.item-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 44rpx;
font-size: 29rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.cont-box {
width: calc(100% - 225rpx);
box-sizing: border-box;
padding: 0 10rpx 0 20rpx;
float: left;
.item-tags {
margin-bottom: 14rpx;
.item-tag-view {
white-space: nowrap;
height: 44rpx;
.item-tag {
height: 38rpx;
line-height: 34rpx;
box-sizing: border-box;
font-size: 20rpx;
background-color: #fff;
color: #06f;
border: 2rpx solid #06f;
padding: 0 20rpx;
border-radius: 8rpx;
margin-right: 20rpx;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
box-sizing: border-box;
&.active-tag {
// background-color: #ffa150;
color: #ffa150;
border: 2rpx solid #ffa150;
}
}
}
}
.item-publish,
.item-num {
height: 32rpx;
line-height: 32rpx;
font-size: 20rpx;
color: rgb(102, 102, 102);
margin-top: 6rpx;
white-space: nowrap;
}
.item-publish {
margin-top: 0rpx;
}
}
}
</style>
@@ -0,0 +1,187 @@
<template>
<view class="item">
<view class="top">
<view class="type">
{{ subject_type_text }}
</view>
<view class="score">5</view>
<view class="fl1"></view>
<view class="right">
<view class="mark_and_feedback_div">
<image
:src="markIcon(props.item.mark ? '#0066FF' : '#ABABAB')"
class="img"
@click="mark_click()"
></image>
<image :src="feedbackIcon()" class="img" @click="feedback_click()"></image>
</view>
<!-- <view class="right_text">展开</view>
<uv-icon class="arrow-icon" name="arrow-right"></uv-icon> -->
</view>
</view>
<view class="question_text">
{{ subject_data.qnsContent }}
</view>
<view class="option_div">
<questionVue @changePlay="changePlay" :dataInfo="talkingInfo" :activeVoiceId="questionValue"></questionVue>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed, toRaw, watch } from 'vue';
import questionVue from './question.vue';
import radioSubjectItem from './radio-subject-item.vue';
import { markIcon, feedbackIcon } from '@/common/imgSvg';
import { SUBJECT_TYPE } from '@/enum.js';
const questionValue = ref('');
const props = defineProps({
item: {
type: Object,
required: true
},
mode: {
type: String,
required: true
},
activeVoiceId: {
type: String,
required: true
},
voicePathValue: {
type: String,
required: true
}
});
const emit = defineEmits(['subject_click']);
const talkingInfo = ref({
id: '3',
intrctWay: '02',
intrctContent: '中的i id一二三。',
talkingText: '中的i id一二三。',
talkingVoice: ''
});
watch(
() => props.voicePathValue,
(val) => {
if (val) {
talkingInfo.value = {
id: '3',
intrctWay: '02',
intrctContent: '中的i id一二三。',
talkingText: '中的i id一二三。',
talkingVoice: val
};
}
}
);
const feedback_click = () => {
emit('subject_click', 'feedback', props.item);
};
const mark_click = () => {
emit('subject_click', 'mark', props.item);
};
const customStyle = {
borderRadius: '16rpx',
width: '292rpx',
backgroundColor: '#0066FF',
border: '1rpx solid rgb(12, 123, 245)'
};
const subject_data = reactive({
qnsId: '',
qnsContent: '',
qnsTyp: ''
});
Object.assign(subject_data, {
qnsId: props.item.qnsId,
qnsContent: props.item.qnsContent,
qnsTyp: props.item.qnsTyp
});
//
const subject_type_text = computed(() => SUBJECT_TYPE.find((res) => subject_data.qnsTyp === res.value)?.label || '');
const changePlay = (id) => {
console.log('changePlay');
questionValue.value = id;
// emit('handleEvents', 'changePlay', props.talkingInfo, id)
};
</script>
<style scoped lang="scss">
.mark_and_feedback_div {
display: flex;
height: 100%;
justify-content: end;
align-items: center;
.img {
width: 38rpx;
height: 38rpx;
margin-left: 4rpx;
padding: 4rpx;
}
}
.option_div {
padding-top: 46rpx;
}
.item {
// height: 170rpx;
overflow: hidden;
display: flex;
flex-direction: column;
padding-bottom: 40rpx;
.top {
display: flex;
align-items: center;
.type {
width: 108rpx;
height: 46rpx;
background: #267eff;
border-radius: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 25rpx;
color: #ffffff;
text-align: center;
line-height: 46rpx;
}
.score {
margin-left: 24rpx;
font-family: ArialMT;
font-size: 30rpx;
color: #333333;
text-align: left;
}
.right {
display: flex;
align-items: center;
.right_text {
font-family: PingFangSC;
font-weight: 600;
font-size: 28rpx;
color: #333;
line-height: 40rpx;
text-align: left;
font-style: normal;
}
}
}
.question_text {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 20rpx;
}
}
</style>
@@ -0,0 +1,148 @@
<template>
<view class="analysis_div" @click.stop="click_option()" :class="status">
<view class="line">
<text class="tr">维度评分</text>
<text class="td">{{ dimension_all_score }}</text>
<view class="td_success_error_img">
<image :src="select_status_icon" class="img"></image>
</view>
</view>
<view class="line" v-for="item in evalSettingVos">
<text class="tr">{{ item.gradeDimens }}</text>
<text class="td">{{ item.anlyGrade }}</text>
<view class="your_answer" v-if="item.dimensCode === '01'">
<rich-text :nodes="highlightedText" class="content" />
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
const emit = defineEmits(['click_option']);
const props = defineProps({
value: {
type: Object,
default: () => {},
required: false
},
judgeResult: {
type: Boolean,
default: false,
required: false
},
anserResult: {
type: String,
required: true
}
});
const evalSettingVos = computed(() => props.value.evalSettingVos);
const status = computed(() => (props.judgeResult ? 'success' : 'error'));
const select_status_icon = computed(() => {
if (props.judgeResult) {
return optionSuccessIcon('#15B859');
} else {
return optionErrorIcon();
}
});
const keywords = computed(() => {
if (!props.value.relKeyword) return [];
return props.value.relKeyword.split(/[,、/]/).map((key) => key.trim());
});
const highlightedText = computed(() => {
//
if (!keywords.value.length) return '你的答案:' + props.anserResult;
//
//
const escapedKeywords = keywords.value.map((keyword) => keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
// gi
const regex = new RegExp(`(${escapedKeywords.join('|')})`, 'gi');
// span
return '你的答案:' + props.anserResult.replace(regex, (match) => `<span class="highlight">${match}</span>`);
});
const dimension_all_score = computed(() => {
return (
evalSettingVos.value
// mapitemanlyGrade0
.map((item) => (item?.anlyGrade && typeof item.anlyGrade === 'number' ? item.anlyGrade : 0))
// reduce
.reduce((total, current) => total + current, 0)
);
});
const click_option = () => {
emit('click_option', props.analysisVo);
};
</script>
<style scoped lang="scss">
.success {
background-color: #e7f8ed;
.td {
color: #15b859;
}
:deep(.highlight) {
color: #15b859;
}
}
.error {
background-color: #fce8e9;
.td {
color: #f5212d;
}
:deep(.highlight) {
color: #f5212d;
}
}
.your_answer {
:deep(.highlight) {
text-decoration: underline;
}
}
.analysis_div {
margin-top: 30rpx;
border-radius: 16rpx;
padding: 22rpx 15rpx 15rpx 34rpx;
.line {
margin: 12rpx 0;
position: relative;
}
.tr {
font-weight: 600;
font-size: 30rpx;
color: #333333;
}
.td {
font-weight: 600;
font-size: 30rpx;
}
.your_answer {
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 46rpx;
text-align: left;
margin-top: 12rpx;
}
.td_success_error_img {
position: absolute;
right: 0;
top: 0;
.img {
width: 30rpx;
height: 30rpx;
padding-right: 20rpx;
}
}
}
</style>
@@ -0,0 +1,399 @@
<template>
<view class="talking-line">
<view class="talking-info" v-if="status === '02'">
<view :class="['talking-gif', isPlaying ? 'is-play' : '']">
<!-- <CommonLoading color="#fff" /> -->
</view>
<view class="right-time">{{ voiceTime }}</view>
<view class="talking-cont">
<text :class="['talking-text translate-text loading']" v-show="textShow">
{{ showContent }}
</text>
<view class="talking-text translate-text" v-if="textLoading">
<CommonLoading color="#fff" />
</view>
</view>
<view class="btns-cont">
<view class="play-btn">
<image
v-show="!isPlaying"
class="icon"
@click="playVoice"
src="@/static/images/course/play-blue.png"
style="width: 100%; height: 100%"
></image>
<image
v-show="isPlaying"
class="icon"
@click.stop="playVoice"
src="@/static/images/course/stop-blue.png"
style="width: 100%; height: 100%"
></image>
</view>
<view class="text-btn" v-if="!textShow" @click.stop="translateVoice()"></view>
<view class="fl1"></view>
<view
v-show="!props.isPreview && props.button_again_show"
class="btns-cont-button-text"
@click.stop="buutton_click('again')"
>
重答
</view>
<view
v-show="!props.isPreview && props.button_complete_show"
class="btns-cont-button-text"
@click.stop="buutton_click('complete')"
>
完成
</view>
</view>
</view>
<!-- 01纯文字 -->
<view class="talking-info talking-info-text" v-else-if="status === '01'">
<view class="talking-cont">
<text :class="['talking-text translate-text loading']">
{{ showContent }}
</text>
</view>
<view class="btns-cont" v-show="!props.isPreview">
<view class="fl1"></view>
<view v-if="props.button_again_show" class="btns-cont-button-text" @click.stop="buutton_click('again')">
重答
</view>
<view
v-if="props.button_complete_show"
class="btns-cont-button-text"
@click.stop="buutton_click('complete')"
>
完成
</view>
</view>
</view>
<view class="talking-info talking-info-loading" v-else-if="status === '00'">
<view :class="['talking-gif']">
<CommonLoading color="#fff" />
</view>
</view>
</view>
</template>
<script setup>
import { computed, unref, ref, toRefs, watch, onMounted, nextTick } from 'vue';
import { onUnload } from '@dcloudio/uni-app';
import { useAudio } from './useAudio.js';
import { downloadFile } from '@/api/common.js';
const props = defineProps({
dataInfo: {
default: () => ({}),
type: Object
},
text: {
default: '',
type: String
},
voice: {
//
default: '',
type: String
},
activeVoiceId: {
default: '',
type: String
},
defaultDisplayText: {
default: false,
type: Boolean
},
status: {
default: '',
type: String
},
isPreview: {
//
default: false,
type: Boolean
},
button_again_show: {
//
default: true,
type: Boolean
},
button_complete_show: {
//
default: true,
type: Boolean
}
});
const innerAudio = useAudio();
const status = computed(() => props.status);
watch(
() => props.status,
(val) => {
if (val === '') {
textShow.value = false;
}
}
);
const emit = defineEmits(['changePlay', 'buutton_click']);
//
const showContent = computed(() => {
return props.text || props.dataInfo.anserResult || '';
});
const isPlaying = innerAudio.isPlaying;
const textLoading = ref(false);
const textShow = ref(props.defaultDisplayText); //
const voiceTime = ref('');
const itemVoice = ref('');
//
const buutton_click = (type) => {
if (type === 'again') {
nextTick(() => {
textShow.value = true;
});
setTimeout(() => {
voiceTime.value = '';
}, 0);
}
emit('buutton_click', type, props.dataInfo);
};
//
const translateVoice = (type) => {
if (!textShow.value) {
textLoading.value = true;
setTimeout(() => {
textShow.value = true;
textLoading.value = false;
}, 500);
} else {
textShow.value = false;
}
};
const playVoice = () => {
console.log('点击播放');
innerAudio.playAudio();
};
let unsubscribe = () => {};
onMounted(() => {
console.log('question的onMounted');
// 2. Store isPlaying
// unsubscribe = innerAudio.$subscribe((mutation, state) => {
// isPlaying.value = state.isPlaying;
// });
//
innerAudio.onAudioEnded(() => {
console.log('播放完成(通过回调)');
setTimeout(() => {
isPlaying.value = false;
}, 60);
});
//
innerAudio.onAudioError((error) => {
console.error('播放错误(通过回调):', error);
});
//
innerAudio.onAudioLoaded((e) => {
console.log('声音加载完毕', e.duration);
//
const originalTime = Math.ceil(e.duration) || 1;
let displayTime
if(originalTime >= 118 && originalTime <= 122) {
// 118-123
displayTime = 120
} else if(originalTime >= 58 && originalTime <= 62) {
// 58-62
displayTime = 60
} else {
displayTime = originalTime
}
//
console.log('displayTime', displayTime);
voiceTime.value = displayTime + 's';
});
watch(
() => props.dataInfo.voicePath,
(newVal) => {
console.log('加载声音', props.dataInfo);
if (newVal === '' && props.dataInfo.ossKey) {
// oss
console.log('真下载oss内容');
downloadFile(props.dataInfo.ossKey).then((res) => {
voiceTime.value = '';
innerAudio.setAudioSrc(res.tempFilePath);
});
} else if (newVal === undefined) {
innerAudio.stopAudio();
} else {
voiceTime.value = '';
innerAudio.setAudioSrc(newVal);
}
},
{ immediate: true }
);
});
onUnload(() => {
// 1.
innerAudio.stopAudio();
// 2.
innerAudio.removeCallbacks();
unsubscribe();
});
</script>
<style scoped lang="scss">
.talking-line {
display: flex;
justify-content: flex-end;
}
.talking-info {
padding: 20rpx;
border-radius: 15rpx;
background-color: #06f;
padding-bottom: 30rpx;
position: relative;
color: #fff;
width: 100%;
.talking-gif {
width: 200rpx;
height: 38rpx;
background: url(@/static/images/course/voice-gray.png) no-repeat;
background-size: contain;
padding-left: 168rpx;
&.is-play {
background: url(@/static/images/course/voice-white.gif) no-repeat;
background-size: contain;
}
}
.right-time {
position: absolute;
right: 20rpx;
top: 20rpx;
font-size: 28rpx;
height: 38rpx;
line-height: 38rpx;
}
&.talking-info-loading {
padding-bottom: 20rpx;
}
&.talking-info-text {
padding-bottom: 20rpx;
width: auto;
min-width: calc(100vw * 0.45);
// float: right;
.talking-cont {
// margin-top: 0rpx;
}
}
.talking-cont {
// margin-top: 16rpx;
overflow: hidden;
.talking-text {
font-size: 28rpx;
line-height: 48rpx;
margin-bottom: 20rpx;
word-break: break-all;
white-space: pre-line;
overflow: hidden;
&.translate-text {
min-height: 48rpx;
}
}
}
.btns-cont {
overflow: hidden;
border-top: 3rpx solid #1f79ff;
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
margin-top: 14rpx;
.replay-btn {
float: left;
width: 46rpx;
height: 46rpx;
overflow: hidden;
margin-right: 16rpx;
margin-top: 25rpx;
}
.play-btn {
width: 46rpx;
height: 46rpx;
margin-right: 16rpx;
}
.text-btn {
width: 46rpx;
height: 46rpx;
overflow: hidden;
line-height: 46rpx;
text-align: center;
color: #fff;
margin-right: 16rpx;
background-color: #67a4ff;
font-size: 24rpx;
font-weight: bold;
border-radius: 20rpx;
}
.text-right-btn {
float: right;
padding: 0 10rpx;
height: 46rpx;
overflow: hidden;
line-height: 46rpx;
text-align: center;
margin-left: 16rpx;
margin-top: 25rpx;
}
.next-btn {
float: right;
padding: 0 60rpx 0 20rpx;
height: 62rpx;
line-height: 62rpx;
font-size: 30rpx;
background-color: #06f;
color: #fff;
border-radius: 8rpx;
margin-top: 16rpx;
position: relative;
.icon-iamge {
width: 25rpx;
height: 25rpx;
position: absolute;
right: 20rpx;
top: 50%;
margin-top: -12rpx;
}
}
.btns-cont-button-text {
font-weight: 400;
font-size: 30rpx;
color: #ffffff;
line-height: 30rpx;
text-align: left;
font-style: normal;
margin-left: 18rpx;
}
}
}
</style>
@@ -0,0 +1,124 @@
<template>
<view class="option_item" @click.stop="click_option()" :class="select_status">
<view class="option_text">
{{props.analysisVo.itemNo}}. {{props.analysisVo.itemContent}}
</view>
<image :src="select_status_icon" class="img"></image>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
nextTick,
computed,
toRaw
} from 'vue'
import {
optionErrorIcon,
optionSuccessIcon
} from '@/common/imgSvg'
const emit = defineEmits(['click_option'])
const props = defineProps({
analysisVo: {
type: Object,
required: true,
},
mode: {
type: String,
required: false,
},
my_answer: {
type: Array,
default: () => []
}
});
const select_status = computed(() => {
//
const my_answer_array = props.my_answer;
if(my_answer_array.includes(props.analysisVo.itemId) && props.analysisVo.correctFlag === 'E') return 'error';
// 绿
if(my_answer_array.includes(props.analysisVo.itemId) && props.analysisVo.correctFlag === 'R') return 'success';
// ,
if(my_answer_array.includes(props.analysisVo.itemId)) return 'primary';
//
return 'info';
})
const select_status_icon = computed(() => {
if (select_status.value === 'primary') {
return optionSuccessIcon('#0066FF')
} else if (select_status.value === 'success') {
return optionSuccessIcon('#15B859')
} else if (select_status.value === 'error') {
return optionErrorIcon()
}
})
const click_option = () => {
emit('click_option', props.analysisVo)
}
</script>
<style scoped lang="scss">
.success {
color: #15B859;
background: #E7F8ED;
}
.primary {
color: #333333;
background: #ECF3FE;
}
.info {
color: #333333;
background: #F9F9F9;
}
.error {
color: #F5212D;
background: #FCE8E9;
}
.option_item {
width: 100%;
min-height: 108rpx;
border-radius: 15rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
text-align: left;
font-style: normal;
display: flex;
align-items: center;
margin-top: 32rpx;
padding: 10rpx 10rpx 10rpx 52rpx;
// display: -webkit-box;
// -webkit-box-orient: vertical;
// -webkit-line-clamp: 2;
/* 显示的行数 */
overflow: hidden;
.option_text {
flex: 1;
// white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
}
.img {
width: 30rpx;
height: 30rpx;
padding-right: 20rpx;
}
}
</style>
@@ -0,0 +1,214 @@
<template>
<view class="item">
<view class="top">
<view class="type">
{{subject_type_text}}
</view>
<view class="score" v-if="mode !== 'study'">
5
</view>
<view class="fl1" v-if="mode !== 'study'">
</view>
<view class="right" v-if="mode !== 'study'">
<view class="mark_and_feedback_div">
<image :src="markIcon(props.item.mark?'#0066FF':'#ABABAB')" class="img" @click="mark_click()"></image>
<image :src="feedbackIcon()" class="img" @click="feedback_click()"></image>
</view>
<!-- <view class="right_text">展开</view>
<uv-icon class="arrow-icon" name="arrow-right"></uv-icon> -->
</view>
</view>
<view class="question_text">
{{subject_data.qnsContent}}
</view>
<view class="option_div">
<radioSubjectItem v-for="(analysisVo,index) in subject_data.itemVos" @click_option="click_option" :key="index"
:analysisVo="analysisVo" :my_answer="props.item.my_answer" :mode="props.mode">
</radioSubjectItem>
</view>
<view class="option_div_button" v-if="subject_data.qnsTyp==='MU' && ['practice','study'].includes(props.mode) && !props.isPreview">
<uv-button type="primary" class="send_evaluate" :custom-style="customStyle" :throttleTime="300"
@click="muCheckbox" :hairline="false">确定</uv-button>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
nextTick,
computed,
toRaw
} from 'vue'
import radioSubjectItem from './radio-subject-item.vue'
import {
markIcon,
feedbackIcon
} from '@/common/imgSvg'
import {
SUBJECT_TYPE
} from '@/enum.js'
const customStyle = {
borderRadius: '16rpx',
width: '292rpx',
backgroundColor: '#0066FF',
border: '1rpx solid rgb(12, 123, 245)',
}
const emit = defineEmits(['subject_click'])
const props = defineProps({
item: {
type: Object,
required: true,
},
mode: {
type: String,
required: true,
},
//
isPreview:{
type: Boolean,
default: false
}
});
const feedback_click = () => {
emit('subject_click', 'feedback', props.item)
}
const mark_click = () => {
emit('subject_click', 'mark', props.item)
}
const subject_data = reactive({
qnsId: '',
qnsContent: '',
qnsTyp: '',
itemVos: props.item.itemVos.map(res => ({
...res,
})),
})
Object.assign(subject_data, {
qnsId: props.item.qnsId,
qnsContent: props.item.qnsContent,
qnsTyp: props.item.qnsTyp,
})
const item_answer = ref(props.item?.my_answer.split(','))
//
const subject_type_text = computed(() => SUBJECT_TYPE.find(res => subject_data.qnsTyp === res.value)?.label || '')
//
const click_option = (analysisVo) => {
// //
if (props.item.my_answer.includes(analysisVo.itemId)) {
// props.item.my_answer = props.item.my_answer.filter(id => id !== analysisVo.itemId);
} else if (props.item.qnsTyp === 'SN') { //
//
// props.item.my_answer[0] = analysisVo.itemId
muCheckbox()
} else if (props.item.qnsTyp === 'MU') { //
//
// props.item.my_answer.push(analysisVo.itemId)
} else if (props.item.qnsTyp === 'JD') { //
//
// props.item.my_answer = [analysisVo.itemId]
muCheckbox()
}
}
const muCheckbox = () => {
emit('subject_click', 'answer', {
qnsTyp: props.item.qnsTyp,
qnsId: props.item.qnsId,
answer: props.item.my_answer,
answerItems: subject_data.itemVos.filter(t=> props.item.my_answer.includes(t.itemId))
})
}
</script>
<style scoped lang="scss">
.mark_and_feedback_div {
display: flex;
height: 100%;
justify-content: end;
align-items: center;
.img {
width: 38rpx;
height: 38rpx;
margin-left: 4rpx;
padding: 4rpx;
}
}
.option_div_button {
width: 100%;
height: 84rpx;
padding-top: 30rpx;
// background-color: red;
display: flex;
flex-direction: column;
align-items: center;
}
.item {
// height: 170rpx;
overflow: hidden;
display: flex;
flex-direction: column;
padding-bottom: 40rpx;
.top {
display: flex;
align-items: center;
.type {
width: 108rpx;
height: 46rpx;
background: #267EFF;
border-radius: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 25rpx;
color: #FFFFFF;
text-align: center;
line-height: 46rpx;
}
.score {
margin-left: 24rpx;
font-family: ArialMT;
font-size: 30rpx;
color: #333333;
text-align: left;
}
.right {
display: flex;
align-items: center;
.right_text {
font-family: PingFangSC;
font-weight: 600;
font-size: 28rpx;
color: #333;
text-align: left;
font-style: normal;
}
}
}
.question_text {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 20rpx;
}
}
</style>
@@ -0,0 +1,541 @@
<template>
<view class="item">
<view class="top">
<view class="type">{{ subject_type_text }}</view>
<view class="score" v-if="['settlement', 'examination', 'pk_in'].includes(props.mode)">{{ item.tgtGrade }}</view>
<view class="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'"
:src="markIcon(props.item.mark ? '#e8b531' : '#ABABAB')"
class="img"
@click="mark_click()"
></image>
<image
:src="feedbackIcon()"
class="img"
@click="feedback_click()"
v-if="props.mode2 !== 'mistake'"
></image>
</view>
<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"
:class="{ click_text_icon_action: animationState }"
color="#0066FF"
></uv-icon>
</view>
</template>
</view>
</view>
<view class="question_text">
{{ subject_data.qnsContent }}
</view>
<view :style="{ height: animationHeight }" :class="{ collapse_wrapper: props.mode === 'mistake' }" ref="wrapperRef">
<view class="collapse-content" ref="contentRef">
<view class="option_div">
<template v-if="qnsTyp === 'ES'">
<questionVue
:status="subject_data.es_status"
:dataInfo="props.item?.my_answer"
:isPreview="props.isPreview"
:activeVoiceId="subject_data.qnsId"
@buutton_click="questionButtonClick"
:button_complete_show="['practice', 'pk_in'].includes(props.mode)"
></questionVue>
</template>
<template v-else>
<radioSubjectItem
v-for="(analysisVo, index) in subject_data.itemVos"
@click_option="click_option"
:key="index"
:analysisVo="analysisVo"
:my_answer="item_answer"
></radioSubjectItem>
</template>
</view>
<view
class="option_div_button"
v-if="qnsTyp === 'MU' && ['practice', 'study', 'pk_in'].includes(props.mode) && !props.isPreview && clearResult"
>
<uv-button
type="primary"
class="send_evaluate"
:custom-style="customStyle"
:throttleTime="300"
@click="muBtnClick"
:hairline="false"
>
确定
</uv-button>
</view>
<view
class="analysis_div"
v-if="
!clearResult &&
['study', 'mistake', 'settlement', 'practice', 'practice_record', 'pk_in'].includes(props.mode)
"
>
<view class="analysis_title" v-if="qnsTyp !== 'ES'">
<view class="success_answer">正确答案{{ correctResultLabel.join('') }}</view>
<view class="your_answer">
你的答案
<text :class="right_or_wrong_class ? 'success' : 'error'">{{ myResultLabel.join('') }}</text>
</view>
</view>
<view class="reference_answer" v-if="qnsTyp === 'ES'">
<view class="reference_answer_title">你的答案</view>
{{ subject_data.anserResult ?? '' }}
</view>
<esAnalysis
v-if="qnsTyp === 'ES'"
:value="subject_data.itemVos[0]"
:judgeResult="judgeResultStatus"
:anserResult="subject_data.anserResult"
></esAnalysis>
<!-- 问答题专属参考答案 -->
<view class="reference_answer" v-if="qnsTyp === 'ES'">
<view class="reference_answer_title">参考答案</view>
{{ subject_data.itemVos[0]?.itemAnswer ?? '' }}
</view>
<view class="analysis_note" v-if="props.mode !== 'pk_in'">试题解析{{ subject_data?.analyContent }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, computed, getCurrentInstance, watch } from 'vue';
import radioSubjectItem from './radio-subject-item.vue';
import esAnalysis from './es-analysis.vue';
import questionVue from './question.vue';
import { markIcon, feedbackIcon } from '@/common/imgSvg';
import { SUBJECT_TYPE } from '@/enum.js';
import common from '@/common/common';
//
const customStyle = {
borderRadius: '16rpx',
width: '292rpx',
backgroundColor: '#0066FF',
border: '1rpx solid rgb(12, 123, 245)'
};
const emit = defineEmits(['subject_click', 'update:modelValue']);
const props = defineProps({
modelValue: {
type: [Array, Object],
default: () => []
},
item: {
type: Object,
required: true
},
mode: {
type: String,
required: true,
validator: (value) => {
// : examination
// : practice
// : mistake
// : study
// settlement
// practice_record, 'practice_record'
return ['examination', 'practice', 'mistake', 'study', 'settlement', 'practice_record', 'pk_in'].includes(value);
}
},
//
isPreview: {
type: Boolean,
default: false
},
//
clearResult: {
type: Boolean,
default: true
},
mode2: {
type: String,
default: ''
}
});
//
const isExpanded = ref(false);
const wrapperRef = ref(null);
const contentRef = ref(null);
const judgeResultStatus = computed(() => props.item.judgeResult === 'P');
//
const animationState = ref(props.mode !== 'mistake' ? true : false);
const animationHeight = ref(props.mode !== 'mistake' ? '' : '0px');
const modelValue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
});
let answerText = props.item?.my_answer || props.item.anserResult || [];
// item_answer ,
const item_answer = ref(typeof answerText === 'string' ? answerText.split(',') : answerText);
watch(
() => props.modelValue,
(newVal) => {
console.log('modelValue', newVal);
item_answer.value = typeof newVal === 'string' ? newVal.split(',') : newVal;
},
{ deep: true }
);
// watch(
// item_answer,
// (newVal) => {
// console.log('item_answer', newVal);
// emit('update:modelValue', newVal);
// },
// { deep: true }
// );
const feedback_click = () => {
emit('subject_click', 'feedback', props.item);
};
const instance = getCurrentInstance(); //
// /H5App
const toggleExpand = async () => {
console.log('切换展开/折叠状态', animationHeight.value, animationState.value);
if (!animationState.value) {
animationState.value = true;
const query = uni.createSelectorQuery().in(instance);
query
.select('.collapse-content')
.boundingClientRect((rect) => {
animationHeight.value = `${rect.height}px`;
console.log('rect', rect);
})
.exec();
} else {
animationState.value = false;
animationHeight.value = '0px';
}
};
const queryRect = () => {
// this.$uvGetRectgetRect
return new Promise((resolve) => {
this.$uvGetRect(`#${this.elId}`).then((size) => {
resolve(size);
});
});
};
const mark_click = () => {
emit('subject_click', 'mark', props.item);
};
// SN MU JD ES
const qnsTyp = computed(() => props.item.qnsTyp);
//
const correctResultLabel = computed(() =>
subject_data.value.itemVos.filter((res) => res.correctFlag === 'R').map((res) => res.itemNo)
);
//
const myResultLabel = computed(() => {
if (!props.item.anserResult) return ['无'];
const anserResultArray = props.item.anserResult.split(',');
return subject_data.value.itemVos.filter((res) => anserResultArray.includes(res.itemId)).map((res) => res.itemNo);
});
//
const right_or_wrong_class = computed(
() =>
correctResultLabel.value.length === myResultLabel.value.length &&
myResultLabel.value.every((res) => correctResultLabel.value.includes(res))
);
const subject_data = computed({
get: () => {
return {
qnsId: props.item.qnsId,
qnsContent: props.item.qnsContent,
qnsTyp: props.item.qnsTyp,
es_status: props.item.es_status,
analyContent: props.item.analyContent,
anserResult: props.item.anserResult,
itemVos: (props.item.itemVos ?? []).map((res) => ({
...res,
correctFlag: props.clearResult ? '' : res.correctFlag
}))
};
},
set: (val) => {}
});
//
const questionButtonClick = (type, submitObj) => {
console.log('type', type, submitObj);
//
if (type === 'complete') {
//
} else if (type === 'again') {
//
}
emit('subject_click', type, {
qnsTyp: props.item.qnsTyp,
qnsId: props.item.qnsId,
answer: submitObj,
answerItems: []
});
};
//
const subject_type_text = computed(() => SUBJECT_TYPE.find((res) => qnsTyp.value === res.value)?.label || '');
//
const click_option = (analysisVo) => {
if (props.isPreview || !props.clearResult) return;
//
if (item_answer.value.includes(analysisVo.itemId)) {
item_answer.value = item_answer.value.filter((id) => id !== analysisVo.itemId);
modelValue.value = item_answer.value;
if (!['study'].includes(props.mode)) {
muCheckbox('choose');
}
} else if (props.item.qnsTyp === 'SN') {
//
//
item_answer.value[0] = analysisVo.itemId;
modelValue.value = item_answer.value;
muCheckbox('choose');
} else if (props.item.qnsTyp === 'MU') {
//
//
item_answer.value.push(analysisVo.itemId);
modelValue.value = item_answer.value;
if (!['study'].includes(props.mode)) {
muCheckbox('choose');
}
} else if (props.item.qnsTyp === 'JD') {
//
//
item_answer.value = [analysisVo.itemId];
modelValue.value = item_answer.value;
muCheckbox('choose');
}
};
const muBtnClick = () => {
if (item_answer.value.length === 0) {
common.msg('请至少选择一个选项!');
return;
}
muCheckbox('answer');
};
const muCheckbox = (type = 'answer') => {
emit('subject_click', type, {
qnsTyp: props.item.qnsTyp,
qnsId: props.item.qnsId,
answer: item_answer.value,
answerItems: subject_data.value.itemVos.filter((t) => item_answer.value.includes(t.itemId))
});
};
//
onMounted(() => {});
</script>
<style scoped lang="scss">
.expand_button_div {
/* 相对定位,让两个文本重叠 */
// display: inline-block;
// cursor: pointer;
}
.toggle-container {
position: relative;
height: 42rpx;
width: 60rpx;
font-size: 30rpx;
line-height: 42rpx;
}
.toggle-text {
/* 绝对定位使文本重叠 */
position: absolute;
top: 0;
left: 0;
/* 初始状态:非激活文本透明且轻微下移 */
opacity: 0;
// transform: translateY(15rpx);
/* 添加过渡动画 */
transition: all 0.2s ease-out;
/* 确保文本占据空间一致 */
white-space: nowrap;
}
/* 激活状态:文本可见且位置正常 */
.toggle-text.active {
opacity: 1;
transform: translateY(0);
z-index: 1; /* 确保激活文本在上方 */
}
.collapse_wrapper {
overflow: hidden; /* 关键:隐藏超出部分 */
// height: 0; /* 0 */
transition: height 0.3s ease-out;
}
.click_text_icon_action {
transform: rotate(-90deg);
}
.click_text_icon {
transition: transform 0.3s ease-out;
}
.mark_and_feedback_div {
display: flex;
height: 100%;
justify-content: end;
align-items: center;
.img {
width: 38rpx;
height: 38rpx;
margin-left: 4rpx;
padding: 4rpx;
}
}
.expand_button_div {
display: flex;
align-items: center;
width: 90rpx;
justify-content: space-between;
}
.option_div {
padding-top: 20rpx;
}
.option_div_button {
width: 100%;
height: 84rpx;
padding-top: 30rpx;
// background-color: red;
display: flex;
flex-direction: column;
align-items: center;
}
.analysis_div {
.analysis_title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 32rpx 0 0 0;
.success_answer {
}
.your_answer {
.success {
color: #15b859;
}
.error {
color: #f5212d;
}
}
}
.analysis_note {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 28rpx;
}
.reference_answer {
.reference_answer_title {
font-weight: 700;
font-size: 30rpx;
color: #333333;
text-align: left;
line-height: 30rpx;
margin: 30rpx 0;
}
font-weight: 400;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
}
}
.item {
// height: 170rpx;
overflow: hidden;
display: flex;
flex-direction: column;
padding-bottom: 40rpx;
.top {
display: flex;
align-items: center;
.type {
width: 108rpx;
height: 46rpx;
background: #267eff;
border-radius: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 25rpx;
color: #ffffff;
text-align: center;
line-height: 46rpx;
}
.score {
margin-left: 24rpx;
font-family: ArialMT;
font-size: 30rpx;
color: #333333;
text-align: left;
}
.right {
display: flex;
align-items: center;
.right_text {
font-family: PingFangSC;
font-weight: 400;
font-size: 28rpx;
color: #333333;
line-height: 40px;
text-align: left;
font-style: normal;
}
}
}
.question_text {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 20rpx;
}
}
</style>
@@ -0,0 +1,468 @@
<template>
<view class="talk-btns">
<view class="touch-btn" @click="disabled_click">
<uv-button
class="touch-btn"
@touchstart="startRecord"
@touchend="stopRecord(false)"
@touchmove="handleMove"
v-if="!keyboardIsShow"
:customStyle="{ height: '92rpx' }"
>
按住说话
</uv-button>
<uv-input
v-if="keyboardIsShow"
class="input-box"
ref="inputRef"
v-model="inputText"
:maxlength="props.textValueLength"
:adjustPosition="false"
placeholderStyle="color:#999999;font-size:26rpx"
placeholder="请输入文字"
:disabled="props.isDisabled"
></uv-input>
</view>
<view class="talk-btn-box">
<image
class="talk-btn-icon"
v-show="!keyboardIsShow"
src="@/static/images/course/jianpan.png"
mode=""
@click="changeBtn"
></image>
<image
class="talk-btn-icon"
v-show="keyboardIsShow && !inputText"
src="@/static/images/course/record.png"
mode=""
@click="changeBtn"
></image>
<image
class="talk-btn-icon"
v-show="keyboardIsShow && !!inputText"
src="@/static/images/course/send.png"
mode=""
@click.stop="showKeyboard"
></image>
</view>
<uni-popup ref="popupRef" class="popup">
<view class="overlay-box">
<view class="circle-bg-box">
<view class="icon-box">
<image src="@/static/images/course/talk-icon.png"></image>
</view>
</view>
<view class="tips-box">松开发送</view>
<view class="btns-box">
<view :class="['close-btn', isOut ? 'is-out' : '']" id="circleBox" @click="closePopup()">
取消
<image class="btn-box" v-if="!isOut" src="@/static/images/course/close.png" mode=""></image>
<image class="btn-box" v-else src="@/static/images/course/close-active.png" mode=""></image>
</view>
</view>
<view :class="['talk-dialog', isOut ? 'is-out' : '']">
<image src="@/static/images/course/voice-white.gif" mode="" class="gif-box"></image>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
import common from '@/common/common';
import { onHide } from '@dcloudio/uni-app';
import permission from '@/common/permission.js';
import { getPhoneEnvBool } from '@/common/common.js';
import { initRecord } from '@/composables/useExamSubject.js';
const emit = defineEmits(['submit']);
const props = defineProps({
isDisabled: {
default: false,
type: Boolean
},
isLoading: {
default: false,
type: Boolean
},
loadingText: {
default: '发送回答中,请稍后再试!',
type: String
},
disabledText: {
default: '请稍后再试!',
type: String
},
canAnswer: {
default: true,
type: Boolean
},
canAnswerText: {
default: '当前状态不能发送内容!',
type: String
},
textValueLength: {
type: Number,
default: 500
},
minRecordDuration: {
//
type: Number,
default: 1000
}
});
//
const keyboardIsShow = ref(false);
const popupRef = ref(null);
const inputText = ref('');
let startTime = 0;
// not_started preparing in_recording ending
const recordingStatus = ref('not_started');
// recorderManager
// #ifdef APP-PLUS
const recorderManager = uni.getRecorderManager();
// #endif
// const innerAudioContext = uni.createInnerAudioContext();
const startTimer = ref(null); // ID
const startFlag = ref(1);
const startRecord = async (obj) => {
try {
await initRecord();
} catch (err) {
return;
}
if (props.isDisabled) return;
//
isClickOut.value = false
console.log('开始录音', recordingStatus.value);
if (recordingStatus.value !== 'not_started') return;
touchY.value = obj.changedTouches[0].pageY;
startTimer.value = setTimeout(() => {
console.log('延迟开始');
recordingStatus.value = 'preparing';
popupRef.value.open();
recorderManager.start({ duration: 59000 });
startTime = Date.now();
}, 80);
};
//
const stopRecord = (status = false) => {
console.log('结束录音stopRecord', status);
if (props.isDisabled) return;
if (startTimer.value) {
clearTimeout(startTimer.value);
startTimer.value = null;
}
if (status) {
nextTick(() => {
popupRef.value.close();
});
} else {
const endTime = Date.now();
const times = Math.max(500 - endTime + startTime, 0);
setTimeout(() => {
console.log('停止录音');
recorderManager?.stop();
popupRef.value?.close();
}, times);
}
nextTick(() => (recordingStatus.value = 'not_started'));
};
const closePopup = () => {
console.log('点击关闭');
isClickOut.value = true
stopRecord(false)
}
onMounted(async () => {
// #ifdef APP-PLUS
recorderManager.onStop(function (res) {
console.log('录音文件临时位置', res.tempFilePath, isOut.value, isClickOut.value);
if (isOut.value || isClickOut.value) return; //
const endTime = Date.now();
const recordDuration = endTime - startTime;
console.log('说话时长', recordDuration);
if (props.minRecordDuration > recordDuration) {
nextTick(() => common.msg('说话时间太短,没有听清!'));
// console.log('');
return;
}
popupRef.value?.close();
recordingStatus.value = 'not_started';
stopRecord(true); //
console.log('录音结束,时长:', recordDuration, '秒');
emit('submit', 'voice', res.tempFilePath);
});
recorderManager.onError(function (err) {
isClickOut.value = false
console.log('录音错误', err);
});
recorderManager.onStart(function (res) {
//
console.log('检测是否已经抬手结束了', recordingStatus.value);
if (recordingStatus.value !== 'preparing') {
stopRecord();
}
startTime = Date.now();
isClickOut.value = false
recordingStatus.value = 'in_recording';
});
// #endif
});
onHide(() => {
closePopup()
console.log('onHide');
})
const touchY = ref(0);
const windowInfo = uni.getWindowInfo();
//
const safeArea = windowInfo.safeArea;
//
const isOut = computed(() => {
const btnHeight = (windowInfo.screenWidth / 750) * 234;
return touchY.value < safeArea.bottom - btnHeight;
});
const isClickOut = ref(false);
const activeNum = ref(1);
const handleMove = (obj) => {
touchY.value = obj.changedTouches[0].pageY;
};
const clearinputText = (text = '') => {
const old_text = inputText.value;
inputText.value = text;
return old_text;
};
//
const showKeyboard = () => {
if (keyboardIsShow.value) {
const inputTextTram = inputText.value.trim();
if (inputTextTram) {
emit('submit', 'text', inputTextTram);
} else {
common.msg('不能发送空白消息!');
}
} else {
keyboardIsShow.value = !keyboardIsShow.value;
}
};
//
const changeBtn = () => {
if (props.isDisabled) return;
keyboardIsShow.value = !keyboardIsShow.value;
inputText.value = '';
};
//
const disabled_click = () => {
console.log('禁用时点击给的提示');
if (props.isDisabled) common.msg(props.disabledText);
};
//
const give_up = () => {
console.log('调用结束语音回答');
stopRecord(false)
};
defineExpose({ clearinputText, give_up });
</script>
<style lang="scss" scoped>
.talk-btns {
height: 150rpx;
background-color: #fff;
padding: 25rpx 30rpx 0;
position: relative;
.touch-btn {
height: 92rpx;
box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, 0.06);
text-align: center;
line-height: 92rpx;
color: #000;
font-weight: 400;
position: relative;
.input-box {
width: 100%;
height: 92rpx;
line-height: 92rpx;
text-align: left;
box-sizing: border-box;
padding: 0 40rpx 0 40rpx;
}
}
.talk-btn-box {
position: absolute;
height: 100%;
width: 120rpx;
right: 0;
top: 0;
.talk-btn-icon {
width: 40rpx;
height: 40rpx;
position: absolute;
right: 60rpx;
top: 50%;
margin-top: -20rpx;
}
}
}
.overlay-box {
display: flex;
flex-direction: column-reverse;
// justify-content: flex-end;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.9);
.circle-bg-box {
height: 234rpx;
background: url(@/static/images/course/talking.png) no-repeat;
background-size: cover;
position: relative;
.icon-box {
position: absolute;
left: 50%;
top: 50%;
margin-left: -17rpx;
margin-top: -24rpx;
width: 34rpx;
height: 48rpx;
image {
width: 100%;
height: 100%;
}
}
}
.tips-box {
height: 40rpx;
line-height: 40rpx;
color: #c0c0c0;
text-align: center;
margin: 12rpx 0 24rpx;
}
.btns-box {
height: 270rpx;
// padding: 0 120rpx;
width: 154rpx;
margin: 0 auto 100rpx;
overflow: hidden;
font-size: 28rpx;
text-align: center;
line-height: 116rpx;
.close-btn {
float: left;
height: 100%;
width: 154rpx;
overflow: hidden;
color: #999;
&.is-out {
color: #fff;
}
}
.translate-btn {
float: right;
height: 100%;
width: 154rpx;
overflow: hidden;
color: #999;
}
.btn-box {
width: 100%;
height: 154rpx;
}
}
.talk-dialog {
width: 506rpx;
height: 234rpx;
margin: 0 auto;
background-color: #06f;
border-radius: 32rpx;
position: relative;
&.is-out {
background-color: #ff3e49;
&::after {
background-color: #ff3e49;
}
}
.gif-box {
position: absolute;
width: 200rpx;
height: 80rpx;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&::after {
content: '';
width: 44rpx;
height: 44rpx;
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
background-color: #06f;
overflow: hidden;
position: absolute;
bottom: -22rpx;
left: 50%;
margin-left: -22rpx;
}
}
.white-bg-box {
height: 365rpx;
background-color: #fff;
overflow: hidden;
border-radius: 32rpx 32rpx 0 0;
padding: 58rpx 36rpx;
.box-title {
font-size: 32rpx;
font-weight: bold;
line-height: 46rpx;
margin-bottom: 46rpx;
}
.box-btns {
height: 46rpx;
line-height: 46rpx;
display: flex;
justify-content: space-between;
color: #999;
.is-active {
color: #06f;
}
}
}
}
::v-deep .uv-input__content__field-wrapper__field {
padding-right: 60rpx;
}
</style>
@@ -0,0 +1,225 @@
import {
computed,
reactive,
nextTick,
ref
} from 'vue';
// const innerAudioContext = uni.createInnerAudioContext();
// // 当前播放状态
const audioStopFun = ref(null)
export const useAudio = () => {
const audioInstance = uni.createInnerAudioContext();
const callbacks = reactive({
onEnded: null, // 播放完成回调
onError: null, // 播放错误回调
onLoaded: null, // 音频加载完成回调(可获取时长)
onStop: null
})
let isInitialized = false;
const isPlaying = ref(false)
// 加载状态
const isLoading = ref(false)
// 当前播放的音频地址
const currentSrc = ref('')
const errorMsg = ref('')
// 初始化音频实例(确保全局唯一)
const initAudio = () => {
console.log('初始化音频实例(确保全局唯一)');
// if (isInitialized) return;
// isInitialized = true;
bindAudioEvents();
}
const bindAudioEvents = () => {
if (!audioInstance) return;
// 播放开始事件
audioInstance.onPlay(() => {
isPlaying.value = true;
isLoading.value = false;
errorMsg.value = '';
});
audioInstance.onStop(() => {
console.log('音频停止事件');
isPlaying.value = false;
isLoading.value = false;
// 清空地址避免残留
// 触发自定义回调
if (callbacks.onStop && typeof callbacks.onStop === 'function') {
callbacks.onStop();
}
});
// 播放结束事件
audioInstance.onEnded(() => {
console.log('播放结束事件');
// 清空地址避免残留
// if (audioInstance) {
// audioInstance.src = '';
// }
// 触发自定义回调
if (callbacks.onEnded && typeof callbacks.onEnded === 'function') {
callbacks.onEnded();
}
});
// 音频可以播放时触发(此时通常能获取到时长)
audioInstance.onCanplay(() => {
// 尝试获取时长(部分环境需要延迟一点)
// console.log('尝试获取时长(部分环境需要延迟一点)');
isLoading.value = false;
setTimeout(() => {
const duration = audioInstance.duration || 0;
console.log('尝试获duration', duration);
if (duration > 0) {
// 触发加载完成回调(此时已获取时长)
if (callbacks.onLoaded && typeof callbacks.onLoaded === 'function') {
callbacks.onLoaded({
duration: duration
});
}
}
}, 100); // 短暂延迟确保时长已加载
});
// 错误事件
audioInstance.onError((err) => {
isPlaying.value = false;
isLoading.value = false;
console.error('音频错误:', err);
if (callbacks.onError && typeof callbacks.onError === 'function') {
callbacks.onError(err); // 把错误信息传给外部
}
});
}
// 新增:预设置音频地址并加载(此时不播放,仅获取时长)
const setAudioSrc = (src) => {
console.log('预设置音频地址并加载', src);
if (!src || src === currentSrc.value) return; // 地址不变则不重复加载
isLoading.value = true;
currentSrc.value = src;
errorMsg.value = '';
try {
// 先停止当前播放并清空旧地址
// audioInstance.stop();
// audioInstance.src = '';
// 设置新地址并加载(不自动播放)
audioInstance.src = src;
// audioInstance.load(); // 手动触发加载
} catch (err) {
isLoading.value = false;
errorMsg.value = `设置音频失败: ${err.message}`;
}
}
// 修改:播放当前已设置的音频(需先调用setAudioSrc
const playAudio = async (src) => {
console.log('播放当前已设置的音频', currentSrc.value);
if (!currentSrc.value) {
errorMsg.value = '请先设置音频地址';
return;
}
// 已在播放则暂停
console.log('已在播放则暂停', isPlaying.value);
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
if (isPlaying.value) {
pauseAudio();
return;
}
console.log('当前是是否暂停或停止状态', audioStopFun.value?.paused);
if (!audioStopFun.value?.paused) { // 当前是是否暂停或停止状态,true 表示暂停或停止,false 表示正在播放
audioStopFun.value?.stop();
wait(100)
}
// 执行播放
try {
// 哪个函数的实例正在播放,就把他挂在全局
audioStopFun.value = audioInstance
audioInstance.play();
isPlaying.value = true;
isLoading.value = false;
} catch (err) {
isLoading.value = false;
errorMsg.value = `播放失败: ${err.message}`;
}
}
// 暂停播放
const pauseAudio = () => {
if (audioInstance && isPlaying.value) {
console.log('暂停播放');
audioInstance.pause();
isPlaying.value = false;
}
}
// 停止播放并重置状态
const stopAudio = () => {
if (audioInstance && isPlaying.value) {
audioInstance.stop();
// todo
}
}
// 移除所有回调(组件卸载时调用)
const removeCallbacks = () => {
callbacks.onEnded = null;
callbacks.onError = null;
callbacks.onLoaded = null;
callbacks.onStop = null;
}
// 注册播放完成回调
const onAudioEnded = (callback) => {
if (typeof callback === 'function') {
callbacks.onEnded = callback;
}
}
// 注册播放错误回调
const onAudioError = (callback) => {
if (typeof callback === 'function') {
callbacks.onError = callback;
}
}
// 新增:注册音频加载完成回调(用于获取时长)
const onAudioLoaded = (callback) => {
if (typeof callback === 'function') {
callbacks.onLoaded = callback;
}
}
const onAudioStop = (callback) => {
if (typeof callback === 'function') {
callbacks.onStop = callback;
}
}
initAudio();
return {
isPlaying,
playAudio,
pauseAudio,
stopAudio,
setAudioSrc,
removeCallbacks,
onAudioEnded,
onAudioError,
onAudioLoaded,
onAudioStop
}
}
@@ -0,0 +1,111 @@
<template>
<view class="popup-view" v-if="modelValue">
<view class="close-preview" @click="emit('update:modelValue', false)" >X</view>
<view class="popup-view-preview" v-if="showView">
<web-view :src="showUrl" :fullscreen="true" class="web-view-class" :webview-styles="{
width: '100%',
height: '100%'
}"></web-view>
</view>
</view>
<view v-else></view>
</template>
<script setup>
import {
ref,
unref,
onMounted,
computed,
watch
} from 'vue'
import {
getToken,
getUserInfo
} from "@/common/common.js"
import {
downloadFile
} from "@/api/common.js"
import {
get_base_url
} from '@/api/request.js'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
fileId: {
default: '',
type: String
},
fileName: {
default: '',
type: String
}
})
watch(()=>props.modelValue,(val)=>{
if(val){
openPdfFn()
}else{
showView.value = false
}
})
const userInfo = ref(getUserInfo());
const userParams = '/' + (unref(userInfo)?.userName??'') + ' ' + (unref(userInfo)?.loginName??'');
const base_url = get_base_url('/traask/traFilePreview/')
const showUrl = ref('')
const viewerUrl = ref('/static/pdf-view/web/viewer.html')
const showView = ref(false)
const openPdfFn = (data) => {
const token = getToken();
const header = {};
let _id = data?.fileId || props.fileId
let fileUrl = base_url + '/traask/traFilePreview/' + _id + userParams
if (token)
header['summary'] = token
uni.downloadFile({
//
url: fileUrl,
header: header,
success: (res) => {
//
console.log('下载成功', res.tempFilePath);
//h5
// #ifdef H5
let newUrl = res.tempFilePath
// #endif
//AppURL
// #ifdef APP-PLUS
let newUrl = plus.io.convertLocalFileSystemURL(res.tempFilePath)
// #endif
showUrl.value = viewerUrl.value + '?file=' + newUrl
showView.value = true
}
})
}
</script>
<style lang="scss">
.popup-view{
width: 100vw;
height: 100vh;
}
.popup-view-preview{
height: 100vh;
width: 100%;
z-index: 100;
position: relative;
}
.close-preview{
position: fixed;
right: 20rpx;
top: calc(var(--status-bar-height) + 20rpx);
color: #000;
z-index: 1000;
font-size: 30rpx;
}
</style>
@@ -0,0 +1,191 @@
<template>
<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>
<image class="detail-img" :src="imgUrlShow" mode="aspectFit"></image>
</view>
</uni-popup>
</view>
</template>
<script>
import { previewFile, previewCacheThumbImage } from '@/api/common.js';
import { get_base_url } from '@/api/request.js';
import { useStorageStore } from '@/store/storage.js';
import { getToken } from '@/common/common.js'
// const storageStore = useStorageStore()
export default {
name: 'image-preview',
props: {
imgId: {
default: '',
type: String
},
src: {
default: '',
type: String
},
width: {
default: 50,
type: [String, Number]
},
height: {
default: 50,
type: [String, Number]
},
isCache: {
//
type: Boolean,
default: false
},
mode: {
type: String,
default: 'aspectFill'
},
previewDetail: {
default: '',
type: [String, Boolean]
}
},
data() {
return {
imgUrlShow: '',
watching: false,
base_url: get_base_url(),
showModel: false,
token: getToken()
};
},
computed: {
storageStore: () => useStorageStore(),
imageObj() {
return this.storageStore?.imgObj || {};
}
},
watch: {
imgId: {
handler(newval, oldval) {
if (newval.indexOf('/') === 0) {
this.imgUrlShow = this.base_url + newval;
return;
}
if (newval.indexOf('http') === 0) {
this.imgUrlShow = newval;
return;
}
if (newval && this.watching) {
this.getPreviewUrlBlob(newval);
}
},
immediate: true,
deep: true
},
src: {
handler(newval, oldval) {
if (newval && this.watching) {
this.imgUrlShow = this.base_url + this.src + '?tk=' + this.token;;
}
},
immediate: true,
deep: true
},
showModel(val){
if(!val){
this.storageStore.setTouchBtnZIndex(12);
}
}
},
methods: {
changeShow(data){
if(!data.show){
this.storageStore.setTouchBtnZIndex(12);
}else{
this.storageStore.setTouchBtnZIndex(-1);
}
},
closePopup() {
this.showModel = false;
this.$refs.PopupRef.close();
},
showDetail() {
if (!!this.previewDetail) {
this.showModel = true;
this.$refs.PopupRef.open();
}
},
getPreviewUrlBlob(id) {
if (!id) {
this.imgUrlShow = '';
return;
}
if (this.isCache) {
let _cache = this.imageObj[`/${id}/${this.width}/${this.height}`] || '';
if (_cache) {
this.imgUrlShow = _cache;
return;
}
previewCacheThumbImage(id, this.width, this.height).then((res) => {
this.imgUrlShow = res;
this.storageStore.setStorage('image', `/${id}/${this.width}/${this.height}`, res);
});
} else {
let _cache = this.imageObj[`${id}`] || '';
if (_cache) {
this.imgUrlShow = _cache;
return;
}
previewFile(id).then((result) => {
//#ifdef APP-PLUS
let _header = result.header['Content-Type'].split(';')[0];
// #endif
//#ifndef APP-PLUS
let _header = result.header['content-type'].split(';')[0];
// #endif
let base64 = `data:${_header};base64,` + uni.arrayBufferToBase64(result.data);
this.imgUrlShow = base64;
this.storageStore.setStorage('image', id, base64);
});
}
}
},
mounted() {
if (this.imgId) {
if (this.imgId.indexOf('/') === 0) {
this.imgUrlShow = this.base_url + this.imgId + '?tk=' + this.token;
} else if (this.imgId.indexOf('http') === 0) {
this.imgUrlShow = this.imgId + '?tk=' + this.token;
} else {
this.getPreviewUrlBlob(this.imgId);
}
}
this.watching = true;
if (this.src) {
this.imgUrlShow = this.base_url + this.src + '?tk=' + this.token;
}
}
};
</script>
<style lang="scss" scoped>
.overlay-box {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background-color: rgba(0, 0, 0, 0.65);
.detail-img {
position: absolute;
left: 50%;
top: 50%;
width: 100vw;
max-height: 100%;
transform: translate(-50%, -50%);
}
}
</style>
@@ -0,0 +1,301 @@
<template>
<!-- <uni-popup ref="PopupRef" class="popup" mask-background-color="rgba(0,0,0,.2)" @change="changeShow">
<view class="overlay-box" @click="playEnded" v-if="fileType === 'video'">
<video class="video-cont-center"
:id="`video`+ fileId"
ref="playerRef"
v-if="showModel && fileUrlShow"
:src="fileUrlShow"
:controls="true"
@error="playError"
@ended="playEnded"
object-fit="contain"></video>
</view>
<view class="overlay-box" @click="playEnded" @touchmove.stop v-if="fileType === 'image'">
<image class="detail-img" :src="fileUrlShow" mode="aspectFill"></image>
</view>
</uni-popup> -->
<uv-overlay :show="showModel" opacity=".6" @click="showModel = false">
<view class="overlay-box" @click="playEnded" v-if="fileType === 'video'">
<video class="video-cont-center" :id="`video` + fileId" ref="playerRef" v-if="showModel && fileUrlShow" :src="fileUrlShow" :controls="true" @error="playError" @ended="playEnded" object-fit="contain"></video>
</view>
<view class="overlay-box" @click="playEnded" @touchmove.stop v-if="fileType === 'image'">
<image class="detail-img" :src="fileUrlShow" mode="aspectFill"></image>
</view>
</uv-overlay>
</template>
<script>
import { downloadFile, getPreviewFileUrl } from '@/api/common.js'
import { useStorageStore } from '@/store/storage.js'
import { get_base_url } from '@/api/request.js'
import imagePreviewVue from './image-preview.vue'
export default {
name: 'video-preview',
components: { imagePreviewVue },
props: {
// fileId:{
// default:'',
// type:String
// },
// src:{
// default:'',
// type:String
// },
// fileType:{
// default:'image',
// type:String
// }
},
data() {
return {
showModel: false,
fileUrlShow: '',
watching: false,
videoContext: null,
base_url: get_base_url()
}
},
computed: {
storageStore: () => useStorageStore(),
videoObj() {
return this.storageStore?.videoObj || {}
},
imageObj() {
return this.storageStore?.imgObj || {}
},
showPreviewDetail() {
return !!this.storageStore?.getShowPreviewDetail
},
fileType() {
return this.storageStore?.getFileType
},
fileId() {
return this.storageStore?.getFileId
},
src() {
return this.storageStore?.getFileSrc
}
},
watch: {
fileId: {
handler(newval, oldval) {
if (this.fileType === 'image') {
if (newval.indexOf('/') === 0) {
this.fileUrlShow = this.base_url + newval
return
}
if (newval.indexOf('http') === 0) {
this.fileUrlShow = newval
return
}
if (newval && this.watching) {
this.getImagePreviewUrlBlob(newval)
}
}
if (this.fileType === 'video') {
if (newval.indexOf('http') === 0) {
this.fileUrlShow = newval
} else {
if (newval && this.watching) {
this.getPreviewUrlBlob(newval)
}
}
}
this.videoContext = uni.createVideoContext(`video` + this.fileId)
},
immediate: true,
deep: true
},
src: {
handler(newval, oldval) {
if (newval.indexOf('http') === 0) {
this.fileUrlShow = newval
} else {
this.fileUrlShow = this.base_url + newval
}
},
immediate: true,
deep: true
},
showPreviewDetail(val) {
if (val) {
this.showModelNow()
} else {
// this.$refs.PopupRef.close()
this.showModel = false
}
}
},
methods: {
getImagePreviewUrlBlob(id) {
if (!id) {
this.fileUrlShow = ''
return
}
if (this.isCache) {
let _cache = this.imageObj[`/${id}/${this.width}/${this.height}`] || ''
if (_cache) {
this.fileUrlShow = _cache
return
}
previewCacheThumbImage(id, this.width, this.height).then(res => {
this.fileUrlShow = res
this.storageStore.setStorage('image', `/${id}/${this.width}/${this.height}`, res)
})
} else {
let _cache = this.imageObj[`${id}`] || ''
if (_cache) {
this.fileUrlShow = _cache
return
}
previewFile(id).then(result => {
//#ifdef APP-PLUS
let _header = result.header['Content-Type'].split(';')[0]
// #endif
//#ifndef APP-PLUS
let _header = result.header['content-type'].split(';')[0]
// #endif
let base64 = `data:${_header};base64,` + uni.arrayBufferToBase64(result.data)
this.fileUrlShow = base64
this.storageStore.setStorage('image', id, base64)
})
}
},
changeShow(data) {
if (!data.show) {
this.storageStore.setTouchBtnZIndex(12)
} else {
this.storageStore.setTouchBtnZIndex(-1)
}
},
showModelNow() {
// this.$refs.PopupRef.open()
this.showModel = true
if (this.fileType === 'image') {
if (this.fileId) {
if (this.fileId.indexOf('/') === 0) {
this.fileUrlShow = this.base_url + this.fileId
} else if (this.fileId.indexOf('http') === 0) {
this.fileUrlShow = this.fileId
} else {
this.getPreviewUrlBlob(this.fileId)
}
}
this.watching = true
if (this.src) {
this.fileUrlShow = this.base_url + this.src
}
}
if (this.fileType === 'video') {
if (this.fileId) {
let _newDate = new Date().getTime()
if (this.fileId.indexOf('http') === 0) {
this.fileUrlShow = this.fileId + '?t=' + _newDate
} else {
this.getPreviewUrlBlob(this.fileId)
}
this.videoContext = uni.createVideoContext(`video` + this.fileId)
}
}
},
playError(info) {
console.log('err', info)
},
playEnded() {
this.storageStore.setShowPreviewDetail({
status: false,
fileType: '',
fileId: '',
fileSrc: ''
})
},
getPreviewUrlBlob(id) {
if (!id) {
this.fileUrlShow = ''
return
}
let _cache = this.videoObj[id] || ''
if (_cache) {
this.fileUrlShow = _cache
return
}
let _newDate = new Date().getTime()
this.fileUrlShow = getPreviewFileUrl(this.fileId) + '?t=' + _newDate
// downloadFile(id).then(res=>{
// this.fileUrlShow = res.tempFilePath
// this.storageStore.setStorage('video', id, this.fileUrlShow)
// })
},
getPreviewFileUrlNow(id) {
let _newDate = new Date().getTime()
if (id.indexOf('http') === 0) {
this.fileUrlShow = id + '?t=' + _newDate
return
}
if (id && this.watching) {
this.getPreviewUrlBlob(id)
}
}
},
mounted() {
this.watching = true
}
}
</script>
<style lang="scss" scoped>
.video-view-box {
width: 100%;
height: 100%;
background-color: #fafafa !important;
position: relative;
}
.img-box {
position: absolute;
left: 50%;
top: 50%;
width: 100rpx;
height: 100rpx;
transform: translate(-50%, -50%);
}
.preview-pic {
width: 100%;
height: 100%;
}
.overlay-box .video-cont-center {
width: 100vw;
max-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.overlay-box {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 999;
// background-color: rgba(0, 0, 0, 0.65);
}
.overlay-box {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 999;
// background-color: rgba(0, 0, 0, 0.65);
.detail-img {
position: absolute;
left: 50%;
top: 50%;
width: 100vw;
max-height: 100%;
transform: translate(-50%, -50%);
}
}
</style>
@@ -0,0 +1,296 @@
<template>
<uni-popup ref="PopupRef" class="popup" mask-background-color="rgba(0,0,0,.2)" @change="changeShow">
<view class="overlay-box" @click="playEnded" v-if="fileType === 'video'">
<video class="video-cont-center"
:id="`video`+ fileId"
ref="playerRef"
v-if="showModel && fileUrlShow"
:src="fileUrlShow"
:controls="true"
@error="playError"
@ended="playEnded"
object-fit="contain"></video>
</view>
<view class="overlay-box" @click="playEnded" @touchmove.stop v-if="fileType === 'image'">
<image class="detail-img" :src="fileUrlShow" mode="aspectFit"></image>
</view>
</uni-popup>
</template>
<script>
import { downloadFile,getPreviewFileUrl } from'@/api/common.js'
import { useStorageStore } from "@/store/storage.js"
import { get_base_url } from '@/api/request.js'
import imagePreviewVue from './image-preview.vue'
export default {
name:"video-preview",
components:{imagePreviewVue},
props:{
// fileId:{
// default:'',
// type:String
// },
// src:{
// default:'',
// type:String
// },
// fileType:{
// default:'image',
// type:String
// }
},
data() {
return {
showModel: false,
fileUrlShow:'',
watching:false,
videoContext: null,
base_url: get_base_url()
};
},
computed:{
storageStore:() => useStorageStore(),
videoObj() {
return this.storageStore?.videoObj || {}
},
imageObj() {
return this.storageStore?.imgObj || {};
},
showPreviewDetail() {
return !!this.storageStore?.getShowPreviewDetail
},
fileType(){
return this.storageStore?.getFileType
},
fileId(){
return this.storageStore?.getFileId
},
src(){
return this.storageStore?.getFileSrc
},
},
watch: {
fileId: {
handler(newval, oldval) {
if(this.fileType === 'image'){
if (newval.indexOf('/') === 0) {
this.fileUrlShow = this.base_url + newval;
return;
}
if (newval.indexOf('http') === 0) {
this.fileUrlShow = newval;
return;
}
if (newval && this.watching) {
this.getImagePreviewUrlBlob(newval);
}
}
if(this.fileType === 'video'){
if(newval.indexOf('http') === 0){
this.fileUrlShow = newval
}else{
if (newval && this.watching) {
this.getPreviewUrlBlob(newval);
}
}
}
this.videoContext = uni.createVideoContext(`video`+ this.fileId)
},
immediate: true,
deep: true,
},
src: {
handler(newval, oldval) {
if(newval.indexOf('http') === 0){
this.fileUrlShow = newval
}else{
this.fileUrlShow = this.base_url + newval
}
},
immediate: true,
deep: true,
},
showPreviewDetail(val){
if(val){
this.showModelNow()
} else {
this.$refs.PopupRef.close()
this.showModel = false
}
}
},
methods: {
getImagePreviewUrlBlob(id) {
if (!id) {
this.fileUrlShow = '';
return;
}
if (this.isCache) {
let _cache = this.imageObj[`/${id}/${this.width}/${this.height}`] || '';
if (_cache) {
this.fileUrlShow = _cache;
return;
}
previewCacheThumbImage(id, this.width, this.height).then((res) => {
this.fileUrlShow = res;
this.storageStore.setStorage('image', `/${id}/${this.width}/${this.height}`, res);
});
} else {
let _cache = this.imageObj[`${id}`] || '';
if (_cache) {
this.fileUrlShow = _cache;
return;
}
previewFile(id).then((result) => {
//#ifdef APP-PLUS
let _header = result.header['Content-Type'].split(';')[0];
// #endif
//#ifndef APP-PLUS
let _header = result.header['content-type'].split(';')[0];
// #endif
let base64 = `data:${_header};base64,` + uni.arrayBufferToBase64(result.data);
this.fileUrlShow = base64;
this.storageStore.setStorage('image', id, base64);
});
}
},
changeShow(data){
if(!data.show){
this.storageStore.setTouchBtnZIndex(12);
}else{
this.storageStore.setTouchBtnZIndex(-1);
}
},
showModelNow(){
this.$refs.PopupRef.open()
this.showModel = true
if(this.fileType === 'image'){
if (this.fileId) {
if (this.fileId.indexOf('/') === 0) {
this.fileUrlShow = this.base_url + this.fileId;
} else if (this.fileId.indexOf('http') === 0) {
this.fileUrlShow = this.fileId;
} else {
this.getPreviewUrlBlob(this.fileId);
}
}
this.watching = true;
if (this.src) {
this.fileUrlShow = this.base_url + this.src;
}
}
if(this.fileType === 'video'){
if (this.fileId) {
let _newDate = new Date().getTime()
if(this.fileId.indexOf('http') === 0){
this.fileUrlShow = this.fileId + '?t=' + _newDate
}else{
this.getPreviewUrlBlob(this.fileId);
}
this.videoContext = uni.createVideoContext(`video`+ this.fileId)
}
}
},
playError(info){
console.log('err',info)
},
playEnded(){
this.storageStore.setShowPreviewDetail({
status: false,
fileType: '',
fileId: '',
fileSrc: ''
})
},
getPreviewUrlBlob(id) {
if (!id) {
this.fileUrlShow = ''
return
}
let _cache = this.videoObj[id] || ''
if(_cache){
this.fileUrlShow = _cache;
return
}
let _newDate = new Date().getTime()
this.fileUrlShow = getPreviewFileUrl(this.fileId)+'?t=' + _newDate
// downloadFile(id).then(res=>{
// this.fileUrlShow = res.tempFilePath
// this.storageStore.setStorage('video', id, this.fileUrlShow)
// })
},
getPreviewFileUrlNow(id){
let _newDate = new Date().getTime()
if(id.indexOf('http') === 0){
this.fileUrlShow = id + '?t=' + _newDate
return
}
if (id && this.watching) {
this.getPreviewUrlBlob(id);
}
}
},
mounted() {
this.watching = true
},
beforeUnmount() {
this.playEnded()
}
}
</script>
<style lang="scss" scoped>
.video-view-box{
width: 100%;
height: 100%;
background-color: #fafafa !important;
position: relative;
}
.img-box{
position: absolute;
left: 50%;
top: 50%;
width: 100rpx;
height: 100rpx;
transform: translate(-50%, -50%);
}
.preview-pic{
width: 100%;
height: 100%;
}
.overlay-box .video-cont-center{
width: 100vw;
max-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.overlay-box{
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background-color: rgba(0, 0, 0, .65);
}
.overlay-box {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background-color: rgba(0, 0, 0, 0.65);
.detail-img {
position: absolute;
left: 50%;
top: 50%;
width: calc(100% - 40rpx);
height: calc(100% - 100rpx);
transform: translate(-50%, -50%);
}
}
</style>
@@ -0,0 +1,200 @@
<template>
<view v-bind="$attrs" class="video-view-box">
<imagePreviewVue :imgId="picId" class="preview-pic" v-if="picId" @click="showModelNow"/>
<imagePreviewVue :src="picSrc" class="preview-pic" v-if="picSrc" @click="showModelNow"/>
<view class="img-box">
<image
class="icon"
src="@/static/images/course/play.png"
style="width: 100%;height: 100%;"
></image>
</view>
<!-- <uv-overlay :show="showModel" opacity=".6" @click.stop="showModel = false"> -->
<uni-popup ref="PopupRef" class="popup" mask-background-color="rgba(0,0,0,.2)" @change="changeShow">
<view class="overlay-box" @click="playEnded">
<video class="video-cont-center"
:id="`video`+ fileId"
ref="playerRef"
v-if="showModel && fileUrlShow"
:src="fileUrlShow"
:controls="true"
@error="playError"
@ended="playEnded"
object-fit="contain"></video>
</view>
</uni-popup>
</view>
</template>
<script>
import { downloadFile,getPreviewFileUrl } from'@/api/common.js'
import { useStorageStore } from "@/store/storage.js"
import { get_base_url } from '@/api/request.js'
import imagePreviewVue from './image-preview.vue'
export default {
name:"video-preview",
components:{ imagePreviewVue },
props:{
fileId:{
default:'',
type:String
},
picId:{
default:'',
type:String
},
picSrc:{
default:'',
type:String
},
src:{
default:'',
type:String
},
previewDetail: {
default: false,
type: [String, Boolean]
}
},
data() {
return {
showModel: false,
fileUrlShow:'',
watching:false,
videoContext: null,
base_url: get_base_url()
};
},
computed:{
storageStore:() => useStorageStore(),
videoObj() {
return this.storageStore?.videoObj||{}
}
},
watch: {
fileId: {
handler(newval, oldval) {
if(newval.indexOf('http') === 0){
this.fileUrlShow = newval
}else{
if (newval && this.watching) {
this.getPreviewUrlBlob(newval);
}
}
this.videoContext = uni.createVideoContext(`video`+ this.fileId)
},
immediate: true,
deep: true,
},
src: {
handler(newval, oldval) {
// if (newval && this.watching) {
this.fileUrlShow = this.base_url + newval
console.log('this.fileUrlShow', this.fileUrlShow);
// }
},
immediate: true,
deep: true,
},
},
methods: {
changeShow(data){
if(!data.show){
this.storageStore.setTouchBtnZIndex(12);
}else{
this.storageStore.setTouchBtnZIndex(-1);
}
},
showModelNow(){
if (!!this.previewDetail) {
this.$refs.PopupRef.open()
this.showModel = true
}
},
playError(info){
console.log('err',info)
},
playEnded(){
this.$refs.PopupRef.close()
this.showModel = false
},
getPreviewUrlBlob(id) {
if (!id) {
this.fileUrlShow = ''
return
}
let _cache = this.videoObj[id] || ''
if(_cache){
this.fileUrlShow = _cache;
return
}
let _newDate = new Date().getTime()
this.fileUrlShow = getPreviewFileUrl(this.fileId)+'?t=' + _newDate
// downloadFile(id).then(res=>{
// this.fileUrlShow = res.tempFilePath
// this.storageStore.setStorage('video', id, this.fileUrlShow)
// })
},
getPreviewFileUrlNow(id){
let _newDate = new Date().getTime()
if(id.indexOf('http') === 0){
this.fileUrlShow = id + '?t=' + _newDate
return
}
if (id && this.watching) {
this.getPreviewUrlBlob(id);
}
}
},
mounted() {
if (this.fileId) {
let _newDate = new Date().getTime()
if(this.fileId.indexOf('http') === 0){
this.fileUrlShow = this.fileId + '?t=' + _newDate
}else{
this.getPreviewUrlBlob(this.fileId);
}
this.videoContext = uni.createVideoContext(`video`+ this.fileId)
}
this.watching = true
},
}
</script>
<style lang="scss" scoped>
.video-view-box{
width: 100%;
height: 100%;
background-color: #fafafa !important;
position: relative;
}
.img-box{
position: absolute;
left: 50%;
top: 50%;
width: 100rpx;
height: 100rpx;
transform: translate(-50%, -50%);
}
.preview-pic{
width: 100%;
height: 100%;
}
.overlay-box .video-cont-center{
width: 100vw;
max-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.overlay-box{
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 999;
background-color: rgba(0, 0, 0, .65);
}
</style>
@@ -0,0 +1,26 @@
<template>
<view style="width: 100%;" class="no-data">
<view class="flex-c-c">
<image src="@/static/images/common/default_img.png" mode="aspectFit" style="width:400rpx"></image>
</view>
<view class="text">
<slot ></slot>
</view>
</view>
</template>
<script>
</script>
<style scoped lang="scss">
.no-data{
display: flex;
flex-direction: column;
align-items: center;
}
.text{
color: #666666;
font-size: 30rpx;
}
</style>
+352
View File
@@ -0,0 +1,352 @@
<template>
<view :class="['loading', type]">
<view></view>
<view></view>
<view></view>
<view></view>
<view></view>
<view></view>
<view></view>
<view></view>
</view>
</template>
<script>
import { computed } from 'vue'
export default {
name:"loading",
props:{
type:{
default: 'cr',// cl cr easy
type: String
},
size: {
default: '32',
type: [String, Number]
},
color: {
default: 'black',
type: String
}
},
computed:{
loadingWidth(){
return this.size + 'rpx'
},
easyLoadingWidth(){
return Number(this.size) * 15 / 16 + 'rpx'
},
easyLoadingBorderWidth(){
return Number(this.size) / 16 + 'rpx'
},
loadingColor(){
return this.color
}
},
data() {
return {
};
}
}
</script>
<style lang="scss">
.loading.easy {
width: v-bind(easyLoadingWidth);
height: v-bind(easyLoadingWidth);
border-style: solid;
border-color: v-bind(loadingColor);
border-width: v-bind(easyLoadingBorderWidth);
border-top-color: transparent;
border-radius: 100%;
animation: circle infinite 0.75s linear;
}
//
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.loading,
.loading.cl > view {
position: relative;
box-sizing: border-box;
}
.loading.cl {
display: block;
font-size: 0;
color: v-bind(loadingColor);
}
.loading.cl.la-dark {
color: #333;
}
.loading.cl > view {
display: inline-block;
float: none;
background-color: currentColor;
border: 0 solid currentColor;
}
.loading.cl {
width: v-bind(loadingWidth);
height: v-bind(loadingWidth);
}
.loading.cl > view {
position: absolute;
top: 50%;
left: 50%;
width: 25%;
height: 25%;
transform: translate(-50%, -50%);
border-radius: 100%;
animation: ball-spin-clockwise 1s infinite ease-in-out;
}
.loading.cl > view:nth-child(1) {
top: 5%;
left: 50%;
animation-delay: -0.875s;
}
.loading.cl > view:nth-child(2) {
top: 18.1801948466%;
left: 81.8198051534%;
animation-delay: -0.75s;
}
.loading.cl > view:nth-child(3) {
top: 50%;
left: 95%;
animation-delay: -0.625s;
}
.loading.cl > view:nth-child(4) {
top: 81.8198051534%;
left: 81.8198051534%;
animation-delay: -0.5s;
}
.loading.cl > view:nth-child(5) {
top: 94.9999999966%;
left: 50.0000000005%;
animation-delay: -0.375s;
}
.loading.cl > view:nth-child(6) {
top: 81.8198046966%;
left: 18.1801949248%;
animation-delay: -0.25s;
}
.loading.cl > view:nth-child(7) {
top: 49.9999750815%;
left: 5.0000051215%;
animation-delay: -0.125s;
}
.loading.cl > view:nth-child(8) {
top: 18.179464974%;
left: 18.1803700518%;
animation-delay: 0s;
}
.loading.cl.la-sm {
width: 50%;
height: 50%;
}
.loading.cl.la-sm > view {
width:12.5%;
height:12.5%;
transform: translate(-50%, -50%);
}
.loading.cl.la-2x {
width: 64rpx;
height: 64rpx;
}
.loading.cl.la-2x > view {
width: 50%;
height: 50%;
transform: translate(-50%, -50%);
}
.loading.cl.la-3x {
width: 96rpx;
height: 96rpx;
}
.loading.cl.la-3x > view {
width: 24rpx;
height: 24rpx;
transform: translate(-50%, -50%);
}
@keyframes ball-spin-clockwise {
0%,
100% {
opacity: 1;
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
transform: scale(0);
}
}
.loading.cr,
.loading.cr > view {
position: relative;
box-sizing: border-box;
}
.loading.cr {
display: block;
font-size: 0;
color: v-bind(loadingColor);
}
.loading.cr.la-dark {
color: #333;
}
.loading.cr > view {
display: inline-block;
float: none;
background-color: currentColor;
border: 0 solid currentColor;
}
.loading.cr {
width: v-bind(loadingWidth);
height: v-bind(loadingWidth);
}
.loading.cr > view {
position: absolute;
top: 50%;
left: 50%;
width: 25%;
height: 25%;
transform: translate(-50%, -50%);
border-radius: 100%;
animation: ball-spin 1s infinite ease-in-out;
}
.loading.cr > view:nth-child(1) {
top: 5%;
left: 50%;
animation-delay: -1.125s;
}
.loading.cr > view:nth-child(2) {
top: 18.1801948466%;
left: 81.8198051534%;
animation-delay: -1.25s;
}
.loading.cr > view:nth-child(3) {
top: 50%;
left: 95%;
animation-delay: -1.375s;
}
.loading.cr > view:nth-child(4) {
top: 81.8198051534%;
left: 81.8198051534%;
animation-delay: -1.5s;
}
.loading.cr > view:nth-child(5) {
top: 94.9999999966%;
left: 50.0000000005%;
animation-delay: -1.625s;
}
.loading.cr > view:nth-child(6) {
top: 81.8198046966%;
left: 18.1801949248%;
animation-delay: -1.75s;
}
.loading.cr > view:nth-child(7) {
top: 49.9999750815%;
left: 5.0000051215%;
animation-delay: -1.875s;
}
.loading.cr > view:nth-child(8) {
top: 18.179464974%;
left: 18.1803700518%;
animation-delay: -2s;
}
.loading.cr.la-sm {
width: 50%;
height: 50%;
}
.loading.cr.la-sm > view {
width:12.5%;
height:12.5%;
transform: translate(-50%, -50%);
}
.loading.cr.la-2x {
width: 64rpx;
height: 64rpx;
}
.loading.cr.la-2x > view {
width: 50%;
height: 50%;
transform: translate(-50%, -50%);
}
.loading.cr.la-3x {
width: 96rpx;
height: 96rpx;
}
.loading.cr.la-3x > view {
width: 24rpx;
height: 24rpx;
transform: translate(-50%, -50%);
}
@keyframes ball-spin {
0%,
100% {
opacity: 1;
transform: scale(1);
}
20% {
opacity: 1;
}
80% {
opacity: 0;
transform: scale(0);
}
}
</style>
@@ -0,0 +1,78 @@
<template>
<view>
<rich-text :nodes="mdText"></rich-text>
</view>
</template>
<script>
import MarkdownIt from 'markdown-it';
// import DOMPurify from 'dompurify'
export default {
name:"markdown-preview",
props: {
mdString: {
default: '',
type: String
}
},
data() {
return {
contentHtml:'',
mdObj:null
};
},
watch:{
mdString: {
handler(val){
this.contentHtml = this.parseContent(val)
},
deep: true,
immediate: true
}
},
computed:{
mdText(){
// let _endtext = DOMPurify.sanitize(this.contentHtml)
return this.contentHtml
}
},
methods:{
parseContent(text){
if(this.mdObj){
return this.mdObj.render(text)
}else{
setTimeout(()=>{
return this.parseContent(text)
},500)
}
}
},
mounted(){
this.mdObj = new MarkdownIt({
html: true,
linkify: true,
typographer: true
})
this.contentHtml = this.parseContent(this.mdString)
}
}
</script>
<style scoped lang="scss">
pre{
background-color: #f4f4f4;
padding: 10px;
border-radius: 5px;
overflow: auto;
}
code{
font-family: monospace;
background-color: #e8e8e8;
padding: 2px 4px;
border-radius: 3px;
}
p,span,div,li{
word-break: break-all;
line-height: 50rpx;
}
</style>
@@ -0,0 +1,121 @@
<template>
<div
class="skeleton-container"
:style="containerStyle"
>
<div
v-for="(item, index) in skeletonItems"
:key="index"
class="skeleton-item"
:style="getItemStyle(item)"
>
<!-- 内部内容根据类型渲染 -->
<slot v-if="item.type === 'custom' && index === 0" :item="item" />
</div>
</div>
</template>
<script setup>
import { ref, computed, toRefs } from 'vue'
import type { PropType } from 'vue'
//
interface SkeletonItem {
type?: 'rect' | 'circle' | 'custom' //
width?: string | number //
height?: string | number //
radius?: string | number // rect
margin?: string | number //
animation?: boolean //
}
//
const props = defineProps({
//
items: {
type: Array as PropType<SkeletonItem[]>,
default: () => [
{ type: 'rect', width: '100%', height: '200px', animation: true },
{ type: 'rect', width: '80%', height: '30px', margin: '15px 0', animation: true },
{ type: 'rect', width: '60%', height: '30px', margin: '15px 0', animation: true }
]
},
//
baseColor: {
type: String,
default: '#f2f2f2'
},
//
highlightColor: {
type: String,
default: '#e0e0e0'
},
//
show: {
type: Boolean,
default: true
}
})
//
const containerStyle = computed(() => {
return {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start'
}
})
//
const getItemStyle = (item: SkeletonItem) => {
const style: Record<string, string | number> = {
background: props.baseColor,
margin: item.margin || 0,
animation: props.show && item.animation
? 'skeleton-loading 1.5s infinite'
: 'none'
}
//
if (item.type === 'rect') {
style.width = item.width || '100%'
style.height = item.height || '30px'
style.borderRadius = item.radius || '4px'
} else if (item.type === 'circle') {
style.width = item.width || item.height || '30px'
style.height = item.height || item.width || '30px'
style.borderRadius = '50%'
} else if (item.type === 'custom') {
//
}
return style
}
//
const style = document.createElement('style')
style.innerHTML = `
@keyframes skeleton-loading {
0% { background-position: -468px 0 }
100% { background-position: 468px 0 }
}
.skeleton-item {
background: linear-gradient(90deg,
${props.baseColor} 25%,
${props.highlightColor} 50%,
${props.baseColor} 75%
);
background-size: 468px 104px;
background-repeat: no-repeat;
overflow: hidden;
}
`
document.head.appendChild(style)
</script>
<style scoped>
.skeleton-container {
width: 100%;
box-sizing: border-box;
}
</style>
@@ -0,0 +1,42 @@
<template>
<view class="nav-bar-div" >
<view class="nav-bar" v-if="is_fixed" :style="{ backgroundColor: backgroundColor }"></view>
<view class="nav-bar-seat" v-if="is_seat" :style="{ backgroundColor: backgroundColor }"></view>
</view>
</template>
<script setup>
const props = defineProps({
is_fixed: {
type: Boolean,
default: true
},
is_seat: {
type: Boolean,
default: true
},
backgroundColor: {
type: String,
default: 'transparent'
}
})
</script>
<style lang="scss" scoped>
.nav-bar {
width: 100%;
height: var(--status-bar-height);
position: fixed;
}
.nav-bar-seat {
width: 100%;
height: var(--status-bar-height);
}
.nav-bar-div {
z-index: 1900;
width: 100%;
position: relative;
}
</style>
@@ -0,0 +1,200 @@
<template>
<view class="components_custom_file_upload_div pic-cell">
<view v-for="(item,index) in fileList" class="img_div pic-item" @click="pic_click(item, index)">
<image-preview :imgId="item.fileId" class="img" mode="aspectFill"></image-preview>
</view>
<view @tap="upload" v-show="show_upload_button">
<view class=" add_icon"></view>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed,
watch
} from 'vue'
import common from '@/common/common'
import {
uploadFile
} from '@/api/common'
const show_upload_button = computed(() => {
return !props.readonly && props.limit > fileList.length
})
const pic_click = (item, index) => {
common.show('提示', '是否删除图片' + (index + 1)).then(res => {
if (res) {
fileList.splice(index, 1)
emits('update:modelValue', fileList.map(res => res.fileId))
}
})
}
// props
const props = defineProps({
modelValue: {
type: Array,
default: function() {
return []
}
},
readonly: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 9
},
mark: {
type: [String, Number],
default: ''
},
fileType: {
type: Array,
default: function() {
return ['jpg', 'png']
}
},
})
//
const fileList = reactive([])
watch(() => props.modelValue, (newVal) => {
fileList.length = 0
if (Array.isArray(newVal)) {
fileList.push(...newVal.map(res => ({
fileId: res
})))
}
}, {
deep: true,
immediate: true
})
// emits
const emits = defineEmits(['update:modelValue'])
onMounted(() => {
})
const showPicture = (urls, current) => {
// const urlsList = urls.map((url) => imgPrefix(url));
// uni.previewImage({
// current: current || 0,
// urls: urlsList,
// longPressActions: {
// // itemList: ['', '', ''],
// itemList: [''],
// success: function(data) {
// uni.saveImageToPhotosAlbum({
// filePath: urlsList[data.index],
// success: function() {
// // that.$common.msg('')
// }
// });
// },
// fail: function(err) {}
// }
// });
}
//
const upload = () => {
uni.chooseImage({
count: props.limit,
sizeType: ['original', 'compressed'],
sourceType: ['camera', 'album'],
success: async ({
tempFilePaths
}) => {
const valid = tempFilePaths.every(url => {
if (props.fileType.length === 0) return true;
console.log(url);
const lastDotIndex = url.lastIndexOf('.')
if (lastDotIndex === -1) return false;
const extension = url.slice(lastDotIndex + 1).split('?')[0].toLowerCase()
return props.fileType.includes(extension)
})
if (!valid) return common.msg('您选择的图片格式有误。')
console.log(valid)
const data = {
bizScen: 'S3005'
}
const upload_map = tempFilePaths.map((file) => uploadFile(file, data))
Promise.all(upload_map).then(res => {
fileList.push(...res.splice(0, props.limit - fileList.length))
emits('update:modelValue', fileList.map(res => res.fileId))
}).catch(() => {
})
}
})
}
</script>
<style scoped lang="scss">
.components_custom_file_upload_div {
display: flex;
.img_div {
overflow: hidden;
border: 2rpx solid #C0C0C0;
border-radius: 8rpx;
.img {
width: 100%;
height: 100%;
}
}
}
.pic-cell {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.pic-item {
margin-right: 30rpx;
width: 170rpx;
height: 170rpx;
border-radius: 8rpx;
// background-color: red;
margin-bottom: 24rpx;
}
.add_icon {
position: relative;
width: 170rpx;
height: 170rpx;
border: 2rpx dashed #C0C0C0;
border-radius: 8rpx;
&::before {
content: "";
position: absolute;
background-color: #333;
top: calc(50% - 15rpx);
left: 50%;
width: 2rpx;
height: 30rpx;
transform: translateX(-50%);
}
&::after {
content: "";
position: absolute;
background-color: #333;
top: 50%;
left: calc(50% - 15rpx);
width: 30rpx;
height: 2rpx;
transform: translateY(50%);
}
}
</style>
@@ -0,0 +1,319 @@
<template>
<view class="">
<uni-popup ref="popup" background-color="#222222" :animation="true">
<view class="popup-content" @touchmove.stop :style="{ opacity }">
<view class="max-div">
<view class="title_div">
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-left.png"></image>
<view class="title">{{ title }}</view>
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-right.png"></image>
</view>
<view class="points-div">
<view class="doct_icon left" @click="doct_click(-1)" v-show="multiple">
<view class="doct_icon_div">
<view class="fl1"></view>
<uv-icon size="28" color="#999999" name="arrow-left"></uv-icon>
</view>
</view>
<swiper class="swiper" :current="current">
<swiper-item class="swiper-item-div" v-for="item in badgesList">
<view class="img-div">
<image-preview :src="item.pointsBadgeImg" class="img" mode="aspectFit"></image-preview>
</view>
<view class="points-title-div">{{ item.pointsBadgeName }}</view>
<view class="points-msg-div" v-if="item.remark">{{ item.remark }}</view>
</swiper-item>
</swiper>
<view class="doct_icon right" @click="doct_click(1)" v-show="multiple">
<view class="doct_icon_div">
<uv-icon size="28" color="#999999" name="arrow-right"></uv-icon>
<view class="fl1"></view>
</view>
</view>
</view>
<view class="button-div">
<uv-button
class="button"
:text="multiple ? '一起收下' : '开心收下'"
:custom-style="customStyle"
:color="buttonColor"
@click="clickFun(true)"
></uv-button>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { addPointsByCheckIn, queryCheckIn } from '@/api/pointsAndRank.js';
import common from '@/common/common';
import { arrowRightIcon } from '@/common/imgSvg';
import { nextTick } from 'process';
const popup = ref(null);
const current = ref(0);
const opacity = ref(1);
let CloseComplete;
const badgesList = ref([]);
const pointsList = ref([]);
const multiple = computed(() => badgesList.value.length > 1);
const titleNumberText = ['', '两', '三', '四', '五', '六', '七', '八', '九', '十'];
const title = computed(() =>
badgesList.value.length === 1 ? '恭喜你获得徽章' : `恭喜你获得${titleNumberText[badgesList.value.length - 1]}枚徽章`
);
const buttonColor = 'linear-gradient( 270deg, #F81758 0%, #FFA062 100%)';
const customStyle = {
borderRadius: '40rpx'
};
const doct_click = (step) => {
//
let newIndex = current.value + step;
// n n-1
const maxIndex = badgesList.value.length - 1;
// 0 maxIndex
if (newIndex < 0) {
newIndex = 0;
} else if (newIndex > maxIndex) {
newIndex = maxIndex;
}
console.log('current.value', current.value);
//
current.value = newIndex;
};
const emits = defineEmits(['confirm']);
// 退
const close = () => {
opacity.value = 0;
nextTick(() => {
popup.value.close();
});
};
//
const clickFun = (status = true) => {
emits('confirm', status, pointsList.value, badgesList.value, () => {});
CloseComplete(status, pointsList.value, badgesList.value);
};
const open = (points = [], badges = [], complete = () => {}) => {
if (!Array.isArray(badges) || badges.length === 0) {
console.error('badges 不是有效的数组或为空');
return;
}
console.log('badges', badges);
CloseComplete = complete;
badgesList.value = badges;
pointsList.value = points;
opacity.value = 1;
console.log('成功');
uni.$emit('update_me_data', true);
popup.value.open('center');
};
onUnmounted(() => {});
defineExpose({ open, close });
</script>
<style scoped lang="scss">
.points-div {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.swiper {
height: 700rpx;
flex: 1;
margin: 0 50rpx;
// background-color: red;
.swiper-item-div {
display: flex;
align-items: center;
// justify-content: center;
flex-direction: column;
.points-title-div {
margin-top: 40rpx;
// position: absolute;
// bottom: 60rpx;
}
.points-msg-div {
margin: 30rpx 20rpx 0 20rpx;
// position: absolute;
// bottom: 0rpx;
}
.img-div {
margin-top: 60rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #2d2d2d; /* 中间圆环颜色 */
border-radius: 50%;
width: 400rpx;
height: 400rpx;
&::before {
z-index: 1;
content: '';
position: absolute;
width: 100%;
height: 100%;
border: 2rpx solid #2d2d2d; /* 圆环颜色和粗细 */
border-radius: 50%; /* 圆形 */
transform: scale(0.85);
}
&::after {
z-index: 1;
content: '';
position: absolute;
width: 100%;
height: 100%;
border: 4rpx solid #2d2d2d; /* 中间圆环颜色 */
border-radius: 50%;
transform: scale(0.75);
}
.img {
position: relative;
z-index: 999;
width: 408rpx;
height: 408rpx;
}
}
}
}
.doct_icon {
width: 220rpx;
height: 220rpx;
padding: 0%;
display: flex;
background-color: #2d2d2d;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 999;
.doct_icon_div {
display: flex;
justify-content: space-around;
width: 60%;
// background-color: red;
}
&.left {
left: 0;
transform: translateX(-50%);
}
&.right {
right: 0;
transform: translateX(50%);
}
// width: 8vw;
display: flex;
align-items: center;
flex-direction: column;
}
}
.points-title-div {
// height: 120rpx;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 36rpx;
color: #dec38c;
text-align: center;
}
.points-msg-div {
// height: 120rpx;
font-family: PingFangSC, PingFang SC;
font-weight: 500;
font-size: 26rpx;
color: #dec38c;
text-align: center;
}
.button-div {
height: 80rpx;
width: 442rpx;
margin: 74rpx auto 0 auto;
::v-deep .uv-button__text {
font-size: 34rpx !important;
}
::v-deep .uv-button {
height: 90rpx;
width: 442rpx;
}
}
.continuous-check-in-div {
display: flex;
border-bottom: 2rpx solid #efefef;
margin-bottom: 16rpx;
padding-top: 8rpx;
height: 128rpx;
.img {
width: 112rpx;
height: 102rpx;
}
.text {
margin-left: 16rpx;
font-weight: 400;
font-size: 30rpx;
height: 82rpx;
color: #333333;
display: flex;
align-items: flex-end;
}
.number {
font-weight: 600;
color: #0066ff;
font-family: Arial Black;
}
}
.title_div {
display: flex;
align-items: center;
justify-content: center;
margin-top: 54rpx;
height: 68rpx;
.img {
width: 26rpx;
height: 44rpx;
}
.title {
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
color: #d88f50;
margin: 0rpx 30rpx;
text-align: center;
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 46rpx;
background: linear-gradient(0deg, #ffe5b7 0%, #cc716d 100%);
/* 将背景裁剪为文字形状 */
-webkit-background-clip: text;
background-clip: text;
/* 将文字本身颜色设为透明(显示背景渐变) */
color: transparent;
}
}
.popup-content {
width: 100vw;
height: 100vh;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
// padding: 40rpx 0;
}
.max-div {
width: 100%;
position: relative;
margin-top: calc(var(--status-bar-height) + 13vw);
margin-bottom: 120rpx;
}
</style>
@@ -0,0 +1,484 @@
<template>
<view class="">
<uni-popup ref="popup">
<view class="popup-content" @touchmove.stop>
<view class="fl1"></view>
<view class="max-div">
<view class="popup_back_div" @click="close">
<image :src="optionErrorIcon('#C8BBB7')" class="img"></image>
</view>
<view class="title_div">
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-left.png"></image>
<view class="title">每日签到</view>
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-right.png"></image>
</view>
<view class="continuous-check-in-div">
<image class="img" src="@/static/images/pointsAndRank/badge-icon.png"></image>
<text class="text">
您已连续签到
<text class="number">{{ continuousDay }}</text>
</text>
</view>
<scroll-view scroll-y="true" class="scroll-Y" :show-scrollbar="false">
<view class="calendar-div" :class="{ expand: expand }">
<view
class="calendar-line"
v-for="(line, lineIndex) in dateGroups"
v-show="nowLine === lineIndex || expand"
>
<view
class="calendar-day"
v-for="(day, dayIndex) in line.days"
:class="{ hide: day.index > 30, expand: expand }"
>
<view class="calendar-day-bg" :class="[day.type, isSigned ? 'signed' : 'no-signed']">
<image
v-if="day.type === 'past' || (day.type === 'today' && isSigned)"
:src="optionSuccessIcon('#FFFFFF')"
class="img"
></image>
<text v-else>+{{ day.text }}</text>
<view class="check-in-tip"></view>
</view>
<view class="calendar-day-title">
{{ day.date }}
</view>
</view>
</view>
</view>
</scroll-view>
<view class="switch-div" @click="switchStatus()">
<view class="back_div" :class="expand ? 'up' : 'down'">
<view class="back-icon"></view>
</view>
</view>
<view class="button-div">
<uv-button
class="button"
:text="isSigned ? '已签到' : '签到'"
loadingText="签到中"
:custom-style="customStyle"
:color="buttonColor"
@click="checkIn"
:disabled="isSigned"
:loading="checkInLoading"
></uv-button>
</view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { addPointsByCheckIn, queryCheckIn } from '@/api/pointsAndRank.js';
import common from '@/common/common';
const popup = ref(null);
const buttonColor = 'linear-gradient( 270deg, #E0914A 0%, #EDBE7F 100%)';
const customStyle = {
borderRadius: '40rpx' //
};
let checkInClickCallback;
let checkInSuccessCallback;
const dateGroups = ref([]);
const expand = ref(false); //
const isSigned = ref(false); //
const continuousDay = ref(0); //
const continuousDay30 = computed(() => {
// 30
const mod = continuousDay.value % 30;
// 03030mod1-29
return mod === 0 ? (continuousDay.value === 0 ? 0 : 30) : mod;
});
//
const nowLine = computed(() => {
//
const currentDay = isSigned.value ? continuousDay30.value : continuousDay30.value + 1;
// 701-7 08-14 1...
// 101
const line = Math.floor((currentDay - 1) / 7);
console.log('nowLine', Math.max(line, 0)); // -1
return Math.max(line, 0); // -1 0
});
const emits = defineEmits(['checkInFun']);
//
const switchStatus = () => {
expand.value = !expand.value;
};
// 退
const close = () => {
popup.value.close();
typeof checkInClickCallback === 'function' &&
checkInClickCallback({
status: false
});
};
const checkInLoading = ref(false);
//
const checkIn = () => {
if (checkInLoading.value) return;
checkInLoading.value = true;
//
checkInClickCallback({
status: true,
days: continuousDay.value,
successCb: ({ days, res }) => {
console.log('签到成功', res);
checkInLoading.value = false;
isSigned.value = true;
continuousDay.value = days;
generate(); //
checkInSuccessCallback(true, days, res.body.points, res.body.pointsBadges);
},
failCb: (res) => {
checkInLoading.value = false;
console.log('签到失败', res);
common.msg('签到失败,请稍后再试');
}
});
};
/**
* 生成35天日期列表分5组每组7天包含类型标记和text字段
* "前X天"为第一天第1天显示1每7天显示5第30天显示30
* @param {number} X - 前X天的天数0-35之间的整数总天数=35
* @returns {Array<{
* groupIndex: number, // 0-4
* days: Array<{
* date: string, // mm-dd
* type: 'past' | 'today' | 'future', //
* index: number, //
* text: string // 1/5/30
* }>
* }>} 分组后的日期列表
*/
function generateGroupedDateList(X) {
if (X < 0 || X > 35 || !Number.isInteger(X)) {
throw new Error('X必须是0到35之间的整数');
}
const allDays = [];
const today = new Date();
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
// X(35-X)35
for (let i = -X; i <= 35 - X - 1; i++) {
const currentDate = new Date(today);
currentDate.setDate(today.getDate() + i);
const pureDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
//
let type;
if (pureDate < todayDate) type = 'past';
else if (pureDate > todayDate) type = 'future';
else type = 'today';
//
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
// "X"1
const dayNumber = i - -X + 1; // 1
// text
let text = '1'; // 1
if (dayNumber === 30) {
text = '20'; // 3030
} else if ((dayNumber % 30) % 7 === 0) {
text = '5'; // 77142128355
}
allDays.push({
date: `${month}.${day}`,
type,
index: i - -X + 1,
text,
i
});
}
// 75
const groupedList = [];
for (let i = 0; i < 5; i++) {
const start = i * 7;
const end = start + 7;
groupedList.push({
groupIndex: i,
days: allDays.slice(start, end)
});
}
return groupedList;
}
//
const generate = () => {
try {
const subtractNum = isSigned.value ? 1 : 0;
const X = Math.max(continuousDay30.value - subtractNum, 0);
console.log('取', X);
dateGroups.value = generateGroupedDateList(X);
} catch (err) {
console.error(err.message);
}
};
const open = (isSignedStatus, days, onCheckInClick = () => {}, onCheckInSuccess = () => {}) => {
isSigned.value = isSignedStatus; //
continuousDay.value = days; //
// isSigned.value = false; //
// continuousDay.value = 30; //
checkInClickCallback = onCheckInClick;
checkInSuccessCallback = onCheckInSuccess;
// X=10"10"135
generate();
popup.value.open();
};
defineExpose({ open, close });
</script>
<style scoped lang="scss">
//
.calendar-day {
margin-top: 10rpx;
text-align: center;
// 30
&.hide.expand {
.calendar-day-title {
width: 70rpx;
color: #fff;
}
.calendar-day-bg {
display: none;
}
}
.calendar-day-bg {
width: 70rpx;
height: 84rpx;
border-radius: 8rpx;
//
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-family: Arial, Arial;
font-size: 32rpx;
letter-spacing: -4rpx;
.img {
width: 32rpx;
margin-bottom: 6rpx;
}
&.past {
background-color: #ffd07c;
}
//
&.future {
color: #d88f50;
font-weight: bold;
background: #ffe6b8;
border: 4rpx solid #ffd58a;
}
//
&.signed.today {
background-color: #ffd07c;
}
&.no-signed.today {
color: #d88f50;
font-weight: bold;
background: #ffe6b8;
border: 4rpx solid #ffd58a;
}
//
.check-in-tip {
display: none;
}
&.today.no-signed > .check-in-tip {
display: block;
position: absolute;
top: 0;
right: 0;
width: 30rpx;
height: 30rpx;
font-size: 20rpx;
border-radius: 50%;
border: 2rpx solid #fff;
box-shadow: 0 0 4rpx #fef5e6;
color: #fff;
font-weight: 400;
background-color: #f8831c;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
/* 2. 解决文字被边框挤压:清除默认内边距,重置行高 */
padding: 0; /* 清除默认内边距(部分元素默认有padding) */
line-height: 1; /* 行高与字体大小一致,避免垂直方向多余空间 */
/* 3. 旋转30度(transform 旋转属性) */
transform: translate(30%, -40%) rotate(-30deg);
}
}
.calendar-day-title {
margin: 4rpx 0;
font-family: ArialMT;
font-size: 22rpx;
color: #999999;
line-height: 28rpx;
text-align: center;
font-style: normal;
}
}
.scroll-Y {
/**
* 26rpx 主盒子内间距
* 68rpx 标题高度
* 128rpx 日历高度
* 16rpx 日历下外间距
* 92rpx 下方按钮盒子高度
* 80rpx 切换盒子高度
*/
height: calc(100% - 26rpx - 68rpx - 128rpx - 16rpx - 92rpx - 80rpx - 8rpx);
}
.switch-div {
display: flex;
justify-content: center;
align-items: center;
height: 60rpx;
margin: 10rpx 30%;
.back_div {
.back-icon {
position: relative;
width: 40rpx;
height: 40rpx;
cursor: pointer;
}
.back-icon::before,
.back-icon::after {
content: '';
position: absolute;
transition: all 0.3s ease;
}
.back-icon::after {
top: 25%;
left: 25%;
width: 40%;
height: 40%;
border-top: 2rpx solid #bfbfbf;
border-left: 2rpx solid #bfbfbf;
}
&.up {
transform: rotate(45deg);
}
&.down {
transform: rotate(-135deg);
}
}
}
.calendar-div {
display: flex;
flex-direction: column;
height: 130rpx;
overflow: hidden;
transition: height 0.3s ease;
&.expand {
height: 650rpx;
}
.calendar-line {
display: flex;
justify-content: space-between;
height: 130rpx;
}
}
.button-div {
height: 92rpx;
width: 442rpx;
margin: 0 auto;
.button {
width: 100%;
height: 100%;
}
}
.continuous-check-in-div {
display: flex;
border-bottom: 2rpx solid #efefef;
margin-bottom: 16rpx;
padding-top: 8rpx;
height: 128rpx;
.img {
width: 112rpx;
height: 102rpx;
}
.text {
margin-left: 16rpx;
font-weight: 400;
font-size: 30rpx;
height: 82rpx;
color: #333333;
display: flex;
align-items: flex-end;
}
.number {
font-weight: 600;
color: #0066ff;
font-family: Arial Black;
}
}
.title_div {
display: flex;
align-items: center;
justify-content: center;
margin-top: 34rpx;
height: 68rpx;
.img {
width: 26rpx;
height: 44rpx;
}
.title {
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 46rpx;
color: #d88f50;
margin: 0rpx 30rpx;
text-align: center;
}
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
.img {
margin: 10rpx;
width: 40rpx;
height: 40rpx;
}
}
.popup-content {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
}
.max-div {
width: 80vw;
background-color: #fefbfa;
border-radius: 38rpx;
position: relative;
padding: 26rpx;
overflow: hidden;
margin-top: var(--status-bar-height);
margin-bottom: 120rpx;
}
</style>
@@ -0,0 +1,191 @@
<template>
<view class="">
<uni-popup ref="popup">
<view class="popup-content" @touchmove.stop>
<view class="fl1"></view>
<view class="max-div">
<view class="popup_back_div" @click="clickFun(false)">
<image :src="optionErrorIcon('#C8BBB7')" class="img"></image>
</view>
<view class="title_div">
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-left.png"></image>
<view class="title">恭喜你获得积分</view>
<image class="img" src="@/static/images/pointsAndRank/badge-wheat-right.png"></image>
</view>
<view class="points-div">
<view class="points-img-div">
<image class="img_100" src="@/static/images/pointsAndRank/like.png" mode=""></image>
</view>
<view class="points-num-div">+{{showData.num}}</view>
<view class="points-msg-div" v-show="showData.message"> {{showData.message}}</view>
</view>
<view class="button-div">
<uv-button
class="button"
text="开心收下"
:custom-style="customStyle"
:color="buttonColor"
@click="clickFun(true)"
></uv-button>
</view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { optionErrorIcon, optionSuccessIcon } from '@/common/imgSvg';
import { addPointsByCheckIn, queryCheckIn } from '@/api/pointsAndRank.js';
import common from '@/common/common';
const showData = ref({
num: 1,
message:''
});
const badgesList =ref([]);
const pointsList = ref([]);
const popup = ref(null);
let CloseComplete
const buttonColor = 'linear-gradient( 270deg, #F81758 0%, #FFA062 100%)';
const customStyle = {
borderRadius: '40rpx' //
};
const emits = defineEmits(['confirm']);
const open = (points=[], badges=[], complete = () => {}) => {
if (!Array.isArray(points) || points.length === 0) {
// console.error('points ');
//
emits(
'confirm',
status,
points,
badges
);
complete(false, points, badges)
return;
}
CloseComplete = complete
badgesList.value = badges
//
const [firstItem, ...remainingPoints] = points;
showData.value['num'] = firstItem['changePoints'];
showData.value['message'] = firstItem['message'];
pointsList.value = remainingPoints;
uni.$emit('update_me_data', true);
popup.value.open();
};
const close = () => {
popup.value.close();
};
//
const clickFun = (status = true) => {
emits(
'confirm',
status,
pointsList.value,
badgesList.value,
() => {
}
);
CloseComplete(status, pointsList.value, badgesList.value)
};
defineExpose({ open, close });
</script>
<style scoped lang="scss">
.points-div {
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx 0 0 0;
.points-img-div {
width: 320rpx;
height: 278rpx;
}
.points-num-div {
font-family: Arial, Arial;
font-weight: normal;
font-size: 108rpx;
color: #333333;
line-height: 108rpx;
text-align: center;
font-weight: 600;
}
.points-msg-div {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 30rpx;
color: #ae723b;
line-height: 40px;
text-align: center;
}
}
.button-div {
height: 92rpx;
width: 442rpx;
margin: 40rpx auto 20rpx auto;
.button {
width: 100%;
height: 100%;
}
}
.title_div {
display: flex;
align-items: center;
justify-content: center;
margin-top: 34rpx;
height: 68rpx;
.img {
width: 26rpx;
height: 44rpx;
}
.title {
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 46rpx;
color: #d88f50;
margin: 0rpx 30rpx;
text-align: center;
}
}
.popup_back_div {
width: 60rpx;
height: 60rpx;
position: absolute;
top: 20rpx;
right: 20rpx;
.img {
margin: 10rpx;
width: 40rpx;
height: 40rpx;
}
}
.popup-content {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
}
.max-div {
width: 80vw;
background-color: #fefbfa;
border-radius: 38rpx;
position: relative;
padding: 26rpx;
overflow: hidden;
margin-top: var(--status-bar-height);
margin-bottom: 120rpx;
}
</style>
@@ -0,0 +1,121 @@
/**
完成课程学习 首次完成1分 每天最多获取5分 完成课程学习 crsId 02 完成调用 杨航
完成课程练习 首次完成1分 每天最多获取5分 完成课程练习 crsId 04 完成练习调用 田岩 已完成
通过课程考试 首次完成1分 每天最多获取5分 通过课程考试 crsId 03 通过调用 田岩 已完成
完成考试中心考试 首次完成1分 每天最多获取5分 完成考试中心考试 papersId 05 完成调用 田岩 已完成
完成竞赛PK 每次1分 每天最多获取5分 完成竞赛PK 11 完成调用 田岩 已完成
获取AI问答 次数徽章 06 建立ws完成调用 杨航
通过AI陪练练习 每次1分 每天最多获取5分 通过AI陪练练习 07 通过调用 杨航
通过AI陪练考试 每次1分 每天最多获取5分 通过AI陪练考试 08 通过调用 杨航
签到 每日1积分
第七天5积分
第30天20积分
断签从第一天计算 签到完成 田岩
*/
import {
addPointsByTask
} from '@/api/pointsAndRank.js'
import {
ref,
nextTick
} from 'vue'
import {
pop_up_time
} from '@/enum';
export default function usePointsAndBadge() {
const badgeRef = ref(null);
const pointsRef = ref(null);
/**
* 处理积分和徽章弹窗逻辑提取的独立方法
* @param {number} points - 积分值
* @param {Array} pointsBadges - 积分关联的徽章列表
* @param {Function} complete - 完成回调函数
* @param {any} res - 接口返回数据
*/
const handlePointsBadgePopup = (points, pointsBadges, complete = () => {}, res = {}) => {
// AI问答仅弹徽章弹窗
if (points?.length > 0) {
// 普通场景:先弹积分弹窗,再弹徽章弹窗
pointsRef.value?.open(points, pointsBadges, (status, pointsList, badgesList) => {
pointsRef.value.close();
// 有徽章则弹徽章弹窗,否则直接完成
if (badgesList?.length > 0) {
nextTick(() => {
setTimeout(() => {
badgeRef.value.open(pointsList, badgesList, () => {
badgeRef.value.close();
complete(true, '积分获取成功', res);
});
}, pop_up_time);
});
} else {
complete(true, '积分获取成功', res);
}
});
} else if (pointsBadges?.length > 0) {
badgeRef.value.open([], pointsBadges, () => {
badgeRef.value.close();
complete(true, '积分获取成功', res);
});
}else{
complete(false, '积分接口返回数据异常', res);
}
};
/**
* 积分和徽章处理主方法
* @param {string} type - 任务类型04课程练习06AI问答其他普通任务
* @param {string|null} taskId - 任务ID
* @param {Function} complete - 完成回调status, message, data
*/
const pointAndBadgesRun = async (type, taskId = '', complete = () => {}) => {
try {
console.log('调用积分接口');
const res = await addPointsByTask({
type,
taskId
});
// 防御性处理:确保res.body存在
if (!res?.body) {
complete(false, '积分接口返回数据异常', res);
return;
}
const {
points,
pointsBadges,
message = ''
} = res.body;
// 分支逻辑:按类型处理
switch (type) {
case '04': // 04:课程练习,自定义返回逻辑,不弹弹窗
complete(true, '', res.body); // 补充回调,避免调用方等待
return;
case '06': // 06AI问答,仅弹徽章弹窗
if (pointsBadges?.length > 0) {
handlePointsBadgePopup(points, pointsBadges, complete, res);
} else {
complete(true, '', res);
}
return;
// 其他类型:弹积分+徽章弹窗
default:
handlePointsBadgePopup(points, pointsBadges, complete, res);
return;
}
} catch (error) {
// 细化错误信息:区分网络错误/接口错误
const errorMsg = error?.message || '积分获取失败';
complete(false, `积分获取失败:${errorMsg}`, error);
}
};
return {
pointAndBadgesRun,
handlePointsBadgePopup,
badgeRef,
pointsRef
};
}
@@ -0,0 +1,68 @@
<template>
<uv-input
v-model="inputValue"
:customStyle="fixedCustomStyles"
placeholderStyle="color:#999999;font-size:26rpx"
:placeholder="placeholder"
:adjustPosition="false"
:confirmType="confirmType"
:maxlength="20"
@confirm="search"
>
<template #suffix><uv-icon name="search" color="#2c8ef2" size="24" @click="search(inputValue)"></uv-icon></template>
</uv-input>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch } from 'vue';
const fixedCustomStyles = {
backgroundColor: '#F1F5FA',
paddingLeft: '40rpx',
paddingTop: '5px',
paddingBottom: '5px',
border: 'none'
};
// props
const props = defineProps({
modelValue: {
type: String, // String
default: ''
},
placeholder: {
type: String,
default: ''
},
confirmType: {
type: String,
default: 'search'
},
});
// emits
const emits = defineEmits(['update:modelValue', 'search']);
// inputValueprops.modelValue
const inputValue = ref(props.modelValue);
// inputValue
watch(inputValue, (newVal) => {
emits('update:modelValue', newVal);
});
// modelValue
watch(
() => props.modelValue,
(newVal) => {
inputValue.value = newVal;
}
);
const clearText = () => {
console.log('清除搜索结果');
inputValue.value = ''
};
const search = () => {
emits('search', inputValue.value);
};
defineExpose({clearText})
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,206 @@
<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([]);
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({ orgName: '全部', orgId: 'A', parentId: '' }, 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']);
const open = (status, inPaths) => {
console.log('传入的数据', inPaths);
if (Array.isArray(inPaths) && inPaths.length > 0) {
selectedPaths.value = inPaths;
} else {
selectedPaths.value = [{ orgName: '全部', orgId: 'A', parentId: '' }];
}
openDialog(true);
};
defineExpose({
open,
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,129 @@
<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[selectedPaths.value.length - 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[selectedPathsOrgCodeList.value.length - 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;
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>
@@ -0,0 +1,26 @@
<template>
<view>
<view class="tabbar-shadow"> </view>
</view>
</template>
<script>
export default {
name: "tabbar-shadow",
data() {
return {};
}
}
</script>
<style lang="scss" scoped>
.tabbar-shadow {
position: fixed;
bottom: -14px;
z-index: 3333;
left: 0;
width: 100%;
height: 12px;
background-color: #fff;
box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.2); //
}
</style>
@@ -0,0 +1,238 @@
<template>
<view class="item">
<view class="top">
<view class="type">{{ subject_type_text }}</view>
<view class="score">{{ item.tgtGrade }}</view>
<view class="fl1"></view>
<view class="right" v-if="[''].includes(props.mode)">
<view class="right_text">展开</view>
<uv-icon class="arrow-icon" name="arrow-right"></uv-icon>
</view>
</view>
<view class="text">{{ item.qnsContent }}</view>
<view class="additional_div">
<view class="option_div">
<radioSubjectItem v-for="(analysisVo,index) in item.itemVos" @click_option="click_option" :key="index"
:analysisVo="analysisVo" :my_answer="props.item.anserResult" :backend_answer="[]">
</radioSubjectItem>
<!-- <view class="option_item success">
<view class="text">A.这里是选项 A 的内容</view>
<image class="img" src="@/static/images/common/success.png" mode="heightFix"></image>
</view>
<view class="option_item success">
<view class="text">B.这里是选项 B 的内容</view>
<image class="img" src="@/static/images/common/success.png" mode="heightFix"></image>
</view>
<view class="option_item error">
<view class="text">C.这里是选项 C 的内容</view>
<image class="img" src="@/static/images/common/success.png" mode="heightFix"></image>
</view>
<view class="option_item info">D.这里是选项D的内容</view> -->
</view>
<view class="analysis_div">
<view class="analysis_title">
<view class="success_answer">正确答案{{ correctResultLabel.join('') }}</view>
<view class="your_answer">
你的答案
<text class="red_color">{{ myResultLabel.join('') }}</text>
</view>
</view>
<view class="analysis_note">
题目解析{{item.analyContent}}
</view>
</view>
</view>
</view>
</template>
<script setup>
import radioSubjectItem from '@/components/examination/radio-subject-item.vue'
import { SUBJECT_TYPE } from '@/enum.js';
import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from 'vue';
const props = defineProps({
item: {
type: Object,
required: true
},
mode: {
type: String,
required: true,
validator: (value) => {
// : examination : practice : mistake : study
return ['examination', 'practice', 'mistake', 'study'].includes(value);
}
}
});
//
const qnsTyp = computed(() => props.item.qnsTyp);
//
const correctResultLabel = computed(() =>
props.item.itemVos.filter((res) => res.correctFlag === 'R').map((res) => res.itemNo)
);
//
const myResultLabel = computed(() => {
const anserResultArray = props.item.anserResult.split(',');
return props.item.itemVos.filter((res) => anserResultArray.includes(res.itemId)).map((res) => res.itemNo);
});
console.log(myResultLabel.value, 'myResultLabel');
//
const subject_data = reactive({
qnsId: '',
qnsContent: '',
qnsTyp: '',
itemVos: props.item.itemVos.map((res) => ({
...res,
// , 'info' primary sunccess 绿 error
select_status: 'info'
}))
});
//
const subject_type_text = computed(() => SUBJECT_TYPE.find((res) => qnsTyp.value === res.value)?.label || '');
const click_option = () => {
}
</script>
<style scoped lang="scss">
.success {
color: #15b859;
background: #e7f8ed;
}
.info {
color: #333333;
background: #f9f9f9;
}
.error {
color: #f5212d;
background: #fce8e9;
}
.option_item {
width: 100%;
height: 108rpx;
border-radius: 15rpx;
margin-top: 32rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
line-height: 108rpx;
text-align: left;
font-style: normal;
padding-left: 52rpx;
display: flex;
align-items: center;
.text {
flex: 1;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
}
.img {
height: 30rpx;
padding-right: 30rpx;
}
}
.analysis_div {
.analysis_title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 32rpx 0 28rpx 0;
.success_answer {
}
.your_answer {
}
.red_color {
color: #f5212d;
}
}
.analysis_note {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #666666;
line-height: 48rpx;
text-align: left;
font-style: normal;
}
}
.item {
// height: 170rpx;
border-radius: 16rpx;
margin: 12rpx 0 50rpx 0;
padding: 34rpx 30rpx 34rpx 36rpx;
background-color: #fff;
display: flex;
flex-direction: column;
.top {
display: flex;
align-items: center;
.type {
width: 108rpx;
height: 46rpx;
background: #267eff;
border-radius: 8rpx;
font-family: PingFangSC;
font-weight: 500;
font-size: 25rpx;
color: #ffffff;
text-align: center;
line-height: 46rpx;
}
.score {
margin-left: 24rpx;
font-family: ArialMT;
font-size: 30rpx;
color: #333333;
text-align: left;
}
.right {
display: flex;
align-items: center;
.right_text {
font-family: PingFangSC;
font-weight: 600;
font-size: 28rpx;
color: #333;
line-height: 40rpx;
text-align: left;
font-style: normal;
}
}
}
.text {
font-family: PingFangSC;
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 48rpx;
text-align: left;
font-style: normal;
margin-top: 20rpx;
}
}
</style>
@@ -0,0 +1,114 @@
<template>
<KeepAlive>
<view class="watermark-container" v-if="showWatermark">
<view class="watermark-item" v-for="row in rowCount" :key="row" :style="{ top: `${row * itemHeight}px` }">
<view
class="watermark-text"
v-for="(cols, col) in colCount"
:key="col"
:style="{
left: `${col * itemWidth}px`
}"
>
{{ textValue }}
</view>
</view>
</view>
</KeepAlive>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getUserInfo } from '@/common/common';
// props
const props = defineProps({
// px
itemWidth: {
type: Number,
default: 180
},
// px
itemHeight: {
type: Number,
default: 120
},
//
showWatermark: {
type: Boolean,
default: true
}
});
// data
const rowCount = ref(0); //
const colCount = ref(0); //
const windowWidth = ref(0); //
const windowHeight = ref(0); //
const textValue = ref('');
//
const calcGridCount = () => {
// = / + 21
colCount.value = Math.ceil(windowWidth.value / props.itemWidth) + 2;
// = / + 21
rowCount.value = Math.ceil(windowHeight.value / props.itemHeight) + 2;
};
// App
const getWindowInfo = () => {
uni.getSystemInfo({
success: (res) => {
windowWidth.value = res.windowWidth;
windowHeight.value = res.windowHeight;
calcGridCount(); //
}
});
};
// mounted
onMounted(() => {
getWindowInfo();
updateWatermarkText();
});
//
const updateWatermarkText = () => {
const userInfo = getUserInfo();
textValue.value = `${userInfo?.userName ?? ''} ${userInfo?.loginName ?? ''}`;
};
defineExpose({
updateWatermarkText
});
</script>
<style scoped>
.watermark-container {
position: fixed;
top: var(--status-bar-height); /* 适配状态栏 */
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none; /* 不拦截点击事件 */
z-index: 99999;
overflow: hidden;
}
/* 行容器 */
.watermark-item {
position: absolute;
width: 100%;
height: auto;
}
/* 单个水印文本 */
.watermark-text {
position: absolute;
white-space: nowrap; /* 避免文本换行 */
user-select: none; /* 禁止选中 */
/* letter-spacing: 2px; */
color: rgba(30, 30, 70, 0.1);
font-size: 16px;
transform: rotate(-30deg);
}
</style>
+226
View File
@@ -0,0 +1,226 @@
import {
queryCheckIn,
addPointsByCheckIn
} from '@/api/pointsAndRank.js';
import common from '@/common/common';
import {
getUserInfo
} from '@/common/common';
import {
ref
} from 'vue';
export default function useCheckIn() {
const checkInRef = ref(null)
// ========== 核心工具方法 ==========
/**
* 获取当前用户的签到存储key区分不同用户
*/
const getKey = () => {
const userInfo = getUserInfo();
const userId = userInfo?.userId || ''; // 修复原代码userId重复赋值问题
if (!userId) {
console.warn('用户ID为空,无法获取签到状态');
return '';
}
return `${userId}_user_sign_in_status`;
};
/**
* 获取今日日期YYYY-MM-DD
*/
const getTodayDate = () => {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
};
/**
* 读取本地存储的签到状态
* @returns {Object} { isSigned: boolean, signDate: string }
*/
const getLocalSignStatus = () => {
const key = getKey();
if (!key) return {
signDate: '',
checkInDays: 0,
PopupDay: ''
};
try {
// 读取本地存储的签到信息(兼容JSON格式)
const rawData = common.getValue(key);
if (!rawData) return {
signDate: '',
checkInDays: 0,
popupDay: ''
};
// 兼容存储的是字符串或JSON对象的情况
const signStatus = rawData;
return {
signDate: signStatus.signDate || '',
checkInDays: signStatus.checkInDays || 0,
popupDay: signStatus.popupDay || '',
};
} catch (e) {
console.error('读取本地签到状态失败', e);
return {
signDate: '',
checkInDays: 0,
popupDay: ''
};
}
};
/**
* 更新本地签到状态支持按需更新字段
* @param {boolean|undefined} isSigned - 是否签到可选传入则更新
* @param {number|undefined} checkInDays - 签到天数可选传入则更新
* @param {string|undefined} popupDay - 弹窗关闭日期可选传入则更新
*/
const setLocalSignStatus = (isSigned, checkInDays, popupDay) => {
const key = getKey();
if (!key) return;
// 1. 读取本地原有数据(若无则初始化空对象)
let oldSignStatus = {};
try {
oldSignStatus = common.getValue(key) || {}; // 兼容common.getValue返回null/undefined的情况
} catch (e) {
console.warn('读取原有签到状态失败,使用空对象初始化', e);
oldSignStatus = {};
}
// 2. 初始化新状态:先继承原有数据
const newSignStatus = { ...oldSignStatus };
// 3. 按需更新字段(仅当参数传入时覆盖)
// 处理isSigned:传入则更新,且为true时自动设置signDate
if (isSigned !== undefined) {
// 保持原有逻辑:isSigned为true时,signDate设为今日
newSignStatus.signDate = isSigned ? getTodayDate() : oldSignStatus.signDate;
}
// 处理checkInDays:传入则更新,未传入保留原有值
if (checkInDays !== undefined) {
newSignStatus.checkInDays = checkInDays;
}
// 处理popupDay:传入非空则更新,未传入/空值保留原有值
if (popupDay !== undefined) {
newSignStatus.popupDay = popupDay? getTodayDate() : oldSignStatus.popupDay;
}
// 4. 重新存入本地(覆盖原有数据)
try {
console.log('存入的数据(原有+更新)', newSignStatus);
common.setValue(key, newSignStatus);
} catch (e) {
console.error('更新本地签到状态失败', e);
}
};
// ========== 对外暴露的核心方法 ==========
/**
* 初始化签到状态页面加载时调用同步本地与后端状态
* @returns {Promise<{ isSigned: boolean, checkInDays?: number }>} 签到状态 + 连续签到天数
*/
const initCheckInStatus = async (complete = () => {}) => {
const today = getTodayDate();
const localStatus = getLocalSignStatus();
console.log('localStatus', localStatus);
// 1. 本地已有今日签到记录 → 直接返回本地状态
if (localStatus.signDate === today) {
complete(true, localStatus.checkInDays, localStatus.popupDay === today)
return true;
}
// 2. 本地记录过期/无记录 → 请求接口获取最新状态
try {
const res = await queryCheckIn(); // 调用查询签到状态接口
const {
body
} = res || {};
console.log('bodbodyy', body);
// // 接口返回格式适配(根据实际后端返回调整)
const isSigned = body?.checkInToday === 'Y';
const checkInDays = body?.days || 0;
setLocalSignStatus(isSigned, checkInDays);
// if (!isSigned) { // 没签到执行签到弹窗
// console.log('没签到执行签到弹窗');
// //如果今日点击过x那么久不弹了
// // if(localStatus.popupDay === today) return;
// complete(isSigned, checkInDays, localStatus.popupDay === today)
// }
complete(isSigned, checkInDays, localStatus.popupDay === today)
} catch (e) {
console.error('请求签到状态接口失败', e);
}
};
/**
* 判断今日是否已签到简化版供快速判断
* @returns {boolean}
*/
const getTodayCheckInStatus = () => {
const localStatus = getLocalSignStatus();
const today = getTodayDate();
// 日期匹配且已签到,才返回true
return localStatus.signDate === today;
};
/**
* 执行签到操作核心方法
* @param {Function} successCb - 签到成功回调
* @param {Function} failCb - 签到失败回调
* @returns {Promise<boolean>} 签到是否成功
*/
const checkInFun = async ({
status,
days=0,
successCb=() =>{},
failCb=() =>{}
}) => {
if(!status) { // 点击了关闭
console.log('点击了关闭');
setLocalSignStatus(undefined, undefined, true);
return;
}
console.log('请求接口', days);
addPointsByCheckIn({
days
})
.then((res) => {
console.log('接口成功回调', res);
setLocalSignStatus(true, days + 1);
successCb?.({
days: days + 1,
res
});
}).catch((error) => {
failCb?.(error)
})
};
/**
* 重置签到状态可选用于特殊场景
*/
const resetCheckInStatus = () => {
const key = getKey();
if (key) common.removeValue(key); // 需确保common有remove方法,无则补充
};
// ========== 返回对外暴露的方法 ==========
return {
checkInRef,
initCheckInStatus, // 初始化签到状态(页面加载时调用)
checkInFun, // 执行签到
getLocalSignStatus, // 获取签到缓存信息
getTodayCheckInStatus // 获取今天签到状态
};
}
+10
View File
@@ -0,0 +1,10 @@
import RecordPermission from '@/common/recordPermission.js'
export const initRecord = () => RecordPermission.requestPermission();
export default function useIndexList() {
return {
};
}
+33
View File
@@ -0,0 +1,33 @@
import {
computed,
reactive,
nextTick,
ref
} from 'vue';
export default function useIndexList() {
const tabAct = ref(0);
const listItemRefs = ref([]);
const tabChange = (item) => {
tabAct.value = item.index;
};
const swiperChange = (item) => {
tabAct.value = item.detail.current;
};
const search = (value) => {
console.log('search', value);
listItemRefs.value.forEach((item, index) => {
item?.search(value, tabAct.value === index);
});
};
return {
tabAct,
listItemRefs,
tabChange,
swiperChange,
search,
};
}
@@ -0,0 +1,83 @@
import {
reactive,
ref,
computed,
nextTick
} from 'vue';
import {
uploadFile
} from '@/api/common';
import {
queryCurrentUser
} from '@/api/login.js';
import common from '@/common/common';
import {
updateDfSysUserExtandInfoImageAddr
} from '@/api/user.js';
export default function useUpdateAvatar({
success = () => {}
}) {
const avatarCropperUrl = ref('');
const avatarCropperStatus = ref(true);
// 点击修改头像
const click_update_avatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
success: (resPicture) => {
uni.getFileInfo({
filePath: resPicture.tempFilePaths[0],
success: function(resSize) {
if (resSize.size > 10 * 1024 * 1024) return common.msg(
'图片过大,请重新选择');
avatarCropperUrl.value = resPicture.tempFilePaths[0];
avatarCropperStatus.value = true;
},
fail: function(err) {
common.msg('获取图片失败,请选择其他图片');
}
});
}
});
};
// 选择头像取消
const avatarOnCancel = (value) => {
avatarCropperStatus.value = false;
};
// 选择头像确定
const avatarOnConfirm = (file) => {
avatarCropperStatus.value = false;
nextTick(async () => {
common.loading('更新头像中');
try {
const data = {
bizScen: 'S3007',
thumbnailFlag: true,
thumbImgSize: 100
};
// 上传图片
const res = await uploadFile(file.tempFilePath, data);
// 更新个人信息
await updateDfSysUserExtandInfoImageAddr({
imageAddr: res.thumbFileId
});
// 更新个人信息
const userData = await queryCurrentUser();
common.msg('修改成功');
success(userData)
} catch {
common.msg('修改失败');
}
common.hideLoading();
});
};
return {
avatarCropperUrl,
avatarCropperStatus,
click_update_avatar,
avatarOnCancel,
avatarOnConfirm,
};
}
+65
View File
@@ -0,0 +1,65 @@
import {
computed,
reactive,
nextTick,
ref
} from 'vue';
export default function useZpaging({
fetchFunction, // 必传:接口请求函数
searchKey = '' // 可选:搜索参数名,默认'searchText'
}) {
const total = ref(-1);
const data_list = ref([]);
const searchText = ref('');
const pagingRef = ref(null);
const queryList = async (pageNo, pageSize) => {
try {
const requestParams = {
page: pageNo,
limit: pageSize
};
// 只有当searchKey有效(非空字符串/非undefined)且searchText有值时,才添加搜索参数
if (searchKey && searchKey.trim() && searchText.value) {
requestParams[searchKey] = searchText.value;
}
const res = await fetchFunction(requestParams);
total.value = res.total??res.body.length
pagingRef.value.completeByTotal(res.body, res.total);
} catch (err) {
console.error('分页查询失败:', err);
pagingRef.value?.complete(false);
}
};
//刷新列表要求
const reload = () => {
pagingRef.value?.reload()
};
// 清空分页数据,pageNo恢复为默认值。
const clear = () => {
total.value = -1
pagingRef.value?.clear();
}
// 搜索
const search = (value, state=true) => {
console.log('搜索', value, state);
searchText.value = value;
if (state) {
reload()
} else {
clear()
}
};
return {
pagingRef,
queryList,
reload,
search,
clear,
searchText,
total,
data_list
};
}
+51
View File
@@ -0,0 +1,51 @@
export const MAX_UPLOAD_PICTURE_SIZE = 2048 // 最大图片上传限制
export const Max_Upload_Document_Size = 102400 // 最大文档上传限制
export const TextareaInputLimitCharacter = 160 // 全局多行文本输入框限制字数
// 公共tab页的图片
export const LineBg =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAABL0lEQVQ4T6XSv0uCURTG8e9jgpRNDS1BU1sELUFLQVM1tgQFtcR7A8m1LQqaLGhOkQKH/o5aKiiHlgiE0K1oCYLAMk9ov/RF8e313u2ew4dzzzmi2+NsA9gH3hCr6srzbAJxDkS/nbvwYNJilMkDow1FXYUHneWAlV/MqN3ZcKCzHWDb164MGa3/H/Qshdj0YbdUmORIL8HBNRsgShZjoQkznvioY/e19wCgCccSsAcM+bBnjHmyuvx5bw8mrJ93FoEkYrzFej0g5kjrpjH2BTrro8oIYhgYQ0xhTCPibfb0ggjLHKrojwvPEogDIBZgycsYKQrscqpKq3zh7BXo7YBVgRMibLWqqvnLzh6BwTZgESNHD8edoL+hOJupT9CII0oYJcQ14oy0CgHa0JTyCeBLVNeWygE5AAAAAElFTkSuQmCC";
// 性别
export const GENDER = [{
value: "m",
label: "男性",
},
{
value: "f",
label: "女性",
}
]
// 试题题型
export const SUBJECT_TYPE = [{
value: "SN",
label: "单选题",
},
{
value: "MU",
label: "多选题",
},
{
value: "JD",
label: "判断题",
},
{
value: "ES",
label: "问答题",
}
]
export const defaultAvatar = '/static/images/me/default_user_avatar.png'
export const defaultAnonymousAvatar = '/static/images/competition/competition_default_avatar.png'
export const styleButton = {
style_button_1: {
color: '#FFFFFF',
fontSize: '30rpx',
backgroundColor: '#0066FF',
borderRadius: '8rpx'
},
style_button_2: {
color: '#0066FF',
fontSize: '30rpx',
backgroundColor: '#E2EDFF',
borderRadius: '8rpx'
}
}
export const pop_up_time = 500;
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
import App from './App'
import { createSSRApp } from 'vue'
import pinia from './store.js'
import CommonLoading from './components/loading.vue'
uni.$zp = {
config: {
'default-page-size': 20,
'show-loading-more-when-reload': true, // 列表刷新时自动显示加载更多view,且为加载中状态
'show-scrollbar': false, // 控制是否出现滚动条
'refresher-refreshing-scrollable': false, // 自定义下拉刷新刷新中状态是否允许列表滚动
"min-delay":300,// 触发@query后最小延迟处理的时间,单位为毫秒
'empty-view-img': '/static/images/common/default_img.png',
'empty-view-center': false, // 空数据图片是否垂直居中,默认为是,若设置为否即为从空数据容器顶部开始显示
//'auto-hide-empty-view-when-pull':false,// 加载中时是否自动隐藏空数据图
}
}
export function createApp() {
const app = createSSRApp(App)
app.use(pinia)
app.component('CommonLoading',CommonLoading)
return {
app
}
}
+103
View File
@@ -0,0 +1,103 @@
{
"name" : "tra-app",
"appid" : "__UNI__EDE0E52",
"description" : "",
"versionName" : "0.0.3",
"versionCode" : 3,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"renderer" : "auto",
"orientation" : [ "portrait-primary" ],
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : false,
"autoclose" : true,
"delay" : 0
},
"safearea" : {
"background" : "#CCCCCC",
"bottom" : {
"offset" : "none|auto"
},
"left" : {
"offset" : "none|auto"
},
"right" : {
"offset" : "none|auto"
}
},
/* */
"modules" : {
"Camera" : {},
"Barcode" : {},
"Record" : {},
"VideoPlayer" : {},
"LivePusher" : {}
},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {
"dSYMs" : false
},
/* SDK */
"sdkConfigs" : {},
"splashscreen" : {
"androidStyle" : "common",
"android" : {
"hdpi" : "src/static/images/login/open.9.png",
"xhdpi" : "src/static/images/login/open.9.png",
"xxhdpi" : "src/static/images/login/open.9.png"
},
"useOriginalMsgbox" : true
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}
+599
View File
@@ -0,0 +1,599 @@
{
"pages": [
{
"path": "pages/index/dialog",
"style": {
"navigationBarTitleText": "AI问答",
"app-plus": {
"titleNView": false,
"animationType": "fade-in"
}
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseDetail/index",
"style": {
"navigationBarTitleText": "课程详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseDetail/submit",
"style": {
"navigationBarTitleText": "提交评价",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/setting/index",
"style": {
"navigationBarTitleText": "设置",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/welcome/index",
"style": {
"navigationBarTitleText": "欢迎",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/test/index",
"style": {
"navigationBarTitleText": "测试",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/study/index",
"style": {
"navigationBarTitleText": "学习",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/scene/index",
"style": {
"navigationBarTitleText": "场景",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/me/index",
"style": {
"navigationBarTitleText": "我的",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/other/web_view",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/login/ysxy",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/mascot/mascot_select_list",
"style": {
"navigationBarTitleText": "吉祥物列表",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/preview/index",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/favorites/index",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/messageNotification/index",
"style": {
"navigationBarTitleText": "消息",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/messageNotification/list",
"style": {
"navigationBarTitleText": "消息列表",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/testingHall/index",
"style": {
"navigationBarTitleText": "考试中心",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/testingHall/details",
"style": {
"navigationBarTitleText": "考试详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/wrongQuestionRecord/index",
"style": {
"navigationBarTitleText": "错题本",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/wrongQuestionRecord/correct",
"style": {
"navigationBarTitleText": "错题本做题页",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/examination/index",
"style": {
"navigationBarTitleText": "考试",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/examination/result",
"style": {
"navigationBarTitleText": "考试结果",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/practice/index",
"style": {
"navigationBarTitleText": "练习",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/learningTasks/index",
"style": {
"navigationBarTitleText": "学习任务",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/learningTasks/details",
"style": {
"navigationBarTitleText": "学习详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/examRanking/index",
"style": {
"navigationBarTitleText": "考试排行榜页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseRecord/index",
"style": {
"navigationBarTitleText": "课程记录页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/examinationRecord/index",
"style": {
"navigationBarTitleText": "考试记录页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseRecord/courseStudyRecordDetail",
"style": {
"navigationBarTitleText": "课程记录学习记录详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseRecord/coursePracticeRecordDetail",
"style": {
"navigationBarTitleText": "课程记录练习记录详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/wrongQuestionRecord/result",
"style": {
"navigationBarTitleText": "答题本答题完成结果页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/practice/result",
"style": {
"navigationBarTitleText": "练习完成结果页",
"app-plus": {
"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
}
}
},
{
"path": "pages/pointsAndRank/badge",
"style": {
"navigationBarTitleText": "徽章墙",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/competitionRecord/index",
"style": {
"navigationBarTitleText": "竞赛记录",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/pointsRedemption/index",
"style": {
"navigationBarTitleText": "积分兑换",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/test/testChat",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/course/index",
"style": {
"navigationBarTitleText": "课程中心",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/course/study",
"style": {
"navigationBarTitleText": "课程学习",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/charts/index",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/index/preview",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/search/result",
"style": {
"navigationBarTitleText": "",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/example/dialog",
"style": {
"navigationBarTitleText": "AI问答演示",
"app-plus": {
"titleNView": false,
"animationType": "fade-in"
}
}
},
{
"path": "pages/sparring/index",
"style": {
"navigationBarTitleText": "AI陪练",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/detail",
"style": {
"navigationBarTitleText": "AI陪练详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/tip",
"style": {
"navigationBarTitleText": "AI陪练提示",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/chooseRole",
"style": {
"navigationBarTitleText": "AI陪练选择角色",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/talk",
"style": {
"navigationBarTitleText": "AI陪练通话",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/sparring/result",
"style": {
"navigationBarTitleText": "AI陪练报告",
"disableSwipeBack": true,
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/analytics/study",
"style": {
"navigationBarTitleText": "学习分析",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/traRecord/index",
"style": {
"navigationBarTitleText": "陪练记录页",
"app-plus": {
"titleNView": false
}
}
}
],
"tabBar": {
"list": [
{
"text": "首页",
"pagePath": "pages/index/index"
},
{
"text": "学习",
"pagePath": "pages/course/index"
},
{
"text": "场景",
"pagePath": "pages/sparring/index"
},
{
"text": "我的",
"pagePath": "pages/me/index"
}
],
"color": "#666666",
"selectedColor": "#0066FF",
"fontSize": "16px",
"borderStyle": "white",
"borderColor": "#ffffff",
"height": "70px",
"spacing": "10px",
"midButton": {
"iconPath": "/static/images/icon/fawn-1.png",
"width": "86px",
"height": "86px",
"iconWidth": "90px"
}
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "AI智能培训",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom",
"app-plus": {
"titleView": false,
"bounce": "none",
"softinputNavBar": "none"
}
},
"uniIdRouter": {}
}
+628
View File
@@ -0,0 +1,628 @@
<template>
<view class="study-analytics max_page">
<view class="cont-body">
<view class="top-box">
<view class="top-title">
<view class="back-icon" @click="common.navigateBack()"></view>
</view>
</view>
<view class="analytics-box">
<view class="tabs-box">
<uv-tabs :list="tabList" @click="changeTab" :scrollable="true" active-style="font-weight:bold; color:#000;"
:current="currentTab" class="font_pf my_tabs"
:inactive-style="{ color: '#333333', fontSize: '28rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '28rpx', fontWeight: '600' }"></uv-tabs>
</view>
<!-- <view class="calendar-box" @click="openCalendar">统计周期</view>
<uv-calendar ref="calendarRef" mode="range" @confirm="calendarConfirm" minDate="2025-01-01" :maxDate="maxDate" monthNum="100" :allowSameDay="true"></uv-calendar> -->
<view class="date-input" v-show="activeTabVal === '04'">
<uni-datetime-picker class="custom-datepicker" ref="dateinputRef" type="daterange" v-model="daterange"
start="2025-01-01" :end="maxDate" rangeSeparator="至" @change="changeDate"></uni-datetime-picker>
</view>
<view class="analytics-items">
<view class="analytics-item">
<view class="analytics-item-title">
课程学习时长
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.crsStdyTmLen }}</text>
<text class="analytics-item-value-unit">h</text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
完成课程数
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.completeCrsCnt }}</text>
<text class="analytics-item-value-unit"></text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
课程完成率
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.crsCompleteRate }}</text>
<text class="analytics-item-value-unit">%</text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
完成任务数
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.completeTaskCnt }}</text>
<text class="analytics-item-value-unit"></text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
完成考试数
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.completedExamCnt }}</text>
<text class="analytics-item-value-unit"></text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
考试及格率
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.passExamRate }}</text>
<text class="analytics-item-value-unit">%</text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
问答次数
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.askCnt }}</text>
<text class="analytics-item-value-unit"></text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
陪练次数
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.partnerCnt }}</text>
<text class="analytics-item-value-unit"></text>
</view>
</view>
<view class="analytics-item">
<view class="analytics-item-title">
陪练通过率
</view>
<view class="analytics-item-value">
<text class="analytics-item-value-v">{{statisticsObj.partnerRate }}</text>
<text class="analytics-item-value-unit">%</text>
</view>
</view>
</view>
</view>
<view class="evaluation-box dens" v-if="loading">
<CommonLoading color="#06f" class="loading-box"/>
</view>
<view class="evaluation-box dens" v-if="!loading">
<view class="dens-item" v-for="(item, index) in dimesionList">
<view :class="['item-title', index%2===0?'study1-tip': 'study2-tip']">{{item.title}}</view>
<view :class="['item-cont ', index%2===0?'study1-bg': 'study2-bg']">{{item.content}}</view>
</view>
</view>
<view class="evaluation-box study" v-if="loading">
<CommonLoading color="#06f" class="loading-box"/>
</view>
<view class="evaluation-box study" v-if="!loading">
<view class="study-item" v-for="(item, index) in studyList">
<view :class="['item-title', index%2===0?'study1-tip': 'study2-tip']">{{item.title}}</view>
<view :class="['item-cont ', index%2===0?'study1-bg': 'study2-bg']">{{item.content}}</view>
</view>
</view>
<view class="evaluation-box study1" v-if="loading">
<CommonLoading color="#06f" class="loading-box"/>
</view>
<view class="evaluation-box study1" v-if="!loading">
<view class="study1-item">
<view class="item-cont study3-bg">{{suggestObj.content}}</view>
</view>
</view>
<view class="evaluation-box course" v-if="loading">
<CommonLoading color="#06f" class="loading-box"/>
</view>
<view class="evaluation-box course" v-if="!loading">
<CourseItem v-for="item in courseList" :itemInfo="item" @clickItem="toCourseDetail(item)" />
<view class="empty-box" v-if="courseList.length === 0">暂无数据</view>
</view>
</view>
</view>
</template>
<script setup>
import common from '@/common/common'
import {
queryStudyStatistics,
queryRecmdCrsInfo,
queryDimesionInfo,
queryHabitInfo,
querySuggestInfo,
} from '@/api/analytics.js'
import {
onMounted,
ref,
computed,
} from 'vue'
import CourseItem from '@/components/course-item/course-item.vue'
const tabList = ref([{
name: '本月',
value: '01'
},
{
name: '本年',
value: '02'
},
{
name: '累计',
value: '03'
},
{
name: '自定义',
value: '04'
}
])
const courseList = ref([])
const currentTab = ref(0)
const activeTabVal = ref('01')
const daterange = ref(['', ''])
const dateinputRef = ref()
const maxDate = computed(() => {
let _date = new Date()
let _year = _date.getFullYear()
let _month = _date.getMonth() + 1
let _day = new Date(_year, _month, 0).getDate()
return `${_year}-${_month < 10 ? '0' + _month: _month}-${_day < 10 ? '0' + _day: _day}`
})
const calendarRef = ref()
const statisticsObj = ref({
crsStdyTmLen: 0,
completeTaskCnt: 0,
completedExamCnt: 0,
passExamRate: 0,
partnerCnt: 0,
askCnt: 0,
completeCrsCnt: 0,
crsCompleteRate: 0,
partnerRate: 0
})
const dimesionList = ref([])
const studyList = ref([])
const suggestObj = ref({
content: ''
})
// const openCalendar = () => {
// calendarRef.value.open()
// }
// const calendarConfirm = (data) => {
// console.log('data:', data)
// currentTab.value = -1
// }
const changeDate = (date) => {
if (date.length > 0) {
if (!!date[0]) {
getInfoNow({
dateType: '04',
startDate: date[0],
endDate: date[1],
})
}
}
}
const loading = ref(false)
const getInfoNow = async (params) => {
loading.value = true
try {
let _res1 = await queryStudyStatistics(params)
statisticsObj.value = {..._res1.body}
let _res2 = await queryDimesionInfo(params)
dimesionList.value = [..._res2.body]
let _res3 = await queryHabitInfo(params)
studyList.value = [..._res3.body]
let _res4 = await querySuggestInfo(params)
if(_res4.body && _res4.body.length > 0){
suggestObj.value = {..._res4.body[0]}
}
} catch {
} finally{
loading.value = false
}
}
//
const toCourseDetail = item => {
common.navigateTo('/pages/courseDetail/index?crsNum=' + item.crsNum + '&crsId=' + item.crsId)
}
const changeTab = (info) => {
activeTabVal.value = info.value
if (info.value === '04') {
dateinputRef.value&&dateinputRef.value?.clear()
daterange.value = ['', '']
}else{
getInfoNow({
dateType: activeTabVal.value
})
}
}
const getCrsListNow = () => {
queryRecmdCrsInfo().then(res => {
console.log(res)
courseList.value = res.body || []
})
}
onMounted(() => {
getCrsListNow()
getInfoNow({
dateType: activeTabVal.value
})
})
</script>
<style lang="scss" scoped>
.my_tabs {
// 穿
:deep(.uv-tabs__wrapper__nav__line) {
background: linear-gradient(90deg, #06f 0%, rgb(0, 159, 255) 50%, rgb(255, 255, 255) 100%) !important;
}
:deep(.uv-tabs__wrapper__nav__item) {
padding: 0 30rpx !important;
}
}
.study-analytics {
position: relative;
background-color: #ddeafa;
.cont-body {
position: absolute;
width: 100vw;
height: 100vh;
padding: var(--status-bar-height) 20rpx 24rpx;
// padding: 280rpx 20rpx 24rpx;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
background-image: url(@/static/images/analytics/bg1.png);
background-repeat: no-repeat;
background-size: 100% auto;
background-position: top center;
background-attachment: local;
.top-box {
height: 280rpx;
width: 100%;
// position: fixed;
// float: left;
// margin-top: var(--status-bar-height);
.top-title {
height: 90rpx;
// background-color: red;
text-align: center;
font-size: 33rpx;
padding: 10rpx 0rpx 0;
line-height: 46rpx;
box-sizing: border-box;
font-weight: 500;
color: #000;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.back-icon {
position: absolute;
width: 40rpx;
height: 40rpx;
cursor: pointer;
left: 30rpx;
top: 33rpx;
transform: translateY(-50%);
}
.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);
}
}
}
.analytics-box {
min-height: 490rpx;
border-radius: 15rpx;
// overflow: hidden;
background: url(@/static/images/analytics/bg2.png) no-repeat;
background-size: cover;
background-position: center;
position: relative;
margin-bottom: 24rpx;
backdrop-filter: blur(10rpx);
.calendar-box {
position: absolute;
right: 25rpx;
top: 25rpx;
color: #06f;
padding-left: 40rpx;
background: url(@/static/images/analytics/date-icon.png) no-repeat;
background-size: 30rpx 30rpx;
background-position: left center;
font-size: 29rpx;
}
.tabs-box {
width: 600rpx;
}
.analytics-items {
padding-top: 20rpx;
padding-bottom: 20rpx;
width: 100%;
overflow: hidden;
.analytics-item {
padding-bottom: 12rpx;
box-sizing: border-box;
width: 33.3%;
float: left;
text-align: center;
height: 128rpx;
display: flex;
flex-direction: column;
justify-content: space-around;
&-title {
font-size: 25rpx;
color: #666;
}
&-value {
font-size: 54rpx;
font-weight: 600;
&-v {
font-family: DINAlternate, DINAlternate;
}
&-unit {
font-size: 28rpx;
margin-left: 8rpx;
}
}
}
}
}
.evaluation-box {
background-color: rgba(255, 255, 255, .8);
background-repeat: no-repeat;
background-size: auto 78rpx;
background-position: 26rpx 26rpx;
padding: 80rpx 26rpx 24rpx;
border-radius: 15rpx;
margin-bottom: 24rpx;
position: relative;
min-height: 200rpx;
.loading-box{
position: absolute;
top: 120rpx;
left: 50%;
transform: translateX(-50%);
}
&::before{
content: '';
display: block;
position: absolute;
width: 208rpx;
height: 76rpx;
position: absolute;
left: -7rpx;
top: -12rpx;
background-size: cover;
}
&.dens {
&::before{
background-image: url(@/static/images/analytics/weidu-1.png);
}
}
&.study {
&::before{
background-image: url(@/static/images/analytics/study1-1.png)
}
}
&.study1 {
&::before{
background-image: url(@/static/images/analytics/study-1.png);
}
}
&.course {
&::before{
background-image: url(@/static/images/analytics/course-1.png);
}
}
// .dens-item {
// padding: 23rpx 0;
// .item-title{
// line-height: 40rpx;
// position: relative;
// padding: 8rpx 46rpx 8rpx 26rpx;
// line-height: 42rpx;
// font-size: 29rpx;
// margin-bottom: 16rpx;
// &::before{
// content: '';
// display: block;
// background-color: #000;
// width: 12rpx;
// height: 12rpx;
// border-radius: 50%;
// position: absolute;
// left: 0rpx;
// top: 22rpx;
// }
// }
// .item-cont {
// font-size: 28rpx;
// line-height: 50rpx;
// }
// }
.study-item,
.study1-item,
.dens-item {
padding: 12rpx 0;
.item-title {
line-height: 40rpx;
position: relative;
padding: 8rpx 46rpx 8rpx 40rpx;
line-height: 42rpx;
font-size: 29rpx;
margin-bottom: 16rpx;
&::before {
content: '';
display: block;
// background-color: #000;
width: 28rpx;
height: 28rpx;
// border-radius: 50%;
position: absolute;
left: 0rpx;
top: 14rpx;
background-image: url(@/static/images/analytics/den-tips.png);
background-repeat: no-repeat;
background-size: 100% 100%;
}
&.study2-tip{
&::before {
background-image: url(@/static/images/analytics/den-tips2.png);
}
}
&.bg-tm {
padding-left: 54rpx;
background-image: url(@/static/images/analytics/time-icon.png);
background-repeat: no-repeat;
background-size: 38rpx 40rpx;
background-position: left center;
&::before {
display: none;
}
}
&.bg-know {
padding-left: 54rpx;
background-image: url(@/static/images/analytics/knowledge.png);
background-repeat: no-repeat;
background-size: 38rpx 40rpx;
background-position: left center;
&::before {
display: none;
}
}
}
.item-cont {
font-size: 28rpx;
line-height: 50rpx;
&.study1-bg {
background-color: #e4efff;
border-radius: 15rpx;
padding: 12rpx 24rpx;
// width: calc(100% - 54rpx);
// margin-left: 54rpx;
}
&.study2-bg {
background-color: #f6f0df;
border-radius: 15rpx;
padding: 12rpx 24rpx;
// width: calc(100% - 54rpx);
// margin-left: 54rpx;
}
&.study3-bg {
background-color: #e4efff;
border-radius: 15rpx;
padding: 12rpx 24rpx;
// width: calc(100% - 54rpx);
// margin-left: 54rpx;
}
}
}
.empty-box {
text-align: center;
font-size: 25rpx;
color: #ccc;
}
}
}
}
.date-input {
.custom-datepicker {
width: 500rpx;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
right: 0;
width: 10rpx;
height: 10rpx;
border-top: 3rpx solid #999;
border-left: 3rpx solid #999;
transform: rotate(135deg) translateY(50%);
}
::v-deep .uni-date-x {
background-color: rgba(255, 255, 255, 0);
width: 500rpx;
}
::v-deep .uni-date-x--border {
border: 0;
width: 500rpx;
}
::v-deep .range-separator {
font-size: 13px;
}
}
}
</style>
+598
View File
@@ -0,0 +1,598 @@
<template>
<view class="exam-ranking">
<!-- <image src="@/static/images/charts/bg-top.png" mode="aspectFit" class="bg-box"></image> -->
<view class="bg-box"></view>
<view class="ranking-box">
<view class="nav-div">
<view class="back_div" @click="backRouter()">
<view class="back-icon"></view>
</view>
</view>
<view class="title-view">
<!-- <view class="title-info">考试排行榜</view> -->
<image class="title-image" src="@/static/images/charts/text.png" mode="aspectFit"></image>
<!-- <view class="title-desc">吾日三省吾身传不习乎</view> -->
<image class="cup-image" src="@/static/images/charts/cup.png" mode="aspectFit"></image>
</view>
<view class="ranking-body">
<view class="tabs-view">
<uv-tabs :list="tabList" @click="changeTab" :scrollable="false" :current="chooseTyp" active-style="font-weight:bold; color:#000;"
class="font_pf my_tabs" :inactive-style="{ color: '#333333', fontSize: '30rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '30rpx', fontWeight: '600' }"></uv-tabs>
<uv-tabs :list="tabSecList" @click="changeTabSec" :scrollable="false" :current="chooseTypSec"
active-style="font-weight:bold; color:#000;" class="font_pf my_tabs2"
:inactive-style="{ color: '#333333', fontSize: '24rpx', fontWeight: '500' }"
:activeStyle="{ color: '#0066FF', fontSize: '24rpx', fontWeight: '500' }" :lineColor="`#06f`"></uv-tabs>
<!-- :lineColor="`url(${LineBg}) 2% 2%`" -->
<uv-scroll-list class="tags-view" :indicator="false">
<uv-tags :text="item.label" v-for="item in tagsList" :key="item.value" size="medium" :bgColor="'#f6f8fa'"
:borderColor="activeTag === item.value ? '#06f' : '#f6f8fa'"
:color="activeTag === item.value ? '#06f' : '#333'" @click="chooseTag(item)"
:class="activeTag === item.value ? 'tags-item active-tags-item' : 'tags-item'"></uv-tags>
</uv-scroll-list>
</view>
<view class="list-header list-item">
<view class="rank-column">排名</view>
<view class="user-column">用户名/所属机构</view>
<view class="score-column">{{activeNameSec}}</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.grade}}</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.grade}}</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { defaultAvatar } from "@/enum.js"
//
import { getUserInfo } from '@/common/common';
import { leaderBoardApi } from '@/api/charts.js'
import {
reactive,
ref,
computed,
nextTick,
onMounted,
watch
} from 'vue';
import common from '@/common/common';
export default {
data() {
return {
crsId: '',
papersId : '',
defaultAvatarSrc: defaultAvatar,
chooseTyp: 0,
chooseTypSec: 0,
tagsList:[{
label: '本年',
value: 'YEAR',
},{
label: '本月',
value: 'MONTH',
},{
label: '本周',
value: 'WEEK',
}],
activeTag: 'YEAR',
rankList: []
}
},
// onLoad(params){
// this.getList()
// },
onShow(){
this.getList()
},
computed: {
tabList() {
return [{
name: '课程排行榜',
value: 0,
key: '01'
},{
name: 'AI陪练排行榜',
value: 1,
key: '02'
},{
name: '考试排行榜',
value: 2,
key: '03'
}]
},
tabSecList() {
let _arr = []
if(this.chooseTyp === 0){
_arr = [
{
name: '学习时长',
value: 0,
},
{
name: '练习完成次数',
value: 1
},
{
name: '考试通过次数',
value: 2
},
{
name: '完成课程数',
value: 3
}
]
} else if(this.chooseTyp === 1){
_arr = [
{
name: '练习通过次数',
value: 0
},
{
name: '考试通过次数',
value: 1
},
{
name: '完成AI陪练数',
value: 2
}
]
} else if(this.chooseTyp === 2){
_arr = [
{
name: '考试通过次数',
value: 0
},
{
name: '完成考试数',
value: 1
}
]
}
return _arr
},
activeNameSec(){
if(this.tabSecList.length >= this.chooseTypSec){
return this.tabSecList[this.chooseTypSec].name|| '--'
}else{
return '--'
}
},
userRankObj(){
if(this.rankList.length >= this.chooseTypSec){
return this.rankList[this.chooseTypSec] || {}
}else{
return {}
}
},
userList(){
return this.userRankObj.rankingListVos || []
},
},
methods: {
getList(){
leaderBoardApi({
leaderBoardType: this.tabList[this.chooseTyp].key,
timeType: this.activeTag
}).then(res=>{
console.log(res)
this.rankList = res.body||[]
})
},
changeTab(item){
if(this.chooseTyp === item.value) return
this.chooseTyp = item.value
this.chooseTypSec = 0
this.getList()
},
changeTabSec(item){
this.chooseTypSec = item.value
},
chooseTag (item){
this.activeTag = item.value
this.getList()
},
backRouter(){
common.navigateBack()
}
}
}
</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: 400rpx;
background: linear-gradient(0deg , #F8F8FA 0%, #6DA7FF 100%);
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-top: 20rpx;
.cup-image {
position: absolute;
right: 46rpx;
bottom: -33rpx;
width: 208rpx;
height: 208rpx;
z-index: 0;
}
.title-desc {
color: #666;
line-height: 34rpx;
font-size: 25rpx;
}
.title-image {
width: 230rpx;
height: 111rpx;
position: absolute;
bottom: 20rpx;
left: 46rpx;
}
}
.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;
position: relative;
z-index: 2;
.scroll-items-view{
height: calc(100% - 308rpx);
}
}
.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;
}
}
&.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: 375rpx;
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: 150rpx;
text-align: right;
float: left;
font-weight: 600;
line-height: 108rpx;
color: #333;
font-size: 31rpx;
white-space: nowrap;
}
}
.ranking-self {
height: 170rpx;
overflow: hidden;
background-color: #f5f9ff;
padding-top: 4rpx;
}
}
.tags-view {
height: 84rpx;
display: block;
padding: 15rpx 20rpx 15rpx 20rpx;
box-sizing: border-box;
white-space: nowrap;
// border-bottom: 2rpx solid #eee;
.tags-item {
margin-right: 30rpx;
display: inline-block;
&.active-tags-item {
position: relative;
overflow: hidden;
border-radius: 4rpx;
overflow: hidden;
&::before {
content: '';
display: block;
width: 40rpx;
height: 40rpx;
position: absolute;
right: -20rpx;
bottom: -20rpx;
transform: rotateZ(-45deg);
background-color: #06f;
overflow: hidden;
z-index: 10;
}
&::after {
content: '';
display: block;
width: 8rpx;
height: 4rpx;
position: absolute;
right: 3rpx;
bottom: 8rpx;
transform: rotateZ(-45deg);
border-left: 1px solid #fff;
border-bottom: 1px solid #fff;
z-index: 11;
}
}
::v-deep .uv-tags {
padding: 0 20rpx;
.uv-tags__text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
}
}
}
.my_tabs {
// 穿
:deep(.uv-tabs__wrapper__nav__line) {
// height: 30rpx !important;
// left: 60rpx;
// background-size: 24rpx !important;
// background-repeat: no-repeat !important;
background: linear-gradient(90deg, #06f 0%, rgb(0, 159, 255) 50%, rgb(255, 255, 255) 100%) !important;
}
:deep(.uv-tabs__wrapper__nav__item) {
padding: 0 30rpx !important;
}
}
.my_tabs2 {
// 穿
:deep(.uv-tabs__wrapper__nav__line) {
background-size: 24rpx !important;
background: linear-gradient(90deg, #06f 0%, rgb(0, 159, 255) 50%, rgb(255, 255, 255) 100%) !important;
}
:deep(.uv-tabs__wrapper__nav__item) {
padding: 0 20rpx !important;
}
}
</style>
+845
View File
@@ -0,0 +1,845 @@
<template>
<view class="max_page detail-main">
<nav-bar :is_seat="false"></nav-bar>
<view class="fixed_search_div">
<view class="back_div" @click="common.navigateBack()">
<view class="back-icon"></view>
</view>
<view class="title font_pf">排行榜</view>
</view>
<view class="detail-body">
<view class="body-tabs">
<view class="tabs-sty">
<view :class="tabAct == index ?'active-tabs-item-sty':'tabs-item-sty'"
v-for="(item,index) in tabList" @click="tabChange(index,item)">{{item.name}}</view>
</view>
</view>
<view class="date-body">
<view class="date-sty">
<view :class="itemIndex == index ?'active-date-item-sty':'date-item-sty'"
v-for="(item,index) in dateTabList" @click="itemFun(index,item)">{{item.name}}</view>
</view>
</view>
</view>
<view class="rank-style">
<view class="rank-top3">
<view class="rank-content rank-content-backgound-color1" v-if="chartsList.length > 1">
<image src="/static/images/charts/02.svg" class="crown1"></image>
<image v-if="chartsList[1].imgAddr" :src="chartsList[1].imgAddr" class="top-avator"></image>
<image v-else src="/static/images/charts/avator.png" class="top-avator"></image>
<view class="content1">top·2</view>
<view class="content2">{{chartsList[1].userName}}</view>
<view class="content3">{{chartsList[1].score}}</view>
<view class="content4">{{chartsList[1].dept}}</view>
</view>
<view class="rank-content rank-content-backgound-color2" v-if="chartsList.length > 0">
<image src="/static/images/charts/01.svg" class="crown2"></image>
<image v-if="chartsList[0].imgAddr" :src="chartsList[0].imgAddr" class="top-avator"></image>
<image v-else src="/static/images/charts/avator.png" class="top-avator"></image>
<view class="content11">top·1</view>
<view class="content2">{{chartsList[0].userName}}</view>
<view class="content33">{{chartsList[0].score}}</view>
<view class="content44">{{chartsList[0].dept}}</view>
</view>
<view class="rank-content rank-content-backgound-color3" v-if="chartsList.length > 2">
<image src="/static/images/charts/03.svg" class="crown3"></image>
<image v-if="chartsList[2].imgAddr" :src="chartsList[2].imgAddr" class="top-avator"></image>
<image v-else src="/static/images/charts/avator.png" class="top-avator"></image>
<view class="content111">top·3</view>
<view class="content2">{{chartsList[2].userName}}</view>
<view class="content333">{{chartsList[2].score}}</view>
<view class="content444">{{chartsList[2].dept}}</view>
</view>
</view>
<!-- table形式排名 -->
<!-- <view class="rank-other">
<uni-table ref="table">
<uni-tr>
<uni-th align="center" width="100">排名</uni-th>
<uni-th align="left" width="180">用户名/所属机构</uni-th>
<uni-th align="center" >{{tableLastName}}</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in newChartsList" :key="index">
<uni-td :class="item.name == '江月心'?'active-rank-sty':'rank-sty'" align="center">{{ item.rank }}</uni-td>
<uni-td align="center">
<image src="/static/images/charts/avator.png" class="avator"></image>
<view style="float: left;margin-left: 18rpx;">
<view :class="item.name == '江月心'?'active-name':'name'">{{ item.name }}</view>
<view class="belong">{{ item.belong }}</view>
</view>
</uni-td>
<uni-td class="score-sty" align="center">{{ item.score }}</uni-td>
</uni-tr>
</uni-table>
<view style="width: 100%;height: 200rpx;"></view>
</view>
<view class="rank-mine">
<view class="rank-mine-content">
<view style="font-size: 26rpx;font-weight: 400;color: #999999;margin-left: 10rpx;">我的排名</view>
<view style="width: 100%;">
<view style="width: 100rpx;font-size: 40rpx;color: #0066FF;font-style: italic;;font-weight: 600;float: left;margin-top:30rpx;margin-left: 18rpx;">44</view>
<view style="width: 200rpx;float: left;margin-left: 46rpx;margin-top: 12rpx;width: 370rpx;">
<image src="/static/images/charts/avator.png" class="my-avator"></image>
<view style="font-size: 30rpx;font-weight: 600;color:#000000;">张伟</view>
<view style="font-size: 26rpx;font-weight: 400;color: #999999;margin-top:8rpx;">吉林银行大连分行</view>
</view>
<view style="width: 170rpx;font-size: 32rpx;text-align: center;color: #333333;font-weight: 600;float: left;margin-top: 32rpx;">91</view>
</view>
</view>
</view> -->
<!-- list形式排名 -->
<uni-list class="rank-other">
<uni-list-item>
<!-- 自定义 header -->
<template v-slot:header>
<text class="rank-other-rank-sty rank-uni-sty">排名</text>
</template>
<!-- 自定义 body -->
<template v-slot:body>
<text class="rank-other-user-sty rank-uni-sty">用户名/所属机构</text>
</template>
<!-- 自定义 footer-->
<template v-slot:footer>
<text class="rank-other-last-sty rank-uni-sty">{{tableLastName}}</text>
</template>
</uni-list-item>
<uni-list-item v-if="chartsList.length < 4">
<template v-slot:header>
<text style="width:100%;text-align: center;color: #999999;">暂无数据</text>
</template>
</uni-list-item>
<uni-list-item v-for="(item, index) in newChartsList" :key="index" v-else>
<!-- 自定义 header -->
<template v-slot:header>
<text style="width: 20%;text-align: center;margin-top:15rpx"
:class="item.userId == userId?'active-rank-sty':'rank-sty'">{{item.position}}</text>
</template>
<!-- 自定义 body -->
<template v-slot:body>
<view style="width: 55%;text-align: center;margin-left: 10rpx;">
<image v-if="item.imgAddr" :src="item.imgAddr" class="avator"></image>
<image v-else src="/static/images/charts/avator.png" class="avator"></image>
<view style="float: left;margin-left: 18rpx;">
<view :class="item.userId == userId?'active-name':'name'">{{ item.userName }}</view>
<view style="width: 250rpx;text-align: left;margin-top: 10rpx;" class="belong">{{ item.dept }}</view>
</view>
</view>
</template>
<!-- 自定义 footer-->
<template v-slot:footer>
<text style="text-align: center;" class="score-sty score-sty-footer">{{ item.score }}</text>
</template>
</uni-list-item>
<view style="width: 100%;height: 200rpx;"></view>
<uni-list-item class="rank-mine" v-if="!myselfObj.userName">
<!-- 自定义 header -->
<template v-slot:header>
<text class="rank-sty my-rank-title">我的排名</text>
<view style="width: 100%;text-align: center;color: #999999;font-size: 28rpx;">暂无排名</view>
</template>
</uni-list-item>
<uni-list-item class="rank-mine" v-else>
<!-- 自定义 header -->
<template v-slot:header>
<text class="rank-sty my-rank-title">我的排名</text>
<view class="my-rank-content">{{myselfObj.position}}</view>
</template>
<!-- 自定义 body -->
<template v-slot:body>
<view style="width: 65%;text-align: center;margin-top: 30rpx;">
<image v-if="myselfObj.imgAddr" :src="myselfObj.imgAddr" class="avator" style="margin-top: 4rpx;"></image>
<image v-else src="/static/images/charts/avator.png" class="avator" style="margin-top: 4rpx;"></image>
<view style="float: left;margin-left: 18rpx;">
<view class="my-rank-name">{{myselfObj.userName}}</view>
<view style="width: 260rpx;text-align: left;margin-top: 5rpx;" class="my-rank-belong">{{myselfObj.dept}}</view>
</view>
</view>
</template>
<!-- 自定义 footer-->
<template v-slot:footer>
<text class="my-rank-score">{{myselfObj.score}}</text>
</template>
</uni-list-item>
</uni-list>
</view>
</view>
</template>
<script setup>
import {
getLeaderBoardStdyTmApi,
getLeaderBoardPractCntApi,
getLeaderBoardExamCntApi,
getLeaderBoardCrsCntApi
} from "@/api/leaderBoard.js"
//
import { getUserInfo } from '@/common/common';
import {
reactive,
ref,
computed,
nextTick,
onMounted,
watch
} from 'vue';
import common from '@/common/common';
const userId = ref("");
const itemIndex = ref(0);
const learnTimeList = ref({})
const practCntList = ref({})
const examCntList = ref({})
const crsCntList = ref({})
const myselfObj = ref({})
const tableLastName = ref("学习时长(h)");
const taskRef = ref(null);
const tabAct = ref(0);
const tabDateAct = ref(0);
const allChartsList = ref({});
const newChartsList = ref([]);
const tabList = ref([{
name: '学习时长'
}, {
name: '练习次数'
}, {
name: '考试次数'
}, {
name: '通关课程数'
}]);
const dateTabList = ref([{
name: '本年',
value: 'annual'
}, {
name: '本月',
value: 'month'
}, {
name: '本周',
value: 'week'
}]);
const chartsList = ref([]);
const tabChange = (index, item) => {
itemIndex.value = 0;
tabAct.value = index;
tableLastName.value = item.name;
if (tableLastName.value.includes('学习时长') ) {
tableLastName.value = '学习时长(h)'
}
if (tableLastName.value.includes('学习时长')) {
allChartsList.value = learnTimeList.value;
chartsList.value = learnTimeList.value.annual?learnTimeList.value.annual.list:[];
myselfObj.value = allChartsList.value.annual?allChartsList.value.annual.self:{};
myselfObj.value.score = allChartsList.value.annual?allChartsList.value.annual.self.annlStdyTmLen:"";
chartsList.value.forEach(item => item.score = item.annlStdyTmLen)
} else if (tableLastName.value == '练习次数') {
allChartsList.value = practCntList.value;
chartsList.value = practCntList.value.annual?practCntList.value.annual.list:[];
myselfObj.value = allChartsList.value.annual?allChartsList.value.annual.self:{};
myselfObj.value.score = allChartsList.value.annual?allChartsList.value.annual.self.annlPractCnt:"";
chartsList.value.forEach(item => item.score = item.annlPractCnt);
} else if (tableLastName.value == '考试次数') {
allChartsList.value = examCntList.value;
chartsList.value = examCntList.value.annual?examCntList.value.annual.list:[];
myselfObj.value = allChartsList.value.annual?allChartsList.value.annual.self:{};
myselfObj.value.score = allChartsList.value.annual?allChartsList.value.annual.self.annlExamCnt:"";
chartsList.value.forEach(item => item.score = item.annlExamCnt);
} else if (tableLastName.value == '通关课程数') {
allChartsList.value = crsCntList.value;
chartsList.value = crsCntList.value.annual?crsCntList.value.annual.list:[];
myselfObj.value = allChartsList.value.annual?allChartsList.value.annual.self:{};
myselfObj.value.score = allChartsList.value.annual?allChartsList.value.annual.self.annlCrsCnt:"";
chartsList.value.forEach(item => item.score = item.annlCrsCnt);
}
if (chartsList.value.length > 3) {
newChartsList.value = chartsList.value.slice(3).concat();
} else {
newChartsList.value = chartsList.value;
}
console.log(tableLastName.value, 'tablelastname')
};
const itemFun = (index, item) => {
itemIndex.value = index;
if (item.value == 'annual') {
chartsList.value = allChartsList.value.annual ? allChartsList.value.annual.list : [];
myselfObj.value = allChartsList.value.annual ? allChartsList.value.annual.self : {};
if (tableLastName.value.includes('学习时长')) {
chartsList.value.forEach(item => item.score = item.annlStdyTmLen);
myselfObj.value.score = myselfObj.value.annlStdyTmLen;
} else if (tableLastName.value == '练习次数') {
chartsList.value.forEach(item => item.score = item.annlPractCnt);
myselfObj.value.score = myselfObj.value.annlPractCnt;
} else if (tableLastName.value == '考试次数') {
chartsList.value.forEach(item => item.score = item.annlExamCnt);
myselfObj.value.score = myselfObj.value.annlExamCnt;
} else if (tableLastName.value == '通关课程数') {
chartsList.value.forEach(item => item.score = item.annlCrsCnt);
myselfObj.value.score = myselfObj.value.annlCrsCnt;
}
} else if (item.value == 'month') {
console.log('month')
chartsList.value = allChartsList.value.month ? allChartsList.value.month.list : [];
myselfObj.value = allChartsList.value.month ? allChartsList.value.month.self : {};
console.log(chartsList.value,'chartsList.value')
console.log(myselfObj.value,'myselfObj.value')
if (tableLastName.value.includes('学习时长')) {
chartsList.value.forEach(item => item.score = item.thisMonthStdyTmLen);
myselfObj.value.score = myselfObj.value.thisMonthStdyTmLen;
console.log(chartsList.value,'chartsList.valueMonth')
console.log(myselfObj.value,'myselfObj.valueMonth')
} else if (tableLastName.value == '练习次数') {
chartsList.value.forEach(item => item.score = item.thisMonthPractCnt);
myselfObj.value.score = myselfObj.value.thisMonthPractCnt;
} else if (tableLastName.value == '考试次数') {
chartsList.value.forEach(item => item.score = item.thisMonthExamCnt);
myselfObj.value.score = myselfObj.value.thisMonthExamCnt;
} else if (tableLastName.value == '通关课程数') {
chartsList.value.forEach(item => item.score = item.thisMonthCrsCnt);
myselfObj.value.score = myselfObj.value.thisMonthCrsCnt;
}
} else if (item.value == 'week') {
chartsList.value = allChartsList.value.week ? allChartsList.value.week.list : [];
myselfObj.value = allChartsList.value.week ? allChartsList.value.week.self : {};
if (tableLastName.value.includes('学习时长')) {
chartsList.value.forEach(item => item.score = item.thisWeekStdyTmLen);
myselfObj.value.score = myselfObj.value.thisWeekStdyTmLen;
console.log(chartsList.value,'chartsList.valueWeek')
console.log(myselfObj.value,'myselfObj.valueWeek')
} else if (tableLastName.value == '练习次数') {
chartsList.value.forEach(item => item.score = item.thisWeekPractCnt);
myselfObj.value.score = myselfObj.value.thisWeekPractCnt;
} else if (tableLastName.value == '考试次数') {
chartsList.value.forEach(item => item.score = item.thisWeekExamCnt);
myselfObj.value.score = myselfObj.value.thisWeekExamCnt;
} else if (tableLastName.value == '通关课程数') {
chartsList.value.forEach(item => item.score = item.thisWeekCrsCnt);
myselfObj.value.score = myselfObj.value.thisWeekCrsCnt;
}
}
if (chartsList.value.length > 3) {
newChartsList.value = chartsList.value.slice(3).concat();
} else {
newChartsList.value = chartsList.value;
}
}
const userInfo = computed(() => getUserInfo())
userId.value = userInfo.value.userId;
console.log(userId.value,'userId')
onMounted(() => {
getLeaderBoardStdyTmApi().then(res => {
console.log(res, 'getLeaderBoardStdyTmApi')
learnTimeList.value = res.body || {};
allChartsList.value = learnTimeList.value;
chartsList.value = allChartsList.value.annual?allChartsList.value.annual.list:[];
myselfObj.value = allChartsList.value.annual?allChartsList.value.annual.self:{};
console.log(chartsList.value,'chartsList.value')
console.log(myselfObj.value,myselfObj.value,'myselfObj.value')
chartsList.value.forEach(item => item.score = item.annlStdyTmLen);
myselfObj.value.score = myselfObj.value.annlStdyTmLen;
if (chartsList.value.length > 3) {
newChartsList.value = chartsList.value.slice(3).concat();
} else {
newChartsList.value = chartsList.value;
}
console.log(newChartsList.value, 'newChartsList')
})
getLeaderBoardPractCntApi().then(res => {
console.log(res, 'getLeaderBoardPractCntApi')
practCntList.value = res.body || {};
console.log(practCntList.value, 'practCntList')
})
getLeaderBoardExamCntApi().then(res => {
console.log(res, 'getLeaderBoardExamCntApi')
examCntList.value = res.body || {};
console.log(examCntList.value, 'examCntList')
})
getLeaderBoardCrsCntApi().then(res => {
console.log(res, 'getLeaderBoardCrsCntApi')
crsCntList.value = res.body || {};
console.log(crsCntList.value, 'crsCntList')
})
})
</script>
<style lang="scss" scoped>
$bg-margin-left: 50rpx;
.my_tabs {
// 穿
:deep(.uv-tabs__wrapper__nav__line) {
height: 26rpx !important;
left: 70rpx;
background-size: 28rpx !important;
background-repeat: no-repeat !important;
}
}
.detail-body {
background: #ffffff;
border-radius: 15rpx;
z-index: 101;
overflow: hidden;
.body-tab {
height: 88rpx;
border-bottom: 1rpx solid #BBBBBB;
}
.body-tabs {
width: 100%;
height: 99rpx;
line-height: 96rpx;
border-bottom: 1rpx solid #BBBBBB;
}
}
.detail-main {
background-color: #F6F8FF;
overflow: hidden;
}
.title {
color: #000000;
font-size: 34rpx;
height: 34rpx;
font-weight: 600;
line-height: 34rpx;
}
.fixed_search_div {
padding: calc(20rpx + var(--status-bar-height)) 15rpx 24rpx 30rpx;
display: flex;
align-items: center;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
.back_div {
position: absolute;
left: 20rpx;
padding: 10rpx;
.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 #000000;
border-left: 4rpx solid #000000;
transform: rotate(-45deg);
}
}
}
.date-body {
height: 114rpx;
line-height: 114rpx;
}
.date-sty {
margin-left: 32rpx;
height: 60rpx;
line-height: 60rpx;
display: inline-block;
.date-item-sty {
display: inline-block;
width: 56rpx;
height: 60rpx;
line-height: 60rpx;
color: #040404;
text-align: center;
font-size: 28rpx;
margin-left: 42rpx;
margin-right: 42rpx;
}
.active-date-item-sty {
display: inline-block;
width: 140rpx;
height: 60rpx;
color: #FFFFFF;
text-align: center;
font-size: 28rpx;
border-radius: 38rpx;
background-color: #4080FF;
}
}
//
// .tabs-sty{
// font-size: 28rpx;
// font-weight: 600;
// display: flex;
// flex-direction: row;
// align-items: center;
// justify-content: center;
// position: relative;
// }
// .tabs-item-sty{
// width: 25%;
// text-align: center;
// height: 100rpx;
// justify-content: center;
// }
// .active-tabs-item-sty{
// color: #2572F0;
// width: 25%;
// text-align: center;
// background-color: #E6F7FF;
// border-bottom: 2px solid #2572F0;
// }
//
.tabs-sty {
font-size: 28rpx;
font-weight: 600;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
}
.tabs-item-sty {
color: #999999;
font-weight: 500;
width: 25%;
text-align: center;
height: 100rpx;
}
.active-tabs-item-sty {
position: relative;
font-size: 30rpx;
color: #333333;
font-weight: 600;
width: 25%;
text-align: center;
}
.active-tabs-item-sty::after {
content: "";
position: absolute;
width: 90rpx;
height: 1rpx;
border-bottom: 4rpx solid #4080FF;
top: 91rpx;
left: 25%;
}
.uni-table th {
border: none;
}
.uni-table td {
// border: none;
}
.rank-style {
width: 100%;
.rank-top3 {
background-color: #FFFFFF;
padding: 0 32rpx 36rpx;
display: flex;
justify-content: space-between;
.rank-content {
text-align: center;
border-radius: 32rpx;
padding-top: 24rpx;
position: relative;
.crown1 {
width: 32rpx;
height: 24rpx;
position: absolute;
top: 10rpx;
left: 130rpx;
}
.crown2 {
width: 32rpx;
height: 24rpx;
position: absolute;
top: 10rpx;
left: 140rpx;
}
.crown3 {
width: 32rpx;
height: 24rpx;
position: absolute;
top: 16rpx;
left: 130rpx;
}
.top-avator {
width: 158rpx;
height: 158rpx;
}
.content1 {
font-size: 28rpx;
color: #737FA6;
font-weight: 500;
}
.content11 {
font-size: 28rpx;
color: #AFA455;
font-weight: 500;
}
.content111 {
font-size: 28rpx;
color: #B07653;
font-weight: 500;
}
.content2 {
font-size: 32rpx;
color: #000000;
font-weight: 700;
}
.content3 {
font-size: 28rpx;
color: #436197;
font-weight: 600;
}
.content33 {
font-size: 28rpx;
color: #986C1A;
font-weight: 600;
}
.content333 {
font-size: 28rpx;
color: #8A4815;
font-weight: 600;
}
.content4 {
font-size: 24rpx;
color: #6682B7;
font-weight: 400;
width: 158rpx;
margin: 0 auto;
}
.content44 {
font-size: 24rpx;
color: #986C1A;
font-weight: 400;
width: 158rpx;
margin: 0 auto;
}
.content444 {
font-size: 24rpx;
color: #986C1A;
font-weight: 400;
width: 158rpx;
margin: 0 auto;
}
}
.rank-content-backgound-color1 {
margin-top: 32rpx;
width: 206rpx;
// height: 418rpx;
padding-bottom: 20rpx;
background-color: #E8F3FF;
padding-bottom: 10rpx;
}
.rank-content-backgound-color2 {
width: 220rpx;
// height: 432rpx;
padding-bottom: 20rpx;
background: linear-gradient(180deg, #FEEFB7 0%, #FADB66 100%);
}
.rank-content-backgound-color3 {
margin-top: 42rpx;
width: 206rpx;
// height: 418rpx;
padding-bottom: 20rpx;
background: linear-gradient(180deg, #FCDAB7 0%, #F9B16C 100%);
}
}
}
.rank-other {
width: 100%;
:deep(.uni-table) {
min-width: 0 !important;
}
:deep(.uni-list--border-top) {
display: none;
}
.rank-uni-sty {
font-weight: 400;
font-size: 26rpx;
color: #999999;
}
.rank-other-rank-sty {
width: 20%;
text-align: center;
}
.rank-other-user-sty {
width: 50%;
}
.rank-other-last-sty {
width: 30%;
text-align: center;
}
.rank-sty {
font-size: 40rpx;
font-weight: 600;
color: #333333;
}
.active-rank-sty {
font-size: 40rpx;
font-weight: 600;
color: #0066FF;
}
.score-sty {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.score-sty-footer {
width: 30%;
text-align: center;
margin-top: 15rpx
}
.avator {
width: 80rpx;
height: 80rpx;
float: left;
}
.name {
font-size: 30rpx;
font-weight: 600;
text-align: left;
color: #000000;
}
.active-name {
font-size: 30rpx;
font-weight: 600;
text-align: left;
color: #0066FF;
}
.belong {
font-size: 26rpx;
text-align: left;
font-weight: 400;
color: #999999;
}
:deep(.uni-table-th-content) {
font-weight: 400;
font-size: 26rpx;
color: #999999;
}
}
.rank-mine {
width: 100%;
height: 176rpx;
background-color: #f5f9ff !important;
box-shadow: 0rpx 1rpx 0rpx 0rpx #EFEFEF;
position: fixed;
bottom: 0;
.rank-mine-content {
width: 100%;
margin-left: 46rpx;
margin-top: 14rpx;
.my-avator {
width: 80rpx;
height: 80rpx;
float: left;
margin-right: 18rpx;
}
}
:deep(.uni-list-item__container) {
align-items: center;
height: 100%;
}
.my-rank-title {
position: absolute;
width: 20%;
top: 16rpx;
left: 32rpx;
text-align: center;
font-size: 26rpx;
font-weight: 400;
color: #999999;
}
.my-rank-content {
width: 20%;
text-align: center;
font-size: 40rpx;
color: #0066FF;
font-style: italic;
font-weight: 600;
margin-top: 20rpx
}
.my-rank-name {
font-size: 30rpx;
font-weight: 600;
color: #000000;
text-align: left;
}
.my-rank-belong {
font-size: 26rpx;
font-weight: 400;
color: #999999;
}
.my-rank-score {
width: 30%;
text-align: center;
font-size: 32rpx;
text-align: center;
color: #333333;
font-weight: 600;
margin-top: 15rpx;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More