Commit 7e79f207 authored by 王能飞's avatar 王能飞

Merge branch 'c_client_taro-pack' into 'c_client_taro'

新模版合并

See merge request !38
parents 10bc6224 105bc537
No preview for this file type
```
taobao-mini-template
└─ c_client
├─ .editorconfig
├─ .eslintrc.js
├─ .gitignore
├─ babel.config.js
├─ config
│ ├─ default.js
│ ├─ dev.js
│ ├─ index.js
│ └─ prod.js
├─ jsconfig.json
├─ minigame
│ ├─ package-lock.json
│ ├─ package.json
│ └─ yarn.lock
├─ package-lock.json
├─ package.json
├─ plugin
│ └─ changeAppJsonPlugin.js
├─ project.config.json
├─ src
│ ├─ api.js
│ ├─ app.config.js
│ ├─ app.js
│ ├─ app.less
│ ├─ components
│ │ ├─ _base
│ │ │ ├─ Modal
│ │ │ │ ├─ Modal.config.js
│ │ │ │ ├─ Modal.jsx
│ │ │ │ └─ Modal.module.less
│ │ │ ├─ Overlay
│ │ │ │ ├─ OverLay.module.less
│ │ │ │ └─ Overlay.jsx
│ │ │ └─ Popup
│ │ │ ├─ Popup.jsx
│ │ │ └─ Popup.module.less
│ │ ├─ _miniprogram
│ │ │ ├─ game
│ │ │ │ ├─ game.acss
│ │ │ │ ├─ game.axml
│ │ │ │ ├─ game.js
│ │ │ │ ├─ game.json
│ │ │ │ ├─ output.js
│ │ │ │ └─ utils.js
│ │ │ ├─ lottieAnimation
│ │ │ │ ├─ lottieAnimation.acss
│ │ │ │ ├─ lottieAnimation.axml
│ │ │ │ ├─ lottieAnimation.js
│ │ │ │ └─ lottieAnimation.json
│ │ │ ├─ lottieBlank
│ │ │ │ ├─ lottieBlank.acss
│ │ │ │ ├─ lottieBlank.axml
│ │ │ │ ├─ lottieBlank.js
│ │ │ │ └─ lottieBlank.json
│ │ │ └─ memberModal
│ │ │ ├─ memberModal.acss
│ │ │ ├─ memberModal.axml
│ │ │ ├─ memberModal.js
│ │ │ └─ memberModal.json
│ │ ├─ _tb_comps
│ │ │ ├─ CountDown
│ │ │ │ ├─ CountDown.jsx
│ │ │ │ └─ CountDown.less
│ │ │ ├─ GoodsList
│ │ │ │ ├─ GoodsList.jsx
│ │ │ │ └─ GoodsList.module.less
│ │ │ ├─ GoodsTitle
│ │ │ │ ├─ GoodsTitle.jsx
│ │ │ │ └─ GoodsTitle.module.less
│ │ │ ├─ Input
│ │ │ ├─ Progress
│ │ │ │ ├─ Progress.jsx
│ │ │ │ └─ Progress.module.less
│ │ │ ├─ RankList
│ │ │ │ ├─ RankList.jsx
│ │ │ │ └─ RankList.module.less
│ │ │ ├─ RankTitle
│ │ │ │ ├─ RankTitle.jsx
│ │ │ │ └─ RankTitle.module.less
│ │ │ ├─ ScrollXView
│ │ │ │ ├─ ScrollXView.jsx
│ │ │ │ └─ ScrollXView.module.less
│ │ │ └─ Swiper
│ │ │ ├─ Swiper.jsx
│ │ │ └─ Swiper.module.less
│ │ ├─ _tb_custom
│ │ │ └─ Test
│ │ │ ├─ Test.jsx
│ │ │ └─ Test.module.less
│ │ └─ _tb_modal
│ │ ├─ DeliveryModal
│ │ │ ├─ DeliveryModal.jsx
│ │ │ └─ DeliveryModal.module.less
│ │ ├─ DoHelpModal
│ │ │ ├─ DoHelpModal.jsx
│ │ │ └─ DoHelpModal.module.less
│ │ ├─ PrizeModal
│ │ │ ├─ PrizeModal.jsx
│ │ │ └─ PrizeModal.module.less
│ │ ├─ RuleModal
│ │ │ ├─ RuleModal.jsx
│ │ │ └─ RuleModal.module.less
│ │ ├─ TasksModal
│ │ │ ├─ TasksModal.config.js
│ │ │ ├─ TasksModal.jsx
│ │ │ └─ TasksModal.module.less
│ │ └─ ThanksModal
│ │ ├─ ThanksModal.jsx
│ │ └─ ThanksModal.module.less
│ ├─ config.js
│ ├─ const.js
│ ├─ hooks
│ │ ├─ useAuth.js
│ │ ├─ useDebounce.js
│ │ ├─ useLogin.js
│ │ ├─ useRequest.js
│ │ ├─ useTasks.js
│ │ └─ useThrottle.js
│ ├─ index.html
│ ├─ lottie
│ │ ├─ index.js
│ │ └─ lottieData.json
│ ├─ mock.js
│ ├─ pages
│ │ ├─ index
│ │ │ ├─ index.config.js
│ │ │ ├─ index.jsx
│ │ │ └─ index.module.less
│ │ ├─ packageGame
│ │ │ └─ game
│ │ │ ├─ game.config.js
│ │ │ ├─ game.jsx
│ │ │ └─ game.module.less
│ │ ├─ packageGood
│ │ │ ├─ browseGoods
│ │ │ │ ├─ browseGoods.config.js
│ │ │ │ ├─ browseGoods.jsx
│ │ │ │ └─ browseGoods.module.less
│ │ │ ├─ cartGoods
│ │ │ │ ├─ cartGoods.config.js
│ │ │ │ ├─ cartGoods.jsx
│ │ │ │ └─ cartGoods.module.less
│ │ │ ├─ collectGoods
│ │ │ │ ├─ collectGoods.config.js
│ │ │ │ ├─ collectGoods.jsx
│ │ │ │ └─ collectGoods.module.less
│ │ │ └─ orderGoods
│ │ │ ├─ orderGoods.config.js
│ │ │ ├─ orderGoods.jsx
│ │ │ └─ orderGoods.module.less
│ │ └─ packagePrize
│ │ └─ myPrize
│ │ ├─ myPrize.config.js
│ │ ├─ myPrize.jsx
│ │ └─ myPrize.module.less
│ ├─ store
│ │ ├─ activityInfo.js
│ │ ├─ index.js
│ │ └─ loginInfo.js
│ ├─ styles
│ └─ utils
│ ├─ date.js
│ ├─ mdProxy.js
│ └─ util.js
└─ yarn.lock
```
\ No newline at end of file
1.安装依赖
```
npm install
```
2.运行项目(淘宝环境)
```
npm run dev:alipay
```
3.使用淘宝IDE打开dist文件夹关联相应小程序
4.生产环境打包
```
npm run build:alipay
```
5.使用IDE上传版本
export default {
hasGameModule:false, // 是否开启游戏模块自动注入dist
hasLottieAnimation: false,// 使用Lottie动画
hasCanvas: false, // 是否开启游戏模块自动注入dist
defaultPlugins: [
[
'@pluve/taro-plugin-mars', // taro暂时未支持阿里小程序自定义插件 需要引用插件的页面需配置
{
usingComponents: {// 原生组件引入
enable: true,
include: [ 'pages/index/index', 'pages/myPrize/myPrize' ]
enable: true,
include: [ 'pages/index/index', 'pages/packagePrize/myPrize/myPrize','pages/packageGame/game/game']
}
}
]
],
addPages: ['pages/games/games'],// 游戏页面(自动注入app.json)'pages/games/games'
gamePages: [// 拷贝相关页面和依赖到dist目录
{ from: 'src/pages/games', to: 'dist/pages/games' },
canvasPages: [
{'root': 'pages/packageGame/game/game.json', 'comRoot': 'components/_miniprogram/canvasComp/canvasComp'}, // 游戏组件
// {'root': 'pages/index/index.json', 'comRoot': 'components/_miniprogram/lottieAnimation/lottieAnimation'} // Lottie组件
],// 引入cavas组件页面(更改canvas引用路径)
canvasComps: [// 拷贝相关页面和依赖到dist目录
{ from: 'minigame/node_modules', to: 'dist/node_modules'},
{ from: 'src/components/_miniprogram/taskModal', to: 'dist/components/taskModal'},
// { from: 'src/lottie', to: 'dist/lottie'},
// { from: 'src/components/_miniprogram/lottieAnimation', to: 'dist/components/lottieAnimation'}
],
lottiePages: [
{ from: 'minigame/node_modules', to: 'dist/node_modules'},
{ from: 'src/lottie', to: 'dist/lottie'},
{ from: 'src/components/_miniprogram/lottieAnimation', to: 'dist/components/lottieAnimation'}
{ from: 'src/components/_miniprogram/canvasComp', to: 'dist/components/_miniprogram/canvasComp'}, // 游戏组建引入
// { from: 'src/components/_miniprogram/lottieAnimation', to: 'dist/components/_miniprogram/lottieAnimation'}, // lottie 动画组件
// { from: 'src/lottie', to: 'dist/lottie'},// lottie json文件
]
}
\ No newline at end of file
// eslint-disable-next-line import/no-commonjs
const path = require('path')
const { hasGameModule, defaultPlugins, addPages, gamePages, hasLottieAnimation, lottiePages } = require('./default').default
const { hasCanvas, defaultPlugins, canvasComps } = require('./default').default
const config = {
projectName: 'taro-test',
......@@ -16,22 +16,22 @@ const config = {
alias: {
'@': path.resolve(__dirname, '..', 'src/')
},
plugins: hasGameModule || hasLottieAnimation ? [...defaultPlugins,[ // 是否开启原生页面自动注入
path.resolve(__dirname,'..')+'/plugin/changeAppJsonPlugin.js',
{
hasGameModule,
hasLottieAnimation
}
]] : defaultPlugins,
plugins: hasCanvas ? [...defaultPlugins,[ // 是否开启原生页面自动注入
path.resolve(__dirname,'..')+'/plugin/changeAppJsonPlugin.js'
]
] : defaultPlugins,
defineConstants: {
},
copy: {
patterns: hasGameModule ? gamePages : hasLottieAnimation ? lottiePages : [],
patterns: hasCanvas ? canvasComps : [],
options: {
}
},
framework: 'react',
mini: {
compile: {
exclude: []
},
postcss: {
pxtransform: {
enable: true,
......
......@@ -4,7 +4,7 @@
"requires": true,
"dependencies": {
"fyge": {
"version": "2.0.23",
"version": "2.0.31",
"resolved": "https://registry.npmjs.org/fyge/-/fyge-2.0.23.tgz",
"integrity": "sha512-5EA6FHrwpp9sAMyctuyE+gyUH10uGT5PPp1O1gZfohJNx1/bkCPcUomnXApa8367tJ5UiPhiVi3dZeqUdJiZVQ=="
}
......
......@@ -4,6 +4,6 @@
"main": "",
"license": "MIT",
"dependencies": {
"fyge": "*"
"fyge": "2.0.31"
}
}
......@@ -2,20 +2,7 @@
# yarn lockfile v1
duiba-utils@^1.0.0:
version "1.0.9"
resolved "http://npm.dui88.com:80/duiba-utils/-/duiba-utils-1.0.9.tgz#7a1e09e08a8754a1563b18aa6b117cb270f76877"
integrity sha512-3YB5w05gNruS4yyN0cx2Qktr0vG0AXYOumfV6WYlzlMx3kGX3AihBLT2/PouERtkLfFk2q4cIikSkgwramHTmg==
fyge@*:
version "2.0.23"
resolved "http://npm.dui88.com:80/fyge/-/fyge-2.0.23.tgz#36550f93d9578cd3b02ae9772f93aa94730abd25"
integrity sha512-nNUD5HAPyknIMKdxxa50HzriLuBFmk4XLnZw5CgiX3mH7ROJzsh/ToRMTKXcxwH3r3ICid95cgEo0iCPIHSIsQ==
dependencies:
duiba-utils "^1.0.0"
tslib "^2.1.0"
tslib@^2.1.0:
version "2.2.0"
resolved "http://npm.dui88.com:80/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha1-+yxHWXfjXiQTEe3iaTzuHsZpj1w=
fyge@2.0.31:
version "2.0.31"
resolved "https://registry.yarnpkg.com/fyge/-/fyge-2.0.31.tgz#49afcdfef92e23b53e42bef6a93f3b2211af281e"
integrity sha512-HIt26NvgQ5udlI5jJwYgVSqBdfy1qX0+L1TseJNuyUVWgzkNGjQy2rGnXXA2yuWoSgWoDGtKfL94CC9FCAsNyg==
This diff is collapsed.
......@@ -26,7 +26,8 @@
"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:quickapp": "npm run build:quickapp -- --watch",
"preview": "taobaodev build-preview --debug"
},
"browserslist": [
"last 3 versions",
......@@ -38,33 +39,33 @@
"@babel/helper-create-class-features-plugin": "^7.13.0",
"@babel/runtime": "^7.7.7",
"@pluve/taro-plugin-mars": "^1.1.1",
"@tarojs/cli": "v3.2.0-canary.9",
"@tarojs/components": "v3.2.0-canary.9",
"@tarojs/react": "v3.2.0-canary.9",
"@tarojs/runtime": "v3.2.0-canary.9",
"@tarojs/taro": "v3.2.0-canary.9",
"@tarojs/cli": "3.3.2",
"@tarojs/components": "3.3.2",
"@tarojs/react": "3.3.2",
"@tarojs/runtime": "3.3.2",
"@tarojs/taro": "3.3.2",
"@tbmp/mp-cloud-sdk": "^1.4.2",
"classnames": "^2.2.6",
"hox": "^1.1.2",
"lodash": "4.17.15",
"react": "^16.10.0",
"react-dom": "^16.10.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"taro-ui": "^3.0.0-alpha.3",
"tbcc-sdk-ts": "^1.0.5"
"tbcc-sdk-ts": "^1.0.7"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@tarojs/mini-runner": "v3.2.0-canary.9",
"@tarojs/webpack-runner": "v3.2.0-canary.9",
"@tarojs/mini-runner": "3.3.2",
"@tarojs/webpack-runner": "3.3.2",
"@types/react": "^16.0.0",
"@types/webpack-env": "^1.13.6",
"babel-eslint": "^10.1.0",
"babel-preset-taro": "v3.2.0-canary.9",
"babel-preset-taro": "3.3.2",
"eslint": "^7.12.1",
"eslint-config-standard": "^16.0.2",
"eslint-config-standard-jsx": "^10.0.0",
"eslint-config-standard-react": "^11.0.1",
"eslint-config-taro": "v3.2.0-canary.9",
"eslint-config-taro": "3.3.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
......@@ -73,4 +74,4 @@
"style-resources-loader": "^1.4.1",
"stylelint": "9.3.0"
}
}
}
\ No newline at end of file
const fs = require('fs');
const path = require('path')
const { addPages } = require('../config/default').default
const { canvasPages, hasCanvas } = require('../config/default').default
export default (ctx, options) => {
// plugin 主体
const { hasGameModule = false, hasLottieAnimation = false } = options
ctx.onBuildStart(() => {
// console.log('编译开始!')
})
ctx.onBuildFinish(() => {
if(hasGameModule) {
const dirUrl = path.resolve(__dirname,'..')+'/dist/app.json'
const rawdata = fs.readFileSync(dirUrl);
const { pages, ...ext } = JSON.parse(rawdata);
let new_pages = [...pages,...addPages]
const _new_info = {
...ext,
pages: new_pages
}
fs.writeFileSync(dirUrl, JSON.stringify(_new_info));
}
if(hasLottieAnimation) {
const dirUrl = path.resolve(__dirname,'..')+'/dist/pages/index/index.json'
const rawdata = fs.readFileSync(dirUrl);
const { usingComponents, ...ext } = JSON.parse(rawdata);
const new_info = {
...ext,
usingComponents: {
...usingComponents,
"lottie-modal":"/components/lottieAnimation/lottieAnimation"
if(hasCanvas) {
for(let i = 0; i < canvasPages.length; i++) {
const itm = canvasPages[i]
const dirUrl = path.resolve(__dirname,'..')+`/dist/${itm.root}`
const rawdata = fs.readFileSync(dirUrl);
const { usingComponents, ...ext } = JSON.parse(rawdata);
const new_info = {
...ext,
usingComponents: {
...usingComponents,
"canvas-comp": `/${itm.comRoot}`
}
}
fs.writeFileSync(dirUrl, JSON.stringify(new_info));
}
fs.writeFileSync(dirUrl, JSON.stringify(new_info));
}
console.log('文件写入成功!');
//ctx.writeFileToDist({ filePath: string, content: string })
......
import cloud from '@tbmp/mp-cloud-sdk'
import tbccTs from 'tbcc-sdk-ts'
import conifg from './config'
import conifg from './config/config'
const { utils: { generateAPI }, request: tbccRequest, capi, capiFn } = tbccTs
const { utils: { generateAPI }, request: tbccRequest, capi } = tbccTs
const { cloudName, requestType } = conifg
const request = tbccRequest({
......@@ -11,7 +10,6 @@ const request = tbccRequest({
cloudName,
requestType
})
/**
const defaultConfig = {
method: 'GET',
......@@ -22,27 +20,36 @@ const request = tbccRequest({
// api handle 列表
const apiList = {
...capi,
receiveObjectPrize: {
handle: 'receiveObjectPrize',
method: 'GET',
isShowLoading: false,
toastError: false
},
receiveEnamePrize: {
handle: 'receiveEnamePrize',
method: 'GET',
isShowLoading: false,
toastError: false
},
getActivityBaseInfoById: {
handle: 'getActivityBaseInfoById',
method: 'GET',
isShowLoading: false
},
login: 'login',
getTaskList: 'getTaskList',
doCompleteTask: 'doCompleteTask',
receiveTaskRewards: 'receiveTaskRewards',
addStat: 'addStat',
doHelp: 'doHelp',
drawLotteryPrize: 'drawLotteryPrize',
resetTest: 'resetTest',
drawLottery: 'drawLottery',
getShareInfo: 'getShareInfo',
getVipInfo: 'getVipInfo',
getCollectGoodsList: 'getCollectGoodsList',
getItemListByItemIds: 'getItemListByItemIds'
getCollectGoods: 'getCollectGoods',
updateEnamePrizeReceived: 'updateEnamePrizeReceived', // 会员权益领取
getRotatePrizeListInfo:'getRotatePrizeListInfo',//获取奖品列表接口,暂未实现
drawRotatePrize:'drawRotatePrize',//大转盘抽奖接口,暂未实现
exchangePrize: 'exchangePrize', // 兑换奖品
getUserInfo: 'getUserInfo', // 获取用户信息
}
// 生成API
const API = generateAPI(apiList, request)
API.fn = capiFn(apiList, request)
export default API
export default {
pages: [
'pages/index/index',
'pages/myPrize/myPrize',
'pages/browseGoods/browseGoods',
'pages/collectGoods/collectGoods',
'pages/orderGoods/orderGoods'
'pages/index/index'
],
subPackages: [
{
"root": "pages/packagePrize",
"pages": [
'myPrize/myPrize'
]
},
{
"root": "pages/packageGood",
"pages": [
'browseGoods/browseGoods',
'collectGoods/collectGoods',
'orderGoods/orderGoods',
'cartGoods/cartGoods'
]
},
{
"root": "pages/packageGame",
"pages": [
'game/game'
]
},
{
"root": "pages/packageComs",
"pages": [
'comsTest/comsTest'
]
}
],
window: {
enableSkia: 'true',
allowsBounceVertical: 'NO',
navigationBarTitleText: '活动名称'
navigationBarTitleText: '活动名称',
navigationBarForceEnable: false
},
plugins: {
cemMember: {
......
......@@ -4,7 +4,7 @@ import './app.less'
// 使用taro-ui 按需引入的组件样式
import './taro-ui.scss'
import config from './config'
import config from './config/config'
import './utils/mdProxy'
const { env, tornadoAPI, defaultActivityId } = config
......@@ -20,6 +20,10 @@ class App extends Component {
app.tornadoAPI = tornadoAPI
app.activityId = activityId || defaultActivityId
}
// 页面后台切前台
componentDidShow () {}
// 页面前台切后台
componentDidHide() {}
render() {
return this.props.children
}
......
import React, { useState, useEffect, useRef } from 'react'
import { View, Text } from '@tarojs/components'
import { useDidShow, useDidHide } from '@tarojs/taro'
import tbcc from 'tbcc-sdk-ts'
import './CountDown.less'
const { getServerTime } = tbcc.tb
export default function CountDown(props) {
const { endTime, startTime, onUpdate, type = 1, color = '#000', bgColor = 'transparent', fontSize = '26rpx', padding = '0' } = props
const [countDown, setCountDown] = useState(type === 1 ? ['00', ':', '00', ':', '00'] : ['00', '天', '00', '时', '00', '分', '00', '秒'])
const [nowTime, setNowTime] = useState(startTime || Date.now())
const countTimer = useRef(null)
const isAccessRender = useRef(false)
useEffect(() => {
countTimeFn()
return () => clearInterval(countTimer.current)
}, [endTime, nowTime])
useDidShow(() => {
if(isAccessRender.current) {
setNowTime(startTime || Date.now())
}
isAccessRender.current = true
})
const countTimeFn = async () => {
const _nowTime = await getServerTime()
let diffTime = endTime - (startTime || _nowTime)
countTimer.current = setInterval(() => {
if (diffTime > 1000) {
let day = Math.floor(diffTime / (3600 * 1000) / 24)
let hour = Math.floor((diffTime / 1000 / 3600) % 24)
let minute = Math.floor((diffTime / 1000 / 60) % 60)
let second = Math.floor(diffTime / 1000 % 60)
day = day < 10 ? '0' + day : day
hour = hour < 10 ? '0' + hour : hour
minute = minute < 10 ? '0' + minute : minute
second = second < 10 ? '0' + second : second
setCountDown(type === 1 ? [hour, ':', minute, ':', second] : [day, '天', hour, '时', minute, '分', second, '秒'])
diffTime -= 1000
} else {
setCountDown(type === 1 ? ['00', ':', '00', ':', '00'] : ['00', '天', '00', '时', '00', '分', '00', '秒'])
clearInterval(countTimer.current)
onUpdate && onUpdate()
}
}, 1000)
}
return (
<View className="count-down">
{
countDown.map((item, i) => {
return (
<Text className="count-down-time" key={i} style={{ backgroundColor: item !== ':' ? bgColor : 'transparent', fontSize, color, padding }}>{item}</Text>
)
})
}
</View>
)
}
\ No newline at end of file
.count-down {
display: flex;
align-items: center;
}
.count-down-time {
border-radius: 4rpx;
}
\ No newline at end of file
import { View } from '@tarojs/components'
import React, { useState } from 'react'
import { useThrottle } from '@/hooks/useThrottle'
import Modal from '@/components/_base/Modal/Modal'
import styles from './DoHelpModal.module.less'
import API from '@/api'
import { showToast } from '@tarojs/taro'
import { useLoginInfoModel } from '@/store'
import { checkIsMember } from 'tbcc-sdk-ts/lib/utils'
import { commonToast } from 'tbcc-sdk-ts/lib/core/tb'
export default function RuleModal(props) {
const { bg = '', width = 300, height = 300, onClose = () => {}, closePostion, top = '40%', newVip = false } = props
const [ memberModalVisible, setMemberModalVisible ] = useState(false)
const { loginInfo: { inviteId } } = useLoginInfoModel()
const onAuthSuccess = async() => {
const helpRes = await API.doHelp({ inviteId })
if (helpRes?.success) {
showToast({ title: '助力成功' })
} else {
commonToast(helpRes?.message)
}
onClose()
setMemberModalVisible(false)
}
const onAuthFail = () => {
setMemberModalVisible(false)
}
const onDoHelp = useThrottle(async() => {
// 条件不是需要新会员的情况下,直接助力
if (!newVip) {
const helpRes = await API.doHelp({ inviteId })
if (helpRes?.success) {
showToast({ title: '助力成功' })
} else {
commonToast(helpRes?.message)
}
onClose()
}
// 助力条件为新会员
const isVip = await checkIsMember()
// 已经是会员
if (isVip) {
commonToast('新会员才能助力哦~')
} else {
// 还不是会员,弹出会员弹窗
setMemberModalVisible(true)
}
}, 2000)
const closeMemberModal = () => {
setMemberModalVisible(false)
}
return (
<View>
<Modal onClose={onClose} closePostion={closePostion} top={top}>
<View className={styles.content} style={{ width: `${width / 100}rem`, height: `${height / 100}rem`, backgroundImage: `url(${bg})` }}>
<View className={styles.title}>接受邀请</View>
<View className={styles.image} />
<View className={styles.desc}>是一段描述</View>
<View className={styles.help_button} onTap={onDoHelp}>助力按钮</View>
</View>
</Modal>
{
memberModalVisible &&
<member-modal
onClose={() => closeMemberModal()}
onAuthFail={onAuthFail}
onAuthSuccess={onAuthSuccess}
/>
}
</View>
)
}
.content {
.image-property();
background-color: #fff;
overflow: hidden;
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 40px;
text-align: center;
.image-property();
}
.image {
.wh(200px, 200px);
.image("//yun.duiba.com.cn/taobaomini/template/250x250.png");
margin: 40px 0 0 0;
}
.desc {
font-size: 24px;
margin: 30px 0 0 0;
text-align: center;
}
.help_button {
text-align: center;
.wh(150px, 40px);
margin: 40px 0 0 0;
border: 1px solid #000;
}
import { View, ScrollView, Image } from '@tarojs/components'
import React from 'react'
import tbccTs from 'tbcc-sdk-ts'
const { openDetail, collectGoods, checkGoodsCollectedStatus, commonToast } = tbccTs.tb
import './GoodsPage.less'
export default function GoodsPage(props) {
const {
isBackFlag = 0, goodsList = [],
task = {
itemId: '617724147979,617724563528,617300295119',
taskType: 'browseGoods',
type: '02', // 01 banner 02
bannerHeight: 430,
image: {
bg: '//yun.dui88.com/taobaomini/clientCTest/goods_bg@2x.png',
banner: '//yun.dui88.com/taobaomini/clientCTest/goods_banner@2x.png',
title: '//yun.dui88.com/taobaomini/clientCTest/goods_title@2x.png',
collect: '//yun.dui88.com/taobaomini/clientCTest/goods_collection@2x.png',
no_collect: '//yun.dui88.com/taobaomini/clientCTest/collection_no_collect@2x.png',
img: '//yun.dui88.com/taobaomini/clientCTest/goods_img@2x.png'
},
color: '#181818'
},
onOpenDetail,
onCompleteTask
} = props
const goToGoodsDetail = async(item) => {
const { taskType } = task
const { itemId } = item
if (taskType === 'browseGoods') {
onOpenDetail && onOpenDetail(itemId)
}
await openDetail(String(itemId))
}
// 收藏商品
const goToCollectGoods = async(item) => {
const { itemId, collected } = item
const { taskType } = task
if (collected) {
commonToast('您已收藏过该商品了')
return
}
// 判断是否活动外已收藏商品
const isCollected = await checkGoodsCollectedStatus(+itemId)
if (isCollected) {
onCompleteTask && onCompleteTask(taskType, itemId)
return
}
const result = await collectGoods(+itemId)
if (result) {
onCompleteTask && onCompleteTask(taskType, itemId)
}
}
return (
<View className='goods-modal-container' style={{ background: `url(${task.image.bg}) no-repeat`, backgroundSize: '750rpx 1624rpx' }}>
{task.type === '01'
? <View
style={{ background: `url(${task.image.title}) no-repeat`, backgroundSize: '100% 100%' }}
className='goods-modal-container__title'
/>
: <View className='goods-modal-container__banner' style={{ background: `url(${task.image.banner}) no-repeat`, backgroundSize: '100% 100%', height: task.bannerHeight + 'rpx' }} />}
<View className='goods-modal-container__content' style={{ top: task.type === '02' ? task.bannerHeight + 'rpx' : '209rpx' }}>
<ScrollView scrollY className='goods-modal-container__scroll'>
<View className='goods-modal-container__content-list'>
{
goodsList.map((item, i) => {
return (
<View className='goods-modal-container__content-item' key={'goods_'+i}>
<View className='goods-modal-container__content-item__img' onClick={() => goToGoodsDetail(item)}>
<Image mode='scaleToFill' src={item.image} />
</View>
<View className='goods-modal-container__content-item__name' style={{ color: task.color }}>{item.name}</View>
<View className='goods-modal-container__content-item__price'>
<View className='goods-modal-container__content-item__price-num'><text>¥</text>{item.price}</View>
{
task.taskType === 'collectGoods' &&
<View
onClick={() => goToCollectGoods(item)}
className={item.collected ? 'goods-modal-container__content-item__price-collect' : 'goods-modal-container__content-item__price-no-collect'}
>
<Image mode='scaleToFill' src={item.collected ? task.image.collect : task.image.no_collect} />
</View>
}
</View>
</View>
)
})
}
</View>
</ScrollView>
</View>
</View>
)
}
import { View, ScrollView, Image } from '@tarojs/components'
import React, { useState, useEffect } from 'react'
import Popup from '@/components/_base/Popup/Popup'
import { noopFn } from '@/utils/util'
import { useTasks } from '@/hooks/useTasks'
import tbccTs from 'tbcc-sdk-ts'
import API from '@/api'
import config from '@/config'
import { useThrottle } from '@/hooks/useThrottle'
import { TASK_STATUS, TASK_CONFIG } from '@/const'
import './TasksModal.less'
import { useDidShow } from '@tarojs/taro'
const { commonToast, favorShop, navigateTo, navigateToOutside, showSharePanel } = tbccTs.tb
export default function TasksModal(props) {
const app = getApp()
const { onClose = noopFn, onUpdate, taskFlag } = props
const [ memberShopVisible, setMemberShopVisible ] = useState(false)
const [ currentTaskType, setCurrentTaskType ] = useState('')
const [ preBrowseTime, setPreBrowseTime ] = useState(null)
const { taskList, fetchTaskList } = useTasks()
const { taskIcon, commonTaskTxt, doTaskTxt, browseType, browseTime } = TASK_CONFIG
useDidShow(() => {
if (app.isFlashTask) {
app.isFlashTask = false
if (preBrowseTime && ((Date.now() - preBrowseTime) / 1000) >= browseTime) {
doCompleteTaskHandle(currentTaskType, true)
} else {
commonToast(`未达到${browseTime}秒~`)
fetchTaskList()
}
}
})
const handleTapItem = useThrottle(async(item) => {
const { status, taskType } = item
const tapFn = {
[TASK_STATUS.WAIT_RECEIVE]: async() => {
const { success, data } = await API.receiveTaskRewards({ taskType })
if (success && data) {
const { rewards } = data
commonToast(`领取成功,次数+${rewards}`)
fetchTaskList()
onUpdate && onUpdate()
}
},
[TASK_STATUS.WAIT_DO]: () => goToComplete(item)
}
tapFn[status] && tapFn[status]()
})
const goToComplete = async(item) => {
const { taskType, url, itemIds, keepTime } = item
setCurrentTaskType(taskType)
const completeFn = {
member: () => setMemberShopVisible(true),
invites: () => showSharePanel(),
follow: async() => {
const isFollow = await favorShop(config.sellerId)
if (isFollow) {
doCompleteTaskHandle(taskType)
}
},
// 跳转任务 https://www.feizhu.com
jumpLink: async() => {
navigateToOutside(url)
if (browseType === 2) {
doCompleteTaskHandle(taskType)
} else {
app.isFlashTask = true
setPreBrowseTime(Date.now())
}
},
browseGoods: async() => {
app.isFlashTask = true
if (url) {
navigateToOutside(url)
return
}
navigateTo(`/pages/browseGoods/browseGoods?itemIds=${itemIds}&keepTime=${keepTime}`)
},
orderGoods: async() => {
app.isFlashTask = true
if (url) {
navigateToOutside(url)
return
}
navigateTo(`/pages/orderGoods/orderGoods?itemIds=${itemIds}`)
},
collectGoods: async() => {
app.isFlashTask = true
navigateTo(`/pages/collectGoods/collectGoods?itemIds=${itemIds}`)
},
sign: () => doCompleteTaskHandle(taskType)
}
completeFn[taskType] && completeFn[taskType]()
}
const doCompleteTaskHandle = async(taskType, flag) => {
const { success } = await API.doCompleteTask({ taskType })
if (success) {
await fetchTaskList()
flag && setPreBrowseTime(null)
}
}
const onCloseMemberModal = () => {
setMemberShopVisible(false)
}
const onAuthSuccess = () => {
setMemberShopVisible(false)
fetchTaskList()
}
const onAuthFail = () => {
setMemberShopVisible(false)
}
const onCloseModal = () => {
onClose && onClose()
}
return (
<>
<Popup onClose={onCloseModal} height='700rpx'>
<View className='task-modal-container'>
<View className='task-modal-content'>
<View className='task-modal-content__title'>任务模块</View>
<View className='task-modal-content__list'>
<ScrollView scroll-y='{{true}}' className='task-modal-content__scroll'>
{
taskList.map((item, i) => {
return (
<View className='task-modal-content-item' key={'task_I' + i}>
<View className='task-modal-content-item__left'>
<Image src={taskIcon[item.taskType]} mode='widthFix' />
</View>
<View className='task-modal-content-item__left-label'>
<View className='task-modal-content-item__left-title'>{item.title}</View>
<View className='task-modal-content-item__left-reward'>抽盒次数+{item.rewards}</View>
</View>
<View className={'task-modal-content__list-item__right' + ' ' + 'task-item__status-' + item.status} onClick={() => handleTapItem(item)}>
{
item.status === TASK_STATUS.WAIT_DO ? (doTaskTxt[item.taskType] || '去完成') : commonTaskTxt[item.status]
}
</View>
</View>
)
})
}
</ScrollView>
</View>
</View>
</View>
</Popup>
{memberShopVisible && <member-modal onClose={onCloseMemberModal} onAuthFail={onAuthFail} onAuthSuccess={onAuthSuccess} />}
</>
)
}
import { View } from '@tarojs/components'
import React from 'react'
import styles from './ContainerFit.module.less'
export default function ContainerFit(props) {
const { bg = '', hasFitPsd = false } = props
return (
<View className={styles['page-container']}>
<View className={styles['page-psd-container']} style={{ background: `url(${bg}) no-repeat`, backgroundSize: '7.5rem 16.24rem' }}>
{!hasFitPsd && <View className={styles['page-content']}>
{props.children}
</View>}
{hasFitPsd && props.children}
</View>
</View>
)
}
\ No newline at end of file
### 设计稿适配方案
#### 入参
| 参数 | 类型 | 描述 |
| --- | --- | --- |
|bg| String |背景图(1624px),非必填|
|hasFitPsd| Boolean |内容块是否对标设计稿(750px * 1624px),非必填|
#### 使用
``` jsx
import ContainerFit from '@/components/_base/ContainerFit/ContainerFit'
<ContainerFit hasFitPsd={true} bg={''}>
</ContainerFit>
```
\ No newline at end of file
.page-container {
position: relative;
top: 0;
right: 0;
left: 0;
width: 100vw;
height: 100vh;
min-height: 100vh;
overflow: hidden;
}
.page-psd-container {
position: absolute;
width: 100vw;
height: 1624px;
left: 0;
right: 0;
top: 50%;
transform: translate(0,-50%);
}
.page-content {
position: absolute;
width: 100vw;
height: 100vh;
left: 0;
right: 0;
top: 50%;
transform: translate(0,-50%);
}
\ No newline at end of file
import { ScrollView } from '@tarojs/components'
import React, { useState, useImperativeHandle, forwardRef } from 'react'
import { useThrottle } from '@/hooks/useThrottle'
import styles from './ContainerScroll.module.less'
const ContainerFit = forwardRef((props, ref) => {
const { } = props
// 滚动
const [scrollTop,setScrollTop] = useState(0)
// 滚动指定位置
useImperativeHandle(ref,() => ({ scrollFixTop }))
const scrollFixTop = (topFix) => {
setScrollTop(topFix)
}
// 滑动
const onScroll = useThrottle((e) => {
const { scrollTop: _scrollTop } = e.detail
setScrollTop(_scrollTop)
})
return (
<ScrollView className={styles['scroll-container']}
scrollY scrollTop={scrollTop}
onScroll={(e) => onScroll(e)}
onScrollToUpper={() => console.warn(1)}
scrollWithAnimation
trapScroll={false}
>
{props.children}
</ScrollView>
)
})
export default ContainerFit
\ No newline at end of file
### 滚动容器
#### 入参
| 参数 | 描述 |
| --- | --- |
|ref| ref实例),非必填|
#### 父组件设置滚动具体位置方案
``` jsx
// 滚动 600px
containerRef.current?.scrollFixTop(600)
```
#### 使用
``` jsx
import ContainerScroll from '@/components/_base/ContainerScroll/ContainerScroll'
const containerRef = createRef()
// ref实例
<ContainerFit ref={containerRef}>
</ContainerFit>
```
\ No newline at end of file
.scroll-container {
position: relative;
top: 0;
right: 0;
left: 0;
width: 100vw;
height: 100vh;
min-height: 100vh;
}
\ No newline at end of file
import { View } from '@tarojs/components'
import classnames from 'classnames'
import React, { useState } from 'react'
import Overlay from '../Overlay/Overlay'
import styles from './modal.module.less'
import classnames from 'classnames'
export default function Modal(props) {
const { closePostion = 'top-right', onClose = () => { }, top = '40%', hideCloseButton = false, closeOnClickOverlay = false } = props
const { closePostion = 'top-right', onClose = () => { }, top = '40%', hideCloseButton = false, closeOnClickOverlay = false, closePos = { right: 0, top: -30 } } = props
const closeBtnClass = classnames(styles.closeButton, {
[`${styles.closeButton_bottom}`]: closePostion === 'bottom',
......@@ -32,10 +31,10 @@ export default function Modal(props) {
return (
<View className={modalClass}>
<Overlay onTap={onClickOverlay} />
<Overlay onClick={() => onClickOverlay()} />
<View className={`${styles.content} animate-zoom-in`} style={{ top }}>
{!hideCloseButton && <View className={closeBtnClass} onTap={onCloseModal} />}
{props.children}
{!hideCloseButton && <View className={closeBtnClass} onClick={() => onClose()} style={{ top: closePos.top / 100 + 'rem', right: closePos.right / 100 + 'rem' }} />}
</View>
</View>
......
......@@ -6,7 +6,7 @@
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0);
transform: translate(-50%, -50%);
z-index: 22;
}
......
.overlay {
.cover();
z-index: 21;
opacity: 0;
background-color: rgba(0, 0, 0, 0.7);
}
.canvas {
width:100%;
height:100vh;
flex: 1;
display: flex;
flex-direction: column;
background: #f5cccd;
}
.test-btn-box {
position: absolute;
top: 320rpx;
background-color: #000;
}
\ No newline at end of file
<view class="page-game">
<canvas id="canvas" type="2d" class="canvas" disable-scroll="true"
onTouchStart="onMouseEvent"
onTouchMove="onMouseEvent"
onTouchEnd="onMouseEvent"
onReady="onCanvasReady"
></canvas>
</view>
<task-modal />
\ No newline at end of file
<view class="farmer-container-plants canvas">
<canvas id="canvas" type="2d" class="canvas" disable-scroll="true"
onTouchStart="onMouseEvent"
onTouchMove="onMouseEvent"
onTouchEnd="onMouseEvent"
onReady="onCanvasReady"
></canvas>
</view>
\ No newline at end of file
import { Main } from './output';
/**
* 直接取,app
*/
const app = getApp()
Component({
isUpdateFlag: true,
data: {},
props: {
data: {}
},
didMount(query) {},
didUpdate(preProps,preData) {
// 实时监听数据更新
const { updateFlag, data } = this.props
if(updateFlag !== 1 && updateFlag !== preProps.updateFlag) {
this.postMessage(data.type, data)
}
},
didUnMount() {
// 页面被关闭
this.main && this.main.destroy();
},
methods: {
onCanvasReady() {
console.log("进canvas了")
// 创建Main,授权登录完后才创建
const self = this;
my.createCanvas({
id: 'canvas',
success: (ccc) => {
const dpr = my.getSystemInfoSync().pixelRatio
const windowWidth = my.getSystemInfoSync().windowWidth;
const windowHeight = my.getSystemInfoSync().windowHeight;
ccc.width = windowWidth * dpr + dpr;//重新修改会有误差
ccc.height = windowHeight * dpr + dpr;
// console.log(Main)
console.log(ccc.width, ccc.height)
if (!this.main) {
try {
this.main = new Main(ccc)
//添加事件,为了和main里的通信
this.main.addGlobalEvent("onMessage", self.onMessage, self)
} catch (err) {
console.error(err)
}
}
}
})
},
//和main通信的方法,接收main的信息
onMessage(e) {
//接口名字
let netName = e.data.netName;
//接口参数
let parameter = e.data.parameter;
if(netName.indexOf("mine") == 0) {
// 和游戏页面通信
this.props.onMessage({
type: 'message',
target: {
id: this.props.id
},
data: {
netName,
parameter
}
})
}else {
this.postMessage(netName, { success: false })
}
},
postMessage(netName, data) {
this.main && this.main.dispatchGlobalEvent({ type: netName }, data)//以防复用事件event串了,就用这种方式
},
//鼠标事件
onMouseEvent(e) {
if (this.main) this.main.stage.onMouseEvent(e)
},
}
});
This diff is collapsed.
This diff is collapsed.
{
"component": true
}
\ No newline at end of file
import * as fyge from "fyge"
import * as fyge from "@/components/_miniprogram/canvasComp/node_modules/fyge"
import lottieData from '/lottie/index.js'
Component({
......@@ -26,6 +26,7 @@ Component({
my.createCanvas({
id: 'canvas',
success: (ccc) => {
console.warn(JSON.stringify(ccc))
this.canvas = ccc;
const dpr = my.getSystemInfoSync().pixelRatio
//显示尺寸
......@@ -37,11 +38,13 @@ Component({
//兼容小程序
fyge.initedByCanvas(ccc)
var stage = this.stage = new fyge.Stage(ccc, 750, 1624, windowWidth, windowHeight,1);
var stage = this.stage = new fyge.Stage(ccc, 750, 1624, windowWidth, windowHeight);
//加载
console.warn(lottieData[type])
var l = stage.addChild(new fyge.Lottie(lottieData[type]))
l.play()
l.play(1, () => {
console.warn('动画播放结束')
})
// fyge.GlobalLoader.loadJson((s, json) => {
// console.warn(lottieData)
// var l = stage.addChild(new fyge.Lottie(lottieData))
......
<member-shop-center expend="{{true}}" onClose="onMemberModalClose" onAuthFail="onAuthFail" onAuthSuccess="onAuthSuccess"/>
<member-shop-center expend="{{expend}}" sellerId="{{sellerId}}" onClose="onMemberModalClose" onAuthFail="onAuthFail" onAuthSuccess="onAuthSuccess"/>
Component({
data: {
visible: true
},
data: {},
props: { expend: false, sellerId: '' },
didMount(){},
methods: {
onMemberModalClose() {
// console.log('this.props :>> ', this.props);
......
.dialog-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
}
.dialog-modal__shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.75);
}
.dialog-modal__container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.dialog-modal__container-bottom {
animation: bottom-up 1s linear forwards;
}
.dialog-modal__close {
position: absolute;
right: 20rpx;
top: -80rpx;
height: 48rpx;
width: 48rpx;
background: url('//yun.dui88.com/taobaomini/clientCTest/modal_close_btn.png') center center no-repeat;
background-size: 48rpx 48rpx;
}
@keyframes bottom-up {
from {
transform: translate(0,100vh);
}
to {
transform: translate(0,0);
}
}
.task-modal-container {
width: 100%;
height: 700rpx;
position: absolute;
bottom: 0;
left: 0;
background-color: #ffffff;
}
.task-modal-content__title {
text-align: center;
margin: 30rpx 0;
}
.task-modal-content__list {
width: 100%;
height: 600rpx;
padding: 0 16rpx;
}
.task-modal-content__scroll {
width: 100%;
height: 100%;
}
.task-modal-content-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding: 16rpx;
border: 1rpx solid #eee;
}
.task-modal-content-item__left {
width: 120rpx;
height: 120rpx;
display: flex;
justify-content: center;
align-items: center;
background-color: #eee;
}
.task-modal-content-item__left image {
width: 80rpx;
}
.task-modal-content-item__left-label {
flex: 1;
flex-direction: column;
align-items: center;
margin: 0 16rpx;
}
.task-modal-content-item__left-title {
}
.task-modal-content-item__left-reward {
margin-top: 10rpx;
}
.task-modal-content__list-item__right {
display: flex;
justify-content: center;
align-items: center;
width: 150rpx;
height: 60rpx;
background-color: #eee;
border-radius: 30rpx;
}
.task-item__status-1 {
}
.task-item__status-2 {
}
.task-item__status-3 {
}
\ No newline at end of file
<view class="dialog-container">
<view class="dialog-modal__shade modal-animate-fade-in" />
<view class="dialog-modal__container {{ 'dialog-modal__container-bottom'}} ">
<view class="task-modal-container">
<view class="dialog-modal__close" onTap="onModalClose" />
<view class="task-modal-content">
<view class="task-modal-content__title">任务模块</view>
<view class="task-modal-content__list">
<scroll-view scroll-y="{{true}}" class="task-modal-content__scroll">
<view class="task-modal-content-item" a:for="{{taskList}}">
<view class="task-modal-content-item__left">
<image src="{{taskIcon[item.taskType]}}" mode="widthFix" />
</view>
<view class="task-modal-content-item__left-label">
<view class="task-modal-content-item__left-title">{{item.title}}</view>
<view class="task-modal-content-item__left-reward">抽盒次数+{{item.rewards}}</view>
</view>
<view class="task-modal-content__list-item__right {{'task-item__status-'+item.status}}" data-item="{{item}}" onTap="handleTapItem">
{{
item.status == 1 ? doTaskTxt[item.taskType] : commonTaskTxt[taskType]
}}
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</view>
<member-shop
a:if="{{memberShopVisible}}"
expend="{{memberShopVisible}}"
onSuccess="onSuccess"
onFail="closeCommonModal"
onClose="closeCommonModal" />
\ No newline at end of file
import { cloudRequest, throttleHandle, commonToast, favorShop, navigateTo, navigateToOutside, showSharePanel } from './utils';
const app = getApp();
const taskConfig = {
taskStatus: { // 任务状态
1: '//yun.duiba.com.cn/baicaoweiFarmer/task_btn_01.png',
2: '//yun.duiba.com.cn/baicaoweiFarmer/task_btn_02.png',
3: '//yun.duiba.com.cn/baicaoweiFarmer/task_btn_03.png'
},
taskIcon: {// 任务图标
follow: '//yun.duiba.com.cn/baicaoweiFarmer/follow.png',
member: '//yun.duiba.com.cn/baicaoweiFarmer/memberStar1.png',
invites: '//yun.duiba.com.cn/baicaoweiFarmer/invites.png',
jumpLink: '//yun.duiba.com.cn/baicaoweiFarmer/browseGoodsLink.png',
browseGoods: '//yun.duiba.com.cn/baicaoweiFarmer/browseGoods.png',
orderGoods: '//yun.duiba.com.cn/baicaoweiFarmer/orderGoods.png',
collectGoods: '//yun.duiba.com.cn/baicaoweiFarmer/orderGoods.png',
sign: '//yun.duiba.com.cn/baicaoweiFarmer/orderGoods.png',
},
commonTaskTxt: {
1: '去完成',
2: '待领取',
3: '已完成'
},
doTaskTxt: {
follow: '立即关注',
invites: '去邀请',
member: '1秒入会',
sign: '立即签到',
jumpLink: '去完成',
browseGoods: '去完成',
orderGoods: '去完成',
collectGoods: '去完成',
},
browseType: 1, // 1 跳转即算完成 2 跳转浏览15s 才算完成
browseTime: 15, // 浏览时间
}
const cloudName = 'luolai'
const GO_TO_INVITE = 2 // 数据埋点
const SELLER_ID = 2374579403 // 商家sellerId
Component({
mixins: [],
data: {
taskStatus: taskConfig.taskStatus,
taskIcon: taskConfig.taskIcon,
commonTaskTxt: taskConfig.commonTaskTxt,
doTaskTxt: taskConfig.doTaskTxt,
taskList: [
{
taskType: 'follow',
title: '关注店铺',
taskRateType: 1,
times: 1,
completeTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'member',
title: '加入会员',
taskRateType: 1,
times: 1,
completeTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'invites',
title: '邀请好友入会',
taskRateType: 1,
times: 3,
completeTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'jumpLink',
title: '浏览店铺15s',
taskRateType: 2,
times: 1,
completeTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'browseGoods',
title: '浏览商品',
taskRateType: 2,
times: 1,
completeTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'orderGoods',
title: '下单任务',
taskRateType: 2,
times: 1,
completeTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'collectGoods',
title: '收藏商品',
taskRateType: 2,
times: 1,
todayCompleteTimes: 0,
status: 1,
rewards: 1
},
{
taskType: 'sign',
title: '每日签到',
taskRateType: 2,
times: 1,
todayCompleteTimes: 0,
status: 1,
rewards: 1
}
],
memberShopVisible: false,
currentTaskType: null,
preBrowseTime: null
},
props: {
taskFlag: 1,
dataName: '',
onModalClose: () => {},
onUpdate: () => {}
},
didMount() {
this.getTaskList()
},
didUpdate(preProps,preData) {
const { taskFlag } = this.props
const { isFlashTask = false } = app
const { browseTime } = taskConfig
const { preBrowseTime, currentTaskType } = this.data
if(preProps.taskFlag !== taskFlag && isFlashTask) {
// console.log((Date.now() - preBrowseTime)/1000)
if(preBrowseTime && ((Date.now() - preBrowseTime) / 1000) >= browseTime) {
this.doCompleteTask(currentTaskType,true)
}
else {
if(currentTaskType === 'jumpLink') {
this.setData({ preBrowseTime: null, currentTaskType: null})
}
this.getTaskList(true)
}
}
},
didUnmount() {
},
methods: {
// 关闭弹窗
onModalClose() {
const { onModalClose, dataName } = this.props;
onModalClose && onModalClose(dataName);
},
// 按钮
handleTapItem: throttleHandle(async function(e) {
const { activityId } = app
const { status, taskType, url, rewards, itemIds, keepTime } = e.target.dataset.item
if(status == 3) return;
if(status == 2) {
const { success, data } = await cloudRequest(cloudName,{ activityId, taskType }, 'receiveTaskRewards').catch(res => {
commonToast(res && res.message);
}) || {};
if (success && data) {
const { rewards, receiveTimes } = data
let message = taskType == 'invites' ?
`成功邀请${receiveTimes}名好友,抽盒次数+${rewards}`:`抽盒次数+${rewards}`
commonToast(message)
this.getTaskList()
this.props.onUpdate()
}
}
else {
this.goToComplete(taskType,url,itemIds,keepTime)
}
},2000),
// 做任务
async goToComplete(taskType,url,itemIds,keepTime) {
const { activityId } = app
const { browseType } = taskConfig
switch(taskType) {
case 'member':
// 去加入会员
this.setData({ memberShopVisible: true })
break;
case 'follow':
let isFollow = await favorShop(SELLER_ID)
if(isFollow) {
this.doCompleteTask(taskType)
}
break;
case 'invites':
// 邀请好友
showSharePanel();
await cloudRequest(cloudName,{ activityId, type: GO_TO_INVITE }, 'addStat')
break;
case 'jumpLink':
// 跳转任务
navigateToOutside(url || 'https://www.feizhu.com')
if(browseType === 1) {
this.doCompleteTask(taskType)
}
else {
app.isFlashTask = true
this.setData({ preBrowseTime: Date.now(), currentTaskType: taskType})
}
break;
case 'browseGoods':
// 跳转浏览页面
app.isFlashTask = true
navigateTo(`/pages/browseGoods/browseGoods?itemIds=${itemIds}&keepTime=${keepTime}`)
break;
case 'orderGoods':
// 跳转下单页面
app.isFlashTask = true
navigateTo(`/pages/orderGoods/orderGoods?itemIds=${itemIds}`)
break;
case 'collectGoods':
// 跳转收藏页面
app.isFlashTask = true
navigateTo(`/pages/collectGoods/collectGoods?itemIds=${itemIds}`)
break;
case 'sign':
// 签到
this.doCompleteTask(taskType)
break;
}
},
// 完成任务
async doCompleteTask(taskType, flag) {
const { activityId } = app
let { success, data } = await cloudRequest(cloudName,{ activityId, taskType }, 'doCompleteTask') || {}
if(success) {
this.getTaskList()
flag && this.setData({ preBrowseTime: null })
}
},
// 获取任务列表
async getTaskList(flag) {
const { activityId } = app
const { success, data } = await cloudRequest(cloudName,{ activityId }, 'getTaskList').catch(res => {
commonToast(res && res.message);
}) || {};
if (success && data) {
const { list } = data
app.isFlashTask = flag ? false : true
// this.setData({ taskList: list })
}
},
// 关闭入会组件
closeCommonModal() {
this.setData({ memberShopVisible: false })
},
// 入会成功
onSuccess() {
this.getTaskList()
this.setData({ memberShopVisible: false })
}
},
});
{
"component": true,
"usingComponents": {
"member-shop":"/components/_miniprogram/memberModal/memberModal"
}
}
\ No newline at end of file
/**
* 获取任务数据
* @param {*} _cloudName
* @param {*} params
* @param {*} loading
*/
export const cloudRequest = async (_cloudName, params, handle, loading = false) => {
const { cloud } = getApp();
loading && my.showLoading()
return new Promise((resolve, reject) => {
cloud.function.invoke(_cloudName, params, handle).then(res => {
console.log(`调用${handle}接口返回结果`, res)
loading && my.hideLoading()
if (res && res.success) {
resolve(res);
} else {
reject(res);
}
return res;
}).catch(() => {
loading && my.hideLoading()
reject();
});
});
}
/**
* 函数节流,普通防连点
* @param {fun} fun 函数
* @param {delay} delay 时间
*/
export const throttleHandle = (fun, delay = 1000) => {
let last, deferTimer;
return function () {
let now = +new Date();
if (last && now < last + delay) {
clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = now;
}, delay);
} else {
last = now;
fun.apply(this, arguments);
}
};
}
/**
* toast
* @param {*} content
* @param {*} duration
* @param {*} successCb
*/
export const commonToast = (content, duration, successCb) => {
my.showToast({
content: content || '换个姿势再试一次~',
duration: duration || 3000,
success: successCb
})
};
/**
* 执行关注店铺
* @param {number} sellerId 店铺归属的卖家Id
* @returns {boolean} 关注状态
*/
export const favorShop = async (sellerId) => {
if (!sellerId) {
return false;
}
return new Promise((resolve, reject) => {
// @ts-ignore
my.tb.favorShop({
id: +sellerId,
success: res => {
if(res.error === 11) resolve(true)
resolve(res)
},
fail: err => {
reject(err)
}
})
})
};
/**
* 跳转到外部链接
* @param {string} url 跳转链接
*/
export const navigateToOutside = url => {
if (!url) {
console.error('navigateToOutside: 请传入url');
return false;
}
my.call('navigateToOutside',{ url });
};
/**
* 跳转到内部链接(新开窗口)
* @param {string} url 跳转链接
*/
export const navigateTo = url => {
if (!url) {
console.error('navigateTo: 请传入url');
return false;
}
my.navigateTo({ url });
};
/**
* 调起分享面板
*/
export const showSharePanel = async () => {
my.showSharePanel();
}
\ No newline at end of file
# 垂直弹幕参数
| 参数 | 说明 | 类型 | 默认值 |
| :------------------------- | :------------- | :------ | :----- |
| dataList | 弹幕列表 | String[] | - |
| barrageNum | 弹幕循环数量 | Number | 30 |
| quantity | 弹幕同时显示数量 | Number | 2 |
## 垂直弹幕使用步骤
1. 引入VerticalBarrage
2. 直接进入文件的<View className={styles.text}>{item}</View>处,定制自己需要的样式
# 横向弹幕参数
| 参数 | 说明 | 类型 | 默认值 |
| :------------------------- | :------------- | :------ | :----- |
| dataList | 弹幕列表 | String[] | - |
| trackCount | 弹幕轨道数量 | Number | 1 |
## 横向弹幕使用步骤
1. 引入Barrage
2. 直接进入文件的<View className={styles.text}>{item}</View>处,定制自己需要的样式
\ No newline at end of file
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import styles from './Barrage.module.less'
const Barrage = props => {
const {
dataList = [
'1撒撒打算打算打算的',
'2撒撒打算打算打算的',
'3撒撒打算打算打算的',
'4撒撒打算打算打算的',
'5撒撒打算打算打算的',
'3撒撒打算打算打算的',
'3撒撒打算打算打算的',
'3撒撒打算打算打算的',
'3撒撒打算打算打算的',
'3撒撒打算打算打算的',
'3撒撒打算打算打算的',
'3撒撒打算打算打算的'
],
interval = 5000,
width = 500,
trackCount = 1
} = props
const [ barrageList, setBarrageList ] = useState([])
const [ animationData, setAnimationData ] = useState(null)
const timer = useRef(null)
const nowIndex = useRef(0)
const createData = () => {
const Tracks = Array.from({ length: trackCount }, (v, k) => (k + 1) * 40 - 40)
// TODO 多轨道弹幕
setBarrageList(
dataList.map((item, index) => ({
top: Tracks[index < Tracks.length ? index : index % trackCount],
context: item
}))
)
}
const callback = useCallback(() => {
timer.current = setInterval(() => {
setAnimationData(
Taro.createAnimation()
.translateX(nowIndex.current === 0 ? 0 : Taro.pxTransform(-width * nowIndex.current))
.step({
duration: nowIndex.current === 0 ? 0 : 1000
})
.export()
)
if (nowIndex.current < barrageList.length - 1) {
nowIndex.current = nowIndex.current + 1
} else {
nowIndex.current = 0
}
}, interval + 1000)
}, [ barrageList, width ])
useEffect(() => {
callback()
return () => clearInterval(timer.current)
}, [ callback ])
useEffect(() => {
createData()
return () => {}
}, [])
return (
<View id='barrage_wrap' style={`width:${width / 100}rem`} className={styles['barrage-wrap']}>
<View id='barrage_container' animation={animationData} className={styles['barrage-fly']}>
{barrageList.map((item, index) => {
return (
<View id={`barrage-${index}`} key={index} style={`width:${width / 100}rem`} className={styles['barrage-textFly']}>
{/* 弹幕内容 */}
<Text className={styles['barrage-text']}>{item.context}</Text>
</View>
)
})}
</View>
</View>
)
}
export default Barrage
.barrage-wrap {
height: 64px;
border: 1px solid red;
overflow: hidden;
}
.barrage-fly {
position: relative;
height: 100%;
z-index: 3;
display: flex;
}
.barrage-textFly {
flex-shrink: 0;
height: 64px;
overflow: hidden;
line-height: 64px;
color: #f9c797;
font-size: 32px;
padding: 0 10px;
box-sizing: border-box;
text-align: center;
}
.barrage-text {
white-space: nowrap;
}
@keyframes first {
from {
left: 100%;
}
to {
left: -100%;
}
}
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'
import { View } from '@tarojs/components'
import Taro, { useReady } from '@tarojs/taro'
import styles from './VerticalBarrage.module.less'
const animation = Taro.createAnimation({
transformOrigin: 'top right',
duration: 850,
timeFunction: 'ease-in-out',
delay: 150
})
const VerticalBarrage = (props) => {
const { dataList, barrageNum = 30, quantity = 2 } = props
const [ height, setHeight ] = useState(0)
const [ animationData, setAnimationData ] = useState(null)
const nowIndex = useRef(0)
const timer = useRef(null)
useReady(() => {
const query = Taro.createSelectorQuery()
query.select('#barrage_0').boundingClientRect().exec((res) => {
console.log(res)
const h = res[0].height
setHeight(h)
})
})
const barrageList = useMemo(() => {
if (!dataList.length) return []
if (barrageNum && dataList.length < barrageNum) {
let reslut = [ ...dataList ]
while (reslut.length < barrageNum) {
reslut = [ ...reslut, ...dataList ]
}
return [ ...reslut.slice(0, barrageNum), ...reslut.slice(0, quantity) ]
} else {
return [ ...dataList.slice(0, barrageNum), ...dataList.slice(0, quantity) ]
}
}, [ dataList ])
const callback = useCallback(() => {
timer.current = setInterval(() => {
const data = animation.translateY(-nowIndex.current * height).step()
setAnimationData(data.export())
if (nowIndex.current < barrageList.length - quantity) {
nowIndex.current = nowIndex.current + 1
} else {
nowIndex.current = 0
}
}, 1000)
}, [ barrageList, height ])
useEffect(() => {
callback()
return () => clearInterval(timer.current)
}, [ callback ])
// 控制淡入淡出
const barrageStyle = (index) => {
if (quantity === 1) {
return `${(nowIndex.current === index && nowIndex.current !== 0) ? styles.in : ''} ${(nowIndex.current - 1 === index) ? styles.out : ''}`
} else {
return `${(nowIndex.current + quantity - 1 === index && nowIndex.current !== 0) ? styles.in : ''} ${nowIndex.current - 1 === index ? styles.out : ''}`
}
}
return (
<View className={styles['barrage-container']} style={{ height: height * quantity }}>
<View
className={styles['barrage-box']}
animation={animationData}
style={nowIndex.current === 0 && { transform: 'translateY(0)' }}
>
{
barrageList.map((item, i) => (
<View key={`barrage-${i}`} id={`barrage_${i}`} className={barrageStyle(i)}>
<View className={styles.text}>{item}</View>
</View>
))
}
</View>
</View>
)
}
export default VerticalBarrage
.barrage-container {
border: 1px solid red;
overflow: hidden;
}
.barrage-box {
z-index: 1;
.text {
padding: 10px 0;
}
}
.in {
animation: fadeIn 1.5s;
animation-fill-mode: forwards;
}
.out {
animation: fadeOut 1s;
animation-fill-mode: forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
\ No newline at end of file
import VerticalBarrage from './VerticalBarrage/VerticalBarrage'
import Barrage from './HorizontalBarrage/Barrage'
export {
VerticalBarrage,
Barrage
}
import React, { useState, useEffect } from "react";
import { View, Image, Text } from "@tarojs/components";
import LoadingBar,{LOAD_TYPE} from "../LoadingBar/LoadingBar";
import styles from './BrowserLoading.module.less'
import {BROWSE_COUNTDOWN_OBJ} from '@/config/browse.config'
const BrowserLoading = (props) => {
const {loadFlag,count} = props;
return (
<View className={styles["browse_loading"]}>
{loadFlag == BROWSE_COUNTDOWN_OBJ.NUMBER && (
<>
<View className={styles["count_img"]}>
<View className={styles["count_time"]}>{count}s</View>
</View>
</>
)}
{loadFlag == BROWSE_COUNTDOWN_OBJ.PICTURE && <LoadingBar type={LOAD_TYPE.TASK} />}
</View>
);
};
export default BrowserLoading;
.browse_loading {
width: 200px;
height: 200px;
position: fixed;
right: 20px;
top: 400px;
border: 1px solid cyan;
box-sizing: border-box;
}
.count_img {
width: 100%;
height: 100%;
border: 1px solid rgb(47, 131, 99);
box-sizing: border-box;
background-image: url("//yun.dui88.com/7f705da9-5f16-4cf1-a239-3903c5c23995.png");
.image-property(contain, center center);
}
.count_time {
width: 100px;
height: 60px;
line-height: 60px;
margin: 40px auto 0;
border: 1px solid cyan;
box-sizing: border-box;
font-size: 40px;
font-weight: 600;
color: #000;
text-align: center;
}
##### 是否展示浏览页中的倒计时
+ 入参
| 配置项 | 类型 | 描述 | 默认值 | 备注 |
| :-------- | :-----: | :--------: | :-----: | :---:|
| loadFlag | String | 倒计时类型"NUMBER","PICTURE" | 默认空 '' | NUMBER:数字倒计时 PICTURE:loading倒计时(默认15s,可在css中修改) |
| count | Number | 倒计时时间 | 15s | 默认倒计时15s(可改配置) |
+ 使用
```jsx
import BrowserLoading from '../BrowserLoading/BrowserLoading'
export const Index = () => {
return(
<BrowserLoading loadFlag={task.loadFlag} count={count} />
)
}
```
-------
###### loadingBar组件
+ 入参
| 配置项 | 类型 | 描述 | 默认值 | 备注 |
| :-------- | :-----: | :--------: | :-----: | :---:|
| bg | String | 背景loading | 'https://yun.duiba.com.cn/spark/assets/5a725b9e4ebf436a0a92c6317a5a4e47d87c8c1f.png' | / |
| bgTop | String | 上层loading | 'https://yun.duiba.com.cn/spark/assets/ccc405e28a5140d3aff9dff27b97fa5fb6bd7ff5.png' | / |
| type | String | "TASK","NORMAL" | 'NORMAL' | "TASK":自动loading倒计时 "NORMAL":静态的loading进度显示 |
| total | Number | loading总值 | / | type为"NORMAL"传入 |
| current | Number | 当前值 | 0 | type为"NORMAL"传入 |
+ 使用
```jsx
import LoadingBar,{LOAD_TYPE} from "../LoadingBar/LoadingBar";
export const Index = () => {
return(
<LoadingBar type={LOAD_TYPE.NORMAL} total={100} current={10} />
<LoadingBar type={LOAD_TYPE.TASK} />
)
}
```
\ No newline at end of file
import React from 'react'
import { View, Text } from '@tarojs/components'
import tbcc from 'tbcc-sdk-ts'
import './Countdown.less'
const { getServerTime } = tbcc.tb
export default class Countdown extends React.Component {
static defaultProps = {
targetTime: '', // 以时间来进行倒计时
startTime: '', // 开始时间
count: '', // 以秒数来进行倒计时
// targetTime 模式下生效
showText: false, // 显示时分秒
showDay: false, // 显示天数
symbol: ':', // 间隔符号
isClose: false, // 手动关闭倒计时
// 共用事件
onTick: () => { }, // 倒计时过程事件
onEnd: () => { } // 倒计时结束事件
};
constructor() {
super(...arguments)
this.state = {
day: '0',
hour: '00',
minute: '00',
second: '00',
countText: '0'
}
this.targetTimestamp = null
this.startTimestamp = null
this.timer = null
}
componentDidMount() {
const { targetTime, count } = this.props
if (targetTime) {
this.formatTargetTime(targetTime)
} else if (count) {
this.setCountdown(count)
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { targetTime, count } = nextProps
if (targetTime && targetTime !== this.props.targetTime) {
this.formatTargetTime(targetTime)
} else if (count && count !== this.props.count) {
clearInterval(this.timer)
this.setCountdown(count)
}
}
componentWillUnmount() {
this.targetTimestamp = null
clearInterval(this.timer)
}
componentDidHide() {
this.targetTimestamp = null
clearInterval(this.timer)
}
componentDidShow() {
const { targetTime, count } = this.props
if (targetTime) {
this.formatTargetTime(targetTime)
} else if (count) {
this.setCountdown(count)
}
}
setCountdown(count) {
if (count <= 0) {
return
}
let second = count
this.setState({
countText: second
})
this.timer = setInterval(() => {
second--
this.setState({
countText: second
})
this.props.onTick && this.props.onTick(second)
if (second <= 0) {
clearInterval(this.timer)
this.onTimeEnd()
}
}, 1000)
}
// 处理日期格式
formatTargetTime(targetTime) {
if (!targetTime) {
return
}
// 避免iOS端上的日期格式有问题
const time = targetTime.replace(/-/g, '/')
this.targetTimestamp = new Date(time).getTime()
this.getRemainingSecond()
}
// 计算剩余时间秒数
async getRemainingSecond() {
if (!this.targetTimestamp) return
// 当前时间
const currentTimestamp = await getServerTime()
// 剩余时间
const remainingSecond = Math.floor((this.targetTimestamp - currentTimestamp) / 1000)
// 天 时 分 秒
let day = ''
let hour = ''
let minute = ''
let second = ''
if (remainingSecond > 0 && !this.props.isClose) {
day = this.formatNum(parseInt(remainingSecond / 86400))
hour = this.props.showDay
? this.formatNum(parseInt((remainingSecond % 86400) / 3600))
: this.formatNum(parseInt(remainingSecond / 3600))
minute = this.formatNum(parseInt((remainingSecond % 3600) / 60))
second = this.formatNum(parseInt((remainingSecond % 3600) % 60))
this.timeTick(remainingSecond)
} else {
day = '0'
hour = minute = second = '00'
this.onTimeEnd()
}
this.setTimeState(day, hour, minute, second)
}
// 设置时间
setTimeState(day, hour, minute, second) {
// 是否显示天时分秒文字
if (this.props.showText) {
hour += '时'
minute += '分'
second += '秒'
}
this.setState({
day,
hour,
minute,
second
})
}
// 格式数字
formatNum(num) {
return num > 9 ? `${num}` : `0${num}`
}
// 倒计时过程事件
timeTick(remainingSecond) {
this.props.onTick && this.props.onTick(remainingSecond)
setTimeout(() => {
this.getRemainingSecond()
}, 1000)
}
// 倒计时结束触发事件
onTimeEnd() {
this.targetTimestamp = null
this.props.onEnd && this.props.onEnd()
}
render() {
const { symbol, showDay, showText, className, targetTime } = this.props
const { day, hour, minute, second, countText } = this.state
// 末尾有空格
let countdownClass = 'countdown '
if (className) countdownClass += className
if (targetTime) {
return (
<View className={countdownClass}>
{showDay && day !== '00' && (
<Text>
<Text className='day'>{day}</Text>
<Text className='day-text'></Text>
</Text>
)}
<Text>
<Text className='hour'>{hour}</Text>
{!showText && <Text className='symbol'>{symbol}</Text>}
</Text>
<Text className='minute'>{minute}</Text>
{!showText && <Text className='symbol'>{symbol}</Text>}
<Text className='second'>{second}</Text>
</View>
)
}
return (
<View className={countdownClass}>
<Text className='second'>{countText}</Text>
</View>
)
}
}
.countdown {
display: inline-block;
box-sizing: border-box;
}
\ No newline at end of file
## 参数
| 参数 | 说明 | 类型 | 默认值 | 注意 |
| :------------------------- | :------------- | :------ | :----- | :------ |
| targetTime | 目标时间 | String | - | 优先级比count高 |
| count | 倒计秒数 | Number | - | - |
| 以下为使用targetTime 生效 |
| symbol | 间隔符号 | String | - | - |
| showDay | 是否显示天 | Boolean | false | 配合showText同时搭配 |
| showText | 是否显示时分秒 | Boolean | false | 打开自动无视symbol |
| isClose | 是否关闭倒计时 | Boolean | false | - |
## 事件
| 事件名称 | 说明 | 返回参数 |
| :------- | :--------------- | :--------- |
| onTick | 倒计时过程事件 | 剩余的秒数 |
| onEnd | 倒计时结束时触发 | - |
## 实例
```jsx
<CounDown targetTime="2021-10-01 09:00:00">
```
import Countdown from './CountDown'
export default Countdown
import React, { useState, useEffect, useRef } from 'react'
import { View, Image, ScrollView } from '@tarojs/components'
import classnames from 'classnames'
import tbccTs from 'tbcc-sdk-ts'
const { openDetail } = tbccTs.tb
import styles from './GoodsList.module.less'
function GoodsList(props) {
const {
goodsList = [],
task = {
itemId: '617724147979,617724563528,617300295119',
taskType: 'browseGoods',
image: {
collect: '//yun.dui88.com/taobaomini/clientCTest/goods_collection@2x.png',
no_collect: '//yun.dui88.com/taobaomini/clientCTest/collection_no_collect@2x.png',
img: '//yun.dui88.com/taobaomini/clientCTest/goods_img@2x.png'
},
color: '#181818'
},
onOpenDetail,
onCompleteTask
} = props
const goToGoodsDetail = async(item) => {
const { taskType } = task
const { itemId } = item
if (taskType === 'browseGoods') {
onOpenDetail && onOpenDetail(itemId)
}
await openDetail(String(itemId))
}
// 收藏商品
const goToCollectGoods = async(item) => {
const { itemId, collected } = item
onCompleteTask && onCompleteTask(itemId,collected)
}
// 收藏按钮
const getCollectStyle = (collected) => {
return classnames(styles['item__price-collect'],{
[`${styles['item__price-no-collect']}`]: collected
})
}
return (
<ScrollView scrollY className={styles['container__scroll']}>
<View className={styles['container__content-list']}>
{
goodsList.map((item, i) => {
return (
<View className={styles['content-item']} key={'goods_'+i}>
<View className={styles['item__img']} onClick={() => goToGoodsDetail(item)}>
<Image mode='scaleToFill' src={item.image} />
</View>
<View className={styles['item__name']} style={{ color: task.color }}>{item.name}</View>
<View className={styles['item__price']}>
<View className={styles['item__price-num']}><text>¥</text>{item.price}</View>
{
task.taskType === 'collectGoods' &&
<View
onClick={() => goToCollectGoods(item)}
className={() => getCollectStyle(item.collected)}
>
<Image mode='scaleToFill' src={item.collected ? task.image.collect : task.image.no_collect} />
</View>
}
</View>
</View>
)
})
}
</View>
</ScrollView>
)
}
export default GoodsList
\ No newline at end of file
.goods-modal-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
min-height: 100vh;
/* background: url('//yun.dui88.com/taobaomini/clientCTestgoods_bg@2x.png') no-repeat; */
background-size: 750px 1624px;
overflow: hidden;
z-index: 20;
}
.goods-modal-container__title {
width: 750px;
height: 120px;
margin: 96px 0 11px;
}
.goods-modal-container__banner {
width: 750px;
height: 400px;
margin-bottom: 30px;
background: #e1e1e1;
}
.goods-modal-container__content {
position: absolute;
top: 190px;
left: 0;
bottom: 0;
padding-top: 41px;
padding: 41px 25px 0;
width: 750px;
/* height: inherit; */
}
.goods-modal-container__scroll {
.container__scroll {
height: 100%;
}
.goods-modal-container__content-list {
.container__content-list {
width: 100%;
display: flex;
flex-wrap: wrap;
}
.goods-modal-container__content-item {
.content-item {
width: 340px;
height: 510px;
background: #fff;
margin-bottom: 20px;
}
.goods-modal-container__content-item:nth-child(even) {
.content-item:nth-child(even) {
margin-left: 20px;
}
.goods-modal-container__content-item__img {
.item__img {
width: 340px;
height: 340px;
background: #e1e1e1;
}
.goods-modal-container__content-item__img image {
.item__img image {
width: 100%;
height: 100%;
}
.goods-modal-container__content-item__name {
.item__name {
margin: 25px 0 31px 21px;
width: 310px;
height: 60px;
......@@ -71,35 +37,35 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-modal-container__content-item__price {
.item__price {
padding: 0 31px 0 24px;
display: flex;
justify-content: space-between;
}
.goods-modal-container__content-item__price-num {
.item__price-num {
font-size: 32px;
font-family: PingFang SC;
font-weight: 600;
color: rgba(255, 42, 0, 1);
}
.goods-modal-container__content-item__price-num text {
.item__price-num text {
font-size: 24px;
margin-right: 2px;
}
.goods-modal-container__content-item__price-collect {
.item__price-collect {
width: 118px;
height: 31px;
background-size: 100% 100%;
}
.goods-modal-container__content-item__price-collect image {
.item__price-collect image {
width: 100%;
height: 100%;
}
.goods-modal-container__content-item__price-no-collect {
.item__price-no-collect {
width: 30px;
height: 30px;
}
.goods-modal-container__content-item__price-no-collect image {
.item__price-no-collect image {
width: 100%;
height: 100%;
}
......@@ -111,3 +77,4 @@
right: 19px;
background: #eee;
}
\ No newline at end of file
import React,{useState, useEffect} from 'react'
import {View,Image,Text} from '@tarojs/components'
import styles from './LoadingBar.module.less'
export const LOAD_TYPE = {
'TASK':"TASK",
'NORMAL':"NORMAL"
}
const LoadingBar = (props) => {
const {
bg='https://yun.duiba.com.cn/spark/assets/5a725b9e4ebf436a0a92c6317a5a4e47d87c8c1f.png',
bgTop='https://yun.duiba.com.cn/spark/assets/ccc405e28a5140d3aff9dff27b97fa5fb6bd7ff5.png',
type='',
total=0,
current=0
} = props;
const [percent,setPercent] = useState(0);
useEffect(()=>{
showCurrentLoading();
},[])
/**
* @description 显示当前进度(进度条上没有其他内容)
*/
const showCurrentLoading = () => {
if(type == LOAD_TYPE.NORMAL){
let cur = current >= total ? toal : current;
setPercent((cur / total * 100).toFixed(1))
}
}
return(
<View className={styles['load_container']} >
<View className={styles['load_content']} >
<View className={styles['load_bg']} style={{backgroundImage:`url(${bg})`}} ></View>
<View
className={`${styles['load_top']} ${type == LOAD_TYPE.TASK && 'loadAni'}`}
style={{
backgroundImage:`url(${bgTop})`,
clipPath:`${type==LOAD_TYPE.NORMAL && `inset(0% 0% 0% ${percent}%)`}`
}}
></View>
</View>
</View>
)
}
export default LoadingBar
\ No newline at end of file
.load_container{
.load_content{
width: 200px;
height: 80px;
margin: 0 auto;
overflow: hidden;
}
.load_bg,
.load_top{
width: 200px;
height: 60px;
position: absolute;
background-size: cover;
background-repeat: no-repeat;
}
.loadAni{
animation: move 15s linear forwards;
}
}
@keyframes move {
0%{
clip-path: inset(0% 0% 0% 0%);
}
99%{
opacity: 1;
}
100%{
clip-path: inset(0% 100% 0% 0%);
opacity: 0;
}
}
\ No newline at end of file
import React, { useState } from 'react'
import { View, Image } from '@tarojs/components'
import classnames from 'classnames'
import styles from './Progress.module.less'
import { PROGRESS_LIST } from '@/mock'
const PRO_ICON = {
icon: '//yun.duiba.com.cn/taobaomini/provideBeauty/total/progress_icon.png'
}
function Progress(props) {
const { data = {}, width = 692, type = 1, hasIcon = false, hasTxt = true } = props
const { allHotValue = 0, levleList = PROGRESS_LIST } = data
const maxLevel = levleList[levleList.length -1]
const getProItm = classnames(styles['progress-box-com'],{
[`${styles['progress-box-com-02']}`]: allHotValue === maxLevel
})
const tranHotValue = (value) => {
const _num = parseInt(value / 10000)
if(_num) return _num + '万'
return value
}
const getTextStyle = classnames(styles['progress-box__txt'],{
[`${styles['progress-box__txt-bottom']}`]: type === 2
})
const getDescStyle = (itm) => {
return classnames(styles['progress-box__txt-item'],{
[`${styles['progress-txt-active']}`]: itm <= allHotValue
})
}
return (
<View className={styles['progress-box']} style={{ width: width / 100 + 'rem'}}>
<View className={getProItm} style={{width: allHotValue >= maxLevel ? '100%' : (allHotValue / maxLevel) * 100 +'%'}}></View>
{hasIcon && <View className={styles['progress-box__icon']}>
{ levleList.map((itm,i) => {
return(
<Image className={styles['progress-box__icon-itm']}
style={{left: (itm / maxLevel) * width / 100 + 'rem'}} src={PRO_ICON['icon']} />
)
})}
</View>}
{hasTxt && <View className={getTextStyle}>
{ levleList.map((itm,i) => {
return(
<View className={getDescStyle(itm)}
style={{left: (itm / maxLevel) * width / 100 + 'rem'}}>{tranHotValue(itm)}</View>
)
})}
</View>}
</View>
)
}
export default Progress
\ No newline at end of file
.progress-box {
width: 692px;
height: 20px;
background: #371B15;
border-radius: 4px;
position: relative;
}
.progress-box-com {
height: 20px;
background: linear-gradient(270deg, #FFDE8B 0%, #FFCE38 100%);
border-radius: 4px;
position: absolute;
top: 0;
left: 0;
transition: all 0.5s cubic-bezier(0, 0.64, 0.36, 1);
}
.progress-box__icon, .progress-box__txt {
.wh(100%,100%);
position: absolute;
top: 0;
left: 0;
}
.progress-box__icon-itm {
.wh(32px,32px);
position: absolute;
top: -4px;
transform: translate(-50%,0);
}
.progress-box__icon-itm:last-child {
transform: none;
}
.progress-box__txt {
top: -8px;
}
.progress-box__txt-bottom {
top: 46px;
}
.progress-box__txt-item {
.wh(78px,36px);
font-size: 30px;
background-color: #3C1D17;
border-radius: 4px;
.flex-row-center();
font-size: 26px;
font-family: SourceHanSansCN;
font-weight: 500;
color: #FFF1CC;
position: absolute;
transform: translate(-50%,0);
top: 0;
white-space: nowrap;
}
.progress-box__txt-item:last-child {
transform: translate(-100%,0);
}
.progress-txt-active {
background-color: #FFD64D;
color: #302A28;
}
\ No newline at end of file
# 排行榜组件
---
## 何时使用
- 当某个页面需要展示累计邀请、分等级奖励进度条、累计签到等。
## 目录结构
`RankTitle` 排行榜头部
`RankList` 排行榜列表
## RankTitle API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --------- | ------------ | ----------- | ------------------------------------------------------- | ---- |
| titleList | 表头 | ArrayString | - | |
| myRank | 个人排名信息 | Object | { rank: '11', userNick: '用户名', rankValue: '534443' } | |
## RankList API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| -------- | ----------- | ----------- | -------- | ---- |
| rankList | 排名信息 | ArrayObject | 见示例一 | |
| rankIcon | 前三名 Icon | Object | 见示例二 | |
## 示例一
```
[{
rank: 1,
userNick: '用户昵称',
hotValue: 98882892
}]
```
## 示例二
```
{
1: '//yun.duiba.com.cn/taobaomini/pike_call/icon_06.png',
2: '//yun.duiba.com.cn/taobaomini/pike_call/icon_07.png',
3: '//yun.duiba.com.cn/taobaomini/pike_call/icon_08.png'
}
```
import React ,{ memo } from 'react'
import { View, Image, ScrollView } from '@tarojs/components'
import { RANK_LIST } from '@/mock'
import styles from './RankList.module.less'
const RANK_ICON = {
1: '//yun.duiba.com.cn/taobaomini/pike_call/icon_06.png',
2: '//yun.duiba.com.cn/taobaomini/pike_call/icon_07.png',
3: '//yun.duiba.com.cn/taobaomini/pike_call/icon_08.png'
}
const RankList = memo((props) => {
const { rankList = RANK_LIST, rankIcon = RANK_ICON } = props
return (
<ScrollView scrollY className={styles['scroll-container']}>
{rankList.map((itm, i) => {
return (
<View
className={`${styles['scroll-item']} ${
styles['scroll-item' + (i + 1)]
}`}
>
<View className={styles['scroll-item__rank']}>
{i <= 2 && (
<Image className={styles['rank-icon']} src={rankIcon[i + 1]} />
)}
<View className={styles['rank-num']}>{itm.rank}</View>
</View>
<View className={styles['scroll-item__user']}>{itm.userNick}</View>
<View className={styles['scroll-item__score']}>
{itm.rankValue}
</View>
</View>
)
})}
</ScrollView>
)
})
export default RankList
.scroll-container {
.wh(100%,100%);
max-height: 700px;
}
.text-overflow-el() {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.scroll-item {
.flex-row-space();
.wh(100%,70px);
background-color: #F7F0E6;
border: 1px solid #ECDBC0;
box-sizing: border-box;
}
.scroll-item__rank {
width: 33%;
.flex-row-center();
.text-overflow-el();
position: relative;
}
.rank-icon {
position: absolute;
left: 50%;
top: 50%;
.wh(30px,30px);
transform: translate(-50px,-50%);
}
.rank-num {
// transform: translate(-40px,0);
font-size: 30px;
font-family: YouSheBiaoTiHei;
font-weight: 400;
color: #2D2B28;
font-weight: 600;
}
.scroll-item__user {
width: 34%;
.flex-row-center();
.text-overflow-el();
font-size: 26px;
font-family: SourceHanSansCN;
font-weight: 400;
color: #666666;
}
.scroll-item__score {
width: 33%;
.flex-row-center();
.text-overflow-el();
font-size: 28px;
font-family: WorkSans;
font-weight: 600;
color: #2D2B28;
}
\ No newline at end of file
import React, { memo } from 'react'
import { View } from '@tarojs/components'
import styles from './RankTitle.module.less'
const RankTitle = memo((props) => {
const {
titleList = ['排名', '用户名', '人气值'],
myRank = { rank: '11', userNick: '用户名', rankValue: '534443' }
} = props
return (
<View className={styles['rank-box']}>
<View className={styles['rank-box__title']}>
{titleList.map(val => {
return <View className={styles['box-itm']}>{val}</View>
})}
</View>
<View className={styles['rank-box__info']}>
<View className={styles['box-itm']}>{myRank.rank}</View>
<View className={styles['box-itm']}>{myRank.userNick}</View>
<View className={styles['box-itm']}>{myRank.rankValue}</View>
</View>
</View>
)
})
export default RankTitle
.rank-box {
.wh(100%,168px);
}
.rank-box__title {
.wh(100%,102px);
.flex-row-space();
.image('//yun.duiba.com.cn/taobaomini/pike_call/rank_title_bg.png');
}
.rank-box__info {
.wh(100%,66px);
.flex-row-space();
background: linear-gradient(-90deg, #FFDE8B 0%, #FFEAB5 100%);
}
.box-itm {
height: 66px;
width: 33%;
.flex-row-center();
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.box-itm:first-child {
font-size: 30px;
font-family: YouSheBiaoTiHei;
font-weight: 600;
color: #2D2B28;
}
.box-itm:nth-child(2) {
width: 34%;
font-size: 26px;
font-family: SourceHanSansCN;
font-weight: 400;
color: #666666;
}
.box-itm:last-child {
font-size: 28px;
font-family: WorkSans;
font-weight: 600;
color: #2D2B28;
}
\ No newline at end of file
import { RankList, RankTitle } from '..'
const Demo = () => {
return (
<>
<RankTitle />
<RankList />
</>
)
}
export default Demo
export { default as RankList } from './RankList/RankList'
export { default as RankTitle } from './RankTitle/RankTitle'
import React, { useState, useEffect } from "react";
import { View, Image, Text } from "@tarojs/components";
import styles from "./RotateWheel.module.less";
import { useThrottle } from "@/hooks/useThrottle";
import API from "@/api";
import { prizeList } from "@/mock";
import Taro, { showToast, redirectTo, navigateTo } from "@tarojs/taro";
const oneTurn = 360;
const RotateWheel = props => {
const {
containerInfo={width:600,height:600},
bg = "https://yun.duiba.com.cn/spark/assets/8b6e920ffd09fab8f9ac2de09f9154879f4d0607.png",
ratio = 0.65,
radius = 300,
circles = 4,
divideNum = 8,
duration = 5000,
timeFunction = "ease-out",
nodeInfo = { width: 100, height: 100 },
imgInfo = { width: 80, height: 80 },
showWay = "negative",
isShowPrizeName = true,
prizeNameWidthRatio = 1.2,
callback = () => {}
} = props;
/* 奖品列表 */
const [prizelist, setPrizelist] = useState(prizeList);
/* 平分角度 */
const [angle, setAngle] = useState(oneTurn / divideNum);
/* 展示奖品列表 */
const [show, setShow] = useState(false);
/* 转动一圈时的角度值 */
const [rotateInOneTurn, setRotateInOneTurn] = useState(0);
/* 动画参数 */
const [ani, setAni] = useState({
startFlag: false,
option: { duration: duration, timeFunction: timeFunction, rotate: 0 }
});
useEffect(() => {
computePosition();
}, [prizeList]);
/**
* @description 获取奖品列表
*/
// const getPrizeListInfo = async () => {
// const {success,data,code,message} = await API.getRotatePrizeListInfo().catch((res)=>{
// showToast({title:res?.message ? res?.message : '网络异常,请稍后再试'})
// })
// if(success && data){
// const {list} = data;
// // setPrizelist(list)
// computePosition(list)
// }
// }
/**
* @description 计算奖品位置,旋转角度
*/
const computePosition = () => {
if (!prizeList.length) return;
let centerX, centerY, color, rotate;
/* 获取每块奖品的中心位置 */
prizelist.forEach((ele, i) => {
if (i % 2 == 0) {
color = "#527aff";
} else {
color = "#f13082";
}
// 当前奖品左上角位置
if (showWay === "negative") {
centerX =
radius -
ratio * radius * Math.sin(((angle / 2 + angle * i) / 180) * Math.PI);
centerY =
radius -
ratio * radius * Math.cos(((angle / 2 + angle * i) / 180) * Math.PI);
rotate = -(angle / 2 + angle * i);
} else {
centerX =
radius -
ratio * radius * Math.sin(((angle / 2 + angle * -i) / 180) * Math.PI);
centerY =
radius -
ratio * radius * Math.cos(((angle / 2 + angle * -i) / 180) * Math.PI);
// 旋转角度
rotate = -(angle / 2 + angle * -i);
}
ele.rotate = rotate;
ele.color = color;
// 奖品位置
let disleft = centerX - nodeInfo.width / 2;
let distop = centerY - nodeInfo.height / 2;
ele.centerX = disleft;
ele.centerY = distop;
ele.num = i;
// 测试:设置奖品Id
ele.prizeId = 100 + i;
});
console.log("prizelist", prizelist);
setPrizelist(prizelist);
setShow(true);
};
/**
* @description 抽奖
*/
const drawPrize = useThrottle(async () => {
// const {success,data,message,code} = await API.drawRotatePrize().catch((res)=>{
// showToast({title:res?.message ? res?.message : '网络异常,请稍后再试'})
// });
// if(success && data){
// const {prizeId,id} = data;
// }
// test
let prizeId = Math.floor(Math.random() * prizelist.length) + 100;
startRotation(prizeId);
setTimeout(() => {
// 弹出弹窗
callback && callback(); //prizeInfo
}, duration + 1000);
}, duration);
/**
* @description 根据奖品id开始旋转
* @param {*} prizeId 抽中的奖品id
*/
const startRotation = prizeId => {
console.log("start");
let rotateAngle;
let num;
let oneturnAngle;
prizelist.forEach(ele => {
if (ele.prizeId === prizeId) {
num = ele.num;
}
});
// 旋转角度 ( 旋转圈数 + 对应位置(0,7) ) * 平分角度 + 平分角度 / 2
if (showWay === "negative") {
rotateAngle = oneTurn * circles + num * angle + angle / 2;
oneturnAngle = num * angle + angle / 2;
} else {
rotateAngle =
oneTurn * circles + (prizelist.length - num) * angle + angle / 2;
oneturnAngle = (prizelist.length - num) * angle + angle / 2;
}
console.log("rotateAngle", rotateAngle, num, prizeId);
/* 执行动画 */
setAni({
startFlag: true,
option: {
...ani.option,
rotate: ani.option.rotate - rotateInOneTurn + rotateAngle
}
});
setRotateInOneTurn(oneturnAngle);
};
return (
<View
className={styles["rotate"]}
style={{
width: `${containerInfo.width / 100}rem`,
height: `${containerInfo.height / 100}rem`,
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
>
<View
className={`${styles["rotate_container"]}`}
style={{
backgroundImage: `url(${bg})`,
transform: ani.startFlag ? `rotate(${ani.option.rotate}deg)` : "",
transition: ani.startFlag
? `all ${ani.option.timeFunction} ${ani.option.duration}ms`
: ""
}}
>
{show &&
prizelist.length &&
prizelist.map((ele, i) => {
return (
<View
className={styles["item"]}
style={{
// backgroundColor:`${ele?.color}`,
width: `${nodeInfo.width / 100}rem`,
height: `${nodeInfo.height / 100}rem`,
transform: `rotate(${ele?.rotate}deg)`,
top: `${ele.centerY / 100}rem`,
left: `${ele.centerX / 100}rem`,
position: "absolute"
}}
key={"item" + i}
>
{isShowPrizeName && (
<View
className={styles["prize_name"]}
style={{
width: `${prizeNameWidthRatio * 100}%`,
height: `${30 / 100}rem`
}}
>
{ele.name}
</View>
)}
<View
className={styles["prize_img"]}
style={{
backgroundImage: `url(${ele.image})`,
width: `${imgInfo.width / 100}rem`,
height: `${imgInfo.height / 100}rem`
}}
></View>
</View>
);
})}
</View>
<View className={styles["circle"]} onTap={drawPrize}>
draw
</View>
</View>
);
};
export default RotateWheel;
.rotate{
margin: 0 auto;
}
.rotate_container{
width: 100%;
height: 100%;
// border: 1px solid cyan;
// box-sizing: border-box;
position: relative;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
.image-property(cover, center center);
.item{
position: absolute;
display: flex;
justify-content: space-evenly;
align-items: center;
transform-origin: center;
flex-direction: column;
// border: 1px solid cyan;
// box-sizing: border-box;
.prize_name{
// margin: 0 auto;
// border: 1px solid cyan;
// box-sizing: border-box;
// display: flex;
// justify-content: center;
// align-items: center;
text-align: center;
white-space: nowrap;
font-size: 18px;
text-overflow: ellipsis;
overflow: hidden;
}
.prize_img{
.image-property(cover, center center);
margin: 0 auto;
}
}
}
.circle{
// border-radius: 50%;
width: 100px;
height: 100px;
background-color: rgb(78, 159, 165);
display: flex;
justify-content: center;
align-items: center;
position: absolute;
clip-path: polygon(50% 0%,0% 100%,100% 100%);
}
<mxfile host="65bd71144e">
<diagram id="aWOBYljMTH3HiLopJFIH" name="第 1 页">
<mxGraphModel dx="899" dy="594" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="3" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;shadow=1;fillColor=#FFFFFF;" vertex="1" parent="1">
<mxGeometry x="200" y="160" width="300" height="300" as="geometry"/>
</mxCell>
<mxCell id="6" value="" style="endArrow=none;dashed=1;html=1;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="200" y="310" as="sourcePoint"/>
<mxPoint x="500" y="310" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="8" value="" style="endArrow=none;dashed=1;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;fillColor=#FF9999;" edge="1" parent="1" source="3" target="3">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="430" y="330" as="sourcePoint"/>
<mxPoint x="480" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="9" value="" style="rounded=0;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFCCCC;rotation=45;" vertex="1" parent="1">
<mxGeometry x="379" y="210" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="10" value="" style="rounded=0;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFCCCC;rotation=45;" vertex="1" parent="1">
<mxGeometry x="379" y="340" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="11" value="" style="rounded=0;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFCCCC;rotation=45;" vertex="1" parent="1">
<mxGeometry x="250" y="210" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="12" value="" style="rounded=0;whiteSpace=wrap;html=1;shadow=0;fillColor=#FFCCCC;rotation=45;" vertex="1" parent="1">
<mxGeometry x="250" y="340" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="13" value="1.找到每个奖品的中心位置&lt;br&gt;2.每个奖品旋转相应角度&lt;br&gt;3.给相应的奖品区域设置样式" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;" vertex="1" parent="1">
<mxGeometry x="490" y="180" width="250" height="50" as="geometry"/>
</mxCell>
<mxCell id="16" value="旋转逻辑" style="rounded=0;whiteSpace=wrap;html=1;shadow=0;strokeColor=#000000;fillColor=#FFCCCC;" vertex="1" parent="1">
<mxGeometry x="80" y="510" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="17" value="给每个奖品设置一个索引值,抽中的奖品id与奖品列表对应,则根据索引值旋转对应的角度。" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;shadow=0;" vertex="1" parent="1">
<mxGeometry x="80" y="590" width="200" height="110" as="geometry"/>
</mxCell>
<mxCell id="18" value="" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;shadow=0;strokeColor=#000000;fillColor=#FFCCCC;" vertex="1" parent="1">
<mxGeometry x="310" y="270" width="80" height="80" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
\ No newline at end of file
**记事本打开((cmd + k) + v)**
##### 大转盘配置项入参
| 配置项 | 类型 | 描述 | 默认值 | 备注 |
| :-------- | :-----: | :--------: | :-----: | :---:|
| bg | String | 转盘背景 | / | / |
| radius | Number | 转盘半径大小 | 300 | px |
| ratio | Number | 距离中心点的比例系数 | 0.65 | 小于1 |
| divideNum | Number | 转盘划分为几等分 | 8 | 最少2等分|
| circles | Number | 转盘基本转动圈数 | 4 | / |
| duration | Number | 转盘持续时间 | 4000 | 毫秒 |
| timeFunction | String | 缓动动画方式 | 'ease-out' | 'ease-in-out', cubic-bezier(xx,xx,xx,xx) |
| nodeInfo | Object | 单个奖品区域大小 | {width:100,height:100} | 宽高100px |
| imgInfo | Object | 奖品图片大小 | {width:80,height:80} | 宽高80px |
| showWay | String | 奖品排列方式 | 'negative' |negative逆时针排列 positive顺时针排列|
| isShowPrizeName | Boolean | 是否展示奖品名称 | true | true展示 false不展示 |
| prizeNameWidthRatio | Number | 奖品名称容器宽度与nodeInfo宽度的比值 | 1.2 | 超过容器宽度则...省略 |
##### 出参
| 配置项 | 类型 | 描述 | 默认值 | 备注 |
| :-------- | :-----: | :--------: | :-----: | :---:|
| callback | Function | 回调函数 | / |抽中奖品后的回调方法,后续的弹窗处理|
+ 使用
```jsx
import RotateWheel from '@/components/_tb_comps/RotateWheel/RotateWheel'
const rotateConfig = { //不传走默认配置
bg: 'https://yun.duiba.com.cn/spark/assets/8b6e920ffd09fab8f9ac2de09f9154879f4d0607.png',
ratio: 0.65,
radius: 300,
circles: 4,
divideNum: 8,
duration: 5000,
timeFunction:'ease-out',
nodeInfo:{
width:100,
height:100
},
imgInfo:{
width:80,
height:80
},
showWay:'negative',//positive/negative
isShowPrizeName:true
}
export const Index = () => {
return(
<RotateWheel {...rotateConfig} callback={()=>{}} />
)
}
```
\ No newline at end of file
import React, { useState } from 'react'
import { View, Image, ScrollView } from '@tarojs/components'
import Taro, { useShareAppMessage, useDidShow } from '@tarojs/taro'
import classnames from 'classnames'
import styles from './ScrollXView.module.less'
function ScrollXView(props) {
const {
prizeList = [],
containerWidth = 300,
containerHeight = 300,
imgWidth=240,
imgHeight=240,
isShowPrizeName=true,
prizeInCenterNum=2,
marginRight=26,
backgroundColor='#E7E7EF',
backgroundImage=''
} = props
const showPrize = classnames(styles['show-pize-box'],{
[`${styles['show-pize-box__two']}`]: prizeList.length <= prizeInCenterNum,
[`${styles['show-pize-box__more']}`]: prizeList.length > prizeInCenterNum,
})
return (
<>
<ScrollView scrollX className={styles['scroll-x-box']}>
{<View className={showPrize}>
{
prizeList.length > 0 && prizeList.map((itm,i) => {
return(
<View
key={'prize'+i}
className={styles['show-pirze__item']}
style={{
width:`${containerWidth/100}rem`,
height:`${containerHeight/100}rem`,
backgroundImage:`${backgroundImage}`,
backgroundColor:backgroundColor,
marginRight:`${((prizeList.length > prizeInCenterNum) &&(prizeList.length - 1) !== i) ? `${marginRight/100}rem` : ''}`
}}
>
<View
className={styles['pirze__item-img']}
style={{
width:`${imgWidth/100}rem`,
height:`${imgHeight/100}rem`
}}
>
<Image
src={itm.image}
className={styles['pirze__item-img-icon']}
style={{
width:`${imgWidth/100}rem`,
height:`${imgHeight/100}rem`
}}
/>
</View>
{
isShowPrizeName &&
<View
className={styles['pirze__item-name']}
style={{
height:`${60/100}rem`,
fontSize:`${24/100}rem`
}}
>{itm.name}</View>
}
</View>
)
})
}
</View>}
</ScrollView>
</>
)
}
export default ScrollXView
\ No newline at end of file
##### ScrollXView滑动配置项
+ 入参
| 配置项 | 类型 | 描述 | 默认值 | 备注 |
| :-------- | :-----: | :--------: | :-----: | :---:|
| prizeList | Array | 滑动数据 | [...] | / |
| containerWidth | Number | 单个奖品容器的宽度 | 300 | / |
| containerHeight | Number | 单个奖品容器的高度 | 300 | / |
| imgWidth | Number | 奖品图片宽度 | 200 | / |
| imgHeight | Number | 奖品图片高度 | 200 | / |
| isShowPrizeName | Boolean | 是否展示奖品名称 | true | true:展示 false:不展示 |
| prizeInCenterNum | Number | 奖品少于n个时居中展示 | n=2 | 多于n个,可滑动查看 |
| marginRight | Number | 奖品容器之间间距 | 26 | px |
| backgroundColor | String | 奖品容器背景颜色 | #E7E7EF | / |
| backgroundImage | String | 奖品容器背景图 | '' | 默认为空 |
+ prizeList数组格式
```json
[
{
"image":"",
"name":"xxx",
// ...
},
// ...
]
```
+ 使用
```jsx
import ScrollXView from '@/components/_tb_comps/ScrollXView/ScrollXView'
// 组件不一定在该目录下,仅做举例引入
const SCROLLXVIEW_CONFIG = { //不传走默认配置
prizeList: [],
containerWidth : 300,
containerHeight : 300,
imgWidth:200,
imgHeight:200,
isShowPrizeName:true,
prizeInCenterNum:2,
marginRight:26,
backgroundColor:'#E7E7EF',
backgroundImage:''
}
export const Index = () => {
return(
<View className="scroll">
<ScrollXView {...SCROLLXVIEW_CONFIG} prizeList={prizeList} />
</View>
)
}
```
\ No newline at end of file
.scroll-x-box {
.wh(100%,100%);
display: flex;
justify-content: flex-start;
align-items: center;
}
.show-pize-box {
display: flex;
justify-content: flex-start;
align-items: center;
}
.show-pize-box__two {
width: 100%;
display: flex;
justify-content: space-around;
align-items: center;
}
.show-pirze__item {
// background: #E7E7EF;
border-radius: 25px;
// margin-left: 26px;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
.image-property(cover, center center);
}
// .show-pirze__item:last-child {
// margin-right: 26px;
// }
// .show-pirze__item:first-child {
// margin-left: 10px;
// }
.show-pize-box__more{
// padding: 10px;
}
.pirze__item-img {
// width: 224px;
// height: 224px;
background: #FFFFFF;
border-radius: 25px;
margin: 16px auto 20px;
}
.pirze__item-img-icon {
// width: 224px;
// height: 224px;
border-radius: 25px;
.image-property(cover, center center);
}
.pirze__item-name {
width: 90%;
font-family: FZLTHProGlobal;
font-weight: 400;
color: #1C1C1C;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
// 单行
// white-space: nowrap;
// 多行
// display: -webkit-box;
// -webkit-box-orient:vertical;
// -webkit-line-clamp: 2;
}
\ No newline at end of file
import React, { useState, useEffect,useRef, useMemo, useCallback } from "react";
import { View, Image, Text, Swiper, SwiperItem } from "@tarojs/components";
import './SwiperView.less'
import Taro,{showToast,redirectTo,navigateTo} from '@tarojs/taro'
export const EASING_WAYS = {
'DEFAULT':'default',
'LINEAR':'linear',
'EASEIN':'easeInCubic',
'EASEOUT':'easeOutCubic',
'EASEINOUT':'easeInOutCubic'
}
const SwiperView = (props) => {
/**
* @description swiperList
* {image,content,hasBtn}
*/
const {
swiperList=[],
easingFunction=`${EASING_WAYS.LINEAR}`,
circular=false,
autoplay=false,
vertical=false,
duration=1000,
interval=1500,
disableTouch=false,
swiperGroup={width:300,height:300,overflow:'visible'},
swiperContent={width:250,height:300,backgroundColor:'#fff',backgroundImage:'',borderRadius:25},
imgContent={width:200,height:200},
textContent={width:200,height:60},
showLeftRightBtn=true,
} = props;
const [currentIndex, setCurrentIndex] = useState(1);
const indexref = useRef(0)
// const index = useMemo(()=>{
// return currentIndex;
// },currentIndex)
/**
* @description onchange事件
* @param {*} e
*/
const swiperChange = e => {
console.log("e", e);
console.log('swiperChange-----currentindex',currentIndex,indexref.current,e.detail.current);
setCurrentIndex(e.detail.current);
};
const onTransition = (e) => {
// console.warn(e)
// setCurrentIndex(indexref.current);
}
/**
* @description 入场/出场动画
* @param {*} index 数组索引值
* @returns 返回类名
*/
const useCustomAni =((i) => {
return `${((currentIndex < i && ((i !== swiperList.length - 1)||currentIndex!==0)) || ((currentIndex === swiperList.length - 1) && i == 0 ))
? 'item_right'
:
(((currentIndex > i && (i !== 0 || currentIndex !== swiperList.length - 1)) || (currentIndex !== 0 && i !== swiperList.length - 1)) ? 'item_left' : 'item_middle')
}`
})
/**
* @description 获取当前卡片详情之类
*/
const getCurrentDetail = (item) => {
showToast({
title:'111'
})
}
const motionType = {
LEFT:"LEFT",
RIGHT:"RIGHT"
}
/**
* @description
*/
const turnRound = ((type) => {
const motion = {
[motionType.LEFT]:()=>{
setTimeout(() => {
// setCurrentIndex((indexref.current==0) ? (swiperList.length - 1) : (indexref.current - 1))
// indexref.current = (indexref.current==0) ? (swiperList.length - 1) : (indexref.current - 1)
// console.log('left----',currentIndex,indexref.current);
if(currentIndex >= 1){
setCurrentIndex(currentIndex - 1)
}
}, 0);
},
[motionType.RIGHT]:()=>{
setTimeout(() => {
// setCurrentIndex((indexref.current == (swiperList.length - 1)) ? 0 : (indexref.current + 1))
// indexref.current = (indexref.current == (swiperList.length - 1)) ? 0 : (indexref.current + 1)
// console.log('right----',currentIndex,indexref.current);
if(currentIndex < swiperList.length - 1){
setCurrentIndex(currentIndex + 1)
}
}, 0);
}
}
motion[type]();
})
useEffect(()=>{
console.log('cur----',currentIndex);
},[currentIndex])
return (
<View className="contain">
<View className='left_btn' onTap={()=>turnRound('LEFT')}>&lt;&lt;</View>
<View
className="scroll_container"
style={{
width:`${700/100}rem`,
overflow:'hidden'
}}
>
<Swiper
className="swiper_group"
current={currentIndex}
easingFunction={easingFunction}
onChange={swiperChange}
circular={circular}
vertical={vertical}
autoplay={autoplay}
duration={duration}
interval={interval}
disableTouch={disableTouch}
style={{
width:`${swiperGroup.width/100}rem`,
height:`${swiperGroup.height/100}rem`,
overflow:`${swiperGroup.overflow}`
}}
>
{swiperList.length &&
swiperList.map((item, i) => (
<SwiperItem>
<View
className={`scroll_item ${i === currentIndex ? "current_item" : ""} ${useCustomAni(i)}`}
style={{
width:`${swiperContent.width/100}rem`,
height:`${swiperContent.height/100}rem`,
backgroundColor:`${ swiperContent.backgroundColor}`,
backgroundImage:`${ swiperContent.backgroundImage}`,
borderRadius:`${swiperContent.borderRadius/100}rem`
}}
onTap={()=>getCurrentDetail(item)}
>
{/* {i === currentIndex && (
<Image
className="checkPic"
src="https://yun.dui88.com/tebuXinYuan/checkGoods.png"
></Image>
)} */}
<View
className="item_img_content"
>
{/* 单个swiper主要内容 */}
<Image
className="img"
style={{
width:`${imgContent.width/100}rem`,
height:`${imgContent.height/100}rem`
}}
src={item.image}></Image>
{
item?.content &&
<View
className='text_content'
style={{
width:`${textContent.width/100}rem`,
height:`${textContent.height/100}rem`,
}}
>
<View className='text' >{item.content}</View>
</View>
}
</View>
</View>
</SwiperItem>
))}
</Swiper>
</View>
<View className='right_btn' onTap={()=>turnRound('RIGHT')}>&gt;&gt;</View>
</View>
);
};
export default SwiperView;
.contain{
width: 100%;
position: relative;
}
.left_btn{
width: 80px;
height: 40px;
display: flex;
position: absolute;
justify-content: center;
align-items: center;
z-index: 1;
top: 50%;
transform: translateY(-50%);
border: 1px solid cyan;
box-sizing: border-box;
}
.right_btn{
width: 80px;
height: 40px;
display: flex;
position: absolute;
justify-content: center;
align-items: center;
z-index: 1;
top: 50%;
transform: translateY(-50%);
border: 1px solid cyan;
box-sizing: border-box;
}
.left_btn{
left: 10px;
}
.right_btn{
right: 10px;
}
.scroll_container{
// width: 450px;
// overflow: hidden;
width: 80%;
margin: 0 auto;
display: flex;
justify-content: space-around;
align-items: center;
position: relative;
.swiper_group {
margin: 0 auto;
position: relative;
swiper-item {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
.image-property(cover, center center);
// border: 1px solid cyan;
// box-sizing: border-box;
}
.scroll_item {
// width: 300px;
margin: 0 auto;
position: relative;
border-radius: 20px;
.image-property(cover, top center);
.checkPic {
width: 60px;
height: 60px;
border-radius: 50%;
position: absolute;
right: 40px;
top: 40px;
}
.text_content{
margin: 0 auto;
box-sizing: border-box;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
overflow: hidden;
.text{
color: #232323;
text-align: center;
line-height: 30px;
font-size: 24px;
}
}
&.current_item {
.item_img_content {
.img{}
.text_content{
.text{}
}
}
}
}
// &.item_left{
// transform: translateX(-300px);
// transition: all 1s ease;
// }
// &.move_right{
// transform: translateX(300px);
// transition: all 1s ease;
// }
// &.item_middle{
// transform: translateX(0px);
// transition: all 1s ease;
// }
}
}
@keyframes movein {
from {
transform: perspective(1000px) translate3d(0,0,0) rotate3d(0, 1, 0, 25deg) scale(0.85);
transform-style: preserve-3d;
transform-origin: 45% center;
}
50%{
transform: perspective(1000px) translate3d(0,20px,0) rotate3d(0, 1, 0, 25deg) scale(0.85);
transform-style: preserve-3d;
transform-origin: 45% center;
}
to {
transform: perspective(1100px) translate3d(0,0,0) rotate3d(0, 1, 0, 0deg) scale(1);
transform-origin: 45% center;
transform-style: preserve-3d;
}
}
@keyframes moveout{
from {
transform: perspective(1000px) rotate3d(0, 1, 0, 0deg) scale(1);
transform-style: preserve-3d;
transform-origin: 45% center;
}
50%{
transform: perspective(1000px) rotate3d(0, 1, 0, 25deg) scale(1);
transform-style: preserve-3d;
transform-origin: 45% center;
}
to {
transform: perspective(1200px) rotate3d(0, 1, 0, 25deg) scale(0.85);
transform-origin: 45% center;
transform-style: preserve-3d;
}
}
@keyframes scales {
from{
transform: scale(1,1);
transform-origin: center;
}
50%{
transform: scale(1.1,1.1);
transform-origin: center;
}
to{
transform: scale(1,1);
transform-origin: center;
}
}
\ No newline at end of file
##### swiperView 滑动
| 配置项 | 类型 | 描述 | 默认值 | 备注 |
| :-------- | :-----: | :--------: | :-----: | :---:|
| swiperList | Array | 滑动数组 | [...] | / |
| disableTouch | Boolean | 是否可手动滑动 | false | true 可滑动 false 不可滑动 |
| autoplay | Boolean | 是否自动切换 | false |/|
| circular | Boolean | 是否采用衔接滑动| false |/|
| vertical | Boolean | 是否垂直切换| false |/|
| duration | Number | 滑动动画时长| 1000 |/|
| interval | Number | 自动切换时间间隔| 1500 |/|
| easingFunction|String | 缓动动画类型| linear |'default','linear' 'easeInCubic' 'easeOutCubic' 'easeInOutCubic'|
| swiperGroup | Object | 整个swiper的宽高 |{width:300,height:300,overflow:''}|/|
| swiperContent | Object | 单个swiperItem的宽高 |{width:250,height:300,backgroundColor:'#fff',borderRadius:25,backgroundImage:''}|/|
| imgContent | Object | 图片宽高 |{width:200,height:200}|/|
| textContent | Object | 文本宽高 |{width:300,height:60}|/|
| showLeftRightBtn | Boolean | 是否显示左右点击按钮|/|
#### swiperList数据
```json
[
{
"image":"",//图片链接,图片宽高需要在less中修改
"content":"",//图片描述
// 或者手动修改组件中的字段名称 ...
},
// ...
]
```
+ 使用
```jsx
import SwiperView from '@/components/_tb_comps/SwiperView/SwiperView'
const swiper_config = {//不传走默认配置
easingFunction:`linear`,
circular:true,
autoplay:false,
vertical:false,
duration:1000,
interval:1500,
disableTouch:false,
swiperGroup:{width:300,height:300,overflow:''},
swiperContent:{width:250,height:300,backgroundColor:'#fff',backgroundImage:'',borderRadius:25},
imgContent:{width:200,height:200},
textContent:{width:240,height:60}
}
export const Index = () => {
return(
<SwiperView {...swiperConfig} swiperList={swiperList} />
)
}
```
\ No newline at end of file
import React, { useState } from 'react'
import { View, Image, ScrollView } from '@tarojs/components'
import classnames from 'classnames'
import styles from './TitleImg.module.less'
function TitleImg(props) {
const { config } = props
return (
<View className={styles['title-box']}>
<Image src={config.image} mode="widthFix" style={{width: config.width / 100 + 'rem', height: config.height / 100 + 'rem' }} />
</View>
)
}
export default TitleImg
\ No newline at end of file
.title-box {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
\ No newline at end of file
/**
* 全局配置
*/
import React from 'react'
export const ProgressGlobalConfig = {
type: 1,
maxLevel: 15,
width: 500
}
export const ProgressContext = React.createContext()
import { View } from '@tarojs/components'
import React, { memo, useContext, useMemo } from 'react'
import classnames from 'classnames'
import tbcc from 'tbcc-sdk-ts'
import { ProgressContext } from '../ProgressContext'
// css
import styles from './index.module.less'
const { throttleHandle } = tbcc.utils
const ProgressNode = memo(props => {
/**
* isReach 是否达到当前进度
* levelValue 节点值
* levelPos 节点偏移
* onReceviceClick 达到当前进度才可点击触发
*/
const { isReach, levelValue, levelPos, onReceviceClick = () => {} } = props
const { maxLevel, nodeClassName } = useMemo(
() => useContext(ProgressContext),
ProgressContext
)
const getNodeStyle = classnames(styles['defualt-progress-node'], {
[nodeClassName]: nodeClassName
})
const onNodeClick = throttleHandle(() => {
isReach && onReceviceClick()
}, 2000)
return (
<View
onTap={onNodeClick}
className={getNodeStyle}
style={{
left: levelPos || (levelValue / maxLevel) * 100 + '%'
}}
>
{/* 自定义 */}
</View>
)
})
export default ProgressNode
.defualt-progress-node {
display: flex;
flex-direction: column;
position: absolute;
z-index: 10;
font-size: 20px;
transform: translate(-100%, -100%);
white-space: nowrap;
}
// 描述
.defualt-progress-node-desc {
position: absolute;
right: -110px;
top: -54px;
z-index: 11;
}
// Icon
.defualt-progress-node-icon {
.wh(32px, 32px);
}
// Icon贴图
.defualt-progress-node-icon-iconImg {
.wh(155px, 151px);
position: absolute;
top: -54px;
}
// Icon文本
.defualt-progress-node-icon-txt {
font-size: 22px;
}
// 按钮
.defualt-progress-node-btn-txt {
position: absolute;
top: 80px;
left: 6px;
}
# 进度条组件
---
## 何时使用
- 当某个页面需要展示累计邀请、分等级奖励进度条、累计签到等。
## 目录结构
`ProgressContext` 进度条全局默认配置
`ProgressNode` 进度条节点 自定义
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| levelList | 节点数据 | ArrayObject | 见下面示例一 | |
| currentValue | 当前进度值 | Numbver | 0 | |
| className | 进度条节点组样式 | Object | - | |
| maxLevel | 最大值 | Number | 15 | |
| width | 进度条长度 | Number | 500 | |
| onReceviceClick | 点击进度按钮回调 | Function | ()=>{} | |
| nodeClassName | 节点样式 | Object | - | |
## 示例一
```
[
{
levelValue: 0, //节点值
...
},
{
levelValue: 5,
...
]
```
import React, { useState } from 'react'
import { View } from '@tarojs/components'
import Progress from '../index'
import styles from './index.module.less'
const initLevelList = [
{
id: 0,
levelValue: 0,
isReceived: true,
iconInfo: {
iconUrl: '//yun.dui88.com/tebuXinYuan/main-circle-tip.png',
iconTxt: ''
},
descInfo: {
descTxt: '等级1'
}
},
{
id: 1,
levelValue: 5,
isReceived: false,
iconInfo: {
iconUrl: '//yun.dui88.com/tebuXinYuan/main-circle-tip.png',
iconTxt: ''
},
descInfo: {
descTxt: '等级2'
}
},
{
id: 2,
levelValue: 10,
isReceived: false,
iconInfo: {
iconUrl: '//yun.dui88.com/tebuXinYuan/main-circle-tip.png',
iconTxt: ''
},
descInfo: {
descTxt: '等级3'
}
},
{
id: 3,
levelValue: 15,
isReceived: false,
iconInfo: {
iconUrl: '//yun.dui88.com/tebuXinYuan/main-circle-tip.png',
iconTxt: ''
},
descInfo: {
descTxt: '等级4'
}
}
]
const Demo = props => {
const [levelList, setLevelList] = useState(initLevelList)
const [currentValue, setCurrentValue] = useState(10)
const onReceviceClick = id => {
console.log('点击:', id)
}
return (
<View className={styles['progress']}>
<Progress
levelList={levelList}
currentValue={currentValue}
btnClassName={styles['btnClassName']}
activeBtnClassName={styles['activeBtn']}
disableBtnClassName={styles['disableBtn']}
onReceviceClick={onReceviceClick}
/>
</View>
)
}
export default Demo
.progress {
margin-top: 100px;
display: flex;
justify-content: center;
}
.disableBtn {
.wh(108px, 42px);
.image('//yun.dui88.com/tebuXinYuan/main-received-btn.png');
}
.activeBtn {
.wh(108px, 42px);
.image('//yun.dui88.com/tebuXinYuan/main-waitReceive-btn.png');
}
.btnClassName {
.wh(108px, 42px);
.image('//yun.dui88.com/tebuXinYuan/main-waitUnlock-btn.png');
}
import { View } from '@tarojs/components'
import React,{ memo, useMemo, useCallback } from 'react'
import classnames from 'classnames'
import ProgressNode from './ProgressNode'
import styles from './index.module.less'
import {
ProgressContext,
ProgressGlobalConfig
} from './ProgressContext'
const Progress = memo(props => {
const {
onReceviceClick,
levelList = [],
currentValue = 0,
className,
maxLevel = ProgressGlobalConfig.maxLevel,
width = ProgressGlobalConfig.width
} = props
const LevelList = useMemo(() => {
return levelList
}, levelList)
// 合并全局配置
const mergeConfigToDefault = useCallback(config => {
return { ...ProgressGlobalConfig, ...config }
}, [])
/**
* 过滤props
* @param {*} props 过滤对象
* @param {*} filterPropArr 需要过滤掉的属性名称
* @returns
*/
const filterProps = (props, filterPropArr) => {
if (typeof props !== 'object' || !Array.isArray(filterPropArr)) {
throw new Error('参数格式不正确')
}
let obj = {}
Object.keys(props)
.filter(key => !filterPropArr.includes(key))
.map(key => {
obj[key] = props[key]
})
return obj
}
const getProgressStyle = classnames(styles['defualt-progress'], {
[className]: className
})
const getProItm = classnames(styles['progress-box-com'], {
[`${styles['progress-box-com-02']}`]: currentValue === maxLevel
})
return (
// 全局配置注入
<ProgressContext.Provider
value={mergeConfigToDefault(
filterProps(props, ['levelList', 'currentValue'])
)}
>
{/* 进度条-all */}
<View
className={styles['progress-box']}
style={{ width: width / 100 + 'rem' }}
>
{/* 进度条-current */}
<View
className={getProItm}
style={{
width:
currentValue >= maxLevel
? '100%'
: (currentValue / maxLevel) * 100 + '%'
}}
/>
{/* 进度条节点 */}
<View className={getProgressStyle}>
{LevelList.map((item, index) => {
return (
<ProgressNode
key={index}
{...item}
onReceviceClick={onReceviceClick}
isReach={item.levelValue <= currentValue}
/>
)
})}
</View>
</View>
</ProgressContext.Provider>
)
})
export default Progress
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment