‘add第一次提交’
This commit is contained in:
commit
a969b6482b
7
.env
Normal file
7
.env
Normal 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
9
.env.test
Normal 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
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
!.prettierrc.js
|
||||||
68
.eslintrc.cjs
Normal file
68
.eslintrc.cjs
Normal 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
35
.gitignore
vendored
Normal 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
1
.husky/commit-msg
Executable file
@ -0,0 +1 @@
|
|||||||
|
npx --no-install commitlint --edit "$1"
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
|||||||
|
npm run lint-staged
|
||||||
14
.prettierrc.cjs
Normal file
14
.prettierrc.cjs
Normal 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
30
.vscode/extensions.json
vendored
Normal 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
5
README.md
Normal 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).
|
||||||
67
backup-config/.eslintrc.js
Normal file
67
backup-config/.eslintrc.js
Normal 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'],
|
||||||
|
}
|
||||||
14
backup-config/.prettierrc.js
Normal file
14
backup-config/.prettierrc.js
Normal 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',
|
||||||
|
}
|
||||||
36
backup-config/settings.json
Normal file
36
backup-config/settings.json
Normal 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
24
commitlint.config.cjs
Normal 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
40
index.html
Normal 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
104
package.json
Normal 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
7604
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal 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
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
19
src/App.vue
Normal 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
79
src/api/chat.ts
Normal 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
|
||||||
32
src/api/classroomInteraction.ts
Normal file
32
src/api/classroomInteraction.ts
Normal 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
284
src/api/global.ts
Normal 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
140
src/api/http.ts
Normal 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
55
src/api/log.ts
Normal 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
84
src/api/login.ts
Normal 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
65
src/api/only-last.ts
Normal 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
40
src/api/order.ts
Normal 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
34
src/api/ranking.ts
Normal 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
90
src/api/request.ts
Normal 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
30
src/api/service/basic.ts
Normal 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 || {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/api/service/coin-transaction.ts
Normal file
17
src/api/service/coin-transaction.ts
Normal 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()
|
||||||
17
src/api/service/self-coin-detail.ts
Normal file
17
src/api/service/self-coin-detail.ts
Normal 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
172
src/api/service/task.ts
Normal 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()
|
||||||
17
src/api/service/user-task.ts
Normal file
17
src/api/service/user-task.ts
Normal 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()
|
||||||
48
src/api/service/word-table.ts
Normal file
48
src/api/service/word-table.ts
Normal 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()
|
||||||
126
src/api/service/word-test.ts
Normal file
126
src/api/service/word-test.ts
Normal 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
330
src/api/studentManage.ts
Normal 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
80
src/api/studyAnalysis.ts
Normal 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
879
src/api/subject/config.ts
Normal 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))
|
||||||
|
}
|
||||||
83
src/api/subject/dictionaries.ts
Normal file
83
src/api/subject/dictionaries.ts
Normal 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
46
src/api/subject/doWork.ts
Normal 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
|
||||||
71
src/api/subject/happyRead.ts
Normal file
71
src/api/subject/happyRead.ts
Normal 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
|
||||||
48
src/api/subject/homework/index.ts
Normal file
48
src/api/subject/homework/index.ts
Normal 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 }
|
||||||
269
src/api/subject/homework/type.ts
Normal file
269
src/api/subject/homework/type.ts
Normal 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[]
|
||||||
|
}
|
||||||
67
src/api/subject/languageSense.ts
Normal file
67
src/api/subject/languageSense.ts
Normal 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
|
||||||
132
src/api/subject/learnWord.ts
Normal file
132
src/api/subject/learnWord.ts
Normal 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
|
||||||
21
src/api/subject/questionExplain.ts
Normal file
21
src/api/subject/questionExplain.ts
Normal 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
|
||||||
29
src/api/subject/sentenceCollect.ts
Normal file
29
src/api/subject/sentenceCollect.ts
Normal 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
328
src/api/subject/subject.ts
Normal 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除状态(0正常,1已删除)
|
||||||
|
*/
|
||||||
|
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
|
||||||
31
src/api/subject/wordBook.ts
Normal file
31
src/api/subject/wordBook.ts
Normal 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 }
|
||||||
39
src/api/subject/wordDetail.ts
Normal file
39
src/api/subject/wordDetail.ts
Normal 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 }
|
||||||
230
src/api/subject/wordTable.ts
Normal file
230
src/api/subject/wordTable.ts
Normal 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
150
src/api/subject/wordTest.ts
Normal 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 }
|
||||||
136
src/api/subject/wrongQuestion.ts
Normal file
136
src/api/subject/wrongQuestion.ts
Normal 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
|
||||||
21
src/api/system/activity.ts
Normal file
21
src/api/system/activity.ts
Normal 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
|
||||||
26
src/api/system/announcement.ts
Normal file
26
src/api/system/announcement.ts
Normal 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
324
src/api/system/homeWork.ts
Normal 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}¤tTitleId=${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
63
src/api/system/mailbox.ts
Normal 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
87
src/api/system/setting.ts
Normal 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
81
src/api/system/user.ts
Normal 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
75
src/api/task.ts
Normal 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
166
src/api/urge.ts
Normal 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
|
||||||
52
src/components/AbbrImg/index.vue
Normal file
52
src/components/AbbrImg/index.vue
Normal 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>
|
||||||
70
src/components/Accent/index.vue
Normal file
70
src/components/Accent/index.vue
Normal 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>
|
||||||
352
src/components/AiText/index.scss
Normal file
352
src/components/AiText/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/components/AiText/index.vue
Normal file
143
src/components/AiText/index.vue
Normal 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>
|
||||||
105
src/components/AiText/languages.json
Normal file
105
src/components/AiText/languages.json
Normal 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"
|
||||||
|
}
|
||||||
53
src/components/BackBar/index.vue
Normal file
53
src/components/BackBar/index.vue
Normal 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>
|
||||||
26
src/components/BigTitle/index.vue
Normal file
26
src/components/BigTitle/index.vue
Normal 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>
|
||||||
175
src/components/Btn/index.vue
Normal file
175
src/components/Btn/index.vue
Normal 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>
|
||||||
99
src/components/ClozeTest/index.vue
Normal file
99
src/components/ClozeTest/index.vue
Normal 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>
|
||||||
113
src/components/ConfirmDia/index.vue
Normal file
113
src/components/ConfirmDia/index.vue
Normal 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>
|
||||||
186
src/components/DifficultyStart/index.vue
Normal file
186
src/components/DifficultyStart/index.vue
Normal 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>
|
||||||
155
src/components/Empty/index.vue
Normal file
155
src/components/Empty/index.vue
Normal 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>
|
||||||
213
src/components/EnlargePicture/index.vue
Normal file
213
src/components/EnlargePicture/index.vue
Normal 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>
|
||||||
48
src/components/Horn/index.vue
Normal file
48
src/components/Horn/index.vue
Normal 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
66
src/components/HornC.vue
Normal 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>
|
||||||
91
src/components/Icon/index.vue
Normal file
91
src/components/Icon/index.vue
Normal 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>
|
||||||
261
src/components/ImageCropper/index.vue
Normal file
261
src/components/ImageCropper/index.vue
Normal 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>
|
||||||
24
src/components/ImgBg/index.vue
Normal file
24
src/components/ImgBg/index.vue
Normal 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>
|
||||||
127
src/components/ImgView/index.vue
Normal file
127
src/components/ImgView/index.vue
Normal 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>
|
||||||
178
src/components/ItemPopover/index.vue
Normal file
178
src/components/ItemPopover/index.vue
Normal 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>
|
||||||
65
src/components/LazyLoad.vue
Normal file
65
src/components/LazyLoad.vue
Normal 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>
|
||||||
74
src/components/LineFont/index.vue
Normal file
74
src/components/LineFont/index.vue
Normal 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>
|
||||||
85
src/components/PostLoad/index.vue
Normal file
85
src/components/PostLoad/index.vue
Normal 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>
|
||||||
98
src/components/ProgressBar/index.vue
Normal file
98
src/components/ProgressBar/index.vue
Normal 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>
|
||||||
133
src/components/RewardDia/index.vue
Normal file
133
src/components/RewardDia/index.vue
Normal 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>
|
||||||
243
src/components/Select/index.vue
Normal file
243
src/components/Select/index.vue
Normal 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>
|
||||||
81
src/components/SelectTab/index.vue
Normal file
81
src/components/SelectTab/index.vue
Normal 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>
|
||||||
108
src/components/ShowLottie/ShowLottie.vue
Normal file
108
src/components/ShowLottie/ShowLottie.vue
Normal 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>
|
||||||
15
src/components/ShowLottie/type.ts
Normal file
15
src/components/ShowLottie/type.ts
Normal 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 // 设置播放方向(direction:1 表示正向,-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
|
||||||
|
}
|
||||||
161
src/components/Sidebar/index.vue
Normal file
161
src/components/Sidebar/index.vue
Normal 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>
|
||||||
211
src/components/SignaturePad/index.vue
Normal file
211
src/components/SignaturePad/index.vue
Normal 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>
|
||||||
95
src/components/SimpleTable/index.vue
Normal file
95
src/components/SimpleTable/index.vue
Normal 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>
|
||||||
22
src/components/Table/column/index.vue
Normal file
22
src/components/Table/column/index.vue
Normal 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>
|
||||||
229
src/components/Table/index.vue
Normal file
229
src/components/Table/index.vue
Normal 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>
|
||||||
21
src/components/Table/render/index.vue
Normal file
21
src/components/Table/render/index.vue
Normal 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>
|
||||||
182
src/components/UrmLoad/index.vue
Normal file
182
src/components/UrmLoad/index.vue
Normal 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>
|
||||||
119
src/components/UrmLoad/urmUrl.json
Normal file
119
src/components/UrmLoad/urmUrl.json
Normal 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
Loading…
x
Reference in New Issue
Block a user