king 2 years ago
commit 4556afa855
  1. 15
      .editorconfig
  2. 2
      .env
  3. 7
      .gitignore
  4. 8
      .idea/.gitignore
  5. 7
      .idea/inspectionProfiles/Project_Default.xml
  6. 8
      .idea/modules.xml
  7. 6
      .idea/vcs.xml
  8. 9
      .idea/video.iml
  9. 11
      babel.config.js
  10. 9
      config/dev.js
  11. 82
      config/index.js
  12. 37
      config/prod.js
  13. 89
      package.json
  14. 13147
      pnpm-lock.yaml
  15. 15
      project.config.json
  16. 13
      project.tt.json
  17. 62
      src/api/curriculum.ts
  18. 2
      src/api/index.ts
  19. 123
      src/api/manage.ts
  20. 43
      src/api/public.ts
  21. 105
      src/api/request.ts
  22. 53
      src/api/user.ts
  23. 52
      src/app.config.ts
  24. 330
      src/app.scss
  25. 72
      src/app.tsx
  26. 25
      src/components/collapse/collapse.module.scss
  27. 29
      src/components/collapse/collapse.tsx
  28. 123
      src/components/icon/icon.scss
  29. 228
      src/components/icon/index.tsx
  30. 34
      src/components/icon/list.tsx
  31. 36
      src/components/loading/index.module.scss
  32. 27
      src/components/loading/index.tsx
  33. 65
      src/components/popPut/popPut.tsx
  34. 55
      src/components/tabs/tabs.scss
  35. 64
      src/components/tabs/tabs.tsx
  36. 23
      src/components/video/type.ts
  37. 63
      src/components/video/video.tsx
  38. 68
      src/components/videoCover/videoCover.scss
  39. 46
      src/components/videoCover/videoCover.tsx
  40. 17
      src/index.html
  41. 3
      src/pages/business/course/course.config.ts
  42. 75
      src/pages/business/course/course.tsx
  43. 3
      src/pages/business/userInfo/userInfo.config.ts
  44. 22
      src/pages/business/userInfo/userInfo.module.scss
  45. 80
      src/pages/business/userInfo/userInfo.tsx
  46. 75
      src/pages/business/videoInfo/components/ContainDeps.tsx
  47. 91
      src/pages/business/videoInfo/components/catalogue.tsx
  48. 33
      src/pages/business/videoInfo/components/hours.tsx
  49. 3
      src/pages/business/videoInfo/videoInfo.config.ts
  50. 50
      src/pages/business/videoInfo/videoInfo.scss
  51. 49
      src/pages/business/videoInfo/videoInfo.tsx
  52. 15
      src/pages/index/components/search.tsx
  53. 52
      src/pages/index/components/videoList.tsx
  54. 3
      src/pages/index/index.config.ts
  55. 43
      src/pages/index/index.module.scss
  56. 40
      src/pages/index/index.tsx
  57. 3
      src/pages/login/login.config.ts
  58. 66
      src/pages/login/login.module.scss
  59. 157
      src/pages/login/login.tsx
  60. 3
      src/pages/manage/addCur/addCur.config.ts
  61. 137
      src/pages/manage/addCur/addCur.tsx
  62. 3
      src/pages/manage/addStudent/addStudent.config.ts
  63. 19
      src/pages/manage/addStudent/addStudent.scss
  64. 170
      src/pages/manage/addStudent/addStudent.tsx
  65. 3
      src/pages/manage/college/college.config.ts
  66. 11
      src/pages/manage/college/college.scss
  67. 39
      src/pages/manage/college/college.tsx
  68. 3
      src/pages/manage/curAdmin/curAdmin.config.ts
  69. 10
      src/pages/manage/curAdmin/curAdmin.tsx
  70. 3
      src/pages/manage/curriculum/curriculum.config.ts
  71. 11
      src/pages/manage/curriculum/curriculum.module.scss
  72. 66
      src/pages/manage/curriculum/curriculum.tsx
  73. 3
      src/pages/manage/depAdmin/depAdmin.config.ts
  74. 158
      src/pages/manage/depAdmin/depAdmin.tsx
  75. 4
      src/pages/manage/depCur/depCur.config.ts
  76. 103
      src/pages/manage/depCur/depCur.tsx
  77. 48
      src/pages/manage/studentAdmin/student.scss
  78. 3
      src/pages/manage/studentAdmin/studentAdmin.config.ts
  79. 140
      src/pages/manage/studentAdmin/studentAdmin.tsx
  80. 4
      src/pages/manage/studentRecord/studentRecord.config.ts
  81. 102
      src/pages/manage/studentRecord/studentRecord.tsx
  82. 24
      src/pages/my/components/header/header.tsx
  83. 60
      src/pages/my/components/header/service.tsx
  84. 66
      src/pages/my/components/header/time.tsx
  85. 3
      src/pages/my/my.config.ts
  86. 87
      src/pages/my/my.module.scss
  87. 29
      src/pages/my/my.tsx
  88. 53
      src/static/css/module.scss
  89. BIN
      src/static/img/avatar.png
  90. BIN
      src/static/img/buy.png
  91. BIN
      src/static/img/cur.png
  92. BIN
      src/static/img/curriculum1.png
  93. BIN
      src/static/img/curriculum2.png
  94. BIN
      src/static/img/dep.png
  95. BIN
      src/static/img/incomplete.png
  96. BIN
      src/static/img/over.png
  97. BIN
      src/static/img/play-ok.png
  98. BIN
      src/static/img/play.png
  99. BIN
      src/static/img/student.png
  100. BIN
      src/static/img/time1.png
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

@ -0,0 +1,2 @@
#TARO_APP_API=http://192.168.1.19:9898
TARO_APP_API=https://yjx.dev.yaojiankang.top

7
.gitignore vendored

@ -0,0 +1,7 @@
dist/
deploy_versions/
.temp/
.rn_temp/
node_modules/
.DS_Store
.swc

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/video.iml" filepath="$PROJECT_DIR$/.idea/video.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,11 @@
// babel-preset-taro 更多选项和默认值:
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
module.exports = {
presets: [
['taro', {
framework: 'react',
ts: true
}]
],
plugins: []
}

@ -0,0 +1,9 @@
module.exports = {
env: {
NODE_ENV: '"development"'
},
defineConstants: {
},
mini: {},
h5: {}
}

@ -0,0 +1,82 @@
const path = require('path')
const config = {
projectName: 'video',
date: '2023-6-29',
designWidth: 750,
deviceRatio: {
640: 2.34 / 2,
750: 1,
828: 1.81 / 2,
375: 2 / 1
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: [],
defineConstants: {},
copy: {
patterns: [],
options: {}
},
framework: 'react',
compiler: 'webpack5',
cache: {
enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
},
sass: {},
alias: {
"@": path.resolve(__dirname, '..', 'src')
},
mini: {
postcss: {
pxtransform: {
enable: true,
config: {
selectorBlackList: ['nut-']
}
},
url: {
enable: true,
config: {
limit: 1024 // 设定转换尺寸上限
}
},
cssModules: {
enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
},
h5: {
publicPath: '/',
staticDirectory: 'static',
postcss: {
pxtransform: {
enable: true,
config: {
selectorBlackList: ['nut-']
}
},
autoprefixer: {
enable: true,
config: {}
},
cssModules: {
enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
}
}
module.exports = function (merge) {
if (process.env.NODE_ENV === 'development') {
return merge({}, config, require('./dev'))
}
return merge({}, config, require('./prod'))
}

@ -0,0 +1,37 @@
module.exports = {
env: {
NODE_ENV: '"production"'
},
defineConstants: {
},
mini: {},
h5: {
/**
* WebpackChain 插件配置
* @docs https://github.com/neutrinojs/webpack-chain
*/
// webpackChain (chain) {
// /**
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
// */
// chain.plugin('analyzer')
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
// /**
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
// */
// const path = require('path')
// const Prerender = require('prerender-spa-plugin')
// const staticDir = path.join(__dirname, '..', 'dist')
// chain
// .plugin('prerender')
// .use(new Prerender({
// staticDir,
// routes: [ '/pages/index/index' ],
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
// }))
// }
}
}

@ -0,0 +1,89 @@
{
"name": "video",
"version": "1.0.0",
"private": true,
"description": "",
"templateInfo": {
"name": "react-NutUI",
"typescript": true,
"css": "sass"
},
"scripts": {
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"build:qq": "taro build --type qq",
"build:jd": "taro build --type jd",
"build:quickapp": "taro build --type quickapp",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch"
},
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"author": "",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.7.7",
"@tarojs/components": "3.6.8",
"@tarojs/helper": "3.6.8",
"@tarojs/plugin-framework-react": "3.6.8",
"@tarojs/plugin-platform-alipay": "3.6.8",
"@tarojs/plugin-platform-h5": "3.6.8",
"@tarojs/plugin-platform-jd": "3.6.8",
"@tarojs/plugin-platform-qq": "3.6.8",
"@tarojs/plugin-platform-swan": "3.6.8",
"@tarojs/plugin-platform-tt": "3.6.8",
"@tarojs/plugin-platform-weapp": "3.6.8",
"@tarojs/react": "3.6.8",
"@tarojs/runtime": "3.6.8",
"@tarojs/shared": "3.6.8",
"@tarojs/taro": "3.6.8",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-refresh": "^0.11.0",
"unstated-next": "^1.1.0"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@tarojs/cli": "3.6.8",
"@tarojs/taro-loader": "3.6.8",
"@tarojs/webpack5-runner": "3.6.8",
"@types/node": "^18.15.11",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-router-dom": "^5.1.7",
"@types/react-syntax-highlighter": "^13.5.2",
"@types/react-test-renderer": "^18.0.0",
"@types/react-transition-group": "^4.4.4",
"@types/webpack-env": "^1.13.6",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"babel-plugin-import": "^1.13.3",
"babel-preset-taro": "3.6.8",
"eslint": "^8.12.0",
"eslint-config-taro": "3.6.8",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-react": "^7.8.2",
"eslint-plugin-react-hooks": "^4.2.0",
"postcss": "^8.4.18",
"style-loader": "1.3.0",
"stylelint": "^14.4.0",
"ts-node": "^10.9.1",
"typescript": "^4.1.0",
"webpack": "^5.78.0"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
{
"miniprogramRoot": "./dist",
"projectname": "video",
"description": "",
"appid": "wx703940a70f0f1be7",
"setting": {
"urlCheck": true,
"es6": false,
"enhance": false,
"compileHotReLoad": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram"
}

@ -0,0 +1,13 @@
{
"miniprogramRoot": "Progress/",
"projectname": "video",
"description": "",
"appid": "wx703940a70f0f1be7",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram"
}

@ -0,0 +1,62 @@
import {request} from "@/api/request";
export interface RecordData {
key: string
value: number
}
export interface CourseDepData {
chapters: Chapters[]
course: Curriculum
hours: Hours
/** 是否必修 */
is_required: boolean
learn_hour_records: LearnHourRecords
learn_record: LearnRecord | null
}
export interface HourPlayData {
/** 时间 */
duration: number
/** 格式 */
extension: string
/** 地址 */
url: string
}
export interface Course {
courses: Curriculum[]
learn_course_records: unknown
user_course_hour_count: unknown
stats: CueStats
}
export const curriculum = {
use() {
return request<Manage[]>('/api/v1/user/all', 'GET')
},
/** 学习记录 */
record(id: number) {
return request<RecordData[]>(`/api/v1/user/record/${id}`, "GET")
},
/** 查看课程课时数据 */
courseDep(id: number, depId: number | null) {
return request<CourseDepData>(`/api/v1/course/${id}${depId ? `/dep/${depId}` : ''}`, "GET")
},
hourPlay(courseId: number, id: number) {
return request<HourPlayData>(`/api/v1/course/${courseId}/hour/${id}/play`, "GET")
},
/** 删除课程 */
delCur(dep_id: number, course_id: number) {
return request(`/api/v1/department/assign/${dep_id}?course_id=${course_id}`, "DELETE")
},
/** 部门课程进度 */
course() {
return request<Course>(`/api/v1/user/courses`, "GET")
},
/** 课程结束 */
curEnd(courseId: number, id: number, duration: number) {
return request(`/api/v1/course/${courseId}/hour/${id}/record`, "POST", {duration})
}
}

@ -0,0 +1,2 @@
export * from './user'
export * from './curriculum'

@ -0,0 +1,123 @@
import {request} from "@/api/request";
export interface Student {
company_id:number
avatar: string
dep_ids: number[]
email: string
id_card: string
name: string
password: string
phone_number: string
}
interface UserInfoData {
dep_ids: number[]
user: Student
}
interface setRoleTypeData {
/** 当前超级管理员ID */
auth_id: number
/** 管理员权限,0 为员工,1为管理员,2为超级管理员 */
role_type: number
}
type DepList = Record<number, Manage[]>
export interface AddDepProps {
id?: number | null
name: string
parent_id: number
company_id: number
sort: number
}
interface DepCurData {
data: Curriculum[]
total: number
}
export interface depCurProps {
page: number
size: number
id: number
}
interface AddCurProps {
course_id: number[]
dep_id: number[]
is_required: 1 | 0
}
export interface CurLearningRecord {
course: Curriculum
data: User[]
/** 部门 */
departments: Record<number, string>
/** 第一次记录 */
user_course_hour_user_first_at: Record<string, unknown>
/** 学习记录 */
user_course_records: Record<string, LearnRecord>
/** 学员部门 */
user_dep_ids: Record<number, number[]>
total: number
}
export const ManageApi = {
/** 添加学员 */
addUser(data: Student) {
return request('/api/v1/user/create', "POST", data)
},
userInfo(id: number) {
return request<UserInfoData>(`/api/v1/user/${id}`, "GET")
},
putUser(id: number, data: Student) {
return request(`/api/v1/user/front/${id}`, "PUT", data)
},
del(id: number) {
return request(`/api/v1/user/${id}`, "DELETE")
},
/** 修改学员类型 */
setRoleType(id: number, data: setRoleTypeData) {
return request(`/api/v1/user/${id}`, "PUT", data)
},
/** 部门 */
depList() {
return request<DepList>('/api/v1/department/index', "GET")
},
addDep(data: AddDepProps) {
return request('/api/v1/department/save', "POST", data)
},
putDep(data: AddDepProps) {
return request(`/api/v1/department/${data.id}`, "PUT", data)
},
delDep(id: number) {
return request(`/api/v1/department/${id}`, "DELETE")
},
/** 部门课程 */
depCur(data: depCurProps) {
return request<DepCurData>(`/api/v1/department/${data.id}/courses?page=${data.page}&size=${data.size}`, "GET")
},
/** 未添加课程 */
optionalCur(dep_id: number, category_id: number) {
return request<Curriculum[]>(`/api/v1/department/${dep_id}/optional?category_id=${category_id}`, "GET")
},
addCur(data: AddCurProps) {
return request('/api/v1/course/user', "POST", data)
},
buyAll() {
return request<Curriculum[]>('/api/v1/course/buy/all', "GET")
},
buy(data_list: number[]) {
return request(`/api/v1/course/buy?data_list=${data_list}`, "POST")
},
/** 课程绑定部门 */
bingDep(dep_id: number) {
return request<Curriculum & { department: Department[] }>(`/api/v1/course/all/bind/${dep_id}`, "GET")
},
/** 课程学员学习记录 */
curLearningRecord(cur_id: number | string,data:{page:number,size:number}) {
return request<CurLearningRecord>(`/api/v1/course/${cur_id}/user/index`, "GET", data)
}
}

@ -0,0 +1,43 @@
import {request} from "@/api/request";
export interface Category {
id: number
name: string
parent_chain: string
parent_id: number
sort: number
}
interface CategoryList {
categories: Record<number, Category[]>
}
export interface Courses {
/** 已完成 */
is_finished: Curriculum[]
/** 未完成 */
is_not_finished: Curriculum[]
/** 选秀 */
is_not_required: Curriculum[]
/** 必修 */
is_required: Curriculum[]
}
export type CoursesKey = keyof Courses
export type Cur = Category & {
courses: Courses
name: string
resourceCategory?: Cur[]
}
export const publicApi = {
category() {
return request<CategoryList>('/api/v1/category/all', "GET")
},
/** 课程 */
curs() {
return request<Cur[]>('/api/v1/category/index', "GET")
}
}

@ -0,0 +1,105 @@
import Taro from "@tarojs/taro";
interface Method {
/** HTTP 请求 OPTIONS */
OPTIONS
/** HTTP 请求 GET */
GET
/** HTTP 请求 HEAD */
HEAD
/** HTTP 请求 POST */
POST
/** HTTP 请求 PUT */
PUT
/** HTTP 请求 PATCH */
PATCH
/** HTTP 请求 DELETE */
DELETE
/** HTTP 请求 TRACE */
TRACE
/** HTTP 请求 CONNECT */
CONNECT
}
/** 请求不成功各种状态的错误 */
export const ERROR_STATUS: Record<number | string, string> = {
'400': '400: 语法错误~',
'401': '401: 未授权~',
'403': '403: 拒绝访问~',
'404': '404: 资源不存在~',
'405': '405: 未允许~',
'429': '请求过于频繁',
'408': '408: 请求超时~',
'500': '500: 服务器错误',
'501': '501: 无请求功能~',
'502': '502: 错误网关~',
'503': '503: 服务不可用~',
'504': '504: 网关超时~',
'505': '505: http版本错误~',
'DEFAULT': '请求错误~',
'request:fail timeout': '请求超时~',
'NETWORK_ERROR': '网络不可用~',
'INVALID_DATA': '服务器响应异常~',
'OVERSTEP': '请求越界~'
}
export function request<T = unknown>(
url: string,
method: keyof Method,
data?: Record<string, any>
): Promise<T> {
const option: Taro.request.Option<T> = {
url: process.env.TARO_APP_API + url,
method: method,
dataType: 'json',
timeout: 30000,
header: {
'Content-Type': 'application/json;charset=UTF-8',
}
}
const token = JSON.parse(Taro.getStorageSync('profile') || '{}')?.token
if (token) {
option.header ??= {}
option.header['Authorization'] = `Bearer ${token}`
}
if (method === 'GET' && data) {
let parameter = ''
Object.entries(data).forEach(([key, value], index) => {
parameter += (index === 0 ? '?' : '&') + key + '=' + JSON.stringify(value)
})
option.url += parameter
}
data && (option.data = data)
return new Promise<T>((resolve, reject) => {
Taro.request<T>({
...option,
success(res) {
try {
const data = res.data as any
if (data?.code === 0 && res.statusCode === 200) {
resolve(data.data || [])
} else if (res.statusCode === 401) {
Taro.showModal({
title: "登录过期,需重新登陆",
showCancel: false,
success({confirm}) {
confirm && Taro.reLaunch({url: '/pages/login/login'})
}
})
} else {
Taro.showToast({title: data.msg || ERROR_STATUS[res.statusCode] || '请求错误~', icon: 'error'})
reject(null)
}
} catch (e) {
reject(null)
}
},
fail(err) {
const errMsg = err.errMsg
Taro.showToast({title: ERROR_STATUS[errMsg] || ERROR_STATUS['DEFAULT'], icon: 'error'})
reject(null)
}
})
})
}

@ -0,0 +1,53 @@
import {request} from "./request";
interface Login {
access_token: string
code?: {
image: string
}
company: Company
token: string
user: User
catch_key: string
}
interface CheckoutBody {
catch_key: string
code: string
phone_number: string
}
interface CheckoutData {
token: string
phone_number: string
user: User
company: Company
}
interface DepListData {
departments: Department[]
user: User
}
export const userApi = {
login(code: string) {
return request<Login>('/api/v1/auth/login/wechat', 'POST', {code})
},
checkout(data: CheckoutBody) {
return request<CheckoutData>('/api/v1/auth/login/checkout', 'POST', data)
},
unbind(id: number) {
return request(`/api/v1/auth/login/unbind/${id}`, "PUT")
},
putName(id: number, name: string) {
return request<User>(`/api/v1/auth/login/${id}`, "PUT", {name})
},
/** 所属部门 */
depList() {
return request<DepListData>(`/api/v1/user/detail`, "GET")
},
code(catch_key: string) {
return request(`/api/v1/auth/login/code`, "GET", {open_id:catch_key})
}
}

@ -0,0 +1,52 @@
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/login/login',
'pages/my/my'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '课程',
navigationBarTextStyle: 'black'
},
tabBar: {
list: [
{text: '课题', pagePath: 'pages/index/index'},
{text: "我的", pagePath: 'pages/my/my'}
]
},
preloadRule: {
'pages/index/index': {
network: 'all',
packages: ['pages/business']
},
'pages/my/my': {
network: 'all',
packages: ['pages/manage']
}
},
subpackages: [
{
root: 'pages/business',
pages: [
'course/course',
'userInfo/userInfo',
'videoInfo/videoInfo'
]
},
{
root: 'pages/manage',
pages: [
'depAdmin/depAdmin',
'studentAdmin/studentAdmin',
'college/college',
'curriculum/curriculum',
'addStudent/addStudent',
'depCur/depCur',
'addCur/addCur',
'studentRecord/studentRecord',
]
}
]
})

@ -0,0 +1,330 @@
@import "static/css/module";
.flex {display: flex !important;flex-direction:row}
.flex-row{ flex-direction:row!important}
.flex-column{ flex-direction:column!important}
.flex-row-reverse{ flex-direction:row-reverse!important}
.flex-column-reverse{ flex-direction:column-reverse!important}
.flex-wrap{ flex-wrap:wrap}
.flex-nowrap{ flex-wrap:nowrap}
.justify-start{justify-content:flex-start}
.justify-end{justify-content:flex-end}
.justify-around{justify-content:space-around}
.justify-between{justify-content:space-between}
.justify-center{justify-content:center}
.flex-wrap{flex-wrap:wrap}
.align-center{ align-items: center}
.align-stretch{ align-items: stretch}
.align-start{ align-items: flex-start}
.align-end{ align-items: flex-end}
.content-start {align-content: flex-start}
.content-end {align-content: flex-end}
.content-center {align-content: center}
.content-between {align-content: space-between}
.content-around {align-content: space-around}
.content-stretch {align-content: stretch}
.flex-1{flex: 1}
.flex-2{flex: 2}
.flex-3{flex: 3}
.flex-4{flex: 4}
.flex-5{flex: 5}
.flex-shrink{flex-shrink: 0}
.w-1 {width: 10%;min-width: 75rpx}
.w-2 {width: 20%;min-width: 150rpx}
.w-3 {width: 30%;min-width: 225rpx}
.w-4 {width: 40%;min-width: 300rpx}
.w-5 {width: 50%;min-width: 375rpx}
.w-6 {width: 60%;min-width: 450rpx}
.w-7 {width: 70%;min-width: 525rpx}
.w-8 {width: 80%;min-width: 600rpx}
.w-9 {width: 90%;min-width: 675rpx}
.w-10 {width: 100%;min-width: 750rpx}
.h-1 {height: 10vh}
.h-2 {height: 20vh}
.h-3 {height: 30vh}
.h-4 {height: 40vh}
.h-5 {height: 50vh}
.h-6 {height: 60vh}
.h-7 {height: 70vh}
.h-8 {height: 80vh}
.h-9 {height: 90vh}
.h-10 {height: 100vh}
.m-0 {margin: 0}
.m-auto{margin: auto}
.m-1 {margin: 10rpx}
.m-2 {margin: 20rpx}
.m-3 {margin: 30rpx}
.m-4 {margin: 40rpx}
.m-5 {margin: 50rpx}
.mt-0 {margin-top: 0}
.mt-1 {margin-top: 10rpx}
.mt-2 {margin-top: 20rpx}
.mt-3 {margin-top: 30rpx}
.mt-4 {margin-top: 40rpx}
.mt-5 {margin-top: 50rpx}
.mt-6 {margin-top: 60rpx}
.mt-7 {margin-top: 70rpx}
.mt-8 {margin-top: 80rpx}
.mt-9 {margin-top: 80rpx}
.mt-10 {margin-top: 100rpx}
.mt-12 {margin-top: 120rpx}
.mb-0 {margin-bottom: 0}
.mb-auto {margin-bottom: auto}
.mb-1 {margin-bottom: 10rpx}
.mb-1_5 {margin-bottom: 15rpx}
.mb-2 {margin-bottom: 20rpx}
.mb-3 {margin-bottom: 30rpx}
.mb-4 {margin-bottom: 40rpx}
.mb-5 {margin-bottom: 50rpx}
.mb-7 {margin-bottom: 70rpx}
.mb-9 {margin-bottom: 90rpx}
.mb-11 {margin-bottom: 110rpx}
.mb-12 {margin-bottom: 120rpx}
.mb-13 {margin-bottom: 130rpx}
.mb-14 {margin-bottom: 140rpx}
.mb-15 {margin-bottom: 150rpx}
.ml-0 {margin-left: 0}
.ml-auto {margin-left: auto}
.ml-0_6 {margin-left: 6rpx}
.ml-1 {margin-left: 10rpx}
.ml-2 {margin-left: 20rpx}
.ml-3 {margin-left: 30rpx}
.ml-4 {margin-left: 40rpx}
.ml-5 {margin-left: 50rpx}
.ml-8 {margin-left: 80rpx}
.ml-9 {margin-left: 90rpx}
.ml-12 {margin-left: 120rpx}
.mr-0 {margin-right: 0}
.mr-auto {margin-right: auto}
.mr-0_6 {margin-right: 6rpx}
.mr-1 {margin-right: 10rpx}
.mr-1_5 {margin-right: 15rpx}
.mr-2 {margin-right: 20rpx}
.mr-3 {margin-right: 30rpx}
.mr-4 {margin-right: 40rpx}
.mr-5 {margin-right: 50rpx}
.mr-6 {margin-right: 60rpx}
.mr-8 {margin-right: 80rpx}
.mr-9 {margin-right: 90rpx}
.mr-11 {margin-right:110rpx}
.my-0 {margin-top: 0;margin-bottom: 0}
.my-auto {margin-top: auto;margin-bottom: auto}
.my-1 {margin-top: 10rpx; margin-bottom: 10rpx}
.my-2 {margin-top: 20rpx; margin-bottom: 20rpx}
.my-3 {margin-top: 30rpx; margin-bottom: 30rpx}
.my-4 {margin-top: 40rpx; margin-bottom: 40rpx}
.my-5 {margin-top: 50rpx; margin-bottom: 50rpx}
.mx-0 { margin-left: 0; margin-right: 0;}
.mx-auto { margin-left: auto; margin-right: auto;}
.mx-1 { margin-left: 10rpx; margin-right: 10rpx;}
.mx-2 { margin-left: 20rpx; margin-right: 20rpx;}
.mx-3 { margin-left: 30rpx; margin-right: 30rpx;}
.mx-4 { margin-left: 40rpx; margin-right: 40rpx;}
.mx-5 { margin-left: 50rpx; margin-right: 50rpx;}
.p-0 { padding: 0; }
.p { padding: 5rpx; }
.p-1 { padding: 10rpx; }
.p-2 { padding: 20rpx; }
.p-3 { padding: 30rpx; }
.p-4 { padding: 40rpx; }
.p-5 { padding: 50rpx; }
.pt-0 { padding-top: 0; }
.pt { padding-top: 5rpx; }
.pt-1 { padding-top: 10rpx; }
.pt-2 { padding-top: 20rpx; }
.pt-3 { padding-top: 30rpx; }
.pt-4 { padding-top: 40rpx; }
.pt-5 { padding-top: 50rpx; }
.pt-6 { padding-top: 60rpx; }
.pt-7 { padding-top: 70rpx; }
.pt-8 { padding-top: 80rpx; }
.pt-9 { padding-top: 90rpx; }
.pt-10{ padding-top: 100rpx;}
.pt-11{ padding-top: 110rpx;}
.pb-0 { padding-bottom: 0; }
.pb-1 { padding-bottom: 10rpx; }
.pb { padding-bottom: 5rpx; }
.pb-2 { padding-bottom: 20rpx; }
.pb-3 { padding-bottom: 30rpx; }
.pb-4 { padding-bottom: 40rpx; }
.pb-5 { padding-bottom: 50rpx; }
.pl-0 { padding-left: 0; }
.pl { padding-left: 5rpx; }
.pl-1 { padding-left: 10rpx; }
.pl-2 { padding-left: 20rpx; }
.pl-3 { padding-left: 30rpx; }
.pl-3_5 { padding-left: 35rpx; }
.pl-4 { padding-left: 40rpx; }
.pl-5 { padding-left: 50rpx; }
.pl-6 { padding-left: 60rpx; }
.pl-7 { padding-left: 70rpx; }
.pr-0 { padding-right: 0; }
.pr { padding-right: 5rpx; }
.pr-1 { padding-right: 10rpx; }
.pr-2 { padding-right: 20rpx; }
.pr-3 { padding-right: 30rpx; }
.pr-4 { padding-right: 40rpx; }
.pr-5 { padding-right: 50rpx; }
.py-0 { padding-top: 0; padding-bottom: 0; }
.py { padding-top: 5rpx; padding-bottom: 5rpx; }
.py-1 { padding-top: 10rpx; padding-bottom: 10rpx; }
.py-1_5 { padding-top: 15rpx; padding-bottom: 15rpx; }
.py-2 { padding-top: 20rpx; padding-bottom: 20rpx; }
.py-3 { padding-top: 30rpx; padding-bottom: 30rpx; }
.py-4 { padding-top: 40rpx; padding-bottom: 40rpx; }
.py-5 { padding-top: 50rpx; padding-bottom: 50rpx; }
.py-6 { padding-top: 50rpx; padding-bottom: 60rpx; }
.py-7 { padding-top: 50rpx; padding-bottom: 70rpx; }
.py-8 { padding-top: 80rpx; padding-bottom: 80rpx; }
.py-9 { padding-top: 90rpx; padding-bottom: 90rpx; }
.px-0 { padding-left: 0; padding-right: 0; }
.px-1 { padding-left: 10rpx; padding-right: 10rpx;}
.px { padding-left: 5rpx; padding-right: 5rpx;}
.px-2 { padding-left: 20rpx; padding-right: 20rpx;}
.px-3 { padding-left: 30rpx; padding-right: 30rpx;}
.px-4 { padding-left: 40rpx; padding-right: 40rpx;}
.px-5 { padding-left: 50rpx; padding-right: 50rpx;}
.px-7 { padding-left: 70rpx; padding-right: 70rpx;}
.px-8 { padding-left: 70rpx; padding-right: 80rpx;}
.px-9 { padding-left: 90rpx; padding-right: 90rpx;}
.font-12{font-size: 12rpx;line-height: 1;}
.font-16{font-size: 16rpx;line-height: 1;}
.font-20{font-size:20rpx;line-height: 1;}
.font-24{font-size:24rpx;line-height: 1;}
.font-26{font-size:26rpx;line-height: 1;}
.font-28{font-size:28rpx;line-height: 1;}
.font-30{font-size:30rpx;line-height: 1;}
.font-32{font-size:32rpx;line-height: 1;}
.font-34{font-size:34rpx;line-height: 1;}
.font-36{font-size:36rpx;line-height: 1;}
.font-40{font-size: 40rpx;line-height: 1;}
.font-45{font-size: 45rpx;line-height: 1;}
.font-50{font-size: 50rpx;line-height: 1;}
.font-52{font-size: 52rpx;line-height: 1;}
.font-60{font-size: 60rpx;line-height: 1;}
.font-68{font-size: 68rpx;line-height: 1;}
.font-weight{font-weight: bold}
.text-indent{text-indent:2;}
.text-through{text-decoration:line-through;}
.text-left { text-align: left;}
.text-right { text-align: right;}
.text-center { text-align: center;}
.text-justify{ text-align: justify;}
.letspac3{ letter-spacing: 3px;}
.letspac5{ letter-spacing: 5px;}
/* 背景颜色 */
.bg-f2{background-color:#F2F2F2;}
.bg-danger { background-color: red;}
.bg-dark { background-color: #343a40;}
.bg-hover-dark { background-color: #1d2124;}
.bg-white { background-color: #ffffff;}
.bg-orange{ background-color: orange;}
.bg-gray{ background-color: gray;}
.bg-transparent { background-color: transparent;}
/* 文字颜色 */
.text-white {color: #ffffff }
.text-primary {color: #007bff;}
.text-hover-primary { color: #0056b3;}
.text-secondary {color: #6c757d;}
.text-hover-secondary { color: #494f54;}
.text-success {color: #28a745;}
.text-hover-success{color: #19692c;}
.text-info { color: #17a2b8;}
.text-hover-info {color: #0f6674;}
.text-warning {color: #FF9E5F;}
.text-danger { color: #dc3545;}
.text-hover-danger { color: #a71d2a;}
.text-light { color: #f8f9fa;}
.text-hover-light { color: #cbd3da;}
.text-dark { color: #343a40;}
.text-hover-dark{ color: #121416;}
.text-body { color: #212529;}
.text-muted { color: #6c757d;}
/* 圆角 */
.rounded { border-radius: 8rpx;}
.rounded-lg { border-radius: 14rpx;}
.rounded-10 { border-radius: 10rpx;}
.rounded-12 { border-radius: 12rpx;}
.rounded-15 { border-radius: 15rpx;}
.rounded-20 { border-radius: 20rpx;}
.rounded-30 { border-radius: 30rpx;}
.rounded-40 { border-radius: 40rpx;}
.rounded-50 { border-radius: 50rpx;}
.rounded-botm-30{border-bottom-left-radius: 30rpx;border-bottom-right-radius: 30rpx;}
.rounded-top-30 {border-top-left-radius: 30rpx;border-top-right-radius: 30rpx;}
.rounded-top {
border-top-left-radius: 8rpx;
border-top-right-radius: 8rpx;
}
.rounded-top-lg {
border-top-left-radius: 14rpx;
border-top-right-radius: 14rpx;
}
.rounded-right {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.rounded-bottom {
border-bottom-right-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.rounded-bottom-lg {
border-bottom-right-radius: 14rpx;
border-bottom-left-radius: 14rpx;
}
.rounded-bottom-lg40 {
border-bottom-right-radius: 40rpx;
border-bottom-left-radius: 40rpx;
}
.rounded-left-50{
border-top-left-radius: 50rpx;
border-bottom-left-radius: 50rpx;
}
.rounded-bottom-right-50{
border-bottom-right-radius: 50rpx;
}
.rounded-top-right-30{
border-top-right-radius: 30rpx;
}
.rounded-left {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.rounded-100 { border-radius: 100rpx;}
.rounded-0 { border-radius: 0;}
.border-none{border: none}

@ -0,0 +1,72 @@
import {useEffect} from 'react'
import Taro, {useDidShow, useDidHide} from '@tarojs/taro'
import './app.scss'
import {CustomWrapper} from "@tarojs/components";
function updateApp() {
if (Taro.canIUse('getUpdateManager')) {
const updateManager = Taro.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
console.log('新版本', res.hasUpdate)
})
updateManager.onUpdateReady(() => {
Taro.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(() => {
// 新版本下载失败
Taro.showToast({title: '新版本下载失败'})
})
}
}
function App(props) {
// 可以使用所有的 React Hooks
useEffect(() => {
updateApp()
const token = JSON.parse(Taro.getStorageSync('profile') || '{}')?.token
if (!token) {
Taro.switchTab({url: '/pages/login/login'})
}
}, [])
Taro.getSystemInfo({
success(res) {
Taro.getApp().globalData = {
statusBarHeight: res.statusBarHeight,
screenWidth: res.screenWidth,
screenHeight: res.screenHeight,
safeArea: res.safeArea,
}
}
})
// 对应 onShow
useDidShow(() => {
})
// 对应 onHide
useDidHide(() => {
})
return (
<CustomWrapper>
{props.children}
</CustomWrapper>
)
}
export default App

@ -0,0 +1,25 @@
.collapse {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
line-height: 84rpx;
background: #F5F8F7;
border-radius: 24rpx;
padding: 0 24rpx;
margin-top: 20rpx;
}
.children {
overflow: hidden;
max-height: 0;
transition: max-height 300ms ease;
}
.open {
max-height: 1000px !important;
}
.close {
max-height: 0 !important;
}

@ -0,0 +1,29 @@
import styles from './collapse.module.scss'
import {FC, useState} from "react";
import {View} from "@tarojs/components";
import Icon from "@/components/icon";
interface Props {
title: string
children?: JSX.Element | string
onChange?: () => void
}
const Collapse: FC<Props> = (props: Props) => {
const [show, setShow] = useState(false)
return (
<>
<View className={styles.collapse} onClick={() => setShow(!show)}>
<View>{props.title}</View>
<Icon name={show ? 'chevron-up' : 'chevron-down'}/>
</View>
<View className={`${styles.children} ${show ? styles.open : styles.close}`}>
{props?.children}
</View>
</>
)
}
export default Collapse

File diff suppressed because one or more lines are too long

@ -0,0 +1,228 @@
import {CSSProperties, FC} from "react";
import {Text} from "@tarojs/components";
import {ITouchEvent} from "@tarojs/components/types/common";
import './icon.scss'
export type IconName =
| 'add'
| 'add-circle'
| 'subtract'
| 'subtract-circle'
| 'align-center'
| 'align-left'
| 'align-right'
| 'arrow-down'
| 'arrow-left'
| 'arrow-right'
| 'arrow-up'
| 'bell'
| 'blocked'
| 'bookmark'
| 'bullet-list'
| 'calendar'
| 'camera'
| 'check-circle'
| 'chevron-down'
| 'chevron-left'
| 'chevron-right'
| 'chevron-up'
| 'clock'
| 'close-circle'
| 'close'
| 'credit-card'
| 'download-cloud'
| 'download'
| 'edit'
| 'equalizer'
| 'external-link'
| 'eye'
| 'file-audio'
| 'file-code'
| 'file-generic'
| 'file-jpg'
| 'file-new'
| 'file-png'
| 'file-svg'
| 'file-video'
| 'filter'
| 'folder'
| 'font-color'
| 'heart'
| 'help'
| 'home'
| 'image'
| 'iphone-x'
| 'iphone'
| 'lightning-bolt'
| 'link'
| 'list'
| 'lock'
| 'mail'
| 'map-pin'
| 'menu'
| 'message'
| 'money'
| 'next'
| 'numbered-list'
| 'pause'
| 'phone'
| 'play'
| 'playlist'
| 'prev'
| 'reload'
| 'repeat-play'
| 'search'
| 'settings'
| 'share-2'
| 'share'
| 'shopping-bag-2'
| 'shopping-bag'
| 'shopping-cart'
| 'shuffle-play'
| 'sketch'
| 'sound'
| 'star'
| 'stop'
| 'streaming'
| 'tag'
| 'tags'
| 'text-italic'
| 'text-strikethrough'
| 'text-underline'
| 'trash'
| 'upload'
| 'user'
| 'video'
| 'volume-minus'
| 'volume-off'
| 'volume-plus'
| 'analytics'
| 'star-2'
| 'check'
| 'heart-2'
| 'loading'
| 'loading-2'
| 'loading-3'
| 'alert-circle'
export const icons: IconName[] = [
'add',
'add-circle',
'subtract',
'subtract-circle',
'align-center',
'align-left',
'align-right',
'arrow-down',
'arrow-left',
'arrow-right',
'arrow-up',
'bell',
'blocked',
'bookmark',
'bullet-list',
'calendar',
'camera',
'check-circle',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up',
'clock',
'close-circle',
'close',
'credit-card',
'download-cloud',
'download',
'edit',
'equalizer',
'external-link',
'eye',
'file-audio',
'file-code',
'file-generic',
'file-jpg',
'file-new',
'file-png',
'file-svg',
'file-video',
'filter',
'folder',
'font-color',
'heart',
'help',
'home',
'image',
'iphone-x',
'iphone',
'lightning-bolt',
'link',
'list',
'lock',
'mail',
'map-pin',
'menu',
'message',
'money',
'next',
'numbered-list',
'pause',
'phone',
'play',
'playlist',
'prev',
'reload',
'repeat-play',
'search',
'settings',
'share-2',
'share',
'shopping-bag-2',
'shopping-bag',
'shopping-cart',
'shuffle-play',
'sketch',
'sound',
'star',
'stop',
'streaming',
'tag',
'tags',
'text-italic',
'text-strikethrough',
'text-underline',
'trash',
'upload',
'user',
'video',
'volume-minus',
'volume-off',
'volume-plus',
'analytics',
'star-2',
'check',
'heart-2',
'loading',
'loading-2',
'loading-3',
'alert-circle',
];
export interface IconProps {
name: IconName
size?: string | number
color?: string
onClick?: (event: ITouchEvent) => void
}
const Icon: FC<IconProps> = (props) => {
const size = typeof props.size === 'string' ? props.size : `${props.size ?? 16}px`
const color = props.color ?? 'currentColor'
const fontStyle: CSSProperties = {fontSize: size, color}
return (
<Text className={`icon icon-${props.name}`} style={fontStyle} onClick={props.onClick} />
)
}
export default Icon

@ -0,0 +1,34 @@
import {FC, useEffect, useState} from "react";
import {Input, Text, View} from "@tarojs/components";
import Icon, {icons, IconName} from './index'
const IconList: FC = () => {
const [list, setList] = useState<IconName[]>(icons)
const [keyword, setKeyword] = useState("")
useEffect(() => {
setList(() => {
if (!keyword) return [...icons]
return icons.filter(name => name.includes(keyword));
});
}, [keyword])
return (
<View>
<View>
<Text></Text>
<Input style={{border: '1px solid #ddd', marginBottom: "12px", padding: '8px'}} onInput={(e) => setKeyword(e.detail.value)}/>
</View>
<View style={{display:"flex", flexWrap:'wrap'}}>
{list.map((name) => (
<View style={{display: "flex", flexDirection: 'column', alignItems: "center", justifyContent: 'center', width: '100px', height: '100px', border: '1px solid #ddd'}}>
<Icon name={name} size={26}/>
<Text>{name}</Text>
</View>
))}
</View>
</View>
)
}
export default IconList

@ -0,0 +1,36 @@
.loading {
position: relative;
}
.background,
.foreground {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid currentColor;
}
.background {
z-index: 1;
opacity: 0.3;
}
.foreground {
z-index: 2;
border-color: currentColor transparent transparent transparent;
//animation: loading 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
animation: loading .5s linear infinite;
}
@keyframes loading {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@ -0,0 +1,27 @@
import {CSSProperties, FC} from "react";
import {View} from "@tarojs/components";
import styles from './index.module.scss'
interface LoadingProps {
size?: string | number
color?: string
}
const Loading: FC<LoadingProps> = (props) => {
const size = typeof props.size === 'string' ? props.size : `${props.size ?? 16}px`
const color = props.color ?? 'currentColor'
const sizeStyle: CSSProperties = {
width: size,
height: size,
color
}
return (
<View className={styles.loading} style={sizeStyle}>
<View className={styles.background} />
<View className={styles.foreground} />
</View>
)
}
export default Loading

@ -0,0 +1,65 @@
import {FC, ReactNode, useEffect, useState} from "react";
import {View, Image, Text, PageContainer} from "@tarojs/components";
import Icon from "@/components/icon";
interface Props {
height?: number | string
title: string
content?: string
chevron?: boolean
image?: string
isProp?: boolean
children?: ReactNode
show?: boolean
onClick?: () => void
no_border?: boolean
}
const PopPut: FC<Props> = ({title, chevron, content, image, isProp, children, show, ...opt}: Props) => {
const [PageShow, setShow] = useState(false)
useEffect(() => {
if (PageShow) {
setShow(false)
}
}, [show])
function click() {
setShow(true)
opt.onClick?.()
}
const style = (): string => {
let css = ''
if (opt.height) {
css += ` height:${opt.height}px;font-size:16px`
}
if (opt.no_border) {
css += ` border:none`
}
return css
}
return (
<>
<View className='card' onClick={click} style={style()}>
<View>{title}</View>
<View className='card-content'>
<Text>{content}</Text>
{!chevron && <Icon name='chevron-right'/>}
{image && <Image src={image} mode='scaleToFill'/>}
</View>
</View>
{
isProp
&& <PageContainer show={PageShow} position='bottom' round onBeforeLeave={() => setShow(false)}>
{children}
</PageContainer>
}
</>
)
}
export default PopPut

@ -0,0 +1,55 @@
View::-webkit-scrollbar {
display: none !important;
}
.tabs {
width: 100%;
height: 100%;
overflow: hidden;
.tabs-scroll {
white-space: nowrap;
position: relative;
display: -webkit-flex;
display: flex;
text-align: center;
}
.current {
position: relative;
font-weight: bold;
&:after {
position: absolute;
left: 50%;
bottom: 0;
width: 32rpx;
height: 6rpx;
background: #45D4A8;
border-radius: 12rpx;
opacity: 1;
content: '';
display: block;
animation: spread 200ms ease-out;
transform: translateX(-18rpx);
transition: all 200ms;
}
}
.tabs-item {
padding: 25rpx;
}
}
@keyframes spread {
from {
width: 0%;
transform: translateX(0);
}
to {
width: 32rpx;
transform: translateX(-18rpx);
}
}

@ -0,0 +1,64 @@
import {FC, useEffect, useState} from "react";
import './tabs.scss'
import {ScrollView, View} from "@tarojs/components";
import Taro from "@tarojs/taro";
export interface TabList<T = unknown> {
title: string
value?: number | string | Record<string, T> | T
[key: string]: any
}
export type OnChangOpt<T = unknown> = {
index: number,
tab?: { title: string, value?: number | string | Record<string, T> | T }
}
interface TabsProps {
current?: number | string
tabList: TabList[]
onChange?: (data: OnChangOpt) => void
}
const Tabs: FC<TabsProps> = (opt: TabsProps) => {
const {screenWidth} = Taro.getApp().globalData
const [current, setCurrent] = useState<number | string>(opt.current || 0)
const [left, setLeft] = useState(0)
useEffect(() => {
setCurrent(opt.current || 0)
}, [opt.current])
function onChange(event: any, index: number, tab: TabList) {
const offsetLeft = event.target.offsetLeft
setLeft(offsetLeft - screenWidth / 2)
setCurrent(index)
opt?.onChange?.({index, tab})
}
function is_current(value: unknown, index: number): boolean {
if (opt.current) {
return String(value) === String(opt.current)
}
return index === current
}
return (
<View className='tabs'>
<ScrollView scrollX scrollLeft={left} scrollWithAnimation showScrollbar={false}>
<View className={'tabs-scroll ' + (opt.tabList.length < 5 ? 'justify-around' : '')}>
{opt.tabList.map((d, index) => <View
className={'tabs-item ' + (is_current(d.value, index) ? 'current' : null)}
onClick={(event) => onChange(event, index, d)}>
{d.title}
</View>)}
</View>
</ScrollView>
</View>
)
}
export default Tabs

@ -0,0 +1,23 @@
interface Breakpoint {
id: number
/** 秒 */
time: number
}
export interface HVideoOptions {
/** 视频时长s */
duration: number
/** 是否预览 */
preview: boolean
/** 视频播放地址 */
src: string
/** 视频封面 */
poster?: string
/** 视频断点 */
breakpoint: Breakpoint[]
/** 进入断点 */
onBreakpoint: (id: number) => void
/** 视频播放结束 */
onEnded: () => void
}

@ -0,0 +1,63 @@
import {BaseEventOrig, Video, VideoProps} from "@tarojs/components";
import {HVideoOptions} from "@/components/video/type";
import Taro from "@tarojs/taro";
import {FC, useState} from "react";
import {Profile} from '@/store'
const deviation: number = 0.5
const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
const {user} = Profile.useContainer()
const video = Taro.createVideoContext('video')
const [currentTime, setCurrentTime] = useState(0)
function onTimeUpdate(event: BaseEventOrig<VideoProps.onTimeUpdateEventDetail>) {
if (opt.preview) return;
if (user?.role_type !== 0) return;
const time = event.detail.currentTime
/** 前进回退 */
if (currentTime + deviation < time) {
video.seek(currentTime)
return
}
setCurrentTime(time)
/** 判断是否进入断点 */
opt.breakpoint.forEach(d => {
if (time < d.time + deviation && time > d.time - deviation) {
opt.onBreakpoint(d.id)
video.pause()
video.seek(d.time - deviation)
return
}
})
}
function onEnded() {
console.log(user)
if (user?.role_type !== 0) return;
if (currentTime + 1 > opt.duration) {
opt.onEnded()
} else {
video.seek(currentTime)
video.play()
}
}
return (
<>
<Video
id='video'
style={'width:100%'}
poster={opt.poster}
src={opt.src}
enableProgressGesture={false}
direction={90}
onTimeUpdate={onTimeUpdate}
onEnded={onEnded}
/>
</>
)
}
export default HVideo

@ -0,0 +1,68 @@
.videoBox {
padding: 10px;
width: 50%;
box-sizing: border-box;
margin-bottom: 10px;
}
.video {
width: 100%;
background: #fff;
border-radius: 10rpx;
overflow: hidden;
position: relative;
.content {
position: absolute;
color: #fff;
left: 0;
top: 172rpx;
width: 100%;
line-height: 48rpx;
font-size: 24rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
background: rgba(#000, .5);
text-align: center;
}
.box {
padding: 20rpx;
.title{
width: 100%;
//word-break: break-word;
//white-space: pre-line;
font-size: 28rpx;
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.marker {
position: absolute;
background: rgba(#000, .5);
color: #fff;
padding: 0 10px;
border-radius: 0 0 0 10px;
top: 0;
right: 0;
}
Image {
width: 100%;
height: 220rpx;
display: block;
}
.videoButton{
margin-top: 20rpx;
color: #909795;
font-size: 22rpx;
}
}

@ -0,0 +1,46 @@
import {Image, View} from "@tarojs/components";
import {FC} from "react";
import './videoCover.scss'
import Taro from "@tarojs/taro";
interface VideoCoverProps {
thumb: string
title: string | JSX.Element
/** 右上角标签 */
marker?: string | JSX.Element
content?: string | JSX.Element
id: number
/** 课程id */
depId: number | null
/** 时间 */
time?: string
/** 学习进度 */
schedule?: string
}
const VideoCover: FC<VideoCoverProps> = (opt: VideoCoverProps) => {
function jump() {
Taro.navigateTo({url: `/pages/business/videoInfo/videoInfo?id=${opt.id}&depId=${opt.depId || ''}`})
}
return (
<View className='videoBox'>
<View className='video' >
<View onClick={jump}>
<Image src={opt.thumb} mode='scaleToFill'/>
{opt.content && <View className='content'>{opt.content}</View>}
{opt.marker && <View className='marker'>{opt.marker}</View>}
</View>
<View className='box'>
<View className='title'>{opt.title}</View>
<View className='flex justify-between videoButton'>
<View>{opt?.time}</View>
<View>{opt?.schedule}</View>
</View>
</View>
</View>
</View>
)
}
export default VideoCover

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no,address=no">
<meta name="apple-mobile-web-app-status-bar-style" content="white">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
<title>video</title>
<script><%= htmlWebpackPlugin.options.script %></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '课程'
})

@ -0,0 +1,75 @@
import {CustomWrapper, PageContainer, ScrollView} from "@tarojs/components";
import {useEffect, useState} from "react";
import HVideo from "@/components/video/video";
import {getCurrentInstance} from "@tarojs/runtime";
import {curriculum, HourPlayData} from "@/api";
import {Profile} from '@/store'
import Taro from "@tarojs/taro";
const Course = () => {
const {courseId, id, preview} = getCurrentInstance()?.router?.params as {
courseId: number,
id: number,
preview: string | null
}
const [breakpoint, setBreakpoint] = useState<any[]>([])
const [show, setShow] = useState(false)
const [data, setData] = useState<HourPlayData | null>(null)
async function onEnded() {
try {
await curriculum.curEnd(courseId, id, data?.duration || 0)
Taro.showModal({title: "学习完成"})
} catch (e) {
}
}
function onBreakpoint(id: number) {
setBreakpoint(breakpoint.filter(d => d.id != id))
setShow(true)
}
async function getData() {
const res = await curriculum.hourPlay(courseId, id)
if (res) {
setData(res)
}
}
useEffect(() => {
getData()
}, [])
return (
<CustomWrapper>
<Profile.Provider>
{data && <HVideo
duration={data?.duration}
preview={!!preview}
src={data?.url || ''}
onEnded={onEnded}
breakpoint={breakpoint}
onBreakpoint={onBreakpoint}
/>}
<view>
<PageContainer
show={show}
position='bottom'
round
>
<ScrollView
style='height:70vh'
scrollY
scrollTop={0}
scrollWithAnimation
>
</ScrollView>
</PageContainer>
</view>
</Profile.Provider>
</CustomWrapper>
)
}
export default Course

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '个人中心'
})

@ -0,0 +1,22 @@
.box {
width: 690rpx;
height: 412rpx;
background: #FFF;
border-radius: 20rpx;
margin: auto;
overflow: hidden;
margin-top: 20px;
padding: 10px 0;
}
.button{
width: 690rpx;
line-height: 76rpx;
background: #45D4A8;
border-radius: 40rpx;
color: #fff;
position: fixed;
bottom:100px;
left: 30rpx;
font-size: 32rpx;
}

@ -0,0 +1,80 @@
import {FC, useState} from "react";
import {Profile} from '@/store'
import avatar from "@/static/img/avatar.png"
import PopPut from "@/components/popPut/popPut";
import {Button, CustomWrapper, Input, View} from "@tarojs/components";
import Taro from "@tarojs/taro";
import {userApi} from "@/api";
import styles from './userInfo.module.scss'
const List = () => {
const {empty, user, setUser} = Profile.useContainer()
const [show, setShow] = useState(false)
const [name, setName] = useState<string>(user?.name || '')
function unbind() {
Taro.showModal({
title: '解绑微信',
async success({confirm}) {
if (confirm) {
const res = await userApi.unbind(user?.id!)
if (res) {
empty()
}
}
}
})
}
async function putName() {
if (!name) {
Taro.showToast({title: "名称不能为空", icon: 'error'})
return
}
const res = await userApi.putName(user?.id!, name)
if (res) {
setUser(res)
setShow(!show)
Taro.showToast({title: '修改成功'})
}
}
return (
<>
<View className={styles.box}>
<PopPut title='头像' image={avatar} chevron no_border/>
<PopPut title='手机号' content={user?.phone_number} chevron no_border/>
<PopPut title='昵称' content={user?.name} isProp show={show} no_border>
<View className='h-6 pt-4 px-3'>
<View className='text-center font-weight'>{name}</View>
<Input className='input'
placeholder='请输入昵称'
onInput={(event) => setName(event.detail.value)}
value={name}
/>
<View className='text-muted mt-2 font-24'>4-20_-</View>
<Button className={styles.button} onClick={putName}></Button>
</View>
</PopPut>
<PopPut title='解绑微信' onClick={unbind} no_border/>
</View>
<Button className={styles.button} onClick={empty}>退</Button>
</>
)
}
const userInfo: FC = () => {
return (
<CustomWrapper>
<Profile.Provider>
<List/>
</Profile.Provider>
</CustomWrapper>
)
}
export default userInfo

@ -0,0 +1,75 @@
import {FC, useState} from "react";
import {View} from "@tarojs/components";
import {ManageApi} from "@/api/manage";
import {Profile} from '@/store'
import PopPut from "@/components/popPut/popPut";
import Taro from "@tarojs/taro";
interface Props {
cur_id: number
name?: string
}
const Dep: FC<Props> = ({cur_id}: Props) => {
const [bindDep, setBindDep] = useState<Department[]>([])
async function getBind() {
try {
const res = await ManageApi.bingDep(cur_id)
setBindDep(res.department)
} catch (e) {
}
}
function jump(item: Department) {
Taro.showModal({
title: "是否查看" + item.name,
success({confirm}) {
confirm && Taro.navigateTo({url: `/pages/manage/depCur/depCur?id=${item.id}`})
}
})
}
Taro.useDidShow(() => {
getBind()
})
return (<>
{bindDep && <View className='header'>
<View className='font-weight font-26'></View>
{bindDep && bindDep.map(d => (
<PopPut title={d.name} height={40} content={'查看'} onClick={() => jump(d)}/>
))}
</View>
}
</>)
}
const StudentRecord: FC<Props> = ({cur_id, name}: Props) => {
function jump() {
Taro.navigateTo({url: `/pages/manage/studentRecord/studentRecord?cur_id=${cur_id}&name=${name}`})
}
return (
<View className='header'>
<PopPut title={'学员学习记录'} height={40} content={'查看'} onClick={jump}/>
</View>
)
}
const ContainDeps: FC<Props> = ({cur_id,name}: Props) => {
const {user} = Profile.useContainer()
return (
<>
{user?.role_type !== 0 ?
<View>
<Dep cur_id={cur_id}/>
<StudentRecord cur_id={cur_id} name={name}/>
</View>
: null}
</>
)
}
export default ContainDeps

@ -0,0 +1,91 @@
import {FC, useEffect, useState} from "react";
import Tabs, {OnChangOpt, TabList} from "@/components/tabs/tabs";
import {View} from "@tarojs/components";
import {Profile} from '@/store'
import {CourseDepData} from "@/api";
import Collapse from "@/components/collapse/collapse";
import Hours from "@/pages/business/videoInfo/components/hours";
import Taro from "@tarojs/taro";
interface Props {
data: CourseDepData | null
}
const Catalogue: FC<Props> = ({data}: Props) => {
const {user} = Profile.useContainer()
const [current, setCurrent] = useState(1)
const [tabList, setTabList] = useState<TabList[]>([
{title: '介绍', value: 0},
{title: '目录', value: 1},
{title: '评价', value: 2},
])
useEffect(() => {
if (user?.role_type && user?.role_type > 0) {
setTabList([...tabList, {title: '学员管理', value: 3}])
}
}, [])
function tabChange({tab}: OnChangOpt) {
setCurrent(tab?.value as number)
}
function getHors(chapter_id: number): Hour[] | null {
for (const d of Object.values(data?.hours || {})) {
if (d[0].chapter_id === chapter_id) {
return d
}
}
return null
}
function complete(id: number): boolean {
return !!data?.learn_hour_records[id]?.is_finished
}
function jump(id) {
Taro.navigateTo({url: `/pages/business/course/course?courseId=${data?.course.id}&id=${id}`})
}
return (
<View className='catalogue'>
<Tabs tabList={tabList} onChange={tabChange} current={current}/>
<View className='py-2'>
{current === 0 && <View className='short_desc'>{data?.course.short_desc}</View>}
{current === 1 && <View>
<View className='font-weight'></View>
{data?.chapters.length ? Object.values(data?.chapters || {}).map((d, index) => <View>
<Collapse title={`${index + 1}.${d.name}`}>
<>
{getHors(d.id)?.map((hor, index) => <Hours
id={hor.id}
index={index}
title={hor.title}
duration={hor.duration}
complete={complete}
click={jump}
/>
)}
</>
</Collapse>
</View>)
: data?.hours?.[0].map((hor, index) => <Hours
id={hor.id}
index={index}
title={hor.title}
duration={hor.duration}
complete={complete}
click={jump}
/>
)}
</View>}
{current === 2 && <View className='text-center'></View>}
</View>
</View>
)
}
export default Catalogue

@ -0,0 +1,33 @@
import {FC} from "react";
import '../videoInfo.scss'
import {Image, View} from "@tarojs/components";
import playOk from "@/static/img/play-ok.png";
import play from "@/static/img/play.png";
import {formatMinute} from "@/utils/time";
interface Props {
id: number
index: number
title: string
duration: number
click: (id: number) => void
complete: (id: number) => boolean
}
const Hours: FC<Props> = (opt: Props) => {
return (
<>
<View className={'hor' + ` ${opt.complete(opt.id) ? 'complete' : null}`}
onClick={() => opt.click(opt.id)}
>
<Image src={opt.complete(opt.id) ? playOk : play} mode='aspectFit'/>
<View>
<View>{opt.index + 1}.{opt.title}</View>
<View className='font-26'>{formatMinute(opt.duration)}</View>
</View>
</View>
</>
)
}
export default Hours

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '课程'
})

@ -0,0 +1,50 @@
.content {
.image {
width: 100%;
display: block;
}
.header {
margin-bottom: 10px;
border-radius: 0 0 40rpx 40rpx;
padding: 30rpx 30rpx;
background: #fff;
}
}
.catalogue {
background: #fff;
border-radius: 40rpx;
padding: 24rpx;
margin-top: 20rpx;
.short_desc {
color: #606563;
line-height: 1.75;
}
}
.hor {
padding: 20px 0;
display: flex;
Image {
width: 40rpx;
height: 40rpx;
margin-top: 6px;
}
& > View {
flex: 1;
margin-left: 20px;
border-bottom: 1px solid #ddd;
View {
margin-bottom: 20px;
}
}
}
.complete {
color: #45D4A8;
}

@ -0,0 +1,49 @@
import {Image, Text, View} from "@tarojs/components";
import {FC, useState} from "react";
import {getCurrentInstance} from "@tarojs/runtime";
import {CourseDepData, curriculum} from "@/api";
import './videoInfo.scss'
import Taro from "@tarojs/taro";
import {Profile} from '@/store'
import Catalogue from "@/pages/business/videoInfo/components/catalogue";
const VideoInfo: FC = () => {
const {id, depId} = getCurrentInstance()?.router?.params as { id: number, depId: number | null }
const [data, setData] = useState<CourseDepData | null>(null)
async function getData() {
const res = await curriculum.courseDep(id, depId)
if (res) {
setData(res)
}
}
Taro.useDidShow(getData)
return (
<Profile.Provider>
<View className='content'>
<Image src={data?.course.thumb || ''} className='image' mode='scaleToFill'/>
{/*<ContainDeps cur_id={id} name={data?.course.title}/>*/}
<View className='header'>
<View className='flex justify-between text-muted'>
<Text className='font-34 text-warning'>{data?.is_required ? '必修' : '选秀'}</Text>
<Text>{data?.course.class_hour}</Text>
</View>
<View className='font-weight font-40 my-4'>{data?.course.title}</View>
<View className='text-muted font-26'>
{/*<Text className='mr-3'>时长:32:10</Text>*/}
<Text>{data?.learn_record?.finished_count || 0}/{data?.learn_record?.hour_count || Object.keys(data?.hours || {}).length}</Text>
</View>
</View>
<Catalogue data={data}/>
</View>
</Profile.Provider>
)
}
export default VideoInfo

@ -0,0 +1,15 @@
import {FC} from "react";
import {Text, View} from "@tarojs/components";
import styles from "@/pages/index/index.module.scss";
import Icon from "@/components/icon";
export const Search: FC = () => {
return (
<View className={styles.search}>
<View>
<Icon name='search' size={18}/>
<Text></Text>
</View>
</View>
)
}

@ -0,0 +1,52 @@
import {FC, useState} from "react";
import Taro from "@tarojs/taro";
import {View} from "@tarojs/components";
import {CoursesKey, Cur, publicApi} from "@/api/public";
import VideoCover from "@/components/videoCover/videoCover";
import styles from '../index.module.scss'
interface Props {
categoryId: CoursesKey
}
export const VideoList: FC<Props> = ({categoryId}: Props) => {
const [data, setDta] = useState<Cur[] | null>(null)
async function getData() {
const res = await publicApi.curs()
setDta(res)
}
Taro.useDidShow(() => {
getData()
})
function rateOfLearning(id: number, class_hour: number): JSX.Element {
console.log(id)
return (<View>{`${class_hour}节/已学${0}`}</View>)
}
return (
<>
{data?.map(d => (
<>{
d.courses?.[categoryId].length ? <View>
<View className='font-weight'>{d.name}</View>
<View className={'py-2 flex justify-between flex-wrap ' + styles.videoListBox}>
{d.courses[categoryId].map(d => (
<VideoCover
thumb={d.thumb}
title={d.title}
id={d.id}
depId={d.id}
content={rateOfLearning(d.id, d.class_hour)}
/>
))}
</View>
</View> : null
}</>
))}
</>
)
}

@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})

@ -0,0 +1,43 @@
.content {
position: relative;
min-height: 100vh;
padding: 0 20px;
&:after {
min-height: 100vh;
position: absolute;
top: 0;
left: -10%;
width: 120%;
content: '';
display: block;
background: linear-gradient(40deg, #fff 50rpx, #caf0e2, #92ecc5) no-repeat;
min-height: 100vh;
background-size: 100% 600rpx;
filter: blur(50px);
z-index: -1;
}
}
.search {
View {
width: 710rpx;
margin: 34rpx 0 0;
background: #fff;
border-radius: 100px;
line-height: 68rpx;
color: #bbb;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
Text {
padding-right: 20px;
}
}
}
.videoListBox {
border-radius: 20px;
}

@ -0,0 +1,40 @@
import {FC, useState} from "react";
import {View} from "@tarojs/components";
import Taro from "@tarojs/taro";
import styles from './index.module.scss'
import Tabs, {OnChangOpt, TabList} from "@/components/tabs/tabs";
import {Profile} from '@/store'
import {Search} from "@/pages/index/components/search";
import {VideoList} from "@/pages/index/components/videoList";
import {CoursesKey} from "@/api/public";
const Index: FC = () => {
const category: TabList[] = [
{title: "必修", value: 'is_required'},
{title: "选修", value: 'is_not_required'},
{title: "已完成", value: 'is_finished'},
{title: "未完成", value: 'is_not_finished'},
]
const [categoryId, setCategoryId] = useState<CoursesKey>('is_required')
function tabChange(data: OnChangOpt) {
setCategoryId(data.tab?.value as CoursesKey)
}
const globalData = Taro.getApp().globalData
return (
<Profile.Provider>
<View className={styles.content} style={`paddingTop:${globalData.statusBarHeight}px`}>
<View className='text-center font-weight font-34 mt-3'></View>
<Search/>
<Tabs tabList={category} onChange={tabChange} current={categoryId}/>
<VideoList categoryId={categoryId}/>
<View className='text-center text-muted mt-3'>- -</View>
</View>
</Profile.Provider>
)
}
export default Index

@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom',
})

@ -0,0 +1,66 @@
.container {
position: relative;
}
.navbar,
.brand {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.navbar {
position: relative;
line-height: 1;
font-size: 28px;
}
.brand {
width: 140px;
height: 140px;
background: #fff;
border-radius: 20px;
margin: 250px auto 145px;
image {
width: 100px;
height: 100px;
}
}
.loginTips {
margin: 24px;
text-align: center;
}
.submit {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
background: red;
color: #fff;
border-radius: 20px;
margin: 0 56px;
}
.errorTips {
position: absolute;
top: 100%;
left: 24px;
right: 24px;
background: red;
color: white;
padding: 24px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.bing {
height: 50vh;
padding: 50px 30px 0;
}

@ -0,0 +1,157 @@
import {FC, useEffect, useRef, useState} from "react";
import {Profile} from "@/store";
import {Button, CustomWrapper, Form, Image, Input, PageContainer, Text, View} from "@tarojs/components";
import Taro from "@tarojs/taro";
import styles from './login.module.scss'
import Loading from "@/components/loading";
import Icon from "@/components/icon";
import {userApi} from "@/api";
import {regexTel} from "@/utils/regu";
interface BingProps {
code: string
catch_key: string
}
const Bing: FC<BingProps> = ({code, catch_key}: BingProps) => {
const [useCode, setUseCode] = useState<string>(code)
const form = useRef<HTMLFormElement | null>(null)
const [loading, setLoading] = useState(false)
const {setUser, setToken, setCompany} = Profile.useContainer()
useEffect(() => {
form.current?.reset?.()
setUseCode(code)
}, [code])
async function refreshCode() {
try {
await userApi.code(catch_key)
} catch (e) {
}
}
async function Submit(data) {
Taro.showLoading()
setLoading(true)
const value = data.target.value
if (!regexTel.exec(value.phone_number)) {
Taro.showToast({title: '手机号错误', icon: 'error'})
setLoading(false)
return
}
try {
const res = await userApi.checkout({...value, catch_key})
if (res) {
setCompany(res.company)
setUser(res.user)
setToken(res.token)
Taro.switchTab({url: '/pages/index/index'})
}
} catch (e) {
}
Taro.hideLoading()
setLoading(false)
}
return (
<View className='h-5 pt-6 px-3'>
<Form className='form' onSubmit={Submit} ref={form}>
<View className='item'>
<View className='label'></View>
<Input name='phone_number' placeholder={'请输入手机号'}/>
</View>
<View className='item'>
<View className='label'></View>
<View className='flex align-center'>
<Input name='code' className='flex-1' placeholder={'请输入验证码'}/>
<Image className='w-2 ml-1' style='height:28px' src={useCode} onClick={refreshCode}/>
</View>
</View>
<Button className={styles.submit} style='margin:30px 0' formType='submit' disabled={loading}></Button>
</Form>
</View>
)
}
const Login = () => {
const {statusBarHeight = 0} = Taro.getSystemInfoSync()
const bbc = Taro.getMenuButtonBoundingClientRect();
const navHeight = bbc.bottom + (bbc.top - statusBarHeight) - statusBarHeight
const [isLoading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [validateCode, setCode] = useState<string | null>(null)
const [catch_key, setCatch_key] = useState<string | null>(null)
const {setUser, setToken, setCompany} = Profile.useContainer()
function login() {
if (isLoading) return;
setLoading(true)
Taro.login({
success: async (res) => {
try {
const {code, catch_key, user, token, company} = await userApi.login(res.code)
if (!code) {
setUser(user)
setToken(token)
setCompany(company)
Taro.switchTab({url: '/pages/index/index'})
return
}
setCatch_key(catch_key)
setCode(code.image)
} catch (e) {
}
setLoading(false)
},
fail: (res) => {
setError(res.errMsg)
setLoading(false)
},
})
}
return (
<View className={styles.container}>
<PageContainer show={!!validateCode} position='bottom' round onBeforeLeave={() => setCode(null)}>
{validateCode && <Bing code={validateCode!} catch_key={catch_key!}/>}
</PageContainer>
<View className={styles.navbar} style={`height:${navHeight}px;margin-top:${statusBarHeight}px`}>
<Text></Text>
{error ? <View className={styles.errorTips}>
<View style={{flex: 1}}>{error}</View>
<View>
<Icon name={'close'} onClick={() => setError(null)}/>
</View>
</View> : null}
</View>
<View className={styles.brand}>
<Image mode={'scaleToFill'} src="https://admin.playedu.xyz/favicon.ico"/>
</View>
<View className={styles.loginTips}>
<Text>使</Text>
</View>
<Button className={styles.submit} onClick={login} disabled={isLoading}>
{isLoading ? <Loading/> : null}
<Text></Text>
</Button>
</View>
)
}
const Index: FC = () => {
return (
<CustomWrapper>
<Profile.Provider>
<Login/>
</Profile.Provider>
</CustomWrapper>
)
}
export default Index

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '添加课程',
})

@ -0,0 +1,137 @@
import {CustomWrapper, View} from "@tarojs/components";
import {getCurrentInstance} from "@tarojs/runtime";
import {ManageApi} from "@/api/manage";
import React, {FC, useEffect, useState} from "react";
import Tabs, {TabList} from "@/components/tabs/tabs";
import {Category, publicApi} from "@/api/public";
import Taro from "@tarojs/taro";
import VideoCover from "@/components/videoCover/videoCover";
interface AddProps {
cur_id: number,
name: string,
index: number
}
const AddCur = () => {
const {id} = getCurrentInstance()?.router?.params as { id: string }
const [category, setCategory] = useState<TabList[]>([])
const [categoryId, setCategoryId] = useState<number>(0)
const [dataMap, setDataMap] = useState<Map<number, Curriculum[]>>(new Map())
async function getCategory() {
try {
const data: (TabList)[] = []
const {categories} = await publicApi.category()
Object.values(categories).map(d => {
data.push(...d.map<TabList<Category>>(c => ({title: c.name, value: c.id})))
})
setCategory(data)
setCategoryId(data[0].value as number)
} catch (e) {
}
}
async function getData() {
if (categoryId !== null) {
const res = await ManageApi.optionalCur(Number(id), categoryId)
const map = new Map(dataMap)
const data = map.get(categoryId)
if (!data) {
map.delete(categoryId)
map.set(categoryId, res)
setDataMap(map)
} else {
res.forEach(d => {
const index = data.findIndex(c => c.id === d.id)
if (index === -1) {
data.push(d)
} else {
data.splice(index, 1, d)
}
})
map.delete(categoryId)
map.set(categoryId, data)
setDataMap(map)
}
}
}
const Add: FC<AddProps> = ({cur_id, name, index}: AddProps) => {
function addCur() {
function required() {
Taro.showModal({
title: '课程是否为必修',
cancelText: '选修',
confirmText: '必修',
async success({confirm}) {
try {
const is_required = confirm ? 1 : 0
Taro.showLoading()
await ManageApi.addCur({course_id: [cur_id], dep_id: [Number(id)], is_required})
const map = new Map(dataMap)
const data = map.get(categoryId!) || []
if (data) {
data.splice(index, 1)
}
map.delete(categoryId!)
map.set(categoryId!, data)
setDataMap(map)
Taro.showToast({title: "添加成功"})
} catch (e) {
}
Taro.hideLoading()
}
})
}
Taro.showModal({
title: '确定添加' + name,
success({confirm}) {
confirm && required()
}
})
}
return (
<View className='text-center mt-1' onClick={addCur}></View>
)
}
useEffect(() => {
getCategory().then()
}, [])
useEffect(() => {
getData().then()
}, [categoryId])
return (
<CustomWrapper>
<View className='bg-white'>
<Tabs tabList={category} onChange={(data) => setCategoryId(data.tab?.value as number)} current={categoryId}/>
</View>
{
(categoryId && dataMap.get(categoryId)?.length) ?
<View className='bg-white mt-2 py-2 flex flex-wrap'>
{dataMap.get(categoryId)?.map((d, index) => (
<VideoCover
key={d.id}
thumb={d.thumb}
title={<Add cur_id={d.id} name={d.title} index={index}/>}
id={d.id}
depId={Number(id)}
content={d.title}
/>
))}
</View>
: null
}
<View className='text-center mt-3'>- -</View>
</CustomWrapper>
)
}
export default AddCur

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '请填写学员信息',
})

@ -0,0 +1,19 @@
.dep {
background: #ddd;
padding: 5px 15px;
border-radius: 3px;
}
.depSelected {
background: #9ee0a3;
color: #3f6942;
}
.add {
border-radius: 10px;
background: linear-gradient(to right, #8284f7, #5a93f9);
color: #fff;
position: fixed;
width: 710rpx;
bottom: 20px;
}

@ -0,0 +1,170 @@
import {Button, CustomWrapper, Form, Input, PageContainer, View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import {ManageApi, Student} from "@/api/manage";
import Icon from "@/components/icon";
import Taro from "@tarojs/taro";
import {curriculum} from "@/api";
import './addStudent.scss'
import {getCurrentInstance} from "@tarojs/runtime";
import {Profile} from '@/store'
interface Department {
id: number
title: string
}
const AddStudent = () => {
const [userInfo, setUerInfo] = useState<Student | null>(null)
const [department, setDepartment] = useState<Department[]>([])
const [show, setShow] = useState(false)
const [depIds, setDepIds] = useState<number[]>([])
const params = getCurrentInstance()?.router?.params as { id?: number }
const [disable, setDisable] = useState(false)
const {company} = Profile.useContainer()
async function getDepartment() {
Taro.showLoading()
const res = await curriculum.use()
if (res) {
setDepartment(res.map(d => ({title: d.name, id: d.id})))
}
Taro.hideLoading()
}
async function submit(event) {
const value: Student = event.detail.value
for (const [key, value1] of Object.entries(value)) {
if (!value1 && !['id_card', 'password'].includes(key)) {
Taro.showToast({title: "请填写完整", icon: 'error'})
return
}
}
Taro.showLoading()
setDisable(true)
try {
if (params.id) {
await ManageApi.putUser(params.id, {...value, dep_ids: depIds, company_id: company?.id || 0})
} else {
await ManageApi.addUser({...value, dep_ids: depIds, company_id: company?.id || 0})
}
Taro.hideLoading()
Taro.showToast({title: "添加成功", icon: 'success'})
setTimeout(() => {
Taro.navigateBack()
}, 500)
} catch (e) {
console.log(e)
}
Taro.hideLoading()
setDisable(true)
}
function changeDepIds(item: Department) {
const ids = JSON.parse(JSON.stringify(depIds))
const index = depIds.indexOf(item.id)
if (index === -1) {
ids.push(item.id)
} else {
ids.splice(index, 1)
}
setDepIds(ids)
}
function formatDep() {
const selected = department.filter(d => depIds.includes(d.id)).map(d => d.title)
const top4 = selected.splice(0, 3).join('、')
return top4 + (selected.length ? "+" + selected.length : '')
}
async function getUserInfo() {
const res = await ManageApi.userInfo(params.id!)
setUerInfo(res.user)
setDepIds(res.dep_ids)
}
useEffect(() => {
getDepartment()
if (params.id) {
getUserInfo()
}
}, [params.id])
return (
<View className='bg-white px-2'>
<Form className='form' onSubmit={submit}>
<View className='item'>
<View></View>
<Input placeholder='请输入学员姓名' name='name' value={userInfo?.name}
onInput={(event) => setUerInfo({...userInfo, name: event.detail.value} as Student)}/>
</View>
<View className='item'>
<View></View>
<Input placeholder='请输入手机号' name='phone_number' value={userInfo?.phone_number}
onInput={(event) => setUerInfo({...userInfo, phone_number: event.detail.value} as Student)}/>
</View>
<View className='item'>
<View></View>
<Input placeholder='请输入登录邮箱' name='email' value={userInfo?.email}
onInput={(event) => setUerInfo({...userInfo, email: event.detail.value} as Student)}/>
</View>
<View className='item'>
<View></View>
<Input password placeholder='请输入登录密码' name='password' value={userInfo?.password}
onInput={(event) => setUerInfo({...userInfo, password: event.detail.value} as Student)}/>
</View>
<View className='item'>
<View></View>
<View className='flex align-center' onClick={() => setShow(true)}>
<View>
{
depIds.length ? formatDep() : '请选择'
}
</View>
<Icon name='chevron-right'/>
</View>
</View>
<View className='item'>
<View></View>
<Input password placeholder='请输入身份证号' name='id_card' value={userInfo?.id_card}
onInput={(event) => setUerInfo({...userInfo, id_card: event.detail.value} as Student)}/>
</View>
<Button className='add' formType='submit' disabled={disable}></Button>
</Form>
<PageContainer show={show} round>
<View className='px-2 pt-1' style='text-align:right' onClick={() => setShow(false)}></View>
<View className='h-4 p-2 flex flex-wrap align-start'>
{department?.map(item => {
return (
<View className={'mx-2 dep ' + (depIds.includes(item.id) ? ' depSelected' : '')}
onClick={() => changeDepIds(item)}>
{item.title}
</View>
)
})}
</View>
</PageContainer>
</View>
)
}
const AddPage: FC = () => {
return (
<CustomWrapper>
<Profile.Provider>
<AddStudent/>
</Profile.Provider>
</CustomWrapper>
)
}
export default AddPage

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '学员',
})

@ -0,0 +1,11 @@
.college {
background: #ffffff;
width: 100%;
box-sizing: border-box;
padding: 20px;
border-bottom: 1px solid #ddd;
View {
padding-bottom: 10px;
}
}

@ -0,0 +1,39 @@
import {getCurrentInstance} from "@tarojs/runtime";
import {curriculum, RecordData} from "@/api";
import {useEffect, useState} from "react";
import {View, Progress, CustomWrapper} from "@tarojs/components";
import './college.scss'
import Taro from "@tarojs/taro";
const College = () => {
const {id, name} = getCurrentInstance()?.router?.params as any
const [data, setData] = useState<RecordData[]>([])
const getData = () => {
curriculum.record(id!).then(res => {
setData(res)
})
}
useEffect(() => {
Taro.setNavigationBarTitle({title:name})
getData()
}, [])
return (
<CustomWrapper>
{
data.map(d => {
return (
<View className='college'>
<View>{d.key}</View>
<Progress percent={d.value} activeColor='red' active showInfo/>
</View>
)
})
}
<View className='text-center py-1'></View>
</CustomWrapper>
)
}
export default College

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '课程管理',
})

@ -0,0 +1,10 @@
import {FC} from "react";
import {View} from "@tarojs/components";
const CurAdmin: FC = () => {
return (
<View>sd</View>
)
}
export default CurAdmin

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '所有课程',
})

@ -0,0 +1,11 @@
.curBuy {
background: #c94f4f;
color: #fff;
margin: auto;
width: 250px;
line-height: 70px;
border-radius: 70px;
text-align: center;
font-size: 30rpx;
font-weight: bold;
}

@ -0,0 +1,66 @@
import {CustomWrapper, View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import VideoCover from "@/components/videoCover/videoCover";
import styles from './curriculum.module.scss'
import {ManageApi} from "@/api/manage";
import Taro from "@tarojs/taro";
const Curriculum = () => {
const [data, setDta] = useState<Curriculum[]>([])
async function getData() {
try {
const res = await ManageApi.buyAll()
setDta(res)
} catch (e) {
}
}
const Add: FC<{ id: number }> = ({id}: { id: number }) => {
function buy() {
Taro.showModal({
title: '是否购买',
async success({confirm}) {
if (confirm) {
try {
await ManageApi.buy([id])
Taro.showToast({title: "购买成功"})
await getData()
} catch (e) {
}
}
}
})
}
return (
<View className={styles.curBuy} onClick={buy}></View>
)
}
useEffect(() => {
getData()
}, [])
return (
<CustomWrapper>
{data.length ?
<View className='bg-white flex flex-wrap'>
{data.map(d => (<VideoCover
marker='限时免费'
thumb={d.thumb}
title={<Add id={d.id}/>}
id={d.id}
depId={null}
content={d.title}/>
))}
</View>
: null
}
<View className='text-center mt-3'>- -</View>
</CustomWrapper>
)
}
export default Curriculum

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '部门管理',
})

@ -0,0 +1,158 @@
import {FC, useEffect, useState} from "react";
import {AddDepProps, ManageApi} from "@/api/manage";
import '../studentAdmin/student.scss'
import {Button, Text, View, PageContainer, Input, Form, CustomWrapper} from "@tarojs/components";
import Taro from "@tarojs/taro";
import {Profile} from '@/store'
interface ChangeDataProps {
putCompany: Manage | null
getDeps: () => Promise<void>
}
const ChangeData: FC<ChangeDataProps> = ({putCompany, getDeps}: ChangeDataProps) => {
const {company} = Profile.useContainer()
const [name, setName] = useState<string>('')
const [sort, setSort] = useState<number>(putCompany?.sort || 0)
const [disable, setDisable] = useState(false)
const company_id = putCompany?.company_id || company?.id || 0
useEffect(() => {
if (putCompany) {
setName(putCompany.name)
}
}, [])
async function submit() {
if (!name) {
Taro.showToast({title: "请认真填写", icon: "error"})
return
}
setDisable(true)
Taro.showLoading()
try {
const data: AddDepProps = {
id: putCompany?.id || null,
name,
parent_id: putCompany?.parent_id || 0,
company_id: company_id,
sort: sort
}
if (putCompany) {
await ManageApi.putDep(data)
} else {
await ManageApi.addDep(data)
}
setTimeout(() => Taro.showToast({title: '操作成功'}))
await getDeps()
} catch (e) {
}
Taro.hideLoading()
setDisable(false)
}
return (
<View className='p-2'>
<View className='mt-2 text-center font-weight font-34'></View>
<Form className='form mt-3'>
<View className='item'>
<View></View>
<Input placeholder='请输入部门名称' value={name} onInput={(event) => setName(event.detail.value)}/>
</View>
<View className='item'>
<View></View>
<Input
type="number"
placeholder='请输入部门名称'
value={String(sort)}
onInput={(event) => setSort(Number(event.detail.value))}/>
</View>
<Button className='mt-3' formType='submit' onClick={submit} disabled={disable}></Button>
</Form>
</View>
)
}
const DepAdmin: FC = () => {
const [data, setData] = useState<Manage[]>([])
const [show, setShow] = useState(false)
const [putCompany, setPutCompany] = useState<Manage | null>(null)
async function getData() {
show && setShow(false)
const res = await ManageApi.depList()
if (res) {
const formatData: Manage[] = []
Object.values(res)?.forEach(d => {
formatData.push(...d)
})
setData(formatData)
}
}
function showPop(company: Manage | null) {
setPutCompany(company)
setShow(true)
}
function del(name: string, id: number) {
Taro.showModal({
title: name + '删除后将不可恢复',
async success({confirm}) {
if (confirm) {
try {
Taro.showLoading()
await ManageApi.delDep(id)
Taro.hideLoading()
Taro.showToast({title: "删除成功"})
await getData()
} catch (e) {
}
}
}
})
}
function jumpCur(id: number) {
Taro.navigateTo({url: `/pages/manage/depCur/depCur?id=${id}`})
}
useEffect(() => {
getData()
}, [])
return (
<CustomWrapper>
<Profile.Provider>
{data.map(d => (
<View className='bg-white user' key={d.id}>
<View className='flex mt-3 header p-2 justify-between'>
<View className='flex'>
<Text>{d.id}</Text>
<View className='font-weight'>{d.name}</View>
</View>
</View>
<View className='flex justify-between p-2 operation'>
<View onClick={() => showPop(d)}></View>
<View onClick={() => jumpCur(d.id)}></View>
<View className='text-danger' onClick={() => del(d.name, d.id)}></View>
</View>
</View>
))}
<View className='mt-3 text-center'>- -</View>
<Button className='add' onClick={() => showPop(null)}></Button>
<PageContainer show={show} round onAfterLeave={() => setShow(false)}>
<View className='h-4'>
{show && <ChangeData getDeps={getData} putCompany={putCompany}/>}
</View>
</PageContainer>
</Profile.Provider>
</CustomWrapper>
)
}
export default DepAdmin

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '部门课程',
onReachBottomDistance: 30
})

@ -0,0 +1,103 @@
import {getCurrentInstance} from "@tarojs/runtime";
import Taro, {useReachBottom} from "@tarojs/taro";
import {ManageApi} from "@/api/manage";
import React, {FC, useState} from "react";
import {Button, CustomWrapper, View} from "@tarojs/components";
import VideoCover from "@/components/videoCover/videoCover";
import {curriculum} from "@/api";
interface DelOpt {
id: number
findDel: (id: number) => Promise<void>
}
const Del: FC<DelOpt> = ({id, findDel}: DelOpt) => {
function del() {
Taro.showModal({
title: "是否删除课程",
success({confirm}) {
confirm && findDel(id)
}
})
}
return (
<View className='text-center p-2' onClick={del}></View>
)
}
let page = 1
const DepCur: FC = () => {
const {id} = getCurrentInstance()?.router?.params as { id: number }
const [total, setTotal] = useState<number>(0)
const [data, setData] = useState<Curriculum[]>([])
const getData = async (init?: boolean) => {
try {
Taro.showLoading({title: "课程查询中"})
if (init) {
page += 1
} else {
page = 1
}
const res = await ManageApi.depCur({id, size: 10, page: page})
setTotal(res.total)
const oldData: Curriculum[] = JSON.parse(JSON.stringify(data))
res.data.forEach(d => {
const index = oldData.findIndex(c => c.id === d.id)
if (index === -1) {
oldData.push(d)
} else {
oldData.splice(index, 1, d)
}
})
setData(oldData)
} catch (e) {
}
Taro.hideLoading()
}
async function findDel(cur_id: number) {
try {
await curriculum.delCur(id, cur_id)
const index = data.findIndex(d => d.id === cur_id)
if (index > -1) {
const oldData: Curriculum[] = JSON.parse(JSON.stringify(data))
oldData.splice(index, 1)
setData(oldData)
}
Taro.showToast({title: '删除成功'})
} catch (e) {
}
}
function jumpAddCur() {
Taro.navigateTo({url: "/pages/manage/addCur/addCur?id="+id})
}
Taro.useDidShow(() => getData())
useReachBottom(() => {
if (data.length < total) {
getData(true)
}
})
return (
<CustomWrapper>
<View className='flex p-1 flex-wrap'>
{data.map(d => <VideoCover
key={d.id}
thumb={d.thumb}
title={<Del id={d.id} findDel={findDel}/>}
id={d.id}
depId={id}
content={d.title}
/>)}
</View>
<Button className='add' onClick={jumpAddCur}></Button>
<View className='text-center p-3'>- -</View>
</CustomWrapper>
)
}
export default DepCur

@ -0,0 +1,48 @@
.user {
.header {
border-bottom: 1px solid #ddd;
.lock {
padding: 4px 20px;
border-radius: 5px;
color: #fff;
margin-right: 20px;
}
Text {
color: #6e6e6e;
}
.del {
color: red;
}
}
.info {
Image {
width: 150px;
height: 150px;
background: #ddd;
border-radius: 50%;
}
}
}
.add {
margin: 20px;
border-radius: 10px;
background: linear-gradient(to right, #8284f7, #5a93f9);
color: #fff;
position: fixed;
width: 710rpx;
bottom: 20px;
}
.operation {
border-top: 1px solid #ddd;
View {
width: 50%;
text-align: center;
}
}

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '学员管理',
})

@ -0,0 +1,140 @@
import {Button, CustomWrapper, Image, Text, View} from "@tarojs/components";
import {curriculum} from "@/api";
import {FC, useState} from "react";
import Taro, {useDidShow} from "@tarojs/taro";
import Tabs, {OnChangOpt, TabList} from "@/components/tabs/tabs";
import './student.scss'
import {Profile} from '@/store'
import {ManageApi} from "@/api/manage";
interface RoleTypeProps {
id: number
role_type: number
getData: () => Promise<void>
}
const RoleType: FC<RoleTypeProps> = ({id, role_type, getData}: RoleTypeProps) => {
const {user} = Profile.useContainer()
function setRoleType() {
if (role_type === 2) {
Taro.showModal({title: "禁止修改超级管理员"})
return
}
const type = role_type === 0 ? 1 : 0
Taro.showModal({
title: "设置为" + ['学员', '管理员'][type],
async success({confirm}) {
if (confirm) {
try {
Taro.showLoading()
await ManageApi.setRoleType(id, {auth_id: user?.id!, role_type: type})
Taro.hideLoading()
Taro.showToast({title: "设置成功"})
await getData()
} catch (e) {
}
}
}
})
}
return (user?.role_type === 2 ?
<View onClick={setRoleType}>{['设置管理员', '设置学员', '超级管理员'][role_type]}</View>
: null)
}
const studentAdmin = () => {
const [list, setList] = useState<TabList[]>([])
const [user, setUser] = useState<ManageUsers[]>([])
async function getData() {
Taro.showLoading()
const res = await curriculum.use()
if (res) {
setList(res.map(d => ({title: d.name, value: d})))
setUser(res[0].users)
}
Taro.hideLoading()
}
useDidShow(getData)
function listClick(data: OnChangOpt) {
setUser((data.tab?.value as Manage).users)
}
function jumCollege(id: number, name: string) {
Taro.navigateTo({url: `/pages/manage/college/college?id=${id}&name=${name}`})
}
function changeStudent(id?: number) {
Taro.navigateTo({url: "/pages/manage/addStudent/addStudent" + (id ? `?id=${id}` : '')})
}
function del(id: number) {
Taro.showModal({
title: '是否确认删除',
async success({confirm}) {
if (confirm) {
await ManageApi.del(id)
Taro.showToast({title: '删除成功'})
await getData()
}
}
})
}
return (
<CustomWrapper>
<Profile.Provider>
<View className='bg-white mb-3'>
<Tabs tabList={list} onChange={listClick}/>
</View>
{user.length ? user.map((d) => (
<View className='bg-white user'>
<View className='flex mt-3 header p-2 justify-between'>
<View className='flex'>
<View className='lock'
style={`background:${['#73c057', '#c94f4f'][d.is_lock]}`}>{['正常', '警用'][d.is_lock]}</View>
<Text> {d.id}</Text>
</View>
<View className='del' onClick={() => del(d.id)}></View>
</View>
<View className='p-2 flex info justify-between'>
<View>
<View className='font-weight my-3'>{d.name}</View>
<View className='flex mb-3'>
<View style='width:60px' className='text-muted'></View>
<View>{['学员', '管理员', '超级管理员'][d.role_type]}</View>
</View>
<View className='flex mb-3'>
<View style='width:60px' className='text-muted'></View>
<View>{d.phone_number}</View>
</View>
<View className='flex mb-3'>
<View style='width:60px' className='text-muted'></View>
<View>{d.email}</View>
</View>
</View>
<Image src={d.avatar} mode='widthFix'/>
</View>
<View className='flex justify-between p-2 operation'>
<View onClick={() => changeStudent(d.id)}></View>
<View onClick={() => jumCollege(d.id, d.name)}></View>
<RoleType id={d.id} role_type={d.role_type} getData={getData}/>
</View>
</View>
))
: <View className='text-center'></View>}
<View className='py-8'/>
<Button className='add' onClick={() => changeStudent()}></Button>
</Profile.Provider>
</CustomWrapper>
)
}
export default studentAdmin

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '学员学习记录',
onReachBottomDistance:30
})

@ -0,0 +1,102 @@
import {CustomWrapper, Image, Text, View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import {getCurrentInstance} from "@tarojs/runtime";
import Taro, {useReachBottom} from "@tarojs/taro";
import {CurLearningRecord, ManageApi} from "@/api/manage";
import '@/pages/manage/studentAdmin/student.scss'
const StudentRecord: FC = () => {
const [page, setPage] = useState(1)
const {cur_id, name} = getCurrentInstance()?.router?.params as { cur_id: string, name: string }
const [data, setData] = useState<CurLearningRecord | null>(null)
const [total, setTotal] = useState(0)
async function getData() {
try {
const res = await ManageApi.curLearningRecord(cur_id, {page, size: 10})
if (!data) {
setData(res)
} else {
const oldData: CurLearningRecord = JSON.parse(JSON.stringify(data))
oldData.data.push(...res.data)
oldData.departments = res.departments
Object.entries(res.user_dep_ids).forEach(([key, value]) => {
oldData.user_dep_ids[key] = value
})
Object.entries(res.user_course_records).forEach(([key, value]) => {
oldData.user_course_records[key] = value
})
Object.entries(res.user_course_hour_user_first_at).forEach(([key, value]) => {
oldData.user_course_hour_user_first_at[key] = value
})
setData(oldData)
}
setTotal(res.total)
} catch (e) {
}
}
function getDep(user_id: number): string {
const cur_ids = data?.user_dep_ids[user_id]
if (cur_ids) {
return cur_ids.map(d => data?.departments[d]).join('、')
}
return ''
}
useReachBottom(() => {
if (data && data.data.length < total) {
setPage(page + 1)
}
})
useEffect(() => {
getData()
}, [page])
Taro.setNavigationBarTitle({title: name})
return (
<CustomWrapper>
{data?.data.map(d => (
<View className='bg-white user'>
<View className='flex mt-3 header p-2 justify-between'>
<View className='flex'>
<Text> {d.id}</Text>
</View>
</View>
<View className='p-2 flex info justify-between'>
<View>
<View className='font-weight my-3'>{d.name}</View>
<View className='flex mb-3'>
<View style='width:80px' className='text-muted'></View>
<View>{['学员', '管理员', '超级管理员'][d.role_type]}</View>
</View>
<View className='flex mb-3'>
<View style='width:80px' className='text-muted'></View>
<View>{d.phone_number}</View>
</View>
<View className='flex mb-3'>
<View style='width:80px' className='text-muted'></View>
<View>{d.email}</View>
</View>
<View className='flex mb-3'>
<View style='width:80px' className='text-muted'></View>
<View>{getDep(d.id)}</View>
</View>
<View className='flex mb-3'>
<View style='width:80px' className='text-muted'></View>
<View>{data?.user_course_records[d.id]?.finished_count || 0}/{data?.course.class_hour}</View>
</View>
</View>
<Image src={d.avatar} mode='widthFix'/>
</View>
<View className='flex justify-between p-2 operation'>
</View>
</View>
))}
</CustomWrapper>
)
}
export default StudentRecord

@ -0,0 +1,24 @@
import {Profile} from "@/store";
import {Image, Text, View} from "@tarojs/components";
import styles from "@/pages/my/my.module.scss";
import avatar from "@/static/img/avatar.png"
const Header = () => {
const {user} = Profile.useContainer()
return (
<View className={styles.header}>
<View className='flex'>
<Image src={avatar}/>
<View className='flex-1'>
<View className='font-32 font-weight'>{user?.name}</View>
<View className='login font-24 mt-2 text-secondary flex justify-between content-start'>
<Text>{user?.phone_number}</Text>
</View>
</View>
</View>
</View>
)
}
export default Header

@ -0,0 +1,60 @@
import {useEffect, useState} from "react";
import {Image, View} from "@tarojs/components";
import Taro from "@tarojs/taro";
import {Profile} from '@/store/profile'
import styles from '../../my.module.scss'
import dep from '@/static/img/dep.png'
import cur from '@/static/img/cur.png'
import student from '@/static/img/student.png'
import buy from '@/static/img/buy.png'
interface List {
title: string;
src: string;
router: string;
}
const Service = () => {
const [list, setList] = useState<List[]>([
{title: '设置', src: dep, router: '/pages/business/userInfo/userInfo'}
])
const {user} = Profile.useContainer()
useEffect(() => {
const oldList: List[] = JSON.parse(JSON.stringify(list))
if ([1, 2].includes(user?.role_type || 0)) {
oldList.unshift(...[
{title: '部门管理', src: dep, router: '/pages/manage/depAdmin/depAdmin'},
{title: '学员管理', src: student, router: '/pages/manage/studentAdmin/studentAdmin'},
{title: '课程管理', src: cur, router: ''},
{title: '课程购买', src: buy, router: '/pages/manage/curriculum/curriculum'},
])
setList(oldList)
}
}, [])
function jump(url: string) {
Taro.navigateTo({url})
}
return (
<View className={'mt-3 ' + styles.tool}>
<View className='font-weight font-32'></View>
<View className={'mt-4 ' + styles.service}>
{
list.map(d => {
return (
<View onClick={() => jump(d.router)}>
<Image src={d.src} mode='aspectFit' className={styles.serviceImage}/>
<View>{d.title}</View>
</View>
)
})
}
</View>
</View>
)
}
export default Service

@ -0,0 +1,66 @@
import {Image, View} from "@tarojs/components";
import styles from '../../my.module.scss'
import curriculum1 from '@/static/img/curriculum1.png'
import curriculum2 from '@/static/img/curriculum2.png'
import Taro from "@tarojs/taro";
import {curriculum} from "@/api";
import {useState} from "react";
import {formatMinute} from "@/utils/time";
import time1 from "@/static/img/time1.png";
import time2 from "@/static/img/time2.png";
import over from '@/static/img/over.png'
import incomplete from '@/static/img/incomplete.png'
interface List {
title: string
time: string | number
src: string
}
const Time = () => {
const [list, setList] = useState<List[]>([
{title: '今日时长', time: '00:00', src: time1},
{title: '累计时长', time: '00:00', src: time2},
{title: '必修课', time: '0/0', src: curriculum1},
{title: '选修课', time: '0/0', src: curriculum2},
{title: '已完成', time: '0', src: over},
{title: '未完成', time: '0', src: incomplete},
])
Taro.useDidShow(async () => {
try {
const {stats} = await curriculum.course()
const oldList: List[] = JSON.parse(JSON.stringify(list))
oldList[0].time = formatMinute(stats.today_learn_duration)
oldList[1].time = formatMinute(stats.learn_duration)
oldList[2].time = stats.required_course_count
oldList[3].time = stats.nun_required_course_count
oldList[4].time = stats.required_finished_course_count + stats.nun_required_finished_course_count
oldList[5].time = stats.total_course_count - (stats.required_finished_course_count + stats.nun_required_finished_course_count)
setList(oldList)
} catch (e) {
}
})
return (
<View className='flex mt-3 justify-between flex-wrap'>
{
list.map(d => {
return (
<View className={'flex justify-between ' + styles.timeBox} key={d.title}>
<View>
<View className='font-weight'>{d.title}</View>
<View className='text-muted'>{d.time}</View>
</View>
<Image src={d.src} mode='aspectFit' className={styles.timeImag}/>
</View>
)
})
}
</View>
)
}
export default Time

@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})

@ -0,0 +1,87 @@
page {
background: #F2F8F6 !important;
}
.content {
background: linear-gradient(180deg, #45D4A8 0%, rgba(69, 212, 168, 0) 100%) no-repeat;
background-size: 100% 458rpx;
position: relative;
&:after {
content: '';
display: block;
position: absolute;
top: 106rpx;
right: -100rpx;
width: 290rpx;
height: 290rpx;
background: #FFFF;
opacity: 0.2;
border-radius: 290rpx;
}
&:before {
content: '';
display: block;
position: absolute;
top: -80rpx;
left: -80rpx;
width: 230rpx;
height: 230rpx;
background: #FFFF;
opacity: 0.2;
border-radius: 230rpx;
}
}
.header {
padding: 130px 20px 0;
Image {
width: 100px;
height: 100px;
margin-right: 30px;
margin-top: -10px;
}
}
.ribbon {
padding: 20px;
}
.timeBox {
width: 40%;
padding: 20px;
border-radius: 20px;
line-height: 1.7;
margin-bottom: 20px;
background: #fff;
}
.timeImag {
width: 80px;
height: 80px;
margin-left: 20px;
}
.service {
display: grid;
margin-top: 48rpx;
grid-template-columns:1fr 1fr 1fr 1fr;
grid-auto-rows: 100px 100px 100px 100px;
grid-gap: 60px;
font-size: 28px;
text-align: center;
}
.serviceImage {
width: 48rpx;
height: 48rpx;
}
.tool {
background: #fff;
border-radius: 20px;
padding:30rpx 20px;
}

@ -0,0 +1,29 @@
import {CustomWrapper, View} from "@tarojs/components";
import Taro from "@tarojs/taro";
import styles from './my.module.scss'
import {Profile} from '@/store'
import Header from "./components/header/header";
import {FC} from "react";
import Time from "@/pages/my/components/header/time";
import Service from "@/pages/my/components/header/service";
const My: FC = () => {
const globalData = Taro.getApp().globalData
return (
<CustomWrapper>
<Profile.Provider>
<View className={styles.content} style={`paddingTop:${globalData.statusBarHeight}px`}>
<Header/>
<View className={styles.ribbon}>
<Time/>
<Service/>
</View>
</View>
</Profile.Provider>
</CustomWrapper>
)
}
export default My

@ -0,0 +1,53 @@
page {
background-color: #efeff7;
font-family: PingFang SC-Bold, PingFang SC;
}
.input {
height: 24px;
padding: 11px 15px;
border-bottom: 1px solid #ddd;
}
.form {
.item {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ddd;
height: 80px;
}
Input {
flex: 1;
text-align: right;
}
}
.card {
height: 100px;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ddd;
padding: 0 20rpx;
font-size: 35rpx;
&-content {
font-size: 27rpx;
color: #8c8c8c;
display: flex;
align-items: center;
justify-content: center;
}
Image {
width: 80px;
height: 80px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

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

Loading…
Cancel
Save