add
This commit is contained in:
commit
c1aa714e11
9
.env
Normal file
9
.env
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#VITE_HOST = http://hjcy.niuniuhotpot.fr/prod-api
|
||||||
|
VITE_BASE_URL = https://ai.xuexiaole.com
|
||||||
|
|
||||||
|
# VITE_WS_URL = ws://8.138.151.141:8888/
|
||||||
|
VITE_WS_URL = ws://ec2-13-36-229-106.eu-west-3.compute.amazonaws.com:8888/
|
||||||
|
|
||||||
|
# VITE_HOST = http://192.168.2.118:8888
|
||||||
|
|
||||||
|
#VITE_WS_URL = ws://192.168.2.28:8888/
|
||||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 周艳凯
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# uni-app Vue3 Vite4 pinia2 TypeScript 基础框架
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
- **uni-app Vue3 Vite4 pinia2 TypeScript Tailwindcss 基础框架**
|
||||||
|
- cli 创建的 Vue3/Vite 项目 与 使用 HBuilderX 导入插件 的包有差异,最好使用vscode开发,请直接访问 [开源地址](https://gitee.com/zhou-yankai/uniapp-vite-vue3-pinia-ts)
|
||||||
|
|
||||||
|
### 说明
|
||||||
|
|
||||||
|
- 框架完全基于 Vue3 `<script setup>` 写法,不支持 Vue2;
|
||||||
|
- 可用于学习与交流;
|
||||||
|
- 目前测试 H5、微信小程序,APP(Android)通过;
|
||||||
|
- 其他平台暂未测试,后续会增加;
|
||||||
|
- 如发现问题或建议可在评论区留言, 看到会及时处理;
|
||||||
|
- 如有需求亦可在评论区留言,或在此项目基础上增加;
|
||||||
|
- 代码规范 husky、prettier、eslint、lint-staged、stylelint 的作用和使用
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **最新技术栈**:使用 Vue3/Vite4/pinia ,TypeScript 等前端前沿技术开发;
|
||||||
|
- **vue3组件库**:
|
||||||
|
[Vin UI](https://vingogo.cn/docs/guide/quickstart.html) 组件库,基于Taro版NutUi 4.x版本修改而来,适配了uni-app, 使用 Vue 技术栈开发小程序应用,开箱即用,拥有丰富的业务组件。
|
||||||
|
- **Tailwind CSS**:
|
||||||
|
[Tailwind CSS](https://www.tailwindcss.cn/docs/installation) 是一个功能类优先的 CSS 框架,它集成了诸如 flex, pt-4, text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计。
|
||||||
|
- **Eslint/Prettier/stylelint**: 规范代码样式格式,统一编码;
|
||||||
|
- **husky**: 提交代码钩子
|
||||||
|
- **路由拦截**:
|
||||||
|
[uni-mini-router](https://gitee.com/fant-mini/uni-mini-router) 类似Vue Router的API和功能,在uni-app中进行路由跳转、传参、拦截等常用操作;
|
||||||
|
- **请求拦截**:
|
||||||
|
[luch-request](https://www.quanzhan.co/luch-request/handbook) 是一个基于Promise 开发的uni-app跨平台、项目级别的请求库,它有更小的体积,易用的api,方便简单的自定义能力。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```shell
|
||||||
|
|
||||||
|
├─ src
|
||||||
|
│ ├─api # 接口文件目录
|
||||||
|
│ ├─static # 静态公共文件
|
||||||
|
│ │ ├─ images # 图片
|
||||||
|
│ │ │ ├─.png
|
||||||
|
│ │ │ └─...
|
||||||
|
│ │ │
|
||||||
|
│ │ └─ ...
|
||||||
|
│ │
|
||||||
|
│ ├─components # 组件目录
|
||||||
|
│ ├─enum # 枚举
|
||||||
|
│ ├─pages # 页面
|
||||||
|
│ │ ├─ index
|
||||||
|
│ │ │ └─index.vue
|
||||||
|
│ │ └─...
|
||||||
|
│ │
|
||||||
|
│ ├─style # 样式
|
||||||
|
│ │
|
||||||
|
│ ├─state # 状态管理模式(pinia)
|
||||||
|
│ │ ├─ modules # 数据模块
|
||||||
|
│ │ │ ├─auth.ts
|
||||||
|
│ │ │ └─...
|
||||||
|
│ │ │
|
||||||
|
│ │ └─ index.ts
|
||||||
|
│ │
|
||||||
|
│ └─utils # 工具类
|
||||||
|
│ ├─ cache # 缓存相关目录
|
||||||
|
│ └─ request.ts #api请求拦截
|
||||||
|
│ └─ layout #公共的方法
|
||||||
|
├─ .env
|
||||||
|
├─ .env.development
|
||||||
|
├─ .env.production
|
||||||
|
├─ .eslintignore
|
||||||
|
├─ .eslintrc.js
|
||||||
|
├─ .gitignore
|
||||||
|
├─ .prettierignore
|
||||||
|
├─ .prettierrc.js
|
||||||
|
├─ .commitlint.config.js
|
||||||
|
├─ .stylelint.config
|
||||||
|
├─ index.html
|
||||||
|
├─ package.json
|
||||||
|
├─ README.md
|
||||||
|
├─ tailwind.config.js
|
||||||
|
├─ tsconfig.json
|
||||||
|
└─ vite.config.ts
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装使用
|
||||||
|
|
||||||
|
- 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
- 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 其他端请查看 package.json script
|
||||||
|
npm dev:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
- 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 其他端请查看 package.json script
|
||||||
|
npm build:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
- npm run cz提交命令 先git add. 后在使用这个命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run cz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提交类型
|
||||||
|
|
||||||
|
| 提交类型 | 标题 | 描述 |
|
||||||
|
| ---------- | ------------------ | ------------------------------------------------------------------------------------- |
|
||||||
|
| `feat` | 特征 | 新功能、新特性 |
|
||||||
|
| `fix` | Bug 修复 | bug 修复 |
|
||||||
|
| `docs` | 文档 | 仅文档更改 |
|
||||||
|
| `style` | 风格 | 不影响代码含义的更改(空格、格式、缺少分号等) |
|
||||||
|
| `refactor` | 代码重构 | 重构,在不影响代码内部行为,功能下的代码修改 |
|
||||||
|
| `perf` | 性能改进 | 更改代码,以提高性能 |
|
||||||
|
| `test` | 测试 | 添加缺失的测试或纠正现有的测试 |
|
||||||
|
| `build` | 构建 | 影响构建系统或外部依赖项的更改(示例范围:gulp、broccoli、npm) |
|
||||||
|
| `ci` | 持续集成 | 对我们的 CI 配置文件和脚本的更改(示例范围:Travis、Circle、BrowserStack、SauceLabs) |
|
||||||
|
| `chore` | 其他文件修改 | 不修改 src 或测试文件的其他更改 |
|
||||||
|
| `revert` | 还原 | 恢复之前的提交 |
|
||||||
|
| `release` | 发布新版本 | \- |
|
||||||
|
| `workflow` | 工作流相关文件修改 | \- |
|
||||||
|
|
||||||
|

|
||||||
47
commitlint.config.js
Normal file
47
commitlint.config.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// type 类别:用于表明本次提交做了那种类型的改动。
|
||||||
|
// - feat:新增功能
|
||||||
|
// - fix:缺陷修复
|
||||||
|
// - perf:性能优化
|
||||||
|
// - refactor:重构代码(既没有新增功能,也没有修复 bug)
|
||||||
|
// - style:不影响程序逻辑的代码修改(代码风格样式等,没有改变代码逻辑)
|
||||||
|
// - docs:文档更新
|
||||||
|
// - build:项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
|
||||||
|
// - revert:回滚某个更早之前的提交
|
||||||
|
// - chore:不属于以上类型的其他类型
|
||||||
|
|
||||||
|
// optional scope:一个可选的修改范围。用于标识此次提交主要涉及到代码中哪个模块。
|
||||||
|
|
||||||
|
// description:一句话描述此次提交的主要内容,做到言简意赅。
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ignores: [(commit) => commit.includes('init')],
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
rules: {
|
||||||
|
'body-leading-blank': [2, 'always'],
|
||||||
|
'footer-leading-blank': [1, 'always'],
|
||||||
|
'header-max-length': [2, 'always', 108],
|
||||||
|
'subject-empty': [2, 'never'],
|
||||||
|
'type-empty': [2, 'never'],
|
||||||
|
'type-enum': [
|
||||||
|
2,
|
||||||
|
'always',
|
||||||
|
[
|
||||||
|
'feat',
|
||||||
|
'fix',
|
||||||
|
'perf',
|
||||||
|
'style',
|
||||||
|
'docs',
|
||||||
|
'test',
|
||||||
|
'refactor',
|
||||||
|
'build',
|
||||||
|
'ci',
|
||||||
|
'chore',
|
||||||
|
'revert',
|
||||||
|
'wip',
|
||||||
|
'workflow',
|
||||||
|
'types',
|
||||||
|
'release',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
20
index.html
Normal file
20
index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16638
package-lock.json
generated
Normal file
16638
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
111
package.json
Normal file
111
package.json
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"name": "uni-preset-vue",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev:app": "uni -p app",
|
||||||
|
"dev:app-android": "uni -p app-android",
|
||||||
|
"dev:app-ios": "uni -p app-ios",
|
||||||
|
"dev:custom": "uni -p",
|
||||||
|
"dev:h5": "uni --minify",
|
||||||
|
"dev:h5:ssr": "uni --ssr",
|
||||||
|
"dev:mp-alipay": "uni -p mp-alipay",
|
||||||
|
"dev:mp-baidu": "uni -p mp-baidu",
|
||||||
|
"dev:mp-jd": "uni -p mp-jd",
|
||||||
|
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||||
|
"dev:mp-lark": "uni -p mp-lark",
|
||||||
|
"dev:mp-qq": "uni -p mp-qq",
|
||||||
|
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||||
|
"dev:mp-weixin": "uni -p mp-weixin --minify",
|
||||||
|
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||||
|
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||||
|
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||||
|
"build:app": "uni build -p app",
|
||||||
|
"build:app-android": "uni build -p app-android",
|
||||||
|
"build:app-ios": "uni build -p app-ios",
|
||||||
|
"build:custom": "uni build -p",
|
||||||
|
"build:h5": "uni build --minify",
|
||||||
|
"build:h5:ssr": "uni build --ssr",
|
||||||
|
"build:mp-alipay": "uni build -p mp-alipay",
|
||||||
|
"build:mp-baidu": "uni build -p mp-baidu",
|
||||||
|
"build:mp-jd": "uni build -p mp-jd",
|
||||||
|
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||||
|
"build:mp-lark": "uni build -p mp-lark",
|
||||||
|
"build:mp-qq": "uni build -p mp-qq",
|
||||||
|
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||||
|
"build:mp-weixin": "uni build -p mp-weixin --minify",
|
||||||
|
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||||
|
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||||
|
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||||
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
|
||||||
|
"cz": "cz"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-app": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-alipay": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-baidu": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-jd": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-kuaishou": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-lark": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-qq": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-toutiao": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-quickapp-webview": "3.0.0-3080720230703001",
|
||||||
|
"@vingogo/uni-ui": "^0.7.1",
|
||||||
|
"luch-request": "^3.1.0",
|
||||||
|
"pinia": "2.0.36",
|
||||||
|
"terser": "^5.16.1",
|
||||||
|
"uni-mini-router": "^0.1.3",
|
||||||
|
"uni-read-pages-vite": "^0.0.6",
|
||||||
|
"uniapp-nutui": "^0.1.8",
|
||||||
|
"vue": "^3.2.45",
|
||||||
|
"vue-i18n": "^9.1.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^17.6.7",
|
||||||
|
"@commitlint/config-conventional": "^17.6.7",
|
||||||
|
"@dcloudio/types": "^3.3.2",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-3080720230703001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-3080720230703001",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
|
"@typescript-eslint/parser": "^5.59.7",
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"commitizen": "^4.3.0",
|
||||||
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"lint-staged": "^13.2.3",
|
||||||
|
"postcss": "^8.4.27",
|
||||||
|
"postcss-rem-to-responsive-pixel": "^5.1.3",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"stylelint": "^14.13.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.5",
|
||||||
|
"stylelint-config-recommended-scss": "^7.0.0",
|
||||||
|
"stylelint-config-recommended-vue": "^1.5.0",
|
||||||
|
"stylelint-config-standard": "^29.0.0",
|
||||||
|
"stylelint-config-standard-scss": "^4.0.0",
|
||||||
|
"stylelint-order": "^6.0.3",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "4.0.4",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vue-tsc": "^1.0.24",
|
||||||
|
"weapp-tailwindcss": "^2.6.2"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "./node_modules/cz-conventional-changelog"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11671
pnpm-lock.yaml
generated
Normal file
11671
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
postcss.config.js
Normal file
14
postcss.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
'postcss-rem-to-responsive-pixel': {
|
||||||
|
// 32 意味着 1rem = 32rpx
|
||||||
|
rootValue: 32,
|
||||||
|
// 默认所有属性都转化
|
||||||
|
propList: ['*'],
|
||||||
|
// 转化的单位,可以变成 px / rpx
|
||||||
|
transformUnit: 'rpx',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/App.vue
Normal file
19
src/App.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
|
||||||
|
|
||||||
|
onLaunch(() => {
|
||||||
|
console.log('App Launch');
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
console.log('App Show');
|
||||||
|
});
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
console.log('App Hide');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import './style/global.scss';
|
||||||
|
</style>
|
||||||
91
src/api/homework.ts
Normal file
91
src/api/homework.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
|
export interface PaperRecord {
|
||||||
|
id: string;
|
||||||
|
paperId: string;
|
||||||
|
paperName: string;
|
||||||
|
subjectId: string;
|
||||||
|
subjectName: string;
|
||||||
|
publisherName: string;
|
||||||
|
createTime: string;
|
||||||
|
endTime: string;
|
||||||
|
remark: string;
|
||||||
|
costTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取未完成作业列表 */
|
||||||
|
export const getUnfinishedHomework = () =>
|
||||||
|
request<PaperRecord[]>({
|
||||||
|
url: '/school/paper/userRecord/listExamRecord',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 开始答题 */
|
||||||
|
export const startExam = (recordId: string) =>
|
||||||
|
request<any>({
|
||||||
|
url: '/school/paper/exam',
|
||||||
|
method: 'POST',
|
||||||
|
data: { recordId },
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 保存单道题目答案 */
|
||||||
|
export const saveRecordAnswer = (data: {
|
||||||
|
answer: string;
|
||||||
|
answerPic: string;
|
||||||
|
costTime: number;
|
||||||
|
itemId: string;
|
||||||
|
recordId: string;
|
||||||
|
}) =>
|
||||||
|
request({
|
||||||
|
url: '/school/paper/saveRecordAnswer',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 提交作业 */
|
||||||
|
export const submitExam = (data: {
|
||||||
|
answerDtoList: Array<{ itemId: string; answer: string; answerPic: string }>;
|
||||||
|
costTime: number;
|
||||||
|
recordId: string;
|
||||||
|
}) =>
|
||||||
|
request({
|
||||||
|
url: '/school/paper/submit',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 已完成作业列表 */
|
||||||
|
export const getListCompleteRecord = (params?: {
|
||||||
|
gradingStatus?: string;
|
||||||
|
subjectId?: string;
|
||||||
|
}) =>
|
||||||
|
request<any[]>({
|
||||||
|
url: '/school/paper/userRecord/listCompleteRecord',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 作业详情(含题目) */
|
||||||
|
export const getPaperRecordDetail = (data: { recordId: string; reportFlag: number }) =>
|
||||||
|
request<any>({
|
||||||
|
url: '/school/paper/getPaperRecordDetail',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 作业整体分析 */
|
||||||
|
export const getJobAnalyse = (params: { recordId: string }) =>
|
||||||
|
request<any>({
|
||||||
|
url: '/sc/job/getJobAnalyse',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 知识点分析 */
|
||||||
|
export const getKnowledgeAnalyse = (params: { recordId: string }) =>
|
||||||
|
request<any[]>({
|
||||||
|
url: '/sc/job/statisticalKnowledge',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
45
src/api/login.ts
Normal file
45
src/api/login.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
|
export interface AccountLoginParams {
|
||||||
|
account: string;
|
||||||
|
password: string;
|
||||||
|
/** 老师登录传 'MANAGE',学生登录默认 'PC' */
|
||||||
|
clientType?: string;
|
||||||
|
companyId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账号密码登录 */
|
||||||
|
export const psdLogin = (data: AccountLoginParams) =>
|
||||||
|
request<string>({
|
||||||
|
url: '/login',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: '' },
|
||||||
|
data: {
|
||||||
|
clientType: 'PC',
|
||||||
|
simSerialNumber: 'web',
|
||||||
|
model: 'web',
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 退出登录 */
|
||||||
|
export const logout = () =>
|
||||||
|
request({
|
||||||
|
url: '/logout',
|
||||||
|
method: 'GET',
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取学生用户首页信息 */
|
||||||
|
export const getUserInfo = () =>
|
||||||
|
request<any>({
|
||||||
|
url: '/personal/getHomePage',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 老师登录用户 */
|
||||||
|
export const getLoginUser = () =>
|
||||||
|
request<any>({
|
||||||
|
url: '/getLoginUser',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
181
src/api/teacher.ts
Normal file
181
src/api/teacher.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
|
export interface TeacherUserInfo {
|
||||||
|
id?: string;
|
||||||
|
account?: string;
|
||||||
|
nickName?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
schoolId?: string;
|
||||||
|
schoolName?: string;
|
||||||
|
subjectId?: string | number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeacherClassItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
classId?: string;
|
||||||
|
className?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeacherSubjectItem {
|
||||||
|
subjectId?: string | number;
|
||||||
|
id?: string | number;
|
||||||
|
value?: string | number;
|
||||||
|
subjectName?: string;
|
||||||
|
name?: string;
|
||||||
|
label?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TopErrorTitle {
|
||||||
|
titleId?: number | string;
|
||||||
|
titleContent?: string;
|
||||||
|
subjectId?: number;
|
||||||
|
subjectName?: string;
|
||||||
|
errorCount?: number;
|
||||||
|
errorRate?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiCorrectionOverview {
|
||||||
|
classId: number | string;
|
||||||
|
className: string;
|
||||||
|
aiPaperCount: number;
|
||||||
|
aiTitleCount: number;
|
||||||
|
teacherSaveTimeMinutes: number;
|
||||||
|
excellentCount: number;
|
||||||
|
excellentRate: number;
|
||||||
|
goodCount: number;
|
||||||
|
goodRate: number;
|
||||||
|
passCount: number;
|
||||||
|
passRate: number;
|
||||||
|
unsubmittedCount: number;
|
||||||
|
unsubmittedRate: number;
|
||||||
|
topErrorTitles?: TopErrorTitle[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassHomeworkSubjectStat {
|
||||||
|
subjectId: number;
|
||||||
|
subjectName: string;
|
||||||
|
homeworkCount: number;
|
||||||
|
completedCount: number;
|
||||||
|
uncompletedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassHomeworkStudentItem {
|
||||||
|
userId: number | string;
|
||||||
|
userName: string;
|
||||||
|
totalCount: number;
|
||||||
|
completedCount: number;
|
||||||
|
uncompletedCount: number;
|
||||||
|
subjectStats: ClassHomeworkSubjectStat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassHomeworkReportData {
|
||||||
|
classId: number | string;
|
||||||
|
className: string;
|
||||||
|
subjects: { subjectId: number; subjectName: string }[];
|
||||||
|
studentList: ClassHomeworkStudentItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeakKnowledgeItem {
|
||||||
|
knowledgeId: number | string;
|
||||||
|
knowledgeName: string;
|
||||||
|
subjectId: number | string;
|
||||||
|
subjectName: string;
|
||||||
|
errorCount: number;
|
||||||
|
titleCount: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeakKnowledgeTop10Data {
|
||||||
|
classId: number | string;
|
||||||
|
className: string;
|
||||||
|
topWeakKnowledgeList: WeakKnowledgeItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formEncode = (value: string) => encodeURIComponent(value).replace(/%20/g, '+');
|
||||||
|
|
||||||
|
const appendDateRange = (query: string[], startTime?: string, endTime?: string) => {
|
||||||
|
if (startTime?.trim()) {
|
||||||
|
const value = startTime.trim();
|
||||||
|
query.push('startTime=' + formEncode(value.includes(' ') ? value : `${value} 00:00:00`));
|
||||||
|
}
|
||||||
|
if (endTime?.trim()) {
|
||||||
|
const value = endTime.trim();
|
||||||
|
query.push('endTime=' + formEncode(value.includes(' ') ? value : `${value} 23:59:59`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildReportQuery = (params: {
|
||||||
|
classId: string | number;
|
||||||
|
subjectIds?: number[];
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
schoolId?: string;
|
||||||
|
}) => {
|
||||||
|
const query = ['classId=' + formEncode(String(params.classId))];
|
||||||
|
if (params.schoolId) query.push('schoolId=' + formEncode(String(params.schoolId)));
|
||||||
|
params.subjectIds?.forEach((id) => query.push('subjectIds=' + formEncode(String(id))));
|
||||||
|
appendDateRange(query, params.startTime, params.endTime);
|
||||||
|
return query.join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLoginUser = () =>
|
||||||
|
request<TeacherUserInfo>({
|
||||||
|
url: '/getLoginUser',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getClassListByTeacher = (params?: { gradeId?: string; gradeName?: string }) =>
|
||||||
|
request<TeacherClassItem[]>({
|
||||||
|
url: '/org/class/listByTeacher',
|
||||||
|
method: 'GET',
|
||||||
|
params: params || {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSubjectListByClassId = (classId: string | number) =>
|
||||||
|
request<TeacherSubjectItem[]>({
|
||||||
|
url: '/org/class/listSubjectInfoByClassId',
|
||||||
|
method: 'GET',
|
||||||
|
params: { classId },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getAiCorrectionOverview = (params: {
|
||||||
|
classId: string | number;
|
||||||
|
subjectIds?: number[];
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
schoolId?: string;
|
||||||
|
}) =>
|
||||||
|
request<AiCorrectionOverview>({
|
||||||
|
url: '/school/report/aiCorrectionEfficiency/overview?' + buildReportQuery(params),
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getClassHomeworkReport = (params: {
|
||||||
|
classId: string | number;
|
||||||
|
subjectIds?: number[];
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}) =>
|
||||||
|
request<ClassHomeworkReportData>({
|
||||||
|
url: '/school/report/classHomework/list?' + buildReportQuery(params),
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getWeakKnowledgeTop10 = (params: {
|
||||||
|
classId: string | number;
|
||||||
|
subjectIds?: number[];
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
schoolId?: string;
|
||||||
|
}) =>
|
||||||
|
request<WeakKnowledgeTop10Data>({
|
||||||
|
url: '/school/report/weakKnowledge/top10?' + buildReportQuery(params),
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
34
src/api/upload.ts
Normal file
34
src/api/upload.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { getCache } from '@/utils/cache';
|
||||||
|
import { HOST, TENANT_ID } from '@/utils/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片到后端,返回 { url, ... }
|
||||||
|
*/
|
||||||
|
export const uploadFile = (filePath: string): Promise<{ url: string; [key: string]: any }> => {
|
||||||
|
const token = getCache('token');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: `${HOST}/api/main/sysFileInfo/tenUploadAll`,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
Authorization: token ? `Bearer ${token}` : '',
|
||||||
|
'x-tenant-id': TENANT_ID,
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
||||||
|
if (data.code === 200) {
|
||||||
|
resolve(data.data);
|
||||||
|
} else {
|
||||||
|
reject(new Error(data.message || '上传失败'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error('上传返回格式异常'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => reject(err),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
105
src/components/Empty/index.vue
Normal file
105
src/components/Empty/index.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<view class="empty">
|
||||||
|
<view class="empty-illu" :class="`empty-illu-${type}`">
|
||||||
|
<!-- Loading -->
|
||||||
|
<view v-if="type === 'loading'" class="empty-loader">
|
||||||
|
<view class="dot dot-1"></view>
|
||||||
|
<view class="dot dot-2"></view>
|
||||||
|
<view class="dot dot-3"></view>
|
||||||
|
</view>
|
||||||
|
<!-- Network -->
|
||||||
|
<svg v-else-if="type === 'network'" width="80" height="80" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 13a10 10 0 0 1 14 0M8.5 16.5a5 5 0 0 1 7 0M12 20h0M2 8.82a15 15 0 0 1 20 0"
|
||||||
|
stroke="#A78BFA" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<line x1="3" y1="3" x2="21" y2="21" stroke="#EF4444" stroke-width="1.6" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
<!-- Task / 默认空盒子 -->
|
||||||
|
<svg v-else-if="type === 'task'" width="80" height="80" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||||
|
stroke="#A78BFA" stroke-width="1.6" stroke-linejoin="round" />
|
||||||
|
<path d="M14 2v6h6" stroke="#A78BFA" stroke-width="1.6" stroke-linejoin="round" />
|
||||||
|
<path d="M9 13h6M9 17h4" stroke="#A78BFA" stroke-width="1.6" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
<!-- Learn -->
|
||||||
|
<svg v-else width="80" height="80" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"
|
||||||
|
stroke="#A78BFA" stroke-width="1.6" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<view class="empty-text">{{ content }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
type?: 'loading' | 'network' | 'task' | 'learn';
|
||||||
|
content?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
type: 'task',
|
||||||
|
content: '暂无数据',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.empty {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-illu {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 60rpx;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #eef2ff 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow:
|
||||||
|
0 16rpx 40rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -6rpx 0 rgba(199, 210, 254, 0.6),
|
||||||
|
inset 0 4rpx 0 rgba(255, 255, 255, 0.9);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* loading dots */
|
||||||
|
.empty-loader {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #818cf8;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out both;
|
||||||
|
|
||||||
|
&.dot-1 { animation-delay: -0.32s; background: #4f46e5; }
|
||||||
|
&.dot-2 { animation-delay: -0.16s; background: #818cf8; }
|
||||||
|
&.dot-3 { background: #c7d2fe; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.6; }
|
||||||
|
40% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
600
src/components/ImageCropper/index.vue
Normal file
600
src/components/ImageCropper/index.vue
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
<template>
|
||||||
|
<view :class="['cropper-wrapper', { show: visible }]">
|
||||||
|
<!-- 显示层:用 image 展示图片,裁剪框覆盖其上,避免真机上 canvas 把裁剪框盖住 -->
|
||||||
|
<view
|
||||||
|
class="display-layer"
|
||||||
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||||||
|
@touchstart="onTouchStart"
|
||||||
|
@touchmove="onTouchMove"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="imageSrc"
|
||||||
|
:src="imageSrc"
|
||||||
|
class="display-image"
|
||||||
|
mode="aspectFit"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 工作 canvas:放到屏幕外,只用于真正的绘制和导出 -->
|
||||||
|
<canvas
|
||||||
|
id="imageCropperCanvas"
|
||||||
|
canvas-id="imageCropperCanvas"
|
||||||
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||||||
|
class="work-canvas"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
|
<!-- 用于导出裁剪结果的离屏 canvas(必须有真实节点) -->
|
||||||
|
<canvas
|
||||||
|
id="tempCropCanvas"
|
||||||
|
canvas-id="tempCropCanvas"
|
||||||
|
:style="{ width: exportSize.width + 'px', height: exportSize.height + 'px' }"
|
||||||
|
class="temp-canvas"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="crop-frame"
|
||||||
|
:style="{
|
||||||
|
left: cropFrame.x + 'px',
|
||||||
|
top: cropFrame.y + 'px',
|
||||||
|
width: cropFrame.width + 'px',
|
||||||
|
height: cropFrame.height + 'px',
|
||||||
|
}"
|
||||||
|
@touchstart.stop.prevent="onFrameTouchStart"
|
||||||
|
@touchmove.stop.prevent="onFrameTouchMove"
|
||||||
|
@touchend.stop="onFrameTouchEnd"
|
||||||
|
>
|
||||||
|
<view class="resize-handle top-left" data-direction="tl"></view>
|
||||||
|
<view class="resize-handle top-right" data-direction="tr"></view>
|
||||||
|
<view class="resize-handle bottom-left" data-direction="bl"></view>
|
||||||
|
<view class="resize-handle bottom-right" data-direction="br"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="crop-controls" :style="{ bottom: 8 + safeAreaBottom + 'px' }">
|
||||||
|
<button @tap="cancelCrop">取消</button>
|
||||||
|
<button :disabled="isProcessing" @tap="confirmCrop">
|
||||||
|
{{ isProcessing ? '处理中...' : '确定' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted, nextTick, getCurrentInstance } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
imageSrc: string;
|
||||||
|
outputWidth?: number; // 裁剪输出宽度
|
||||||
|
outputHeight?: number; // 裁剪输出高度
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
outputWidth: 750, // 默认输出宽度
|
||||||
|
outputHeight: 750, // 默认输出高度
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
(e: 'confirm', filePath: string): void;
|
||||||
|
(e: 'cancel'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const ctx = ref<UniApp.CanvasContext | null>(null);
|
||||||
|
const imgInfo = ref<UniApp.GetImageInfoSuccessData | null>(null);
|
||||||
|
|
||||||
|
const canvasWidth = ref(0);
|
||||||
|
const canvasHeight = ref(0);
|
||||||
|
const imageDisplay = ref({ x: 0, y: 0, width: 0, height: 0 }); // 图片在canvas上的显示位置和尺寸
|
||||||
|
const controlsReservePx = 64; // 给底部按钮预留空间,避免真机遮挡图片
|
||||||
|
|
||||||
|
const cropFrame = ref({ x: 0, y: 0, width: 0, height: 0 }); // 裁剪框位置和尺寸
|
||||||
|
const isDraggingImage = ref(false);
|
||||||
|
const isResizingFrame = ref(false);
|
||||||
|
const isMovingFrame = ref(false);
|
||||||
|
const startX = ref(0);
|
||||||
|
const startY = ref(0);
|
||||||
|
const lastFrame = ref({ x: 0, y: 0, width: 0, height: 0 }); // 用于记录上一次裁剪框的位置和大小
|
||||||
|
const resizeDirection = ref('');
|
||||||
|
const isProcessing = ref(false); // 裁剪处理中状态
|
||||||
|
|
||||||
|
const safeAreaTop = ref(0);
|
||||||
|
const safeAreaBottom = ref(0);
|
||||||
|
const componentProxy = getCurrentInstance()?.proxy as any;
|
||||||
|
const exportSize = ref({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const rpxToPx = (rpx: number) => {
|
||||||
|
// 按小程序 rpx 规则:750rpx = windowWidth(px)
|
||||||
|
if (!canvasWidth.value) return Math.max(1, Math.round(rpx));
|
||||||
|
return Math.max(1, Math.round((canvasWidth.value / 750) * rpx));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTouchPoint = (e: any) => {
|
||||||
|
const t = e?.touches?.[0] || e?.changedTouches?.[0] || {};
|
||||||
|
const x = t.x ?? t.clientX ?? t.pageX ?? 0;
|
||||||
|
const y = t.y ?? t.clientY ?? t.pageY ?? 0;
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryInitAndLoad = () => {
|
||||||
|
if (!props.visible) return;
|
||||||
|
if (!props.imageSrc) return;
|
||||||
|
if (!ctx.value) return;
|
||||||
|
if (!canvasWidth.value || !canvasHeight.value) return;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
exportSize.value = {
|
||||||
|
width: rpxToPx(props.outputWidth),
|
||||||
|
height: rpxToPx(props.outputHeight),
|
||||||
|
};
|
||||||
|
loadImage();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取设备信息,用于计算canvas大小和安全区
|
||||||
|
uni.getSystemInfo({
|
||||||
|
success: (res) => {
|
||||||
|
canvasWidth.value = res.windowWidth;
|
||||||
|
safeAreaTop.value = res.safeArea?.top || 0;
|
||||||
|
safeAreaBottom.value =
|
||||||
|
(res.screenHeight && res.safeArea ? res.screenHeight - res.safeArea.bottom : 0) || 0;
|
||||||
|
// 预留底部按钮区 + 安全区,避免按钮覆盖图片(尤其真机横屏)
|
||||||
|
const bottomReserve = controlsReservePx + safeAreaBottom.value;
|
||||||
|
canvasHeight.value = Math.max(1, res.windowHeight - bottomReserve);
|
||||||
|
// 自定义组件内的 canvas 必须传组件实例,否则会出现“黑屏/不绘制”
|
||||||
|
ctx.value = uni.createCanvasContext('imageCropperCanvas', componentProxy);
|
||||||
|
tryInitAndLoad();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => props.visible, () => props.imageSrc, canvasWidth, canvasHeight, ctx],
|
||||||
|
() => {
|
||||||
|
if (!props.visible) {
|
||||||
|
imgInfo.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tryInitAndLoad();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadImage = () => {
|
||||||
|
uni.showLoading({ title: '加载中...' });
|
||||||
|
uni.getImageInfo({
|
||||||
|
src: props.imageSrc,
|
||||||
|
success: (res) => {
|
||||||
|
imgInfo.value = res;
|
||||||
|
drawImage();
|
||||||
|
uni.hideLoading();
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '图片加载失败', icon: 'none' });
|
||||||
|
console.error('图片加载失败', err);
|
||||||
|
cancelCrop();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawImage = () => {
|
||||||
|
if (!ctx.value || !imgInfo.value) return;
|
||||||
|
|
||||||
|
const { path, width: originalWidth, height: originalHeight } = imgInfo.value;
|
||||||
|
|
||||||
|
// 计算图片在Canvas上的显示尺寸,使其适应Canvas,并居中
|
||||||
|
const canvasRatio = canvasWidth.value / canvasHeight.value;
|
||||||
|
const imageRatio = originalWidth / originalHeight;
|
||||||
|
|
||||||
|
let displayWidth = 0;
|
||||||
|
let displayHeight = 0;
|
||||||
|
|
||||||
|
if (imageRatio > canvasRatio) {
|
||||||
|
displayWidth = canvasWidth.value;
|
||||||
|
displayHeight = canvasWidth.value / imageRatio;
|
||||||
|
} else {
|
||||||
|
displayHeight = canvasHeight.value;
|
||||||
|
displayWidth = canvasHeight.value * imageRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageDisplay.value = {
|
||||||
|
x: (canvasWidth.value - displayWidth) / 2,
|
||||||
|
y: (canvasHeight.value - displayHeight) / 2,
|
||||||
|
width: displayWidth,
|
||||||
|
height: displayHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
|
||||||
|
ctx.value.drawImage(
|
||||||
|
path,
|
||||||
|
imageDisplay.value.x,
|
||||||
|
imageDisplay.value.y,
|
||||||
|
imageDisplay.value.width,
|
||||||
|
imageDisplay.value.height
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化裁剪框为图片显示区域的大小
|
||||||
|
cropFrame.value = {
|
||||||
|
x: imageDisplay.value.x,
|
||||||
|
y: imageDisplay.value.y,
|
||||||
|
width: imageDisplay.value.width,
|
||||||
|
height: imageDisplay.value.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.value.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchStart = (e: any) => {
|
||||||
|
if (!imgInfo.value) return;
|
||||||
|
const { x, y } = getTouchPoint(e);
|
||||||
|
startX.value = x;
|
||||||
|
startY.value = y;
|
||||||
|
|
||||||
|
// 判断是否在拖动图片
|
||||||
|
if (
|
||||||
|
x > imageDisplay.value.x &&
|
||||||
|
x < imageDisplay.value.x + imageDisplay.value.width &&
|
||||||
|
y > imageDisplay.value.y &&
|
||||||
|
y < imageDisplay.value.y + imageDisplay.value.height
|
||||||
|
) {
|
||||||
|
isDraggingImage.value = true;
|
||||||
|
// 记录图片当前位置,用于计算偏移
|
||||||
|
lastFrame.value = { ...imageDisplay.value };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchMove = (e: any) => {
|
||||||
|
if (!isDraggingImage.value || !imgInfo.value || !ctx.value) return;
|
||||||
|
|
||||||
|
const { x, y } = getTouchPoint(e);
|
||||||
|
const deltaX = x - startX.value;
|
||||||
|
const deltaY = y - startY.value;
|
||||||
|
|
||||||
|
let newX = lastFrame.value.x + deltaX;
|
||||||
|
let newY = lastFrame.value.y + deltaY;
|
||||||
|
|
||||||
|
// 边界检查:确保图片不会完全移出canvas
|
||||||
|
if (newX > canvasWidth.value - imageDisplay.value.x) {
|
||||||
|
newX = canvasWidth.value - imageDisplay.value.x;
|
||||||
|
}
|
||||||
|
if (newY > canvasHeight.value - imageDisplay.value.y) {
|
||||||
|
newY = canvasHeight.value - imageDisplay.value.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newX < -imageDisplay.value.width + imageDisplay.value.x) {
|
||||||
|
newX = -imageDisplay.value.width + imageDisplay.value.x;
|
||||||
|
}
|
||||||
|
if (newY < -imageDisplay.value.height + imageDisplay.value.y) {
|
||||||
|
newY = -imageDisplay.value.height + imageDisplay.value.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageDisplay.value.x = newX;
|
||||||
|
imageDisplay.value.y = newY;
|
||||||
|
|
||||||
|
ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
|
||||||
|
ctx.value.drawImage(
|
||||||
|
imgInfo.value.path,
|
||||||
|
imageDisplay.value.x,
|
||||||
|
imageDisplay.value.y,
|
||||||
|
imageDisplay.value.width,
|
||||||
|
imageDisplay.value.height
|
||||||
|
);
|
||||||
|
ctx.value.draw(true); // true表示保留上一次绘制内容
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = () => {
|
||||||
|
isDraggingImage.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
const onFrameTouchMove = (e: any) => {
|
||||||
|
if (!imgInfo.value) return;
|
||||||
|
const { x, y } = getTouchPoint(e);
|
||||||
|
const deltaX = x - startX.value;
|
||||||
|
const deltaY = y - startY.value;
|
||||||
|
|
||||||
|
if (isMovingFrame.value) {
|
||||||
|
// 移动裁剪框
|
||||||
|
let newX = lastFrame.value.x + deltaX;
|
||||||
|
let newY = lastFrame.value.y + deltaY;
|
||||||
|
|
||||||
|
// 边界检查
|
||||||
|
if (newX < 0) newX = 0;
|
||||||
|
if (newY < 0) newY = 0;
|
||||||
|
if (newX + cropFrame.value.width > canvasWidth.value)
|
||||||
|
newX = canvasWidth.value - cropFrame.value.width;
|
||||||
|
if (newY + cropFrame.value.height > canvasHeight.value)
|
||||||
|
newY = canvasHeight.value - cropFrame.value.height;
|
||||||
|
|
||||||
|
cropFrame.value.x = newX;
|
||||||
|
cropFrame.value.y = newY;
|
||||||
|
} else if (isResizingFrame.value) {
|
||||||
|
// 缩放裁剪框
|
||||||
|
let newX = lastFrame.value.x;
|
||||||
|
let newY = lastFrame.value.y;
|
||||||
|
let newWidth = lastFrame.value.width;
|
||||||
|
let newHeight = lastFrame.value.height;
|
||||||
|
|
||||||
|
const minSize = 50; // 最小尺寸
|
||||||
|
|
||||||
|
switch (resizeDirection.value) {
|
||||||
|
case 'tl': // Top-left
|
||||||
|
newX = Math.min(x, lastFrame.value.x + lastFrame.value.width - minSize);
|
||||||
|
newY = Math.min(y, lastFrame.value.y + lastFrame.value.height - minSize);
|
||||||
|
newWidth = lastFrame.value.x + lastFrame.value.width - newX;
|
||||||
|
newHeight = lastFrame.value.y + lastFrame.value.height - newY;
|
||||||
|
break;
|
||||||
|
case 'tr': // Top-right
|
||||||
|
newX = lastFrame.value.x;
|
||||||
|
newY = Math.min(startY.value, lastFrame.value.y + lastFrame.value.height - minSize, y);
|
||||||
|
newWidth = Math.max(minSize, lastFrame.value.width + deltaX);
|
||||||
|
newHeight = lastFrame.value.y + lastFrame.value.height - newY;
|
||||||
|
break;
|
||||||
|
case 'bl': // Bottom-left
|
||||||
|
newX = Math.min(startX.value, lastFrame.value.x + lastFrame.value.width - minSize, x);
|
||||||
|
newY = lastFrame.value.y;
|
||||||
|
newWidth = lastFrame.value.x + lastFrame.value.width - newX;
|
||||||
|
newHeight = Math.max(minSize, lastFrame.value.height + deltaY);
|
||||||
|
break;
|
||||||
|
case 'br': // Bottom-right
|
||||||
|
newX = lastFrame.value.x;
|
||||||
|
newY = lastFrame.value.y;
|
||||||
|
newWidth = Math.max(minSize, lastFrame.value.width + deltaX);
|
||||||
|
newHeight = Math.max(minSize, lastFrame.value.height + deltaY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 裁剪框不能超出canvas边界
|
||||||
|
if (newX < 0) newX = 0;
|
||||||
|
if (newY < 0) newY = 0;
|
||||||
|
if (newX + newWidth > canvasWidth.value) newWidth = canvasWidth.value - newX;
|
||||||
|
if (newY + newHeight > canvasHeight.value) newHeight = canvasHeight.value - newY;
|
||||||
|
|
||||||
|
cropFrame.value = { x: newX, y: newY, width: newWidth, height: newHeight };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFrameTouchEnd = () => {
|
||||||
|
isMovingFrame.value = false;
|
||||||
|
isResizingFrame.value = false;
|
||||||
|
resizeDirection.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 阻止事件冒泡,因为canvas的touchmove事件会在外部被阻止
|
||||||
|
// 这里需要处理裁剪框的触摸事件,不阻止冒泡会导致裁剪框无法拖动和缩放
|
||||||
|
const onFrameTouchStart = (e: any) => {
|
||||||
|
const { x, y } = getTouchPoint(e);
|
||||||
|
startX.value = x;
|
||||||
|
startY.value = y;
|
||||||
|
lastFrame.value = { ...cropFrame.value }; // 记录当前裁剪框状态
|
||||||
|
|
||||||
|
const targetDataset = e.target.dataset;
|
||||||
|
if (targetDataset && targetDataset.direction) {
|
||||||
|
isResizingFrame.value = true;
|
||||||
|
resizeDirection.value = targetDataset.direction;
|
||||||
|
} else {
|
||||||
|
isMovingFrame.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmCrop = () => {
|
||||||
|
if (!ctx.value || !imgInfo.value || isProcessing.value) return;
|
||||||
|
|
||||||
|
isProcessing.value = true;
|
||||||
|
uni.showLoading({ title: '裁剪中...', mask: true });
|
||||||
|
|
||||||
|
const { path: imagePath, width: originalWidth, height: originalHeight } = imgInfo.value;
|
||||||
|
|
||||||
|
// 计算实际图片上的裁剪区域
|
||||||
|
// 图片显示区域与原始图片的比例
|
||||||
|
const scaleX = originalWidth / imageDisplay.value.width;
|
||||||
|
const scaleY = originalHeight / imageDisplay.value.height;
|
||||||
|
|
||||||
|
const sourceX = (cropFrame.value.x - imageDisplay.value.x) * scaleX;
|
||||||
|
const sourceY = (cropFrame.value.y - imageDisplay.value.y) * scaleY;
|
||||||
|
const sourceWidth = cropFrame.value.width * scaleX;
|
||||||
|
const sourceHeight = cropFrame.value.height * scaleY;
|
||||||
|
|
||||||
|
// 创建一个新的canvas用于裁剪,确保尺寸和输出尺寸匹配
|
||||||
|
const tempCanvasId = 'tempCropCanvas';
|
||||||
|
const tempCtx = uni.createCanvasContext(tempCanvasId, componentProxy);
|
||||||
|
|
||||||
|
// 设置裁剪输出尺寸
|
||||||
|
const outputWidthPx = rpxToPx(props.outputWidth);
|
||||||
|
const outputHeightPx = rpxToPx(props.outputHeight);
|
||||||
|
exportSize.value = { width: outputWidthPx, height: outputHeightPx };
|
||||||
|
|
||||||
|
// 绘制图片到临时canvas,进行裁剪和缩放
|
||||||
|
tempCtx.clearRect(0, 0, outputWidthPx, outputHeightPx);
|
||||||
|
tempCtx.drawImage(
|
||||||
|
imagePath,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
outputWidthPx,
|
||||||
|
outputHeightPx
|
||||||
|
);
|
||||||
|
|
||||||
|
// 确保绘制完成
|
||||||
|
tempCtx.draw(false, () => {
|
||||||
|
let done = false;
|
||||||
|
const finish = (ok: boolean, payload?: any) => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
isProcessing.value = false;
|
||||||
|
uni.hideLoading();
|
||||||
|
if (!ok) return;
|
||||||
|
emit('confirm', payload);
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 兜底:防止部分机型 canvasToTempFilePath 不回调导致“一直处理中”
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
uni.showToast({ title: '裁剪超时,请重试', icon: 'none' });
|
||||||
|
finish(false);
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
// 延迟一段时间,确保canvas内容已经渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvasId: tempCanvasId,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: outputWidthPx,
|
||||||
|
height: outputHeightPx,
|
||||||
|
destWidth: outputWidthPx,
|
||||||
|
destHeight: outputHeightPx,
|
||||||
|
success: (res) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
finish(true, res.tempFilePath);
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
uni.showToast({ title: '裁剪失败', icon: 'none' });
|
||||||
|
console.error('canvasToTempFilePath failed', err);
|
||||||
|
finish(false);
|
||||||
|
},
|
||||||
|
}, componentProxy);
|
||||||
|
}, 100); // 短暂延迟
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelCrop = () => {
|
||||||
|
emit('cancel');
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cropper-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: #000;
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden; // 隐藏超出部分
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: block;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-canvas {
|
||||||
|
position: fixed;
|
||||||
|
left: -99999px;
|
||||||
|
top: -99999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-canvas {
|
||||||
|
position: fixed;
|
||||||
|
left: -99999px;
|
||||||
|
top: -99999px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crop-frame {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid #00c0ff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none; // 禁用浏览器默认触摸行为
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: #00c0ff;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
opacity: 0.9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-left {
|
||||||
|
top: -16px;
|
||||||
|
left: -16px;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
.top-right {
|
||||||
|
top: -16px;
|
||||||
|
right: -16px;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
.bottom-left {
|
||||||
|
bottom: -16px;
|
||||||
|
left: -16px;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
}
|
||||||
|
.bottom-right {
|
||||||
|
bottom: -16px;
|
||||||
|
right: -16px;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crop-controls {
|
||||||
|
position: absolute;
|
||||||
|
// left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crop-controls button {
|
||||||
|
|
||||||
|
min-width: 30px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #007aff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crop-controls button:first-child {
|
||||||
|
background: #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crop-controls button[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
89
src/components/Loading/index.vue
Normal file
89
src/components/Loading/index.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<view class="loading">
|
||||||
|
<view class="loading-clay">
|
||||||
|
<view class="loading-orbit">
|
||||||
|
<view class="loading-dot loading-dot-1"></view>
|
||||||
|
<view class="loading-dot loading-dot-2"></view>
|
||||||
|
<view class="loading-dot loading-dot-3"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="loading-text">{{ text }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{ text?: string }>(),
|
||||||
|
{ text: '加载中…' },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-clay {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #eef2ff 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 32rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.6),
|
||||||
|
inset 0 4rpx 0 rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-orbit {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
position: relative;
|
||||||
|
animation: orbit-spin 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dot {
|
||||||
|
position: absolute;
|
||||||
|
width: 18rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.loading-dot-1 {
|
||||||
|
background: #4f46e5;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -9rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading-dot-2 {
|
||||||
|
background: #818cf8;
|
||||||
|
bottom: 0;
|
||||||
|
left: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading-dot-3 {
|
||||||
|
background: #22c55e;
|
||||||
|
bottom: 0;
|
||||||
|
right: 14rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes orbit-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
src/components/NavBar/index.vue
Normal file
162
src/components/NavBar/index.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<view class="navbar" :style="navbarStyle">
|
||||||
|
<view class="navbar-status" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||||
|
<view class="navbar-bar" :style="{ height: navHeight + 'px' }">
|
||||||
|
<view v-if="showBack" class="navbar-back" @click="onBack">
|
||||||
|
<view class="navbar-back-arrow"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="navbar-title">
|
||||||
|
<text>{{ title }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<slot name="right">
|
||||||
|
<view class="navbar-right" :style="{ width: menuButtonWidthPx + 'px' }"></view>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 通用导航栏(竖屏专用)
|
||||||
|
* - 自动适配状态栏与小程序胶囊按钮高度
|
||||||
|
* - showBack 控制返回按钮显示
|
||||||
|
*/
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
showBack?: boolean;
|
||||||
|
bg?: string;
|
||||||
|
color?: string;
|
||||||
|
transparent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '',
|
||||||
|
showBack: true,
|
||||||
|
bg: '',
|
||||||
|
color: '',
|
||||||
|
transparent: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'back'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusBarHeight = ref(20);
|
||||||
|
const navHeight = ref(44);
|
||||||
|
const menuButtonWidthPx = ref(0);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
const sys = uni.getSystemInfoSync();
|
||||||
|
statusBarHeight.value = sys.statusBarHeight || 20;
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const menu = uni.getMenuButtonBoundingClientRect();
|
||||||
|
if (menu) {
|
||||||
|
navHeight.value = (menu.top - statusBarHeight.value) * 2 + menu.height;
|
||||||
|
menuButtonWidthPx.value = sys.windowWidth - menu.left + 8;
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('NavBar getSystemInfo error', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const navbarStyle = computed(() => {
|
||||||
|
const bg = props.transparent
|
||||||
|
? 'transparent'
|
||||||
|
: props.bg || 'rgba(255,255,255,0.92)';
|
||||||
|
return {
|
||||||
|
background: bg,
|
||||||
|
color: props.color || '#312E81',
|
||||||
|
backdropFilter: props.transparent ? 'none' : 'blur(12px)',
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onBack = () => {
|
||||||
|
emit('back');
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack();
|
||||||
|
} else {
|
||||||
|
uni.reLaunch({ url: '/pages/index/index' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.navbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-bar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-back {
|
||||||
|
position: absolute;
|
||||||
|
left: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 16rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(-50%) scale(0.92);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(99, 102, 241, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-back-arrow {
|
||||||
|
width: 18rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
border-left: 5rpx solid #4f46e5;
|
||||||
|
border-bottom: 5rpx solid #4f46e5;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
margin-left: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
max-width: 60vw;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-right {
|
||||||
|
height: 1rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/components/ProgressBar/index.vue
Normal file
66
src/components/ProgressBar/index.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<view class="progress" :style="{ width: width + 'rpx' }">
|
||||||
|
<view class="progress-track" :style="{ background: bg, borderColor: border }">
|
||||||
|
<view
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{
|
||||||
|
width: percent + '%',
|
||||||
|
background: activeBg,
|
||||||
|
}"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
rate: number; // 0~1
|
||||||
|
width?: number | string;
|
||||||
|
bg?: string;
|
||||||
|
border?: string;
|
||||||
|
activeBg?: string;
|
||||||
|
color?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
rate: 0,
|
||||||
|
width: 200,
|
||||||
|
bg: '#E0E7FF',
|
||||||
|
border: '#C7D2FE',
|
||||||
|
activeBg: 'linear-gradient(90deg, #818CF8 0%, #4F46E5 100%)',
|
||||||
|
color: '#4F46E5',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const percent = computed(() => {
|
||||||
|
const r = Number(props.rate);
|
||||||
|
if (Number.isNaN(r)) return 0;
|
||||||
|
return Math.max(0, Math.min(100, Math.round(r * 100)));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.progress {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2rpx solid;
|
||||||
|
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
transition: width 400ms ease-out;
|
||||||
|
box-shadow: inset 0 -2rpx 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
src/components/Select/index.vue
Normal file
147
src/components/Select/index.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<view class="select" @click.stop="toggle">
|
||||||
|
<view class="select-current">
|
||||||
|
<text>{{ currentLabel }}</text>
|
||||||
|
<view class="select-arrow" :class="{ open: visible }"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="visible" class="select-mask" @click.stop="visible = false"></view>
|
||||||
|
<view v-if="visible" class="select-panel">
|
||||||
|
<view
|
||||||
|
v-for="item in selectList"
|
||||||
|
:key="String(item.value)"
|
||||||
|
:class="['select-option', { active: modelValue === item.value }]"
|
||||||
|
@click.stop="onPick(item)"
|
||||||
|
>
|
||||||
|
<text>{{ item.label }}</text>
|
||||||
|
<view v-if="modelValue === item.value" class="select-check"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 下拉选择器(Claymorphism)
|
||||||
|
* 与原 qy-Select API 一致:v-model + selectList + @selectChange
|
||||||
|
*/
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string | number;
|
||||||
|
selectList: Option[];
|
||||||
|
placeholder?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', v: string | number): void;
|
||||||
|
(e: 'selectChange', item: Option): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
|
||||||
|
const currentLabel = computed(() => {
|
||||||
|
const found = props.selectList.find((i) => i.value === props.modelValue);
|
||||||
|
return found?.label || props.placeholder || '请选择';
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
visible.value = !visible.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPick = (item: Option) => {
|
||||||
|
visible.value = false;
|
||||||
|
if (props.modelValue === item.value) return;
|
||||||
|
emit('update:modelValue', item.value);
|
||||||
|
emit('selectChange', item);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-current {
|
||||||
|
height: 64rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(99, 102, 241, 0.12),
|
||||||
|
inset 0 -3rpx 0 rgba(199, 210, 254, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-arrow {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-right: 4rpx solid #4f46e5;
|
||||||
|
border-bottom: 4rpx solid #4f46e5;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
margin-top: -6rpx;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: rotate(-135deg);
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 12rpx);
|
||||||
|
right: 0;
|
||||||
|
z-index: 101;
|
||||||
|
min-width: 220rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 16rpx 40rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option {
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #312e81;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-check {
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
src/components/Sidebar/index.vue
Normal file
103
src/components/Sidebar/index.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<view class="sidebar">
|
||||||
|
<view
|
||||||
|
v-for="item in config"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['sidebar-item', { active: modelValue === item.id }]"
|
||||||
|
@click="onSelect(item)"
|
||||||
|
>
|
||||||
|
<view class="sidebar-dot" v-if="item.warning && modelValue !== item.id"></view>
|
||||||
|
<text>{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 学科 / 分组 选择侧边栏
|
||||||
|
* - v-model 绑定当前激活项 id
|
||||||
|
* - config: [{ id, name, warning? }]
|
||||||
|
*
|
||||||
|
* 与原 qy-Sidebar API 保持一致
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
id: string | number;
|
||||||
|
name: string;
|
||||||
|
warning?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string | number;
|
||||||
|
config: Item[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: string | number): void;
|
||||||
|
(e: 'handleChange', item: Item): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onSelect = (item: Item) => {
|
||||||
|
if (props.modelValue === item.id) return;
|
||||||
|
emit('update:modelValue', item.id);
|
||||||
|
emit('handleChange', item);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 24rpx rgba(99, 102, 241, 0.1),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f7ff;
|
||||||
|
color: #6366f1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 16rpx rgba(79, 70, 229, 0.35),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.12),
|
||||||
|
inset 0 2rpx 0 rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ef4444;
|
||||||
|
box-shadow: 0 0 0 4rpx rgba(239, 68, 68, 0.18);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
src/enum/http.ts
Normal file
12
src/enum/http.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* @author zhouyankai
|
||||||
|
* @description 这是http的枚举类
|
||||||
|
* @Date: 2022-07-31
|
||||||
|
*/
|
||||||
|
export const filterRouter = ['/login'];
|
||||||
|
|
||||||
|
export enum sex {
|
||||||
|
'男' = 0,
|
||||||
|
'女',
|
||||||
|
'未知',
|
||||||
|
}
|
||||||
8
src/env.d.ts
vendored
Normal file
8
src/env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createSSRApp } from 'vue';
|
||||||
|
import '@vingogo/uni-ui/lib/style.css';
|
||||||
|
import App from './App.vue';
|
||||||
|
import pinia from './state/index';
|
||||||
|
import router from './router';
|
||||||
|
import './style/index.css';
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App);
|
||||||
|
app.use(pinia);
|
||||||
|
app.use(router);
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
};
|
||||||
|
}
|
||||||
94
src/manifest.json
Normal file
94
src/manifest.json
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"name" : "灵犀学",
|
||||||
|
"appid" : "__UNI__F5571DB",
|
||||||
|
"description" : "",
|
||||||
|
"versionName" : "1.0.0",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"transformPx" : false,
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp" : {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "wxa5522671f30c5891",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false,
|
||||||
|
"minified" : true,
|
||||||
|
"ignoreUploadUnusedFiles" : false
|
||||||
|
},
|
||||||
|
"usingComponents" : true,
|
||||||
|
"packOptions" : {
|
||||||
|
"ignore" : [],
|
||||||
|
"include" : [
|
||||||
|
{
|
||||||
|
"type" : "folder",
|
||||||
|
"value" : "uni_modules/qiun-data-charts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3",
|
||||||
|
"h5" : {
|
||||||
|
"router" : {
|
||||||
|
"mode" : "hash",
|
||||||
|
"base" : "./"
|
||||||
|
},
|
||||||
|
"optimization" : {
|
||||||
|
"treeShaking" : {
|
||||||
|
"enable" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
268
src/pages.json
Normal file
268
src/pages.json
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
{
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true,
|
||||||
|
"custom": {
|
||||||
|
"^vin-(.*)": "@vingogo/uni-ui/lib/components/$1/index.vue",
|
||||||
|
"^xy-(.*)": "@/components/$1/index.vue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"name": "home",
|
||||||
|
"meta": {
|
||||||
|
"islogin": false
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "学小乐AI",
|
||||||
|
"enablePullDownRefresh": false,
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"name": "login",
|
||||||
|
"meta": {
|
||||||
|
"islogin": false
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/homework/index",
|
||||||
|
"name": "homework",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的作业",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/homework/history/index",
|
||||||
|
"name": "homework-history",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "作业记录",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/homework/reports/index",
|
||||||
|
"name": "homework-reports",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "作业报告",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/wrong/index",
|
||||||
|
"name": "wrong",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "错题练习",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/doWork/index",
|
||||||
|
"name": "doWork",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "答题",
|
||||||
|
"disableScroll": true,
|
||||||
|
"pageOrientation": "landscape",
|
||||||
|
"app-plus": {
|
||||||
|
"bounce": "none",
|
||||||
|
"allowsBounceVertical": "NO"
|
||||||
|
},
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "landscape"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/wode/index",
|
||||||
|
"name": "wode",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "pages/teacher",
|
||||||
|
"name": "teacher",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index/index",
|
||||||
|
"name": "teacher-home",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "teacher"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "老师首页",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "homework/filter",
|
||||||
|
"name": "teacher-homework-filter",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "teacher"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "筛选条件",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "homework/index",
|
||||||
|
"name": "teacher-homework",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "teacher"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "学情报告",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "weakKnowledge/index",
|
||||||
|
"name": "teacher-weak-knowledge",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "teacher"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "薄弱知识点",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/parent",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "home/index",
|
||||||
|
"name": "parent-home",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "parent"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "成绩查询家长主页",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "reports/index",
|
||||||
|
"name": "parent-reports",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "parent"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "考试报告",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "reports/detail",
|
||||||
|
"name": "parent-report-detail",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "parent"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "作业详情",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "analysis/index",
|
||||||
|
"name": "parent-analysis",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "parent"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "学情追踪",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "profile/index",
|
||||||
|
"name": "parent-profile",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "parent"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "个人中心",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "学小乐AI",
|
||||||
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
|
"backgroundColor": "#F5F7FF",
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
2031
src/pages/doWork/components/Question.vue
Normal file
2031
src/pages/doWork/components/Question.vue
Normal file
File diff suppressed because it is too large
Load Diff
695
src/pages/doWork/index.vue
Normal file
695
src/pages/doWork/index.vue
Normal file
@ -0,0 +1,695 @@
|
|||||||
|
<template>
|
||||||
|
<view class="dw-page">
|
||||||
|
<!-- 装饰背景 -->
|
||||||
|
<view class="bg-decor bg-decor-a"></view>
|
||||||
|
<view class="bg-decor bg-decor-b"></view>
|
||||||
|
|
||||||
|
<!-- 顶部条 -->
|
||||||
|
<view class="top">
|
||||||
|
<view class="top-back" @click="handleBack">
|
||||||
|
<view class="back-arrow"></view>
|
||||||
|
</view>
|
||||||
|
<view class="top-title">
|
||||||
|
<text class="ellipsis">{{ title }}</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="!pageLoading"
|
||||||
|
class="top-meta"
|
||||||
|
:style="{ paddingRight: menuButtonRightRpx + 'rpx' }"
|
||||||
|
>
|
||||||
|
<view class="top-progress">
|
||||||
|
<text class="cur">{{ activeIdx + 1 }}</text>
|
||||||
|
<text class="divider">/</text>
|
||||||
|
<text class="total">{{ paperList.length }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="top-clock">
|
||||||
|
<view class="clock-icon">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="#4F46E5" stroke-width="1.4" />
|
||||||
|
<path d="M8 5v3l2 1.2" stroke="#4F46E5" stroke-width="1.4"
|
||||||
|
stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<text>{{ timeFilter }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 主体 -->
|
||||||
|
<view v-if="!pageLoading" class="main">
|
||||||
|
<view class="book-box">
|
||||||
|
<Question
|
||||||
|
v-if="paperList.length && paperList[activeIdx]"
|
||||||
|
:key="`question-${activeIdx}-${paperList[activeIdx]?.id ?? ''}`"
|
||||||
|
:data="paperList[activeIdx]"
|
||||||
|
:idx="activeIdx + 1"
|
||||||
|
:total="paperList.length"
|
||||||
|
:resourceType="resourceType"
|
||||||
|
:reportFlag="reportFlag"
|
||||||
|
:subjectId="paperInfo.subjectId"
|
||||||
|
@submit="submitQuestion"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部题号 + 操作 -->
|
||||||
|
<view class="bottom">
|
||||||
|
<scroll-view class="indicator" scroll-x :show-scrollbar="false">
|
||||||
|
<view class="indicator-track">
|
||||||
|
<view
|
||||||
|
v-for="(item, idx) in paperList"
|
||||||
|
v-show="!item.unLook"
|
||||||
|
:key="item.id || idx"
|
||||||
|
:class="[
|
||||||
|
'indicator-item',
|
||||||
|
{
|
||||||
|
active: activeIdx === idx,
|
||||||
|
success: reportFlag && (item.correctResult || item.markFlag),
|
||||||
|
error: reportFlag && !(item.correctResult || item.markFlag) && (item.submitAnswer || item.submitAnswerPic),
|
||||||
|
empty: !reportFlag && !item.submitAnswer && !item.submitAnswerPic,
|
||||||
|
answered: !reportFlag && (item.submitAnswer || item.submitAnswerPic),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@click="switchQuestion(idx)"
|
||||||
|
>
|
||||||
|
<text>{{ idx + 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="!reportFlag"
|
||||||
|
:class="['btn-next', { disabled: submitLoading, submit: activeIdx >= paperList.length - 1 }]"
|
||||||
|
@click="handleNext"
|
||||||
|
>
|
||||||
|
<text>{{ activeIdx < paperList.length - 1 ? '下一题' : '提交' }}</text>
|
||||||
|
<view class="btn-arrow">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="m6 4 4 4-4 4" stroke="#FFFFFF" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<view v-else class="loading-page">
|
||||||
|
<Loading :text="reportFlag ? '正在加载报告中...' : '正在精选题目中...'" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import {
|
||||||
|
startExam,
|
||||||
|
saveRecordAnswer,
|
||||||
|
submitExam,
|
||||||
|
getPaperRecordDetail,
|
||||||
|
} from '@/api/homework';
|
||||||
|
import Loading from '@/components/Loading/index.vue';
|
||||||
|
import Question from './components/Question.vue';
|
||||||
|
|
||||||
|
const RESOURCE_TYPE_HOMEWORK = 'homework';
|
||||||
|
|
||||||
|
const pageLoading = ref(true);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const menuButtonRightRpx = ref(0);
|
||||||
|
|
||||||
|
const paperList = ref<any[]>([]);
|
||||||
|
const paperInfo = ref<any>({});
|
||||||
|
const activeIdx = ref(0);
|
||||||
|
|
||||||
|
const reportFlag = ref<0 | 1>(0);
|
||||||
|
const params = ref<any>({});
|
||||||
|
|
||||||
|
let timer: any = null;
|
||||||
|
const initialIdx = ref(0);
|
||||||
|
|
||||||
|
const resourceType = computed(() => params.value.resourceType || RESOURCE_TYPE_HOMEWORK);
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
const name = paperInfo.value.paperName;
|
||||||
|
if (name) {
|
||||||
|
return name.replace(/-\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, '');
|
||||||
|
}
|
||||||
|
return '作业';
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeFilter = computed(() => {
|
||||||
|
const cost = paperInfo.value.costTime || 0;
|
||||||
|
const h = Math.floor(cost / 3600);
|
||||||
|
const m = Math.floor((cost % 3600) / 60);
|
||||||
|
const s = cost % 60;
|
||||||
|
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||||
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
initialIdx.value = Number(options.idx) || 0;
|
||||||
|
if (options.id) {
|
||||||
|
reportFlag.value = 1;
|
||||||
|
params.value = { ...options, id: options.id };
|
||||||
|
} else {
|
||||||
|
reportFlag.value = Number(options.reportFlag || 0) as 0 | 1;
|
||||||
|
params.value = { ...options };
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (reportFlag.value) {
|
||||||
|
uni.navigateBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '是否确认退出,退出后不保留答题记录',
|
||||||
|
confirmColor: '#4f46e5',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
clearTimer();
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchQuestion = async (idx?: number) => {
|
||||||
|
if (idx === activeIdx.value) {
|
||||||
|
if (!reportFlag.value && resourceType.value === RESOURCE_TYPE_HOMEWORK) {
|
||||||
|
await saveAnswer();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (idx !== undefined) {
|
||||||
|
if (!reportFlag.value && resourceType.value === RESOURCE_TYPE_HOMEWORK) {
|
||||||
|
await saveAnswer();
|
||||||
|
}
|
||||||
|
activeIdx.value = idx;
|
||||||
|
} else if (activeIdx.value < paperList.value.length - 1) {
|
||||||
|
if (!reportFlag.value && resourceType.value === RESOURCE_TYPE_HOMEWORK) {
|
||||||
|
await saveAnswer();
|
||||||
|
}
|
||||||
|
activeIdx.value++;
|
||||||
|
}
|
||||||
|
if (!reportFlag.value && paperList.value[activeIdx.value]) {
|
||||||
|
paperList.value[activeIdx.value].unLook = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
if (submitLoading.value) return;
|
||||||
|
if (activeIdx.value < paperList.value.length - 1) {
|
||||||
|
await switchQuestion();
|
||||||
|
} else {
|
||||||
|
if (paperList.value.some((i) => !i.submitAnswer && !i.submitAnswerPic)) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '还有题目未作答,确定要提交答案吗?',
|
||||||
|
confirmColor: '#4f46e5',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) submitTrain();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
submitTrain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAnswer = async () => {
|
||||||
|
const cur = paperList.value[activeIdx.value];
|
||||||
|
if (!cur) return;
|
||||||
|
try {
|
||||||
|
await saveRecordAnswer({
|
||||||
|
answer: cur.submitAnswer || '',
|
||||||
|
answerPic: cur.submitAnswerPic || '',
|
||||||
|
costTime: (paperInfo.value.costTime || 0) * 1000,
|
||||||
|
itemId: cur.itemId || cur.id,
|
||||||
|
recordId: params.value.recordId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[doWork] save err', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitQuestion = (answer: any) => {
|
||||||
|
if (reportFlag.value) return;
|
||||||
|
const idx = activeIdx.value;
|
||||||
|
if (answer.submitAnswer !== undefined) {
|
||||||
|
paperList.value[idx].submitAnswer = answer.submitAnswer;
|
||||||
|
}
|
||||||
|
if (answer.submitAnswerPic !== undefined) {
|
||||||
|
paperList.value[idx].submitAnswerPic = answer.submitAnswerPic;
|
||||||
|
}
|
||||||
|
if (answer.toNext && activeIdx.value < paperList.value.length - 1) {
|
||||||
|
setTimeout(() => switchQuestion(), 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitTrain = async () => {
|
||||||
|
clearTimer();
|
||||||
|
submitLoading.value = true;
|
||||||
|
uni.showLoading({ title: '提交中...', mask: true });
|
||||||
|
try {
|
||||||
|
await saveAnswer();
|
||||||
|
await submitExam({
|
||||||
|
answerDtoList: paperList.value.map((item) => ({
|
||||||
|
itemId: item.itemId || item.id,
|
||||||
|
answer: item.submitAnswer || '',
|
||||||
|
answerPic: item.submitAnswerPic || '',
|
||||||
|
})),
|
||||||
|
costTime: (paperInfo.value.costTime || 0) * 1000,
|
||||||
|
recordId: params.value.recordId,
|
||||||
|
});
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '试卷提交成功请等待批改',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '好的',
|
||||||
|
confirmColor: '#4f46e5',
|
||||||
|
success: () => uni.navigateBack(),
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: err?.message || '提交失败', icon: 'none' });
|
||||||
|
startTimer();
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTimer = (init?: boolean) => {
|
||||||
|
clearTimer();
|
||||||
|
if (init) paperInfo.value.costTime = 0;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
paperInfo.value.costTime = (paperInfo.value.costTime || 0) + 1;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearTimer = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initReport = async () => {
|
||||||
|
const res = await getPaperRecordDetail({
|
||||||
|
recordId: params.value.recordId || params.value.id,
|
||||||
|
reportFlag: 1,
|
||||||
|
});
|
||||||
|
paperInfo.value = res.data;
|
||||||
|
paperList.value = res.data.subjectTitleVoList || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initNewTrain = async () => {
|
||||||
|
const res: any = await startExam(params.value.recordId);
|
||||||
|
paperInfo.value = res.data || res;
|
||||||
|
if (paperInfo.value.costTime) {
|
||||||
|
paperInfo.value.costTime = Math.floor(paperInfo.value.costTime / 1000);
|
||||||
|
}
|
||||||
|
paperList.value = (res.data?.subjectTitleVoList || res.subjectTitleVoList || []).map(
|
||||||
|
(item: any, idx: number) => ({
|
||||||
|
...item,
|
||||||
|
unLook: idx > 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuButtonInfo = () => {
|
||||||
|
try {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const m = uni.getMenuButtonBoundingClientRect();
|
||||||
|
const sys = uni.getSystemInfoSync();
|
||||||
|
if (m && sys) {
|
||||||
|
const rightPx = sys.windowWidth - m.right;
|
||||||
|
const widthPx = m.width;
|
||||||
|
const pxToRpx = 750 / sys.windowWidth;
|
||||||
|
menuButtonRightRpx.value = Math.ceil((rightPx + widthPx) * pxToRpx) + 16;
|
||||||
|
} else {
|
||||||
|
menuButtonRightRpx.value = 120;
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
menuButtonRightRpx.value = 0;
|
||||||
|
// #endif
|
||||||
|
} catch {
|
||||||
|
menuButtonRightRpx.value = 120;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
pageLoading.value = true;
|
||||||
|
if (reportFlag.value) {
|
||||||
|
await initReport();
|
||||||
|
} else {
|
||||||
|
await initNewTrain();
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
const target = Math.min(initialIdx.value, paperList.value.length - 1);
|
||||||
|
activeIdx.value = Math.max(0, target);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[doWork] init err', err);
|
||||||
|
uni.showToast({ title: err?.message || '加载失败', icon: 'none' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 1500);
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getMenuButtonInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimer();
|
||||||
|
paperInfo.value = {};
|
||||||
|
paperList.value = [];
|
||||||
|
activeIdx.value = 0;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dw-page {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
background: #f5f7ff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 装饰背景 */
|
||||||
|
.bg-decor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(60rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
&-a {
|
||||||
|
top: -80rpx;
|
||||||
|
right: -80rpx;
|
||||||
|
width: 280rpx;
|
||||||
|
height: 280rpx;
|
||||||
|
background: radial-gradient(circle, rgba(129, 140, 248, 0.35), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-b {
|
||||||
|
bottom: -80rpx;
|
||||||
|
left: -80rpx;
|
||||||
|
width: 280rpx;
|
||||||
|
height: 280rpx;
|
||||||
|
background: radial-gradient(circle, rgba(34, 197, 94, 0.3), transparent 70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部条 */
|
||||||
|
.top {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12rpx 20rpx 8rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-back {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 10rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -3rpx 0 rgba(199, 210, 254, 0.6);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-arrow {
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
border-left: 4rpx solid #4f46e5;
|
||||||
|
border-bottom: 4rpx solid #4f46e5;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #312e81;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
text {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
background: linear-gradient(180deg, #818cf8, #4f46e5);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 10rpx rgba(79, 70, 229, 0.3),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.cur {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
font-size: 16rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
font-size: 18rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-clock {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 10rpx rgba(99, 102, 241, 0.15),
|
||||||
|
inset 0 -3rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
|
||||||
|
.clock-icon {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主体 */
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0 20rpx 12rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-box {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
flex: 1;
|
||||||
|
width: 0;
|
||||||
|
height: 56rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-track {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 4rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 8rpx rgba(99, 102, 241, 0.12),
|
||||||
|
inset 0 -3rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
transition: all 150ms ease;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(180deg, #818cf8, #4f46e5);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(79, 70, 229, 0.4),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.answered {
|
||||||
|
background: linear-gradient(180deg, #c7d2fe, #818cf8);
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
background: #f1f5f9;
|
||||||
|
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: linear-gradient(180deg, #4ade80, #16a34a);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 12rpx rgba(22, 163, 74, 0.3),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: linear-gradient(180deg, #f87171, #dc2626);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 12rpx rgba(220, 38, 38, 0.3),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-next {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 56rpx;
|
||||||
|
padding: 0 28rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
background: linear-gradient(180deg, #4ade80, #16a34a);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(22, 163, 74, 0.35),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 150ms ease;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-arrow {
|
||||||
|
width: 22rpx;
|
||||||
|
height: 22rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.submit {
|
||||||
|
background: linear-gradient(180deg, #818cf8, #4f46e5);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(79, 70, 229, 0.4),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(2rpx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载 */
|
||||||
|
.loading-page {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
702
src/pages/homework/history/index.vue
Normal file
702
src/pages/homework/history/index.vue
Normal file
@ -0,0 +1,702 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hist-page">
|
||||||
|
<!-- 装饰背景 -->
|
||||||
|
<view class="bg-decor bg-decor-a"></view>
|
||||||
|
<view class="bg-decor bg-decor-b"></view>
|
||||||
|
|
||||||
|
<!-- 状态筛选 (sticky 段控件) -->
|
||||||
|
<view class="filter-bar">
|
||||||
|
<view class="filter-segment">
|
||||||
|
<view
|
||||||
|
v-for="item in statusList"
|
||||||
|
:key="item.value"
|
||||||
|
:class="['filter-tab', { active: activeStatus === item.value }]"
|
||||||
|
@click="onChangeStatus(item.value)"
|
||||||
|
>
|
||||||
|
<text>{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 学科横向滚动 -->
|
||||||
|
<view v-if="subjectList.length" class="subject-bar">
|
||||||
|
<scroll-view scroll-x :show-scrollbar="false" class="subject-scroll">
|
||||||
|
<view class="subject-list">
|
||||||
|
<view
|
||||||
|
v-for="item in subjectListAll"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['subject-pill', { active: activeSubjectId === item.id }]"
|
||||||
|
@click="onSelectSubject(item.id)"
|
||||||
|
>
|
||||||
|
<text>{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 列表区 -->
|
||||||
|
<scroll-view class="list-scroll" scroll-y :show-scrollbar="false">
|
||||||
|
<view v-if="jobList.length" class="job-list">
|
||||||
|
<view
|
||||||
|
v-for="item in jobList"
|
||||||
|
:key="item.id"
|
||||||
|
class="job-card"
|
||||||
|
@click="onTapJob(item)"
|
||||||
|
>
|
||||||
|
<!-- 上方左右两栏 -->
|
||||||
|
<view class="job-head">
|
||||||
|
<view class="job-subject">
|
||||||
|
<view class="job-subject-emoji">{{ getSubjectEmoji(item.subjectName) }}</view>
|
||||||
|
<text>{{ item.subjectName || activeSubjectName }}</text>
|
||||||
|
</view>
|
||||||
|
<view :class="['job-status', item.gradingStatus === 1 ? 'corrected' : 'pending']">
|
||||||
|
<view class="status-dot"></view>
|
||||||
|
<text>{{ item.gradingStatus === 1 ? '已批改' : '待批改' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="job-title">{{ item.paperName }}</view>
|
||||||
|
|
||||||
|
<!-- 已批改 -> 显示得分卡 -->
|
||||||
|
<view v-if="item.gradingStatus === 1" class="job-score" :class="getRateType(item)">
|
||||||
|
<view class="score-left">
|
||||||
|
<text class="score-label">得分率</text>
|
||||||
|
<view class="score-num">
|
||||||
|
<text class="num">{{ getScoreRate(item) }}</text>
|
||||||
|
<text class="unit">%</text>
|
||||||
|
</view>
|
||||||
|
<ProgressBar
|
||||||
|
:rate="(item.score || 0) / (item.totalScore || 1)"
|
||||||
|
:width="280"
|
||||||
|
v-bind="getProgressColor(item)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="score-right">
|
||||||
|
<view class="score-circle">
|
||||||
|
<text class="rank-emoji">{{ getRankEmoji(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.addDiamond || item.addExp" class="reward-row">
|
||||||
|
<view v-if="item.addDiamond" class="reward-pill diamond">
|
||||||
|
<view class="reward-icon">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M3 6 8 2l5 4-5 9-5-9z" fill="#0EA5E9" stroke="#0369A1"
|
||||||
|
stroke-width="1" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<text>+{{ item.addDiamond }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.addExp" class="reward-pill exp">
|
||||||
|
<view class="reward-icon">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="m8 1 2.2 4.5 5 .7-3.6 3.5.9 5-4.5-2.4-4.5 2.4.9-5L.7 6.2l5-.7L8 1z"
|
||||||
|
fill="#F59E0B" stroke="#B45309" stroke-width="1" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<text>+{{ item.addExp }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 未批改 -> 提示信息 -->
|
||||||
|
<view v-else class="job-pending">
|
||||||
|
<view class="pending-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="9" stroke="#94A3B8" stroke-width="1.6" />
|
||||||
|
<path d="M12 7v5l3 2" stroke="#94A3B8" stroke-width="1.6"
|
||||||
|
stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<text>等待老师批改中…</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<view class="job-meta">
|
||||||
|
<view v-if="item.createTime" class="meta-line">
|
||||||
|
<text class="meta-label">发布</text>
|
||||||
|
<text class="meta-value">{{ formatTime(item.createTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.markTime" class="meta-line">
|
||||||
|
<text class="meta-label">批改</text>
|
||||||
|
<text class="meta-value">{{ formatTime(item.markTime) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<Empty
|
||||||
|
v-else-if="!listLoading"
|
||||||
|
class="empty"
|
||||||
|
:type="pageError ? 'network' : 'learn'"
|
||||||
|
:content="pageError ? '网络出问题了啦' : '暂无作业记录,快去做作业吧!'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view style="height: 80rpx"></view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="listLoading" class="page-loader">
|
||||||
|
<Loading text="加载中..." />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import Empty from '@/components/Empty/index.vue';
|
||||||
|
import Loading from '@/components/Loading/index.vue';
|
||||||
|
import ProgressBar from '@/components/ProgressBar/index.vue';
|
||||||
|
import { getListCompleteRecord } from '@/api/homework';
|
||||||
|
import { formatTime } from '@/utils/format';
|
||||||
|
|
||||||
|
const statusList = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '已批改', value: '1' },
|
||||||
|
{ label: '未批改', value: '0' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SubjectItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeStatus = ref('');
|
||||||
|
const subjectList = ref<SubjectItem[]>([]);
|
||||||
|
const activeSubjectId = ref<string>('');
|
||||||
|
const jobList = ref<any[]>([]);
|
||||||
|
const listLoading = ref(false);
|
||||||
|
const pageError = ref(false);
|
||||||
|
|
||||||
|
const subjectListAll = computed<SubjectItem[]>(() => {
|
||||||
|
return [{ id: '', name: '全部' }, ...subjectList.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeSubjectName = computed(() => {
|
||||||
|
return subjectList.value.find((i) => i.id === activeSubjectId.value)?.name || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorConfig: Record<string, any> = {
|
||||||
|
success: { color: '#16A34A', border: '#16A34A', bg: '#DCFCE7', activeBg: '#22C55E' },
|
||||||
|
primary: { color: '#4F46E5', border: '#4F46E5', bg: '#E0E7FF', activeBg: '#818CF8' },
|
||||||
|
warning: { color: '#D97706', border: '#D97706', bg: '#FEF3C7', activeBg: '#F59E0B' },
|
||||||
|
error: { color: '#DC2626', border: '#DC2626', bg: '#FEE2E2', activeBg: '#EF4444' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScoreRate = (item: any) => {
|
||||||
|
if (!item.totalScore) return 0;
|
||||||
|
return Math.round((item.score / item.totalScore) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRateType = (item: any) => {
|
||||||
|
const r = getScoreRate(item);
|
||||||
|
if (r >= 80) return 'success';
|
||||||
|
if (r >= 60) return 'primary';
|
||||||
|
if (r >= 40) return 'warning';
|
||||||
|
return 'error';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressColor = (item: any) => colorConfig[getRateType(item)] || colorConfig.primary;
|
||||||
|
|
||||||
|
const getRankEmoji = (item: any) => {
|
||||||
|
const r = getScoreRate(item);
|
||||||
|
if (r >= 95) return '🏆';
|
||||||
|
if (r >= 80) return '🌟';
|
||||||
|
if (r >= 60) return '👍';
|
||||||
|
if (r >= 40) return '💪';
|
||||||
|
return '📚';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubjectEmoji = (name: string) => {
|
||||||
|
if (!name) return '📘';
|
||||||
|
if (name.includes('语文')) return '📖';
|
||||||
|
if (name.includes('数学')) return '🧮';
|
||||||
|
if (name.includes('英语')) return '🔤';
|
||||||
|
if (name.includes('物理')) return '🔬';
|
||||||
|
if (name.includes('化学')) return '⚗️';
|
||||||
|
if (name.includes('生物')) return '🌿';
|
||||||
|
if (name.includes('历史')) return '📜';
|
||||||
|
if (name.includes('地理')) return '🌏';
|
||||||
|
if (name.includes('政治')) return '⚖️';
|
||||||
|
return '📘';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeStatus = (val: string) => {
|
||||||
|
activeStatus.value = val;
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectSubject = (id: string) => {
|
||||||
|
activeSubjectId.value = id;
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTapJob = (item: any) => {
|
||||||
|
if (item.gradingStatus !== 1) {
|
||||||
|
uni.showToast({ title: '作业未批改,请耐心等待老师批改', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/homework/reports/index?id=${item.id}&name=${encodeURIComponent(item.paperName || '')}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSubject = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getListCompleteRecord({});
|
||||||
|
const records = res.data || [];
|
||||||
|
const map = new Map<string, SubjectItem>();
|
||||||
|
records.forEach((item: any) => {
|
||||||
|
const sid = String(item.subjectId);
|
||||||
|
if (!map.has(sid)) {
|
||||||
|
map.set(sid, { id: sid, name: item.subjectName });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
subjectList.value = Array.from(map.values());
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[history] init subject err', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchJobList = async () => {
|
||||||
|
const params: Record<string, any> = {};
|
||||||
|
if (activeStatus.value !== '') params.gradingStatus = activeStatus.value;
|
||||||
|
if (activeSubjectId.value) params.subjectId = activeSubjectId.value;
|
||||||
|
const res = await getListCompleteRecord(params);
|
||||||
|
jobList.value = res.data || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
pageError.value = false;
|
||||||
|
listLoading.value = true;
|
||||||
|
jobList.value = [];
|
||||||
|
try {
|
||||||
|
await fetchJobList();
|
||||||
|
} catch (err) {
|
||||||
|
pageError.value = true;
|
||||||
|
console.error('[history] fetch job err', err);
|
||||||
|
} finally {
|
||||||
|
listLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
listLoading.value = true;
|
||||||
|
try {
|
||||||
|
await initSubject();
|
||||||
|
await fetchJobList();
|
||||||
|
} catch (err) {
|
||||||
|
pageError.value = true;
|
||||||
|
} finally {
|
||||||
|
listLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (subjectList.value.length || jobList.value.length) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hist-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7ff;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-decor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(60rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
&-a {
|
||||||
|
top: -120rpx;
|
||||||
|
left: -100rpx;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
background: radial-gradient(circle, rgba(34, 197, 94, 0.35), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-b {
|
||||||
|
top: 320rpx;
|
||||||
|
right: -120rpx;
|
||||||
|
width: 320rpx;
|
||||||
|
height: 320rpx;
|
||||||
|
background: radial-gradient(circle, rgba(129, 140, 248, 0.35), transparent 70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态筛选 */
|
||||||
|
.filter-bar {
|
||||||
|
padding: 24rpx 32rpx 8rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-segment {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
box-shadow: inset 0 4rpx 8rpx rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
flex: 1;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 200ms ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(79, 70, 229, 0.3),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 学科横滚 */
|
||||||
|
.subject-bar {
|
||||||
|
padding: 12rpx 0 8rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-list {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-pill {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6366f1;
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 10rpx rgba(99, 102, 241, 0.1),
|
||||||
|
inset 0 -3rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(180deg, #4ade80 0%, #16a34a 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 16rpx rgba(22, 163, 74, 0.3),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表 */
|
||||||
|
.list-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-list {
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 24rpx rgba(99, 102, 241, 0.12),
|
||||||
|
inset 0 -5rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
transition: transform 150ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-subject {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 6rpx 18rpx 6rpx 12rpx;
|
||||||
|
background: linear-gradient(135deg, #eef2ff, #e0e7ff);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
|
||||||
|
.job-subject-emoji {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 6rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.corrected {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #312e81;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已批改得分卡 */
|
||||||
|
.job-score {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: linear-gradient(135deg, rgba(220, 252, 231, 0.7), rgba(187, 247, 208, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: linear-gradient(135deg, rgba(224, 231, 255, 0.7), rgba(199, 210, 254, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: linear-gradient(135deg, rgba(254, 243, 199, 0.7), rgba(253, 230, 138, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: linear-gradient(135deg, rgba(254, 226, 226, 0.7), rgba(254, 202, 202, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.score-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-num {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
margin: 4rpx 0 12rpx;
|
||||||
|
|
||||||
|
.num {
|
||||||
|
font-size: 64rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-right {
|
||||||
|
width: 140rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-circle {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 12rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -3rpx 0 rgba(199, 210, 254, 0.6);
|
||||||
|
|
||||||
|
.rank-emoji {
|
||||||
|
font-size: 48rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6rpx;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
&.diamond {
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.exp {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-icon {
|
||||||
|
width: 22rpx;
|
||||||
|
height: 22rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 未批改提示 */
|
||||||
|
.job-pending {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
background: #f5f7ff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
|
||||||
|
.pending-icon {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* meta */
|
||||||
|
.job-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 2rpx 10rpx;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 200rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-loader {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(245, 247, 255, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
695
src/pages/homework/index.vue
Normal file
695
src/pages/homework/index.vue
Normal file
@ -0,0 +1,695 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hw-page">
|
||||||
|
<!-- 顶部装饰 -->
|
||||||
|
<view class="bg-decor bg-decor-a"></view>
|
||||||
|
<view class="bg-decor bg-decor-b"></view>
|
||||||
|
|
||||||
|
<!-- 主体 -->
|
||||||
|
<scroll-view class="hw-scroll" scroll-y :show-scrollbar="false" :enhanced="true">
|
||||||
|
<!-- 学科tab组 -->
|
||||||
|
<view v-if="subjectList.length" class="subject-bar">
|
||||||
|
<scroll-view scroll-x :show-scrollbar="false" class="subject-scroll">
|
||||||
|
<view class="subject-list">
|
||||||
|
<view
|
||||||
|
v-for="item in subjectList"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['subject-tab', { active: activeSubjectId === item.id }]"
|
||||||
|
@click="onSelectSubject(item)"
|
||||||
|
>
|
||||||
|
<view class="subject-emoji">{{ getSubjectEmoji(item.name) }}</view>
|
||||||
|
<view class="subject-name">{{ item.name }}</view>
|
||||||
|
<view class="subject-count">{{ getSubjectCount(item.id) }}</view>
|
||||||
|
<view v-if="item.warning" class="subject-dot"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 头部统计卡片 -->
|
||||||
|
<view v-if="totalCount > 0" class="stats-card">
|
||||||
|
<view class="stats-bg"></view>
|
||||||
|
<view class="stats-content">
|
||||||
|
<view class="stats-left">
|
||||||
|
<view class="stats-number">{{ totalCount }}</view>
|
||||||
|
<view class="stats-label">份待完成作业</view>
|
||||||
|
</view>
|
||||||
|
<view class="stats-right">
|
||||||
|
<view class="stats-illu">
|
||||||
|
<svg viewBox="0 0 80 80" fill="none">
|
||||||
|
<rect x="14" y="10" width="48" height="60" rx="6"
|
||||||
|
fill="#FFFFFF" stroke="#312E81" stroke-width="2" />
|
||||||
|
<rect x="22" y="20" width="32" height="3" rx="1.5" fill="#22C55E" />
|
||||||
|
<rect x="22" y="30" width="24" height="3" rx="1.5" fill="#94A3B8" />
|
||||||
|
<rect x="22" y="38" width="28" height="3" rx="1.5" fill="#94A3B8" />
|
||||||
|
<rect x="22" y="46" width="20" height="3" rx="1.5" fill="#94A3B8" />
|
||||||
|
<circle cx="55" cy="58" r="10" fill="#22C55E" stroke="#312E81" stroke-width="2" />
|
||||||
|
<path d="m51 58 3 3 6-6" stroke="#FFFFFF" stroke-width="2.4"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 作业列表 -->
|
||||||
|
<view v-if="paperList.length" class="paper-list">
|
||||||
|
<view
|
||||||
|
v-for="paper in paperList"
|
||||||
|
:key="paper.id"
|
||||||
|
class="paper-card"
|
||||||
|
@click="onStartPaper(paper)"
|
||||||
|
>
|
||||||
|
<!-- 顶部标签 -->
|
||||||
|
<view class="paper-head">
|
||||||
|
<view :class="['paper-status', paper.costTime ? 'continue' : 'new']">
|
||||||
|
<view class="status-dot"></view>
|
||||||
|
<text>{{ paper.costTime ? '继续答题' : '未开始' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="paper-deadline">
|
||||||
|
<text class="deadline-label">截止</text>
|
||||||
|
<text class="deadline-time">{{ formatDeadline(paper.endTime) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<view class="paper-title">{{ paper.paperName }}</view>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<view v-if="paper.remark" class="paper-remark">
|
||||||
|
<view class="remark-icon">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M2 4h12M2 8h12M2 12h8" stroke="#94A3B8"
|
||||||
|
stroke-width="1.6" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<text>{{ paper.remark }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<view class="paper-meta">
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-avatar">
|
||||||
|
<text>{{ (paper.publisherName || '师').charAt(0) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="meta-text">{{ paper.publisherName || '老师' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="meta-spacer"></view>
|
||||||
|
<view class="meta-time">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="#94A3B8" stroke-width="1.4" />
|
||||||
|
<path d="M8 5v3l2 1" stroke="#94A3B8" stroke-width="1.4" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
<text>{{ formatTime(paper.createTime) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- CTA 按钮 -->
|
||||||
|
<view :class="['paper-cta', paper.costTime ? 'continue' : 'start']">
|
||||||
|
<text>{{ paper.costTime ? '继续答题' : '开始答题' }}</text>
|
||||||
|
<view class="cta-arrow">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="m6 4 4 4-4 4" stroke="#FFFFFF" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<Empty
|
||||||
|
v-else-if="!pageLoading && !paperList.length && subjectList.length"
|
||||||
|
class="empty"
|
||||||
|
type="task"
|
||||||
|
content="该科目暂无待完成作业"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 整页空状态 -->
|
||||||
|
<Empty
|
||||||
|
v-else-if="!pageLoading && !subjectList.length"
|
||||||
|
class="empty empty-page"
|
||||||
|
type="task"
|
||||||
|
content="还没有作业哦~"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 底部留白 -->
|
||||||
|
<view style="height: 60rpx"></view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="pageLoading" class="page-loader">
|
||||||
|
<Loading text="加载作业中..." />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import Empty from '@/components/Empty/index.vue';
|
||||||
|
import Loading from '@/components/Loading/index.vue';
|
||||||
|
import { getUnfinishedHomework, type PaperRecord } from '@/api/homework';
|
||||||
|
import { formatTime } from '@/utils/format';
|
||||||
|
|
||||||
|
const RESOURCE_TYPE = 'homework';
|
||||||
|
|
||||||
|
interface SubjectItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
warning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLoading = ref(false);
|
||||||
|
const subjectList = ref<SubjectItem[]>([]);
|
||||||
|
const subjectPaperMap = ref(new Map<string, PaperRecord[]>());
|
||||||
|
const activeSubjectId = ref('');
|
||||||
|
|
||||||
|
const paperList = computed<PaperRecord[]>(() => {
|
||||||
|
if (!activeSubjectId.value) return [];
|
||||||
|
return subjectPaperMap.value.get(activeSubjectId.value) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
subjectPaperMap.value.forEach((list) => {
|
||||||
|
count += list.length;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSubjectCount = (id: string) => subjectPaperMap.value.get(id)?.length || 0;
|
||||||
|
|
||||||
|
/** 学科emoji映射 */
|
||||||
|
const getSubjectEmoji = (name: string) => {
|
||||||
|
if (!name) return '📘';
|
||||||
|
if (name.includes('语文') || name.includes('国学')) return '📖';
|
||||||
|
if (name.includes('数学') || name.includes('算')) return '🧮';
|
||||||
|
if (name.includes('英语') || name.includes('英文')) return '🔤';
|
||||||
|
if (name.includes('物理')) return '🔬';
|
||||||
|
if (name.includes('化学')) return '⚗️';
|
||||||
|
if (name.includes('生物')) return '🌿';
|
||||||
|
if (name.includes('历史')) return '📜';
|
||||||
|
if (name.includes('地理')) return '🌏';
|
||||||
|
if (name.includes('政治') || name.includes('道德')) return '⚖️';
|
||||||
|
if (name.includes('音乐')) return '🎵';
|
||||||
|
if (name.includes('美术') || name.includes('画')) return '🎨';
|
||||||
|
if (name.includes('体育')) return '⚽';
|
||||||
|
if (name.includes('科学')) return '🧪';
|
||||||
|
if (name.includes('信息') || name.includes('计算机')) return '💻';
|
||||||
|
return '📘';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 截止时间友好格式 */
|
||||||
|
const formatDeadline = (time: string) => {
|
||||||
|
if (!time) return '不限';
|
||||||
|
const target = new Date(time).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = target - now;
|
||||||
|
if (diff < 0) return '已截止';
|
||||||
|
const day = Math.floor(diff / 86400000);
|
||||||
|
if (day < 1) {
|
||||||
|
const hour = Math.floor(diff / 3600000);
|
||||||
|
if (hour < 1) return '不足1小时';
|
||||||
|
return `${hour}小时后`;
|
||||||
|
}
|
||||||
|
if (day < 7) return `${day}天后`;
|
||||||
|
return formatTime(time, 'MM/DD');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectSubject = (item: SubjectItem) => {
|
||||||
|
activeSubjectId.value = item.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStartPaper = (paper: PaperRecord) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/doWork/index?resourceType=${RESOURCE_TYPE}&recordId=${paper.id}&reportFlag=0&subjectId=${activeSubjectId.value}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initData = async () => {
|
||||||
|
pageLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getUnfinishedHomework();
|
||||||
|
const records = res.data || [];
|
||||||
|
|
||||||
|
const map = new Map<string, PaperRecord[]>();
|
||||||
|
const subjects: SubjectItem[] = [];
|
||||||
|
|
||||||
|
records.forEach((item) => {
|
||||||
|
const list = map.get(item.subjectId);
|
||||||
|
if (list) {
|
||||||
|
list.push(item);
|
||||||
|
} else {
|
||||||
|
map.set(item.subjectId, [item]);
|
||||||
|
subjects.push({
|
||||||
|
id: item.subjectId,
|
||||||
|
name: item.subjectName,
|
||||||
|
warning: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
subjectPaperMap.value = map;
|
||||||
|
subjectList.value = subjects;
|
||||||
|
|
||||||
|
if (subjects.length && (!activeSubjectId.value || !map.has(activeSubjectId.value))) {
|
||||||
|
activeSubjectId.value = subjects[0].id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[homework] init err', err);
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initData();
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (subjectList.value.length) {
|
||||||
|
initData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hw-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7ff;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 装饰背景 */
|
||||||
|
.bg-decor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(60rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
&-a {
|
||||||
|
top: -100rpx;
|
||||||
|
right: -120rpx;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
background: radial-gradient(circle, rgba(129, 140, 248, 0.45), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-b {
|
||||||
|
bottom: 200rpx;
|
||||||
|
left: -100rpx;
|
||||||
|
width: 320rpx;
|
||||||
|
height: 320rpx;
|
||||||
|
background: radial-gradient(circle, rgba(34, 197, 94, 0.3), transparent 70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hw-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 学科 tab */
|
||||||
|
.subject-bar {
|
||||||
|
padding: 24rpx 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-list {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-tab {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 130rpx;
|
||||||
|
padding: 20rpx 12rpx 18rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 16rpx rgba(99, 102, 241, 0.12),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 10rpx 24rpx rgba(79, 70, 229, 0.45),
|
||||||
|
inset 0 -5rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
.subject-name {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-count {
|
||||||
|
color: #c7d2fe;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-emoji {
|
||||||
|
font-size: 44rpx;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-name {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #312e81;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-count {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2rpx 0;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(199, 210, 254, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ef4444;
|
||||||
|
border: 3rpx solid #ffffff;
|
||||||
|
box-shadow: 0 0 0 2rpx rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-card {
|
||||||
|
margin: 24rpx 32rpx 8rpx;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 24rpx rgba(245, 158, 11, 0.25),
|
||||||
|
inset 0 -6rpx 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.6), transparent 50%),
|
||||||
|
radial-gradient(circle at 20% 80%, rgba(245, 158, 11, 0.2), transparent 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 36rpx 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-number {
|
||||||
|
font-size: 88rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #92400e;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #b45309;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-right {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
filter: drop-shadow(0 6rpx 12rpx rgba(146, 64, 14, 0.2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 作业列表 */
|
||||||
|
.paper-list {
|
||||||
|
padding: 24rpx 32rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 28rpx rgba(99, 102, 241, 0.15),
|
||||||
|
inset 0 -6rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
transition: transform 150ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.new {
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
background: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.continue {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-deadline {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.deadline-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deadline-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #312e81;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-remark {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 18rpx 20rpx;
|
||||||
|
background: #f5f7ff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.remark-icon {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-avatar {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: linear-gradient(135deg, #818cf8, #4f46e5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 4rpx 8rpx rgba(79, 70, 229, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-cta {
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
|
||||||
|
&.start {
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 20rpx rgba(79, 70, 229, 0.35),
|
||||||
|
inset 0 -5rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.continue {
|
||||||
|
background: linear-gradient(180deg, #4ade80 0%, #16a34a 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 20rpx rgba(22, 163, 74, 0.35),
|
||||||
|
inset 0 -5rpx 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-arrow {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-page {
|
||||||
|
padding: 200rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-loader {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(245, 247, 255, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1037
src/pages/homework/reports/index.vue
Normal file
1037
src/pages/homework/reports/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
737
src/pages/index/index.vue
Normal file
737
src/pages/index/index.vue
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
<template>
|
||||||
|
<view class="home">
|
||||||
|
<!-- 顶部装饰云朵 -->
|
||||||
|
<view class="decor decor-1"></view>
|
||||||
|
<view class="decor decor-2"></view>
|
||||||
|
<view class="decor decor-3"></view>
|
||||||
|
|
||||||
|
<!-- 头部 -->
|
||||||
|
<view class="hero">
|
||||||
|
<view class="hero-greet">
|
||||||
|
<view class="hero-hello">{{ greeting }} 👋</view>
|
||||||
|
<view class="hero-title">学小乐<text class="hero-title-ai">AI</text></view>
|
||||||
|
<view class="hero-sub">让学习像玩游戏一样有趣</view>
|
||||||
|
</view>
|
||||||
|
<view class="hero-mascot">
|
||||||
|
<view class="mascot-blob"></view>
|
||||||
|
<view class="mascot-face">
|
||||||
|
<view class="mascot-eye left"></view>
|
||||||
|
<view class="mascot-eye right"></view>
|
||||||
|
<view class="mascot-smile"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户卡 / 登录卡 -->
|
||||||
|
<view v-if="isLogin" class="user-card">
|
||||||
|
<view class="user-avatar-wrap">
|
||||||
|
<image class="user-avatar" :src="avatarUrl" mode="aspectFill" />
|
||||||
|
<view class="user-badge">VIP</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-info">
|
||||||
|
<view class="user-name">{{ userInfo.nickName || '小同学' }}</view>
|
||||||
|
<view class="user-school" v-if="userInfo.schoolName || userInfo.className">
|
||||||
|
<view class="ic ic-school">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 3 2 9l10 6 10-6-10-6z" stroke="currentColor" stroke-width="1.6"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="m6 11 0 5c0 1 2 3 6 3s6-2 6-3v-5" stroke="currentColor" stroke-width="1.6"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<text>{{ userInfo.schoolName || '' }} · {{ userInfo.className || '' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-stats">
|
||||||
|
<view class="user-stat">
|
||||||
|
<view class="user-stat-num">{{ userInfo.diamond || 0 }}</view>
|
||||||
|
<view class="user-stat-label">钻石</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-stat-divider"></view>
|
||||||
|
<view class="user-stat">
|
||||||
|
<view class="user-stat-num">{{ userInfo.experience || 0 }}</view>
|
||||||
|
<view class="user-stat-label">经验</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="user-card login-card" @click="goLogin">
|
||||||
|
<view class="login-illu">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="8" r="4" stroke="#4F46E5" stroke-width="1.8" />
|
||||||
|
<path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8" stroke="#4F46E5" stroke-width="1.8" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<view class="login-text">
|
||||||
|
<view class="login-title">点击登录账号</view>
|
||||||
|
<view class="login-desc">登录后查看作业 · 同步进度</view>
|
||||||
|
</view>
|
||||||
|
<view class="login-arrow"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 主功能入口 -->
|
||||||
|
<view class="section-title">
|
||||||
|
<view class="section-title-bar"></view>
|
||||||
|
<text>开始学习</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grid">
|
||||||
|
<view class="grid-card grid-card-primary" @click="navigate('/pages/homework/index')">
|
||||||
|
<view class="grid-card-decor"></view>
|
||||||
|
<view class="grid-card-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 4h12l4 4v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"
|
||||||
|
stroke="#fff" stroke-width="1.8" stroke-linejoin="round" />
|
||||||
|
<path d="M16 4v4h4M7 13h8M7 17h6" stroke="#fff" stroke-width="1.8" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<view class="grid-card-info">
|
||||||
|
<view class="grid-card-title">我的作业</view>
|
||||||
|
<view class="grid-card-desc">
|
||||||
|
<text v-if="unfinishedCount > 0">{{ unfinishedCount }} 项待完成</text>
|
||||||
|
<text v-else>暂无待完成作业</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="unfinishedCount > 0" class="grid-card-badge">{{ unfinishedCount }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grid-card grid-card-cta" @click="navigate('/pages/homework/history/index')">
|
||||||
|
<view class="grid-card-decor"></view>
|
||||||
|
<view class="grid-card-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="9" stroke="#fff" stroke-width="1.8" />
|
||||||
|
<path d="M12 7v5l3 2" stroke="#fff" stroke-width="1.8" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<view class="grid-card-info">
|
||||||
|
<view class="grid-card-title">作业记录</view>
|
||||||
|
<view class="grid-card-desc">查看历史作业与成绩</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 推荐 / 工具入口 -->
|
||||||
|
<view class="section-title">
|
||||||
|
<view class="section-title-bar"></view>
|
||||||
|
<text>学习工具</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tools">
|
||||||
|
<view class="tool-item" v-for="t in tools" :key="t.id" @click="onToolClick(t)">
|
||||||
|
<view class="tool-icon" :style="{ background: t.color }">
|
||||||
|
<text class="tool-icon-emoji" v-if="t.emoji">{{ t.emoji }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="tool-name">{{ t.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出按钮 -->
|
||||||
|
<view v-if="isLogin" class="logout" @click="onLogout">
|
||||||
|
<text>退出登录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部装饰 -->
|
||||||
|
<view class="footer">
|
||||||
|
<text>· 用心做教育,用爱伴成长 ·</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import { getUnfinishedHomework } from '@/api/homework';
|
||||||
|
import { getCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { userInfo, token, isLogin } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const unfinishedCount = ref(0);
|
||||||
|
|
||||||
|
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
||||||
|
const avatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
|
||||||
|
|
||||||
|
const greeting = computed(() => {
|
||||||
|
const h = new Date().getHours();
|
||||||
|
if (h < 6) return '夜深啦,注意休息';
|
||||||
|
if (h < 11) return '早上好';
|
||||||
|
if (h < 13) return '中午好';
|
||||||
|
if (h < 18) return '下午好';
|
||||||
|
return '晚上好';
|
||||||
|
});
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{ id: 'speak', name: '口算练习', emoji: '🧮', color: 'linear-gradient(135deg,#fde68a 0%,#f59e0b 100%)' },
|
||||||
|
{ id: 'word', name: '英语单词', emoji: '🅰️', color: 'linear-gradient(135deg,#bfdbfe 0%,#3b82f6 100%)' },
|
||||||
|
{ id: 'wrong', name: '错题本', emoji: '📕', color: 'linear-gradient(135deg,#fecaca 0%,#ef4444 100%)' },
|
||||||
|
{ id: 'rank', name: '排行榜', emoji: '🏆', color: 'linear-gradient(135deg,#bbf7d0 0%,#22c55e 100%)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onToolClick = (t: any) => {
|
||||||
|
if (t.id === 'wrong') {
|
||||||
|
navigate('/pages/wrong/index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.showToast({ title: `${t.name} · 即将上线`, icon: 'none' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigate = (url: string) => {
|
||||||
|
if (!token.value) {
|
||||||
|
uni.navigateTo({ url: `/pages/login/index?redirect=${encodeURIComponent(url)}` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goLogin = () => {
|
||||||
|
uni.navigateTo({ url: '/pages/login/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
confirmColor: '#4F46E5',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) userStore.logout();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUnfinished = async () => {
|
||||||
|
if (!token.value) {
|
||||||
|
unfinishedCount.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res: any = await getUnfinishedHomework();
|
||||||
|
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.records) ? res.data.records : [];
|
||||||
|
unfinishedCount.value = list.length || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('fetchUnfinished err', e);
|
||||||
|
unfinishedCount.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (token.value && getCache('role') === 'teacher') {
|
||||||
|
uni.reLaunch({ url: '/pages/teacher/index/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token.value) {
|
||||||
|
userStore.fetchUserInfo().catch(() => { /* ignore */ });
|
||||||
|
fetchUnfinished();
|
||||||
|
} else {
|
||||||
|
unfinishedCount.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.home {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #eef2ff 0%, #f5f7ff 50%, #ffffff 100%);
|
||||||
|
padding: 0 32rpx 60rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 装饰云朵 */
|
||||||
|
.decor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(24rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
&.decor-1 {
|
||||||
|
top: 60rpx;
|
||||||
|
right: -60rpx;
|
||||||
|
width: 280rpx;
|
||||||
|
height: 280rpx;
|
||||||
|
background: radial-gradient(circle, rgba(129, 140, 248, 0.5), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.decor-2 {
|
||||||
|
top: 320rpx;
|
||||||
|
left: -80rpx;
|
||||||
|
width: 240rpx;
|
||||||
|
height: 240rpx;
|
||||||
|
background: radial-gradient(circle, rgba(34, 197, 94, 0.35), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.decor-3 {
|
||||||
|
bottom: 120rpx;
|
||||||
|
right: -100rpx;
|
||||||
|
width: 320rpx;
|
||||||
|
height: 320rpx;
|
||||||
|
background: radial-gradient(circle, rgba(199, 210, 254, 0.6), transparent 70%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 32rpx 0 24rpx;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-greet {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-hello {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
|
||||||
|
.hero-title-ai {
|
||||||
|
margin-left: 12rpx;
|
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #22c55e 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 吉祥物 */
|
||||||
|
.hero-mascot {
|
||||||
|
position: relative;
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot-blob {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
border-radius: 50% 50% 48% 52% / 56% 50% 50% 44%;
|
||||||
|
box-shadow:
|
||||||
|
0 16rpx 32rpx rgba(79, 70, 229, 0.35),
|
||||||
|
inset 0 -10rpx 0 rgba(0, 0, 0, 0.08),
|
||||||
|
inset 0 8rpx 0 rgba(255, 255, 255, 0.25);
|
||||||
|
animation: blob 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot-face {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot-eye {
|
||||||
|
position: absolute;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 56rpx;
|
||||||
|
|
||||||
|
&.left { left: 48rpx; }
|
||||||
|
&.right { right: 48rpx; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot-smile {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50rpx;
|
||||||
|
width: 50rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border: 4rpx solid #fff;
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 0 0 50rpx 50rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blob {
|
||||||
|
0%, 100% { border-radius: 50% 50% 48% 52% / 56% 50% 50% 44%; }
|
||||||
|
50% { border-radius: 48% 52% 56% 44% / 50% 56% 44% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户卡 */
|
||||||
|
.user-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28rpx 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
gap: 24rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 16rpx 40rpx rgba(99, 102, 241, 0.16),
|
||||||
|
inset 0 -6rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
border: 4rpx solid #ffffff;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba(99, 102, 241, 0.25);
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background: linear-gradient(135deg, #fde68a 0%, #f59e0b 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
box-shadow: 0 4rpx 8rpx rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #312e81;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-school {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
color: #818cf8;
|
||||||
|
font-size: 22rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ic {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stat {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&-num {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stat-divider {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
background: #e0e7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡 */
|
||||||
|
.user-card.login-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-illu {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: #eef2ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-desc {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-arrow {
|
||||||
|
width: 18rpx;
|
||||||
|
height: 18rpx;
|
||||||
|
border-right: 4rpx solid #4f46e5;
|
||||||
|
border-top: 4rpx solid #4f46e5;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section title */
|
||||||
|
.section-title {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin: 48rpx 0 24rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-bar {
|
||||||
|
width: 8rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
background: linear-gradient(180deg, #4f46e5 0%, #22c55e 100%);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主功能 grid */
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
height: 240rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 16rpx 40rpx rgba(99, 102, 241, 0.2),
|
||||||
|
inset 0 -8rpx 0 rgba(0, 0, 0, 0.06),
|
||||||
|
inset 0 4rpx 0 rgba(255, 255, 255, 0.18);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(4rpx) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grid-card-primary {
|
||||||
|
background: linear-gradient(135deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grid-card-cta {
|
||||||
|
background: linear-gradient(135deg, #4ade80 0%, #16a34a 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-decor {
|
||||||
|
position: absolute;
|
||||||
|
top: -40rpx;
|
||||||
|
right: -40rpx;
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(8rpx);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-info {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-desc {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-card-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
right: 24rpx;
|
||||||
|
min-width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
padding: 0 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具栏 */
|
||||||
|
.tools {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 16rpx 40rpx rgba(99, 102, 241, 0.12),
|
||||||
|
inset 0 -6rpx 0 rgba(199, 210, 254, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx 0;
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(0, 0, 0, 0.1),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon-emoji {
|
||||||
|
font-size: 44rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-name {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出 */
|
||||||
|
.logout {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin: 64rpx auto 0;
|
||||||
|
width: 320rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 16rpx rgba(239, 68, 68, 0.12),
|
||||||
|
inset 0 -4rpx 0 rgba(254, 202, 202, 0.5);
|
||||||
|
transition: transform 150ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
color: #c7d2fe;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
550
src/pages/login/index.vue
Normal file
550
src/pages/login/index.vue
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login">
|
||||||
|
<!-- 装饰背景 -->
|
||||||
|
<view class="bg-blob blob-a"></view>
|
||||||
|
<view class="bg-blob blob-b"></view>
|
||||||
|
<view class="bg-blob blob-c"></view>
|
||||||
|
|
||||||
|
<!-- 主内容 -->
|
||||||
|
<view class="content">
|
||||||
|
<!-- 品牌区 -->
|
||||||
|
<view class="brand">
|
||||||
|
<view class="brand-logo" @click="onLogoTap">
|
||||||
|
<view class="brand-logo-glow"></view>
|
||||||
|
<image :src="logoUrl" mode="aspectFit" class="brand-logo-img" />
|
||||||
|
</view>
|
||||||
|
<view class="brand-name">学小乐<text class="brand-name-ai">AI</text></view>
|
||||||
|
<view class="brand-slogan">智能学习助手 · 让学习更轻松</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 角色切换 -->
|
||||||
|
<view class="role-switch">
|
||||||
|
<view class="role-switch-bg" :class="role"></view>
|
||||||
|
<view
|
||||||
|
v-for="item in roleOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:class="['role-tab', { active: role === item.value }]"
|
||||||
|
@click="role = item.value"
|
||||||
|
>
|
||||||
|
<text>{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表单 -->
|
||||||
|
<view class="form">
|
||||||
|
<view class="form-item" :class="{ focus: focus === 'account' }">
|
||||||
|
<view class="form-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="8" r="4" stroke="#4F46E5" stroke-width="1.6" />
|
||||||
|
<path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8" stroke="#4F46E5" stroke-width="1.6"
|
||||||
|
stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
v-model="account"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入账号"
|
||||||
|
placeholder-class="form-placeholder"
|
||||||
|
:adjust-position="false"
|
||||||
|
@focus="focus = 'account'"
|
||||||
|
@blur="focus = ''"
|
||||||
|
@confirm="onLogin"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item" :class="{ focus: focus === 'password' }">
|
||||||
|
<view class="form-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="4" y="11" width="16" height="9" rx="2" stroke="#4F46E5" stroke-width="1.6" />
|
||||||
|
<path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="#4F46E5" stroke-width="1.6" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
class="form-input"
|
||||||
|
:password="hidePassword"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
placeholder-class="form-placeholder"
|
||||||
|
:adjust-position="false"
|
||||||
|
@focus="focus = 'password'"
|
||||||
|
@blur="focus = ''"
|
||||||
|
@confirm="onLogin"
|
||||||
|
/>
|
||||||
|
<view class="form-eye" @click="hidePassword = !hidePassword">
|
||||||
|
<svg v-if="hidePassword" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="m2 2 20 20M10 5c.7-.1 1.3-.1 2-.1 7 0 10 7 10 7-.6 1.4-1.5 2.6-2.5 3.6M6.6 6.6C3.5 8.6 2 12 2 12s3 7 10 7c1.4 0 2.6-.3 3.7-.7"
|
||||||
|
stroke="#94A3B8" stroke-width="1.6" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z" stroke="#4F46E5"
|
||||||
|
stroke-width="1.6" />
|
||||||
|
<circle cx="12" cy="12" r="3" stroke="#4F46E5" stroke-width="1.6" />
|
||||||
|
</svg>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="agreement">
|
||||||
|
<text>登录即表示同意</text>
|
||||||
|
<text class="link">《用户协议》</text>
|
||||||
|
<text>与</text>
|
||||||
|
<text class="link">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<view :class="['btn-login', { loading: submitting }]" @click="onLogin">
|
||||||
|
<text v-if="!submitting">立 即 登 录</text>
|
||||||
|
<view v-else class="btn-loader">
|
||||||
|
<view class="btn-loader-dot"></view>
|
||||||
|
<view class="btn-loader-dot"></view>
|
||||||
|
<view class="btn-loader-dot"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hint">
|
||||||
|
<text>登录账号由学校老师统一发放</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import { useTeacherStore } from '@/state/modules/teacher';
|
||||||
|
import { setCache, getCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
||||||
|
const logoUrl = `${OSS_URL}/urm/logo.png`;
|
||||||
|
|
||||||
|
const account = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const hidePassword = ref(true);
|
||||||
|
const redirect = ref('');
|
||||||
|
const submitting = ref(false);
|
||||||
|
const focus = ref<string>('');
|
||||||
|
type LoginRole = 'student' | 'teacher' | 'parent';
|
||||||
|
const role = ref<LoginRole>('student');
|
||||||
|
const enterDebug = ref(0);
|
||||||
|
const roleOptions: { value: LoginRole; label: string }[] = [
|
||||||
|
{ value: 'student', label: '学生' },
|
||||||
|
{ value: 'teacher', label: '老师' },
|
||||||
|
{ value: 'parent', label: '家长' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const teacherStore = useTeacherStore();
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
const cachedRole = getCache('role');
|
||||||
|
if (cachedRole === 'teacher' || cachedRole === 'student' || cachedRole === 'parent') {
|
||||||
|
role.value = cachedRole;
|
||||||
|
}
|
||||||
|
if (options?.redirect) {
|
||||||
|
redirect.value = decodeURIComponent(options.redirect);
|
||||||
|
}
|
||||||
|
if (options?.a) {
|
||||||
|
account.value = options.a;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLogoTap = () => {
|
||||||
|
enterDebug.value++;
|
||||||
|
if (enterDebug.value >= 8) {
|
||||||
|
enterDebug.value = 0;
|
||||||
|
uni.showToast({ title: 'Debug 模式', icon: 'none' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLoginTargetUrl = () => {
|
||||||
|
const teacherHome = '/pages/teacher/index/index';
|
||||||
|
const parentHome = '/pages/parent/home/index';
|
||||||
|
const studentHome = '/pages/index/index';
|
||||||
|
const value = redirect.value.trim();
|
||||||
|
|
||||||
|
if (role.value === 'teacher') {
|
||||||
|
if (!value) return teacherHome;
|
||||||
|
if (value === 'teacher-home' || value === '/pages/teacher') return teacherHome;
|
||||||
|
return value.startsWith('/pages/teacher/') ? value : teacherHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.value === 'parent') {
|
||||||
|
if (!value) return parentHome;
|
||||||
|
if (value === 'parent-home' || value === '/pages/parent') return parentHome;
|
||||||
|
return value.startsWith('/pages/parent/') ? value : parentHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) return studentHome;
|
||||||
|
if (value === 'home') return studentHome;
|
||||||
|
return value.startsWith('/pages/teacher') || value.startsWith('/pages/parent') ? studentHome : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLogin = async () => {
|
||||||
|
if (submitting.value) return;
|
||||||
|
if (role.value === 'parent') {
|
||||||
|
setCache('role', 'parent');
|
||||||
|
uni.reLaunch({ url: '/pages/parent/home/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!account.value.trim()) {
|
||||||
|
uni.showToast({ title: '请输入账号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password.value) {
|
||||||
|
uni.showToast({ title: '请输入密码', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
uni.showLoading({ title: '登录中...', mask: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data: any = {
|
||||||
|
account: account.value.trim(),
|
||||||
|
password: password.value,
|
||||||
|
};
|
||||||
|
if (role.value === 'teacher') {
|
||||||
|
data.clientType = 'MANAGE';
|
||||||
|
}
|
||||||
|
await userStore.accountLogin(data);
|
||||||
|
setCache('role', role.value);
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (role.value === 'teacher') {
|
||||||
|
try {
|
||||||
|
await teacherStore.getLoginUser();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('teacher.getLoginUser err', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const targetUrl = getLoginTargetUrl();
|
||||||
|
if (
|
||||||
|
targetUrl === '/pages/index/index'
|
||||||
|
|| targetUrl === '/pages/teacher/index/index'
|
||||||
|
|| targetUrl === '/pages/parent/home/index'
|
||||||
|
) {
|
||||||
|
uni.reLaunch({ url: targetUrl });
|
||||||
|
} else {
|
||||||
|
uni.redirectTo({ url: targetUrl });
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: e?.message || '登录失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #eef2ff 0%, #c7d2fe 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 56rpx 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景装饰 */
|
||||||
|
.bg-blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(40rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-a {
|
||||||
|
top: -120rpx;
|
||||||
|
right: -120rpx;
|
||||||
|
width: 400rpx;
|
||||||
|
height: 400rpx;
|
||||||
|
background: radial-gradient(circle, rgba(129, 140, 248, 0.7), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-b {
|
||||||
|
top: 280rpx;
|
||||||
|
left: -160rpx;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
background: radial-gradient(circle, rgba(34, 197, 94, 0.4), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-c {
|
||||||
|
bottom: -80rpx;
|
||||||
|
right: -80rpx;
|
||||||
|
width: 380rpx;
|
||||||
|
height: 380rpx;
|
||||||
|
background: radial-gradient(circle, rgba(254, 215, 170, 0.5), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主体 */
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding-top: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 品牌 */
|
||||||
|
.brand {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
position: relative;
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
margin: 0 auto 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo-glow {
|
||||||
|
position: absolute;
|
||||||
|
inset: -16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.9), transparent 70%);
|
||||||
|
filter: blur(8rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo-img {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 32rpx rgba(99, 102, 241, 0.3),
|
||||||
|
inset 0 -6rpx 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
|
||||||
|
.brand-name-ai {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #22c55e 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-slogan {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 角色切换 */
|
||||||
|
.role-switch {
|
||||||
|
position: relative;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
box-shadow: inset 0 4rpx 8rpx rgba(99, 102, 241, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-switch-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
left: 8rpx;
|
||||||
|
width: calc(33.333% - 8rpx);
|
||||||
|
height: calc(100% - 16rpx);
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(79, 70, 229, 0.35),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
&.teacher {
|
||||||
|
transform: translateX(100%);
|
||||||
|
background: linear-gradient(180deg, #4ade80 0%, #16a34a 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(22, 163, 74, 0.35),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.parent {
|
||||||
|
transform: translateX(200%);
|
||||||
|
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 6rpx 14rpx rgba(8, 145, 178, 0.35),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-tab {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
transition: color 200ms ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单 */
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 104rpx;
|
||||||
|
padding: 0 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
transition: box-shadow 200ms ease, transform 150ms ease;
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 20rpx rgba(99, 102, 241, 0.1),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
|
||||||
|
&.focus {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 4rpx rgba(129, 140, 248, 0.4),
|
||||||
|
0 8rpx 24rpx rgba(99, 102, 241, 0.18),
|
||||||
|
inset 0 -4rpx 0 rgba(199, 210, 254, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-icon {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #312e81;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-eye {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 协议 */
|
||||||
|
.agreement {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.btn-login {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow:
|
||||||
|
0 12rpx 28rpx rgba(79, 70, 229, 0.4),
|
||||||
|
inset 0 -6rpx 0 rgba(0, 0, 0, 0.15),
|
||||||
|
inset 0 4rpx 0 rgba(255, 255, 255, 0.2);
|
||||||
|
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(4rpx);
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 12rpx rgba(79, 70, 229, 0.4),
|
||||||
|
inset 0 -3rpx 0 rgba(0, 0, 0, 0.15),
|
||||||
|
inset 0 4rpx 0 rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-loader {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
&-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out both;
|
||||||
|
|
||||||
|
&:nth-child(1) { animation-delay: -0.32s; }
|
||||||
|
&:nth-child(2) { animation-delay: -0.16s; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: scale(0.5); }
|
||||||
|
40% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
488
src/pages/parent/analysis/index.vue
Normal file
488
src/pages/parent/analysis/index.vue
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<template>
|
||||||
|
<ParentPage active="reports" :show-tab="false" :use-scroll-view="false">
|
||||||
|
<view class="filter-row">
|
||||||
|
<picker class="filter-picker" :range="subjectOptions" :value="subjectIndex" @change="onSubjectChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ subjectOptions[subjectIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="filter-picker" :range="gradeOptions" :value="gradeIndex" @change="onGradeChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ gradeOptions[gradeIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="filter-picker" :range="categoryOptions" :value="categoryIndex" @change="onCategoryChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ categoryOptions[categoryIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="module-section">
|
||||||
|
<ParentCard title="错题知识点占比" subtitle="按错题占比识别最近需要优先补强的知识点">
|
||||||
|
<view v-if="filteredKnowledge.length" class="ratio-layout">
|
||||||
|
<view class="ratio-main">
|
||||||
|
<view class="ratio-total">
|
||||||
|
<text class="ratio-number">{{ mainWrongPercent }}%</text>
|
||||||
|
<text class="ratio-label">最高错题占比</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-for="item in filteredKnowledge"
|
||||||
|
:key="`bar-${item.name}`"
|
||||||
|
class="ratio-bar"
|
||||||
|
>
|
||||||
|
<view class="ratio-fill" :class="item.tone" :style="{ width: `${item.wrongPercent}%` }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="legend-list">
|
||||||
|
<view v-for="item in filteredKnowledge" :key="item.name" class="legend-item">
|
||||||
|
<view class="legend-dot" :class="item.tone"></view>
|
||||||
|
<text class="legend-name ellipsis">{{ item.name }}</text>
|
||||||
|
<text class="legend-percent">{{ item.wrongPercent }}%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<xy-Empty v-else type="task" content="暂无符合条件的知识点数据" />
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="module-section">
|
||||||
|
<ParentCard title="知识点得分" subtitle="可切换得分等级,快速定位当前薄弱段">
|
||||||
|
<view class="level-tabs">
|
||||||
|
<view
|
||||||
|
v-for="level in scoreLevels"
|
||||||
|
:key="level.key"
|
||||||
|
class="level-tab"
|
||||||
|
:class="{ active: activeLevel === level.key }"
|
||||||
|
@click="activeLevel = level.key"
|
||||||
|
>
|
||||||
|
{{ level.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="levelKnowledge.length" class="score-table">
|
||||||
|
<view class="score-row table-head">
|
||||||
|
<text>知识点</text>
|
||||||
|
<text>满分</text>
|
||||||
|
<text>得分</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in levelKnowledge" :key="`${item.name}-${activeLevel}`" class="score-row">
|
||||||
|
<text class="knowledge-name ellipsis">{{ item.name }}</text>
|
||||||
|
<text>{{ item.fullScore }}</text>
|
||||||
|
<text class="score-value">{{ item.score }}<text class="score-rate">({{ scoreRate(item) }}%)</text></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<xy-Empty v-else type="learn" content="当前等级暂无知识点" />
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="module-section">
|
||||||
|
<ParentCard title="得分能力" subtitle="综合最近多次练习得分,观察能力变化和稳定性">
|
||||||
|
<view class="ucharts-wrap">
|
||||||
|
<qiun-data-charts
|
||||||
|
class="ability-chart"
|
||||||
|
type="line"
|
||||||
|
canvasId="parentAbilityTrendCanvas2dLineChart"
|
||||||
|
background="rgba(255,255,255,0)"
|
||||||
|
:canvas2d="true"
|
||||||
|
:in-scroll-view="false"
|
||||||
|
:ontouch="false"
|
||||||
|
:disable-scroll="false"
|
||||||
|
:reshow="chartReshow"
|
||||||
|
:opts="abilityChartOpts"
|
||||||
|
:chartData="abilityChartData"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="summary-grid">
|
||||||
|
<view v-for="item in abilitySummary" :key="item.label" class="summary-item">
|
||||||
|
<text class="summary-label">{{ item.label }}</text>
|
||||||
|
<text class="summary-value">{{ item.value }}</text>
|
||||||
|
<text class="summary-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
</ParentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onReady, onShow } from '@dcloudio/uni-app';
|
||||||
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
|
import {
|
||||||
|
abilitySummary,
|
||||||
|
abilityTrend,
|
||||||
|
categoryOptions,
|
||||||
|
gradeOptions,
|
||||||
|
knowledgeWeakPoints,
|
||||||
|
subjectOptions,
|
||||||
|
type KnowledgeWeakPoint,
|
||||||
|
} from '../utils/data';
|
||||||
|
|
||||||
|
type ScoreLevel = 'weak' | 'pass' | 'good' | 'excellent';
|
||||||
|
|
||||||
|
const subjectIndex = ref(0);
|
||||||
|
const gradeIndex = ref(1);
|
||||||
|
const categoryIndex = ref(0);
|
||||||
|
const activeLevel = ref<ScoreLevel>('weak');
|
||||||
|
|
||||||
|
const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
||||||
|
{ key: 'weak', label: '薄弱' },
|
||||||
|
{ key: 'pass', label: '合格' },
|
||||||
|
{ key: 'good', label: '良好' },
|
||||||
|
{ key: 'excellent', label: '优秀' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredKnowledge = computed(() => {
|
||||||
|
const subject = subjectOptions[subjectIndex.value];
|
||||||
|
const grade = gradeOptions[gradeIndex.value];
|
||||||
|
const category = categoryOptions[categoryIndex.value];
|
||||||
|
return knowledgeWeakPoints.filter((item) => {
|
||||||
|
const subjectMatched = subject === '全部学科' || item.subject === subject;
|
||||||
|
const gradeMatched = grade === '全部年级' || item.grade === grade;
|
||||||
|
const categoryMatched = category === '全部类型' || item.category === category;
|
||||||
|
return subjectMatched && gradeMatched && categoryMatched;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainWrongPercent = computed(() => Math.max(...filteredKnowledge.value.map((item) => item.wrongPercent), 0));
|
||||||
|
|
||||||
|
const scoreRate = (item: KnowledgeWeakPoint) => Math.round((item.score / item.fullScore) * 100);
|
||||||
|
|
||||||
|
const levelOf = (item: KnowledgeWeakPoint): ScoreLevel => {
|
||||||
|
const rate = scoreRate(item);
|
||||||
|
if (rate < 60) return 'weak';
|
||||||
|
if (rate < 75) return 'pass';
|
||||||
|
if (rate < 90) return 'good';
|
||||||
|
return 'excellent';
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelKnowledge = computed(() => filteredKnowledge.value.filter((item) => levelOf(item) === activeLevel.value));
|
||||||
|
|
||||||
|
const abilityChartData = ref({});
|
||||||
|
const chartReshow = ref(false);
|
||||||
|
|
||||||
|
const buildAbilityChartData = () => ({
|
||||||
|
categories: abilityTrend.map((item) => item.date),
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '得分能力',
|
||||||
|
data: abilityTrend.map((item) => item.score),
|
||||||
|
color: '#0891b2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fillChart = () => {
|
||||||
|
abilityChartData.value = JSON.parse(JSON.stringify(buildAbilityChartData()));
|
||||||
|
chartReshow.value = !chartReshow.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
onReady(() => {
|
||||||
|
setTimeout(fillChart, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (Object.keys(abilityChartData.value).length) {
|
||||||
|
chartReshow.value = !chartReshow.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const abilityChartOpts = {
|
||||||
|
color: ['#0891b2'],
|
||||||
|
padding: [18, 12, 0, 12],
|
||||||
|
enableScroll: false,
|
||||||
|
legend: { show: false },
|
||||||
|
xAxis: {
|
||||||
|
disableGrid: false,
|
||||||
|
gridColor: '#eef2f7',
|
||||||
|
fontColor: '#64748b',
|
||||||
|
fontSize: 10,
|
||||||
|
boundaryGap: 'justify',
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
gridType: 'dash',
|
||||||
|
dashLength: 2,
|
||||||
|
gridColor: '#eef2f7',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
min: 40,
|
||||||
|
max: 100,
|
||||||
|
splitNumber: 4,
|
||||||
|
fontColor: '#94a3b8',
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
line: {
|
||||||
|
type: 'curve',
|
||||||
|
width: 3,
|
||||||
|
activeType: 'hollow',
|
||||||
|
},
|
||||||
|
markLine: {
|
||||||
|
type: 'dash',
|
||||||
|
dashLength: 6,
|
||||||
|
data: [
|
||||||
|
{ value: 85, lineColor: 'rgba(244, 63, 94, 0.32)', showLabel: true, labelText: '优秀' },
|
||||||
|
{ value: 70, lineColor: 'rgba(244, 63, 94, 0.28)', showLabel: true, labelText: '良好' },
|
||||||
|
{ value: 60, lineColor: 'rgba(244, 63, 94, 0.22)', showLabel: true, labelText: '一般' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubjectChange = (e: any) => { subjectIndex.value = Number(e.detail.value); };
|
||||||
|
const onGradeChange = (e: any) => { gradeIndex.value = Number(e.detail.value); };
|
||||||
|
const onCategoryChange = (e: any) => { categoryIndex.value = Number(e.detail.value); };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-picker {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill {
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 22rpx rgba(8, 145, 178, 0.08),
|
||||||
|
inset 0 -2rpx 0 rgba(207, 250, 254, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8rpx solid transparent;
|
||||||
|
border-right: 8rpx solid transparent;
|
||||||
|
border-top: 10rpx solid #0891b2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-section {
|
||||||
|
margin-top: 44rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row + .module-section {
|
||||||
|
margin-top: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-main {
|
||||||
|
width: 232rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-total {
|
||||||
|
min-height: 116rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(180deg, #ecfeff 0%, #ffffff 100%);
|
||||||
|
box-shadow: inset 0 -2rpx 0 rgba(34, 211, 238, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-number {
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-label {
|
||||||
|
margin-top: 2rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-bar {
|
||||||
|
height: 16rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
|
||||||
|
&.blue { background: #3b82f6; }
|
||||||
|
&.orange { background: #f97316; }
|
||||||
|
&.green { background: #22c55e; }
|
||||||
|
&.rose { background: #f43f5e; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-list {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
min-height: 46rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.blue { background: #3b82f6; }
|
||||||
|
&.orange { background: #f97316; }
|
||||||
|
&.green { background: #22c55e; }
|
||||||
|
&.rose { background: #f43f5e; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-percent {
|
||||||
|
color: #0891b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tab {
|
||||||
|
height: 64rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #475569;
|
||||||
|
background: #ffffff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #0891b2;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
||||||
|
box-shadow: 0 10rpx 20rpx rgba(8, 145, 178, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-table {
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 112rpx 150rpx;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 76rpx;
|
||||||
|
padding: 0 22rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 2rpx solid #f1f5f9;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-head {
|
||||||
|
color: #475569;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-name {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
color: #ea580c;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-rate {
|
||||||
|
margin-left: 2rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ucharts-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 360rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label,
|
||||||
|
.summary-desc {
|
||||||
|
display: block;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
display: block;
|
||||||
|
margin: 8rpx 0;
|
||||||
|
color: #0891b2;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
src/pages/parent/components/ParentCard.vue
Normal file
69
src/pages/parent/components/ParentCard.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<view class="parent-card" :class="{ clickable }" @click="emit('click')">
|
||||||
|
<view v-if="title || subtitle" class="card-head">
|
||||||
|
<view>
|
||||||
|
<text v-if="title" class="card-title">{{ title }}</text>
|
||||||
|
<text v-if="subtitle" class="card-subtitle">{{ subtitle }}</text>
|
||||||
|
</view>
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
</view>
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
clickable?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
subtitle: '',
|
||||||
|
clickable: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 点击事件由卡片统一透出,页面里可直接把整张卡作为操作入口。
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'click'): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.parent-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
box-shadow:
|
||||||
|
0 14rpx 36rpx rgba(8, 145, 178, 0.08),
|
||||||
|
inset 0 -3rpx 0 rgba(207, 250, 254, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable:active {
|
||||||
|
transform: translateY(2rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
src/pages/parent/components/ParentPage.vue
Normal file
92
src/pages/parent/components/ParentPage.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<view class="parent-page" :class="{ 'native-scroll': !useScrollView }">
|
||||||
|
<view class="decor decor-a"></view>
|
||||||
|
<view class="decor decor-b"></view>
|
||||||
|
<scroll-view v-if="useScrollView" class="page-scroll" scroll-y enhanced :show-scrollbar="false">
|
||||||
|
<view class="page-content" :class="{ 'with-tab': showTab }">
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view v-else class="page-scroll native-page-scroll">
|
||||||
|
<view class="page-content" :class="{ 'with-tab': showTab }">
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<ParentTabBar v-if="showTab" :active="active" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ParentTabBar from './ParentTabBar.vue';
|
||||||
|
|
||||||
|
// 家长端页面统一背景、滚动区域和底部导航,业务页只关注内容块。
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
active?: 'home' | 'reports' | 'profile';
|
||||||
|
showTab?: boolean;
|
||||||
|
useScrollView?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
active: 'home',
|
||||||
|
showTab: true,
|
||||||
|
useScrollView: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.parent-page {
|
||||||
|
position: relative;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(180deg, #ecfeff 0%, #f5f7ff 54%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-scroll {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(64rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decor-a {
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
top: 90rpx;
|
||||||
|
right: -150rpx;
|
||||||
|
background: rgba(34, 211, 238, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.decor-b {
|
||||||
|
width: 300rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
left: -120rpx;
|
||||||
|
bottom: 170rpx;
|
||||||
|
background: rgba(34, 197, 94, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-page-scroll {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 24rpx 28rpx 0;
|
||||||
|
|
||||||
|
&.with-tab {
|
||||||
|
padding-bottom: calc(150rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
89
src/pages/parent/components/ParentTabBar.vue
Normal file
89
src/pages/parent/components/ParentTabBar.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<view class="parent-tab">
|
||||||
|
<view
|
||||||
|
v-for="item in tabs"
|
||||||
|
:key="item.key"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: item.key === props.active }"
|
||||||
|
@click="goPage(item)"
|
||||||
|
>
|
||||||
|
<view class="tab-icon">
|
||||||
|
<text>{{ item.icon }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="tab-label">{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type TabKey = 'home' | 'reports' | 'profile';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
active: TabKey;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tabs: { key: TabKey; label: string; icon: string; url: string }[] = [
|
||||||
|
{ key: 'home', label: '首页', icon: '首', url: '/pages/parent/home/index' },
|
||||||
|
{ key: 'reports', label: '考试报告', icon: '报', url: '/pages/parent/reports/index' },
|
||||||
|
{ key: 'profile', label: '个人中心', icon: '我', url: '/pages/parent/profile/index' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 分包内页面模拟 tab 切换,用 redirectTo 避免重复堆叠页面栈。
|
||||||
|
const goPage = (item: (typeof tabs)[number]) => {
|
||||||
|
if (item.key === props.active) return;
|
||||||
|
uni.redirectTo({ url: item.url });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.parent-tab {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
height: calc(112rpx + env(safe-area-inset-bottom));
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border-top: 2rpx solid rgba(14, 116, 144, 0.08);
|
||||||
|
box-shadow: 0 -12rpx 36rpx rgba(15, 118, 110, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #0891b2;
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
||||||
|
box-shadow: 0 8rpx 18rpx rgba(8, 145, 178, 0.28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
304
src/pages/parent/home/index.vue
Normal file
304
src/pages/parent/home/index.vue
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<ParentPage active="home">
|
||||||
|
<view class="profile">
|
||||||
|
<view class="avatar">{{ activeChild.name.slice(0, 1) }}</view>
|
||||||
|
<view class="profile-main">
|
||||||
|
<view class="name-row">
|
||||||
|
<text class="child-name">{{ activeChild.name }}家长</text>
|
||||||
|
<text class="vip-tag">家长会员</text>
|
||||||
|
</view>
|
||||||
|
<text class="school ellipsis">{{ activeChild.school }} · {{ activeChild.className }}</text>
|
||||||
|
<text class="exam-no">考号:{{ activeChild.examNo }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grade-card">
|
||||||
|
<ParentCard>
|
||||||
|
<view v-for="band in gradeBands" :key="band.level" class="band-row">
|
||||||
|
<view class="band-badge" :class="band.level">{{ band.label.slice(0, 2) }}</view>
|
||||||
|
<view v-if="band.subjects.length" class="subject-list">
|
||||||
|
<view v-for="item in band.subjects" :key="`${band.level}-${item.subject}`" class="subject-chip">
|
||||||
|
<text>{{ item.subject }}</text>
|
||||||
|
<text class="trend" :class="item.status">{{ trendText(item.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-else class="empty-text">暂无需要关注的学科</text>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="member-card">
|
||||||
|
<ParentCard clickable @click="showComingSoon('家长会员')">
|
||||||
|
<view class="member-tag">家长会员</view>
|
||||||
|
<view class="member-main">
|
||||||
|
<view>
|
||||||
|
<text class="member-title">心理关怀 · 习惯规划 · 深度学情</text>
|
||||||
|
<text class="member-desc">不只看分数,陪孩子建立稳定学习节奏。</text>
|
||||||
|
</view>
|
||||||
|
<text class="member-arrow">查看</text>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="read-card">
|
||||||
|
<ParentCard title="本周家长必读" subtitle="系统按孩子最近表现生成关注建议">
|
||||||
|
<text class="read-text">数学周练后情绪略低,建议先肯定一次认真订正,再安排一条免费话术在「家长关怀」里。</text>
|
||||||
|
<view class="read-link" @click="showComingSoon('家长关怀')">进入家长关怀中心</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="quick-grid">
|
||||||
|
<ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)">
|
||||||
|
<view class="quick-icon" :class="item.tone">{{ item.title.slice(0, 1) }}</view>
|
||||||
|
<text class="quick-title">{{ item.title }}</text>
|
||||||
|
<text class="quick-desc">{{ item.desc }}</text>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
</ParentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
|
import { gradeBands, parentChildren, quickActions, type QuickAction, type SubjectTrend } from '../utils/data';
|
||||||
|
|
||||||
|
const activeChild = parentChildren[0];
|
||||||
|
|
||||||
|
const trendText = (status: SubjectTrend['status']) => {
|
||||||
|
const map: Record<SubjectTrend['status'], string> = {
|
||||||
|
up: '进步',
|
||||||
|
down: '退步',
|
||||||
|
stable: '稳定',
|
||||||
|
};
|
||||||
|
return map[status];
|
||||||
|
};
|
||||||
|
|
||||||
|
const showComingSoon = (name: string) => {
|
||||||
|
uni.showToast({
|
||||||
|
title: `${name}建设中`,
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickAction = (item: QuickAction) => {
|
||||||
|
if (item.title === '学情追踪') {
|
||||||
|
uni.navigateTo({ url: '/pages/parent/analysis/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.title === '错题练习') {
|
||||||
|
uni.navigateTo({ url: '/pages/wrong/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showComingSoon(item.title);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 22rpx;
|
||||||
|
padding: 30rpx 6rpx 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 108rpx;
|
||||||
|
height: 108rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
||||||
|
box-shadow: 0 12rpx 28rpx rgba(8, 145, 178, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-name {
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-tag,
|
||||||
|
.member-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #92400e;
|
||||||
|
background: #fef3c7;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school,
|
||||||
|
.exam-no {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-card,
|
||||||
|
.member-card,
|
||||||
|
.read-card {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18rpx;
|
||||||
|
min-height: 70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-row + .band-row {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-badge {
|
||||||
|
width: 58rpx;
|
||||||
|
height: 58rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
|
||||||
|
&.excellent { background: #22c55e; }
|
||||||
|
&.good { background: #3b82f6; }
|
||||||
|
&.middle { background: #f59e0b; }
|
||||||
|
&.weak { background: #ef4444; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #164e63;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend {
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 18rpx;
|
||||||
|
|
||||||
|
&.up { background: #22c55e; }
|
||||||
|
&.down { background: #f43f5e; }
|
||||||
|
&.stable { background: #0891b2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(236, 254, 255, 0.94));
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-title {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-desc,
|
||||||
|
.read-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-arrow,
|
||||||
|
.read-link {
|
||||||
|
color: #0891b2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-link {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-card {
|
||||||
|
padding: 24rpx 14rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin: 0 auto 14rpx;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
|
||||||
|
&.cyan { background: #22d3ee; }
|
||||||
|
&.green { background: #22c55e; }
|
||||||
|
&.orange { background: #f59e0b; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-title {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
217
src/pages/parent/profile/index.vue
Normal file
217
src/pages/parent/profile/index.vue
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<ParentPage active="profile">
|
||||||
|
<view class="account-card">
|
||||||
|
<ParentCard>
|
||||||
|
<view class="account-row">
|
||||||
|
<view class="account-avatar">家</view>
|
||||||
|
<view class="account-main">
|
||||||
|
<text class="phone">138****4546</text>
|
||||||
|
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="children-card">
|
||||||
|
<ParentCard title="孩子管理" subtitle="切换后首页与报告将默认展示该孩子">
|
||||||
|
<view class="child-grid">
|
||||||
|
<view
|
||||||
|
v-for="(child, index) in parentChildren"
|
||||||
|
:key="child.id"
|
||||||
|
class="child-card"
|
||||||
|
:class="{ active: index === activeChildIndex }"
|
||||||
|
@click="selectChild(index)"
|
||||||
|
>
|
||||||
|
<text class="child-name">{{ child.name }}</text>
|
||||||
|
<text class="child-account">{{ child.account }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="bind-btn" @click="bindChild">+ 绑定孩子账号</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-card">
|
||||||
|
<ParentCard>
|
||||||
|
<view class="menu-item" @click="showAbout">
|
||||||
|
<text>关于我们</text>
|
||||||
|
<text class="menu-arrow">查看</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-line"></view>
|
||||||
|
<view class="menu-item danger" @click="handleLogout">
|
||||||
|
<text>退出登录</text>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
</ParentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
|
import { parentChildren } from '../utils/data';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const activeChildIndex = ref(0);
|
||||||
|
const activeChild = computed(() => parentChildren[activeChildIndex.value]);
|
||||||
|
|
||||||
|
const selectChild = (index: number) => {
|
||||||
|
activeChildIndex.value = index;
|
||||||
|
uni.showToast({
|
||||||
|
title: `已切换到${parentChildren[index].name}`,
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindChild = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '绑定功能建设中',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAbout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '关于学小乐AI',
|
||||||
|
content: '学小乐AI为学生、老师和家长提供学习过程分析与成长陪伴。',
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定退出家长端吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
await userStore.logout();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.account-card {
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-avatar {
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 38rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
background: linear-gradient(180deg, #67e8f9 0%, #0891b2 100%);
|
||||||
|
box-shadow: 0 12rpx 26rpx rgba(8, 145, 178, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-child {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.children-card,
|
||||||
|
.menu-card {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-card {
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #0891b2;
|
||||||
|
background: #ecfeff;
|
||||||
|
box-shadow: inset 0 -3rpx 0 rgba(34, 211, 238, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-name {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-account {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-btn {
|
||||||
|
height: 78rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 2rpx dashed #67e8f9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #0891b2;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-card {
|
||||||
|
padding-top: 16rpx;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
min-height: 82rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-line {
|
||||||
|
height: 2rpx;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
774
src/pages/parent/reports/detail.vue
Normal file
774
src/pages/parent/reports/detail.vue
Normal file
@ -0,0 +1,774 @@
|
|||||||
|
<template>
|
||||||
|
<ParentPage active="reports" :show-tab="false">
|
||||||
|
<view class="detail-page">
|
||||||
|
<ParentCard class="hero-card">
|
||||||
|
<view class="hero-top">
|
||||||
|
<view>
|
||||||
|
<text class="category-tag">{{ report.category }}</text>
|
||||||
|
<text class="report-title">{{ report.title }}</text>
|
||||||
|
<text class="report-date">{{ report.date }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="subject-text">{{ report.subject }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="score-grid">
|
||||||
|
<view v-for="item in detail.scoreBlocks" :key="item.label" class="score-item">
|
||||||
|
<text class="score-label">{{ item.label }}</text>
|
||||||
|
<text class="score-value">{{ item.value }}</text>
|
||||||
|
<text class="score-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<ParentCard class="nav-card">
|
||||||
|
<view class="tab-row">
|
||||||
|
<view
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: activeTab === tab.key }"
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="rank-grid">
|
||||||
|
<view v-for="item in detail.ranks" :key="item.label" class="rank-item">
|
||||||
|
<text class="rank-label">{{ item.label }}</text>
|
||||||
|
<text class="rank-value">{{ item.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<template v-if="activeTab === 'analysis'">
|
||||||
|
<ParentCard class="section-card" title="题型得分" subtitle="按题型拆解本次卷面得分">
|
||||||
|
<view class="data-table">
|
||||||
|
<view class="table-row table-head">
|
||||||
|
<text>题号</text>
|
||||||
|
<text>题型</text>
|
||||||
|
<text>得分</text>
|
||||||
|
<text>得分率</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="row in detail.analysisRows" :key="row.range" class="table-row">
|
||||||
|
<text>{{ row.range }}</text>
|
||||||
|
<text>{{ row.type }}</text>
|
||||||
|
<text class="strong">{{ row.score }}</text>
|
||||||
|
<text class="rate-good">{{ row.rate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<ParentCard class="section-card" title="难度得分" subtitle="关注中难题稳定性,决定后续练习重点">
|
||||||
|
<view class="data-table">
|
||||||
|
<view class="table-row table-head">
|
||||||
|
<text>难度</text>
|
||||||
|
<text>题号</text>
|
||||||
|
<text>得分</text>
|
||||||
|
<text>得分率</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="row in detail.difficultyRows" :key="row.difficulty" class="table-row">
|
||||||
|
<text>{{ row.difficulty }}</text>
|
||||||
|
<text>{{ row.range }}</text>
|
||||||
|
<text class="strong">{{ row.score }}</text>
|
||||||
|
<text :class="row.rate === '100%' ? 'rate-good' : 'rate-warn'">{{ row.rate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="activeTab === 'answers'">
|
||||||
|
<ParentCard class="section-card" title="答题卡详情" subtitle="点击题号查看每道题的答题情况">
|
||||||
|
<view class="answer-legend">
|
||||||
|
<view><text class="legend-dot correct"></text>正确</view>
|
||||||
|
<view><text class="legend-dot wrong"></text>错误</view>
|
||||||
|
</view>
|
||||||
|
<view class="question-grid">
|
||||||
|
<view
|
||||||
|
v-for="question in detail.questions"
|
||||||
|
:key="question.no"
|
||||||
|
class="question-no"
|
||||||
|
:class="[question.status, { active: selectedNo === question.no }]"
|
||||||
|
@click="selectedNo = question.no"
|
||||||
|
>
|
||||||
|
{{ question.no }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<ParentCard class="section-card" title="当前题目详情">
|
||||||
|
<view class="question-head">
|
||||||
|
<text>第 {{ selectedQuestion.no }} 题</text>
|
||||||
|
<text>{{ selectedQuestion.type }}</text>
|
||||||
|
<text :class="selectedQuestion.status === 'correct' ? 'status-correct' : 'status-wrong'">
|
||||||
|
{{ selectedQuestion.status === 'correct' ? '正确' : '错误' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-fields">
|
||||||
|
<view class="field-card">
|
||||||
|
<text class="field-label">题干</text>
|
||||||
|
<text class="field-content">{{ selectedQuestion.stem }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="field-card success">
|
||||||
|
<text class="field-label">正确答案</text>
|
||||||
|
<text class="field-content">{{ selectedQuestion.correctAnswer }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="field-card student">
|
||||||
|
<text class="field-label">学员答案</text>
|
||||||
|
<text class="field-content">{{ selectedQuestion.studentAnswer }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="field-card">
|
||||||
|
<text class="field-label">得分情况</text>
|
||||||
|
<text class="field-content">{{ selectedQuestion.score }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="field-card">
|
||||||
|
<text class="field-label">班级得分率</text>
|
||||||
|
<text class="field-content">{{ selectedQuestion.classRate }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="field-card">
|
||||||
|
<text class="field-label">学校得分率</text>
|
||||||
|
<text class="field-content">{{ selectedQuestion.schoolRate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<ParentCard class="section-card" title="每道题具体情况">
|
||||||
|
<scroll-view scroll-x class="question-table-scroll">
|
||||||
|
<view class="question-table">
|
||||||
|
<view class="question-row table-head">
|
||||||
|
<text>题号</text>
|
||||||
|
<text>题型</text>
|
||||||
|
<text>得分</text>
|
||||||
|
<text>状态</text>
|
||||||
|
<text>知识点</text>
|
||||||
|
<text>班级得分率</text>
|
||||||
|
<text>学校得分率</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="question in detail.questions" :key="`row-${question.no}`" class="question-row">
|
||||||
|
<text>{{ question.no }}</text>
|
||||||
|
<text>{{ question.type }}</text>
|
||||||
|
<text>{{ question.score }}</text>
|
||||||
|
<text :class="question.status === 'correct' ? 'status-correct' : 'status-wrong'">
|
||||||
|
{{ question.status === 'correct' ? '正确' : '错误' }}
|
||||||
|
</text>
|
||||||
|
<text>{{ question.knowledge }}</text>
|
||||||
|
<text>{{ question.classRate }}</text>
|
||||||
|
<text>{{ question.schoolRate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</ParentCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<ParentCard class="section-card" title="错题知识点占比" subtitle="用于判断最近应优先补强的知识点">
|
||||||
|
<view class="knowledge-summary">
|
||||||
|
<view class="wrong-total">
|
||||||
|
<text class="wrong-number">{{ wrongCount }}</text>
|
||||||
|
<text class="wrong-label">错题数</text>
|
||||||
|
</view>
|
||||||
|
<view class="ratio-list">
|
||||||
|
<view v-for="item in detail.knowledgeRatios" :key="item.name" class="ratio-row">
|
||||||
|
<view class="ratio-meta">
|
||||||
|
<text>{{ item.name }}</text>
|
||||||
|
<text>{{ item.percent }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="ratio-track">
|
||||||
|
<view class="ratio-fill" :class="item.tone" :style="{ width: `${item.percent}%` }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<ParentCard class="section-card" title="知识点得分">
|
||||||
|
<view class="level-tabs">
|
||||||
|
<view
|
||||||
|
v-for="level in knowledgeTabs"
|
||||||
|
:key="level.key"
|
||||||
|
class="level-tab"
|
||||||
|
:class="{ active: activeKnowledgeLevel === level.key }"
|
||||||
|
@click="activeKnowledgeLevel = level.key"
|
||||||
|
>
|
||||||
|
{{ level.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="data-table">
|
||||||
|
<view class="table-row table-head knowledge-row">
|
||||||
|
<text>知识点</text>
|
||||||
|
<text>满分</text>
|
||||||
|
<text>得分</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in filteredKnowledgeScores" :key="item.name" class="table-row knowledge-row">
|
||||||
|
<text class="knowledge-name">{{ item.name }}</text>
|
||||||
|
<text>{{ item.fullScore }}</text>
|
||||||
|
<text class="score-danger">{{ item.score }}[{{ knowledgeRate(item) }}%]</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<xy-Empty v-if="!filteredKnowledgeScores.length" type="learn" content="当前分段暂无知识点" />
|
||||||
|
</ParentCard>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-actions">
|
||||||
|
<button class="download-btn" @click="downloadReport">下载报告</button>
|
||||||
|
<!-- <view class="home-btn" @click="goHome">
|
||||||
|
<text class="home-roof"></text>
|
||||||
|
<text class="home-body"></text>
|
||||||
|
</view> -->
|
||||||
|
</view>
|
||||||
|
</ParentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
|
import { reportDetails, reportList, type ReportKnowledgeScore } from '../utils/data';
|
||||||
|
|
||||||
|
type DetailTab = 'analysis' | 'answers' | 'knowledge';
|
||||||
|
type KnowledgeLevel = 'all' | 'weak' | 'pass' | 'good';
|
||||||
|
|
||||||
|
const tabs: { key: DetailTab; label: string }[] = [
|
||||||
|
{ key: 'analysis', label: '试卷分析' },
|
||||||
|
{ key: 'answers', label: '答题卡' },
|
||||||
|
{ key: 'knowledge', label: '知识点' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const knowledgeTabs: { key: KnowledgeLevel; label: string }[] = [
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'weak', label: '薄弱' },
|
||||||
|
{ key: 'pass', label: '合格' },
|
||||||
|
{ key: 'good', label: '良好' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const reportId = ref('r1');
|
||||||
|
const activeTab = ref<DetailTab>('analysis');
|
||||||
|
const selectedNo = ref(1);
|
||||||
|
const activeKnowledgeLevel = ref<KnowledgeLevel>('all');
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
reportId.value = String(query?.id || 'r1');
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = computed(() => reportList.find((item) => item.id === reportId.value) || reportList[0]);
|
||||||
|
const detail = computed(() => reportDetails[report.value.id] || reportDetails.r1);
|
||||||
|
const selectedQuestion = computed(() => detail.value.questions.find((item) => item.no === selectedNo.value) || detail.value.questions[0]);
|
||||||
|
const wrongCount = computed(() => detail.value.questions.filter((item) => item.status === 'wrong').length);
|
||||||
|
|
||||||
|
const knowledgeRate = (item: ReportKnowledgeScore) => Math.round((item.score / item.fullScore) * 100);
|
||||||
|
|
||||||
|
const knowledgeLevelOf = (item: ReportKnowledgeScore): KnowledgeLevel => {
|
||||||
|
const rate = knowledgeRate(item);
|
||||||
|
if (rate < 60) return 'weak';
|
||||||
|
if (rate < 80) return 'pass';
|
||||||
|
return 'good';
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredKnowledgeScores = computed(() => {
|
||||||
|
if (activeKnowledgeLevel.value === 'all') return detail.value.knowledgeScores;
|
||||||
|
return detail.value.knowledgeScores.filter((item) => knowledgeLevelOf(item) === activeKnowledgeLevel.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadReport = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '报告生成中',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
uni.reLaunch({ url: '/pages/parent/home/index' });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.detail-page {
|
||||||
|
padding-bottom: calc(144rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(20, 184, 166, 0.08)),
|
||||||
|
rgba(255, 255, 255, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #1d4ed8;
|
||||||
|
background: #dbeafe;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-title {
|
||||||
|
display: block;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-date {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-text {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item {
|
||||||
|
min-height: 138rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 0 -2rpx 0 rgba(191, 219, 254, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-label,
|
||||||
|
.score-desc {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
margin: 8rpx 0 4rpx;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-card,
|
||||||
|
.section-card {
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
height: 70rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 25rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #1d4ed8;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10rpx 22rpx rgba(37, 99, 235, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-item {
|
||||||
|
min-height: 110rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-value {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
min-height: 74rpx;
|
||||||
|
align-items: center;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 2rpx solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-head {
|
||||||
|
color: #475569;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong,
|
||||||
|
.rate-good,
|
||||||
|
.status-correct {
|
||||||
|
color: #16a34a;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-warn,
|
||||||
|
.score-danger,
|
||||||
|
.status-wrong {
|
||||||
|
color: #ef4444;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 28rpx;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-legend view {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot.correct {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot.wrong {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-no {
|
||||||
|
height: 58rpx;
|
||||||
|
border: 2rpx solid #bbf7d0;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #15803d;
|
||||||
|
background: #f0fdf4;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-no.wrong {
|
||||||
|
border-color: #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-no.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 4rpx rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-fields {
|
||||||
|
display: grid;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card {
|
||||||
|
padding: 18rpx 20rpx;
|
||||||
|
border: 2rpx solid #dbeafe;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card.success {
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card.student {
|
||||||
|
border-color: #ddd6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #2563eb;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card.success .field-label {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card.student .field-label {
|
||||||
|
background: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-content {
|
||||||
|
display: block;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-table-scroll {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-table {
|
||||||
|
width: 1060rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90rpx 140rpx 110rpx 110rpx 210rpx 190rpx 190rpx;
|
||||||
|
min-height: 72rpx;
|
||||||
|
align-items: center;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 23rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 2rpx solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-row:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrong-total {
|
||||||
|
width: 170rpx;
|
||||||
|
height: 170rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(180deg, #eff6ff 0%, #ffffff 100%);
|
||||||
|
box-shadow: inset 0 0 0 16rpx #dbeafe;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrong-number {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 46rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrong-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-list {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-row + .ratio-row {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-track {
|
||||||
|
height: 16rpx;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-fill.blue {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-fill.orange {
|
||||||
|
background: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-fill.green {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tab {
|
||||||
|
height: 62rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #475569;
|
||||||
|
background: #ffffff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tab.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-row {
|
||||||
|
grid-template-columns: 1fr 120rpx 180rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-name {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions {
|
||||||
|
position: fixed;
|
||||||
|
left: 28rpx;
|
||||||
|
right: 28rpx;
|
||||||
|
bottom: calc(22rpx + env(safe-area-inset-bottom));
|
||||||
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 84rpx;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #0891b2 100%);
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
box-shadow: 0 16rpx 32rpx rgba(37, 99, 235, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-btn {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-left: -12rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #2563eb;
|
||||||
|
box-shadow: 0 16rpx 32rpx rgba(37, 99, 235, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-roof {
|
||||||
|
position: absolute;
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
border-top: 5rpx solid #ffffff;
|
||||||
|
border-left: 5rpx solid #ffffff;
|
||||||
|
transform: translateY(-8rpx) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-body {
|
||||||
|
width: 34rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
border: 5rpx solid #ffffff;
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
206
src/pages/parent/reports/index.vue
Normal file
206
src/pages/parent/reports/index.vue
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<ParentPage active="reports">
|
||||||
|
<view class="filter-row">
|
||||||
|
<picker class="filter-picker" :range="subjectOptions" :value="subjectIndex" @change="onSubjectChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ subjectOptions[subjectIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="filter-picker" :range="gradeOptions" :value="gradeIndex" @change="onGradeChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ gradeOptions[gradeIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="filter-picker" :range="categoryOptions" :value="categoryIndex" @change="onCategoryChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ categoryOptions[categoryIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="filteredReports.length" class="report-list">
|
||||||
|
<ParentCard
|
||||||
|
v-for="item in filteredReports"
|
||||||
|
:key="item.id"
|
||||||
|
class="report-card"
|
||||||
|
clickable
|
||||||
|
@click="openReport(item.id)"
|
||||||
|
>
|
||||||
|
<view class="report-body">
|
||||||
|
<view class="subject-badge" :class="subjectClass(item.subject)">{{ item.subject }}</view>
|
||||||
|
<view class="report-main">
|
||||||
|
<view class="category">[{{ item.category }}]</view>
|
||||||
|
<text class="report-title ellipsis-2">{{ item.title }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="score-wrap">
|
||||||
|
<text class="score">{{ formatScore(item.score) }}</text>
|
||||||
|
<text class="date">{{ item.date }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ParentCard>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="empty-card">
|
||||||
|
<xy-Empty type="task" content="暂无符合条件的考试报告" />
|
||||||
|
</view>
|
||||||
|
</ParentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
|
import { categoryOptions, gradeOptions, reportList, subjectOptions, type ReportItem } from '../utils/data';
|
||||||
|
|
||||||
|
const subjectIndex = ref(0);
|
||||||
|
const gradeIndex = ref(0);
|
||||||
|
const categoryIndex = ref(0);
|
||||||
|
|
||||||
|
const filteredReports = computed(() => {
|
||||||
|
const subject = subjectOptions[subjectIndex.value];
|
||||||
|
const grade = gradeOptions[gradeIndex.value];
|
||||||
|
const category = categoryOptions[categoryIndex.value];
|
||||||
|
return reportList.filter((item) => {
|
||||||
|
const subjectMatched = subject === '全部学科' || item.subject === subject;
|
||||||
|
const gradeMatched = grade === '全部年级' || item.grade === grade;
|
||||||
|
const categoryMatched = category === '全部类型' || item.category === category;
|
||||||
|
return subjectMatched && gradeMatched && categoryMatched;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubjectChange = (e: any) => { subjectIndex.value = Number(e.detail.value); };
|
||||||
|
const onGradeChange = (e: any) => { gradeIndex.value = Number(e.detail.value); };
|
||||||
|
const onCategoryChange = (e: any) => { categoryIndex.value = Number(e.detail.value); };
|
||||||
|
|
||||||
|
const formatScore = (score: number) => `${score.toFixed(1).replace(/\.0$/, '')}%`;
|
||||||
|
|
||||||
|
const subjectClass = (subject: ReportItem['subject']) => {
|
||||||
|
if (subject === '数学') return 'math';
|
||||||
|
if (subject === '语文') return 'chinese';
|
||||||
|
if (subject === '英语') return 'english';
|
||||||
|
return 'multi';
|
||||||
|
};
|
||||||
|
|
||||||
|
const openReport = (id: string) => {
|
||||||
|
uni.navigateTo({ url: `/pages/parent/reports/detail?id=${id}` });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-picker {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill {
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 22rpx rgba(8, 145, 178, 0.08),
|
||||||
|
inset 0 -2rpx 0 rgba(207, 250, 254, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8rpx solid transparent;
|
||||||
|
border-right: 8rpx solid transparent;
|
||||||
|
border-top: 10rpx solid #0891b2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-list {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card {
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-badge {
|
||||||
|
width: 86rpx;
|
||||||
|
height: 86rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.math { background: #14b8a6; }
|
||||||
|
&.chinese { background: #f97316; }
|
||||||
|
&.english { background: #ec4899; }
|
||||||
|
&.multi { background: #3b82f6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-title {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-wrap {
|
||||||
|
width: 112rpx;
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
display: block;
|
||||||
|
color: #ea580c;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-card {
|
||||||
|
height: 520rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
354
src/pages/parent/utils/data.ts
Normal file
354
src/pages/parent/utils/data.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
export interface ParentChild {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
account: string;
|
||||||
|
school: string;
|
||||||
|
className: string;
|
||||||
|
examNo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubjectTrend {
|
||||||
|
subject: string;
|
||||||
|
status: 'up' | 'down' | 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GradeBand {
|
||||||
|
label: string;
|
||||||
|
level: 'excellent' | 'good' | 'middle' | 'weak';
|
||||||
|
subjects: SubjectTrend[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportItem {
|
||||||
|
id: string;
|
||||||
|
subject: string;
|
||||||
|
category: string;
|
||||||
|
title: string;
|
||||||
|
score: number;
|
||||||
|
date: string;
|
||||||
|
grade: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportScoreBlock {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportRank {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportAnalysisRow {
|
||||||
|
range: string;
|
||||||
|
type: string;
|
||||||
|
score: string;
|
||||||
|
rate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportDifficultyRow {
|
||||||
|
difficulty: string;
|
||||||
|
range: string;
|
||||||
|
score: string;
|
||||||
|
rate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportQuestion {
|
||||||
|
no: number;
|
||||||
|
type: string;
|
||||||
|
score: string;
|
||||||
|
status: 'correct' | 'wrong';
|
||||||
|
stem: string;
|
||||||
|
correctAnswer: string;
|
||||||
|
studentAnswer: string;
|
||||||
|
classRate: string;
|
||||||
|
schoolRate: string;
|
||||||
|
knowledge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportKnowledgeRatio {
|
||||||
|
name: string;
|
||||||
|
percent: number;
|
||||||
|
tone: 'blue' | 'orange' | 'green';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportKnowledgeScore {
|
||||||
|
name: string;
|
||||||
|
fullScore: number;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportDetail {
|
||||||
|
id: string;
|
||||||
|
scoreBlocks: ReportScoreBlock[];
|
||||||
|
ranks: ReportRank[];
|
||||||
|
analysisRows: ReportAnalysisRow[];
|
||||||
|
difficultyRows: ReportDifficultyRow[];
|
||||||
|
questions: ReportQuestion[];
|
||||||
|
knowledgeRatios: ReportKnowledgeRatio[];
|
||||||
|
knowledgeScores: ReportKnowledgeScore[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickAction {
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
tone: 'cyan' | 'green' | 'orange';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeWeakPoint {
|
||||||
|
name: string;
|
||||||
|
subject: string;
|
||||||
|
grade: string;
|
||||||
|
category: string;
|
||||||
|
fullScore: number;
|
||||||
|
score: number;
|
||||||
|
wrongPercent: number;
|
||||||
|
tone: 'blue' | 'orange' | 'green' | 'rose';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AbilityTrendPoint {
|
||||||
|
date: string;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AbilitySummary {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 家长端首版先用结构化本地数据承载页面功能,后续接接口只需要替换这里的来源。
|
||||||
|
export const parentChildren: ParentChild[] = [
|
||||||
|
{
|
||||||
|
id: 'child-1',
|
||||||
|
name: '小星河',
|
||||||
|
account: '3620012788',
|
||||||
|
school: '山东蓝翔技校(初中)',
|
||||||
|
className: '初2027级3班',
|
||||||
|
examNo: '3620012788',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'child-2',
|
||||||
|
name: '小灵犀',
|
||||||
|
account: '3620012563',
|
||||||
|
school: '山东蓝翔技校(初中)',
|
||||||
|
className: '初2027级3班',
|
||||||
|
examNo: '3620012563',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const gradeBands: GradeBand[] = [
|
||||||
|
{
|
||||||
|
label: '优势保持',
|
||||||
|
level: 'excellent',
|
||||||
|
subjects: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '良好巩固',
|
||||||
|
level: 'good',
|
||||||
|
subjects: [{ subject: '数学', status: 'down' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重点提升',
|
||||||
|
level: 'middle',
|
||||||
|
subjects: [
|
||||||
|
{ subject: '语文', status: 'up' },
|
||||||
|
{ subject: '英语', status: 'up' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '及时补弱',
|
||||||
|
level: 'weak',
|
||||||
|
subjects: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const quickActions: QuickAction[] = [
|
||||||
|
{ title: '学情追踪', desc: '查看本周学习变化', tone: 'cyan' },
|
||||||
|
{ title: '错题练习', desc: '按薄弱点再练一遍', tone: 'orange' },
|
||||||
|
{ title: '课程学习', desc: '进入课程任务', tone: 'green' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const reportList: ReportItem[] = [
|
||||||
|
{
|
||||||
|
id: 'r1',
|
||||||
|
subject: '数学',
|
||||||
|
category: '周作业',
|
||||||
|
title: '2023-2024学年(上)初2023级七年级上期末模拟(五)数学',
|
||||||
|
score: 100,
|
||||||
|
date: '2024-01-12',
|
||||||
|
grade: '初一',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r2',
|
||||||
|
subject: '多科',
|
||||||
|
category: '月考',
|
||||||
|
title: '2023-2024学年(上)初2023级阶段性练习',
|
||||||
|
score: 60,
|
||||||
|
date: '2024-01-08',
|
||||||
|
grade: '初一',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r3',
|
||||||
|
subject: '语文',
|
||||||
|
category: '周作业',
|
||||||
|
title: '2023-2024学年(上)初2023级七年级上期末模拟(三)语文',
|
||||||
|
score: 92,
|
||||||
|
date: '2023-12-28',
|
||||||
|
grade: '初一',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r4',
|
||||||
|
subject: '英语',
|
||||||
|
category: '周作业',
|
||||||
|
title: '2023-2024学年(上)初2023级七年级上期末模拟(三)英语',
|
||||||
|
score: 88.5,
|
||||||
|
date: '2023-12-28',
|
||||||
|
grade: '初一',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const reportDetails: Record<string, ReportDetail> = {
|
||||||
|
r1: {
|
||||||
|
id: 'r1',
|
||||||
|
scoreBlocks: [
|
||||||
|
{ label: '我的得分', value: '120', desc: '总分 120' },
|
||||||
|
{ label: '满分', value: '120', desc: '本次卷面' },
|
||||||
|
{ label: '得分率', value: '100.0%', desc: '超过多数同学' },
|
||||||
|
],
|
||||||
|
ranks: [
|
||||||
|
{ label: '班级排名', value: '1/45' },
|
||||||
|
{ label: '年级排名', value: '6/680' },
|
||||||
|
],
|
||||||
|
analysisRows: [
|
||||||
|
{ range: '1-10', type: '选择题', score: '40/40', rate: '100%' },
|
||||||
|
{ range: '11-16', type: '填空题', score: '30/30', rate: '100%' },
|
||||||
|
{ range: '17-21', type: '解答题', score: '20/20', rate: '100%' },
|
||||||
|
{ range: '22-23', type: '压轴题', score: '5/10', rate: '50%' },
|
||||||
|
],
|
||||||
|
difficultyRows: [
|
||||||
|
{ difficulty: '简单题', range: '1-8', score: '32/32', rate: '100%' },
|
||||||
|
{ difficulty: '中等题', range: '9-18', score: '42/44', rate: '95.5%' },
|
||||||
|
{ difficulty: '难题', range: '19-23', score: '18/24', rate: '75.0%' },
|
||||||
|
],
|
||||||
|
questions: [
|
||||||
|
{ no: 1, type: '选择题', score: '4/4', status: 'correct', stem: '请结合材料完成本题要求。', correctAnswer: 'A', studentAnswer: 'A', classRate: '88%', schoolRate: '84%', knowledge: '字音字形' },
|
||||||
|
{ no: 2, type: '选择题', score: '4/4', status: 'correct', stem: '根据题意选择最符合要求的一项。', correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '81%', knowledge: '词语运用' },
|
||||||
|
{ no: 3, type: '选择题', score: '4/4', status: 'correct', stem: '阅读材料并判断关键条件。', correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '78%', knowledge: '病句辨析' },
|
||||||
|
{ no: 4, type: '选择题', score: '4/4', status: 'correct', stem: '从选项中选出表达准确的一项。', correctAnswer: 'D', studentAnswer: 'D', classRate: '76%', schoolRate: '75%', knowledge: '文学常识' },
|
||||||
|
{ no: 5, type: '选择题', score: '4/4', status: 'correct', stem: '根据语境判断标点使用。', correctAnswer: 'A', studentAnswer: 'A', classRate: '72%', schoolRate: '72%', knowledge: '标点符号' },
|
||||||
|
{ no: 6, type: '选择题', score: '4/4', status: 'correct', stem: '判断句子修辞手法。', correctAnswer: 'B', studentAnswer: 'B', classRate: '88%', schoolRate: '69%', knowledge: '修辞手法' },
|
||||||
|
{ no: 7, type: '选择题', score: '4/4', status: 'correct', stem: '结合古诗文选择正确理解。', correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '84%', knowledge: '古诗文默写' },
|
||||||
|
{ no: 8, type: '选择题', score: '4/4', status: 'correct', stem: '阅读片段后概括中心信息。', correctAnswer: 'D', studentAnswer: 'D', classRate: '80%', schoolRate: '81%', knowledge: '名著阅读' },
|
||||||
|
{ no: 9, type: '选择题', score: '4/4', status: 'correct', stem: '判断句间衔接是否连贯。', correctAnswer: 'A', studentAnswer: 'A', classRate: '76%', schoolRate: '78%', knowledge: '语段衔接' },
|
||||||
|
{ no: 10, type: '选择题', score: '4/4', status: 'correct', stem: '完成句式转换。', correctAnswer: 'C', studentAnswer: 'C', classRate: '72%', schoolRate: '75%', knowledge: '句式转换' },
|
||||||
|
{ no: 11, type: '填空题', score: '5/5', status: 'correct', stem: '补全古文实词解释。', correctAnswer: '明察', studentAnswer: '明察', classRate: '88%', schoolRate: '72%', knowledge: '古文实词' },
|
||||||
|
{ no: 12, type: '填空题', score: '5/5', status: 'correct', stem: '补全古文虚词用法。', correctAnswer: '之', studentAnswer: '之', classRate: '84%', schoolRate: '69%', knowledge: '古文虚词' },
|
||||||
|
{ no: 13, type: '填空题', score: '5/5', status: 'correct', stem: '判断文言断句位置。', correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '84%', knowledge: '文言断句' },
|
||||||
|
{ no: 14, type: '填空题', score: '5/5', status: 'correct', stem: '翻译关键句子。', correctAnswer: '符合语境', studentAnswer: '符合语境', classRate: '76%', schoolRate: '81%', knowledge: '翻译理解' },
|
||||||
|
{ no: 15, type: '填空题', score: '5/5', status: 'correct', stem: '赏析诗句表达效果。', correctAnswer: '借景抒情', studentAnswer: '借景抒情', classRate: '72%', schoolRate: '78%', knowledge: '诗歌鉴赏' },
|
||||||
|
{ no: 16, type: '填空题', score: '5/5', status: 'correct', stem: '概括现代文段落作用。', correctAnswer: '承上启下', studentAnswer: '承上启下', classRate: '68%', schoolRate: '74%', knowledge: '段落作用' },
|
||||||
|
{ no: 17, type: '解答题', score: '4/4', status: 'correct', stem: '结合文本分析人物形象。', correctAnswer: '要点完整', studentAnswer: '要点完整', classRate: '81%', schoolRate: '77%', knowledge: '人物形象' },
|
||||||
|
{ no: 18, type: '解答题', score: '4/4', status: 'correct', stem: '解释标题的含义。', correctAnswer: '双层含义', studentAnswer: '双层含义', classRate: '79%', schoolRate: '73%', knowledge: '标题含义' },
|
||||||
|
{ no: 19, type: '解答题', score: '4/4', status: 'correct', stem: '分析语句表达效果。', correctAnswer: '修辞与情感', studentAnswer: '修辞与情感', classRate: '73%', schoolRate: '70%', knowledge: '语句赏析' },
|
||||||
|
{ no: 20, type: '解答题', score: '4/4', status: 'correct', stem: '根据材料提出观点。', correctAnswer: '观点明确', studentAnswer: '观点明确', classRate: '70%', schoolRate: '68%', knowledge: '观点表达' },
|
||||||
|
{ no: 21, type: '解答题', score: '4/4', status: 'correct', stem: '完成综合探究。', correctAnswer: '材料充分', studentAnswer: '材料充分', classRate: '66%', schoolRate: '63%', knowledge: '综合探究' },
|
||||||
|
{ no: 22, type: '压轴题', score: '2/5', status: 'wrong', stem: '结合整篇文章完成开放表达。', correctAnswer: '围绕主题并联系生活', studentAnswer: '联系不足', classRate: '52%', schoolRate: '49%', knowledge: '开放表达' },
|
||||||
|
{ no: 23, type: '压轴题', score: '3/5', status: 'wrong', stem: '完成写作片段升格。', correctAnswer: '结构完整、语言有层次', studentAnswer: '结构较散', classRate: '48%', schoolRate: '44%', knowledge: '写作表达' },
|
||||||
|
],
|
||||||
|
knowledgeRatios: [
|
||||||
|
{ name: '函数与图像', percent: 40, tone: 'blue' },
|
||||||
|
{ name: '几何证明', percent: 35, tone: 'orange' },
|
||||||
|
{ name: '方程应用', percent: 25, tone: 'green' },
|
||||||
|
],
|
||||||
|
knowledgeScores: [
|
||||||
|
{ name: '《西游记》', fullScore: 2, score: 0 },
|
||||||
|
{ name: '情感主旨', fullScore: 2, score: 0 },
|
||||||
|
{ name: '环境描写', fullScore: 4, score: 1.5 },
|
||||||
|
{ name: '开放表达', fullScore: 5, score: 2 },
|
||||||
|
{ name: '写作表达', fullScore: 5, score: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subjectOptions = ['全部学科', '数学', '语文', '英语', '多科'];
|
||||||
|
export const gradeOptions = ['全部年级', '初一', '初二', '初三'];
|
||||||
|
export const categoryOptions = ['全部类型', '周作业', '月考'];
|
||||||
|
|
||||||
|
export const knowledgeWeakPoints: KnowledgeWeakPoint[] = [
|
||||||
|
{
|
||||||
|
name: '函数与图像',
|
||||||
|
subject: '数学',
|
||||||
|
grade: '初一',
|
||||||
|
category: '周作业',
|
||||||
|
fullScore: 20,
|
||||||
|
score: 8,
|
||||||
|
wrongPercent: 40,
|
||||||
|
tone: 'blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '几何证明',
|
||||||
|
subject: '数学',
|
||||||
|
grade: '初一',
|
||||||
|
category: '周作业',
|
||||||
|
fullScore: 20,
|
||||||
|
score: 9,
|
||||||
|
wrongPercent: 45,
|
||||||
|
tone: 'orange',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '方程应用',
|
||||||
|
subject: '数学',
|
||||||
|
grade: '初一',
|
||||||
|
category: '月考',
|
||||||
|
fullScore: 20,
|
||||||
|
score: 15,
|
||||||
|
wrongPercent: 25,
|
||||||
|
tone: 'green',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '现代文阅读',
|
||||||
|
subject: '语文',
|
||||||
|
grade: '初一',
|
||||||
|
category: '周作业',
|
||||||
|
fullScore: 16,
|
||||||
|
score: 10,
|
||||||
|
wrongPercent: 38,
|
||||||
|
tone: 'rose',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '完形填空',
|
||||||
|
subject: '英语',
|
||||||
|
grade: '初一',
|
||||||
|
category: '月考',
|
||||||
|
fullScore: 15,
|
||||||
|
score: 11,
|
||||||
|
wrongPercent: 27,
|
||||||
|
tone: 'blue',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const abilityTrend: AbilityTrendPoint[] = [
|
||||||
|
{ date: '11-22', score: 62 },
|
||||||
|
{ date: '11-28', score: 68 },
|
||||||
|
{ date: '12-05', score: 72 },
|
||||||
|
{ date: '12-13', score: 91 },
|
||||||
|
{ date: '12-20', score: 75 },
|
||||||
|
{ date: '12-27', score: 78 },
|
||||||
|
{ date: '01-03', score: 81 },
|
||||||
|
{ date: '01-08', score: 76 },
|
||||||
|
{ date: '01-14', score: 79 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const abilitySummary: AbilitySummary[] = [
|
||||||
|
{
|
||||||
|
label: '得分能力综合评估',
|
||||||
|
value: '良好',
|
||||||
|
desc: '最近一次峰值明显,整体维持在良好区间。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '得分能力稳定性',
|
||||||
|
value: '相对波动',
|
||||||
|
desc: '12月中旬后回落,需要关注高分后的复盘质量。',
|
||||||
|
},
|
||||||
|
];
|
||||||
384
src/pages/teacher/homework/filter.vue
Normal file
384
src/pages/teacher/homework/filter.vue
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
<template>
|
||||||
|
<view class="filter-page">
|
||||||
|
<view class="bg-decor"></view>
|
||||||
|
<scroll-view class="filter-scroll" scroll-y enhanced :show-scrollbar="false">
|
||||||
|
<view class="content">
|
||||||
|
<view class="section-card clay-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="section-title">选择班级</text>
|
||||||
|
<text class="section-required">必选</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="classLoading" class="state-line">加载班级中...</view>
|
||||||
|
<view v-else-if="!classList.length" class="state-line">暂无班级数据</view>
|
||||||
|
<view v-else class="option-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in classList"
|
||||||
|
:key="item.id"
|
||||||
|
class="option-item"
|
||||||
|
:class="{ active: selectedClassId === item.id }"
|
||||||
|
@click="selectedClassId = item.id"
|
||||||
|
>
|
||||||
|
<text class="option-text">{{ item.name }}</text>
|
||||||
|
<text v-if="selectedClassId === item.id" class="check-mark">✓</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card clay-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="section-title">选择学科</text>
|
||||||
|
<text class="section-hint">可多选,不选默认全部</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="!selectedClassId" class="state-line">请先选择班级</view>
|
||||||
|
<view v-else-if="subjectLoading" class="state-line">加载学科中...</view>
|
||||||
|
<view v-else-if="!subjectList.length" class="state-line">该班级暂无学科数据</view>
|
||||||
|
<view v-else class="option-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in subjectList"
|
||||||
|
:key="item.value"
|
||||||
|
class="option-item"
|
||||||
|
:class="{ active: selectedSubjectIds.includes(item.value) }"
|
||||||
|
@click="toggleSubject(item.value)"
|
||||||
|
>
|
||||||
|
<text class="option-text">{{ item.label }}</text>
|
||||||
|
<text v-if="selectedSubjectIds.includes(item.value)" class="check-mark">✓</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card clay-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="section-title">时间范围</text>
|
||||||
|
<text class="section-hint">默认今天</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-row">
|
||||||
|
<picker mode="date" :value="startTime" @change="onStartChange">
|
||||||
|
<view class="date-box">{{ startTime || '开始日期' }}</view>
|
||||||
|
</picker>
|
||||||
|
<text class="date-sep">至</text>
|
||||||
|
<picker mode="date" :value="endTime" @change="onEndChange">
|
||||||
|
<view class="date-box">{{ endTime || '结束日期' }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view v-if="startTime || endTime" class="clear-date" @click="clearDate">清除时间</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-space"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="bottom-bar">
|
||||||
|
<view class="reset-btn" @click="handleReset">重置</view>
|
||||||
|
<view class="confirm-btn clay-button" :class="{ disabled: !selectedClassId }" @click="handleConfirm">
|
||||||
|
{{ confirmButtonText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useTeacherStore } from '@/state/modules/teacher';
|
||||||
|
import { getClassListByTeacher, getSubjectListByClassId } from '@/api/teacher';
|
||||||
|
|
||||||
|
const teacherStore = useTeacherStore();
|
||||||
|
const { teacherInfo } = storeToRefs(teacherStore);
|
||||||
|
|
||||||
|
const target = ref('');
|
||||||
|
const classList = ref<{ id: string; name: string }[]>([]);
|
||||||
|
const classLoading = ref(false);
|
||||||
|
const selectedClassId = ref('');
|
||||||
|
const subjectList = ref<{ value: number; label: string }[]>([]);
|
||||||
|
const subjectLoading = ref(false);
|
||||||
|
const selectedSubjectIds = ref<number[]>([]);
|
||||||
|
|
||||||
|
const getTodayStr = () => {
|
||||||
|
const d = new Date();
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
const startTime = ref(getTodayStr());
|
||||||
|
const endTime = ref(getTodayStr());
|
||||||
|
|
||||||
|
const confirmButtonText = computed(() => target.value === 'weakKnowledge' ? '查看薄弱知识点' : '查询学情报告');
|
||||||
|
|
||||||
|
const normalizeClassList = (data: any) => {
|
||||||
|
const rows = Array.isArray(data) ? data : data?.rows || data?.list || [];
|
||||||
|
return rows.map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
id: String(item.id ?? item.classId ?? ''),
|
||||||
|
name: item.name ?? item.className ?? '未命名班级',
|
||||||
|
})).filter((item: any) => item.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchClassList = async () => {
|
||||||
|
classLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getClassListByTeacher();
|
||||||
|
classList.value = normalizeClassList(res.data);
|
||||||
|
if (!selectedClassId.value && classList.value.length === 1) {
|
||||||
|
selectedClassId.value = classList.value[0].id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取班级失败', err);
|
||||||
|
classList.value = [];
|
||||||
|
} finally {
|
||||||
|
classLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(selectedClassId, async (value) => {
|
||||||
|
subjectList.value = [];
|
||||||
|
selectedSubjectIds.value = [];
|
||||||
|
if (!value) return;
|
||||||
|
subjectLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getSubjectListByClassId(value);
|
||||||
|
const rows = Array.isArray(res.data) ? res.data : [];
|
||||||
|
subjectList.value = rows.map((item: any) => ({
|
||||||
|
value: Number(item.subjectId ?? item.id ?? item.value),
|
||||||
|
label: item.subjectName ?? item.name ?? item.label ?? '',
|
||||||
|
})).filter((item) => item.value && item.label);
|
||||||
|
const teacherSubjectId = Number(teacherInfo.value.subjectId);
|
||||||
|
if (teacherSubjectId && subjectList.value.some((item) => item.value === teacherSubjectId)) {
|
||||||
|
selectedSubjectIds.value = [teacherSubjectId];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取学科失败', err);
|
||||||
|
} finally {
|
||||||
|
subjectLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSubject = (value: number) => {
|
||||||
|
selectedSubjectIds.value = selectedSubjectIds.value.includes(value)
|
||||||
|
? selectedSubjectIds.value.filter((item) => item !== value)
|
||||||
|
: [...selectedSubjectIds.value, value];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStartChange = (e: any) => { startTime.value = e.detail?.value || ''; };
|
||||||
|
const onEndChange = (e: any) => { endTime.value = e.detail?.value || ''; };
|
||||||
|
const clearDate = () => {
|
||||||
|
startTime.value = '';
|
||||||
|
endTime.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
selectedClassId.value = '';
|
||||||
|
selectedSubjectIds.value = [];
|
||||||
|
startTime.value = '';
|
||||||
|
endTime.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!selectedClassId.value) {
|
||||||
|
uni.showToast({ title: '请先选择班级', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedClass = classList.value.find((item) => item.id === selectedClassId.value);
|
||||||
|
const query: string[] = [
|
||||||
|
`classId=${encodeURIComponent(selectedClassId.value)}`,
|
||||||
|
`className=${encodeURIComponent(selectedClass?.name || '')}`,
|
||||||
|
];
|
||||||
|
if (selectedSubjectIds.value.length) {
|
||||||
|
query.push(`subjectIds=${encodeURIComponent(selectedSubjectIds.value.join(','))}`);
|
||||||
|
}
|
||||||
|
if (startTime.value) query.push(`startTime=${encodeURIComponent(startTime.value)}`);
|
||||||
|
if (endTime.value) query.push(`endTime=${encodeURIComponent(endTime.value)}`);
|
||||||
|
|
||||||
|
const path = target.value === 'weakKnowledge'
|
||||||
|
? '/pages/teacher/weakKnowledge/index'
|
||||||
|
: '/pages/teacher/homework/index';
|
||||||
|
uni.navigateTo({ url: `${path}?${query.join('&')}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
target.value = options?.target || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
try {
|
||||||
|
if (!teacherInfo.value?.id) await teacherStore.getLoginUser();
|
||||||
|
if (!classList.value.length) await fetchClassList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('筛选页初始化失败', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #eef2ff 0%, #f8fafc 62%, #ffffff 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-decor {
|
||||||
|
position: absolute;
|
||||||
|
width: 420rpx;
|
||||||
|
height: 420rpx;
|
||||||
|
right: -160rpx;
|
||||||
|
top: 180rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(129, 140, 248, 0.24);
|
||||||
|
filter: blur(70rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24rpx 28rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
padding: 30rpx 28rpx;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-required,
|
||||||
|
.section-hint {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-required {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
min-height: 76rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 26rpx;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%);
|
||||||
|
border-color: #4f46e5;
|
||||||
|
box-shadow: 0 10rpx 22rpx rgba(79, 70, 229, 0.22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
max-width: 540rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-mark {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-line {
|
||||||
|
padding: 28rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-box {
|
||||||
|
min-width: 220rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2rpx solid #e0e7ff;
|
||||||
|
text-align: center;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-sep {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-date {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-space {
|
||||||
|
height: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 9;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 -8rpx 24rpx rgba(99, 102, 241, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn,
|
||||||
|
.confirm-btn {
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
flex: 1;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
flex: 2;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
407
src/pages/teacher/homework/index.vue
Normal file
407
src/pages/teacher/homework/index.vue
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
<template>
|
||||||
|
<view class="report-page">
|
||||||
|
<view class="summary-card clay-card" @click="goBackFilter">
|
||||||
|
<view>
|
||||||
|
<text class="summary-title">{{ className || reportData?.className || '班级作业完成情况' }}</text>
|
||||||
|
<text class="summary-desc">{{ filterSummary }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="summary-action">修改</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<xy-Loading text="加载报告中..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorMsg" class="state-card">
|
||||||
|
<xy-Empty type="network" :content="errorMsg" />
|
||||||
|
</view>
|
||||||
|
<scroll-view v-else class="report-scroll" scroll-y enhanced :show-scrollbar="false">
|
||||||
|
<view v-if="reportData" class="content">
|
||||||
|
<view class="overview-card clay-card">
|
||||||
|
<view class="overview-item">
|
||||||
|
<text class="overview-value">{{ studentCount }}</text>
|
||||||
|
<text class="overview-label">学生数</text>
|
||||||
|
</view>
|
||||||
|
<view class="overview-item">
|
||||||
|
<text class="overview-value">{{ totalHomework }}</text>
|
||||||
|
<text class="overview-label">作业总数</text>
|
||||||
|
</view>
|
||||||
|
<view class="overview-item">
|
||||||
|
<text class="overview-value">{{ completeRate }}</text>
|
||||||
|
<text class="overview-label">完成率</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!reportData.studentList?.length" class="state-card in-scroll">
|
||||||
|
<xy-Empty type="task" content="暂无学生作业数据" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-for="student in reportData.studentList" :key="student.userId" class="student-card clay-card">
|
||||||
|
<view class="student-head">
|
||||||
|
<view class="avatar">{{ firstName(student.userName) }}</view>
|
||||||
|
<view class="student-main">
|
||||||
|
<text class="student-name">{{ student.userName || '未命名学生' }}</text>
|
||||||
|
<view class="progress-row">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill" :class="{ full: studentRate(student) >= 100 }" :style="{ width: studentRate(student) + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="progress-text">{{ studentRate(student) }}%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="stat-grid">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ student.totalCount || 0 }}</text>
|
||||||
|
<text class="stat-label">总数</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item done">
|
||||||
|
<text class="stat-value">{{ student.completedCount || 0 }}</text>
|
||||||
|
<text class="stat-label">完成</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item undone">
|
||||||
|
<text class="stat-value">{{ student.uncompletedCount || 0 }}</text>
|
||||||
|
<text class="stat-label">未完成</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="student.subjectStats?.length" class="subject-wrap">
|
||||||
|
<view
|
||||||
|
v-for="item in student.subjectStats"
|
||||||
|
:key="item.subjectId"
|
||||||
|
class="subject-chip"
|
||||||
|
:class="{ full: item.homeworkCount > 0 && item.completedCount >= item.homeworkCount }"
|
||||||
|
>
|
||||||
|
<text>{{ item.subjectName }}</text>
|
||||||
|
<text>{{ item.completedCount }}/{{ item.homeworkCount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-space"></view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="state-card in-scroll">
|
||||||
|
<xy-Empty type="task" content="暂无报告数据" />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import { getClassHomeworkReport, type ClassHomeworkReportData, type ClassHomeworkStudentItem } from '@/api/teacher';
|
||||||
|
|
||||||
|
const subjectMap: Record<number, string> = {
|
||||||
|
2: '语文',
|
||||||
|
3: '数学',
|
||||||
|
4: '英语',
|
||||||
|
5: '科学',
|
||||||
|
6: '物理',
|
||||||
|
7: '化学',
|
||||||
|
8: '历史',
|
||||||
|
9: '道德与法治',
|
||||||
|
10: '地理',
|
||||||
|
11: '生物',
|
||||||
|
12: '政治',
|
||||||
|
13: '信息',
|
||||||
|
14: '通用',
|
||||||
|
15: '日语',
|
||||||
|
};
|
||||||
|
|
||||||
|
const classId = ref('');
|
||||||
|
const className = ref('');
|
||||||
|
const subjectIds = ref<number[]>([]);
|
||||||
|
const startTime = ref('');
|
||||||
|
const endTime = ref('');
|
||||||
|
const reportData = ref<ClassHomeworkReportData | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref('');
|
||||||
|
|
||||||
|
const studentCount = computed(() => reportData.value?.studentList?.length || 0);
|
||||||
|
const totalHomework = computed(() => (reportData.value?.studentList || []).reduce((sum, item) => sum + (item.totalCount || 0), 0));
|
||||||
|
const completedHomework = computed(() => (reportData.value?.studentList || []).reduce((sum, item) => sum + (item.completedCount || 0), 0));
|
||||||
|
const completeRate = computed(() => totalHomework.value ? `${Math.round(completedHomework.value / totalHomework.value * 100)}%` : '0%');
|
||||||
|
|
||||||
|
const filterSummary = computed(() => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (subjectIds.value.length) {
|
||||||
|
const labels = subjectIds.value.map((id) => subjectMap[id]).filter(Boolean);
|
||||||
|
parts.push(labels.length ? labels.join('、') : `${subjectIds.value.length}个学科`);
|
||||||
|
} else {
|
||||||
|
parts.push('全部学科');
|
||||||
|
}
|
||||||
|
if (startTime.value || endTime.value) {
|
||||||
|
parts.push(`${startTime.value || '...'} 至 ${endTime.value || '...'}`);
|
||||||
|
}
|
||||||
|
return parts.join(' · ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseSubjectIds = (value: any) => {
|
||||||
|
if (!value) return [];
|
||||||
|
return decodeURIComponent(String(value)).split(',').map(Number).filter((item) => !Number.isNaN(item));
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstName = (name?: string) => (name || '学').slice(0, 1);
|
||||||
|
const studentRate = (student: ClassHomeworkStudentItem) => {
|
||||||
|
if (!student.totalCount) return 0;
|
||||||
|
return Math.min(100, Math.round(student.completedCount / student.totalCount * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchReport = async () => {
|
||||||
|
if (!classId.value) {
|
||||||
|
errorMsg.value = '缺少班级参数,请重新筛选';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
errorMsg.value = '';
|
||||||
|
try {
|
||||||
|
const res = await getClassHomeworkReport({
|
||||||
|
classId: classId.value,
|
||||||
|
subjectIds: subjectIds.value.length ? subjectIds.value : undefined,
|
||||||
|
startTime: startTime.value || undefined,
|
||||||
|
endTime: endTime.value || undefined,
|
||||||
|
});
|
||||||
|
reportData.value = res.data;
|
||||||
|
if (!className.value) className.value = res.data?.className || '';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取学情报告失败', err);
|
||||||
|
errorMsg.value = '学情报告加载失败,请稍后重试';
|
||||||
|
reportData.value = null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBackFilter = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
classId.value = options?.classId ? decodeURIComponent(String(options.classId)) : '';
|
||||||
|
className.value = options?.className ? decodeURIComponent(String(options.className)) : '';
|
||||||
|
subjectIds.value = parseSubjectIds(options?.subjectIds);
|
||||||
|
startTime.value = options?.startTime ? decodeURIComponent(String(options.startTime)) : '';
|
||||||
|
endTime.value = options?.endTime ? decodeURIComponent(String(options.endTime)) : '';
|
||||||
|
fetchReport();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.report-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #eef2ff 0%, #f8fafc 60%, #ffffff 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
margin: 20rpx 28rpx 0;
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-title {
|
||||||
|
display: block;
|
||||||
|
color: #312e81;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-action {
|
||||||
|
padding: 12rpx 22rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #4f46e5;
|
||||||
|
background: #eef2ff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-scroll {
|
||||||
|
height: calc(100vh - 160rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20rpx 28rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 28rpx 16rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-value {
|
||||||
|
color: #4f46e5;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 82rpx;
|
||||||
|
height: 82rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: linear-gradient(180deg, #818cf8 0%, #4f46e5 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-name {
|
||||||
|
display: block;
|
||||||
|
color: #312e81;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-row {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eef2ff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f59e0b;
|
||||||
|
|
||||||
|
&.full {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
width: 72rpx;
|
||||||
|
text-align: right;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 0;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rpx;
|
||||||
|
|
||||||
|
&.done .stat-value {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.undone .stat-value {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: #312e81;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-chip {
|
||||||
|
display: flex;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 12rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #475569;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
|
||||||
|
&.full {
|
||||||
|
color: #16a34a;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
height: 520rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.in-scroll {
|
||||||
|
height: 420rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-space {
|
||||||
|
height: calc(60rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
639
src/pages/teacher/index/index.vue
Normal file
639
src/pages/teacher/index/index.vue
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
<template>
|
||||||
|
<view class="teacher-page">
|
||||||
|
<view class="bg-decor bg-decor-a"></view>
|
||||||
|
<view class="bg-decor bg-decor-b"></view>
|
||||||
|
|
||||||
|
<scroll-view class="page-scroll" scroll-y enhanced :show-scrollbar="false">
|
||||||
|
<view class="content">
|
||||||
|
<view class="profile-card clay-card">
|
||||||
|
<image class="avatar" :src="avatarUrl" mode="aspectFill" />
|
||||||
|
<view class="profile-main">
|
||||||
|
<text class="hello">您好,{{ teacherName }}</text>
|
||||||
|
<text class="school ellipsis">{{ teacherInfo.schoolName || '欢迎使用学小乐AI教师端' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="logout-btn" @click="handleLogout">退出</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="quick-grid">
|
||||||
|
<view class="quick-card clay-card" @click="goReportFilter">
|
||||||
|
<view class="quick-icon">
|
||||||
|
<text class="quick-icon-text">报</text>
|
||||||
|
</view>
|
||||||
|
<text class="quick-title">学情报告</text>
|
||||||
|
<text class="quick-desc">查看班级作业完成情况</text>
|
||||||
|
</view>
|
||||||
|
<view class="quick-card clay-card" @click="goWeakFilter">
|
||||||
|
<view class="quick-icon orange">
|
||||||
|
<text class="quick-icon-text orange">弱</text>
|
||||||
|
</view>
|
||||||
|
<text class="quick-title">薄弱知识点</text>
|
||||||
|
<text class="quick-desc">定位 Top10 高频薄弱点</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-card clay-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<text class="section-title">AI 批改看板</text>
|
||||||
|
<text class="section-sub">按班级、学科和日期查看效率数据</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<picker :range="classNames" :value="classPickerIndex" @change="onClassPickerChange">
|
||||||
|
<view class="picker-row">
|
||||||
|
<text class="picker-label">班级</text>
|
||||||
|
<text class="picker-value ellipsis">{{ selectedClassName || '请选择班级' }}</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<scroll-view v-if="subjectList.length" class="subject-scroll" scroll-x enhanced :show-scrollbar="false">
|
||||||
|
<view class="subject-list">
|
||||||
|
<view
|
||||||
|
v-for="item in subjectList"
|
||||||
|
:key="item.value"
|
||||||
|
class="subject-pill"
|
||||||
|
:class="{ active: selectedSubjectIds.includes(item.value) }"
|
||||||
|
@click="toggleSubject(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="date-row">
|
||||||
|
<picker mode="date" :value="startTime" @change="onStartChange">
|
||||||
|
<view class="date-box">{{ startTime || '开始日期' }}</view>
|
||||||
|
</picker>
|
||||||
|
<text class="date-sep">至</text>
|
||||||
|
<picker mode="date" :value="endTime" @change="onEndChange">
|
||||||
|
<view class="date-box">{{ endTime || '结束日期' }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="query-btn clay-button" @click="fetchOverview">查询数据</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<xy-Loading text="加载看板中..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!selectedClassId" class="state-card">
|
||||||
|
<xy-Empty type="task" content="请选择班级查看看板" />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorMsg" class="state-card">
|
||||||
|
<xy-Empty type="network" :content="errorMsg" />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="overviewData" class="overview">
|
||||||
|
<view class="metric-grid">
|
||||||
|
<view class="metric-card clay-card">
|
||||||
|
<text class="metric-value">{{ overviewData.aiPaperCount || 0 }}</text>
|
||||||
|
<text class="metric-label">批改试卷</text>
|
||||||
|
</view>
|
||||||
|
<view class="metric-card clay-card">
|
||||||
|
<text class="metric-value">{{ overviewData.aiTitleCount || 0 }}</text>
|
||||||
|
<text class="metric-label">批改题目</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="save-card clay-card">
|
||||||
|
<text class="save-title">节省批阅时间</text>
|
||||||
|
<text class="save-text">{{ saveTimeText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grade-card clay-card">
|
||||||
|
<view class="section-head compact">
|
||||||
|
<text class="section-title">成绩分布</text>
|
||||||
|
</view>
|
||||||
|
<view class="grade-list">
|
||||||
|
<view v-for="item in gradeItems" :key="item.label" class="grade-item">
|
||||||
|
<view class="grade-top">
|
||||||
|
<text class="grade-name">{{ item.label }}</text>
|
||||||
|
<text class="grade-count">{{ item.count }}人 · {{ formatRate(item.rate) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="grade-bar">
|
||||||
|
<view class="grade-fill" :class="item.type" :style="{ width: rateWidth(item.rate) }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-space"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useTeacherStore } from '@/state/modules/teacher';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import {
|
||||||
|
getAiCorrectionOverview,
|
||||||
|
getClassListByTeacher,
|
||||||
|
getSubjectListByClassId,
|
||||||
|
type AiCorrectionOverview,
|
||||||
|
} from '@/api/teacher';
|
||||||
|
|
||||||
|
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
||||||
|
const defaultAvatar = `${OSS_URL}/urm/logo.png`;
|
||||||
|
|
||||||
|
const teacherStore = useTeacherStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { teacherInfo, schoolId } = storeToRefs(teacherStore);
|
||||||
|
|
||||||
|
const classList = ref<{ id: string; name: string }[]>([]);
|
||||||
|
const classPickerIndex = ref(0);
|
||||||
|
const selectedClassId = ref('');
|
||||||
|
const subjectList = ref<{ value: number; label: string }[]>([]);
|
||||||
|
const selectedSubjectIds = ref<number[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref('');
|
||||||
|
const overviewData = ref<AiCorrectionOverview | null>(null);
|
||||||
|
|
||||||
|
const avatarUrl = computed(() => teacherInfo.value.avatarUrl || teacherInfo.value.avatar || defaultAvatar);
|
||||||
|
const teacherName = computed(() => teacherInfo.value.nickName || teacherInfo.value.name || teacherInfo.value.account || '老师');
|
||||||
|
const classNames = computed(() => classList.value.map((item) => item.name));
|
||||||
|
const selectedClassName = computed(() => classList.value.find((item) => item.id === selectedClassId.value)?.name || '');
|
||||||
|
|
||||||
|
const getTodayStr = () => {
|
||||||
|
const d = new Date();
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
const startTime = ref(getTodayStr());
|
||||||
|
const endTime = ref(getTodayStr());
|
||||||
|
|
||||||
|
const saveTimeText = computed(() => {
|
||||||
|
const mins = overviewData.value?.teacherSaveTimeMinutes || 0;
|
||||||
|
if (mins <= 0) return 'AI 小助手已准备好为您分担批阅工作。';
|
||||||
|
if (mins >= 1440) return `AI 小助手已为您节省 ${(mins / 1440).toFixed(1).replace(/\.0$/, '')} 天。`;
|
||||||
|
if (mins >= 60) return `AI 小助手已为您节省 ${(mins / 60).toFixed(1).replace(/\.0$/, '')} 小时。`;
|
||||||
|
return `AI 小助手已为您节省 ${mins} 分钟。`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const gradeItems = computed(() => {
|
||||||
|
const data = overviewData.value;
|
||||||
|
return [
|
||||||
|
{ label: '优秀', count: data?.excellentCount || 0, rate: data?.excellentRate || 0, type: 'excellent' },
|
||||||
|
{ label: '良好', count: data?.goodCount || 0, rate: data?.goodRate || 0, type: 'good' },
|
||||||
|
{ label: '合格', count: data?.passCount || 0, rate: data?.passRate || 0, type: 'pass' },
|
||||||
|
{ label: '未提交', count: data?.unsubmittedCount || 0, rate: data?.unsubmittedRate || 0, type: 'unsubmit' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeRate = (rate?: number | null) => {
|
||||||
|
if (!rate) return 0;
|
||||||
|
return rate > 1 ? rate : rate * 100;
|
||||||
|
};
|
||||||
|
const formatRate = (rate?: number | null) => `${normalizeRate(rate).toFixed(1).replace(/\.0$/, '')}%`;
|
||||||
|
const rateWidth = (rate?: number | null) => `${Math.min(100, Math.max(0, normalizeRate(rate)))}%`;
|
||||||
|
|
||||||
|
const normalizeClassList = (data: any) => {
|
||||||
|
const rows = Array.isArray(data) ? data : data?.rows || data?.list || [];
|
||||||
|
return rows.map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
id: String(item.id ?? item.classId ?? ''),
|
||||||
|
name: item.name ?? item.className ?? '未命名班级',
|
||||||
|
})).filter((item: any) => item.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTeacherInfo = async () => {
|
||||||
|
if (!userStore.token) return;
|
||||||
|
await teacherStore.getLoginUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchClassList = async () => {
|
||||||
|
const res = await getClassListByTeacher();
|
||||||
|
classList.value = normalizeClassList(res.data);
|
||||||
|
if (!selectedClassId.value && classList.value.length) {
|
||||||
|
selectedClassId.value = classList.value[0].id;
|
||||||
|
classPickerIndex.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(selectedClassId, async (value) => {
|
||||||
|
subjectList.value = [];
|
||||||
|
selectedSubjectIds.value = [];
|
||||||
|
overviewData.value = null;
|
||||||
|
if (!value) return;
|
||||||
|
try {
|
||||||
|
const res = await getSubjectListByClassId(value);
|
||||||
|
const rows = Array.isArray(res.data) ? res.data : [];
|
||||||
|
subjectList.value = rows.map((item: any) => ({
|
||||||
|
value: Number(item.subjectId ?? item.id ?? item.value),
|
||||||
|
label: item.subjectName ?? item.name ?? item.label ?? '',
|
||||||
|
})).filter((item) => item.value && item.label);
|
||||||
|
const teacherSubjectId = Number(teacherInfo.value.subjectId);
|
||||||
|
if (teacherSubjectId && subjectList.value.some((item) => item.value === teacherSubjectId)) {
|
||||||
|
selectedSubjectIds.value = [teacherSubjectId];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取学科失败', err);
|
||||||
|
}
|
||||||
|
fetchOverview();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClassPickerChange = (e: any) => {
|
||||||
|
const index = Number(e.detail.value);
|
||||||
|
const item = classList.value[index];
|
||||||
|
if (!item) return;
|
||||||
|
classPickerIndex.value = index;
|
||||||
|
selectedClassId.value = item.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSubject = (value: number) => {
|
||||||
|
selectedSubjectIds.value = selectedSubjectIds.value.includes(value)
|
||||||
|
? selectedSubjectIds.value.filter((item) => item !== value)
|
||||||
|
: [...selectedSubjectIds.value, value];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStartChange = (e: any) => { startTime.value = e.detail?.value || ''; };
|
||||||
|
const onEndChange = (e: any) => { endTime.value = e.detail?.value || ''; };
|
||||||
|
|
||||||
|
const fetchOverview = async () => {
|
||||||
|
if (!selectedClassId.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
errorMsg.value = '';
|
||||||
|
try {
|
||||||
|
const res = await getAiCorrectionOverview({
|
||||||
|
classId: selectedClassId.value,
|
||||||
|
schoolId: schoolId.value || undefined,
|
||||||
|
subjectIds: selectedSubjectIds.value.length ? selectedSubjectIds.value : undefined,
|
||||||
|
startTime: startTime.value,
|
||||||
|
endTime: endTime.value,
|
||||||
|
});
|
||||||
|
overviewData.value = res.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取看板失败', err);
|
||||||
|
errorMsg.value = '看板数据加载失败,请稍后重试';
|
||||||
|
overviewData.value = null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goReportFilter = () => {
|
||||||
|
uni.navigateTo({ url: '/pages/teacher/homework/filter' });
|
||||||
|
};
|
||||||
|
const goWeakFilter = () => {
|
||||||
|
uni.navigateTo({ url: '/pages/teacher/homework/filter?target=weakKnowledge' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定退出老师端吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return;
|
||||||
|
teacherStore.clear();
|
||||||
|
await userStore.logout();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
try {
|
||||||
|
await fetchTeacherInfo();
|
||||||
|
if (!classList.value.length) await fetchClassList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('老师首页初始化失败', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.teacher-page {
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #eef2ff 0%, #f5f7ff 54%, #ffffff 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-decor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(60rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-decor-a {
|
||||||
|
width: 360rpx;
|
||||||
|
height: 360rpx;
|
||||||
|
top: 120rpx;
|
||||||
|
right: -140rpx;
|
||||||
|
background: rgba(129, 140, 248, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-decor-b {
|
||||||
|
width: 320rpx;
|
||||||
|
height: 320rpx;
|
||||||
|
bottom: 140rpx;
|
||||||
|
left: -130rpx;
|
||||||
|
background: rgba(34, 197, 94, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24rpx 28rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.school {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
padding: 14rpx 22rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fff1f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-grid,
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-card {
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #eef2ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
|
||||||
|
&.orange {
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon-text {
|
||||||
|
color: #4f46e5;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
|
||||||
|
&.orange {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card,
|
||||||
|
.grade-card,
|
||||||
|
.save-card {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #312e81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-sub {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2rpx solid #e0e7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-value {
|
||||||
|
max-width: 420rpx;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-scroll {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-list {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-pill {
|
||||||
|
padding: 14rpx 24rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-box {
|
||||||
|
min-width: 210rpx;
|
||||||
|
padding: 18rpx 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2rpx solid #e0e7ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-sep {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-btn {
|
||||||
|
height: 88rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
height: 420rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
min-height: 160rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 52rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-title {
|
||||||
|
display: block;
|
||||||
|
color: #16a34a;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #334155;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-count {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-bar {
|
||||||
|
height: 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eef2ff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
transition: width 240ms ease;
|
||||||
|
|
||||||
|
&.excellent { background: #22c55e; }
|
||||||
|
&.good { background: #3b82f6; }
|
||||||
|
&.pass { background: #f59e0b; }
|
||||||
|
&.unsubmit { background: #ef4444; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-space {
|
||||||
|
height: calc(60rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
261
src/pages/teacher/weakKnowledge/index.vue
Normal file
261
src/pages/teacher/weakKnowledge/index.vue
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<template>
|
||||||
|
<view class="weak-page">
|
||||||
|
<view class="summary-card clay-card" @click="goBackFilter">
|
||||||
|
<view>
|
||||||
|
<text class="summary-title">{{ listData?.className || className || '薄弱知识点 Top10' }}</text>
|
||||||
|
<text class="summary-desc">{{ filterSummary }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="summary-action">修改</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<xy-Loading text="分析薄弱点中..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorMsg" class="state-card">
|
||||||
|
<xy-Empty type="network" :content="errorMsg" />
|
||||||
|
</view>
|
||||||
|
<scroll-view v-else class="list-scroll" scroll-y enhanced :show-scrollbar="false">
|
||||||
|
<view class="content">
|
||||||
|
<view v-if="!topList.length" class="state-card in-scroll">
|
||||||
|
<xy-Empty type="learn" content="暂无薄弱知识点数据" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-for="(item, index) in topList" :key="`${item.knowledgeId}-${index}`" class="knowledge-card clay-card">
|
||||||
|
<view class="rank" :class="`rank-${index + 1}`">{{ index + 1 }}</view>
|
||||||
|
<view class="knowledge-main">
|
||||||
|
<text class="knowledge-name ellipsis-2">{{ item.knowledgeName || '未命名知识点' }}</text>
|
||||||
|
<view class="meta-row">
|
||||||
|
<text v-if="item.subjectName" class="subject-tag">{{ item.subjectName }}</text>
|
||||||
|
<text class="meta danger">错 {{ item.errorCount || 0 }} 次</text>
|
||||||
|
<text class="meta">涉及 {{ item.titleCount || 0 }} 题</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="bottom-space"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useTeacherStore } from '@/state/modules/teacher';
|
||||||
|
import { getWeakKnowledgeTop10, type WeakKnowledgeItem, type WeakKnowledgeTop10Data } from '@/api/teacher';
|
||||||
|
|
||||||
|
const teacherStore = useTeacherStore();
|
||||||
|
const { schoolId } = storeToRefs(teacherStore);
|
||||||
|
|
||||||
|
const classId = ref('');
|
||||||
|
const className = ref('');
|
||||||
|
const subjectIds = ref<number[]>([]);
|
||||||
|
const startTime = ref('');
|
||||||
|
const endTime = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref('');
|
||||||
|
const listData = ref<WeakKnowledgeTop10Data | null>(null);
|
||||||
|
|
||||||
|
const topList = computed<WeakKnowledgeItem[]>(() => listData.value?.topWeakKnowledgeList || []);
|
||||||
|
const filterSummary = computed(() => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
parts.push(subjectIds.value.length ? `${subjectIds.value.length} 个学科` : '全部学科');
|
||||||
|
if (startTime.value || endTime.value) {
|
||||||
|
parts.push(`${startTime.value || '...'} 至 ${endTime.value || '...'}`);
|
||||||
|
}
|
||||||
|
return parts.join(' · ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseSubjectIds = (value: any) => {
|
||||||
|
if (!value) return [];
|
||||||
|
return decodeURIComponent(String(value)).split(',').map(Number).filter((item) => !Number.isNaN(item));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
if (!classId.value) {
|
||||||
|
errorMsg.value = '缺少班级参数,请重新筛选';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
errorMsg.value = '';
|
||||||
|
try {
|
||||||
|
const res = await getWeakKnowledgeTop10({
|
||||||
|
classId: classId.value,
|
||||||
|
schoolId: schoolId.value || undefined,
|
||||||
|
subjectIds: subjectIds.value.length ? subjectIds.value : undefined,
|
||||||
|
startTime: startTime.value || undefined,
|
||||||
|
endTime: endTime.value || undefined,
|
||||||
|
});
|
||||||
|
listData.value = res.data;
|
||||||
|
if (!className.value) className.value = res.data?.className || '';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取薄弱知识点失败', err);
|
||||||
|
errorMsg.value = '薄弱知识点加载失败,请稍后重试';
|
||||||
|
listData.value = null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBackFilter = () => {
|
||||||
|
uni.navigateBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
classId.value = options?.classId ? decodeURIComponent(String(options.classId)) : '';
|
||||||
|
className.value = options?.className ? decodeURIComponent(String(options.className)) : '';
|
||||||
|
subjectIds.value = parseSubjectIds(options?.subjectIds);
|
||||||
|
startTime.value = options?.startTime ? decodeURIComponent(String(options.startTime)) : '';
|
||||||
|
endTime.value = options?.endTime ? decodeURIComponent(String(options.endTime)) : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
try {
|
||||||
|
if (!schoolId.value) await teacherStore.getLoginUser();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('刷新老师信息失败', err);
|
||||||
|
}
|
||||||
|
fetchList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.weak-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #eef2ff 0%, #f8fafc 58%, #ffffff 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
margin: 20rpx 28rpx 0;
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-title {
|
||||||
|
display: block;
|
||||||
|
color: #312e81;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-action {
|
||||||
|
padding: 12rpx 22rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #4f46e5;
|
||||||
|
background: #eef2ff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-scroll {
|
||||||
|
height: calc(100vh - 160rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24rpx 28rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 22rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
width: 62rpx;
|
||||||
|
height: 62rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #c7d2fe;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.rank-1 {
|
||||||
|
background: linear-gradient(180deg, #fb7185 0%, #ef4444 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-2 {
|
||||||
|
background: linear-gradient(180deg, #fbbf24 0%, #f97316 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-3 {
|
||||||
|
background: linear-gradient(180deg, #60a5fa 0%, #3b82f6 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-name {
|
||||||
|
display: block;
|
||||||
|
color: #312e81;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 1.45;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-tag,
|
||||||
|
.meta {
|
||||||
|
padding: 8rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-tag {
|
||||||
|
color: #4f46e5;
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: #64748b;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
color: #ef4444;
|
||||||
|
background: #fff1f2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
height: 520rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.in-scroll {
|
||||||
|
height: 500rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-space {
|
||||||
|
height: calc(60rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
122
src/pages/wode/index.vue
Normal file
122
src/pages/wode/index.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<vin-cell title="基础弹框" @click="baseClick"></vin-cell>
|
||||||
|
<vin-dialog
|
||||||
|
v-model:visible="visible1"
|
||||||
|
title="基础弹框"
|
||||||
|
content="这是基础弹框。"
|
||||||
|
@cancel="onCancel"
|
||||||
|
@ok="onOk"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<vin-cell title="无标题弹框" @click="noTitleClick"></vin-cell>
|
||||||
|
<vin-dialog
|
||||||
|
v-model:visible="visible2"
|
||||||
|
content="这是无标题弹框。"
|
||||||
|
@cancel="onCancel"
|
||||||
|
@ok="onOk"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<vin-cell title="提示弹框" @click="tipsClick"></vin-cell>
|
||||||
|
<vin-dialog
|
||||||
|
v-model:visible="visible3"
|
||||||
|
no-cancel-btn
|
||||||
|
title="温馨提示"
|
||||||
|
content="这是提示弹框。"
|
||||||
|
@cancel="onCancel"
|
||||||
|
@ok="onOk"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<vin-cell title="底部按钮 垂直调用" @click="verticalClick"></vin-cell>
|
||||||
|
<vin-dialog
|
||||||
|
v-model:visible="visible5"
|
||||||
|
footer-direction="vertical"
|
||||||
|
teleport="#app"
|
||||||
|
title="温馨提示"
|
||||||
|
content="这是提示弹框。"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<vin-cell title="异步关闭" @click="componentClick"></vin-cell>
|
||||||
|
<vin-dialog
|
||||||
|
title="异步关闭"
|
||||||
|
:content="closeContent"
|
||||||
|
:visible="visible4"
|
||||||
|
@cancel="onCancel"
|
||||||
|
@ok="onOkAsync"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
const visible1 = ref(false);
|
||||||
|
const visible2 = ref(false);
|
||||||
|
const visible3 = ref(false);
|
||||||
|
const visible4 = ref(false);
|
||||||
|
const visible5 = ref(false);
|
||||||
|
const closeContent = ref('');
|
||||||
|
const sleep = () => new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
const countDown = (second: number) => `倒计时 ${second} 秒`;
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
console.log('event cancel');
|
||||||
|
};
|
||||||
|
const onOk = () => {
|
||||||
|
console.log('event ok');
|
||||||
|
};
|
||||||
|
const onOkAsync = () => {
|
||||||
|
sleep()
|
||||||
|
.then(() => {
|
||||||
|
closeContent.value = countDown(2);
|
||||||
|
return sleep();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
closeContent.value = countDown(1);
|
||||||
|
return sleep();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
closeContent.value = countDown(0);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
visible4.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseClick = (): void => {
|
||||||
|
visible1.value = true;
|
||||||
|
};
|
||||||
|
const noTitleClick = () => {
|
||||||
|
visible2.value = true;
|
||||||
|
};
|
||||||
|
const tipsClick = () => {
|
||||||
|
visible3.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentClick = () => {
|
||||||
|
closeContent.value = `点击确定时3s后关闭`;
|
||||||
|
visible4.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const verticalClick = () => {
|
||||||
|
visible5.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible1,
|
||||||
|
visible2,
|
||||||
|
visible3,
|
||||||
|
visible4,
|
||||||
|
visible5,
|
||||||
|
onCancel,
|
||||||
|
onOk,
|
||||||
|
closeContent,
|
||||||
|
onOkAsync,
|
||||||
|
baseClick,
|
||||||
|
noTitleClick,
|
||||||
|
componentClick,
|
||||||
|
tipsClick,
|
||||||
|
verticalClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
971
src/pages/wrong/index.vue
Normal file
971
src/pages/wrong/index.vue
Normal file
@ -0,0 +1,971 @@
|
|||||||
|
<template>
|
||||||
|
<view class="wrong-page">
|
||||||
|
<view class="bg-blur bg-blur-a"></view>
|
||||||
|
<view class="bg-blur bg-blur-b"></view>
|
||||||
|
|
||||||
|
<scroll-view class="page-scroll" scroll-y :show-scrollbar="false">
|
||||||
|
<view class="hero">
|
||||||
|
<view>
|
||||||
|
<view class="eyebrow">错题练习</view>
|
||||||
|
<view class="hero-title">把失分题变成提分点</view>
|
||||||
|
<view class="hero-desc">按学科、题型和掌握状态整理错题,复习时更聚焦。</view>
|
||||||
|
</view>
|
||||||
|
<view class="hero-badge">
|
||||||
|
<text class="hero-badge-num">{{ totalWrong }}</text>
|
||||||
|
<text class="hero-badge-text">道错题</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!pageLoading" class="content">
|
||||||
|
<view class="subject-grid">
|
||||||
|
<view
|
||||||
|
v-for="subject in subjectStats"
|
||||||
|
:key="subject.name"
|
||||||
|
class="subject-card"
|
||||||
|
:class="{ active: selectedSubject === subject.name }"
|
||||||
|
@click="selectSubject(subject.name)"
|
||||||
|
>
|
||||||
|
<view class="subject-icon" :class="subject.tone">
|
||||||
|
<text>{{ subject.shortName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="subject-main">
|
||||||
|
<view class="subject-name">{{ subject.name }}</view>
|
||||||
|
<view class="subject-count">{{ subject.total }} 道待复盘</view>
|
||||||
|
</view>
|
||||||
|
<view class="subject-meta">
|
||||||
|
<view>
|
||||||
|
<text>{{ subject.newCount }}</text>
|
||||||
|
<text>新增</text>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>{{ subject.reviewCount }}</text>
|
||||||
|
<text>复习本</text>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>{{ subject.masteredCount }}</text>
|
||||||
|
<text>已掌握</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tabs">
|
||||||
|
<view
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: activeTab === tab.key }"
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
>
|
||||||
|
<text>{{ tab.label }}</text>
|
||||||
|
<text class="tab-count">{{ getTabCount(tab.key) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-row">
|
||||||
|
<picker class="filter-picker" :range="gradeOptions" :value="gradeIndex" @change="onGradeChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ gradeOptions[gradeIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="filter-picker" :range="typeOptions" :value="typeIndex" @change="onTypeChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ typeOptions[typeIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="filter-picker" :range="examOptions" :value="examIndex" @change="onExamChange">
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text>{{ examOptions[examIndex] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="filteredQuestions.length" class="question-list">
|
||||||
|
<view v-for="item in filteredQuestions" :key="item.id" class="question-card">
|
||||||
|
<view class="question-head">
|
||||||
|
<view class="question-index">
|
||||||
|
<text>第{{ item.number }}题</text>
|
||||||
|
<text>满分{{ item.score }}分</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="item.status">{{ getStatusText(item.status) }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="exam-name">{{ item.examName }}</view>
|
||||||
|
<view class="question-title">
|
||||||
|
<rich-text :nodes="item.title" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="item.options.length" class="option-list">
|
||||||
|
<view v-for="option in item.options" :key="option.key" class="option-item">
|
||||||
|
<text class="option-key">{{ option.key }}</text>
|
||||||
|
<view class="option-value">
|
||||||
|
<rich-text :nodes="option.value" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="answer-tabs">
|
||||||
|
<view
|
||||||
|
class="answer-tab"
|
||||||
|
:class="{ active: item.openPanel === 'mine' }"
|
||||||
|
@click="item.openPanel = 'mine'"
|
||||||
|
>
|
||||||
|
我的作答
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="answer-tab"
|
||||||
|
:class="{ active: item.openPanel === 'analysis' }"
|
||||||
|
@click="item.openPanel = 'analysis'"
|
||||||
|
>
|
||||||
|
答案&解析
|
||||||
|
</view>
|
||||||
|
<view class="answer-tab action" @click="toggleReview(item)">
|
||||||
|
{{ item.inReviewBook ? '移出复习本' : '加入复习本' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="item.openPanel === 'mine'" class="info-box mine">
|
||||||
|
<view class="info-title">我的作答</view>
|
||||||
|
<view class="info-content">{{ item.myAnswer || '未作答' }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="info-box correct">
|
||||||
|
<view class="info-title">正确答案</view>
|
||||||
|
<view class="info-content">{{ item.answer || '暂无答案' }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.analysis" class="info-box analysis">
|
||||||
|
<view class="info-title">试题解析</view>
|
||||||
|
<view class="info-content">
|
||||||
|
<rich-text :nodes="item.analysis" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.knowledge" class="info-box knowledge">
|
||||||
|
<view class="info-title">知识点</view>
|
||||||
|
<view class="info-content">{{ item.knowledge }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="mark-btn" @click="markMastered(item)">
|
||||||
|
{{ item.status === 'mastered' ? '标为未掌握' : '标为已掌握' }}
|
||||||
|
</view>
|
||||||
|
<view class="practice-btn" @click="practiceAgain(item)">再练一次</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="empty-wrap">
|
||||||
|
<xy-Empty type="learn" content="当前筛选下暂无错题" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="loading-wrap">
|
||||||
|
<xy-Empty type="loading" content="正在整理错题..." />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="safe-bottom"></view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="home-float" @click="goHome">
|
||||||
|
<view class="home-roof"></view>
|
||||||
|
<view class="home-body"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!pageLoading && filteredQuestions.length" class="bottom-bar">
|
||||||
|
<view class="download-btn" @click="downloadWrongQuestions">一键下载错题</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import { getListCompleteRecord, getPaperRecordDetail } from '../../api/homework';
|
||||||
|
|
||||||
|
type WrongStatus = 'new' | 'review' | 'mastered';
|
||||||
|
type TabKey = 'all' | 'new' | 'mastered';
|
||||||
|
|
||||||
|
interface WrongOption {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WrongQuestion {
|
||||||
|
id: string;
|
||||||
|
recordId: string;
|
||||||
|
subject: string;
|
||||||
|
grade: string;
|
||||||
|
type: string;
|
||||||
|
examName: string;
|
||||||
|
number: number;
|
||||||
|
score: number;
|
||||||
|
title: string;
|
||||||
|
options: WrongOption[];
|
||||||
|
myAnswer: string;
|
||||||
|
answer: string;
|
||||||
|
analysis: string;
|
||||||
|
knowledge: string;
|
||||||
|
status: WrongStatus;
|
||||||
|
inReviewBook: boolean;
|
||||||
|
openPanel: 'mine' | 'analysis';
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLoading = ref(true);
|
||||||
|
const wrongQuestions = ref<WrongQuestion[]>([]);
|
||||||
|
const selectedSubject = ref('全部');
|
||||||
|
const activeTab = ref<TabKey>('all');
|
||||||
|
const gradeIndex = ref(0);
|
||||||
|
const typeIndex = ref(0);
|
||||||
|
const examIndex = ref(0);
|
||||||
|
|
||||||
|
const tabs: { key: TabKey; label: string }[] = [
|
||||||
|
{ key: 'all', label: '全部错题' },
|
||||||
|
{ key: 'new', label: '未掌握' },
|
||||||
|
{ key: 'mastered', label: '已掌握' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const subjectTones = ['blue', 'green', 'orange', 'rose', 'violet'];
|
||||||
|
|
||||||
|
const totalWrong = computed(() => wrongQuestions.value.length);
|
||||||
|
|
||||||
|
const subjectStats = computed(() => {
|
||||||
|
const subjects = ['全部', ...Array.from(new Set(wrongQuestions.value.map((item) => item.subject).filter(Boolean)))];
|
||||||
|
return subjects.map((name, index) => {
|
||||||
|
const list = name === '全部' ? wrongQuestions.value : wrongQuestions.value.filter((item) => item.subject === name);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
shortName: name === '全部' ? '全' : name.slice(0, 1),
|
||||||
|
tone: subjectTones[index % subjectTones.length],
|
||||||
|
total: list.length,
|
||||||
|
newCount: list.filter((item) => item.status === 'new').length,
|
||||||
|
reviewCount: list.filter((item) => item.inReviewBook).length,
|
||||||
|
masteredCount: list.filter((item) => item.status === 'mastered').length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const scopedQuestions = computed(() => {
|
||||||
|
let list = wrongQuestions.value;
|
||||||
|
if (selectedSubject.value !== '全部') {
|
||||||
|
list = list.filter((item) => item.subject === selectedSubject.value);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
const gradeOptions = computed(() => ['不限年级', ...Array.from(new Set(scopedQuestions.value.map((item) => item.grade).filter(Boolean)))]);
|
||||||
|
const typeOptions = computed(() => ['不限类型', ...Array.from(new Set(scopedQuestions.value.map((item) => item.type).filter(Boolean)))]);
|
||||||
|
const examOptions = computed(() => ['选择考试', ...Array.from(new Set(scopedQuestions.value.map((item) => item.examName).filter(Boolean)))]);
|
||||||
|
|
||||||
|
const filteredQuestions = computed(() => {
|
||||||
|
const grade = gradeOptions.value[gradeIndex.value];
|
||||||
|
const type = typeOptions.value[typeIndex.value];
|
||||||
|
const exam = examOptions.value[examIndex.value];
|
||||||
|
return scopedQuestions.value.filter((item) => {
|
||||||
|
const tabMatched = activeTab.value === 'all'
|
||||||
|
|| (activeTab.value === 'new' && item.status !== 'mastered')
|
||||||
|
|| (activeTab.value === 'mastered' && item.status === 'mastered');
|
||||||
|
const gradeMatched = !grade || grade === '不限年级' || item.grade === grade;
|
||||||
|
const typeMatched = !type || type === '不限类型' || item.type === type;
|
||||||
|
const examMatched = !exam || exam === '选择考试' || item.examName === exam;
|
||||||
|
return tabMatched && gradeMatched && typeMatched && examMatched;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTabCount = (key: TabKey) => {
|
||||||
|
if (key === 'all') return scopedQuestions.value.length;
|
||||||
|
if (key === 'new') return scopedQuestions.value.filter((item) => item.status !== 'mastered').length;
|
||||||
|
return scopedQuestions.value.filter((item) => item.status === 'mastered').length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectSubject = (name: string) => {
|
||||||
|
selectedSubject.value = name;
|
||||||
|
gradeIndex.value = 0;
|
||||||
|
typeIndex.value = 0;
|
||||||
|
examIndex.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGradeChange = (e: any) => { gradeIndex.value = Number(e.detail.value); };
|
||||||
|
const onTypeChange = (e: any) => { typeIndex.value = Number(e.detail.value); };
|
||||||
|
const onExamChange = (e: any) => { examIndex.value = Number(e.detail.value); };
|
||||||
|
|
||||||
|
const normalizeHtml = (html?: string) => {
|
||||||
|
if (!html) return '';
|
||||||
|
return String(html)
|
||||||
|
.replace(/http:\/\//gi, 'https://')
|
||||||
|
.replace(/<img(?![^>]*formula-img)[^>]*\/?>/gi, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAnswer = (value: any) => {
|
||||||
|
if (Array.isArray(value)) return value.join('、');
|
||||||
|
if (value == null) return '';
|
||||||
|
return String(value).replace(/^\[|\]$/g, '').replace(/"/g, '').replace(/,/g, '、');
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseOptions = (raw: any): WrongOption[] => {
|
||||||
|
let list = raw;
|
||||||
|
if (typeof list === 'string') {
|
||||||
|
try {
|
||||||
|
list = JSON.parse(list);
|
||||||
|
} catch {
|
||||||
|
list = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(list)) return [];
|
||||||
|
return list.map((item, index) => ({
|
||||||
|
key: typeof item === 'string' ? String.fromCharCode(65 + index) : (item?.key || item?.optionKey || item?.label || String.fromCharCode(65 + index)),
|
||||||
|
value: normalizeHtml(typeof item === 'string' ? item : (item?.value || item?.optionValue || '')),
|
||||||
|
})).filter((item) => item.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGradeText = (record: any, item: any) => {
|
||||||
|
return item.gradeName || item.grade || record.gradeName || record.grade || '未分年级';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWrongQuestion = (item: any) => {
|
||||||
|
const hasAnswer = Boolean(item.submitAnswer || item.submitAnswerPic);
|
||||||
|
const correct = item.correctResult === 1 || item.correctResult === true || item.markFlag === 1 || item.markFlag === true;
|
||||||
|
return hasAnswer && !correct;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapQuestion = (record: any, item: any, index: number): WrongQuestion => ({
|
||||||
|
id: String(item.itemId || item.id || `${record.id}-${index}`),
|
||||||
|
recordId: String(record.id || record.recordId || ''),
|
||||||
|
subject: item.subjectName || record.subjectName || '其他',
|
||||||
|
grade: getGradeText(record, item),
|
||||||
|
type: item.titleChannelTypeName || item.chanelTypeName || '综合题',
|
||||||
|
examName: record.paperName || record.jobName || item.paperName || '未命名考试',
|
||||||
|
number: Number(item.titleSort || item.sort || index + 1),
|
||||||
|
score: Number(item.titleScore || item.score || 0),
|
||||||
|
title: normalizeHtml(item.titleMu || item.title || item.pidTitle || '暂无题干'),
|
||||||
|
options: parseOptions(item.optionsMu ?? item.options),
|
||||||
|
myAnswer: normalizeAnswer(item.submitAnswer || item.submitAnswerPic),
|
||||||
|
answer: normalizeAnswer(item.answerMu || item.answer),
|
||||||
|
analysis: normalizeHtml(item.titleAnalyzeMu || item.titleAnalyze || item.aiAnalyze),
|
||||||
|
knowledge: item.knowledgeName || item.knowledge || item.knowledgePointName || '待归类',
|
||||||
|
status: 'new',
|
||||||
|
inReviewBook: false,
|
||||||
|
openPanel: 'analysis',
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadWrongQuestions = async () => {
|
||||||
|
pageLoading.value = true;
|
||||||
|
try {
|
||||||
|
const recordsRes: any = await getListCompleteRecord();
|
||||||
|
const records = Array.isArray(recordsRes?.data) ? recordsRes.data : Array.isArray(recordsRes) ? recordsRes : [];
|
||||||
|
const detailRecords = await Promise.all(
|
||||||
|
records.slice(0, 20).map(async (record: any) => {
|
||||||
|
try {
|
||||||
|
const detailRes: any = await getPaperRecordDetail({
|
||||||
|
recordId: record.id || record.recordId,
|
||||||
|
reportFlag: 1,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
record,
|
||||||
|
questions: detailRes?.data?.subjectTitleVoList || [],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[wrong] detail err', err);
|
||||||
|
return { record, questions: [] };
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
wrongQuestions.value = detailRecords.reduce<WrongQuestion[]>((list, { record, questions }) => {
|
||||||
|
const current = questions
|
||||||
|
.filter(isWrongQuestion)
|
||||||
|
.map((item: any, index: number) => mapQuestion(record, item, index));
|
||||||
|
return list.concat(current);
|
||||||
|
}, []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[wrong] load err', err);
|
||||||
|
uni.showToast({ title: '错题加载失败', icon: 'none' });
|
||||||
|
wrongQuestions.value = [];
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: WrongStatus) => {
|
||||||
|
if (status === 'mastered') return '已掌握';
|
||||||
|
if (status === 'review') return '复习中';
|
||||||
|
return '未掌握';
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleReview = (item: WrongQuestion) => {
|
||||||
|
item.inReviewBook = !item.inReviewBook;
|
||||||
|
if (item.inReviewBook && item.status === 'new') item.status = 'review';
|
||||||
|
if (!item.inReviewBook && item.status === 'review') item.status = 'new';
|
||||||
|
uni.showToast({ title: item.inReviewBook ? '已加入复习本' : '已移出复习本', icon: 'none' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const markMastered = (item: WrongQuestion) => {
|
||||||
|
item.status = item.status === 'mastered' ? 'new' : 'mastered';
|
||||||
|
if (item.status === 'mastered') item.inReviewBook = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const practiceAgain = (item: WrongQuestion) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/doWork/index?recordId=${item.recordId}&reportFlag=1&idx=${Math.max(item.number - 1, 0)}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadWrongQuestions = () => {
|
||||||
|
uni.showToast({ title: '下载功能对接中', icon: 'none' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
uni.reLaunch({ url: '/pages/index/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
loadWrongQuestions();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wrong-page {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #ecfeff 0%, #f8fafc 42%, #ffffff 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blur {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(56rpx);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blur-a {
|
||||||
|
top: 40rpx;
|
||||||
|
right: -120rpx;
|
||||||
|
width: 320rpx;
|
||||||
|
height: 320rpx;
|
||||||
|
background: rgba(34, 211, 238, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blur-b {
|
||||||
|
top: 520rpx;
|
||||||
|
left: -140rpx;
|
||||||
|
width: 300rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
background: rgba(34, 197, 94, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 40rpx 32rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 8rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: #0e7490;
|
||||||
|
background: rgba(207, 250, 254, 0.9);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 46rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
max-width: 460rpx;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
width: 156rpx;
|
||||||
|
height: 156rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
||||||
|
box-shadow: 0 18rpx 34rpx rgba(8, 145, 178, 0.22), inset 0 -6rpx 0 rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge-num {
|
||||||
|
font-size: 46rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge-text {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 96rpx 1fr;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border: 2rpx solid rgba(226, 232, 240, 0.9);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 14rpx 28rpx rgba(8, 145, 178, 0.08);
|
||||||
|
transition: transform 160ms ease, border-color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-card:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-card.active {
|
||||||
|
border-color: #22d3ee;
|
||||||
|
box-shadow: 0 18rpx 34rpx rgba(8, 145, 178, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-icon {
|
||||||
|
grid-row: span 2;
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-icon.blue { background: linear-gradient(180deg, #38bdf8, #0284c7); }
|
||||||
|
.subject-icon.green { background: linear-gradient(180deg, #4ade80, #16a34a); }
|
||||||
|
.subject-icon.orange { background: linear-gradient(180deg, #fbbf24, #f97316); }
|
||||||
|
.subject-icon.rose { background: linear-gradient(180deg, #fb7185, #e11d48); }
|
||||||
|
.subject-icon.violet { background: linear-gradient(180deg, #a78bfa, #7c3aed); }
|
||||||
|
|
||||||
|
.subject-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-name {
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-count {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-meta view {
|
||||||
|
padding: 12rpx 10rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-meta text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-meta text:first-child {
|
||||||
|
color: #0891b2;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-meta text:last-child {
|
||||||
|
margin-top: 2rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(226, 232, 240, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
height: 66rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
||||||
|
box-shadow: 0 10rpx 18rpx rgba(8, 145, 178, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-count {
|
||||||
|
min-width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: inherit;
|
||||||
|
background: rgba(255, 255, 255, 0.24);
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-picker {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill {
|
||||||
|
height: 70rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
color: #334155;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 7rpx solid transparent;
|
||||||
|
border-right: 7rpx solid transparent;
|
||||||
|
border-top: 9rpx solid #0891b2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 34rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-index {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-index text:first-child {
|
||||||
|
color: #f97316;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-index text:last-child {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 8rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag.new {
|
||||||
|
color: #b45309;
|
||||||
|
background: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag.review {
|
||||||
|
color: #0369a1;
|
||||||
|
background: #e0f2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag.mastered {
|
||||||
|
color: #15803d;
|
||||||
|
background: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-name {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-title {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-title :deep(image),
|
||||||
|
.option-value :deep(image),
|
||||||
|
.info-content :deep(image) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-list {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-key {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
margin-top: 24rpx;
|
||||||
|
border-top: 2rpx solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-tab {
|
||||||
|
min-height: 76rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-tab.active {
|
||||||
|
color: #0891b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-tab.action {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box.mine {
|
||||||
|
border-color: #fde68a;
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box.correct {
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box.analysis {
|
||||||
|
border-color: #bae6fd;
|
||||||
|
background: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box.knowledge {
|
||||||
|
border-color: #fde68a;
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 23rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
color: #334155;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark-btn,
|
||||||
|
.practice-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark-btn {
|
||||||
|
color: #0891b2;
|
||||||
|
background: #ecfeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-btn {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #22c55e 0%, #16a34a 100%);
|
||||||
|
box-shadow: 0 10rpx 18rpx rgba(22, 163, 74, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-wrap,
|
||||||
|
.loading-wrap {
|
||||||
|
height: 560rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-float {
|
||||||
|
position: fixed;
|
||||||
|
right: 32rpx;
|
||||||
|
bottom: 144rpx;
|
||||||
|
z-index: 10;
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(180deg, #38bdf8, #0284c7);
|
||||||
|
box-shadow: 0 12rpx 24rpx rgba(2, 132, 199, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-roof {
|
||||||
|
position: absolute;
|
||||||
|
top: 27rpx;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border-left: 5rpx solid #ffffff;
|
||||||
|
border-top: 5rpx solid #ffffff;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-body {
|
||||||
|
width: 34rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
border: 5rpx solid #ffffff;
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 0 0 6rpx 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9;
|
||||||
|
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 -10rpx 24rpx rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(180deg, #38bdf8 0%, #0284c7 100%);
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-bottom {
|
||||||
|
height: 150rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
src/router/Routeinterception.ts
Normal file
53
src/router/Routeinterception.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 路由拦截:未登录用户访问需登录页面时,自动跳转到登录页
|
||||||
|
*/
|
||||||
|
import type { Router } from 'uni-mini-router';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import { getCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
// 免登录白名单(路由 name)
|
||||||
|
const whiteList = ['home', 'login', 'parent-home', 'parent-reports', 'parent-profile'];
|
||||||
|
|
||||||
|
export function userRouternext(router: Router) {
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
if (userStore.isLogin) {
|
||||||
|
const currentRole = getCache('role');
|
||||||
|
const targetRole = (to as any).meta?.role;
|
||||||
|
if (currentRole === 'teacher' && to.name === 'home') {
|
||||||
|
return next({
|
||||||
|
name: 'teacher-home',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (currentRole === 'parent' && to.name === 'home') {
|
||||||
|
return next({
|
||||||
|
name: 'parent-home',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetRole && currentRole && targetRole !== currentRole) {
|
||||||
|
return next({
|
||||||
|
name: currentRole === 'teacher' ? 'teacher-home' : currentRole === 'parent' ? 'parent-home' : 'home',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
if (whiteList.includes(String(to.name))) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
// 未登录跳转到登录页,并保留目标地址
|
||||||
|
return next({
|
||||||
|
name: 'login',
|
||||||
|
query: { redirect: to.fullPath || (to.name as string) },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
if (userStore.isLogin && to?.name === 'login') {
|
||||||
|
const role = getCache('role');
|
||||||
|
if (!role) return;
|
||||||
|
router.replaceAll({ name: role === 'teacher' ? 'teacher-home' : role === 'parent' ? 'parent-home' : 'home' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
9
src/router/index.ts
Normal file
9
src/router/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createRouter } from 'uni-mini-router';
|
||||||
|
import { userRouternext } from './Routeinterception';
|
||||||
|
const router = createRouter({
|
||||||
|
routes: [...ROUTES], // 路由表信息
|
||||||
|
});
|
||||||
|
|
||||||
|
userRouternext(router); //使用路由拦截
|
||||||
|
|
||||||
|
export default router;
|
||||||
6
src/shime-uni.d.ts
vendored
Normal file
6
src/shime-uni.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export {};
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
type Hooks = App.AppInstance & Page.PageInstance;
|
||||||
|
interface ComponentCustomOptions extends Hooks {}
|
||||||
|
}
|
||||||
22
src/state/app.ts
Normal file
22
src/state/app.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
conunt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppStore = defineStore({
|
||||||
|
id: 'app-store',
|
||||||
|
state: (): AppState => ({
|
||||||
|
conunt: 1,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getNum(): number {
|
||||||
|
return this.conunt;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addnum() {
|
||||||
|
this.conunt++;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
5
src/state/index.ts
Normal file
5
src/state/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
const store = createPinia();
|
||||||
|
|
||||||
|
export default store;
|
||||||
47
src/state/modules/teacher.ts
Normal file
47
src/state/modules/teacher.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { getLoginUser as getLoginUserApi, type TeacherUserInfo } from '@/api/teacher';
|
||||||
|
import { setCache, getCache, removeCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
export type TeacherInfo = TeacherUserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 老师 store - 管理老师身份的登录用户信息
|
||||||
|
*/
|
||||||
|
export const useTeacherStore = defineStore('teacher', () => {
|
||||||
|
const teacherInfo = ref<TeacherInfo>(getCache('teacherInfo') || {});
|
||||||
|
const isTeacherLoggedIn = computed(() => !!teacherInfo.value?.id);
|
||||||
|
const schoolId = computed(() => String(teacherInfo.value?.schoolId || ''));
|
||||||
|
|
||||||
|
/** 拉取老师身份的当前登录用户 */
|
||||||
|
const getLoginUser = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getLoginUserApi();
|
||||||
|
teacherInfo.value = res.data || {};
|
||||||
|
setCache('teacherInfo', teacherInfo.value);
|
||||||
|
return teacherInfo.value;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[teacher] getLoginUser error', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTeacherInfo = (info: Partial<TeacherInfo>) => {
|
||||||
|
teacherInfo.value = { ...teacherInfo.value, ...info };
|
||||||
|
setCache('teacherInfo', teacherInfo.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
teacherInfo.value = {};
|
||||||
|
removeCache('teacherInfo');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
teacherInfo,
|
||||||
|
isTeacherLoggedIn,
|
||||||
|
schoolId,
|
||||||
|
getLoginUser,
|
||||||
|
updateTeacherInfo,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
|
});
|
||||||
119
src/state/modules/user.ts
Normal file
119
src/state/modules/user.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { setCache, getCache, removeCache } from '@/utils/cache';
|
||||||
|
import {
|
||||||
|
psdLogin as psdLoginApi,
|
||||||
|
getUserInfo as getUserInfoApi,
|
||||||
|
logout as logoutApi,
|
||||||
|
type AccountLoginParams,
|
||||||
|
} from '@/api/login';
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id?: string;
|
||||||
|
account?: string;
|
||||||
|
nickName?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
learnGradeId?: string;
|
||||||
|
schoolName?: string;
|
||||||
|
className?: string;
|
||||||
|
classId?: string;
|
||||||
|
diamond?: number;
|
||||||
|
experience?: number;
|
||||||
|
schoolClass?: any[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学生用户 store(兼容老接口)
|
||||||
|
*/
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const userInfo = ref<UserInfo>(getCache('userInfo') || {});
|
||||||
|
const token = ref<string>(getCache('token') || '');
|
||||||
|
|
||||||
|
const isLogin = computed(() => !!token.value);
|
||||||
|
const gradeId = computed(() => userInfo.value.learnGradeId || '');
|
||||||
|
|
||||||
|
/** 清空登录态与缓存 */
|
||||||
|
const clear = async () => {
|
||||||
|
token.value = '';
|
||||||
|
userInfo.value = {};
|
||||||
|
removeCache('token');
|
||||||
|
removeCache('userInfo');
|
||||||
|
removeCache('teacherInfo');
|
||||||
|
removeCache('role');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 拉取并解析用户信息 */
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getUserInfoApi();
|
||||||
|
const info: UserInfo = res.data || {};
|
||||||
|
|
||||||
|
// schoolClass 字段后端有时是 JSON 字符串
|
||||||
|
if (typeof info.schoolClass === 'string') {
|
||||||
|
try {
|
||||||
|
info.schoolClass = JSON.parse(info.schoolClass);
|
||||||
|
} catch {
|
||||||
|
info.schoolClass = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取学校班级到顶层方便展示
|
||||||
|
if (Array.isArray(info.schoolClass) && info.schoolClass.length) {
|
||||||
|
const school = info.schoolClass[0];
|
||||||
|
info.schoolName = school?.schoolName || '';
|
||||||
|
const cls = school?.classList?.[0];
|
||||||
|
if (cls) {
|
||||||
|
info.className = cls.className;
|
||||||
|
info.classId = cls.classId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo.value = info;
|
||||||
|
setCache('userInfo', info);
|
||||||
|
return info;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[user] fetchUserInfo error', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 账号密码登录 */
|
||||||
|
const accountLogin = async (data: AccountLoginParams) => {
|
||||||
|
await clear();
|
||||||
|
setCache('role', data.clientType === 'MANAGE' ? 'teacher' : 'student');
|
||||||
|
const res = await psdLoginApi(data);
|
||||||
|
token.value = res.data;
|
||||||
|
setCache('token', res.data);
|
||||||
|
await fetchUserInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 退出登录 */
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await logoutApi();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[user] logout api error', e);
|
||||||
|
}
|
||||||
|
await clear();
|
||||||
|
uni.reLaunch({ url: '/pages/index/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 部分更新本地用户信息 */
|
||||||
|
const updateUserInfo = (info: Partial<UserInfo>) => {
|
||||||
|
userInfo.value = { ...userInfo.value, ...info };
|
||||||
|
setCache('userInfo', userInfo.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
token,
|
||||||
|
isLogin,
|
||||||
|
gradeId,
|
||||||
|
accountLogin,
|
||||||
|
fetchUserInfo,
|
||||||
|
logout,
|
||||||
|
clear,
|
||||||
|
updateUserInfo,
|
||||||
|
};
|
||||||
|
});
|
||||||
BIN
src/static/back.jpg
Normal file
BIN
src/static/back.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
src/static/image/ab464291d2f39f4e38178ac21ca4f58.png
Normal file
BIN
src/static/image/ab464291d2f39f4e38178ac21ca4f58.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
src/static/logo.png
Normal file
BIN
src/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
110
src/style/global.scss
Normal file
110
src/style/global.scss
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/* ==========================================
|
||||||
|
学小乐 AI · 全局样式
|
||||||
|
设计风格: Claymorphism (软立体 / 童趣)
|
||||||
|
主色: Indigo + Green
|
||||||
|
========================================== */
|
||||||
|
|
||||||
|
/* ----------- CSS 变量 ----------- */
|
||||||
|
page {
|
||||||
|
--primary: #4f46e5;
|
||||||
|
--primary-light: #818cf8;
|
||||||
|
--primary-soft: #c7d2fe;
|
||||||
|
--primary-bg: #eef2ff;
|
||||||
|
--cta: #22c55e;
|
||||||
|
--cta-light: #4ade80;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--text: #312e81;
|
||||||
|
--text-secondary: #6366f1;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--bg: #f5f7ff;
|
||||||
|
--card: #ffffff;
|
||||||
|
--border: #e0e7ff;
|
||||||
|
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', PingFangSC-Regular,
|
||||||
|
'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------- 通用工具类 ----------- */
|
||||||
|
.flex { display: flex; }
|
||||||
|
.flex-col { flex-direction: column; }
|
||||||
|
.flex-center { display: flex; align-items: center; justify-content: center; }
|
||||||
|
.flex-between { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.flex-1 { flex: 1; min-width: 0; min-height: 0; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.justify-center { justify-content: center; }
|
||||||
|
.gap-12 { gap: 12rpx; }
|
||||||
|
.gap-16 { gap: 16rpx; }
|
||||||
|
.gap-24 { gap: 24rpx; }
|
||||||
|
|
||||||
|
/* 单行省略 */
|
||||||
|
.ellipsis {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 多行省略 */
|
||||||
|
.ellipsis-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------- Claymorphism 卡片基础类 ----------- */
|
||||||
|
.clay-card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 24rpx rgba(99, 102, 241, 0.08),
|
||||||
|
0 2rpx 8rpx rgba(99, 102, 241, 0.04),
|
||||||
|
inset 0 -2rpx 0 rgba(199, 210, 254, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clay-button {
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 16rpx rgba(79, 70, 229, 0.3),
|
||||||
|
inset 0 -4rpx 0 rgba(0, 0, 0, 0.12),
|
||||||
|
inset 0 2rpx 0 rgba(255, 255, 255, 0.3);
|
||||||
|
transition: all 200ms ease-out;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(2rpx);
|
||||||
|
box-shadow:
|
||||||
|
0 4rpx 8rpx rgba(79, 70, 229, 0.25),
|
||||||
|
inset 0 -2rpx 0 rgba(0, 0, 0, 0.12),
|
||||||
|
inset 0 2rpx 0 rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------- 滚动条隐藏 ----------- */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------- 重置 ----------- */
|
||||||
|
view, text, button, input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/style/index.css
Normal file
3
src/style/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
2
src/type.d.ts
vendored
Normal file
2
src/type.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
declare type anyObj = Record<string, any>;
|
||||||
|
declare const ROUTES: any[];
|
||||||
76
src/uni.scss
Normal file
76
src/uni.scss
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 这里是uni-app内置的常用样式变量
|
||||||
|
*
|
||||||
|
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||||
|
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||||
|
*
|
||||||
|
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 颜色变量 */
|
||||||
|
|
||||||
|
/* 行为相关颜色 */
|
||||||
|
$uni-color-primary: #007aff;
|
||||||
|
$uni-color-success: #4cd964;
|
||||||
|
$uni-color-warning: #f0ad4e;
|
||||||
|
$uni-color-error: #dd524d;
|
||||||
|
|
||||||
|
/* 文字基本颜色 */
|
||||||
|
$uni-text-color: #333; // 基本色
|
||||||
|
$uni-text-color-inverse: #fff; // 反色
|
||||||
|
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
|
||||||
|
$uni-text-color-placeholder: #808080;
|
||||||
|
$uni-text-color-disable: #c0c0c0;
|
||||||
|
|
||||||
|
/* 背景颜色 */
|
||||||
|
$uni-bg-color: #fff;
|
||||||
|
$uni-bg-color-grey: #f8f8f8;
|
||||||
|
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
|
||||||
|
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
$uni-border-color: #c8c7cc;
|
||||||
|
|
||||||
|
/* 尺寸变量 */
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$uni-font-size-sm: 12px;
|
||||||
|
$uni-font-size-base: 14px;
|
||||||
|
$uni-font-size-lg: 16;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$uni-img-size-sm: 20px;
|
||||||
|
$uni-img-size-base: 26px;
|
||||||
|
$uni-img-size-lg: 40px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
$uni-border-radius-sm: 2px;
|
||||||
|
$uni-border-radius-base: 3px;
|
||||||
|
$uni-border-radius-lg: 6px;
|
||||||
|
$uni-border-radius-circle: 50%;
|
||||||
|
|
||||||
|
/* 水平间距 */
|
||||||
|
$uni-spacing-row-sm: 5px;
|
||||||
|
$uni-spacing-row-base: 10px;
|
||||||
|
$uni-spacing-row-lg: 15px;
|
||||||
|
|
||||||
|
/* 垂直间距 */
|
||||||
|
$uni-spacing-col-sm: 4px;
|
||||||
|
$uni-spacing-col-base: 8px;
|
||||||
|
$uni-spacing-col-lg: 12px;
|
||||||
|
|
||||||
|
/* 透明度 */
|
||||||
|
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||||
|
|
||||||
|
/* 文章场景相关 */
|
||||||
|
$uni-color-title: #2c405a; // 文章标题颜色
|
||||||
|
$uni-font-size-title: 20px;
|
||||||
|
$uni-color-subtitle: #555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle: 18px;
|
||||||
|
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph: 15px;
|
||||||
320
src/uni_modules/qiun-data-charts/changelog.md
Normal file
320
src/uni_modules/qiun-data-charts/changelog.md
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
## 2.5.0-20230101(2023-01-01)
|
||||||
|
- 秋云图表组件 修改条件编译顺序,确保uniapp的cli方式的项目依赖不完整时可以正常显示
|
||||||
|
- 秋云图表组件 恢复props属性directory的使用,以修复vue3项目中,开启echarts后,echarts目录识别错误的bug
|
||||||
|
- uCharts.js 修复区域图、混合图只有一个数据时图表显示不正确的bug
|
||||||
|
- uCharts.js 修复折线图、区域图中时间轴类别图表tooltip指示点显示不正确的bug
|
||||||
|
- uCharts.js 修复x轴使用labelCount时,并且boundaryGap = 'justify' 并且关闭Y轴显示的时候,最后一个坐标值不显示的bug
|
||||||
|
- uCharts.js 修复折线图只有一组数据时 ios16 渲染颜色不正确的bug
|
||||||
|
- uCharts.js 修复玫瑰图半径显示不正确的bug
|
||||||
|
- uCharts.js 柱状图、山峰图增加正负图功能,y轴网格如果需要显示0轴则由 min max 及 splitNumber 确定,后续版本优化自动显示0轴
|
||||||
|
- uCharts.js 柱状图column增加 opts.extra.column.labelPosition,数据标签位置,有效值为 outside外部, insideTop内顶部, center内中间, bottom内底部
|
||||||
|
- uCharts.js 雷达图radar增加 opts.extra.radar.labelShow,否显示各项标识文案是,默认true
|
||||||
|
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.boxPadding,提示窗边框填充距离,默认3px
|
||||||
|
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.fontSize,提示窗字体大小配置,默认13px
|
||||||
|
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.lineHeight,提示窗文字行高,默认20px
|
||||||
|
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.legendShow,是否显示左侧图例,默认true
|
||||||
|
- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.legendShape,图例形状,图例标识样式,有效值为 auto自动跟随图例, diamond◆, circle●, triangle▲, square■, rect▬, line-
|
||||||
|
- uCharts.js 标记线markLine增加 opts.extra.markLine.labelFontSize,字体大小配置,默认13px
|
||||||
|
- uCharts.js 标记线markLine增加 opts.extra.markLine.labelPadding,标签边框内填充距离,默认6px
|
||||||
|
- uCharts.js 折线图line增加 opts.extra.line.linearType,渐变色类型,可选值 none关闭渐变色,custom 自定义渐变色。使用自定义渐变色时请赋值serie.linearColor作为颜色值
|
||||||
|
- uCharts.js 折线图line增加 serie.linearColor,渐变色数组,格式为2维数组[起始位置,颜色值],例如[[0,'#0EE2F8'],[0.3,'#2BDCA8'],[0.6,'#1890FF'],[1,'#9A60B4']]
|
||||||
|
- uCharts.js 折线图line增加 opts.extra.line.onShadow,是否开启折线阴影,开启后请赋值serie.setShadow阴影设置
|
||||||
|
- uCharts.js 折线图line增加 serie.setShadow,阴影配置,格式为4位数组:[offsetX,offsetY,blur,color]
|
||||||
|
- uCharts.js 折线图line增加 opts.extra.line.animation,动画效果方向,可选值为vertical 垂直动画效果,horizontal 水平动画效果
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.lineHeight,X轴字体行高,默认20px
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.marginTop,X轴文字距离轴线的距离,默认0px
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.title,当前X轴标题
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.titleFontSize,标题字体大小,默认13px
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.titleOffsetY,标题纵向偏移距离,负数为向上偏移,正数向下偏移
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.titleOffsetX,标题横向偏移距离,负数为向左偏移,正数向右偏移
|
||||||
|
- uCharts.js X轴xAxis增加 opts.xAxis.titleFontColor,标题字体颜色,默认#666666
|
||||||
|
|
||||||
|
## 报错TypeError: Cannot read properties of undefined (reading 'length')
|
||||||
|
- 如果是uni-modules版本组件,请先登录HBuilderX账号;
|
||||||
|
- 在HBuilderX中的manifest.json,点击重新获取uniapp的appid,或者删除appid重新粘贴,重新运行;
|
||||||
|
- 如果是cli项目请使用码云上的非uniCloud版本组件;
|
||||||
|
- 或者添加uniCloud的依赖;
|
||||||
|
- 或者使用原生uCharts;
|
||||||
|
## 2.4.5-20221130(2022-11-30)
|
||||||
|
- uCharts.js 优化tooltip当文字很多变为左侧显示时,如果画布仍显显示不下,提示框错位置变为以左侧0位置起画
|
||||||
|
- uCharts.js 折线图修复特殊情况下只有单点数据,并改变线宽后点变为圆形的bug
|
||||||
|
- uCharts.js 修复Y轴disabled启用后无效并报错的bug
|
||||||
|
- uCharts.js 修复仪表盘起始结束角度特殊情况下显示不正确的bug
|
||||||
|
- uCharts.js 雷达图新增参数 opts.extra.radar.radius , 自定义雷达图半径
|
||||||
|
- uCharts.js 折线图、区域图增加tooltip指示点,opts.extra.line.activeType/opts.extra.area.activeType,可选值"none"不启用激活指示点,"hollow"空心点模式,"solid"实心点模式
|
||||||
|
## 2.4.4-20221102(2022-11-02)
|
||||||
|
- 秋云图表组件 修复使用echarts时reload、reshow无法调用重新渲染的bug,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/40)
|
||||||
|
- 秋云图表组件 修复使用echarts时,初始化时宽高不正确的bug,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/42)
|
||||||
|
- 秋云图表组件 修复uniapp的h5使用history模式时,无法加载echarts的bug
|
||||||
|
- 秋云图表组件 小程序端@complete、@scrollLeft、@scrollRight、@getTouchStart、@getTouchMove、@getTouchEnd事件增加opts参数传出,方便一些特殊需求的交互获取数据。
|
||||||
|
|
||||||
|
- uCharts.js 修复calTooltipYAxisData方法内formatter格式化方法未与y轴方法同步的问题,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/43)
|
||||||
|
- uCharts.js 地图新增参数opts.series[i].fillOpacity,以透明度方式来设置颜色过度效果,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/38)
|
||||||
|
- uCharts.js 地图新增参数opts.extra.map.active,是否启用点击激活变色
|
||||||
|
- uCharts.js 地图新增参数opts.extra.map.activeTextColor,是否启用点击激活变色
|
||||||
|
- uCharts.js 地图新增渲染完成事件renderComplete
|
||||||
|
- uCharts.js 漏斗图修复当部分数据相同时tooltip提示窗点击错误的bug
|
||||||
|
- uCharts.js 漏斗图新增参数series.data[i].centerText 居中标签文案
|
||||||
|
- uCharts.js 漏斗图新增参数series.data[i].centerTextSize 居中标签文案字体大小,默认opts.fontSize
|
||||||
|
- uCharts.js 漏斗图新增参数series.data[i].centerTextColor 居中标签文案字体颜色,默认#FFFFFF
|
||||||
|
- uCharts.js 漏斗图新增参数opts.extra.funnel.minSize 最小值的最小宽度,默认0
|
||||||
|
- uCharts.js 进度条新增参数opts.extra.arcbar.direction,动画方向,可选值为cw顺时针、ccw逆时针
|
||||||
|
- uCharts.js 混合图新增参数opts.extra.mix.line.width,折线的宽度,默认2
|
||||||
|
- uCharts.js 修复tooltip开启horizentalLine水平横线标注时,图表显示错位的bug
|
||||||
|
- uCharts.js 优化tooltip当文字很多变为左侧显示时,如果画布仍显显示不下,提示框错位置变为以左侧0位置起画
|
||||||
|
- uCharts.js 修复开启滚动条后X轴文字超出绘图区域后的隐藏逻辑
|
||||||
|
- uCharts.js 柱状图、条状图修复堆叠模式不能通过{value,color}赋值单个柱子颜色的问题
|
||||||
|
- uCharts.js 气泡图修复不识别series.textSize和series.textColor的bug
|
||||||
|
|
||||||
|
## 报错TypeError: Cannot read properties of undefined (reading 'length')
|
||||||
|
1. 如果是uni-modules版本组件,请先登录HBuilderX账号;
|
||||||
|
2. 在HBuilderX中的manifest.json,点击重新获取uniapp的appid,或者删除appid重新粘贴,重新运行;
|
||||||
|
3. 如果是cli项目请使用码云上的非uniCloud版本组件;
|
||||||
|
4. 或者添加uniCloud的依赖;
|
||||||
|
5. 或者使用原生uCharts;
|
||||||
|
## 2.4.3-20220505(2022-05-05)
|
||||||
|
- 秋云图表组件 修复开启canvas2d后将series赋值为空数组显示加载图标时,再次赋值后画布闪动的bug
|
||||||
|
- 秋云图表组件 修复升级hbx最新版后ECharts的highlight方法报错的bug
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.gridEval,数据点位网格抽希,默认1
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.axisLabel, 是否显示刻度点值,默认false
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.axisLabelTofix,刻度点值小数位数,默认0
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.labelPointShow,是否显示末端刻度圆点,默认false
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.labelPointRadius,刻度圆点的半径,默认3
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.labelPointColor,刻度圆点的颜色,默认#cccccc
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.linearType,渐变色类型,可选值"none"关闭渐变,"custom"开启渐变
|
||||||
|
- uCharts.js 雷达图新增参数opts.extra.radar.customColor,自定义渐变颜色,数组类型对应series的数组长度以匹配不同series颜色的不同配色方案,例如["#FA7D8D", "#EB88E2"]
|
||||||
|
- uCharts.js 雷达图优化支持series.textColor、series.textSize属性
|
||||||
|
- uCharts.js 柱状图中温度计式图标,优化支持全圆角类型,修复边框有缝隙的bug,详见官网【演示】中的温度计图表
|
||||||
|
- uCharts.js 柱状图新增参数opts.extra.column.activeWidth,当前点击柱状图的背景宽度,默认一个单元格单位
|
||||||
|
- uCharts.js 混合图增加opts.extra.mix.area.gradient 区域图是否开启渐变色
|
||||||
|
- uCharts.js 混合图增加opts.extra.mix.area.opacity 区域图透明度,默认0.2
|
||||||
|
- uCharts.js 饼图、圆环图、玫瑰图、漏斗图,增加opts.series[0].data[i].labelText,自定义标签文字,避免formatter格式化的繁琐,详见官网【演示】中的饼图
|
||||||
|
- uCharts.js 饼图、圆环图、玫瑰图、漏斗图,增加opts.series[0].data[i].labelShow,自定义是否显示某一个指示标签,避免因饼图类别太多导致标签重复或者居多导致图形变形的问题,详见官网【演示】中的饼图
|
||||||
|
- uCharts.js 增加opts.series[i].legendText/opts.series[0].data[i].legendText(与series.name同级)自定义图例显示文字的方法
|
||||||
|
- uCharts.js 优化X轴、Y轴formatter格式化方法增加形参,统一为fromatter:function(value,index,opts){}
|
||||||
|
- uCharts.js 修复横屏模式下无法使用双指缩放方法的bug
|
||||||
|
- uCharts.js 修复当只有一条数据或者多条数据值相等的时候Y轴自动计算的最大值错误的bug
|
||||||
|
- 【官网模板】增加外部自定义图例与图表交互的例子,[点击跳转](https://www.ucharts.cn/v2/#/layout/info?id=2)
|
||||||
|
|
||||||
|
## 注意:非unimodules 版本如因更新 hbx 至 3.4.7 导致报错如下,请到码云更新非 unimodules 版本组件,[点击跳转](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6)
|
||||||
|
> Error in callback for immediate watcher "uchartsOpts": "SyntaxError: Unexpected token u in JSON at position 0"
|
||||||
|
## 2.4.2-20220421(2022-04-21)
|
||||||
|
- 秋云图表组件 修复HBX升级3.4.6.20220420版本后echarts报错的问题
|
||||||
|
## 2.4.2-20220420(2022-04-20)
|
||||||
|
## 重要!此版本uCharts新增了很多功能,修复了诸多已知问题
|
||||||
|
- 秋云图表组件 新增onzoom开启双指缩放功能(仅uCharts),前提需要直角坐标系类图表类型,并且ontouch为true、opts.enableScroll为true,详见实例项目K线图
|
||||||
|
- 秋云图表组件 新增optsWatch是否监听opts变化,关闭optsWatch后,动态修改opts不会触发图表重绘
|
||||||
|
- 秋云图表组件 修复开启canvas2d功能后,动态更新数据后画布闪动的bug
|
||||||
|
- 秋云图表组件 去除directory属性,改为自动获取echarts.min.js路径(升级不受影响)
|
||||||
|
- 秋云图表组件 增加getImage()方法及@getImage事件,通过ref调用getImage()方法获,触发@getImage事件获取当前画布的base64图片文件流。
|
||||||
|
- 秋云图表组件 支付宝、字节跳动、飞书、快手小程序支持开启canvas2d同层渲染设置。
|
||||||
|
- 秋云图表组件 新增加【非uniCloud】版本组件,避免有些不需要uniCloud的使用组件发布至小程序需要提交隐私声明问题,请到码云[【非uniCloud版本】](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6),或npm[【非uniCloud版本】](https://www.npmjs.com/package/@qiun/uni-ucharts)下载使用。
|
||||||
|
- uCharts.js 新增dobuleZoom双指缩放功能
|
||||||
|
- uCharts.js 新增山峰图type="mount",数据格式为饼图类格式,不需要传入categories,具体详见新版官网在线演示
|
||||||
|
- uCharts.js 修复折线图当数据中存在null时tooltip报错的bug
|
||||||
|
- uCharts.js 修复饼图类当画布比较小时自动计算的半径是负数报错的bug
|
||||||
|
- uCharts.js 统一各图表类型的series.formatter格式化方法的形参为(val, index, series, opts),方便格式化时有更多参数可用
|
||||||
|
- uCharts.js 标记线功能增加labelText自定义显示文字,增加labelAlign标签显示位置(左侧或右侧),增加标签显示位置微调labelOffsetX、labelOffsetY
|
||||||
|
- uCharts.js 修复条状图当数值很小时开启圆角后样式错误的bug
|
||||||
|
- uCharts.js 修复X轴开启disabled后,X轴仍占用空间的bug
|
||||||
|
- uCharts.js 修复X轴开启滚动条并且开启rotateLabel后,X轴文字与滚动条重叠的bug
|
||||||
|
- uCharts.js 增加X轴rotateAngle文字旋转自定义角度,取值范围(-90至90)
|
||||||
|
- uCharts.js 修复地图文字标签层级显示不正确的bug
|
||||||
|
- uCharts.js 修复饼图、圆环图、玫瑰图当数据全部为0的时候不显示数据标签的bug
|
||||||
|
- uCharts.js 修复当opts.padding上边距为0时,Y轴顶部刻度标签位置不正确的bug
|
||||||
|
|
||||||
|
## 另外我们还开发了各大原生小程序组件,已发布至码云和npm
|
||||||
|
[https://gitee.com/uCharts/uCharts](https://gitee.com/uCharts/uCharts)
|
||||||
|
[https://www.npmjs.com/~qiun](https://www.npmjs.com/~qiun)
|
||||||
|
|
||||||
|
## 对于原生uCharts文档我们已上线新版官方网站,详情点击下面链接进入官网
|
||||||
|
[https://www.uCharts.cn/v2/](https://www.ucharts.cn/v2/)
|
||||||
|
## 2.3.7-20220122(2022-01-22)
|
||||||
|
## 重要!使用vue3编译,请使用cli模式并升级至最新依赖,HbuilderX编译需要使用3.3.8以上版本
|
||||||
|
- uCharts.js 修复uni-app平台组件模式使用vue3编译到小程序报错的bug。
|
||||||
|
## 2.3.7-20220118(2022-01-18)
|
||||||
|
## 注意,使用vue3的前提是需要3.3.8.20220114-alpha版本的HBuilder!
|
||||||
|
## 2.3.67-20220118(2022-01-18)
|
||||||
|
- 秋云图表组件 组件初步支持vue3,全端编译会有些问题,具体详见下面修改:
|
||||||
|
1. 小程序端运行时,在uni_modules文件夹的qiun-data-charts.js中搜索 new uni_modules_qiunDataCharts_js_sdk_uCharts_uCharts.uCharts,将.uCharts去掉。
|
||||||
|
2. 小程序端发行时,在uni_modules文件夹的qiun-data-charts.js中搜索 new e.uCharts,将.uCharts去掉,变为 new e。
|
||||||
|
3. 如果觉得上述步骤比较麻烦,如果您的项目只编译到小程序端,可以修改u-charts.js最后一行导出方式,将 export default uCharts;变更为 export default { uCharts: uCharts }; 这样变更后,H5和App端的renderjs会有问题,请开发者自行选择。(此问题非组件问题,请等待DC官方修复Vue3的小程序端)
|
||||||
|
## 2.3.6-20220111(2022-01-11)
|
||||||
|
- 秋云图表组件 修改组件 props 属性中的 background 默认值为 rgba(0,0,0,0)
|
||||||
|
## 2.3.6-20211201(2021-12-01)
|
||||||
|
- uCharts.js 修复bar条状图开启圆角模式时,值很小时圆角渲染错误的bug
|
||||||
|
## 2.3.5-20211014(2021-10-15)
|
||||||
|
- uCharts.js 增加vue3的编译支持(仅原生uCharts,qiun-data-charts组件后续会支持,请关注更新)
|
||||||
|
## 2.3.4-20211012(2021-10-12)
|
||||||
|
- 秋云图表组件 修复 mac os x 系统 mouseover 事件丢失的 bug
|
||||||
|
## 2.3.3-20210706(2021-07-06)
|
||||||
|
- uCharts.js 增加雷达图开启数据点值(opts.dataLabel)的显示
|
||||||
|
## 2.3.2-20210627(2021-06-27)
|
||||||
|
- 秋云图表组件 修复tooltipCustom个别情况下传值不正确报错TypeError: Cannot read property 'name' of undefined的bug
|
||||||
|
## 2.3.1-20210616(2021-06-16)
|
||||||
|
- uCharts.js 修复圆角柱状图使用4角圆角时,当数值过大时不正确的bug
|
||||||
|
## 2.3.0-20210612(2021-06-12)
|
||||||
|
- uCharts.js 【重要】uCharts增加nvue兼容,可在nvue项目中使用gcanvas组件渲染uCharts,[详见码云uCharts-demo-nvue](https://gitee.com/uCharts/uCharts)
|
||||||
|
- 秋云图表组件 增加tapLegend属性,是否开启图例点击交互事件
|
||||||
|
- 秋云图表组件 getIndex事件中增加返回uCharts实例中的opts参数,以便在页面中调用参数
|
||||||
|
- 示例项目 pages/other/other.vue增加app端自定义tooltip的方法,详见showOptsTooltip方法
|
||||||
|
## 2.2.1-20210603(2021-06-03)
|
||||||
|
- uCharts.js 修复饼图、圆环图、玫瑰图,当起始角度不为0时,tooltip位置不准确的bug
|
||||||
|
- uCharts.js 增加温度计式柱状图开启顶部半圆形的配置
|
||||||
|
## 2.2.0-20210529(2021-05-29)
|
||||||
|
- uCharts.js 增加条状图type="bar"
|
||||||
|
- 示例项目 pages/ucharts/ucharts.vue增加条状图的demo
|
||||||
|
## 2.1.7-20210524(2021-05-24)
|
||||||
|
- uCharts.js 修复大数据量模式下曲线图不平滑的bug
|
||||||
|
## 2.1.6-20210523(2021-05-23)
|
||||||
|
- 秋云图表组件 修复小程序端开启滚动条更新数据后滚动条位置不符合预期的bug
|
||||||
|
## 2.1.5-2021051702(2021-05-17)
|
||||||
|
- uCharts.js 修复自定义Y轴min和max值为0时不能正确显示的bug
|
||||||
|
## 2.1.5-20210517(2021-05-17)
|
||||||
|
- uCharts.js 修复Y轴自定义min和max时,未按指定的最大值最小值显示坐标轴刻度的bug
|
||||||
|
## 2.1.4-20210516(2021-05-16)
|
||||||
|
- 秋云图表组件 优化onWindowResize防抖方法
|
||||||
|
- 秋云图表组件 修复APP端uCharts更新数据时,清空series显示loading图标后再显示图表,图表抖动的bug
|
||||||
|
- uCharts.js 修复开启canvas2d后,x轴、y轴、series自定义字体大小未按比例缩放的bug
|
||||||
|
- 示例项目 修复format-e.vue拼写错误导致app端使用uCharts渲染图表
|
||||||
|
## 2.1.3-20210513(2021-05-13)
|
||||||
|
- 秋云图表组件 修改uCharts变更chartData数据为updateData方法,支持带滚动条的数据动态打点
|
||||||
|
- 秋云图表组件 增加onWindowResize防抖方法 fix by ど誓言,如尘般染指流年づ
|
||||||
|
- 秋云图表组件 H5或者APP变更chartData数据显示loading图表时,原数据闪现的bug
|
||||||
|
- 秋云图表组件 props增加errorReload禁用错误点击重新加载的方法
|
||||||
|
- uCharts.js 增加tooltip显示category(x轴对应点位)标题的功能,opts.extra.tooltip.showCategory,默认为false
|
||||||
|
- uCharts.js 修复mix混合图只有柱状图时,tooltip的分割线显示位置不正确的bug
|
||||||
|
- uCharts.js 修复开启滚动条,图表在拖动中动态打点,滚动条位置不正确的bug
|
||||||
|
- uCharts.js 修复饼图类数据格式为echarts数据格式,series为空数组报错的bug
|
||||||
|
- 示例项目 修改uCharts.js更新到v2.1.2版本后,@getIndex方法获取索引值变更为e.currentIndex.index
|
||||||
|
- 示例项目 pages/updata/updata.vue增加滚动条拖动更新(数据动态打点)的demo
|
||||||
|
- 示例项目 pages/other/other.vue增加errorReload禁用错误点击重新加载的demo
|
||||||
|
## 2.1.2-20210509(2021-05-09)
|
||||||
|
秋云图表组件 修复APP端初始化时就传入chartData或lacaldata不显示图表的bug
|
||||||
|
## 2.1.1-20210509(2021-05-09)
|
||||||
|
- 秋云图表组件 变更ECharts的eopts配置在renderjs内执行,支持在config-echarts.js配置文件内写function配置。
|
||||||
|
- 秋云图表组件 修复APP端报错Prop being mutated: "onmouse"错误的bug。
|
||||||
|
- 秋云图表组件 修复APP端报错Error: Not Found:Page[6][-1,27] at view.umd.min.js:1的bug。
|
||||||
|
## 2.1.0-20210507(2021-05-07)
|
||||||
|
- 秋云图表组件 修复初始化时就有数据或者数据更新的时候loading加载动画闪动的bug
|
||||||
|
- uCharts.js 修复x轴format方法categories为字符串类型时返回NaN的bug
|
||||||
|
- uCharts.js 修复series.textColor、legend.fontColor未执行全局默认颜色的bug
|
||||||
|
## 2.1.0-20210506(2021-05-06)
|
||||||
|
- 秋云图表组件 修复极个别情况下报错item.properties undefined的bug
|
||||||
|
- 秋云图表组件 修复极个别情况下关闭加载动画reshow不起作用,无法显示图表的bug
|
||||||
|
- 示例项目 pages/ucharts/ucharts.vue 增加时间轴折线图(type="tline")、时间轴区域图(type="tarea")、散点图(type="scatter")、气泡图demo(type="bubble")、倒三角形漏斗图(opts.extra.funnel.type="triangle")、金字塔形漏斗图(opts.extra.funnel.type="pyramid")
|
||||||
|
- 示例项目 pages/format-u/format-u.vue 增加X轴format格式化示例
|
||||||
|
- uCharts.js 升级至v2.1.0版本
|
||||||
|
- uCharts.js 修复 玫瑰图面积模式点击tooltip位置不正确的bug
|
||||||
|
- uCharts.js 修复 玫瑰图点击图例,只剩一个类别显示空白的bug
|
||||||
|
- uCharts.js 修复 饼图类图点击图例,其他图表tooltip位置某些情况下不准的bug
|
||||||
|
- uCharts.js 修复 x轴为矢量轴(时间轴)情况下,点击tooltip位置不正确的bug
|
||||||
|
- uCharts.js 修复 词云图获取点击索引偶尔不准的bug
|
||||||
|
- uCharts.js 增加 直角坐标系图表X轴format格式化方法(原生uCharts.js用法请使用formatter)
|
||||||
|
- uCharts.js 增加 漏斗图扩展配置,倒三角形(opts.extra.funnel.type="triangle"),金字塔形(opts.extra.funnel.type="pyramid")
|
||||||
|
- uCharts.js 增加 散点图(opts.type="scatter")、气泡图(opts.type="bubble")
|
||||||
|
- 后期计划 完善散点图、气泡图,增加markPoints标记点,增加横向条状图。
|
||||||
|
## 2.0.0-20210502(2021-05-02)
|
||||||
|
- uCharts.js 修复词云图获取点击索引不正确的bug
|
||||||
|
## 2.0.0-20210501(2021-05-01)
|
||||||
|
- 秋云图表组件 修复QQ小程序、百度小程序在关闭动画效果情况下,v-for循环使用图表,显示不正确的bug
|
||||||
|
## 2.0.0-20210426(2021-04-26)
|
||||||
|
- 秋云图表组件 修复QQ小程序不支持canvas2d的bug
|
||||||
|
- 秋云图表组件 修复钉钉小程序某些情况点击坐标计算错误的bug
|
||||||
|
- uCharts.js 增加 extra.column.categoryGap 参数,柱状图类每个category点位(X轴点)柱子组之间的间距
|
||||||
|
- uCharts.js 增加 yAxis.data[i].titleOffsetY 参数,标题纵向偏移距离,负数为向上偏移,正数向下偏移
|
||||||
|
- uCharts.js 增加 yAxis.data[i].titleOffsetX 参数,标题横向偏移距离,负数为向左偏移,正数向右偏移
|
||||||
|
- uCharts.js 增加 extra.gauge.labelOffset 参数,仪表盘标签文字径向便宜距离,默认13px
|
||||||
|
## 2.0.0-20210422-2(2021-04-22)
|
||||||
|
秋云图表组件 修复 formatterAssign 未判断 args[key] == null 的情况导致栈溢出的 bug
|
||||||
|
## 2.0.0-20210422(2021-04-22)
|
||||||
|
- 秋云图表组件 修复H5、APP、支付宝小程序、微信小程序canvas2d模式下横屏模式的bug
|
||||||
|
## 2.0.0-20210421(2021-04-21)
|
||||||
|
- uCharts.js 修复多行图例的情况下,图例在上方或者下方时,图例float为左侧或者右侧时,第二行及以后的图例对齐方式不正确的bug
|
||||||
|
## 2.0.0-20210420(2021-04-20)
|
||||||
|
- 秋云图表组件 修复微信小程序开启canvas2d模式后,windows版微信小程序不支持canvas2d模式的bug
|
||||||
|
- 秋云图表组件 修改非uni_modules版本为v2.0版本qiun-data-charts组件
|
||||||
|
## 2.0.0-20210419(2021-04-19)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
|
||||||
|
## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑;
|
||||||
|
## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。
|
||||||
|
## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。
|
||||||
|
## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
|
||||||
|
## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! </font>
|
||||||
|
## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
- uCharts.js 修复混合图中柱状图单独设置颜色不生效的bug
|
||||||
|
- uCharts.js 修复多Y轴单独设置fontSize时,开启canvas2d后,未对应放大字体的bug
|
||||||
|
## 2.0.0-20210418(2021-04-18)
|
||||||
|
- 秋云图表组件 增加directory配置,修复H5端history模式下如果发布到二级目录无法正确加载echarts.min.js的bug
|
||||||
|
## 2.0.0-20210416(2021-04-16)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
|
||||||
|
## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑;
|
||||||
|
## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。
|
||||||
|
## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。
|
||||||
|
## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
|
||||||
|
## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! </font>
|
||||||
|
## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
- 秋云图表组件 修复APP端某些情况下报错`Not Found Page`的bug,fix by 高级bug开发技术员
|
||||||
|
- 示例项目 修复APP端v-for循环某些情况下报错`Not Found Page`的bug,fix by 高级bug开发技术员
|
||||||
|
- uCharts.js 修复非直角坐标系tooltip提示窗右侧超出未变换方向显示的bug
|
||||||
|
## 2.0.0-20210415(2021-04-15)
|
||||||
|
- 秋云图表组件 修复H5端发布到二级目录下echarts无法加载的bug
|
||||||
|
- 秋云图表组件 修复某些情况下echarts.off('finished')移除监听事件报错的bug
|
||||||
|
## 2.0.0-20210414(2021-04-14)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
|
||||||
|
## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑;
|
||||||
|
## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。
|
||||||
|
## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。
|
||||||
|
## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
|
||||||
|
## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! </font>
|
||||||
|
## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
- 秋云图表组件 修复H5端在cli项目下ECharts引用地址错误的bug
|
||||||
|
- 示例项目 增加ECharts的formatter用法的示例(详见示例项目format-e.vue)
|
||||||
|
- uCharts.js 增加圆环图中心背景色的配置extra.ring.centerColor
|
||||||
|
- uCharts.js 修复微信小程序安卓端柱状图开启透明色后显示不正确的bug
|
||||||
|
## 2.0.0-20210413(2021-04-13)
|
||||||
|
- 秋云图表组件 修复百度小程序多个图表真机未能正确获取根元素dom尺寸的bug
|
||||||
|
- 秋云图表组件 修复百度小程序横屏模式方向不正确的bug
|
||||||
|
- 秋云图表组件 修改ontouch时,@getTouchStart@getTouchMove@getTouchEnd的触发条件
|
||||||
|
- uCharts.js 修复饼图类数据格式series属性不生效的bug
|
||||||
|
- uCharts.js 增加时序区域图 详见示例项目中ucharts.vue
|
||||||
|
## 2.0.0-20210412-2(2021-04-12)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
|
||||||
|
## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX。如仍不好用,请重启电脑,此问题已于DCloud官方确认,HBuilderX下个版本会修复。
|
||||||
|
## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
- 秋云图表组件 修复uCharts在APP端横屏模式下不能正确渲染的bug
|
||||||
|
- 示例项目 增加ECharts柱状图渐变色、圆角柱状图、横向柱状图(条状图)的示例
|
||||||
|
## 2.0.0-20210412(2021-04-12)
|
||||||
|
- 秋云图表组件 修复created中判断echarts导致APP端无法识别,改回mounted中判断echarts初始化
|
||||||
|
- uCharts.js 修复2d模式下series.textOffset未乘像素比的bug
|
||||||
|
## 2.0.0-20210411(2021-04-11)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
|
||||||
|
## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,并清空小程序开发者工具缓存。
|
||||||
|
## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
- uCharts.js 折线图区域图增加connectNulls断点续连的功能,详见示例项目中ucharts.vue
|
||||||
|
- 秋云图表组件 变更初始化方法为created,变更type2d默认值为true,优化2d模式下组件初始化后dom获取不到的bug
|
||||||
|
- 秋云图表组件 修复左右布局时,右侧图表点击坐标错误的bug,修复tooltip柱状图自定义颜色显示object的bug
|
||||||
|
## 2.0.0-20210410(2021-04-10)
|
||||||
|
- 修复左右布局时,右侧图表点击坐标错误的bug,修复柱状图自定义颜色tooltip显示object的bug
|
||||||
|
- 增加标记线及柱状图自定义颜色的demo
|
||||||
|
## 2.0.0-20210409(2021-04-08)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧【使用HBuilderX导入插件】即可体验,DEMO演示及在线生成工具(v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
- uCharts.js 修复钉钉小程序百度小程序measureText不准确的bug,修复2d模式下饼图类activeRadius为按比例放大的bug
|
||||||
|
- 修复组件在支付宝小程序端点击位置不准确的bug
|
||||||
|
## 2.0.0-20210408(2021-04-07)
|
||||||
|
- 修复组件在支付宝小程序端不能显示的bug(目前支付宝小程不能点击交互,后续修复)
|
||||||
|
- uCharts.js 修复高分屏下柱状图类,圆弧进度条 自定义宽度不能按比例放大的bug
|
||||||
|
## 2.0.0-20210407(2021-04-06)
|
||||||
|
## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧【使用HBuilderX导入插件】即可体验,DEMO演示及在线生成工具(v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn)
|
||||||
|
## 增加 通过tofix和unit快速格式化y轴的demo add by `howcode`
|
||||||
|
## 增加 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
|
||||||
|
## 2.0.0-20210406(2021-04-05)
|
||||||
|
# 秋云图表组件+uCharts v2.0版本同步上线,使用方法详见https://demo.ucharts.cn帮助页
|
||||||
|
## 2.0.0(2021-04-05)
|
||||||
|
# 秋云图表组件+uCharts v2.0版本同步上线,使用方法详见https://demo.ucharts.cn帮助页
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container loading1">
|
||||||
|
<view class="shape shape1"></view>
|
||||||
|
<view class="shape shape2"></view>
|
||||||
|
<view class="shape shape3"></view>
|
||||||
|
<view class="shape shape4"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'loading1',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped="true">
|
||||||
|
.container {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.container.loading1 {
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .shape {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
.container .shape.shape1 {
|
||||||
|
left: 0;
|
||||||
|
background-color: #1890FF;
|
||||||
|
}
|
||||||
|
.container .shape.shape2 {
|
||||||
|
right: 0;
|
||||||
|
background-color: #91CB74;
|
||||||
|
}
|
||||||
|
.container .shape.shape3 {
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #FAC858;
|
||||||
|
}
|
||||||
|
.container .shape.shape4 {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #EE6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading1 .shape1 {
|
||||||
|
-webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation1shape1 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation1shape1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(16px, 16px);
|
||||||
|
transform: translate(16px, 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation1shape1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(16px, 16px);
|
||||||
|
transform: translate(16px, 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading1 .shape2 {
|
||||||
|
-webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation1shape2 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation1shape2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-16px, 16px);
|
||||||
|
transform: translate(-16px, 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation1shape2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-16px, 16px);
|
||||||
|
transform: translate(-16px, 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading1 .shape3 {
|
||||||
|
-webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation1shape3 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation1shape3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(16px, -16px);
|
||||||
|
transform: translate(16px, -16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation1shape3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(16px, -16px);
|
||||||
|
transform: translate(16px, -16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading1 .shape4 {
|
||||||
|
-webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation1shape4 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation1shape4 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-16px, -16px);
|
||||||
|
transform: translate(-16px, -16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation1shape4 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-16px, -16px);
|
||||||
|
transform: translate(-16px, -16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container loading2">
|
||||||
|
<view class="shape shape1"></view>
|
||||||
|
<view class="shape shape2"></view>
|
||||||
|
<view class="shape shape3"></view>
|
||||||
|
<view class="shape shape4"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'loading2',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped="true">
|
||||||
|
.container {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.loading2 {
|
||||||
|
-webkit-transform: rotate(10deg);
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
.container.loading2 .shape {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.container.loading2{
|
||||||
|
-webkit-animation: rotation 1s infinite;
|
||||||
|
animation: rotation 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .shape {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
.container .shape.shape1 {
|
||||||
|
left: 0;
|
||||||
|
background-color: #1890FF;
|
||||||
|
}
|
||||||
|
.container .shape.shape2 {
|
||||||
|
right: 0;
|
||||||
|
background-color: #91CB74;
|
||||||
|
}
|
||||||
|
.container .shape.shape3 {
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #FAC858;
|
||||||
|
}
|
||||||
|
.container .shape.shape4 {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #EE6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.loading2 .shape1 {
|
||||||
|
-webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation2shape1 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation2shape1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(20px, 20px);
|
||||||
|
transform: translate(20px, 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation2shape1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(20px, 20px);
|
||||||
|
transform: translate(20px, 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading2 .shape2 {
|
||||||
|
-webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation2shape2 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation2shape2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-20px, 20px);
|
||||||
|
transform: translate(-20px, 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation2shape2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-20px, 20px);
|
||||||
|
transform: translate(-20px, 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading2 .shape3 {
|
||||||
|
-webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation2shape3 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation2shape3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(20px, -20px);
|
||||||
|
transform: translate(20px, -20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation2shape3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(20px, -20px);
|
||||||
|
transform: translate(20px, -20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading2 .shape4 {
|
||||||
|
-webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation2shape4 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation2shape4 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-20px, -20px);
|
||||||
|
transform: translate(-20px, -20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation2shape4 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-20px, -20px);
|
||||||
|
transform: translate(-20px, -20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container loading3">
|
||||||
|
<view class="shape shape1"></view>
|
||||||
|
<view class="shape shape2"></view>
|
||||||
|
<view class="shape shape3"></view>
|
||||||
|
<view class="shape shape4"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'loading3',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped="true">
|
||||||
|
.container {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.loading3 {
|
||||||
|
-webkit-animation: rotation 1s infinite;
|
||||||
|
animation: rotation 1s infinite;
|
||||||
|
}
|
||||||
|
.container.loading3 .shape1 {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
}
|
||||||
|
.container.loading3 .shape2 {
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
.container.loading3 .shape3 {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
}
|
||||||
|
.container.loading3 .shape4 {
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .shape {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
.container .shape.shape1 {
|
||||||
|
left: 0;
|
||||||
|
background-color: #1890FF;
|
||||||
|
}
|
||||||
|
.container .shape.shape2 {
|
||||||
|
right: 0;
|
||||||
|
background-color: #91CB74;
|
||||||
|
}
|
||||||
|
.container .shape.shape3 {
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #FAC858;
|
||||||
|
}
|
||||||
|
.container .shape.shape4 {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #EE6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading3 .shape1 {
|
||||||
|
-webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation3shape1 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation3shape1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(5px, 5px);
|
||||||
|
transform: translate(5px, 5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation3shape1 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(5px, 5px);
|
||||||
|
transform: translate(5px, 5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading3 .shape2 {
|
||||||
|
-webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation3shape2 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation3shape2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-5px, 5px);
|
||||||
|
transform: translate(-5px, 5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation3shape2 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-5px, 5px);
|
||||||
|
transform: translate(-5px, 5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading3 .shape3 {
|
||||||
|
-webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation3shape3 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation3shape3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(5px, -5px);
|
||||||
|
transform: translate(5px, -5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation3shape3 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(5px, -5px);
|
||||||
|
transform: translate(5px, -5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading3 .shape4 {
|
||||||
|
-webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
|
||||||
|
animation: animation3shape4 0.5s ease 0s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation3shape4 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-5px, -5px);
|
||||||
|
transform: translate(-5px, -5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation3shape4 {
|
||||||
|
from {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: translate(-5px, -5px);
|
||||||
|
transform: translate(-5px, -5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,222 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container loading5">
|
||||||
|
<view class="shape shape1"></view>
|
||||||
|
<view class="shape shape2"></view>
|
||||||
|
<view class="shape shape3"></view>
|
||||||
|
<view class="shape shape4"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'loading5',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped="true">
|
||||||
|
.container {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.loading5 .shape {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .shape {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
.container .shape.shape1 {
|
||||||
|
left: 0;
|
||||||
|
background-color: #1890FF;
|
||||||
|
}
|
||||||
|
.container .shape.shape2 {
|
||||||
|
right: 0;
|
||||||
|
background-color: #91CB74;
|
||||||
|
}
|
||||||
|
.container .shape.shape3 {
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #FAC858;
|
||||||
|
}
|
||||||
|
.container .shape.shape4 {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #EE6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading5 .shape1 {
|
||||||
|
animation: animation5shape1 2s ease 0s infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation5shape1 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, 15px);
|
||||||
|
transform: translate(0, 15px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(15px, 15px);
|
||||||
|
transform: translate(15px, 15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(15px, 0);
|
||||||
|
transform: translate(15px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation5shape1 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, 15px);
|
||||||
|
transform: translate(0, 15px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(15px, 15px);
|
||||||
|
transform: translate(15px, 15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(15px, 0);
|
||||||
|
transform: translate(15px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading5 .shape2 {
|
||||||
|
animation: animation5shape2 2s ease 0s infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation5shape2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(-15px, 0);
|
||||||
|
transform: translate(-15px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-15px, 15px);
|
||||||
|
transform: translate(-15px, 15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, 15px);
|
||||||
|
transform: translate(0, 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation5shape2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(-15px, 0);
|
||||||
|
transform: translate(-15px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-15px, 15px);
|
||||||
|
transform: translate(-15px, 15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, 15px);
|
||||||
|
transform: translate(0, 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading5 .shape3 {
|
||||||
|
animation: animation5shape3 2s ease 0s infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation5shape3 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(15px, 0);
|
||||||
|
transform: translate(15px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(15px, -15px);
|
||||||
|
transform: translate(15px, -15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, -15px);
|
||||||
|
transform: translate(0, -15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation5shape3 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(15px, 0);
|
||||||
|
transform: translate(15px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(15px, -15px);
|
||||||
|
transform: translate(15px, -15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, -15px);
|
||||||
|
transform: translate(0, -15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading5 .shape4 {
|
||||||
|
animation: animation5shape4 2s ease 0s infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation5shape4 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, -15px);
|
||||||
|
transform: translate(0, -15px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-15px, -15px);
|
||||||
|
transform: translate(-15px, -15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(-15px, 0);
|
||||||
|
transform: translate(-15px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation5shape4 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, -15px);
|
||||||
|
transform: translate(0, -15px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-15px, -15px);
|
||||||
|
transform: translate(-15px, -15px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(-15px, 0);
|
||||||
|
transform: translate(-15px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container loading6">
|
||||||
|
<view class="shape shape1"></view>
|
||||||
|
<view class="shape shape2"></view>
|
||||||
|
<view class="shape shape3"></view>
|
||||||
|
<view class="shape shape4"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'loading6',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped="true">
|
||||||
|
.container {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.loading6 {
|
||||||
|
-webkit-animation: rotation 1s infinite;
|
||||||
|
animation: rotation 1s infinite;
|
||||||
|
}
|
||||||
|
.container.loading6 .shape {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.container .shape {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
.container .shape.shape1 {
|
||||||
|
left: 0;
|
||||||
|
background-color: #1890FF;
|
||||||
|
}
|
||||||
|
.container .shape.shape2 {
|
||||||
|
right: 0;
|
||||||
|
background-color: #91CB74;
|
||||||
|
}
|
||||||
|
.container .shape.shape3 {
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #FAC858;
|
||||||
|
}
|
||||||
|
.container .shape.shape4 {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #EE6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.loading6 .shape1 {
|
||||||
|
-webkit-animation: animation6shape1 2s linear 0s infinite normal;
|
||||||
|
animation: animation6shape1 2s linear 0s infinite normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation6shape1 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, 18px);
|
||||||
|
transform: translate(0, 18px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(18px, 18px);
|
||||||
|
transform: translate(18px, 18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(18px, 0);
|
||||||
|
transform: translate(18px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation6shape1 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, 18px);
|
||||||
|
transform: translate(0, 18px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(18px, 18px);
|
||||||
|
transform: translate(18px, 18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(18px, 0);
|
||||||
|
transform: translate(18px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading6 .shape2 {
|
||||||
|
-webkit-animation: animation6shape2 2s linear 0s infinite normal;
|
||||||
|
animation: animation6shape2 2s linear 0s infinite normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation6shape2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(-18px, 0);
|
||||||
|
transform: translate(-18px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-18px, 18px);
|
||||||
|
transform: translate(-18px, 18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, 18px);
|
||||||
|
transform: translate(0, 18px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation6shape2 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(-18px, 0);
|
||||||
|
transform: translate(-18px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-18px, 18px);
|
||||||
|
transform: translate(-18px, 18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, 18px);
|
||||||
|
transform: translate(0, 18px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading6 .shape3 {
|
||||||
|
-webkit-animation: animation6shape3 2s linear 0s infinite normal;
|
||||||
|
animation: animation6shape3 2s linear 0s infinite normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation6shape3 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(18px, 0);
|
||||||
|
transform: translate(18px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(18px, -18px);
|
||||||
|
transform: translate(18px, -18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, -18px);
|
||||||
|
transform: translate(0, -18px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation6shape3 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(18px, 0);
|
||||||
|
transform: translate(18px, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(18px, -18px);
|
||||||
|
transform: translate(18px, -18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(0, -18px);
|
||||||
|
transform: translate(0, -18px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loading6 .shape4 {
|
||||||
|
-webkit-animation: animation6shape4 2s linear 0s infinite normal;
|
||||||
|
animation: animation6shape4 2s linear 0s infinite normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes animation6shape4 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, -18px);
|
||||||
|
transform: translate(0, -18px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-18px, -18px);
|
||||||
|
transform: translate(-18px, -18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(-18px, 0);
|
||||||
|
transform: translate(-18px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animation6shape4 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: translate(0, 0);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
-webkit-transform: translate(0, -18px);
|
||||||
|
transform: translate(0, -18px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: translate(-18px, -18px);
|
||||||
|
transform: translate(-18px, -18px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
-webkit-transform: translate(-18px, 0);
|
||||||
|
transform: translate(-18px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<Loading1 v-if="loadingType==1"/>
|
||||||
|
<Loading2 v-if="loadingType==2"/>
|
||||||
|
<Loading3 v-if="loadingType==3"/>
|
||||||
|
<Loading4 v-if="loadingType==4"/>
|
||||||
|
<Loading5 v-if="loadingType==5"/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading1 from "./loading1.vue";
|
||||||
|
import Loading2 from "./loading2.vue";
|
||||||
|
import Loading3 from "./loading3.vue";
|
||||||
|
import Loading4 from "./loading4.vue";
|
||||||
|
import Loading5 from "./loading5.vue";
|
||||||
|
export default {
|
||||||
|
components:{Loading1,Loading2,Loading3,Loading4,Loading5},
|
||||||
|
name: 'qiun-loading',
|
||||||
|
props: {
|
||||||
|
loadingType: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
@ -0,0 +1,422 @@
|
|||||||
|
/*
|
||||||
|
* uCharts®
|
||||||
|
* 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
|
||||||
|
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
|
||||||
|
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||||
|
* 复制使用请保留本段注释,感谢支持开源!
|
||||||
|
*
|
||||||
|
* uCharts®官方网站
|
||||||
|
* https://www.uCharts.cn
|
||||||
|
*
|
||||||
|
* 开源地址:
|
||||||
|
* https://gitee.com/uCharts/uCharts
|
||||||
|
*
|
||||||
|
* uni-app插件市场地址:
|
||||||
|
* http://ext.dcloud.net.cn/plugin?id=271
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 通用配置项
|
||||||
|
|
||||||
|
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
|
||||||
|
const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
|
||||||
|
|
||||||
|
const cfe = {
|
||||||
|
//demotype为自定义图表类型
|
||||||
|
"type": ["pie", "ring", "rose", "funnel", "line", "column", "area", "radar", "gauge","candle","demotype"],
|
||||||
|
//增加自定义图表类型,如果需要categories,请在这里加入您的图表类型例如最后的"demotype"
|
||||||
|
"categories": ["line", "column", "area", "radar", "gauge", "candle","demotype"],
|
||||||
|
//instance为实例变量承载属性,option为eopts承载属性,不要删除
|
||||||
|
"instance": {},
|
||||||
|
"option": {},
|
||||||
|
//下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
|
||||||
|
"formatter":{
|
||||||
|
"tooltipDemo1":function(res){
|
||||||
|
let result = ''
|
||||||
|
for (let i in res) {
|
||||||
|
if (i == 0) {
|
||||||
|
result += res[i].axisValueLabel + '年销售额'
|
||||||
|
}
|
||||||
|
let value = '--'
|
||||||
|
if (res[i].data !== null) {
|
||||||
|
value = res[i].data
|
||||||
|
}
|
||||||
|
// #ifdef H5
|
||||||
|
result += '\n' + res[i].seriesName + ':' + value + ' 万元'
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
result += '<br/>' + res[i].marker + res[i].seriesName + ':' + value + ' 万元'
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
legendFormat:function(name){
|
||||||
|
return "自定义图例+"+name;
|
||||||
|
},
|
||||||
|
yAxisFormatDemo:function (value, index) {
|
||||||
|
return value + '元';
|
||||||
|
},
|
||||||
|
seriesFormatDemo:function(res){
|
||||||
|
return res.name + '年' + res.value + '元';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在eopts参数,会将demotype与eopts中option合并后渲染图表。
|
||||||
|
"demotype":{
|
||||||
|
"color": color,
|
||||||
|
//在这里填写echarts的option即可
|
||||||
|
|
||||||
|
},
|
||||||
|
//下面是自定义配置,请添加项目所需的通用配置
|
||||||
|
"column": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'axis'
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"top": 30,
|
||||||
|
"bottom": 50,
|
||||||
|
"right": 15,
|
||||||
|
"left": 40
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"bottom": 'left',
|
||||||
|
},
|
||||||
|
"toolbox": {
|
||||||
|
"show": false,
|
||||||
|
},
|
||||||
|
"xAxis": {
|
||||||
|
"type": 'category',
|
||||||
|
"axisLabel": {
|
||||||
|
"color": '#666666'
|
||||||
|
},
|
||||||
|
"axisLine": {
|
||||||
|
"lineStyle": {
|
||||||
|
"color": '#CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boundaryGap": true,
|
||||||
|
"data": []
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"type": 'value',
|
||||||
|
"axisTick": {
|
||||||
|
"show": false,
|
||||||
|
},
|
||||||
|
"axisLabel": {
|
||||||
|
"color": '#666666'
|
||||||
|
},
|
||||||
|
"axisLine": {
|
||||||
|
"lineStyle": {
|
||||||
|
"color": '#CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'bar',
|
||||||
|
"data": [],
|
||||||
|
"barwidth": 20,
|
||||||
|
"label": {
|
||||||
|
"show": true,
|
||||||
|
"color": "#666666",
|
||||||
|
"position": 'top',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"line": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'axis'
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"top": 30,
|
||||||
|
"bottom": 50,
|
||||||
|
"right": 15,
|
||||||
|
"left": 40
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"bottom": 'left',
|
||||||
|
},
|
||||||
|
"toolbox": {
|
||||||
|
"show": false,
|
||||||
|
},
|
||||||
|
"xAxis": {
|
||||||
|
"type": 'category',
|
||||||
|
"axisLabel": {
|
||||||
|
"color": '#666666'
|
||||||
|
},
|
||||||
|
"axisLine": {
|
||||||
|
"lineStyle": {
|
||||||
|
"color": '#CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boundaryGap": true,
|
||||||
|
"data": []
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"type": 'value',
|
||||||
|
"axisTick": {
|
||||||
|
"show": false,
|
||||||
|
},
|
||||||
|
"axisLabel": {
|
||||||
|
"color": '#666666'
|
||||||
|
},
|
||||||
|
"axisLine": {
|
||||||
|
"lineStyle": {
|
||||||
|
"color": '#CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'line',
|
||||||
|
"data": [],
|
||||||
|
"barwidth": 20,
|
||||||
|
"label": {
|
||||||
|
"show": true,
|
||||||
|
"color": "#666666",
|
||||||
|
"position": 'top',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'axis'
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"top": 30,
|
||||||
|
"bottom": 50,
|
||||||
|
"right": 15,
|
||||||
|
"left": 40
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"bottom": 'left',
|
||||||
|
},
|
||||||
|
"toolbox": {
|
||||||
|
"show": false,
|
||||||
|
},
|
||||||
|
"xAxis": {
|
||||||
|
"type": 'category',
|
||||||
|
"axisLabel": {
|
||||||
|
"color": '#666666'
|
||||||
|
},
|
||||||
|
"axisLine": {
|
||||||
|
"lineStyle": {
|
||||||
|
"color": '#CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boundaryGap": true,
|
||||||
|
"data": []
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"type": 'value',
|
||||||
|
"axisTick": {
|
||||||
|
"show": false,
|
||||||
|
},
|
||||||
|
"axisLabel": {
|
||||||
|
"color": '#666666'
|
||||||
|
},
|
||||||
|
"axisLine": {
|
||||||
|
"lineStyle": {
|
||||||
|
"color": '#CCCCCC'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'line',
|
||||||
|
"data": [],
|
||||||
|
"areaStyle": {},
|
||||||
|
"label": {
|
||||||
|
"show": true,
|
||||||
|
"color": "#666666",
|
||||||
|
"position": 'top',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"pie": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'item'
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"top": 40,
|
||||||
|
"bottom": 30,
|
||||||
|
"right": 15,
|
||||||
|
"left": 15
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"bottom": 'left',
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'pie',
|
||||||
|
"data": [],
|
||||||
|
"radius": '50%',
|
||||||
|
"label": {
|
||||||
|
"show": true,
|
||||||
|
"color": "#666666",
|
||||||
|
"position": 'top',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ring": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'item'
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"top": 40,
|
||||||
|
"bottom": 30,
|
||||||
|
"right": 15,
|
||||||
|
"left": 15
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"bottom": 'left',
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'pie',
|
||||||
|
"data": [],
|
||||||
|
"radius": ['40%', '70%'],
|
||||||
|
"avoidLabelOverlap": false,
|
||||||
|
"label": {
|
||||||
|
"show": true,
|
||||||
|
"color": "#666666",
|
||||||
|
"position": 'top',
|
||||||
|
},
|
||||||
|
"labelLine": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rose": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'item'
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"top": 'bottom'
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'pie',
|
||||||
|
"data": [],
|
||||||
|
"radius": "55%",
|
||||||
|
"center": ['50%', '50%'],
|
||||||
|
"roseType": 'area',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"funnel": {
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": 'item',
|
||||||
|
"formatter": "{b} : {c}%"
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"top": 'bottom'
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'funnel',
|
||||||
|
"left": '10%',
|
||||||
|
"top": 60,
|
||||||
|
"bottom": 60,
|
||||||
|
"width": '80%',
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"minSize": '0%',
|
||||||
|
"maxSize": '100%',
|
||||||
|
"sort": 'descending',
|
||||||
|
"gap": 2,
|
||||||
|
"label": {
|
||||||
|
"show": true,
|
||||||
|
"position": 'inside'
|
||||||
|
},
|
||||||
|
"labelLine": {
|
||||||
|
"length": 10,
|
||||||
|
"lineStyle": {
|
||||||
|
"width": 1,
|
||||||
|
"type": 'solid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"itemStyle": {
|
||||||
|
"bordercolor": '#fff',
|
||||||
|
"borderwidth": 1
|
||||||
|
},
|
||||||
|
"emphasis": {
|
||||||
|
"label": {
|
||||||
|
"fontSize": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gauge": {
|
||||||
|
"color": color,
|
||||||
|
"tooltip": {
|
||||||
|
"formatter": '{a} <br/>{b} : {c}%'
|
||||||
|
},
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '业务指标',
|
||||||
|
"type": 'gauge',
|
||||||
|
"detail": {"formatter": '{value}%'},
|
||||||
|
"data": [{"value": 50, "name": '完成率'}]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"candle": {
|
||||||
|
"xAxis": {
|
||||||
|
"data": []
|
||||||
|
},
|
||||||
|
"yAxis": {},
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"text": ''
|
||||||
|
},
|
||||||
|
"dataZoom": [{
|
||||||
|
"type": 'inside',
|
||||||
|
"xAxisIndex": [0, 1],
|
||||||
|
"start": 10,
|
||||||
|
"end": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"show": true,
|
||||||
|
"xAxisIndex": [0, 1],
|
||||||
|
"type": 'slider',
|
||||||
|
"bottom": 10,
|
||||||
|
"start": 10,
|
||||||
|
"end": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seriesTemplate": {
|
||||||
|
"name": '',
|
||||||
|
"type": 'k',
|
||||||
|
"data": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default cfe;
|
||||||
@ -0,0 +1,606 @@
|
|||||||
|
/*
|
||||||
|
* uCharts®
|
||||||
|
* 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
|
||||||
|
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
|
||||||
|
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||||
|
* 复制使用请保留本段注释,感谢支持开源!
|
||||||
|
*
|
||||||
|
* uCharts®官方网站
|
||||||
|
* https://www.uCharts.cn
|
||||||
|
*
|
||||||
|
* 开源地址:
|
||||||
|
* https://gitee.com/uCharts/uCharts
|
||||||
|
*
|
||||||
|
* uni-app插件市场地址:
|
||||||
|
* http://ext.dcloud.net.cn/plugin?id=271
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
|
||||||
|
const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
|
||||||
|
|
||||||
|
//事件转换函数,主要用作格式化x轴为时间轴,根据需求自行修改
|
||||||
|
const formatDateTime = (timeStamp, returnType)=>{
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(timeStamp * 1000);
|
||||||
|
var y = date.getFullYear();
|
||||||
|
var m = date.getMonth() + 1;
|
||||||
|
m = m < 10 ? ('0' + m) : m;
|
||||||
|
var d = date.getDate();
|
||||||
|
d = d < 10 ? ('0' + d) : d;
|
||||||
|
var h = date.getHours();
|
||||||
|
h = h < 10 ? ('0' + h) : h;
|
||||||
|
var minute = date.getMinutes();
|
||||||
|
var second = date.getSeconds();
|
||||||
|
minute = minute < 10 ? ('0' + minute) : minute;
|
||||||
|
second = second < 10 ? ('0' + second) : second;
|
||||||
|
if(returnType == 'full'){return y + '-' + m + '-' + d + ' '+ h +':' + minute + ':' + second;}
|
||||||
|
if(returnType == 'y-m-d'){return y + '-' + m + '-' + d;}
|
||||||
|
if(returnType == 'h:m'){return h +':' + minute;}
|
||||||
|
if(returnType == 'h:m:s'){return h +':' + minute +':' + second;}
|
||||||
|
return [y, m, d, h, minute, second];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfu = {
|
||||||
|
//demotype为自定义图表类型,一般不需要自定义图表类型,只需要改根节点上对应的类型即可
|
||||||
|
"type":["pie","ring","rose","word","funnel","map","arcbar","line","column","mount","bar","area","radar","gauge","candle","mix","tline","tarea","scatter","bubble","demotype"],
|
||||||
|
"range":["饼状图","圆环图","玫瑰图","词云图","漏斗图","地图","圆弧进度条","折线图","柱状图","山峰图","条状图","区域图","雷达图","仪表盘","K线图","混合图","时间轴折线","时间轴区域","散点图","气泡图","自定义类型"],
|
||||||
|
//增加自定义图表类型,如果需要categories,请在这里加入您的图表类型,例如最后的"demotype"
|
||||||
|
//自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴(矢量x轴)类图表,没有categories,不需要加入categories
|
||||||
|
"categories":["line","column","mount","bar","area","radar","gauge","candle","mix","demotype"],
|
||||||
|
//instance为实例变量承载属性,不要删除
|
||||||
|
"instance":{},
|
||||||
|
//option为opts及eopts承载属性,不要删除
|
||||||
|
"option":{},
|
||||||
|
//下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
|
||||||
|
"formatter":{
|
||||||
|
"yAxisDemo1":function(val, index, opts){return val+'元'},
|
||||||
|
"yAxisDemo2":function(val, index, opts){return val.toFixed(2)},
|
||||||
|
"xAxisDemo1":function(val, index, opts){return val+'年';},
|
||||||
|
"xAxisDemo2":function(val, index, opts){return formatDateTime(val,'h:m')},
|
||||||
|
"seriesDemo1":function(val, index, series, opts){return val+'元'},
|
||||||
|
"tooltipDemo1":function(item, category, index, opts){
|
||||||
|
if(index==0){
|
||||||
|
return '随便用'+item.data+'年'
|
||||||
|
}else{
|
||||||
|
return '其他我没改'+item.data+'天'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pieDemo":function(val, index, series, opts){
|
||||||
|
if(index !== undefined){
|
||||||
|
return series[index].name+':'+series[index].data+'元'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
//这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在opts参数,会将demotype与opts中option合并后渲染图表。
|
||||||
|
"demotype":{
|
||||||
|
//我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
|
||||||
|
"type": "line",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,10,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"gridType": "dash",
|
||||||
|
"dashLength": 2,
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"line": {
|
||||||
|
"type": "curve",
|
||||||
|
"width": 2
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//下面是自定义配置,请添加项目所需的通用配置
|
||||||
|
"pie":{
|
||||||
|
"type": "pie",
|
||||||
|
"color": color,
|
||||||
|
"padding": [5,5,5,5],
|
||||||
|
"extra": {
|
||||||
|
"pie": {
|
||||||
|
"activeOpacity": 0.5,
|
||||||
|
"activeRadius": 10,
|
||||||
|
"offsetAngle": 0,
|
||||||
|
"labelWidth": 15,
|
||||||
|
"border": true,
|
||||||
|
"borderWidth": 3,
|
||||||
|
"borderColor": "#FFFFFF"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ring":{
|
||||||
|
"type": "ring",
|
||||||
|
"color": color,
|
||||||
|
"padding": [5,5,5,5],
|
||||||
|
"rotate": false,
|
||||||
|
"dataLabel": true,
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"position": "right",
|
||||||
|
"lineHeight": 25,
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "收益率",
|
||||||
|
"fontSize": 15,
|
||||||
|
"color": "#666666"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"name": "70%",
|
||||||
|
"fontSize": 25,
|
||||||
|
"color": "#7cb5ec"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"ring": {
|
||||||
|
"ringWidth":30,
|
||||||
|
"activeOpacity": 0.5,
|
||||||
|
"activeRadius": 10,
|
||||||
|
"offsetAngle": 0,
|
||||||
|
"labelWidth": 15,
|
||||||
|
"border": true,
|
||||||
|
"borderWidth": 3,
|
||||||
|
"borderColor": "#FFFFFF"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rose":{
|
||||||
|
"type": "rose",
|
||||||
|
"color": color,
|
||||||
|
"padding": [5,5,5,5],
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"position": "left",
|
||||||
|
"lineHeight": 25,
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"rose": {
|
||||||
|
"type": "area",
|
||||||
|
"minRadius": 50,
|
||||||
|
"activeOpacity": 0.5,
|
||||||
|
"activeRadius": 10,
|
||||||
|
"offsetAngle": 0,
|
||||||
|
"labelWidth": 15,
|
||||||
|
"border": false,
|
||||||
|
"borderWidth": 2,
|
||||||
|
"borderColor": "#FFFFFF"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"word":{
|
||||||
|
"type": "word",
|
||||||
|
"color": color,
|
||||||
|
"extra": {
|
||||||
|
"word": {
|
||||||
|
"type": "normal",
|
||||||
|
"autoColors": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"funnel":{
|
||||||
|
"type": "funnel",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,15,0,15],
|
||||||
|
"extra": {
|
||||||
|
"funnel": {
|
||||||
|
"activeOpacity": 0.3,
|
||||||
|
"activeWidth": 10,
|
||||||
|
"border": true,
|
||||||
|
"borderWidth": 2,
|
||||||
|
"borderColor": "#FFFFFF",
|
||||||
|
"fillOpacity": 1,
|
||||||
|
"labelAlign": "right"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"map":{
|
||||||
|
"type": "map",
|
||||||
|
"color": color,
|
||||||
|
"padding": [0,0,0,0],
|
||||||
|
"dataLabel": true,
|
||||||
|
"extra": {
|
||||||
|
"map": {
|
||||||
|
"border": true,
|
||||||
|
"borderWidth": 1,
|
||||||
|
"borderColor": "#666666",
|
||||||
|
"fillOpacity": 0.6,
|
||||||
|
"activeBorderColor": "#F04864",
|
||||||
|
"activeFillColor": "#FACC14",
|
||||||
|
"activeFillOpacity": 1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arcbar":{
|
||||||
|
"type": "arcbar",
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"name": "百分比",
|
||||||
|
"fontSize": 25,
|
||||||
|
"color": "#00FF00"
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"name": "默认标题",
|
||||||
|
"fontSize": 15,
|
||||||
|
"color": "#666666"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"arcbar": {
|
||||||
|
"type": "default",
|
||||||
|
"width": 12,
|
||||||
|
"backgroundColor": "#E9E9E9",
|
||||||
|
"startAngle": 0.75,
|
||||||
|
"endAngle": 0.25,
|
||||||
|
"gap": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"line":{
|
||||||
|
"type": "line",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,10,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"gridType": "dash",
|
||||||
|
"dashLength": 2,
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"line": {
|
||||||
|
"type": "straight",
|
||||||
|
"width": 2,
|
||||||
|
"activeType": "hollow"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tline":{
|
||||||
|
"type": "line",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,10,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": false,
|
||||||
|
"boundaryGap":"justify",
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"gridType": "dash",
|
||||||
|
"dashLength": 2,
|
||||||
|
"data":[
|
||||||
|
{
|
||||||
|
"min":0,
|
||||||
|
"max":80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"line": {
|
||||||
|
"type": "curve",
|
||||||
|
"width": 2,
|
||||||
|
"activeType": "hollow"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tarea":{
|
||||||
|
"type": "area",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,10,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
"boundaryGap":"justify",
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"gridType": "dash",
|
||||||
|
"dashLength": 2,
|
||||||
|
"data":[
|
||||||
|
{
|
||||||
|
"min":0,
|
||||||
|
"max":80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"area": {
|
||||||
|
"type": "curve",
|
||||||
|
"opacity": 0.2,
|
||||||
|
"addLine": true,
|
||||||
|
"width": 2,
|
||||||
|
"gradient": true,
|
||||||
|
"activeType": "hollow"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"column":{
|
||||||
|
"type": "column",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,15,0,5],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"data":[{"min":0}]
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"column": {
|
||||||
|
"type": "group",
|
||||||
|
"width": 30,
|
||||||
|
"activeBgColor": "#000000",
|
||||||
|
"activeBgOpacity": 0.08
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mount":{
|
||||||
|
"type": "mount",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,15,0,5],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"data":[{"min":0}]
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"mount": {
|
||||||
|
"type": "mount",
|
||||||
|
"widthRatio": 1.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bar":{
|
||||||
|
"type": "bar",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,30,0,5],
|
||||||
|
"xAxis": {
|
||||||
|
"boundaryGap":"justify",
|
||||||
|
"disableGrid":false,
|
||||||
|
"min":0,
|
||||||
|
"axisLine":false
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"bar": {
|
||||||
|
"type": "group",
|
||||||
|
"width": 30,
|
||||||
|
"meterBorde": 1,
|
||||||
|
"meterFillColor": "#FFFFFF",
|
||||||
|
"activeBgColor": "#000000",
|
||||||
|
"activeBgOpacity": 0.08
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"area":{
|
||||||
|
"type": "area",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,15,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"gridType": "dash",
|
||||||
|
"dashLength": 2,
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"area": {
|
||||||
|
"type": "straight",
|
||||||
|
"opacity": 0.2,
|
||||||
|
"addLine": true,
|
||||||
|
"width": 2,
|
||||||
|
"gradient": false,
|
||||||
|
"activeType": "hollow"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radar":{
|
||||||
|
"type": "radar",
|
||||||
|
"color": color,
|
||||||
|
"padding": [5,5,5,5],
|
||||||
|
"dataLabel": false,
|
||||||
|
"legend": {
|
||||||
|
"show": true,
|
||||||
|
"position": "right",
|
||||||
|
"lineHeight": 25,
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"radar": {
|
||||||
|
"gridType": "radar",
|
||||||
|
"gridColor": "#CCCCCC",
|
||||||
|
"gridCount": 3,
|
||||||
|
"opacity": 0.2,
|
||||||
|
"max": 200,
|
||||||
|
"labelShow": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gauge":{
|
||||||
|
"type": "gauge",
|
||||||
|
"color": color,
|
||||||
|
"title": {
|
||||||
|
"name": "66Km/H",
|
||||||
|
"fontSize": 25,
|
||||||
|
"color": "#2fc25b",
|
||||||
|
"offsetY": 50
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"name": "实时速度",
|
||||||
|
"fontSize": 15,
|
||||||
|
"color": "#1890ff",
|
||||||
|
"offsetY": -50
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"gauge": {
|
||||||
|
"type": "default",
|
||||||
|
"width": 30,
|
||||||
|
"labelColor": "#666666",
|
||||||
|
"startAngle": 0.75,
|
||||||
|
"endAngle": 0.25,
|
||||||
|
"startNumber": 0,
|
||||||
|
"endNumber": 100,
|
||||||
|
"labelFormat": "",
|
||||||
|
"splitLine": {
|
||||||
|
"fixRadius": 0,
|
||||||
|
"splitNumber": 10,
|
||||||
|
"width": 30,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"childNumber": 5,
|
||||||
|
"childWidth": 12
|
||||||
|
},
|
||||||
|
"pointer": {
|
||||||
|
"width": 24,
|
||||||
|
"color": "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"candle":{
|
||||||
|
"type": "candle",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,15,0,15],
|
||||||
|
"enableScroll": true,
|
||||||
|
"enableMarkLine": true,
|
||||||
|
"dataLabel": false,
|
||||||
|
"xAxis": {
|
||||||
|
"labelCount": 4,
|
||||||
|
"itemCount": 40,
|
||||||
|
"disableGrid": true,
|
||||||
|
"gridColor": "#CCCCCC",
|
||||||
|
"gridType": "solid",
|
||||||
|
"dashLength": 4,
|
||||||
|
"scrollShow": true,
|
||||||
|
"scrollAlign": "left",
|
||||||
|
"scrollColor": "#A6A6A6",
|
||||||
|
"scrollBackgroundColor": "#EFEBEF"
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"candle": {
|
||||||
|
"color": {
|
||||||
|
"upLine": "#f04864",
|
||||||
|
"upFill": "#f04864",
|
||||||
|
"downLine": "#2fc25b",
|
||||||
|
"downFill": "#2fc25b"
|
||||||
|
},
|
||||||
|
"average": {
|
||||||
|
"show": true,
|
||||||
|
"name": ["MA5","MA10","MA30"],
|
||||||
|
"day": [5,10,20],
|
||||||
|
"color": ["#1890ff","#2fc25b","#facc14"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"markLine": {
|
||||||
|
"type": "dash",
|
||||||
|
"dashLength": 5,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"value": 2150,
|
||||||
|
"lineColor": "#f04864",
|
||||||
|
"showLabel": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2350,
|
||||||
|
"lineColor": "#f04864",
|
||||||
|
"showLabel": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mix":{
|
||||||
|
"type": "mix",
|
||||||
|
"color": color,
|
||||||
|
"padding": [15,15,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": true,
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"disabled": false,
|
||||||
|
"disableGrid": false,
|
||||||
|
"splitNumber": 5,
|
||||||
|
"gridType": "dash",
|
||||||
|
"dashLength": 4,
|
||||||
|
"gridColor": "#CCCCCC",
|
||||||
|
"padding": 10,
|
||||||
|
"showTitle": true,
|
||||||
|
"data": []
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"mix": {
|
||||||
|
"column": {
|
||||||
|
"width": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scatter":{
|
||||||
|
"type": "scatter",
|
||||||
|
"color":color,
|
||||||
|
"padding":[15,15,0,15],
|
||||||
|
"dataLabel":false,
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": false,
|
||||||
|
"gridType":"dash",
|
||||||
|
"splitNumber":5,
|
||||||
|
"boundaryGap":"justify",
|
||||||
|
"min":0
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"disableGrid": false,
|
||||||
|
"gridType":"dash",
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"scatter": {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bubble":{
|
||||||
|
"type": "bubble",
|
||||||
|
"color":color,
|
||||||
|
"padding":[15,15,0,15],
|
||||||
|
"xAxis": {
|
||||||
|
"disableGrid": false,
|
||||||
|
"gridType":"dash",
|
||||||
|
"splitNumber":5,
|
||||||
|
"boundaryGap":"justify",
|
||||||
|
"min":0,
|
||||||
|
"max":250
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"disableGrid": false,
|
||||||
|
"gridType":"dash",
|
||||||
|
"data":[{
|
||||||
|
"min":0,
|
||||||
|
"max":150
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"bubble": {
|
||||||
|
"border":2,
|
||||||
|
"opacity": 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default cfu;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
# uCharts JSSDK说明
|
||||||
|
1、如不使用uCharts组件,可直接引用u-charts.js,打包编译后会`自动压缩`,压缩后体积约为`120kb`。
|
||||||
|
2、如果120kb的体积仍需压缩,请手到uCharts官网通过在线定制选择您需要的图表。
|
||||||
|
3、config-ucharts.js为uCharts组件的用户配置文件,升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
|
||||||
|
4、config-echarts.js为ECharts组件的用户配置文件,升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。
|
||||||
7706
src/uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.js
Normal file
7706
src/uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.js
Normal file
File diff suppressed because it is too large
Load Diff
18
src/uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.min.js
vendored
Normal file
18
src/uni_modules/qiun-data-charts/js_sdk/u-charts/u-charts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
201
src/uni_modules/qiun-data-charts/license.md
Normal file
201
src/uni_modules/qiun-data-charts/license.md
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
80
src/uni_modules/qiun-data-charts/package.json
Normal file
80
src/uni_modules/qiun-data-charts/package.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"id": "qiun-data-charts",
|
||||||
|
"displayName": "秋云 ucharts echarts 高性能跨全端图表组件",
|
||||||
|
"version": "2.5.0-20230101",
|
||||||
|
"description": "uCharts 新增正负柱状图!支持H5及APP用 ucharts echarts 渲染图表,uniapp可视化首选组件",
|
||||||
|
"keywords": [
|
||||||
|
"ucharts",
|
||||||
|
"echarts",
|
||||||
|
"f2",
|
||||||
|
"图表",
|
||||||
|
"可视化"
|
||||||
|
],
|
||||||
|
"repository": "https://gitee.com/uCharts/uCharts",
|
||||||
|
"engines": {
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": "474119"
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": "https://www.npmjs.com/~qiun",
|
||||||
|
"type": "component-vue"
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "y",
|
||||||
|
"联盟": "y"
|
||||||
|
},
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/uni_modules/qiun-data-charts/readme.md
Normal file
84
src/uni_modules/qiun-data-charts/readme.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
[](https://gitee.com/uCharts/uCharts/stargazers)
|
||||||
|
[](https://gitee.com/uCharts/uCharts/members)
|
||||||
|
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
|
[](https://www.npmjs.com/~qiun)
|
||||||
|
|
||||||
|
|
||||||
|
## uCharts简介
|
||||||
|
|
||||||
|
`uCharts`是一款基于`canvas API`开发的适用于所有前端应用的图表库,开发者编写一套代码,可运行到 Web、iOS、Android(基于 uni-app / taro )、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等更多支持 canvas API 的平台。
|
||||||
|
|
||||||
|
## 官方网站
|
||||||
|
|
||||||
|
## [https://www.ucharts.cn](https://www.ucharts.cn)
|
||||||
|
|
||||||
|
## 快速体验
|
||||||
|
|
||||||
|
一套代码编到多个平台,依次扫描二维码,亲自体验uCharts图表跨平台效果!其他平台请自行编译。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 致开发者
|
||||||
|
|
||||||
|
感谢各位开发者`五年`来对秋云及uCharts的支持,uCharts的进步离不开各位开发者的鼓励与贡献。为更好的帮助各位开发者使用图表工具,我们推出了新版官网,增加了在线定制、问答社区、在线配置等一些增值服务,为确保您能更好的应用图表组件,建议您先`仔细阅读官网指南`以及`常见问题`,而不是下载下来`直接使用`。如仍然不能解决,请到`官网社区`或开通会员后加入`专属VIP会员群`提问将会很快得到回答。
|
||||||
|
|
||||||
|
## 视频教程
|
||||||
|
|
||||||
|
## [uCharts新手入门教程](https://www.bilibili.com/video/BV1qA411Q7se/?share_source=copy_web&vd_source=42a1242f9aaade6427736af69eb2e1d9)
|
||||||
|
|
||||||
|
|
||||||
|
## 社群支持
|
||||||
|
|
||||||
|
uCharts官方拥有5个2000人的QQ群及专属VIP会员群支持,庞大的用户量证明我们一直在努力,请各位放心使用!uCharts的开源图表组件的开发,团队付出了大量的时间与精力,经过四来的考验,不会有比较明显的bug,请各位放心使用。如果您有更好的想法,可以在`码云提交Pull Requests`以帮助更多开发者完成需求,再次感谢各位对uCharts的鼓励与支持!
|
||||||
|
|
||||||
|
#### 官方交流群
|
||||||
|
- 交流群1:371774600(已满)
|
||||||
|
- 交流群2:619841586(已满)
|
||||||
|
- 交流群3:955340127(已满)
|
||||||
|
- 交流群4:641669795(已满)
|
||||||
|
- 交流群5:236294809(只能扫码加入)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 口令`uniapp`
|
||||||
|
|
||||||
|
#### 专属VIP会员群
|
||||||
|
- 开通会员后详见【账号详情】页面中顶部的滚动通知
|
||||||
|
- 口令`您的用户ID`
|
||||||
|
|
||||||
|
## 版权信息
|
||||||
|
|
||||||
|
uCharts始终坚持开源,遵循 [Apache Licence 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 开源协议,意味着您无需支付任何费用,即可将uCharts应用到您的产品中。
|
||||||
|
|
||||||
|
注意:这并不意味着您可以将uCharts应用到非法的领域,比如涉及赌博,暴力等方面。如因此产生纠纷或法律问题,uCharts相关方及秋云科技不承担任何责任。
|
||||||
|
|
||||||
|
## 合作伙伴
|
||||||
|
|
||||||
|
[](https://www.diygw.com/)
|
||||||
|
[](https://gitee.com/howcode/has-chat)
|
||||||
|
[](https://www.uviewui.com/)
|
||||||
|
[](https://ext.dcloud.net.cn/plugin?id=7088)
|
||||||
|
[](https://ext.dcloud.net.cn/publisher?id=202)
|
||||||
|
[](https://www.firstui.cn/)
|
||||||
|
[](https://ext.dcloud.net.cn/plugin?id=5169)
|
||||||
|
[](https://www.graceui.com/)
|
||||||
|
|
||||||
|
|
||||||
|
## 更新记录
|
||||||
|
|
||||||
|
详见官网指南中说明,[点击此处查看](https://www.ucharts.cn/v2/#/guide/index?id=100)
|
||||||
|
|
||||||
|
|
||||||
|
## 相关链接
|
||||||
|
- [uCharts官网](https://www.ucharts.cn)
|
||||||
|
- [DCloud插件市场地址](https://ext.dcloud.net.cn/plugin?id=271)
|
||||||
|
- [uCharts码云开源托管地址](https://gitee.com/uCharts/uCharts) [](https://gitee.com/uCharts/uCharts/stargazers)
|
||||||
|
- [uCharts npm开源地址](https://www.ucharts.cn)
|
||||||
|
- [ECharts官网](https://echarts.apache.org/zh/index.html)
|
||||||
|
- [ECharts配置手册](https://echarts.apache.org/zh/option.html)
|
||||||
|
- [图表组件在项目中的应用 ReportPlus数据报表](https://www.ucharts.cn/v2/#/layout/info?id=1)
|
||||||
23
src/uni_modules/qiun-data-charts/static/app-plus/echarts.min.js
vendored
Normal file
23
src/uni_modules/qiun-data-charts/static/app-plus/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
23
src/uni_modules/qiun-data-charts/static/h5/echarts.min.js
vendored
Normal file
23
src/uni_modules/qiun-data-charts/static/h5/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
32
src/utils/cache.ts
Normal file
32
src/utils/cache.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 本地缓存工具
|
||||||
|
* 所有 key 自动加上 QY_ 前缀避免冲突
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PREFIX = 'QY_';
|
||||||
|
|
||||||
|
export const setCache = (key: string, value: any) => {
|
||||||
|
if (!key) throw new Error('cache key is required');
|
||||||
|
uni.setStorageSync(`${PREFIX}${key.toUpperCase()}`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCache = (key: string) => {
|
||||||
|
if (!key) throw new Error('cache key is required');
|
||||||
|
return uni.getStorageSync(`${PREFIX}${key.toUpperCase()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeCache = (key: string) => {
|
||||||
|
if (!key) throw new Error('cache key is required');
|
||||||
|
uni.removeStorageSync(`${PREFIX}${key.toUpperCase()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearCache = () => {
|
||||||
|
uni.clearStorageSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 常用 key 常量
|
||||||
|
export const CACHE_KEYS = {
|
||||||
|
TOKEN: 'token',
|
||||||
|
USER_INFO: 'userInfo',
|
||||||
|
ROLE: 'role',
|
||||||
|
} as const;
|
||||||
1
src/utils/cache/index.ts
vendored
Normal file
1
src/utils/cache/index.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export function ces() {}
|
||||||
74
src/utils/format.ts
Normal file
74
src/utils/format.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 通用格式化工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pad = (n: number) => (n < 10 ? `0${n}` : String(n));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间,模板支持 YYYY/MM/DD HH:mm:ss
|
||||||
|
*/
|
||||||
|
export const formatTime = (
|
||||||
|
time: string | number | Date | undefined | null,
|
||||||
|
template = 'YYYY/MM/DD HH:mm',
|
||||||
|
): string => {
|
||||||
|
if (!time) return '';
|
||||||
|
let d: Date;
|
||||||
|
if (typeof time === 'string') {
|
||||||
|
const fixed = time.includes('T') ? time : time.replace(/-/g, '/');
|
||||||
|
d = new Date(fixed);
|
||||||
|
} else {
|
||||||
|
d = new Date(time);
|
||||||
|
}
|
||||||
|
if (isNaN(d.getTime())) return '';
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace('YYYY', String(d.getFullYear()))
|
||||||
|
.replace('MM', pad(d.getMonth() + 1))
|
||||||
|
.replace('DD', pad(d.getDate()))
|
||||||
|
.replace('HH', pad(d.getHours()))
|
||||||
|
.replace('mm', pad(d.getMinutes()))
|
||||||
|
.replace('ss', pad(d.getSeconds()));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化耗时(毫秒)为 “1时2分3秒”
|
||||||
|
*/
|
||||||
|
export const formatCostTime = (ms: number): string => {
|
||||||
|
const total = Math.floor((Number(ms) || 0) / 1000);
|
||||||
|
const h = Math.floor(total / 3600);
|
||||||
|
const m = Math.floor((total % 3600) / 60);
|
||||||
|
const s = total % 60;
|
||||||
|
if (h > 0) return `${h}时${m}分${s}秒`;
|
||||||
|
if (m > 0) return `${m}分${s}秒`;
|
||||||
|
return `${s}秒`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时长(秒)为 mm:ss / HH:mm:ss
|
||||||
|
*/
|
||||||
|
export const formatDuration = (sec: number): string => {
|
||||||
|
const total = Math.max(0, Number(sec) || 0);
|
||||||
|
const h = Math.floor(total / 3600);
|
||||||
|
const m = Math.floor((total % 3600) / 60);
|
||||||
|
const s = total % 60;
|
||||||
|
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
|
||||||
|
return `${pad(m)}:${pad(s)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单延迟 promise
|
||||||
|
*/
|
||||||
|
export const sleep = (ms: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
clearTimeout(t);
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单 toast 包装
|
||||||
|
*/
|
||||||
|
export const showToast = (title: string, icon: 'none' | 'success' | 'error' | 'loading' = 'none') => {
|
||||||
|
uni.showToast({ title, icon, duration: 2000 });
|
||||||
|
};
|
||||||
27
src/utils/layout.ts
Normal file
27
src/utils/layout.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @author zhouyankai
|
||||||
|
* @description 这是公用的utils方法
|
||||||
|
* @Date: 2022-07-31
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断url是否是http或https
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isHttp(url: string) {
|
||||||
|
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为空 空为true 不是空为false
|
||||||
|
*/
|
||||||
|
export function validatenull(val: null | undefined | string) {
|
||||||
|
return (
|
||||||
|
val === null ||
|
||||||
|
val === undefined ||
|
||||||
|
val === '' ||
|
||||||
|
(Array.isArray(val) && val.length === 0) ||
|
||||||
|
(typeof val === 'object' && Object.keys(val).length === 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
153
src/utils/request.ts
Normal file
153
src/utils/request.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import Request from 'luch-request';
|
||||||
|
import { getCache, removeCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学小乐 AI 通用 HTTP 客户端(基于 luch-request)
|
||||||
|
*
|
||||||
|
* - 自动注入 Bearer Token
|
||||||
|
* - 自动注入租户头 x-tenant-id
|
||||||
|
* - code !== 200 全局 toast 错误信息
|
||||||
|
* - 401 自动清缓存并跳转登录
|
||||||
|
* - 网络异常时弹窗提示
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HOST = (import.meta.env.VITE_BASE_URL as string) || '';
|
||||||
|
const BASE_URL = `${HOST}/api/main`;
|
||||||
|
const TENANT_ID = '1966391196339204098';
|
||||||
|
let isReloginHandling = false;
|
||||||
|
|
||||||
|
interface BackendResp<T = any> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
msg?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
code: number;
|
||||||
|
data: any;
|
||||||
|
constructor(message: string, code: number, data?: any) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const http = new Request({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
timeout: 30000,
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8',
|
||||||
|
'x-tenant-id': TENANT_ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUnauthorized = (message = '登录已失效,请重新登录') => {
|
||||||
|
if (isReloginHandling) return;
|
||||||
|
isReloginHandling = true;
|
||||||
|
removeCache('token');
|
||||||
|
removeCache('userInfo');
|
||||||
|
removeCache('teacherInfo');
|
||||||
|
removeCache('role');
|
||||||
|
uni.showToast({ title: message, icon: 'none', duration: 2500 });
|
||||||
|
// 避免并发 401 触发多次跳转与多次弹窗
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/login/index' });
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------- 请求拦截 -----------
|
||||||
|
http.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = getCache('token');
|
||||||
|
if (token && config.header) {
|
||||||
|
// 默认带上 Authorization;接口可显式覆盖(传空字符串以匿名调用)
|
||||||
|
if (!('Authorization' in config.header)) {
|
||||||
|
(config.header as any).Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 始终携带租户 ID
|
||||||
|
if (config.header && !('x-tenant-id' in config.header)) {
|
||||||
|
(config.header as any)['x-tenant-id'] = TENANT_ID;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(err) => Promise.reject(err),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------- 响应拦截 -----------
|
||||||
|
http.interceptors.response.use(
|
||||||
|
(response: any) => {
|
||||||
|
const data = response.data as BackendResp;
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.code === 200) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.code === 401) {
|
||||||
|
const msg401 = data.message || data.msg || '登录已失效,请重新登录';
|
||||||
|
handleUnauthorized(msg401);
|
||||||
|
return Promise.reject(new HttpError(msg401, 401, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务错误
|
||||||
|
const msg = data.message || data.msg || '请求失败';
|
||||||
|
if (!(response.config as any)?.silent) {
|
||||||
|
uni.showToast({ title: msg, icon: 'none', duration: 2500 });
|
||||||
|
}
|
||||||
|
return Promise.reject(new HttpError(msg, data.code, data));
|
||||||
|
},
|
||||||
|
(err: any) => {
|
||||||
|
const status = err?.statusCode || err?.status;
|
||||||
|
let msg = err?.errMsg || err?.message || '网络异常';
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
msg = '登录已失效,请重新登录';
|
||||||
|
handleUnauthorized(msg);
|
||||||
|
} else if (status === 500) {
|
||||||
|
msg = '服务异常,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 401) {
|
||||||
|
uni.showToast({ title: msg, icon: 'none', duration: 2500 });
|
||||||
|
}
|
||||||
|
return Promise.reject(new HttpError(msg, status || -1, err));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认导出 luch-request 实例
|
||||||
|
* 同时暴露通用调用方法,兼容老接口的 `request({ url, method, data, params, headers })` 风格
|
||||||
|
*/
|
||||||
|
export interface RequestOptions {
|
||||||
|
url: string;
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
data?: anyObj;
|
||||||
|
params?: anyObj;
|
||||||
|
headers?: anyObj;
|
||||||
|
silent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const request = <T = any>(opt: RequestOptions): Promise<BackendResp<T>> => {
|
||||||
|
const cfg: any = {
|
||||||
|
url: opt.url,
|
||||||
|
method: opt.method || 'GET',
|
||||||
|
custom: { silent: opt.silent },
|
||||||
|
};
|
||||||
|
if (opt.headers) cfg.header = opt.headers;
|
||||||
|
if (cfg.method === 'GET') {
|
||||||
|
cfg.params = opt.params || opt.data;
|
||||||
|
} else {
|
||||||
|
cfg.data = opt.data;
|
||||||
|
if (opt.params) cfg.params = opt.params;
|
||||||
|
}
|
||||||
|
if (opt.silent) cfg.silent = true;
|
||||||
|
return http.request(cfg) as Promise<BackendResp<T>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default request;
|
||||||
|
export { http, HOST, BASE_URL, TENANT_ID };
|
||||||
114
stylelint.config.js
Normal file
114
stylelint.config.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'stylelint-config-standard',
|
||||||
|
'stylelint-config-prettier',
|
||||||
|
'stylelint-config-recommended-vue',
|
||||||
|
'stylelint-config-recommended-scss',
|
||||||
|
],
|
||||||
|
plugins: ['stylelint-order'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.vue'],
|
||||||
|
customSyntax: 'postcss-html',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'at-rule-no-unknown': [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignoreAtRules: ['plugin'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
|
||||||
|
'rule-empty-line-before': [
|
||||||
|
'always',
|
||||||
|
{
|
||||||
|
except: ['after-single-line-comment', 'first-nested'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'selector-pseudo-class-no-unknown': [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignorePseudoClasses: ['deep'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'order/properties-order': [
|
||||||
|
'position',
|
||||||
|
'top',
|
||||||
|
'right',
|
||||||
|
'bottom',
|
||||||
|
'left',
|
||||||
|
'z-index',
|
||||||
|
'display',
|
||||||
|
'justify-content',
|
||||||
|
'align-items',
|
||||||
|
'float',
|
||||||
|
'clear',
|
||||||
|
'overflow',
|
||||||
|
'overflow-x',
|
||||||
|
'overflow-y',
|
||||||
|
'margin',
|
||||||
|
'margin-top',
|
||||||
|
'margin-right',
|
||||||
|
'margin-bottom',
|
||||||
|
'margin-left',
|
||||||
|
'padding',
|
||||||
|
'padding-top',
|
||||||
|
'padding-right',
|
||||||
|
'padding-bottom',
|
||||||
|
'padding-left',
|
||||||
|
'width',
|
||||||
|
'min-width',
|
||||||
|
'max-width',
|
||||||
|
'height',
|
||||||
|
'min-height',
|
||||||
|
'max-height',
|
||||||
|
'font-size',
|
||||||
|
'font-family',
|
||||||
|
'font-weight',
|
||||||
|
'border',
|
||||||
|
'border-style',
|
||||||
|
'border-width',
|
||||||
|
'border-color',
|
||||||
|
'border-top',
|
||||||
|
'border-top-style',
|
||||||
|
'border-top-width',
|
||||||
|
'border-top-color',
|
||||||
|
'border-right',
|
||||||
|
'border-right-style',
|
||||||
|
'border-right-width',
|
||||||
|
'border-right-color',
|
||||||
|
'border-bottom',
|
||||||
|
'border-bottom-style',
|
||||||
|
'border-bottom-width',
|
||||||
|
'border-bottom-color',
|
||||||
|
'border-left',
|
||||||
|
'border-left-style',
|
||||||
|
'border-left-width',
|
||||||
|
'border-left-color',
|
||||||
|
'border-radius',
|
||||||
|
'text-align',
|
||||||
|
'text-justify',
|
||||||
|
'text-indent',
|
||||||
|
'text-overflow',
|
||||||
|
'text-decoration',
|
||||||
|
'white-space',
|
||||||
|
'color',
|
||||||
|
'background',
|
||||||
|
'background-position',
|
||||||
|
'background-repeat',
|
||||||
|
'background-size',
|
||||||
|
'background-color',
|
||||||
|
'background-clip',
|
||||||
|
'opacity',
|
||||||
|
'filter',
|
||||||
|
'list-style',
|
||||||
|
'outline',
|
||||||
|
'visibility',
|
||||||
|
'box-shadow',
|
||||||
|
'text-shadow',
|
||||||
|
'resize',
|
||||||
|
'transition',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
13
tailwind.config.js
Normal file
13
tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./public/index.html', './src/**/*.{html,js,ts,jsx,tsx,vue}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
corePlugins: {
|
||||||
|
// 不需要 preflight,因为这主要是给 h5 的,如果你要同时开发小程序和 h5 端,你应该使用环境变量来控制它
|
||||||
|
preflight: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"strict": true,
|
||||||
|
"isolatedModules": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"types": [
|
||||||
|
"@dcloudio/types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/uni_modules/**"]
|
||||||
|
}
|
||||||
87
vite.config.ts
Normal file
87
vite.config.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import uni from '@dcloudio/vite-plugin-uni';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import autoprefixer from 'autoprefixer';
|
||||||
|
import rems from 'postcss-rem-to-responsive-pixel';
|
||||||
|
import { UnifiedViteWeappTailwindcssPlugin as uvwt } from 'weapp-tailwindcss/vite';
|
||||||
|
import TransformPages from 'uni-read-pages-vite';
|
||||||
|
// import eslintPlugin from 'vite-plugin-eslint'; //eslint自动检查插件
|
||||||
|
|
||||||
|
const isH5 = process.env.UNI_PLATFORM === 'h5';
|
||||||
|
const isApp = process.env.UNI_PLATFORM === 'app-plus';
|
||||||
|
const WeappTailwindcssDisabled = isH5 || isApp;
|
||||||
|
|
||||||
|
const postcssPlugins = [tailwindcss(), autoprefixer()];
|
||||||
|
if (!WeappTailwindcssDisabled) {
|
||||||
|
postcssPlugins.push(
|
||||||
|
rems({
|
||||||
|
// 32 意味着 1rem = 32rpx
|
||||||
|
rootValue: 32,
|
||||||
|
// 默认所有属性都转化
|
||||||
|
propList: ['*'],
|
||||||
|
// 转化的单位,可以变成 px / rpx
|
||||||
|
transformUnit: 'rpx',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
//跨域配置
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
open: true, //是否自动打开浏览器
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
minify: 'terser',
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||||
|
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 关闭文件计算
|
||||||
|
reportCompressedSize: false,
|
||||||
|
// 关闭生成map文件 可以达到缩小打包体积
|
||||||
|
sourcemap: false, // 这个生产环境一定要关闭,不然打包的产物会很大
|
||||||
|
},
|
||||||
|
// 路径配置
|
||||||
|
resolve: {
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
find: '@',
|
||||||
|
replacement: resolve(__dirname, 'src'), //配置@ 路径
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
ROUTES: new TransformPages().routes, // 注入路由表
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
uni(),
|
||||||
|
uvwt({
|
||||||
|
disabled: WeappTailwindcssDisabled,
|
||||||
|
}),
|
||||||
|
// eslintPlugin({
|
||||||
|
// include: ['src/**/*.ts', 'src/**/*.vue', 'src/*.ts', 'src/*.vue'],
|
||||||
|
// }),
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: postcssPlugins,
|
||||||
|
},
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: '@import "uniapp-nutui/styles/variables.scss";',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user