diff --git a/src/app.tsx b/src/app.tsx index 4c2be8c..745132d 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -7,6 +7,7 @@ import { REFRESH_UNREAD_COUNT_TIME } from '@/constants/message'; import http from '@/http'; import store from '@/store'; import { requestUnreadMessageCount } from '@/utils/message'; +import { getInviteCode } from '@/utils/partner'; import qiniuUpload from '@/utils/qiniu-upload'; import { requestUserInfo, updateLastLoginTime } from '@/utils/user'; @@ -16,7 +17,11 @@ function App({ children }: PropsWithChildren) { useLaunch(async () => { console.log('App launched.'); await http.init(); - requestUserInfo(); + requestUserInfo().then(userInfo => { + if (userInfo.isPartner) { + getInviteCode(); + } + }); updateLastLoginTime(); qiniuUpload.init(); requestUnreadMessageCount(); diff --git a/src/components/partner-banner/index.less b/src/components/partner-banner/index.less new file mode 100644 index 0000000..2f506ce --- /dev/null +++ b/src/components/partner-banner/index.less @@ -0,0 +1,41 @@ +@import '@/styles/variables.less'; +@import '@/styles/common.less'; + +.partner-fragment-banner { + width: 100%; + height: 90px; + position: relative; + margin-bottom: 24px; + + &__image { + width: 100%; + height: 100%; + } + + &__btn { + width: 100%; + height: 100%; + position: absolute; + left: 0; + padding: 0; + margin: 0; + top: 0; + font-size: 0; + background: transparent; + border: none; + &::after { + border: none; + } + &:active { + background: transparent; + } + } + + &__close { + width: 64px; + height: 38px; + position: absolute; + right: 0; + top: 0; + } +} diff --git a/src/components/partner-banner/index.tsx b/src/components/partner-banner/index.tsx new file mode 100644 index 0000000..920c2fe --- /dev/null +++ b/src/components/partner-banner/index.tsx @@ -0,0 +1,66 @@ +import { Button, Image } from '@tarojs/components'; + +import { useCallback, useState } from 'react'; + +import LoginDialog from '@/components/login-dialog'; +import { PageUrl } from '@/constants/app'; +import useUserInfo from '@/hooks/use-user-info'; +import { becomePartner, getPartnerBannerClose, setPartnerBannerClose } from '@/utils/partner'; +import { navigateTo } from '@/utils/route'; +import { isNeedPhone } from '@/utils/user'; +import './index.less'; + +const PREFIX = 'partner-fragment-banner'; + +export default function PartnerBanner() { + const userInfo = useUserInfo(); + const needPhone = isNeedPhone(userInfo); + const isPartner = userInfo.isPartner; + + const [visible, setVisible] = useState(false); + const [bannerClose, setBannerClose] = useState(getPartnerBannerClose()); + + const handlePartnerBannerClose = useCallback(e => { + e.preventDefault(); + e.stopPropagation(); + setBannerClose(true); + setPartnerBannerClose(); + }, []); + + const handleBind = useCallback(async () => { + if (!isPartner) { + await becomePartner(); + } + await navigateTo(PageUrl.Partner); + }, [isPartner]); + + const handleClick = useCallback(async () => { + if (needPhone) { + return; + } + await handleBind(); + }, [handleBind, needPhone]); + + if (bannerClose) { + return null; + } + + return ( + <> +
+ + {needPhone && ( + + )} +
+
+ {visible && setVisible(false)} onSuccess={handleBind} needPhone={needPhone} />} + + ); +} diff --git a/src/components/partner-entry/index.less b/src/components/partner-entry/index.less index 1f48207..41ed9e2 100644 --- a/src/components/partner-entry/index.less +++ b/src/components/partner-entry/index.less @@ -44,87 +44,4 @@ color: @blHighlightColor; } } - - &__kanban { - border-radius: 16px; - margin-bottom: 24px; - position: relative; - background: #6d3df5; - color: #fff; - - &-bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - &-content { - position: relative; - min-height: 280px; - box-sizing: border-box; - padding: 36px 40px; - z-index: 1; - } - - &-button { - position: absolute; - z-index: 2; - top: 44px; - right: 56px; - font-size: 24px; - line-height: 24px; - &__image { - width: 20px; - height: 20px; - display: inline-block; - margin-left: 4px; - } - } - &-title { - font-style: normal; - font-weight: 400; - letter-spacing: 0.02em; - opacity: 0.7; - } - &-money { - font-family: 'Helvetica Neue'; - font-style: normal; - font-weight: 700; - } - &-total { - margin-bottom: 24px; - .partner-fragment-entry__kanban { - &-title { - font-size: 26px; - line-height: 40px; - margin-bottom: 12px; - } - &-money { - font-size: 48px; - line-height: 42px; - } - } - } - &-details { - .flex-row(); - &-part { - flex: 1; - - .partner-fragment-entry__kanban { - &-title { - font-size: 24px; - line-height: 36px; - margin-bottom: 4px; - } - &-money { - font-size: 32px; - line-height: 42px; - } - } - - } - } - } } diff --git a/src/components/partner-entry/index.tsx b/src/components/partner-entry/index.tsx index 8229586..38f62b3 100644 --- a/src/components/partner-entry/index.tsx +++ b/src/components/partner-entry/index.tsx @@ -1,12 +1,12 @@ -import { BaseEventOrig, Button, ButtonProps, Image } from '@tarojs/components'; +import { Button } from '@tarojs/components'; import { useCallback, useState } from 'react'; -import { PageUrl } from '@/constants/app'; +import LoginDialog from '@/components/login-dialog'; +import PartnerKanban from '@/components/partner-kanban'; import useUserInfo from '@/hooks/use-user-info'; -import { navigateTo } from '@/utils/route'; -import Toast from '@/utils/toast'; -import { requestUserInfo, setPhoneNumber } from '@/utils/user'; +import { becomePartner } from '@/utils/partner'; +import { isNeedPhone, requestUserInfo } from '@/utils/user'; import './index.less'; const PREFIX = 'partner-fragment-entry'; @@ -16,93 +16,42 @@ type JoinEntryProps = { }; function JoinEntry({ onBindSuccess }: JoinEntryProps) { const userInfo = useUserInfo(); + const needPhone = isNeedPhone(userInfo); + const [visible, setVisible] = useState(false); - const hasPhone = !!userInfo.phone; - - const handleGetPhoneNumber = useCallback(async (e: BaseEventOrig) => { - const encryptedData = e.detail.encryptedData; - const iv = e.detail.iv; - if (!encryptedData || !iv) { - return Toast.error('取消授权'); - } - - try { - await setPhoneNumber({ encryptedData, iv }); - await requestUserInfo(); - Toast.success('绑定成功'); - onBindSuccess(); - } catch (err) { - Toast.error('绑定失败'); - } - }, []); return ( -
-
- 加入播络合伙人,高达75%分成 + <> +
+
+ 加入播络合伙人,高达75%分成 +
+
模式简单,分成比例高,欢迎各位群主、经纪人或机构
+ {!needPhone && ( + + )} + {needPhone && ( + + )}
-
模式简单,分成比例高,欢迎各位群主、经纪人或机构
- {hasPhone && ( - - )} - {!hasPhone && ( - - )} -
- ); -} - -function PartnerKanban() { - const handleNavigate = useCallback(() => { - navigateTo(PageUrl.Partner); - }, []); - return ( -
- -
-
- 查看详情 - -
-
-
总收益(元)
-
1666.66
-
-
-
-
可提现(元)
-
666.23
-
-
-
提现中(元)
-
666.23
-
-
-
待分账(元)
-
666.23
-
-
-
-
+ {visible && setVisible(false)} onSuccess={onBindSuccess} needPhone={needPhone} />} + ); } export default function PartnerEntry() { - const [state, setState] = useState(1); - // TODO: 开通状态检查 - const handleBindSuccess = useCallback(() => { - setState(0); + const userInfo = useUserInfo(); + + const handleBindSuccess = useCallback(async () => { + await becomePartner(); + await requestUserInfo(); }, []); - if (state === 0) { - return ; + if (userInfo.isPartner) { + return ; } - return ; + return ; } diff --git a/src/components/partner-intro/index.less b/src/components/partner-intro/index.less index da7034f..7609cd7 100644 --- a/src/components/partner-intro/index.less +++ b/src/components/partner-intro/index.less @@ -12,8 +12,8 @@ line-height: 88px; text-align: center; font-size: 28px; - position: absolute; - top: 0; + position: fixed; + top: 98px; left: 0; right: 0; width: 100vw; @@ -121,3 +121,10 @@ margin-left: 32px; } } + +#posterCanvas { + position: fixed; + bottom: -99999px; + left: -99999px; + visibility: hidden; +} diff --git a/src/components/partner-intro/index.tsx b/src/components/partner-intro/index.tsx index 6ae4965..6018799 100644 --- a/src/components/partner-intro/index.tsx +++ b/src/components/partner-intro/index.tsx @@ -1,12 +1,120 @@ -import { Button } from '@tarojs/components'; +import { Button, Canvas } from '@tarojs/components'; +import Taro from '@tarojs/taro'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { openCustomerServiceChat } from '@/utils/common'; +import { getPartnerQrcode } from '@/utils/partner'; import './index.less'; const PREFIX = 'partner-intro'; + export default function PartnerIntro() { + const [posterPath, setPosterPath] = useState(''); + const getQrcode = async () => { + try { + const data = await getPartnerQrcode(); // 假设 getPartnerQrcode 返回 ArrayBuffer + const base64 = Taro.arrayBufferToBase64(data); + return `data:image/png;base64,${base64}`; + } catch (error) { + console.error('获取二维码失败', error); + Taro.showToast({ title: '获取二维码失败', icon: 'none' }); + throw error; + } + }; + const saveCanvasToTempFile = (): Promise => { + return new Promise((resolve, reject) => { + try { + const query = Taro.createSelectorQuery().select('#posterCanvas'); + query.fields({ node: true }).exec(async res => { + const canvas = res[0].node; + const tempFilePath = await Taro.canvasToTempFilePath({ + canvas, + x: 0, + y: 0, + width: 1500, // 实际绘制宽度 + height: 2668, // 实际绘制高度 + destWidth: 750, // 目标显示宽度 + destHeight: 1334, // 目标显示高度 + fileType: 'jpg', + }); + + setPosterPath(tempFilePath.tempFilePath); + + resolve(tempFilePath.tempFilePath); + }); + } catch (error) { + console.error('保存 Canvas 到临时文件失败', error); + Taro.showToast({ title: '保存 Canvas 失败', icon: 'none' }); + reject(error); + } + }); + }; + const drawCanvas = (qrCode: string): Promise => { + const query = Taro.createSelectorQuery().select('#posterCanvas'); + return new Promise(resolve => { + query.fields({ node: true, size: true }).exec(async res => { + const canvas = res[0].node; + const ctx = canvas.getContext('2d'); + + canvas.width = 1500; + canvas.height = 2668; + ctx.scale(2, 2); + + // 绘制背景图片 + const bgImage = canvas.createImage(); + const poster = 'https://publiccdn.neighbourhood.com.cn/img/poster.png' + bgImage.src = poster; + bgImage.onload = () => { + ctx.drawImage(bgImage, 0, 0, 750, 1334); + + const qrCodeImage = canvas.createImage(); + qrCodeImage.src = qrCode; // 假设 getQrcode() 返回的是二维码图片的路径 + qrCodeImage.onload = () => { + ctx.drawImage(qrCodeImage, 235, 894, 280, 280); // 绘制二维码,位置和大小 + saveCanvasToTempFile().then(tempPath => { + resolve(tempPath); + }); + }; + }; + bgImage.onerror = err => { + console.error(err); + }; + }); + }); + }; + const savePoster = async () => { + let filePath = posterPath; + if (!filePath) { + Taro.showLoading({ title: '正在生成海报' }); + const qrCode = await getQrcode(); + filePath = await drawCanvas(qrCode); + Taro.hideLoading(); + } + + const res = await Taro.getSetting(); + const hasPermission = res.authSetting['scope.writePhotosAlbum']; + if (hasPermission === false) { + Taro.showModal({ + title: '提示', + content: '需要访问相册权限才能保存图片,请前往设置开启权限', + showCancel: false, + success() { + Taro.openSetting(); + }, + }); + } else { + try { + await Taro.authorize({ scope: 'scope.writePhotosAlbum' }); + await Taro.saveImageToPhotosAlbum({ filePath }); + Taro.showToast({ title: '保存成功', icon: 'success' }); + } catch (error) { + console.error(error); + Taro.showToast({ title: '保存失败', icon: 'none' }); + } + } + }; + const handleOpenService = useCallback(() => { openCustomerServiceChat('https://work.weixin.qq.com/kfid/kfc4fcf6b109b3771d7'); }, []); @@ -45,17 +153,16 @@ export default function PartnerIntro() {
注:收益不设时限,可重复享有,播络保留活动最终解释权
- - ); -} + -export function PartnerIntroFooter() { - return ( -
- - +
+ + +
); } diff --git a/src/components/partner-invite-list/index.less b/src/components/partner-invite-list/index.less new file mode 100644 index 0000000..46cd21f --- /dev/null +++ b/src/components/partner-invite-list/index.less @@ -0,0 +1,86 @@ +@import '@/styles/common.less'; + +.partner-invite-list { + padding-top: 72px; + &__title { + height: 72px; + width: 100%; + background: #f7f7f7; + padding: 0 24px; + box-sizing: border-box; + line-height: 72px; + font-size: 24px; + color: rgba(0, 0, 0, 0.5); + position: fixed; + top: 98rpx; + left: 0; + z-index: 1; + .flex-row(); + + &-time-id { + padding: 0 8px; + flex: 1; + } + + &-created { + padding: 0 8px; + min-width: 96px; + max-width: 196px; + flex-shrink: 0; + } + + &-joined { + text-align: right; + width: 96px; + padding: 0 8px; + flex-shrink: 0; + } + } + + &__item { + height: 131px; + width: 100%; + background: #fff; + padding: 24px 32px; + box-sizing: border-box; + font-size: 28px; + + &-content { + .flex-row(); + width: 100%; + border-bottom: 1px solid #e6e7e8; + } + + &-time-id { + padding-right: 8px; + flex: 1; + } + &-item { + line-height: 40px; + padding-bottom: 8px; + .noWrap(); + } + &-id { + font-size: 24px; + line-height: 36px; + color: #999999; + .noWrap(); + } + + &-created { + padding: 0 8px; + min-width: 96px; + max-width: 196px; + line-height: 83px; + flex-shrink: 0; + } + + &-joined { + width: 96px; + text-align: right; + line-height: 83px; + padding-left: 8px; + flex-shrink: 0; + } + } +} diff --git a/src/components/partner-invite-list/index.tsx b/src/components/partner-invite-list/index.tsx new file mode 100644 index 0000000..1836a60 --- /dev/null +++ b/src/components/partner-invite-list/index.tsx @@ -0,0 +1,119 @@ +import { List, PullRefresh } from '@taroify/core'; +import classNames from 'classnames'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import ListPlaceholder from '@/components/list-placeholder'; +import { InviteUserInfo } from '@/types/partner'; +import { logWithPrefix } from '@/utils/common'; +import { formatTimestamp, formatUserId, getPartnerInviteList as requestData } from '@/utils/partner'; + +import './index.less'; + +const PREFIX = 'partner-invite-list'; +const log = logWithPrefix(PREFIX); + +function PartnerList(props: { + refreshDisabled?: boolean; + visible?: boolean; + listHeight?: number; + className?: string; + onListEmpty?: () => void; +}) { + const { className, listHeight, refreshDisabled, visible = true, onListEmpty } = props; + const [refreshing, setRefreshing] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [loadMoreError, setLoadMoreError] = useState(false); + const [dataList, setDataList] = useState([]); + const onListEmptyRef = useRef(onListEmpty); + + const handleRefresh = useCallback(async () => { + log('start pull refresh'); + try { + setRefreshing(true); + setLoadMoreError(false); + const list = await requestData(); + setDataList(list); + !list.length && onListEmptyRef.current?.(); + log('pull refresh success'); + } catch (e) { + setDataList([]); + setLoadMoreError(true); + log('pull refresh failed'); + } finally { + setRefreshing(false); + } + }, []); + + useEffect(() => { + onListEmptyRef.current = onListEmpty; + }, [onListEmpty]); + + // 初始化数据&配置变更后刷新数据 + useEffect(() => { + // 列表不可见时,先不做处理 + if (!visible) { + log('visible changed, but is not visible, only clear list'); + return; + } + + const refresh = async () => { + log('visible changed, start refresh list data'); + try { + setDataList([]); + setLoadingMore(true); + setLoadMoreError(false); + const list = await requestData(); + setDataList(list); + !list.length && onListEmptyRef.current?.(); + } catch (e) { + setDataList([]); + setLoadMoreError(true); + } finally { + log('visible changed, refresh list data end'); + setLoadingMore(false); + } + }; + refresh(); + }, [visible]); + + return ( +
+
+
邀请时间|用户编号
+
模卡
+
合伙人
+
+ + {}} + loading={loadingMore || refreshing} + disabled={loadMoreError} + fixedHeight={typeof listHeight !== 'undefined'} + style={listHeight ? { height: `${listHeight}px` } : undefined} + > + {dataList.map(item => ( +
+
+
+
{formatTimestamp(item.created)}
+
{formatUserId(item.userId)}
+
+
{item.isCreateResume ? '已创建' : '未创建'}
+
{item.isPartner ? '已加入' : '未加入'}
+
+
+ ))} + +
+
+
+ ); +} + +export default PartnerList; diff --git a/src/components/partner-kanban/index.less b/src/components/partner-kanban/index.less new file mode 100644 index 0000000..5486725 --- /dev/null +++ b/src/components/partner-kanban/index.less @@ -0,0 +1,174 @@ +@import '@/styles/variables.less'; +@import '@/styles/common.less'; + +.partner-kanban { + border-radius: 16px; + margin-bottom: 24px; + position: relative; + background: #6d3df5; + color: #fff; + + &__simple { + } + + &__bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + border-radius: 16px; + height: 100%; + } + + &__content { + position: relative; + min-height: 280px; + box-sizing: border-box; + padding: 36px 40px; + z-index: 1; + } + + &__button { + position: absolute; + z-index: 2; + top: 44px; + right: 56px; + font-size: 24px; + line-height: 24px; + &-image { + width: 20px; + height: 20px; + display: inline-block; + margin-left: 4px; + } + } + &__title { + font-style: normal; + font-weight: 400; + letter-spacing: 0.02em; + opacity: 0.7; + } + &__money { + font-family: 'Helvetica Neue'; + font-style: normal; + font-weight: 700; + } + &__total { + margin-bottom: 24px; + .partner-kanban { + &__title { + font-size: 26px; + line-height: 40px; + margin-bottom: 12px; + } + &__money { + font-size: 48px; + line-height: 42px; + } + } + } + &__details { + .flex-row(); + &-part { + flex: 1; + + .partner-kanban { + &__title { + font-size: 24px; + line-height: 36px; + margin-bottom: 4px; + } + &__money { + font-size: 32px; + line-height: 42px; + } + } + } + } + &__buttons { + margin-top: 30px; + .flex-row(); + gap: 24px; + } + + &__withdraw { + .button(@height: 72px; @borderRadius: 8px; @highlight: 0); + background: #fff; + flex: 1; + } + + &__record { + .button(@height: 72px; @borderRadius: 8px; @highlight: 1); + background: #b6bef4; + flex: 1; + } + + &-tip-dialog { + &__container { + .flex-column(); + } + + &__title { + font-size: 28px; + line-height: 42px; + color: #000; + } + + &__confirm-button { + .button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px); + margin-top: 40px; + } + } + + &-withdraw-dialog { + &__container { + .flex-column(); + color: @blColor; + } + + &__title { + font-weight: 500; + font-size: 36px; + line-height: 57px; + text-align: center; + margin-bottom: 32px; + } + + &__count { + font-weight: 500; + font-size: 80px; + line-height: 57px; + text-align: center; + margin-bottom: 38px; + + .yuan { + display: inline-block; + margin-left: 8px; + font-size: 28px; + font-weight: 500; + } + } + &__hint { + font-weight: 500; + font-size: 24px; + line-height: 36px; + text-align: center; + color: @blColorG1; + margin-bottom: 40px; + } + &__confirm-button { + .button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px); + margin-bottom: 40px; + } + + &__cancel-button { + width: 204px; + height: 39px; + text-align: center; + font-weight: 400; + font-size: 28px; + line-height: 39px; + color: @blHighlightColor; + } + } +} diff --git a/src/components/partner-kanban/index.tsx b/src/components/partner-kanban/index.tsx new file mode 100644 index 0000000..d8b6ceb --- /dev/null +++ b/src/components/partner-kanban/index.tsx @@ -0,0 +1,148 @@ +import { Button, Image } from '@tarojs/components'; + +import { Dialog } from '@taroify/core'; +import { Question } from '@taroify/icons'; +import { useCallback, useState, useEffect } from 'react'; + +import { PageUrl } from '@/constants/app'; +import { PartnerProfitsState } from '@/types/partner'; +import { formatMoney, getPartnerProfitStat } from '@/utils/partner'; +import { navigateTo } from '@/utils/route'; + +import './index.less'; + +const PREFIX = 'partner-kanban'; + +function TipDialog(props: { open: boolean; onClose: () => void }) { + return ( + + +
+
{`会员支付的收益无需提现,\n支付15日后自动分账至微信零钱`}
+ +
+
+
+ ); +} + +function WithdrawDialog(props: { open: boolean; onClose: () => void; count: number }) { + const handleWithdraw = useCallback(() => {}, []); + return ( + + +
+
本次申请提现金额为
+
+ {props.count} +
+
+
单笔最大500元
+ +
+ 取消 +
+
+
+
+ ); +} + +type PartnerKanbanProps = { + simple?: boolean; +}; +export default function PartnerKanban({ simple }: PartnerKanbanProps) { + const [tipOpen, setTipOpen] = useState(false); + const [withdrawOpen, setWithdrawOpen] = useState(false); + const [stats, setStats] = useState({ + withdraw: 0, + available: 0, + withdrawing: 0, + }); + const total = stats.withdrawing + stats.available + stats.withdraw; + const handleNavigate = useCallback(() => { + navigateTo(PageUrl.Partner); + }, []); + const handleNavigateRecord = useCallback(() => { + navigateTo(PageUrl.WithdrawRecord); + }, []); + const handleViewTip = useCallback(() => { + setTipOpen(true); + }, []); + const handleTipClose = useCallback(() => { + setTipOpen(false); + }, []); + const handleViewWithdraw = useCallback(() => { + setWithdrawOpen(true); + }, []); + const handleWithdrawClose = useCallback(() => { + setWithdrawOpen(false); + }, []); + const getProfitStats = useCallback(async () => { + const data = await getPartnerProfitStat(); + setStats(data); + }, []); + useEffect(() => { + getProfitStats(); + }, []); + return ( +
+ +
+ {simple && ( +
+ 查看详情 + +
+ )} +
+
总收益(元)
+
{formatMoney(total)}
+
+
+
+
可提现(元)
+
{formatMoney(stats.available)}
+
+
+
提现中(元)
+
{formatMoney(stats.withdrawing)}
+
+
+
+ 已提现(元) + {!simple && } +
+
{formatMoney(stats.withdraw)}
+
+
+ {!simple && ( +
+ + +
+ )} +
+ {!simple && } + {!simple && } +
+ ); +} diff --git a/src/components/partner-profit/ProfitList.tsx b/src/components/partner-profit/ProfitList.tsx new file mode 100644 index 0000000..5bc3a67 --- /dev/null +++ b/src/components/partner-profit/ProfitList.tsx @@ -0,0 +1,117 @@ +import { List, PullRefresh } from '@taroify/core'; +import classNames from 'classnames'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import ListPlaceholder from '@/components/list-placeholder'; +import { GetProfitRequest, PartnerProfitItem, ProfitType } from '@/types/partner'; +import { logWithPrefix } from '@/utils/common'; +import { formatMoney, formatTimestamp, getProfitList as requestData } from '@/utils/partner'; + +import './index.less'; +import { PROFIT_STATUS_MAP, PROFIT_TYPE_MAP } from '@/constants/partner'; + +export interface IPartnerProfitListProps extends GetProfitRequest { + visible?: boolean; + refreshDisabled?: boolean; + listHeight?: number; + className?: string; + onListEmpty?: () => void; +} + +const PREFIX = 'partner-profit'; +const log = logWithPrefix(PREFIX); + +function ProfitList(props: IPartnerProfitListProps) { + const { className, listHeight, refreshDisabled, visible = true, profitType, onListEmpty } = props; + const [refreshing, setRefreshing] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [loadMoreError, setLoadMoreError] = useState(false); + const [dataList, setDataList] = useState([]); + const onListEmptyRef = useRef(onListEmpty); + + const handleRefresh = useCallback(async () => { + log('start pull refresh'); + try { + setRefreshing(true); + setLoadMoreError(false); + const list = await requestData({ profitType }); + setDataList(list); + !list.length && onListEmptyRef.current?.(); + log('pull refresh success'); + } catch (e) { + setDataList([]); + setLoadMoreError(true); + log('pull refresh failed'); + } finally { + setRefreshing(false); + } + }, []); + + useEffect(() => { + onListEmptyRef.current = onListEmpty; + }, [onListEmpty]); + + // 初始化数据&配置变更后刷新数据 + useEffect(() => { + // 列表不可见时,先不做处理 + if (!visible) { + log('visible changed, but is not visible, only clear list'); + return; + } + + const refresh = async () => { + log('visible changed, start refresh list data'); + try { + setDataList([]); + setLoadingMore(true); + setLoadMoreError(false); + const list = await requestData({ profitType }); + setDataList(list); + !list.length && onListEmptyRef.current?.(); + } catch (e) { + setDataList([]); + setLoadMoreError(true); + } finally { + log('visible changed, refresh list data end'); + setLoadingMore(false); + } + }; + refresh(); + }, [visible, profitType]); + + return ( +
+ + {}} + loading={loadingMore || refreshing} + disabled={loadMoreError} + fixedHeight={typeof listHeight !== 'undefined'} + style={listHeight ? { height: `${listHeight}px` } : undefined} + > + {dataList.map(item => ( +
+
+
{formatTimestamp(item.created)}
+
{PROFIT_TYPE_MAP[profitType]}
+
+ {profitType === ProfitType.Anchor ? '已结算' : PROFIT_STATUS_MAP[item.status]} +
+
+{formatMoney(item.profit)}
+
+
+ ))} + +
+
+
+ ); +} + +export default ProfitList; diff --git a/src/components/partner-profit/index.less b/src/components/partner-profit/index.less new file mode 100644 index 0000000..754d771 --- /dev/null +++ b/src/components/partner-profit/index.less @@ -0,0 +1,96 @@ +@import '@/styles/variables.less'; +@import '@/styles/common.less'; + +.partner-profit { + height: calc(100vh - 98rpx); + overflow: hidden; + width: 100%; + .flex-column(); + align-items: normal; + + &__top { + padding: 12px 24px; + } + &__main { + position: relative; + flex-grow: 1; + overflow: hidden; + } + + &__tabs { + height: 100%; + .taroify-tabs__content { + height: calc(100% - 98rpx); + } + .taroify-tabs__tab-pane { + height: 100%; + position: relative; + } + } + + &__title { + height: 72px; + width: 100%; + background: #f7f7f7; + padding: 0 32px; + box-sizing: border-box; + line-height: 72px; + font-size: 24px; + color: rgba(0, 0, 0, 0.5); + position: absolute; + top:0; + left:0; + z-index: 1; + .flex-row(); + } + + &__row { + padding: 0 32px; + } + + &__row-content { + border-bottom: 1px solid #e6e7e8; + font-size: 28px; + color: @blColor; + height: 100px; + .flex-row(); + + .income { + font-weight: 600; + font-size: 30px; + color: #ff5051; + } + } + + &__item { + padding: 0 8px; + &:first-child { + padding-left: 0; + } + &:last-child { + padding-right: 0; + } + &.time, + &.project { + flex: 2; + } + &.status { + width: 96px; + padding: 0 8px; + flex-shrink: 0; + } + &.income { + text-align: right; + flex: 1; + } + } + + &__tab-content { + padding-top: 72rpx; + height: 100%; + box-sizing: border-box; + background: #fff; + overflow-y: auto; + overflow-x: hidden; + } +} diff --git a/src/components/partner-profit/index.tsx b/src/components/partner-profit/index.tsx new file mode 100644 index 0000000..1489272 --- /dev/null +++ b/src/components/partner-profit/index.tsx @@ -0,0 +1,47 @@ +import { Tabs } from '@taroify/core'; + +import PartnerKanban from '@/components/partner-kanban'; +import { ProfitType } from '@/types/partner'; + +import ProfitList from './ProfitList'; + +import './index.less'; + +const PREFIX = 'partner-profit'; + +function TableTitle() { + return ( +
+
结算时间
+
项目
+
状态
+
收入(元)
+
+ ); +} + +export default function PartnerProfit() { + return ( +
+
+ +
+
+ + + + + + + + + + + + + + +
+
+ ); +} diff --git a/src/components/slogan/index.less b/src/components/slogan/index.less index 25438a2..403a019 100644 --- a/src/components/slogan/index.less +++ b/src/components/slogan/index.less @@ -30,4 +30,9 @@ &__protocol { margin-top: 20px; } -} \ No newline at end of file + + .page-user--1 ~ & { + position: static; + padding-bottom: 174px; + } +} diff --git a/src/constants/app.ts b/src/constants/app.ts index 5ebf71c..273a7a0 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -72,6 +72,7 @@ export enum PageUrl { ProtocolWebview = 'pages/protocol-webview/index', PrivacyWebview = 'pages/privacy-webview/index', Partner = 'pages/partner/index', + WithdrawRecord = 'pages/withdraw-record/index', } export enum PluginUrl { diff --git a/src/constants/cache-key.ts b/src/constants/cache-key.ts index b0af3c0..3a0563c 100644 --- a/src/constants/cache-key.ts +++ b/src/constants/cache-key.ts @@ -8,5 +8,6 @@ export enum CacheKey { APP_MODE = '__bl_app_mode__', APP_MODE_NEW = '__bl_app_mode_2__', LAST_SELECT_MY_JOB = '__last_select_my_job__', - CLOSE_PARTNER_BANNER = '__last_close_partner_banner__', + CLOSE_PARTNER_BANNER = '__close_partner_banner__', + INVITE_CODE = '__invite_code__', } diff --git a/src/constants/partner.ts b/src/constants/partner.ts new file mode 100644 index 0000000..8d3e65d --- /dev/null +++ b/src/constants/partner.ts @@ -0,0 +1,21 @@ +export enum ProfitType { + Anchor = '1', + Member = '2', + Partner = '3', +} +export enum ProfitStatus { + AVAILABLE = '1', + WITHDRAWING = '2', + WITHDRAW = '3', +} + +export const PROFIT_TYPE_MAP = { + [ProfitType.Anchor]: '主播被开聊', + [ProfitType.Member]: '会员支付', + [ProfitType.Partner]: '合伙人收益分成', +}; +export const PROFIT_STATUS_MAP = { + [ProfitStatus.AVAILABLE]: '可提现', + [ProfitStatus.WITHDRAWING]: '提现中', + [ProfitStatus.WITHDRAW]: '已提现', +}; diff --git a/src/fragments/job/base/index.less b/src/fragments/job/base/index.less index bf7b481..3d6a6df 100644 --- a/src/fragments/job/base/index.less +++ b/src/fragments/job/base/index.less @@ -146,22 +146,3 @@ margin-top: 30px; } } - -.partner-banner { - width: 100%; - height: 90px; - position: relative; - - &__image { - width: 100%; - height: 100%; - } - - &__close { - width: 64px; - height: 38px; - position: absolute; - right: 0; - top: 0; - } -} diff --git a/src/fragments/job/base/index.tsx b/src/fragments/job/base/index.tsx index 9d7c532..82cfbd8 100644 --- a/src/fragments/job/base/index.tsx +++ b/src/fragments/job/base/index.tsx @@ -4,10 +4,11 @@ import { NodesRef, useDidHide } from '@tarojs/taro'; import { Tabs } from '@taroify/core'; import { ArrowUp, ArrowDown } from '@taroify/icons'; import classNames from 'classnames'; -import { useCallback, useEffect, useState, MouseEvent } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import JobList, { IJobListProps } from '@/components/job-list'; import Overlay from '@/components/overlay'; +import PartnerBanner from '@/components/partner-banner'; import SalarySelect from '@/components/salary-select'; import SearchInput from '@/components/search'; import { APP_TAB_BAR_ID, PageUrl } from '@/constants/app'; @@ -18,8 +19,6 @@ import { SalaryRange } from '@/types/job'; import { Coordinate } from '@/types/location'; import { logWithPrefix } from '@/utils/common'; import { navigateTo } from '@/utils/route'; -import { getPartnerBannerClose, setPartnerBannerClose } from '@/utils/partner'; - import './index.less'; interface IProps { @@ -43,18 +42,6 @@ const CALC_LIST_PROPS: IUseListHeightProps = { const log = logWithPrefix(PREFIX); -const PartnerBanner = (props: { onClose: (e: MouseEvent) => void }) => { - const handleClick = useCallback(() => { - navigateTo(PageUrl.Partner); - }, []); - return ( -
- -
-
- ); -}; - const NoGroupTips = (props: { className?: string; height?: number }) => { const { className, height } = props; return ( @@ -93,7 +80,6 @@ function JobFragment(props: IProps) { const [salaryRange, setSalaryRange] = useState(); const [showSalarySelect, setShowSalarySelect] = useState(false); const { latitude, longitude } = coordinate; - const [bannerClose, setBannerClose] = useState(getPartnerBannerClose()); const handleClickSearch = useCallback(() => navigateTo(PageUrl.JobSearch, { city: cityCode }), [cityCode]); @@ -117,13 +103,6 @@ function JobFragment(props: IProps) { [setTabType] ); - const handlePartnerBannerClose = useCallback(e => { - e.preventDefault(); - e.stopPropagation(); - setBannerClose(true); - setPartnerBannerClose(); - }, []); - useDidHide(() => setShowSalarySelect(false)); return ( @@ -153,7 +132,7 @@ function JobFragment(props: IProps) { {JOB_TABS.map(tab => ( - {!bannerClose && } + { @@ -161,6 +165,9 @@ export default function AnchorPage() { }, [location]); useLoad(async () => { + const query = getPageQuery(); + getInviteCodeFromQueryAndUpdate(query); + try { const { jobResults = [] } = await requestJobManageList({ status: JobManageStatus.Open }); if (!jobResults.length) { @@ -178,6 +185,10 @@ export default function AnchorPage() { } }); + useShareAppMessage(() => { + return getCommonShareMessage(true, inviteCode); + }); + useDidShow(() => requestUnreadMessageCount()); return ( @@ -204,6 +215,9 @@ export default function AnchorPage() { {showFilter ? : }
+
+ +
openCustomerServiceChat(group.serviceUrl), []); - useShareAppMessage(() => getCommonShareMessage()); + useLoad(() => { + const query = getPageQuery(); + getInviteCodeFromQueryAndUpdate(query); + }); + + useShareAppMessage(() => getCommonShareMessage(true, inviteCode)); return ( diff --git a/src/pages/job-detail/index.tsx b/src/pages/job-detail/index.tsx index a5fefb8..a939a34 100644 --- a/src/pages/job-detail/index.tsx +++ b/src/pages/job-detail/index.tsx @@ -15,6 +15,7 @@ import { RoleType, EventName, PageUrl } from '@/constants/app'; import { CertificationStatusType } from '@/constants/company'; import { CollectEventName, ReportEventId } from '@/constants/event'; import { EMPLOY_TYPE_TITLE_MAP } from '@/constants/job'; +import useInviteCode from '@/hooks/use-invite-code'; import useUserInfo from '@/hooks/use-user-info'; import useRoleType from '@/hooks/user-role-type'; import { RESPONSE_ERROR_CODE } from '@/http/constant'; @@ -27,6 +28,7 @@ import { getJobTitle, getJobSalary, postPublishJob, requestJobDetail } from '@/u import { calcDistance, isValidLocation } from '@/utils/location'; import { requestProfileDetail } from '@/utils/material'; import { isChatWithSelf, postCreateChat } from '@/utils/message'; +import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner'; import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route'; import { getCommonShareMessage } from '@/utils/share'; import { formatDate } from '@/utils/time'; @@ -181,6 +183,7 @@ export default function JobDetail() { const userInfo = useUserInfo(); const [data, setData] = useState(null); const isOwner = roleType === RoleType.Company && userInfo.userId === data?.userId; + const inviteCode = useInviteCode(); const onDev = useCallback(async () => data && copy(data.id), [data]); @@ -216,7 +219,8 @@ export default function JobDetail() { }, []); useLoad(async () => { - const query = getPageQuery>(); + const query = getPageQuery & { c: string }>(); + getInviteCodeFromQueryAndUpdate(query); const jobId = query?.id; if (!jobId) { return; @@ -232,11 +236,11 @@ export default function JobDetail() { useShareAppMessage(() => { if (!data) { - return getCommonShareMessage(); + return getCommonShareMessage(true, inviteCode); } return { title: getJobTitle(data) || '', - path: getJumpUrl(PageUrl.JobDetail, { id: data.id, share: true }), + path: getJumpUrl(PageUrl.JobDetail, { id: data.id, share: true, c: inviteCode }), }; }); diff --git a/src/pages/job/index.tsx b/src/pages/job/index.tsx index 768ffb9..384c54b 100644 --- a/src/pages/job/index.tsx +++ b/src/pages/job/index.tsx @@ -10,12 +10,14 @@ import MaterialGuide from '@/components/material-guide'; import { EventName, OpenSource, PageUrl } from '@/constants/app'; import { EmployType, JOB_PAGE_TABS, SortType } from '@/constants/job'; import JobFragment from '@/fragments/job/base'; +import useInviteCode from '@/hooks/use-invite-code'; import useLocation from '@/hooks/use-location'; import useNavigation from '@/hooks/use-navigation'; import { Coordinate } from '@/types/location'; import { logWithPrefix } from '@/utils/common'; import { getWxLocation, isNotNeedAuthorizeLocation, requestLocation } from '@/utils/location'; import { requestUnreadMessageCount } from '@/utils/message'; +import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner'; import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route'; import { getCommonShareMessage } from '@/utils/share'; import Toast from '@/utils/toast'; @@ -29,6 +31,7 @@ const log = logWithPrefix(PREFIX); export default function Job() { const location = useLocation(); const { barHeight, statusBarHeight } = useNavigation(); + const inviteCode = useInviteCode(); const [tabType, setTabType] = useState(EmployType.All); const [sortType, setSortType] = useState(SortType.RECOMMEND); const [cityCode, setCityCode] = useState(location.cityCode); @@ -101,11 +104,12 @@ export default function Job() { }, [location]); useLoad(async () => { - const query = getPageQuery<{ sortType: SortType }>(); + const query = getPageQuery<{ sortType: SortType; c?: string; scene?: string }>(); const type = query.sortType; if (type === SortType.CREATE_TIME) { setSortType(type); } + getInviteCodeFromQueryAndUpdate(query); if (await isNotNeedAuthorizeLocation()) { log('not need authorize location'); requestLocation(); @@ -121,10 +125,10 @@ export default function Job() { if (sortType === SortType.CREATE_TIME) { return { title: '这里有今日全城新增通告,快来看看', - path: getJumpUrl(PageUrl.Job, { sortType }), + path: getJumpUrl(PageUrl.Job, { sortType, c: inviteCode }), }; } - return getCommonShareMessage(); + return getCommonShareMessage(true, inviteCode); }); return ( diff --git a/src/pages/material-view/index.tsx b/src/pages/material-view/index.tsx index e1dce7b..442faa3 100644 --- a/src/pages/material-view/index.tsx +++ b/src/pages/material-view/index.tsx @@ -10,6 +10,7 @@ import { EventName, OpenSource, PageUrl } from '@/constants/app'; import { CollectEventName } from '@/constants/event'; import { MaterialViewSource } from '@/constants/material'; import ProfileViewFragment from '@/fragments/profile/view'; +import useInviteCode from '@/hooks/use-invite-code'; import { RESPONSE_ERROR_CODE } from '@/http/constant'; import { HttpError } from '@/http/error'; import { JobManageInfo } from '@/types/job'; @@ -20,9 +21,9 @@ import { collectEvent } from '@/utils/event'; import { requestHasPublishedJob, requestJobDetail } from '@/utils/job'; import { getMaterialShareMessage, requestReadProfile, requestShareProfile } from '@/utils/material'; import { isChatWithSelf, postCreateChat } from '@/utils/message'; +import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner'; import { getPageQuery, navigateBack, navigateTo, redirectTo } from '@/utils/route'; import Toast from '@/utils/toast'; - import './index.less'; const PREFIX = 'page-material-view'; @@ -37,6 +38,7 @@ interface IShareContext { resumeId: string; source: MaterialViewSource.Share; shareCode: string; + c?: string; } const isShareContext = (context: IViewContext | IShareContext): context is IShareContext => { @@ -68,6 +70,7 @@ export default function MaterialViewPage() { const [noTimeDialogVisible, setNoTimeDialogVisible] = useState(false); const [noVipLimitVisible, setNoVipLimitVisible] = useState(false); const [vipExpiredVisible, setVipExpiredVisible] = useState(false); + const inviteCode = useInviteCode(); const onDev = useCallback(async () => profile && copy(profile.userId), [profile]); @@ -139,6 +142,7 @@ export default function MaterialViewPage() { useLoad(async () => { const context = getPageQuery(); + getInviteCodeFromQueryAndUpdate(context as BL.Anything); try { const profileDetail = await requestProfile(context); setProfile(profileDetail); @@ -172,7 +176,7 @@ export default function MaterialViewPage() { }); useShareAppMessage(async () => { - const shareMessage = await getMaterialShareMessage(profile); + const shareMessage = await getMaterialShareMessage(profile, true, inviteCode); return shareMessage as BL.Anything; }); diff --git a/src/pages/partner/index.less b/src/pages/partner/index.less index eee325d..7520aa8 100644 --- a/src/pages/partner/index.less +++ b/src/pages/partner/index.less @@ -6,5 +6,17 @@ --tabs-active-color: @blHighlightColor; --tabs-nav-background-color: #fff; --tabs-wrap-height: 98px; + + > .taroify-tabs__wrap { + position: fixed; + width: 100vw; + top: 0; + left: 0; + z-index: 2; + } + + > .taroify-tabs__content { + padding-top: var(--tabs-wrap-height); + } } } diff --git a/src/pages/partner/index.tsx b/src/pages/partner/index.tsx index 64cb79e..d1e4d7d 100644 --- a/src/pages/partner/index.tsx +++ b/src/pages/partner/index.tsx @@ -1,35 +1,38 @@ import { useShareAppMessage } from '@tarojs/taro'; import { Tabs } from '@taroify/core'; -import { useState } from 'react'; -import PartnerIntro, { PartnerIntroFooter } from '@/components/partner-intro'; +import PartnerIntro from '@/components/partner-intro'; +import PartnerInviteList from '@/components/partner-invite-list'; +import PartnerProfit from '@/components/partner-profit'; +import useInviteCode from '@/hooks/use-invite-code'; import { getCommonShareMessage } from '@/utils/share'; + import './index.less'; const PREFIX = 'partner'; export default function Partner() { - const [tab, setTab] = useState(0); + const inviteCode = useInviteCode(); useShareAppMessage(() => { - return getCommonShareMessage(false); + console.log('Partner inviteCode', inviteCode); + return getCommonShareMessage(false, inviteCode); }); return (
- + - 邀请名单 + - 我的收益 + - {tab === 0 && }
); } diff --git a/src/pages/user/index.less b/src/pages/user/index.less index f8829b5..7cc9570 100644 --- a/src/pages/user/index.less +++ b/src/pages/user/index.less @@ -84,4 +84,4 @@ width: 36px; height: 36px; } -} \ No newline at end of file +} diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index d3a262b..2061971 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -45,7 +45,7 @@ export default function User() { -
+
{ + return getCommonShareMessage(false); + }); + + return
; +} diff --git a/src/statics/png/partner_banner.png b/src/statics/png/partner_banner.png deleted file mode 100644 index a16e598..0000000 Binary files a/src/statics/png/partner_banner.png and /dev/null differ diff --git a/src/statics/png/partner_bg.png b/src/statics/png/partner_bg.png deleted file mode 100644 index 314a9d7..0000000 Binary files a/src/statics/png/partner_bg.png and /dev/null differ diff --git a/src/store/actions/partner.ts b/src/store/actions/partner.ts new file mode 100644 index 0000000..2f43bda --- /dev/null +++ b/src/store/actions/partner.ts @@ -0,0 +1,3 @@ +import { SET_INVITE_CODE } from '../constants'; + +export const setInviteCode = (value: string) => ({ type: SET_INVITE_CODE, value }); diff --git a/src/store/constants.ts b/src/store/constants.ts index 74b9fb2..02411e3 100644 --- a/src/store/constants.ts +++ b/src/store/constants.ts @@ -4,3 +4,4 @@ export const SET_LOCATION_INFO = 'SET_LOCATION_INFO'; export const SET_USER_INFO = 'SET_USER_INFO'; export const SET_BIND_PHONE = 'SET_BIND_PHONE'; export const SET_USER_MESSAGE = 'SET_USER_MESSAGE'; +export const SET_INVITE_CODE = 'SET_INVITE_CODE'; diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index 86e0d01..82d0607 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -2,10 +2,12 @@ import { combineReducers } from 'redux'; import appState from './app'; import message from './message'; +import partnerInfo from './partner'; import userInfo from './user'; export default combineReducers({ appState, userInfo, message, + partnerInfo, }); diff --git a/src/store/reducers/partner.ts b/src/store/reducers/partner.ts new file mode 100644 index 0000000..7828c66 --- /dev/null +++ b/src/store/reducers/partner.ts @@ -0,0 +1,22 @@ +import { Action } from 'redux'; + +import { PartnerInfo } from '@/types/partner'; + +import { SET_INVITE_CODE } from '../constants'; + +const INIT_STATE: Partial = {}; + +const partnerInfo = (state: Partial = INIT_STATE, action: Action): Partial => { + const { type, value } = action as BL.Anything; + switch (type) { + case SET_INVITE_CODE: { + return { + inviteCode: value, + }; + } + default: + return state; + } +}; + +export default partnerInfo; diff --git a/src/store/selector/partner.ts b/src/store/selector/partner.ts new file mode 100644 index 0000000..271bd7d --- /dev/null +++ b/src/store/selector/partner.ts @@ -0,0 +1,5 @@ +import { IState } from '@/types/store'; + +export const selectPartnerInfo = (state: IState) => { + return state.partnerInfo; +}; diff --git a/src/types/partner.ts b/src/types/partner.ts new file mode 100644 index 0000000..1c98508 --- /dev/null +++ b/src/types/partner.ts @@ -0,0 +1,60 @@ +export interface PartnerProfitsState { + withdraw: number; + available: number; + withdrawing: number; +} +export interface PartnerInviteCode { + inviteCode: string; +} +export interface PartnerInfo extends PartnerInviteCode {} +export interface InviteUserInfo { + id: string; // 用户ID,可选 + userId: string; // 用户唯一标识 + nickName: string; // 用户昵称 + bandPhone: boolean; // 是否绑定了手机号,可选 + avatarUrl: string; // 用户头像URL,可选 + isDefaultAvatar: boolean; // 是否使用默认头像,可选 + isDefaultNickname: boolean; // 是否使用默认昵称,可选 + isBindPhone: boolean; // 是否绑定了手机号,可选 + created: string; // 用户创建时间戳,可选 + updated: string; // 用户更新时间戳,可选 + isBoss: boolean; // 是否是老板,可选 + isPartner: boolean; // 是否是合作伙伴,可选 + userBoss: { + level: string; // 上级老板的等级,可选 + expireTime: string; // 上级老板的过期时间,可选 + isExpire: boolean; // 上级老板是否过期,可选 + }; + bossAuthStatus: string; // 老板认证状态,可选 + existAvailableJob: boolean; // 是否存在可用的工作,可选 + isCreateResume: boolean; // 是否创建了简历,可选 + imAcctNo: string; // 即时通讯账号,可选 + phone: string; // 用户手机号,可选 + roleType: string; // 角色类型,可选 +} +export enum ProfitType { + Anchor = '1', + Member = '2', + Partner = '3', +} +export enum ProfitStatus { + AVAILABLE = '1', + WITHDRAWING = '2', + WITHDRAW = '3', +} +export interface GetProfitRequest { + profitType: ProfitType; +} +export interface PartnerProfitItem { + id: number; // 唯一标识 + userId: string; // 用户ID + profit: number; // 利润 + profitType: ProfitType; // 利润类型 + status: ProfitStatus; // 状态 + relatedId: string; // 相关ID + remark: string; // 备注 + created: string; // 创建时间 + updated: string; // 更新时间 + profitTypeEnum: string; // 利润类型枚举 + statusEnum: string; // 状态枚举 +} diff --git a/src/types/store.ts b/src/types/store.ts index 6c243c3..2ee95ae 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -1,5 +1,6 @@ import { RoleType, PageType } from '@/constants/app'; import { UserMessage } from '@/types/message'; +import { PartnerInfo } from '@/types/partner'; import { LocationInfo } from './location'; import { UserInfo } from './user'; @@ -8,6 +9,7 @@ export interface IState { appState: AppState; userInfo: UserInfo; message: UserMessage; + partnerInfo: PartnerInfo; } export interface AppState { diff --git a/src/types/user.ts b/src/types/user.ts index 2aef464..2f10366 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -13,6 +13,7 @@ export interface UserInfo { created?: string; updated?: string; isBoss?: boolean; // 是否通告主 + isPartner?: boolean; userBoss?: { level: string; expireTime: string; diff --git a/src/utils/material.ts b/src/utils/material.ts index 617d0c7..d829fc3 100644 --- a/src/utils/material.ts +++ b/src/utils/material.ts @@ -139,7 +139,11 @@ export const requestAnchorList = async (data: GetAnchorListRequest) => { return http.post(API.GET_ANCHOR_LIST, { data }); }; -export const getMaterialShareMessage = async (profile?: MaterialProfile | null, needShareCode: boolean = true) => { +export const getMaterialShareMessage = async ( + profile?: MaterialProfile | null, + needShareCode: boolean = true, + inviteCode?: string +) => { if (!profile) { return null; } @@ -149,7 +153,12 @@ export const getMaterialShareMessage = async (profile?: MaterialProfile | null, const title = `${name} ${workedSecCategoryStr ? `播过 ${workedSecCategoryStr}` : ''}`.trim(); return { title, - path: getJumpUrl(PageUrl.MaterialView, { shareCode, resumeId: id, source: MaterialViewSource.Share }), + path: getJumpUrl(PageUrl.MaterialView, { + shareCode, + resumeId: id, + source: MaterialViewSource.Share, + c: inviteCode, + }), }; } catch (error: unknown) { const e = error as HttpError; diff --git a/src/utils/partner.ts b/src/utils/partner.ts index 5d4a257..35ca1e2 100644 --- a/src/utils/partner.ts +++ b/src/utils/partner.ts @@ -1,7 +1,110 @@ import Taro from '@tarojs/taro'; import { CacheKey } from '@/constants/cache-key'; +import http from '@/http'; +import { API } from '@/http/api'; +import store from '@/store'; +import { setInviteCode } from '@/store/actions/partner'; +import { + GetProfitRequest, + InviteUserInfo, + PartnerInviteCode, + PartnerProfitItem, + PartnerProfitsState, +} from '@/types/partner'; +import { requestUserInfo } from '@/utils/user'; + +export const getInviteCodeFromQuery = (query: Record): string | undefined => { + if (query) { + if (query.scene) { + return query.scene.replace('c%3D', ''); + } + if (query.c) { + return query.c; + } + } + return undefined; +}; + +export const getInviteCodeFromQueryAndUpdate = (query: Record) => { + const code = getInviteCodeFromQuery(query); + if (!code) { + return; + } + console.log('get code', code); + requestUserInfo(code); +}; export const getPartnerBannerClose = (): boolean => Taro.getStorageSync(CacheKey.CLOSE_PARTNER_BANNER); export const setPartnerBannerClose = () => Taro.setStorageSync(CacheKey.CLOSE_PARTNER_BANNER, true); + +export const becomePartner = async () => { + await http.post(API.BECOME_PARTNER); + await getInviteCode(); +}; +export const getPartnerQrcode = async () => { + return await http.post(API.PARTNER_QRCODE, { + responseType: 'arraybuffer', + }); +}; +export const getPartnerProfitStat = async () => { + return await http.post(API.GET_PROFIT_STAT); +}; +export const getPartnerInviteList = async () => { + return await http.post(API.GET_INVITE_LIST); +}; +export const dispatchUpdateInviteCode = (code: string) => store.dispatch(setInviteCode(code)); +export const getInviteCode = async () => { + const { inviteCode } = await http.post(API.GET_INVITE_CODE); + dispatchUpdateInviteCode(inviteCode); + return inviteCode; +}; +export const getProfitList = async (data: GetProfitRequest) => { + return await http.post(API.GET_PROFIT_LIST, { data }); +}; +export const formatMoney = (cents: number) => { + if (!cents) { + return '0'; + } + const yuan = cents / 100; + return yuan.toFixed(2); +}; +export function formatTimestamp(timestamp: string): string { + // 将字符串时间戳转换为数字类型 + const time = Number(timestamp); + + // 创建 Date 对象 + const date = new Date(time); + + // 获取年、月、日、时、分、秒 + const YYYY = date.getFullYear(); + const MM = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1 + const DD = String(date.getDate()).padStart(2, '0'); + const HH = String(date.getHours()).padStart(2, '0'); + const mm = String(date.getMinutes()).padStart(2, '0'); + const ss = String(date.getSeconds()).padStart(2, '0'); + + // 拼接成所需的格式 + return `${YYYY}.${MM}.${DD} ${HH}:${mm}:${ss}`; +} + +export function formatUserId(input: string): string { + const length = input.length; + + // 如果长度小于8,隐藏最后4位 + if (length < 8) { + const beforeMask = input.slice(0, length - 4); // 前部分 + const maskedPart = '****'; // 替换最后4位为星号 + return beforeMask + maskedPart; + } + + // 如果长度大于或等于8,隐藏倒数第五位到倒数第二位 + const start = length - 8; // 倒数第八个字符的索引 + const beforeMask = input.slice(0, start); // 前部分 + const maskedPart = '****'; // 替换倒数第五位到倒数第二位为星号 + const afterMask = input.slice(start + 4); // 后部分 + + // 拼接结果 + return beforeMask + maskedPart + afterMask; +} diff --git a/src/utils/product.ts b/src/utils/product.ts index 5885e57..e731531 100644 --- a/src/utils/product.ts +++ b/src/utils/product.ts @@ -75,7 +75,7 @@ export async function requestAllBuyProduct(productCode: ProductType) { export async function requestCsQrCode(_type: QrCodeType) { const result = await http.post(API.CS_QR_CODE); - return `${DOMAIN}/${result.vxQrCode}`; + return result.vxQrCode; } export async function requestCreatePayInfo(params: Omit) { diff --git a/src/utils/share.ts b/src/utils/share.ts index d182717..eff7582 100644 --- a/src/utils/share.ts +++ b/src/utils/share.ts @@ -15,10 +15,11 @@ const getRandomCount = () => { return (seed % 300) + 500; }; -export const getCommonShareMessage = (useCapture: boolean = true): ShareAppMessageReturn => { +export const getCommonShareMessage = (useCapture: boolean = true, inviteCode?: string): ShareAppMessageReturn => { + console.log('share share message', getJumpUrl(PageUrl.Job, inviteCode ? { c: inviteCode } : undefined)); return { title: `昨天新增了${getRandomCount()}条主播通告,宝子快来看看`, - path: getJumpUrl(PageUrl.Job), + path: getJumpUrl(PageUrl.Job, inviteCode ? { c: inviteCode } : undefined), imageUrl: useCapture ? undefined : imageUrl, }; }; diff --git a/src/utils/user.ts b/src/utils/user.ts index b450ae9..98cc251 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -33,6 +33,8 @@ export const isValidUserInfo = (info: UserInfo) => !!info.userId; export const isNeedLogin = (info: UserInfo) => !info.isBindPhone; // export const isNeedLogin = (info: UserInfo) => !info.isBindPhone || info.userId === '534740874077898752'; +export const isNeedPhone = (info: UserInfo) => isNeedLogin(info) || !info.phone; + export const updateLastLoginTime = () => { lastOpenMiniProgramTime = Taro.getStorageSync(CacheKey.LAST_OPEN_MINI_PROGRAM_TIME) ?? null; const now = Date.now(); @@ -98,8 +100,8 @@ export const ensureUserInfo = async (info: UserInfo, toast = true) => { export const dispatchUpdateUser = (userInfo: Partial) => store.dispatch(setUserInfo(userInfo)); -export async function requestUserInfo() { - const userInfo = await http.post(API.USER); +export async function requestUserInfo(inviteCode?: string) { + const userInfo = await http.post(API.USER, { data: inviteCode ? { inviteCode } : {} }); dispatchUpdateUser(userInfo); return userInfo; }