add:第一次提交

This commit is contained in:
阿梦 2025-07-11 17:43:34 +08:00
commit 6460344c7a
990 changed files with 160864 additions and 0 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
VITE_HOST = https://pi.xuexiaole.com
# VITE_HOST = http://192.168.0.103
VITE_OSS_HOST = https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient
VITE_WS_URL = wss://pi.xuexiaole.com/ws/device

3
.env.prod Normal file
View File

@ -0,0 +1,3 @@
VITE_HOST = https://test.pi.xuexiaole.com
VITE_OSS_HOST = https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient
VITE_WS_URL = wss://test.pi.xuexiaole.com/ws/device

3
.env.test Normal file
View File

@ -0,0 +1,3 @@
VITE_HOST = https://test.pi.xuexiaole.com
VITE_OSS_HOST = https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient
VITE_WS_URL = wss://test.pi.xuexiaole.com/ws/device

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
src/uni_modules/**

54
.eslintrc.js Normal file
View File

@ -0,0 +1,54 @@
module.exports = {
extends: ['alloy', 'alloy/vue', 'alloy/typescript', 'plugin:@typescript-eslint/recommended'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: {
js: '@babel/eslint-parser',
jsx: '@babel/eslint-parser',
ts: '@typescript-eslint/parser',
tsx: '@typescript-eslint/parser',
},
},
rules: {
'@typescript-eslint/consistent-type-assertions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'no-return-assign': 'off',
'guard-for-in': 'off',
'vue/no-setup-props-destructure': 'off',
'vue/no-duplicate-attr-inheritance': 'off',
'no-eq-null': 'off', // 允许 == 用于 null
'vue/v-on-event-hyphenation': 'off', // 关闭 vue 中 @ 使用短横线命名
'no-duplicate-imports': 'off', // 使用ts-eslint的重复导入规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'vue/no-duplicate-attributes': [
'error',
{
allowCoexistClass: true, // 启用v-bind:class指令可以与普通class属性共存。默认值为true。
allowCoexistStyle: true,
},
],
// 圈复杂度 每个函数的最高圈复杂度
complexity: [
'error',
{
max: 12,
},
],
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly',
},
plugins: ['@typescript-eslint'],
};

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# 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?
dist*

16
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,16 @@
{ // launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm run lint-staged

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
src/uni_modules/**

13
.prettierrc.js Normal file
View File

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

51
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,51 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 1,
"editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "off",
"git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"files.associations": {
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll": "never",
"source.fixAll.eslint": "explicit"
},
"iconify.excludes": ["el"],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": [
"src/uni_modules/uni-table/i18n",
"src/uni_modules/wot-design-uni/locale",
"src/uni_modules/wot-design-uni/locale/lang",
"src/uni_modules/uni-calendar/components/uni-calendar/i18n",
"src/uni_modules/uni-countdown/components/uni-countdown/i18n",
"src/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n",
"src/uni_modules/uni-fav/components/uni-fav/i18n",
"src/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n",
"src/uni_modules/uni-load-more/components/uni-load-more/i18n",
"src/uni_modules/uni-pagination/components/uni-pagination/i18n",
"src/uni_modules/uni-popup/components/uni-popup/i18n",
"src/uni_modules/uni-search-bar/components/uni-search-bar/i18n",
"src/uni_modules/z-paging/components/z-paging/i18n"
]
}

BIN
client-dist.zip Normal file

Binary file not shown.

24
commitlint.config.js Normal file
View File

@ -0,0 +1,24 @@
// build主要目的是修改项目构建系统(例如 glupwebpackrollup 的配置等)的提交
// ci主要目的是修改项目继续集成流程(例如 TravisJenkinsGitLab CICircle 等)的提交
// docs文档更新
// feat新增功能
// fixbug 修复
// perf性能优化
// refactor重构代码(既没有新增功能,也没有修复 bug)
// style不影响程序逻辑的代码修改(修改空白字符,补全缺失的分号等)
// test新增测试用例或是更新现有测试
// revert回滚某个更早之前的提交
// chore不属于以上类型的其他类型(日常事务)
module.exports = {
extends: ['@commitlint/config-conventional'],
};

20
index.html Normal file
View 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>

13983
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

126
package.json Normal file
View File

@ -0,0 +1,126 @@
{
"name": "xuexiaole-client",
"version": "0.1.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 -p h5-test",
"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",
"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 -p h5-prod",
"build-test:h5": "uni build -p h5-test",
"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",
"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": "eslint --fix",
"lint-staged": "lint-staged"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-3081220230817001",
"@dcloudio/uni-app-plus": "3.0.0-3081220230817001",
"@dcloudio/uni-components": "3.0.0-3081220230817001",
"@dcloudio/uni-h5": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-alipay": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-baidu": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-jd": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-lark": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-qq": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-toutiao": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-weixin": "3.0.0-3081220230817001",
"@dcloudio/uni-quickapp-webview": "3.0.0-3081220230817001",
"code-inspector-plugin": "^0.10.1",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"lint-staged": "^15.0.1",
"pinia": "^2.0.36",
"sass": "^1.72.0",
"unplugin-vue-define-options": "^1.4.2",
"vue": "^3.3.4",
"vue-i18n": "^9.1.9",
"@vueuse/core": "^10.11.0",
"vue-qrcode-reader": "^5.5.7",
"weixin-js-sdk": "^1.6.5"
},
"devDependencies": {
"@babel/core": "^7.23.2",
"@babel/eslint-parser": "^7.22.15",
"@commitlint/cli": "^17.8.0",
"@commitlint/config-conventional": "^17.8.0",
"@dcloudio/types": "^3.3.2",
"@dcloudio/uni-automator": "3.0.0-3081220230817001",
"@dcloudio/uni-cli-shared": "3.0.0-3081220230817001",
"@dcloudio/uni-stacktracey": "3.0.0-3081220230817001",
"@dcloudio/vite-plugin-uni": "3.0.0-3081220230817001",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"@vue/runtime-core": "^3.2.45",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.51.0",
"eslint-config-alloy": "^5.1.2",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-vue": "^9.17.0",
"husky": "^8.0.0",
"prettier": "^3.0.3",
"typescript": "^4.9.5",
"vite": "4.1.4",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.0.24"
},
"lint-staged": {
"*.{json,md,css,less}": [
"prettier --write"
],
"*.{js,ts,jsx,tsx,vue}": [
"prettier --write",
"eslint --fix"
]
},
"uni-app": {
"scripts": {
"h5-test": {
"title": "h5-test",
"env": {
"UNI_PLATFORM": "h5",
"VITE_HOST": "https://test.pi.xuexiaole.com",
"VITE_WS_URL": "wss://test.pi.xuexiaole.com/ws/device"
}
},
"h5-prod": {
"title": "h5-prod",
"env": {
"UNI_PLATFORM": "h5",
"VITE_HOST": "https://pi.xuexiaole.com",
"VITE_WS_URL": "wss://pi.xuexiaole.com/ws/device"
}
}
}
}
}

10587
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
shims-uni.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types='@dcloudio/types' />
import 'vue';
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

19
src/App.vue Normal file
View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { onMounted, nextTick } from 'vue';
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">
page {
background-color: #f7f8fa;
}
</style>

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

@ -0,0 +1,56 @@
import $req from './request';
// 获取用户信息
export const getUserInfo = () =>
$req({
url: '/sysUser/selectUser',
});
// 获取客服信息
export const getCsInfo = (id: string) =>
$req({
url: `/customerService/getInfoByUserId/${id}`,
});
// 更新用户信息
export const updateUserInfo = (data: anyObj) =>
$req({
url: '/sysUser/editPerfectMessage',
method: 'post',
data,
});
// 获取协议
export const getAgreeInfo = (id: string, isChat = false) =>
$req({
url: '/agreement/' + id,
headers: {
Authorization: '',
},
});
// 获取字典
export const getDict = () =>
$req({
url: '/sysDictType/tree',
});
// 设备通知注册
export const bindRegId = (data: anyObj) =>
$req({
url: '/sysUser/dealRegistrationId',
method: 'post',
data,
});
// 获取未读消息数量
export const getUnreadNum = () => $req({ url: '/customerService/getUnreadNum' });
// 支付创建订单
export const createInspectorPrepayOrder = (data: anyObj) => {
return $req({
method: 'post',
url: '/orderManagement/createInspectorPrepayOrder',
data,
});
};

15
src/api/index.ts Normal file
View File

@ -0,0 +1,15 @@
export * from './global';
export * from './login';
export * from './modules/warranty';
export * from './modules/awardManage';
export * from './modules/taskManage';
export * from './modules/bindDevice';
export * from './modules/parent';
export * from './modules/mySupervisionService';
export * from './modules/bindDevice';
export * from './modules/mine';
export * from './modules/academicReport';
export * from './modules/deviceScreenshotRecord';
export * from './modules/inviteBind';
export * from './modules/home';
export * from './modules/applicationManagement';

36
src/api/inspector/mine.ts Normal file
View File

@ -0,0 +1,36 @@
import $req from '../request';
/**
*
* @param data
*/
export const getUserInfo = (data = {}) => {
return $req({
method: 'post',
url: '/phone/inspectorTeacher/myInfo',
data,
});
};
/**
*
* @param params
*/
export const queryUserRoles = (params = {}) => {
return $req({
method: 'get',
url: '/sysUser/getAdminTypeByPhone',
params,
});
};
/**
*
*/
export const switchRole = (adminType: number) => {
return $req({
method: 'get',
url: '/sysUser/changeAdminType',
params: {
adminType,
},
});
};

View File

@ -0,0 +1,111 @@
import $req from '../request';
/**
*
* @param params
*/
export const getStudentDetail = (params = {}) => {
return $req({
method: 'get',
url: '/phone/inspectorTeacher/getStuVoByOrderIdAndCourseDate',
params,
});
};
/**
*
* @param data
*/
export const getUserReportPage = (data = {}) => {
return $req({
method: 'post',
url: '/phone/inspectorTeacher/getUserReportPage',
data,
});
};
/**
*
* @param params
*/
export const getUserInspectorCourseReportInfo = (params = {}) => {
return $req({
method: 'get',
url: '/phone/inspectorTeacher/userInspectorCourseReportInfo',
params,
});
};
/**
*
* @param data
*/
export const queryStudyPlanList = (data = {}) => {
return $req({
method: 'post',
url: '/phone/inspectorTeacher/queryStuPlanPage',
data,
});
};
/**
*
* @param planId id
*/
export const deletePlanRequest = (planId: string) => {
return $req({
method: 'post',
url: `/phone/inspectorTeacher/deletePlanById?planId=${planId}`,
});
};
/**
*
* @param gradeId id
*/
export const getSubjectListByGrade = (gradeId: string) => {
return $req({
method: 'get',
url: `/subject/list/${gradeId}`,
});
};
/**
*
* @param params
*/
export const queryStudyDuration = (params = {}) => {
return $req({
method: 'get',
url: '/userSubjectReport/study/duration',
params,
});
};
/**
*
* @param params
*/
export const queryVideoStudy = (params = {}) => {
return $req({
method: 'get',
url: '/userSubjectReport/study/video',
params,
});
};
/**
*
* @param params
*/
export const queryStudyKnowledge = (params = {}) => {
return $req({
method: 'get',
url: '/userSubjectReport/study/knowledge',
params,
});
};
/**
*
* @param params
*/
export const queryStudentWrongQuestion = (params = {}) => {
return $req({
method: 'get',
url: '/userSubjectReport/study/error',
params,
});
};

View File

@ -0,0 +1,27 @@
import $req from '../request';
/**
*
* @param data
*/
export const getCourseTable = (data={}) => {
return $req({
method: 'get',
url: '/phone/inspectorTeacher/getCourseTableById',
data,
});
};
/**
*
* @param data
*/
export const queryStudentList = (params={}) => {
return $req({
method:'get',
url:'/phone/inspectorTeacher/getMyStudent',
params,
});
}

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

@ -0,0 +1,88 @@
import $req from './request';
/**
*
* @param data
*/
export const getBindPhoneTypeApi = data => {
$req({
method: 'post',
url: '/xcx/login/getBindRelationList',
data,
});
};
/**
*
* @param data
*/
export const getAdminTypeByPhoneApi = (params: anyObj) =>
$req({
method: 'get',
url: '/sysUser/getAdminTypeByPhone',
params,
});
/**
*
* @param data
*/
export const sendCodeMessageApi = data =>
$req({
method: 'post',
url: `/sms/sendMessage?phoneNumbers=${data}`,
});
/**
*
* @param data
*/
export const smsLoginApi = data =>
$req({
method: 'post',
url: '/smsLogin',
data,
});
/**
*
* @param data
*/
export const getUserInfoApi = () =>
$req({
method: 'get',
url: '/sysUser/selectUser',
});
/**
*
* @param data
*/
export const updateUserInfoApi = (data: anyObj) =>
$req({
method: 'post',
url: '/sysUser/updateInfo',
data,
});
/**
*
* @param data
*/
export const bindAuthInfoApi = data =>
$req({
method: 'post',
url: '/wechatPublic/bindAuthInfo',
data,
});
/**
*
* @param data
*/
export const getAuthUrlApi = () =>
$req({
method: 'get',
url: '/wechatPublic/getAuthUrl',
});
// 退出登录
export const logout = () =>
$req({
url: '/logout',
});

View File

@ -0,0 +1,60 @@
import $req from '../request';
/**
* -
* @param data
*/
export const subjectApi = (gradeId: string) =>
$req({
method: 'get',
url: `/subject/list/${gradeId}`,
});
/**
* -
* @param data
*/
export const studyTimeStatApi = (params: anyobj) =>
$req({
method: 'get',
url: '/userSubjectReport/study/duration',
params,
});
/**
* -
* @param data
*/
export const videoStudyStatApi = (params: anyobj) =>
$req({
method: 'get',
url: '/userSubjectReport/study/video',
params,
});
/**
* -
* @param data
*/
export const knowledgeStudyStatApi = (params: anyobj) =>
$req({
method: 'get',
url: '/userSubjectReport/study/knowledge',
params,
});
/**
* -
* @param data
*/
export const studyErrorStatApi = (params: anyobj) =>
$req({
method: 'get',
url: '/userSubjectReport/study/error',
params,
});
/**
* -
* @param data
*/
export const englishLanguageStatApi = (data: anyobj) =>
$req({
method: 'post',
url: '/userSentenceLearn/queryStatListByTimeType',
data,
});

View File

@ -0,0 +1,10 @@
import $req from '../request';
/**
*
* @param data
*/
export const queryAppRecordApi = (serialNum: string) =>
$req({
method: 'get',
url: `/userAppRecord/queryAppRecord/${serialNum}`,
});

View File

@ -0,0 +1,49 @@
import $req from '../request';
/**
* -
* @param data
*/
export const getRewardListApi = params =>
$req({
method: 'get',
url: '/userRewardMapping/list',
params,
});
/**
* -
* @param data
*/
export const addRewardApi = data =>
$req({
method: 'post',
url: '/userRewardMapping',
data,
});
/**
* -
* @param data
*/
export const updateRewardApi = data =>
$req({
method: 'put',
url: '/userRewardMapping',
data,
});
/**
* -
* @param data
*/
export const deleteRewardApi = ids =>
$req({
method: 'delete',
url: `/userRewardMapping/${ids}`,
});
/**
* -
* @param data
*/
export const getRewardDetailApi = id =>
$req({
method: 'get',
url: `/userRewardMapping/${id}`,
});

View File

@ -0,0 +1,78 @@
import $req from '../request';
/**
*
* @param params
*/
export const getCurInfo = params =>
$req({
method: 'get',
url: '/parentBind/curInfo',
params,
});
/**
*
* @param data
*/
export const parentBindAdmin = data =>
$req({
method: 'post',
url: '/parentBind/admin',
data,
});
/**
*
* @param data
*/
export const parentBindApply = data =>
$req({
method: 'post',
url: '/parentBind/apply',
data,
});
/**
* -
* @param params
*/
export const adminInfo = params =>
$req({
method: 'get',
url: '/parentBind/adminInfo',
params,
});
/**
* -
* @param params
*/
export const applyInfo = params =>
$req({
method: 'get',
url: '/parentBind/apply/info',
params,
});
/**
*
* @param data
*/
export const adminApproval = data =>
$req({
method: 'post',
url: '/parentBind/admin/approval',
data,
});
/**
*
* @param data
*/
export const deviceTimeControl = data =>
$req({
method: 'post',
url: '/deviceTimeControl',
data,
});

View File

@ -0,0 +1,12 @@
import $req from '../request';
/**
* -
* @param params
*/
export const deviceScreenshotRecordList = params =>
$req({
method: 'get',
url: '/deviceScreenshotRecord/list',
params,
});

22
src/api/modules/home.ts Normal file
View File

@ -0,0 +1,22 @@
import $req from '../request';
/**
*
*
* @param params
*/
export const getDeviceScreenControlApi = (simSerialNumber: string) =>
$req({
method: 'get',
url: `/deviceTimeControl/${simSerialNumber}`,
});
/**
*
*
* @param params
*/
export const lockDeviceScreenApi = (simSerialNumber: string) =>
$req({
method: 'post',
url: `/deviceTimeControl/update/lock/${simSerialNumber}`,
});

View File

@ -0,0 +1,197 @@
import $req from '../request';
import { storeToRefs } from 'pinia';
import { user } from '@/store';
import { getCache } from '@/utils';
// 课表日历
export const getScheduleList = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: '/phone/inspectorTeacher/getCourseTableById',
data,
});
// 查询指定日期学生数据
export const getCourseDate = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url:
`/phone/inspectorTeacher/getCourseTabletStudentVoByTime` +
`${data.date ? `?courseDate=${data.date}` : ''}`,
// data,
});
// 开始督学
export const startInpector = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/phone/inspectorTeacher/startInspector?courseDate=${data.courseDate}`,
});
// 结束督学
export const endInpector = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/phone/inspectorTeacher/endInspector?courseDate=${data.courseDate}`,
});
// 督学实况
export const condition = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/phone/inspectorTeacher/inspector_real?courseDate=${data.courseDate}`,
});
// 获取学员课表日历
export const getCourseData = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/phone/inspectorTeacher/getStuCourseTable?userId=${data.id}`,
// data,
});
// 获取学科
export const getSubject = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/subject/list/${data.gradeId}`,
});
// 获取学科教材
export const getBooks = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/userTextBook/list?gradeId=${data.gradeId}&subjectId=${data.subjectId}&userId=${data.userId}&current=${data.current}`,
});
// 获取教材章节
export const getChaper = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/subjectChapter/tree?textBookId=${data.textBookId}`,
});
// 获取教材章节知识点
export const getKnowledge = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/subjectKnowledge/tree?chapterId=${data.chapterId}`,
});
// 发布计划
export const publishPlan = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/phone/inspectorTeacher/publishPlan`,
data,
});
// 获取学员计划
export const getStuPlan = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/phone/inspectorTeacher/getStuPlanByDate?userId=${data.userId}&courseDate=${data.courseDate}`,
});
// 获取督学师信息
export const getUserInfo = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/phone/inspectorTeacher/getCurrentTeacher`,
});
// 文件上传
export const uploadFile = async (file): Promise<any> => {
const { token } = storeToRefs(user());
const mergerToken = token.value || getCache('token');
const data = await uni.uploadFile({
url: `/api/main/sysFileInfo/tenUploadAll`, // ${import.meta.env.VITE_HOST}
file: file,
name: file.fileName,
header: {
// 'content-type': 'multipart/form-data',
Authorization: mergerToken ? `Bearer ${mergerToken}` : token,
},
});
return JSON.parse(data.data);
};
// 完善督学师资料 /phone/inspectorTeacher/updateTeacherInfo
export const updateTeacherInfo = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/phone/inspectorTeacher/updateTeacherInfo`,
data,
});
// 沟通列表
export const getTalkList = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/phone/inspectorTeacher/inspectorTalkListPage?current=${data.current}`,
});
// 沟通详情
export const getTalkDetails = (data?: Record<string, any>): Promise<any> => {
const param = data.inspectorTalkRecordId
? `?inspectorTalkRecordId=${data.inspectorTalkRecordId}&userId=${data.userId}`
: `?userId=${data.userId}`;
return $req({
method: 'get',
url: `/phone/inspectorTeacher/studentInspectorTalkPage${param}`,
});
};
// 消息存储
export const saveTalk = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/inspectorStudent/sendInspectorTalkRecord`,
data,
});
// 验证码校验
export const validCode = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/sms/validateMessage?phoneNumbers=${data.phone}&code=${data.code}`,
data,
});
// 获取我的未读信息
export const getUnreadCount = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/phone/inspectorTeacher/myUnreadCount`,
});
// 消息标记为已读
export const readMsg = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'post',
url: `/inspectorStudent/readTalkRecord`,
data,
});
// 督学师是否已完善资料
export const needPerfectInfo = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/phone/inspectorTeacher/is_perfect`,
});
// 学生是否可以进行精准学判断
export const getPrecisionFlag = (data?: Record<string, any>): Promise<any> =>
$req({
method: 'get',
url: `/userTrainTitle/airPrecisionFlag?subjectId=${data.subjectId}&userId=${data.userId}`,
});
export const getKnowledgeFlag = (knowledgeId: string): Promise<boolean> => {
return $req({
method: 'get',
url: `/subjectKnowledge/titleFlag?id=${knowledgeId}`,
});
}
export const getKnowledgeVideo = (chapterId: string | number): Promise<any> => {
return $req({
method: 'get',
url: `/subjectChapterVideo/list?chapterId=${chapterId}`,
});
}

View File

@ -0,0 +1,30 @@
import $req from '../request';
/**
*
* @param data
*/
export const getJsapiSignatureApi = (params: anyobj) =>
$req({
method: 'get',
url: '/wechatPublic/createJsapiSignature',
params,
});
/**
*
* @param data
*/
export const parentBindInviteApi = (data: anyobj) =>
$req({
method: 'post',
url: '/parentBindInvite',
data,
});
/**
*
* @param data
*/
export const getInviteInfoApi = id =>
$req({
method: 'get',
url: `/parentBindInvite/${id}`,
});

78
src/api/modules/mine.ts Normal file
View File

@ -0,0 +1,78 @@
import $req from '../request';
/**
*
* @param params
*/
export const getParentBindChildApi = (params: anyObj) =>
$req({
method: 'get',
url: '/parentBindChild/list',
params,
});
/**
*
* @param params
*/
export const getParentBindDeviceApi = (params: anyObj) =>
$req({
method: 'get',
url: '/parentBindDevice/list',
params,
});
/**
* -
* @param params
*/
export const parentBindChildApi = (id: string) =>
$req({
method: 'get',
url: `/parentBindChild/${id}`,
});
/**
* -
* @param params
*/
export const childUnBoundParentApi = (data: anyObj) =>
$req({
method: 'post',
url: '/parentBindChild/unBound',
data,
});
/**
* -
* @param params
*/
export const parentUnBoundDeviceApi = (data: anyObj) =>
$req({
method: 'post',
url: '/parentBindDevice/unBound',
data,
});
/**
* -
* @param params
*/
export const parentDeviceCurrentLoginApi = (id: string) =>
$req({
method: 'get',
url: `/parentBindDevice/${id}`,
});
/**
* -
* @param params
*/
export const applyBindInfoListApi = () =>
$req({
method: 'get',
url: '/parentBind/applyInfoList',
});
/**
* -
* @param params
*/
export const userInfoPermitApi = () =>
$req({
method: 'get',
url: '/parentBind/applyInfoPermit',
});

View File

@ -0,0 +1,54 @@
import $req from '../request';
export const getInspectorCourseList = (params: {userId: string | number}) => {
return $req({
method: 'get',
url: '/inspectorParent/userInspectorCourseList',
params
});
}
export const getInspectorModeList = () => {
return $req({
method: 'get',
url: '/inspectorParent/inspectorModeList',
});
}
// 最近或最后一次课时督学师和同学信息
export const getInspectorTeacherStudentInfo = (params: {orderId: string}) => {
return $req({
method: 'get',
url: '/inspectorParent/inspectorTeacherStudentInfo',
params
});
}
// 督学订单列表
export const getInspectorOrderList = (params: {userId: string | number}) => {
return $req({
method: 'get',
url: '/inspectorParent/userInspectorOrderList',
params
});
}
// 查询督学计划
export const getInspectorPlan = (params: {inspectorPlanId: string}) => {
return $req({
method: 'get',
url: '/inspectorParent/inspectorPlanInfo',
params
});
}
// 督学计划报告
export const getInspectorPlanReport = (params: {userId: string}) => {
return $req({
method: 'get',
url: '/inspectorParent/userInspectorCourseReportList',
params
});
}

12
src/api/modules/parent.ts Normal file
View File

@ -0,0 +1,12 @@
import $req from '../request';
export const getChildList = () => {
return $req({
url: '/inspectorParent/childList',
});
};
export const getModelList = () => {
return $req({
url: '/inspectorParent/inspectorModeList',
});
};

View File

@ -0,0 +1,72 @@
import $req from '../request';
/**
/**
* -
* @param data
*/
export const getTaskListApi = params =>
$req({
method: 'get',
url: '/scTask/list',
params,
});
/**
* -
* @param data
*/
export const addTaskApi = data =>
$req({
method: 'post',
url: '/scTask',
data,
});
/**
* -
* @param data
*/
export const getFamilyTaskStuApi = params =>
$req({
method: 'get',
url: '/scTask/getFamilyTaskStuList',
params,
});
/**
* -
* @param data
*/
export const taskConfigListApi = params =>
$req({
method: 'get',
url: '/scTaskConfig/list',
params,
});
/**
* -
* @param data
*/
export const addTaskConfigApi = data =>
$req({
method: 'post',
url: '/scTaskConfig',
data,
});
/**
* -
* @param data
*/
export const deleteTaskConfigApi = ids =>
$req({
method: 'delete',
url: `/scTaskConfig/${ids}`,
});
/**
* -
* @param data
*/
export const updateTaskStatusApi = data =>
$req({
method: 'post',
url: '/scTask/updateTaskStatus',
data,
});

View File

@ -0,0 +1,31 @@
import $req from '../request';
/**
* -
* @param data
*/
export const getWarrantyCardMsgApi = () =>
$req({
method: 'get',
url: '/parentBind/getBindDeviceList',
});
/**
* -
* @param data
*/
export const applyServiceApi = (data: anyObj) =>
$req({
method: 'post',
url: '/deviceWarrantyRecord',
data,
});
/**
* -
* @param data
*/
export const getServiceHistoryApi = (params: anyObj) =>
$req({
method: 'get',
url: '/deviceWarrantyRecord/list',
params,
});

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

@ -0,0 +1,93 @@
import { user } from '@/store';
import { storeToRefs } from 'pinia';
import { getCache } from '@/utils';
const CONFIG = {
// host: import.meta.env.VITE_HOST || '',
// baseURL: 'http://127.0.0.1:9053/api/main',
baseURL: '/api/main',
timeout: 60000,
method: 'GET',
};
export class HttpError extends Error {
data: any;
constructor(message: string, data: Record<string, any>) {
super(message);
this.data = data;
}
}
const request = async (config: Record<string, any>): Promise<any> => {
const networkType = await uni.getNetworkType();
if (networkType.networkType === 'none') {
uni.showModal({
content: '暂无网络,请恢复网络后使用',
confirmText: '知道了',
showCancel: false,
confirmColor: '#ffe60f',
});
return Promise.reject(
new HttpError('暂无网络,请恢复网络后使用', {
code: '-1',
}),
);
}
return new Promise((resolve, reject) => {
const { clear } = user();
const { token } = storeToRefs(user());
const mergerToken = token.value || getCache('token');
const method = (config.method || CONFIG.method).toUpperCase();
uni.request({
method,
// url: (config.host || CONFIG.host) + (config.baseURL || CONFIG.baseURL) + config.url,
url: (config.baseURL || CONFIG.baseURL) + config.url,
data: method === 'GET' ? config.params : config.data,
timeout: config.timeout || CONFIG.timeout,
header: {
Authorization: mergerToken ? `Bearer ${mergerToken}` : '',
...config.headers,
},
complete(res: anyObj) {
if (res.statusCode === 200) {
if (res.data.code === 200) {
return resolve(res.data);
} else {
if (!config.showToast) {
uni.showToast({
title: res.data?.message,
icon: 'none',
duration: 3000,
});
}
return reject(new HttpError(res.data?.message, res.data));
}
} else {
res.href = (config.baseURL || CONFIG.baseURL) + config.url;
if (res.statusCode === 401) {
clear();
uni.reLaunch({
url: '/pages/login/index',
});
} else {
uni.showToast({
icon: 'none',
title:
res.statusCode === 500 ? '服务异常' : res?.data?.message || '服务异常,请稍后重试',
});
}
return reject(
new HttpError(res.data?.message, {
code: res.statusCode,
data: res.data,
}),
);
}
},
});
});
};
export default request;

View File

@ -0,0 +1,57 @@
<template>
<uni-nav-bar
fixed
shadow
background-color="#fff"
color="#0A1F13"
:border="false"
:left-icon="leftIcon ? 'left' : ''"
:title="title"
@clickLeft="onLeftClick"
>
<template #right> <slot></slot> </template>
</uni-nav-bar>
</template>
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
default: '',
},
leftIcon: {
type: Boolean,
default: true,
},
custom: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['back']);
function onLeftClick() {
emits('back');
if (props.custom) {
console.log('自定义返回路径');
return;
}
console.log('默认组件返回路径');
const canNavBack = getCurrentPages();
if (canNavBack && canNavBack.length > 1) {
uni.navigateBack({
delta: 1,
});
// if (canNavBack[canNavBack.length - 1].route === canNavBack[canNavBack.length - 2].route) {
// uni.navigateTo({
// url: `/${canNavBack[canNavBack.length - 3].route}`,
// });
// } else {
// uni.navigateBack({
// delta: 1,
// });
// }
} else {
history.back();
}
}
</script>

View File

@ -0,0 +1,102 @@
<template>
<view class="calender-popup">
<view class="open-btn" @click="openCalender">
<slot>
<view class="default-btn">
<view>日历</view>
<wd-icon name="calendar" size="35rpx"></wd-icon>
</view>
</slot>
</view>
<wd-popup v-model="showCalenderPopup" position="bottom" custom-style="border-radius: 30rpx 30rpx 0 0;">
<student-calender v-model:value="currentDate"></student-calender>
<view class="calender-popup-footer">
<wd-button :round="false" class="cancel" @click="closeCalender">取消</wd-button>
<wd-button :round="false" class="confirm" @click="confirm">确定</wd-button>
</view>
</wd-popup>
</view>
</template>
<script setup lang=ts>
import {ref, watch} from "vue";
import StudentCalender from "@/components/student-calendar/index"
import dayjs from "dayjs";
const props = defineProps({
value: {
type: [String, dayjs],
},
});
const emit = defineEmits(['update:value', 'change'])
const showCalenderPopup = ref(false)
const currentDate = ref("")
const openCalender = () => {
showCalenderPopup.value = true
}
const closeCalender = () => {
showCalenderPopup.value = false
}
watch(() => props.value, value => {
if (value) {
currentDate.value = typeof value === "string" ? value : value.format('YYYY-MM-DD')
} else {
currentDate.value = dayjs().format('YYYY-MM-DD')
}
}, {immediate: true, deep: true})
const confirm = () => {
emit('update:value', dayjs(currentDate.value))
emit("change", dayjs(currentDate.value))
closeCalender()
}
</script>
<style scoped lang="scss">
.calender-popup {
display: inline-block;
.open-btn {
display: inline-block;
.default-btn {
display: inline-flex;
align-items: center;
gap: 10rpx;
color: #666;
user-select: none;
}
}
}
.calender-popup-footer {
height: 122rpx;
box-sizing: border-box;
padding: 16rpx 30rpx;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.cancel {
width: 230rpx;
height: 90rpx;
border-radius: 20rpx;
background-color: #EFEFFF;
color: #615DFF;
font-size: 30rpx;
font-weight: 500;
}
.confirm {
width: 440rpx;
height: 90rpx;
border-radius: 20rpx;
background-color: #615DFF;
color: #fff;
font-size: 30rpx;
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
// import { handleBack } from '../../hooks';
const props = defineProps<{
title: string;
}>();
function handleBack() {
uni.navigateBack({ delta: 1 });
}
</script>
<template>
<wd-navbar
:title="title"
:bordered="false"
safeAreaInsetTop
fixed
placeholder
left-arrow
:z-index="0"
@click-left="handleBack"
>
<template #left>
<image
src="https://box-1313840333.cos.ap-guangzhou.myqcloud.com/subpackage/back.svg"
class="back_icon"
></image>
</template>
</wd-navbar>
</template>
<style lang="scss" scoped>
.back_icon {
width: 48rpx;
height: 48rpx;
}
:deep(.wd-navbar) {
.wd-navbar {
&__left {
padding-left: $space;
width: 48rpx;
}
&__title {
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<view class="custom_popup">
<uni-popup ref="popupRef" :is-mask-click="false" animation type="center">
<view class="popup_box">
<view class="container">
<view v-if="titleText" class="title">
{{ titleText }}
</view>
<!-- 自定义内容 -->
<slot></slot>
</view>
<view class="footer">
<text v-if="cancelBtnText" class="btn cancel_btn" @click="tapCancelBtn">{{
cancelBtnText
}}</text>
<text class="btn confirm_btn" @click="tapConfirmBtn">{{ confirmBtnText }}</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
titleText: {
type: String,
default: '',
},
cancelBtnText: {
type: String,
default: '取消',
},
confirmBtnText: {
type: String,
default: '确定',
},
});
const emits = defineEmits(['update:modelValue', 'handleConfirmBtn', 'handleCancelBtn']);
const popupRef = ref(null);
//
function tapConfirmBtn() {
emits('handleConfirmBtn');
}
//
function tapCancelBtn() {
popupRef.value.close();
emits('update:modelValue', false);
emits('handleCancelBtn');
}
watch(
() => props.modelValue,
() => {
if (props.modelValue) {
nextTick(() => {
popupRef.value.open();
});
} else {
nextTick(() => {
popupRef.value && popupRef.value.close();
});
}
},
{
immediate: true,
deep: true,
},
);
</script>
<style lang="scss" scoped>
.custom_popup {
.popup_box {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 594rpx;
background-color: #fff;
border-radius: 20rpx;
text-align: center;
color: #333333;
.container {
min-height: 223rpx;
.title {
padding: 30rpx 0 0 0;
line-height: 48rpx;
font-weight: 500;
font-style: 34rpx;
}
}
.footer {
display: flex;
justify-content: center;
width: 100%;
height: 108rpx;
line-height: 108rpx;
border-top: 1px solid #eeeeee;
color: #333333;
font-size: 34rpx;
font-weight: 500;
.btn {
display: block;
flex: 1;
text-align: center;
}
.cancel_btn {
border-right: 1px solid #eeeeee;
}
.confirm_btn {
color: #615dff;
}
}
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<wd-segmented
v-model:value="current"
:options="list"
custom-class="main-view"
@change="handleChange"
>
<!-- <template #label="{ option }">-->
<!-- <view class="option-view">{{ option.value }}</view>-->
<!-- </template>-->
</wd-segmented>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps({
value: {
type: Number,
required: true,
},
list: {
type: Array,
required: true,
},
});
const emit = defineEmits(['update:value']);
const current = ref(0);
watch(
() => props.value,
value => {
current.value = value;
},
);
const handleChange = (value: number) => {
current.value = value;
emit('update:value', value);
};
</script>
<style scoped lang="scss">
.main-view {
padding: 0;
height: 52rpx;
line-height: 52rpx;
border-radius: 10rpx;
overflow: hidden;
:deep(.wd-segmented__item) {
background-color: #f7f8fa;
font-size: 26rpx;
padding: 0;
border-radius: 0;
&.is-active {
background-color: #615dff;
color: white;
}
}
:deep(.wd-segmented__item--active) {
display: none;
background-color: #615dff;
//z-index: 1 !important;
height: 100%;
color: white !important;
}
}
.option-view {
//background-color: #f7f8fa;
}
</style>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const props = withDefaults(
defineProps<{
content?: string;
width?: number | string;
height?: number | string;
delay?: number | string;
marginTop?: number | string;
}>(),
{
content: '暂无内容',
width: 300,
height: 300,
delay: 0,
marginTop: 100,
},
);
const show = ref(false);
const OSS_URL = import.meta.env.VITE_OSS_HOST;
onMounted(() => {
setTimeout(() => (show.value = true), Number(props.delay));
});
</script>
<template>
<div v-if="show" class="empty" :style="{ marginTop: `${marginTop}rpx` }">
<slot>
<image
:src="`${OSS_URL}/empty.png`"
:style="{
width: `${width}rpx`,
height: `${height}rpx`,
}"
/>
<div class="empty-content">
{{ content }}
</div>
</slot>
</div>
</template>
<style lang="scss" scoped>
.empty {
width: 100%;
height: inherit;
display: flex;
flex: 1;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
justify-content: center;
text-align: center;
.empty-content {
width: 100%;
color: #666666;
font-size: 28rpx;
line-height: 42rpx;
margin-top: 50rpx;
}
}
</style>

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import type { TPopupConfig } from '@/types/draw';
import { computed } from 'vue';
const props = defineProps<{
modelValue: boolean;
config: TPopupConfig;
}>();
const emit = defineEmits(['update:modelValue', 'onChange']);
const show = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
</script>
<template>
<wd-popup
v-model="show"
position="bottom"
safe-area-inset-bottom
custom-style="border-radius: 32rpx 32rpx 0 0;"
:close-on-click-modal="false"
@click-modal="show = false"
>
<view class="wrapper">
<view class="header">
{{ config?.title }}
<image
class="close-icon"
src="@/static/common/icon_down_out.png"
@click.stop="show = false"
/>
</view>
<view class="footer">
<button
v-for="(item, index) in config?.btnInfo"
:key="index"
class="footer-button"
@click.stop="
() => {
item?.fn?.();
show = false;
}
"
>
{{ item?.text }}
</button>
</view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped>
.wrapper {
background: #fff;
color: $font-color;
padding: 32upx;
color: $font-color;
.header {
height: 56upx;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
font-size: 32upx;
position: relative;
.close-icon {
width: 56upx;
height: 56upx;
position: absolute;
right: 0;
}
}
.footer {
width: 100%;
background: #fff;
padding-top: 16upx;
&-button {
width: 100%;
height: 88upx;
line-height: 88upx;
margin-top: 32upx;
color: $font-color;
border-radius: 44upx;
font-size: 28upx;
background: $bg-fill;
}
}
}
</style>

View File

@ -0,0 +1,119 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
modelValue: boolean;
type?: string; // alert confirm
title?: string;
content?: string;
cancelBtnText?: string;
confirmBtnText?: string;
}>();
const emits = defineEmits(['update:modelValue', 'onConfirm', 'onCancel']);
const show = computed({
get: () => props.modelValue,
set: value => emits('update:modelValue', value),
});
function handleCancel() {
emits('onCancel');
show.value = false;
}
function handleConfirm() {
emits('onConfirm');
show.value = false;
}
</script>
<template>
<wd-overlay :show="show" :z-index="999">
<view class="wrapper">
<wd-transition :show="show" name="zoom-in">
<view class="block">
<view v-if="title" class="title">{{ title }}</view>
<view class="content">
{{ content || '提示内容' }}
</view>
<view class="footer">
<template v-if="type === 'confirm'">
<button
class="footer-button"
:style="{ marginRight: '24upx' }"
@click.stop="handleCancel"
>
{{ cancelBtnText || '取消' }}
</button>
<button class="footer-button primary" @click.stop="handleConfirm">
{{ confirmBtnText || '确定' }}
</button>
</template>
<template v-else>
<button class="footer-button primary" @click.stop="show = false">
{{ confirmBtnText || '好的,我知道了' }}
</button>
</template>
</view>
</view>
</wd-transition>
</view>
</wd-overlay>
</template>
<style lang="scss" scoped>
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.block {
width: 612upx;
/* padding: 32upx 40upx; */
padding: 32upx 0;
box-sizing: border-box;
background-color: #fff;
border-radius: 30upx;
display: flex;
flex-direction: column;
align-items: center;
.title {
font-size: 32upx;
color: $font-color;
font-weight: bold;
}
.content {
color: #333;
font-size: 28upx;
margin-top: 36upx;
padding: 0 40upx;
}
.footer {
width: 100%;
display: flex;
justify-content: space-between;
margin-top: 72upx;
padding: 0 0 0 24upx;
box-sizing: border-box;
&-button {
flex: 1;
height: 88upx;
background: $bg-fill;
border-radius: 44upx;
font-size: 32upx;
font-weight: bold;
color: $font-color;
margin-right: 24rpx;
&.primary {
background: $theme-color;
}
}
}
}
}
</style>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
modelValue: boolean;
title?: string;
}>();
const emit = defineEmits(['update:modelValue', 'onClose']);
const show = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
function handleClose() {
emit('update:modelValue', false);
emit('onClose');
}
</script>
<template>
<wd-popup
v-model="show"
position="bottom"
safe-area-inset-bottom
custom-style="border-radius: 32rpx 32rpx 0 0;"
:close-on-click-modal="false"
@click-modal="handleClose"
>
<view class="mfx_popup">
<view class="mfx_popup-header">
{{ title || '' }}
<image
class="mfx_popup-header-close"
src="@/static/common/icon_down_out.png"
@click.stop="handleClose"
/>
</view>
<slot></slot>
</view>
</wd-popup>
</template>
<style lang="scss" scoped>
.mfx_popup {
background: #fff;
color: $font-color;
padding: 32upx;
color: $font-color;
&-header {
height: 56upx;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
font-size: 32upx;
position: relative;
&-close {
width: 56upx;
height: 56upx;
position: absolute;
right: 0;
}
}
}
</style>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
modelValue: boolean;
disabled?: boolean;
}>();
const emits = defineEmits(['update:modelValue', 'onChange']);
const switchValue = computed({
get: () => props.modelValue,
set: value => emits('update:modelValue', value),
});
function handleChange() {
if (props?.disabled) return;
switchValue.value = !switchValue.value;
emits('onChange', switchValue.value);
}
</script>
<template>
<view
class="mfx-switch"
:class="{ disabled }"
:style="{ background: `${switchValue ? '#64EEEE' : '#EAEEF2'}` }"
@click.stop="handleChange"
>
<view
class="mfx-switch-circle"
:style="{
transform: `translateX(${switchValue ? 66 : 14}rpx)`,
background: `${switchValue ? '#1a1a1a' : '#ffffff'}`,
}"
></view>
</view>
</template>
<style lang="scss" scoped>
.mfx-switch {
width: 108rpx;
height: 56rpx;
background: #eaeef2;
border-radius: 28rpx;
display: flex;
align-items: center;
position: relative;
&.disabled {
opacity: 0.5;
}
&-circle {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #ffffff;
position: absolute;
transform: translateX(14rpx);
transition: transform 0.4s ease;
}
}
</style>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
//
</script>
<template>
<view class="loading-wrapper">
<wd-loading :size="40" color="#64eeee" />
</view>
</template>
<style lang="scss" scoped>
.loading-wrapper {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.85);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<view class="picker_box">
<uni-popup ref="popupRef" :is-mask-click="false" animation type="bottom">
<view class="popup_box">
<view v-if="title" class="header_title">
{{ title }}
</view>
<picker-view
:indicator-style="indicatorStyle"
:overshoot="false"
:value="pickValue"
class="picker-view"
@change="bindChange"
>
<picker-view-column>
<view
v-for="(item, index) in pickList"
:key="index"
:class="['picker_item', pickValue[0] === index ? 'selected_item' : '']"
>{{ item.name }}</view
>
</picker-view-column>
</picker-view>
<view class="operation">
<button class="cancel_btn" @tap="tapCancelBtn">取消</button>
<button class="confirm_btn" @tap="tapConfirmBtn">确定</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
const props = defineProps({
title: {
type: String,
default: '',
},
modelValue: {
type: Boolean,
default: false,
},
pickList: {
type: Array,
default: () => [],
},
pickVal: {
type: Array,
},
});
const emits = defineEmits(['update:modelValue', 'confirm']);
const btnDisabled = ref(true); //
const popupRef = ref(null);
const indicatorStyle = ref(`height: 50px;`);
const pickValue = ref([]);
const isBindChange = ref(false); //
//
function bindChange(e) {
isBindChange.value = true;
btnDisabled.value = false;
pickValue.value = e.detail.value;
}
//
function tapCancelBtn() {
emits('update:modelValue', false);
}
//
function tapConfirmBtn() {
setTimeout(() => {
if (isBindChange.value) {
//
if (!btnDisabled.value) {
emits('confirm', pickValue.value);
}
} else {
//
emits('confirm', pickValue.value);
}
}, 620);
}
watch(
() => props.modelValue,
() => {
if (props.modelValue) {
nextTick(() => {
popupRef.value.open();
pickValue.value = props.pickVal;
btnDisabled.value = true;
isBindChange.value = false;
});
} else {
if (popupRef.value) {
popupRef.value.close();
isBindChange.value = false;
btnDisabled.value = true;
}
}
},
{
immediate: true,
deep: true,
},
);
</script>
<style lang="scss" scoped>
.picker_box {
.popup_box {
position: relative;
height: 660rpx;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
overflow: hidden;
padding: 50rpx 0 0 0;
.header_title {
position: absolute;
top: 30rpx;
left: 30rpx;
z-index: 99;
color: #333;
font-size: 32rpx;
}
.picker-view {
width: 750rpx;
height: 450rpx;
margin: 20rpx 0 20rpx 0;
}
.picker_item {
line-height: 100rpx;
text-align: center;
}
.selected_item {
color: #615dff;
}
.operation {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
padding-top: 16rpx;
border-top: 1px solid #eeeeee;
bottom: 60rpx;
button::after {
border: none;
}
.cancel_btn {
width: 230rpx;
height: 90rpx;
color: #615dff;
background-color: #ececff;
}
.confirm_btn {
margin-left: 20rpx;
height: 90rpx;
width: 440rpx;
color: #fff;
background-color: #615dff;
}
}
}
}
</style>

View File

@ -0,0 +1,278 @@
<template>
<view class="supervisor-teacher">
<template v-if="!teacher">
<view class="empty-content">
<view class="avatar-image">
<view class="image-box">
<image :src="`${OSS_URL}/patriarch_header_1.png`"></image>
</view>
</view>
<view class="dot-list">
<view class="dot-item"></view>
<view class="dot-item"></view>
<view class="dot-item"></view>
</view>
<view class="teacher-icon">
<wd-icon name="user" size="22px"></wd-icon>
</view>
</view>
<view class="tip">正在为您精心匹配督学师请耐心等待</view>
</template>
<template v-else>
<view class="base-info">
<view class="avatar">
<image :src="teacher.avatar"></image>
</view>
<view class="info-box">
<view class="name">{{ teacher.name }}</view>
<view class="school-edu">
<text>{{ teacher.school }}</text>
<text>{{ teacher.edu }}</text>
</view>
<view class="label-list">
<view
v-for="item in teacher.labelList"
:key="item.name"
class="label"
:style="{ color: item.color, backgroundColor: item.bgColor }"
>{{ item.name }}</view
>
</view>
</view>
</view>
<view v-show="open" class="description">
<view class="title-label">
<template v-for="(item, index) in teacher.titleLabel" :key="index">
<text>{{ item }}</text>
<text v-if="index !== teacher.titleLabel.length - 1"> </text>
</template>
</view>
<view class="desc-list">
{{teacher.desc}}
</view>
</view>
<view class="show-open-row">
<view class="show-open" @click="changeOpen">
<view>{{ open ? '收起' : '展开' }}</view>
<wd-icon v-if="!open" name="arrow-down" size="24rpx"></wd-icon>
<wd-icon v-else name="arrow-up" size="24rpx"></wd-icon>
</view>
</view>
</template>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
export interface ITeacher {
name: string;
school: string;
edu: string;
avatar: string;
labelList: {
name: string;
color: string;
bgColor: string;
}[];
titleLabel: string[];
desc: string;
}
const props = defineProps<{
teacher: ITeacher | null | undefined;
}>();
const OSS_URL = import.meta.env.VITE_OSS_HOST;
const open = ref(false);
const changeOpen = () => {
open.value = !open.value;
};
</script>
<style scoped lang="scss">
.supervisor-teacher {
margin-bottom: 30rpx;
border-radius: 20rpx;
padding: 30rpx;
box-sizing: border-box;
background-color: #fff;
.empty-content {
display: flex;
align-items: center;
gap: 50rpx;
.avatar-image {
width: 232rpx;
height: 232rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.image-box {
border-radius: 50%;
width: 140rpx;
height: 140rpx;
background-color: #ededff;
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 4;
image {
width: 100rpx;
height: 100rpx;
border: 2rpx solid #615dff;
border-radius: 50%;
}
}
&::before,
&::after {
content: '';
position: absolute;
border-radius: 50%;
}
&::before {
background-color: #fafaff;
width: 100%;
height: 100%;
z-index: 2;
}
&::after {
background-color: #f4f4ff;
width: 184rpx;
height: 184rpx;
z-index: 3;
}
}
.dot-list {
display: flex;
align-items: center;
justify-content: space-between;
width: 62rpx;
height: 14rpx;
border-radius: 7rpx;
position: relative;
.dot-item {
width: 14rpx;
height: 14rpx;
background-color: transparent;
border-radius: 50%;
&:nth-child(1) {
background-color: #9592ff;
}
&:nth-child(2) {
background-color: #dbdaff;
}
&:nth-child(3) {
background-color: #dbdaff;
}
}
}
.teacher-icon {
width: 100rpx;
height: 100rpx;
background-color: #ededf0;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
}
.tip {
text-align: center;
color: #666;
font-size: 30rpx;
line-height: 42rpx;
margin-top: 30rpx;
}
.base-info {
display: flex;
border-bottom: 1px solid #eeeeee;
padding-bottom: 20rpx;
.avatar {
margin-right: 20rpx;
image {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
}
}
.info-box {
display: flex;
flex-direction: column;
gap: 10rpx;
.name {
font-size: 36rpx;
line-height: 48rpx;
color: #0a1f13;
font-weight: 500;
}
.school-edu {
color: #a9aab5;
font-size: 30rpx;
line-height: 48rpx;
}
.label-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
.label {
padding: 0 16rpx;
font-size: 26rpx;
line-height: 48rpx;
}
}
}
}
.description {
padding-top: 20rpx;
.title-label {
margin-bottom: 10rpx;
font-size: 30rpx;
line-height: 48rpx;
}
.desc-list {
color: #666;
font-size: 30rpx;
line-height: 48rpx;
.desc-list-item {
display: flex;
align-items: flex-start;
.dot {
width: 48rpx;
height: 48rpx;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10rpx;
&::before {
content: '';
display: block;
width: 14rpx;
height: 14rpx;
background-color: #666666;
border-radius: 50%;
}
}
}
}
}
.show-open-row {
display: flex;
justify-content: center;
margin-top: 20rpx;
.show-open {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
font-size: 24rpx;
color: #a8aab5;
user-select: none;
height: 36rpx;
padding: 0 20rpx;
}
}
}
</style>

View File

@ -0,0 +1,161 @@
<template>
<view class="tab-bar">
<view
v-for="(item, index) in list"
:key="index"
class="tab-bar-item"
@click="switchTab(item, index)"
>
<view class="tab-box">
<image
class="tab_img"
:src="currentIndex === index ? item.selectedIconPath : item.iconPath"
></image>
<text v-if="notice && item.text === '我的'" class="notice"></text>
</view>
<view class="tab_text" :style="{ color: currentIndex === index ? selectedColor : color }">{{
item.text
}}</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app';
import { onMounted, reactive, ref } from 'vue';
import { bindApplyStore } from '@/store';
import { getCache } from '@/utils';
const props = withDefaults(
defineProps<{
selectedIndex: number;
}>(),
{ selectedIndex: 0 },
);
const { getApplyBindInfoList } = bindApplyStore();
const currentIndex = ref(0);
const list = ref([]);
const color = ref<string>('#8E8D9D');
const selectedColor = ref<string>('#625EFF');
const notice = ref(false);
function switchTab(item, index) {
currentIndex.value = index;
let url = item.pagePath;
uni.redirectTo({ url: url });
}
onMounted(async () => {
currentIndex.value = props.selectedIndex;
let adminType = getCache('userInfo').adminType;
if (adminType === 16) {
//
await getApplyBindInfoList();
notice.value = getCache('applyBindMsg');
list.value = [
{
pagePath: '/pages/home/index',
iconPath: '/static/tabBar/homeno.png',
selectedIconPath: '/static/tabBar/home.png',
text: '首页',
},
{
pagePath: '/pages/academicReport/index',
iconPath: '/static/tabBar/studyReportNo.png',
selectedIconPath: '/static/tabBar/studyReport.png',
text: '学情报告',
},
{
pagePath: '/pages/applicationManagement/index',
iconPath: '/static/tabBar/applicationNo.png',
selectedIconPath: '/static/tabBar/application.png',
text: '应用管控',
},
{
pagePath: '/pages/mine/index',
iconPath: '/static/tabBar/mineno.png',
selectedIconPath: '/static/tabBar/mine.png',
text: '我的',
},
];
} else if (adminType === 6) {
//
uni.reLaunch({
url: '/pages/inspector/schedule/index',
});
list.value = [
{
pagePath: '/pages/inspector/schedule/index',
iconPath: '/static/tabBar/scheduleNo.png',
selectedIconPath: '/static/tabBar/schedule.png',
text: '课表',
},
{
pagePath: '/pages/inspector/student/index',
iconPath: '/static/tabBar/studentNo.png',
selectedIconPath: '/static/tabBar/student.png',
text: '学员',
},
{
pagePath: '/pages/inspector/mine/index',
iconPath: '/static/tabBar/mineno.png',
selectedIconPath: '/static/tabBar/mine.png',
text: '我的',
},
];
}
});
</script>
<style lang="scss" scoped>
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
background: white;
display: flex;
justify-content: center;
align-items: center;
border-top: 1rpx solid #eee;
padding-bottom: env(safe-area-inset-bottom); // iphoneX
z-index: 99999;
.tab-bar-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.tab-box {
position: relative;
width: 50rpx;
height: 50rpx;
.notice {
position: absolute;
top: 0;
right: 0;
display: block;
background-color: red;
width: 14rpx;
height: 14rpx;
border-radius: 50%;
}
.tab_img {
width: 50rpx;
height: 50rpx;
}
}
.tab_text {
font-size: 22rpx;
margin-top: 12rpx;
}
}
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<view class="tip_popup">
<uni-popup ref="popupRef" :is-mask-click="false" animation type="center">
<view class="popup_box">
<view class="container">
<image
v-if="showSuccessIcon"
:src="
type === 'success'
? `${OSS_URL}/iconfont/success_icon.png`
: `${OSS_URL}/iconfont/fail_icon.png`
"
class="icon"
mode=""
></image>
<view :class="[type === 'success' ? 'success' : 'fail']"> {{ text }} </view>
<view v-if="messageTip" class="message_tip" v-html="messageTip"></view>
</view>
<view v-if="showBtn" class="btn" @tap="tapBack">
{{ btnText }}
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
// :success,fail
type: {
type: String,
default: 'success',
},
//
showSuccessIcon: {
type: Boolean,
default: true,
},
//
showBtn: {
type: Boolean,
default: false,
},
//
btnText: {
type: String,
default: '返回',
},
text: {
type: String,
default: '加入成功',
},
messageTip: {
type: String,
default: '',
},
});
const emits = defineEmits(['update:modelValue', 'backBtn']);
const OSS_URL = import.meta.env.VITE_OSS_HOST;
const popupRef = ref(null);
//
function tapBack() {
popupRef.value.close();
emits('update:modelValue', false);
emits('backBtn');
}
watch(
() => props.modelValue,
() => {
if (props.modelValue) {
nextTick(() => {
popupRef.value.open();
if (!props.showBtn) {
setTimeout(() => {
popupRef.value.close();
emits('update:modelValue', false);
}, 1500);
}
});
}
},
{
immediate: true,
deep: true,
},
);
</script>
<style lang="scss" scoped>
.tip_popup {
.popup_box {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 594rpx;
height: 346rpx;
background-color: #fff;
border-radius: 20rpx;
text-align: center;
.container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.icon {
width: 60rpx;
height: 60rpx;
}
.success {
margin-top: 30rpx;
color: #21d17a;
font-size: 34rpx;
font-weight: 600;
}
.fail {
margin-top: 30rpx;
font-size: 34rpx;
font-weight: 600;
color: #ff2828;
}
.message_tip {
margin-top: 20rpx;
font-size: 30rpx;
color: #333;
}
}
.btn {
width: 100%;
height: 108rpx;
line-height: 108rpx;
border-top: 1px solid #eeeeee;
color: #333333;
font-size: 34rpx;
}
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<view class="table-view">
<view class="table-view-ths table-view-item">
<view v-for="column in columns" :key="column.key">{{ column.title }}</view>
</view>
<view v-for="(item, index) in data" :key="index" class="table-view-item table-view-tds">
<view v-for="column in columns" :key="column.key">{{ item[column.key] }}</view>
</view>
</view>
</template>
<script setup lang="ts">
const props = defineProps<{
columns: Record<string, any>[];
data: Record<string, any>[];
}>();
</script>
<style scoped lang="scss">
.table-view {
border-radius: 20rpx;
font-size: 26rpx;
font-style: normal;
line-height: normal;
overflow: hidden;
&-item {
width: 100%;
display: flex;
justify-content: space-between;
border-bottom: 2rpx solid #d9e1f2;
&:last-child{
border-bottom: none;
}
&.table-view-ths {
background-color: #ebf2ff;
color: #333;
font-size: 26rpx;
font-style: normal;
font-weight: 500;
line-height: normal;
}
&.table-view-tds {
background-color: #F7FAFF;
&:nth-child(odd) {
background-color: #ebf2ff;
}
}
& > view {
width: 33.33%;
text-align: center;
border-right: 2rpx solid #d9e1f2;
padding: 20rpx 0;
&:first-child {
width: 230rpx;
text-align: left;
box-sizing: border-box;
padding-left: 37rpx;
}
&:last-child {
border-right: none;
}
}
}
}
</style>

View File

@ -0,0 +1,323 @@
<template>
<view class="calendar-box">
<view class="calendar-wrapper">
<view v-if="headerSlot">
<slot name="header" :date="{ monthDate: currentDateStr, date: dateForm.currentDay }"></slot>
</view>
<view v-if="showHeader && !headerSlot" class="calendar-toolbar">
<view class="current" @click="dateForm.showMonthPicker = true">
{{ currentDateStr }}
</view>
<view class="steps">
<view class="step prev" @click="prevMonth">
<wd-icon name="arrow-left" size="30rpx"></wd-icon>
</view>
<view class="step next" @click="nextMonth">
<wd-icon name="arrow-right" size="30rpx"></wd-icon>
</view>
</view>
</view>
<view class="calendar-week">
<view v-for="item of weekList" :key="item" class="week-item calendarBorder">
{{ item }}
</view>
</view>
<view class="calendar-inner">
<view
v-for="(item, index) of calendarList"
:key="index"
:class="{
'calendar-item': true,
calendarBorder: true,
'calendar-item-checked': dateForm.multipleList.includes(item.date),
'disabled': item.disabled
}"
@click="handleClickDay(item)"
>
<view
:class="{
date: true,
'calendar-item-hover': item.isCurrentMonth,
'calendar-item-disabled': !item.isCurrentMonth,
'calendar-item-current':
dateForm.multipleList.includes(item.date) && item.isCurrentMonth,
}"
>{{ item.day }}</view
>
<view v-if="cellSlot">
<slot name="cell" :date="item"></slot>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { reactive, ref, computed, onMounted, watch, useSlots, nextTick } from 'vue';
import dayjs from 'dayjs';
import { type CalendarDay, generateMonthCalendar } from '@/utils/calendar';
const props = defineProps({
minDate: {
type: Date,
// required: true,
},
maxDate: {
type: Date,
// required: true
},
auto: {
type: Boolean,
default: true,
},
multiple: {
type: Boolean,
default: false,
},
limit: {
type: Number,
default: 0,
},
selectedDay: {
type: String,
// required: true,
default: dayjs().format('YYYY-MM-DD'),
},
// //
canSeletion: {
type: Array,
default: [],
},
calendar: {
type: Object,
default: () => {},
},
calendarDays: {
type: Array,
default: [],
// required: true,
},
showHeader: {
type: Boolean,
default: true,
},
IndexDayCount: {
type: Array,
default: [],
// required: true,
},
dateDisabled: {
type: Function
}
});
const emits = defineEmits(['selectedDay', 'changeMonth', 'selection']);
const uSlot = useSlots();
const dateForm = reactive({
showMonthPicker: false,
currentDay: dayjs(new Date()).format('YYYY-MM-DD'),
multipleList: [],
});
const weekList = ['一', '二', '三', '四', '五', '六', '日']; //
const monthFirstDay = ref(dayjs().startOf('month'));
const calendarList = computed(() => {
return generateMonthCalendar(monthFirstDay.value.toDate()).flat().map(item => {
return {
...item,
disabled: props.dateDisabled && typeof props.dateDisabled === 'function' ? props.dateDisabled(item) : false
}
});
});
const currentDateStr = computed(() => {
return dayjs(monthFirstDay.value).format('YYYY年MM月');
});
const headerSlot = computed(() => {
return uSlot.header;
});
const cellSlot = computed(() => {
return uSlot.cell;
});
watch(
() => props.selectedDay,
val => {
nextTick(() => {
//
if (!props.auto) {
dateForm.multipleList = [];
} else if (!props.multiple || props.auto) {
dateForm.multipleList = [val];
}
monthFirstDay.value = dayjs(val).startOf('month');
});
},
{ immediate: true, deep: true },
);
const prevMonth = () => {
monthFirstDay.value = monthFirstDay.value.subtract(1, 'month');
setCurrentDay();
};
const nextMonth = () => {
monthFirstDay.value = monthFirstDay.value.add(1, 'month');
setCurrentDay();
};
function setCurrentDay() {
if (!props.multiple) {
const date = dayjs(monthFirstDay.value).format('YYYY-MM-DD');
dateForm.multipleList = props.auto ? [date] : [];
dateForm.currentDay = date;
}
}
//
function handleClickDay(item: any) {
if (!item || !item.isCurrentMonth || item.disabled) return;
if (props.canSeletion.length) {
if (!props.canSeletion.includes(item.date)) {
return;
}
}
//
if (props.multiple) {
if (dateForm.multipleList.includes(item.date)) {
dateForm.multipleList = dateForm.multipleList.filter(date => date !== item.date);
} else {
if (dateForm.multipleList.length < props.limit || !props.limit) {
dateForm.multipleList.push(item.date);
}
}
emits('selection', dateForm.multipleList);
} else {
dateForm.multipleList = [item.date];
dateForm.currentDay = item.date;
}
emits('selectedDay', item);
}
defineExpose({
prevMonth,
nextMonth,
});
</script>
<style lang="scss" scoped>
.calendar-box {
width: 690rpx;
min-height: 637rpx;
display: flex;
justify-content: space-around;
align-items: center;
// background: #fff;
font-size: 28rpx;
margin: 0 auto;
.calendar-wrapper {
border-radius: 20rpx;
padding-top: 20rpx;
box-sizing: border-box;
background-color: #fff;
.calendar-toolbar {
width: 690rpx;
height: 100rpx;
display: flex;
justify-content: space-between;
align-items: center;
.steps {
display: flex;
margin-right: 20rpx;
}
.steps .step {
width: 48rpx;
height: 48rpx;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
background: #615dff;
text-align: center;
border-radius: 50%;
}
.prev,
.next,
.current {
font-size: 32rpx;
font-weight: 500;
color: #333333;
margin-left: 20rpx;
cursor: pointer;
}
.van-icon-arrow-down {
vertical-align: -15%;
}
}
.calendar-week {
width: 690rpx;
border-bottom: 1rpx solid #e8e8e8;
display: flex;
flex-wrap: wrap;
// justify-content: space-around;
.week-item {
width: calc(100% / 7);
height: 50rpx;
}
}
.calendar-inner {
width: 690rpx;
display: flex;
flex-wrap: wrap;
padding-bottom: 20rpx;
.calendar-item {
width: calc(100% / 7);
min-height: 112rpx;
position: relative;
border-left: 1rpx solid #e8e8e8;
border-bottom: 1rpx solid #e8e8e8;
box-sizing: border-box;
&.disabled {
background-color: #f9f9f9;
}
}
.calendar-item:nth-child(7n) {
border-right: 1rpx solid #e8e8e8;
}
// .calendar-item:nth-child(7n+1){
// border-left: 0;
// }
.calendar-item-hover {
cursor: pointer;
//&:hover {
// color: #000;
// border-radius: 40rpx;
// background: #ffffff;
// box-shadow: 0 2rpx 30rpx 0 rgba(137, 138, 255, 0.55);
//}
}
.calendar-item-disabled {
color: #ccc !important;
cursor: not-allowed;
}
.calendar-item-checked {
color: #615dff !important;
background-color: #f0f0ff;
}
.calendar-item-current {
color: #615dff !important;
}
.date {
text-align: center;
font-size: 32rpx;
line-height: 44rpx;
color: #0a1f13;
}
}
.calendarBorder {
text-align: center;
// display: flex;
// justify-content: center;
// align-items: center;
// border-bottom: 1px solid #eee;
// border-right: 1px solid #eee;
}
}
}
.van-popup--bottom.van-popup--round {
border-radius: 24rpx 24rpx 0 0;
}
</style>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
name: string;
size?: string | number;
color?: string;
img?: boolean;
}>(),
{ img: false, size: '40rpx' },
);
const emit = defineEmits(['click']);
defineOptions({
options: {
virtualHost: true,
},
});
</script>
<template>
<nut-icon
v-if="props.img"
:name="`https://box-1313840333.cos.ap-guangzhou.myqcloud.com/mobole-terminal/icon/${props.name}.svg`"
:width="props.size"
:height="props.size"
@click="(e: anyObj) => emit('click', e)"
></nut-icon>
<nut-icon
v-else
font-class-name="iconfont"
class-prefix="icon"
:name="props.name"
:custom-color="props.color"
:size="props.size"
:width="props.size"
:height="props.size"
@click="(e: anyObj) => emit('click', e)"
></nut-icon>
</template>
<style lang="scss" scoped>
.iconfont {
color: $uni-text-color;
}
</style>

View File

@ -0,0 +1,5 @@
export interface AddLowerDataItem {
text: string;
value: number;
isAdd: boolean;
}

View File

@ -0,0 +1,118 @@
<template>
<view class="type-view">
<view class="type-view-increase">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="16"
viewBox="0 0 32 16"
fill="none"
>
<rect x="1.5" y="4.5" width="29" height="7" fill="#3FD15F" stroke="white" />
</svg>
增加
</view>
<view class="type-view-lower">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="16"
viewBox="0 0 32 16"
fill="none"
>
<rect x="1.5" y="4.5" width="29" height="7" fill="#FF8074" stroke="white" />
</svg>
减少
</view>
</view>
<view :id="htmlId" :style="{ width: '100%', height: `400rpx` }"></view>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import { nextTick, watchEffect } from 'vue';
import type { AddLowerDataItem } from '@/components/echarts-add-lower/add-lower';
const props = withDefaults(
defineProps<{
htmlId: string;
data: AddLowerDataItem[];
}>(),
{},
);
const initEcharts = async () => {
const myChart = echarts.init(document.getElementById(props.htmlId));
console.log('myChart', myChart);
const option = {
grid: {
left: '40px',
right: '20px',
top: '20',
bottom: '20',
},
legend: {
data: ['增加', '减少'],
},
xAxis: {
type: 'category',
data: props.data.map(item => {
return item.text;
}),
},
yAxis: {
type: 'value',
},
series: [
{
type: 'bar',
barWidth: '40',
label: {
show: true,
position: 'top',
valueAnimation: true,
},
data: props.data.map((item: AddLowerDataItem) => {
return {
value: item.value,
itemStyle: {
color: item.isAdd ? '#3FD15F' : '#FF8074',
},
};
}),
},
],
};
myChart && myChart.setOption(option);
};
watchEffect(() => {
if (props.data && props.data.length > 0) {
console.log('props.data', props.data);
nextTick(() => {
initEcharts();
});
}
});
</script>
<style scoped lang="scss">
.k-g-s-title {
color: #333333;
text-align: center;
font-size: 32rpx;
font-style: normal;
font-weight: 500;
line-height: 48rpx; /* 150% */
}
.type-view {
display: flex;
gap: 20rpx;
justify-content: center;
align-items: center;
padding: 30rpx 0;
svg {
width: 32rpx;
height: 16rpx;
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<view
:id="htmlId"
ref="echartsRef"
:style="{ width: '100%', height: `${height ? height : data.length * 100}rpx` }"
@click="viewClick"
></view>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import { computed, onMounted, ref } from 'vue';
const props = withDefaults(
defineProps<{
htmlId: string;
data: DataItem[];
height?: number;
legendVisible?: boolean;
}>(),
{},
);
interface DataItem {
value: number;
name: string;
color?: string;
labelVisible?: boolean;
labelPosition?: string;
labelFormatter?: (param: any) => string;
}
const myChart = ref<echarts.ECharts | null>(null);
const colors = [
'#FBCA85',
'#F5EB82',
'#FB8F85',
'#82F58B',
'#85FBDE',
'#85DEFB',
'#9FA7FB',
'#D59FFB',
'#FB9FD5',
];
const viewClick = (e: MouseEvent) => {};
const seriesData = computed(() => {
return props.data.map((item, index) => {
return {
value: item.value,
name: item.name,
itemStyle: {
color: item.color ?? colors[index],
},
label: {
show: item.labelVisible ?? true,
formatter: (param: any) => {
if (item.labelFormatter && typeof item.labelFormatter === 'function') {
return item.labelFormatter(param);
}
return `${param.value}%`;
},
color: colors[index],
position: item.labelPosition ?? 'outside',
},
};
});
});
const options = computed(() => {
return {
tooltip: {
trigger: 'item',
},
legend: props.legendVisible ?? {
bottom: '0%',
left: 'center',
icon: 'circle',
itemWidth: 8,
itemHeight: 8,
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '40%'],
startAngle: 0,
endAngle: 360,
data: seriesData.value,
},
],
};
});
const initEcharts = async () => {
myChart.value = echarts.init(document.getElementById(props.htmlId));
myChart.value && myChart.value.setOption(options.value);
};
onMounted(() => {
initEcharts();
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,110 @@
<template>
<view
:id="htmlId"
ref="echartsRef"
:style="{ width: '100%', height: `${yData[0]?.data?.length * 60}rpx` }"
@click="viewClick"
></view>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import {computed, markRaw, onMounted, ref} from 'vue';
const props = defineProps({
htmlId: String,
yData: {
type: Array,
default: () => [],
},
xData: {
type: Array,
default: () => [],
},
});
let myChart = ref();
const colors = [
'#FBCA85',
'#F5EB82',
'#FB8F85',
'#82F58B',
'#85FBDE',
'#85DEFB',
'#9FA7FB',
'#D59FFB',
'#FB9FD5',
];
const viewClick = (e: MouseEvent) => {
// myChart.value.dispatchAction({
// type: 'showTip',
// seriesIndex:0,
// dataIndex: 0,
// });
};
const seriesData = computed(() => {
return props.yData.map((item, index) => {
return {
data: item.data.map((value, i) => {
return {
value,
itemStyle: {
color: colors[i],
},
};
}),
type: 'line',
smooth: true,
name: item.name,
};
});
});
const legendData = computed(() => {
return props.yData.map(item => item.name);
});
const initEcharts = async () => {
myChart.value = echarts.init(document.getElementById(props.htmlId),undefined,{
renderer:'svg'
});
const option = {
tooltip: {
// trigger: 'axis',
triggerOn: 'click',
padding: 10,
backgroundColor: '#ccc',
borderColor: '#aaa',
borderWidth: 1
},
legend: {
data: legendData.value,
},
grid: {
left: '40px',
right: '20px',
top: '40',
bottom: '20',
},
xAxis: {
type: 'category',
boundaryGap: false,
data: props.xData,
},
yAxis: {
type: 'value',
},
series: seriesData.value,
};
myChart.value && myChart.value.setOption(option);
};
onMounted(() => {
initEcharts();
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,85 @@
<template>
<view
:id="htmlId"
ref="echartsRef"
:style="{ width: '100%', height: `${yData.length * 100}rpx` }"
></view>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import { computed, onMounted, ref } from 'vue';
const props = defineProps({
htmlId: String,
yData: {
type: Array,
default: () => [],
},
xData: {
type: Array,
default: () => [],
},
});
const echartsRef = ref(null);
const colors = [
'#FBCA85',
'#F5EB82',
'#FB8F85',
'#82F58B',
'#85FBDE',
'#85DEFB',
'#9FA7FB',
'#D59FFB',
'#FB9FD5',
];
const seriesData = computed(() => {
return props.xData.map((item, index) => {
return {
value: item,
itemStyle: {
color: colors[index],
},
};
});
});
const initEcharts = async () => {
const myChart = echarts.init(document.getElementById(props.htmlId));
const option = {
grid: {
left: '40px',
right: '20px',
top: '0%',
bottom: '20',
},
xAxis: {
type: 'value',
},
yAxis: {
type: 'category',
data: props.yData,
},
series: [
{
// 460, 280, 200, 260, 560, 280, 880, 590, 500
data: seriesData.value,
type: 'bar',
label: {
show: true,
position: 'right',
valueAnimation: true,
},
},
],
};
myChart && myChart.setOption(option);
};
onMounted(() => {
initEcharts();
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,30 @@
<template>
<view class="not-data">
<image :src="notInspector" alt="" />
<view>{{ text }}</view>
</view>
</template>
<script setup lang="ts">
import notInspector from '@/static/svg/not-inspector.svg';
defineProps({
text: String,
});
</script>
<style scoped lang="scss">
.not-data {
color: #666;
text-align: center;
font-size: 28rpx;
font-style: normal;
font-weight: 400;
& > image {
width: 250rpx;
height: 250rpx;
margin-bottom: 20rpx;
}
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<view class="load-view">
<view v-if="loading" class="load-view-loading">
<wd-loading size="40" color="#999999"></wd-loading>
<!-- <view class="load-text">{{ loadText }}</view>-->
<div class="loading-text">
加载中
<span></span>
<span></span>
<span></span>
</div>
</view>
<view v-if="empty && !loading" class="not-data">
<image :src="notInspector" alt="" />
<view>{{ emptyText }}</view>
</view>
<view v-else-if="!empty">
<slot></slot>
</view>
</view>
</template>
<script setup lang="ts">
import notInspector from '@/static/svg/not-inspector.svg';
import { ref, watchEffect } from 'vue';
const props = withDefaults(
defineProps<{
emptyText?: string;
loading: boolean;
empty: boolean;
}>(),
{
emptyText: '暂无数据',
empty: true,
},
);
const loadText = ref('加载中...');
let loadInterval = ref(undefined);
watchEffect(() => {
const loading = props.loading;
if (loading) {
const text = '加载中';
const dots = ['.', '..', '...'];
let index = 0;
loadInterval.value = setInterval(() => {
index = index === 2 ? 0 : index + 1;
loadText.value = text + dots[index];
}, 500);
} else {
clearInterval(loadInterval.value);
loadInterval.value = undefined;
}
});
</script>
<style scoped lang="scss">
.load-view {
position: relative;
&-loading {
width: 750rpx;
min-height: 300rpx;
position: absolute;
display: flex;
place-content: center;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.02);
}
}
.not-data {
color: #666;
text-align: center;
font-size: 28rpx;
font-style: normal;
font-weight: 400;
& > image {
width: 250rpx;
height: 250rpx;
margin-bottom: 20rpx;
}
}
.loading-text {
//width: 100rpx;
font-size: 24rpx;
white-space: nowrap;
overflow: hidden;
position: relative;
color: #999999;
margin-top: 10rpx;
}
.loading-text span {
display: inline-block;
width: 10rpx;
height: 10rpx;
margin-left: 2rpx;
background: #999999; /* 点的颜色 */
border-radius: 50%;
animation: loading 1.5s linear infinite;
opacity: 0;
}
.loading-text span:nth-child(1) {
animation-delay: 0s;
//background-color: red;
}
.loading-text span:nth-child(2) {
animation-delay: 0.5s;
//background-color: red;
}
.loading-text span:nth-child(3) {
animation-delay: 1s;
//background-color: red;
}
@keyframes loading {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(0.5);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<view>
<slot></slot>
</view>
<wd-loadmore :state="state" @reload="loadmore" />
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { onReachBottom } from '@dcloudio/uni-app';
const props = withDefaults(
defineProps<{
data: any;
}>(),
{},
);
const emit = defineEmits(['reload']);
const state = ref('loading');
watchEffect(() => {
if (props.data && props.data.records?.length < props.data?.size) {
state.value = 'finished';
} else {
state.value = 'loading';
}
});
onReachBottom(() => {
loadmore();
// if (state.value === 'loading') return;
});
const loadmore = () => {
// state.value = 'loading';
emit('reload');
};
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,78 @@
<template>
<wd-tabbar
v-model="tabbar"
fixed
safeAreaInsetBottom
placeholder
:z-index="9"
active-color="#625EFF"
inactive-color="#8E8D9D"
@change="tabbarChange"
>
<wd-tabbar-item
v-for="menu in menus"
:key="menu.key"
:name="menu.key"
:title="menu.title"
:icon="menu.icon"
>
<template #icon>
<wd-img
round
height="50rpx"
width="50rpx"
:src="tabbar === menu.key ? menu.activeIcon : menu.icon"
></wd-img>
</template>
</wd-tabbar-item>
</wd-tabbar>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
const props = defineProps<{
currentMenu: string;
}>();
const tabbar = ref('mine');
const menus = [
{
title: '课表',
key: 'schedule',
icon: '/static/tabBar/scheduleNo.png',
activeIcon: '/static/tabBar/schedule.png',
url: '/pages/inspector/schedule/index',
},
{
title: '学员',
key: 'student',
icon: '/static/tabBar/studentNo.png',
activeIcon: '/static/tabBar/student.png',
url: '/pages/inspector/student/index',
},
{
title: '我的',
key: 'mine',
icon: '/static/tabBar/mineno.png',
activeIcon: '/static/tabBar/mine.png',
url: '/pages/inspector/mine/index',
},
];
watchEffect(() => {
tabbar.value = menus.find(menu => menu.key === props.currentMenu)?.key ?? 'mine';
});
const tabbarChange = ({ value }) => {
console.log(value, 'value');
if (value === props.currentMenu) {
return;
}
const menu = menus.find(menu => menu.key === value);
uni.redirectTo({
url: menu.url,
});
};
</script>

View File

@ -0,0 +1,119 @@
<template>
<view class="plan-view-item">
<view class="order-or-finished">
<image
v-if="plan.finished"
:src="`${OSS_URL}/duxueshi/finished_icon.png`"
class="finised_icon"
/>
<text v-else class="order">{{ plan.order < 10 ? `0${plan.order}` : plan.order }}</text>
</view>
<view class="plan-view-item-content">
<view class="plan-view-item-content-info">
<view>
<text class="course-name">{{ plan.courseName }}</text>
<text class="tag" :style="{ backgroundColor: plan.bgColor }">{{ plan.label }}</text>
</view>
<view>
<text :style="{ color: plan.finished ? '#21d17a' : '#FF2828' }">{{
plan.progressText
}}</text>
<text>|</text>
<text>{{ plan.time }}</text>
</view>
</view>
<view class="content-intro">{{ plan.note }}</view>
</view>
</view>
</template>
<script setup lang="ts">
import type { PlanItem } from '@/components/inspector/plan-item';
defineProps<{
plan: PlanItem;
}>();
const OSS_URL = import.meta.env.VITE_OSS_HOST;
</script>
<style scoped lang="scss">
.plan-view-item {
display: flex;
padding: 20rpx 16rpx 0 16rpx;
//align-items: flex-start;
// gap: 20rpx;
//align-self: stretch;
.order-or-finished {
width: 46rpx;
margin-right: 20rpx;
color: #333;
text-align: center;
font-size: 30rpx;
font-style: normal;
font-weight: 500;
line-height: 46rpx; /* 153.333% */
.finised_icon {
width: 46rpx;
height: 46rpx;
}
.order {
display: block;
width: 46rpx;
}
}
&-content {
width: 100%;
width: calc(100% - 66rpx);
border-bottom: 2rpx solid #ececec;
padding-bottom: 20rpx;
.tag {
display: inline-block;
padding: 0 10rpx;
color: #fff;
font-size: 22rpx;
font-weight: 500;
line-height: 30rpx;
border-radius: 6rpx;
}
&-info {
display: flex;
justify-content: space-between;
// gap: 10rpx;
& > view {
display: flex;
gap: 10rpx;
align-items: center;
& > text {
font-size: 24rpx;
font-style: normal;
font-weight: 500;
color: #a9aab5;
&.course-name {
font-size: 30rpx;
font-weight: 500;
color: #0a1f13;
}
&.progress-rate {
color: #21d17a;
}
}
}
}
.content-intro {
color: #333333;
font-size: 28rpx;
margin-top: 10rpx;
word-wrap: break-word;
}
}
}
</style>

View File

@ -0,0 +1,43 @@
export interface PlanItem {
order: number;
finished: boolean;
courseName: string;
label: string;
progressText: string;
time: string;
note: string;
bgColor?: string;
}
export const TASK_TYPE = {
'0': {
label: '自由学',
notePrefix: `观看学习视频:《#{chapterName}》章节《#{courseTimeName}》视频`,
bgColor: '#FFA65D',
},
'1': {
label: '自由学',
notePrefix: '完成章节练习:《#{chapterName}》',
bgColor: '#FFA65D',
},
'2': {
label: '精准学',
notePrefix: 'AI精准学#{finishedNum}次',
bgColor: '#4FD6EE',
},
'3': {
label: '语感训练',
notePrefix: '语感训练#{finishedNum}次',
bgColor: '#B4B3FF',
},
'4': {
label: '错题本',
notePrefix: '订正错题本#{finishedNum}道',
},
};
export function formatTaskTypeNotePrefix(notePrefix, values, defaultValue = 0) {
return notePrefix.replace(/#{(.*?)}/g, (match, key) => {
return values[key] || defaultValue;
});
}

View File

@ -0,0 +1,155 @@
<template>
<view class="calendar-main">
<view class="month-view">
<wd-icon name="arrow-left" size="40rpx" @click="preMonth"></wd-icon>
<view class="month-text">{{ monthFirstDay.format('YYYY年MM月') }}</view>
<wd-icon name="arrow-right" size="40rpx" @click="nextMonth"></wd-icon>
</view>
<view class="days-view">
<view class="days-view-weeks">
<view v-for="week in weeks" :key="week">{{ week }}</view>
</view>
<view class="days-view-day">
<view v-for="(weekDays, index) in dayArray" :key="index" class="week-day">
<view
v-for="day in weekDays"
:key="day"
:class="[
'day-item',
{
'not-current': !day.isCurrentMonth,
'is-active': value === day.date,
'current-day': day.date === dayjs().format('YYYY-MM-DD'),
},
]"
@click="chooseDay(day)"
>
<view class="day-text">{{ day.day }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import dayjs from 'dayjs';
import { type CalendarDay, generateMonthCalendar } from '@/utils/calendar';
defineProps({
value: {
type: String,
default: dayjs().format('YYYY-MM-DD'),
},
});
const _emit = defineEmits(['update:value']);
const monthFirstDay = ref(dayjs().startOf('month'));
const weeks = ['一', '二', '三', '四', '五', '六', '日'];
const dayArray = computed(() => {
return generateMonthCalendar(monthFirstDay.value.toDate());
});
const chooseDay = (_day: CalendarDay) => {
_emit('update:value', _day.date);
};
const preMonth = () => {
monthFirstDay.value = monthFirstDay.value.subtract(1, 'month');
};
const nextMonth = () => {
monthFirstDay.value = monthFirstDay.value.add(1, 'month');
};
</script>
<style scoped lang="scss">
.calendar-main {
}
.month-view {
font-size: 30rpx;
color: #666666;
text-align: center;
padding: 30rpx 0;
display: flex;
place-content: center;
align-items: center;
.month-text {
padding: 0 20rpx;
}
}
.days-view {
padding: 0 20rpx;
box-sizing: border-box;
&-weeks {
display: flex;
border-bottom: 2rpx solid #cccccc;
color: #999999;
padding: 20rpx 0;
& > view {
width: 100%;
text-align: center;
}
}
&-day {
.week-day {
display: flex;
//border-bottom: 2rpx solid #cccccc;
color: #999999;
padding: 20rpx 0;
& > .day-item {
width: 100%;
text-align: center;
color: #000000;
box-sizing: border-box;
&.not-current {
color: #cccccc;
}
&.is-active {
& > .day-text {
border: 4rpx solid #615dff;
}
}
&.current-day {
&.is-active {
& > .day-text {
background-color: #615dff;
color: #ffffff !important;
}
}
& > .day-text {
color: #615dff;
}
}
& > .day-text {
font-size: 26rpx;
width: 60rpx;
height: 60rpx;
display: grid;
place-content: center;
border-radius: 60rpx;
font-weight: 550;
border: 4rpx solid white;
}
}
}
}
}
</style>

View File

@ -0,0 +1,180 @@
<template>
<report-content-base :title="title">
<isp-data-load :loading="loading" :empty-text="emptyText" :empty="yData.length === 0">
<view class="charts-box" :style="{ height: `${height}rpx` }">
<qiun-data-charts
type="bar"
canvas2d
:opts="opts"
:tooltipShow="false"
:chartData="chartData"
/>
</view>
</isp-data-load>
<!-- <view :id="htmlId" ref="echartsRef" :style="{ width: '100%', height: `${height}rpx` }"></view> -->
</report-content-base>
</template>
<script setup lang="ts">
import ReportContentBase from '@/pages/inspector/student-detail/components/report-content-base.vue';
import * as echarts from 'echarts';
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue';
const props = defineProps({
title: String,
emptyText: String,
htmlId: String,
yData: {
type: Array,
default: () => [],
},
xData: {
type: Array,
default: () => [],
},
});
const opts = ref({
padding: [0, 42, 0, 0],
enableScroll: false,
legend: {
position: 'top',
padding: 2,
show: false,
},
xAxis: {
boundaryGap: 'justify',
disableGrid: false,
min: 0,
max: 10,
axisLine: false,
labelCount: 4, //
type: 'grid', // X
gridType: 'dash', // X线
gridColor: '#d4d4d4',
splitNumber: 10,
},
yAxis: {},
extra: {
bar: {
type: 'group',
width: 25,
meterBorde: 1,
meterFillColor: '#FFFFFF',
activeBgColor: '#000000',
activeBgOpacity: 0.05,
barBorderCircle: false,
seriesGap: 2,
categoryGap: 2,
},
},
});
const chartData = ref({
categories: [] as any,
series: [
{
data: [] as any,
},
],
});
const height = computed(() => {
return props.yData.length * 120;
});
const loading = ref(false);
watchEffect(() => {
if (props.yData && props.yData.length > 0) {
nextTick(() => {
chartData.value.categories = props.yData;
for (let i in props.xData) {
chartData.value.series[0].data[i] = props.xData[i];
}
console.log('props.yData', props.yData, props.xData);
});
}
});
// const props = defineProps({
// title: String,
// htmlId: String,
// yData: {
// type: Array,
// default: () => [],
// },
// xData: {
// type: Array,
// default: () => [],
// },
// });
// const echartsRef = ref(null);
// const colors = [
// '#FBCA85',
// '#F5EB82',
// '#FB8F85',
// '#82F58B',
// '#85FBDE',
// '#85DEFB',
// '#9FA7FB',
// '#D59FFB',
// '#FB9FD5',
// ];
// const height = computed(() => {
// return props.yData.length * 100;
// });
// const seriesData = computed(() => {
// return props.xData.map((item, index) => {
// return {
// value: item,
// itemStyle: {
// color: colors[index],
// },
// };
// });
// });
// const initEcharts = async () => {
// const myChart = echarts.init(document.getElementById(props.htmlId));
// const option = {
// grid: {
// left: '40px',
// right: '20px',
// top: '0%',
// bottom: '20',
// },
// xAxis: {
// type: 'value',
// },
// yAxis: {
// type: 'category',
// data: props.yData,
// },
// series: [
// {
// data: props.xData.map((item, index) => {
// return {
// value: item,
// itemStyle: {
// color: colors[index],
// },
// };
// }),
// type: 'bar',
// label: {
// show: true,
// position: 'right',
// valueAnimation: true,
// },
// },
// ],
// };
// setTimeout(() => {
// myChart.setOption(option);
// }, 300);
// };
</script>
<style lang="scss" scoped>
:deep(.not-data) {
padding: 30rpx 0;
background-color: #fff;
border-radius: 20rpx;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<view class="report_box">
<!-- 学习汇总 -->
<study-summary :report="report" />
<!-- 督学计划完成情况 -->
<plan-finish-data :tasks="report.tasks ?? []" />
<horizontal-chart
title="各学科练习题数统计"
emptyText="暂无练习数据"
:x-data="practiceQuestionStatistics.xData"
:y-data="practiceQuestionStatistics.yData"
html-id="practise-chart"
/>
<horizontal-chart
title="各学科观看视频统计"
emptyText="暂无观看视频数据"
:x-data="watchVideoStatistics.xData"
:y-data="watchVideoStatistics.yData"
html-id="video-see-chart"
/>
<!-- 知识图谱统计 -->
<knowledge-graph-statistics :knowledge-graph-statistics="report.knowledgeGraphStatistics" />
<!-- 已学知识点及掌握程度 -->
<knowledge-mastery :learned-knowledge="report.learnedKnowledge || []" />
</view>
</template>
<script setup lang="ts">
import StudySummary from '@/components/student-report/study-summary.vue';
import PlanFinishData from '@/components/student-report/plan-finish-data.vue';
import HorizontalChart from '@/components/student-report/horizontal-chart.vue';
import KnowledgeGraphStatistics from '@/components/student-report/knowledge-graph-statistics.vue';
import KnowledgeMastery from '@/components/student-report/knowledge-mastery.vue';
import { computed } from 'vue';
const props = withDefaults(
defineProps<{
report: any;
}>(),
{
report: {},
},
);
const colors = [
'#FB9FD5',
'#D59FFB',
'#9FA7FB',
'#85DEFB',
'#85FBDE',
'#FBCA85',
'#FB8F85',
'#82F58B',
'#F5EB82',
'#7a6bff',
];
//
const practiceQuestionStatistics = computed(() => {
const practiceQuestionStatistics = props.report.practiceQuestionStatistics ?? [];
const yData = practiceQuestionStatistics.map((item: any) => item.subjectName);
const xData = practiceQuestionStatistics.map((item: any) => {
return { value: item.number, color: colors[Number(item.subjectId) - 1] };
});
return {
yData,
xData,
};
});
//
const watchVideoStatistics = computed(() => {
const watchVideoStatistics = props.report.watchVideoStatistics ?? [];
const yData = watchVideoStatistics.map((item: any) => item.subjectName);
const xData = watchVideoStatistics.map((item: any) => item.number);
return {
yData,
xData,
};
});
console.log('督学报告', props.report);
</script>
<style scoped lang="scss">
.report_box {
padding: 0 30rpx;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<report-content-base>
<view class="chart_title">
<text class="txt">知识图谱统计</text>
<view class="underline"></view>
</view>
<view class="k-g-s-title">我的知识图谱变化情况</view>
<view class="chart_label">
<view class="increase"><text class="iden_color"></text>增加</view>
<view class="decrease"><text class="iden_color"></text>减少</view>
</view>
<ColumnChart :chartData="data" />
<!-- <qy-echarts-add-lower
v-if="data.length > 0"
html-id="knowledge-graph-stats-echarts"
:data="data"
/> -->
</report-content-base>
</template>
<script setup lang="ts">
import ColumnChart from '@/pages/academicReport/components/Echart/ColumnChart.vue';
import ReportContentBase from '@/pages/inspector/student-detail/components/report-content-base.vue';
import { computed, ref } from 'vue';
const props = defineProps<{
knowledgeGraphStatistics: any;
}>();
const data: any = computed(() => {
if (!props.knowledgeGraphStatistics) {
return {};
}
return {
categories: ['陌生数', '熟悉数', '掌握数'],
series: [
{
data: [
{
color: props.knowledgeGraphStatistics.unfamiliarCount < 0 ? '#ff8074' : '#3fd15f',
value: Math.abs(props.knowledgeGraphStatistics.unfamiliarCount),
},
{
color: props.knowledgeGraphStatistics.knowCount < 0 ? '#ff8074' : '#3fd15f',
value: Math.abs(props.knowledgeGraphStatistics.knowCount),
},
{
color: props.knowledgeGraphStatistics.graspCount < 0 ? '#ff8074' : '#3fd15f',
value: Math.abs(props.knowledgeGraphStatistics.graspCount),
},
],
},
],
};
});
</script>
<style scoped lang="scss">
.chart_title {
position: relative;
margin: auto;
width: 204rpx;
height: 60rpx;
font-size: 34rpx;
font-weight: 600;
line-height: 60rpx;
text-align: center;
color: #000000;
.txt {
position: absolute;
display: block;
z-index: 99;
}
.underline {
position: absolute;
bottom: 8rpx;
left: 0;
width: 100%;
height: 12rpx;
background-color: #dad9ff;
}
}
.chart_label {
display: flex;
justify-content: center;
font-size: 26rpx;
color: rgba(102, 102, 102, 1);
.iden_color {
display: inline-block;
width: 30rpx;
height: 8rpx;
margin-right: 4rpx;
}
.increase {
display: flex;
align-items: center;
margin-right: 16rpx;
.iden_color {
background-color: #3fd15f;
}
}
.decrease {
display: flex;
align-items: center;
.iden_color {
background-color: #ff8074;
}
}
}
.k-g-s-title {
margin: 50rpx auto 30rpx;
color: #333333;
text-align: center;
font-size: 32rpx;
font-style: normal;
font-weight: 500;
line-height: 48rpx; /* 150% */
}
.type-view {
display: flex;
gap: 20rpx;
justify-content: center;
align-items: center;
padding: 30rpx 0;
svg {
width: 32rpx;
height: 16rpx;
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<report-content-base title="已学知识点及掌握程度">
<qy-custom-table :columns="columns" :data="knowledgeStudyData" />
</report-content-base>
</template>
<script setup lang="ts">
import ReportContentBase from '@/pages/inspector/student-detail/components/report-content-base.vue';
import { computed } from 'vue';
const props = withDefaults(
defineProps<{
learnedKnowledge: LearnedKnowledgeItem[];
}>(),
{},
);
interface LearnedKnowledgeItem {
name: string;
answerCount: number;
masteryDegree: number;
}
const columns = [
{
key: 'name',
title: '知识点名称',
},
{
key: 'frequency',
title: '答题频次',
},
{
key: 'mastery',
title: '掌握程度',
},
];
const knowledgeStudyData = computed(() => {
return (props.learnedKnowledge ?? []).map(item => {
return {
name: item.name,
frequency: item.answerCount,
// 1- 2- 3-
mastery: item.masteryDegree === 1 ? '陌生' : item.masteryDegree === 2 ? '熟悉' : '掌握',
};
});
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,88 @@
<template>
<view>
<view class="finish-title">督学计划完成情况</view>
<view class="plan-view">
<isp-plan-item v-for="item in showTasks" :key="item" :plan="item" />
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import dayjs from 'dayjs';
import { formatTaskTypeNotePrefix, TASK_TYPE } from '@/components/inspector/plan-item';
const props = defineProps({
tasks: {
type: Array,
default: () => [],
},
});
const showTasks = computed(() => {
return props.tasks.map((task: any, index) => {
const _type = TASK_TYPE[task.taskType];
return {
order: index + 1,
courseName: task.subjectName,
label: _type.label,
bgColor: _type.bgColor,
finished: task.taskType === 0 ? Number(task.progress) > 0.8 : task.finishedNum >= task.num,
progressText:
task.taskType === 0
? Number(task.progress) > 0.8
? '1/1'
: '0/1'
: `${task.finishedNum}/${task.num}`,
time: `${dayjs(task.inspectorTimeStart).format('HH:mm')}-${dayjs(
task.inspectorTimeEnd,
).format('HH:mm')}`,
note: formatTaskTypeNotePrefix(_type.notePrefix, task),
};
});
});
// const plans = [
// {
// order: 1,
// finished: false,
// courseName: '',
// label: '',
// progressText: '100%',
// time: '19:00-19:20',
// note: ' ',
// },
// {
// order: 1,
// finished: true,
// courseName: '',
// label: '',
// progressText: '100%',
// time: '19:00-19:20',
// note: ' ',
// },
// ];
</script>
<style scoped lang="scss">
.finish-title {
margin: 70rpx 0 30rpx 0;
color: #000;
text-align: center;
font-size: 32rpx;
font-style: normal;
font-weight: 500;
line-height: 48rpx; /* 150% */
}
.plan-view {
background-color: white;
border-radius: 20rpx;
border: 1rpx solid #ececec;
box-sizing: border-box;
:deep(.plan-view-item:last-child) {
.plan-view-item-content {
border-bottom: none;
}
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<view class="summary-view">
<qy-up-down-flat v-for="(item, index) in studySummary" :key="index" :data="item" />
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { minToHour } from '@/utils';
import { UpDownFlatState, UpDownFlatType } from '@/components/up-down-flat/index';
const props = withDefaults(
defineProps<{
report: any;
}>(),
{
report: {},
},
);
const studySummary = computed(() => {
const studyVideoState = [UpDownFlatState.FLAT, UpDownFlatState.UP, UpDownFlatState.DOWN];
const exercisesState = [UpDownFlatState.FLAT, UpDownFlatState.UP, UpDownFlatState.DOWN];
const correctnessState = [UpDownFlatState.FLAT, UpDownFlatState.UP, UpDownFlatState.DOWN];
return [
{
title: '已学视频',
type: UpDownFlatType.NORMAL,
value: [props.report.studyVideoCount],
unit: '个',
state: studyVideoState[props.report.studyVideoCountCompare],
},
{
title: '练习题数',
type: UpDownFlatType.NORMAL,
value: [props.report.exercisesCount],
unit: '道',
state: exercisesState[props.report.exercisesCountCompare],
},
{
title: '答题正确率',
type: UpDownFlatType.NORMAL,
value: [`${props.report.correctness}%`],
state:
props.report.correctnessCompare === -1
? UpDownFlatState.DOWN
: correctnessState[props.report.correctnessCompare],
},
{
title: '实际督学时长',
type: UpDownFlatType.TIME,
value: minToHour(props.report.inspectorTimeMinute),
},
];
});
</script>
<style scoped lang="scss">
.summary-view {
width: 100%;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
&-item {
width: 305rpx;
height: 155rpx;
border-radius: 20rpx;
background: #f7faff;
overflow: hidden;
box-sizing: border-box;
margin-bottom: 20rpx;
&-title {
display: grid;
place-content: center;
height: 76rpx;
background-color: #ebf2ff;
color: #333;
font-size: 26rpx;
font-style: normal;
font-weight: 500;
}
&-content {
display: flex;
gap: 10rpx;
height: 79rpx;
align-items: center;
place-content: center;
.normal-text {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
.value-text {
font-size: 28rpx;
font-style: normal;
font-weight: 500;
}
.up-icon {
width: 26rpx;
height: 26rpx;
background: url('~@/static/svg/up.svg') no-repeat;
background-size: 100% 100%;
}
.up-color {
color: #3fd15f;
}
.down-icon {
width: 26rpx;
height: 26rpx;
background: url('~@/static/svg/decline.svg') no-repeat;
background-size: 100% 100%;
}
.down-color {
color: #ff2828;
}
.flat-icon {
width: 26rpx;
height: 26rpx;
background: url('~@/static/svg/flat.svg') no-repeat;
background-size: 100% 100%;
}
.flat-color {
color: #ff8d5f;
}
}
}
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
>
<path
d="M3.125 11.875H26.875V25C26.875 25.6904 26.3154 26.25 25.625 26.25H4.375C3.68464 26.25 3.125 25.6904 3.125 25V11.875Z"
stroke="#666666"
stroke-width="2.5"
stroke-linejoin="round"
/>
<path
d="M3.125 5.625C3.125 4.93464 3.68464 4.375 4.375 4.375H25.625C26.3154 4.375 26.875 4.93464 26.875 5.625V11.875H3.125V5.625Z"
stroke="#666666"
stroke-width="2.5"
stroke-linejoin="round"
/>
<path
d="M10 2.5V7.5"
stroke="#666666"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M20 2.5V7.5"
stroke="#666666"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.5 21.25H21.25"
stroke="#666666"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8.75 21.25H12.5"
stroke="#666666"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.5 16.25H21.25"
stroke="#666666"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8.75 16.25H12.5"
stroke="#666666"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,22 @@
<script setup lang="ts"></script>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
<path
d="M4.375 26.25H26.875"
stroke="#DEDFE9"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M6.875 16.6999V21.25H11.4482L24.375 8.31756L19.8094 3.75L6.875 16.6999Z"
fill="#DEDFE9"
stroke="#DEDFE9"
stroke-width="2.5"
stroke-linejoin="round"
/>
</svg>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,14 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
<path
d="M22.5 11.875L15 19.375L7.5 11.875H22.5Z"
fill="#DEDFE9"
stroke="#DEDFE9"
stroke-width="2.5"
stroke-linejoin="round"
/>
</svg>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,50 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="120"
height="120"
viewBox="0 0 120 120"
fill="none"
>
<g filter="url(#filter0_d_251_23854)">
<circle cx="60" cy="60" r="50" fill="white" />
</g>
<rect x="40" y="57" width="40" height="6" rx="3" fill="#615DFF" />
<rect x="57" y="80" width="40" height="6" rx="3" transform="rotate(-90 57 80)" fill="#615DFF" />
<defs>
<filter
id="filter0_d_251_23854"
x="0"
y="0"
width="120"
height="120"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.89083 0 0 0 0 0.90362 0 0 0 0 0.929199 0 0 0 1 0"
/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_251_23854" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_251_23854"
result="shape"
/>
</filter>
</defs>
</svg>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,18 @@
export enum UpDownFlatType {
NORMAL = 'normal',
TIME = 'time',
}
export enum UpDownFlatState {
UP = 'up',
DOWN = 'down',
FLAT = 'flat',
}
export interface UpDownFlat {
title: string;
value: any[];
unit: string;
type: UpDownFlatType;
state: UpDownFlatState;
}

View File

@ -0,0 +1,133 @@
<template>
<view class="summary-view-item">
<view class="summary-view-item-title">{{ data.title }}</view>
<view v-if="data.type === 'normal'" class="summary-view-item-content">
<text :class="['value-text', state.color]">{{ data.value[0] }}</text>
<text class="normal-text">{{ data.unit }}</text>
<text v-if="state.icon !== 'none'" :class="state.icon"></text>
</view>
<view v-else class="summary-view-item-content">
<text :class="['value-text', state.color]">{{ data.value[0] }}</text>
<text class="normal-text"></text>
<text :class="['value-text', state.color]">{{ data.value[1] }}</text>
<text class="normal-text"></text>
<text v-if="state.icon !== 'none'" :class="state.icon"></text>
</view>
</view>
</template>
<script setup lang="ts">
import type { UpDownFlat } from '@/components/up-down-flat/index';
import { computed } from 'vue';
const props = defineProps<{
data: UpDownFlat;
}>();
// const { value } = props.data;
const states = {
up: {
icon: 'up-icon',
color: 'up-color',
},
down: {
icon: 'down-icon',
color: 'down-color',
},
flat: {
icon: 'flat-icon',
color: 'flat-color',
},
default: {
icon: 'none',
color: 'default-color',
},
};
const state = computed(() => {
const _state = props.data.state ?? 'default';
return states[_state];
});
</script>
<style scoped lang="scss">
.summary-view-item {
width: 100%;
height: 155rpx;
border-radius: 20rpx;
background: #f7faff;
overflow: hidden;
box-sizing: border-box;
margin-bottom: 20rpx;
&-title {
display: grid;
place-content: center;
height: 76rpx;
background-color: #ebf2ff;
color: #333;
font-size: 26rpx;
font-style: normal;
font-weight: 500;
}
&-content {
display: flex;
gap: 10rpx;
height: 79rpx;
align-items: center;
place-content: center;
.normal-text {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
.value-text {
font-size: 28rpx;
font-style: normal;
font-weight: 500;
}
.up-icon {
width: 26rpx;
height: 26rpx;
background: url('~@/static/svg/up.svg') no-repeat;
background-size: 100% 100%;
}
.up-color {
color: #3fd15f;
}
.down-icon {
width: 26rpx;
height: 26rpx;
background: url('~@/static/svg/decline.svg') no-repeat;
background-size: 100% 100%;
}
.down-color {
color: #ff2828;
}
.flat-icon {
width: 26rpx;
height: 26rpx;
background: url('~@/static/svg/flat.svg') no-repeat;
background-size: 100% 100%;
}
.flat-color {
color: #ff8d5f;
}
.default-color {
color: #615dff;
}
}
}
</style>

8
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
export default component;
}

2
src/hooks/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './useKey';
export * from './useTools';

97
src/hooks/useGlobal.js Normal file
View File

@ -0,0 +1,97 @@
// import qs from 'qs';
// function formatUrlWithParams(url, params) {
// return `${url}?${qs.stringify(params, { encode: false })}`;
// }
/**
* uni 页面跳转修改支持传入对象参数保留当前页面跳转到应用内的某个页面
* @param url uni 跳转的页面
* @param params 参数
*/
function navigateTo(url, params = {}) {
uni.navigateTo({
url: formatUrlWithParams(url, params)
});
}
//修改时间格式
export const formatSecond = (val) => {
let time = Number(val)
if (time > 3600) {
// 大于一个小时
let h = parseInt(time / 60 / 60 % 24)
h = h > 0 ? h + '小时' : ''
let m = parseInt(time / 60 % 60)
m = m > 0 ? m + '分钟' : ''
return h + m
} else if (time < 3600 && time > 60) {
let m = parseInt(time / 60 % 60)
m = m > 0 ? m + '分钟' : ''
let s = parseInt(time % 60)
s = s > 0 ? s + '秒' : ''
return m + s
} else if (time < 60 && time > 0) {
return time + '秒'
} else {
return 0
}
}
export const formatSecondEn = (val) => {
let time = Number(val)
if (time > 3600) {
// 大于一个小时
let h = parseInt(time / 60 / 60 % 24)
h = h > 0 ? h + 'h' : ''
let m = parseInt(time / 60 % 60)
m = m > 0 ? m + 'm' : ''
return h + m
} else if (time < 3600 && time > 60) {
let m = parseInt(time / 60 % 60)
m = m > 0 ? m + 'm' : ''
let s = parseInt(time % 60)
s = s > 0 ? s + 's' : ''
return m + s
} else if (time < 60 && time > 0) {
return time + 's'
} else {
return 0
}
}
// 2024-01-21转1.21
export const formatDate = (dateString) => {
// 将字符串格式的日期转换成Date对象
var date = new Date(dateString);
// 获取月份和日期
var month = date.getMonth() + 1; // 月份从0开始所以要加1
var day = date.getDate();
// 拼接月份和日期并返回结果
var formattedDate = month + '.' + day;
return formattedDate;
}
/**
* 时间格式化 YY.MM.DD hh:mm 或者YY.MM.DD
* @param
* @returns
*/
export const formatDate1 = (date, toMinute) => {
let date1 = new Date(date);
let year = date1.getFullYear();
let month = date1.getMonth() + 1; // 月份是从0开始的
let day = date1.getDate();
let hour = date1.getHours()
let minute = date1.getMinutes()
if (toMinute) {
return year + '.' + (month < 10 ? '0' + month : month) + '.' + (day < 10 ? '0' + day : day) + ' ' + (hour <
10 ? '0' + hour : hour) + ':' +
(minute < 10 ? '0' + minute : minute);
} else {
return year + '.' + (month < 10 ? '0' + month : month) + '.' + (day < 10 ? '0' + day : day);
}
}

64
src/hooks/useImage.js Normal file
View File

@ -0,0 +1,64 @@
// 获取学生头像
export const studentHeadImage = sex => {
let headPic = '';
switch (sex) {
case 0:
// 男
headPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/boy.png';
return headPic;
case 1:
// 女
headPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/girl.png';
return headPic;
}
};
// 获取成就勋章
export const getMedalImage = type => {
let medalPic = '';
switch (type) {
case 1:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/medal_1.png';
return medalPic;
case 2:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/medal_2.png';
return medalPic;
case 3:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/medal_3.png';
return medalPic;
case 4:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/medal_4.png';
return medalPic;
}
};
// 获取置灰成就勋章
export const getGrayMedalImage = type => {
let medalPic = '';
switch (type) {
case 1:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/gray_medal_1.png';
return medalPic;
case 2:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/gray_medal_2.png';
return medalPic;
case 3:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/gray_medal_3.png';
return medalPic;
case 4:
medalPic =
'https://xuexiaole-1313840333.cos.ap-guangzhou.myqcloud.com/xuexiaoleClient/gray_medal_4.png';
return medalPic;
}
};
// 获取升将持平
export const getRankFlagImage = url => {
return '/static/iconPicture/' + url;
};

67
src/hooks/useKey.ts Normal file
View File

@ -0,0 +1,67 @@
const OSS_URL = import.meta.env.VITE_OSS_HOST;
// 学历
export const useEducation = {
0: '小学',
1: '初中',
2: '高中',
3: '中专',
4: '大专',
5: '本科',
6: '研究生',
7: '博士',
8: '博士后',
};
// 家长称谓
export const useRelation1 = {
1: '父亲',
2: '母亲',
3: '爷爷',
4: '奶奶',
5: '外公',
6: '外婆',
7: '其他',
};
export const useRelation = [
{
name: '爸爸',
icon: `${OSS_URL}/urm/bindDevice/baba.png`,
id: 1,
color: '#63B1FF',
},
{
name: '妈妈',
icon: `${OSS_URL}/urm/bindDevice/mama.png`,
id: 2,
color: '#FF8A8A',
},
{
name: '爷爷',
icon: `${OSS_URL}/urm/bindDevice/yeye.png`,
id: 3,
color: '#8979FF',
},
{
name: '奶奶',
icon: `${OSS_URL}/urm/bindDevice/nainai.png`,
id: 4,
color: '#FFB152FB',
},
{
name: '外公',
icon: `${OSS_URL}/urm/bindDevice/wg.png`,
id: 5,
color: '#69CBFF',
},
{
name: '外婆',
icon: `${OSS_URL}/urm/bindDevice/waipo.png`,
id: 6,
color: '#E27AFF',
},
{
name: '其他',
icon: `${OSS_URL}/urm/bindDevice/qita.png`,
id: 7,
color: '#5BE19E',
},
];

10
src/hooks/useObj.js Normal file
View File

@ -0,0 +1,10 @@
// 对象属性key改名
export const renameKeys = (obj, oldKeys, newKeys) => {
for (let i in oldKeys) {
let oldKey = oldKeys[i]
let newKey = newKeys[i]
obj[newKey] = obj[oldKey]
// delete obj[oldKey]
}
return obj
}

127
src/hooks/useSocket.ts Normal file
View File

@ -0,0 +1,127 @@
import { useWebSocket } from '@vueuse/core';
import { user } from '@/store';
import { ref } from 'vue';
import type { UseWebSocketReturn } from '@vueuse/core';
let SOCKET: UseWebSocketReturn<any>;
export const useSocket = () => {
const { token, userInfo } = user();
const inspectorMessage = ref<anyObj>();
const inspectorDesktopMessage = ref([]);
const desktopPath = ref<string>();
enum SOCKET_MSG_TYPE {
lockScreen = 'lockScreen',
unLockScreen = 'unLockScreen',
desktopPreview = 'desktopPreview',
inspectorTalkMessage = 'inspectorTalkMessage',
desktopScreenshotImage = 'desktopScreenshotImage',
}
function init() {
SOCKET = useWebSocket(
`${process.env.VITE_WS_URL}?accessToken=${token.replace('Bearer ', '')}`,
{
autoReconnect: {
retries: 5,
delay: 3000,
onFailed() {
console.log('重新连接失败');
},
},
heartbeat: {
message: '{"type":"ping"}',
interval: 5000,
pongTimeout: 3000,
},
onConnected,
onDisconnected,
onError,
onMessage,
},
);
sendMsg({
type: 'ping',
});
}
function onConnected(ws: WebSocket) {
// ws.binaryType = 'arraybuffer';
console.log('ws已连接');
}
function onDisconnected() {
console.log('ws已关闭');
}
function onError(ws: WebSocket, event: Event) {
// console.log(event);
}
function onMessage(ws: WebSocket, event: MessageEvent) {
const data = JSON.parse(event.data);
console.log(data);
switch (data.type) {
// case SOCKET_MSG_TYPE.lockScreen:
// // 执行锁屏
// useLog(LogType.LockScreenTime, {
// simSerialNumber: SN,
// lockFlag: 1,
// });
// break;
// case SOCKET_MSG_TYPE.unLockScreen:
// // 执行解锁
// useLog(LogType.LockScreenTime, {
// simSerialNumber: SN,
// lockFlag: 0,
// });
// break;
case SOCKET_MSG_TYPE.desktopPreview:
onDesktopPreview();
break;
case SOCKET_MSG_TYPE.inspectorTalkMessage:
inspectorMessage.value = data.msg;
break;
case SOCKET_MSG_TYPE.desktopScreenshotImage:
inspectorDesktopMessage.value = data;
break;
}
}
// 桌面预览
async function onDesktopPreview() {
// const { data: res } = await uploadFile({});
// sendMsg({
// type: 'desktopScreenshotImage',
// userId: userInfo.userId,
// simSerialNumber: SN,
// msg: '',
// });
// SOCKET.send(
// JSON.stringify({
// type: 'desktopScreenshotImage',
// userId: userInfo.userId,
// simSerialNumber: SN,
// msg: ''
// }),
// );
}
// 发送消息
function sendMsg(data: anyObj, config = {}) {
if (!SOCKET) {
console.error('ws不存在');
return;
}
inspectorDesktopMessage.value = [];
SOCKET.send(JSON.stringify(data));
}
function textToBuffer(str: string) {
return new TextEncoder().encode(str);
}
function bufferToText(buffer: ArrayBuffer) {
return new TextDecoder().decode(buffer);
}
return {
init,
sendMsg,
inspectorMessage,
inspectorDesktopMessage,
};
};

22
src/hooks/useTools.ts Normal file
View File

@ -0,0 +1,22 @@
export const maskPhone = (phone: string) => {
if (!phone || phone.length !== 11) return phone;
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1***$2');
};
export const debounce = (fn: (...args: any[]) => void, delay: number, immediate = false) => {
let timer: number | null = null;
return function (this: any, ...args: any[]) {
timer && clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
callNow && fn.apply(this, args);
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
};
};

18
src/main.ts Normal file
View File

@ -0,0 +1,18 @@
import { createSSRApp } from 'vue';
import App from './App.vue';
import * as Pinia from 'pinia';
import './styles/global.scss';
import './styles/font.scss';
import './styles/iconfont.scss';
import BackBar from '@/components/BackBar/index';
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
app.component('BackBar', BackBar);
return {
app,
Pinia,
};
}

91
src/manifest.json Normal file
View File

@ -0,0 +1,91 @@
{
"name": "学小乐管理端",
"appid": "__UNI__A05ABA1",
"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": "wxd13aabeb40898e1d",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"title": "学小乐",
"devServer": {
"proxy": {
"/api": {
"target": "https://test.pi.xuexiaole.com",
"changeOrigin": true,
"pathRewrite": {
"^/api": "" //
}
}
},
"https": false
},
"router": {
"mode": "hash",
"base": "./"
}
}
}

402
src/pages.json Normal file
View File

@ -0,0 +1,402 @@
{
"pages": [
{
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "学小乐"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/academicReport/index",
"style": {
"navigationBarTitleText": "学情报告"
}
},
{
"path": "pages/applicationManagement/index",
"style": {
"navigationBarTitleText": "应用管控"
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/mine/bindApply/index",
"style": {
"navigationBarTitleText": "绑定申请",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/Warranty/index",
"style": {
"navigationBarTitleText": "电子保修卡",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/Warranty/applyService",
"style": {
"navigationBarTitleText": "申请售后",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/Warranty/serviceHistory",
"style": {
"navigationBarTitleText": "售后历史",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/Warranty/serviceHistoryDetails",
"style": {
"navigationBarTitleText": "售后详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/taskManage/index",
"style": {
"navigationBarTitleText": "任务管理",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/taskManage/taskItem",
"style": {
"navigationBarTitleText": "新增任务",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/taskManage/taskConfigList",
"style": {
"navigationBarTitleText": "选择任务",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/taskManage/patriarchSendObject",
"style": {
"navigationBarTitleText": "选择发送对象",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/awardManage/index",
"style": {
"navigationBarTitleText": "奖励管理",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/awardManage/awardItem",
"style": {
"navigationBarTitleText": "添加奖励",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/prefectInformation/index",
"style": {
"navigationBarTitleText": "完善信息",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/prefectInformation/cropper",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/mine/details/childrenDetails",
"style": {
"navigationBarTitleText": "账号详情",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/mine/details/deviceDetails",
"style": {
"navigationBarTitleText": "设备详情",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/mine/inviteBind/index",
"style": {
"navigationBarTitleText": "邀请绑定",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/home/bindDevice/index",
"style": {
"navigationBarTitleText": "扫码绑定",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/bindDevice/applyForBinding",
"style": {
"navigationBarTitleText": "申请绑定",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/bindDevice/bindChildAccount",
"style": {
"navigationBarTitleText": "绑定申请",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/bindDevice/familyRelationships",
"style": {
"navigationBarTitleText": "亲子关系",
"enablePullDownRefresh": false
}
},
{
"path": "pages/mine/inviteBind/relationShips",
"style": {
"navigationBarTitleText": "亲子关系",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/TimeManagement/index",
"style": {
"navigationBarTitleText": "时间管控",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/DesktopPreview/index",
"style": {
"navigationBarTitleText": "设备截屏",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/DesktopPreview/historyScreenShot",
"style": {
"navigationBarTitleText": "历史截屏",
"enablePullDownRefresh": false
}
},
{
"path": "pages/home/LockScreen/index",
"style": {
"navigationBarTitleText": "一键锁屏",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/personalData/index",
"style": {
"navigationBarTitleText": "个人资料",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/personalData/personalInformation",
"style": {
"navigationBarTitleText": "个人简介",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/personalData/editPhone",
"style": {
"navigationBarTitleText": "修改手机号",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/personalData/certificateView",
"style": {
"navigationBarTitleText": "资质证书",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/personalData/certificateAdd",
"style": {
"navigationBarTitleText": "资质证书",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/personalData/cropper",
"style": {
"navigationBarTitleText": "头像裁剪",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/personalData/subject",
"style": {
"navigationBarTitleText": "擅长科目",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/schedule/index",
"style": {
"navigationBarTitleText": "课表",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/schedule/condition",
"style": {
"navigationBarTitleText": "督学时况",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/chat/index",
"style": {
"navigationBarTitleText": "沟通",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/chat/chatDetails",
"style": {
"navigationBarTitleText": "沟通",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/mine/index",
"style": {
"navigationBarTitleText": "我的",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/student/index",
"style": {
"navigationBarTitleText": "学员",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/student-detail/index",
"style": {
"navigationBarTitleText": "学员详情",
"enablePullDownRefresh": true
}
},
{
"path": "pages/inspector/releasePlan/index",
"style": {
"navigationBarTitleText": "发布计划",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/releasePlan/selectPlan",
"style": {
"navigationBarTitleText": "选择任务",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/releasePlan/selectBook",
"style": {
"navigationBarTitleText": "选择教材",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/releasePlan/selectChapter",
"style": {
"navigationBarTitleText": "选择章节",
"enablePullDownRefresh": false
}
},
{
"path": "pages/inspector/releasePlan/selectVideo",
"style": {
"navigationBarTitleText": "选择视频",
"enablePullDownRefresh": false
}
},
{
"path": "pages/parents/mySupervisionService/index",
"style": {
"navigationBarTitleText": "我的督学服务"
}
},
{
"path": "pages/parents/mySupervisionService/studyPlan",
"style": {
"navigationBarTitleText": "查看学习计划"
}
},
{
"path": "pages/parents/supervisionService/index",
"style": {
"navigationBarTitleText": "督学服务"
}
},
{
"path": "pages/parents/supervisionService/buyService",
"style": {
"navigationBarTitleText": "督学服务"
}
},
{
"path": "pages/parents/supervisionService/serviceDetail",
"style": {
"navigationBarTitleText": "督学服务"
}
}
/*==============END=======================*/
],
"tabBar": {
"color": "#333333",
"selectedColor": "#333333",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": []
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "学小乐",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom"
},
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^qy-(.*)": "~@/components/$1/index.vue",
"^svg-(.*)": "~@/components/svgs/$1/index.vue",
"^svgs-(.*)": "~@/components/svgs/svgs-$1.vue",
"^isp-(.*)": "~@/components/inspector/isp-$1.vue"
}
}
}

View File

@ -0,0 +1,83 @@
<template>
<view class="charts-box" :style="{ width: width, height: height }">
<qiun-data-charts
type="bar"
:canvas2d="canvas2d"
:opts="opts"
:tooltipShow="false"
:chartData="_chartData"
/>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
interface SeriesType {
data: { color?: string; value: number }[];
}
interface ChartDataType {
categories: string[];
series: SeriesType[];
}
const props = withDefaults(
defineProps<{
width?: string;
height?: string;
chartData: ChartDataType;
}>(),
{ height: '420rpx', categories: () => [] },
);
const canvas2d = ref(true);
const _chartData = ref();
const opts = ref({
padding: [0, 42, 0, 0],
enableScroll: false,
legend: {
position: 'top',
padding: 2,
show: false,
},
xAxis: {
boundaryGap: 'justify',
disableGrid: false,
min: 0,
max: 10,
axisLine: false,
type: 'grid', // X
gridType: 'dash', // X线
gridColor: '#d4d4d4',
splitNumber: 10,
format: 'xAixsHours',
},
yAxis: {},
extra: {
bar: {
type: 'group',
width: 25,
meterBorde: 1,
meterFillColor: '#FFFFFF',
activeBgColor: '#000000',
activeBgOpacity: 0.05,
barBorderCircle: false,
seriesGap: 2,
categoryGap: 2,
},
},
});
watch(
() => props.chartData,
val => {
_chartData.value = val;
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="scss" scoped>
.charts-box {
width: 100%;
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<div class="charts-box" :style="{ width: width, height: height }">
<qiun-data-charts
type="column"
:canvas2d="canvas2d"
:opts="opts"
:tooltipShow="false"
:chartData="chartData"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface SeriesType {
data: { color?: string; value: number }[];
}
interface ChartDataType {
categories: string[];
series: SeriesType[];
}
const props = withDefaults(
defineProps<{
width?: string;
height?: string;
chartData: ChartDataType;
}>(),
{ height: '440rpx', categories: () => [] },
);
const canvas2d = ref(true);
const chartData = ref(props.chartData);
const opts = ref({
padding: [20, 25, 0, 0],
enableScroll: false,
legend: {
show: false,
},
xAxis: {
disableGrid: true,
},
yAxis: {
// format: 'integerFormat',
},
extra: {
column: {
type: 'group',
width: 30,
activeBgColor: '#000000',
activeBgOpacity: 0.05,
barBorderCircle: false,
seriesGap: 5,
linearOpacity: 0.5,
},
},
});
</script>
<style scoped>
/* 请根据实际需求修改父元素尺寸,组件自动识别宽高 */
.charts-box {
width: 100%;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<view class="charts-box" :style="{ width: width, height: height }">
<qiun-data-charts
type="line"
canvas2d
:opts="opts"
:chartData="chartData"
ontouch
tooltip-format="tooltipExer"
/>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface SeriesType {
name: string;
date: any[];
color?: string;
}
interface ChartDataType {
categories: any[];
series: SeriesType[];
}
const props = withDefaults(
defineProps<{
width?: string;
height?: string;
chartData: any;
tooltipFormatType?: string;
}>(),
{ height: '420rpx', categories: () => [] },
);
const chartData = ref(props.chartData);
const opts = ref({
padding: [15, 20, 0, 15],
enableScroll: false,
dataLabel: false,
dataPointShape: false,
legend: { position: 'top', padding: 2 },
xAxis: {
disableGrid: true,
labelCount: 6, //
format: 'xAixsHoursMin',
},
yAxis: {
gridType: 'dash',
dashLength: 2,
calibration: true,
},
extra: {
line: {
type: 'curve',
width: 2,
activeType: 'none',
},
tooltip: {
bgColor: '#21D17A',
fontColor: '#fff',
bgOpacity: 1,
borderRadius: 8,
gridColor: '#21D17A',
labelFontColor: '#333',
legendShow: false,
borderOpacity: 1,
labelBgOpacity: 1,
},
},
});
</script>
<style scoped>
/* 请根据实际需求修改父元素尺寸,组件自动识别宽高 */
.charts-box {
width: 100%;
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<view class="charts-box">
<qiun-data-charts type="ring" canvas2d :opts="opts" :chartData="chartData" />
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { onReady } from '@dcloudio/uni-app';
import { watch } from 'vue';
interface SeriesType {
data: any[];
}
interface ChartDataType {
categories?: string[];
series: SeriesType[];
}
const props = withDefaults(
defineProps<{
width?: string;
height?: string;
chartData: ChartDataType;
showLegend?: boolean;
title?: string;
subtitle?: string;
}>(),
{ height: '420rpx', categories: () => [], showLegend: true, title: '', subtitle: '' },
);
const chartData = ref(props.chartData);
const opts = ref({
rotate: false,
rotateLock: false,
padding: [0, 0, 0, 0],
dataLabel: true,
enableScroll: false,
legend: {
show: props.showLegend,
position: 'bottom',
lineHeight: 16,
},
title: {
name: props.title,
fontSize: 15,
color: 'rgba(102, 102, 102, 1)',
},
subtitle: {
name: '',
fontSize: 25,
color: 'rgba(51, 51, 51, 1)',
},
extra: {
ring: {
ringWidth: 60,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: '#FFFFFF',
},
},
});
watch(
() => props.subtitle,
val => {
if (val) {
opts.value.subtitle.name = val;
} else {
opts.value.subtitle.name = props.title ? '0%' : '';
}
},
{
immediate: true,
},
);
</script>
<style lang="scss" scoped>
.charts-box {
width: 100%;
height: 500rpx;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<view class="single_box children_box">
<view class="children">
<view class="left">
<image
:src="showChildren?.childAvatar ? showChildren?.childAvatar : defaultAvatar"
mode=""
class="children_avatar"
></image>
<view>
<view class="name_vip">
<text class="name">{{
showChildren?.childName ? showChildren?.childName : '未绑定账号'
}}</text>
<image
v-if="showChildren?.currentLevel"
:src="`${OSS_URL}/iconfont/VIP/VIP_${showChildren?.currentLevel}.png`"
class="vip"
/>
</view>
<view class="time">
<template v-if="showChildren?.vipEndTime">
有效期至{{
showChildren?.vipEndTime
? dayjs(showChildren?.vipEndTime).format('YYYY.MM.DD')
: '-'
}}
</template>
<template v-else>请先绑定孩子账号</template>
</view>
</view>
</view>
<view v-if="list.length > 1">
<image
:class="{ icon: true, rotate: showAllChildren }"
:src="arrow"
@click="showAllChildren = !showAllChildren"
/>
</view>
</view>
<template v-if="showAllChildren">
<view
v-for="(i, idx) in otherChildrenList"
:key="idx"
class="children"
@click="handleShowChildren(i)"
>
<view class="left">
<image
:src="i.childAvatar ? i.childAvatar : defaultAvatar"
mode=""
class="children_avatar"
></image>
<view>
<view class="name_vip">
<text class="name">{{ i.childName }}</text>
<image
v-if="i.currentLevel"
:src="`${OSS_URL}/iconfont/VIP/VIP_${i.currentLevel}.png`"
class="vip"
/>
</view>
<view class="time">
有效期至{{ i.vipEndTime ? dayjs(i.vipEndTime).format('YYYY.MM.DD') : '-' }}
</view>
</view>
</view>
<image
class="icon checked_icon"
:src="
checkedId === i.childId
? `${OSS_URL}/iconfont/checked.png`
: `${OSS_URL}/iconfont/unchecked.png`
"
/>
</view>
</template>
<view class="children_info">
<view class="item">
年级<text class="level">{{ showChildren?.gradeName || '-' }}</text>
</view>
<view class="item">
等级<text class="level">{{ showChildren?.expLevel || '-' }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import type { ChildrenType } from '@/pages/mine/interface';
import '../index.scss';
import dayjs from 'dayjs';
const props = withDefaults(
defineProps<{
list: ChildrenType[];
showItem: ChildrenType;
}>(),
{
list: () => [],
showItem: () => ({}) as ChildrenType,
},
);
const emits = defineEmits(['updateShowItem']);
const otherChildrenList = computed(() =>
props.list.filter(i => i.childId !== props.showItem.childId),
);
const showChildren = computed({
get() {
return props.showItem;
},
set(newVal) {
emits('updateShowItem', newVal);
},
});
const OSS_URL = import.meta.env.VITE_OSS_HOST;
const arrow = `${OSS_URL}/iconfont/down_arrow.png`;
const defaultAvatar = `${OSS_URL}/urm/default_avatar.png`;
const showAllChildren = ref(false);
const checkedId = ref<string>();
function handleShowChildren(i: ChildrenType) {
checkedId.value = i.childId;
setTimeout(() => {
showChildren.value = i;
}, 200);
}
</script>

View File

@ -0,0 +1,87 @@
<template>
<view class="date_filter">
<text
v-for="(i, idx) in list"
:key="idx"
:class="{ label: true, active_label: i.value === selectVal }"
@click="handleFilter(i)"
>
{{ i.label }}
</text>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
export interface selectType {
value: string | number;
label: string | number;
}
const props = withDefaults(
defineProps<{
list?: selectType[]; // 0- 1- 2-30 3-
dateType?: number | string;
}>(),
{
list: () => [
{ label: '今天', value: 0 },
{ label: '近一周', value: 1 },
{ label: '近一月', value: 2 },
{ label: '总计', value: 3 },
],
},
);
const emits = defineEmits(['filter']);
const selectVal = ref<number | string>();
function handleFilter(i: any) {
emits('filter', i);
selectVal.value = i.value;
}
watch(
() => props.dateType,
val => {
selectVal.value = val;
},
);
watch(
() => props.list,
val => {
if (val) {
selectVal.value = val[0].value;
}
},
{
immediate: true,
},
);
</script>
<style lang="scss" scoped>
.date_filter {
.label {
display: inline-block;
width: 108rpx;
height: 52rpx;
background-color: #f7f8fa;
font-size: 26rpx;
line-height: 52rpx;
text-align: center;
color: #666666;
box-sizing: border-box;
border-right: 1rpx solid #d9e1f2;
&:first-child {
border-radius: 12rpx 0 0 12rpx;
}
&:last-child {
border-radius: 0 12rpx 12rpx 0;
border-right: none;
}
}
.active_label {
background-color: #615dff;
color: #ffffff;
}
}
</style>

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