Explorar el Código

简单的架子

harry hace 1 año
padre
commit
af7c1c306b
Se han modificado 44 ficheros con 2324 adiciones y 108 borrados
  1. 91 88
      package.json
  2. 1 1
      project.config.json
  3. 21 1
      src/app.config.ts
  4. 80 6
      src/app.ts
  5. 99 0
      src/class/AbGroupContext.ts
  6. 69 0
      src/class/GlobalContext.ts
  7. 42 0
      src/class/Network.ts
  8. 30 0
      src/class/Route.ts
  9. 7 0
      src/class/WorkletContext.ts
  10. 0 0
      src/components/Navbar/index.config.ts
  11. 21 0
      src/components/Navbar/index.less
  12. 32 0
      src/components/Navbar/index.tsx
  13. 5 0
      src/components/VideoSwiper/index.config.ts
  14. 225 0
      src/components/VideoSwiper/index.less
  15. 376 0
      src/components/VideoSwiper/index.tsx
  16. 4 0
      src/config/index.ts
  17. 20 0
      src/constants/commentTypes.ts
  18. 3 0
      src/custom-tab-bar/index.config.ts
  19. 44 0
      src/custom-tab-bar/index.less
  20. 54 0
      src/custom-tab-bar/index.tsx
  21. 40 0
      src/custom-tab-bar/index.wxss
  22. 5 0
      src/hooks/index.ts
  23. 13 0
      src/hooks/useHotLaunch.ts
  24. 17 0
      src/http/api/base.ts
  25. 59 0
      src/http/api/index.ts
  26. 152 0
      src/http/index.ts
  27. 4 0
      src/pages/home/home.config.ts
  28. 0 0
      src/pages/home/home.less
  29. 25 0
      src/pages/home/home.tsx
  30. 3 1
      src/pages/index/index.config.ts
  31. 20 0
      src/pages/index/index.less
  32. 172 10
      src/pages/index/index.tsx
  33. 18 0
      src/pages/index/index.wxss
  34. 3 0
      src/pages/user-videos/user-videos.config.ts
  35. 0 0
      src/pages/user-videos/user-videos.less
  36. 16 0
      src/pages/user-videos/user-videos.tsx
  37. 149 0
      src/shareHelper/index.ts
  38. 32 0
      src/store/features/counter.ts
  39. 13 0
      src/store/index.ts
  40. 217 0
      src/utils/index.ts
  41. 57 0
      src/utils/oss.ts
  42. 0 0
      src/utils/updateGlobal.ts
  43. 3 0
      src/utils/userCenter.ts
  44. 82 1
      types/global.d.ts

+ 91 - 88
package.json

@@ -1,90 +1,93 @@
 {
 {
-  "name": " pqVideoMiniprogram",
-  "version": "1.0.0",
-  "private": true,
-  "description": " 票圈视频新小程序,票圈好看视频",
-  "templateInfo": {
-    "name": "default",
-    "typescript": true,
-    "css": "Less",
-    "framework": "React"
-  },
-  "scripts": {
-    "build:weapp": "taro build --type weapp",
-    "build:swan": "taro build --type swan",
-    "build:alipay": "taro build --type alipay",
-    "build:tt": "taro build --type tt",
-    "build:h5": "taro build --type h5",
-    "build:rn": "taro build --type rn",
-    "build:qq": "taro build --type qq",
-    "build:jd": "taro build --type jd",
-    "build:quickapp": "taro build --type quickapp",
-    "build:harmony-hybrid": "taro build --type harmony-hybrid",
-    "dev:weapp": "npm run build:weapp -- --watch",
-    "dev:swan": "npm run build:swan -- --watch",
-    "dev:alipay": "npm run build:alipay -- --watch",
-    "dev:tt": "npm run build:tt -- --watch",
-    "dev:h5": "npm run build:h5 -- --watch",
-    "dev:rn": "npm run build:rn -- --watch",
-    "dev:qq": "npm run build:qq -- --watch",
-    "dev:jd": "npm run build:jd -- --watch",
-    "dev:quickapp": "npm run build:quickapp -- --watch",
-    "dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch",
-    "test": "jest"
-  },
-  "browserslist": [
-    "last 3 versions",
-    "Android >= 4.1",
-    "ios >= 8"
-  ],
-  "author": "",
-  "dependencies": {
-    "@babel/runtime": "^7.21.5",
-    "@tarojs/components": "3.6.23",
-    "@tarojs/helper": "3.6.23",
-    "@tarojs/plugin-platform-weapp": "3.6.23",
-    "@tarojs/plugin-platform-alipay": "3.6.23",
-    "@tarojs/plugin-platform-tt": "3.6.23",
-    "@tarojs/plugin-platform-swan": "3.6.23",
-    "@tarojs/plugin-platform-jd": "3.6.23",
-    "@tarojs/plugin-platform-qq": "3.6.23",
-    "@tarojs/plugin-platform-h5": "3.6.23",
-    "@tarojs/plugin-platform-harmony-hybrid": "3.6.23",
-    "@tarojs/runtime": "3.6.23",
-    "@tarojs/shared": "3.6.23",
-    "@tarojs/taro": "3.6.23",
-    "@tarojs/plugin-framework-react": "3.6.23",
-    "@tarojs/react": "3.6.23",
-    "react-dom": "^18.0.0",
-    "react": "^18.0.0"
-  },
-  "devDependencies": {
-    "@babel/core": "^7.8.0",
-    "@tarojs/cli": "3.6.23",
-    "@types/webpack-env": "^1.13.6",
-    "@tarojs/test-utils-react": "^0.1.1",
-    "@types/react": "^18.0.0",
-    "webpack": "5.78.0",
-    "@tarojs/taro-loader": "3.6.23",
-    "@tarojs/webpack5-runner": "3.6.23",
-    "babel-preset-taro": "3.6.23",
-    "eslint-config-taro": "3.6.23",
-    "eslint": "^8.12.0",
-    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
-    "react-refresh": "^0.11.0",
-    "eslint-plugin-react": "^7.8.2",
-    "eslint-plugin-import": "^2.12.0",
-    "eslint-plugin-react-hooks": "^4.2.0",
-    "stylelint": "^14.4.0",
-    "@typescript-eslint/parser": "^6.2.0",
-    "@typescript-eslint/eslint-plugin": "^6.2.0",
-    "typescript": "^5.1.0",
-    "tsconfig-paths-webpack-plugin": "^4.1.0",
-    "postcss": "^8.4.18",
-    "ts-node": "^10.9.1",
-    "@types/node": "^18.15.11",
-    "@types/jest": "^29.3.1",
-    "jest": "^29.3.1",
-    "jest-environment-jsdom": "^29.5.0"
-  }
+	"name": " pqVideoMiniprogram",
+	"version": "1.0.0",
+	"private": true,
+	"description": " 票圈视频新小程序,票圈好看视频",
+	"templateInfo": {
+		"name": "default",
+		"typescript": true,
+		"css": "Less",
+		"framework": "React"
+	},
+	"scripts": {
+		"build:weapp": "taro build --type weapp",
+		"build:swan": "taro build --type swan",
+		"build:alipay": "taro build --type alipay",
+		"build:tt": "taro build --type tt",
+		"build:h5": "taro build --type h5",
+		"build:rn": "taro build --type rn",
+		"build:qq": "taro build --type qq",
+		"build:jd": "taro build --type jd",
+		"build:quickapp": "taro build --type quickapp",
+		"build:harmony-hybrid": "taro build --type harmony-hybrid",
+		"dev:weapp": "npm run build:weapp -- --watch",
+		"dev:swan": "npm run build:swan -- --watch",
+		"dev:alipay": "npm run build:alipay -- --watch",
+		"dev:tt": "npm run build:tt -- --watch",
+		"dev:h5": "npm run build:h5 -- --watch",
+		"dev:rn": "npm run build:rn -- --watch",
+		"dev:qq": "npm run build:qq -- --watch",
+		"dev:jd": "npm run build:jd -- --watch",
+		"dev:quickapp": "npm run build:quickapp -- --watch",
+		"dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch",
+		"test": "jest"
+	},
+	"browserslist": [
+		"last 3 versions",
+		"Android >= 4.1",
+		"ios >= 8"
+	],
+	"author": "",
+	"dependencies": {
+		"@babel/runtime": "^7.21.5",
+        "@reduxjs/toolkit": "^1.9.7",
+		"@tarojs/components": "3.6.23",
+		"@tarojs/helper": "3.6.23",
+		"@tarojs/plugin-platform-weapp": "3.6.23",
+		"@tarojs/plugin-platform-alipay": "3.6.23",
+		"@tarojs/plugin-platform-tt": "3.6.23",
+		"@tarojs/plugin-platform-swan": "3.6.23",
+		"@tarojs/plugin-platform-jd": "3.6.23",
+		"@tarojs/plugin-platform-qq": "3.6.23",
+		"@tarojs/plugin-platform-h5": "3.6.23",
+		"@tarojs/plugin-platform-harmony-hybrid": "3.6.23",
+		"@tarojs/runtime": "3.6.23",
+		"@tarojs/shared": "3.6.23",
+		"@tarojs/taro": "3.6.23",
+		"@tarojs/plugin-framework-react": "3.6.23",
+		"@tarojs/react": "3.6.23",
+		"@types/react-redux": "^7.1.30",
+		"react-dom": "^18.0.0",
+		"react": "^18.0.0",
+		"react-redux": "^8.1.3"
+	},
+	"devDependencies": {
+		"@babel/core": "^7.8.0",
+		"@tarojs/cli": "3.6.23",
+		"@types/webpack-env": "^1.13.6",
+		"@tarojs/test-utils-react": "^0.1.1",
+		"@types/react": "^18.0.0",
+		"webpack": "5.78.0",
+		"@tarojs/taro-loader": "3.6.23",
+		"@tarojs/webpack5-runner": "3.6.23",
+		"babel-preset-taro": "3.6.23",
+		"eslint-config-taro": "3.6.23",
+		"eslint": "^8.12.0",
+		"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
+		"react-refresh": "^0.11.0",
+		"eslint-plugin-react": "^7.8.2",
+		"eslint-plugin-import": "^2.12.0",
+		"eslint-plugin-react-hooks": "^4.2.0",
+		"stylelint": "^14.4.0",
+		"@typescript-eslint/parser": "^6.2.0",
+		"@typescript-eslint/eslint-plugin": "^6.2.0",
+		"typescript": "^5.1.0",
+		"tsconfig-paths-webpack-plugin": "^4.1.0",
+		"postcss": "^8.4.18",
+		"ts-node": "^10.9.1",
+		"@types/node": "^18.15.11",
+		"@types/jest": "^29.3.1",
+		"jest": "^29.3.1",
+		"jest-environment-jsdom": "^29.5.0"
+	}
 }
 }

+ 1 - 1
project.config.json

@@ -2,7 +2,7 @@
   "miniprogramRoot": "./dist",
   "miniprogramRoot": "./dist",
   "projectname": " pqVideoMiniprogram",
   "projectname": " pqVideoMiniprogram",
   "description": " 票圈视频新小程序,票圈好看视频",
   "description": " 票圈视频新小程序,票圈好看视频",
-  "appid": "touristappid",
+  "appid": "wxf275bb93cdcc9d87",
   "setting": {
   "setting": {
     "urlCheck": true,
     "urlCheck": true,
     "es6": false,
     "es6": false,

+ 21 - 1
src/app.config.ts

@@ -1,11 +1,31 @@
 export default defineAppConfig({
 export default defineAppConfig({
   pages: [
   pages: [
-    'pages/index/index'
+    'pages/index/index',
+    'pages/user-videos/user-videos',
+    'pages/home/home'
   ],
   ],
   window: {
   window: {
     backgroundTextStyle: 'light',
     backgroundTextStyle: 'light',
     navigationBarBackgroundColor: '#fff',
     navigationBarBackgroundColor: '#fff',
     navigationBarTitleText: 'WeChat',
     navigationBarTitleText: 'WeChat',
     navigationBarTextStyle: 'black'
     navigationBarTextStyle: 'black'
+  },
+  "subpackages": [],
+
+  tabBar: {
+    custom: true,
+    color: '#ffffff',
+    selectedColor: '#DC143C',
+    backgroundColor: '#000000',
+    list: [
+      {
+        pagePath: 'pages/index/index',
+        text: '首页'
+      },
+      {
+        pagePath: 'pages/home/home',
+        text: '我的'
+      }
+    ]
   }
   }
 })
 })

+ 80 - 6
src/app.ts

@@ -1,15 +1,89 @@
 import { PropsWithChildren } from 'react'
 import { PropsWithChildren } from 'react'
-import { useLaunch } from '@tarojs/taro'
+import Taro, { useDidHide, useDidShow, useLaunch } from '@tarojs/taro'
+import { globalApp } from '@/class/GlobalContext'
+import {
+    isEasyMode, createSessionId, formatQuery, formatDate
+  } from '@/utils'
+import useHotLaunch from '@/hooks/useHotLaunch'
+
 import './app.less'
 import './app.less'
 
 
 function App({ children }: PropsWithChildren<any>) {
 function App({ children }: PropsWithChildren<any>) {
 
 
-  useLaunch(() => {
-    console.log('App launched.')
-  })
+    useLaunch(async (launch) => {
+        globalApp()
+
+        // // create session id
+        // updateSession()
+
+        // system info
+        updateSystem()
+
+        // updateGlobal(launch)
+
+        // // network change
+        // listeningConnectStatus()
+
+        // // userinfo
+        // await updateUserInfo()
+
+        // mid
+        // const mid = await requestMid()
+
+        // Taro.$global.set('mid', mid)
+        // Taro.setStorageSync('mid', mid)
+
+        // getAccountInfoInApp()
+    })
+
+    // useHotLaunch((options) => {
+    //     Taro.$global.set('subSessionid', createSessionId())
+
+    //     const launchOption = Taro.$global.get('launchOption')
+    //     Taro.$global.set('launchOption', {
+    //         ...launchOption,
+    //         hotScene: options.scene
+    //     })
+
+    //     reportShareVideo()
+    // })
+
+    useDidShow((options) => {
+        // updateAbGroup()
+        // reportUserActiveLog(options)
+        // onEnvByShake()
+    })
+
+    useDidHide(() => {
+        // offEnvByShake()
+
+        Taro.$abGroupInstance
+            .cleanLoop()
+            .cleanStack()
+    })
+    useHotLaunch((options) => {
+        Taro.$global.set('subSessionid', createSessionId())
+    
+        const launchOption = Taro.$global.get('launchOption')
+        Taro.$global.set('launchOption', {
+          ...launchOption,
+          hotScene: options.scene
+        })
+    
+        // reportShareVideo()
+      })
+    // children 是将要会渲染的页面
+    return children
+}
 
 
-  // children 是将要会渲染的页面
-  return children
+// system
+function updateSystem() {
+    const systemInfo = Taro.getSystemInfoSync()
+  
+    Taro.$global.set('systemInfo', {
+      ...systemInfo,
+      easyMode: isEasyMode(systemInfo) && 2 || 1// 关怀模式
+    }) 
 }
 }
 
 
 export default App
 export default App

+ 99 - 0
src/class/AbGroupContext.ts

@@ -0,0 +1,99 @@
+import Taro from '@tarojs/taro'
+
+export const WX_AB_GROUP = { ab_test010: 'ab100'}
+for (let i = 1; i < 10; i++)
+  WX_AB_GROUP[`ab_test00${i}`] = 'ab100'
+
+class AbGroupContext implements AbGroupContextType {
+  #group: object = WX_AB_GROUP
+  #evtStack: Array<Function> = []
+  #timer: NodeJS.Timeout | null
+  #groupMap = new Map()
+
+  constructor() {
+    this.#group = {
+      ...this.#group,
+      ...Taro.getExptInfoSync()
+    }
+  }
+
+  listen(evt) {
+    if (typeof evt !== 'function')
+      return this
+
+    this.#evtStack.push(evt)
+
+    return this
+  }
+
+  emit() {
+    this.#evtStack.forEach(evt => evt())
+
+    return this
+  }
+
+  loop(time: number = 60000) {
+    if (this.#timer)
+      return this
+
+    this.#timer = setInterval(() => {
+      this.emit()
+    }, time)
+
+    return this
+
+  }
+
+  cleanLoop() {
+    if (this.#timer) 
+      clearInterval(this.#timer)
+
+    return this
+  }
+
+  cleanStack() {
+    this.#evtStack = []
+
+    return this
+  }
+
+  get() {
+    return this.#group
+  }
+
+  storeGroup(config) {
+    Object.keys(config).forEach(level => {
+      const abGroupConfigArray = config[level][this.#group[level]] || []
+      abGroupConfigArray.forEach(item => {
+        this.#groupMap.set(item.abExpCode, {
+          level: level,
+          group: this.#group[level],
+          experiment: item.abExpCode,
+          config: item.configValue
+        })
+      })
+    })
+
+    this.setStorage(config)
+  }
+
+  validExperiment(name) {
+    return this.#groupMap.has(name + '')
+  }
+
+  getExperimentConfigByName(name) {
+    return this.#groupMap.get(name + '')
+  }
+
+  setStorage(config) {
+    const EXPERIMENT_CONFIG = {}
+    Object.keys(config).forEach(level => {
+      const abGroupConfigArray = config[level][this.#group[level]] || []
+      EXPERIMENT_CONFIG[level] = abGroupConfigArray
+    })
+    Taro.setStorageSync('$EXPERIMENT_CONFIG', EXPERIMENT_CONFIG)
+  }
+
+}
+
+export default AbGroupContext

+ 69 - 0
src/class/GlobalContext.ts

@@ -0,0 +1,69 @@
+import Taro from '@tarojs/taro'
+import AbGroupContext from '@/class/AbGroupContext'
+import network from '@/class/Network'
+import http from '@/http/index'
+// import { changeLocalEnv } from '@/env'
+
+class GlobalContext {
+  private context = {}
+
+  constructor() {}
+
+  get(key) {
+    return this.context[key]
+  }
+
+  set(key, val) {
+    return this.context[key] = val
+  }
+
+  watch() {}
+
+  dispatch() {}
+}
+
+export default GlobalContext
+
+export function globalApp() {
+  if (Taro.$app)
+    return Taro.$app
+
+  const originGetApp = Taro.getApp
+
+  Taro.getApp = function (...options) {
+    const app = originGetApp.apply(Taro, options)
+    app.$global = Object.freeze(new GlobalContext())
+    // app.$changeEnv = changeLocalEnv
+
+    return new Proxy(app, {
+      get(target, key) {
+        if (target[key] != null)
+          return target[key]
+
+        return target.$global[key]
+      },
+      set() {
+        return false
+      }
+    })
+  }
+
+  Taro.$app = Taro.getApp()
+  Taro.$global = Taro.$app.$global
+  Taro.$http = http
+  Taro.$abGroupInstance = new AbGroupContext()
+  Taro.$wx = wx
+  Taro.$network = network
+  Taro.loginSync = loginSync
+  
+  return Taro.$app
+}
+
+function loginSync(): Promise<TaroGeneral.CallbackResult> {
+  return new Promise((success, fail) => {
+    Taro.login({
+      success,
+      fail
+    })
+  })
+}

+ 42 - 0
src/class/Network.ts

@@ -0,0 +1,42 @@
+import Taro from '@tarojs/taro'
+
+class NetWork {
+  #listenStack: Array<Function> = []
+  networkType = 'none'
+
+  constructor() {
+    Taro.onNetworkStatusChange(res => {
+      this.#listenStack.forEach(evt => evt(res))
+    })
+
+    this.getNetworkType().then((res: Taro.getNetworkType.SuccessCallbackResult) => {
+      const { networkType } = res
+      this.networkType = networkType
+    })
+
+    this.listen((res: Taro.onNetworkStatusChange.CallbackResult) => {
+      const { networkType } = res
+      this.networkType = networkType
+    })
+
+  }
+
+  listen(evt) {
+    if (typeof evt !== 'function')
+      return
+
+    this.#listenStack.push(evt)
+  }
+
+  getNetworkType() {
+    return new Promise((success, fail) => {
+      Taro.getNetworkType({
+        success,
+        fail
+      })
+    })
+  }
+}
+
+
+export default new NetWork()

+ 30 - 0
src/class/Route.ts

@@ -0,0 +1,30 @@
+import Taro from '@tarojs/taro'
+import { type, stringify } from '@/utils'
+
+export default class Route {
+  pageStacks
+  page
+  path
+
+  constructor() {
+    this.pageStacks = Taro.getCurrentPages() || []
+    this.page = this.pageStacks[this.pageStacks.length - 1]
+    this.path = this.page?.route || ''
+  }
+
+  push(obj) {
+    if (obj == null)
+      return
+    
+    if (type(obj, 'String')) {
+      Taro.navigateTo({
+        url: obj
+      })
+    } else if (type(obj, 'Object')) {
+      const { url, query } = obj
+      Taro.navigateTo({
+        url: `${url}?${stringify(query)}`
+      })
+    }
+  }
+}

+ 7 - 0
src/class/WorkletContext.ts

@@ -0,0 +1,7 @@
+class WorkletContext {
+  runOnUI() {}
+  runOnJS() {}
+  shared() {}
+}
+
+export default new WorkletContext()

+ 0 - 0
src/components/Navbar/index.config.ts


+ 21 - 0
src/components/Navbar/index.less

@@ -0,0 +1,21 @@
+.nav-bar {
+  .nav-bar-navigation {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: #000;
+    position: relative;
+
+
+    .back-icon {
+      position: absolute;
+      left: 40px;
+      height: 20px;
+      width: 20px;
+      border-width: 0 0 4px 4px;
+      border-color: #000;
+      border-style: solid;
+      transform: matrix(0.71, 0.71, -.71, 0.71, 0, 0);
+    }
+  }
+}

+ 32 - 0
src/components/Navbar/index.tsx

@@ -0,0 +1,32 @@
+import Taro from '@tarojs/taro'
+import { View } from '@tarojs/components'
+import { getTopSafeHeight } from '@/utils'
+import './index.less'
+
+type NavbarPropsType = {
+  title?: string
+  status?: boolean
+  navigation?: boolean
+  navigationColor?: string
+  textColor?: string
+}
+function Navbar({title, status, navigation, textColor='', navigationColor=''}: NavbarPropsType) {
+  const { navigationBarHeight, statusBarHeight} = getTopSafeHeight()
+
+  function navigateBack() {
+    Taro.navigateBack()
+  }
+
+  return (
+    <View className='nav-bar' style={{ height: 'auto', width: '100vw'}}>
+      {status && <View className='nav-bar-status' style={{ height: `${statusBarHeight}px`}}></View>}
+
+      {navigation && <View className='nav-bar-navigation' style={{ height: `${navigationBarHeight}px`, color: textColor}}>
+        <View className='back-icon' style={{borderColor: navigationColor}} onClick={navigateBack}></View>
+        {title}
+      </View>}
+    </View>
+  )
+}
+
+export default Navbar

+ 5 - 0
src/components/VideoSwiper/index.config.ts

@@ -0,0 +1,5 @@
+export default definePageConfig({
+  // 兼容 skyline 不要删除
+  disableScroll: true,
+  navigationStyle: 'custom'
+})

+ 225 - 0
src/components/VideoSwiper/index.less

@@ -0,0 +1,225 @@
+.video-swiper {
+  width: 100%;
+  height: 100%;
+  background-color: #000;
+
+  .video-swiper-item {
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    box-sizing: border-box;
+
+    .custom-video {
+      width: 100%;
+      height: calc(100% - 100px);
+      position: relative;
+
+      .video {
+        width: 100%;
+        height: 100%
+      }
+
+      .video-play-end-cover {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        background: rgba(0, 0, 0, .95);
+        z-index: 1;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+
+        .container {
+          width: 100%;
+          display: flex;
+          justify-content: space-around;
+          color: #fff;
+          margin-top: 150px;
+          margin-bottom: 300px;
+
+          .item {
+            display: flex;
+            flex-direction: column;
+            font-size: 30px;
+            align-items: center;
+            margin: 0;
+            padding: 0;
+            background: transparent;
+            color: #fff;
+
+            Image {
+              width: 120px;
+              height: 120px;
+              margin-bottom: 20px;
+            }
+          }
+        }
+
+        .more-video {
+          position: relative;
+          width: 100%;
+          color: #fff;
+          font-size: 26px;
+          display: flex;
+          align-items: center;
+          flex-direction: column;
+
+          Image {
+            position: absolute;
+            bottom: 0;
+            width: 108px;
+            height: 106px;
+            animation: turn 1.5s infinite;
+          }
+        }
+      }
+    }
+
+    .video-bar {
+      display: flex;
+      align-items: center;
+      box-sizing: border-box;
+      width: 100%;
+      height: 100px;
+      padding: 0 40px;
+      justify-content: space-between;
+
+      .operation {
+        height: 60px;
+        display: flex;
+        margin-right: 40px;
+        position: relative;
+        align-items: center;
+
+        .bot {
+          width: 2px;
+          height: 2px;
+          border: 2px solid #fff;
+          border-radius: 50%;
+        }
+
+        .margin10 {
+          margin: 0 10px;
+        }
+
+        .tips {
+          position: absolute;
+          width: 100px;
+          height: 40px;
+          border-radius: 4px;
+          background: #fff;
+          font-size: 22px;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          padding: 4px 10px;
+          color: #6a6969;
+          top: -40px;
+          z-index: 1;
+
+          image {
+            width: 40px;
+            height: 40px;
+            margin-right: 8px;
+          }
+        }
+
+        .tips::after {
+          position: absolute;
+          content: ' ';
+          bottom: -10px;
+          left: 4px;
+          width: 0;
+          height: 0;
+          border-top: 16px solid #fff;
+          border-left: 16px solid transparent;
+          border-right: 16px solid transparent;
+        }
+      }
+
+      .right {
+        display: flex;
+        align-items: center;
+
+        .like {
+          display: flex;
+          align-items: center;
+          height: 100px;
+          color: #fff;
+          font-size: 24px;
+  
+          .like-icon {
+            width: 60px;
+            height: 60px;
+          }
+        }
+
+        .share-btn-f {
+          margin-left: 20px;
+          height: 60px;
+          background: #55c57c;
+          display: flex;
+          align-items: center;
+          color: #fff;
+          font-size: 28px;
+
+          .wechat-icon {
+            margin-right: 10px;
+            width: 40px;
+            height: 40px;
+            background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABGCAMAAABsQOMZAAAAflBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////vroaSAAAAKXRSTlMA9wjz1ykDEMQx5IHeXG7ovVQao2jtzqmdkXoVi0g9tU1CIMmvlmI4dBMRDGIAAALGSURBVFjDtddnc6MwEAbglRCYYtPdwL2///8P3lzgBoWiQnLPp8zGZrza1SLRqPy1P4vEYc7yfi4+b04/scieAt+xNHJpptvewSg/88heUDBMCitum+wWauJKNq4htIqaTPEDTCSBabp3mHGOZIL7MMVeJs9LYWFFOt4ZNhztOh5gZ7nQ9AuTP1y+Lzs0JmNndcJC3mFfuyED1DFlqWO5hLcmVozE5H20UVU4QUdQY6WJIaOhsc/51LhoYhA0yYfE4U0wUsQa7uSeY5Admqno6GL4qDKWPWvyLqE+ltKE/XCQMmhjLBScxqWwE+5WJ1e1U5awkMZtLeqc04Q1TLFtQMRPUSoSBsBZ+sUqpwEGQzuXeHl2elHx7I8ex3DprlRH49mk1xlr+Fjw/XQumxN17jAQ01tAgR26Gu2gV3oRg9rGpVYJrY/3gJbzL20XOpG3tXrTCKj5XgEj6/aJFZSYG8FQ0uzInKkTdlmXVZR9BCRstypTqfcH82Zoyf0up1v/jHHsfb9p8dxRdWAp/d2v4pb+4t0GamdarCjdQkox6G/WeDABM91MfAToXJp3hvRuGPTJo4nkISa85AU+97si/CrrVa5SO3qDqWV015AUOfGKoXMPyMsSSEpqHNlEE/ZbjfW7uRcoqHVyRpsmg6HhGSVYjv07hqVEfanwI1hiJBkOAfGzB96HA8Q65ZA6OQbYak5RBrNb6gTrB+6GhxL/xE/xtq15lMBO2T/XbY7U8Gr3fTy6e9i5fT/XiYx6AljxqbPF8uXRgMDMSxYXMacRL1gQnvRArj2D62VkwGUw9SAjlfEuqcnMzu6SqucVdld9PZPDzTIgC17FdB1dk51AQOXpkS1+cDDtRDMsqgRTDjQLfz36P5O1JaG5vHd03oTs67p3f65cHifSbXe+RXc149UaQEW/aBE52NCvqvcsp9+Vu/Sf/AGRfvtxDfRcnAAAAABJRU5ErkJggg==) no-repeat no-repeat center/contain;
+          }
+        }
+      }
+
+    }
+
+    .video-introduce {
+      box-sizing: border-box;
+      width: 100%;
+      position: absolute;
+      left: 0;
+      bottom: 180px;
+      color: #fff;
+      font-size: 30px;
+      padding: 0 20px;
+      text-shadow: #000 1px 0 6px;
+      background: linear-gradient(to bottom, rgba(0, 0, 0, 0), 10%, rgba(0, 0, 0, .1), rgba(0, 0, 0, 0));
+
+      .video-user-info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          width: 50px;
+          height: 50px;
+          border-radius: 50%;
+          margin-right: 15px;
+        }
+      }
+
+      .video-title {
+        margin: 10px 0;
+      }
+
+      .video-play-count {
+        font-size: 28px;
+      }
+    }
+
+    @keyframes turn { 
+      0% {             
+          transform: translate(0px, -150px);            
+       }            
+       50% {                
+          transform: translate(0px, -50px);            
+       }            
+       100% {                
+          transform: translate(0px, -150px);
+       }
+    }
+  }
+}

+ 376 - 0
src/components/VideoSwiper/index.tsx

@@ -0,0 +1,376 @@
+import { useEffect, useState, useCallback } from 'react'
+import Taro, { useReady } from '@tarojs/taro'
+import {
+  Swiper, SwiperItem, Video,
+  View,  Image, Button
+} from '@tarojs/components'
+import { DETAIL_PAGESOURCE, CATEGORY_PAGESOURCE, LIKE_ICON, DEFAULT_ICON, VIEW_AUTO_TYPE } from '@/const'
+import { favoriteUrl, unfavoriteUrl } from '@/http/api'
+import Route from '@/class/Route'
+import { videoViewReport, videoPlayReport, videoActionReport } from '@/logger'
+import { once } from '@/utils'
+import './index.less'
+
+
+let activeIndex = 0
+let favoritePending = false
+const videoViewedMap = new Map()
+
+export default function VideoSwiper({ list, onFinish, circular, onFavorited }) {
+  const [currentIndex, setCurrentIndex] = useState(0)
+
+  useReady(() => {
+    activeIndex = 0
+    videoViewedMap.clear()
+  })
+
+  function onSwiperChange(res) {
+    const { detail } = res || {}
+    const { current } = detail || {}
+    activeIndex = current
+  }
+
+  function onAnimationFinish() {
+    onFinish(activeIndex)
+    setCurrentIndex(activeIndex)
+  }
+
+  return (
+    <Swiper
+      className='video-swiper'
+      vertical
+      circular={circular}
+      current={currentIndex}
+      onChange={onSwiperChange}
+      onAnimationFinish={onAnimationFinish}
+    >
+      {list.map((video, index) => {
+        return (
+          <SwiperItem className='video-swiper-item' key={video.id}>
+            <CustomVideo video={video} current={currentIndex} index={index}/>
+            <VideoIntroduce detail={video} />
+            <VideoBar video={video} onFavorited={onFavorited}/>
+          </SwiperItem>
+        )
+      })}
+    </Swiper>
+  )
+}
+
+function CustomVideo({ video, current, index }) {
+  const [showEndCover, setShowEndCover] = useState(false)
+  let videoContext = Taro.createVideoContext(`${video.id}`)
+  const logReportVideoPlaySuccessOnce = once(logReportVideoPlaySuccess)
+  const logReportVideoRealPlayOnce = once(logReportVideoRealPlay)
+  const logReportVideoPlayEndOnce = once(logReportVideoPlayEnd)
+  
+  useEffect(() => {
+    logReportVideoView(video, current, index)
+
+    if (current === index) {
+      videoContext.play()
+
+      logReportVideoPlay(video)
+    } else {
+      videoContext.pause()
+    }
+  }, [current, video])
+
+  useEffect(() => {
+    setShowEndCover(false)
+  }, [current])
+
+  function onTimeUpdate(evt) {
+    if (current !== index)
+      return
+    
+    const { detail } = evt || {}
+    const { currentTime, duration } = detail || {}
+
+    logReportVideoPlaySuccessOnce(video)
+
+    if (currentTime / duration >= 0.3 || currentTime >= 20)
+      logReportVideoRealPlayOnce(video)
+  }
+
+  function onEnded() {
+    logReportVideoPlayEndOnce(video)
+    current === index && setShowEndCover(true)
+  }
+
+  function onRePlay() {
+    setShowEndCover(false)
+    videoContext.seek(0)
+    videoContext.play()
+
+    logReportVideoPlay(video)
+  }
+
+  return (
+    <View className='custom-video'>
+      <Video
+        className='video'
+        id={`${video.id}`}
+        src={video.videoPath || ''}
+        controls
+        autoplay={false}
+        showCenterPlayBtn={false}
+        poster={video.videoCoverSnapshotPath}
+        onTimeUpdate={onTimeUpdate}
+        onEnded={onEnded}
+      />
+
+      {showEndCover && <VideoPlayEndCover onRePlay={onRePlay} />}
+    </View>
+  )
+}
+
+function VideoIntroduce({detail}) {
+  return (
+    <View className='video-introduce'>
+      <View className='video-user-info'>
+        <Image className='avatar' src={detail.avatarUrl} />
+        {detail.nickName}
+      </View>
+      <View className='video-title'>
+        {detail.title}
+      </View>
+      <View className='video-play-count'>
+        播放{detail.playCount}次
+      </View>
+    </View>
+  )
+}
+
+function VideoBar({ video, onFavorited }) {
+  const [likeIcon, setLikeIcon] = useState(video.favorited ? LIKE_ICON : DEFAULT_ICON)
+  const [showTips, setShowTips] = useState(false)
+
+  useEffect(() => {
+    setLikeIcon(video.favorited ? LIKE_ICON : DEFAULT_ICON)
+  }, [video.favorited])
+
+  function clickLikeIcon(favorite) {
+    if (favoritePending)
+      return
+
+    favoritePending = true
+
+    const url = favorite ? favoriteUrl : unfavoriteUrl
+    onFavorited(video.id, favorite)
+    
+    Taro.$http.post(url, {
+      videoId: video.id
+    }).then((res: RequestType) => {
+      const { code } = res
+
+      favoritePending = false
+
+      if (code !== 0) {
+        onFavorited(video.id, !favorite)
+        Taro.showToast({
+          title: '收藏失败',
+          icon: 'error'
+        })
+        return
+      }
+    }).catch(() => {
+      favoritePending = false
+      onFavorited(video.id, !favorite)
+      Taro.showToast({
+        title: '收藏失败',
+        icon: 'error'
+      })
+    })
+  }
+
+  function clickOperationBar(e) {
+    e.stopPropagation()
+    setShowTips(!showTips)
+  }
+
+  function clickTips() {
+    const route = new Route()
+    route.push({
+      url: '/pages/complaining/index',
+      query: {
+        videoId: video.id
+      }
+    })
+  }
+
+  function clickVideoBar(e) {
+    e.stopPropagation()
+    setShowTips(false)
+  }
+
+  return (
+    <View className='video-bar' onClick={clickVideoBar}>
+      <View className='operation' onClick={clickOperationBar}>
+        <View className='bot'></View>
+        <View className='bot margin10'></View>
+        <View className='bot'></View>
+        {showTips && <View className='tips' onClick={clickTips}>
+          <Image src='http://weapppiccdn.yishihui.com/wxicon/common/icon_jubao.png' />
+          举报
+        </View>}
+      </View>
+      <View className='right'>
+        <View className='like'>
+          <Image className='like-icon' src={likeIcon} onClick={() => clickLikeIcon(!video.favorited)}/>
+        </View>
+        <Button className='share-btn-f' openType='share' data-button-type={6}>
+          <View className='wechat-icon' />
+          发给好友
+        </Button>
+      </View>
+    </View>
+  )
+}
+
+function VideoPlayEndCover({ onRePlay }) {
+  return (
+    <View className='video-play-end-cover'>
+      <View className='container'>
+        <View className='item left'>
+          <Image src='http://weapppiccdn.yishihui.com/wxicon/common/icon_replay_01.png' onClick={onRePlay} />
+          重播
+        </View>
+        <Button className='item right' openType='share' data-button-type={10}>
+          <Image src='http://weapppiccdn.yishihui.com/wxicon/common/icon_share_wechat_01.png' />
+          发给好友
+        </Button>
+      </View>
+      <View className='more-video'>
+        <Image src='http://weapppiccdn.yishihui.com/wxicon/common/icon_slide_tips.png' />
+        上滑看更多精彩视频
+      </View>
+    </View>
+  )
+}
+
+// 视频曝光
+function logReportVideoView(video, current, index) {
+  if (videoViewedMap.has(video.id))
+    return
+
+  if (current !== index)
+    return
+
+  if (video.type === 'temp')
+    return
+
+  videoViewReport({
+    actionPosition: index,
+    autoType: VIEW_AUTO_TYPE.clickView,
+    extParams: JSON.stringify({
+      recomTraceId: video.recomTraceId,
+    }),
+    viewId: video.viewId,
+    flowPool: video.flowPool || '',
+    measureId: video.measure,
+    measureType: video.measureType,
+    pageSource: DETAIL_PAGESOURCE,
+    recommendLogVO: video.recommendLogVO || '',
+    recommendSource: video.recommendSource,
+    rootPageSource: '', // TODO: 分享之后加上
+    rootPageTimestamp: '', // TODO: 分享之后加上
+    shareDepth: '', // TODO: 分享之后加上
+    videoIds: [video.id]
+  }).then(() => videoViewedMap.set(video.id, video.id))
+}
+// 视频播放
+function logReportVideoPlay(video) {
+  if (video.type === 'temp')
+    return
+
+  videoPlayReport({
+    videoId: video.id,
+    pageSource: DETAIL_PAGESOURCE,
+    rootPageSource: CATEGORY_PAGESOURCE,
+    shareDepth: '', // TODO: 分享之后
+    rootPageTimestamp: '', // TODO: 分享之后
+    rootMid: '', // TODO: 分享之后
+    recommendSource: video.recommendSource || 0,
+    autoType: VIEW_AUTO_TYPE.clickView,
+    playId: video.playId,
+    viewId: video.viewId,
+    measureType: video.measureType || 0,
+    measureId: video.measure,
+    flowPool: video.flowPool || '',
+    recommendId: video.recommendId || '',
+    recommendLogVO: video.recommendLogVO || ''
+  })
+}
+// 播放成功
+function logReportVideoPlaySuccess(video) {
+  if (video.type === 'temp')
+    return
+
+  videoActionReport({
+    businessType: 'videoPlaySuccess',
+    videoId: video.id,
+    pageSource: DETAIL_PAGESOURCE,
+    rootPageSource: CATEGORY_PAGESOURCE,
+    shareDepth: '', // TODO: 分享之后
+    rootPageTimestamp: '', // TODO: 分享之后
+    rootMid: '', // TODO: 分享之后
+    recommendSource: video.recommendSource || 0,
+    autoType: VIEW_AUTO_TYPE.clickView,
+    playId: video.playId,
+    viewId: video.viewId,
+    measureType: video.measureType || 0,
+    measureId: video.measure,
+    flowPool: video.flowPool || '',
+    recommendId: video.recommendId || '',
+    recommendLogVO: video.recommendLogVO || ''
+  })
+}
+// 真实播放
+function logReportVideoRealPlay(video) {
+  if (video.type === 'temp')
+    return
+
+  videoActionReport({
+    businessType: 'videoRealPlay',
+    videoId: video.id,
+    pageSource: DETAIL_PAGESOURCE,
+    rootPageSource: CATEGORY_PAGESOURCE,
+    shareDepth: '', // TODO: 分享之后
+    rootPageTimestamp: '', // TODO: 分享之后
+    rootMid: '', // TODO: 分享之后
+    recommendSource: video.recommendSource || 0,
+    autoType: VIEW_AUTO_TYPE.clickView,
+    playId: video.playId,
+    viewId: video.viewId,
+    measureType: video.measureType || 0,
+    measureId: video.measure,
+    flowPool: video.flowPool || '',
+    recommendId: video.recommendId || '',
+    recommendLogVO: video.recommendLogVO || ''
+  })
+}
+// 播放结束
+function logReportVideoPlayEnd(video) {
+  if (video.type === 'temp')
+    return
+
+  videoActionReport({
+    businessType: 'videoPlayEnd',
+    videoId: video.id,
+    pageSource: DETAIL_PAGESOURCE,
+    rootPageSource: CATEGORY_PAGESOURCE,
+    shareDepth: '', // TODO: 分享之后
+    rootPageTimestamp: '', // TODO: 分享之后
+    rootMid: '', // TODO: 分享之后
+    recommendSource: video.recommendSource || 0,
+    autoType: VIEW_AUTO_TYPE.clickView,
+    playId: video.playId,
+    viewId: video.viewId,
+    measureType: video.measureType || 0,
+    measureId: video.measure,
+    flowPool: video.flowPool || '',
+    recommendId: video.recommendId || '',
+    recommendLogVO: video.recommendLogVO || ''
+  })
+}

+ 4 - 0
src/config/index.ts

@@ -0,0 +1,4 @@
+export const APP_ID = 'wxf275bb93cdcc9d87'
+export const APP_TYPE = 23
+export const APP_VERSION_CODE = '2.0.768'
+export const VERSION_CODE = 768

+ 20 - 0
src/constants/commentTypes.ts

@@ -0,0 +1,20 @@
+export const PlayState = {
+    playing: 0,
+    pause: 1,
+    stoped: 2 
+}
+
+export type VideoInfo = {
+    id: number;
+    title: string;
+    coverImgPath: string;
+    shareTitle: string;
+    videoPath: string;
+    recomTraceId: string;
+    flowPool: string;
+    user: {
+        uid: number;
+        nickName: string;
+        avatarUrl: string;
+    }
+}

+ 3 - 0
src/custom-tab-bar/index.config.ts

@@ -0,0 +1,3 @@
+export default {
+    "component": true
+  }

+ 44 - 0
src/custom-tab-bar/index.less

@@ -0,0 +1,44 @@
+.tab-bar {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 110px;
+    background: white;
+    display: flex;
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+  
+  .tab-bar-border {
+    background-color: rgba(0, 0, 0, 0.33);
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 1px;
+    transform: scaleY(0.5);
+  }
+  
+  .tab-bar-item {
+    flex: 1;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    .line {
+        width: 100px;
+        height: 8px;
+        margin-top: 20px;
+        background-color: white;
+    }
+  }
+  
+  .tab-bar-item cover-image {
+    width: 54px;
+    height: 54px;
+  }
+  
+  .tab-bar-item cover-view {
+    font-size: 20px;
+  }

+ 54 - 0
src/custom-tab-bar/index.tsx

@@ -0,0 +1,54 @@
+import { Component } from 'react'
+import Taro from '@tarojs/taro'
+import { CoverView, View } from '@tarojs/components'
+
+import './index.less'
+
+export default class Index extends Component {
+  state = {
+    selected: 0,
+    color: '#999999',
+    selectedColor: '#ffffff',
+    backgroundColor: '#000000',
+    fontSize: 22,
+    list: [
+      {
+        pagePath: '/pages/index/index',
+        text: '首页'
+      },
+      {
+        pagePath: '/pages/home/home',
+        text: '我的'
+      }
+    ]
+  }
+
+  switchTab(index, url) {
+    this.setSelected(index)
+    Taro.switchTab({ url })
+  }
+
+  setSelected (idx: number) {
+    this.setState({
+      selected: idx
+    })
+  }
+
+  render() {
+    const { list, selected, color, selectedColor, backgroundColor, fontSize } = this.state
+
+    return (
+      <CoverView className='tab-bar' style={{ backgroundColor: backgroundColor}}>
+        <CoverView className='tab-bar-border'></CoverView>
+        {list.map((item, index) => {
+          return (
+            <CoverView key={index} className='tab-bar-item' onClick={this.switchTab.bind(this, index, item.pagePath)}>
+              <CoverView style={{ color: selected === index ? selectedColor : color, fontSize: fontSize }}>{item.text}</CoverView>
+              <View className='line' style={{ backgroundColor: selected === index ? 'white' : 'transparent'  }}></View>
+            </CoverView>
+          )
+        })}
+      </CoverView>
+    )
+  }
+}

+ 40 - 0
src/custom-tab-bar/index.wxss

@@ -0,0 +1,40 @@
+.tab-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 110px;
+  background: white;
+  display: flex;
+  padding-bottom: env(safe-area-inset-bottom);
+}
+.tab-bar-border {
+  background-color: rgba(0, 0, 0, 0.33);
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 1px;
+  transform: scaleY(0.5);
+}
+.tab-bar-item {
+  flex: 1;
+  text-align: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.tab-bar-item .line {
+  width: 100px;
+  height: 8px;
+  margin-top: 20px;
+  background-color: white;
+}
+.tab-bar-item cover-image {
+  width: 54px;
+  height: 54px;
+}
+.tab-bar-item cover-view {
+  font-size: 20px;
+}

+ 5 - 0
src/hooks/index.ts

@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
+import type { RootState, AppDispatch } from '../store'
+
+export const useAppDispatch: () => AppDispatch = useDispatch
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

+ 13 - 0
src/hooks/useHotLaunch.ts

@@ -0,0 +1,13 @@
+import { useDidShow } from '@tarojs/taro'
+
+type useHotLaunchCbType = (options) => void
+
+export default function useHotLaunch(cb: useHotLaunchCbType) {
+  useDidShow((options) => {
+    useHotLaunch.valid && cb(options)
+
+    useHotLaunch.valid = true
+  })
+}
+
+useHotLaunch.valid = true

+ 17 - 0
src/http/api/base.ts

@@ -0,0 +1,17 @@
+export const BASE_DOMAIN = {
+  development: 'https://videotest.yishihui.com',
+  staging: 'https://videopre.piaoquantv.com',
+  production: 'https://vlogapi.piaoquantv.com'
+}
+
+export const INVITE_API_DOMAIN = {
+  development: 'https://testapi.piaoquantv.com',
+  staging: 'https://preapi.piaoquantv.com',
+  production: 'https://api.piaoquantv.com'
+}
+
+export const BASE_COMMON_DOMAIN = {
+  development: 'https://videotest.yishihui.com',
+  staging: 'https://precommon.piaoquantv.com',
+  production: 'https://common.piaoquantv.com'
+}

+ 59 - 0
src/http/api/index.ts

@@ -0,0 +1,59 @@
+import Taro from '@tarojs/taro'
+import { BASE_DOMAIN, INVITE_API_DOMAIN, BASE_COMMON_DOMAIN } from './base'
+
+function env(BASE_DOMAIN) {
+  const envApi = Taro.getStorageSync('env')
+  return BASE_DOMAIN[envApi || 'production']
+}
+
+export const ossSignatureUrl = `${env(BASE_DOMAIN)}/longvideoapi/oss/signature`
+
+export const getWxUserInfoByCode = `${env(BASE_DOMAIN)}/longvideoapi/user/getWxUserInfoByCode`
+
+export const getAbtestConfig = `${env(BASE_DOMAIN)}/longvideoapi/abtest/config`
+
+export const getPositionInfo = `${env(INVITE_API_DOMAIN)}/ad/position/info`
+
+export const generateMidv2 = `${env(BASE_COMMON_DOMAIN)}/commonapi/generateMid/v2`
+
+export const getUserFrontConfig = `${env(BASE_DOMAIN)}/longvideoapi/frontConfig/getUserFrontConfig`
+
+export const getSysConfig = `${env(BASE_DOMAIN)}/longvideoapi/sys/config`
+
+export const videoListV2 = `${env(BASE_DOMAIN)}/longvideoapi/video/distribute/category/videoList/v2`
+
+export const videoDetail = `${env(BASE_DOMAIN)}/longvideoapi/video/v2/detail`
+
+export const recommendSharePageList = `${env(BASE_DOMAIN)}/longvideoapi/video/recommend/sharePage/list`
+
+export const favoriteUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/favorite`
+
+export const unfavoriteUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/unfavorite`
+
+export const getUserInfoExt = `${env(BASE_DOMAIN)}/longvideoapi/user/info/getUserInfoExt`
+
+export const getHomepageHead = `${env(BASE_DOMAIN)}/longvideoapi/user/info/homepageHead`
+
+export const getFavoriteList = `${env(BASE_DOMAIN)}/longvideoapi/video/v2/favorite/list`
+
+export const userInfoUpdateUrl = `${env(BASE_DOMAIN)}/longvideoapi/user/info/update`
+
+export const reasonList = `${env(BASE_DOMAIN)}/longvideoapi/video/report/reason/list`
+
+export const videoReport = `${env(BASE_DOMAIN)}/longvideoapi/video/report`
+
+export const videoActionPreReportUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/videoActionPreReport`
+
+export const videoViewUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/view`
+
+export const playedUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/played`
+
+export const videoActionReportUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/videoActionReport`
+
+export const shareReportUrl = `${env(BASE_DOMAIN)}/longvideoapi/user/share/report`
+
+export const weixinFriendUrl = `${env(BASE_DOMAIN)}/longvideoapi/video/shared/weixin/friend`
+
+export const shareClickUrl = `${env(BASE_DOMAIN)}/longvideoapi/user/share/click`
+
+export const uploadLogFromFrontendUrl = `${env(INVITE_API_DOMAIN)}/log-center/statistics/uploadLogFromFrontend`

+ 152 - 0
src/http/index.ts

@@ -0,0 +1,152 @@
+import Taro from '@tarojs/taro'
+import Route from '@/class/Route'
+import { APP_ID, APP_TYPE, APP_VERSION_CODE, VERSION_CODE } from '@/config'
+
+class Http {
+  get(url: string, data = {}, config: ConfigType = {}) {
+    const { header = {} } = config
+
+    data = wrapData(data)
+
+    return new Promise((success, fail) => {
+      const expParams: ConfigType = {
+        header: {
+          'content-type': 'application/x-www-form-urlencoded',
+          ...header
+        }
+      }
+
+      Taro.request({
+        url,
+        data,
+        success,
+        fail,
+        ...expParams
+      })
+    })
+  }
+
+  post(url: string, data = {}, config: ConfigType = {}) {
+    const { header = {} } = config
+
+    data = wrapData(data)
+
+    if ((data as DataType).baseInfo)
+      data = createBaseInfoToFetchData(data)
+
+    return new Promise((resolve, reject) => {
+      const expParams: ConfigType = {
+        header: {
+          'content-type': 'application/x-www-form-urlencoded',
+          ...header
+        },
+        method: 'POST'
+      }
+
+      Taro.request({
+        url,
+        data,
+        success(res) {
+          const { data, statusCode } = res
+
+          if (statusCode !== 200) {
+            reject(data)
+            return
+          }
+
+          resolve(data)
+        },
+        fail(res) {
+          reject(res)
+        },
+        ...expParams
+      })
+    })
+  }
+}
+
+export function wrapData(params) {
+  const systemInfo = Taro.$global.get('systemInfo')
+  const launchOption = Taro.$global.get('launchOption')
+  const isReturningUser = Taro.$global.get('isReturningUser')
+  const userInfo = Taro.$global.get('userInfo') || {}
+  const EXPERIMENT_CONFIG = Taro.getStorageSync('$EXPERIMENT_CONFIG')
+
+  const route = new Route()
+
+  const defaultInfo = {
+    appId: APP_ID,
+    appType: APP_TYPE,
+    realAppType: APP_TYPE, // ?
+    versionCode: VERSION_CODE,
+    appVersionCode: APP_VERSION_CODE,
+    token: userInfo.accessToken || '',
+    loginUid: userInfo.uid || '',
+    clientTimestamp: new Date().getTime(),
+    pageSource: route.path,
+    machineCode: Taro.$global.get('mid') || '',
+    machineInfo: getMachineInfoFromSystem(),
+    networkType: Taro.$network.networkType,
+    network: Taro.$network.networkType,
+    platform: systemInfo.platform,
+    system: systemInfo.system,
+    careModelStatus: systemInfo.easyMode,
+    senceType: launchOption.scene,
+    hotSenceType: launchOption.hotScene,
+    abExpInfo: JSON.stringify(EXPERIMENT_CONFIG),
+    sessionId: Taro.$global.get('sessionId'),
+    subSessionId: Taro.$global.get('subSessionId'),
+  }
+
+  // 单独处理30天无回流
+  if (isReturningUser) {
+    params.extParams = JSON.stringify({
+      ...params?.extParams,
+      has30DayReturnCnt: isReturningUser
+    })
+  }
+
+  return {
+    ...defaultInfo,
+    ...params
+  }
+}
+
+export function createBaseInfoToFetchData(data) {
+  const baseKey = ['token', 'loginUid', 'appType', 'machineCode', 'platform', 'machineInfo', 'networkType', 'pageSource', 'rootPageSource', 'clientTimestamp', 'sessionId', 'subSessionId', 'requestId', 'pageCategoryId', 'rootPageCategoryId', 'openType', 'shareDepth', 'eventId', 'videoReportMeta', 'rootPageTimestamp', 'returnId', 'system']
+
+  return Object.keys(data).reduce((calc, key) => {
+    if (baseKey.includes(key)) {
+      calc.baseInfo[key] = data[key]
+    } else {
+      calc.params[key] = data[key]
+    }
+
+    return calc
+  }, { baseInfo: {}, params: {} })
+}
+
+export function getMachineInfoFromSystem() {
+  const systemInfo = Taro.$global.get('systemInfo')
+  const keys = ['sdkVersion', 'brand', 'language', 'model', 'platform', 'system', 'version', 'screenHeight', 'screenWidth', 'pixelRatio', 'windowHeight', 'windowWidth']
+
+  const result = keys.reduce((calc, key) => {
+    // 适配其他小程序数据上报
+    if (key === 'version')
+      key = 'weChatVersion'
+
+    if (key === 'SDKVersion')
+      key = 'sdkVersion'
+
+    calc[key] = systemInfo[key]
+
+    return calc
+  }, {})
+
+  return JSON.stringify({
+    softVersion: '', // TODO: appConfig.versionName
+    ...result
+  })
+}
+
+export default new Http()

+ 4 - 0
src/pages/home/home.config.ts

@@ -0,0 +1,4 @@
+export default definePageConfig({
+  navigationBarTitleText: '个人主页',
+  usingComponents: {},
+})

+ 0 - 0
src/pages/home/home.less


+ 25 - 0
src/pages/home/home.tsx

@@ -0,0 +1,25 @@
+import { useMemo } from 'react'
+import type CustomTabBar from '../../custom-tab-bar'
+import { View, Text } from '@tarojs/components'
+import Taro, { useLoad, useDidShow } from '@tarojs/taro'
+import './home.less'
+
+export default function Index() {
+  const page = useMemo(() => Taro.getCurrentInstance().page, [])
+
+  useLoad(() => {
+    console.log('Page loaded.')
+  })
+
+  useDidShow(() =>{
+    const tabbar = Taro.getTabBar<CustomTabBar>(page)
+    tabbar?.setSelected(1)
+
+  })
+
+  return (
+    <View className='index'>
+      <Text>Hello world!</Text>
+    </View>
+  )
+}

+ 3 - 1
src/pages/index/index.config.ts

@@ -1,3 +1,5 @@
 export default definePageConfig({
 export default definePageConfig({
-  navigationBarTitleText: '首页'
+  navigationBarTitleText: '首页',
+  navigationStyle: 'custom',
+  usingComponents: {},
 })
 })

+ 20 - 0
src/pages/index/index.less

@@ -0,0 +1,20 @@
+.container {
+    background-color: black;
+    width: 100%;
+    height: calc(100% - 110px);
+    position: absolute;
+    top: 0;
+
+    .video_list {
+        display: flex;
+        overflow: scroll;
+        width: 100%;
+        height: 100%;
+
+        .videoplayer {
+            width: 100%;
+            height: calc(100% - 40px);
+            margin-top: 40px;
+        }
+    }
+}

+ 172 - 10
src/pages/index/index.tsx

@@ -1,16 +1,178 @@
-import { View, Text } from '@tarojs/components'
-import { useLoad } from '@tarojs/taro'
+import Taro, { useLoad, useDidShow, useReady, useDidHide, usePullDownRefresh, useShareAppMessage, useRouter } from '@tarojs/taro'
+import { useMemo, useState } from 'react'
+import type CustomTabBar from '@/custom-tab-bar'
+import { View, Swiper, SwiperItem, Video } from '@tarojs/components'
+import { VideoInfo } from '@/constants/commentTypes'
+import { videoListV2 } from '@/http/api'
+// import { getTopSafeHeight, throttle, formatSecondsAsTime, getPreIds } from '@/utils'
+
+// import Route from '@/class/Route'
 import './index.less'
 import './index.less'
 
 
+let pageNo = 1
+let pending = false
+
 export default function Index() {
 export default function Index() {
+    const page = useMemo(() => Taro.getCurrentInstance().page, []);
+    const router = useRouter()
+    const { params } = router || {}
+
+
+    let state = {
+        currentIdx: 0,
+        currentSwiperIdex: 0,
+    }
+    const [videoList, setVideoList] = useState<VideoInfo[]>([])
+    const [refresherTriggered, setRefresherTriggered] = useState(false)
+
+    useLoad(() => {
+        const { redirect } = params
+        if (redirect) {
+            // const route = new Route()
+            // route.push(decodeURIComponent(redirect))
+        }
+        fecthVideoListV2()
+    })
+
+    useDidShow(() => {
+        const tabbar = Taro.getTabBar<CustomTabBar>(page)
+        tabbar?.setSelected(0)
+
+    })
+
+    useReady(() => {
+        setVideoList([
+            {
+                id: 8987051,
+                videoPath: 'http://rescdn.yishihui.com/longvideo/multitranscode/video/vpc/20210821/1244745QN8L8QWQjlbUmyQ5yI20210822131000216222495-3HD.mp4',
+                title: '标题 1',
+                shareTitle: '标题 1',
+                coverImgPath: 'http://rescdn.yishihui.com/longvideo/snapshot/vpc/20210821/1244745QN8L8QWQjlbUmyQ5yI_0?x-oss-process=image/resize,m_fill,w_600,h_480,limit_0/format,jpg/watermark,image_eXNoL3BpYy93YXRlcm1hcmtlci9iZ195eS5wbmc_eC1vc3MtcHJvY2Vzcz1pbWFnZS9yZXNpemUsaF80OS9yZXNpemUsbV9maWxsLHdfMTg1LGhfNDksbGltaXRfMA==,g_nw,x_408,y_414/watermark,type_d3F5LW1pY3JvaGVp,size_31,text_NS405LiH5Lq655yL6L-H,color_FFFFFF,t_100,g_nw,x_420,y_423/watermark,image_eXNoL3BpYy93YXRlcm1hcmtlci9pY29uX3BsYXlfd2hpdGUucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLHdfMTQ0,g_center',
+                recomTraceId: '',
+                flowPool: '',
+                user: {
+                    uid: 6282380,
+                    nickName: 'dax15818675906',
+                    avatarUrl: 'http://rescdn.yishihui.com/longvideo/pic/vpc/20201219/1244745CJ2mPPZ3eXhsjjjf8t?x-oss-process=image/rotate,0/resize,m_fill,w_100,h_100,limit_0/format,jpg'
+                }
+
+            },
+            {
+                id: 9105322,
+                videoPath: 'http://visionularcdn.yishihui.com/output/longvideo/multitranscode/video/vpc/20211112/12188054KWIADXkh5GTl8w2ijD20211114191000658534245-1LD.mp4',
+                title: '生存法则',
+                shareTitle: '生存法则',
+                coverImgPath: 'http://rescdn.yishihui.com/longvideo/pic/62e7c34380754aeab87fc1386fb879d61631682451076?x-oss-process=image/resize,m_fill,w_600,h_480,limit_0/format,jpg/watermark,image_eXNoL3BpYy93YXRlcm1hcmtlci9iZ195eS5wbmc_eC1vc3MtcHJvY2Vzcz1pbWFnZS9yZXNpemUsaF80OS9yZXNpemUsbV9maWxsLHdfMTg1LGhfNDksbGltaXRfMA==,g_nw,x_408,y_414/watermark,type_d3F5LW1pY3JvaGVp,size_31,text_NS4y5LiH5Lq655yL6L-H,color_FFFFFF,t_100,g_nw,x_420,y_423/watermark,image_eXNoL3BpYy93YXRlcm1hcmtlci9pY29uX3BsYXlfd2hpdGUucG5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLHdfMTQ0,g_center',
+                recomTraceId: '',
+                flowPool: '',
+                user: {
+                    uid: 6282380,
+                    nickName: '寰娱圈子',
+                    avatarUrl: 'http://rescdn.yishihui.com/longvideo/pic/vpc/20210401/151084326SAWDfr35MbV13Yqjf?x-oss-process=image/quality,q_10'
+                }
+
+            }
+        ])
+    })
+
+    useDidHide(() => {
+
+    })
+
+    usePullDownRefresh(() => {
+
+    })
+
+    useShareAppMessage(() => {
+        return {
+            title: '好友分享给你一个视频,点击查看~',
+        }
+    })
+
+    // const fecthVideoList = throttle(fecthVideoListV2)
+    function fecthVideoListV2(cleanOldCardList = false) {
+        Taro.$http.post(videoListV2, {
+            categoryId: 55,
+            pageNo,
+            pageSize: 6,
+            pageSource: 'pages/category'
+        }).then((res: RequestType) => {
+            const { code, data } = res
+            if (code != 0)
+                return
+
+            let oldCardList = videoList
+
+            if (cleanOldCardList) {
+                oldCardList = []
+                setRefresherTriggered(false)
+            }
+
+            pageNo++
+
+            setVideoList([
+                ...oldCardList,
+                ...filterCardProps(data)
+            ])
+
+        }).catch(() => {
+            pending = false
+        })
+    }
+
+    function pause() {
+        console.log('hhz-')
+    }
+
+    // 视频卡片需要参数过滤重组一次,并不需要把所有的数据都传进去
+    function filterCardProps(list) {
+        return list.map((item: RoughCardType) => {
+            const {
+                title, coverImg, user, id, videoPath,
+                recomTraceId, flowPool, shareTitle = ''
+            } = item || {}
+            const { coverImgPath = '' } = coverImg || {}
+            const { avatarUrl = '', nickName = '', uid = 0 } = user || {}
+
+            return {
+                id,
+                title,
+                coverImgPath,
+                shareTitle,
+                videoPath,
+                recomTraceId,
+                flowPool,
+                user:{
+                    uid,
+                    avatarUrl,
+                    nickName,
+                }
+            }
+        })
+    }
 
 
-  useLoad(() => {
-    console.log('Page loaded.')
-  })
+    return (
+        <View className='container'>
+            <Swiper
+                className="video_list"
+                duration={300}
+                circular
+                vertical
+            >
+                {videoList.map(item => (
+                    <SwiperItem key={item.id} className="banner_list__item">
+                        <Video className='videoplayer'
+                            src={item.videoPath}
+                            loop
+                            controls={false}
+                            enableProgressGesture={false}
+                            onTap={pause}
+                        >
 
 
-  return (
-    <View className='index'>
-      <Text>Hello world!</Text>
-    </View>
-  )
+                        </Video>
+                    </SwiperItem>
+                ))}
+            </Swiper>
+        </View>
+    )
 }
 }

+ 18 - 0
src/pages/index/index.wxss

@@ -0,0 +1,18 @@
+.container {
+  background-color: black;
+  width: 100%;
+  height: calc(100% - 110px);
+  position: absolute;
+  top: 0;
+}
+.container .video_list {
+  display: flex;
+  overflow: scroll;
+  width: 100%;
+  height: 100%;
+}
+.container .video_list .videoplayer {
+  width: 100%;
+  height: calc(100% - 40px);
+  margin-top: 40px;
+}

+ 3 - 0
src/pages/user-videos/user-videos.config.ts

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

+ 0 - 0
src/pages/user-videos/user-videos.less


+ 16 - 0
src/pages/user-videos/user-videos.tsx

@@ -0,0 +1,16 @@
+import { View, Text } from '@tarojs/components'
+import { useLoad } from '@tarojs/taro'
+import './user-videos.less'
+
+export default function Index() {
+
+  useLoad(() => {
+    console.log('Page loaded.')
+  })
+
+  return (
+    <View className='index'>
+      <Text>Hello world!</Text>
+    </View>
+  )
+}

+ 149 - 0
src/shareHelper/index.ts

@@ -0,0 +1,149 @@
+import Taro from '@tarojs/taro'
+import { stringify, S4 } from '@/utils'
+import useHotLaunch from '@/hooks/useHotLaunch'
+import { RECOMMEND_PAGESOURCE, USER_SHARE_PAGESOURCE, CATEGORY_PAGESOURCE, DETAIL_PAGESOURCE, DETAIL_RECOMMEND } from '@/const'
+import { shareReport, weixinFriend, shareClickReport } from '@/logger'
+import { USER_SENCE } from '@/const/index'
+
+export function sharePageAppMessage({ video, activeIndex, router, shareRes }) {
+  useHotLaunch.valid = false
+  const { path, params } = router
+  const launchOption = Taro.$global.get('launchOption')
+  const { target } = shareRes || {}
+  const { dataset } = target || {}
+
+  return shareVideoToWechat({
+    path,
+    video,
+    pageSource: USER_SHARE_PAGESOURCE,
+    recommendPageType: RECOMMEND_PAGESOURCE,
+    parentRootPageSource: CATEGORY_PAGESOURCE,
+
+    rootPageSource: params.rootPageSource,
+    shareDepth: params.shareDepth,
+    rootPageTimestamp: params.rootPageTimestamp,
+    rootShareId: params.rootShareId,
+    rootMid: params.rootMid,
+    rootLaunchShareId: params.rootLaunchShareId,
+    parentEventIds: params.eventIds,
+
+    actionPosition: activeIndex,
+    scene: launchOption.scene,
+    shareButtonType: dataset?.buttonType || 0,
+    prePageSource: '',
+    userSence: params.userSence
+  })
+}
+
+export function shareVideoToWechat(params) {
+  const { video } = params
+  const shareId = Taro.$global.get('mid') + '-' + S4() + new Date().getTime()
+
+  // TODO: 处理 pagesource
+  let pageSource = video.head ? USER_SHARE_PAGESOURCE : RECOMMEND_PAGESOURCE
+
+  if (params.userSence !== USER_SENCE.share)
+    pageSource = video.head ? DETAIL_PAGESOURCE : DETAIL_RECOMMEND
+
+  const userInfo = Taro.$global.get('userInfo')
+
+  const query = {
+      userSence: USER_SENCE.share,
+      mid: Taro.$global.get('mid'),
+      rootMid: params.rootMid || Taro.$global.get('mid'),
+      shareId,
+      pageSource,
+      rootPageSource: params.rootPageSource || pageSource,
+      measureType: video.measureType,
+      measureId: video.measure,
+      flowPool: video.flowPool || '',
+      launchscene:params.scene,
+      su: userInfo.uid,
+
+      parentShareId: params.parentShareId || shareId,
+      rootPageTimestamp: params.rootPageTimestamp || new Date().getTime(),
+      rootLaunchShareId: params.rootLaunchShareId || shareId,
+      rootShareId: params.rootShareId || shareId,
+      rootSharePageType: video.sharePageType,
+
+      viewId: video.viewId,
+      playId: video.playId,
+      shareDepth:  +params.shareDepth + 1 || 0,
+      isRecommendShare: video.isRecommendShare,
+      recommendSource: video.recommendSource,
+      recommendLogVO: video.recommendLogVO || '{}',
+      parentRootPageSource: params.parentRootPageSource || params.rootPageSource,
+      shareImageId: video.shareImgId,
+      shareTitleId: video.titleId,
+      shareButtonType: params.shareButtonType, // 按钮类型
+      videoReuseData: JSON.stringify({
+          id: video.id || 0,
+          width: video.width,
+          height: video.height,
+          rotate: video.rotate,
+          videoPath: video.videoPath.replace(/\?/g, '+++').replace(/=/g, '---') || '',
+          transcodeStatus: video.transcodeStatus,
+          recommendSource: video.recommendSource || 0,
+          measureId: video.measure || '',
+          measureType: video.measureType || 0,
+          user: { uid: video.uid || 0 }
+      }),
+      ...videoPlayParams(video)
+  }
+
+  const detailPath = `${params.path}?${stringify(query)}`
+
+  shareReport(query)
+  weixinFriend(query)
+
+  return {
+    title: video.title || '好友分享给你一个视频,点击查看~',
+    imageUrl: video.shareImgPath,
+    path: `/pages/category/index?redirect=${encodeURIComponent(detailPath)}`
+  }
+}
+
+function videoPlayParams(video) {
+  const {
+    id, title, shareImgPath,
+    videoCoverSnapshotPath, videoPath, avatarUrl,
+    playCount, nickName, favorited
+  } = video
+
+  return {
+    title: encodeURIComponent(title || ''),
+    videoCoverSnapshotPath: encodeURIComponent(videoCoverSnapshotPath),
+    videoPath: encodeURIComponent(videoPath),
+    id,
+    avatarUrl: encodeURIComponent(avatarUrl),
+    playCount,
+    nickName,
+    shareImgPath: encodeURIComponent(shareImgPath),
+    favorited
+  }
+}
+
+// 分享回流上报
+export function returnedCrowdClickReport(params) {
+  if (params.userSence !== USER_SENCE.share)
+    return
+
+  shareClickReport({
+    shareId: params.shareId,
+    clickObjectId: params.id || 0,
+    pageSource: params.pageSource,
+    rootPageSource: params.rootPageSource,
+    shareButtonType: params.shareButtonType,
+    shareDepth: params.shareDepth || 1,
+    rootLaunchShareId: params.rootLaunchShareId || '',
+    rootShareId: params.rootShareId || '',
+    shareTitle: params.title,
+    shareImageUrl: params.shareImgPath,
+    shareTitleId: params.shareTitleId,
+    shareImageId: params.shareImageId,
+    parentShareId: params.parentShareId,
+    extJson: JSON.stringify({
+        parentRootPageSource: params.parentRootPageSource || params.rootPageSource,
+    })
+  })
+}

+ 32 - 0
src/store/features/counter.ts

@@ -0,0 +1,32 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+interface CounterState {
+  value: number
+}
+
+const initialState: CounterState = {
+  value: 0
+}
+
+export const counterSlice = createSlice({
+  name: 'counter',
+  initialState,
+  reducers: {
+    increment: state => {
+      console.log('increment')
+      state.value += 1
+    },
+    decrement: state => {
+      console.log('decrement')
+      state.value -= 1
+    },
+    incrementByAmount: (state, action: PayloadAction<number>) => {
+      console.log('incrementByAmount')
+      state.value += action.payload
+    },
+  },
+})
+
+export const { increment, decrement, incrementByAmount } = counterSlice.actions
+
+export default counterSlice.reducer

+ 13 - 0
src/store/index.ts

@@ -0,0 +1,13 @@
+import { configureStore } from '@reduxjs/toolkit'
+import counterReducer from './features/counter'
+
+const store = configureStore({
+  reducer: {
+    counter: counterReducer
+  }
+})
+
+export type RootState = ReturnType<typeof store.getState>
+export type AppDispatch = typeof store.dispatch
+
+export default store

+ 217 - 0
src/utils/index.ts

@@ -0,0 +1,217 @@
+import Taro from '@tarojs/taro'
+
+export function isEasyMode(systemInfo: Taro.getSystemInfoSync.Result) {
+  const { fontSizeSetting = 0 } = systemInfo
+
+  if (systemInfo.platform.indexOf('ios') !== -1)
+    return fontSizeSetting > 16
+
+  if (systemInfo.platform.indexOf('Android') !== -1)
+    return fontSizeSetting > 17
+
+  return false
+}
+
+export function S4() {
+  return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
+}
+
+export function guid() {
+  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()
+}
+
+function sessionGuid() {
+  return `${new Date().getTime()}-${guid()}`
+}
+
+export function getPreIds() {
+  return 'pre-' + sessionGuid()
+}
+
+export function getViewIds() {
+  return 'view-' + sessionGuid()
+}
+
+export function createSessionId() {
+  return `${new Date().getTime()}-${guid()}`
+}
+
+export function getTopSafeHeight() {
+  // 状态栏 刘海屏
+  const systemInfo = Taro.getSystemInfoSync()
+  const { statusBarHeight = 0 } = systemInfo
+  // 胶囊信息
+  const menuInfo = Taro.getMenuButtonBoundingClientRect()
+  // 导航高度固定 44px
+  let navigationBarHeight = (menuInfo.top - statusBarHeight) * 2 + menuInfo.height
+
+  navigationBarHeight = Math.min(44, navigationBarHeight)
+
+  return {
+    navigationBarHeight,
+    statusBarHeight
+  }
+}
+
+export function throttle(fn, wait = 50) {
+  let previous = 0
+  return function(...args) {
+    let now = +new Date()
+    if (now - previous > wait) {
+      previous = now
+      fn.apply(this, args)
+    }
+  }
+}
+
+export function formatSecondsAsTime(secs) {
+  var hr = Math.floor(secs / 3600) + ''
+  var min = Math.floor((secs - +hr * 3600) / 60) + ''
+  var sec = Math.floor(secs - +hr * 3600 - +min * 60) + ''
+  var text
+  if (+hr < 10) {
+      hr = '0' + hr
+  }
+  if (+min < 10) {
+      min = '0' + min
+  }
+  if (+sec < 10) {
+      sec = '0' + sec
+  }
+  if (+hr == 0) {
+      hr = ''
+  }
+  if (hr.length > 0) {
+      text = hr + ':' + min + ':' + sec
+  } else {
+      text = min + ':' + sec
+  }
+  return text
+}
+
+export function type(object, type) {
+  return Object.prototype.toString.call(object) === `[object ${type}]`
+}
+
+export function stringify(obj) {
+  if (!type(obj, 'Object'))
+    return ''
+
+  return Object.entries(obj).reduce((calc, [key, val], index) => {
+    calc += `${key}=${val}`
+
+    if (index !== Object.entries(obj).length - 1)
+      calc += '&'
+
+    return calc
+  }, '')
+}
+
+export const squaredImageUrl = (originalURL, rotate, width) => {
+  // 正方形: rotate 旋转角度(整型),  方形图片
+  var url = originalURL
+  var idx = originalURL.indexOf('?')
+  if (idx !== -1) {
+      // 防止该URL已经拼接过参数
+      url = originalURL.substringToIndex(idx)
+  }
+  return url + '?x-oss-process=image/rotate,' + Math.ceil(rotate) + '/resize,m_fill,w_' + Math.ceil(width) + ',h_' + Math.ceil(width) + ',limit_0' + '/format,jpg'
+}
+
+export function filterCoverPath(path) {
+  if (path && path.indexOf('?') === -1) {
+      path += `?t=${Date.now()}`
+  }
+  path = path.replace(/resupload\.piaoquantv\.com/g, 'rescdn.yishihui.com')
+  path = path.replace(/weappupload\.piaoquantv\.com/g, 'rescdn.yishihui.com')
+
+  return path
+}
+
+export function mediaRotate(orientation) {
+  switch (orientation) {
+    case 'down':
+        return 180
+
+    case 'left':
+        return -90
+
+    case 'right':
+        return 90
+
+    case 'up-mirrored':
+        return 0
+
+    case 'down-mirrored':
+        return 180
+
+    case 'left-mirrored':
+        return -90
+
+    case 'right-mirrored':
+        return 90
+
+    default:
+        return 0
+  }
+}
+
+export function once(cb) {
+  let count = 1
+
+  return (...args) => {
+    if (count++ > 1)
+      return
+
+    cb.apply(null, args)
+  }
+}
+
+export function formatQuery(options) {
+  if (Object.prototype.toString.call(options) !== '[object Object]')
+    return options
+
+  const orgOptions = options
+  const params = {}
+
+  let jumpPage = ''
+
+  Object.keys(options).forEach((key) => {
+    try {
+        const decodeVal = decodeURIComponent(options[key])
+        
+        params[key] = decodeVal
+
+        if (key === 'jumpPage')
+            jumpPage = decodeVal
+
+    } catch (e) {}
+  })
+
+  return {
+    params,
+    jumpPage,
+    orgOptions
+  }
+}
+
+export function formatDate(date, fmt) {
+  var o = {
+      'M+': date.getMonth() + 1, //月份
+      'd+': date.getDate(), //日
+      'h+': date.getHours(), //小时
+      'm+': date.getMinutes(), //分
+      's+': date.getSeconds(), //秒
+      'q+': Math.floor((date.getMonth() + 3) / 3), //季度
+      S: date.getMilliseconds() //毫秒
+  }
+  if (/(y+)/.test(fmt)) {
+      fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+  }
+  for (var k in o) {
+      if (new RegExp('(' + k + ')').test(fmt)) {
+          fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
+      }
+  }
+  return fmt
+}

+ 57 - 0
src/utils/oss.ts

@@ -0,0 +1,57 @@
+import Taro from '@tarojs/taro'
+import http from '@/http'
+import { ossSignatureUrl } from '@/http/api'
+import network from '@/class/Network';
+
+export async function uploadImageFile(imageInfo) {
+  const signature = await _getSignature(1)
+
+  if (!signature.accessId)
+    return Promise.reject({ message: 'no signature accessId' })
+
+  return _uploadFile(imageInfo.tempFilePath, signature)
+}
+
+function _getSignature(fileType) {
+  return http.post(ossSignatureUrl, { fileType: fileType })
+    .then((res: RequestType) => {
+      const { data, code } = res
+      if (code !== 0)
+        return {}
+
+      return data
+    }).catch(err => {
+      console.log(err)
+      return {}
+    })
+}
+
+function _uploadFile(fileTempPath, signature) {
+  return new Promise((resolve, reject) => {
+    Taro.uploadFile({
+      filePath: fileTempPath,
+      url: signature.host,
+      name: 'file',
+      formData: {
+          key: signature.fileName,
+          OSSAccessKeyId: signature.accessId,
+          policy: signature.policy,
+          signature: signature.signature,
+          success_action_status: '200',
+          name: fileTempPath,
+          networkType: network.networkType
+      },
+      success() {
+        resolve({
+          fileUrl: signature.host + signature.fileName,
+          objectKey: signature.fileName,
+          host: signature.host,
+          relativeUrl: signature.fileName
+        })
+      },
+      fail(err) {
+        reject(err)
+      }
+    })
+  })
+}

+ 0 - 0
src/utils/updateGlobal.ts


+ 3 - 0
src/utils/userCenter.ts

@@ -0,0 +1,3 @@
+// class UserCenter {
+//   getUserInfo
+// }

+ 82 - 1
types/global.d.ts

@@ -26,4 +26,85 @@ declare namespace NodeJS {
   }
   }
 }
 }
 
 
-
+interface RequestType {
+    code: number,
+    data: any,
+    msg: string
+  }
+  
+  interface AbGroupContextType {
+    listen(evt: Function): this
+    emit(): this
+    loop(time?: number): this
+    cleanLoop(): this
+    cleanStack(): this
+    get(): object
+    storeGroup(config: object): void
+    getExperimentConfigByName(name: string|number): object | undefined
+    validExperiment(name: string|number): boolean
+  }
+  
+  type ConfigType = {
+    header?: {},
+    method?: 'GET' | 'POST',
+    timeout?: number
+  }
+  
+  type DataType = {
+    [key: string]: any
+    baseInfo?: object
+  }
+  
+  type HttpInstanceType = {
+    get(url: string, data?: {}, config?: ConfigType): Promise<any>
+    post(url: string, data?: {}, config?: ConfigType): Promise<any>
+  }
+  
+  type RoughCardType = {
+    title: string
+    shareTitle: string
+    coverImg: any
+    playCount: number
+    totalTime: number
+    user: any
+    id: number
+    videoPath: string
+    videoCoverSnapshotPath: string
+    shareImgPath: string
+    favorited: boolean
+    recomTraceId: number
+    flowPool: string
+    measure: number
+    measureType: number
+    recommendLogVO: string
+    recommendSource: number
+  }
+  
+  type CardType = {
+    title: string
+    coverImgPath: string
+    playCount: number
+    totalTime: number
+    avatarUrl: string
+    nickName: string
+    id: number
+    videoPath: string
+    videoCoverSnapshotPath: string
+    shareImgPath: string
+    favorited: boolean
+  }
+  
+  declare namespace Taro {
+    // eslint-disable-next-line @typescript-eslint/no-empty-interface
+    interface TaroStatic {
+      $app: Taro.getApp.Instance<TaroGeneral.IAnyObject>
+      $global: any
+      $wx: any
+      $http: HttpInstanceType
+      $abGroupInstance: AbGroupContextType
+      $network: any
+      loginSync(): Promise<TaroGeneral.CallbackResult>
+    }
+  }
+  
+  declare const wx