diff --git a/src/app.tsx b/src/app.tsx index da271d6..ad02ef3 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -15,6 +15,29 @@ import { requestUserInfo, updateLastLoginTime } from '@/utils/user'; import './app.less'; function App({ children }: PropsWithChildren) { + const checkBasicUpdate = () => { + if (process.env.TARO_ENV !== 'weapp' || !Taro.canIUse('getUpdateManager')) { + return; + } + const manager = Taro.getUpdateManager(); + manager.onCheckForUpdate(res => { + console.log('onCheckForUpdate', res); + }); + + // 新版本下载完成 + manager.onUpdateReady(() => { + Taro.showModal({ + title: '更新提示', + content: '新版本已经准备好,是否重启应用?', + success: function (res) { + if (res.confirm) { + // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 + manager.applyUpdate(); + } + }, + }); + }); + }; useLaunch(async ({ query }) => { console.log('App launched.'); await http.init(getInviteCodeFromQuery(query)); @@ -30,6 +53,7 @@ function App({ children }: PropsWithChildren) { }); useDidShow(options => { + checkBasicUpdate(); requestCityConfigs(); console.log(options); diff --git a/src/components/invite-operations-banner/index.less b/src/components/invite-operations-banner/index.less new file mode 100644 index 0000000..b98e1b1 --- /dev/null +++ b/src/components/invite-operations-banner/index.less @@ -0,0 +1,22 @@ +@import '@/styles/variables.less'; +@import '@/styles/common.less'; + +.invite-operations-fragment-banner { + width: 100%; + height: 90px; + position: relative; + margin-bottom: 24px; + + &__image { + width: 100%; + height: 100%; + } + + &__close { + width: 64px; + height: 38px; + position: absolute; + right: 0; + top: 0; + } +} diff --git a/src/components/invite-operations-banner/index.tsx b/src/components/invite-operations-banner/index.tsx new file mode 100644 index 0000000..cbcee24 --- /dev/null +++ b/src/components/invite-operations-banner/index.tsx @@ -0,0 +1,40 @@ +import { Image } from '@tarojs/components'; + +import { useCallback, useState } from 'react'; + +import { PageUrl } from '@/constants/app'; +import { getPartnerBannerClose, setPartnerBannerClose } from '@/utils/partner'; +import { navigateTo } from '@/utils/route'; +import './index.less'; + +const PREFIX = 'invite-operations-fragment-banner'; + +export default function InviteOperationsBanner() { + const [bannerClose, setBannerClose] = useState(getPartnerBannerClose()); + + const handlePartnerBannerClose = useCallback(e => { + e.preventDefault(); + e.stopPropagation(); + setBannerClose(true); + setPartnerBannerClose(); + }, []); + + const handleClick = useCallback(async () => { + navigateTo(PageUrl.InviteOperations); + }, []); + + if (bannerClose) { + return null; + } + + return ( +
+ +
+
+ ); +} diff --git a/src/components/login-button/index.tsx b/src/components/login-button/index.tsx index 9ad38de..b5c1697 100644 --- a/src/components/login-button/index.tsx +++ b/src/components/login-button/index.tsx @@ -6,7 +6,7 @@ import { useCallback, useState } from 'react'; import { AgreementPopup } from '@/components/agreement-popup'; import LoginDialog from '@/components/login-dialog'; import useUserInfo from '@/hooks/use-user-info'; -import { getAgreementSigned, isNeedLogin, setAgreementSigned } from '@/utils/user'; +import { getAgreementSigned, isNeedLogin, requestUserInfo, setAgreementSigned } from '@/utils/user'; import './index.less'; @@ -18,13 +18,15 @@ export enum BindPhoneStatus { export interface ILoginButtonProps extends ButtonProps { needPhone?: boolean; + needRefresh?: boolean; + onRefresh?: (() => void) | (() => Promise); needAssignment?: boolean; } const PREFIX = 'login-button'; function LoginButton(props: ILoginButtonProps) { - const { className, children, needPhone, onClick, ...otherProps } = props; + const { className, children, needPhone, onRefresh, onClick, needRefresh, ...otherProps } = props; const userInfo = useUserInfo(); const [loginVisible, setLoginVisible] = useState(false); const [assignVisible, setAssignVisible] = useState(false); @@ -68,11 +70,15 @@ function LoginButton(props: ILoginButtonProps) { }, []); const handleLoginSuccess = useCallback( - e => { + async e => { setLoginVisible(false); + if (needRefresh) { + requestUserInfo(); + onRefresh && (await onRefresh()); + } onClick?.(e); }, - [onClick] + [needRefresh, onClick] ); return ( diff --git a/src/components/partner-intro/index.tsx b/src/components/partner-intro/index.tsx index 7aa4a55..3af3ceb 100644 --- a/src/components/partner-intro/index.tsx +++ b/src/components/partner-intro/index.tsx @@ -146,25 +146,6 @@ export default function PartnerIntro() {
-
-
- 群主认证 -
- - 强烈推荐 -
-
-
-
-
- 完成群主认证后,群成员通过该群访问任何人分享的播络小程序进行注册,你都能获得分成 -
-
- -
-
3重收益
@@ -194,6 +175,25 @@ export default function PartnerIntro() {
+
+
+ 群主认证 +
+ + 强烈推荐 +
+
+
+
+
+ 完成群主认证后,群成员通过该群访问任何人分享的播络小程序进行注册,你都能获得分成 +
+
+ +
+
交流群
diff --git a/src/components/prejob-popup/index.tsx b/src/components/prejob-popup/index.tsx index fbbb6e2..587a810 100644 --- a/src/components/prejob-popup/index.tsx +++ b/src/components/prejob-popup/index.tsx @@ -1,18 +1,20 @@ import { Button, Image } from '@tarojs/components'; +import Taro from '@tarojs/taro'; -import { Popup, Dialog } from '@taroify/core'; +import { Dialog, Popup } from '@taroify/core'; import { Fragment, useCallback, useState } from 'react'; import JobBuy from '@/components/product-dialog/steps-ui/job-buy'; import SafeBottomPadding from '@/components/safe-bottom-padding'; import { PageUrl } from '@/constants/app'; +import { CacheKey } from '@/constants/cache-key'; import { GET_CONTACT_TYPE } from '@/constants/job'; -import { navigateTo } from '@/utils/route'; - +import { navigateTo, switchTab } from '@/utils/route'; import './index.less'; interface IProps { onCancel: () => void; + isCreateResume?: boolean; onConfirm: (type: GET_CONTACT_TYPE) => void; } @@ -30,23 +32,48 @@ const GET_CONTACT_TYPE_OPTIONS = [ type: GET_CONTACT_TYPE.VIP, icon: 'https://publiccdn.neighbourhood.com.cn/img/diamond.svg', title: '播络会员', - desc: '开通会员每天可查看10个', + desc: '开通会员每天免费查看', btnText: '开通', }, + // { + // type: GET_CONTACT_TYPE.GROUP, + // icon: 'https://publiccdn.neighbourhood.com.cn/img/group-avatar.png', + // title: '进群领会员(免费报单)', + // desc: '群内定期发放会员,免费报单', + // btnText: '进群', + // }, + { + type: GET_CONTACT_TYPE.INVITE, + icon: 'https://publiccdn.neighbourhood.com.cn/img/invite-operations.png', + title: '邀请运营进群(送会员)', + desc: '每邀请进一个主播群送一个日会员', + btnText: '邀请', + }, ]; -export function PrejobPopup({ onCancel, onConfirm }: IProps) { +export function PrejobPopup({ onCancel, isCreateResume, onConfirm }: IProps) { const [openPopup, setOpenPopup] = useState(true); const [openDialog, setOpenDialog] = useState(false); + const [clicked, setClicked] = useState(!!Taro.getStorageSync(CacheKey.JOIN_GROUP_POPUP_CLICKED)); const handleClick = (type: GET_CONTACT_TYPE) => () => { if (type === GET_CONTACT_TYPE.MATERIAL) { navigateTo(PageUrl.MaterialUploadVideo); onConfirm(type); } + if (type === GET_CONTACT_TYPE.INVITE) { + navigateTo(PageUrl.InviteOperations); + onConfirm(type); + } if (type === GET_CONTACT_TYPE.VIP) { setOpenPopup(false); setOpenDialog(true); } + if (type === GET_CONTACT_TYPE.GROUP) { + Taro.setStorageSync(CacheKey.JOIN_GROUP_POPUP_CLICKED, true); + setClicked(true); + switchTab(PageUrl.GroupV2); + onConfirm(type); + } }; const handleAfterBuy = useCallback(async () => { onConfirm(GET_CONTACT_TYPE.VIP); @@ -58,6 +85,9 @@ export function PrejobPopup({ onCancel, onConfirm }: IProps) {
以下方式任选其一均可获取联系方式
{GET_CONTACT_TYPE_OPTIONS.map(option => { + if (clicked && option.type === GET_CONTACT_TYPE.GROUP) { + return; + } return (
@@ -81,7 +111,7 @@ export function PrejobPopup({ onCancel, onConfirm }: IProps) { - + diff --git a/src/components/product-dialog/index.less b/src/components/product-dialog/index.less index 73e41cd..9f068c2 100644 --- a/src/components/product-dialog/index.less +++ b/src/components/product-dialog/index.less @@ -2,7 +2,6 @@ @import '@/styles/variables.less'; .product-dialog { - .layout() { display: flex; flex-direction: column; @@ -31,7 +30,7 @@ font-weight: 500; line-height: 72px; border-radius: 44px; - color: #FFFFFF; + color: #ffffff; background: @blHighlightColor; margin-top: 40px; } @@ -42,7 +41,7 @@ font-size: 24px; line-height: 48px; color: @blHighlightColor; - background: #6D3DF514; + background: #6d3df514; border-radius: 8px; padding: 32px 72px; margin-top: 40px; @@ -73,7 +72,7 @@ &__describe { .describe-font(); - margin-top: 24px + margin-top: 24px; } &__content { @@ -173,8 +172,8 @@ } &.disabled { - border-color: #E0E0E0; - background: #F7F7F7; + border-color: #e0e0e0; + background: #f7f7f7; } &.selected { @@ -237,9 +236,7 @@ color: @blHighlightColor; display: inline; } - } - } // ============================================= 群 ================================================= // @@ -323,7 +320,7 @@ line-height: 36px; text-align: center; border-radius: 50%; - border: 2px solid #E0E0E0; + border: 2px solid #e0e0e0; } &__qr-code__step-title { @@ -338,7 +335,7 @@ position: relative; width: 2px; height: 40px; - background: #E0E0E0; + background: #e0e0e0; margin: 4px 0; margin-left: 18px; } @@ -365,7 +362,7 @@ .divider { width: 540px; height: 1px; - background: #E0E0E0; + background: #e0e0e0; margin: 40px 0; } @@ -417,7 +414,7 @@ margin-top: 40px; .highlight { - color: @blHighlightColor + color: @blHighlightColor; } } @@ -523,7 +520,6 @@ .button(); } - // ============================================= 发布通告的企业会员 ================================================= // &__publish-job-buy__header { .header-font(); @@ -576,21 +572,21 @@ &__item { position: relative; - width: 230px; + width: 182px; height: 156px; .flex-column(); justify-content: center; border: 2px solid @blHighlightColor; border-radius: 8px; - margin-right: 24px; + margin-right: 15px; &:last-child { margin-right: 0; } &.disabled { - border-color: #E0E0E0; - background: #F7F7F7; + border-color: #e0e0e0; + background: #f7f7f7; } &.selected { @@ -635,11 +631,11 @@ } &__left-line { - background: linear-gradient(270deg, #E0E0E0 0%, #FFFFFF 100%); + background: linear-gradient(270deg, #e0e0e0 0%, #ffffff 100%); } &__right-line { - background: linear-gradient(90deg, #E0E0E0 0%, #FFFFFF 100%); + background: linear-gradient(90deg, #e0e0e0 0%, #ffffff 100%); } &__title { @@ -680,5 +676,4 @@ margin-top: 32px; } } - } diff --git a/src/components/product-dialog/job-contact/index.tsx b/src/components/product-dialog/job-contact/index.tsx new file mode 100644 index 0000000..b7b9bff --- /dev/null +++ b/src/components/product-dialog/job-contact/index.tsx @@ -0,0 +1,175 @@ +import Taro from '@tarojs/taro'; + +import { Dialog } from '@taroify/core'; +import { useCallback, useEffect, useState } from 'react'; + +import { DialogStatus, PREFIX } from '@/components/product-dialog/const'; +import JobBuy from '@/components/product-dialog/steps-ui/job-buy'; +import ContactCustomerService from '@/components/product-dialog/steps-ui/job-contact-customer'; +import ContactDirect from '@/components/product-dialog/steps-ui/job-contact-direct'; +import UnableUnlockContent from '@/components/product-dialog/steps-ui/job-unable'; +import { DeclarationType, ProductType } from '@/constants/product'; +import { JobDetails } from '@/types/job'; +import { GetProductIsUnlockResponse, ProductInfo } from '@/types/product'; +import { logWithPrefix } from '@/utils/common'; +import { requestUseProduct } from '@/utils/product'; +import Toast from '@/utils/toast'; + +import '../index.less'; + +interface IProps { + data: JobDetails; + /** Product use record from parent - if exists, user has already unlocked this job */ + productRecord?: GetProductIsUnlockResponse | null; + /** Product balance info from parent */ + productInfo?: ProductInfo; + /** Callback to refresh product balance in parent after purchase */ + onRefreshBalance?: () => Promise; + onClose: () => void; +} + +const PRODUCT_CODE = ProductType.VIP; +const log = logWithPrefix('product-job-contact-dialog'); + +/** + * Integrated dialog component for job contact flow + * Handles: balance check -> buy if needed -> use product -> show contact info + * + * @param productRecord - Pass from parent to avoid duplicate API calls + * @param productInfo - Pass from parent to avoid duplicate API calls + * @param onRefreshBalance - Callback to refresh balance in parent after purchase + */ +function ProductJobContactDialog(props: IProps) { + const { data, productRecord, productInfo, onRefreshBalance, onClose } = props; + const [status, setStatus] = useState(DialogStatus.LOADING); + const [publisherAcctNo, setPublisherAcctNo] = useState(''); + + /** + * Handle contact display based on declaration type + */ + const showContactResult = useCallback((declarationTypeResult?: ProductInfo['declarationTypeResult']) => { + if (declarationTypeResult?.type === DeclarationType.Direct && declarationTypeResult.publisherAcctNo) { + log('show JOB_CONTACT_DIRECT', declarationTypeResult.publisherAcctNo); + setPublisherAcctNo(declarationTypeResult.publisherAcctNo); + setStatus(DialogStatus.JOB_CONTACT_DIRECT); + } else { + log('show JOB_CONTACT_CS'); + setStatus(DialogStatus.JOB_CONTACT_CS); + } + }, []); + + /** + * Use product and show contact info + */ + const consumeProductAndShowContact = useCallback(async () => { + const productResult = await requestUseProduct(PRODUCT_CODE, { jobId: data.id }); + log('consumeProductAndShowContact result', productResult); + // Refresh balance in parent after consuming product + await onRefreshBalance?.(); + showContactResult(productResult?.declarationTypeResult); + }, [data.id, showContactResult, onRefreshBalance]); + + /** + * Callback after successful purchase + * Refresh balance via parent callback and use product to show contact info + */ + const handleAfterBuy = useCallback(async () => { + log('handleAfterBuy - start'); + try { + Taro.showLoading({ mask: true, title: '加载中...' }); + + // Refresh balance via parent callback + await onRefreshBalance?.(); + + // Use product and show contact info after purchase + await consumeProductAndShowContact(); + } catch (e) { + log('handleAfterBuy error', e); + Toast.error('出错了,请重试'); + onClose(); + } finally { + Taro.hideLoading(); + } + }, [consumeProductAndShowContact, onRefreshBalance, onClose]); + + const handleReport = useCallback(() => { + log('report', data.id); + }, [data.id]); + + /** + * Initialize dialog on mount + */ + useEffect(() => { + let isMounted = true; + + const init = async () => { + try { + Taro.showLoading({ mask: true, title: '加载中...' }); + + log('init with productRecord', productRecord); + log('init with productInfo', productInfo); + + // Step 1: Already unlocked - show contact directly + if (productRecord) { + log('show JOB_CONTACT_DIRECT from productRecord', productRecord.declarationTypeResult); + showContactResult(productRecord.declarationTypeResult); + return; + } + + // Step 2: No productInfo - error state + if (!productInfo) { + log('no productInfo provided, closing'); + Toast.error('出错了,请重试'); + onClose(); + return; + } + + // Step 3: Determine status based on balance + if (!productInfo.isPaidVip && !productInfo.freeBalance) { + log('show JOB_BUY'); + if (isMounted) setStatus(DialogStatus.JOB_BUY); + } else if (!productInfo.balance) { + log('show JOB_UNABLE_UNLOCK'); + if (isMounted) setStatus(DialogStatus.JOB_UNABLE_UNLOCK); + } else { + await consumeProductAndShowContact(); + } + } catch (e) { + log('init error', e); + Toast.error('出错了,请重试'); + onClose(); + } finally { + Taro.hideLoading(); + } + }; + + init(); + + return () => { + isMounted = false; + }; + // Only run on mount - props are captured at dialog open time + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (status === DialogStatus.LOADING) { + return null; + } + + return ( + + + {status === DialogStatus.JOB_CONTACT_CS && } + {status === DialogStatus.JOB_CONTACT_DIRECT && ( + + )} + {status === DialogStatus.JOB_BUY && ( + + )} + {status === DialogStatus.JOB_UNABLE_UNLOCK && } + + + ); +} + +export default ProductJobContactDialog; diff --git a/src/components/product-dialog/steps-ui/company-publish-job-buy.tsx b/src/components/product-dialog/steps-ui/company-publish-job-buy.tsx index daddf71..7cc20d5 100644 --- a/src/components/product-dialog/steps-ui/company-publish-job-buy.tsx +++ b/src/components/product-dialog/steps-ui/company-publish-job-buy.tsx @@ -2,16 +2,23 @@ import Taro from '@tarojs/taro'; import { Button } from '@taroify/core'; import classNames from 'classnames'; -import { useCallback, useEffect, useState } from 'react'; +import { Fragment, useCallback, useEffect, useState } from 'react'; import Badge from '@/components/badge'; import { PREFIX } from '@/components/product-dialog/const'; import { CollectEventName, ReportEventId } from '@/constants/event'; import { OrderStatus, OrderType, ProductSpecId, ProductType } from '@/constants/product'; import { SubscribeTempId } from '@/constants/subscribe'; +import { ProductSpecResult } from '@/types/product'; import { logWithPrefix } from '@/utils/common'; import { collectEvent, reportEvent } from '@/utils/event'; -import { getOrderPrice, isCancelPay, requestCreatePayInfo, requestOrderInfo, requestPayment } from '@/utils/product'; +import { + isCancelPay, + requestCreatePayInfo, + requestOrderInfo, + requestPayment, + requestProductTypeList, +} from '@/utils/product'; import { postSubscribe, subscribeMessage } from '@/utils/subscribe'; import Toast from '@/utils/toast'; @@ -82,22 +89,35 @@ const subscribe = async () => { postSubscribe(TEMP_IDS, successIds); }; +const getJsonContent = (jsonString: string) => { + try { + return JSON.parse(jsonString); + } catch (e) { + return []; + } +}; + export default function CompanyPublishJobBuy(props: IProps) { const { onNext } = props; - const [selectItem, setSelectItem] = useState(LIST[0]); + const [productList, setProductList] = useState([]); + const [selectItem, setSelectItem] = useState(); - const handleClickItem = useCallback((newSelectItem: Item) => setSelectItem(newSelectItem), []); + const handleClickItem = useCallback((newSelectItem: ProductSpecResult) => setSelectItem(newSelectItem), []); const handleBuy = useCallback(async () => { log('handleBuy'); reportEvent(ReportEventId.CLICK_PAY_PUBLISH_JOB); + if (!selectItem) { + Toast.error('请选择购买的产品'); + return; + } try { Taro.showLoading(); const { payOrderNo, createPayInfo } = await requestCreatePayInfo({ type: OrderType.CompanyVip, - amt: getOrderPrice(selectItem.amt), + amt: selectItem.payPrice, productCode: ProductType.BOSS_VIP_NEW, - productSpecId: selectItem.id, + productSpecId: selectItem.productSpecId, }); log('handleBuy payInfo', payOrderNo, createPayInfo); await requestPayment({ @@ -121,8 +141,14 @@ export default function CompanyPublishJobBuy(props: IProps) { log('handleBuy error', e); } }, [selectItem, onNext]); + const handleGetProductInfo = useCallback(async () => { + const result = await requestProductTypeList(ProductType.BOSS_VIP_NEW); + setProductList(result); + setSelectItem(result[0]); + }, []); useEffect(() => { + handleGetProductInfo(); collectEvent(CollectEventName.CREATE_ORDER_VIEW, { orderType: OrderType.BossVip, source: 'user-page' }); }, []); @@ -130,19 +156,21 @@ export default function CompanyPublishJobBuy(props: IProps) {
发认证通告限时折扣
- {LIST.map(item => ( + {productList.map(item => (
handleClickItem(item)} + onClick={item.payPrice === 0 ? undefined : () => handleClickItem(item)} > -
+
{item.title}
-
{item.price}
+
{item.priceText}
{item.badge && }
))} @@ -152,20 +180,24 @@ export default function CompanyPublishJobBuy(props: IProps) {
包含
-
- {selectItem.contents.map(i => ( -
- {i.content} - {i.inlineHighlight &&
{i.inlineHighlight}
} + {selectItem && ( + +
+ {getJsonContent(selectItem.contentsJson).map(i => ( +
+ {i.content} + {i.inlineHighlight &&
{i.inlineHighlight}
} +
+ ))}
- ))} -
- + + + )}
); } diff --git a/src/components/product-dialog/steps-ui/job-buy.tsx b/src/components/product-dialog/steps-ui/job-buy.tsx index 301103c..77dfd2c 100644 --- a/src/components/product-dialog/steps-ui/job-buy.tsx +++ b/src/components/product-dialog/steps-ui/job-buy.tsx @@ -2,55 +2,64 @@ import Taro from '@tarojs/taro'; import { Button } from '@taroify/core'; import classNames from 'classnames'; -import { useCallback, useEffect, useState } from 'react'; +import { Fragment, useCallback, useEffect, useState } from 'react'; import Badge from '@/components/badge'; import { PREFIX } from '@/components/product-dialog/const'; +import { PageUrl } from '@/constants/app'; import { CollectEventName } from '@/constants/event'; -import { OrderStatus, OrderType, ProductSpecId, ProductType } from '@/constants/product'; +import { OrderStatus, OrderType, ProductType } from '@/constants/product'; import { SubscribeTempId } from '@/constants/subscribe'; +import { ProductSpecResult } from '@/types/product'; import { logWithPrefix } from '@/utils/common'; import { collectEvent } from '@/utils/event'; -import { getOrderPrice, isCancelPay, requestCreatePayInfo, requestOrderInfo, requestPayment } from '@/utils/product'; +import { + isCancelPay, + requestCreatePayInfo, + requestOrderInfo, + requestPayment, + requestProductTypeList, +} from '@/utils/product'; +import { navigateTo } from '@/utils/route'; import { postSubscribe, subscribeMessage } from '@/utils/subscribe'; import Toast from '@/utils/toast'; interface IProps { - buyOnly?: boolean; onConfirm: () => void; + isCreateResume?: boolean; } -interface Item { - id: ProductSpecId; - title: string; - content: string; - buyOnlyContent?: string; - price: string; - amt: number; - badge?: string; -} +// interface Item { +// id: ProductSpecId; +// title: string; +// content: string; +// buyOnlyContent?: string; +// price: string; +// amt: number; +// badge?: string; +// } -const LIST: Item[] = [ - { id: ProductSpecId.WeeklyVIP, title: '非会员', content: '每日2次', price: '免费', amt: 0 }, - { - id: ProductSpecId.DailyVIP, - title: '日会员', - content: '每日+10次', - buyOnlyContent: '每日12次', - price: '60播豆', - amt: 6, - badge: '限时体验', - }, - { - id: ProductSpecId.WeeklyVIP, - title: '周会员', - content: '每日+10次', - buyOnlyContent: '每日12次', - price: '180播豆', - amt: 18, - badge: ' 超值', - }, -]; +// const LIST: Item[] = [ +// { id: ProductSpecId.WeeklyVIP, title: '非会员', content: '每日2次', price: '免费', amt: 0 }, +// { +// id: ProductSpecId.DailyVIP, +// title: '日会员', +// content: '每日+10次', +// buyOnlyContent: '每日12次', +// price: '60播豆', +// amt: 6, +// badge: '限时体验', +// }, +// { +// id: ProductSpecId.WeeklyVIP, +// title: '周会员', +// content: '每日+10次', +// buyOnlyContent: '每日12次', +// price: '180播豆', +// amt: 18, +// badge: ' 超值', +// }, +// ]; const log = logWithPrefix('job-buy'); const SUBSCRIBE_ID = SubscribeTempId.SUBSCRIBE_VIP; @@ -66,20 +75,26 @@ const subscribe = async () => { }; export default function JobBuy(props: IProps) { - const { onConfirm, buyOnly } = props; - const [selectItem, setSelectItem] = useState(LIST[1]); + const { onConfirm, isCreateResume } = props; + const [productList, setProductList] = useState([]); + const [selectItem, setSelectItem] = useState(); - const handleClickItem = useCallback((newSelectItem: Item) => setSelectItem(newSelectItem), []); + const handleClickItem = useCallback((newSelectItem: ProductSpecResult) => setSelectItem(newSelectItem), []); const handleBuy = useCallback(async () => { log('handleBuy', selectItem); + if (!selectItem) { + Toast.error('请选择购买的产品'); + return; + } try { Taro.showLoading(); + const { payOrderNo, createPayInfo } = await requestCreatePayInfo({ type: OrderType.VIP, - amt: getOrderPrice(selectItem.amt), + amt: selectItem.payPrice, productCode: ProductType.VIP, - productSpecId: selectItem.id, + productSpecId: selectItem.productSpecId, }); log('handleBuy payInfo', payOrderNo, createPayInfo); await requestPayment({ @@ -104,59 +119,81 @@ export default function JobBuy(props: IProps) { } }, [selectItem, onConfirm]); + const handleGetProductInfo = useCallback(async () => { + const result = await requestProductTypeList(ProductType.VIP); + setProductList(result); + setSelectItem(result[0]); + }, []); + + const handleResume = useCallback(() => { + navigateTo(PageUrl.MaterialUploadVideo); + }, []); + useEffect(() => { + handleGetProductInfo(); collectEvent(CollectEventName.CREATE_ORDER_VIEW, { orderType: OrderType.VIP }); }, []); return (
- {buyOnly ? ( -
开通播络会员即可直接查看联系方式
+ {isCreateResume ? ( + +
+
今日免费查看次数
+
已用完
+
+
+
+
明日
+
再来
+
+
升级会员
+
+
) : ( -
-
今日通告对接次数
-
已用完
-
- )} - {buyOnly ? ( -
每天可获取12个联系方式
- ) : ( -
-
-
明日
-
再来 或
-
升级会员
-
+ +
+
开通会员即可查看联系方式
+
+
+
完善模卡,每日可免费查看
+
+ 去完善 +
+
+
)}
- {LIST.map(item => { - if (buyOnly && !item.amt) { - return null; - } + {productList.map(item => { return (
handleClickItem(item)} + onClick={item.payPrice === 0 ? undefined : () => handleClickItem(item)} > -
+
{item.title}
-
{buyOnly ? item.buyOnlyContent : item.content}
-
{item.price}
+
{item.contentSingle}
+
{item.showPrice}元
{item.badge && }
); })}
-
- 注:日会员有效期为
支付后24小时
-
- {/*
{`已选:${selectItem.title},含进本地群服务`}
*/}
diff --git a/src/components/product-dialog/steps-ui/job-contact-direct.tsx b/src/components/product-dialog/steps-ui/job-contact-direct.tsx index 57b3231..e997753 100644 --- a/src/components/product-dialog/steps-ui/job-contact-direct.tsx +++ b/src/components/product-dialog/steps-ui/job-contact-direct.tsx @@ -10,7 +10,7 @@ interface IContactDirectProps { } export default function ContactDirect(props: IContactDirectProps) { - const { publisherAcctNo, onAfterConfirm, onReport } = props; + const { publisherAcctNo, onAfterConfirm } = props; const handleCopyAndContact = async () => { await copy(publisherAcctNo); diff --git a/src/constants/app.ts b/src/constants/app.ts index 03530fc..d9601e4 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -17,6 +17,7 @@ export enum EventName { ADD_GROUP = 'add_group', SELECT_CITY = 'select_city', CREATE_PROFILE = 'create_profile', + READ_CONTACT = 'read_contact', UPDATE_PROFILE = 'update_profile', EDIT_JOB_DESCRIBE = 'edit_job_describe', JOB_UPDATE = 'job_update', @@ -36,6 +37,7 @@ export enum OpenSource { AnchorPage = 'anchor_page', MaterialViewPage = 'material_view_page', GroupOwnerCertificate = 'group_owner_certificate', + InviteOperations = 'invite_operations', } export enum PageUrl { @@ -81,6 +83,7 @@ export enum PageUrl { GiveVip = 'pages/give-vip/index', GroupOwnerCertificate = 'pages/group-owner-certification/index', PartnerShareVip = 'pages/partner-share-vip/index', + InviteOperations = 'pages/invite-operations/index', } export enum PluginUrl { diff --git a/src/constants/cache-key.ts b/src/constants/cache-key.ts index f45331f..d1c0495 100644 --- a/src/constants/cache-key.ts +++ b/src/constants/cache-key.ts @@ -13,4 +13,5 @@ export enum CacheKey { AGREEMENT_SIGNED = '__agreement_signed__', CITY_CODES = '__city_codes__', JOIN_GROUP_CARD_CLICKED = '__join_group_card_clicked__', + JOIN_GROUP_POPUP_CLICKED = '__join_group_popup_clicked__', } diff --git a/src/constants/job.ts b/src/constants/job.ts index 5d2e6c5..ef07746 100644 --- a/src/constants/job.ts +++ b/src/constants/job.ts @@ -213,4 +213,6 @@ export const FULL_PRICE_OPTIONS = FULL_EMPLOY_SALARY_OPTIONS.filter(o => !!o.val export enum GET_CONTACT_TYPE { VIP = 'vip', MATERIAL = 'material', + GROUP = 'group', + INVITE = 'invite', } diff --git a/src/fragments/job/base/index.tsx b/src/fragments/job/base/index.tsx index 294c4fc..9fdaee4 100644 --- a/src/fragments/job/base/index.tsx +++ b/src/fragments/job/base/index.tsx @@ -9,7 +9,7 @@ import { useCallback, useEffect, useState } from 'react'; import EmployTypeSelect from '@/components/employ-type-select'; import JobList, { IJobListProps } from '@/components/job-list'; import Overlay from '@/components/overlay'; -import PartnerBanner from '@/components/partner-banner'; +// 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'; @@ -30,6 +30,7 @@ import { Coordinate } from '@/types/location'; import { logWithPrefix } from '@/utils/common'; import { navigateTo } from '@/utils/route'; import './index.less'; +import InviteOperationsBanner from '@/components/invite-operations-banner'; interface IProps { cityCode: string; @@ -173,7 +174,8 @@ function JobFragment(props: IProps) { {JOB_TABS.map(tab => ( - + {/**/} + switchRoleType(RoleType.Company), []); + const handleClickInviteOperations = useCallback(() => navigateTo(PageUrl.InviteOperations), []); + return (
@@ -29,6 +32,21 @@ export default function AnchorFragment() { className={`${PREFIX}__cell`} onClick={handleClickMyDeclaration} /> + + 免费领主播会员 +
+ + 强烈推荐 +
+
+ } + className={`${PREFIX}__cell`} + onClick={handleClickInviteOperations} + /> (); + const [staffInfo, setStaffInfo] = useState(null); + + const handleClickCityMenu = useCallback(() => { + navigateTo(PageUrl.CitySearch, { city: cityCode || location.cityCode, source: OpenSource.InviteOperations }); + }, [cityCode, location.cityCode]); + + const handleCityChange = useCallback(data => { + console.log('handleCityChange', data); + const { openSource, cityCode: cCode } = data; + if (openSource !== OpenSource.InviteOperations) { + return; + } + setCityCode(cCode); + }, []); + + const handleCopy = useCallback(() => { + copy(`我的播络ID是:${userInfo.userId},邀你进群`); + }, [userInfo.userId]); + + useEffect(() => { + Taro.eventCenter.on(EventName.SELECT_CITY, handleCityChange); + return () => { + Taro.eventCenter.off(EventName.SELECT_CITY, handleCityChange); + }; + }, [handleCityChange]); + + const useCopyRef = useRef(false); + + useEffect(() => { + if (!userInfo.userId) return; + if (useCopyRef.current) return; + handleCopy(); + useCopyRef.current = true; + }, [handleCopy, userInfo.userId]); + + useEffect(() => { + if (!cityCode) return; + + getStaffInfo(cityCode) + .then(data => { + setStaffInfo(data); + }) + .catch(() => { + setStaffInfo(null); + }); + }, [cityCode]); + + return ( +
+
+
+
+
活动说明
+
+
+
邀请播络运营进带货主播群
+
+ 每邀请进一个群送一个日会员 +
+
注:只能邀请带货主播群,请勿邀请其他群
+
+
+
+
+
邀请方法
+
+
+
+
+ 加运营为好友并将以下信息
粘贴发送给运营
+
+
+
我的播络ID是:{userInfo.userId},邀你进群
+ +
+
+
选择城市,添加运营
+
+
+ {cityCode ? CITY_CODE_TO_NAME_MAP.get(cityCode) : '请选择城市'} + +
+ {staffInfo && ( + +
+
长按并识别二维码添加运营
+
+ +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/pages/job-detail/index.less b/src/pages/job-detail/index.less index 871e0f8..1a16fec 100644 --- a/src/pages/job-detail/index.less +++ b/src/pages/job-detail/index.less @@ -269,6 +269,21 @@ flex: 2 2; .button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;); margin-left: 32px; + position: relative; + overflow: visible; + + &-tag { + position: absolute; + right: 8px; + top: -12px; + font-weight: 400; + font-size: 24px; + line-height: 32px; + color: #FFFFFF; + padding: 2px 8px; + background: #FF5051; + border-radius: 20px 24px 24px 0px; + } } -} \ No newline at end of file +} diff --git a/src/pages/job-detail/index.tsx b/src/pages/job-detail/index.tsx index 35d61a0..9f2888c 100644 --- a/src/pages/job-detail/index.tsx +++ b/src/pages/job-detail/index.tsx @@ -12,7 +12,7 @@ import { JoinGroupHint } from '@/components/join-group-hint'; import LoginButton from '@/components/login-button'; import PageLoading from '@/components/page-loading'; import { PrejobPopup } from '@/components/prejob-popup'; -import ProductJobDialog from '@/components/product-dialog/job'; +import ProductJobContactDialog from '@/components/product-dialog/job-contact'; import CompanyPublishJobBuy from '@/components/product-dialog/steps-ui/company-publish-job-buy'; import { EventName, PageUrl, RoleType } from '@/constants/app'; import { CertificationStatusType } from '@/constants/company'; @@ -27,6 +27,7 @@ import { RESPONSE_ERROR_CODE } from '@/http/constant'; import { HttpError } from '@/http/error'; import { JobDetails } from '@/types/job'; import { IMaterialMessage } from '@/types/message'; +import { GetProductIsUnlockResponse, ProductInfo } from '@/types/product'; import { switchRoleType } from '@/utils/app'; import { copy, logWithPrefix } from '@/utils/common'; import { reportEvent } from '@/utils/event'; @@ -40,8 +41,7 @@ import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route'; import { getCommonShareMessage } from '@/utils/share'; import { formatDate } from '@/utils/time'; import Toast from '@/utils/toast'; -import { isNeedCreateMaterial } from '@/utils/user'; - +import { isNeedPhone } from '@/utils/user'; import './index.less'; const PREFIX = 'job-detail'; @@ -70,8 +70,30 @@ const getMapCallout = (data: JobDetails): MapProps.callout | undefined => { const AnchorFooter = (props: { data: JobDetails }) => { const { data } = props; const [errorTips, setErrorTips] = useState(''); - const [dialogVisible, setDialogVisible] = useState(false); + const [showJobContactDialog, setShowJobContactDialog] = useState(false); const [showMaterialGuide, setShowMaterialGuide] = useState(false); + const [productInfo, setProductInfo] = useState(); + const [productRecord, setProductRecord] = useState(); + const userInfo = useUserInfo(); + const needPhone = isNeedPhone(userInfo); + + const getProductRecord = useCallback(async () => { + const result = await requestProductUseRecord(ProductType.VIP, { jobId: data.id }); + setProductRecord(result); + }, [data.id]); + + const getProductBalance = useCallback(async (loading?: boolean) => { + if (loading) { + Taro.showLoading(); + } + const [, resp] = await requestProductBalance(ProductType.VIP); + setProductInfo(resp); + console.log(resp); + if (loading) { + Taro.hideLoading(); + } + return resp; + }, []); const handleClickContact = useCallback(async () => { log('handleClickContact'); @@ -79,21 +101,8 @@ const AnchorFooter = (props: { data: JobDetails }) => { return; } reportEvent(ReportEventId.CLICK_JOB_CONTACT); - try { - const needCreateMaterial = await isNeedCreateMaterial(); - if (data.sourcePlat !== 'bl') { - if (needCreateMaterial) { - const result = await requestProductUseRecord(ProductType.VIP, { jobId: data.id }); - if (!result) { - const [time, isPaidVip] = await requestProductBalance(ProductType.VIP); - if (time <= 0 || !isPaidVip) { - setShowMaterialGuide(true); - return; - } - } - } - } + try { if (data.isAuthed) { const toUserId = data.userId; if (isChatWithSelf(toUserId)) { @@ -102,7 +111,7 @@ const AnchorFooter = (props: { data: JobDetails }) => { } const chat = await postCreateChat(toUserId); let materialMessage: null | IMaterialMessage = null; - if (!needCreateMaterial) { + if (!!productInfo?.isCreateResume) { const profile = await requestProfileDetail(); if (profile) { materialMessage = { @@ -124,7 +133,13 @@ const AnchorFooter = (props: { data: JobDetails }) => { jobId: data.id, }); } else { - setDialogVisible(true); + // Show material guide if no resume and no VIP and no free balance + if (!productRecord && !productInfo?.isCreateResume && !productInfo?.isPaidVip && !productInfo?.freeBalance) { + setShowMaterialGuide(true); + return; + } + // Open integrated dialog - it handles buy + contact internally + setShowJobContactDialog(true); } } catch (error) { const e = error as HttpError; @@ -135,31 +150,99 @@ const AnchorFooter = (props: { data: JobDetails }) => { Toast.error('请求失败请重试'); } } - }, [data]); + }, [data, productInfo?.freeBalance, productInfo?.isCreateResume, productInfo?.isPaidVip]); + + const handleDialogClose = useCallback(() => { + setShowJobContactDialog(false); + // Refresh data after dialog closes + getProductRecord(); + }, [getProductRecord]); + + const handleConfirmPrejob = useCallback( + (type: GET_CONTACT_TYPE) => { + setShowMaterialGuide(false); + if (GET_CONTACT_TYPE.VIP === type) { + getProductBalance().then(() => { + setShowJobContactDialog(true); + }); + } + }, + [getProductBalance] + ); + + // const unAuthedButtonText = useMemo(() => { + // if (haveSeen) { + // return '查看联系方式'; + // } + // + // if (productInfo?.isPaidVip) { + // return '您是会员,可直接查看'; + // } + // + // if (productInfo?.freeBalance) { + // return `还剩${productInfo.freeBalance}次查看次数`; + // } + // + // return productInfo?.isCreateResume? '升级会员即可查看': '创建模卡,免费查看'; + // }, [productInfo, haveSeen]); + + const handleRefresh = useCallback(async () => { + await getProductBalance(true); + }, [getProductBalance]); + + useEffect(() => { + Taro.eventCenter.on(EventName.CREATE_PROFILE, getProductBalance); + return () => { + Taro.eventCenter.off(EventName.CREATE_PROFILE); + }; + }, [getProductBalance]); + + useEffect(() => { + getProductBalance(); + }, [getProductBalance]); + + useEffect(() => { + getProductRecord(); + }, [getProductRecord]); - const handleDialogHidden = useCallback(() => { - setDialogVisible(false); - }, []); - const handleConfirmPrejob = useCallback((type: GET_CONTACT_TYPE) => { - setShowMaterialGuide(false); - if (GET_CONTACT_TYPE.VIP === type) { - setDialogVisible(true); - } - }, []); return ( <>
- - {data.isAuthed ? '在线沟通' : '立即联系'} + + {data.isAuthed ? '在线沟通' : '查看联系方式'} + {needPhone ? ( +
登录后可免费报单
+ ) : !productRecord && (data.isAuthed || productInfo?.content) ? ( +
+ {data.isAuthed ? '急招岗位可免费查看' : productInfo?.content} +
+ ) : null}
- {dialogVisible && } + {showJobContactDialog && ( + + )} {showMaterialGuide && ( - setShowMaterialGuide(false)} onConfirm={handleConfirmPrejob} /> + setShowMaterialGuide(false)} + onConfirm={handleConfirmPrejob} + /> )} 录屏是企业最关注的资料,建议提供多个风格和品类
+
注:请勿乱传,也不要上传简历,会被封号
{videoList.map(video => ( { navigateTo(PageUrl.GroupDelegatePublish); @@ -80,7 +80,40 @@ export default function BizService() {
- + +
+
+
服务城市
+
江、沪、皖-上海、南京、合肥
+
粤、闽-广州、深圳、佛山、厦门、福州、泉州
+
京、鲁-北京、青岛
+
鄂、豫、湘、陕-长沙、武汉、郑州、西安
+
川、渝、云-成都、重庆、昆明
+
服务方式及收费标准
+
服务方式:提供录屏和基本资料供挑选,挑中安排面试
+
收费标准:安排一场面试200元
+
收费方式:预付费,按安排面试场数扣费
+
服务能力
+
+ 我们在每个城市均有数量众多的主播群,少则几十个,多则上千个,有各种类型和层次的带货主播资源 +
+
+ +
+
+
+
+ + 群代发 + + + } + >
- - 主播群 - - - } - > + - -
-
-
服务城市
-
江、沪、皖-上海、南京、合肥
-
粤、闽-广州、深圳、佛山、厦门、福州、泉州
-
京、鲁-北京、青岛
-
鄂、豫、湘、陕-长沙、武汉、郑州、西安
-
川、渝、云-成都、重庆、昆明
-
服务方式及收费标准
-
服务方式:提供录屏和基本资料供挑选,挑中安排面试
-
收费标准:安排一场面试200元
-
收费方式:预付费,按安排面试场数扣费
-
服务能力
-
- 我们在每个城市均有数量众多的主播群,少则几十个,多则上千个,有各种类型和层次的带货主播资源 -
-
- -
-
-
-
diff --git a/src/types/product.ts b/src/types/product.ts index 239a452..dfc892f 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -8,13 +8,18 @@ export interface DeclarationTypeResult { } export interface ProductInfo { - productCode: ProductType; - productId: ProductType; + // productCode: ProductType; + // productId: ProductType; + balance: number; - created: number; - updated: number; - isPaidVip?: boolean; - // 报单类型信息,只有 use 接口返回值才有 + plaidBalance: number; + freeBalance: number; + content: string; + created: string; + updated: string; + isPaidVip: boolean; + isCreateResume: boolean; + allowBuyProduct: boolean; declarationTypeResult?: DeclarationTypeResult; } @@ -107,3 +112,22 @@ export interface GetOrderInfoRequest { payOrderNo: string; userId: string; } + +export interface ProductSpecResult { + id: string; + productId: string; + productSpecId: ProductSpecId; + productType: ProductType; + productName: string; + title: string; + priceText: string; + expire: number; + payPrice: number; // 分 + showPrice: number; + originalPrice: number; + badge: string; + contentSingle: string; + contentsJson: string; + sort: number; + createTime: string; +} diff --git a/src/utils/common.ts b/src/utils/common.ts index f6e881c..cffa537 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -15,12 +15,10 @@ export const isDesktop = (() => { return info.platform === 'windows' || info.platform === 'mac'; })(); -export const logWithPrefix = isDev() - ? (prefix: string) => - (...args: BL.Anything[]) => - console.log(`[${prefix}]`, ...args) - : (_prefix: string) => - (..._args: BL.Anything[]) => {}; +export const logWithPrefix = + (prefix: string) => + (...args: BL.Anything[]) => + console.log(`[${prefix}]`, ...args); export const safeJsonParse = (str: string, defaultValue: BL.Anything = {}): T => { try { diff --git a/src/utils/product.ts b/src/utils/product.ts index c473758..0ad61f1 100644 --- a/src/utils/product.ts +++ b/src/utils/product.ts @@ -15,7 +15,9 @@ import { CreatePayOrderParams, GetOrderInfoRequest, OrderInfo, + ProductSpecResult, } from '@/types/product'; +import { buildUrl } from '@/utils/common'; import { getUserId } from '@/utils/user'; export const isCancelPay = err => err?.errMsg === 'requestPayment:fail cancel'; @@ -44,6 +46,11 @@ export async function requestProductUseRecord( }); } +export async function requestProductTypeList(productType: ProductType) { + const list = await http.get(buildUrl(API.LIST_PRODUCT_TYPE, { productType })); + return list.sort((a, b) => a.sort - b.sort); +} + // 使用某一个产品 export async function requestUseProduct( productCode: ProductType, @@ -54,13 +61,14 @@ export async function requestUseProduct( } // 获取某个产品的剩余解锁次数 -export async function requestProductBalance(productCode: ProductType): Promise<[number, boolean | undefined]> { +export async function requestProductBalance(productCode: ProductType): Promise<[number, ProductInfo]> { const data: GetProductDetailRequest = { productCode, userId: getUserId() }; - const { balance, isPaidVip } = await http.post(API.GET_PRODUCT_DETAIL, { + const result = await http.post(API.GET_PRODUCT_DETAIL, { data, contentType: 'application/x-www-form-urlencoded', }); - return [balance, isPaidVip]; + + return [result.balance, result]; } // 是否可以购买某一个产品