‘add第一次提交’

This commit is contained in:
shawko 2026-05-09 10:34:51 +08:00
commit a969b6482b
666 changed files with 130383 additions and 0 deletions

7
.env Normal file
View File

@ -0,0 +1,7 @@
VITE_WS_URL = wss://ai.xuexiaole.com/ws/device
VITE_OSS_URL = https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com
VITE_CDN_URL = https://dmgcdn-1313840333.cos.ap-guangzhou.myqcloud.com
VITE_SERVER_URL = https://ai.xuexiaole.com/api/main
VITE_XXL_VERSION = 2.13.2
VITE_INIT_VCONSOLE = 0
VITE_TEACHER_ADMIN_URL = https://edu.xuexiaole.com

9
.env.test Normal file
View File

@ -0,0 +1,9 @@
VITE_WS_URL = ws://43.136.52.196:27001/device
# VITE_WS_URL = ws://ai.xuexiaole.com/ws/device
VITE_OSS_URL = https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com
VITE_CDN_URL = https://dmgcdn-1313840333.cos.ap-guangzhou.myqcloud.com
VITE_SERVER_URL = http://127.0.0.1:9053/api/main
# VITE_SERVER_URL = http://test.web.xuexiaole.com/api/main
VITE_XXL_VERSION = 2.13.2
VITE_INIT_VCONSOLE = 0
VITE_TEACHER_ADMIN_URL = https://test.edu.xuexiaole.com

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
build/
!.prettierrc.js

68
.eslintrc.cjs Normal file
View File

@ -0,0 +1,68 @@
module.exports = {
extends: [
'alloy',
'alloy/vue',
'alloy/typescript',
'plugin:@typescript-eslint/recommended',
'./.auto-import.json',
],
parser: 'vue-eslint-parser',
parserOptions: {
parser: {
js: '@babel/eslint-parser',
jsx: '@babel/eslint-parser',
ts: '@typescript-eslint/parser',
tsx: '@typescript-eslint/parser',
},
},
rules: {
// 'vue/html-indent': ['error', 4],
// 'vue/max-attributes-per-line': 'off',
// 'vue/html-self-closing': 'off',
// no-duplicate-attr-inheritance 禁止重复属性继承
'@typescript-eslint/consistent-type-imports': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/method-signature-style': 'off',
'no-return-assign': 'off',
'guard-for-in': 'off',
'vue/custom-event-name-casing': 'off',
'vue/no-duplicate-attributes': 'off',
'no-param-reassign': 'off',
'no-inner-declarations': 'off',
'vue/no-setup-props-destructure': 'off',
'vue/no-duplicate-attr-inheritance': 'off',
'vue/prefer-true-attribute-shorthand': 'off',
'no-eq-null': 'off', // 允许 == 用于 null
'vue/v-on-event-hyphenation': 'off', // 关闭 vue 中 @ 使用短横线命名
'vue/require-toggle-inside-transition': 'off',
'no-duplicate-imports': 'off', // 使用ts-eslint的重复导入规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'max-params': ['error', { max: 4 }],
// 圈复杂度 每个函数的最高圈复杂度
complexity: [
'error',
{
max: 30,
},
],
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
anyObj: true,
__VERSION__: true,
},
plugins: ['@typescript-eslint'],
}

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
yarn.lock
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.auto-import.d.ts
.auto-import.json
.components.d.ts
dist*
/*.gz
/build.sh
.temp

1
.husky/commit-msg Executable file
View File

@ -0,0 +1 @@
npx --no-install commitlint --edit "$1"

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
npm run lint-staged

14
.prettierrc.cjs Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
jsxSingleQuote: true,
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'avoid',
trailingComma: 'all',
vueIndentScriptAndStyle: false,
endOfLine: 'lf',
}

30
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"recommendations": [
// Vue 3
"Vue.volar",
// ESLint JavaScript VS Code
"dbaeumer.vscode-eslint",
// Visual Studio Code Stylelint
"stylelint.vscode-stylelint",
// 使 Prettier
"esbenp.prettier-vscode",
// dotenv
"mikestead.dotenv",
//
"streetsidesoftware.code-spell-checker",
// Tailwind CSS VS Code
"bradlc.vscode-tailwindcss",
// iconify
"antfu.iconify",
// i18n
"Lokalise.i18n-ally",
// CSS
"vunguyentuan.vscode-css-variables",
// package.json PNPM catalog
"antfu.pnpm-catalog-lens"
],
"unwantedRecommendations": [
// volar
"octref.vetur"
]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

View File

@ -0,0 +1,67 @@
module.exports = {
extends: [
'alloy',
'alloy/vue',
'alloy/typescript',
'plugin:@typescript-eslint/recommended',
'./.auto-import.json',
],
parser: 'vue-eslint-parser',
parserOptions: {
parser: {
js: '@babel/eslint-parser',
jsx: '@babel/eslint-parser',
ts: '@typescript-eslint/parser',
tsx: '@typescript-eslint/parser',
},
},
rules: {
// 'vue/html-indent': ['error', 4],
// 'vue/max-attributes-per-line': 'off',
// 'vue/html-self-closing': 'off',
// no-duplicate-attr-inheritance 禁止重复属性继承
'@typescript-eslint/consistent-type-imports': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/method-signature-style': 'off',
'no-return-assign': 'off',
'guard-for-in': 'off',
'vue/custom-event-name-casing': 'off',
'vue/no-duplicate-attributes': 'off',
'no-param-reassign': 'off',
'vue/no-setup-props-destructure': 'off',
'vue/no-duplicate-attr-inheritance': 'off',
'vue/prefer-true-attribute-shorthand': 'off',
'no-eq-null': 'off', // 允许 == 用于 null
'vue/v-on-event-hyphenation': 'off', // 关闭 vue 中 @ 使用短横线命名
'vue/require-toggle-inside-transition': 'off',
'no-duplicate-imports': 'off', // 使用ts-eslint的重复导入规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'max-params': ['error', { max: 4 }],
// 圈复杂度 每个函数的最高圈复杂度
complexity: [
'error',
{
max: 30,
},
],
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
anyObj: true,
__VERSION__: true,
},
plugins: ['@typescript-eslint'],
}

View File

@ -0,0 +1,14 @@
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
jsxSingleQuote: true,
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'avoid',
trailingComma: 'all',
vueIndentScriptAndStyle: false,
endOfLine: 'lf',
}

View File

@ -0,0 +1,36 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.requireConfig": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"eslint.format.enable": true
}

24
commitlint.config.cjs Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// type 类型定义,表示 git 提交的 type 必须在以下类型范围内
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复
'docs', // 文档变更
'style', // 代码格式(不影响代码运行的变动)
'refactor', // 重构既不是增加feature,也不是修复bug
'pref', // 性能优化
'test', // 增加测试
'chore', // 构建过程或辅助工具的变动
'revert', // 回退
'build', // 打包
],
],
// subject 大小写不做校验
'subject-case': [0],
},
}

40
index.html Normal file
View File

@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link id="sass-favicon" rel="icon" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="sass-title">快乐学习</title>
<!-- <script crossorigin="anonymous" src="http://43.136.52.196:6752/page-spy/index.min.js"></script> -->
<!-- <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> -->
<!-- PageSpy SDK -->
<!-- <script
crossorigin="anonymous"
src="https://log.xuexiaole.com/page-spy/index.min.js"
></script>
<script
crossorigin="anonymous"
src="https://log.xuexiaole.com/plugin/data-harbor/index.min.js"
></script>
<script
crossorigin="anonymous"
src="https://log.xuexiaole.com/plugin/rrweb/index.min.js"
></script> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
// window.$harbor = new DataHarborPlugin()
// window.$rrweb = new RRWebPlugin()
// ;[window.$harbor, window.$rrweb].forEach(p => {
// PageSpy.registerPlugin(p)
// })
</script>
<!-- <script>
window.$pageSpy = new PageSpy();
</script> -->
</body>
</html>

104
package.json Normal file
View File

@ -0,0 +1,104 @@
{
"name": "xuexiaole-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode test",
"build": "vue-tsc --noEmit --skipLibCheck && vite build",
"build-test": "vue-tsc --noEmit --skipLibCheck && vite build --mode test",
"preview": "vite preview",
"prepare": "husky",
"type-check": "vue-tsc --build",
"lint": "eslint --config .eslintrc.cjs --fix \"src/**/*.{js,ts,vue}\"",
"lint-staged": "lint-staged",
"format": "prettier --config .prettierrc.cjs --write \"src/**/*.{js,ts,vue,json,css,scss}\""
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@types/hammerjs": "^2.0.45",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@vant/touch-emulator": "^1.4.0",
"@vscode/markdown-it-katex": "^1.1.1",
"@vueuse/core": "^10.11.0",
"animejs": "^3.2.2",
"axios": "^1.7.2",
"chart.js": "^4.4.3",
"compare-versions": "^6.1.1",
"dayjs": "^1.11.12",
"echarts": "^5.5.1",
"element-plus": "^2.7.8",
"hammerjs": "^2.0.8",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"jsencrypt": "^3.3.2",
"jslib-html5-camera-photo": "^3.3.4",
"jspdf": "^3.0.1",
"katex": "^0.16.21",
"lodash-es": "^4.17.21",
"lottie-web": "^5.12.2",
"markdown-it": "^14.1.0",
"mathjax": "3.0.1",
"mermaid-it-markdown": "^1.0.8",
"nprogress": "^0.2.0",
"pinia": "^2.2.0",
"postcss": "^8.4.40",
"qrcode": "^1.5.4",
"qs": "^6.12.3",
"signature_pad": "^5.0.4",
"vant": "^4.9.4",
"vconsole": "^3.15.1",
"vue": "^3.4.31",
"vue-cropper": "^1.1.4",
"vue-router": "^4.4.0",
"vxe-pc-ui": "^4.6.20",
"vxe-table": "^4.13.37",
"xe-utils": "^3.7.5",
"xgplayer": "^3.0.19"
},
"devDependencies": {
"@babel/core": "^7.19.6",
"@babel/eslint-parser": "^7.25.1",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/node": "^22.0.0",
"@types/qs": "^6.9.15",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-config-alloy": "^5.1.2",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-vue": "^9.27.0",
"husky": "^9.1.3",
"lint-staged": "^15.2.7",
"markdown-it-link-attributes": "^4.0.1",
"postcss-px-to-viewport": "^1.1.1",
"prettier": "^3.3.3",
"sass": "^1.77.6",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3",
"vite": "^5.3.4",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^2.0.24"
},
"lint-staged": {
"*.{json,md,css,less}": [
"prettier --write"
],
"*.{js,ts,jsx,tsx,vue}": [
"prettier --write",
"eslint --fix"
]
}
}

7604
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,6 @@
allowBuilds:
'@parcel/watcher': true
core-js: false
es5-ext: false
esbuild: true
vue-demi: true

233
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

19
src/App.vue Normal file
View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
zhCn.el.datepicker.weeks = {
sun: '星期日',
mon: '星期一',
tue: '星期二',
wed: '星期三',
thu: '星期四',
fri: '星期五',
sat: '星期六',
}
</script>
<template>
<el-config-provider :locale="zhCn">
<RouterView />
</el-config-provider>
</template>

79
src/api/chat.ts Normal file
View File

@ -0,0 +1,79 @@
import { request, SseConfig } from '@/utils/sse'
import { get } from './request'
export interface Session {
id: any
title: string
updateTime: string
num: number
local?: boolean
messages?: Message[]
}
export interface Message {
id: any
role: 'user' | 'ai'
content: string
createTime: string
reasoning?: string | null
}
export interface AiReply {
type: number
content: string
}
export type ChatsConfig = Omit<SseConfig, 'onMessage' | 'method'> & {
onMessage: (reasoningText: string, normalText: string) => void
}
const getSessionsApi = async (): Promise<Session[]> => {
return await get('/chats')
}
const getMessagesApi = async (sessionId: any): Promise<Message[]> => {
return await get(`/chats/${sessionId}`)
}
const requestChats = async (method: 'GET' | 'POST', config: ChatsConfig) => {
// 思考的文字
let reasoningText = ''
// 普通文字
let normalText = ''
// 请求
await request({
...(config as SseConfig),
method,
onMessage: message => {
// 提取并解析 JSON
const json = JSON.parse(message)
if (json.type === 3 || !json.content) return
if (json.type === 1) {
// 思考
reasoningText += json.content
} else {
// 普通
normalText += json.content
}
// 更新消息
config.onMessage(reasoningText, normalText)
},
})
}
const postChats = async (config: ChatsConfig) => {
return await requestChats('POST', config)
}
const getChats = async (config: ChatsConfig) => {
return await requestChats('GET', config)
}
const chatApi = {
getSessionsApi,
getMessagesApi,
postChats,
getChats,
}
export default chatApi

View File

@ -0,0 +1,32 @@
import { http } from './http'
const config = {
baseURL: '/api/campus',
}
const classroomInteractionApi = {
/**
*
*/
getClassDetectionRecordList: (params: anyObj) => {
return http.request({
url: '/sc/classDetectionRecord/list',
params,
...config,
})
},
/**
*
*/
addClassDetectionRecord: (data: anyObj) => {
return http.request({
url: '/sc/classDetectionRecord',
method: 'POST',
data,
...config,
})
},
}
export default classroomInteractionApi

284
src/api/global.ts Normal file
View File

@ -0,0 +1,284 @@
import str from '@/utils/str'
import { http } from './http'
import { get } from './request'
import { $XXL } from '@/utils'
export type Option = {
value: any
label: any
}
interface EnglishSubjectExpLevelVo {
currentName: string
currentLevelNum: number
currentExp: number
currentExpStart: number
currentExpEnd: number
nextName: string
nextLevelNum: number
nextExp: number
currentLevelCode: null
}
interface VipUserInfo {
vipType: number
vipExp: number
currentLevel: string
currentLevelIntVal: number
currentLevelStartExp: number
nextLevelExp: number
nextLevelName: string
nextLevelIntVal: number
vipStartTime: string
vipEndTime: string
vipLengthTime: string
}
interface PersonInUsePersonalities {
createTime: string
createUser: string
updateTime: string
updateUser: string
id: any
name: string
resourceUrl: string
personalInfoResourceUrl: string
homePageResourceUrl: string
introduction: string
status: number
productCode: string
currencyNum: number
showTag: number
discount: null
discountNum: null
validityTime: number
limitType: number
sort: number
remark: number
delFlag: number
defaultFlag: number
usageFlag: number
unlockId: string
useStatus: number
lockStatus: null
expireTime: null
}
interface Medal {
medalId: any
}
export interface UserData {
userId: string
schoolId: any
account: string
name: string
nickName: string
sex: number
schoolName: string
education: number
className: string
experience: number
currency: string
diamond: string
levelPassNum: null
personalImageNum: number
englishSubjectExpLevelVo: EnglishSubjectExpLevelVo
vipUserInfo: VipUserInfo
avatarUrl: string
avatarFrameUrl: null
personInUsePersonalities: PersonInUsePersonalities[]
nickNameChangeCardCount: number
popNotices: any[]
unCheckMedal: Medal[]
learnGradeId: string
learnTargetType: number
weakSubjectIds: string
isShowReading: number
isShowAnimation: number
classId: any
smartUserIdentifier: string
schoolPersonType: number
lockFlag: number
isTeacher: boolean
// 是否为可以接受任务的学员
isTaskStudent: boolean
taskRewardStatus: any
schoolClass: SchoolClass[]
}
type SchoolClass = {
schoolName: any
schoolId: any
classList: {
className: any
classId: any
}[]
}
// 获取用户红点提示
export type GetUserPointApi = {
messageBox: boolean
activity: boolean
announcement: boolean
config: boolean
knapsack: boolean
homeWork: boolean
}
const globalApi = {
/**
*
*/
getDictionaries: () => {
return http.request({
url: '/sysDictType/tree',
headers: {
Authorization: '',
},
})
},
/**
*
*/
getUserInfoApi: async (headers?: anyObj): Promise<UserData> => {
return (await get('/personal/getHomePage', undefined, {
headers,
})) as UserData
},
// 获取用户红点提示
getUserPointApi: async (): Promise<GetUserPointApi> => {
return await get('/personal/getRedPoint')
},
// 获取地区下拉列表
getRegionSelector: () => {
return http.request({
url: '/sysUser/regionSelector',
})
},
// 获取年级下拉列表
getClassSelector: () => {
return http.request({
url: '/intrudeLevel/getSchoolGradeList',
})
},
// 已读弹窗提醒
closePopWin: (id: any) =>
http.request({
url: `/personal/closePopWin/${id}`,
}),
// 获取年级列表
getGradeList: (params?: anyObj) =>
http.request({
url: '/grade/list',
params,
}),
// 获取学科列表
getSubjectListById: (id: any) =>
http.request({
url: `/subject/list/${id}`,
}),
// 生词本-标生标熟
changeWordType: (data: anyObj) => {
return http.request({
url: `/word/operate`,
data,
method: 'POST',
})
},
// 上传文件
uploadFile: (data: any) =>
http.request({
url: '/sysFileInfo/tenUploadAll',
method: 'post',
timeout: 15000,
headers: {
'content-type': 'multipart/form-data',
},
data,
}),
// 用户视频表-id查询
getUserVideoById: (id: any) =>
http.request({
url: `/userVideo/${id}`,
}),
// 获取静态资源地址
getAssetsList: () =>
http.request({
url: '/listFrontStaticFiles',
headers: {
Authorization: '',
},
}),
// 检查APK版本
getVersions: async (params: anyObj) => {
const url = location.href
if (
url.includes('localhost') ||
url.includes('192.168.') ||
url.includes('127.0.0.1') ||
url.includes('lingxixue.com')
) {
params = {
magic: str.getMagic(),
...(params || {}),
}
}
return get<any>('/apkManagement/list', params)
},
// 检查版本
checkVersion: (params: anyObj) =>
http.request({
url: '/apkManagement/queryLatest',
headers: {
Authorization: '',
},
params,
}),
// 更新APPs
updateApps: (data: anyObj) =>
http.request({
url: '/userAppRecord/updateApp',
method: 'POST',
data,
}),
// 获取未记录的APP
updateUnRecordApp: (data: anyObj) =>
http.request({
url: '/appLogoRecord/filterUnLoadedLogo',
method: 'POST',
data,
}),
// 记录APP信息
updateAppInfo: (data: anyObj) =>
http.request({
url: '/appLogoRecord/batchSave',
method: 'POST',
data,
}),
// 更新徽章
updateMedal: (data: anyObj) =>
http.request({
url: '/userMedal/updateCheck',
method: 'POST',
data,
}),
}
export default globalApi

140
src/api/http.ts Normal file
View File

@ -0,0 +1,140 @@
/* eslint-disable */
import axios, { AxiosInstance } from 'axios'
import qs from 'qs'
import router from '@/router'
import { getCache, delCache } from '@/utils'
import { storeToRefs } from 'pinia'
import { globalStore, userStore } from '@/stores'
import hud from '@/utils/hud'
import onlyLast from './only-last'
import sass from '@/utils/sass'
// let controller = new AbortController()
const timeout = 90000
const instance: AxiosInstance = axios.create({
baseURL: '/api/main',
method: 'get',
headers: {
'Content-Type': 'application/json',
},
timeout,
})
/**
*
*/
instance.interceptors.request.use(
(config: any) => {
config.baseURL = config.baseURL || '/api/main'
config.timeout = config.timeout || timeout
if (config.method === 'get') {
config.paramsSerializer = {
serialize: function (params: any) {
return qs.stringify(params, {
arrayFormat: 'repeat',
allowDots: true,
})
},
}
}
onlyLast.processRequest(config)
// 租户id
config.headers['X-Tenant-Id'] = sass.platform.tenantId
// 自定义token
if (config.headers['Authorization'] || config.headers['Authorization'] === '') {
return config
}
if (config.baseURL === '/api/campus') {
config.headers['Authorization'] = getCache('workToken')
} else {
config.headers['Authorization'] = getCache('token')
}
return config
},
error => Promise.reject(error),
)
const buildError = (response: any, show = true) => {
const {
data: { message, msg },
} = response
let errorMsg = message || msg
let showed = false
if (errorMsg) {
if (errorMsg.includes('服务器')) {
errorMsg = '网络不稳定,请稍后再试~'
}
// if (traceId) {
// errorMsg = `${errorMsg}[${traceId}]`
// }
if (show) {
hud.error(errorMsg)
showed = true
}
}
if (errorMsg?.includes('非本平台用户')) {
// 重新登录
delCache('token')
delCache('workToken')
router.replace('/login')
}
return {
message: errorMsg,
showed,
}
}
/**
*
*/
instance.interceptors.response.use(
response => {
const {
data: { code, message },
} = response
if (!onlyLast.processResponse(response.config)) {
// 过滤请求
return Promise.reject(response)
}
// 文件流不拦截
if (response.headers['content-type']?.indexOf('octet-stream') !== -1 || code == 200)
return response
return Promise.reject(buildError(response))
},
async error => {
const { response } = error
if (!onlyLast.processResponse(error.config)) {
// 过滤请求
return Promise.reject(error)
}
if (response?.status === 401 || response?.status === 403) {
const user = userStore()
const { token } = storeToRefs(user)
if (token.value) {
hud.error('登录已过期,请重新登录')
user.$reset()
}
delCache('token')
delCache('workToken')
router.replace('/login')
// 解锁
const { system } = storeToRefs(globalStore())
system.value.isLock = false
}
console.log('error2', response)
return Promise.reject(buildError(response, false))
},
)
export const http = instance

55
src/api/log.ts Normal file
View File

@ -0,0 +1,55 @@
import { http } from './http'
// 上报锁定屏幕时间
export interface LockScreenTime {
simSerialNumber: string
lockFlag: 0 | 1 // 0-解锁 1-锁定
}
// 上报视频播放时间
export interface VideoPlayTime {
resourceType: 0 | 1 | 2 | 3 // 0-自由学 1-AI精准学 2-错题本 3-举一反三
origin: 1 | 2 // 1-章节 2-知识点
subjectId: string
playDuration: number // 视频最后播放位置
userId: string
videoId: string
videoName: string
endTime: number | ''
startTime: number | ''
}
// 上报学习时长
export interface StudyTime {
resourceType: 0 | 1 | 2 | 3 // 0-自由学 1-AI精准学 2-错题本 3-举一反三
studyTime: number
subjectId: string
userId: string
endTime: number
startTime: number
}
const logApi = {
reportLockScreenTime: (data: LockScreenTime) =>
http.request({
url: '/deviceTimeControl/update/timeStamp',
method: 'post',
data,
}),
reportVideoPlayTime: (data: VideoPlayTime) =>
http.request({
url: '/userVideo',
method: 'put',
data,
}),
reportStudyTime: (data: StudyTime) =>
http.request({
url: '/userSubjectReport',
method: 'put',
data,
}),
}
export default logApi

84
src/api/login.ts Normal file
View File

@ -0,0 +1,84 @@
import { http } from './http'
import { isWebEvn } from '@/utils'
import { post } from './request'
interface accountLoginType {
account: string
password: string
simSerialNumber?: string
model?: string
}
interface smsLoginType {
phone: string
verifyCode: string
}
/**
*
* @param account
* @param password
* @param clientType
*/
const psdLogin = async (data: accountLoginType) => {
return await post('/login', {
clientType: isWebEvn ? 'PC' : 'PAD',
...data,
})
}
// 退出登录
const logout = () => {
return http.request({
url: '/logout',
})
}
// 获取是否首次绑定
const getFirstBind = (sn: string) => {
return http.request({
url: `/sysVisLog/getBindRelationByDeviceAndAccount/${sn}`,
})
}
// 获取机器绑定码
const getBindQrCode = (data: anyObj) => {
return http.request({
url: '/sysFileInfo/generateQrCodeAndUpload',
method: 'post',
data,
})
}
// 获取作业TOKEN
const workLogin = (data: anyObj) => {
return http.request({
baseURL: '/api/campus',
url: '/sc/stu/stuLogin',
headers: {
Authorization: '',
},
method: 'POST',
data,
})
}
// 作业服务退出登录
const workLogout = () => {
return http.request({
baseURL: '/api/campus',
url: '/logout',
method: 'POST',
})
}
const loginApi = {
psdLogin,
logout,
getFirstBind,
getBindQrCode,
workLogin,
workLogout,
}
export default loginApi
export type { accountLoginType, smsLoginType }

65
src/api/only-last.ts Normal file
View File

@ -0,0 +1,65 @@
import type { AxiosRequestConfig } from 'axios'
import obj from '@/utils/obj'
// 同时有多个请求时,只有最后一个请求生效
export const only_last_id = 'on_last_id'
const only_last_req_id = 'only_last_req_id'
const controllers = {} as Record<string, AbortController>
const reqIds = {} as Record<string, string>
export type RequestConfig = AxiosRequestConfig & {
[only_last_id]?: any
[only_last_req_id]?: any
}
function processRequest(config: RequestConfig) {
if (!config) return
const onlyLastId = config[only_last_id]
if (!onlyLastId) return
// 取消上一个请求
let ctl = controllers[onlyLastId]
if (ctl) {
ctl.abort()
// console.log('❌取消❌', onlyLastId, reqIds[onlyLastId], config.url)
}
// 重新赋值
ctl = new AbortController()
config.signal = ctl.signal
controllers[onlyLastId] = ctl
// 赋值一个请求的唯一id
const reqId = obj.snowflakeId()
reqIds[onlyLastId] = reqId
config[only_last_req_id] = reqId
// console.log('💪🏻开启💪🏻', onlyLastId, reqId, config.url)
}
function processResponse(config: RequestConfig) {
if (!config) return true
const onlyLastId = config[only_last_id]
// 忽略
if (!onlyLastId) return true
const reqId = config[only_last_req_id]
if (reqId === reqIds[onlyLastId]) {
// console.log('✅选用✅', onlyLastId, reqId, config.url)
delete reqIds[onlyLastId]
delete controllers[onlyLastId]
// 忽略
return true
} else {
// console.log('⭕️抛弃⭕️', onlyLastId, reqId, config.url)
// 过滤请求
return false
}
}
const onlyLastApi = {
processRequest,
processResponse,
}
export default onlyLastApi

40
src/api/order.ts Normal file
View File

@ -0,0 +1,40 @@
import { http } from './http'
const orderApi = {
// 订单支付状态
getPayStatus: (id: any) =>
http.request({
url: `/orderManagement/${id}`,
}),
// 订单-VIP续费
createVipRenewPrepayOrder: (data: anyObj) =>
http.request({
url: '/orderManagement/createVipRenewPrepayOrder',
method: 'post',
data,
}),
// 订单-督学
createInspectorPrepayOrder: (data: anyObj) =>
http.request({
url: '/orderManagement/createInspectorPrepayOrder',
method: 'post',
data,
}),
// VIP订单记录
getVipOrderRecord: (params: anyObj) =>
http.request({
url: '/orderManagement/vipPayRecord',
params,
}),
// 查询是否是督学中
getIsInspectorIng: () =>
http.request({
url: '/inspectorStudent/inspectorButtonData',
}),
}
export default orderApi

34
src/api/ranking.ts Normal file
View File

@ -0,0 +1,34 @@
import { http } from './http'
import { only_last_id } from './only-last'
import { get } from './request'
const rankingApi = {
/**
*
*/
getRankList: async (params: anyObj) => {
return await get('/intrudeLevel/rankList', params, {
[only_last_id]: 'ranking_getRankList',
})
},
/**
*
*/
queryTargetUserIdInfo: async (id: any): Promise<any> => {
return await get(`/personal/queryTargetUserIdInfo/${id}`)
},
/**
*
*/
operateLikeOrUnLike: (data: anyObj) => {
return http.request({
url: `/userLikeInfo/operateLikeOrUnLike`,
data,
method: 'POST',
})
},
}
export default rankingApi

90
src/api/request.ts Normal file
View File

@ -0,0 +1,90 @@
import { http } from './http'
import type { RequestConfig } from './only-last'
export type PageResponse<T> = {
current: number
size: number
totalPage: number
totalRows: number
rows: T[]
}
// 发送get请求
export async function get<T>(
uri: string,
params?: Record<string, any>,
config?: RequestConfig,
): Promise<T> {
if (params) {
config = config ? { ...config, params } : { params }
}
return (await http.get<any>(uri, config)).data['data'] as T
}
// 发送delete请求
export async function remove<T>(
uri: string,
params?: Record<string, any>,
config?: RequestConfig,
): Promise<T> {
if (params) {
config = config ? { ...config, params } : { params }
}
return (await http.delete<any>(uri, config)).data['data'] as T
}
// 处理请求data可能有文件
const processFormData = (data: any): any => {
if (!data) return data
const vs = Object.values(data)
let hasFile = false
for (const v of vs) {
if (v instanceof File) {
hasFile = true
break
}
}
if (!hasFile) return data
// 如果有文件
const form = new FormData()
const ks = Object.keys(data)
for (const k_ of ks) {
const k = k_ as string
const v = data[k]
if (v) {
form.append(k, v)
}
}
return form
}
// 发送post请求JSON格式
export async function post<T>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return (await http.post(url, data, config)).data['data'] as T
}
// 发送put请求JSON格式
export async function put<T>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return (await http.put(url, data, config)).data['data'] as T
}
// 发送post请求表单格式
export async function postForm<T>(url: string, data?: any, config?: RequestConfig): Promise<T> {
data = processFormData(data)
if (data) {
let type = 'application/x-www-form-urlencoded'
if (data instanceof FormData) {
// 文件上传
type = 'multipart/form-data'
}
const headers = { 'Content-Type': type }
config = config
? {
...config,
headers,
}
: { headers }
}
return (await http.post(url, data, config)).data['data'] as T
}

30
src/api/service/basic.ts Normal file
View File

@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { get, post } from '../request'
import type { PageResponse } from '../request'
export abstract class BasicService<T> {
protected abstract findPageUrl(): string
protected abstract saveOrUpdateUrl(): string
protected abstract removeUrl(): string
// 增/改自动根据id判断
async saveOrUpdate(entity: T) {
return await post(this.saveOrUpdateUrl(), entity)
}
// id、id[]
async remove(ids: any): Promise<any> {
if (!Array.isArray(ids)) {
ids = [ids]
}
return await post(this.removeUrl(), ids)
}
// 分页查询
async findPage<T>(current: number, size: number, condition: any): Promise<PageResponse<T>> {
return await get<PageResponse<T>>(this.findPageUrl(), {
size,
current,
...(condition || {}),
})
}
}

View File

@ -0,0 +1,17 @@
import { BasicService } from './basic'
class CoinTransactionService extends BasicService<any> {
protected saveOrUpdateUrl(): string {
return '/org/school/recharge'
}
protected removeUrl(): string {
throw new Error('Method not implemented.')
}
protected findPageUrl(): string {
return '/org/school/transaction/list'
}
}
export const coinTransactionService = new CoinTransactionService()

View File

@ -0,0 +1,17 @@
import { BasicService } from './basic'
class SelfCoinDetailService extends BasicService<any> {
protected saveOrUpdateUrl(): string {
throw new Error('Method not implemented.')
}
protected removeUrl(): string {
throw new Error('Method not implemented.')
}
protected findPageUrl(): string {
return '/reward/coin/list'
}
}
export const selfCoinDetailService = new SelfCoinDetailService()

172
src/api/service/task.ts Normal file
View File

@ -0,0 +1,172 @@
/* eslint-disable @typescript-eslint/member-ordering */
import { get, post } from '../request'
import { getSubjectById } from '../subject/config'
import { BasicService } from './basic'
// 任务配置
type TaskConfig = {
id?: any
taskId?: any
}
export type TaskWordTableConfig = {
bookId: any // 教材
stageId: any // 阶段
amount: any // 复习数量
} & TaskConfig
export type TaskWordQuestConfig = {
level: any // 入门、进阶
amount: any // 满星关卡数
} & TaskConfig
export type TaskSentenceConfig = {
level: any // 第几阶
amount: any // 句子数量
} & TaskConfig
export type TaskFreeLearnConfig = {
bookId: any // 教材id
chapterIds: any // 章节id
amount: any // 通关次数
} & TaskConfig
type Config = {
gradeId: any
subjectId: any
chapters: any
stageId: any // 阶段
stageName: string
versionId: any // 版本
versionName: string
bookName: string
bookId: any // 教材
levelName: string
}
export type Task = {
id: any
schoolId: string
objTypeCode: string
personCount: number
completedPersonCount: number
objTypeList: {
id: any
taskId: string
objectId: string
}[]
startTime: string
endTime: string
delFlag: number
priority: number
createTime: string
createUserId: string
createUserName: string
taskExtraRewardList: {
id: any
taskId: string
amount: number
startRank: number
endRank: number
}[]
taskFreeLearnConfig: TaskFreeLearnConfig // null 使用 any 类型
taskWordMasteryConfig: TaskWordTableConfig // null 使用 any 类型
taskWordQuestConfig: TaskWordQuestConfig // null 使用 any 类型
taskSentenceConfig: TaskSentenceConfig // null 使用 any 类型
taskWordReviewConfig: TaskWordTableConfig
topicTypeCode: string
status: number
target: string
reward: number
punish: number
remark: string
config: any
}
class TaskService extends BasicService<any> {
protected saveOrUpdateUrl(): string {
return '/supervisor/task'
}
protected removeUrl(): string {
throw new Error('Method not implemented.')
}
protected findPageUrl(): string {
return '/supervisor/task/list'
}
/**
*
*/
async getProcessingTasks(config?: any): Promise<any> {
return await get('/supervisor/usertask/process/list', undefined, config)
}
async claimReward(data: any): Promise<any> {
return await post('/supervisor/usertask/receive/reward', data)
}
/**
*
* @param
* @returns
*/
async batchClaimReward(): Promise<any> {
return await post('/supervisor/usertask/receive/rewards/all')
}
/**
*
*/
async cancel(taskId: any) {
await post(`/supervisor/task/cancel/${taskId}`)
}
/** 获得任务详情 */
async getDetail(taskId: any): Promise<Task> {
return await get(`/supervisor/task/get/${taskId}`)
}
// 获得额外奖励
getExtraRewardList(task: Task) {
const list = task.taskExtraRewardList
if (!list?.length) return undefined
list.sort((a: any, b: any) => {
const res = a.startRank - b.startRank
return res ? res : a.endRank - b.endRank
})
return list
}
// 获得书本的名称
getBookName(task: Task) {
let cfg = task.config as Config
if (typeof cfg === 'string') {
try {
cfg = JSON.stringify(cfg) as any
} catch {
return
}
}
if (!cfg) return
if (cfg.bookName) {
if (cfg.subjectId) {
// 有学科
return `[${getSubjectById(cfg.subjectId).name}]${cfg.bookName}`
} else if (cfg.stageName) {
// 有阶段
return cfg.stageName + '-' + cfg.versionName + '-' + cfg.bookName
} else {
return cfg.bookName
}
} else if (cfg.stageName) {
// 只有阶段
return cfg.stageName
}
}
}
export const taskService = new TaskService()

View File

@ -0,0 +1,17 @@
import { BasicService } from './basic'
class UserTaskService extends BasicService<any> {
protected saveOrUpdateUrl(): string {
throw new Error('Method not implemented.')
}
protected removeUrl(): string {
throw new Error('Method not implemented.')
}
protected findPageUrl(): string {
return '/supervisor/usertask/list'
}
}
export const userTaskService = new UserTaskService()

View File

@ -0,0 +1,48 @@
import { only_last_id } from '../only-last'
import { get, type PageResponse } from '../request'
import { BasicService } from './basic'
type FindParams = {
current: any
size: any
grasp: any
userId: any
startTime: any
endTime: any
}
class WordTableService extends BasicService<any> {
async findStage(
data: FindParams & {
vocabularyId: any
},
) {
return await get<PageResponse<any>>('/wordTable/wordTableStage', data, {
[only_last_id]: 'word-table-list',
})
}
async findBook(
data: FindParams & {
textbookId: any
},
) {
return await get<PageResponse<any>>('/wordTable/wordTableTextbook', data, {
[only_last_id]: 'word-table-list',
})
}
protected saveOrUpdateUrl(): string {
throw new Error('Method not implemented.')
}
protected removeUrl(): string {
throw new Error('Method not implemented.')
}
protected findPageUrl(): string {
throw new Error('Method not implemented.')
}
}
export const wordTableService = new WordTableService()

View File

@ -0,0 +1,126 @@
/* eslint-disable @typescript-eslint/member-ordering */
import str from '@/utils/str'
import { get, post, type PageResponse } from '../request'
import wordTestApi from '../subject/wordTest'
import { BasicService } from './basic'
function getGraspText(val: any, total: any) {
if (val === 0) return '0'
return `${val}(${Math.round((val / total) * 100)}%)`
}
class WordTestService extends BasicService<any> {
private parseJsonItem(item: any) {
if (typeof item.extra === 'string') {
item.extra = JSON.parse(item.extra)
}
if (typeof item.bonus === 'string') {
item.bonus = JSON.parse(item.bonus)
}
if (typeof item.graspInfo === 'string') {
const info = JSON.parse(item.graspInfo)
info.strangeText = getGraspText(info.predictStrangeWordNum, info.totalNum)
info.knowText = getGraspText(info.predictKnowWordNum, info.totalNum)
info.knowWellText = getGraspText(info.predictKnowWellWordNum, info.totalNum)
info.graspText = getGraspText(info.predictGraspWordNum, info.totalNum)
item.graspInfo = info
if (typeof item.lastExamWordGraspInfo === 'string') {
const lastInfo = JSON.parse(item.lastExamWordGraspInfo)
item.lastExamWordGraspInfo = lastInfo
info.strangeChange = info.predictStrangeWordNum - lastInfo.predictStrangeWordNum
if (info.strangeChange > 0) {
info.strangeChange = `+${info.strangeChange}`
}
info.knowChange = info.predictKnowWordNum - lastInfo.predictKnowWordNum
if (info.knowChange > 0) {
info.knowChange = `+${info.knowChange}`
}
info.knowWellChange = info.predictKnowWellWordNum - lastInfo.predictKnowWellWordNum
if (info.knowWellChange > 0) {
info.knowWellChange = `+${info.knowWellChange}`
}
info.graspChange = info.predictGraspWordNum - lastInfo.predictGraspWordNum
if (info.graspChange > 0) {
info.graspChange = `+${info.graspChange}`
}
}
}
// 展示的名字
item.displayName = item.nickName || item.name
// 打码
item.redactionDisplayName = str.redaction(item.displayName)
// 学习的范围
const stageName = item.extra?.stageName
if (stageName) {
item.studyRange = stageName
} else {
const book = item.extra?.book
if (book) {
item.studyRange = `${book.versionName}-${book.bookName}`
}
}
}
async getWords(data: any) {
return await wordTestApi.getWordTestWordsV2(data)
}
async getRecordDetail(sessionId: any) {
const data = await get<any>('/wordExamRecord/getTotalInfoWordsBySessionId', { sessionId })
this.parseJsonItem(data)
return data
}
// 分页查询
async findPage<T>(current: number, size: number, condition: any): Promise<PageResponse<T>> {
const data = (await super.findPage<T>(current, size, condition)) as PageResponse<T>
for (const item of data.rows as any) {
this.parseJsonItem(item)
}
return data
}
async saveWordRecord(data: any) {
return await post('/wordExamRecord/saveRecord', data)
}
async saveTotalRecords(data: any) {
const res = await post('/wordExamRecord/saveTotalRecord', data)
this.parseJsonItem(res)
return res
}
/**
*
*/
async getLatestRecords() {
const res = await get<any>('/wordExamTotalRecord/getRecentRecords')
for (const item of res) {
this.parseJsonItem(item)
}
return res
}
/**
*
*/
async getConsumeCoin(data: any) {
return await get<any>('/wordExamRecord/getUserConsume', data)
}
protected saveOrUpdateUrl(): string {
throw new Error('Method not implemented.')
}
protected removeUrl(): string {
throw new Error('Method not implemented.')
}
protected findPageUrl(): string {
return '/wordExamTotalRecord/list'
}
}
export const wordTestService = new WordTestService()

330
src/api/studentManage.ts Normal file
View File

@ -0,0 +1,330 @@
import dayjs from 'dayjs'
import { get, post, remove } from './request'
import type { PageResponse } from './request'
import type { TextBook } from './subject/subject'
import obj from '@/utils/obj'
import type { RequestConfig } from './only-last'
interface Personality {
name: string
resourceUrl: string
personalInfoResourceUrl: string
productCode: string
remark: number
}
export type Group = {
id: any
name: any
label: any
students: Student[]
}
export interface Student {
id: any
account: string
nickName: string
name: string
sex: number
avatarUrl: string
lastLoginTime: string
className: string
classId: string
groupName: string
groupId: string
cumulativeTimeDuration: number
currentName: string
currentLevelNum: number
currentExp: number
currentExpStart: number
currentExpEnd: number
nextName: string
nextLevelNum: number
nextExp: number
personInUsePersonalities: Personality[]
englishSubjectExpLevelVo: any // null in JSON
learnGradeId: string
currency: string
diamond: string
status: number
}
export type Class = {
classId: any
className: any
}
export type School = {
schoolId: any
schoolName: any
schoolCurrency: any
classList: Class[]
}
export type ClassStudent = {
students: {
list: Student[]
map: Record<string, Student>
}
groups: {
list: Group[]
map: Record<string, Group>
}
}
const studentManageApi = {
// 获取学生列表
getClassStudent: async (classId: any, config?: RequestConfig): Promise<ClassStudent> => {
const data = (
(await get(
'/schoolUser/list',
{
classId,
size: -1,
},
config,
)) as any
)?.rows
if (!data?.length) return undefined as any
const classStudent = { students: {}, groups: {} } as ClassStudent
// 学员
classStudent.students.list = data
classStudent.students.map = obj.list2map(data, 'id')
// 分组
const groupMap = {} as Record<string, Group>
classStudent.groups.map = groupMap
const groupList = [] as Group[]
classStudent.groups.list = groupList
const defaultGroup = {
id: 0,
name: '未分组',
students: [] as Student[],
} as Group
for (const student of data) {
if (student.groupId) {
// 有分组
let group = groupMap[student.groupId]
if (!group) {
group = {
id: student.groupId,
name: student.groupName,
label: student.groupName,
students: [],
}
groupMap[student.groupId] = group
groupList.push(group)
}
group.students.push(student)
} else {
// 无分组
defaultGroup.students.push(student)
}
}
// 按照组名排序分组
groupList.sort((a, b) => a.name.localeCompare(b.name))
return classStudent
},
// 获取学校列表
getSchoolList: async (): Promise<School[]> => {
return await get('/schoolUser/schoolClassList')
},
// 获取班级列表
getClassListBySchoolId: async (id: any): Promise<Class[]> => {
return await get(`/schoolUser/classListBySchoolId/${id}`)
},
// 获取班级汇总数据
getClassTotalLearnData: async (params: anyObj, config?: RequestConfig) => {
return await get('/sysUser/queryClassTotalLearnData', params, config)
},
// 获取班级每日数据
getClassTodayLearnData: async (params: anyObj, config?: RequestConfig) => {
return await get('/sysUser/queryClassTodayLearnData', params, config)
},
// 获取综合排名
getClassRankingData: async (params: anyObj, config?: RequestConfig) => {
return await get('/sysUser/queryClassRankingData', params, config)
},
// 获取学科数据
getSubjectStudyData: async (params: anyObj, config?: RequestConfig) => {
const data = (await get('/sysUser/queryClassSubjectLearnData', params, config)) as any
// learnTime转为整数
data?.forEach((item: anyObj) => {
item.learnTime = Number(item.learnTime)
})
return data
},
// 重置密码
resetPassword: async (ids: any) => {
if (!Array.isArray(ids)) {
ids = [ids]
}
return await post('/sysUser/resetPwdFront', { ids })
},
// 教师查询教材列表
getClassBookList: async (data: GetBookListApi, config?: RequestConfig): Promise<TextBook[]> => {
return await get('/teacherTextBook/list', data, config)
},
// 教师删除教材
removeClassBook: async (id: any) => {
return await remove(`/teacherTextBook/${id}`)
},
// 教师添加教材
addClassBook: async (data: AddBookApi) => {
return await post('/teacherTextBook', data)
},
// 增加乐贝
addCoin: async (data: CoinDiamondApi) => {
processCoinDiamondApi(data, true)
return await post('/reward/coin/reward', data)
},
// 减少乐贝
subCoin: async (data: CoinDiamondApi) => {
processCoinDiamondApi(data, false)
return await post('/reward/coin/reduce', data)
},
// 批量操作乐贝
coinBatch: async (data: CoinDiamondBatchApi) => {
data = processedCoinDiamondBatchApi(data)
return await post('/reward/coin/rewardBatch', data)
},
// 增加钻石
addDiamond: async (data: CoinDiamondApi) => {
processCoinDiamondApi(data, true)
return await post('/reward/diamond/reward', data)
},
// 减少钻石
subDiamond: async (data: CoinDiamondApi) => {
processCoinDiamondApi(data, false)
return await post('/reward/diamond/reduce', data)
},
// 批量操作钻石
diamondBatch: async (data: CoinDiamondBatchApi) => {
data = processedCoinDiamondBatchApi(data)
return await post('/reward/diamond/rewardBatch', data)
},
// 修改学员状态
changeStatus: async (data: ChangeStatusApi) => {
return await post('/sysUser/changeStatusFront', data)
},
changeStatusBatch: async (data: ChangeStatusBatchApi) => {
return await post('/sysUser/changeStatusFrontBatch', data)
},
// 获取乐币流水
getCoinTransaction: async (
data: GetCoinTransactionApi,
): Promise<PageResponse<CoinTransaction>> => {
return await get('/org/school/transaction/list', data)
},
}
function processCoinDiamondApi(data: CoinDiamondApi, add: boolean) {
const amount = Number(data.amount)
if (isNaN(amount) || amount <= 0) {
throw '请输入正确的数量'
}
if (!data.bizDate) {
data.bizDate = dayjs().format('YYYY-MM-DD HH:mm:ss')
}
if (!data.reason) {
data.reason = add ? '手动奖励' : '手动扣除'
}
}
function processedCoinDiamondBatchApi(data: CoinDiamondBatchApi) {
const arr = [] as CoinDiamondBatchApi
for (const item of data) {
try {
processCoinDiamondApi(item, item.operation === operation_add)
arr.push(item)
} catch (e) {}
}
if (arr.length === 0) {
throw '没有需要操作的数据'
}
return arr
}
type GetBookListApi = {
classId: any
subjectId: any
}
type AddBookApi = {
classId: any
textBookId: any
}
export const type_coin = 1
export const type_diamond = 2
export type CoinDiamondApi = {
reason?: string
amount: any
userId: any
bizDate?: string
remark?: string
typeId?: any
}
export const operation_add = 1
export const operation_sub = 2
export type CoinDiamondBatchApi = (CoinDiamondApi & {
operation: any
})[]
type ChangeStatusApi = {
id?: any
classId?: any
status: any
}
type ChangeStatusBatchApi = {
id: any
status: any
}[]
// 获取乐币流水
type GetCoinTransactionApi = {
schoolId: any
typeCode: any
keyword?: any
current: any
size: any
}
export type CoinTransaction = {
id: any
schoolId: any
schoolName: string
operation: any
typeCode: any
amount: any
userId: any
transactionId: string
bizDate: string
remark: string
record: any
}
export const status_enabled = 0
export const status_disabled = 1
export default studentManageApi

80
src/api/studyAnalysis.ts Normal file
View File

@ -0,0 +1,80 @@
import { http } from './http'
import { get } from './request'
const studyAnalysisApi = {
/**
* -
*/
getReportDuration: (params: anyObj) => {
return http.request({
url: `/userSubjectReport/study/duration`,
params,
})
},
/**
* -
*/
getReportError: (params: anyObj) => {
return http.request({
url: `/userSubjectReport/study/error`,
params,
})
},
/**
* -
*/
getReportKnowledge: (params: anyObj) => {
return http.request({
url: `/userSubjectReport/study/knowledge`,
params,
})
},
/**
* -
*/
getReportvideo: (params: anyObj) => {
return http.request({
url: `/userSubjectReport/study/video`,
params,
})
},
/**
* -
*/
getStatListByTimeFlag: (data: anyObj) => {
return http.request({
url: `/userSentenceLearn/queryStatListByTimeFlag`,
data,
method: 'post',
})
},
/**
* -
*/
getWordReportData: (params: anyObj) => {
return http.request({
url: `/userSubjectReport/eng/word`,
params,
})
},
// 获取学员信息
getUserInfoById: async (stuId: any): Promise<any> => {
return await get(`/schoolUser/getInfo`, { stuId })
},
// 练习统计
getTrainRecord: (params: anyObj) => {
return http.request({
url: `/userSubjectReport/study/trainRecord`,
params,
})
},
}
export default studyAnalysisApi

879
src/api/subject/config.ts Normal file
View File

@ -0,0 +1,879 @@
/* eslint-disable @typescript-eslint/prefer-for-of */
import type { BookVersion, Clean, TextBook } from './subject'
const _getBookColor = (bookName: string) => {
let index = bookVersionNames.length - 1
let stop = false
// 看一下bookName包含bookVersionNames的哪一个
for (let i = 0; i < bookVersionNames.length; i++) {
for (let j = 0; j < bookVersionNames[i].length; j++) {
if (bookName.indexOf(bookVersionNames[i][j]) !== -1) {
index = i
stop = true
break
}
}
if (stop) {
break
}
}
return bookColors[index]
}
const TAGS = ['五四', '六三', '课改']
const replaced_tags: any = {
: '五四',
: '六三',
'-广州用': '(广州)',
'(北京课改版)': '(课改版)',
'-开心学英语': '(开心学英语)',
: '(一年级起点)',
: '(三年级起点)',
: '(新启航)',
: '(新视角)',
: '(新交际)',
}
// 处理名字,分割成标签、非标签
const _processName = (name: string) => {
// 先统一括号为英文括号
name = name.replace(//g, '(').replace(//g, ')')
// 替换
for (const key in replaced_tags) {
name = name.replace(key, replaced_tags[key])
}
// 区分
const data: {
tags: string[]
name: string
} = {
tags: [],
name,
}
// 如果名字里面带有括号
const bracket: any = Array.from(name.matchAll(/\(([^\]*?)\)/g), match => match[1])
if (bracket) {
data.tags = bracket
for (const b of bracket) {
name = name.replace(`(${b})`, '')
}
data.name = name
}
// 先处理标签
for (const tag of TAGS) {
const idx = name.indexOf(tag)
if (idx === -1) continue
data.tags.push(tag)
data.name = name.slice(0, idx) + name.slice(idx + tag.length)
break
}
// 有些标签包含了/
if (data.tags) {
let newTags: any = []
for (const tag of data.tags) {
newTags = [...newTags, ...tag.split('/')]
}
newTags.sort()
data.tags = newTags
}
// // 去掉括号
// if (data.tag.startsWith('(')) {
// data.tag = data.tag.slice(1, -1)
// }
return data
}
const _processObj = (obj: any) => {
const sourceName = obj?.clean?.name || ''
const name = _processName(sourceName)
obj.clean.tags = name.tags || []
obj.clean.name = name.name || sourceName
}
const _longest = (...args: string[]) => {
let size = 0
let longest = ''
for (const arg of args) {
if (!arg) continue
if (arg.length > size) {
size = arg.length
longest = arg
}
}
return longest
}
// 清洗一下版本数据
export const cleanVersions = (versions: BookVersion[]) => {
for (const version of versions) {
version.clean = {} as Clean
// 设置名字
version.clean.name = version.shortName || version.name
// 处理对象
_processObj(version)
// 处理书本
for (const book of version.textBookVoList) {
book.clean = {} as Clean
// 设置名字
if (version.clean.tags?.length) {
book.clean.name = book.displayName || book.shortName || book.name
} else {
book.clean.name = _longest(book.name, book.displayName, book.shortName)
}
// 处理对象
_processObj(book)
// 全名
book.clean.fullName = _getBookName(book, version)
// 覆盖颜色
book.clean.color = _getBookColor(book.clean.fullName!)
version.clean.color = book.clean.color
// 去除不必要的tag
if (book.clean.tags?.length) {
book.clean.tags = book.clean.tags.filter((tag: string) => {
// 如果跟版本名字相同,直接去掉
return tag !== version.clean.name
})
}
// 合并tag
book.clean.all_tags = [
...new Set([...(book.clean?.tags || []), ...(version.clean?.tags || [])]),
]
}
}
}
// 清洗一下教材数据
export const cleanBooks = (books: TextBook[]) => {
for (const book of books) {
book.clean = {} as Clean
book.version = {
clean: {} as Clean,
} as BookVersion
// 设置名字
book.clean.name = book.textBookName
book.version.clean.name = book.bookVersionName || book.versionName
// 处理对象
_processObj(book)
_processObj(book.version)
// 全名
book.clean.fullName = _getBookName(book, book.version)
book.clean.color = _getBookColor(book.clean.fullName!)
book.version.clean.color = book.clean.color
// 去除不必要的tag
if (book.clean.tags?.length) {
book.clean.tags = book.clean.tags.filter((tag: string) => {
// 如果跟版本名字相同,直接去掉
return tag !== book.version.clean.name
})
}
// 合并tag
book.clean.all_tags = [
...new Set([...(book.clean.tags || []), ...(book.version.clean.tags || [])]),
]
}
}
const _escapeRegExp = (text: string) => {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const _trimRegex = (source: string, regex: string) => {
const safeRegex = _escapeRegExp(regex || '')
const reg = new RegExp(`\\s*${safeRegex}\\s*`, 'g')
return source.replace(reg, '')
}
// 需要去掉的名字
const trim_names = ['人教版数学', '沪教版', '小学', '英语', '少儿']
// 获取书本名字
const _getBookName = (book: any, version: any) => {
// 第1步去掉书本名字中的教材版本名字
for (const name of trim_names) {
book.clean.name = _trimRegex(book.clean.name, name)
}
book.clean.name = _trimRegex(book.clean.name, version.clean.name)
// 第2步
let name = version.clean.name
const tags = version.clean.tags || []
const step = '·'
if (tags.length) {
name += step + tags.join(step)
}
name += '·' + book.clean.name
return name
}
const bookVersionNames = [
['人', '编'],
['辽', '陕', '鲁', '青'],
['北', '冀', '清', '河'],
['苏', '浙', '沪', '杭', '上海'],
['鄂', '湘', '闽', '湖', '福'],
['重', '渝', '川'],
['粤', '桂', '琼', '广'],
['五四', '六三'],
['大象', '接力', '仁', '译'],
['外', 'W', '牛津', '剑桥', '朗文'],
['新', '起点'],
['大学', '科', '师'],
]
const bookColors = [
{
// 深绿
color: '#21CC18',
bgColor: '#93F28D',
},
{
// 卡其
color: '#C7B767',
bgColor: '#DBD5B8',
},
{
// 深蓝
color: '#04A6FF',
bgColor: '#20CDFF',
},
{
// 浅紫
color: '#967ACC',
bgColor: '#C4B8DB',
},
{
// 米黄色
color: '#FF7438',
bgColor: '#FFC49F',
},
{
// 红褐色
color: '#FF6363',
bgColor: '#FF9F9F',
},
{
// 蓝紫色
color: '#5B5DFF',
bgColor: '#989AFF',
},
{
// 深紫色
color: '#AB5CFF',
bgColor: '#D199FF',
},
{
// 蓝色
color: '#5C89FF',
bgColor: '#99BBFF',
},
{
// 黄色
color: '#E5B000',
bgColor: '#FCEB5B',
},
{
// 粉灰色
color: '#CC7A8D',
bgColor: '#DBC1C7',
},
{
// 粉紫
color: '#FC5EC9',
bgColor: '#FF99E1',
},
{
// 紫色
color: '#FF5CFF',
bgColor: '#FF99FF',
},
{
// 浅绿
color: '#6BD600',
bgColor: '#CCF37C',
},
{
// 天蓝
color: '#00C7D6',
bgColor: '#45ECF5',
},
]
// 年级
const grade_1 = 1
const grade_2 = 2
const grade_3 = 3
const grade_4 = 4
const grade_5 = 5
const grade_6 = 6
const grade_7 = 7
const grade_8 = 8
const grade_9 = 9
const grade_high_1 = 10
const grade_high_2 = 11
const grade_high_3 = 12
const xiaoxue = [grade_1, grade_2, grade_3, grade_4, grade_5, grade_6]
const chuzhong = [grade_7, grade_8, grade_9]
const gaozhong = [grade_high_1, grade_high_2, grade_high_3]
const grade_map: any = {
[grade_1]: '一年级',
[grade_2]: '二年级',
[grade_3]: '三年级',
[grade_4]: '四年级',
[grade_5]: '五年级',
[grade_6]: '六年级',
[grade_7]: '七年级',
[grade_8]: '八年级',
[grade_9]: '九年级',
[grade_high_1]: '高一',
[grade_high_2]: '高二',
[grade_high_3]: '高三',
}
const base_grade_1 = 1
const base_grade_2 = 2
const base_grade_3 = 3
const base_grade_4 = 4
const base_grade_5 = 5
const base_grade_6 = 6
const base_grade_7 = 7
const base_grade_8 = 8
const base_grade_9 = 9
const base_grade_high_1 = 10
const base_grade_high_2 = 11
const base_grade_high_3 = 12
const base_grade_map: any = {
[base_grade_1]: grade_1,
[base_grade_2]: grade_2,
[base_grade_3]: grade_3,
[base_grade_4]: grade_4,
[base_grade_5]: grade_5,
[base_grade_6]: grade_6,
[base_grade_7]: grade_7,
[base_grade_8]: grade_8,
[base_grade_9]: grade_9,
[base_grade_high_1]: grade_high_1,
[base_grade_high_2]: grade_high_2,
[base_grade_high_3]: grade_high_3,
}
export const grades = [
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '1',
baseId: '1',
name: '一年级',
sort: 0,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '2',
baseId: '2',
name: '二年级',
sort: 1,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '3',
baseId: '3',
name: '三年级',
sort: 2,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '4',
baseId: '4',
name: '四年级',
sort: 3,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '5',
baseId: '5',
name: '五年级',
sort: 4,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '6',
baseId: '6',
name: '六年级',
sort: 5,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '7',
baseId: '7',
name: '七年级',
sort: 7,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '8',
baseId: '8',
name: '八年级',
sort: 8,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '9',
baseId: '9',
name: '九年级',
sort: 9,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '10',
baseId: '10',
name: '高一',
sort: 10,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '11',
baseId: '11',
name: '高二',
sort: 11,
delFlag: 0,
},
{
createTime: '2024-08-28 14:57:45',
createUser: '-1',
updateTime: '2024-08-28 14:57:45',
updateUser: '-1',
id: '12',
baseId: '12',
name: '高三',
sort: 12,
delFlag: 0,
},
]
// 阶段
export const stage_xiaoxue = 1
export const stage_chuzhong = 2
export const stage_gaozhong = 3
export const stage_options = [
{
label: '小学',
value: stage_xiaoxue,
},
{
label: '初中',
value: stage_chuzhong,
},
{
label: '高中',
value: stage_gaozhong,
},
]
// 类型
export type SubjectItem = {
subjectId: any
name: string
subject: string
pictureUrl: string
sort: any
}
// 学科
const chinese_subject_id = 1
const math_subject_id = 2
const english_subject_id = 3
export const subject_english = {
subjectId: english_subject_id,
name: '英语',
subject: '英语',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_yingyu.png',
sort: null,
} as SubjectItem
export const subject_chinese = {
subjectId: chinese_subject_id,
name: '语文',
subject: '语文',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_yuwen.png',
sort: null,
} as SubjectItem
export const subject_math = {
subjectId: math_subject_id,
name: '数学',
subject: '数学',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_shuxue.png',
sort: null,
} as SubjectItem
const subject_physics = {
subjectId: 4,
name: '物理',
subject: '物理',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_wuli.png',
sort: null,
} as SubjectItem
const subject_chemistry = {
subjectId: 5,
name: '化学',
subject: '化学',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_huaxue.png',
sort: null,
} as SubjectItem
const subject_bio = {
subjectId: 6,
name: '生物学',
subject: '生物学',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_shengwu.png',
sort: null,
} as SubjectItem
const subject_law = {
subjectId: 7,
name: '政治(道法)',
subject: '政治(道法)',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_fazhi.png',
sort: null,
} as SubjectItem
const subject_history = {
subjectId: 8,
name: '历史',
subject: '历史',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_lishi.png',
sort: null,
} as SubjectItem
const subject_geo = {
subjectId: 9,
name: '地理',
subject: '地理',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_dili.png',
sort: null,
} as SubjectItem
const subject_science = {
subjectId: 10,
name: '科学',
subject: '科学',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_kexue.png',
sort: null,
} as SubjectItem
const subject_history_society = {
subjectId: 11,
name: '历史与社会',
subject: '历史与社会',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_lishiyushehui.png',
sort: null,
} as SubjectItem
const subject_it = {
subjectId: 12,
name: '信息技术',
subject: '信息技术',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_xinxi.png',
sort: null,
} as SubjectItem
const subject_music = {
subjectId: 13,
name: '音乐',
subject: '音乐',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_yinyue.png',
sort: null,
} as SubjectItem
const subject_art = {
subjectId: 14,
name: '美术',
subject: '美术',
pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_meishu.png',
sort: null,
} as SubjectItem
const xiaoxue_subjects = [
subject_english,
subject_chinese,
subject_math,
subject_science,
subject_law,
]
const chuzhong_subjects = [
subject_english,
subject_chinese,
subject_math,
subject_physics,
subject_chemistry,
subject_history,
subject_geo,
subject_bio,
subject_science,
subject_law,
]
const gaozhong_subjects = [
subject_english,
subject_chinese,
subject_math,
subject_physics,
subject_chemistry,
subject_history,
subject_geo,
subject_bio,
subject_law,
subject_it,
// {
// subjectId: 14,
// name: '通用',
// subject: '通用',
// pictureUrl: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/subject_tongyong.png',
// sort: null,
// },
]
export const subjects = [
{
id: chinese_subject_id,
name: '语文',
subject: '语文',
pictureUrl: null,
tableMark: '1',
status: 0,
createUser: null,
updateUser: null,
},
{
id: math_subject_id,
name: '数学',
subject: '数学',
pictureUrl: null,
tableMark: '2',
status: 0,
createUser: null,
updateUser: null,
},
{
id: english_subject_id,
name: '英语',
subject: '英语',
pictureUrl: null,
tableMark: '3',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 4,
name: '物理',
subject: '物理',
pictureUrl: null,
tableMark: '4',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 5,
name: '化学',
subject: '化学',
pictureUrl: null,
tableMark: '5',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 6,
name: '生物学',
subject: '生物学',
pictureUrl: null,
tableMark: '6',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 7,
name: '政治(道法)',
subject: '政治(道法)',
pictureUrl: null,
tableMark: '7',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 8,
name: '历史',
subject: '历史',
pictureUrl: null,
tableMark: '8',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 9,
name: '地理',
subject: '地理',
pictureUrl: null,
tableMark: '9',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 10,
name: '科学',
subject: '科学',
pictureUrl: null,
tableMark: '10',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 11,
name: '历史与社会',
subject: '历史与社会',
pictureUrl: null,
tableMark: '11',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 12,
name: '信息技术',
subject: '信息技术',
pictureUrl: null,
tableMark: '12',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 13,
name: '音乐',
subject: '音乐',
pictureUrl: null,
tableMark: '13',
status: 0,
createUser: null,
updateUser: null,
},
{
id: 14,
name: '美术',
subject: '美术',
pictureUrl: null,
tableMark: '14',
status: 0,
createUser: null,
updateUser: null,
},
]
export type Subject = {
id?: number
subjectId: number
name: string
subject: string
pictureUrl: string
}
// 获得真正的年级
export const getRealGrade = (grade: any) => {
grade = Number(grade)
if (grade <= base_grade_high_3) {
return base_grade_map[grade]
}
return grade
}
export const getStageByGrade = (grade: any) => {
grade = getRealGrade(grade)
if (gaozhong.includes(grade)) {
return stage_gaozhong
}
if (chuzhong.includes(grade)) {
return stage_chuzhong
}
return stage_xiaoxue
}
export const getSubjectById = (subjectId: any): Subject => {
subjectId = Number(subjectId)
for (const subject of subjects) {
if (subject.id === subjectId) {
return subject as any
}
}
return undefined as any
}
export const getSubjectsByStage = (stage: any): Subject[] => {
if (!stage) return []
stage = Number(stage)
if (stage === stage_xiaoxue) return xiaoxue_subjects
if (stage === stage_chuzhong) return chuzhong_subjects
return gaozhong_subjects
}
export const getSubjectsByGrade = (grade: any): Subject[] => {
if (!grade) return []
grade = getRealGrade(grade)
if (xiaoxue.includes(grade)) return xiaoxue_subjects
if (chuzhong.includes(grade)) return chuzhong_subjects
return gaozhong_subjects
}
// 根据班级名称获得年级
export const getGradeByClassName = (className: string) => {
for (const grade in grade_map) {
const gradeName = grade_map[grade]
if (className.includes(gradeName)) return grade
}
return undefined
}
// 根据班级名称获得学科
export const getSubjectsByClassName = (className: string) => {
return getSubjectsByGrade(getGradeByClassName(className))
}

View File

@ -0,0 +1,83 @@
import { http } from '../http'
// 词典
interface SearchWordParams {
keyword: string
current?: number
size?: number
[key: string]: any
}
interface SearchHistoryParams {
current?: number
size?: number
stageState?: number
[key: string]: any
}
interface AddSearchHistoryData {
wordId: string
searchValue: string
stageState: number
wordSpell: string
wordZnDesc?: string
spell?: string
}
/**
* @description:
* @param {SearchWordParams} params
* @return {*}
*/
const searchWordApi = (params: SearchWordParams) => {
return http.request({
url: '/word/search',
params,
})
}
/**
* @description:
* @param {SearchHistoryParams} params
* @return {*}
*/
const getSearchHistoryApi = (params: SearchHistoryParams) => {
return http.request({
url: '/searchLog/list',
params,
})
}
/**
* @description: -
* @param {AddSearchHistoryData} data
* @return {*}
*/
const addSearchHistoryApi = (data: AddSearchHistoryData) => {
return http.request({
url: '/searchLog',
data,
method: 'POST',
})
}
/**
* @description: -
* @param {*} params
* @return {*}
*/
const deleteSearchHistoryApi = () => {
return http.request({
url: '/searchLog/deleteAll',
method: 'DELETE',
})
}
const dictionariesApi = {
searchWordApi,
getSearchHistoryApi,
addSearchHistoryApi,
deleteSearchHistoryApi,
}
export default dictionariesApi

46
src/api/subject/doWork.ts Normal file
View File

@ -0,0 +1,46 @@
import { http } from '../http'
import { get, post } from '../request'
const doWorkApi = {
/*
**
*/
// 训练记录表-新增
addTrain: async (data: anyObj) => {
return await post<any>('/userTrainRecord', data)
},
// 训练记录表-id查询
getTrainSingle: async (id: any): Promise<any> => {
return await get('/userTrainRecord/' + id)
},
// 训练记录表-查询列表
getTrainRecordList: async (params: anyObj) => {
return await get<any>('/userTrainRecord/list', params)
},
// 训练记录表-查询列表
submitTrainRecord: async (data: anyObj) => {
return await post('/userTrainRecord/submitTrainRecord', data)
},
// 题目反馈表-新增
submitTitleFeedback: (data: anyObj) =>
http.request({
url: '/titleFeedback',
data,
method: 'POST',
}),
// 用户组卷表-开始组卷
startGroupPaper: (data: anyObj) =>
http.request({
url: '/userPaper/start',
data,
method: 'POST',
timeout: 28000,
}),
}
export default doWorkApi

View File

@ -0,0 +1,71 @@
import { http } from '../http'
const happyReadApi = {
/**
*
*/
readingLearnRecord: (data: anyObj) =>
http.request({
url: '/readingLearnRecord/saveRecord',
method: 'POST',
data,
}),
// 自然拼读关卡-开始练习(智能选择关卡)
getNaturalLangBeginTrain: () =>
http.request({
url: '/naturalLangLevel/getLevelContentById/beginTrain',
}),
// 句子单词关联-获取该单词关联关卡练习(若返回空则表示该单词没有相关关卡)
getTestOrWordDetail: (data: anyObj) =>
http.request({
url: '/textSentenceWord/queryThroughLevelTrain',
method: 'POST',
data,
}),
/**
*
*/
// 短篇阅读获取首页数据
shortQueryFirstPageInfo: () =>
http.request({
url: '/shortTextReadingLevel/queryFirstPageInfo',
}),
// 短篇阅读开始练习
shortReadBeginTrain: (params?: anyObj) =>
http.request({
url: '/shortTextReadingLevel/beginTrain',
params,
}),
// sessionid 生成
getReadSessionid: () =>
http.request({
url: '/shortTextReadingLevel/sessionIdSender',
}),
// 阶数选择
getReadGradeList: () =>
http.request({
url: '/shortTextReadingLevel/gradleSelector',
}),
// 获取答题列表
getReadRecoredList: (params?: anyObj) =>
http.request({
url: '/readingLearnRecord/list',
params,
}),
// 获取答题详情
getReadRecoredDetail: (params?: anyObj) =>
http.request({
url: '/shortTextReadingLevel/detailRecord',
params,
}),
}
export default happyReadApi

View File

@ -0,0 +1,48 @@
import { get, post } from '../../request'
import { PaperRecord, HomeworkExam, SubjectPaperRecord } from './type'
export * from './type'
const getUnfinishedHomework = async () => {
const records = await get<PaperRecord[]>('/school/paper/userRecord/listExamRecord')
// 利用map优化
const map = new Map<string, SubjectPaperRecord>()
records.forEach(item => {
const record = map.get(item.subjectId)
if (record) {
record.records.push(item)
} else {
map.set(item.subjectId, {
subjectId: item.subjectId,
subjectName: item.subjectName,
records: [item],
})
}
})
return map
}
const startExam = async (recordId: any) => {
return post<HomeworkExam>(`/school/paper/exam`, { recordId })
}
const homeworkApi = { getUnfinishedHomework, startExam }
export default homeworkApi
// 保存题目
const saveRecordAnswer = async (data: any) => {
return post<HomeworkExam>(`/school/paper/saveRecordAnswer`, data)
}
export { saveRecordAnswer }
// 提交作业
const submitExam = async (data: any) => {
return post<HomeworkExam>(`/school/paper/submit`, data)
}
export { submitExam }
// 作业记录列表
const getListCompleteRecord = async (data: any) => {
return get<HomeworkExam>(`/school/paper/userRecord/listCompleteRecord`, data)
}
export { getListCompleteRecord }

View File

@ -0,0 +1,269 @@
/**
*
*/
export enum TestTypeEnum {
/** 单元测试 */
UNIT_TEST = 1,
/** 周测 */
WEEKLY_TEST = 2,
/** 月测 */
MONTHLY_TEST = 3,
/** 随堂练习 */
CLASS_PRACTICE = 4,
/** 课前预习 */
PRE_CLASS_PREVIEW = 5,
}
export interface SubjectPaperRecord {
/** 学科id */
subjectId: string
/** 学科名称 */
subjectName: string
/** 试卷记录列表 */
records: PaperRecord[]
}
/**
*
*/
export interface PaperRecord {
/** 主键 */
id: number
/** 学校id */
schoolId: number
/** 发布人 */
publisherName: string
/** 用户id */
userId: number
/** 用户名称 */
userName: string
/** 试卷id */
paperId: number
/** 试卷名称 */
paperName: string
/** 学科id */
subjectId: string
/** 学科名称 */
subjectName: string
/** 总分 */
totalScore: number
/** 得分 */
score: number
/** 作答时长 */
costTime: number
/** 提交时间 */
submitTime: string
/** 状态0-未作答1-已作答 */
status: 0 | 1
/** 测试类型 1: 单元测试 2: 周测 3 月测, 4 随堂练习 5 课前预习 */
type: TestTypeEnum
/** 创建时间 */
createTime: string
endTime: string
remark: string
}
/**
*
*/
export type OptionList = string[][]
/**
*
*/
export interface SubjectTitleVo {
/** 题目ID */
id: string
/** 学校ID */
schoolId: string
/** 教材ID */
textBookId: string
/** 教材名称 */
textBookName: string
/** 学段父级ID */
stagePid: string
/** 学段名称 */
stageName: string
/** 学科ID */
subjectId: string
/** 学科名称 */
subjectName: string
/** 基础ID */
baseId: string | null
/** 原始ID */
originId: string
/** 题目标题 */
title: string
/** 题目类型 */
titleType: number
/** 父级ID */
pid: string | null
/** 考试类型 */
examType: number
/** 来源ID */
sourceId: string | null
/** 来源名称 */
sourceName: string
/** 题目渠道类型 */
titleChannelType: string
/** 题目渠道类型名称 */
titleChannelTypeName: string
/** 题目类型基础ID */
titleTypeBaseId: string
/** 难度 */
difficulty: number
/** 地址ID */
addrId: string | null
/** 排序 */
sort: number
/** 语音URL */
voiceUrl: string | null
/** 图片URL */
pictureUrl: string | null
/** 题目解析 */
titleAnalyze: string
/** 题目解析图片 */
titleAnalyzePicture: string | null
/** 题目解析JSON */
titleAnalyzeJson: string | null
/** 答案 */
answer: string
/** 年份 */
years: string
/** 省份 */
provinces: string
/** 题目来源IDs */
titleSourceIds: string | null
/** 关联试卷 */
linkPaper: number
/** 分组数量 */
groupCount: number
/** 简化标题 */
simpTitle: string | null
/** 题目数据 */
titleData: string | null
/** 解析数据 */
analyzeData: string | null
/** 方法 */
method: string | null
/** 讨论 */
discuss: string | null
/** 序号 */
seq: number
/** 状态 */
status: number
/** 删除标志 */
delFlag: number
/** 选项列表 */
optionList: OptionList
/** 项目ID */
itemId: string
/** 分数 */
score: number | null
/** 提交答案 */
submitAnswer: string | null
/** 提交答案图片 */
submitAnswerPic: string | null
/** 正确结果 */
correctResult: string | null
/** 题干类型: 1原始题 2选择题 3复合题 */
stemType: number
/** 题干HTML */
stemHtml: string | null
/** 选择题选项HTML */
optionHtml: string | null
/** 答案HTML */
answerHtml: string | null
/** 子题题干HTML */
stemSqs: string | null
/** 小题选项数组JSON */
optionSqs: string | null
/** 小题答案 */
answerSqs: string | null
}
/**
*
*/
export interface HomeworkExam {
/** 试卷ID */
paperId: string
paperName: string
/** 记录ID */
recordId: string
/** 学科题目列表 */
titleVoList: SubjectTitleVo[]
}

View File

@ -0,0 +1,67 @@
import { http } from '../http'
import { get } from '../request'
const languageSenseApi = {
/**
* id查询例句
*/
getSentenceById: (grade: number) => {
return http.request({
url: '/userSentenceLearn/sentence',
timeout: 5000,
method: 'POST',
params: {
grade,
},
})
},
/**
* id查询例句
*/
getSentenceBySessionId: (sessionId: string) => {
return http.request({
url: `/userSentenceLearn/againLearn/${sessionId}`,
})
},
/**
*
*/
saveSentenceRecord: (data: anyObj) => {
return http.request({
url: '/userSentenceRecord/record',
data,
method: 'POST',
})
},
/**
*
*/
saveSentenceRecordHistory: (data: anyObj) => {
return http.request({
url: '/userSentenceLearn/history',
data,
method: 'POST',
})
},
/*
**
*/
// 根据时间维度获取统计数据
queryStatByTimeFlag: (data: anyObj) =>
http.request({
url: '/userSentenceLearn/queryStatByTimeFlag',
method: 'POST',
data,
}),
// 获得语感阶段列表
getSenseStagesApi: async (): Promise<any[]> => {
return await get('/sysDictType/listSysDictData/grade_label')
},
}
export default languageSenseApi

View File

@ -0,0 +1,132 @@
import { http } from '../http'
const learnWordApi = {
// 获取阶段
getStageList: (params: object | any) => {
return http.request({
url: '/stage/list',
params,
})
},
// 获取混合闯关首页内容
getMixIntrudeIndex: (stageId: string) => {
return http.request({
url: `/intrudeLevel/mixIntrudeIndex/${stageId}`,
})
},
// 根据教材id 获取混合闯关首页内容
getMixIntrudeIndexFromTextBookId: (textbookId: string, params: object) => {
return http.request({
url: `/intrudeLevel/intrudeIndex/textbook/${textbookId}`,
params,
})
},
// 获取闯关首页内容
getLearnWordInfo: (params: object | any) => {
return http.request({
url: `/intrudeLevel/intrudeIndex`,
params,
})
},
/**
* @description: --
* @return {*}
*/
getSimpleTopic: (levelId: string, params: object | any) => {
return http.request({
url: `/intrudeLevel/simpleLevelTile/${levelId}`,
params,
})
},
/**
* @description: -
* @param {*} data
* @return {*}
*/
postSimpleRecord: (data: object | any) => {
// return request('/intrudeLevel/simpleRecord', 'POST', data);
return http.request({
url: `/intrudeLevel/saveAndReturnSimpleResult`,
method: 'POST',
timeout: 8000,
data,
})
},
/**
* @description: ,
* @param {*} data
* @return {*}
*/
postWordRecord: (data: object | any) => {
// return request('/intrudeLevel/wordRecord', 'POST', data);
return http.request({
url: `/intrudeLevel/wordRecordV2`,
method: 'POST',
data,
})
},
/**
* @description: --
* @return {*}
*/
getHardTopic: (levelId: string, data: object | any) => {
return http.request({
url: `/intrudeLevel/hardLevelTile/${levelId}`,
data,
})
},
/**
* @description: -
* @param {*} data
* @return {*}
*/
postHardRecord: (data: object | any) => {
// return request('/intrudeLevel/hardRecord', 'POST', data);
return http.request({
url: `/intrudeLevel/saveAndReturnHardResult`,
method: 'POST',
timeout: 8000,
data,
})
},
/**
* @description: 使 1使 2使
* @param {*} data
* @return {*}
*/
postUseItems: (data: object | any) => {
return http.request({
url: `/sysUser/useItems`,
method: 'POST',
data,
})
},
/* ----------------------------------------- 测试用 api ----------------------------------------- */
simpleRecordTest: (data: object | any) => {
return http.request({
url: `/intrudeLevel/simpleRecordTest`,
method: 'POST',
data,
})
},
hardRecordTest: (data: object | any) => {
return http.request({
url: `/intrudeLevel/hardRecordTest`,
method: 'POST',
data,
})
},
}
export default learnWordApi

View File

@ -0,0 +1,21 @@
import { get } from '../request'
const questionExplainApi = {
/** 获取我的班级信息 */
myClass: async () => {
return await get<any>('/school/questionExplain/myClass')
},
/** 根据发布记录ID获取试卷详情讲解页面使用 */
getPaperDetailByReleaseRecordId: async (recordId: string) => {
return await get<any>('/school/paper/getPaperDetailByReleaseRecordId', { recordId })
},
/** 获取当前班级的讲解状态 */
current: async (classId: string) => {
return await get<{ explaining: boolean; questionId: string; recordId: number }>(
'/school/questionExplain/current',
{ classId },
)
},
}
export default questionExplainApi

View File

@ -0,0 +1,29 @@
import { http } from '../http'
/*
**
*/
const sentenceCollectApi = {
// 获取当前用户收藏的句子
getLangSenseCollectList: (params: anyObj) =>
http.request({
url: '/langSenseSentenceCollect/queryCurrentUserCollect',
params,
}),
// 收藏句子
collectSentence: (data: anyObj) =>
http.request({
url: '/langSenseSentenceCollect/collectSentence',
method: 'POST',
data,
}),
// 取消收藏句子
cancelCollectSentence: (data: anyObj) =>
http.request({
url: '/langSenseSentenceCollect/cancelCollectSentence',
method: 'POST',
data,
}),
}
export default sentenceCollectApi

328
src/api/subject/subject.ts Normal file
View File

@ -0,0 +1,328 @@
import { http } from '../http'
import { get, type PageResponse } from '../request'
export type GetSubjectTextBookVersionApi = {
subjectId: any
gradeId?: any
stageId?: any
size?: any
resource: any
}
interface Video {
// 根据实际视频数据结构补充
[key: string]: any
}
interface Knowledge {
id: any
baseId: any
pid: any
pids: string
name: string
status: number
createTime: string
childrenList: any[] // 可根据实际数据结构进一步定义
children: any[] | null
hasVideoFlag: number | null
videoList: Video[] | null
}
interface ChapterVideo {
// 根据实际章节视频数据结构补充
[key: string]: any
}
export interface Chapter {
id: any
subjectId: number
pid: any
pids: string
name: string
sort: number
learnFlag: number
hasVideoFlag: number
chapterVideoVoList: ChapterVideo[] | null
children: Chapter[] // 递归类型,表示子章节
knowledgeVoList: Knowledge[]
correctCount: any
maxCorrectCount: any
lastCorrectCount: any
progressText: any
progressClass: any
}
interface SubjectChapterTreeParams {
textBookId: any
[key: string]: any
}
interface UserTextBookListParams {
subjectId?: any
gradeId?: any
size?: number
[key: string]: any
}
interface SaveUserTextBookData {
textBookId: any
subjectId: any
gradeId: any
[key: string]: any
}
interface UpdateUserTextBookData {
id: any
[key: string]: any
}
interface SubjectKnowledgeTreeParams {
chapterId: any
[key: string]: any
}
interface SubjectChapterVideosParams {
chapterId: any
[key: string]: any
}
interface SubjectKnowledgeVideosParams {
knowledgeId: any
[key: string]: any
}
export interface Color {
color: string
bgColor: string
}
export interface Clean {
name: string
tags: string[]
all_tags: string[]
fullName?: string
color: Color
}
export interface TextBook {
id: any
textBookId: any
baseId: any
gradeId: any
subjectId: any
bookVersionId: any
name: string
shortName: string
textBookName: string
displayName: string
businessFlag: string
addFlag: number
progress: any
bookVersionName: any
versionName: any
currentChapterId: any
chapterBaseId: any
chapterTreeNodeList: any
clean: Clean
locked: boolean
syncFlag: number
version: BookVersion
bookType: number
currentChapterIds: any
}
// 学校教材
export const book_type_school = 1
// 其他教材
export const book_type_other = 2
export const book_sync_locked = 1
export const book_sync_unlocked = 0
export interface BookVersion {
id: any
baseId: any
name: string
shortName: string
delFlag: number
textBookVoList: TextBook[]
clean: Clean
}
export interface PracticeModule {
/**
*
*/
id: number
/**
* id
*/
schoolId: number
/**
*
*/
sort: number
/**
*
*/
name: string
/**
* 0 1
*/
status: 0 | 1
/**
* 01
*/
delFlag: 0 | 1
}
export const subjectApi = {
// 获取教材出版社
getSubjectTextBookVersion: async (params: GetSubjectTextBookVersionApi) => {
const res = (await get('/subjectTextBookVersion/list', {
size: -1,
...params,
})) as any
if (Array.isArray(res?.rows)) return res.rows
if (Array.isArray(res?.data?.rows)) return res.data.rows
if (Array.isArray(res)) return res
return []
},
// 获取章节
getSubjectChapterTree: (params: SubjectChapterTreeParams) => {
return http.request({
url: '/subjectChapter/tree',
params,
})
},
// 获取用户教材
getUserTextBookList: async (params: UserTextBookListParams) => {
if (!params.size) {
params.size = -1
}
return await get<any>('/userTextBook/list', params)
},
// 添加用户教材
saveUserTextBook: (data: SaveUserTextBookData) => {
return http.request({
url: '/userTextBook',
method: 'post',
data,
})
},
// 更新用户教材
updateUserTextBook: (data: UpdateUserTextBookData) => {
return http.request({
url: '/userTextBook',
method: 'put',
data,
})
},
// 删除用户教材
delUserTextBookToId: (id: any) => {
return http.request({
url: `/userTextBook/${id}`,
method: 'delete',
})
},
// 学科知识点-查询章节下的知识点树
getSubjectKnowledgeTree: (params: SubjectKnowledgeTreeParams) => {
return http.request({
url: `/subjectKnowledge/tree`,
params,
})
},
// 学科章节-视频表-查询列表
getSubjectChapterVideos: (params: SubjectChapterVideosParams) =>
http.request({
url: '/subjectChapterVideo/list',
params,
}),
// 学科知识点-视频表-查询列表
getSubjectKnowledgeVideos: (params: SubjectKnowledgeVideosParams) =>
http.request({
url: '/subjectKnowledgeVideo/list',
params,
}),
// 用户教材表-修改当前所处章节
updateCurrentChapter: (data: anyObj) =>
http.request({
url: '/userTextBook/updateCurrentChapter',
method: 'post',
data,
}),
/**
*
* @param id
* @returns
*/
getTextBookById: async (id: string) => {
return await get<TextBook>(`/userTextBook/${id}`)
},
/**
*
* @param params
* @returns
*/
getPracticeModuleList: async (params: any) => {
return await get<PageResponse<PracticeModule>>('/school/practiceModule/list', params)
},
/*
**
*/
// 用户错题本表-查询列表
getWrongQuesList: (params: anyObj) =>
http.request({
url: '/userErrorEdition/list',
params,
}),
// 用户错题本表-查询列表
getUserErrorInfo: (params: anyObj) =>
http.request({
url: '/userErrorEdition/getUserErrorInfo',
params,
}),
/*
** ai精准学
*/
// 用户知识点表-id查询
getSujectKnowledgeStatistics: (params: anyObj) =>
http.request({
url: '/userKnowledge/getSujectKnowledgeStatistics',
params,
}),
// 用户组卷表-查询列表
getGroupPaperList: (params: anyObj) =>
http.request({
url: '/userPaper/list',
params,
}),
// 用户组卷表-id查询
getGroupPaperDetail: (id: any) =>
http.request({
url: '/userPaper/' + id,
}),
}
export default subjectApi

View File

@ -0,0 +1,31 @@
import { http } from '../http'
interface StrangeWordParams {
current?: number
size?: number
textbookId?: string
[key: string]: any
}
/*
**
*/
const wordBookApi = {
// 获取生词本
getStrangeWord: (params: StrangeWordParams) =>
http.request({
url: '/userStrangeWord/list',
params,
}),
// 生词本删除
deleteStrangeWord: (id: any) =>
http.request({
url: `/userStrangeWord/deleteBatch?ids=${id}`,
method: 'DELETE',
}),
}
export default wordBookApi
export type { StrangeWordParams }

View File

@ -0,0 +1,39 @@
import { http } from '../http'
interface GetWordParams {
spell?: string
id?: any
wordId?: string
[key: string]: any
}
interface OperateTagData {
id: any
meaningId?: string
levelId?: string
knowFlag?: number | null
strangeFlag?: number
wordId?: string
}
// 获取单词详情
const getWord = (params: GetWordParams) =>
http.request({
url: '/word/get',
params,
})
const operateTag = (data: OperateTagData) =>
http.request({
url: '/word/operate',
method: 'POST',
data,
})
const wordDetailApi = {
getWord,
operateTag,
}
export default wordDetailApi
export type { GetWordParams, OperateTagData }

View File

@ -0,0 +1,230 @@
import { http } from '../http'
import { only_last_id } from '../only-last'
import { get } from '../request'
const extractList = (res: any): any[] => {
if (Array.isArray(res)) return res
if (res && typeof res === 'object') {
return res.records || res.rows || res.list || []
}
return []
}
const processMemoryTips = (tips: any) => {
if (!tips) {
return []
}
if (typeof tips === 'string') {
try {
const parsedTips = JSON.parse(tips)
if (Array.isArray(parsedTips)) {
return _processMemoryTipsArray(parsedTips)
} else {
return [parsedTips]
}
} catch (e) {
return [tips]
}
} else if (Array.isArray(tips)) {
return _processMemoryTipsArray(tips)
} else {
return [tips]
}
}
const _processMemoryTipsArray = (memoryTips: any[]) => {
return memoryTips.map(tip => {
if (typeof tip === 'string') {
return tip
}
return tip
})
}
const wordTableApi = {
/**
* @description:
* @param {*} params
* @return {*}
*/
getWordTableStage: () => {
return http.request({
url: `/vocabulary/helpVocabulary`,
})
},
/**
* @description:
* @param {*} params
* @return {*}
*/
getWordTableFamiliarity: () => {
return http.request({
url: `/wordTable/graspEnum`,
})
},
/**
* @description:
* @param {WordTableStageParams} params
* @return {*}
*/
getWordTableStageList: (params: WordTableStageParams) => {
return http.request({
url: `/wordTable/wordTableStage`,
params,
[only_last_id]: 'word-table-list',
})
},
/**
* @description:
* @param {*} params
* @return {*}
*/
getTextbookStage: () => {
return http.request({
url: `/textbookStage/wordTableUseList`,
})
},
/**
* @description:
* @param {*} params
* @return {*}
*/
getTextbookVersion: async (stageId: any): Promise<any[]> => {
return await get(`/textbookVersion/list/${stageId}`)
},
/**
* @description:
* @param {*} params
* @return {*}
*/
getTextbookList: async (versionId: any): Promise<any[]> => {
return await get(`/textbook/list?versionId=${versionId ?? ''}`)
},
getXkwStageList: async (): Promise<any[]> => {
const res = await get<any>(`/xkw/stage/list`, { size: -1, status: 0 })
return extractList(res)
},
getXkwCourseList: async (stageId: any): Promise<any[]> => {
const res = await get<any>(`/xkw/course/list`, { size: -1, status: 0, stageId })
return extractList(res)
},
getXkwTextbookVersionList: async (courseId: any): Promise<any[]> => {
const res = await get<any>(`/xkw/textbookVersion/list`, { size: -1, status: 0, courseId })
return extractList(res)
},
getXkwTextbookList: async (versionId: any): Promise<any[]> => {
const res = await get<any>(`/xkw/textbook/list`, { size: -1, status: 0, versionId })
return extractList(res)
},
getXkwTextbookCatalogList: async (textbookId: any): Promise<any[]> => {
const res = await get<any>(`/xkw/textbookCatalog/list`, { parentId: 0, textbookId, size: -1 })
return extractList(res)
},
/**
* @description:
* @param {*} params
* @return {*}
*/
getChapterList: async (textbookId: any): Promise<any[]> => {
return await get(`/textbookChapter/data/list`, { textbookId })
},
/**
* @description:
* @param {TextbookWordParams} params
* @return {*}
*/
getTextbookWord: (params: TextbookWordParams) => {
return http.request({
url: `/wordTable/wordTableTextbook`,
params,
[only_last_id]: 'word-table-list',
})
},
/**
* @description:
* @param {WordCardDetailParams} params
* @return {*}
*/
getWordCardDetail: async (params: WordCardDetailParams): Promise<any> => {
const data = (await get(`/wordTable/wordDetail`, params)) as any
data.memoryTips = JSON.parse(data.memoryTips)
processMemoryTips(data.memoryTips)
return data
},
/**
* @description:
* @param {UserWordFilterData} data
* @return {*}
*/
saveUserWordFilterRecord: (data: UserWordFilterData) => {
return http.request({
url: `/sysUser/setConfigInfo`,
data,
method: 'POST',
})
},
/**
* @description:
* @param {UserWordFilterParams} params
* @return {*}
*/
getUserWordFilterRecord: (params: UserWordFilterParams) => {
return http.request({
url: `/sysUser/getConfigInfo`,
params,
})
},
}
interface WordTableStageParams {
current?: number
size?: number
[key: string]: any
}
interface TextbookWordParams {
textbookId?: string
chapterId?: string
[key: string]: any
}
interface WordCardDetailParams {
wordId: string
[key: string]: any
}
interface UserWordFilterData {
textbookId?: string
chapterId?: string
familiarityId?: string
[key: string]: any
}
interface UserWordFilterParams {
textbookId?: string
[key: string]: any
}
export default wordTableApi
export type {
WordTableStageParams,
TextbookWordParams,
WordCardDetailParams,
UserWordFilterData,
UserWordFilterParams,
}

150
src/api/subject/wordTest.ts Normal file
View File

@ -0,0 +1,150 @@
import { http } from '../http'
import { get, post, put } from '../request'
/**
* @description: -
* @param {*} params
* @return {*}
*/
interface SaveWordTestData {
testType?: number
textbookId?: string
chapterId?: string
[key: string]: any
}
interface CompleteWordTestData {
id: any
score?: number
[key: string]: any
}
interface GetWordTestData {
textbookId?: string
chapterId?: string
testType?: number
questionCount?: number
[key: string]: any
}
interface WordTestRecordParams {
current?: number
size?: number
[key: string]: any
}
const processMemoryTips = (tips: any) => {
if (!tips) return
for (const tip of tips) {
if (!tip.details) continue
let text = ''
let begin = false
for (const char of tip.details) {
if (char === "'") {
if (begin) {
begin = false
text += '</strong>'
text += char
} else {
begin = true
text += char
text += '<strong>'
}
} else {
text += char
}
}
tip.details = text
}
}
const _processMemoryTipsArray = (memoryTips: any[]) => {
if (!memoryTips?.length) return
for (const item of memoryTips) {
item.memoryTips = JSON.parse(item.memoryTips)
processMemoryTips(item.memoryTips)
}
}
const wordTestApi = {
/**
* @description: -
* @param {*} params
* @return {*}
*/
saveWordTest: (data: SaveWordTestData) => {
return http.request({
url: `/wordTestRecord/saveWordTest`,
method: 'POST',
data,
})
},
/**
* @description: -
* @param {*} params
* @return {*}
*/
completeWordTest: async (data: CompleteWordTestData) => {
return await put(`/wordTestRecord/completeWordTest`, data)
},
/**
* @description: -
* @param {GetWordTestData} data
* @return {*}
*/
getWordTestWordsV2: async (data: GetWordTestData): Promise<any> => {
const res = (await get<any>('/wordExamRecord/getWords', data)) as any
_processMemoryTipsArray(res.chooseTitle)
_processMemoryTipsArray(res.spellTitle)
return res
},
/**
* @description: -
* @param {GetWordTestData} data
* @return {*}
*/
getWordTestWords: async (data: GetWordTestData): Promise<any> => {
const res = (await post('/wordTestRecord/getWordTestWords2', data)) as any
_processMemoryTipsArray(res.chooseTitle)
_processMemoryTipsArray(res.spellTitle)
return res
},
/**
* @description: --
* @param {WordTestRecordParams} params
* @return {*}
*/
getWordTestRecordList: (params: WordTestRecordParams) => {
return http.request({
url: `/wordTestRecord/list`,
params,
})
},
/**
* @description: --
* @param {*} params
* @return {*}
*/
getWordTestRecordDetail: (id: any) => {
return http.request({
url: `/wordTestRecord/${id}`,
})
},
checkMedalStatus: (data: { checkFlag: 0 | 1; ids: string }) => {
return http.request({
url: `/userMedal/updateCheck`,
method: 'POST',
data,
})
},
}
export default wordTestApi
export type { SaveWordTestData, CompleteWordTestData, GetWordTestData, WordTestRecordParams }

View File

@ -0,0 +1,136 @@
import { get, post } from '../request'
import type { PageResponse } from '../request'
export interface QuestionTitle {
actualScore: number
aiAnalyze: string
aiAnswer: string
answer: string
answerHtml: string
answerSqs: string
correctResult: number
courseId: number
delFlag: number
difficulty: number
difficultyCode: number
difficultyLevel: string
explanation: string
id: string
itemId: number
media: number
optionHtml: string
optionSqs: string
point: string
schoolId: number
score: number
sort: number
status: number
stem: string
stemHtml: string
stemSqs: string
stemType: number
submitAnswer: string
submitAnswerPic: string
textBookId: number
typeId: string
typeName: string
year: number
}
export interface WrongQuestionItem {
id: number
questionId: string
userId: number
subjectId: number
textbookId: number
chapterId: number
courseId: number
errorCount: number
errorType: number
masteryLevel: number
status: number
delFlag: number
firstErrorTime: string
lastErrorTime: string
lastCorrectTime: string
questionTitle: QuestionTitle
}
export interface WrongQuestionParams {
current: number
size: number
chapterId?: number
courseId?: number
startTime?: string
endTime?: string
subjectId?: number
}
export interface RetryStartParams {
chapterId?: number
courseId?: number
questionCount?: number
subjectId?: number
textbookId?: number
}
export interface RetryStartResult {
costTime: number
paperId: number
paperName: string
recordId: number
remark: string
titleVoList: QuestionTitle[]
}
export interface FilterSubject {
id: number
name: string
}
export interface FilterTextbook {
id: number
name: string
}
export interface FilterChapter {
id: number
name: string
parentId: number
textbookId: number
type: string
}
const wrongQuestionApi = {
getList: async (params: WrongQuestionParams): Promise<PageResponse<WrongQuestionItem>> => {
return await get<PageResponse<WrongQuestionItem>>('/xkw/wrong/question/filter/page', params)
},
retryStart: async (params: RetryStartParams): Promise<RetryStartResult> => {
return await post<RetryStartResult>('/xkw/wrong/question/retry/start', params)
},
similarRecommend: async (params: { questionId: string }): Promise<RetryStartResult> => {
return await post<RetryStartResult>('/school/paper/similarPractice/start', params)
},
getFilterSubjects: async (): Promise<FilterSubject[]> => {
return await get<FilterSubject[]>('/xkw/wrong/question/filter/subjects')
},
getFilterTextbooks: async (params: { subjectId: number }): Promise<FilterTextbook[]> => {
return await get<FilterTextbook[]>('/xkw/wrong/question/filter/textbooks', params)
},
getTextbookCatalogList: async (params: {
textbookId: number
size?: number
}): Promise<FilterChapter[]> => {
const res = await get<any>('/xkw/textbookCatalog/list', {
textbookId: params.textbookId,
size: params.size ?? -1,
})
if (Array.isArray(res)) return res
if (res && typeof res === 'object') {
return res.rows || res.records || res.list || []
}
return []
},
}
export default wrongQuestionApi

View File

@ -0,0 +1,21 @@
import { http } from '../http'
const activityApi = {
// 获取用户活动列表
getUserActivityList: (params?: anyObj) => {
return http.request({
url: '/personal/getUserActivityTask',
params,
})
},
// 打开活动奖励
openActivityReward: (params?: anyObj) => {
return http.request({
url: '/personal/openActivityReward',
params,
})
},
}
export default activityApi

View File

@ -0,0 +1,26 @@
import { http } from '../http'
const announcementApi = {
/**
*
*/
getAnnouncementListApi: (data: anyObj) => {
return http.request({
url: '/userAnnouncement/getAnnouncementTitleByPage',
method: 'POST',
data,
})
},
/**
*
*/
updateAnnouncemenStatusApi: (id: any) => {
return http.request({
url: `/userAnnouncement/updateReadStatusByAnnounceId/${id}`,
method: 'POST',
})
},
}
export default announcementApi

324
src/api/system/homeWork.ts Normal file
View File

@ -0,0 +1,324 @@
import { http } from '../http'
import { get, post } from '../request'
// import { getCache } from '@/utils';
const config = {
baseURL: '/api/main',
}
const homeWorkApi = {
/**
* @description:
* @param {*} params
* @return {*}
*/
getSubjectList: (params: object) =>
http.request({
url: '/sc/job/getSubjectList',
params,
...config,
}),
/**
* @description:
* @param {*} params
* @return {*}
*/
getJobList: async (params: object) => await get<any>('/sc/job/list', params),
// /**
// * @description: 客户端学生作业
// * @param {*} params
// * @return {*}
// */
// getJobTittle: async (params: object) =>
// (
// await http.request({
// url: '/sc/job/getJobTittle',
// params,
// ...config,
// })
// ).data,
/**
* @description:
* @param {*} data
* @return {*}
*/
getPaperRecordDetail: async (data: object) =>
(
await http.request({
url: '/school/paper/getPaperRecordDetail',
data,
method: 'POST',
...config,
})
).data,
/**
* @description: id
* @param {*} params
* @return {*}
*/
submitCurrentTitleId: (data: anyObj) =>
http.request({
url: `/sc/jobStu/submitCurrentTitleId?jobStuId=${data.jobStuId}&currentTitleId=${data.currentTitleId}&costTime=${data.costTime}`,
method: 'POST',
...config,
}),
/**
* @description: -
* @param {*} params
* @return {*}
*/
submitAnswer: (data: anyObj) =>
http.request({
url: '/sc/jobStuTitle/submitAnswer',
method: 'POST',
data,
...config,
}),
/**
* @description:
* @param {*} params
* @return {*}
*/
submitPaper: async (data: anyObj) =>
await http.request({
url: '/sc/jobStu/submit',
method: 'POST',
data,
...config,
}),
/**
* @description:
* @param {*} params
* @return {*}
*/
getAnalyseSubjectList: (params: object) =>
http.request({
url: '/sc/job/statisticalSubject',
params,
...config,
}),
/**
* @description:
* @param {*} id
* @return {*}
*/
getJobAnalyse: ({ id, stuId }: anyObj) =>
http.request({
url: `/sc/job/getJobAnalyse/${id}?stuId=${stuId}`,
...config,
}),
/**
* @description:
* @param {*} id
* @return {*}
*/
getJobAnalyseNew: (params: anyObj) =>
http.request({
url: `/sc/job/getJobAnalyse`,
...config,
params,
}),
// /**
// * @description: 获取作业的知识点分析
// * @param {*} id
// * @return {*}
// */
// getKnowledgeAnalyse: ({ id, stuId }: anyObj) =>
// http.request({
// url: `/sc/jobStu/knowledgeAnalyse?jobId=${id}&stuId=${stuId}`,
// ...config,
// }),
/**
* @description:
* @param {*} id
* @return {*}
*/
getKnowledgeAnalyse: ({ recordId }: anyObj) =>
http.request({
url: `/sc/job/statisticalKnowledge?recordId=${recordId}`,
...config,
}),
/**
* @description:
* @param {*} id
* @return {*}
*/
getJobStatistics: (params: anyObj) =>
http.request({
url: '/sc/jobStu/title/statistics',
...config,
params,
}),
/**
* @description:
*/
getSubjectAll: () =>
http.request({
url: '/sc/subject/getList',
...config,
}),
/**
* @description:
*/
getWrongList: (params: anyObj) =>
http.request({
url: '/sc/userErrorEdition/list',
params,
...config,
}),
/**
* @description:
*/
getWrongNum: (params: anyObj) =>
http.request({
url: '/sc/userErrorEdition/getNum',
params,
...config,
}),
/**
* @description:
*/
homeWorkInferences: (params: anyObj) =>
http.request({
url: '/sc/userDrawInferences/generated',
params,
...config,
timeout: 28000,
}),
/**
* @description:
*/
submitHomeWorkInferences: async (data: anyObj) => {
return await post('/sc/userDrawInferences/summit', data, { ...config, timeout: 28000 })
},
/**
* @description:
*/
errorEdition: (id: any) =>
http.request({
url: '/sc/userErrorEdition/correct/' + id,
method: 'POST',
...config,
}),
/**
* @description:
*/
errorEditionObjective: (params: anyObj) =>
http.request({
url: '/sc/userErrorEdition/generated',
params,
...config,
timeout: 28000,
}),
/**
* @description:
*/
submitErrorEditionObjective: async (data: anyObj) => {
return await post('/sc/userErrorEdition/summit', data, {
...config,
timeout: 28000,
})
},
/**
* @description:
*/
getStatisticalAnalysis: (params: anyObj) =>
http.request({
url: '/sc/job/statisticalAnalysis',
params,
...config,
}),
/**
* @description:
*/
getStatisticalSubject: (params: anyObj) =>
http.request({
url: '/sc/job/scoringAbility',
params,
...config,
}),
/**
* @description:
*/
getKnowledgeSubject: (params: anyObj) =>
http.request({
url: '/sc/job/statisticalKnowledge',
params,
...config,
}),
/**
* @description:
*/
uploadTemporaryJob: (data: anyObj) =>
http.request({
url: '/sc/temporaryJob',
method: 'POST',
data,
...config,
}),
/**
* @description:
*/
temporaryJobList: (params: anyObj) =>
http.request({
url: '/sc/temporaryJob/list',
params,
...config,
}),
/**
* @description:
*/
temporaryJobDetail: (id: any) =>
http.request({
url: `/sc/temporaryJob/${id}`,
...config,
}),
/**
* @description:
*/
getTemporarySubjectList: () =>
http.request({
url: '/sc/subject/list',
...config,
}),
/**
* @description: AI智能批改
* @param {*} id id
* @return {*}
*/
gradeQuestionTitleByAI: (id: string) =>
http.request({
url: `/school/paper/userRecord/gradeQuestionTitleByAI/${id}`,
method: 'POST',
...config,
}),
}
export default homeWorkApi

63
src/api/system/mailbox.ts Normal file
View File

@ -0,0 +1,63 @@
import { http } from '../http'
const mailboxApi = {
// 信箱表-查询列表
getMailList: (data: anyObj) =>
http.request({
url: '/userMessageBox/getMessageBoxListByMessageTypePage',
method: 'POST',
data,
}),
// 信箱表-id查询
getMailDetail: (id: any) =>
http.request({
url: '/userMessageBox/' + id,
}),
// 根据信箱id一键领取信箱礼物
delMail: (id: any) =>
http.request({
url: '/userMessageBox/' + id,
method: 'DELETE',
}),
// 根据信箱id一键领取信箱礼物
pickMail: (id: any) =>
http.request({
url: '/userMessageBox/pickUpAll/' + id,
method: 'POST',
}),
// 一键已读-全部奖励聚合查询
pickMailAllRewards: (data?: anyObj) =>
http.request({
url: '/userMessageBox/readWithOneButton/allRewards',
method: 'POST',
data,
}),
// 一键已读-确认录取
pickMailAll: (data?: anyObj) =>
http.request({
url: '/userMessageBox/readWithOneButton',
method: 'POST',
data,
}),
// 删除已读的邮件
delMailAll: (data?: anyObj) =>
http.request({
url: '/userMessageBox/removeReadMessageByMessageFlag',
method: 'POST',
data,
}),
// 信箱未读提示
getMsgRedPoint: () =>
http.request({
url: '/userMessageBox/getMsgRedPoint',
}),
}
export default mailboxApi

87
src/api/system/setting.ts Normal file
View File

@ -0,0 +1,87 @@
import { http } from '../http'
interface PersonalConfigData {
oldPassword?: string
newPassword?: string
confirmPassword?: string
[key: string]: any
}
interface CheckPasswordData {
oldPassword: string
}
interface ChangeLoginAccData {
account: string
}
interface DeleteDeviceRelateAccData {
id?: any
[key: string]: any
}
const settingApi = {
/**
*
*/
getPersonalConfigApi: () => {
return http.request({
url: '/personal/getPersonalConfig',
})
},
/**
*
*/
modifyPersonalConfigApi: (data: PersonalConfigData) => {
return http.request({
url: '/personal/modifyPersonalConfig',
method: 'PUT',
data,
})
},
/**
*
*/
checkPasswordApi: (data: CheckPasswordData) => {
return http.request({
url: '/personal/checkPassword',
method: 'POST',
data,
})
},
/**
*
*/
getCurrentDeviceRelateAccApi: () => {
return http.request({
url: '/personal/queryCurrentDeviceRelateAcc',
})
},
/**
*
*/
changeLoginAccApi: (data: ChangeLoginAccData) => {
return http.request({
url: '/personal/changeLoginAcc',
method: 'POST',
data,
})
},
/**
*
*/
deleteDeviceRelateAccApi: (data: DeleteDeviceRelateAccData) => {
return http.request({
url: '/userDeviceAccountRecord',
method: 'PUT',
data,
})
},
}
export default settingApi

81
src/api/system/user.ts Normal file
View File

@ -0,0 +1,81 @@
import { http } from '../http'
const userApi = {
// 个人信息调整-根据标识查询用户解锁和正在使用的头像、头像框、昵称框
getUnlockInfoByRemark: (remark: number) =>
http.request({
url: '/personal/getUnlockInfoByRemark/' + remark,
}),
// 个人信息调整-修改个性化配置
updatePersonalConfig: (data: anyObj) =>
http.request({
url: '/personal/updatePersonalConfig',
method: 'POST',
data,
}),
// 个人信息调整-修改个性化配置
updateUserSex: (data: anyObj) =>
http.request({
url: '/personal/updateUserSex',
method: 'POST',
data,
}),
// 昵称校验检查
nickNameCheck: (data: anyObj) =>
http.request({
url: '/userProperty/nickNameCheck',
method: 'POST',
data,
}),
// 使用昵称修改卡道具
useNickNameCard: (data: anyObj) =>
http.request({
url: '/userProperty/useNickNameCard',
method: 'POST',
data,
}),
// 砖石修改昵称
useDiamondChangeNickName: (data: anyObj) =>
http.request({
url: '/personal/useNickNameDiamond',
method: 'POST',
data,
}),
// 兑换虚拟物品
exchangeVirtualItem: (params: anyObj) =>
http.request({
url: '/personal/updateUnlockInfoById',
method: 'POST',
params,
}),
// 获取成就勋章
getPersonalMedalV2Api: () =>
http.request({
url: '/personal/getPersonalMedalV2',
}),
// 更新学习情况
updateUserLearn: (data: anyObj) =>
http.request({
url: '/personal/updateUserLearn',
method: 'POST',
data,
}),
// 切换账号
switchUser: (data: anyObj) =>
http.request({
url: '/personal/changeLoginAcc',
method: 'POST',
data,
}),
}
export default userApi

75
src/api/task.ts Normal file
View File

@ -0,0 +1,75 @@
import { get, post } from './request'
const taskApi = {
/**
*
*/
getUserDailyTask: async (config: any): Promise<any> => {
return await get('/taskConfig/getUserDailyTask', undefined, config)
},
/**
*
*/
getUserWeekTask: async (config: any): Promise<any> => {
return await get('/taskConfig/getUserWeekTask', undefined, config)
},
/**
*
*/
getParentOrTeacherTask: async (config: any): Promise<any> => {
return await get('/taskConfig/getParentOrTeacherTask/parent', undefined, config)
},
/**
*
*/
getNewbieTask: async (config: any): Promise<any> => {
return await get('/taskConfig/getUserNewbieTask', undefined, config)
},
/**
*
*/
getUserAchievementTask: async (config: any): Promise<any> => {
return await get('/taskConfig/getUserAchievementTask', undefined, config)
},
/**
*
*/
openReward: async (data: anyObj): Promise<any> => {
return await post('/taskConfig/openReward', data)
},
/**
*
*/
openDynamicReward: async (data: anyObj): Promise<any> => {
return await post('/taskConfig/openDynamicReward', data)
},
/**
*
*/
openParentOrTeacherReward: async (data: anyObj): Promise<any> => {
return await post('/taskConfig/openParentOrTeacherReward', data)
},
/**
*
*/
openAllReward: async (data: anyObj): Promise<any> => {
return await post('/taskConfig/openAllReward', data)
},
/**
*
*/
getTaskRewardStatus: async (): Promise<any> => {
return await get('/taskConfig/getTaskRewardStatus')
},
}
export default taskApi

166
src/api/urge.ts Normal file
View File

@ -0,0 +1,166 @@
import { http } from './http'
const urgeApi = {
/**
*
*/
getInspectorModeList: () => {
return http.request({
url: '/inspectorStudent/inspectorModeList',
})
},
/**
*
*/
getUserInspectorCourseList: () => {
return http.request({
url: '/inspectorStudent/userInspectorCourseList',
})
},
/**
*
*/
getUserInspectorOrderList: () => {
return http.request({
url: '/inspectorStudent/userInspectorOrderList',
})
},
/*
*
*/
getLastInspectorCourseInfo: () => {
return http.request({
url: '/inspectorStudent/lastInspectorCourseInfo',
})
},
/*
* id查询学习计划信息
*/
getInspectorPlanInfo: (inspectorPlanId: number) => {
return http.request({
url: '/inspectorStudent/inspectorPlanInfo',
params: { inspectorPlanId },
})
},
/**
* id查询最近或最后一次课时督学师和同学信息
*/
getInspectorTeacherStudentInfo: (orderId: number) => {
return http.request({
url: '/inspectorStudent/inspectorTeacherStudentInfo',
params: { orderId },
})
},
/**
*
*/
getUserInspectorCourseReportList: () => {
return http.request({
url: '/inspectorStudent/userInspectorCourseReportList',
})
},
/**
*
*/
getUserHistoryStatisticsReportInfo: () => {
return http.request({
url: '/inspectorStudent/userHistoryStatisticsReportInfo',
})
},
/**
* id查询督学师和同学信息
*/
getInspectorTeacherStudentInfoByInspectorCourseId: (inspectorCourseId: number) => {
return http.request({
url: '/inspectorStudent/inspectorTeacherStudentInfoByInspectorCourseId',
params: { inspectorCourseId },
})
},
/**
* id查询学生督学课程报告信息
*/
getUserInspectorCourseReportInfo: (inspectorCourseId: number) => {
return http.request({
url: '/inspectorStudent/userInspectorCourseReportInfo',
params: { inspectorCourseId },
})
},
/**
*
*/
getUserTodayInspectorPlanInfo: () => {
return http.request({
url: '/inspectorStudent/userTodayInspectorPlanInfo',
})
},
/**
*
*/
sendInspectorTalkRecord: (params: anyObj) => {
return http.request({
url: '/inspectorStudent/sendInspectorTalkRecord',
method: 'post',
data: params,
})
},
/**
* id查询沟通记录()
*/
getInspectorTalkRecordPage: (params: anyObj) => {
return http.request({
url: '/inspectorStudent/inspectorTalkRecordPage',
params,
})
},
/**
*
*/
readTalkRecord: (params: anyObj) => {
return http.request({
url: '/inspectorStudent/readTalkRecord',
method: 'post',
data: params,
})
},
/**
*
*/
getInspectorButtonData: () => {
return http.request({
url: '/inspectorStudent/inspectorButtonData',
})
},
/**
* -id查询
*/
getInspectorPlanTask: (id: any) => {
return http.request({
url: `/inspectorPlanTask/${id}`,
})
},
// 学科章节-视频表-id查询
getChapterVideoById: (params: anyObj) => {
return http.request({
url: `/subjectChapterVideo/getInfo`,
params,
})
},
}
export default urgeApi

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import { px2vw } from '@/utils'
const props = withDefaults(
defineProps<{
height?: number | string // img
gap?: number | string //
abbr: any
}>(),
{
height: '42',
gap: 20,
},
)
const OSS_URL = import.meta.env.VITE_OSS_URL
const _height = px2vw(props.height)
const _gap = px2vw(props.gap)
const srcList = computed(() => {
return typeof props.abbr === 'object'
? props.abbr.map((item: string) => item.split('/')).flat()
: props.abbr.split('/')
})
function getSrc(src: string) {
if (src.includes('phrasal')) {
src = 'phrase.'
}
const type = src[src.length - 1] === '.' ? src : src + '.'
return `${OSS_URL}/icon/part_${type}png`
}
</script>
<template>
<div v-if="abbr" class="abbr-box">
<img v-for="i in srcList" :key="i" class="img-Speech" :src="getSrc(i)" alt="" />
</div>
</template>
<style lang="scss" scoped>
.abbr-box {
display: inline-flex;
justify-content: center;
align-items: center;
.img-Speech {
height: v-bind(_height);
width: auto;
margin-right: v-bind(_gap);
vertical-align: middle;
}
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<div :class="{ icon: true, active: active }">
<div
:style="{
'background-image': bg,
}"
class="left"
></div>
<div
:style="{
'background-image': bg,
}"
class="right"
></div>
</div>
</template>
<script setup lang="ts">
import { px2vh } from '@/utils'
const props = withDefaults(
defineProps<{
width?: number | string
active: boolean
name: 'us' | 'br'
}>(),
{
name: 'br',
active: false,
},
)
const OSS_URL = import.meta.env.VITE_OSS_URL
const _width = px2vh(props.width || 56)
const bg = `url(${OSS_URL}/icon/accent_${props.name}.svg)`
</script>
<style lang="scss" scoped>
.icon {
width: v-bind(_width);
height: v-bind(_width);
transform: translate(0px);
display: flex;
.left,
.right {
height: 100%;
width: 50%;
background-repeat: no-repeat;
background-size: 4.67vh 3.17vh;
}
.left {
background-position: 0 50%;
}
.right {
background-position: 105% 50%;
}
}
.icon.active .right {
animation: ani 0.4s linear alternate infinite;
}
@keyframes ani {
from {
transform: translateX(0) scale(1);
opacity: 1;
}
to {
transform: translateX(-0.83vh) scale(0.2);
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,352 @@
$font_size: 2vh;
$margin_h: 0.83vh;
$code_bg: rgb(40, 44, 52);
$corner_radius: 4px;
$theme: #009874;
$shadow:
0 2px 5px 0 rgba(220, 220, 220, 0.6),
0 2px 10px 0 rgba(220, 220, 220, 0.2);
$font: Menlo, Monaco, 'Courier New', monospace;
.markdown-body {
background-color: transparent;
font-size: $font_size;
margin-top: -1.25vh;
margin-bottom: -1.25vh;
p,
h1,
h2,
h3,
h4,
h5,
h6,
ul,
ol,
dl,
dd,
blockquote,
pre,
code,
hr,
table,
figure {
margin: $margin_h 0;
}
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: $code_bg;
border-radius: $corner_radius;
overflow: hidden;
text-indent: 0;
code.hljs {
padding: 0 15px;
font-size: $font_size;
color: #fff;
counter-reset: line;
.hljs-line::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 20px;
margin-right: 15px;
color: #999;
text-align: right;
}
}
}
.hljs-string {
color: rgb(152, 195, 121);
}
.hljs-keyword {
color: rgb(249, 38, 114);
}
.hljs-comment {
color: #7f8c99;
font-style: normal;
}
.code-block-wrapper {
position: relative;
padding-top: 3.75vh;
}
.code-block-header {
position: absolute;
top: 0;
right: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 0.01vh dashed #888;
color: #fff;
background-color: $code_bg;
&__lang {
background-color: #424242;
padding: 0.42vh 15px;
display: inline-block;
}
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
background-color: #424242;
padding: 0.42vh 15px;
display: inline-block;
&:hover {
color: $theme;
}
}
}
div[id^='mermaid-container'] {
padding: 0.33vh;
// border-radius: 0.33vh;
margin: $margin_h 0;
// text-align: center;
overflow-x: auto !important;
// background-color: #fff;
// border: 1px solid #e5e5e5;
}
a {
color: #576b95;
}
h1 {
text-align: center;
line-height: 1.75;
font-family: $font;
font-size: $font_size;
display: table;
padding: 0.5em 1em;
border-bottom: 0.17vh solid $theme;
margin: 2em auto 1em;
color: hsl(var(--foreground));
font-weight: bold;
text-shadow: 0.17vh 0.17vh 0.33vh rgba(0, 0, 0, 0.1);
margin-top: 0;
}
h2 {
text-align: center;
line-height: 1.75;
font-family: $font;
font-size: $font_size;
display: table;
padding: 0.3em 1em;
margin: 4em auto 2em;
color: #fff;
background: $theme;
font-weight: bold;
border-radius: 0.67vh;
box-shadow: $shadow;
}
h3 {
text-align: left;
line-height: 1.2;
font-family: $font;
font-size: $font_size;
padding-left: 12px;
padding-bottom: 0.42vh;
padding-top: 0.42vh;
border-left: 0.33vh solid $theme;
margin: 2em 8px 1em 0;
color: hsl(var(--foreground));
font-weight: bold;
border-bottom: 0.08vh solid $theme;
}
h4 {
display: inline-block;
border: 0.08vh solid $theme;
border-top: 0.33vh solid $theme;
padding-left: 5px;
padding-right: 5px;
}
strong {
text-align: left;
line-height: 1.75;
font-family: $font;
font-size: inherit;
color: $theme;
font-weight: bold;
}
ul {
text-align: left;
line-height: 1.75;
list-style: none;
font-family: $font;
padding-left: 1.5em;
margin-left: 0;
color: hsl(var(--foreground));
> li::before {
content: '';
position: absolute;
left: -27px;
}
}
li {
text-align: left;
line-height: 1.75;
font-family: $font;
display: block;
margin: 0.5em 8px;
text-indent: 20px;
color: hsl(var(--foreground));
position: relative;
pre {
text-indent: 0;
}
}
ol {
text-align: left;
line-height: 1.75;
font-family: $font;
font-size: $font_size;
padding-left: 1.5em;
margin-left: 0;
color: hsl(var(--foreground));
counter-reset: section; /* 创建计数器 */
> li::before {
counter-increment: section; /* 递增计数器 */
content: counter(section) '. '; /* 显示数字如1.、2. */
position: absolute;
left: -50px;
}
}
code {
color: rgb(221, 17, 68);
background-color: rgba(27, 31, 35, 0.05);
border-radius: 0.33vh;
padding: 0.42vh;
}
p {
text-align: left;
line-height: 1.75;
font-family: $font;
letter-spacing: 0.1em;
color: hsl(var(--foreground));
}
img {
text-align: left;
line-height: 1.75;
font-size: 1.17vh;
display: block;
max-width: 100%;
margin: 0.1em auto 0.5em;
border-radius: $corner_radius;
box-shadow: $shadow;
}
blockquote {
text-align: left;
line-height: 1.5;
font-family: $font;
padding: 1.25vh;
border-left: 0.33vh solid $theme;
border-radius: $corner_radius;
color: rgba(0, 0, 0, 0.6);
background: #e8f2fb;
margin: 1.67vh 0;
box-shadow: $shadow;
p {
margin: 1.25vh 0;
}
}
hr {
height: 0.08vh;
border: none;
border-top: 0.08vh solid #eee;
box-shadow: $shadow;
}
table {
display: table;
border-collapse: separate;
box-sizing: border-box;
text-indent: initial;
unicode-bidi: isolate;
line-height: normal;
font-weight: normal;
font-size: medium;
font-style: normal;
color: -internal-quirk-inherit;
text-align: center;
border-spacing: 2px;
border-color: gray;
white-space: normal;
font-variant: normal;
thead {
line-height: 1.75;
font-family: $font;
font-size: $font_size;
background: rgba(0, 0, 0, 0.05);
font-weight: bold;
color: #3f3f3f;
td {
// text-align: left;
line-height: 1.75;
font-family: $font;
font-size: $font_size;
border: 0.08vh solid #dfdfdf;
padding: 0.5em 1em;
color: #3f3f3f;
word-break: keep-all;
}
}
td {
// text-align: left;
line-height: 1.75;
font-family: $font;
font-size: $font_size;
border: 0.08vh solid #dfdfdf;
padding: 0.5em 1em;
color: #3f3f3f;
word-break: keep-all;
}
}
}

View File

@ -0,0 +1,143 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import MarkdownIt from 'markdown-it'
import MdKatex from '@vscode/markdown-it-katex'
import MdLinkAttributes from 'markdown-it-link-attributes'
import MdMermaid from 'mermaid-it-markdown'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.min.css'
import languages from './languages.json'
import { copy } from '@/utils/copy'
interface Props {
inversion?: boolean
error?: boolean
text?: string
asRawText?: boolean
}
const props = defineProps<Props>()
const textRef = ref<HTMLElement>()
const mdi = new MarkdownIt({
html: true,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
}
return highlightBlock(hljs.highlightAuto(code).value, '')
},
})
mdi
.use(MdLinkAttributes, { attrs: { target: '_blank', rel: 'noopener' } })
.use(MdKatex)
.use(MdMermaid)
const text = computed(() => {
const value = props.text ?? ''
if (!props.asRawText) {
// $$
const escapedText = escapeBrackets(escapeDollarNumber(value))
return mdi.render(escapedText)
}
return value
})
function highlightBlock(str: string, lang?: string) {
//
const originLang = (lang?.trim().toLowerCase() as string) || 'txt'
let language = (languages as any)[originLang]
if (!language) {
if (originLang.length === 1) {
language = originLang.toUpperCase()
} else {
language = originLang.charAt(0).toUpperCase() + originLang.slice(1)
}
}
//
str = str
.trim()
.split('\n')
.map(line => `<span class="hljs-line">${line}</span>`)
.join('\n')
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${language}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
}
function addCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach(btn => {
btn.addEventListener('click', () => {
copy(btn.parentElement?.nextElementSibling?.textContent)
})
})
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach(btn => {
btn.removeEventListener('click', () => {})
})
}
}
function escapeDollarNumber(text: string) {
let escapedText = ''
for (let i = 0; i < text.length; i += 1) {
let char = text[i]
const nextChar = text[i + 1] || ' '
if (char === '$' && nextChar >= '0' && nextChar <= '9') char = '\\$'
escapedText += char
}
return escapedText
}
function escapeBrackets(text: string) {
const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g
return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
if (codeBlock) return codeBlock
else if (squareBracket) return `$$${squareBracket}$$`
else if (roundBracket) return `$${roundBracket}$`
return match
})
}
onMounted(() => {
addCopyEvents()
})
onUpdated(() => {
addCopyEvents()
})
onUnmounted(() => {
removeCopyEvents()
})
</script>
<template>
<div class="text-black">
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion">
<div v-if="!asRawText" class="markdown-body" v-html="text" />
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
</div>
</template>
<style lang="scss">
@use './index.scss' as *;
</style>

View File

@ -0,0 +1,105 @@
{
"html": "HTML",
"txt": "TXT",
"xml": "XML",
"svg": "SVG",
"mathml": "MathML",
"css": "CSS",
"clike": "C-like",
"js": "JavaScript",
"javascript": "JavaScript",
"abap": "ABAP",
"apacheconf": "Apache Configuration",
"apl": "APL",
"arff": "ARFF",
"asciidoc": "AsciiDoc",
"adoc": "AsciiDoc",
"asm6502": "6502 Assembly",
"aspnet": "ASP.NET (C#)",
"autohotkey": "AutoHotkey",
"autoit": "AutoIt",
"basic": "BASIC",
"csharp": "C#",
"dotnet": "C#",
"cpp": "C++",
"cil": "CIL",
"csp": "Content-Security-Policy",
"css-extras": "CSS Extras",
"django": "Django/Jinja2",
"jinja2": "Django/Jinja2",
"dockerfile": "Docker",
"erb": "ERB",
"fsharp": "F#",
"gcode": "G-code",
"gedcom": "GEDCOM",
"glsl": "GLSL",
"gml": "GameMaker Language",
"gamemakerlanguage": "GameMaker Language",
"graphql": "GraphQL",
"hcl": "HCL",
"http": "HTTP",
"hpkp": "HTTP Public-Key-Pins",
"hsts": "HTTP Strict-Transport-Security",
"ichigojam": "IchigoJam",
"inform7": "Inform 7",
"javastacktrace": "Java stack trace",
"json": "JSON",
"jsonp": "JSONP",
"latex": "LaTeX",
"emacs": "Lisp",
"elisp": "Lisp",
"emacs-lisp": "Lisp",
"lolcode": "LOLCODE",
"markup-templating": "Markup templating",
"matlab": "MATLAB",
"mel": "MEL",
"n1ql": "N1QL",
"n4js": "N4JS",
"n4jsd": "N4JS",
"nand2tetris-hdl": "Nand To Tetris HDL",
"nasm": "NASM",
"nginx": "Nginx",
"nsis": "NSIS",
"objectivec": "Objective-C",
"ocaml": "OCaml",
"opencl": "OpenCL",
"parigp": "PARI/GP",
"objectpascal": "Object Pascal",
"php": "PHP",
"php-extras": "PHP Extras",
"plsql": "PL/SQL",
"powershell": "PowerShell",
"properties": ".properties",
"protobuf": "Protocol Buffers",
"q": "Q (kdb+ database)",
"jsx": "React JSX",
"tsx": "React TSX",
"renpy": "Ren'py",
"rest": "reST (reStructuredText)",
"sas": "SAS",
"sass": "Sass (Sass)",
"scss": "Sass (Scss)",
"sql": "SQL",
"soy": "Soy (Closure Template)",
"sh": "Shell",
"shell": "Shell",
"tap": "TAP",
"toml": "TOML",
"tt2": "Template Toolkit 2",
"ts": "TypeScript",
"vbnet": "VB.Net",
"vhdl": "VHDL",
"vim": "Vim",
"visual-basic": "Visual Basic",
"vb": "Visual Basic",
"wasm": "WebAssembly",
"wiki": "Wiki markup",
"xeoracube": "XeoraCube",
"xojo": "Xojo (REALbasic)",
"xquery": "XQuery",
"yaml": "YAML",
"oc": "Objective-C",
"objc": "Objective-C",
"object-c": "Objective-C",
"objective-c": "Objective-C"
}

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { globalBack } from '@/router'
import { px2vw } from '@/utils'
import { useRoute } from 'vue-router'
const props = withDefaults(
defineProps<{
leftText?: any
index?: boolean
iconSize?: number
titleSize?: number
gap?: number
showOnly?: boolean
}>(),
{
leftText: '',
index: false,
iconSize: 80,
gap: 24,
showOnly: false,
},
)
const emits = defineEmits(['back'])
const route = useRoute()
const gapString = px2vw(props.gap)
function onLeftClick() {
if (props.showOnly) return
emits('back')
globalBack(props.index)
}
</script>
<template>
<nav v-if="!route.meta?.hiddenBack" class="pointer" @click="onLeftClick">
<Icon name="left_arrow" :size="iconSize"></Icon>
<LineFont v-if="leftText" :text="leftText" :size="titleSize" />
</nav>
<LineFont v-else-if="leftText" :text="leftText" :size="titleSize" />
</template>
<style lang="scss" scoped>
nav {
display: flex;
align-items: center;
width: max-content;
.x-icon {
margin-right: v-bind(gapString);
}
}
</style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
defineProps<{
text: string
size: number | string
}>()
</script>
<template>
<div class="big-title">
<LineFont :text="text" :size="size" />
</div>
</template>
<style lang="scss" scoped>
.big-title {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -0,0 +1,175 @@
<script setup lang="ts">
import { px2vw } from '@/utils'
const props = withDefaults(
defineProps<{
type?: 'primary' | 'success' | 'info' | 'danger'
size?: number | string
width?: number | string
height?: number | string
loading?: boolean
tilt?: boolean //
successTwo?: boolean // success
disabled?: boolean
}>(),
{
type: 'primary',
size: 32,
width: 232,
height: 80,
tilt: false,
successTwo: false,
disabled: false,
},
)
const emits = defineEmits(['click'])
const OSS_URL = import.meta.env.VITE_OSS_URL
const TYPE_BG = {
primary: {
left: `${OSS_URL}/urm/btn_primary_left.svg`,
right: `${OSS_URL}/urm/btn_primary_right.svg`,
},
success: {
left: `${OSS_URL}/urm/btn_success_left.svg`,
right: `${OSS_URL}/urm/btn_success_right_two.svg`,
},
danger: {
left: `${OSS_URL}/urm/btn_danger_left.svg`,
right: `${OSS_URL}/urm/btn_danger_right.svg`,
},
info: {
left: `${OSS_URL}/urm/btn_info_left.svg`,
right: `${OSS_URL}/urm/btn_info_right.svg`,
},
}
const _type = computed(() => (props.disabled ? 'info' : props.type))
const _size = computed(() => px2vw(props.size))
const _iconSize = computed(() => (isNaN(Number(props.size)) ? 48 : Number(props.size) * 1.5))
const _width = computed(() => px2vw(props.width))
const _height = computed(() => px2vw(props.height))
function onClick(e: any) {
if (props.loading || props.disabled) return
e.preventDefault()
emits('click', e)
}
</script>
<template>
<button
:class="{
'x-button': true,
'is-loading': loading,
[_type]: true,
tilt,
}"
:disabled="disabled"
@click="onClick"
>
<img v-if="!successTwo" class="x-button-left" :src="TYPE_BG[_type].left" />
<img class="x-button-right" :src="TYPE_BG[_type].right" />
<Icon v-if="loading" name="loading" loading :size="_iconSize"></Icon>
<div v-else class="x-button-text">
<slot></slot>
</div>
</button>
</template>
<style lang="scss" scoped>
.x-button {
width: v-bind(_width);
height: v-bind(_height);
position: relative;
overflow: hidden;
font-family: $font-special;
font-size: v-bind(_size);
cursor: pointer;
line-height: 1.5;
box-shadow: 0 0.5vh 0.17vh 0 rgba(0, 0, 0, 0.25);
outline: none;
// transition: opacity 0.3s;
transition: filter 0.1s ease;
&[disabled],
&.is-loading {
cursor: not-allowed;
&:active {
filter: unset;
}
}
&:active {
filter: brightness(0.9);
}
&.danger {
background: #ff5050;
border-radius: 4.5vh;
}
&.primary {
background: #ffe850;
border-radius: 4.5vh;
}
&.success {
background: linear-gradient(180deg, #70f0a5 0%, #5fe799 100%);
border-radius: 4.5vh;
}
&.info {
background: #83fcf7;
border-radius: 4.5vh;
}
&.tilt {
transform: skew(-20deg);
transform-origin: center center;
border-radius: $radius-5;
.x-button-left,
.x-button-right,
.x-button-text {
transform: skew(20deg);
}
.x-icon {
animation: rotating 2s linear infinite;
@keyframes rotating {
0% {
transform: skew(20deg) rotateZ(0);
}
100% {
transform: skew(20deg) rotateZ(360deg);
}
}
}
}
&-left {
position: absolute;
width: calc(v-bind(_size) * 3);
left: -20px;
top: -1.67vh;
z-index: 0;
}
&-right {
position: absolute;
width: calc(v-bind(_size) * 3);
right: 0;
bottom: 0;
// top: 50%;
// transform: translateY(-38%);
z-index: 0;
}
&-text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
& .x-icon {
top: 0.67vh;
}
}
</style>

View File

@ -0,0 +1,99 @@
<script setup lang="ts">
const props = defineProps<{
options: string
}>()
const optionsHtml = computed(() => {
if (!props.options) return []
let parsed: any = props.options
try {
parsed = JSON.parse(parsed)
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed)
}
} catch (e) {
return []
}
if (!Array.isArray(parsed)) return []
// [{"label":"A","value":"..."}]
// [{ options: [...] }] 便
if (parsed.length > 0) {
const first = parsed[0]
const isFlatOption =
first &&
typeof first === 'object' &&
'label' in first &&
'value' in first &&
!('options' in first)
if (isFlatOption) {
return [{ options: parsed }]
}
}
return parsed
})
</script>
<template>
<div class="box">
<div v-for="(item, index) in optionsHtml" :key="index" class="row">
<span class="index">{{ index + 1 }}</span>
<div class="content">
<template v-if="item?.options && Array.isArray(item.options)">
<span v-for="opt in item.options" :key="opt.label" class="opt-item">
<span class="opt-label">{{ opt.label }}.</span>
<span v-html="opt.value"></span>
</span>
</template>
<template v-else>
{{ item }}
</template>
</div>
</div>
</div>
</template>
<style scoped>
.box {
display: flex;
flex-direction: column;
padding: 10px;
}
.row {
margin-bottom: 20px;
line-height: 1.5;
display: flex;
align-items: flex-start;
}
.content {
flex: 1;
display: flex;
flex-wrap: wrap;
}
.index {
display: inline-block;
width: 40px;
height: 40px;
line-height: 30px;
text-align: center;
border-radius: 50%;
/* background-color: #669cfe; */
border: 5px solid #669cfe;
color: #669cfe;
margin-right: 12px;
margin-top: 5px;
font-size: 25px;
flex-shrink: 0;
}
.opt-item {
margin-right: 24px;
display: inline-block;
margin-bottom: 8px;
}
.opt-label {
font-weight: bold;
margin-right: 2px;
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<div v-if="modelValue" class="mask" :is-mask-click="false" :animation="false">
<div class="dialog_box">
<div class="dialog_main">
<span class="tip_text">{{ text }}</span>
</div>
<div class="dialog_btns">
<div class="btn cancel_btn" @click="tapCancelBtn">
<span class="text">取消</span>
</div>
<div class="btn confirm_btn" @click="() => emits('confirm')">
<span class="text">确认</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
modelValue: boolean
text: string
}>(),
{ text: '确定要把2个单词从熟词本中删除吗' },
)
const emits = defineEmits(['update:modelValue', 'confirm', 'cancelBtn'])
const tapCancelBtn = () => {
emits('update:modelValue', false)
emits('cancelBtn')
}
</script>
<style lang="scss" scoped>
.mask {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.4);
}
.dialog_box {
position: relative;
width: 640px;
height: 24.08vh;
background-color: #fffef5;
border-radius: 3.33vh;
color: #383835;
font-size: 2.67vh;
&::after {
content: '';
position: absolute;
top: -1.13vh;
left: 0;
display: block;
width: 100%;
height: 100%;
border-radius: 3.33vh;
background-color: #dfdaf2;
transform: skew(-5deg, 5deg);
z-index: -1;
}
.dialog_main {
text-align: center;
line-height: 4vh;
.tip_text {
display: inline-block;
padding-top: 7.5vh;
}
}
.dialog_btns {
position: absolute;
bottom: 2.67vh;
display: flex;
padding: 0 128px;
box-sizing: border-box;
.btn {
position: relative;
width: 144px;
height: 4.33vh;
border-radius: 2.17vh;
border: 0.17vh solid #d9d163;
font-size: 2vh;
cursor: pointer;
.text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.cancel_btn {
background-color: #fffef5;
}
.confirm_btn {
margin-left: 96px;
background-color: #fff01f;
}
}
}
</style>

View File

@ -0,0 +1,186 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
modelValue?: number | string | ''
disabled?: boolean
title?: string
emptyText?: string
}>(),
{
modelValue: '',
disabled: false,
title: '难度:',
emptyText: '暂无',
},
)
const emit = defineEmits(['update:modelValue'])
const OSS_URL = `${import.meta.env.VITE_OSS_URL}`
const bg = `url(${OSS_URL}/icon/select_grey.svg) 100% / contain no-repeat`
const dia = reactive<anyObj>({
show: false,
data: 0,
})
function toEdit() {
if (props.disabled) return
dia.show = true
dia.data = props.modelValue
}
const difficultyLabelMap: Record<string, number> = {
容易: 1,
较易: 2,
一般: 3,
较难: 4,
困难: 5,
}
function normalizeToStars(val: number | string | ''): number {
if (val === '' || val === null || val === undefined) return 0
if (typeof val === 'string') return difficultyLabelMap[val] || 0
if (val >= 17 && val <= 21) return val - 16
if (val >= 11 && val <= 15) return val - 10
if (val >= 1 && val <= 5) return val
return 0
}
const starCount = computed(() => normalizeToStars(props.modelValue))
const starLevels = [1, 2, 3, 4, 5]
const selectList = [
{ label: '智能', value: '' },
{ label: '容易', value: '容易' },
{ label: '较易', value: '较易' },
{ label: '一般', value: '一般' },
{ label: '较难', value: '较难' },
{ label: '困难', value: '困难' },
]
function handleEdit() {
emit('update:modelValue', dia.data)
dia.show = false
}
</script>
<template>
<div>
<div class="difficulty-start" @click="toEdit">
<div class="text">{{ title }}</div>
<div class="starts">
<template v-if="!disabled || modelValue">
<div v-for="i in starLevels" :key="i" class="start">
<Icon
:name="
modelValue
? starCount >= i
? 'difficulty_start_active'
: 'difficulty_start_info'
: `difficulty_start_intelligence`
"
size="48"
>
</Icon>
</div>
</template>
<span v-else class="text"> {{ emptyText }} </span>
</div>
<Icon v-if="!disabled" name="edit" width="40" class="pointer"></Icon>
</div>
<mj-dialog v-if="!disabled" v-model="dia.show" :ok="handleEdit">
<div class="dia-main">
<div class="dia-main-title">请选择题目的难度</div>
<div class="dia-main-select">
<Select v-model="dia.data" :selectList="selectList"></Select>
</div>
</div>
</mj-dialog>
</div>
</template>
<style scoped lang="scss">
.difficulty-start {
// width: 488px;
height: 5.33vh;
padding: 0 32px;
border-radius: 5vh;
background: rgba(0, 0, 0, 0.65);
display: flex;
align-items: center;
line-height: 1.5;
.text {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.95);
font-family: OPPOSans;
font-size: 2.33vh;
}
.starts {
flex: 1;
display: flex;
align-items: center;
margin: 0 20px 0 2px;
.start {
margin: 0 4px;
width: 48px;
height: 4vh;
}
}
.x-icon {
flex-shrink: 0;
}
}
.dia-main {
line-height: 1.5;
text-align: center;
margin-bottom: 1.67vh;
&-title {
color: $font-color;
font-family: $font-special;
font-size: 2.67vh;
margin: 1.67vh 0 3.33vh;
}
&-select {
:deep(.select-box) {
margin: 0 auto;
width: 504px;
height: 6.67vh;
flex-shrink: 0;
color: #fff;
text-shadow: 0 0.17vh 0.17vh #4c749e;
font-family: $font-special;
font-size: 2.67vh;
.content {
width: 100%;
height: 6.67vh;
border-radius: 6.67vh;
background: #b9d1ea;
box-shadow: 0 0 0.5vh 0 #4c749e inset;
padding: 0 32px 0 40px;
.icon {
background: v-bind(bg) !important;
width: 40px;
height: 3.33vh;
}
}
.selectList {
width: 100%;
background: #b9d1ea;
box-shadow: 0 0 0.5vh 0 #4c749e inset;
max-height: 33.33vh;
.selectList-box {
width: 100%;
max-height: 33.33vh;
padding: 0.67vh 32px 0.67vh 40px;
.select-item {
color: #fff !important;
height: 4vh;
padding: 0;
height: 4.83vh;
align-items: center;
}
}
}
.mask {
display: none;
}
}
}
}
</style>

View File

@ -0,0 +1,155 @@
<script setup lang="ts">
import { px2vw } from '@/utils'
type showType =
| 'network'
| 'dev'
| 'goods'
| 'email'
| 'announcement'
| 'knowledge'
| 'practice'
| 'test'
| 'report'
| 'course'
| 'activity'
| 'task'
| 'rank'
| 'learn'
| 'search'
| 'loading'
const props = withDefaults(
defineProps<{
type?: showType
content?: string
size?: number | string
delay?: number | string
}>(),
{
type: 'email',
content: '',
size: 320,
delay: 0,
},
)
const DES_MAP: anyObj = {
network: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_network.svg`,
msg: '网络出问题了啦',
},
dev: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_dev.svg`,
msg: '功能正在开发中,敬请期待!',
},
goods: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_goods.svg`,
msg: '还没有商品哦~',
},
email: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_email.svg`,
msg: '还没有收到邮件哦~',
},
announcement: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_announcement.svg`,
msg: '还没有公告哦~',
},
knowledge: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_knowledge.png`,
msg: '暂无知识图谱,快去训练吧!',
},
practice: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_practice.svg`,
msg: '暂无练习记录,快去练习吧!',
},
test: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_test.svg`,
msg: '暂无测试记录,快去测试吧!',
},
report: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_report.svg`,
msg: '暂无督学报告,快去生成吧!',
},
course: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_course.svg`,
msg: '暂无课程表,快去选课吧!',
},
activity: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_activity.svg`,
msg: '还没有活动哦~',
},
task: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_task.svg`,
msg: '还没有任务哦~',
},
rank: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_rank.svg`,
msg: '还没有人上榜哦~',
},
learn: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_learn.svg`,
msg: '暂无学习数据,快去学习吧!',
},
search: {
img: `${import.meta.env.VITE_OSS_URL}/urm/empty_search.svg`,
msg: '搜索内容为空哦~',
},
loading: {
iconName: 'loading',
},
}
const show = ref(false)
onMounted(() => {
setTimeout(() => (show.value = true), Number(props.delay))
})
</script>
<template>
<div v-if="show" class="empty">
<slot>
<template v-if="DES_MAP[type]?.iconName">
<Icon :name="DES_MAP[type].iconName" loading size="96" />
</template>
<template v-else>
<img
:style="{
width: px2vw(size),
height: 'auto',
}"
:src="DES_MAP[type]['img']"
/>
<div class="empty-content">
{{ content || DES_MAP[type]['msg'] }}
</div>
</template>
</slot>
</div>
</template>
<style lang="scss" scoped>
.empty {
width: 100%;
height: inherit;
display: flex;
flex: 1;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
justify-content: center;
text-align: center;
img {
user-select: none;
pointer-events: none;
}
.empty-content {
width: 100%;
color: $font-color;
font-size: $font-size-5;
line-height: 1.83vh;
margin-top: $space-1_h;
font-family: $font-special;
}
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<div v-if="props.modelValue" class="pop__wrapper_box">
<div class="content">
<div class="dialog_box">
<div class="header">
<div class="left">
<div class="t-icon t-icon-fangda handle_icon" @click="tapEnlarge"></div>
<div class="t-icon t-icon-suoxiao handle_icon" @click="tapLessen"></div>
</div>
<div class="right">
<div class="t-icon t-icon-guanbi handle_icon" @click="tapClose"></div>
</div>
</div>
<div class="area">
<div
ref="imgArea"
:style="{
transform: `translate(${movableData.x}px, ${movableData.y}px) scale(${movableData.scale})`,
}"
class="area-content"
@mousedown.prevent="startDrag"
@touchstart.prevent="startDrag"
>
<img ref="img" :src="picture" alt="" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
modelValue: boolean
picture: string
}>(),
{},
)
const emits = defineEmits(['update:modelValue'])
const dragging = ref(false) //
const imgArea = ref()
const img = ref()
const movableData = ref({
x: 0,
y: 0,
scale: 1,
})
const scaleData = ref(1) //
const distance = ref({
startDistance: 0,
endDistance: 0,
})
//
function tapEnlarge() {
if (movableData.value.scale < 2) {
movableData.value.scale += 0.1
}
}
//
function tapLessen() {
if (movableData.value.scale > 0.6) {
movableData.value.scale -= 0.1
}
}
function tapClose() {
emits('update:modelValue', false)
}
function touchTwo(e: anyObj[], mode: 'startDistance' | 'endDistance') {
//
const one = {
x: e[0].pageX, //
y: e[0].pageY, //
} //
const two = {
x: e[1].pageX, //
y: e[1].pageY, //
}
distance.value[mode] = getDistance(one, two)
}
function getDistance(start: anyObj, stop: anyObj) {
return Math.sqrt(
Math.pow(Math.abs(start.x - stop.x), 2) + Math.pow(Math.abs(start.y - stop.y), 2),
)
}
const startDrag = (event: anyObj) => {
if (event.touches?.length === 2) {
//
touchTwo(event.touches, 'startDistance')
return
}
dragging.value = true
const x = (event.touches ? event.touches[0].pageX : event.pageX) - movableData.value.x
const y = (event.touches ? event.touches[0].pageY : event.pageY) - movableData.value.y
const move = (event: anyObj) => {
// console.log('event.touches[0].pageX', event.touches[0])
if (event.touches?.length === 2) {
//
touchTwo(event.touches, 'endDistance')
const scale2 =
scaleData.value + distance.value.endDistance / distance.value.startDistance - 1.01 // 1 0.01
movableData.value.scale = scale2 > 2 ? 2 : scale2 < 0.5 ? 0.5 : scale2 // 2 0.5
return
}
if (dragging.value) {
movableData.value.x = (event.touches ? event.touches[0].pageX : event.pageX) - x
movableData.value.y = (event.touches ? event.touches[0].pageY : event.pageY) - y
}
}
const stopDrag = () => {
distance.value.startDistance = distance.value.endDistance = 0
scaleData.value = movableData.value.scale
dragging.value = false
document.removeEventListener('mousemove', move)
document.removeEventListener('touchmove', move)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchend', stopDrag)
}
document.addEventListener('mousemove', move)
document.addEventListener('touchmove', move)
document.addEventListener('mouseup', stopDrag)
document.addEventListener('touchend', stopDrag)
}
onUnmounted(() => {
document.removeEventListener('mousemove', () => {})
document.removeEventListener('mouseup', () => {})
document.removeEventListener('touchmove', () => {})
document.removeEventListener('touchend', () => {})
})
</script>
<style lang="scss" scoped>
.dialog_box {
position: relative;
width: 1120px;
height: 58.5vh;
background-color: #fffef5;
border-radius: 2.5vh;
color: #383835;
font-size: 2.67vh;
.area {
width: 1120px;
height: 52.5vh;
border-radius: 0 0 2.5vh 2.5vh;
overflow: hidden;
}
.area-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
img {
width: 100%;
max-height: 100%;
object-fit: contain;
cursor: grab;
}
.header {
display: flex;
justify-content: space-between;
padding: 1vh 32px;
height: 6vh;
border-radius: 2.5vh 2.5vh 0 0;
background-color: #f2f1e6;
box-sizing: border-box;
.handle_icon {
width: 48px;
height: 4vh;
cursor: pointer;
}
.left {
display: flex;
.handle_icon {
&:first-child {
margin-right: 24.013px;
}
}
}
}
}
.pop__wrapper_box {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 1;
z-index: 999;
background-color: rgba($color: #000000, $alpha: 0.5);
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div
:class="{ icon: 1, active: active }"
:style="{
'background-image': bg || `url(${OSS_URL}/urm/animation_horn.png)`,
}"
></div>
</template>
<script setup lang="ts">
import { px2vw } from '@/utils'
const props = withDefaults(
defineProps<{
width?: number | string
active: boolean
bg?: string
}>(),
{
active: false,
},
)
const OSS_URL = import.meta.env.VITE_OSS_URL
const _width = px2vw(props.width || 48)
</script>
<style scoped>
.icon {
width: v-bind(_width);
height: v-bind(_width);
transform: translate(-2px);
background-size: calc(v-bind(_width) * 3) v-bind(_width);
background-repeat: no-repeat;
background-position: calc(v-bind(_width) * -2) center;
cursor: pointer;
}
.icon.active {
animation: ani 1s steps(3) infinite;
}
@keyframes ani {
from {
background-position-x: 0;
}
to {
background-position-x: calc(v-bind(_width) * -3);
}
}
</style>

66
src/components/HornC.vue Normal file
View File

@ -0,0 +1,66 @@
<template>
<div class="horn" @click="click">
<Horn width="56" :active="audio.isPlay.value" />
</div>
</template>
<script setup lang="ts">
import useAudio from '@/hooks/useCommonAudio'
const props = withDefaults(
defineProps<{
src: string
autoplay?: boolean
readonly?: boolean
}>(),
{
autoplay: false,
},
)
const audio = useAudio()
function click() {
if (props.readonly) {
return
}
handlePlay()
}
function handlePlay() {
audio.play(props.src)
}
/* 初次加载时,在动画组件初始化后进行自动播放 */
onMounted(() => {
nextTick(() => {
if (props.src && props.autoplay) {
audio.play(props.src)
}
})
})
defineExpose({
handlePlay,
})
watch(
() => props.src,
() => {
if (props.src && props.autoplay) {
audio.play(props.src)
}
},
)
</script>
<style scoped lang="scss">
.horn {
display: inline-block;
width: 64px;
height: 5.33vh;
line-height: 5.33vh;
vertical-align: middle;
display: flex;
align-items: center;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { px2vh } from '@/utils'
const props = withDefaults(
defineProps<{
name: string
size?: string | number
width?: string | number | 'auto'
height?: string // svg height = width
fill?: string
rotate?: string | number
style?: { [key: string]: string }
loading?: boolean
onClick?: (...args: any[]) => void
}>(),
{
width: '30',
},
)
const emit = defineEmits(['click'])
defineOptions({ inheritAttrs: false })
const { size, width, height } = toRefs(props)
const _width = computed(() => (width.value === 'auto' ? 'auto' : px2vh(size.value || width.value)))
const _height = computed(() => (height.value ? px2vh(size.value || height.value) : _width.value))
const _style = computed(() => ({
...props.style,
width: _width.value,
height: _height.value,
transform: `rotate(${props.rotate}deg)`,
fill: props.fill,
}))
const url = computed(() => {
const ext = props.name.includes('.') ? '' : '.svg'
return `${import.meta.env.VITE_OSS_URL}/icon/${props.name}${ext}`
})
</script>
<template>
<i
:class="{
'x-icon': true,
'is-loading': loading,
'x-auto': width === 'auto',
}"
v-bind="$attrs"
:style="_style"
aria-hidden="true"
@click="e => (props.onClick ? props.onClick(e) : emit('click', e))"
>
<img :src="url" :style="{ width: width === 'auto' ? 'auto' : '100%' }" />
</i>
</template>
<style lang="scss" scoped>
.x-icon {
display: inline-block;
position: relative;
&.is-loading {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotateZ(0);
}
100% {
transform: rotateZ(360deg);
}
}
img {
position: absolute;
left: 0;
right: 0;
user-select: none;
-webkit-user-drag: none;
height: 100%;
}
&.x-auto {
position: unset;
img {
position: unset;
}
}
}
</style>

View File

@ -0,0 +1,261 @@
<template>
<div v-if="visible" class="image-cropper-mask">
<div class="cropper-box">
<vue-cropper
ref="cropperRef"
:img="imgUrl"
:output-size="option.size"
:output-type="option.outputType"
:info="true"
:full="option.full"
:can-move="option.canMove"
:can-move-box="option.canMoveBox"
:original="option.original"
:auto-crop="option.autoCrop"
:fixed="option.fixed"
:fixed-number="option.fixedNumber"
:center-box="option.centerBox"
:info-true="option.infoTrue"
:fixed-box="option.fixedBox"
:mode="option.mode"
:auto-crop-width="option.autoCropWidth"
:auto-crop-height="option.autoCropHeight"
@img-load="imgLoad"
></vue-cropper>
<div v-if="imgLoading" class="loading-mask">
<Icon name="global_loading" loading size="96" />
</div>
</div>
<div class="tools">
<mj-button @click="rotateLeft">左旋转</mj-button>
<mj-button @click="rotateRight">右旋转</mj-button>
<mj-button type="warn" @click="handleCancel">取消</mj-button>
<mj-button type="primary" :loading="loading" @click="handleConfirm">确认</mj-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, nextTick } from 'vue'
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
const props = defineProps<{
visible: boolean
img: string | Blob | File
}>()
const emit = defineEmits(['update:visible', 'confirm', 'cancel'])
const cropperRef = ref()
const imgUrl = ref('')
const loading = ref(false)
const imgLoading = ref(true)
// /
const MAX_IMG_DIMENSION = 1280
const option = reactive({
size: 0.85,
full: false,
outputType: 'jpeg',
canMove: true,
original: false,
canMoveBox: true,
autoCrop: true,
autoCropWidth: 0,
autoCropHeight: 0,
centerBox: false,
high: true,
maxImgSize: MAX_IMG_DIMENSION,
fixed: false,
fixedNumber: [1, 1],
infoTrue: false,
fixedBox: false,
mode: 'contain',
})
// props.img
let loadToken = 0
function readFileAsDataURL(file: Blob | File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => resolve((e.target?.result as string) || '')
reader.onerror = reject
reader.readAsDataURL(file)
})
}
// 使 createImageBitmap退 Image
async function loadImageSize(
source: Blob | File | string,
): Promise<{ bitmap?: ImageBitmap; img?: HTMLImageElement; w: number; h: number }> {
// File Blob Blob File
if (typeof createImageBitmap === 'function' && typeof source !== 'string') {
try {
const bitmap = await createImageBitmap(source)
return { bitmap, w: bitmap.width, h: bitmap.height }
} catch {
/* fallthrough */
}
}
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve({ img, w: img.naturalWidth, h: img.naturalHeight })
img.onerror = reject
if (typeof source === 'string') {
img.src = source
} else {
const reader = new FileReader()
reader.onload = e => {
img.src = (e.target?.result as string) || ''
}
reader.onerror = reject
reader.readAsDataURL(source)
}
})
}
async function downscaleImage(source: Blob | File | string): Promise<string> {
try {
const { bitmap, img, w, h } = await loadImageSize(source)
const drawSource = (bitmap || img) as CanvasImageSource
const needScale = w > MAX_IMG_DIMENSION || h > MAX_IMG_DIMENSION
const scale = needScale ? Math.min(MAX_IMG_DIMENSION / w, MAX_IMG_DIMENSION / h) : 1
const canvas = document.createElement('canvas')
canvas.width = Math.max(1, Math.round(w * scale))
canvas.height = Math.max(1, Math.round(h * scale))
const ctx = canvas.getContext('2d')!
ctx.drawImage(drawSource, 0, 0, canvas.width, canvas.height)
bitmap?.close?.()
return canvas.toDataURL('image/jpeg', 0.9)
} catch (e) {
console.error('downscaleImage error', e)
if (typeof source === 'string') return source
try {
return await readFileAsDataURL(source)
} catch {
return ''
}
}
}
watch(
() => props.img,
async (val: string | Blob | File | undefined) => {
//
imgUrl.value = ''
imgLoading.value = true
if (!val) return
const myToken = ++loadToken
try {
const url = await downscaleImage(val)
//
if (myToken !== loadToken) return
imgUrl.value = url
} catch (e) {
console.error('cropper image load error', e)
if (myToken === loadToken) imgLoading.value = false
}
},
{ immediate: true },
)
function imgLoad(msg: string) {
if (msg === 'success') {
imgLoading.value = false
nextTick(() => {
if (cropperRef.value) {
const { trueWidth, trueHeight, scale } = cropperRef.value
option.autoCropWidth = trueWidth * scale
option.autoCropHeight = trueHeight * scale
option.centerBox = true
nextTick(() => {
cropperRef.value.goAutoCrop()
})
}
})
}
}
function rotateLeft() {
cropperRef.value.rotateLeft()
}
function rotateRight() {
cropperRef.value.rotateRight()
}
function handleCancel() {
emit('update:visible', false)
emit('cancel')
}
function handleConfirm() {
loading.value = true
cropperRef.value.getCropBlob((data: Blob) => {
loading.value = false
emit('confirm', data)
emit('update:visible', false)
})
}
</script>
<style scoped lang="scss">
.image-cropper-mask {
position: fixed;
top: 0;
left: 0;
z-index: 10001; // higher than hand-drawn-mask
width: 100vw;
height: 100%;
background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
background: #f7faf9;
display: flex;
flex-direction: column;
.cropper-box {
flex: 1;
width: 100%;
height: calc(100% - 80px);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAC6wGCdbt9AAAAIKFInOMAAAABYktHRACIBR1IAAAAC0lEQVR4nGNgwA0AAJoAAW7mSAAAAAAASUVORK5CYII=');
position: relative;
.loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 10002;
}
:deep(.crop-point) {
width: 20px !important;
height: 20px !important;
background-color: #39f !important;
border-radius: 50%;
opacity: 0.8;
}
}
.tools {
height: 80px;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 20px;
background: #fff;
.mj-button {
margin: 0 4px;
}
}
}
</style>

View File

@ -0,0 +1,24 @@
<template>
<div class="img-bg" />
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
src: string
}>(),
{},
)
const url = `url(${props.src})`
</script>
<style lang="scss" scoped>
.img-bg {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: v-bind(url);
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div v-if="props.modelValue" class="mask" @click.stop="emits('update:modelValue', false)">
<div ref="imgBoxRef" class="img-box" @click.stop="false">
<img :src="url || `${OSS_URL}/urm/book_bg.svg`" />
</div>
</div>
</template>
<script setup lang="ts">
import Hammer from 'hammerjs'
const props = withDefaults(
defineProps<{
modelValue: boolean
url?: string
}>(),
{},
)
const emits = defineEmits(['update:modelValue'])
const OSS_URL = import.meta.env.VITE_OSS_URL
const imgBoxRef = ref()
const imgConfig = reactive({
scaleIndex: 1,
scaleCount: 1,
})
function init() {
nextTick(() => {
let x = 0
let y = 0
let _node = imgBoxRef.value // ref
let hand = new Hammer(_node) // Hammer
// pinch
hand.get('pinch').set({ enable: true })
// pinch
hand.on('pinchmove pinchstart pinchin pinchout', (e: anyObj) => {
if (e.type === 'pinchstart') {
imgConfig.scaleIndex = imgConfig.scaleCount || 1 //
}
imgConfig.scaleCount = imgConfig.scaleIndex * e.scale //
_node.style.transform = 'scale(' + imgConfig.scaleIndex * e.scale + ')' //
})
// doubletap
// hand.on('doubletap', () => {
// x = 0;
// y = 0;
// imgConfig.scaleCount = 1; // 1
// _node.style.transform = 'translateX(0px) translateY(0px) scale(1)'; //
// });
// tap
hand.on('tap', () => {
setTimeout(() => {
emits('update:modelValue', false)
}, 50)
})
// pan
hand.on('panright panleft panup pandown', (e: anyObj) => {
_node.style.transform =
'translateX(' +
(e.deltaX + x) +
'px)' +
'translateY(' +
(e.deltaY + y) +
'px)' +
'scale(' +
imgConfig.scaleCount * e.scale +
')' //
})
// panend便
hand.on('panend', (e: anyObj) => {
x = e.deltaX + x //
y = e.deltaY + y //
})
})
}
watch(
() => props.modelValue,
show => {
if (show) {
init()
}
},
{ immediate: true },
)
</script>
<style lang="scss" scoped>
.mask {
position: fixed;
top: 0;
left: 0;
z-index: 10000;
width: 100vw;
height: 100%;
background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
background-repeat: no-repeat;
background-size: 100vw 623px;
background-position: center;
.img-box {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
img {
max-width: 100%;
max-height: 100%;
min-width: 300px;
min-height: 16.67vh;
object-fit: contain;
}
}
}
</style>

View File

@ -0,0 +1,178 @@
<script setup lang="ts">
import router from '@/router'
defineProps<{
data: anyObj
}>()
const self = ref()
const hide = ref(false)
const defaultNote: anyObj = {
reward_coin: '数字权益,可用于兑换商城物品、奖学金等。',
reward_diamond: '冲关奖励,可用于兑换个性化皮肤、开启一些特权功能。',
reward_exp: '闯关及完成任务获得经验值。',
reward_activity: '达到对应活跃度可领取日常和周常任务奖励。',
}
//
async function showDetail() {
self.value.hide()
hide.value = true
router.push('/self-coin-detail')
}
function onHide() {
hide.value = false
}
</script>
<template>
<el-popover
ref="self"
v-bind="$attrs"
:popper-class="{
'item-popover': true,
hide: hide,
}"
trigger="hover"
:show-arrow="false"
@hide="onHide"
>
<div class="main">
<div class="info">
<div class="info-img">
<Icon v-if="data?.iconName" :name="data?.iconName" size="160" />
<img v-else :src="data?.rewardResourceUrl" />
<span
v-if="data?.count > 1"
:data-content="data?.count"
class="info-img-count px-count"
>{{ data?.count }}</span
>
</div>
<div class="info-right">
<div class="name">
<span>{{ data?.rewardName }}</span>
<span v-if="data?.rewardName === '乐贝'" class="detail" @click.stop="showDetail"
>明细</span
>
</div>
<div class="count">已拥有{{ data?.personalCount }}</div>
</div>
</div>
<div class="desc">
{{ data?.note || defaultNote[data?.iconName] || '' }}
</div>
</div>
<template #reference>
<slot></slot>
</template>
</el-popover>
</template>
<style lang="scss">
.item-popover {
width: 480px !important;
padding: 0 !important;
background: transparent !important;
border: none !important;
&.hide {
display: none;
}
.main {
background-color: #fff;
right: -496px;
width: 480px;
height: 26vh;
border-radius: 2.5vh;
border: 0.33vh solid $font-color;
padding: $space-2;
box-sizing: border-box;
.info {
display: flex;
margin-bottom: 1.33vh;
.info-img {
position: relative;
width: 160px;
height: 13.33vh;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
border: 0.5vh solid #fff;
border-radius: 2.67vh;
box-shadow: 0 0.5vh 0.5vh 0 rgba(0, 0, 0, 0.15);
}
}
.info-right {
width: calc(100% - 176px);
}
.px-count {
@include font-stroke;
}
.info-img-count {
position: absolute;
bottom: 0;
right: 6px;
color: #fff;
text-align: right;
font-family: $font-special;
font-size: 2vh;
font-style: normal;
font-weight: 400;
line-height: 1.5;
}
.name {
width: 100%;
color: $font-color;
font-family: $font-special;
font-size: 3.33vh;
font-style: normal;
font-weight: 400;
line-height: 1.5;
margin-bottom: 1.33vh;
@include single_ellipsis;
display: flex;
justify-content: space-between;
align-items: center;
.detail {
font-family: $font-text;
font-size: 2.5vh;
cursor: pointer;
color: skyblue;
}
}
.count {
width: 100%;
color: #666;
font-family: 'Alibaba Health Font 2.0 CN';
font-size: 2.33vh;
font-style: normal;
font-weight: 600;
line-height: 3.5vh; /* 150% */
padding-bottom: 1.33vh;
border-bottom: 0.25vh dashed #ccc;
@include single_ellipsis;
}
}
.desc {
width: 100%;
color: $font-color;
font-family: 'Alibaba Health Font 2.0 CN';
font-size: 2.67vh;
font-style: normal;
font-weight: 600;
line-height: 4vh; /* 150% */
@include multiple_ellipsis(2);
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<div ref="$container" class="image_wrapper">
<template v-if="props.src">
<div v-show="loading" class="placeholder"></div>
<img
v-show="!loading"
lazy-load
:src="props.src"
:style="{
cursor: props.src ? 'pointer' : 'default',
}"
@load="completed"
@click="tapEnlargePic"
/>
</template>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
src: string | null
}>()
const emit = defineEmits(['onTapEnlargeImg'])
const loading = ref(true)
const $container = ref()
function completed(event: any) {
loading.value = false
}
//
function tapEnlargePic() {
if (props.src) {
emit('onTapEnlargeImg', props.src)
}
}
watch(
() => props.src,
() => {
loading.value = true
},
)
</script>
<style lang="scss" scoped>
.image_wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0;
img {
border-radius: 1.67vh;
width: 100%;
max-height: 100%;
object-fit: contain;
}
.placeholder {
width: 100%;
height: 100%;
background-color: rgba($color: #fff, $alpha: 0.6);
}
}
</style>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import { $font_color } from '@/styles/variate'
import { px2vh } from '@/utils'
const props = withDefaults(
defineProps<{
text: string | number
size?: number | string
reverse?: boolean //
offset?: string | number //
border?: string | number
color?: string
borderColor?: string
space?: string | number //
}>(),
{
text: '',
size: 48,
reverse: false,
offset: 0,
border: 8,
color: '#fff',
borderColor: $font_color,
space: 0,
},
)
const _size = computed(() => px2vh(props.size))
const _offset = computed(() => `-${px2vh(props.offset)}`)
const _border = computed(() => px2vh(props.border))
const _space = computed(() => px2vh(props.space))
</script>
<template>
<span
v-if="text"
:class="{
'x-line-font': true,
'title-outline': true,
'is-reverse': reverse,
}"
:data-text="text"
>
{{ text }}
</span>
</template>
<style lang="scss" scoped>
.title-outline {
font-family: $font-special;
font-size: v-bind(_size);
line-height: 1.5em;
position: relative;
z-index: 0;
color: v-bind(color);
text-shadow: 0 0.5vh 0.5vh rgba(0, 0, 0, 0.2);
letter-spacing: v-bind(_space);
&::after {
content: attr(data-text);
-webkit-text-stroke: v-bind(_border) v-bind(borderColor);
position: absolute;
left: 0;
top: v-bind(_offset);
z-index: -1;
}
&.is-reverse {
color: $font-color;
&::after {
-webkit-text-stroke-color: #fff;
}
}
}
</style>

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import { userStore } from '@/stores'
import { storeToRefs } from 'pinia'
withDefaults(
defineProps<{
modelValue: boolean
tip?: string
}>(),
{
modelValue: false,
tip: '正在统计成绩中',
},
)
const emits = defineEmits(['update:modelValue', 'ok'])
const OSS_URL = import.meta.env.VITE_OSS_URL
const { userInfo } = storeToRefs(userStore())
function getLoading(type: number) {
return `${OSS_URL}/xuexiaole_clinet/learnWord/loading${type}.gif`
}
// function onOk() {
// emits('update:modelValue', false)
// }
</script>
<template>
<div v-if="modelValue" class="mask">
<img :src="(getLoading(userInfo.sex) as any) || 1" />
<div class="tip">
{{ tip }}
<div class="dot">.</div>
<div class="dot">.</div>
<div class="dot">.</div>
</div>
</div>
</template>
<style lang="scss" scoped>
//
.mask {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
bottom: 0;
right: 0;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
background-color: #00000040;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.tip {
font-family: $font-special;
font-size: 3.33vh;
font-weight: 400;
line-height: 5vh;
text-align: center;
color: beige;
display: flex;
.dot {
&:nth-child(2) {
animation: loading 1s 0s linear infinite;
}
&:nth-child(3) {
animation: loading 1s 0.2s linear infinite;
}
}
}
@keyframes loading {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
</style>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import { px2vw } from '@/utils'
const props = withDefaults(
defineProps<{
type?: 'primary' | 'success' | 'error' | 'warning'
rate: number
height?: number | string
width?: number | string
bg?: string
border?: string
activeBg?: string
}>(),
{
rate: 0,
height: 30,
bg: '#cc8a33',
border: '#c08129',
activeBg: '#fffe36',
},
)
const colorConfig = {
primary: ['#2D39BC', '#6B73E5', '#90FCF7'],
success: ['#27A363', '#25BF6E', '#1FFF8A'],
error: ['#B22731', '#CB3741', '#FF3542'],
warning: ['#C08129', '#CC8A33', '#FFFE36'],
}
const _bg = computed(() => (props.type ? colorConfig[props.type][0] : props.bg))
const _border = computed(() => (props.type ? colorConfig[props.type][1] : props.border))
const _activeBg = computed(() => (props.type ? colorConfig[props.type][2] : props.activeBg))
// const roundRate = computed(() => Math.round(props.rate * 10));
const beforeBg = computed(() => (props.rate > 0 ? _activeBg.value : 'transparent'))
const active_width = computed(() => props.rate * 100 + '%')
const __width = computed(() => (props.width ? px2vw(props.width) : '100%'))
const _height = isNaN(Number(props.height)) ? props.height : px2vw(props.height)
</script>
<template>
<div :class="['progress-bar', 'px-progress-bar', rate >= 1 ? 'all' : null]">
<div v-for="i in 10" :key="i" :class="['progress-bar-item', 'px-item', 'item-' + i]"></div>
</div>
</template>
<style scoped lang="scss">
.progress-bar {
box-sizing: border-box;
width: v-bind(__width);
height: v-bind(_height);
border-radius: v-bind(_height);
background: v-bind(_bg);
overflow: hidden;
display: flex;
&.all {
background: v-bind(_activeBg);
}
&.px-progress-bar {
border: 0.25vh solid v-bind(_border);
position: relative;
// &::before {
// position: absolute;
// content: '';
// width: 10%;
// height: 100%;
// left: -5%;
// top: 0;
// background: v-bind(beforeBg);
// }
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(v-bind(active_width) + 10px);
height: 100%;
border-radius: v-bind(_height) 0 0 v-bind(_height);
background: v-bind(beforeBg);
transform: skew(-45deg) translate(-15px);
}
}
&-item {
box-sizing: border-box;
width: 10%;
transform: skew(-45deg);
flex-shrink: 0;
&.px-item {
border-right: 0.42vh solid v-bind(_border);
}
&:last-child {
// width: 12%;
border-right: none;
}
// &.active {
// background: v-bind(activeBg);
// }
}
}
</style>

View File

@ -0,0 +1,133 @@
<script setup lang="ts">
import { userStore } from '@/stores'
import { storeToRefs } from 'pinia'
withDefaults(
defineProps<{
modelValue: boolean
rewardList: anyObj[]
}>(),
{
modelValue: false,
rewardList: () => [],
},
)
const emits = defineEmits(['update:modelValue', 'ok'])
const OSS_URL = import.meta.env.VITE_OSS_URL
const { userInfo } = storeToRefs(userStore())
const typeToIcon = computed<anyObj>(() => ({
currency: {
iconName: 'reward_coin',
rewardName: '乐贝',
personalCount: userInfo.value.currency,
},
diamond: {
iconName: 'reward_diamond',
rewardName: '钻石',
personalCount: userInfo.value.diamond,
},
experience: {
iconName: 'reward_exp',
rewardName: '经验',
personalCount: userInfo.value.experience,
},
activeDegree: {
iconName: 'reward_activity',
rewardName: '活跃度',
personalCount: userInfo.value.dynamic,
},
}))
function onOk() {
emits('update:modelValue', false)
emits('ok')
}
</script>
<template>
<div v-if="modelValue" class="mask">
<XAe ref="XAeRef" name="receive_reward" :frames="72" />
<div class="content">
<div class="reward-list">
<ItemPopover
v-for="(item, index) in rewardList"
:key="index"
:data="{
iconName: typeToIcon[item.rewardCode]?.iconName,
rewardName: typeToIcon[item.rewardCode]?.rewardName,
personalCount: typeToIcon[item.rewardCode]?.personalCount,
}"
>
<div
:style="{
background: `url('${OSS_URL}/icon/task-${item.rewardCode}.svg') 100% / cover no-repeat`,
}"
class="reward-item"
>
{{ item.count }}
</div>
</ItemPopover>
</div>
<mj-button type="warn" class="button" width="386" @click="onOk">确认</mj-button>
</div>
</div>
</template>
<style lang="scss" scoped>
//
.mask {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
bottom: 0;
right: 0;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
background-color: #00000040;
}
.content {
position: absolute;
top: 8.33vh;
width: 100%;
height: 70.58vh;
display: flex;
justify-content: center;
align-items: center;
.reward-list {
width: 100%;
height: 18.33vh;
display: flex;
justify-content: center;
align-items: center;
overflow-y: scroll;
&::-webkit-scrollbar {
width: 0px;
height: 0;
display: none;
}
.reward-item {
height: 16.67vh;
width: 16.67vh;
display: flex;
align-items: flex-end;
justify-content: flex-end;
padding-right: 10px;
font-family: $font-special;
font-size: 3.33vh;
font-weight: 400;
line-height: 5vh;
text-align: center;
color: white;
margin: 0 10px;
}
}
:deep(.button) {
position: absolute;
bottom: 8.33vh;
.x-button-text {
font-size: 4vh;
}
}
}
</style>

View File

@ -0,0 +1,243 @@
<script setup lang="ts">
import { px2vh } from '@/utils'
// interface selectType {
// label: string
// value: string | number
// }
const props = withDefaults(
defineProps<{
modelValue: any
selectList: any
labelKey?: string
valueKey?: string
width?: any
height?: any
}>(),
{
labelKey: 'label',
valueKey: 'value',
width: 264,
height: '5.33vh',
},
)
const emits = defineEmits(['update:modelValue', 'selectChange'])
const selectInstanceId = `select_${Math.random().toString(36).slice(2)}`
let widthUnit = 'px'
let widthValue = 0
if (typeof props.width === 'string') {
const tmpWidth = Number(props.width)
if (isNaN(tmpWidth)) {
//
widthUnit = props.width.slice(-2)
widthValue = Number(props.width.slice(0, -2))
} else {
//
widthValue = tmpWidth
}
} else {
widthValue = props.width
}
let contentWidthS = ''
let listWidthS = ''
if (widthUnit === 'px') {
contentWidthS = px2vh(widthValue)
listWidthS = px2vh(widthValue - 12)
} else {
contentWidthS = widthValue + widthUnit
listWidthS = widthValue - 1 + widthUnit
}
const contentHeightS = props.height
const OSS_URL = `${import.meta.env.VITE_OSS_URL}`
const bg = `url(${OSS_URL}/icon/select.svg) 100% / contain no-repeat`
const bg2 = `url(${OSS_URL}/icon/check_mark.svg) 100% / contain no-repeat`
const showSelect = ref(false) //
const activeSelect = ref({} as any)
const active = computed({
get() {
activeSelect.value =
props.selectList.find((i: any) => i[props.valueKey] === props.modelValue) || {}
return props.modelValue
},
set(val) {
emits('update:modelValue', val)
},
})
watch(
() => props.selectList,
_nv => {
if (props.selectList && props.selectList[0] && !props.modelValue) {
//
active.value = props.selectList[0][props.valueKey]
}
},
{
deep: true,
immediate: true,
},
)
function closeSelect() {
showSelect.value = false
}
const handleGlobalSelectOpen = (event: Event) => {
const customEvent = event as CustomEvent<string>
if (customEvent.detail !== selectInstanceId) {
closeSelect()
}
}
function toggleSelect() {
const nextState = !showSelect.value
if (nextState) {
window.dispatchEvent(new CustomEvent('xuexiaole-select-open', { detail: selectInstanceId }))
}
showSelect.value = nextState
}
onMounted(() => {
window.addEventListener('xuexiaole-select-open', handleGlobalSelectOpen as EventListener)
})
onUnmounted(() => {
window.removeEventListener('xuexiaole-select-open', handleGlobalSelectOpen as EventListener)
})
const selectChange = (item: any) => {
if (active.value === item[props.valueKey]) return
activeSelect.value = item
closeSelect()
active.value = item[props.valueKey]
emits('selectChange', activeSelect.value)
}
</script>
<template>
<div :class="['select-box', { open: showSelect }]">
<div class="content" @click="toggleSelect">
<span>{{ activeSelect[labelKey] }}</span
><span
:class="{ icon: true, iconActive: showSelect }"
:style="{
background: bg,
}"
>
</span>
</div>
<div v-show="showSelect" class="selectList">
<div class="selectList-box hidden_scroll">
<div
v-for="item in selectList"
:key="item[valueKey]"
:class="{ 'select-item': true, active: item[valueKey] === active }"
@click="selectChange(item)"
>
<span>
{{ item[labelKey] }}
</span>
<span
v-show="item[valueKey] === active"
class="icon"
:style="{
background: bg2,
}"
>
</span>
</div>
</div>
</div>
<div v-show="showSelect" class="mask" @click="closeSelect"></div>
</div>
</template>
<style scoped lang="scss">
//
.select-box {
position: relative;
z-index: 1;
font-family: $font-text;
font-size: 2.33vh;
color: #ffffffd9;
&.open {
z-index: 1100;
}
line-height: 4vh;
.content {
background: #00000099;
width: v-bind(contentWidthS);
height: v-bind(contentHeightS);
border-radius: 1.67vh;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
cursor: pointer;
span {
@include single_ellipsis;
}
.icon {
transition: all 0.3s;
height: 2.33vh;
width: 2.92vh;
}
.iconActive {
transform: rotate(180deg);
}
}
.selectList {
width: v-bind(contentWidthS);
max-height: 24.67vh;
background: #4f5458;
margin-top: 0.83vh;
position: absolute;
z-index: 1101;
border-radius: 1.67vh;
box-sizing: border-box;
padding: 0.42vh;
border: 1px solid #5f676d;
}
.selectList-box {
width: v-bind(listWidthS);
max-height: 23.83vh;
overflow: scroll;
padding: 1.25vh;
box-sizing: border-box;
.select-item {
display: flex;
align-items: center;
color: #ccd4d4;
width: 100%;
padding: 0.42vh 0;
justify-content: space-between;
cursor: pointer;
font-size: 2.33vh;
.icon {
width: 32px;
height: 2.67vh;
// background-size: contain;
}
&.active {
color: #fff;
}
}
}
.mask {
position: absolute;
height: 200vh;
width: 200vw;
z-index: 1100;
left: -100vw;
top: -100vh;
}
}
</style>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import { px2vh } from '@/utils'
const props = withDefaults(
defineProps<{
modelValue: string | number
dataList: any
format?: {
label: string
value: string
}
round?: boolean
type?: 'normal' | 'small'
}>(),
{
dataList: () => [{ label: '', value: '' }],
round: false,
type: 'normal',
},
)
const emits = defineEmits(['change', 'update:modelValue'])
const formatValue = computed(() => (props.format ? props.format.value : 'value'))
const handleSelect = (item: any) => {
if (props.modelValue === item[formatValue.value]) return
emits('update:modelValue', item[formatValue.value])
emits('change', item)
}
const borderRadius = computed(() => px2vh(props.round ? 60 : 20))
const itemBorderRadius = computed(() => px2vh(props.round ? 52 : 16))
const itemWidth = computed(() => (props.type === 'small' ? 'max-content' : px2vh(387)))
const itemSize = computed(() => px2vh(props.type === 'small' ? 28 : 32))
</script>
<template>
<div class="select-list">
<div
v-for="item in dataList"
:key="item.value"
:class="{
'data-item': true,
active: props.modelValue === item[formatValue],
}"
@click="handleSelect(item)"
>
{{ item[format ? format.label : 'label'] }}
</div>
</div>
</template>
<style lang="scss" scoped>
.select-list {
height: 5vh;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 0.33vh;
border-radius: v-bind(borderRadius);
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(3px);
.data-item {
cursor: pointer;
font-family: $font-text;
font-size: v-bind(itemSize);
font-weight: 400;
line-height: 4.17vh;
height: 4.33vh;
text-align: center;
padding: 0 16px;
width: v-bind(itemWidth);
border-radius: v-bind(itemBorderRadius);
color: rgba(255, 255, 255, 0.65);
box-sizing: border-box;
&.active {
background: linear-gradient(249deg, #4de798 26.9%, #a2fb6e 79.86%);
backdrop-filter: blur(3px);
color: $font-color;
}
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div :style="{ width, height }" class="lottie__container">
<div
ref="lottieRef"
class="canvasId"
canvas-id="canvasId"
type="2d"
style="width: 100%; height: 100%"
></div>
</div>
</template>
<script setup lang="ts" scoped>
import { onMounted, ref, watch } from 'vue'
import lottie from 'lottie-web'
import type { LottieEvent } from '@/components/ShowLottie/type'
import { onUnmounted } from 'vue'
const props = withDefaults(
defineProps<{
isPlay?: boolean
delay?: number
width?: string
height?: string
renderer?: 'svg' | 'canvas' | 'html'
name?: string
autoplay?: boolean
loop?: boolean
rendererSettings?: {
preserveAspectRatio?: string
}
urlName2?: string
}>(),
{
delay: 0,
renderer: 'svg',
loop: true,
autoplay: true,
},
)
// lottie
const emits = defineEmits<{
(e: 'getAnimation', value: LottieEvent): void
(e: 'getDom', value: Element): void
}>()
const OSS_URL = import.meta.env.VITE_OSS_URL
const animation = ref<LottieEvent>()
const lottieRef = ref()
const getJSON = (name: string | any) => {
return `${OSS_URL}/xuexiaole_clinet/lottieJSON/${name}.json` //
}
function init() {
// lottie.loadAnimation
if (lottieRef.value) {
const config = {
container: lottieRef.value, // the dom element that will contain the animation
renderer: props.renderer,
loop: props.loop,
autoplay: false,
path: getJSON(props.name), // the path to the animation json
}
if (props.rendererSettings) {
config.rendererSettings = props.rendererSettings
}
animation.value = lottie.loadAnimation(config)
if (props.autoplay) {
setTimeout(() => {
animation.value?.play()
}, props.delay)
}
emits('getAnimation', animation.value)
emits('getDom', lottieRef.value)
}
}
defineExpose({
animation,
})
onMounted(() => {
init()
})
onUnmounted(() => {
animation.value?.destroy()
})
watch(
() => props.isPlay,
() => {
if (props.isPlay && lottieRef.value) {
setTimeout(() => {
animation.value!.play()
}, props.delay)
} else if (!props.isPlay && lottieRef.value) {
// console.log('props.isPlay',props.isPlay)
animation.value!.stop()
}
},
{ deep: true, immediate: true },
)
</script>
<style lang="scss" scoped>
.lottie__container {
pointer-events: none;
}
</style>

View File

@ -0,0 +1,15 @@
type Segment = [number, number]
export interface LottieEvent {
play: () => void // 播放
stop: () => void // 停止
setSpeed: (speed: number) => void // 设置播放速度speed: 1表示正常速度
setDirection: (direction: 1 | -1) => void // 设置播放方向direction1 表示正向,-1 表示反向)
goToAndStop: (value: number, isFrame?: boolean) => void // 跳到某个时刻/帧并停止value数值isFrame定义第一个参数是基于时间的值还是基于帧的值默认为 false
goToAndPlay: (value: number, isFrame?: boolean) => void // 跳到某个时刻/帧并播放value数值isFrame定义第一个参数是基于时间的值还是基于帧的值默认为 false
playSegments: (segments: Segment | Segment[], forceFlag?: boolean) => void // 播放片段segments数组。可以包含 2 个数值,这些数值将用作动画的第一帧和最后一帧。或者可以包含一系列数组,每个数组具有 2 个数值forceFlag布尔。如果设置为 false它将等待当前段完成。如果为 true它将立即更新值。
destroy: () => void // 销毁
getDuration: (inFrames: boolean) => void // 获取动画一个周期时间inFrames如果为 true则以帧为单位返回持续时间如果为 false则以秒为单位。
[propname: string]: any
}

View File

@ -0,0 +1,161 @@
<script setup lang="ts">
import { px2vh } from '@/utils'
const props = defineProps<{
modelValue: string | number
config: any[]
prop?: { id?: any; name?: string }
}>()
const emit = defineEmits(['update:modelValue', 'handleChange'])
function handleSelectClass(i: Record<string, string>) {
if (props.modelValue !== i[props.prop?.id || 'id']) {
emit('update:modelValue', i[props.prop?.id || 'id'])
emit('handleChange', i)
}
}
const OSS_URL = import.meta.env.VITE_OSS_URL
// const bgUrl = computed(() => {
// return (isActive: boolean) => {
// if (isActive) {
// return ``;
// } else {
// return ``;
// }
// };
// });
</script>
<template>
<div class="sidebar-box hidden_scroll">
<div class="sidebar">
<!-- viewBox="0 0 4 100%" -->
<svg xmlns="http://www.w3.org/2000/svg" class="px-svg">
<path
class="px-path"
d="M2 55V1000000"
stroke="white"
stroke-opacity="0.45"
stroke-linecap="round"
stroke-linejoin="round"
stroke-dasharray="16 16"
/>
</svg>
<div class="list">
<div
v-for="i in config"
:key="i[prop?.id || 'id']"
:class="{ 'sidebar-item': true, active: modelValue === i[prop?.id || 'id'] }"
:style="{
'font-size': px2vh(32 + (7 - i[prop?.name || 'name'].length) * 3),
}"
@click="handleSelectClass(i)"
>
{{ i[prop?.name || 'name'] }}
<Icon v-if="i.warning" name="warning" width="30" height="30" class="warning_icon"></Icon>
<div v-if="modelValue === i[prop?.id || 'id']" class="active_bg">
<img :src="`${OSS_URL}/urm/sidebar_item_active.png`" />
</div>
<div v-else class="bg">
<img :src="`${OSS_URL}/urm/sidebar_item.svg`" />
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.sidebar-box {
height: 100%;
}
.sidebar {
position: relative;
svg {
position: absolute;
left: 15.83%;
z-index: 0;
height: calc(100% - 3.33vh);
// viewbox: 0 0 4 100%;
}
.px-svg {
width: 1.67%;
.px-path {
stroke-width: 4px;
}
}
.list {
display: flex;
flex-direction: column;
}
&-item {
cursor: pointer;
position: relative;
z-index: 1;
box-sizing: border-box;
height: 9.17vh;
color: #fff;
text-shadow: 0 0.33vh 0.17vh rgba(0, 0, 0, 0.25);
font-family: $font-special;
font-style: normal;
font-weight: 400;
line-height: 5vh;
padding: 2.08vh 0 2.08vh 30%;
margin-bottom: 6vh;
&:last-child {
margin-bottom: 0;
}
.active_bg {
position: absolute;
width: 100%;
height: 9.17vh;
top: 0;
left: 0;
z-index: -1;
img {
width: 100%;
height: 100%;
object-fit: fill;
}
}
.bg {
position: absolute;
// width: 40px;
width: 16.67%;
height: 3.33vh;
top: 2.92vh;
// left: 20px;
left: 8.33%;
z-index: -1;
img {
width: 100%;
height: 100%;
object-fit: fill;
}
}
&.active {
background-position: top left;
background-size: 100% 110px;
padding: 3.17vh 12px 1vh;
color: $font-color;
text-shadow: none;
text-align: center;
position: relative;
width: 20vh;
// min-width: 17.83vh;
text-align: center;
}
.warning_icon {
position: absolute;
left: 0;
top: 1.67vh;
height: 2.67vh !important;
width: 2.67vh !important;
}
}
}
</style>

View File

@ -0,0 +1,211 @@
<script setup lang="ts">
import SignaturePad from 'signature_pad'
const props = withDefaults(
defineProps<{
bg?: string
pen?: string
}>(),
{
bg: '#F8FAF9',
pen: 'black',
},
)
const canvasPanelRef = ref()
const canvasRef = ref()
let signaturePad: any = null
let hasBackground = false
let backgroundSrc: string | null = null
let paintTaskId = 0
function fillCanvasBackground() {
if (!canvasRef.value) return
const canvas = canvasRef.value as HTMLCanvasElement
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = props.bg
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
async function drawBackground(src: string, taskId = paintTaskId) {
if (!canvasRef.value || !src) return
const canvas = canvasRef.value as HTMLCanvasElement
const ctx = canvas.getContext('2d')
if (!ctx) return
const draw = async (imgSrc: string) => {
const img = new Image()
img.src = imgSrc
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve()
img.onerror = () => reject(new Error('Image load error'))
})
if (taskId !== paintTaskId) return
const sw = img.naturalWidth || img.width
const sh = img.naturalHeight || img.height
const cw = canvas.width
const ch = canvas.height
const scale = Math.min(1, cw / sw, ch / sh)
const dw = sw * scale
const dh = sh * scale
const dx = (cw - dw) / 2
const dy = (ch - dh) / 2
ctx.drawImage(img, dx, dy, dw, dh)
}
try {
await draw(src)
} catch {
try {
const res = await fetch(src)
const blob = await res.blob()
const reader = new FileReader()
await new Promise<void>(resolve => {
reader.onload = () => resolve()
reader.readAsDataURL(blob)
})
await draw(String(reader.result || ''))
} catch {}
}
}
async function repaintWith(pointGroups?: any[]) {
if (!signaturePad) return
const taskId = ++paintTaskId
signaturePad.clear()
if (backgroundSrc) {
await drawBackground(backgroundSrc, taskId)
}
if (taskId !== paintTaskId) return
if (pointGroups?.length) {
signaturePad.fromData(pointGroups, backgroundSrc ? { clear: false } : undefined)
}
}
function handleSave() {
if (signaturePad.isEmpty() && !hasBackground) {
return { url: null, blob: null }
}
return {
url: signaturePad.toDataURL('image/jpeg', 0.85),
data: JSON.stringify(signaturePad.toData()),
}
}
function handleSaveAsync(): Promise<{ blob: Blob | null; data: string | null }> {
return new Promise(resolve => {
if (signaturePad.isEmpty() && !hasBackground) {
resolve({ blob: null, data: null })
return
}
const canvas = canvasRef.value as HTMLCanvasElement
const pointData = JSON.stringify(signaturePad.toData())
canvas.toBlob(blob => resolve({ blob, data: pointData }), 'image/jpeg', 0.85)
})
}
async function setBackgroundImage(url: string) {
if (!canvasRef.value || !url || !signaturePad) return
const data = signaturePad.toData()
const taskId = ++paintTaskId
signaturePad.clear()
backgroundSrc = url
await drawBackground(url, taskId)
if (taskId !== paintTaskId) return
if (data?.length) signaturePad.fromData(data, { clear: false })
hasBackground = true
}
function handleRepaint(data?: string) {
if (!signaturePad) return
if (!data) {
repaintWith().then(() => {})
return
}
try {
repaintWith(JSON.parse(data)).then(() => {})
} catch {
repaintWith().then(() => {})
}
}
function handleLoad(data?: string) {
if (!signaturePad || !data) return
try {
repaintWith(JSON.parse(data)).then(() => {})
} catch {
repaintWith().then(() => {})
}
}
function handleFallback() {
if (!signaturePad) return
const data = signaturePad.toData()
data.pop()
repaintWith(data).then(() => {})
}
function handleClear() {
if (!signaturePad) return
paintTaskId++
backgroundSrc = null
hasBackground = false
signaturePad.clear()
fillCanvasBackground()
}
function handleEraser() {
signaturePad.penColor = props.bg
signaturePad.minWidth = 50
signaturePad.maxWidth = 50
}
function handlePen() {
signaturePad.penColor = props.pen
signaturePad.minWidth = 0.5
signaturePad.maxWidth = 2.5
}
onMounted(() => {
if (canvasRef.value) {
canvasRef.value.width = canvasPanelRef.value.clientWidth
canvasRef.value.height = canvasPanelRef.value.clientHeight
signaturePad = new SignaturePad(canvasRef.value, {
backgroundColor: props.bg,
throttle: 0,
})
fillCanvasBackground()
}
})
defineExpose({
handleSave,
handleSaveAsync,
handleRepaint,
handleLoad,
handleFallback,
handleClear,
handlePen,
handleEraser,
setBackgroundImage,
})
</script>
<template>
<div class="signature-pad">
<div ref="canvasPanelRef" class="canvas-panel">
<canvas ref="canvasRef" width="300" height="200"></canvas>
</div>
</div>
</template>
<style scoped lang="scss">
.signature-pad {
width: 100%;
height: 100%;
.canvas-panel {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
const props = defineProps<{
hideHead: boolean
columns: anyObj[]
tableData: anyObj[]
}>()
const emit = defineEmits(['update:modelValue', 'handleChange'])
</script>
<template>
<table class="table">
<thead>
<tr class="table-head" :style="{ display: hideHead ? 'none' : 'table-row' }">
<th v-for="(item, index) in columns" :key="index">
{{ item.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(i, index) in tableData" :key="index" class="table-row">
<td v-for="(c, index) in columns" :key="index">
<div v-if="c.prop === 'value'" class="content">
<!-- <img
v-if="i.abbr"
class="img-Speech"
:src="`${OSS_URL}/icon/part_${i.abbr[i.abbr.length - 1] === '.' ? i.abbr.slice(0, i.abbr.length - 1) : i.abbr}.svg`"
alt=""
/> -->
<AbbrImg height="50" :abbr="i.abbr"></AbbrImg>
{{ i[c.prop] }}
</div>
<span v-else>
{{ i[c.prop] }}
</span>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped lang="scss">
* {
box-sizing: content-box !important;
}
.table {
font-size: 1.17vh;
line-height: 1.1;
width: 100%;
color: #6a3906;
&-head {
background: transparent;
border-radius: 0.67vh;
th {
width: 100px;
font-weight: 400;
text-align: left;
padding: 0.83vh 16px;
&:first-child {
border-radius: 0.67vh 0 0 0.67vh;
}
&:last-child {
border-radius: 0 0.67vh 0.67vh 0;
}
}
}
&-row {
&:last-child {
td {
border-bottom: none;
}
}
td {
height: 4.67vh;
padding: 0.83vh 16px;
box-sizing: border-box;
border-bottom: 0.08vh solid #d9d163;
border-right: 0.08vh solid #d9d163;
&:last-child {
border-right: none;
}
.content {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
// .img-Speech {
// width: auto;
// height: 50px;
// margin-right: 4px;
// }
</style>

View File

@ -0,0 +1,22 @@
<script lang="ts">
import { defineComponent, createVNode, reactive } from 'vue'
import { ElTableColumn } from 'element-plus'
import { uuid } from '@/utils/random'
export default defineComponent({
name: 'Column',
props: {
attr: {
type: Object,
required: true,
},
},
setup(props, { slots }) {
const attr = reactive(props.attr)
attr['column-key'] = attr['column-key'] ? attr['column-key'] : attr.prop || uuid()
return () => {
return createVNode(ElTableColumn, attr, slots.default)
}
},
})
</script>

View File

@ -0,0 +1,229 @@
<script setup lang="ts">
import type { ElTable } from 'element-plus'
// import { ref, nextTick, inject, computed } from 'vue'
// import Column from '/@/components/table/column/index.vue'
// import FieldRender from '/@/components/table/fieldRender/index.vue'
// import { useConfig } from '/@/stores/config'
import type { XbTableClass } from '@/utils/xbTable'
const props = withDefaults(defineProps<Props>(), {
pagination: true,
class: 'xb-table',
})
defineOptions({ inheritAttrs: false })
interface Props extends /* @vue-ignore */ Partial<InstanceType<typeof ElTable>> {
pagination?: boolean
class?: string
}
const tableBox = ref()
const pageBox = ref()
const tableRef = ref<InstanceType<typeof ElTable>>()
const xbTable = inject('xbTable') as XbTableClass
// const onSortChange = ({ order, prop }: { order: string; prop: string }) => {
// xbTable.onTableAction('sort-change', { prop: prop, order: order ? (order == 'ascending' ? 'asc' : 'desc') : '' })
// }
function onTableSelect(selection: anyObj[], row: anyObj) {
xbTable.table.selection = selection
// xbTable.onTableAction('onTableSelect', {
// selection,
// row,
// });
}
function onTableSelectAll(selection: anyObj[]) {
xbTable.table.selection = selection
// xbTable.onTableAction('onTableSelectAll', {
// selection,
// });
}
function onTableSizeChange(val: number) {
xbTable.onTableAction('handlePageSize', { size: val })
}
function onTableCurrentChange(val: number) {
xbTable.onTableAction('handlePageCurrent', { current: val })
}
function onTableSortChange(val: {
column: any
prop: string
order: 'ascending' | 'descending' | null
}) {
xbTable.onTableAction('sortChange', { type: val.order })
}
const getRef = () => {
return tableRef.value
}
const getBox = () => {
let tableRect = tableBox.value?.getBoundingClientRect()
let pageRect = pageBox.value?.getBoundingClientRect()
return {
top: tableRect?.top || 0,
pageHeight: pageRect?.height + 34 || 0,
}
}
function getRowClass(row: { row: any; rowIndex: number }): string {
let data = row.row
let res: string[] = []
if (xbTable.table.extend?.setRowClass) {
res.push(xbTable.table.extend.setRowClass(row))
} else if (data.children && data.children.length > 0) {
res.push('row-expand-has')
} else {
res.push('row-expand-unhas')
}
return res.join(' ')
}
function calculateWidth(baseWidth: number) {
return (baseWidth / 1920) * window.innerWidth
}
defineExpose({
getRef,
getBox,
})
</script>
<template>
<div :class="props.class">
<slot name="neck"></slot>
<!-- @select-all="onSelectAll"
@select="onSelect" @selection-change="onSelectionChange" @sort-change="onSortChange"
@row-dblclick="xbTable.onTableDblclick" -->
<div ref="tableBox">
<el-table
ref="tableRef"
v-loading="xbTable.table.loading || false"
v-bind="$attrs"
class="xb-data-table"
header-cell-class-name="table-header-cell"
style="width: 100%"
:default-expand-all="xbTable.table.expandAll"
:data="xbTable.table.data"
:row-key="xbTable.table.pk"
:expand-row-keys="xbTable.table.expandRowKeys"
:border="xbTable.table.border"
:stripe="xbTable.table.stripe"
:row-class-name="getRowClass"
:header-cell-style="xbTable.table.headerCellStyle"
@select="onTableSelect"
@select-all="onTableSelectAll"
>
<!-- @sort-change="onTableSortChange" -->
<!-- <slot name="columnPrepend"></slot> -->
<!-- :key="item.render == 'switch' ? 'sw-' + item.prop + '-' + key + '-' + scope.row[item.prop!] : key" -->
<template v-for="(item, key) in xbTable.table.column">
<el-table-column
v-if="item.show !== false"
:key="item.prop || key + '-column'"
v-bind="item"
:width="item.type === 'index' ? calculateWidth(100) : item.width"
:class-name="item.type ? `el-table-column--${item.type}` : `column-${item.prop}`"
:selectable="(row: anyObj) => (item.selectable ? item.selectable(row) : true)"
>
<template v-if="item.type === 'sort'">
<Icon
name="iconfont"
class="icon-a-tuodong1616 sort table-row-sort"
color="#999"
size="12"
width="unset"
height="unset"
/>
</template>
<template v-if="item.render" #default="scope">
<Render :render="item.render" :scope="scope"></Render>
</template>
</el-table-column>
</template>
<!-- <slot name="columnAppend"></slot> -->
</el-table>
</div>
<div v-if="props.pagination" ref="pageBox" class="table-pagination">
<el-pagination
:current-page="xbTable.table.page!.current"
:page-size="xbTable.table.page!.size"
:page-sizes="xbTable.table.pageSizes"
background
:layout="
xbTable.table.layout?.shrink
? 'prev, next, jumper'
: 'sizes,total, ->, prev, pager, next, jumper'
"
:total="xbTable.table.page!.total"
@size-change="onTableSizeChange"
@current-change="onTableCurrentChange"
></el-pagination>
</div>
<!-- <slot name="footer"></slot> -->
</div>
</template>
<style scoped lang="scss">
// .xb-data-table {
// width: 100%;
// }
// .xb-data-table :deep(.el-button + .el-button) {
// margin-left: 6px;
// }
// .xb-data-table :deep(.table-header-cell) .cell {
// color: var(--el-text-color-primary);
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
// }
.table-pagination {
box-sizing: border-box;
width: 100%;
max-width: 100%;
background-color: #fff;
padding: 1.08vh 15px;
}
.sort {
cursor: grab;
}
.xb-data-table {
:deep(.el-table-column--sort) {
.cell {
display: flex;
align-items: center;
justify-content: center;
}
}
:deep(.el-button) {
min-width: 80px;
}
:deep(.cell) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.el-tag {
margin-right: 4px;
}
}
}
:deep(.row-expand-unhas .el-table__expand-icon) {
// pointer-events: none;
opacity: 0;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { defineComponent, createVNode, reactive } from 'vue'
import { ElTableColumn } from 'element-plus'
export default defineComponent({
name: 'Render',
props: {
scope: Object,
render: Function,
},
setup(props, { slots }) {
const R: any = markRaw({
render: props.render,
})
return () => {
return props.render ? R.render(h, props.scope) : ''
// return props.render ? props.render(h, props.scope) : ''
}
},
})
</script>

View File

@ -0,0 +1,182 @@
<script setup lang="ts">
import { useLoad } from '@/hooks'
import urmUrl from './urmUrl.json'
import { isWebEvn, $sleep } from '@/utils'
import { loading } from '@/components/mj/mj-loading'
/**
"/urm/system_activity_bg.svg",
"/urm/studyAnalysis_bg.svg",
"/urm/textbook_dia_bg.svg",
"/urm/task-week-kelingqu.svg",
"/urm/task-week-normal.svg",
"/urm/task-week-yilingqu.svg",
"/urm/task_teacher.svg",
"/urm/task_week.svg"
"/urm/mail_bg2.svg",
"/urm/task-day-kelingqu.svg",
"/urm/task-day-normal.svg",
"/urm/task-day-yilingqu.svg",
"/urm/task-lead-kelingqu.svg",
"/urm/task-lead-normal.svg",
*/
const props = withDefaults(
defineProps<{
urls?: string[]
duration?: number
}>(),
{
urls: () => urmUrl.urls,
duration: 0,
},
)
const emits = defineEmits(['loaded'])
const OSS_URL = import.meta.env.VITE_OSS_URL
const bg = `url(${OSS_URL}/urm/urm_load_bg.png) 100%/cover no-repeat`
const { size, loadProgress, loadSize, loaded, loadText, init } = useLoad()
const simulationLoadProgress = ref('0%') //
//
loading.hide()
async function envInit() {
if (!isWebEvn) {
await $sleep(3000)
}
init(props.urls.map(i => `${OSS_URL}${i}`))
}
if (props.duration === 0) {
envInit()
// if (isWebEvn) {
// init(props.urls.map(i => `${OSS_URL}${i}`));
// } else {
// getAssetsList()
// .then(({ data: { data: res } }) => {
// init(res, { android: true });
// })
// .catch(() => {
// loaded.value = true;
// });
// }
watch(loaded, newVal => {
if (newVal) {
emits('loaded', loadSize.value)
}
})
} else {
//
let step = 0
const totalStep = (props.duration * 1000) / 300
const stepLength = 100 / totalStep
let timer: any = setInterval(() => {
if (step >= totalStep) {
timer = null
clearInterval(timer)
emits('loaded', 100)
return
}
step++
let value = stepLength * step
if (value > 100) value = 100
simulationLoadProgress.value = value.toFixed(0) + '%'
}, 300)
}
const progress = computed(() =>
props.duration ? simulationLoadProgress.value : loadProgress.value,
)
</script>
<template>
<div class="load">
<div class="box">
<div class="progress">
<div class="strip">
<Icon name="urm_load_logo" size="160" />
</div>
</div>
<span class="num">{{ progress }}</span>
</div>
<div class="text">
{{ loadText || '学习资源加载中' }}
<template v-if="size > 0"> {{ loadSize }}/{{ size }} </template>
</div>
</div>
</template>
<style lang="scss" scoped>
.load {
width: 100%;
height: 100%;
background: v-bind(bg);
padding: 0 160px;
position: relative;
z-index: 9999;
.box {
position: absolute;
bottom: 7.5vh;
display: flex;
align-items: center;
.progress {
width: 1500px;
height: 2.67vh;
border-radius: 2.5vh;
background: rgba(0, 0, 0, 0.65);
box-shadow: 0 0 0.42vh 0 rgba(0, 0, 0, 0.25) inset;
.strip {
// transition: width 0.3s linear;
width: v-bind(progress);
height: 100%;
background: $theme-color;
border-radius: 2.5vh;
position: relative;
background: linear-gradient(249deg, #4de798 26.9%, #a2fb6e 79.86%);
box-shadow: 0 0.33vh 0.33vh 0 rgba(0, 0, 0, 0.15);
.x-icon {
position: absolute;
right: -80px;
top: -14.17vh;
}
&::after {
content: '';
display: block;
position: absolute;
right: -28px;
top: -0.83vh;
width: 4.67vh;
height: 4.67vh;
border-radius: 50%;
box-shadow: 0 0.33vh 0.33vh rgba(0, 0, 0, 0.15);
background: radial-gradient(189.06% 189.06% at 49.22% 50.78%, #ffffff 0%, #becadf 100%);
}
}
}
.num {
color: $font-color;
font-family: $font-text;
font-size: 2.67vh;
line-height: 1.5em;
margin-left: $space-3;
}
}
.text {
position: absolute;
width: 1500px;
text-align: center;
bottom: 2vh;
color: $font-color;
font-family: $font-text;
font-size: 2.67vh;
line-height: 1.5em;
}
}
</style>

View File

@ -0,0 +1,119 @@
{
"urls": [
"/subject/book_shelf_left.svg",
"/subject/book_shelf_top.svg",
"/subject/book_shelf_bottom.svg",
"/urm/bg_home.svg",
"/urm/bg_login.svg",
"/urm/book_bg.svg",
"/urm/mail_bg.svg",
"/urm/main_bg.svg",
"/urm/urm_load_bg.png",
"/urm/subject_target_dia_bg.svg",
"/urm/ip_boy.png",
"/urm/ip_girl.png",
"/icon/nav_activity.svg",
"/icon/nav_analysis.svg",
"/icon/nav_mailbox.svg",
"/icon/nav_notice.svg",
"/icon/nav_ranking.svg",
"/icon/nav_setting.svg",
"/icon/nav_homeWork.svg",
"/urm/home_app.png",
"/urm/home_subject.png",
"/urm/home_task.png",
"/urm/home_urge.png",
"/urm/home_english.png",
"/icon/diamond.svg",
"/icon/coin.svg",
"/icon/warning.svg",
"/icon/gender_1.svg",
"/icon/gender_2.svg",
"/icon/close_dia.svg",
"/icon/left_arrow.svg",
"/icon/mail_open.svg",
"/icon/open_switch.svg",
"/icon/close_switch.svg",
"/icon/leaf_solid.svg",
"/icon/thumbs.svg",
"/icon/vip_level_1.svg",
"/icon/vip_level_10.svg",
"/icon/vip_level_11.svg",
"/icon/vip_level_12.svg",
"/icon/vip_level_13.svg",
"/icon/vip_level_14.svg",
"/icon/vip_level_15.svg",
"/icon/vip_level_16.svg",
"/icon/vip_level_17.svg",
"/icon/vip_level_18.svg",
"/icon/vip_level_19.svg",
"/icon/vip_level_2.svg",
"/icon/vip_level_20.svg",
"/icon/vip_level_3.svg",
"/icon/vip_level_4.svg",
"/icon/vip_level_5.svg",
"/icon/vip_level_6.svg",
"/icon/vip_level_7.svg",
"/icon/vip_level_8.svg",
"/icon/vip_level_9.svg",
"/urm/userInfo_center_bg.png",
"/urm/btn_primary_left.svg",
"/urm/btn_primary_right.svg",
"/urm/empty_activity.svg",
"/urm/empty_announcement.svg",
"/urm/empty_course.svg",
"/urm/empty_dev.svg",
"/urm/empty_email.svg",
"/urm/empty_goods.svg",
"/urm/empty_knowledge.png",
"/urm/empty_learn.svg",
"/urm/empty_network.svg",
"/urm/empty_practice.svg",
"/urm/empty_rank.svg",
"/urm/empty_report.svg",
"/urm/empty_search.svg",
"/urm/empty_task.svg",
"/urm/empty_test.svg",
"/urm/sidebar_item_active.png",
"/urm/sidebar_item.svg",
"/urm/header_bg.svg",
"/urm/subject_item_pattern.svg",
"/urm/textbook_tool_ziyouxue.png",
"/urm/textbook_tool_aijingzhunxue.png",
"/urm/textbook_tool_cuotiben.png",
"/urm/textbook_tool_ziyouzujuan.png",
"/urm/ranking_bg_thumbs.svg",
"/urm/subject_dili.png",
"/urm/subject_fazhi.png",
"/urm/subject_huaxue.png",
"/urm/subject_item_pattern.svg",
"/urm/subject_kexue.png",
"/urm/subject_lishi.png",
"/urm/subject_shengwu.png",
"/urm/subject_shuxue.png",
"/urm/subject_target_dia_bg.svg",
"/urm/subject_wuli.png",
"/urm/subject_yingyu.png",
"/urm/subject_yuwen.png",
"/urm/subject_zhengzhi.png",
"/urm/task-yilingqu.png",
"/urm/task-kelingqu.png",
"/urm/task-normal.png",
"/urm/task_achievement.svg",
"/urm/task_day.svg",
"/urm/task_parent.svg",
"/urm/task_lead.svg"
]
}

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