Merge remote-tracking branch 'origin/master'

v2
xing 1 year ago
commit 19e01cf623
  1. 6
      .env
  2. 2
      package.json
  3. 2207
      pnpm-lock.yaml
  4. 8
      src/api/brand.ts
  5. 2
      src/api/curriculum.ts
  6. 22
      src/api/home.ts
  7. 12
      src/api/illness.ts
  8. 4
      src/api/public.ts
  9. 22
      src/api/request.ts
  10. 11
      src/api/search.ts
  11. 12
      src/api/user.ts
  12. 11
      src/app.config.ts
  13. 17
      src/app.scss
  14. 20
      src/app.tsx
  15. 5
      src/components/empty/empty.module.scss
  16. 2
      src/components/empty/empty.tsx
  17. 12
      src/components/image/image.module.scss
  18. 77
      src/components/image/image.tsx
  19. 47
      src/components/loginView/index.module.scss
  20. 75
      src/components/loginView/index.tsx
  21. 33
      src/components/navigationBar/navigationBar.module.scss
  22. 56
      src/components/navigationBar/navigationBar.tsx
  23. 7
      src/components/popPut/popPut.tsx
  24. 169
      src/components/spinner/index.tsx
  25. 13
      src/components/spinner/loading.svg
  26. 76
      src/components/spinner/style.scss
  27. 16
      src/components/tabs/tabs.scss
  28. 5
      src/components/tabs/tabs.tsx
  29. 40
      src/components/topic/single.module.scss
  30. 106
      src/components/topic/single.tsx
  31. 11
      src/components/video/type.ts
  32. 34
      src/components/video/video.tsx
  33. 6
      src/components/videoCover/videoCover.scss
  34. 5
      src/components/videoCover/videoCover.tsx
  35. 5
      src/pages/business/history/history.module.scss
  36. 7
      src/pages/business/history/history.tsx
  37. 3
      src/pages/business/sort/sort.config.ts
  38. 21
      src/pages/business/sort/sort.tsx
  39. 2
      src/pages/business/userInfo/userInfo.tsx
  40. 34
      src/pages/business/videoInfo/components/catalogue.tsx
  41. 116
      src/pages/business/videoInfo/components/course.tsx
  42. 8
      src/pages/business/videoInfo/components/hours.tsx
  43. 10
      src/pages/business/videoInfo/videoInfo.scss
  44. 23
      src/pages/business/videoInfo/videoInfo.tsx
  45. 2
      src/pages/check/check.tsx
  46. 60
      src/pages/home/components/adware.tsx
  47. 6
      src/pages/home/components/curRecommended.tsx
  48. 7
      src/pages/home/components/feature.tsx
  49. 72
      src/pages/home/components/feature_recommended.tsx
  50. 14
      src/pages/home/components/search.tsx
  51. 4
      src/pages/home/home.config.ts
  52. 128
      src/pages/home/home.module.scss
  53. 78
      src/pages/home/home.tsx
  54. 75
      src/pages/index/components/videoList.tsx
  55. 4
      src/pages/index/index.config.ts
  56. 19
      src/pages/index/index.tsx
  57. 1
      src/pages/login/login.config.ts
  58. 2
      src/pages/login/login.module.scss
  59. 22
      src/pages/login/login.tsx
  60. 2
      src/pages/manage/courseAdmin/components/search.tsx
  61. 14
      src/pages/manage/courseAdmin/courseAdmin.tsx
  62. 4
      src/pages/manage/depAdmin/depAdmin.tsx
  63. 4
      src/pages/manage/selectDep/selectDep.tsx
  64. 17
      src/pages/manage/spotMeeting/spotMeeting.tsx
  65. 5
      src/pages/manage/userInfo/components/info.tsx
  66. 1
      src/pages/manage/userInfo/userInfo.module.scss
  67. 2
      src/pages/meeting/meeting.tsx
  68. 28
      src/pages/my/components/header/header.tsx
  69. 8
      src/pages/my/components/header/service.tsx
  70. 8
      src/pages/my/components/header/time.tsx
  71. 50
      src/pages/my/my.module.scss
  72. 72
      src/pages/my/my.tsx
  73. 13
      src/pages/preview/brand/article/article.tsx
  74. 13
      src/pages/preview/brand/info/info.module.scss
  75. 157
      src/pages/preview/brand/info/info.tsx
  76. 38
      src/pages/preview/brand/list/list.module.scss
  77. 97
      src/pages/preview/brand/list/list.tsx
  78. 9
      src/pages/preview/health/health.module.scss
  79. 27
      src/pages/preview/health/health.tsx
  80. 2
      src/pages/preview/illness/article/article.config.ts
  81. 25
      src/pages/preview/illness/article/article.module.scss
  82. 50
      src/pages/preview/illness/article/article.tsx
  83. 4
      src/pages/preview/illness/list/list.config.ts
  84. 38
      src/pages/preview/illness/list/list.module.scss
  85. 58
      src/pages/preview/illness/list/list.tsx
  86. 13
      src/pages/preview/illness/sort/components/search.tsx
  87. 1
      src/pages/preview/illness/sort/sort.config.ts
  88. 74
      src/pages/preview/illness/sort/sort.module.scss
  89. 140
      src/pages/preview/illness/sort/sort.tsx
  90. 7
      src/pages/preview/profession/profession.module.scss
  91. 20
      src/pages/preview/profession/profession.tsx
  92. 53
      src/pages/preview/search/search/components/list.module.scss
  93. 167
      src/pages/preview/search/search/components/list.tsx
  94. 5
      src/pages/preview/search/search/index.config.ts
  95. 50
      src/pages/preview/search/search/index.module.scss
  96. 116
      src/pages/preview/search/search/index.tsx
  97. 15
      src/pages/preview/videoFull/videoFull.tsx
  98. 4
      src/pages/preview/webView/webView.config.ts
  99. 9
      src/pages/preview/webView/webView.tsx
  100. 16
      src/static/css/module.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,7 +1,5 @@
TARO_APP_API=https://yjx.dev.yaojiankang.top
#TARO_APP_API=https://mooc.yaojiankang.top
#TARO_APP_API=https://yjx.dev.yaojiankang.top
TARO_APP_API=https://mooc.yaojiankang.top
#TARO_APP_API=https://shopfix.yaojiankang.top
#TARO_APP_API=https://playedu.yaojiankang.top
TARO_APP_LGOIN=true

@ -54,9 +54,11 @@
"@tarojs/shared": "3.6.8",
"@tarojs/taro": "3.6.8",
"dayjs": "^1.11.9",
"marked": "^7.0.4",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-refresh": "^0.11.0",
"taro-ui": "^3.1.1",
"unstated-next": "^1.1.0"
},
"devDependencies": {

File diff suppressed because it is too large Load Diff

@ -5,7 +5,7 @@ export type BrandRecord = {
name: string;
id: number
introductory_video: string
brand_album: string[]
brand_album: string
graphic_introduction: string
disabled: number
introductory_video_resource:any
@ -21,7 +21,7 @@ export const brandApi = {
/** 品牌列表 */
list(page: number , page_size: number) {
return request<{
data: BrandRecord[],
list: BrandRecord[],
total: number
}>(`/home/v1/brand/list?page=${page}&page_size=${page_size}` , "GET")
},
@ -30,11 +30,11 @@ export const brandApi = {
return request<BrandRecord>(`/home/v1/brand/${id}`, "GET")
},
/** 文章列表 */
articleList(owner_id: number ) {
articleList(owner_id: number,page:number) {
return request<{
list: ArticleRecord[],
total: number
}>(`/home/v1/article/list?owner_id=${owner_id}&page=1&page_size=1000` , "GET")
}>(`/home/v1/article/list?owner_id=${owner_id}&page=${page}&page_size=10` , "GET")
},
articleInfo(id: number ) {
return request<ArticleRecord>(`/home/v1/article/${id}` , "GET")

@ -81,7 +81,7 @@ export const curriculum = {
},
/** 查看课程课时数据 */
courseDep(id: number, depId: number | null) {
return request<CourseDepData>(`/api/v1/course/${id}${depId ? `/dep/${depId}` : ''}`, "GET")
return request<CourseDepData>(`/api/v1/course/dep`, "GET", {id, depId})
},
/** 播放 */
hourPlay(courseId: number, id: number) {

@ -16,7 +16,7 @@ export interface AdwareLinkType {
}
export interface AdwareType {
image_path:string
image_path: string
advert_link: AdwareLinkType
scope_id: number | string
id: number
@ -29,6 +29,18 @@ export interface AdwareType {
end_time?: number
}
export interface HomeData {
adverts: AdwareType[]
skill: Kill[]
health: Health[]
brand: {
list: Brand[]
}
illness: {
list: Illness[]
}
}
export const HomeApi = {
advert(only_flag: string) {
return request<AdwareType[]>("/home/v1/advert/unique?only_flag=" + only_flag, "GET")
@ -49,7 +61,7 @@ export const HomeApi = {
},
/** 品牌 */
brand(page: number, page_size: number) {
return request<{ data: Brand[], total: number }>('/home/v1/brand/list', "GET", {page, page_size})
return request<{ list: Brand[], total: number }>('/home/v1/brand/list', "GET", {page, page_size})
},
/** 技能 */
skillTop(count: number) {
@ -61,11 +73,17 @@ export const HomeApi = {
skillList(categoryId: number, page: number, page_size: number) {
return request<{ data: Kill[], total: number }>('/home/v1/skill/index', "GET", {categoryId, page, page_size})
},
skillSetPlay(id: number) {
return request(`/home/v1/skill/set_play/${id}`, "PUT")
},
/** 疾病知识 */
illness(page: number, page_size: number) {
return request<{ list: Illness[], total: number }>('/home/v1/article/illness_list', "GET", {page, page_size})
},
category(type: number) {
return request<Category[]>('/home/v1/category/index/info?type=' + type, "GET")
},
home() {
return request<HomeData>('/home/v1/home/home_page', "GET", {count: 3})
}
}

@ -3,13 +3,11 @@ import {request} from "@/api/request";
export const illnessApi = {
/** 疾病列表 */
list(id:number,page: number , page_size: number) {
return request<{
list: any[],
total: number,
}>(`/home/v1/illness/list?page=${page}&page_size=${page_size}&id=${id}` , "GET")
list(id: number, page: number, page_size: number) {
return request<{ list: any[], total: number }>(`/home/v1/illness/list`, "GET", {page, page_size, id})
},
articleInfo(id: number ) {
return request<{ content: string }>(`/home/v1/article/${id}` , "GET")
articleInfo(owner_id: number, page: number, page_size: number) {
return request<{ illness:{name:string;description:string;resource:any;album:string[]};list:{list: any[], total: number} }>
(`/home/v1/article/illness_list`, "GET", {page, page_size, owner_id})
},
}

@ -15,9 +15,9 @@ interface CategoryList {
export interface Courses {
/** 已完成 */
is_finished: Curriculum[]
// is_finished: Curriculum[]
/** 未完成 */
is_not_finished: Curriculum[]
// is_not_finished: Curriculum[]
/** 选秀 */
is_not_required: Curriculum[]
/** 必修 */

@ -44,6 +44,8 @@ export const ERROR_STATUS: Record<number | string, string> = {
'OVERSTEP': '请求越界~'
}
let notLoging = false
export function request<T = unknown>(
url: string,
method: keyof Method,
@ -82,15 +84,19 @@ export function request<T = unknown>(
const data = res?.data as any
if (data?.code === 0 && res?.statusCode === 200) {
resolve(data?.data)
notLoging = false
} else if (res.statusCode === 401) {
// Taro.showModal({
// title: "登录过期,需重新登陆",
// showCancel: false,
// success() {
Taro.clearStorageSync()
Taro.reLaunch({url: '/pages/login/login'})
// }
// })
if (notLoging || !token) return
notLoging = true
Taro.showModal({
title: "登录过期",
confirmText: "登录",
showCancel: false,
success() {
Taro.clearStorage()
Taro.navigateTo({url: '/pages/login/login'})
}
})
} else {
reject(null)
Taro.showToast({

@ -0,0 +1,11 @@
import {request} from "@/api/request";
export const SearchApi = {
/** 品牌列表 */
list(page: number , page_size: number, name: string) {
return request<{
data: any[],
total: number
}>(`/home/v1/search/home?page=${page}&page_size=${page_size}&keywords=${name}&sort_type=1&sort=0` , "GET")
},
}

@ -75,9 +75,6 @@ export const userApi = {
hourCourse(course_id: string, unique_ident: number) {
return request<HourCourse>(`/api/v1/course/${course_id}/info/${unique_ident}`, "GET")
},
meetingSave(data: any) {
return request<LoginData>(`/api/v1/auth/login/meeting/save`, "POST", data)
},
info(user_id: string) {
return request<User>(`/api/v1/statistics/${user_id}`, "GET")
},
@ -89,5 +86,12 @@ export const userApi = {
},
getCode(phone_number: number) {
return request('/api/v1/sms/send?phone_number=' + phone_number, "GET")
}
},
/** 公司列表 */
companyList() {
return request<unknown[]>(`/api/v1/company/mine_list`, "GET")
},
companyReplace(id: number) {
return request(`/api/v1/company/replace/${id}`, "PATCH")
},
}

@ -14,7 +14,7 @@ export default defineAppConfig({
navigationBarTextStyle: 'black'
},
tabBar: {
color: '#909795',
color: '#606563',
selectedColor: '#45D4A8',
list: [
{
@ -45,6 +45,10 @@ export default defineAppConfig({
'pages/my/my': {
network: 'all',
packages: ['pages/manage']
},
'pages/home/home': {
network: 'all',
packages: ['pages/preview']
}
},
subpackages: [
@ -57,8 +61,7 @@ export default defineAppConfig({
'history/history',
'curHistory/curHistory',
'hourHistory/hourHistory',
'courType/courType',
'sort/sort', // 公共二级分类
'courType/courType'
]
},
{
@ -89,6 +92,8 @@ export default defineAppConfig({
'illness/sort/sort',
'illness/list/list',
'illness/article/article',
'webView/webView',
'search/search/index',
]
},
],

@ -1,4 +1,5 @@
@import "static/css/module";
@import 'taro-ui/dist/style/index.scss';
.flex {display: flex !important;flex-direction:row}
.flex-row{ flex-direction:row!important}
@ -12,6 +13,7 @@
.justify-around{justify-content:space-around}
.justify-between{justify-content:space-between}
.justify-center{justify-content:center}
.justify-stretch{justify-content:stretch}
.flex-wrap{flex-wrap:wrap}
@ -34,6 +36,8 @@
.flex-5{flex: 5}
.flex-shrink{flex-shrink: 0}
.gap20rpx{gap: 20rpx}
.w-1 {width: 10%;min-width: 75rpx}
.w-2 {width: 20%;min-width: 150rpx}
.w-3 {width: 30%;min-width: 225rpx}
@ -84,6 +88,7 @@
.mb-1 {margin-bottom: 10rpx}
.mb-1_5 {margin-bottom: 15rpx}
.mb-2 {margin-bottom: 20rpx}
.mb-2_4 {margin-bottom: 24rpx}
.mb-3 {margin-bottom: 30rpx}
.mb-4 {margin-bottom: 40rpx}
.mb-5 {margin-bottom: 50rpx}
@ -328,6 +333,10 @@
.border-none{border: none}
.clip {
overflow: hidden;
}
.text-row1 {
display: -webkit-box;
text-overflow: ellipsis;
@ -336,4 +345,12 @@
-webkit-line-clamp:1;
}
.text-row3 {
display: -webkit-box;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient:vertical;
-webkit-line-clamp:3;
}

@ -32,20 +32,25 @@ function updateApp() {
function App(props) {
// 可以使用所有的 React Hooks
Taro.useLaunch(() => {
updateApp()
storageDep.remove()
})
Taro.getSystemInfo({
success(res) {
success({statusBarHeight = 0, screenWidth, screenHeight, windowHeight, safeArea}) {
const isIos = Taro.getSystemInfoSync().platform === 'ios';
const {top, height} = Taro.getMenuButtonBoundingClientRect()
const textBarHeight = (top - statusBarHeight) * 2 + height
Taro.getApp().globalData = {
statusBarHeight: res.statusBarHeight || 0,
screenWidth: res.screenWidth,
screenHeight: res.screenHeight,
safeArea: res.safeArea,
statusBarHeight,
screenWidth,
screenHeight,
windowHeight,
safeArea,
isIos,
textBarHeight,
menu: Taro.getMenuButtonBoundingClientRect(),
}
}
})
@ -59,7 +64,6 @@ function App(props) {
useDidHide(() => {
})
return (
<CustomWrapper>
<Profile.Provider>

@ -1,5 +1,5 @@
.empty {
width: 750rpx;
width: 100%;
text-align: center;
color: #6c757d;
position: relative;
@ -8,7 +8,8 @@
.image {
display: block;
margin: auto;
width: 600rpx;
max-width: 600rpx;
width: 80%;
}
.name {

@ -10,7 +10,7 @@ interface Props {
const Empty: FC<Props> = ({name}) => {
return (
<View className={styles.empty}>
<Image src={emptyImg} mode='widthFix' className={styles.image}/>
<Image src={emptyImg} mode='widthFix' className={styles.image} fadeIn lazyLoad/>
<View className={styles.name}>{name}</View>
</View>
)

@ -0,0 +1,12 @@
.imgBox {
position: relative;
}
.imgError {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
}

@ -0,0 +1,77 @@
import {FC, useEffect, useState} from "react";
import {Image, ImageProps, View} from "@tarojs/components";
import shard from '@/static/img/shard.png'
import styles from './image.module.scss'
import Taro from "@tarojs/taro";
interface Props extends ImageProps {
width?: number | string
height?: number | string
fallback?: string
}
const Img: FC<Props> = ({src, mode = 'aspectFill', width, height, fallback = shard, ...props}) => {
const [isError, setIsError] = useState(false)
const [loading, setLoading] = useState(true)
const imgAnimation = Taro.createAnimation({duration: 0}).opacity(0).step()
const [animationData, setAnimationData] = useState<TaroGeneral.IAnyObject>(imgAnimation.export())
useEffect(() => {
setIsError(!src)
setLoading(!!src)
}, [src])
// 图片加载失败
function onErrorHandler() {
setLoading(false)
setIsError(true)
}
function onLoadHandler() {
setLoading(false)
setIsError(false)
imgAnimation.opacity(1).step({duration: 200})
setAnimationData(imgAnimation.export())
}
return (
<View
style={{
width: `${width}rpx`,
height: `${height}rpx`,
backgroundColor: '#F8F8F8'
}}
className={`${props?.className} ${styles.imgBox}`}>
{!isError &&
<View animation={animationData}>
<Image
{...props}
src={src}
mode={mode}
lazyLoad
fadeIn
defaultSource={fallback}
style={{width: width ? `${width}rpx` : "100%", height: height ? `${height}rpx` : "100%"}}
onError={onErrorHandler}
onLoad={onLoadHandler}/>
</View>
}
{
isError && !loading &&
<Image
className={styles.imgError}
mode='aspectFill'
src={fallback}
lazyLoad
fadeIn
style={{
width: "100%",
height: "100%",
}}/>
}
</View>
)
}
export default Img

@ -0,0 +1,47 @@
.content {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
image {
width: 320rpx;
height: 208rpx;
}
.title {
font-size: 28rpx;
font-weight: bold;
color: #323635;
}
.label {
font-size: 24rpx;
font-weight: 500;
color: #909795;
line-height: 24rpx;
margin: 20rpx 0 ;
}
.button {
padding: 0 60rpx;
height: 76rpx;
background: #45D4A8;
border-radius: 38rpx 38rpx 38rpx 38rpx;
color: #fff;
line-height: 76rpx;
text-align: center;
font-size: 32rpx;
font-weight: 500;
}
.nextLabel {
font-size: 24rpx;
font-weight: 500;
color: #909795;
line-height: 24rpx;
margin-top: 15rpx;
}
}

@ -0,0 +1,75 @@
import {CSSProperties, FC, useState} from "react";
import {View, Image} from "@tarojs/components";
import styles from './index.module.scss'
import NoLogin from '@/static/img/noLogin.png'
import {Profile} from "@/store";
import Taro from "@tarojs/taro";
import {userApi} from "@/api";
interface Props {
tips?: string
height?: number
paddingTop?: number
style?: CSSProperties
offImage?: boolean
onSuccess?: VoidFunction
}
const LoginView: FC<Props> = (props) => {
const [isLoading, setLoading] = useState(false)
const {setUser, setToken, setCompany} = Profile.useContainer()
const sizeStyle: CSSProperties = {
height: `${props.height || 1000}rpx`,
paddingTop: `${props.paddingTop || 0}rpx`,
...props.style,
}
function login() {
if (isLoading) return;
Taro.showLoading({title: '微信授权中...'})
setLoading(true)
Taro.login({
success: async (res) => {
try {
const {catch_key, user, token, company} = await userApi.login(res.code)
Taro.hideLoading()
if (token) {
Taro.showToast({title: '授权成功', duration: 1500, icon: 'success', mask: true})
setTimeout(() => {
setUser(user)
setToken(token)
setCompany(company)
setLoading(false)
if (props.onSuccess) {
props.onSuccess()
} else {
Taro.switchTab({url: '/pages/home/home'})
}
}, 1500)
} else {
Taro.setStorageSync('openid', catch_key)
Taro.reLaunch({url: '/pages/check/check'})
}
} catch (e) {
Taro.hideLoading()
}
setLoading(false)
}
})
}
return (
<View className={styles.content} style={sizeStyle}>
{
!props.offImage && <>
<Image src={NoLogin}/>
<View className={styles.title}></View>
</>
}
<View className={styles.label}></View>
<View onClick={login} className={styles.button}></View>
</View>
)
}
export default LoginView

@ -0,0 +1,33 @@
.navigation {
position: sticky;
top: 0;
left: 0;
width: 730rpx;
padding-left: 20rpx;
z-index: 10;
overflow: hidden;
background: #fff;
}
.leftNode {
position: absolute;
display: flex;
left: 0;
bottom: 0;
align-items: center;
}
.text {
position: absolute;
left: 0;
right: 0;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
}
.arrow {
width: 32px;
height: 32px;
}

@ -0,0 +1,56 @@
import {Image, View} from "@tarojs/components";
import React, {FC, ReactNode, useMemo} from "react";
import styles from './navigationBar.module.scss'
import Taro from "@tarojs/taro";
import leftArrow from '@/static/img/leftArrow.png'
interface Props {
// 文本
text?: string | ReactNode
children?: ReactNode | ReactNode[]
// 左边节点
leftNode?: string | ReactNode | ReactNode[]
// 字体颜色
color?: string
// 背景颜色
backgroundColor?: string
// 取消返回按钮
cancelBack?: boolean
// 跟随页面滚动
inherit?: boolean
className?: string
}
const NavigationBar: FC<Props> = (props) => {
const globalData = Taro.getApp().globalData
const navigationBarStyle = useMemo((): React.CSSProperties => ({
background: props.backgroundColor,
position: props.inherit ? 'inherit' : "sticky",
paddingTop: globalData.statusBarHeight + 'px',
height: globalData.textBarHeight + globalData.statusBarHeight + 'px',
boxSizing: 'border-box',
}), [props])
const navigationTextStyle = useMemo((): React.CSSProperties => ({
color: props.color,
height: globalData.textBarHeight + 'px',
}), [props])
return (
<View className={`${props.className} ${styles.navigation}`} style={navigationBarStyle}>
<View style={navigationTextStyle} className={styles.leftNode}>
{!props.cancelBack && <View className="flex flex-column justify-center align-center" style={{
width: globalData.textBarHeight + 'px',
height: globalData.textBarHeight + 'px',
}} onClick={() => Taro.navigateBack()}>
<Image src={leftArrow} mode='aspectFill' className={styles.arrow}/>
</View>}
{props.leftNode}
</View>
<View style={navigationTextStyle} className={styles.text}>{props.children || props.text}</View>
</View>
)
}
export default NavigationBar

@ -1,7 +1,8 @@
import {FC, ReactNode, useEffect, useState} from "react";
import {View, Image} from "@tarojs/components";
import {View} from "@tarojs/components";
import Icon from "@/components/icon";
import CustomPageContainer from "@/components/custom-page-container/custom-page-container";
import Img from "@/components/image/image";
interface Props {
height?: number | string
@ -46,13 +47,13 @@ const PopPut: FC<Props> = ({title, chevron, content, image, isProp, children, sh
<>
<View className='card' onClick={click} style={style()}>
<View className='flex align-center'>
{opt.leftImage != null && <Image src={opt.leftImage} className='mr-3 image' mode='aspectFit'/>}
{opt.leftImage != null && <Img width={68} height={68} src={opt.leftImage} className='mr-3' mode='aspectFit'/>}
<View>{title}</View>
</View>
<View className='card-content'>
<View>{content}</View>
{!chevron && <Icon name='chevron-right'/>}
{image && <Image src={image} mode='scaleToFill' className='image'/>}
{image && <Img src={image} mode='scaleToFill' className='image' width={68} height={68}/>}
</View>
</View>
{

@ -0,0 +1,169 @@
import Taro from "@tarojs/taro";
import {View, Image} from '@tarojs/components'
import {Component, ReactNode} from "react";
import indicator from './loading.svg'
import './style.scss'
// 动画状态
type Status =
| 'dismissed'
| 'forward'
| 'reverse'
| 'completed'
interface Props {
enable?: boolean // 控制显现
overlay?: boolean // 页面覆盖
block?: boolean // 块级
}
interface State {
status?: Status
background?: Record<string, unknown>,
rotation?: Record<string, unknown>,
}
type StateSetter = (state: State) => void
type Controller = {
setTick: (enabled: boolean | undefined) => void
clear: () => void
}
function createController(setState: StateSetter): Controller {
const background = Taro.createAnimation({duration: 600})
const rotation = Taro.createAnimation({duration: 600})
let rotateTimer: ReturnType<typeof setTimeout> | undefined
let status: Status | undefined
const notifyListener = () => setState({
status,
background: background.export(),
rotation: rotation.export(),
})
const clearAnimation = (notify = true) => {
// 清空旋转动画定时器
if (rotateTimer != null) {
clearTimeout(rotateTimer)
rotateTimer = undefined
}
// 清空动画
background.step().export()
rotation.step().export()
// 通知 UI 刷新
if (notify) {
notifyListener()
}
}
const setAnimation = (opacity: number) => {
// 停止旋转动画
if (rotateTimer != null) {
clearTimeout(rotateTimer)
}
// 旋转动画定时器
const rotate = () => {
rotation.opacity(opacity).rotate(360).step({duration: 600})
notifyListener()
rotateTimer = setTimeout(rotate, 600)
}
// 背景动画
background.backgroundColor(`rgba(255,255,255,${opacity})`).step()
// 启动旋转动画
rotate()
}
const onFinish = (opacity: number, nextStatus: Status) => {
const lockStatus = status
if (lockStatus === status) {
background.backgroundColor(`rgba(255,255,255,${opacity})`).step({duration: 0})
if (nextStatus === 'dismissed') {
clearAnimation()
}
status = nextStatus
notifyListener()
}
}
const setStatus = (newStatus: Status, opacity: number) => {
if (status !== newStatus) {
status = newStatus
setAnimation(opacity)
if (status === 'reverse') {
onFinish(0, 'dismissed')
} else if (status === 'forward') {
onFinish(1, 'completed')
}
}
}
const setTick = (enabled?: boolean) => {
if (enabled !== true) {
if (status === 'dismissed') {
clearAnimation()
} else if (status !== 'reverse') {
setStatus('reverse', 0)
}
} else if (status === 'completed') {
// ignore
} else if (status !== 'forward') {
setStatus('forward', 1)
}
}
return {
setTick,
clear() {
clearAnimation(false)
},
}
}
export default class Spin extends Component<Props, State> {
constructor(props) {
super(props)
this.controller = createController(this.onAnimation)
this.state = {}
}
controller: Controller
onAnimation = (state: State) => {
this.setState((s) => ({...s, ...state}))
}
// componentDidMount(): void {
// this.controller.setTick(this.props.enable)
// }
componentDidUpdate(): void {
this.controller.setTick(this.props.enable)
}
componentWillUnmount(): void {
this.controller.clear()
}
shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<State>): boolean {
return nextProps.enable !== this.props.enable
|| nextProps.overlay !== this.props.overlay
|| nextState.status != this.state.status
}
render(): ReactNode {
return (
<View
className={`spinner-wrapper ${this.state.status} ${this.props.overlay && 'is-fixed'} ${this.props.block && 'is-block'}`}>
<View className={`spinner ${this.state.status}`}>
<Image className="spinner-icon" src={indicator}/>
</View>
</View>
)
}
}

@ -0,0 +1,13 @@
<svg width="48" height="48" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-icon="spin">
<defs>
<linearGradient x1="0%" y1="100%" x2="100%" y2="100%" id="linearGradient-1">
<stop stop-color="currentColor" stop-opacity="0" offset="0%"></stop>
<stop stop-color="currentColor" stop-opacity="0.50" offset="39.9430698%"></stop>
<stop stop-color="currentColor" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect fill-opacity="0.01" fill="none" x="0" y="0" width="36" height="36"></rect>
<path d="M34,18 C34,9.163444 26.836556,2 18,2 C11.6597233,2 6.18078805,5.68784135 3.59122325,11.0354951" stroke="url(#linearGradient-1)" stroke-width="4" stroke-linecap="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 813 B

@ -0,0 +1,76 @@
.spinner-wrapper {
transition: background-color 1200ms ease-out;
&.is-block {
width: auto;
position: absolute;
z-index: 99999;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&.is-fixed {
background-color: rgba(#fff, 1.0);
z-index: 99999;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&.reverse,
&.dismissed {
background-color: rgba(#fff, 0.0);
}
&.dismissed {
position: fixed;
width: 0;
height: 0;
left: -10000px;
top: -10000px;
right: auto;
bottom: auto;
overflow: hidden;
}
}
.spinner-wrapper,
.spinner {
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
}
.spinner {
padding: 10rpx;
opacity: 1;
transition: opacity 1200ms ease-out;
&.reverse,
&.dismissed {
opacity: 0;
}
}
.spinner-icon {
width: 38px;
height: 38px;
animation: spinner-rotation 600ms linear infinite;
}
@keyframes spinner-rotation {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}

@ -15,11 +15,23 @@ View::-webkit-scrollbar {
display: -webkit-flex;
display: flex;
text-align: center;
align-items: baseline;
}
.tabs-item {
padding: 16rpx 20rpx;
font-size: 30rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #606563;
}
.current {
position: relative;
font-size: 36rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #323635;
&:after {
position: absolute;
@ -37,10 +49,6 @@ View::-webkit-scrollbar {
transition: all 200ms;
}
}
.tabs-item {
padding: 20rpx;
}
}
@keyframes spread {

@ -19,6 +19,7 @@ export type OnChangOpt<T = unknown> = {
interface TabsProps {
current?: number | string
tabList: TabList[]
height?: number | string
onChange?: (data: OnChangOpt) => void
}
@ -47,8 +48,8 @@ const Tabs: FC<TabsProps> = (opt: TabsProps) => {
return (
<View className='tabs'>
<ScrollView scrollX scrollLeft={left} scrollWithAnimation showScrollbar={false}>
<View className='tabs' style={{height: opt.height}}>
<ScrollView scrollX scrollLeft={left} scrollWithAnimation showScrollbar={false} style={{height: opt.height}}>
<View className={'tabs-scroll ' + (opt.tabList.length < 5 ? 'justify-around' : '')}>
{opt.tabList.map((d, index) => <View
key={index}

@ -0,0 +1,40 @@
.singleCover {
position: absolute;
bottom: 0;
top: 0;
margin: auto;
height: 220rpx;
right: 20rpx;
max-width: 50%;
min-width: 300rpx;
padding: 25rpx;
background: rgba(#000, .7);
border-radius: 20rpx;
line-height: 40rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
view:first-child {
font-weight: bold;
color: #fff;
}
view:not(:first-child) {
background: #fff;
border-radius: 10rpx;
padding: 15rpx;
}
}
.correct {
background: #bdf6c1 !important;
color: #fff !important;
}
.mistake {
background: #e7abab !important;
color: #fff !important;
}

@ -0,0 +1,106 @@
import {FC, useEffect, useMemo, useState} from "react";
import {View} from "@tarojs/components";
import Taro from "@tarojs/taro";
import styles from './single.module.scss'
interface Props {
full: boolean
topic?: ShareSubject
examination: (result: boolean) => void
}
type AnswerType = "true" | 'false'
export const Single: FC<Props> = (props) => {
let timer: NodeJS.Timeout
const [lastState, setLastState] = useState<0 | 1>(0) // 0为竖屏,1为横屏
const [result, setResult] = useState<AnswerType | undefined>(undefined)
let lastTime = Date.now()
Taro.startAccelerometer()
Taro.onAccelerometerChange((res) => {
const now = Date.now();
if (now - lastTime < 500) return;
lastTime = now;
let nowState;
const Roll = Math.atan2(-res.x, Math.sqrt(res.y * res.y + res.z * res.z)) * 57.3;
const Pitch = Math.atan2(res.y, res.z) * 57.3;
// 横屏状态
if (Roll > 50) {
if ((Pitch > -180 && Pitch < -60) || (Pitch > 130)) {
nowState = 1;
} else {
nowState = lastState;
}
} else if ((Roll > 0 && Roll < 30) || (Roll < 0 && Roll > -30)) {
let absPitch = Math.abs(Pitch);
// 如果手机平躺,保持原状态不变,40容错率
if ((absPitch > 140 || absPitch < 40)) {
nowState = lastState;
} else if (Pitch < 0) {
// 收集竖向正立的情况
nowState = 0;
} else {
nowState = lastState;
}
} else {
nowState = lastState;
}
// 状态变化时,触发
if (nowState !== lastState) {
setLastState(nowState);
}
})
useEffect(() => {
timer = setTimeout(() => {
props.examination(false)
}, 4000)
}, [props.topic])
const style: React.CSSProperties = useMemo(() => ({
transform: lastState === 1 && props.full ? "rotate(90deg)" : 'none'
}), [lastState, props.full])
function examination(answer: AnswerType) {
if (result) return;
clearTimeout(timer)
setResult(answer)
setTimeout(() => {
props.examination(props.topic?.right_value === result)
setResult(undefined)
}, 2000)
}
function judgment(answer: AnswerType): string {
if (result !== answer) return ''
if (props.topic?.right_answer === answer && result === answer) {
return styles.correct
}
return styles.mistake
}
return (
<>
{
props.topic && <View className={styles.singleCover} style={style}>
<View>{props.topic.question}</View>
<View className={judgment("true")}
onClick={() => examination("true")}>
{props.topic.right_value}
</View>
<View className={judgment("false")}
onClick={() => examination("false")}>
{props.topic.error_value}
</View>
</View>
}
</>
)
}
export default Single

@ -1,3 +1,5 @@
import {ReactNode} from "react";
export interface HVideoOptions {
/** 视频时长s */
duration: number
@ -9,11 +11,12 @@ export interface HVideoOptions {
poster?: string
/** 视频断点 */
breakpoint: number[]
/** 进入断点 */
onBreakpoint: (id: number) => void
onBreakpoint: (time: number) => void
/** 视频播放结束 */
onEnded: () => void
setTime: (fn:(time:number)=>void) => void
// setTime: (fn: (time: number) => void) => void
/** 全屏改变 */
fullChange: (fullScreen: boolean) => void
children?: ReactNode
}

@ -1,7 +1,7 @@
import {BaseEventOrig, Video, VideoProps} from "@tarojs/components";
import {HVideoOptions} from "@/components/video/type";
import Taro from "@tarojs/taro";
import {FC, useState} from "react";
import {FC, useEffect, useState} from "react";
import unique_ident from "@/hooks/unique_ident";
import videoEvents from "@/hooks/videoEvents";
@ -12,6 +12,10 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
try {
video = Taro.createVideoContext('myVideo')
} catch (e) {
}
useEffect(() => {
videoEvents.onSetVideoState(({name}) => {
switch (name) {
case "pause":
@ -22,8 +26,7 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
break
}
})
} catch (e) {
}
}, [])
function onTimeUpdate(event: BaseEventOrig<VideoProps.onTimeUpdateEventDetail>) {
const time = event.detail.currentTime
@ -41,31 +44,15 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
/** 判断是否进入断点 */
opt.breakpoint.forEach(d => {
if (time < d + deviation && time > d - deviation) {
video?.pause()
video?.seek(d - deviation)
if (process.env.TARO_ENV === 'h5') {
try {
document?.exitFullscreen().then()
} catch (e) {
}
}
video?.exitFullScreen()
opt.onBreakpoint(d)
return
}
})
}
opt.setTime((time?: number) => {
if (typeof time === 'number') {
video?.seek(time)
}
video?.play()
})
function onEnded() {
if (currentTime + 1 > opt.duration) {
opt.onEnded()
videoEvents.videoState('pause')
} else {
video?.seek(currentTime)
unique_ident.remove()
@ -98,7 +85,6 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
Taro.useUnload(() => {
unique_ident.put(undefined, currentTime)
unique_ident.remove()
unique_ident.upload().then()
})
@ -116,12 +102,14 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
poster={opt?.poster || ''}
src={opt.src}
enableProgressGesture={opt.preview}
direction={90}
onTimeUpdate={onTimeUpdate}
onEnded={onEnded}
onPlay={onPlay}
onPause={onPause}
/>
onFullScreenChange={(event) => opt.fullChange((event.target as any).fullScreen)}
>
{opt.children}
</Video>
</>
)
}

@ -51,7 +51,7 @@
.box {
box-sizing: border-box;
padding: 15rpx;
padding: 20rpx;
.title {
width: 100%;
@ -61,6 +61,10 @@
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
font-size: 28rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #323635;
}
}

@ -1,7 +1,8 @@
import {Image, View} from "@tarojs/components";
import {View} from "@tarojs/components";
import {FC} from "react";
import './videoCover.scss'
import Taro from "@tarojs/taro";
import Img from "@/components/image/image";
interface VideoCoverProps {
thumb: string
@ -28,7 +29,7 @@ const VideoCover: FC<VideoCoverProps> = (opt: VideoCoverProps) => {
<View className='videoBox'>
<View className='video' onClick={jump}>
<View className='upper'>
<Image src={opt.thumb} mode='widthFix'/>
<Img height={180} src={opt.thumb} mode='widthFix'/>
{opt.content && <View className='content'>{opt.content}</View>}
{opt.marker && <View className='marker'>{opt.marker}</View>}
</View>

@ -6,11 +6,6 @@
background: #fff;
}
.image {
width: 100%;
height: 100%;
display: block;
}
.thumb {
background: #ddd;

@ -1,10 +1,11 @@
import {Image, Text, View} from "@tarojs/components";
import {Text, View} from "@tarojs/components";
import styles from './history.module.scss'
import Taro from "@tarojs/taro";
import {useEffect, useState} from "react";
import {formatMinute} from "@/utils/time";
import Empty from "@/components/empty/empty";
import {userApi} from "@/api";
import Img from "@/components/image/image";
const History = () => {
const [data, setData] = useState<HourHistory[]>([])
@ -25,12 +26,11 @@ const History = () => {
}, [])
return (
<View>
<View className='mt-3'>
{data.length ? data.map((d, index) =>
<View key={index} className={styles.category} onClick={() => jump(d.userCourseRecord.course_id)}>
<View className={styles.thumb}>
<Image src={d.thumb} className={styles.image}/>
<Img src={d.thumb} className={styles.image} width={300} height={188}/>
<View
className={styles.count}>{d.userCourseRecord.hour_count}/{d.userCourseRecord.finished_count}</View>
</View>
@ -43,7 +43,6 @@ const History = () => {
</View>
</View>) : <Empty name='无观看记录'/>}
</View>
</View>
)
}

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

@ -1,21 +0,0 @@
import {FC} from "react";
import {View} from "@tarojs/components";
import {useRouter} from "@tarojs/taro";
type SortType = ''
interface Params {
id: number
type: SortType
jumpUrl: (data:Record<string, any>) => void
}
const Sort: FC = () => {
const params = useRouter().params as unknown as Params
return (
<View>ds</View>
)
}
export default Sort

@ -45,7 +45,7 @@ const List = () => {
Taro.showModal({
title: '是否确定退出登录',
success({confirm}) {
confirm && empty()
confirm && empty(true,true)
}
})
}

@ -9,7 +9,9 @@ import MyButton from "@/components/button/MyButton";
import videoEvents from "@/hooks/videoEvents";
import curRecord from '@/static/img/curRecord.png'
import hourRecord from "@/static/img/hourRecord.png"
import {PageContainerInner} from "@/components/custom-page-container/custom-page-container";
import CustomPageContainer from "@/components/custom-page-container/custom-page-container";
import {Profile} from "@/store";
import LoginView from "@/components/loginView";
interface Props {
data: CourseDepData | null
@ -29,6 +31,7 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
const [current, setCurrent] = useState(1)
const [show, setShow] = useState(false)
const [playing, setPlaying] = useState(false)
const {token} = Profile.useContainer()
videoEvents.onVideoState(({name}) => {
@ -96,7 +99,8 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
function learning() {
const flats: Hour[] = Object.values(data?.hours || {}).flat(Infinity) as Hour[]
if ((data?.learn_hour_records?.length || undefined) == data?.learn_record?.hour_count && flats.length) {
if (!flats.length) return;
if (!data?.learn_hour_records?.length || data.learn_hour_records.length === data.learn_record?.hour_count) {
if (flats[0].id === playId) {
videoEvents.setVideoState('play')
} else {
@ -151,6 +155,19 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
{current === 0 && <View className='short_desc'>{data?.course.short_desc || data?.course.title}</View>}
{current === 1 && <View>
<View className='my-2'></View>
{!token && <LoginView offImage style={{
position: 'absolute',
left: 0,
height: 'auto',
right: 0,
bottom: 0,
top: 0,
margin: 'auto',
paddingTop: '100rpx',
justifyContent: 'flex-end',
flexDirection: 'column-reverse',
background: 'rgba(255,255,255,.9)'
}}/>}
{data?.chapters.length
? Object.values(data?.chapters || {}).map((d, index) => <View key={d.id}>
<Collapse title={`${index + 1}.${d.name}`}>
@ -172,16 +189,17 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
</View>
</View>
<View className='Videobutton'>
{
playing ? <MyButton style={{flex: '1'}} onClick={onPause}></MyButton>
: <MyButton style={{flex: '1'}} onClick={learning}></MyButton>
token && <View className='Videobutton'>
{
playing ? <MyButton className='flex-1' onClick={onPause}></MyButton>
: <MyButton className='flex-1' onClick={learning}></MyButton>
}
<View className='px-3' onClick={() => setShow(true)}>...</View>
</View>
}
<PageContainerInner
<CustomPageContainer
show={show}
position='bottom'
onClickOverlay={() => setShow(false)}>
@ -199,7 +217,7 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
</View>
<MyButton onClick={() => setShow(false)} type='default' fillet></MyButton>
</View>
</PageContainerInner>
</CustomPageContainer>
</>
);
}

@ -1,14 +1,10 @@
import {ScrollView, Text, View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import {FC, useCallback, useEffect, useState} from "react";
import HVideo from "@/components/video/video";
import {curriculum, HourPlayData} from "@/api";
import {Profile} from '@/store'
import Taro from "@tarojs/taro";
import Judge from "@/components/topic/judge";
import unique_ident from "@/hooks/unique_ident";
import MyButton from "@/components/button/MyButton";
import {formatMinute} from "@/utils/time";
import CustomPageContainer from "@/components/custom-page-container/custom-page-container";
import Single from "@/components/topic/single";
interface Props {
id: number,
@ -17,25 +13,23 @@ interface Props {
curEnd: (test?: boolean) => void
}
let seek: (time: number) => void
const Course: FC<Props> = ({id, courseId, preview, curEnd}) => {
const [breakpoint, setBreakpoint] = useState<number[]>([]) // 断点
const [show, setShow] = useState(false) // 题
const [breakpoints, setBreakpoints] = useState<number[]>([]) // 断点
const [isFull, setIsFull] = useState(false)
const [examAll, setExamAll] = useState<Record<number, ShareSubject[]>>([]) // 题库
const [data, setData] = useState<HourPlayData | null>(null)
const [examAll, setExamAll] = useState<Record<number, (ShareSubject | Multi)[]>>([]) // 题库
const [time, setTime] = useState<number>(0) // 进入断点的时间
const [validate, setValidate] = useState(false) // 开启验证
const [record, setRecord] = useState<boolean[]>([]) // 考题记录
const [testId, setTestId] = useState<number | null>(null)
const {user} = Profile.useContainer()
async function onEnded() {
// 学习记录
const startRecording = unique_ident.get()
startRecording && await curriculum.curEnd(courseId, id, {...startRecording, duration: data?.duration!}) // 结束
unique_ident.remove()
if (testId) {
if (preview) { // 预览
if (preview) {
Taro.showModal({
title: "是否前往考试",
success({confirm}) {
@ -56,65 +50,33 @@ const Course: FC<Props> = ({id, courseId, preview, curEnd}) => {
}
}
/** 进入断点 */
function onBreakpoint(breakpoint: number) {
setTime(breakpoint)
setShow(true)
}
async function getData() {
unique_ident.put(id, Date.now())
const res = await curriculum.hourPlay(courseId, id)
if (res) {
setData(res)
setBreakpoint(res.timeList)
setBreakpoints(res.timeList)
setExamAll(res.hourExamQuestions || [])
setTestId(res?.hour_test?.id || null)
}
}
useEffect(() => {
init()
getData()
}, [id])
function init(show = true) {
show && setShow(false)
setValidate(false)
setRecord([])
setTime(0)
}
useEffect(() => {
if (!record.length) return;
const pass = record.every(d => d)
/** 考题正确 */
const examination = useCallback((result: boolean) => {
if (!time) return
const {id: question_id, question_type} = examAll?.[time]?.[0]
curriculum.answerRecord(id, {
is_pass: pass,
is_pass: result,
user_id: user?.id!,
time: time,
question_type,
question_id
}).then()
/** 删除断点 */
const old: number[] = JSON.parse(JSON.stringify(breakpoint))
const index = old.indexOf(time)
old.splice(index, 1)
setBreakpoint(old)
if (pass) {
seek(time)
init()
}
}, [record])
function videoSeek(fn: (time: number) => void) {
seek = fn
}
})
setTime(0)
}, [time])
return (
<>
@ -123,44 +85,16 @@ const Course: FC<Props> = ({id, courseId, preview, curEnd}) => {
preview={preview}
src={data?.url || ''}
onEnded={onEnded}
breakpoint={breakpoint}
onBreakpoint={onBreakpoint}
setTime={videoSeek}
/>
<CustomPageContainer
show={show}
position='bottom'
>
<View>
<View className='text-center mt-2 text-muted'>
<Text className='mr-2'>{formatMinute(time)}</Text>
</View>
{examAll?.[time]?.slice(0, 1)?.map((d) =>
<ScrollView style='height:60vh' scrollY key={d.id}>
{d.question_type === 2 &&
<Judge
data={d as ShareSubject}
validate={validate}
onAnswer={(isAnswer) => setRecord([isAnswer])}
/>}
</ScrollView>
)}
<View>
<View className='statistics'>
{
record.length > 0
? <MyButton fillet onClick={() => {
init();
seek(time)
}}></MyButton>
: <MyButton fillet onClick={() => setValidate(true)}></MyButton>
}
</View>
</View>
</View>
</CustomPageContainer>
breakpoint={breakpoints}
fullChange={(fullScreen) => setIsFull(fullScreen)}
onBreakpoint={(now) => {
time !== now && setTime(now)
}}>
<Single
full={isFull}
examination={examination}
topic={examAll?.[time]?.[0]}/>
</HVideo>
</>
)
}

@ -7,6 +7,7 @@ import {formatMinute} from "@/utils/time";
import Taro from "@tarojs/taro";
import {curriculum} from "@/api";
import lock from '@/static/img/lock.png'
import {Profile} from "@/store";
interface Props {
playId: number | null
@ -23,6 +24,7 @@ async function jumTest(hour: Hour) {
}
const Hours: FC<Props> = ({data, click, learn_hour_records, playId}) => {
const {token, empty} = Profile.useContainer()
const complete = (id: number): number | undefined => {
const find = learn_hour_records?.find(d => d.id === id)
if (find) {
@ -32,7 +34,11 @@ const Hours: FC<Props> = ({data, click, learn_hour_records, playId}) => {
}
}
function onClick(id: number, is_complete: number | undefined, hour: Hour, upId?: number,) {
function onClick(id: number, is_complete: number | undefined, hour: Hour, upId?: number) {
if (!token) {
empty()
return;
}
if (is_complete === 0) {
Taro.showModal({
title: '考卷未完成,是否前往',

@ -7,11 +7,6 @@
height: 500rpx;
}
.image {
width: 100%;
height: 100%;
display: block;
}
.header {
margin-bottom: 10px;
@ -33,6 +28,11 @@
line-height: 1.75;
word-break: break-word;
}
.hours {
position: relative;
min-height: 440rpx;
}
}
.hor {

@ -1,4 +1,4 @@
import {Image, Text, View} from "@tarojs/components";
import {Text, View} from "@tarojs/components";
import {FC, useCallback, useEffect, useState} from "react";
import {CourseDepData, curriculum} from "@/api";
import './videoInfo.scss'
@ -9,6 +9,7 @@ import eventsIndex from "@/hooks/eventsIndex";
import {formatMinute} from "@/utils/time";
import videoEvents from "@/hooks/videoEvents";
import unique_ident from "@/hooks/unique_ident";
import Img from "@/components/image/image";
const VideoInfo: FC = () => {
const {id, depId} = Taro.getCurrentInstance()?.router?.params as any
@ -17,13 +18,13 @@ const VideoInfo: FC = () => {
const [preview, setPreview] = useState(false) // 预览
const [playing, setPlaying] = useState(false) // 学习中
const getData = useCallback(async (playing: boolean) => {
try {
const res = await curriculum.courseDep(id, depId)
if (res) {
setData(res)
}
if (playId != null) { // 用于自动播放 判断当前课程是否完成
currentVideo(res, playing)
res && setData(res)
playId != null && currentVideo(res, playing) // 用于自动播放 判断当前课程是否完成
} catch (e) {
}
}, [playing, playId])
@ -73,9 +74,7 @@ const VideoInfo: FC = () => {
}
}, [playId, data, preview])
/**
*
*/
/** 判断当前课程是否完成 */
const currentVideo = useCallback((data: CourseDepData, playing: boolean) => {
const courseHourRecordsFinish = data?.learn_hour_records.find(d => d.id === playId)?.courseHourRecordsFinish
if (typeof courseHourRecordsFinish === 'number') {
@ -100,18 +99,16 @@ const VideoInfo: FC = () => {
data && getData(playing)
})
Taro.useUnload(() => {
videoEvents.videoOff()
})
return (
<>
<View className='content'>
<View className='content-video'>
{
playId
? <Course id={playId} courseId={id} curEnd={curEnd} preview={preview}/>
: <Image src={data?.course.thumb || ''} className='image' mode='aspectFill'/>
: <Img width={750} height={500} src={data?.course.thumb || ''} mode='aspectFill'/>
}
</View>
@ -128,8 +125,8 @@ const VideoInfo: FC = () => {
</View>
<Catalogue data={data} setHors={setHors} id={id} playId={playId}/>
</View>
</>
)
}
export default VideoInfo

@ -50,7 +50,7 @@ const Bing: FC = () => {
setCompany(res.company)
setUser(res.user)
setToken(res.token)
Taro.switchTab({url: '/pages/index/index'})
Taro.switchTab({url: '/pages/home/home'})
}
} catch (e) {
}

@ -1,16 +1,25 @@
import {Image, Swiper, SwiperItem, View} from "@tarojs/components";
import {FC, useState} from "react";
import {AdwareType, HomeApi} from "@/api";
import {Swiper, SwiperItem, View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import {AdwareType} from "@/api";
import Taro from "@tarojs/taro";
import styles from '../home.module.scss'
import Img from "@/components/image/image";
const Adware: FC = () => {
const [data, setData] = useState<AdwareType[]>([])
interface Props {
data: any[]
only_flag: string
width: number
}
async function getAdware() {
const res = await HomeApi.advert('home_space')
setData(res)
}
const Adware: FC<Props> = ({data, only_flag, width}) => {
const [adverts, setAdverts] = useState<AdwareType[]>([])
const [space, setSpace] = useState<any | null>(null)
useEffect(() => {
const res = data.find(d => d.only_flag === only_flag)
setSpace(res)
setAdverts(res?.adverts || [])
}, [data])
function jumpAdware(url: string) {
if (url.substring(0, 3) === 'wx:') {
@ -20,38 +29,41 @@ const Adware: FC = () => {
}
}
Taro.useLoad(getAdware)
return (
<View>
<>
{
adverts.length > 0 && <View className={styles.adware}>
{
data.length === 1 && <Image
src={data[0].image_url}
adverts.length === 1 && <Img
src={adverts[0].image_url}
mode='scaleToFill'
lazyLoad
fadeIn
onClick={() => jumpAdware(data[0].image_path)}
className={styles.adware}/>
onClick={() => jumpAdware(adverts[0].image_path)}
width={width}
height={(space.height / space.width) * width}/>
}
{
data.length > 1 && <Swiper
adverts.length > 1 && <Swiper
indicatorDots
autoplay
circular
indicatorActiveColor='rgba(255,255,255,0.5)'
className={styles.adware}>
{data.map(d => <SwiperItem key={d.id}>
<Image
style={{width: width + "rpx", height: (space.height / space.width) * width + "rpx", overflow: 'hidden'}}
indicatorActiveColor='rgba(255,255,255,0.5)'>
{adverts.map(d => <SwiperItem key={d.id}>
<Img
src={d.image_url}
mode='widthFix'
lazyLoad
fadeIn
onClick={() => jumpAdware(d.image_path)}
style={{width: "100%"}}/>
width={width}
height={(space.height / space.width) * width}
onClick={() => jumpAdware(d.image_path)}/>
</SwiperItem>)}
</Swiper>
}
</View>
}
</>
)
}

@ -37,9 +37,9 @@ const CurRecommended: FC = () => {
return (
<>
{
data.length > 0 && <View className='mt-6'>
data.length > 0 && <View>
<Image src={courseTag} mode='widthFix' className={styles.courseTag}/>
<View className={'py-2 flex justify-between flex-wrap ' + styles.videoListBox}>
<View className={'pb-2 flex justify-between flex-wrap ' + styles.videoListBox}>
{
data.map(c => <VideoCover
thumb={c.thumb}
@ -52,7 +52,7 @@ const CurRecommended: FC = () => {
</View>
</View>
}
<View className='text-center'>- -</View>
<View className='text-center text-muted pb-3 font-28'>- -</View>
</>
)
}

@ -4,6 +4,7 @@ import illness from '@/static/img/illness.png'
import profession from '@/static/img/profession.png'
import health from '@/static/img/health.png'
import article from '@/static/img/article.png'
import styles from '../home.module.scss'
import Taro from "@tarojs/taro";
const Feature: FC = () => {
@ -19,11 +20,11 @@ const Feature: FC = () => {
}
return (
<View className='flex justify-around mt-4'>
<View className='flex justify-around' style={{marginBottom: '20px'}}>
{
list.map(d => <View className='text-center' onClick={() => jump(d.url)}>
<Image src={d.image} style={{width: '48px',height: '48px'}} />
<View className='mt-1 text-dark font-26 mt-1'>{d.text}</View>
<Image src={d.image} style={{width: '48px', height: '48px'}}/>
<View className={styles.featureList}>{d.text}</View>
</View>)
}
</View>

@ -10,6 +10,7 @@ import brandTop from '@/static/img/brandTop.png'
import illnessTop from '@/static/img/illnessTop.png'
import healthTop from '@/static/img/healthTop.png'
import professionTop from '@/static/img/professionTop.png'
import Img from "@/components/image/image";
interface DataContent {
@ -25,9 +26,17 @@ interface Data {
url: string
detailsUrl: string
data: DataContent[]
type?: 'health' | 'kill'
}
const FeatureRecommended: FC = () => {
interface Props {
skill: Kill[] // 技能
health: Health[] // 健康
brand: Brand[] // 品牌
illness: Illness[] // 疾病
}
const FeatureRecommended: FC<Props> = (props) => {
const [data, setData] = useState<Data[]>([
{
titleUrl: brandTop,
@ -39,18 +48,20 @@ const FeatureRecommended: FC = () => {
titleUrl: healthTop,
url: '/pages/preview/health/health',
detailsUrl: '/pages/preview/videoFull/videoFull',
data: []
data: [],
type: "health"
},
{
titleUrl: professionTop,
url: '/pages/preview/profession/profession',
detailsUrl: '/pages/preview/videoFull/videoFull',
data: []
data: [],
type: 'kill'
},
{
titleUrl: illnessTop,
url: '/pages/preview/illness/sort/sort',
detailsUrl: '/pages/preview/illness/article/article',
detailsUrl: '/pages/preview/illness/list/list',
data: []
},
])
@ -58,8 +69,7 @@ const FeatureRecommended: FC = () => {
/** 品牌 */
async function getBrand(): Promise<DataContent[]> {
try {
const res = await HomeApi.brand(1, 3)
return res.data.map<DataContent>(d => ({
return props.brand.map<DataContent>(d => ({
id: d.id,
title: d.name,
imageUrl: d.brand_album,
@ -74,13 +84,12 @@ const FeatureRecommended: FC = () => {
/** 健康知识 */
async function getHealth(): Promise<DataContent[]> {
try {
const res = await HomeApi.healthTop(3)
return res.map<DataContent>(d => ({
return props.health?.map<DataContent>(d => ({
id: d.id,
title: d.title,
imageUrl: d.url_path,
description: d.introduction,
path: `?url=${d.resource.url}&poster=${d.url_path}&title=${d.resource.name}`
path: `?url=${d.resource?.url}&poster=${d.url_path}&title=${d.title}`
}))
} catch (e) {
}
@ -90,13 +99,12 @@ const FeatureRecommended: FC = () => {
/** 技能 */
async function getKill(): Promise<DataContent[]> {
try {
const res = await HomeApi.skillTop(3)
return res.map<DataContent>(d => ({
return props.skill.map<DataContent>(d => ({
id: d.id,
imageUrl: d.url_path,
description: '',
title: d.resource.name,
path: `?url=${d.resource.url}&poster=${d.url_path}&title=${d.resource.name}`
description: d.introduction,
title: d.title,
path: `?url=${d.resource?.url}&poster=${d.url_path}&title=${d.title}`
}))
} catch (e) {
}
@ -106,12 +114,11 @@ const FeatureRecommended: FC = () => {
/** 疾病 */
async function getIllness(): Promise<DataContent[]> {
try {
const res = await HomeApi.illness(1, 3)
return res.list.map<DataContent>(d => ({
return props.illness.map<DataContent>(d => ({
id: d.id,
imageUrl: '',
description: d.content,
title: d.title,
description: d.description || '暂无简介',
title: d.name,
path: `?id=${d.id}`
}))
} catch (e) {
@ -125,19 +132,26 @@ const FeatureRecommended: FC = () => {
oldData[0].data = brand
oldData[1].data = health
oldData[2].data = kill
console.log(illness)
oldData[3].data = illness
setData(oldData)
})
}, [])
}, [props])
function jump(url: string) {
// TODO 后续增加播放量使用公共接口
function jump(url: string, playId?: number, type?: 'health' | 'kill') {
if (playId && type) {
if (type === 'health') {
HomeApi.healthSetPlay(playId)
} else if (type === 'kill') {
HomeApi.skillSetPlay(playId)
}
}
Taro.navigateTo({url})
}
return (
<View className={styles.feature}>
<Swiper nextMargin='30px' style={{height: '250px'}}>
<Swiper nextMargin='30px' style={{height: '440rpx'}}>
{
data.map(d => <SwiperItem key={d.url}>
<Image
@ -145,17 +159,19 @@ const FeatureRecommended: FC = () => {
className={styles.featureTitle}
onClick={() => jump(d.url)} src={d.titleUrl}/>
{
d.data.map((c, index) => <View
d.data.length > 0 && d.data.map((c, index) => <View
className='flex mb-3'
key={c.id}
onClick={() => jump(d.detailsUrl + c.path)}>
<View>
<Image src={c.imageUrl} className={styles.featureImage} mode='aspectFill'/>
onClick={() => jump(d.detailsUrl + c.path, c.id, d.type)}>
<View style={{position: 'relative'}}>
<View className={styles.featureImage}>
<Img src={c.imageUrl} height={100} width={140}/>
</View>
<Image src={[first, second, third][index]} className={styles.ranking} mode='aspectFill'/>
</View>
<View className={styles.featureText}>
<View className='text-ellipsis text-secondary'>{c.title}</View>
<View className='font-26 mt-1 text-muted text-ellipsis'>{c.description}</View>
<View className={styles.featureTextTitle}>{c.title}</View>
<View className={styles.featureTextDescription}>{c.description}</View>
</View>
</View>)
}

@ -1,13 +1,19 @@
import {FC} from "react";
import {Text, View} from "@tarojs/components";
import {View} from "@tarojs/components";
import styles from "../home.module.scss";
import Icon from "@/components/icon";
import Taro from "@tarojs/taro";
export const Search: FC = () => {
function jump(){
Taro.navigateTo({url:'/pages/preview/search/search/index'})
}
return (
<View className={styles.search}>
<Icon name='search' size={18}/>
<Text className='ml-1'></Text>
<View className={styles.search} onClick={jump}>
<Icon name='search' size={18} color='#808080'/>
<View className='ml-1'></View>
</View>
)
}

@ -1,7 +1,7 @@
export default definePageConfig({
navigationBarTitleText: '康一诺',
// navigationStyle: 'custom',
navigationStyle: 'custom',
navigationBarBackgroundColor: '#92ecc5',
navigationBarTextStyle: 'white',
// navigationBarTextStyle: 'white',
onReachBottomDistance: 50
})

@ -1,5 +1,5 @@
.tipsLogin {
padding: 24rpx;
padding: 20px;
color: #fff;
align-items: center;
position: fixed;
@ -9,59 +9,102 @@
justify-content: space-between;
width: 100%;
box-sizing: border-box;
background: rgba(#000, .7);
background: rgba(#000, .6);
font-size: 28rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.header {
width: 100%;
overflow: hidden;
margin-bottom: 20rpx;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
//&:after {
// min-height: 100vh;
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// content: '';
// display: block;
// background: linear-gradient(to right, #DBF3F5, #B9ECD7, #C1EEDA) no-repeat;
// min-height: 100vh;
// background-size: 100% 600rpx;
// z-index: -1;
//}
}
.headerDivider {
z-index: 99;
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0;
transform: scaleY(0.5);
transform-origin: bottom center;
}
.content {
position: relative;
padding: 0 20px;
min-height: 90vh;
box-sizing: border-box;
width: 750rpx;
overflow: hidden;
&:after {
position: absolute;
top: 0;
left: -10%;
width: 120%;
left: 0;
width: 100%;
height: 400rpx;
content: '';
display: block;
background: linear-gradient(to bottom, #92ecc5, #f1f8f6) no-repeat;
background: linear-gradient(to right, #DBF3F5, #B9ECD7, #C1EEDA) no-repeat;
z-index: -1;
filter: blur(50px);
}
}
.search {
width: 710rpx;
background: #fff;
border-radius: 100px;
line-height: 68rpx;
color: #bbb;
font-size: 28rpx;
display: flex;
align-items: center;
box-sizing: border-box;
justify-content: center;
height: 68rpx;
margin-bottom: 40rpx;
color: #909795;
}
.adware {
//margin-top: 40rpx;
width: 100%;
height: 260rpx;
border-radius: 16rpx;
overflow: hidden;
background: #eee;
margin-bottom: 40rpx;
}
.videoListBox {
border-radius: 20px;
}
.featureList {
font-size: 28rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #323635;
margin-top: 10px;
}
.courseTag {
width: 200px;
margin: auto;
width: 162px;
height: 46rpx;
margin: 0 auto 30rpx;
display: block;
}
@ -69,31 +112,70 @@
color: #323635;
background: #fff;
padding: 30rpx 0 30rpx 30rpx;
margin-top: 40rpx;
border-radius: 30rpx;
margin-bottom: 40rpx;
border-radius: 20rpx;
position: relative;
overflow: hidden;
&:after {
content: '';
display: block;
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #FFFFFF 100%);
}
}
.featureTitle {
max-width: 212px;
height: 50rpx;
padding-bottom: 30rpx;
padding-bottom: 20rpx;
}
.featureText {
width: calc(100% - 140px - 50px);
}
.featureTextTitle {
font-size: 32rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #323635;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
.featureTextDescription {
font-size: 28rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #909795;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
line-height: 2;
}
.ranking {
top: 0;
position: absolute;
left: 24rpx;
width: 24px;
height: 24px;
width: 30px;
height: 30px;
}
.featureImage {
width: 140px;
height: 90px;
height: 100px;
background: #eee;
border-radius: 12rpx;
overflow: hidden;
margin-right: 20rpx;
vertical-align: middle;
}

@ -1,31 +1,85 @@
import {FC} from "react";
import {View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import {Image, View, Text} from "@tarojs/components";
import styles from "./home.module.scss";
// import {Search} from "@/pages/home/components/search";
import Adware from "@/pages/home/components/adware";
import Feature from "@/pages/home/components/feature";
import FeatureRecommended from "@/pages/home/components/feature_recommended";
import CurRecommended from "@/pages/home/components/curRecommended";
import MyButton from "@/components/button/MyButton";
import {Profile} from "@/store";
import Taro from "@tarojs/taro";
import {HomeApi, HomeData} from "@/api";
import logo from '@/static/img/logo.svg'
import Spin from "@/components/spinner";
import NavigationBar from "@/components/navigationBar/navigationBar";
import {Search} from "@/pages/home/components/search";
const Home: FC = () => {
const {token, empty} = Profile.useContainer()
const globalData = Taro.getApp().globalData
const {token} = Profile.useContainer()
const [data, setData] = useState<null | HomeData>(null)
const [enable, setEnable] = useState(true)
const [navbarOpacity, setNavbarOpacity] = useState('0')
const navbarHeight = globalData.statusBarHeight + globalData.textBarHeight;
function unLogin() {
Taro.clearStorage()
Taro.navigateTo({url: '/pages/login/login'})
}
useEffect(() => {
HomeApi.home().then(res => {
setData(res)
})
setTimeout(() => {
setEnable(false)
}, 600)
}, [])
Taro.usePageScroll((e) => {
const v = (Math.min(e.scrollTop / navbarHeight, 1) * 0.9).toFixed(6)
if (v != navbarOpacity) {
setNavbarOpacity(v)
}
})
return (
<View className={styles.content} style={{paddingTop: '20px'}}>
{/*<Search/>*/}
<Adware/>
<>
<NavigationBar
className={styles.header}
backgroundColor={`rgba(255,255,255,${navbarOpacity})`}
// backgroundColor={`rgba(255,255,255,0.89)`}
leftNode={
<>
<Image src={logo} style={{height: "80%"}} mode='heightFix'/>
<Text style={{color: '#45d4a8', fontWeight: 'bold'}}></Text>
</>
}
cancelBack
>
<View className={styles.headerDivider} style={{borderBottom: `1px solid rgba(200,200,200,${navbarOpacity})`}}></View>
</NavigationBar>
<Spin enable={enable} overlay/>
<View className={styles.content} style={{paddingBottom: token ? 0 : '100rpx'}}>
<Search/>
<Adware data={data?.adverts || []} only_flag='routine_home_top_banner' width={710}/>
<Feature/>
<FeatureRecommended/>
<CurRecommended/>
<FeatureRecommended
illness={data?.illness.list || []}
health={data?.health || []}
brand={data?.brand.list || []}
skill={data?.skill || []}
/>
<Adware data={data?.adverts || []} only_flag='routine_home_recommend_banner' width={710}/>
{data && <CurRecommended/>}
{
!token && <View className={styles.tipsLogin} onClick={empty}>
<View>~</View>
<MyButton fillet size='mini'></MyButton>
!token && <View className={styles.tipsLogin}>
<View>~</View>
<MyButton fillet size='mini' onClick={unLogin}></MyButton>
</View>
}
</View>
</>
)
}

@ -1,4 +1,4 @@
import {FC, useEffect, useState} from "react";
import {FC, useEffect, useMemo, useState} from "react";
import {useDidShow} from "@tarojs/taro";
import {ScrollView, Swiper, SwiperItem, View} from "@tarojs/components";
import {Courses, CoursesKey, publicApi} from "@/api/public";
@ -7,7 +7,8 @@ import styles from '../index.module.scss'
import {formatMinute} from "@/utils/time";
import {userApi} from "@/api";
import Empty from "@/components/empty/empty";
import eventsIndex from "@/hooks/eventsIndex";
import {Profile} from "@/store";
import LoginView from "@/components/loginView";
interface Props {
categoryKey: CoursesKey
@ -16,13 +17,14 @@ interface Props {
export const VideoList: FC<Props> = ({categoryKey, setCategoryKey}) => {
const [data, setData] = useState<Courses>({
is_finished: [],
is_not_required: [],
is_required: [],
is_not_finished: [],
is_not_required: [],
// is_finished: [],
// is_not_finished: [],
})
const [page, setPage] = useState(1)
const [records, setRecords] = useState<LearnRecord[]>([])
const {token} = Profile.useContainer()
function screen(oldData: Curriculum[], data: Curriculum[]): Curriculum[] {
return data.reduce((pre, cur) => {
@ -43,8 +45,8 @@ export const VideoList: FC<Props> = ({categoryKey, setCategoryKey}) => {
setData({
is_required: screen(old.is_required, res.is_required || []),
is_not_required: screen(old.is_not_required, res.is_not_required || []),
is_finished: screen(old.is_finished, res.is_finished || []),
is_not_finished: screen(old.is_not_finished, res.is_not_finished || []),
// is_finished: screen(old.is_finished, res.is_finished || []),
// is_not_finished: screen(old.is_not_finished, res.is_not_finished || []),
})
} catch (e) {
}
@ -63,7 +65,6 @@ export const VideoList: FC<Props> = ({categoryKey, setCategoryKey}) => {
switch (categoryKey) {
case "is_required":
case "is_not_required":
case "is_not_finished":
const find = records.find(d => d?.course_id === id)
if (find) {
if (class_hour === find.finished_count) {
@ -72,32 +73,27 @@ export const VideoList: FC<Props> = ({categoryKey, setCategoryKey}) => {
return (<View>{`${class_hour}节/已学${find.finished_count}`}</View>)
}
return (<View>{`${class_hour}节/已学0节`}</View>)
case "is_finished":
return <View></View>
}
}
eventsIndex.on(({id}) => {
if (id == null) return;
for (const [index, notFinished] of data.is_not_finished.entries()) {
if (notFinished.id === id) {
data.is_finished.push(notFinished)
data.is_not_finished.splice(index, 1)
return
}
}
})
useDidShow(() => {
getData().then()
getRecords().then()
})
// eventsIndex.on(({id}) => {
// if (id == null) return;
// for (const [index, notFinished] of data.is_not_finished.entries()) {
// if (notFinished.id === id) {
// data.is_finished.push(notFinished)
// data.is_not_finished.splice(index, 1)
// return
// }
// }
// })
useEffect(() => {
const fetchData = () => {
getData().then()
getRecords().then()
}, [page])
}
useDidShow(fetchData)
useEffect(fetchData, [page])
function changeSwiper(e) {
const index = e.detail.current
@ -105,15 +101,27 @@ export const VideoList: FC<Props> = ({categoryKey, setCategoryKey}) => {
setCategoryKey(categoryKeys[index])
}
const categoryIndex = useMemo(() => {
const index = Object.keys(data).findIndex(d => d === categoryKey)
return index < 0 ? 0 : index
}, [data, categoryKey])
return (
<Swiper className={styles.swiper} onChange={changeSwiper}
current={Object.keys(data).findIndex(d => d === categoryKey)}>
<Swiper className={styles.swiper}
onChange={changeSwiper}
current={categoryIndex}>
{
Object.values(data).map((value) => <SwiperItem>
<ScrollView scrollY className={styles.swiper} onScrollToLower={() => setPage(page + 1)}>
Object.entries(data).map(([key, value]) => <SwiperItem>
<ScrollView scrollY className={styles.swiper} onScrollToLower={() => setPage(page + 1)} enhanced
showScrollbar={false}>
<View className='py-2 flex justify-between flex-wrap'>
{
value?.length ? value?.map(c =>
!token
&& key === 'is_required'
? <LoginView onSuccess={fetchData}/> : value?.length ?
<>
{
value?.map(c =>
<VideoCover
thumb={c.thumb}
title={c.title}
@ -123,6 +131,9 @@ export const VideoList: FC<Props> = ({categoryKey, setCategoryKey}) => {
time={formatMinute(c.course_duration)}
content={rateOfLearning(c.id, c.class_hour)}
/>)
}
<View className='text-center text-muted flex-1'></View>
</>
: <Empty name='暂无课程'/>
}
</View>

@ -1,5 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '医学道',
navigationBarBackgroundColor: '#92ecc5',
navigationBarTextStyle: 'white',
navigationStyle: 'custom',
})

@ -4,13 +4,14 @@ import styles from './index.module.scss'
import {VideoList} from "@/pages/index/components/videoList";
import Tabs, {OnChangOpt, TabList} from "@/components/tabs/tabs";
import {CoursesKey} from "@/api/public";
import NavigationBar from "@/components/navigationBar/navigationBar";
const Index: FC = () => {
const category: TabList[] = [
{title: "必修", value: 'is_required'},
{title: "选修", value: 'is_not_required'},
{title: "已完成", value: 'is_finished'},
{title: "未完成", value: 'is_not_finished'},
// {title: "已完成", value: 'is_finished'},
// {title: "未完成", value: 'is_not_finished'},
]
const [categoryKey, setCategoryKey] = useState<CoursesKey>('is_required')
@ -18,14 +19,18 @@ const Index: FC = () => {
setCategoryKey(data.tab?.value as CoursesKey)
}
return (
<>
<View className={styles.content}>
<Tabs tabList={category} onChange={tabChange} current={categoryKey}/>
<VideoList categoryKey={categoryKey} setCategoryKey={(categoryKey) => setCategoryKey(categoryKey)}/>
<NavigationBar
cancelBack
leftNode={<Tabs tabList={category} onChange={tabChange} current={categoryKey}/>}
backgroundColor={'transparent'}
/>
<VideoList
categoryKey={categoryKey}
setCategoryKey={(categoryKey) => setCategoryKey(categoryKey)}
/>
</View>
</>
)
}

@ -1,4 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '登录'
})

@ -59,3 +59,5 @@
height: 50vh;
padding: 50px 30px 0;
}

@ -8,19 +8,15 @@ import {userApi} from "@/api";
import {loginApi, LoginParams} from "@/api/login";
import MyButton from "@/components/button/MyButton";
const Login: FC = () => {
// const {statusBarHeight = 0} = Taro.getSystemInfoSync()
// const bbc = getMenuButtonBoundingClientRect();
// const navHeight = bbc.bottom + (bbc.top - statusBarHeight) - statusBarHeight
const Login: FC = () => {
const [isLoading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const {setUser, setToken, setCompany} = Profile.useContainer()
const {setUser, setToken, setCompany, empty} = Profile.useContainer()
const [h5params, setH5Params] = useState<LoginParams | null>(null)
useEffect(() => {
empty(false)
if (process.env.TARO_ENV === 'h5') {
setLoading(true);
loginApi.getParams().then((res) => {
@ -62,7 +58,9 @@ const Login: FC = () => {
setCompany(company)
setLoading(false)
Taro.switchTab({url: '/pages/home/home'})
} else {
Taro.setStorageSync('openid', catch_key)
Taro.reLaunch({url: '/pages/check/check'})
}
@ -78,11 +76,11 @@ const Login: FC = () => {
}
}
async function TESTLOGIN() {
const res = await loginApi.testLogin()
Taro.setStorageSync('profile', JSON.stringify(res))
Taro.reLaunch({url: '/pages/home/home'})
}
// async function TESTLOGIN() {
// const res = await loginApi.testLogin()
// Taro.setStorageSync('profile', JSON.stringify(res))
// Taro.reLaunch({url: '/pages/home/home'})
// }
return (
<View className={styles.container}>

@ -88,7 +88,7 @@ export const Search: FC<Props> = ({param, setParam}) => {
{deps.length ?
<>
{
deps.map(dep => <View
deps?.map(dep => <View
key={dep.id}
className={styles.radioBox}
onClick={() => changeDepId(dep.id)}>

@ -6,12 +6,14 @@ import styles from './courseAdmin.module.scss'
import Taro, {useReachBottom} from "@tarojs/taro";
import MyButton from "@/components/button/MyButton";
import storageDep from "@/hooks/storageDep";
import Spin from "@/components/spinner";
const CourseAdmin: FC = () => {
const [total, setTotal] = useState(0)
const [data, setData] = useState<Curriculum[]>([])
const [batch, setBatch] = useState(false)
const [curs, setCurs] = useState<number[]>([])
const [enable, setEnable] = useState(true)
const [param, setParam] = useState<CourseAllParam>({
page: 1,
page_size: 10,
@ -26,7 +28,7 @@ const CourseAdmin: FC = () => {
courseApi.getCourseAll({...param, page_size: param.page_size * (replace ? param.page : 1)}).then(res => {
setTotal(res.total)
if (param.page === 1 || replace) {
setData(res.data)
setData(res.data || [])
} else {
setData([
...data,
@ -34,6 +36,7 @@ const CourseAdmin: FC = () => {
])
}
})
setEnable(false)
}
useEffect(() => {
@ -53,7 +56,7 @@ const CourseAdmin: FC = () => {
if (curs.length === data.length) {
setCurs([])
} else {
setCurs(data.map(d => d.id))
setCurs(data?.map(d => d.id))
}
}
@ -72,7 +75,7 @@ const CourseAdmin: FC = () => {
Taro.showModal({
title: '删除警告',
content: "删除后所有部门不可查看",
confirmText:'删除',
confirmText: '删除',
async success({confirm}) {
if (confirm) {
try {
@ -108,7 +111,7 @@ const CourseAdmin: FC = () => {
* @param required []
*/
function batchChangDep(ids: number[], depList = [], required = []) {
if (!ids.length) {
if (!ids?.length) {
Taro.showToast({title: '请选择课程', icon: 'none'})
return
}
@ -149,11 +152,12 @@ const CourseAdmin: FC = () => {
return (
<View>
<Spin enable={enable} overlay/>
<Search param={param} setParam={setParam}/>
<View className={styles.curList}>
{
data.map((d, index) => <View key={d.id} className={styles.curBox}>
data?.map((d, index) => <View key={d.id} className={styles.curBox}>
<View className={styles.curTitle} onClick={() => addCurs(d.id)}>
{batch && <Radio
checked={curs.includes(d.id)}

@ -8,6 +8,7 @@ import folder from '@/static/img/folder.png'
import ShowModel from "@/components/showModel/showModel";
import Empty from "@/components/empty/empty";
import {Profile} from '@/store'
import Spin from "@/components/spinner";
const DepAdmin: FC = () => {
const router = useRouter()
@ -17,6 +18,7 @@ const DepAdmin: FC = () => {
const [isPut, setIsPut] = useState(false)
const [depName, setDepName] = useState('')
const [reset, setReset] = useState(false)
const [enable, setEnable] = useState(true)
const {company} = Profile.useContainer()
@ -31,6 +33,7 @@ const DepAdmin: FC = () => {
}
} catch (e) {
}
setEnable(false)
}
function showPop(name: string | undefined) {
@ -137,6 +140,7 @@ const DepAdmin: FC = () => {
return (
<>
<Spin overlay enable={enable}/>
<View>
{manages.map(d => <PopPut
key={d.id}

@ -6,6 +6,7 @@ import PopPut from "@/components/popPut/popPut";
import folder from "@/static/img/folder.png";
import MyButton from "@/components/button/MyButton";
import storageDep from "@/hooks/storageDep";
import Spin from "@/components/spinner";
/**
* depIds JSON = number[]
@ -16,11 +17,13 @@ const SelectDep: FC = () => {
const [ids, setIds] = useState<number[]>([])
const [deps, setDeps] = useState<Manage[]>([])
const [required, setRequired] = useState<number[]>([])
const [enable, setEnable] = useState(true)
useEffect(() => {
curriculum.department().then(res => {
setDeps(res.data)
})
setEnable(false)
setIds(JSON.parse(params.depIds))
setRequired(JSON.parse(params.required || "[]"))
}, [])
@ -79,6 +82,7 @@ const SelectDep: FC = () => {
return (
<View className='px-2 bg-white'>
<Spin overlay enable={enable}/>
{deps.map((d) => <View className='flex align-center' key={d.id} onClick={() => onChange(d.id)}>
<Checkbox value={d.id + ''} checked={ids.includes(d.id)} color='#45d4a8'/>
<View style={{flex: 1}}>

@ -25,6 +25,7 @@ const SpotMeeting: FC = () => {
const [loading, setLoading] = useState(false)
const [status, setStatus] = useState(0) // 状态
const [id, setId] = useState<number | undefined>(params.id ? Number(params.id) : undefined)
const pathUrl = process.env.TARO_ENV === 'h5' ? '/official/qrcode' : '/wechat/link'
const {company} = Profile.useContainer()
function setData(res: Meeting | null) {
@ -119,6 +120,9 @@ const SpotMeeting: FC = () => {
//存储二维码
handleWriteFile()
},
fail() {
Taro.showToast({title: '保存授权失败', icon: "error"})
}
});
} else {
handleWriteFile()
@ -133,7 +137,7 @@ const SpotMeeting: FC = () => {
/** 获取二维码地址 */
const downUrl = (id: number, down = true) => {
Taro.showLoading({title: '加载二维码'})
const url = process.env.TARO_APP_API + '/official/qrcode?' + 'meeting_id=' + id + '&path=' + path
const url = process.env.TARO_APP_API + pathUrl + '?meeting_id=' + id + '&path=' + path
if (process.env.TARO_ENV !== 'h5') {
Taro.downloadFile({
@ -302,17 +306,6 @@ const SpotMeeting: FC = () => {
<PopPut title='选择部门' content={manages?.find(x => x.id == depid)?.name}/>
</Picker>
{/*<Textarea*/}
{/* value={description}*/}
{/* className='Textarea'*/}
{/* disabled={!!params.id || status === 1}*/}
{/* placeholder='请输入描述'*/}
{/* maxlength={255}*/}
{/* name='description'*/}
{/* onInput={(e) => setDescription(e.detail.value)}*/}
{/* style={{height: '60px'}}>*/}
{/*</Textarea>*/}
<View className='flex justify-between'>
{
id !== undefined && <>

@ -1,6 +1,7 @@
import {FC} from "react";
import {Image, Text, View} from "@tarojs/components";
import { Text, View} from "@tarojs/components";
import styles from '../userInfo.module.scss'
import Img from "@/components/image/image";
interface Props {
data: User | null
@ -12,7 +13,7 @@ const Info: FC<Props> = ({data}) => {
return (
<>
<View className={styles.box}>
<Image src={data?.avatar || ''} className={styles.image}/>
<Img width={120} height={120} src={data?.avatar || ''} className={styles.image}/>
<View className='ml-2'>
<View>
<Text className='font-weight'>{data?.name}</Text>

@ -15,7 +15,6 @@
.image {
width: 120rpx;
height: 120rpx;
background: #ddd;
border-radius: 10rpx;
overflow: hidden;
}

@ -48,7 +48,7 @@ const MeetingLogin: FC = () => {
Taro.showModal({
title: res.type === 0 ? '暂未开始' : '二维码已过期',
success() {
Taro.reLaunch({url: '/pages/login/login'})
Taro.redirectTo({url: '/pages/login/login'})
}
})
return

@ -2,20 +2,40 @@ 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"
import blacktriang from "@/static/img/blacktriangle.png"
const Header = () => {
const {user} = Profile.useContainer()
const Header = ({showCompany}:{showCompany:()=> void}) => {
const {token,user,company,empty} = Profile.useContainer()
return (
<View className={styles.header}>
<View className='flex'>
{
token ?
<Image src={user?.avatar || avatar} className={styles.avatar}/>:
<Image src={avatar} className={styles.avatar}/>
}
{ token ?
<View className='flex-1'>
<View className='font-32'>{user?.name}</View>
<View className='login font-24 mt-2 text-secondary flex justify-between content-start'>
<Text>{user?.phone_number}</Text>
<View className='flex align-center mt-2' onClick={()=>{
showCompany()
}}>
<Text style={{fontSize:'24rpx',fontWeight:'500',color:'#323635'}}>{company?.name}</Text>
<Image src={blacktriang} style={{width:'20rpx',height:'20rpx'}}></Image>
</View>
</View>:
<View className='flex-1'>
<View className='font-32 mt-2' onClick={()=>{
empty()
}}></View>
</View>
}
{/*<View className='login font-24 mt-2 text-secondary flex justify-between content-start'>*/}
{/* <Text>手机号:{user?.phone_number}</Text>*/}
{/*</View>*/}
</View>
</View>
)

@ -22,7 +22,7 @@ const Service = () => {
{title: '个人中心', src: userInfo, router: '/pages/business/userInfo/userInfo'},
])
const {user, setUser} = Profile.useContainer()
const {token,user, setUser} = Profile.useContainer()
Taro.useDidShow(async () => {
if (user?.id) {
@ -33,6 +33,7 @@ const Service = () => {
useEffect(() => {
const oldList: List[] = JSON.parse(JSON.stringify(list))
if(token){
if ([1, 2].includes(user?.role_type || 0)) {
oldList.unshift(...[
{title: '部门管理', src: dep, router: '/pages/manage/depAdmin/depAdmin'},
@ -42,9 +43,14 @@ const Service = () => {
])
setList(oldList)
}
}
}, [])
function jump(url: string) {
if(!token){
Taro.navigateTo({url:'/pages/login/login'})
return
}
Taro.navigateTo({url})
}

@ -10,6 +10,7 @@ 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'
import {Profile} from "@/store";
interface List {
title: string
@ -19,6 +20,7 @@ interface List {
}
const Time: FC = () => {
const {token} = Profile.useContainer()
const [list, setList] = useState<List[]>([
{title: '今日时长', time: '00:00', src: time1},
{title: '累计时长', time: '00:00', src: time2},
@ -45,6 +47,10 @@ const Time: FC = () => {
})
function jump(type?: number) {
if (!token) {
Taro.navigateTo({url: '/pages/login/login'})
return;
}
if (!type) return;
Taro.navigateTo({url: "/pages/business/courType/courType?type=" + type})
}
@ -59,7 +65,7 @@ const Time: FC = () => {
<View className={styles.timeBoxFlex}>
<View>
<View className='mb-2'>{d.title}</View>
<View className='text-muted font-26'>{d.time}</View>
{token && <View className='text-muted font-26'>{d.time}</View>}
</View>
<Image src={d.src} mode='aspectFit' className={styles.timeImag}/>
</View>

@ -1,5 +1,7 @@
page {
background: #F2F8F6 !important;
width:750rpx;
overflow: hidden;
}
.content {
@ -89,3 +91,51 @@ page {
padding: 30rpx 20px;
margin: 20rpx;
}
.box {
display: flex;
background-color: #fff;
border-radius: 16rpx;
padding: 16rpx 0;
box-sizing: border-box;
border-bottom: 2rpx solid #F5F8F7;
&.noBorder{
border:none;
}
.image{
width: 68rpx;
height:68rpx;
background-color: #eee;
border-radius: 8rpx;
overflow: hidden;
}
.innerBox{
height: 68rpx;
align-items: center;
margin-left: 24rpx;
box-sizing: border-box;
flex: 1;
display: flex;
justify-content: space-between;
.title{
font-size: 32rpx;
font-weight: 500;
color: #323635;
flex: 1;
display: -webkit-box;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient:vertical;
-webkit-line-clamp:1;
}
.icon{
margin-left: 24rpx;
width:24rpx;
height:24rpx;
&.hide{
background: none;
}
}
}
}

@ -1,19 +1,85 @@
import {View} from "@tarojs/components";
import {PageContainer, View, Image} from "@tarojs/components";
import Taro from "@tarojs/taro";
import styles from './my.module.scss'
import Header from "./components/header/header";
import {FC} from "react";
import {FC, useState} from "react";
import Time from "@/pages/my/components/header/time";
import Service from "@/pages/my/components/header/service";
import {Profile} from "@/store";
import Img from "@/components/image/image";
import GreenNike from "@/static/img/greenNike.png"
import {userApi} from "@/api";
const My: FC = () => {
const globalData = Taro.getApp().globalData
const {token, company, setCompany} = Profile.useContainer()
const [companyShow, setCompanyShow] = useState(false)
const [companyList, setCompanyList] = useState<Company[]>([])
Taro.useDidShow(() => {
if(token){
try {
userApi.companyList().then(res => {
setCompanyList(res as Company[])
})
} catch (e) {
}
}
})
return (
<View className={styles.content} style={`paddingTop:${globalData.statusBarHeight}px`}>
<Header/>
<Header showCompany={()=>{
companyList.length >= 2 && setCompanyShow(true)
}}/>
<Time/>
<Service/>
<PageContainer overlayStyle={'background:rgba(0,0,0,0.3)'} position={'bottom'} round={true} show={companyShow}
onClickOverlay={() => setCompanyShow(false)}>
<View className="px-3 py-5">
<View className="font-32 pb-3"
style={{display: 'flex', justifyContent: 'center', borderBottom: '2rpx solid #f5f8f7'}}></View>
{
companyList.length >= 1 &&
companyList.map((d,idx) =>
<View className={`${styles.box} ${companyList.length-1 === idx && styles.noBorder} ` } onClick={async () => {
if(company?.id === d.id){
return
}
Taro.showLoading({
title: '切换公司中',
mask: true,
})
const data = await userApi.companyReplace(d.id)
Taro.hideLoading()
!data && setCompany(d)
!data && Taro.showModal({
title: '友情提示',
content: '切换公司成功,需要重新进入',
showCancel: false,
success: function (res) {
if (res.confirm) {
Taro.reLaunch({url: '/pages/my/my'})
}
}
})
setCompanyShow(false)
}}>
<View className={styles.image}>
<Img width={68} height={68} src={d.logo}/>
</View>
<View className={styles.innerBox}>
<View className={styles.title}>{d.name}</View>
<Image src={company?.id === d.id ? GreenNike : ''} className={styles.icon}></Image>
</View>
</View>
)
}
</View>
</PageContainer>
</View>
)
}

@ -1,16 +1,18 @@
import {FC, useEffect, useState} from "react";
import {FC, useEffect, useMemo, useState} from "react";
import {Image, Text, View} from "@tarojs/components";
import Taro, {useRouter} from "@tarojs/taro";
import {ArticleRecord, brandApi} from "@/api";
import styles from "@/pages/preview/illness/article/article.module.scss";
import down from "@/static/img/doubleDown.png";
import {Profile} from "@/store";
import {parse} from "@/utils/marked/marked";
const article:FC = () => {
const {token} = Profile.useContainer()
const {token,} = Profile.useContainer()
const {id} = useRouter().params as unknown as { id: number}
const [articleInfo,setArticleInfo] = useState<ArticleRecord>()
const { children } = useMemo(() => parse(articleInfo?.content || ''), [articleInfo])
useEffect(() => {
getData()
}, [id])
@ -24,10 +26,11 @@ const article:FC = () => {
}
}
function helloWorld() {
const html = articleInfo?.content;
return (
<>
<View style={{padding:'10px',height:!token ? Taro.getWindowInfo().windowHeight-60+'px' : 'auto',overflow:!token ? 'hidden':'auto'}} dangerouslySetInnerHTML={{ __html: html! }}></View>
<View style={{padding:'10px',height:!token ? Taro.getWindowInfo().windowHeight-60+'px' : 'auto',overflow:!token ? 'hidden':'auto'}}>
{ children }
</View>
{
!token &&
<View className={styles.fixedBox}>
@ -35,7 +38,7 @@ const article:FC = () => {
<View className={styles['fixedBox-inner-icon']}>
<Image src={down}></Image>
</View>
<View className={styles['fixedBox-inner-box']}>
<View className={styles['fixedBox-inner-box']} onClick={()=>{Taro.navigateTo({url: '/pages/login/login'})}}>
<Text></Text>
</View>
</View>

@ -7,7 +7,7 @@ page{
}
.curIndexBox{
position: absolute;
top:450rpx;
top:500rpx;
right:30rpx;
background-color: rgba(0,0,0,0.5);
border-radius: 30rpx;
@ -15,18 +15,17 @@ page{
color:#fff;
}
.body{
border-radius: 32rpx 32rpx 0 0;
background-color:#f1f8f6;
width: 750rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 600rpx;
position: absolute;
top: 520rpx;
min-height: 500rpx;
.top{
position: sticky;
position: -webkit-sticky;
top: 0;
padding:40rpx 30rpx 30rpx 30rpx;
border-radius: 32rpx;
background-color: #fff;
.title{
font-size: 32rpx;
@ -56,7 +55,7 @@ page{
.image{
width: 172rpx;
height:128rpx;
background-color: pink;
background-color: #eee;
border-radius: 8rpx;
}
.leftBox{

@ -1,10 +1,13 @@
import {FC, useEffect, useState} from "react";
import {Image, Swiper, SwiperItem, Text, Video, View} from "@tarojs/components";
import {FC, useCallback, useEffect, useState} from "react";
import {Swiper, SwiperItem, Text, Video, View} from "@tarojs/components";
import {ArticleRecord, brandApi, BrandRecord} from "@/api";
import styles from './info.module.scss'
import Taro, {useRouter} from "@tarojs/taro";
import Taro, {useReachBottom, useRouter} from "@tarojs/taro";
import LineEllipsis from "@/components/textCollapse/collapse";
import Empty from "@/components/empty/empty";
import Img from "@/components/image/image";
import {rfc33392time} from "@/utils/day";
import Spin from "@/components/spinner";
type Params = {
id: number
@ -12,8 +15,11 @@ type Params = {
const BrandInfo: FC = () => {
const {id} = useRouter().params as unknown as Params
const [brandInfo, setBrandInfo] = useState<BrandRecord>()
const [articleList, setArticleList] = useState<ArticleRecord[]>()
const [curIndex,setCurIndex] = useState<number>(1)
const [articleList, setArticleList] = useState<ArticleRecord[]>([])
const [curIndex, setCurIndex] = useState<number>(1)
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const [enable, setEnable] = useState(true)
useEffect(() => {
getData()
@ -24,28 +30,53 @@ const BrandInfo: FC = () => {
const data = await brandApi.info(id)
Taro.setNavigationBarTitle({title: data.name})
setBrandInfo(data)
const data1 = await brandApi.articleList(id)
setArticleList(data1.list)
const data1 = await brandApi.articleList(id, page)
setTotal(data1.total)
setArticleList([
...(articleList || []),
...data1.list
])
} catch (e) {
// setBrandInfo({disabled: 0, graphic_introduction: "", id: 0, introductory_video: "", name: "", brand_album:['1','2','3']})
}
setEnable(false)
}
function onChange(e){
console.log(e)
setCurIndex(+e.detail.current+1)
useReachBottom(useCallback(() => {
if (articleList?.length < total) {
setPage(page + 1)
}
}, [total, articleList]))
useEffect(() => {
brandApi.articleList(id, page).then(res => {
setTotal(res.total)
setArticleList([
...(articleList || []),
...res.list
])
})
}, [page])
function onChange(e) {
setCurIndex(+e.detail.current + 1)
}
return (
<View className='flex flex-column '>
<View className='flex flex-column' style={{display: 'flex'}}>
<Spin enable={enable} overlay/>
{
(brandInfo?.introductory_video_resource?.url || brandInfo?.brand_album?.length) &&
<>
<Swiper
className={styles['swiper']}
indicatorColor='#999'
indicatorActiveColor='#333'
indicatorDots
indicatorDots={false}
onChange={onChange}
>
{ brandInfo?.introductory_video_resource?.url && <SwiperItem>
{brandInfo?.introductory_video_resource?.url && <SwiperItem>
<Video
style={{width: '750rpx', height: '600rpx'}}
playBtnPosition={"center"}
@ -63,17 +94,21 @@ const BrandInfo: FC = () => {
{brandInfo?.brand_album?.length
&& brandInfo?.brand_album?.split(',').map((d) =>
<SwiperItem>
<Image mode={'aspectFill'} style={{width:'750rpx',height:'600rpx'}} src={d}></Image>
<Img mode={'aspectFill'} width={750} height={600} src={d}/>
</SwiperItem>)
}
</Swiper>
<View className={styles.curIndexBox}>
<Text>{curIndex} / {brandInfo?.brand_album?.split(',').length + ((brandInfo && brandInfo.introductory_video_resource) ? 1:0)}</Text>
<Text>{curIndex} / {(brandInfo?.brand_album?.split(',').length || 0) + ((brandInfo && brandInfo.introductory_video_resource) ? 1 : 0)}</Text>
</View>
</>
}
<View className={styles['body']}>
<View className={styles['top']}>
<Text className={styles['title']}>{brandInfo?.name}</Text>
<LineEllipsis text={brandInfo?.graphic_introduction || ''}></LineEllipsis>
<LineEllipsis text={brandInfo?.graphic_introduction || '暂无简介'}></LineEllipsis>
</View>
<View className={styles['bottom']}>
{
@ -85,95 +120,11 @@ const BrandInfo: FC = () => {
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
<View className={styles.desc}>{rfc33392time(i.created_at)} {i.page_view}</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
<View className={styles.image}>
<Img width={172} height={128} src={i.cover}/>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)

@ -1,22 +1,54 @@
.box {
display: flex;
.box2 {
//display: flex;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 36rpx 24rpx;
box-sizing: border-box;
position: relative;
.bnox2Info {
display: flex;
}
}
.box {
display: flex;
//margin-bottom: 20rpx;
//background-color: #fff;
border-radius: 16rpx;
//margin-bottom: 6rpx;
padding: 24rpx;
box-sizing: border-box;
position: relative;
&:not(:last-child)::after {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: 0;
display: block;
content: "";
height: 0;
border-bottom: 1px solid rgba(0,0,0,0.2);
transform-origin: bottom center;
transform: scaleY(0.5);
}
}
.image{
width: 128rpx;
height:128rpx;
background-color: pink;
background-color: #ededed;
border-radius: 8rpx;
}
.rightBox{
padding-left: 24rpx;
box-sizing: border-box;
flex: 1;
}
.desc{
font-size: 24rpx;
font-weight: 500;

@ -1,14 +1,66 @@
import {FC, useCallback, useEffect, useState} from "react";
import {Image, View} from "@tarojs/components";
import {FC, ReactNode, useCallback, useEffect, useState} from "react";
import {Video, View} from "@tarojs/components";
import {brandApi, BrandRecord} from "@/api";
import styles from './list.module.scss'
import Taro, {useReachBottom} from "@tarojs/taro";
import Empty from "@/components/empty/empty";
import Spinner from "@/components/spinner";
import Img from "@/components/image/image";
const BrandItem: FC<{ data: BrandRecord; onClick: VoidFunction }> = ({data, onClick}) => {
let media: ReactNode
if (data.introductory_video_resource) {
media = <Video
style={{width: '100%'}}
src={data.introductory_video_resource.url}
/>
} else if (data.brand_album) {
media = <Img
width={712}
height={320}
src={data.brand_album.split(",")[0]}
mode="aspectFill"
style={{background: '#ededed'}}
onClick={onClick}
/>
}
return (
<View className="bg-white flex flex-column justify-stretch mb-2_4 rounded-10 clip">
{media}
<View className="p-3" onClick={onClick}>
<View className='font-weight mb-2 font-32 flex align-center'>
<Img
width={32}
height={32}
src={data.logo}
mode='aspectFill'
className="rounded-10 clip"
style={{background: '#ededed'}}
/>
<View className="ml-1 flex-1">
<View className="text-row1 font-28">{data.name}</View>
</View>
</View>
<View className="font-24 text-muted mb-4 text-row3" style={{lineHeight:1.4}}>
{data.graphic_introduction}
</View>
<View className="flex gap20rpx font-24 text-muted">
<View>2023/03.29 12:22:01</View>
<View className="flex-1"></View>
<View>123</View>
<View>234</View>
</View>
</View>
</View>
)
}
const BrandList: FC = () => {
const [page, setPage] = useState(1)
const [brands, setBrands] = useState<BrandRecord[]>([])
const [total, setTotal] = useState(0)
const [text, setText] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
getData()
@ -16,13 +68,22 @@ const BrandList: FC = () => {
const getData = useCallback(async () => {
try {
const res = await brandApi.list(1, 10)
const res = await brandApi.list(page, 10)
if (page === 1) {
if (res.list.length < 10) {
setText('没有更多了~')
} else {
setText('上拉加载更多~')
}
}
setTotal(res.total)
setBrands([
...res.data
...brands,
...res.list
])
} catch (e) {
}
setLoading(false)
}, [page])
@ -33,23 +94,27 @@ const BrandList: FC = () => {
useReachBottom(useCallback(() => {
if (brands?.length < total) {
setPage(page + 1)
} else {
setText('没有更多了~')
}
}, [total, brands]))
let content: ReactNode
if (brands.length) {
content = (
<>
{brands.map(d => <BrandItem data={d} key={d.id} onClick={() => jumpInfo(d.id)}/>)}
<View style={{width: '710rpx', textAlign: 'center', color: '#999'}} className="font-28 mt-3">{text}</View>
</>
)
} else {
content = <Empty name='空空如也'/>
}
return (
<View className='p-2'>
{
brands.length ? brands.map((d) =>
<View onClick={() => jumpInfo(d.id)} className={styles.box} key={d.id}>
<Image src={d.logo} mode='aspectFill' className={styles.image}/>
<View className={styles.rightBox}>
<View className='font-weight mb-2 font-28'>{d.name}</View>
<View className={styles.desc}>{d.graphic_introduction}</View>
</View>
</View>)
: <Empty name='空空如也'/>
}
<Spinner enable={loading} overlay/>
{content}
</View>
)
}

@ -1,6 +1,6 @@
.container {
width: 100%;
padding: 0 20rpx;
padding: 20rpx;
box-sizing: border-box;
columns: 2;
column-gap: 20rpx;
@ -13,13 +13,6 @@
overflow: hidden;
margin-bottom: 20rpx;
position: relative;
image,
Image {
background: #eee;
width: 100%;
min-height: 345rpx;
}
}
.play {

@ -1,20 +1,27 @@
import {FC, useEffect, useState} from "react";
import {Image, ScrollView, View} from "@tarojs/components";
import {Image, View} from "@tarojs/components";
import {HomeApi} from "@/api";
import Taro, {useReachBottom} from "@tarojs/taro";
import styles from './health.module.scss'
import play from '@/static/img/play.png'
import Empty from "@/components/empty/empty";
import Spin from "@/components/spinner";
import Img from "@/components/image/image";
const Health: FC = () => {
const [page, setPage] = useState(1)
const [data, setData] = useState<Health[]>([])
const [total, setTotal] = useState(0)
const [enable, setEnable] = useState(true)
async function getData(page: number) {
try {
const res = await HomeApi.health(page, 10)
setData(res.data)
setTotal(res.total)
} catch (e) {
}
setEnable(false)
}
useReachBottom(() => {
@ -31,20 +38,26 @@ const Health: FC = () => {
}
return (
<ScrollView>
<View className={styles.container}>
<>
<Spin enable={enable} overlay/>
<View>
{
data.length > 0
? data.map(d => <View key={d.id} className={styles.health} onClick={() => jump(d)}>
<Image src={d.url_path} mode='widthFix' lazyLoad fadeIn/>
? <>
<View className={styles.container}>
{data.map(d => <View key={d.id} className={styles.health} onClick={() => jump(d)}>
<Img width={370} height={345} src={d.url_path} mode='widthFix'/>
<Image src={play} className={styles.play} mode='aspectFit'/>
<View className='text-ellipsis-2 m-2 text-dark'>{d.title}</View>
<View className='font-26 text-muted mx-2 mb-2'>{d.video_view}</View>
</View>)
</View>)}
</View>
<View className="font-28 mt-3 text-center text-muted"></View>
</>
: <Empty name='暂无数据'/>
}
</View>
</ScrollView>
</>
)
}

@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '',
onReachBottomDistance: 30
onReachBottomDistance: 50
})

@ -1,5 +1,30 @@
.botmBox{
z-index: 99;
position: fixed;
bottom: 0;
width: 750rpx;
box-sizing: border-box;
height: 180rpx;
padding-bottom: 60rpx;
display: flex;
justify-content: center;
align-items: center;
background: #F5F8F7;
view{
width: 560rpx;
height: 76rpx;
background: #45D4A8;
border-radius: 38rpx 38rpx 38rpx 38rpx;
color: #fff;
font-weight: 500;
font-size: 40rpx;
text-align: center;
line-height: 76rpx;
}
}
.fixedBox{
position: fixed;
z-index: 100;
top:0;
width: 100vw;
height: 100vh;

@ -1,16 +1,22 @@
import {FC, useEffect, useState} from "react";
import {Image, Text, View} from "@tarojs/components";
import {FC, useEffect, useMemo, useState} from "react";
import {Image, PageContainer, Text, View} from "@tarojs/components";
import Taro, {useRouter} from "@tarojs/taro";
import {ArticleRecord, brandApi} from "@/api";
import styles from './article.module.scss'
import down from '@/static/img/doubleDown.png'
import {Profile} from "@/store";
import {parse} from "@/utils/marked/marked";
const article:FC = () => {
const {token} = Profile.useContainer()
const {id} = useRouter().params as unknown as { id: number}
const [show,setShow] = useState(false)
const [articleInfo,setArticleInfo] = useState<ArticleRecord>()
const { children, headings } = useMemo(() => parse(articleInfo?.content || ''), [articleInfo])
const query = Taro.createSelectorQuery()
useEffect(() => {
getData()
}, [id])
@ -24,11 +30,33 @@ const article:FC = () => {
} catch (e) {
}
}
function mao(id: string){
console.log(id)
setShow(false)
Taro.nextTick(() => {
query.select(`#${id}`).boundingClientRect()
query.exec((res) => {
if(res.length){
Taro.pageScrollTo({
scrollTop: res[res.length-1].top,
duration: 300}
)
}
})
})
}
function helloWorld() {
const html = articleInfo?.content;
return (
<>
<View style={{padding:'10px',height:!token ? Taro.getWindowInfo().windowHeight-60+'px' : 'auto',overflow:!token ? 'hidden':'auto'}} dangerouslySetInnerHTML={{ __html: html! }}></View>
{/*<View style={{padding:'10px'}}>*/}
{/* { children }*/}
{/*</View>*/}
<View className={styles.botmBox} style={{display:show?'none':'flex'}} onClick={()=>{setShow(true)}}>
<View></View>
</View>
<View style={{padding:'10px',height:!token ? Taro.getWindowInfo().windowHeight-60+'px' : 'auto',overflow:!token ? 'hidden':'auto'}}>
{ children }
</View>
{
!token &&
<View className={styles.fixedBox}>
@ -36,13 +64,23 @@ const article:FC = () => {
<View className={styles['fixedBox-inner-icon']}>
<Image src={down}></Image>
</View>
<View className={styles['fixedBox-inner-box']}>
<View className={styles['fixedBox-inner-box']} onClick={ ()=> Taro.navigateTo({url: '/pages/login/login'})}>
<Text></Text>
</View>
</View>
</View>
}
<PageContainer onClickOverlay={()=>{ setShow(false)}} show={show} round={true} overlay={true} overlayStyle={'background:rgba(0,0,0,0.3)'} >
<View className="px-3 py-5">
{headings.length > 0 &&
headings.map((d) =>
<View className="pb-3" style={{fontSize:'28rpx',fontWeight: '500',color: '#323635'}} onClick={()=>{mao(d.id)}}>{d.text}</View>
)
}
</View>
</PageContainer>
</>
)
}

@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '疾病知识列表',
onReachBottomDistance: 30
navigationBarTitleText: '',
onReachBottomDistance: 50
})

@ -1,34 +1,8 @@
page{
background-color: #fff !important;
}
.box {
display: flex;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 16rpx;
box-sizing: border-box;
}
.image{
width: 128rpx;
height:128rpx;
background-color: pink;
border-radius: 8rpx;
}
.rightBox{
padding-left: 24rpx;
box-sizing: border-box;
flex: 1;
}
.desc{
font-size: 24rpx;
.articles {
font-size: 28rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #909795;
line-height: 34rpx;
display: -webkit-box;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
color: #323635;
padding: 30rpx;
border-bottom: 1px solid #F5F8F7;
}

@ -2,64 +2,62 @@ import {FC, useCallback, useEffect, useState} from "react";
import {View} from "@tarojs/components";
import styles from './list.module.scss'
import Taro, {useReachBottom, useRouter} from "@tarojs/taro";
import Collapse from "@/components/collapse/collapse";
import {illnessApi} from "@/api/illness";
import Empty from "@/components/empty/empty";
import Spin from "@/components/spinner";
const BrandList: FC = () => {
const params = useRouter().params as unknown as { id: number }
const [page, setPage] = useState(1)
const [brands, setBrands] = useState<any[]>([])
const [articles, setArticles] = useState<any[]>([])
// const [illness, setIllness] = useState<{ name: string; description: string; resource: any; album: string[] }>()
const [total, setTotal] = useState(0)
const [fetchDone, setFetchDone] = useState(false)
const [enable, setEnable] = useState(true)
useEffect(() => {
getData()
getData().then()
}, [page])
const getData = useCallback(async () => {
try {
const data = await illnessApi.list(params.id, 1, 100)
setTotal(data.total)
setBrands([
...data.list
])
Taro.setNavigationBarTitle({title: data.list?.[0].resourceCategories?.[0]?.name ?? '文章列表'})
const data = await illnessApi.articleInfo(params.id, page, 20)
Taro.setNavigationBarTitle({title: data?.illness?.name || '暂无文章'})
// setIllness(data.illness)
setTotal(data.list.total)
setArticles([...articles, ...data.list.list])
} catch (e) {
}
setEnable(false)
setFetchDone(true)
Taro.hideLoading()
}, [page])
function jump(id: number) {
Taro.navigateTo({url: '/pages/preview/illness/article/article?id=' + id})
}
useReachBottom(useCallback(() => {
if (brands?.length < total) {
if (articles?.length < total) {
setPage(page + 1)
}
}, [total, brands]))
}, [total, articles]))
return (
<View className='p-2'>
<View style={{display: fetchDone ? 'block' : 'none'}} className='px-2 mt-2'>
<Spin enable={enable} overlay/>
{
brands.length > 0 ?
brands.map((d) =>
<View className={styles.box}>
<View className={styles.rightBox}>
<Collapse title={d.name} children={
<>
articles.length > 0 ?
<View className='bg-white'>
{
d.articles?.map((d) => <View
onClick={() => {
Taro.navigateTo({url: `/pages/preview/illness/article/article?id=${d.id}`})
}}
className='py-3 font-36'
style={{borderBottom: "1px solid #ddd"}}
>{d.title}</View>
)
articles.map((d, index) => <View key={d.id} className={styles.articles} onClick={() => jump(d.id)}>
{index + 1} . {d.title}
</View>)
}
</>
}></Collapse>
</View>
</View>
) : <Empty name='暂无数据'/>
: <Empty name='暂无文章'/>
}
</View>
)

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

@ -1,3 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '疾病知识',
navigationStyle: "custom",
})

@ -1,17 +1,27 @@
.scrollView {
height: auto !important;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
margin: auto;
}
.firstOrder {
width: 300rpx;
height: calc(100vh - env(safe-area-inset-bottom));
width: 200rpx;
height: 100%;
View {
width: 100%;
text-align: center;
min-height: 100rpx;
color: #323635;
color: #606563;
display: flex;
align-items: center;
padding: 10rpx;
padding: 20rpx 20rpx 20rpx 30rpx;
box-sizing: border-box;
justify-content: center;
font-size: 28rpx;
}
}
@ -21,12 +31,13 @@
font-weight: bold;
position: relative;
transition: all 200ms;
background: #fff;
&:after {
content: '';
display: block;
width: 8rpx;
height: 60rpx;
width: 6rpx;
height: 40rpx;
background: #45D4A8;
position: absolute;
left: 0;
@ -37,35 +48,42 @@
}
}
.search {
width: 710rpx;
background: #fff;
border-radius: 100px;
line-height: 68rpx;
color: #bbb;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.tree {
padding: 0 15rpx;
background: #fff;
box-sizing: border-box;
height: calc(100vh - env(safe-area-inset-bottom));
}
.name {
font-size: 32rpx;
height: 100%;
font-size: 28rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #323635;
padding: 40rpx 0 40rpx 15rpx;
}
.secondaryBox {
.list {
padding: 20rpx;
border-bottom: 1px solid #F5F8F7;
width: 100%;
min-height: 100rpx;
color: #323635;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-items: center;
}
.secondary {
width: 50%;
padding: 0 15rpx;
box-sizing: border-box;
color: #323635;
text-align: center;
line-height: 60rpx;
background: #F5F8F7;
border-radius: 8rpx;
margin-bottom: 20rpx;
background-clip: content-box;
.back {
//padding: 25rpx 10rpx 20rpx 30rpx;
width: 30rpx;
height: 30rpx;
}

@ -1,69 +1,153 @@
import {ScrollView, View} from "@tarojs/components";
import {FC, useState} from "react";
import {ScrollView, Image, Swiper, SwiperItem, View} from "@tarojs/components";
import {CSSProperties, FC, useEffect, useMemo, useState} from "react";
import {Category, HomeApi} from "@/api";
import Taro from "@tarojs/taro";
import styles from './sort.module.scss'
import Tabs, {TabList} from "@/components/tabs/tabs";
import {illnessApi} from "@/api/illness";
import Empty from "@/components/empty/empty";
import leftArrow from "@/static/img/leftArrow.png"
import Spin from "@/components/spinner";
// import NavigationBar from "@/components/navigationBar/navigationBar";
const prefix = 'SORT'
const Sort: FC = () => {
const [data, setData] = useState<Category[]>([])
const [select, setSelect] = useState<string | null>(null)
const [firstId, setFirstId] = useState<number | undefined>(undefined) // 一级分类
const [secondId, setSecondId] = useState<number | undefined>(undefined) // 二级分类
const [list, setList] = useState<any[]>([])
const [enable, setEnable] = useState(true)
const [loading, setLoading] = useState(false)
const globalData = Taro.getApp().globalData
const menu = Taro.getMenuButtonBoundingClientRect()
async function getData() {
try {
const res = await HomeApi.category(3)
setData(res)
if (res.length) {
setSelect(`${prefix}-${res[0].id}`)
setFirstId(res[0].id)
setSecondId(res[0]?.resource_category?.[0].id)
}
} catch (e) {
setLoading(false)
}
setEnable(false)
}
function jump(id: number) {
Taro.navigateTo({url: '/pages/preview/illness/list/list?id=' + id})
}
const getTabList = useMemo((): TabList[] => {
return data.map<TabList>(d => ({
title: d.name,
value: d.id
}))
}, [data])
const headerStyles: CSSProperties = {
paddingTop: globalData.statusBarHeight + 'px',
display: 'flex',
alignItems: "center",
width: menu.left - 10 + 'px',
}
function firstIdChange(id: number) {
setFirstId(id)
const resource_category = data.find(d => d.id === id)?.resource_category
setSecondId(resource_category?.[0]?.id)
}
Taro.useLoad(getData)
useEffect(() => {
if (secondId) {
setLoading(true)
illnessApi.list(secondId, 1, 100).then(res => {
setList(res.list)
}).catch(() => {
setList([])
})
} else {
setList([])
}
setLoading(false)
}, [secondId])
function swiperChange(e) {
const firstData = data[e.target.current]
if (!firstData) return;
setFirstId(firstData.id)
setSecondId(firstData?.resource_category?.[0]?.id)
}
return (
<View className='flex'>
<ScrollView
scrollY
scrollWithAnimation
scrollIntoView={select!}
className={styles.firstOrder}>
<View>
<Spin enable={enable} overlay/>
<View style={headerStyles}>
<View
className="flex flex-column align-center justify-center"
style={{width:globalData.textBarHeight+'px', height: globalData.textBarHeight+'px'}}
onClick={() => Taro.navigateBack()}
>
<Image
src={leftArrow}
mode='widthFix'
className={styles.back}
/>
</View>
<View className="flex">
<Tabs
tabList={getTabList}
current={firstId}
onChange={(data) => firstIdChange(data.tab?.value as number)}
/>
<View className="flex-1" />
</View>
</View>
{/*<Search onConfirm={onConfirm}/>*/}
<Swiper
onChange={swiperChange}
className={styles.scrollView}
current={data.findIndex(d => d.id === firstId)}
style={{top: `${globalData.statusBarHeight + 59}px`}}>
{
data.map(d => <SwiperItem key={d.id} className='flex'>
<ScrollView scrollY className={styles.firstOrder} enhanced showScrollbar={false}>
{
data.map(d => <View
d.resource_category?.map(d => <View
id={`${prefix}-${d.id}`}
key={d.id}
onClick={() => setSelect(`${prefix}-${d.id}`)}
className={`${select === `${prefix}-${d.id}` && styles.select}`}>
onClick={() => setSecondId(d.id)}
className={secondId === d.id && styles.select}>
{d.name}
</View>)
}
</ScrollView>
<ScrollView
enhanced
showScrollbar={false}
scrollY
className={styles.tree}
scrollWithAnimation
scrollIntoView={select!}>
scrollWithAnimation>
{
data.map(d => <View key={d.id} id={`${prefix}-${d.id}`}>
<View className={styles.name}
style={{color: (select === `${prefix}-${d.id}` ? '#45D4A8' : undefined)}}>{d.name}</View>
<View className={styles.secondaryBox}>
loading ? <Spin enable={loading}/> : <>
{
d.resource_category?.map(c => <View
onClick={() => jump(c.id)}
key={d.id}
className={styles.secondary}>
<View className='p-1'>{c.name}</View>
</View>)
list.length ?
list.map(d => <View className={styles.list} onClick={() => jump(d.id)}>{d.name}</View>)
: <Empty name='暂无数据'/>
}
</View>
</View>
)
</>
}
</ScrollView>
</SwiperItem>)
}
</Swiper>
</View>
)
}

@ -10,11 +10,10 @@
box-sizing: border-box;
display: flex;
margin-bottom: 20rpx;
}
Image,
image {
width: 200rpx;
.image {
margin-right: 20rpx;
border-radius: 10rpx;
}
}

@ -1,10 +1,12 @@
import {Image, ScrollView, Swiper, SwiperItem, View} from "@tarojs/components";
import {ScrollView, Swiper, SwiperItem, View} from "@tarojs/components";
import {HomeApi} from "@/api";
import {useEffect, useState} from "react";
import Tabs, {OnChangOpt, TabList} from "@/components/tabs/tabs";
import Empty from "@/components/empty/empty";
import Taro from "@tarojs/taro";
import styles from './profession.module.scss'
import Spin from "@/components/spinner";
import Img from "@/components/image/image";
interface KillData {
data: Kill[]
@ -16,12 +18,13 @@ const Profession = () => {
const [tabs, setTabs] = useState<TabList[]>([])
const [categoryId, setCategoryId] = useState<number | null>(null)
const [data, setData] = useState<Map<number, KillData>>(new Map)
const [enable, setEnable] = useState(true)
/**
* more
*/
async function getData(more = false) {
if (categoryId) {
if (!categoryId) return;
const oldData = new Map<number, KillData>(data)
const categoryData = oldData.get(categoryId)
const page = more ? (categoryData?.page || 0) + 1 : categoryData?.page || 1
@ -31,6 +34,7 @@ const Profession = () => {
return
}
try {
const res = await HomeApi.skillList(categoryId!, page, 10)
const dataList = res.data.reduce((pre, cur) => {
const index = pre.findIndex(d => d.id === cur.id)
@ -42,13 +46,13 @@ const Profession = () => {
return pre
}, categoryData?.data || [])
oldData.delete(categoryId)
oldData.set(categoryId, {
data: dataList,
total: res.total,
page: page
})
setData(oldData)
} catch (e) {
}
}
@ -57,10 +61,14 @@ const Profession = () => {
}, [categoryId])
async function getCategory() {
try {
const res = await HomeApi.skillCategory()
const newTabs = res.map<TabList>(d => ({title: d.name, value: d.id}))
setTabs(newTabs)
setCategoryId(newTabs[0].value as number)
} catch (e) {
}
setEnable(false)
}
function tabsChange(tab: OnChangOpt) {
@ -68,6 +76,7 @@ const Profession = () => {
}
function jump(kill: Kill) {
HomeApi.skillSetPlay(kill.id)
Taro.navigateTo({url: `/pages/preview/videoFull/videoFull?url=${kill.resource.url}&poster=${kill.url_path}&title=${kill.resource.name}`})
}
@ -90,8 +99,8 @@ const Profession = () => {
{
data.data.map(d =>
<View className={styles.killBox} onClick={() => jump(d)}>
<Image src={d.url_path} mode='widthFix'/>
<View className='text-ellipsis flex-1'>{d.resource.name}</View>
<Img width={320} height={180} src={d.url_path} mode='widthFix' className={styles.image}/>
<View className='text-ellipsis flex-1'>{d?.resource?.name}</View>
</View>
)
}
@ -102,6 +111,7 @@ const Profession = () => {
return (
<>
<Spin enable={enable} overlay/>
<View className='bg-white'>
<Tabs tabList={tabs} onChange={tabsChange} current={categoryId!}/>
</View>

@ -0,0 +1,53 @@
.box {
margin-left:30rpx;
margin-bottom: 24rpx;
width:690rpx;
display: flex;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
box-sizing: border-box;
position: relative;
}
.image{
background-color: #ededed;
border-radius: 8rpx;
}
.play {
position: absolute;
z-index: 9999;
width: 50rpx !important;
height: 50rpx !important;
top: 99rpx;
left: 99rpx;
background: transparent !important;
}
.rightBox{
padding-left: 24rpx;
box-sizing: border-box;
flex: 1;
}
.articleLeftBox{
padding-right: 24rpx;
box-sizing: border-box;
flex: 1;
}
.videoRightBox{
padding-left: 24rpx;
box-sizing: border-box;
flex: 1;
}
.desc{
font-size: 24rpx;
font-weight: 500;
color: #909795;
line-height: 34rpx;
display: -webkit-box;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
}

@ -0,0 +1,167 @@
import {FC, useCallback, useEffect, useState} from "react";
import {Image, ScrollView, View} from "@tarojs/components";
import styles from './list.module.scss'
import Taro from "@tarojs/taro";
import Empty from "@/components/empty/empty";
import Img from "@/components/image/image";
import { SearchApi } from "@/api/search";
import { AtLoadMore } from 'taro-ui'
import {rfc33392time} from "@/utils/day";
import play from "@/static/img/play.png";
type Props = {
name:string
clear:boolean
}
const SearchList: FC<Props> = ({name,clear}) => {
console.log(name,'name')
const globalData = Taro.getApp().globalData
const [page, setPage] = useState(1)
const [brands, setBrands] = useState<any[]>([])
const [total, setTotal] = useState(0)
const [text, setText] = useState('')
const [loading, setLoading] = useState(true)
useEffect(()=>{
if(!clear){
setBrands([])
setLoading(true)
}
},[clear])
useEffect(() => {
if(name && clear){
getData()
}
}, [page,name,clear])
const getData = useCallback(async () => {
try {
const data = await SearchApi.list(page, 10,name)
if (page === 1) {
if (data.data.length < 10) {
setText('没有更多了~')
} else {
setText('上拉加载更多~')
}
setBrands([
...data.data
])
}else{
setBrands([
...brands,
...data.data
])
}
setTotal(data.total)
} catch (e) {
}
setLoading(false)
}, [page,name])
function jumpInfo(id: number,type:string,health:any) {
console.log(type,'type')
let url = ''
switch (type){
case 'brand':
url = '/pages/preview/brand/info/info';
break;
case 'illness':
url = '/pages/preview/illness/list/list';
break;
case 'article':
url = '/pages/preview/illness/article/article'
break
case 'video_records':
return Taro.navigateTo({url: `/pages/preview/videoFull/videoFull?url=${health.resources.url}&poster=${health.url_path}&title=${health.title}`})
case 'courses':
url = '/pages/business/videoInfo/videoInfo'
break
}
Taro.navigateTo({url: `${url}?id=${id}`})
}
function onScrollToLower(){
if (brands?.length < total) {
setPage(page + 1)
getData().then()
} else {
setText('没有更多了~')
}
}
return (
<ScrollView
className='scrollview'
scrollY
scrollWithAnimation
style={{height:`${globalData.windowHeight-60}px`,backgroundColor:`#f5f5f5`}}
onScrollToLower={onScrollToLower}
>
{ loading && <AtLoadMore status={'loading'}/>}
{
brands.length >= 1 && !loading &&
<>
{brands.map((d) =>
<View onClick={() => jumpInfo(d.data.id,d.data.table,d.data['@data'])} className={styles.box} key={d.data.id}>
{
d.data.table === 'brand' &&
<>
<Img width={128} height={128} src={d.data['@data'].logo} mode='aspectFill' className={styles.image}/>
<View className={styles.rightBox}>
<View className='font-weight mb-2 font-28'>{d.data.title}</View>
<View className={styles.desc}>{d.data.content || '暂无品牌简介'}</View>
</View>
</>
}
{
d.data.table === 'article' &&
<>
<View className={styles.articleLeftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{d.data['@data'].title}</View>
<View className={styles.desc}>{rfc33392time(d.data['@data'].created_at)} {d.data['@data'].page_view}</View>
</View>
<Img width={172} height={128} src={d.data['@data'].logo} mode='aspectFill' className={styles.image}/>
</>
}
{
d.data.table === 'video_records' &&
<>
<Img width={192} height={192} src={d.data['@data'].url_path} mode='aspectFill' className={styles.image}/>
<Image src={play} className={styles.play} mode='aspectFit'/>
<View className={styles.videoRightBox}>
<View className='font-weight mb-2 font-28 lh-40'>{d.data['@data'].title}</View>
<View className={styles.desc}>{d.data['@data'].introduction}</View>
<View className={`${styles.desc} mt-2`}>: {d.data['@data'].video_view}</View>
</View>
</>
}
{
d.data.table === 'courses' &&
<>
<Img width={192} height={138} src={d.data['@data'].thumb} mode='aspectFill' className={styles.image}/>
<View className={styles.articleLeftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{d.data['@data'].title}</View>
<View className={styles.desc}>:{d.data['@data'].class_hour} {d.data['@data'].charge}</View>
</View>
</>
}
</View>
)
}
<View style={{width: '750rpx', textAlign: 'center', color: '#999'}} className="font-28 mt-3 mb-3">{text}</View>
</>
}
{ !loading && brands.length === 0 && <Empty name='空空如也'/>}
</ScrollView>
)
}
export default SearchList

@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '搜索',
navigationBarBackgroundColor:'#F2F8F6',
onReachBottomDistance: 50
})

@ -0,0 +1,50 @@
page{
background-color:#F2F8F6;
padding-left: 30rpx;
.searchBox{
width: 690rpx;
height: 68rpx;
background: #FFFFFF;
border-radius: 32rpx 32rpx 32rpx 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
padding:0 24rpx;
.input{
font-size: 28rpx;
font-weight: 500;
margin-left: 24rpx;
flex:1;
}
}
.titleBox{
margin-top: 60rpx;
width: 100%;
display: flex;
box-sizing: border-box;
padding-right: 30rpx;
justify-content: space-between;
.title{
font-size: 28rpx;
font-weight: bold;
color: #323635;
}
}
.contentBox{
display: flex;
flex-wrap: wrap;
.items{
display: inline-block;
background: #FFFFFF;
border-radius:32rpx;
padding: 10rpx 24rpx;
font-size: 24rpx;
font-weight: 500;
color: #323635;
line-height: 24rpx;
margin-top: 24rpx;
margin-right: 24rpx;
}
}
}

@ -0,0 +1,116 @@
import {Input, View, Text, PageContainer} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import { AtIcon } from 'taro-ui'
import styles from './index.module.scss'
import Taro from "@tarojs/taro";
import { useReady, useDidShow } from '@tarojs/taro'
import SearchList from './components/list'
const Search:FC = () => {
const [value, setValue] = useState('')
const [recentSearch, setRecentSearch] = useState<string[]>([])
const [hotSearch,setHotSearch] = useState<any[]>([])
const [show, setShow] = useState(false)
useReady(()=>{
console.log('onReady')
})
useDidShow(()=>{
getRecentSearch()
})
useEffect(()=>{
if(!show){
getRecentSearch()
}
},[show])
function inpFn(e){
setValue(e.detail.value)
}
function clearSearch(){
Taro.removeStorageSync('recentSearch')
getRecentSearch()
}
function getRecentSearch () {
setRecentSearch(Taro.getStorageSync('recentSearch'))
}
function getSearchItem(value) {
setValue(value)
// Taro.navigateTo({
// url:`/pages/preview/search/list/index?name=${value}`
// })
setShow(true)
}
function searchInput(){
if (value === "") return;
getSearchItem(value)
//记录最近搜索
let recentSearch = Taro.getStorageSync('recentSearch') || [];
recentSearch.unshift(value);
Taro.setStorageSync('recentSearch', [...new Set(recentSearch)])
}
return (
<View className="flex flex-column">
<View className={styles.searchBox}>
<AtIcon value='search' size='20' color='#ccc'></AtIcon>
{ show ?
<Text className={styles.input} >{value}</Text>:
<Input className={styles.input} placeholder="输入关键字搜索" type={'text'} confirmType={'search'} onInput={inpFn} onConfirm={searchInput} />
}
</View>
{
recentSearch.length >= 1 && !show &&
<>
<View className={styles.titleBox} >
<Text className={styles.title}></Text>
<AtIcon onClick={clearSearch} value='trash' size='16' color='#909795'></AtIcon>
</View>
<View className={styles.contentBox}>
{
recentSearch.length &&
recentSearch?.map(d =>
<View className={styles.items}>
<View onClick={()=>{getSearchItem(d)}} className="font-28" >{d}</View>
</View>)
}
</View>
</>
}
{
hotSearch.length >= 1 && !show &&
<>
<View className={`flex justify-between ${styles.titleBox}`} >
<Text className="font-32 fwb"></Text>
</View>
<View className="wid100 pt-3 pl-3 box-b">
<View className="wid100 flex flex-wrap">
{
hotSearch.length &&
hotSearch.map(d =>
<View className="py-1 px-2 rounded-40 mr-2 mb-2 bg-warning text-white">
<View onClick={()=>{getSearchItem(d)}} className="font-28" >{d}</View>
</View>)
}
</View>
</View>
</>
}
<PageContainer onClickOverlay={()=>{ setShow(false)}} show={show} round={true} overlay={true} overlayStyle={'background:rgba(0,0,0,0)'} >
<SearchList name={value} clear={show} />
</PageContainer>
</View>
)
}
export default Search

@ -34,15 +34,27 @@ const VideoFull: FC = () => {
}
}
function onError() {
Taro.showModal({
title: '视频播放错误',
confirmText: '退出',
showCancel: true,
success() {
Taro.navigateBack()
}
})
}
return (
<>
{params.title && <View className={styles.title}>{params.title}</View>}
<Video
posterSize='100%'
id={'myVideo'}
onClick={click}
className={styles.video}
controls
poster={params.poster}
// poster={params.poster}
src={params.url}
autoplay
showCenterPlayBtn
@ -53,6 +65,7 @@ const VideoFull: FC = () => {
enableProgressGesture={false}
onPlay={() => setpalying(true)}
onPause={() => setpalying(false)}
onError={onError}
/>
</>
)

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '链接',
onReachBottomDistance: 30
})

@ -0,0 +1,9 @@
import {FC} from "react";
import {WebView} from "@tarojs/components";
import {useRouter} from "@tarojs/taro";
const Web:FC = () => {
const {url} = useRouter().params as unknown as { url: string }
return <WebView src={url}></WebView>
}
export default Web

@ -1,14 +1,19 @@
page,
.taro_router .taro_page {
background-color: #f1f8f6 !important;
background-color: #f2f8f6 !important;
font-family: PingFang SC-Bold, PingFang SC;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
font-size: 32rpx;
}
body {
font-size: 32rpx;
View::-webkit-scrollbar,
page::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
.weui-cells_checkbox .weui-check:checked + .weui-icon-checked:before,
@ -59,11 +64,6 @@ taro-button-core::after {
align-items: center;
justify-content: center;
}
.image {
width: 68px;
height: 68px;
}
}
.Textarea {

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

Loading…
Cancel
Save