37 Commits

Author SHA1 Message Date
10e7f39c92 feat: update 2025-07-29 23:00:57 +08:00
1c1bb146fc feat: job publish 2025-07-28 23:35:51 +08:00
260e543fe6 feat: 报单优化 2025-07-27 23:23:29 +08:00
0cd1a46762 revert ad 2025-07-27 15:59:28 +08:00
1ddc8b46c9 feat: 去掉login guide 2025-07-12 19:42:22 +08:00
5820fa8d25 feat: 2025-07-12 15:43:19 +08:00
80846d507f feat: 广告还有角色异常报错 2025-07-12 15:28:40 +08:00
42d1208ee4 feat: get is vip 2025-07-07 22:08:51 +08:00
2c48a70b6d feat: 去掉月会员,改文案 2025-07-07 15:13:35 +08:00
e8cf28b6e9 feat: 广告 2025-07-07 01:15:50 +08:00
de7f0e14fe feat:群代发默认选项修改 + 加群卡片 2025-07-02 23:36:34 +08:00
de2f380cd9 feat: update 2025-06-27 22:46:24 +08:00
0020eb8dbe feat: add start to tab 2025-06-27 11:06:23 +08:00
b0dd660dde feat: 群代发改版 2025-06-27 10:56:44 +08:00
56cf10c768 feat: 2025-06-25 22:47:53 +08:00
0ec366cc2e feat: 分享的文案 + 点击头像看模卡 + 提现文案 + 协议弹窗 + 进群限制 2025-06-17 00:03:58 +08:00
828497fcd6 feat: resume 2025-06-16 00:29:17 +08:00
fd5b3dab97 feat: fix conflict 2025-06-15 01:10:47 +08:00
1b782ee82c Merge branch 'trunk' into feat/update-login 2025-06-15 01:09:35 +08:00
80c0d71921 feat: login whatever 2025-06-15 01:09:30 +08:00
c76027b4d9 feat: support click to open material page 2025-06-15 00:19:18 +08:00
c08b0bef95 feat: support click avatar jump to resume page 2025-06-15 00:14:48 +08:00
96eb46821e feat: withdraw 2025-06-14 23:45:47 +08:00
5acc25c8c9 feat: login 2025-06-08 22:57:23 +08:00
082c5483c5 feat: invite capa page 2025-06-05 23:32:04 +08:00
xd
6805b590c7 wip 2025-06-05 22:47:41 +08:00
ed99c7b1ae feat:n 2025-05-29 01:04:31 +08:00
fa30ec2988 Merge branch 'trunk' into feat/partner
* trunk:
  feat: login
2025-05-22 18:13:41 +08:00
6762973e14 feat: login 2025-05-22 18:08:31 +08:00
c0a2c66280 Merge branch 'trunk' into feat/partner
* trunk:
  feat: update
  feat: update
  feat: search city
  feat: s
  新页面
  feat: biz-service
2025-05-20 23:45:59 +08:00
8ebd3363b8 feat: update 2025-05-20 23:22:35 +08:00
c92ad08148 feat: update 2025-05-20 23:22:00 +08:00
74579f09e3 feat: search city 2025-05-20 23:19:46 +08:00
276107722e feat: s 2025-05-20 22:55:48 +08:00
9d6c89e0b6 feat: 暂时不请求 2025-05-20 00:57:13 +08:00
fc37a612cc 新页面 2025-05-20 00:50:07 +08:00
f60a753e5a feat: biz-service 2025-05-19 23:53:57 +08:00
118 changed files with 3051 additions and 1449 deletions

View File

@ -109,5 +109,6 @@
"ignoredBuiltDependencies": [
"@tarojs/binding"
]
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -16,7 +16,19 @@
"coverView": false,
"showShadowRootInWxmlPanel": false,
"packNpmRelationList": [],
"ignoreUploadUnusedFiles": true
"ignoreUploadUnusedFiles": true,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"srcMiniprogramRoot": "dist/",
@ -30,5 +42,6 @@
"ignore": [],
"include": []
},
"appid": "wxf0724a83f8e377d2"
}
"appid": "wxf0724a83f8e377d2",
"simulatorPluginLibVersion": {}
}

View File

@ -7,16 +7,16 @@ 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 { getInviteCode, getInviteCodeFromQuery } from '@/utils/partner';
import qiniuUpload from '@/utils/qiniu-upload';
import { requestUserInfo, updateLastLoginTime } from '@/utils/user';
import './app.less';
function App({ children }: PropsWithChildren<BL.Anything>) {
useLaunch(async () => {
useLaunch(async ({ query }) => {
console.log('App launched.');
await http.init();
await http.init(getInviteCodeFromQuery(query));
requestUserInfo().then(userInfo => {
if (userInfo.isPartner) {
getInviteCode();

View File

@ -0,0 +1,41 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.agreement-popup {
&__content {
.flex-column();
padding: 40px 32px;
}
&__title {
font-size: 32px;
font-weight: 500;
line-height: 48px;
color: @blColor;;
margin-bottom: 24px;
}
&__body {
font-weight: 400;
font-size: 28px;
line-height: 40px;
color: @blColor;
margin-bottom: 24px;
align-self: flex-start;
};
&__btn-group {
margin-top: 32px;
.flex-row();
gap: 30px;
}
&__confirm-button {
.button(@width: 230px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
}
&__cancel-button {
.button(@width: 230px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px, @highlight: 0);
}
}

View File

@ -0,0 +1,66 @@
import { Button, ITouchEvent } from '@tarojs/components';
import { Popup } from '@taroify/core';
import { useCallback } from 'react';
import { BindPhoneStatus } from '@/components/login-button';
import PhoneButton from '@/components/phone-button';
import { ProtocolPrivacy } from '@/components/protocol-privacy';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import './index.less';
interface IProps {
open: boolean;
onCancel: () => void;
onConfirm: (e: ITouchEvent) => void;
needPhone?: boolean;
needBindPhone?: boolean;
}
const PREFIX = 'agreement-popup';
export function AgreementPopup({ open, onCancel, onConfirm, needPhone, needBindPhone }: IProps) {
const handleBindPhone = useCallback(
(status: BindPhoneStatus, e: ITouchEvent) => {
if (status === BindPhoneStatus.Success) {
onConfirm(e);
} else {
onCancel();
}
},
[onCancel, onConfirm]
);
if (!open) {
return null;
}
return (
<Popup rounded className={PREFIX} placement="bottom" open={open} onClose={onCancel}>
<div className={`${PREFIX}__content`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__body`}>
<div></div>
<div>使</div>
</div>
<ProtocolPrivacy divider />
<div className={`${PREFIX}__btn-group`}>
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
</Button>
{needBindPhone ? (
<PhoneButton className={`${PREFIX}__confirm-button`} needPhone={needPhone} onBindPhone={handleBindPhone}>
</PhoneButton>
) : (
<Button className={`${PREFIX}__confirm-button`} onClick={onConfirm}>
</Button>
)}
</div>
</div>
<SafeBottomPadding />
</Popup>
);
}

View File

@ -18,6 +18,7 @@ import './index.less';
interface IProps {
data: AnchorInfo;
jobId?: string;
validator: (onSuccess: () => void) => void;
}
const PREFIX = 'anchor-card';
@ -34,14 +35,17 @@ const getSalary = (data: AnchorInfo) => {
};
function AnchorCard(props: IProps) {
const { data, jobId } = props;
const { data, jobId, validator } = props;
const style = data.isRead ? ({ '--read-color': '#999999' } as React.CSSProperties) : {};
const cover = (data.materialVideoInfoList.find(video => video.isDefault) || data.materialVideoInfoList[0])?.coverUrl;
const handleClick = useCallback(
() => navigateTo(PageUrl.MaterialView, { jobId, resumeId: data.id, source: MaterialViewSource.AnchorList }),
[data, jobId]
);
const handleNavTo = useCallback(() => {
navigateTo(PageUrl.MaterialView, { jobId, resumeId: data.id, source: MaterialViewSource.AnchorList });
}, [data, jobId]);
const handleClick = useCallback(() => {
validator(handleNavTo);
}, [handleNavTo, validator]);
return (
<div className={PREFIX} style={style} onClick={handleClick}>

View File

@ -7,11 +7,16 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import AnchorCard from '@/components/anchor-card';
import ListPlaceholder from '@/components/list-placeholder';
import LoginDialog from '@/components/login-dialog';
import { EventName } from '@/constants/app';
import { AnchorSortType } from '@/constants/material';
import useUserInfo from '@/hooks/use-user-info';
import { AnchorInfo, GetAnchorListRequest, IAnchorFilters } from '@/types/material';
import { logWithPrefix } from '@/utils/common';
import { requestAnchorList as requestData } from '@/utils/material';
import { getAgreementSigned, isNeedLogin, setAgreementSigned } from '@/utils/user';
import { AgreementPopup } from '../agreement-popup';
import './index.less';
@ -55,6 +60,11 @@ function AnchorList(props: IAnchorListProps) {
const requestProps = useRef<IRequestProps>({});
const prevRequestProps = useRef<IRequestProps>({});
const onListEmptyRef = useRef(onListEmpty);
const [openLogin, setLoginOpen] = useState(false);
const [openAssignment, setAssignmentOpen] = useState(false);
const successCallback = useRef<() => void>(() => {});
const userInfo = useUserInfo();
const needLogin = isNeedLogin(userInfo);
const handleRefresh = useCallback(async () => {
log('start pull refresh');
@ -120,6 +130,37 @@ function AnchorList(props: IAnchorListProps) {
[dataList]
);
const handleCancel = useCallback(() => {
setAssignmentOpen(false);
setAgreementSigned(false);
}, []);
const handleConfirm = useCallback(() => {
setAssignmentOpen(false);
setAgreementSigned(true);
successCallback.current();
}, []);
const handleLoginSuccess = useCallback(() => {
setLoginOpen(false);
successCallback.current();
}, []);
const handleLoginCancel = useCallback(() => {
setLoginOpen(false);
}, []);
const validator = (onSuccess: () => void) => {
successCallback.current = onSuccess;
if (!getAgreementSigned()) {
setAssignmentOpen(true);
} else if (needLogin) {
setLoginOpen(true);
} else {
onSuccess();
}
};
useEffect(() => {
onListEmptyRef.current = onListEmpty;
}, [onListEmpty]);
@ -183,26 +224,35 @@ function AnchorList(props: IAnchorListProps) {
}, [jobId, ready, filters, cityCode, sortType]);
return (
<PullRefresh
className={classNames(`${PREFIX}__pull-refresh`, className)}
loading={refreshing}
onRefresh={handleRefresh}
disabled={refreshDisabled || !ready}
>
<List
hasMore={hasMore}
onLoad={handleLoadMore}
loading={loadingMore || refreshing}
disabled={loadMoreError || !ready}
fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
<>
<PullRefresh
className={classNames(`${PREFIX}__pull-refresh`, className)}
loading={refreshing}
onRefresh={handleRefresh}
disabled={refreshDisabled || !ready}
>
{dataList.map(item => (
<AnchorCard data={item} jobId={jobId} key={item.id} />
))}
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List>
</PullRefresh>
<List
hasMore={hasMore}
onLoad={handleLoadMore}
loading={loadingMore || refreshing}
disabled={loadMoreError || !ready}
fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
>
{dataList.map(item => (
<AnchorCard data={item} jobId={jobId} key={item.id} validator={validator} />
))}
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List>
</PullRefresh>
<AgreementPopup
open={openAssignment}
onCancel={handleCancel}
onConfirm={handleConfirm}
needBindPhone={needLogin}
/>
{openLogin && <LoginDialog onCancel={handleLoginCancel} onSuccess={handleLoginSuccess} />}
</>
);
}

View File

@ -1,18 +1,21 @@
import React from 'react';
import BaseTabBar from '@/components/tab-bar';
import { PageType } from '@/constants/app';
import './index.less';
interface IProps extends React.PropsWithChildren {}
interface IProps extends React.PropsWithChildren {
type: PageType;
}
export default function HomePage(props: IProps) {
const { children } = props;
const { children, type } = props;
return (
<React.Fragment>
{children}
<BaseTabBar />
<BaseTabBar type={type} />
</React.Fragment>
);
}

View File

@ -46,8 +46,8 @@ function JobRecommendList(props: IJobListProps) {
setDataList([]);
setLoading(true);
setLoadError(false);
const { jobResults = [] } = await requestMyRecommendJobList({ ...requestProps.current });
setDataList(jobResults);
const { jobResults } = await requestMyRecommendJobList({ ...requestProps.current });
setDataList(jobResults || []);
} catch (e) {
setDataList([]);
setLoadError(true);

View File

@ -0,0 +1,41 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.join-group-hint {
.flex-row();
background: #FFFFFF;
border-radius: 16px;
padding: 32px 24px;
margin-top: 24px;
margin-bottom: 24px;
&__icon {
width: 80px;
height: 80px;
margin-right: 32px;
}
&__left {
flex: 1;
padding-left: 8px;
&-title {
font-weight: 500;
font-size: 32px;
line-height: 40px;
margin-bottom: 16px;
}
&-desc {
font-weight: 400;
font-size: 28px;
line-height: 40px;
}
}
&__right {
flex: 0 0 auto;
}
&__button {
.button(@width: 186px, @height: 64px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 32px, @highlight: 0);
}
}

View File

@ -0,0 +1,63 @@
import { Button, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Plus } from '@taroify/icons';
import { useCallback, useState } from 'react';
import { RoleType } from '@/constants/app';
import { CacheKey } from '@/constants/cache-key';
import { CITY_CODE_TO_NAME_MAP } from '@/constants/city';
import { GROUPS } from '@/constants/group';
import { getRoleTypeWithDefault } from '@/utils/app';
import { openCustomerServiceChat } from '@/utils/common';
import { getCurrentCityCode } from '@/utils/location';
import { checkCityCode, validCityCode } from '@/utils/user';
import './index.less';
const PREFIX = 'join-group-hint';
const DEFAULT_GROUP = {
name: '播络主播招聘群',
serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc60ac7b6420787a8',
};
export function JoinGroupHint() {
const cityCode = getCurrentCityCode();
const roleType = getRoleTypeWithDefault();
const group = GROUPS.find(g => String(g.cityCode) === cityCode);
const [clicked, setClicked] = useState(!!Taro.getStorageSync(CacheKey.JOIN_GROUP_CARD_CLICKED));
const handleClick = useCallback(() => {
if (group && !checkCityCode(cityCode)) {
return;
}
openCustomerServiceChat(group ? group.serviceUrl : DEFAULT_GROUP.serviceUrl);
Taro.setStorageSync(CacheKey.JOIN_GROUP_CARD_CLICKED, true);
setClicked(true);
}, [cityCode, group]);
if (!validCityCode(cityCode) || clicked) {
return null;
}
return (
<div className={PREFIX}>
<Image
className={`${PREFIX}__icon`}
src="https://publiccdn.neighbourhood.com.cn/img/group-avatar.png"
mode="aspectFit"
/>
<div className={`${PREFIX}__left`}>
<div className={`${PREFIX}__left-title`}>
{group ? `${CITY_CODE_TO_NAME_MAP.get(cityCode)}主播招聘群` : DEFAULT_GROUP.name}
</div>
<div className={`${PREFIX}__left-desc`}>{roleType === RoleType.Anchor ? '高薪工作早知道' : '免费招主播'}</div>
</div>
<div className={`${PREFIX}__right`}>
<Button className={`${PREFIX}__button`} onClick={handleClick}>
<Plus />
</Button>
</div>
</div>
);
}

View File

@ -1,11 +1,12 @@
import { Button, ButtonProps } from '@tarojs/components';
import { Button, ButtonProps, ITouchEvent } from '@tarojs/components';
import classNames from 'classnames';
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 { isNeedLogin } from '@/utils/user';
import { getAgreementSigned, isNeedLogin, setAgreementSigned } from '@/utils/user';
import './index.less';
@ -17,6 +18,7 @@ export enum BindPhoneStatus {
export interface ILoginButtonProps extends ButtonProps {
needPhone?: boolean;
needAssignment?: boolean;
}
const PREFIX = 'login-button';
@ -24,12 +26,50 @@ const PREFIX = 'login-button';
function LoginButton(props: ILoginButtonProps) {
const { className, children, needPhone, onClick, ...otherProps } = props;
const userInfo = useUserInfo();
const [visible, setVisible] = useState(false);
const [loginVisible, setLoginVisible] = useState(false);
const [assignVisible, setAssignVisible] = useState(false);
const needLogin = isNeedLogin(userInfo);
// 点击按钮时,协议同意也手机也授权了 -> 下一步
//
// 点击按钮时,协议同意但是手机授权没给 -> 弹登录
// -> 如果授权了手机号就进行下一步
// -> 不给授权没下一步
//
// 点击按钮时,协议不同意 -> 弹协议窗口
// -> 如果同意就进行下一步
// -> 不同意没下一步
const handleClick = useCallback(
(e: ITouchEvent) => {
if (!getAgreementSigned()) {
setAssignVisible(true);
} else if (needLogin) {
setLoginVisible(true);
} else if (onClick) {
onClick(e);
}
},
[needLogin, onClick]
);
const onSuccess = useCallback(
const handleConfirm = useCallback(
(e: ITouchEvent) => {
setAssignVisible(false);
setAgreementSigned(true);
if (onClick) {
onClick(e);
}
},
[onClick]
);
const handleCancel = useCallback(() => {
setAgreementSigned(false);
setAssignVisible(false);
}, []);
const handleLoginSuccess = useCallback(
e => {
setVisible(false);
setLoginVisible(false);
onClick?.(e);
},
[onClick]
@ -37,14 +77,18 @@ function LoginButton(props: ILoginButtonProps) {
return (
<>
<Button
{...otherProps}
className={classNames(PREFIX, className)}
onClick={needLogin ? () => setVisible(true) : onClick}
>
<Button {...otherProps} className={classNames(PREFIX, className)} onClick={handleClick}>
{children}
</Button>
{visible && <LoginDialog onCancel={() => setVisible(false)} onSuccess={onSuccess} needPhone={needPhone} />}
<AgreementPopup
open={assignVisible}
onCancel={handleCancel}
onConfirm={handleConfirm}
needBindPhone={needLogin}
/>
{loginVisible && (
<LoginDialog onCancel={() => setLoginVisible(false)} onSuccess={handleLoginSuccess} needPhone={needPhone} />
)}
</>
);
}

View File

@ -17,22 +17,4 @@
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
margin-top: 40px;
}
&__cancel-button {
min-width: fit-content;
font-size: 28px;
line-height: 32px;
color: @blHighlightColor;
background: transparent;
border: none;
margin-top: 40px;
&::after {
border-color: transparent
}
}
&__checkbox {
margin-top: 40px;
}
}

View File

@ -1,11 +1,6 @@
import { Button } from '@tarojs/components';
import { Dialog } from '@taroify/core';
import { useCallback, useState } from 'react';
import PhoneButton, { IPhoneButtonProps } from '@/components/phone-button';
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
import Toast from '@/utils/toast';
import './index.less';
@ -21,36 +16,20 @@ const PREFIX = 'login-dialog';
export default function LoginDialog(props: IProps) {
const { title = '使用播络服务前,请先登录', needPhone, onSuccess, onCancel, onBindPhone } = props;
const [checked, setChecked] = useState(false);
const handleTipCheck = useCallback(() => {
Toast.info('请先阅读并同意协议');
}, []);
return (
<Dialog open onClose={onCancel}>
<Dialog.Content>
<div className={`${PREFIX}__container`}>
<div className={`${PREFIX}__title`}>{title}</div>
{!checked && (
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
</Button>
)}
{checked && (
<PhoneButton
className={`${PREFIX}__confirm-button`}
onSuccess={onSuccess}
onBindPhone={onBindPhone}
needPhone={needPhone}
>
</PhoneButton>
)}
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
</Button>
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
<PhoneButton
className={`${PREFIX}__confirm-button`}
onSuccess={onSuccess}
onBindPhone={onBindPhone}
needPhone={needPhone}
>
</PhoneButton>
</div>
</Dialog.Content>
</Dialog>

View File

@ -2,13 +2,13 @@ import { Image } from '@tarojs/components';
import classNames from 'classnames';
import { PropsWithChildren, useEffect, useState, useCallback } from 'react';
import { MaterialViewSource } from '@/constants/material';
import { PageUrl } from '@/constants/app';
import { MaterialViewSource } from '@/constants/material';
import useUserInfo from '@/hooks/use-user-info';
import { IChatMessage } from '@/types/message';
import { getScrollItemId } from '@/utils/common';
import { navigateTo } from '@/utils/route';
import { PageUrl } from '@/constants/app';
import './index.less';
@ -19,12 +19,13 @@ export interface IBaseMessageProps {
export interface IUserMessageProps extends PropsWithChildren, IBaseMessageProps {
isRead?: boolean;
resumeId?: string;
}
const PREFIX = 'base-message';
function BaseMessage(props: IUserMessageProps) {
const { id, message, isRead: isReadProps, children } = props;
const { id, message, isRead: isReadProps, children, resumeId } = props;
const { userId } = useUserInfo();
const [isRead, setIsRead] = useState(message.isRead);
const isSender = message.senderUserId === userId;
@ -37,10 +38,12 @@ function BaseMessage(props: IUserMessageProps) {
// const timer = setTimeout(() => setIsRead(true), 1200);
// return () => clearTimeout(timer);
// }, [isSender]);
const handleClick = useCallback(
() => navigateTo(PageUrl.MaterialView, { resumeId: message.jobId, source: MaterialViewSource.Chat }),
[message.jobId]
);
const handleClick = useCallback(() => {
if (!resumeId || isSender) {
return;
}
navigateTo(PageUrl.MaterialView, { resumeId: resumeId, source: MaterialViewSource.Chat });
}, [resumeId, isSender]);
useEffect(() => {
if (isRead) {
return;
@ -53,6 +56,7 @@ function BaseMessage(props: IUserMessageProps) {
<Image
mode="aspectFit"
className={`${PREFIX}__avatar`}
onClick={handleClick}
src={message.senderAvatarUrl || require('@/statics/png/default_avatar.png')}
/>
<div className={`${PREFIX}__content-container`}>

View File

@ -80,4 +80,8 @@
margin-top: 24px;
}
}
}
&__no-subscription {
}
}

View File

@ -57,7 +57,9 @@ export function MessageNoTimesDialog(props: INoTimesProps) {
<Dialog className={NO_TIMES} onClose={onClose} open={open}>
<Dialog.Content>
<div className={`${NO_TIMES}__title`}></div>
<div className={`${NO_TIMES}__tips`}><span className="highlight"></span></div>
<div className={`${NO_TIMES}__tips`}>
<span className="highlight"></span>
</div>
<div className={`${NO_TIMES}__body`}>
<div className={`${NO_TIMES}__times`}>{`未读消息提醒剩余:${times}`}</div>
<Button className={`${NO_TIMES}__btn`} onClick={onClick}>

View File

@ -60,7 +60,9 @@ export default function PartnerBanner() {
)}
<div className={`${PREFIX}__close`} onClick={handlePartnerBannerClose} />
</div>
{visible && <LoginDialog onCancel={() => setVisible(false)} onSuccess={handleBind} needPhone={needPhone} />}
{visible && (
<LoginDialog disableCheck onCancel={() => setVisible(false)} onSuccess={handleBind} needPhone={needPhone} />
)}
</>
);
}

View File

@ -39,7 +39,9 @@ function JoinEntry({ onBindSuccess }: JoinEntryProps) {
</Button>
)}
</div>
{visible && <LoginDialog onCancel={() => setVisible(false)} onSuccess={onBindSuccess} needPhone={needPhone} />}
{visible && (
<LoginDialog disableCheck onCancel={() => setVisible(false)} onSuccess={onBindSuccess} needPhone={needPhone} />
)}
</>
);
}

View File

@ -61,6 +61,10 @@
font-weight: 400;
font-size: 28px;
line-height: 40px;
&.grey {
color: @blColorG2
}
}
&__title {

View File

@ -142,12 +142,18 @@ export default function PartnerIntro() {
</div>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__body`}></div>
</div>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card ${PREFIX}__special`}>
<div className={`${PREFIX}__body`}></div>
<div className={`${PREFIX}__body`}></div>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body grey`}></div>
<Button className={`${PREFIX}__service`} onClick={handleOpenService}>
</Button>
</div>
<div className={`${PREFIX}__tip`}></div>

View File

@ -41,14 +41,18 @@
height: 131px;
width: 100%;
background: #fff;
padding: 24px 32px;
padding: 24px 32px 0 32px;
box-sizing: border-box;
font-size: 28px;
&-border {
border-bottom: 1px solid #e6e7e8;
}
&-content {
.flex-row();
width: 100%;
border-bottom: 1px solid #e6e7e8;
padding-bottom: 24px;
}
&-time-id {

View File

@ -12,6 +12,8 @@ import './index.less';
const PREFIX = 'partner-invite-list';
const log = logWithPrefix(PREFIX);
const FIRST_PAGE = 0;
function PartnerList(props: {
refreshDisabled?: boolean;
visible?: boolean;
@ -20,10 +22,12 @@ function PartnerList(props: {
onListEmpty?: () => void;
}) {
const { className, listHeight, refreshDisabled, visible = true, onListEmpty } = props;
const [hasMore, setHasMore] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState(false);
const [dataList, setDataList] = useState<InviteUserInfo[]>([]);
const currentPage = useRef<number>(FIRST_PAGE);
const onListEmptyRef = useRef(onListEmpty);
const handleRefresh = useCallback(async () => {
@ -31,19 +35,44 @@ function PartnerList(props: {
try {
setRefreshing(true);
setLoadMoreError(false);
const list = await requestData();
setDataList(list);
!list.length && onListEmptyRef.current?.();
const { content, totalPages } = await requestData({ page: 1 });
setDataList(content);
currentPage.current = 1;
setHasMore(currentPage.current < totalPages);
!content.length && onListEmptyRef.current?.();
log('pull refresh success');
} catch (e) {
setDataList([]);
setHasMore(false);
setLoadMoreError(true);
currentPage.current = FIRST_PAGE;
log('pull refresh failed');
} finally {
setRefreshing(false);
}
}, []);
const handleLoadMore = useCallback(async () => {
log('start load more', hasMore);
if (!hasMore) {
return;
}
setLoadMoreError(false);
setLoadingMore(true);
try {
const { totalPages, content } = await requestData({ page: currentPage.current + 1 });
setDataList([...dataList, ...content]);
currentPage.current = currentPage.current + 1;
setHasMore(currentPage.current < totalPages);
log('load more success');
} catch (e) {
setLoadMoreError(true);
log('load more failed');
} finally {
setLoadingMore(false);
}
}, [dataList, hasMore]);
useEffect(() => {
onListEmptyRef.current = onListEmpty;
}, [onListEmpty]);
@ -62,11 +91,14 @@ function PartnerList(props: {
setDataList([]);
setLoadingMore(true);
setLoadMoreError(false);
const list = await requestData();
setDataList(list);
!list.length && onListEmptyRef.current?.();
const { totalPages, content } = await requestData({ page: 1 });
setDataList(content);
currentPage.current = 1;
setHasMore(currentPage.current < totalPages);
!content.length && onListEmptyRef.current?.();
} catch (e) {
setDataList([]);
setHasMore(false);
setLoadMoreError(true);
} finally {
log('visible changed, refresh list data end');
@ -90,22 +122,24 @@ function PartnerList(props: {
disabled={refreshDisabled}
>
<List
hasMore={false}
onLoad={() => {}}
hasMore={hasMore}
onLoad={handleLoadMore}
loading={loadingMore || refreshing}
disabled={loadMoreError}
disabled={loadMoreError || !visible}
fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
>
{dataList.map(item => (
<div className={`${PREFIX}__item`} key={item.id || item.userId}>
<div className={`${PREFIX}__item-content`}>
<div className={`${PREFIX}__item-time-id`}>
<div className={`${PREFIX}__item-time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item-id`}>{formatUserId(item.userId)}</div>
<div className={`${PREFIX}__item-border`}>
<div className={`${PREFIX}__item-content`}>
<div className={`${PREFIX}__item-time-id`}>
<div className={`${PREFIX}__item-time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item-id`}>{formatUserId(item.userId)}</div>
</div>
<div className={`${PREFIX}__item-created`}>{item.isCreateResume ? '已创建' : '未创建'}</div>
<div className={`${PREFIX}__item-joined`}>{item.isPartner ? '已加入' : '未加入'}</div>
</div>
<div className={`${PREFIX}__item-created`}>{item.isCreateResume ? '已创建' : '未创建'}</div>
<div className={`${PREFIX}__item-joined`}>{item.isPartner ? '已加入' : '未加入'}</div>
</div>
</div>
))}

View File

@ -1,14 +1,16 @@
import { Button, Image } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { Dialog } from '@taroify/core';
import { Question } from '@taroify/icons';
import { useCallback, useState, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { PageUrl } from '@/constants/app';
import { PartnerProfitsState } from '@/types/partner';
import { formatMoney, getPartnerProfitStat } from '@/utils/partner';
import { formatMoney, getPartnerProfitStat, withdrawMoney } from '@/utils/partner';
import { navigateTo } from '@/utils/route';
import Toast from '@/utils/toast';
import './index.less';
const PREFIX = 'partner-kanban';
@ -31,17 +33,48 @@ function TipDialog(props: { open: boolean; onClose: () => void }) {
}
function WithdrawDialog(props: { open: boolean; onClose: () => void; count: number }) {
const handleWithdraw = useCallback(() => {}, []);
const handleWithdraw = useCallback(async () => {
if (Taro.canIUse('requestMerchantTransfer')) {
try {
const result = await withdrawMoney();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
wx.requestMerchantTransfer({
mchId: '1642470088',
appId: 'wxf0724a83f8e377d2',
package: result.packageInfo,
success: (res: never) => {
// res.err_msg将在页面展示成功后返回应用时返回ok并不代表付款成功
console.log('success:', res);
Toast.success('提现成功');
props.onClose();
},
fail: (res: never) => {
Toast.error('提现失败');
console.log('fail:', res);
},
});
} catch (e) {
Toast.error('提现订单创建失败');
console.log(e);
}
} else {
await Taro.showModal({
content: '你的微信版本过低,请更新至最新版本。',
showCancel: false,
});
}
}, [props]);
return (
<Dialog open={props.open} onClose={props.onClose}>
<Dialog.Content>
<div className={`${PREFIX}-withdraw-dialog__container`}>
<div className={`${PREFIX}-withdraw-dialog__title`}></div>
<div className={`${PREFIX}-withdraw-dialog__count`}>
{props.count}
{+props.count}
<div className="yuan"></div>
</div>
<div className={`${PREFIX}-withdraw-dialog__hint`}>500</div>
<div className={`${PREFIX}-withdraw-dialog__hint`}>200</div>
<Button className={`${PREFIX}-withdraw-dialog__confirm-button`} onClick={handleWithdraw}>
</Button>
@ -86,20 +119,28 @@ export default function PartnerKanban({ simple }: PartnerKanbanProps) {
const handleTipClose = useCallback(() => {
setTipOpen(false);
}, []);
const handleViewWithdraw = useCallback(() => {
if (stats.availableBalance < 10 * 1000) {
Toast.info('提现金额需大于等于10元');
return;
}
setWithdrawOpen(true);
}, []);
const handleWithdrawClose = useCallback(() => {
setWithdrawOpen(false);
}, []);
const getProfitStats = useCallback(async () => {
const data = await getPartnerProfitStat();
setStats(data);
}, []);
const handleViewWithdraw = useCallback(() => {
if (stats.availableBalance < 0) {
Toast.info('提现金额需大于等于0元');
return;
}
// if (stats.availableBalance < 10 * 1000) {
// Toast.error('提现金额需大于等于10元');
// return;
// }
setWithdrawOpen(true);
}, [stats.availableBalance]);
const handleWithdrawClose = useCallback(() => {
setWithdrawOpen(false);
getProfitStats();
}, [getProfitStats]);
useDidShow(() => {
getProfitStats();
});
useEffect(() => {
getProfitStats();
}, []);
@ -154,7 +195,13 @@ export default function PartnerKanban({ simple }: PartnerKanbanProps) {
)}
</div>
{!simple && <TipDialog open={tipOpen} onClose={handleTipClose} />}
{!simple && <WithdrawDialog count={350} open={withdrawOpen} onClose={handleWithdrawClose} />}
{!simple && (
<WithdrawDialog
count={Math.min(Number(formatMoney(stats.availableBalance)), 200)}
open={withdrawOpen}
onClose={handleWithdrawClose}
/>
)}
</div>
);
}

View File

@ -3,12 +3,12 @@ import classNames from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'react';
import ListPlaceholder from '@/components/list-placeholder';
import { ProfitStatusDescriptions } from '@/constants/partner';
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;
@ -22,7 +22,7 @@ const PREFIX = 'partner-profit';
const log = logWithPrefix(PREFIX);
function ProfitList(props: IPartnerProfitListProps) {
const { className, listHeight, refreshDisabled, visible = true, profitType, onListEmpty } = props;
const { className, listHeight, refreshDisabled, visible = true, type, onListEmpty } = props;
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState(false);
@ -34,7 +34,7 @@ function ProfitList(props: IPartnerProfitListProps) {
try {
setRefreshing(true);
setLoadMoreError(false);
const list = await requestData({ profitType });
const list = await requestData({ type });
setDataList(list);
!list.length && onListEmptyRef.current?.();
log('pull refresh success');
@ -65,7 +65,7 @@ function ProfitList(props: IPartnerProfitListProps) {
setDataList([]);
setLoadingMore(true);
setLoadMoreError(false);
const list = await requestData({ profitType });
const list = await requestData({ type });
setDataList(list);
!list.length && onListEmptyRef.current?.();
} catch (e) {
@ -77,7 +77,7 @@ function ProfitList(props: IPartnerProfitListProps) {
}
};
refresh();
}, [visible, profitType]);
}, [visible, type]);
return (
<div className={`${PREFIX}__tab-content`}>
@ -95,18 +95,22 @@ function ProfitList(props: IPartnerProfitListProps) {
fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
>
{dataList.map(item => (
<div className={`${PREFIX}__row`} key={item.id}>
<div className={`${PREFIX}__row-content`}>
<div className={`${PREFIX}__item time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item project`}>{PROFIT_TYPE_MAP[profitType]}</div>
<div className={`${PREFIX}__item status`}>
{profitType === ProfitType.Anchor ? '已结算' : PROFIT_STATUS_MAP[item.status]}
{dataList.map(item => {
const isChat =
type === ProfitType.CHAT_SHARE || item.earnType.toString().toLowerCase().indexOf('chat') > -1;
return (
<div className={`${PREFIX}__row`} key={item.id}>
<div className={`${PREFIX}__row-content`}>
<div className={`${PREFIX}__item time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item project`}>{isChat ? '主播被开聊' : '会员支付'}</div>
<div className={`${PREFIX}__item status`}>
{isChat ? '已结算' : ProfitStatusDescriptions[item.status]}
</div>
<div className={`${PREFIX}__item income`}>+{formatMoney(item.amount)}</div>
</div>
<div className={`${PREFIX}__item income`}>+{formatMoney(item.profit)}</div>
</div>
</div>
))}
);
})}
<ListPlaceholder hasMore={false} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List>
</PullRefresh>

View File

@ -28,6 +28,14 @@
}
}
&__help-icon {
width: 28px;
height: 28px;
margin-left: 2px;
position: relative;
top: 6px;
}
&__title {
height: 72px;
width: 100%;
@ -70,10 +78,13 @@
&:last-child {
padding-right: 0;
}
&.time,
&.project {
&.time {
flex: 2;
}
&.project {
width: 150px;
flex-shrink: 0;
}
&.status {
width: 96px;
padding: 0 8px;

View File

@ -1,7 +1,10 @@
import { Image } from '@tarojs/components';
import { Tabs } from '@taroify/core';
import PartnerKanban from '@/components/partner-kanban';
import { ProfitType } from '@/types/partner';
import Toast from '@/utils/toast';
import ProfitList from './ProfitList';
@ -21,24 +24,66 @@ function TableTitle() {
}
export default function PartnerProfit() {
const handleClickHelpChat = () => {
Toast.info('主播被开聊14天后会显示收益');
};
const handleClickHelpPay = () => {
Toast.info('会员支付15日后结算收益');
};
const handleClickHelpInvite = () => {
Toast.info('所邀合伙人获得收益后自动获得收益');
};
return (
<div className={PREFIX}>
<div className={`${PREFIX}__top`}>
<PartnerKanban />
</div>
<div className={`${PREFIX}__main`}>
<Tabs className={`${PREFIX}__tabs`}>
<Tabs.TabPane title="推荐主播收益">
<Tabs className={`${PREFIX}__tabs`} ellipsis={false}>
<Tabs.TabPane
title={
<>
<Image
className={`${PREFIX}__help-icon`}
src={require('@/statics/svg/help.svg')}
onClick={handleClickHelpChat}
/>
</>
}
>
<TableTitle />
<ProfitList profitType={ProfitType.Anchor} />
<ProfitList type={ProfitType.CHAT_SHARE} />
</Tabs.TabPane>
<Tabs.TabPane title="推荐会员权益">
<Tabs.TabPane
title={
<>
<Image
className={`${PREFIX}__help-icon`}
src={require('@/statics/svg/help.svg')}
onClick={handleClickHelpPay}
/>
</>
}
>
<TableTitle />
<ProfitList profitType={ProfitType.Member} />
<ProfitList type={ProfitType.PAYMENT_SHARE} />
</Tabs.TabPane>
<Tabs.TabPane title="推荐合伙人收益">
<Tabs.TabPane
title={
<>
<Image
className={`${PREFIX}__help-icon`}
src={require('@/statics/svg/help.svg')}
onClick={handleClickHelpInvite}
/>
</>
}
>
<TableTitle />
<ProfitList profitType={ProfitType.Partner} />
<ProfitList type={ProfitType.INDIRECT_MEMBER_REFERRAL} />
</Tabs.TabPane>
</Tabs>
</div>

View File

@ -19,7 +19,7 @@ export interface IPhoneButtonProps extends ButtonProps {
message?: string;
// 绑定后是否需要刷新接口获取手机号
needPhone?: boolean;
onBindPhone?: (status: BindPhoneStatus) => void;
onBindPhone?: (status: BindPhoneStatus, e: ITouchEvent) => void;
onSuccess?: (e: ITouchEvent) => void;
}
@ -44,17 +44,17 @@ export default function PhoneButton(props: IPhoneButtonProps) {
const encryptedData = e.detail.encryptedData;
const iv = e.detail.iv;
if (!encryptedData || !iv) {
onBindPhone?.(BindPhoneStatus.Cancel);
onBindPhone?.(BindPhoneStatus.Cancel, e as ITouchEvent);
return Toast.error('取消授权');
}
try {
await setPhoneNumber({ encryptedData, iv });
needPhone && (await requestUserInfo());
Toast.success(message);
onBindPhone?.(BindPhoneStatus.Success);
onBindPhone?.(BindPhoneStatus.Success, e as ITouchEvent);
onSuccess?.(e as ITouchEvent);
} catch (err) {
onBindPhone?.(BindPhoneStatus.Error);
onBindPhone?.(BindPhoneStatus.Error, e as ITouchEvent);
Toast.error('绑定失败');
log('bind phone fail', err);
}

View File

@ -0,0 +1,82 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.prejob-popup {
&__content {
padding: 40px 32px;
}
&__title {
font-weight: 500;
font-size: 32px;
line-height: 48px;
margin-bottom: 31px;
text-align: center;
}
&__body {
}
&__item {
.flex-row();
margin-bottom: 40px;
&:last-child {
margin-bottom: 0;
}
&-icon {
width: 88px;
height: 88px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
&.material {
background: #feba00;
> image {
width: 40px;
height: 46px;
}
}
&.vip {
background: #b094ff;
> image {
width: 48px;
height: 42px;
}
}
&.video {
background: #34a853;
> image {
width: 46px;
height: 44px;
}
}
}
&-main {
padding: 0 24px;
flex: 1;
.title {
font-weight: 500;
font-size: 28px;
line-height: 40px;
margin-bottom: 8px;
}
.desc {
font-size: 24px;
line-height: 36px;
color: @blColorG1;
}
}
&-action {
flex: 0 0 auto;
}
}
&__btn {
.button(@width: 136px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 43px, @highlight: 0);
background: #f2f2f2;
}
}

View File

@ -0,0 +1,89 @@
import { Button, Image } from '@tarojs/components';
import { Popup, Dialog } 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 { GET_CONTACT_TYPE } from '@/constants/job';
import { navigateTo } from '@/utils/route';
import './index.less';
interface IProps {
onCancel: () => void;
onConfirm: (type: GET_CONTACT_TYPE) => void;
}
const PREFIX = 'prejob-popup';
const GET_CONTACT_TYPE_OPTIONS = [
{
type: GET_CONTACT_TYPE.MATERIAL,
icon: 'https://publiccdn.neighbourhood.com.cn/img/file.svg',
title: '创建模卡(免费报单)',
desc: '免费报单,优先推荐给企业,机会更多',
btnText: '创建',
},
{
type: GET_CONTACT_TYPE.VIP,
icon: 'https://publiccdn.neighbourhood.com.cn/img/diamond.svg',
title: '播络会员',
desc: '开通会员每天可查看10个',
btnText: '开通',
},
];
export function PrejobPopup({ onCancel, onConfirm }: IProps) {
const [openPopup, setOpenPopup] = useState(true);
const [openDialog, setOpenDialog] = useState(false);
const handleClick = (type: GET_CONTACT_TYPE) => () => {
if (type === GET_CONTACT_TYPE.MATERIAL) {
navigateTo(PageUrl.MaterialUploadVideo);
onConfirm(type);
}
if (type === GET_CONTACT_TYPE.VIP) {
setOpenPopup(false);
setOpenDialog(true);
}
};
const handleAfterBuy = useCallback(async () => {
onConfirm(GET_CONTACT_TYPE.VIP);
}, [onConfirm]);
return (
<Fragment>
<Popup rounded className={PREFIX} placement="bottom" open={openPopup} onClose={onCancel}>
<div className={`${PREFIX}__content`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__body`}>
{GET_CONTACT_TYPE_OPTIONS.map(option => {
return (
<div className={`${PREFIX}__item`} key={option.type}>
<div className={`${PREFIX}__item-icon ${option.type}`}>
<Image mode="aspectFit" src={option.icon} />
</div>
<div className={`${PREFIX}__item-main`}>
<div className="title">{option.title}</div>
<div className="desc">{option.desc}</div>
</div>
<div className={`${PREFIX}__item-action`}>
<Button className={`${PREFIX}__btn`} onClick={handleClick(option.type)}>
{option.btnText}
</Button>
</div>
</div>
);
})}
</div>
</div>
<SafeBottomPadding />
</Popup>
<Dialog open={openDialog} onClose={onCancel}>
<Dialog.Content>
<JobBuy onConfirm={handleAfterBuy} buyOnly />
</Dialog.Content>
</Dialog>
</Fragment>
);
}

View File

@ -3,6 +3,7 @@ export const PREFIX = 'product-dialog';
export enum DialogStatus {
// 加载中
LOADING = 'loading',
PRE_ACTION = 'pre_action',
// 直接联系通告主
JOB_CONTACT_DIRECT = 'job_contact_direct',
// 联系客服去联系通告主 -> 订阅通知

View File

@ -82,7 +82,10 @@ function ProductGroupDialog(props: IProps) {
return;
}
// 否则:如果有解锁次数,显示是否确定消费。无解锁次数,显示不无次数 UI
const [time, detail] = await Promise.all([requestProductBalance(PRODUCT_CODE), requestGroupDetail(blGroupId)]);
const [[time], detail] = await Promise.all([
requestProductBalance(PRODUCT_CODE),
requestGroupDetail(blGroupId),
]);
setGroupDetail(detail);
if (time <= 0) {
setStatus(DialogStatus.GROUP_NEED_BUY_ADD);

View File

@ -133,6 +133,7 @@
&__header {
.flex-row();
.header-font();
font-size: 32px;
.highlight {
color: @blHighlightColor;
@ -143,7 +144,8 @@
&__describe {
.flex-row();
.describe-font();
margin-top: 24px;
margin-top: 18px;
line-height: 40px;
.highlight {
color: @blHighlightColor;
@ -158,13 +160,13 @@
&__item {
position: relative;
width: 170px;
width: 182px;
height: 192px;
.flex-column();
justify-content: center;
border: 2px solid @blHighlightColor;
border-radius: 8px;
margin-right: 24px;
margin-right: 15px;
&:last-child {
margin-right: 0;
@ -660,4 +662,4 @@
}
}
}
}

View File

@ -102,7 +102,7 @@ function ProductJobWithGroupDialog(props: Omit<IProps, 'visible'>) {
return;
}
// 否则:如果有解锁次数,显示是否确定消费。无解锁次数,显示不无次数 UI
const time = await requestProductBalance(ProductType.AddGroup);
const [time] = await requestProductBalance(ProductType.AddGroup);
if (time <= 0) {
setStatus(DialogStatus.JOB_CONTACT_NEED_BUY_GROUP);
} else {
@ -119,7 +119,7 @@ function ProductJobWithGroupDialog(props: Omit<IProps, 'visible'>) {
return;
}
// 自动报单
const time = await requestProductBalance(ProductType.GetJob);
const [time] = await requestProductBalance(ProductType.GetJob);
if (time <= 0) {
setStatus(DialogStatus.JOB_UNABLE_UNLOCK);
} else {

View File

@ -41,7 +41,7 @@ function ProductJobDialog(props: Omit<IProps, 'visible'>) {
}, [onClose]);
const handleAfterBuy = useCallback(async () => {
const time = await requestProductBalance(PRODUCT_CODE);
const [time] = await requestProductBalance(PRODUCT_CODE);
if (time <= 0) {
Toast.error('发生错误请重试');
onClose();
@ -83,7 +83,7 @@ function ProductJobDialog(props: Omit<IProps, 'visible'>) {
handleContact(result.declarationTypeResult);
return;
}
const time = await requestProductBalance(PRODUCT_CODE);
const [time] = await requestProductBalance(PRODUCT_CODE);
if (time <= 0) {
const allowBuy = await requestAllBuyProduct(PRODUCT_CODE);
setStatus(allowBuy ? DialogStatus.JOB_BUY : DialogStatus.JOB_UNABLE_UNLOCK);

View File

@ -99,12 +99,8 @@ export function CompanyPublishJobDialog(props: IProps) {
const handleNext = useCallback(async () => {
onClose(true);
if (userInfo.bossAuthStatus) {
navigateTo(PageUrl.JobPublish);
} else {
navigateTo(PageUrl.CertificationStart);
}
}, [userInfo, onClose]);
navigateTo(PageUrl.JobPublish);
}, [onClose]);
useEffect(() => {
initRef.current = async () => {
@ -115,7 +111,7 @@ export function CompanyPublishJobDialog(props: IProps) {
try {
const productCode = ProductType.CompanyPublishJob;
Taro.showLoading();
const time = await requestProductBalance(productCode);
const [time] = await requestProductBalance(productCode);
if (time <= 0) {
setStatus(DialogStatus.COMPANY_PUBLISH_JOB_BUY);
return;

View File

@ -16,6 +16,7 @@ import { postSubscribe, subscribeMessage } from '@/utils/subscribe';
import Toast from '@/utils/toast';
interface IProps {
defaultSelect?: number;
onNext: () => void;
}
@ -85,8 +86,8 @@ const subscribe = async () => {
};
export default function CompanyPublishJobBuy(props: IProps) {
const { onNext } = props;
const [selectItem, setSelectItem] = useState(LIST[0]);
const { onNext, defaultSelect = 0 } = props;
const [selectItem, setSelectItem] = useState(LIST[defaultSelect]);
const handleClickItem = useCallback((newSelectItem: Item) => setSelectItem(newSelectItem), []);

View File

@ -70,7 +70,7 @@ export default function GroupBuy(props: IProps) {
if (status !== OrderStatus.Success) {
throw new Error('order status error');
}
const time = await requestProductBalance(ProductType.AddGroup);
const [time] = await requestProductBalance(ProductType.AddGroup);
log('handleBuy new addGroupTime', time);
onConfirm(time);
} catch (e) {

View File

@ -16,6 +16,7 @@ import { postSubscribe, subscribeMessage } from '@/utils/subscribe';
import Toast from '@/utils/toast';
interface IProps {
buyOnly?: boolean;
onConfirm: () => void;
}
@ -23,6 +24,7 @@ interface Item {
id: ProductSpecId;
title: string;
content: string;
buyOnlyContent?: string;
price: string;
amt: number;
badge?: string;
@ -31,17 +33,19 @@ interface Item {
const LIST: Item[] = [
{ id: ProductSpecId.WeeklyVIP, title: '非会员', content: '每日2次', price: '免费', amt: 0 },
{
id: ProductSpecId.WeeklyVIP,
title: '会员',
content: '每日5次',
id: ProductSpecId.DailyVIP,
title: '会员',
content: '每日+10次',
buyOnlyContent: '每日12次',
price: '60播豆',
amt: 6,
badge: '限时体验',
},
{
id: ProductSpecId.NewMonthlyVIP,
title: '会员',
content: '每日5次',
id: ProductSpecId.WeeklyVIP,
title: '会员',
content: '每日+10次',
buyOnlyContent: '每日12次',
price: '180播豆',
amt: 18,
badge: ' 超值',
@ -62,7 +66,7 @@ const subscribe = async () => {
};
export default function JobBuy(props: IProps) {
const { onConfirm } = props;
const { onConfirm, buyOnly } = props;
const [selectItem, setSelectItem] = useState(LIST[1]);
const handleClickItem = useCallback((newSelectItem: Item) => setSelectItem(newSelectItem), []);
@ -106,32 +110,47 @@ export default function JobBuy(props: IProps) {
return (
<div className={`${PREFIX}__job-buy`}>
<div className={`${PREFIX}__job-buy__header`}>
<div></div>
<div className="highlight"></div>
</div>
<div className={`${PREFIX}__job-buy__describe`}>
<div></div>
<div className="highlight"></div>
<div> </div>
<div className="highlight"></div>
</div>
{buyOnly ? (
<div className={`${PREFIX}__job-buy__header`}></div>
) : (
<div className={`${PREFIX}__job-buy__header`}>
<div></div>
<div className="highlight"></div>
</div>
)}
{buyOnly ? (
<div className={`${PREFIX}__job-buy__describe`}>12</div>
) : (
<div className={`${PREFIX}__job-buy__describe`}>
<div></div>
<div className="highlight"></div>
<div> </div>
<div className="highlight"></div>
</div>
)}
<div className={`${PREFIX}__job-buy__container`}>
{LIST.map(item => (
<div
key={item.price}
className={classNames(`${PREFIX}__job-buy__item`, {
selected: item.amt === selectItem.amt,
disabled: item.amt === 0,
})}
onClick={item.amt === 0 ? undefined : () => handleClickItem(item)}
>
<div className={classNames(`${PREFIX}__job-buy__item__title`, { free: item.amt === 0 })}>{item.title}</div>
<div className={`${PREFIX}__job-buy__item__content`}>{item.content}</div>
<div className={`${PREFIX}__job-buy__item__price`}>{item.price}</div>
{item.badge && <Badge className={`${PREFIX}__job-buy__item__badge`} text={item.badge} />}
</div>
))}
{LIST.map(item => {
if (buyOnly && !item.amt) {
return null;
}
return (
<div
key={item.price}
className={classNames(`${PREFIX}__job-buy__item`, {
selected: item.amt === selectItem.amt,
disabled: item.amt === 0,
})}
onClick={item.amt === 0 ? undefined : () => handleClickItem(item)}
>
<div className={classNames(`${PREFIX}__job-buy__item__title`, { free: item.amt === 0 })}>
{item.title}
</div>
<div className={`${PREFIX}__job-buy__item__content`}>{buyOnly ? item.buyOnlyContent : item.content}</div>
<div className={`${PREFIX}__job-buy__item__price`}>{item.price}</div>
{item.badge && <Badge className={`${PREFIX}__job-buy__item__badge`} text={item.badge} />}
</div>
);
})}
</div>
<Button className={`${PREFIX}__job-buy__button`} onClick={handleBuy}>
{`支付 ${selectItem.amt}`}

View File

@ -0,0 +1,162 @@
@import '@/styles/variables.less';
@import '@/styles/common.less';
.search-city {
background: #fff;
&__position-title {
font-size: 24px;
color: @blColorG1;
padding: 0 24px;
margin-top: 18px;
}
&__position-city {
font-size: 30px;
font-weight: bold;
color: @blColor;
padding: 0 24px;
margin-top: 18px;
}
&__hot-city-title {
height: 48px;
font-size: 24px;
line-height: 48px;
padding: 0 24px;
color: #999;
background: #f2f5f7;
margin-top: 18px;
}
&__hot-city-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: space-between;
width: 630px;
padding: 12px 90px 26px 30px;
background: #ffffff;
}
&__hot-city-item {
width: 140px;
height: 58px;
font-size: 28px;
line-height: 58px;
text-align: center;
border-radius: 58px;
border: 2px solid @blColorG1;
margin-top: 18px;
}
&__indexes-list {
width: 100%;
/* 兼容 iOS < 11.2 */
padding-bottom: constant(safe-area-inset-bottom);
/* 兼容 iOS >= 11.2 */
padding-bottom: env(safe-area-inset-bottom);
}
&__indexes-fragment {
}
&__indexes-anchor {
height: 48px;
font-size: 24px;
line-height: 48px;
padding: 0 24px;
color: @blColorG1;
background: #f2f5f7;
}
&__indexes-cell {
position: relative;
font-size: 28px;
padding: 30px 24px;
color: @blColor;
&::after {
content: '';
position: absolute;
border-bottom: 1rpx solid #eaeef1;
transform: scaleY(0.5);
bottom: 0;
right: 0;
left: 24px;
}
}
&__indexes-bar {
width: 44rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: fixed;
right: 10px;
}
&__indexes-bar-item {
font-size: 22px;
color: @blColor;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
}
&__indexes-index-alert {
position: absolute;
z-index: 20;
width: 160px;
height: 160px;
left: 50%;
top: 50%;
margin-left: -80px;
margin-top: -80px;
border-radius: 80px;
text-align: center;
line-height: 160px;
font-size: 70px;
color: #ffffff;
background-color: rgba(0, 0, 0, 0.5);
}
&__search-wrapper {
&.group {
background: linear-gradient(2.75deg, #ffffff 7.9%, #f2edff 97.24%);
.search-city__search {
background-color: transparent;
.taroify-search__content--rounded {
border: @blHighlightColor 1px solid;
}
}
}
}
&__banner {
padding: 32px 24px 0;
text-align: center;
font-size: 28px;
line-height: 40px;
color: @blHighlightColor;
.flex-row();
.text {
padding: 0 12px;
flex: 0 1 auto;
}
.dash {
height: 1px;
flex: 1;
&:first-child {
background: linear-gradient(270deg, #6d3df5 0%, #f7f4ff 100%);
}
&:last-child {
background: linear-gradient(90deg, #6d3df5 0%, #f7f4ff 100%);
}
}
}
}

View File

@ -0,0 +1,241 @@
import { BaseEventOrig, InputProps, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Search } from '@taroify/core';
import { useCallback, useEffect, useState } from 'react';
import { CITY_CODE_TO_NAME_MAP, CITY_INDEXES_LIST, GROUP_CITY_INDEXES_LIST } from '@/constants/city';
import { logWithPrefix } from '@/utils/common';
import './index.less';
interface Item {
cityCode: number | string;
cityName: string;
keyword: string;
}
const PREFIX = 'search-city';
const HOT_CITY = [
{ cityCode: 110100, cityName: '北京' },
{ cityCode: 310100, cityName: '上海' },
{ cityCode: 440100, cityName: '广州' },
{ cityCode: 440300, cityName: '深圳' },
{ cityCode: 330100, cityName: '杭州' },
{ cityCode: 430100, cityName: '长沙' },
{ cityCode: 420100, cityName: '武汉' },
{ cityCode: 350200, cityName: '厦门' },
{ cityCode: 610100, cityName: '西安' },
{ cityCode: 410100, cityName: '郑州' },
{ cityCode: 510100, cityName: '成都' },
{ cityCode: 340100, cityName: '合肥' },
];
const OFFSET_INDEX_SIZE = 2;
const log = logWithPrefix(PREFIX);
const realtimeLogger = Taro.getRealtimeLogManager();
realtimeLogger.tag(PREFIX);
const useHeight = () => {
const [winHeight, setWinHeight] = useState(0);
const [indexItemHeight, setIndexItemHeight] = useState(0);
useEffect(() => {
const windowInfo = Taro.getWindowInfo();
const windowHeight = windowInfo.windowHeight;
setWinHeight(windowHeight);
// 上下预留两个选项高度的空白
setIndexItemHeight(Math.floor(windowHeight / (26 + OFFSET_INDEX_SIZE * 2)));
}, []);
return [winHeight, indexItemHeight];
};
export interface SearchCityProps {
onSelectCity: (cityCode: string) => void;
currentCity?: string;
forGroup?: boolean;
banner?: string;
offset?: number;
}
export default function SearchCity({
onSelectCity,
currentCity = '',
banner = '',
forGroup = false,
offset = 0,
}: SearchCityProps) {
const [winHeight, indexItemHeight] = useHeight();
const [touchAnchor, setTouchAnchor] = useState<string | undefined>();
const [touchMoving, setTouchMoving] = useState(false);
const [searchResult, setSearchResult] = useState<Item[]>([]);
const showSearchList = searchResult.length > 0;
const CITY_LIST = forGroup ? GROUP_CITY_INDEXES_LIST : CITY_INDEXES_LIST;
const handleSearchChange = useCallback((event: BaseEventOrig<InputProps.inputEventDetail>) => {
const value = event.detail.value;
log('handleSearchChange', value);
if (!value) {
setSearchResult([]);
return;
}
const result: Item[] = [];
CITY_LIST.forEach(obj => {
obj.data.forEach(city => {
if (city.keyword.includes(value.toLocaleUpperCase())) {
result.push({ ...city });
}
});
});
setSearchResult(result);
}, []);
const handleSelectCity = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const cityCode = e.currentTarget.dataset.code;
onSelectCity(String(cityCode));
},
[onSelectCity]
);
const handleTouchStart = useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const pageY = e.touches[0].pageY;
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
if (index < 0 || index >= CITY_LIST.length) {
return;
}
const item = CITY_LIST[index];
if (item) {
setTouchMoving(true);
setTouchAnchor(item.letter);
}
},
[indexItemHeight]
);
const handleTouchMove = useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const pageY = e.touches[0].pageY;
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
if (index < 0 || index >= CITY_LIST.length) {
return;
}
const item = CITY_LIST[index];
item && setTouchAnchor(item.letter);
},
[indexItemHeight]
);
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLDivElement>) => {
e.stopPropagation();
setTouchMoving(false);
log('touch end');
}, []);
const handleClickAnchor = useCallback((anchor: string) => {
setTouchAnchor(anchor);
log('click anchor', anchor);
}, []);
return (
<div className={PREFIX}>
<ScrollView scrollY style={{ height: winHeight - offset }} scrollIntoView={touchAnchor}>
<div className={`${PREFIX}__search-wrapper ${forGroup ? 'group' : ''}`}>
{forGroup && banner ? (
<div className={`${PREFIX}__banner`}>
<div className="dash"></div>
<div className="text">{banner}</div>
<div className="dash"></div>
</div>
) : null}
<Search
className={`${PREFIX}__search`}
placeholder="输入城市名称"
shape="rounded"
onChange={handleSearchChange}
/>
</div>
{showSearchList && (
<div className={`${PREFIX}__search-list`}>
{searchResult.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__indexes-cell`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
)}
{!showSearchList && (
<div>
<div className={`${PREFIX}__position-title`}></div>
<div className={`${PREFIX}__position-city`}>{CITY_CODE_TO_NAME_MAP.get(currentCity)}</div>
<div className={`${PREFIX}__hot-city-title`}></div>
<div className={`${PREFIX}__hot-city-container`}>
{HOT_CITY.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__hot-city-item`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
<div className={`${PREFIX}__indexes-list`}>
{CITY_LIST.map(item => {
return (
<div key={item.letter} className={`${PREFIX}__indexes-fragment`}>
<div className={`${PREFIX}__indexes-anchor`} id={item.letter}>
{item.letter}
</div>
{item.data.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__indexes-cell`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
);
})}
</div>
</div>
)}
</ScrollView>
<div>
{!showSearchList && (
<div
className={`${PREFIX}__indexes-bar`}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchEnd}
style={{ top: indexItemHeight * OFFSET_INDEX_SIZE + (forGroup ? 72 : 0) }}
>
{CITY_LIST.map(item => {
return (
<div
key={item.letter}
className={`${PREFIX}__indexes-bar-item`}
style={{ height: indexItemHeight }}
onClick={() => handleClickAnchor(item.letter)}
>
{item.letter}
</div>
);
})}
</div>
)}
{touchAnchor && touchMoving && <div className={`${PREFIX}__indexes-index-alert`}>{touchAnchor}</div>}
</div>
</div>
);
}

View File

@ -1,14 +1,11 @@
import { Tabbar } from '@taroify/core';
import classNames from 'classnames';
import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { APP_TAB_BAR_ID, RoleType, PageType, PageUrl } from '@/constants/app';
import { ANCHOR_TAB_LIST, COMPANY_TAB_LIST, TabItemType } from '@/hooks/use-config';
import useMessage from '@/hooks/use-message';
import useRoleType from '@/hooks/user-role-type';
import { changeHomePage } from '@/store/actions';
import { selectHomePageType } from '@/store/selector';
import { logWithPrefix } from '@/utils/common';
import { switchTab } from '@/utils/route';
@ -16,6 +13,7 @@ import './index.less';
interface IProps {
className?: string;
type: PageType;
}
const PREFIX = 'base-tab-bar';
@ -40,25 +38,21 @@ const TabItem = (props: { item: TabItemType }) => {
);
};
function BaseTabBar(props: IProps) {
const { className } = props;
function BaseTabBar({ className, type }: IProps) {
const roleType = useRoleType();
const currentPage = useSelector(selectHomePageType);
const dispatch = useDispatch();
const tabs = roleType === RoleType.Anchor ? ANCHOR_TAB_LIST : COMPANY_TAB_LIST;
const handleTabClick = useCallback(
(value: PageType) => {
if (value === currentPage) {
if (value === type) {
return;
}
dispatch(changeHomePage(value));
const item = tabs.find((i: TabItemType) => i.type === value);
log('tab bar changed', value, item?.pagePath);
item && switchTab(item.pagePath as PageUrl);
},
[tabs, currentPage, dispatch]
[tabs, type]
);
// useEffect(() => {
@ -72,14 +66,14 @@ function BaseTabBar(props: IProps) {
return (
<div className={classNames(`${PREFIX}__wrapper`, className)} id={APP_TAB_BAR_ID}>
<Tabbar className={PREFIX} defaultValue={currentPage}>
<Tabbar className={PREFIX} value={type}>
{tabs.map((item: TabItemType) => {
return (
<Tabbar.TabItem
key={item.pagePath}
value={item.type}
onClick={() => handleTabClick(item.type)}
className={classNames(`${PREFIX}__item`, { selected: item.type === currentPage })}
className={classNames(`${PREFIX}__item`, { selected: item.type === type })}
>
<TabItem item={item} />
</Tabbar.TabItem>

View File

@ -0,0 +1,89 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.user-batch-publish {
padding: 16px 24px;
&__header-image {
width: 100%;
height: 120px;
margin-top: 24px;
}
&__title {
font-size: 32px;
line-height: 48px;
font-weight: 500;
color: @blColor;
margin-top: 24px;
&:first-child {
margin-top: 0;
}
}
&__cell {
height: 100px;
padding-left: 32px;
padding-right: 32px;
border-radius: 16px;
margin-top: 24px;
}
&__cost-describe {
height: 100px;
padding: 0 32px;
border-radius: 16px;
.flex-row();
justify-content: space-between;
background: #FFFFFF;
margin-top: 24px;
&__price {
font-size: 48px;
line-height: 48px;
font-weight: 500;
color: @blHighlightColor;
}
&__original_price {
flex: 1;
font-size: 32px;
line-height: 34px;
font-weight: 400;
color: @blColorG1;
margin-left: 16px;
text-decoration: line-through;
}
}
&__illustrate {
padding: 24px 32px;
margin-top: 24px;
font-size: 28px;
line-height: 48px;
font-weight: 400;
color: @blColorG2;
background: #FFFFFF;
border-radius: 16px;
&__describe {
.flex-row();
font-size: 28px;
line-height: 48px;
font-weight: 400;
color: @blColorG2;
margin-top: 8px;
&__view {
color: @blHighlightColor;
margin-left: 4px;
}
}
}
&__buy-button {
.button(@width: 100%; @height: 80px; @fontSize: 32px);
margin-top: 40px;
}
}

View File

@ -0,0 +1,202 @@
import { Button, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Cell } from '@taroify/core';
import { useCallback, useState, useEffect } from 'react';
import PageLoading from '@/components/page-loading';
import { PublishJobQrCodeDialog } from '@/components/product-dialog/publish-job';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { ISelectOption, PopupSelect } from '@/components/select';
import { PageUrl } from '@/constants/app';
import { OrderStatus, OrderType, ProductSpecId, ProductType } from '@/constants/product';
import { BatchPublishGroup } from '@/types/group';
import { logWithPrefix } from '@/utils/common';
import {
getOrderPrice,
isCancelPay,
requestAllBuyProduct,
requestCreatePayInfo,
requestOrderInfo,
requestPayment,
} from '@/utils/product';
import { navigateTo } from '@/utils/route';
import Toast from '@/utils/toast';
import './index.less';
interface CityValue extends BatchPublishGroup {
cityName: string;
}
interface CityOption extends ISelectOption<CityValue> {
value: CityValue;
}
const PREFIX = 'user-batch-publish';
const log = logWithPrefix(PREFIX);
const SERVICE_ILLUSTRATE = `服务方式:帮您把招聘需求发到众多同城合作主播群
群发次数每日1次连发3天
内容要求:仅限带货主播招聘需求,其他不发
主播联系:内容中留招聘方联系方式,主播直接联系`;
const cityValues: CityValue[] = [
{ cityCode: '440100', cityName: '广州', count: 800 },
{ cityCode: '440300', cityName: '深圳', count: 100 },
{ cityCode: '330100', cityName: '杭州', count: 750 },
{ cityCode: '110100', cityName: '北京', count: 150 },
{ cityCode: '510100', cityName: '成都', count: 100 },
{ cityCode: '500100', cityName: '重庆', count: 50 },
{ cityCode: '430100', cityName: '长沙', count: 50 },
{ cityCode: '350200', cityName: '厦门', count: 50 },
{ cityCode: '310100', cityName: '上海', count: 150 },
{ cityCode: '420100', cityName: '武汉', count: 80 },
{ cityCode: '610100', cityName: '西安', count: 60 },
{ cityCode: '410100', cityName: '郑州', count: 150 },
].sort((a, b) => b.count - a.count);
const MIN_GROUP_SIZE = 20;
const GROUP_OPTIONS = [
{ value: MIN_GROUP_SIZE, productSpecId: ProductSpecId.GroupBatchPublish20, label: '20', price: 18 },
{ value: 50, productSpecId: ProductSpecId.GroupBatchPublish50, label: '50', price: 40 },
{ value: 60, productSpecId: ProductSpecId.GroupBatchPublish60, label: '60', price: 48 },
{ value: 80, productSpecId: ProductSpecId.GroupBatchPublish80, label: '80', price: 58 },
{ value: 100, productSpecId: ProductSpecId.GroupBatchPublish100, label: '100', price: 68 },
{ value: 150, productSpecId: ProductSpecId.GroupBatchPublish150, label: '150', price: 98 },
{ value: 300, productSpecId: ProductSpecId.GroupBatchPublish300, label: '300', price: 128 },
{ value: 500, productSpecId: ProductSpecId.GroupBatchPublish500, label: '500', price: 168 },
{ value: 750, productSpecId: ProductSpecId.GroupBatchPublish750, label: '750', price: 188 },
{ value: 800, productSpecId: ProductSpecId.GroupBatchPublish800, label: '800', price: 198 },
{ value: 1000, productSpecId: ProductSpecId.GroupBatchPublish1000, label: '1000', price: 288 },
];
const calcPrice = (city: CityValue | null) => {
if (!city) {
return {};
}
const { count } = city;
const originalPrice = count * 1;
const price = GROUP_OPTIONS.find(o => o.value === count)?.price || 18;
const productSpecId = GROUP_OPTIONS.find(o => o.value === count)?.productSpecId || ProductSpecId.GroupBatchPublish20;
return { price, originalPrice, productSpecId };
};
export default function UserBatchPublish() {
const [loading, setLoading] = useState(true);
const [showCitySelect, setShowCitySelect] = useState(false);
const [showQrCode, setShowQrCode] = useState(false);
const [city, setCity] = useState<CityOption['value'] | null>(null);
const [cityOptions, setCityOptions] = useState<CityOption[]>([]);
const { price, originalPrice, productSpecId } = calcPrice(city);
const handleClickCity = useCallback(() => setShowCitySelect(true), []);
const handleSelectCity = useCallback(value => {
setCity(value);
setShowCitySelect(false);
}, []);
const handleClickViewGroup = useCallback(() => navigateTo(PageUrl.GroupList, { city: city?.cityCode }), [city]);
const handleClickBuy = useCallback(async () => {
// if (1 < 2) {
// await new Promise(r => setTimeout(r, 3000));
// setShowQrCode(true);
// return;
// }
if (!price || !productSpecId) {
return;
}
try {
Taro.showLoading();
const allowBuy = await requestAllBuyProduct(ProductType.GroupBatchPublish);
if (!allowBuy) {
Taro.hideLoading();
Toast.info('您最近已购买过,可直接联系客服');
setShowQrCode(true);
return;
}
const { payOrderNo, createPayInfo } = await requestCreatePayInfo({
type: OrderType.GroupBatchPublish,
amt: getOrderPrice(price),
// amt: 1,
productCode: ProductType.GroupBatchPublish,
productSpecId: productSpecId,
});
log('handleBuy payInfo', payOrderNo, createPayInfo);
await requestPayment({
timeStamp: createPayInfo.timeStamp,
nonceStr: createPayInfo.nonceStr,
package: createPayInfo.packageVal,
signType: createPayInfo.signType,
paySign: createPayInfo.paySign,
});
const { status } = await requestOrderInfo({ payOrderNo });
log('handleBuy orderInfo', status);
if (status !== OrderStatus.Success) {
throw new Error('order status error');
}
Taro.hideLoading();
setShowQrCode(true);
} catch (e) {
Taro.hideLoading();
Toast.error(isCancelPay(e) ? '取消购买' : '购买失败请重试');
log('handleBuy error', e);
}
}, [price, productSpecId]);
useEffect(() => {
try {
const cOptions: CityOption[] = cityValues.map(value => ({ value, label: value.cityName }));
const initCity = (cOptions.find(o => o.label === '重庆') || cOptions[0]).value;
setLoading(false);
setCity(initCity);
setCityOptions(cOptions);
log('init data done', cOptions);
} catch (e) {
Toast.error('加载失败请重试');
}
}, []);
if (loading) {
return <PageLoading />;
}
return (
<div className={PREFIX}>
{/*<Image mode="widthFix" className={`${PREFIX}__header-image`} src="https://neighbourhood.cn/pubJob.png" />*/}
<div className={`${PREFIX}__title`}></div>
<Cell isLink align="center" className={`${PREFIX}__cell`} title={city?.cityName} onClick={handleClickCity} />
<div className={`${PREFIX}__title`}></div>
<Cell align="center" className={`${PREFIX}__cell`} title={city?.count} />
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__cost-describe`}>
<div className={`${PREFIX}__cost-describe__price`}>{`${price}`}</div>
<div className={`${PREFIX}__cost-describe__original_price`}>{`原价:${originalPrice}`}</div>
</div>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__illustrate`}>
<Text>{SERVICE_ILLUSTRATE}</Text>
<div className={`${PREFIX}__illustrate__describe`}>
<div></div>
<div className={`${PREFIX}__illustrate__describe__view`} onClick={handleClickViewGroup}>
</div>
</div>
</div>
<Button className={`${PREFIX}__buy-button`} onClick={handleClickBuy}>
</Button>
<SafeBottomPadding />
<div>
<PopupSelect
value={city}
options={cityOptions}
open={showCitySelect}
onSelect={handleSelectCity}
onClose={() => setShowCitySelect(false)}
/>
<PublishJobQrCodeDialog onClose={() => setShowQrCode(false)} open={showQrCode} />
</div>
</div>
);
}

View File

@ -74,6 +74,7 @@ export enum PageUrl {
PrivacyWebview = 'pages/privacy-webview/index',
Partner = 'pages/partner/index',
WithdrawRecord = 'pages/withdraw-record/index',
GroupDelegatePublish = 'pages/group-delegate-publish/index',
}
export enum PluginUrl {

View File

@ -10,4 +10,7 @@ export enum CacheKey {
LAST_SELECT_MY_JOB = '__last_select_my_job__',
CLOSE_PARTNER_BANNER = '__close_partner_banner__',
INVITE_CODE = '__invite_code__',
AGREEMENT_SIGNED = '__agreement_signed__',
CITY_CODES = '__city_codes__',
JOIN_GROUP_CARD_CLICKED = '__join_group_card_clicked__',
}

View File

@ -5859,3 +5859,191 @@ export const CITY_INDEXES_LIST = [
],
},
];
export const GROUP_CITY_INDEXES_LIST = [
{
letter: 'B',
data: [
{
cityCode: '110100',
cityName: '北京',
keyword: 'BEIJING北京',
},
],
},
{
letter: 'C',
data: [
{
cityCode: '500100',
cityName: '重庆',
keyword: 'CHONGQING重庆',
},
{
cityCode: '510100',
cityName: '成都',
keyword: 'CHENGDOU成都',
},
{
cityCode: '430100',
cityName: '长沙',
keyword: 'CHANGSHA长沙',
},
],
},
{
letter: 'D',
data: [
{
cityCode: '441900',
cityName: '东莞',
keyword: 'DONGGUAN东莞',
},
],
},
{
letter: 'F',
data: [
{
cityCode: '440600',
cityName: '佛山',
keyword: 'FUSHAN佛山',
},
{
cityCode: '350100',
cityName: '福州',
keyword: 'FUZHOU福州',
},
],
},
{
letter: 'G',
data: [
{
cityCode: '440100',
cityName: '广州',
keyword: 'GUANGZHOU广州',
},
],
},
{
letter: 'H',
data: [
{
cityCode: '330100',
cityName: '杭州',
keyword: 'HANGZHOU杭州',
},
{
cityCode: '340100',
cityName: '合肥',
keyword: 'HEFEI合肥',
},
],
},
{
letter: 'K',
data: [
{
cityCode: '530100',
cityName: '昆明',
keyword: 'KUNMING昆明',
},
],
},
{
letter: 'N',
data: [
{
cityCode: '320100',
cityName: '南京',
keyword: 'NANJING南京',
},
],
},
{
letter: 'Q',
data: [
{
cityCode: '370200',
cityName: '青岛',
keyword: 'QINGDAO青岛',
},
{
cityCode: '350500',
cityName: '泉州',
keyword: 'QUANZHOU泉州',
},
],
},
{
letter: 'S',
data: [
{
cityCode: '310100',
cityName: '上海',
keyword: 'SHANGHAI上海',
},
{
cityCode: '440300',
cityName: '深圳',
keyword: 'SHENZHEN深圳',
},
{
cityCode: '320500',
cityName: '苏州',
keyword: 'SUZHOU苏州',
},
],
},
{
letter: 'T',
data: [
{
cityCode: '120100',
cityName: '天津',
keyword: 'TIANJIN天津',
},
],
},
{
letter: 'W',
data: [
{
cityCode: '330300',
cityName: '温州',
keyword: 'WENZHOU温州',
},
{
cityCode: '420100',
cityName: '武汉',
keyword: 'WUHAN武汉',
},
],
},
{
letter: 'X',
data: [
{
cityCode: '610100',
cityName: '西安',
keyword: 'XIAN西安',
},
{
cityCode: '350200',
cityName: '厦门',
keyword: 'XIAMEN厦门',
},
],
},
{
letter: 'Z',
data: [
{
cityCode: '410100',
cityName: '郑州',
keyword: 'ZHENGZHOU郑州',
},
],
},
];

View File

@ -1,3 +1,5 @@
import { GroupItem } from '@/types/group';
export enum GroupType {
// 所有可加入的群
All = 'ALL',
@ -29,3 +31,30 @@ export const GROUP_STATUS_OPTIONS = [
{ label: '全部', value: GroupStatus.All },
{ label: '已申请', value: GroupStatus.Requested },
];
export const GROUPS: GroupItem[] = [
{ title: '【杭州】', cityCode: 330100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc223f495e159af95e' },
{ title: '【广州】', cityCode: 440100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb4b88b8abb7a7c8b' },
{ title: '【深圳】', cityCode: 440300, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfe70d8736e14bb64' },
{ title: '【北京】', cityCode: 110100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb119c94575e91262' },
{ title: '【上海】', cityCode: 310100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4189e68429cf07f8' },
{ title: '【郑州】', cityCode: 410100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd1c53b7bf8ecdb97' },
{ title: '【长沙】', cityCode: 430100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc76be8f2b3f8aa437' },
{ title: '【成都】', cityCode: 510100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf75cefbdc62946fa' },
{ title: '【重庆】', cityCode: 500100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd7008f747d545f83' },
{ title: '【武汉】', cityCode: 420100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc047c94f8c709b395' },
{ title: '【厦门】', cityCode: 350200, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc2007a895cb48464b' },
{ title: '【西安】', cityCode: 610100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc34768971b7354220' },
{ title: '【合肥】', cityCode: 340100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc41c9785cc2035277' },
{ title: '【南京】', cityCode: 320100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc6dc8d0a9692b70e' },
{ title: '【青岛】', cityCode: 370200, serviceUrl: 'https://work.weixin.qq.com/kfid/kfce8d7a68190f6a1d2' },
{ title: '【佛山】', cityCode: 440600, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfac1132df386fac8' },
{ title: '【东莞】', cityCode: 441900, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb2b0e39026f7dddc' },
{ title: '【苏州】', cityCode: 320500, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4642f90a6e3528ff' },
{ title: '【福州】', cityCode: 350100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc126483dedadde82b' },
{ title: '【泉州】', cityCode: 350500, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4c8c42b1a9337aaf' },
{ title: '【温州】', cityCode: 330300, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb0ea5f197a18b335' },
{ title: '【天津】', cityCode: 120100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcda46c23dade6f6a3' },
{ title: '【昆明】', cityCode: 530100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf2aebcbf3d46d9cd' },
{ title: '【全国】', cityCode: 440300, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc60ac7b6420787a8' },
];

View File

@ -174,3 +174,8 @@ export const FULL_EMPLOY_SALARY_OPTIONS = [
export const PART_PRICE_OPTIONS = PART_EMPLOY_SALARY_OPTIONS.filter(o => !!o.value);
export const FULL_PRICE_OPTIONS = FULL_EMPLOY_SALARY_OPTIONS.filter(o => !!o.value);
export enum GET_CONTACT_TYPE {
VIP = 'vip',
MATERIAL = 'material',
}

View File

@ -1,21 +1,60 @@
export enum ProfitType {
Anchor = '1',
Member = '2',
Partner = '3',
}
export enum ProfitStatus {
AVAILABLE = '1',
WITHDRAWING = '2',
WITHDRAW = '3',
/**
* 待处理/待计算 (例如,等待上游数据或条件满足)
*/
PENDING_CALCULATION = 'PENDING_CALCULATION',
/**
* 待直接结算/待直接分账 (例如T+7 到账)
* 这种类型的佣金会计入银行账户,而非平台余额
*/
DIRECT_SETTLEMENT_PENDING = 'DIRECT_SETTLEMENT_PENDING',
/**
* 直接结算已完成/直接分账已完成
*/
DIRECT_SETTLEMENT_PROCESSING = 'DIRECT_SETTLEMENT_PROCESSING',
/**
* 间接收益已结算到合伙人余额 (例如,主播推荐奖金进入可提现余额)
*/
INDIRECT_SETTLED_TO_BALANCE = 'INDIRECT_SETTLED_TO_BALANCE',
/**
* 佣金已取消 (例如,订单退款,不满足条件等)
*/
CANCELLED = 'CANCELLED',
/**
* 佣金处理失败
*/
FAILED = 'FAILED',
/**
* 其他状态
*/
OTHER = 'OTHER',
/**
* 已完成
*/
FINISHED = 'FINISHED',
}
export const PROFIT_TYPE_MAP = {
[ProfitType.Anchor]: '主播被开聊',
[ProfitType.Member]: '会员支付',
[ProfitType.Partner]: '合伙人收益分成',
// 如果需要为每个枚举值添加描述,可以使用一个单独的映射对象
export const ProfitStatusDescriptions = {
PENDING_CALCULATION: '',
DIRECT_SETTLEMENT_PENDING: '待分账',
DIRECT_SETTLEMENT_PROCESSING: '',
INDIRECT_SETTLED_TO_BALANCE: '',
CANCELLED: '',
FAILED: '',
OTHER: '',
FINISHED: '已分账',
};
export const PROFIT_STATUS_MAP = {
[ProfitStatus.AVAILABLE]: '可提现',
[ProfitStatus.WITHDRAWING]: '提现中',
[ProfitStatus.WITHDRAW]: '已提现',
export const WithdrawStatusDescriptions = {
0: '提现中',
1: '已提现',
2: '失败',
};

View File

@ -27,14 +27,20 @@ export enum ProductSpecId {
AddGroup2 = 'ADDGROUP_2',
AddGroup3 = 'ADDGROUP_3',
BossVip = 'BOSSVIP',
DailyVIP = 'VIP_D',
WeeklyVIP = 'VIP_W',
MonthlyVIP = 'VIP_M', // 30 每天十次
NewMonthlyVIP = 'VIP_M_NEW', // 18 每天五次
GroupBatchPublish20 = 'GROUP_BATCH_PUSH_20',
GroupBatchPublish50 = 'GROUP_BATCH_PUSH_50',
GroupBatchPublish60 = 'GROUP_BATCH_PUSH_60',
GroupBatchPublish80 = 'GROUP_BATCH_PUSH_80',
GroupBatchPublish100 = 'GROUP_BATCH_PUSH_100',
GroupBatchPublish150 = 'GROUP_BATCH_PUSH_150',
GroupBatchPublish300 = 'GROUP_BATCH_PUSH_300',
GroupBatchPublish500 = 'GROUP_BATCH_PUSH_500',
GroupBatchPublish750 = 'GROUP_BATCH_PUSH_750',
GroupBatchPublish800 = 'GROUP_BATCH_PUSH_800',
GroupBatchPublish1000 = 'GROUP_BATCH_PUSH_1000',
BOSS_PUB_JOB_1 = 'BOSS_PUB_JOB_1', // 旧版企业发通告会员
BOSS_VIP_NEW_1 = 'BOSS_VIP_NEW_1', // 新版企业发通告会员 - 周

View File

@ -12,8 +12,8 @@ import { EmployType, EMPLOY_TYPE_TITLE_MAP, FULL_PRICE_OPTIONS, PART_PRICE_OPTIO
import { SalaryRange } from '@/types/job';
import { MaterialProfile } from '@/types/material';
import { logWithPrefix } from '@/utils/common';
import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job'
import { getCurrentCity } from '@/utils/location';
import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
import { getCurrentCityCode } from '@/utils/location';
import { navigateTo } from '@/utils/route';
import './index.less';
@ -62,7 +62,7 @@ const getSalaryPrice = (fullSalary?: SalaryRange, partSalary?: SalaryRange) => {
function ProfileIntentionFragment(props: IProps, ref) {
const { profile } = props;
const [cityCodes, setCityCodes] = useState(calcInitCityCodes(profile.cityCodes));
const [cityCodes, setCityCodes] = useState(calcInitCityCodes(profile.cityCodes || ''));
const [employType, setEmployType] = useState(profile.employType);
const [fullSalary, setFullSalary] = useState(calcSalarySelect(profile, true));
const [partSalary, setPartSalary] = useState(calcSalarySelect(profile, false));
@ -76,9 +76,9 @@ function ProfileIntentionFragment(props: IProps, ref) {
);
const handleClickAddCity = useCallback(() => {
const currentCity = getCurrentCity();
const currentCityCode = getCurrentCityCode();
realtimeLogger.info('handleClickAddCity', OpenSource.AddIndentCity);
navigateTo(PageUrl.CitySearchProfile, { city: currentCity, source: OpenSource.AddIndentCity });
navigateTo(PageUrl.CitySearchProfile, { city: currentCityCode, source: OpenSource.AddIndentCity });
}, []);
const handleSelectCity = useCallback(
@ -98,10 +98,10 @@ function ProfileIntentionFragment(props: IProps, ref) {
const handleEmployTypeChange = useCallback((value: EmployType) => {
setEmployType(value);
if (value === EmployType.Full) {
setPartSalary({ minSalary: 0, maxSalary: 0 })
setPartSalary({ minSalary: 0, maxSalary: 0 });
}
if (value === EmployType.Part) {
setFullSalary({ minSalary: 0, maxSalary: 0 })
setFullSalary({ minSalary: 0, maxSalary: 0 });
}
}, []);
@ -173,24 +173,26 @@ function ProfileIntentionFragment(props: IProps, ref) {
<BlFormRadio name={EmployType.All} text={EMPLOY_TYPE_TITLE_MAP[EmployType.All]} value={employType} />
</BlFormRadioGroup>
</BlFormItem>
{isFullTimePriceRequired(employType) &&
(<BlFormItem title="期望全职薪资">
{isFullTimePriceRequired(employType) && (
<BlFormItem title="期望全职薪资">
<BlFormSelect
title="期望全职薪资"
value={fullSalary}
options={FULL_PRICE_OPTIONS}
onSelect={handleSelectFullSalary}
/>
</BlFormItem>)}
{isPartTimePriceRequired(employType) &&
(<BlFormItem title="期望兼职薪资">
</BlFormItem>
)}
{isPartTimePriceRequired(employType) && (
<BlFormItem title="期望兼职薪资">
<BlFormSelect
title="期望兼职薪资"
value={partSalary}
options={PART_PRICE_OPTIONS}
onSelect={handleSelectPartSalary}
/>
</BlFormItem>)}
</BlFormItem>
)}
<BlFormItem title="是否接受坐班">
<BlFormRadioGroup direction="horizontal" value={acceptWorkForSit} onChange={handleAcceptSittingChange}>
<BlFormRadio name text="接受" value={acceptWorkForSit} />

View File

@ -44,7 +44,7 @@
font-size: 28px;
font-weight: 400;
color: #ffffff;
background: #0000005c;
background: #6D3DF5B2;
border-radius: 48px;
}
@ -232,4 +232,8 @@
}
}
}
.join-group-hint {
margin: 24px;
}
}

View File

@ -17,6 +17,7 @@ import { navigateTo } from '@/utils/route';
import './index.less';
import onChangeEventDetail = SwiperProps.onChangeEventDetail;
import { JoinGroupHint } from '@/components/join-group-hint';
interface IProps {
editable: boolean;
@ -207,6 +208,7 @@ export default function ProfileViewFragment(props: IProps) {
>{`${coverIndex + 1}/${profile.materialVideoInfoList.length}`}</div>
</div>
</div>
{!editable && <JoinGroupHint />}
<div className={`${PREFIX}__body`}>
<div className={`${PREFIX}__basic-info`}>
<div className={`${PREFIX}__basic-info__name-container`}>
@ -228,7 +230,9 @@ export default function ProfileViewFragment(props: IProps) {
{index ? (
<div className={`${PREFIX}__info-group__header__title`}>{data.title}</div>
) : (
<DevDiv className={`${PREFIX}__info-group__header__title`} OnDev={onDev}>{data.title}</DevDiv>
<DevDiv className={`${PREFIX}__info-group__header__title`} OnDev={onDev}>
{data.title}
</DevDiv>
)}
{editable && (
<div className={`${PREFIX}__info-group__header__edit`} onClick={() => handleEditGroupItem(data.type)}>

View File

@ -5,7 +5,6 @@ import { useCallback } from 'react';
import CertificationStatus from '@/components/certification-status';
import WechatCell from '@/components/wx-cell';
import { PageUrl } from '@/constants/app';
import { CertificationStatusType } from '@/constants/company';
import useUserInfo from '@/hooks/use-user-info';
import { navigateTo } from '@/utils/route';
@ -18,10 +17,6 @@ export default function CompanyFragment() {
// const [showPublish, setShowPublish] = useState(false);
const handlePublishJob = useCallback(async () => {
if (userInfo.bossAuthStatus !== CertificationStatusType.Success) {
navigateTo(PageUrl.CertificationStart);
return;
}
navigateTo(PageUrl.CertificationManage);
}, [userInfo]);

View File

@ -0,0 +1,10 @@
import { useSelector } from 'react-redux';
import { selectCreatedJob } from '@/store/selector/job';
function useCachedJob() {
const data = useSelector(selectCreatedJob);
return data;
}
export default useCachedJob;

View File

@ -48,7 +48,7 @@ const CompanyTabs: TabItemType[] = [
{
type: PageType.BatchPublish,
pagePath: PageUrl.UserBatchPublish,
text: '代发',
text: '代招代发',
},
];
@ -97,6 +97,7 @@ export const APP_CONFIG: AppConfigType = {
PageUrl.PrivacyWebview,
PageUrl.Partner,
PageUrl.WithdrawRecord,
PageUrl.GroupDelegatePublish,
// PageUrl.DevDebug,
],
window: {

13
src/hooks/use-previous.ts Normal file
View File

@ -0,0 +1,13 @@
import { useEffect, useRef } from 'react';
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export default usePrevious;

View File

@ -1,9 +1,10 @@
import { useSelector } from 'react-redux';
import { RoleType } from '@/constants/app';
import { selectRoleType } from '@/store/selector';
function useRoleType() {
return useSelector(selectRoleType);
return useSelector(selectRoleType) || RoleType.Anchor;
}
export default useRoleType;

View File

@ -1,4 +1,4 @@
// export const DOMAIN = 'http://192.168.60.120:8082';
// export const DOMAIN = 'http://192.168.60.148:8082';
export const DOMAIN = 'https://neighbourhood.cn';
export const BASE_URL = `${DOMAIN}/api`;
@ -78,8 +78,10 @@ export enum API {
// partner
PARTNER_QRCODE = '/user/getInviteQrCode',
GET_INVITE_CODE = '/user/getUserInviteCode',
GET_INVITE_LIST = '/user/inviteUsers',
GET_INVITE_LIST = '/user/inviteUsers/list',
BECOME_PARTNER = '/user/becomePartner',
GET_PROFIT_LIST = '/profit/profits',
GET_PROFIT_LIST = '/user/profit/list',
GET_PROFIT_STAT = '/user/profits',
WITHDRAW = '/user/withdraw',
GET_WITHDRAW_LIST = '/user/withdraw/records',
}

View File

@ -26,6 +26,7 @@ export enum RESPONSE_ERROR_CODE {
INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE', // 聊天或者模卡查看超出限制
INSUFFICIENT_FREE_BALANCE = 'INSUFFICIENT_FREE_BALANCE', // 免费查看次数(未购买会员)超限
BOSS_VIP_EXPIRED = 'BOSS_VIP_EXPIRED', // 会员过期
CHAT_MSG_SEND_NOT_ALLOW = 'CHAT_MSG_SEND_NOT_ALLOW',
}
export const RESPONSE_ERROR_INFO: { [key in RESPONSE_ERROR_CODE]?: string } = {

View File

@ -107,14 +107,14 @@ class Http {
url: BASE_URL + url,
data,
method: method,
header: { 'content-type': contentType /*, 'user-Id': '588002047871053824' */ },
header: { 'content-type': contentType /*'user-Id': '588002047871053824' */ },
};
return this.request(option);
};
async init() {
async init(inviteCode?: string) {
if (isTokenExpired()) {
await refreshToken();
await refreshToken(inviteCode);
}
}

View File

@ -1,6 +1,6 @@
import Taro from '@tarojs/taro';
import { getRoleType } from '@/utils/app';
import { getRoleTypeWithDefault } from '@/utils/app';
import { isDev } from '@/utils/common';
import { getToken } from './utils';
@ -20,7 +20,7 @@ const tokenInterceptor: Taro.interceptor = (chain: Taro.Chain) => {
const roleInterceptor: Taro.interceptor = (chain: Taro.Chain) => {
const requestParams = chain.requestParams;
const roleType = getRoleType();
const roleType = getRoleTypeWithDefault();
requestParams.header = {
...requestParams.header,
'role-type': roleType,

View File

@ -24,10 +24,10 @@ const clearToken = () => {
Taro.setStorageSync(TOKEN_EXPIRES_TIME, 0);
};
const requestToken = (): Promise<string> => {
const requestToken = (inviteCode?: string): Promise<string> => {
return getCode()
.then(code => {
return http.post<LoginResponse>(API.LOGIN, { data: { code } }).then(data => {
return http.post<LoginResponse>(API.LOGIN, { data: { code, inviteCode } }).then(data => {
const newToken = data?.token || '';
const expires = data?.expires || 0;
if (newToken) {
@ -47,12 +47,12 @@ const requestToken = (): Promise<string> => {
export const isTokenExpired = () => (Taro.getStorageSync(TOKEN_EXPIRES_TIME) || 0) < Date.now();
export const refreshToken = () => {
export const refreshToken = (inviteCode?: string) => {
if (_fetchTokenPromise) {
return _fetchTokenPromise;
}
clearToken();
_fetchTokenPromise = requestToken();
_fetchTokenPromise = requestToken(inviteCode);
return _fetchTokenPromise;
};

View File

@ -1,7 +1,7 @@
import { Image } from '@tarojs/components';
import Taro, { NodesRef, useDidShow, useLoad, useShareAppMessage } from '@tarojs/taro';
import { ArrowUp, ArrowDown } from '@taroify/icons';
import { ArrowDown, ArrowUp } from '@taroify/icons';
import classNames from 'classnames';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react';
@ -14,7 +14,7 @@ import Overlay from '@/components/overlay';
import PageLoading from '@/components/page-loading';
import PartnerBanner from '@/components/partner-banner';
import SwitchBar from '@/components/switch-bar';
import { APP_TAB_BAR_ID, EventName, OpenSource, PageUrl } from '@/constants/app';
import { APP_TAB_BAR_ID, EventName, OpenSource, PageType, PageUrl, RoleType } from '@/constants/app';
import { EmployType, JobManageStatus } from '@/constants/job';
import { ALL_ANCHOR_SORT_TYPES, ANCHOR_SORT_TYPE_TITLE_MAP, AnchorSortType } from '@/constants/material';
import useInviteCode from '@/hooks/use-invite-code';
@ -23,6 +23,7 @@ import useLocation from '@/hooks/use-location';
import { JobManageInfo } from '@/types/job';
import { Coordinate } from '@/types/location';
import { IAnchorFilters } from '@/types/material';
import { switchRoleType } from '@/utils/app';
import { logWithPrefix } from '@/utils/common';
import { getLastSelectMyJobId, requestJobManageList, setLastSelectMyJobId } from '@/utils/job';
import { getWxLocation } from '@/utils/location';
@ -165,13 +166,15 @@ export default function AnchorPage() {
}, [location]);
useLoad(async () => {
switchRoleType(RoleType.Company);
const query = getPageQuery();
getInviteCodeFromQueryAndUpdate(query);
try {
const { jobResults = [] } = await requestJobManageList({ status: JobManageStatus.Open });
if (!jobResults.length) {
Toast.info('当前是根据定位为您展示主播');
// Toast.info('当前是根据定位为您展示主播');
return;
}
const lastSelectJobId = getLastSelectMyJobId();
@ -186,13 +189,13 @@ export default function AnchorPage() {
});
useShareAppMessage(() => {
return getCommonShareMessage(true, inviteCode);
return getCommonShareMessage({ inviteCode, title: '数万名优质主播等你来挑', path: PageUrl.Anchor });
});
useDidShow(() => requestUnreadMessageCount());
return (
<HomePage>
<HomePage type={PageType.Anchor}>
{!!loading && <PageLoading className={`${PREFIX}__loading`} />}
<CustomNavigationBar className={`${PREFIX}__navigation-bar`}>
{selectJob && <SwitchBar title={selectJob.title.substring(0, 4)} onClick={handleClickSwitch} />}

View File

@ -6,14 +6,15 @@ import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import JobManageList, { IJobManageListProps } from '@/components/job-manage-list';
import { CompanyPublishJobDialog } from '@/components/product-dialog/publish-job';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { PageUrl } from '@/constants/app';
import { ReportEventId } from '@/constants/event';
import { JOB_MANAGE_TABS, JobManageStatus, JobManageType } from '@/constants/job';
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
import useUserInfo from '@/hooks/use-user-info';
import { logWithPrefix } from '@/utils/common';
import { reportEvent } from '@/utils/event';
import { navigateTo } from '@/utils/route';
import { ensureUserInfo } from '@/utils/user';
import './index.less';
@ -91,7 +92,6 @@ export default function CertificationManage() {
const userInfo = useUserInfo();
const listHeight = useListHeight(CALC_LIST_PROPS);
const [tabType, setTabType] = useState<JobManageType>(JobManageType.All);
const [showPublish, setShowPublish] = useState(false);
const handleTypeChange = useCallback(value => setTabType(value), []);
@ -101,7 +101,7 @@ export default function CertificationManage() {
if (!(await ensureUserInfo(userInfo))) {
return;
}
setShowPublish(true);
navigateTo(PageUrl.JobPublish);
}, [userInfo]);
return (
@ -124,7 +124,6 @@ export default function CertificationManage() {
</Button>
</div>
<SafeBottomPadding className={SAFE_BOTTOM_PADDING_CLASS} />
<div>{showPublish && <CompanyPublishJobDialog userInfo={userInfo} onClose={() => setShowPublish(false)} />}</div>
</div>
);
}

View File

@ -7,7 +7,7 @@ import BlFormInput from '@/components/bl-form-input';
import BlFormItem from '@/components/bl-form-item';
import LoadingDialog from '@/components/loading-dialog';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { PageUrl } from '@/constants/app';
import { EventName, PageUrl } from '@/constants/app';
import { CertificationStatusType } from '@/constants/company';
import { CollectEventName, ReportEventId } from '@/constants/event';
import useUserInfo from '@/hooks/use-user-info';
@ -16,12 +16,18 @@ import { isValidIdCard, isValidPhone, logWithPrefix } from '@/utils/common';
import { postCertification, getPhone } from '@/utils/company';
import { collectEvent, reportEvent } from '@/utils/event';
import { chooseMedia } from '@/utils/material';
import { redirectTo } from '@/utils/route';
import { redirectTo } from '@/utils/route';
import Toast from '@/utils/toast';
import { dispatchUpdateUser, requestUserInfo } from '@/utils/user';
import { uploadVideo } from '@/utils/video';
import './index.less';
import { requestProductBalance } from '@/utils/product';
import { ProductType } from '@/constants/product';
import useCachedJob from '@/hooks/use-cached-job';
import { postCreateJob } from '@/utils/job';
import { Dialog } from '@taroify/core';
import CompanyPublishJobBuy from '@/components/product-dialog/steps-ui/company-publish-job-buy';
const PREFIX = 'page-certification';
const log = logWithPrefix(PREFIX);
@ -100,6 +106,8 @@ export default function Certification() {
// const [code, setCode] = useState('');
const [company, setCompany] = useState('');
const [open, setOpen] = useState(false);
const [showBuy, setShowBuy] = useState(false);
const cachedJobData = useCachedJob();
const handleClickIdCardLeft = useCallback(async () => {
reportEvent(ReportEventId.CLICK_UPLOAD_ID_CARD, { type: 'left' });
@ -133,6 +141,26 @@ export default function Certification() {
setCompany(value);
}, []);
const handleCreateJob = useCallback(async () => {
if (cachedJobData.employType) {
Taro.showLoading();
try {
await postCreateJob(cachedJobData);
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
await Toast.success('通告创建成功', 1500, true);
redirectTo(PageUrl.CertificationManage);
} catch (e) {
console.error('submit error', e);
Toast.error('通告审核失败请重试');
collectEvent(CollectEventName.PUBLISH_JOB_FAILED, e);
} finally {
Taro.hideLoading();
}
} else {
Toast.error('数据出错');
}
}, [cachedJobData]);
const handleSubmit = useCallback(async () => {
reportEvent(ReportEventId.CLICK_CERTIFICATION_SUBMIT);
const data: ICertificationRequest = {
@ -157,7 +185,16 @@ export default function Certification() {
return;
}
dispatchUpdateUser({ bossAuthStatus: CertificationStatusType.Success });
redirectTo(PageUrl.CertificationManage);
// 判断付钱没有
const [time] = await requestProductBalance(ProductType.CompanyPublishJob);
if (time <= 0) {
// 付钱
setShowBuy(true);
Taro.hideLoading();
} else {
// 获取data创建
await handleCreateJob();
}
} catch (e) {
console.error('submit error', e);
Toast.error('认证失败请重试');
@ -262,6 +299,11 @@ export default function Certification() {
<div>
<LoadingDialog open={open} text="认证中" />
</div>
<Dialog open={showBuy} onClose={() => setShowBuy(false)}>
<Dialog.Content>
<CompanyPublishJobBuy onNext={handleCreateJob} defaultSelect={1} />
</Dialog.Content>
</Dialog>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { Button } from '@taroify/core';
import { useCallback } from 'react';
import HomePage from '@/components/home-page';
import { PageUrl } from '@/constants/app';
import { PageType, PageUrl } from '@/constants/app';
import { copy, logWithPrefix } from '@/utils/common';
import { navigateTo } from '@/utils/route';
@ -136,7 +136,7 @@ export default function DevDebug() {
});
return (
<HomePage>
<HomePage type={PageType.DEV}>
<div className={PREFIX}>
{/* <div>{`最近一次的订单 ID: ${lastOrderNo}`}</div>
<Button onClick={() => handleCreateOrder(OrderType.Group)} style={marginTopStyle} color="primary">

View File

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '群代发',
});

View File

@ -0,0 +1,12 @@
import UserBatchPublish from '@/components/user-batch-publish';
import './index.less';
const PREFIX = 'group-delegate-publish';
export default function Partner() {
return (
<div className={PREFIX}>
<UserBatchPublish />
</div>
);
}

View File

@ -2,77 +2,7 @@
@import '@/styles/variables.less';
.group-v2-page {
padding: 24px;
&__header {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 32px;
&__left-line,
&__right-line {
width: 88px;
height: 1px;
}
&__left-line {
background: linear-gradient(270deg, #CCCCCC -0.05%, rgba(204, 204, 204, 0) 99.95%);
}
&__right-line {
background: linear-gradient(90deg, #CCCCCC 0%, rgba(204, 204, 204, 0) 100%);
}
&__title {
font-size: 28px;
line-height: 40px;
font-weight: 400;
color: @blColorG2;
margin: 0 16px;
}
}
&__group-card {
.flex-row();
width: 100%;
padding: 32px;
background: #FFF;
border-radius: 16px;
margin-top: 24px;
box-sizing: border-box;
&:first-child {
margin-top: 0;
}
&__avatar {
width: 88px;
height: 88px;
border-radius: 6px;
border: 4px solid #D9D9D9;
}
&__title {
flex: 1;
font-size: 32px;
line-height: 40px;
font-weight: 500;
color: @blColor;
align-self: flex-start;
margin-left: 36px;
}
&__button {
.button(@width: 176px; @height: 56px; @fontSize: 28px; @fontWeight: 500);
}
}
&__bottom-padding {
width: 100%;
height: 24px;
}
}
padding-bottom: 200px;
height: 100vh;
box-sizing: border-box;
}

View File

@ -1,101 +1,57 @@
import { Image } from '@tarojs/components';
import { NodesRef, useLoad, useShareAppMessage } from '@tarojs/taro';
import { useLoad, useShareAppMessage } from '@tarojs/taro';
import { List } from '@taroify/core';
import { useCallback } from 'react';
import HomePage from '@/components/home-page';
import LoginButton from '@/components/login-button';
import { APP_TAB_BAR_ID } from '@/constants/app';
import SearchCity from '@/components/search-city';
import { PageType, PageUrl, RoleType } from '@/constants/app';
import { GROUPS } from '@/constants/group';
import useInviteCode from '@/hooks/use-invite-code';
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
import { switchRoleType } from '@/utils/app';
import { openCustomerServiceChat } from '@/utils/common';
import { getCurrentCityCode } from '@/utils/location';
import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner';
import { getPageQuery } from '@/utils/route';
import { getCommonShareMessage } from '@/utils/share';
import { checkCityCode } from '@/utils/user';
import './index.less';
interface GroupItem {
title: string;
serviceUrl: string;
}
const PREFIX = 'group-v2-page';
const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`;
const CALC_LIST_PROPS: IUseListHeightProps = {
selectors: [`.${LIST_CONTAINER_CLASS}`, `#${APP_TAB_BAR_ID}`],
calc: (rects: [NodesRef.BoundingClientRectCallbackResult, NodesRef.BoundingClientRectCallbackResult]) => {
const [rect, diffRect] = rects;
return diffRect.top - rect.top;
},
};
const GROUPS: GroupItem[] = [
{ title: '【杭州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc223f495e159af95e' },
{ title: '【广州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb4b88b8abb7a7c8b' },
{ title: '【深圳】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfe70d8736e14bb64' },
{ title: '【北京】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb119c94575e91262' },
{ title: '【上海】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4189e68429cf07f8' },
{ title: '【郑州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd1c53b7bf8ecdb97' },
{ title: '【长沙】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc76be8f2b3f8aa437' },
{ title: '【成都】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf75cefbdc62946fa' },
{ title: '【重庆】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd7008f747d545f83' },
{ title: '【武汉】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc047c94f8c709b395' },
{ title: '【厦门】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc2007a895cb48464b' },
{ title: '【西安】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc34768971b7354220' },
{ title: '【合肥】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc41c9785cc2035277' },
{ title: '【南京】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc6dc8d0a9692b70e' },
{ title: '【青岛】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfce8d7a68190f6a1d2' },
{ title: '【佛山】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfac1132df386fac8' },
{ title: '【东莞】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb2b0e39026f7dddc' },
{ title: '【苏州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4642f90a6e3528ff' },
{ title: '【福州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc126483dedadde82b' },
{ title: '【泉州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4c8c42b1a9337aaf' },
{ title: '【温州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb0ea5f197a18b335' },
{ title: '【天津】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcda46c23dade6f6a3' },
{ title: '【昆明】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf2aebcbf3d46d9cd' },
{ title: '【全国】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc60ac7b6420787a8' },
];
export default function GroupV2() {
const listHeight = useListHeight(CALC_LIST_PROPS);
const inviteCode = useInviteCode();
const handleClick = useCallback((group: GroupItem) => openCustomerServiceChat(group.serviceUrl), []);
useLoad(() => {
switchRoleType(RoleType.Anchor);
const query = getPageQuery();
getInviteCodeFromQueryAndUpdate(query);
});
useShareAppMessage(() => getCommonShareMessage(true, inviteCode));
useShareAppMessage(() =>
getCommonShareMessage({ inviteCode, title: '邀请你加入本地主播求职招聘群', path: PageUrl.GroupV2 })
);
const handleSelectCity = useCallback(cityCode => {
if (!checkCityCode(cityCode)) {
return;
}
const group = GROUPS.find(g => String(g.cityCode) === cityCode);
if (group) {
openCustomerServiceChat(group.serviceUrl);
}
}, []);
return (
<HomePage>
<HomePage type={PageType.GroupV2}>
<div className={PREFIX}>
<div className={`${PREFIX}__header`}>
<div className={`${PREFIX}__header__left-line`} />
<div className={`${PREFIX}__header__title`}></div>
<div className={`${PREFIX}__header__right-line`} />
</div>
<div className={LIST_CONTAINER_CLASS}>
<List style={{ height: `${listHeight}px` }} disabled fixedHeight>
{GROUPS.map(group => (
<div className={`${PREFIX}__group-card`} key={group.serviceUrl}>
<Image
mode="aspectFit"
className={`${PREFIX}__group-card__avatar`}
src="https://neighbourhood.cn/addGroup.jpg"
/>
<div className={`${PREFIX}__group-card__title`}>{group.title}</div>
<LoginButton className={`${PREFIX}__group-card__button`} onClick={() => handleClick(group)}>
</LoginButton>
</div>
))}
<div className={`${PREFIX}__bottom-padding`} />
</List>
</div>
<SearchCity
onSelectCity={handleSelectCity}
currentCity={getCurrentCityCode()}
forGroup
offset={72}
banner="点击城市加入本地主播招聘群,高薪职位早知道"
/>
</div>
</HomePage>
);

View File

@ -4,6 +4,7 @@ import { Tabs } from '@taroify/core';
import { useCallback, useState } from 'react';
import HomePage from '@/components/home-page';
import { PageType } from '@/constants/app';
import { GroupType, GROUP_PAGE_TABS } from '@/constants/group';
import GroupFragment from '@/fragments/group';
import useNavigation from '@/hooks/use-navigation';
@ -22,7 +23,7 @@ export default function Group() {
useShareAppMessage(() => getCommonShareMessage());
return (
<HomePage>
<HomePage type={PageType.Group}>
<Tabs
swipeable
value={tabType}

View File

@ -7,14 +7,15 @@ import { CertificationStatusIcon } from '@/components/certification-status';
import CommonDialog from '@/components/common-dialog';
import DevDiv from '@/components/dev-div';
import JobRecommendList from '@/components/job-recommend-list';
import { JoinGroupHint } from '@/components/join-group-hint';
import LoginButton from '@/components/login-button';
import MaterialGuide from '@/components/material-guide';
import PageLoading from '@/components/page-loading';
import { PrejobPopup } from '@/components/prejob-popup';
import ProductJobDialog from '@/components/product-dialog/job';
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 { EMPLOY_TYPE_TITLE_MAP, GET_CONTACT_TYPE } from '@/constants/job';
import useInviteCode from '@/hooks/use-invite-code';
import useUserInfo from '@/hooks/use-user-info';
import useRoleType from '@/hooks/user-role-type';
@ -22,6 +23,7 @@ import { RESPONSE_ERROR_CODE } from '@/http/constant';
import { HttpError } from '@/http/error';
import { JobDetails } from '@/types/job';
import { IMaterialMessage } from '@/types/message';
import { switchRoleType } from '@/utils/app';
import { copy, logWithPrefix } from '@/utils/common';
import { collectEvent, reportEvent } from '@/utils/event';
import { getJobTitle, getJobSalary, postPublishJob, requestJobDetail } from '@/utils/job';
@ -74,9 +76,12 @@ const AnchorFooter = (props: { data: JobDetails }) => {
reportEvent(ReportEventId.CLICK_JOB_CONTACT);
try {
const needCreateMaterial = await isNeedCreateMaterial();
if (needCreateMaterial) {
setShowMaterialGuide(true);
return;
if (data.sourcePlat !== 'bl') {
if (needCreateMaterial) {
setShowMaterialGuide(true);
return;
}
}
if (data.isAuthed) {
const toUserId = data.userId;
@ -84,19 +89,27 @@ const AnchorFooter = (props: { data: JobDetails }) => {
Toast.error('不能与自己聊天');
return;
}
const profile = await requestProfileDetail();
const chat = await postCreateChat(toUserId);
const materialMessage: IMaterialMessage = {
id: profile.id,
name: profile.name,
age: profile.age,
height: profile.height,
weight: profile.weight,
shoeSize: profile.shoeSize,
gender: profile.gender,
workedSecCategoryStr: profile.workedSecCategoryStr,
};
navigateTo(PageUrl.MessageChat, { chatId: chat.chatId, material: materialMessage, jobId: data.id });
let materialMessage: null | IMaterialMessage = null;
if (!needCreateMaterial) {
const profile = await requestProfileDetail();
materialMessage = {
id: profile.id,
name: profile.name,
age: profile.age,
height: profile.height,
weight: profile.weight,
shoeSize: profile.shoeSize,
gender: profile.gender,
workedSecCategoryStr: profile.workedSecCategoryStr,
};
}
navigateTo(PageUrl.MessageChat, {
chatId: chat.chatId,
initText: !materialMessage,
material: materialMessage,
jobId: data.id,
});
} else {
setDialogVisible(true);
}
@ -111,8 +124,15 @@ const AnchorFooter = (props: { data: JobDetails }) => {
}
}, [data]);
const handleDialogHidden = useCallback(() => setDialogVisible(false), []);
const handleDialogHidden = useCallback(() => {
setDialogVisible(false);
}, []);
const handleConfirmPrejob = useCallback((type: GET_CONTACT_TYPE) => {
setShowMaterialGuide(false);
if (GET_CONTACT_TYPE.VIP === type) {
setDialogVisible(true);
}
}, []);
return (
<>
<div className={`${PREFIX}__footer`}>
@ -125,7 +145,9 @@ const AnchorFooter = (props: { data: JobDetails }) => {
</div>
<div>
{dialogVisible && <ProductJobDialog data={data} onClose={handleDialogHidden} />}
{showMaterialGuide && <MaterialGuide onClose={() => setShowMaterialGuide(false)} />}
{showMaterialGuide && (
<PrejobPopup onCancel={() => setShowMaterialGuide(false)} onConfirm={handleConfirmPrejob} />
)}
<CommonDialog
content={errorTips}
confirm="确定"
@ -219,7 +241,11 @@ export default function JobDetail() {
}, []);
useLoad(async () => {
const query = getPageQuery<Pick<JobDetails, 'id'> & { c: string }>();
const query = getPageQuery<Pick<JobDetails, 'id'> & { c: string; share: string }>();
if (query?.share === 'true') {
switchRoleType(RoleType.Anchor);
}
getInviteCodeFromQueryAndUpdate(query);
const jobId = query?.id;
if (!jobId) {
@ -236,8 +262,9 @@ export default function JobDetail() {
useShareAppMessage(() => {
if (!data) {
return getCommonShareMessage(true, inviteCode);
return getCommonShareMessage({ inviteCode });
}
return {
title: getJobTitle(data) || '',
path: getJumpUrl(PageUrl.JobDetail, { id: data.id, share: true, c: inviteCode }),
@ -279,6 +306,8 @@ export default function JobDetail() {
{data.companyName && <div className={`${PREFIX}__company`}>{`公司:${data.companyName}`}</div>}
</div>
{!isOwner && <JoinGroupHint />}
<div className={`${PREFIX}__content`}>
<div className={`${PREFIX}__content-title`}></div>
<div className={`${PREFIX}__tags`}>

View File

@ -1,6 +1,7 @@
import { BaseEventOrig, Button, InputProps } from '@tarojs/components';
import Taro, { useLoad } from '@tarojs/taro';
import { Dialog } from '@taroify/core';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
@ -12,15 +13,29 @@ import BlFormSelect from '@/components/bl-form-select';
import BlSalaryInput, { BlSalaryValue } from '@/components/bl-salary-input';
import { CityPickerPopup } from '@/components/city-picker';
import PageLoading from '@/components/page-loading';
import CompanyPublishJobBuy from '@/components/product-dialog/steps-ui/company-publish-job-buy';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { EventName, PageUrl } from '@/constants/app';
import { CertificationStatusType } from '@/constants/company';
import { CollectEventName } from '@/constants/event';
import { EMPLOY_TYPE_TITLE_MAP, EmployType, JOB_TYPE_SELECT_OPTIONS, JobType } from '@/constants/job';
import { ProductType } from '@/constants/product';
import useUserInfo from '@/hooks/use-user-info';
import store from '@/store';
import { cacheCreateJob } from '@/store/actions';
import { CreateJobInfo, JobDetails } from '@/types/job';
import { logWithPrefix } from '@/utils/common';
import { collectEvent } from '@/utils/event';
import { postCloseJob, postCreateJob, postUpdateJob, requestJobDetail, isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
import {
postCloseJob,
postCreateJob,
postUpdateJob,
requestJobDetail,
isFullTimePriceRequired,
isPartTimePriceRequired,
} from '@/utils/job';
import { getCityValues } from '@/utils/location';
import { requestProductBalance } from '@/utils/product';
import { getPageQuery, navigateBack, navigateTo } from '@/utils/route';
import Toast from '@/utils/toast';
@ -91,6 +106,8 @@ export default function JobPublish() {
const [city, setCity] = useState<string[] | undefined>();
const [showCityPicker, setShowCityPicker] = useState(false);
const [address, setAddress] = useState('');
const [showBuy, setShowBuy] = useState(false);
const userInfo = useUserInfo();
const handleEmployTypeChange = useCallback((value: EmployType) => {
setEmployType(value);
@ -144,7 +161,7 @@ export default function JobPublish() {
}
}, [job]);
const handleSubmit = useCallback(async () => {
const getCreateJobInfo = useCallback((): [CreateJobInfo, string[]] => {
const cityCodes = city || [];
const data: CreateJobInfo = {
title,
@ -160,6 +177,11 @@ export default function JobPublish() {
highPriceForPartyTime: !isPartTimePriceRequired(employType) ? 0 : salary[3],
address: address,
};
return [data, cityCodes];
}, [address, category, city, describe, employType, salary, title]);
const handleSubmit = useCallback(async () => {
const [data, cityCodes] = getCreateJobInfo();
const errMsg = isInvalidCreateJobInfo(data);
if (errMsg) {
Toast.info(errMsg);
@ -178,6 +200,20 @@ export default function JobPublish() {
await postUpdateJob(data);
Taro.eventCenter.trigger(EventName.JOB_UPDATE, job!.id);
} else {
if (userInfo.bossAuthStatus !== CertificationStatusType.Success) {
// 去认证
store.dispatch(cacheCreateJob(data));
navigateTo(PageUrl.CertificationStart);
Taro.hideLoading();
return;
}
const [time] = await requestProductBalance(ProductType.CompanyPublishJob);
if (time <= 0) {
// 付钱
setShowBuy(true);
Taro.hideLoading();
return;
}
await postCreateJob(data);
}
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
@ -190,7 +226,30 @@ export default function JobPublish() {
} finally {
Taro.hideLoading();
}
}, [isUpdate, job, title, employType, category, describe, city, salary, address]);
}, [getCreateJobInfo, isUpdate, job, userInfo.bossAuthStatus]);
const handleNext = useCallback(async () => {
const [data, cityCodes] = getCreateJobInfo();
const cityValues = getCityValues(cityCodes);
if (!data.address.startsWith(cityValues)) {
data.address = `${cityValues}${data.address}`;
}
Taro.showLoading();
try {
await postCreateJob(data);
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
await Toast.success('创建成功', 1500, true);
navigateBack();
} catch (e) {
console.error('submit error', e);
Toast.error('审核失败请重试');
collectEvent(CollectEventName.PUBLISH_JOB_FAILED, e);
} finally {
Taro.hideLoading();
}
}, [getCreateJobInfo]);
useEffect(() => {
const callback = (d: string) => setDescribe(d);
@ -306,6 +365,11 @@ export default function JobPublish() {
onConfirm={handleConfirmCityPicker}
onCancel={() => setShowCityPicker(false)}
/>
<Dialog open={showBuy} onClose={() => setShowBuy(false)}>
<Dialog.Content>
<CompanyPublishJobBuy onNext={handleNext} defaultSelect={1}/>
</Dialog.Content>
</Dialog>
</div>
</div>
);

View File

@ -3,17 +3,17 @@ import Taro, { useDidShow, useLoad, useShareAppMessage } from '@tarojs/taro';
import { Tabs } from '@taroify/core';
import { useCallback, useEffect, useRef, useState } from 'react';
import { AgreementPopup } from '@/components/agreement-popup';
import HomePage from '@/components/home-page';
import LocationDialog from '@/components/location-dialog';
import { LoginGuide } from '@/components/login-guide';
import MaterialGuide from '@/components/material-guide';
import { EventName, OpenSource, PageUrl } from '@/constants/app';
import { EventName, OpenSource, PageType, PageUrl, RoleType } 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 { switchRoleType } from '@/utils/app';
import { logWithPrefix } from '@/utils/common';
import { getWxLocation, isNotNeedAuthorizeLocation, requestLocation } from '@/utils/location';
import { requestUnreadMessageCount } from '@/utils/message';
@ -21,8 +21,7 @@ import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner';
import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route';
import { getCommonShareMessage } from '@/utils/share';
import Toast from '@/utils/toast';
import { isNeedCreateMaterial } from '@/utils/user';
import { getAgreementSigned, setAgreementSigned } from '@/utils/user';
import './index.less';
const PREFIX = 'job';
@ -40,9 +39,19 @@ export default function Job() {
longitude: location.longitude,
});
const [showMaterialGuide, setShowMaterialGuide] = useState(false);
const [showAuthorize, setShowAuthorize] = useState(false);
const cityValuesChangedRef = useRef(false);
const [openAgreementPopup, setAgreementPopupOpen] = useState(typeof getAgreementSigned() !== 'boolean');
const getLocation = async () => {
if (await isNotNeedAuthorizeLocation()) {
log('not need authorize location');
requestLocation();
} else {
log('show authorize location dialog');
// setShowAuthorize(true);
requestLocation(true);
}
};
const handleTypeChange = useCallback(value => setTabType(value), []);
const handleClickCity = useCallback(
@ -76,19 +85,19 @@ export default function Job() {
setCityCode(code);
}, []);
const handleAfterBindPhone = useCallback(async () => {
if (await isNeedCreateMaterial()) {
setShowMaterialGuide(true);
}
}, []);
const handleCancelAgreementPopup = () => {
setAgreementPopupOpen(false);
setAgreementSigned(false);
const handleCloseAuthorize = useCallback(() => setShowAuthorize(false), []);
getLocation();
};
const handleClickAuthorize = useCallback(() => {
requestLocation(true);
setShowAuthorize(false);
}, []);
const handleConfirmAgreementPopup = () => {
setAgreementPopupOpen(false);
setAgreementSigned(true);
getLocation();
};
useEffect(() => {
Taro.eventCenter.on(EventName.SELECT_CITY, handleCityChange);
return () => {
@ -104,18 +113,15 @@ export default function Job() {
}, [location]);
useLoad(async () => {
switchRoleType(RoleType.Anchor);
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();
} else {
log('show authorize location dialog');
setShowAuthorize(true);
if (!openAgreementPopup) {
getLocation();
}
});
@ -128,11 +134,11 @@ export default function Job() {
path: getJumpUrl(PageUrl.Job, { sortType, c: inviteCode }),
};
}
return getCommonShareMessage(true, inviteCode);
return getCommonShareMessage({ inviteCode, path: PageUrl.Job });
});
return (
<HomePage>
<HomePage type={PageType.JOB}>
<Tabs
swipeable
value={tabType}
@ -154,9 +160,12 @@ export default function Job() {
))}
</Tabs>
<div>
<LocationDialog open={showAuthorize} onClose={handleCloseAuthorize} onClick={handleClickAuthorize} />
<LoginGuide disabled={showAuthorize} onAfterBind={handleAfterBindPhone} />
{showMaterialGuide && <MaterialGuide onClose={() => setShowMaterialGuide(false)} />}
<AgreementPopup
open={openAgreementPopup}
onCancel={handleCancelAgreementPopup}
onConfirm={handleConfirmAgreementPopup}
/>
</div>
</HomePage>
);

View File

@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react';
import MaterialManagePopup from '@/components/material-manage-popup';
import PageLoading from '@/components/page-loading';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { EventName } from '@/constants/app';
import { EventName, RoleType } from '@/constants/app';
import { CollectEventName } from '@/constants/event';
import { MaterialStatus } from '@/constants/material';
import ProfileViewFragment from '@/fragments/profile/view';
@ -17,6 +17,7 @@ import { getCommonShareMessage } from '@/utils/share';
import Toast from '@/utils/toast';
import './index.less';
import { switchRoleType } from '@/utils/app';
const PREFIX = 'page-material-profile';
@ -70,7 +71,7 @@ export default function MaterialProfilePage() {
useShareAppMessage(async () => {
const shareMessage = await getMaterialShareMessage(profile, false);
return shareMessage || getCommonShareMessage(false);
return shareMessage || getCommonShareMessage({ useCapture: false });
});
if (!profile) {

View File

@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react';
import CommonDialog from '@/components/common-dialog';
import PageLoading from '@/components/page-loading';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { EventName, OpenSource, PageUrl } from '@/constants/app';
import { EventName, OpenSource, PageUrl, RoleType } from '@/constants/app';
import { CollectEventName } from '@/constants/event';
import { MaterialViewSource } from '@/constants/material';
import ProfileViewFragment from '@/fragments/profile/view';
@ -16,6 +16,7 @@ import { HttpError } from '@/http/error';
import { JobManageInfo } from '@/types/job';
import { MaterialProfile } from '@/types/material';
import { IJobMessage } from '@/types/message';
import { switchRoleType } from '@/utils/app';
import { copy } from '@/utils/common';
import { collectEvent } from '@/utils/event';
import { requestHasPublishedJob, requestJobDetail } from '@/utils/job';
@ -141,6 +142,8 @@ export default function MaterialViewPage() {
}, []);
useLoad(async () => {
switchRoleType(RoleType.Company);
const context = getPageQuery<IViewContext | IShareContext>();
getInviteCodeFromQueryAndUpdate(context as BL.Anything);
try {

View File

@ -72,6 +72,7 @@ interface ILoadProps {
chatId: string;
jobId?: string;
job?: string;
initText?: boolean;
material?: string;
}
@ -90,16 +91,18 @@ export default function MessageChat() {
const [input, setInput] = useState('');
const [showMore, setShowMore] = useState(false);
const [chat, setChat] = useState<IChatInfo | null>(null);
const [initText, setInitText] = useState('');
const [reject, setReject] = useState<boolean>(false);
const [receiver, setReceiver] = useState<IChatUser | null>(null);
const [messages, setMessages] = useState<IChatMessage[]>([]);
const [messageStatusList, setMessageStatusList] = useState<IMessageStatus[]>([]);
const [jobId, setJobId] = useState<string>();
const [resumeId, setResumeId] = useState<string>();
const [job, setJob] = useState<IJobMessage>();
const [material, setMaterial] = useState<IMaterialMessage>();
const [scrollItemId, setScrollItemId] = useState<string>();
const scrollToLowerRef = useRef(false);
const autoSendRef = useRef({ sendJob: false, sendMaterial: false });
const autoSendRef = useRef({ sendJob: false, sendMaterial: false, sendText: false });
const loadMoreRef = useRef(async (chatId: string, currentMessages: IChatMessage[], forceScroll?: boolean) => {
try {
const lastMsgId = last(currentMessages)?.msgId;
@ -166,6 +169,9 @@ export default function MessageChat() {
) {
tips = '今日申请交换联系方式次数已用完当前每日限制为5次';
duration = 3000;
} else if (errorCode === RESPONSE_ERROR_CODE.CHAT_MSG_SEND_NOT_ALLOW) {
tips = '账号已在另一台设备上切换身份,本条消息未发送成功,请在本设备重新切换身份后,再发送消息';
duration = 5000;
}
tips.length > 7 ? Toast.info(tips, duration) : Toast.error(tips, duration);
}
@ -174,6 +180,7 @@ export default function MessageChat() {
);
const handleClickReject = useCallback(async () => {
await postAddMessageTimes('message_chat_page');
if (!chat || !receiver || reject) {
return;
}
@ -273,7 +280,11 @@ export default function MessageChat() {
}
job && handleSendJobMessage();
material && handleSendMaterialMessage();
}, [chat, job, material, handleSendJobMessage, handleSendMaterialMessage]);
if (initText && !autoSendRef.current.sendText) {
autoSendRef.current.sendText = true;
handleSendMessage({ type: MessageType.Text, content: '你好,想了解下这个岗位' });
}
}, [chat, job, material, handleSendJobMessage, handleSendMaterialMessage, initText, handleSendMessage]);
useLoad(async () => {
const query = getPageQuery<ILoadProps>();
@ -298,11 +309,15 @@ export default function MessageChat() {
const parseMaterial = query.material ? parseQuery<IMaterialMessage>(query.material) : null;
// log('requestChatDetail', chatDetail, parseJob, parseMaterial);
setChat(chatDetail);
setResumeId(chatDetail.participants.find(u => u.userId !== currentUserId)?.resumeId);
setJobId(query.jobId);
setMessages(chatDetail.messages);
setScrollItemId(getScrollItemId(last(chatDetail.messages)?.msgId));
parseJob && setJob(parseJob);
parseMaterial && setMaterial(parseMaterial);
if (!parseMaterial && query.initText && watchType === ChatWatchType.AnchorReject) {
setInitText('你好,想了解下这个岗位');
}
Taro.setNavigationBarTitle({ title: toUserInfo.nickName });
setReceiver(toUserInfo);
setReject(!watchStatus);
@ -353,6 +368,7 @@ export default function MessageChat() {
id={message.msgId}
key={message.msgId}
message={message}
resumeId={resumeId}
isRead={messageStatusList.some(m => m.msgId === message.msgId && !!m.isRead)}
/>
);

View File

@ -7,8 +7,9 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import HomePage from '@/components/home-page';
import MessageCard from '@/components/message-card';
import { MessageHelpDialog, MessageNoTimesDialog } from '@/components/message-dialog';
import { APP_TAB_BAR_ID, EventName } from '@/constants/app';
import { APP_TAB_BAR_ID, EventName, PageType } from '@/constants/app';
import { REFRESH_CHAT_LIST_TIME } from '@/constants/message';
import { MessageSubscribeIds, SubscribeTempId } from '@/constants/subscribe';
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
import useRoleType from '@/hooks/user-role-type';
import { MainMessage } from '@/types/message';
@ -20,7 +21,10 @@ import {
requestUnreadMessageCount,
} from '@/utils/message';
import { isSubscribeRefused, postSubscribe } from '@/utils/subscribe';
import './index.less';
import { collectEvent } from '@/utils/event';
import { CollectEventName } from '@/constants/event';
const PREFIX = 'page-message';
const HEADER_CLASS = `${PREFIX}__header`;
@ -58,12 +62,52 @@ export default function Message() {
const handleClickHelp = useCallback(() => setShowHelp(true), []);
const handleClickAddMessageTimes = useCallback(async () => {
const addMessageTimes = useCallback(async () => {
await postAddMessageTimes('message_page');
const remain = await requestRemainPushTime();
setTimes(remain);
}, []);
const handleClickAddMessageTimes = useCallback(async () => {
const [hasRefused, acceptIds] = await isSubscribeRefused(MessageSubscribeIds);
if (hasRefused) {
await Taro.showModal({
title: '提示',
content:
'您未订阅消息提醒,不能及时获得招聘消息,请前往“设置”,将“新聊天消息”、“未读消息”“面试邀请”均设置为“接收”',
showCancel: false,
confirmText: '打开设置',
});
await Taro.openSetting({
withSubscriptions: true,
});
const { subscriptionsSetting: { mainSwitch, itemSettings = {} } = {} } = await Taro.getSetting({
withSubscriptions: true,
});
console.log('subscriptionsSetting:', mainSwitch, itemSettings);
const successIds = mainSwitch
? MessageSubscribeIds.reduce<SubscribeTempId[]>((acc, id) => {
if ((!itemSettings[id] || itemSettings[id] === 'accept') && !acceptIds.includes(id)) {
acc.push(id);
}
return acc;
}, [])
: [];
if (!successIds.length) {
return;
}
console.log('successIds:', successIds);
collectEvent(CollectEventName.MESSAGE_DEV_LOG, {
action: 'subscribe_new_message_reminder',
source: 'message_page',
successIds,
});
await postSubscribe(MessageSubscribeIds, successIds);
} else {
await addMessageTimes();
}
}, [addMessageTimes]);
useDidHide(() => (pageVisibleRef.current = false));
useDidShow(() => {
@ -101,7 +145,7 @@ export default function Message() {
}, [roleType]);
return (
<HomePage>
<HomePage type={PageType.Message}>
<div className={PREFIX}>
<div className={HEADER_CLASS}>
<div className={`${HEADER_CLASS}__times`}>

View File

@ -1,33 +1,36 @@
import { useShareAppMessage } from '@tarojs/taro';
import { Tabs } from '@taroify/core';
import { useState } from 'react';
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 inviteCode = useInviteCode();
const [tab, setTab] = useState<number>(0);
const handleChange = v => {
setTab(v);
};
useShareAppMessage(() => {
console.log('Partner inviteCode', inviteCode);
return getCommonShareMessage(false, inviteCode);
return getCommonShareMessage({ useCapture: false, inviteCode });
});
return (
<div className={PREFIX}>
<Tabs className={`${PREFIX}__tabs`}>
<Tabs className={`${PREFIX}__tabs`} value={tab} onChange={handleChange}>
<Tabs.TabPane value={0} title="简介">
<PartnerIntro />
</Tabs.TabPane>
<Tabs.TabPane value={1} title="邀请名单">
<PartnerInviteList />
<PartnerInviteList refreshDisabled={tab !== 1} visible={tab === 1} />
</Tabs.TabPane>
<Tabs.TabPane value={2} title="我的收益">
<PartnerProfit />

View File

@ -1,123 +1,4 @@
@import '@/styles/variables.less';
.search-city-profile {
background: #FFF;
&__position-title {
font-size: 24px;
color: @blColorG1;
padding: 0 24px;
margin-top: 18px;
}
&__position-city {
font-size: 30px;
font-weight: bold;
color: @blColor;
padding: 0 24px;
margin-top: 18px;
}
&__hot-city-title {
height: 48px;
font-size: 24px;
line-height: 48px;
padding: 0 24px;
color: #999;
background: #f2f5f7;
margin-top: 18px;
}
&__hot-city-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: space-between;
width: 630px;
padding: 12px 90px 26px 30px;
background: #FFFFFF;
}
&__hot-city-item {
width: 140px;
height: 58px;
font-size: 28px;
line-height: 58px;
text-align: center;
border-radius: 58px;
border: 2px solid @blColorG1;
margin-top: 18px;
}
&__indexes-list {
width: 100%;
/* 兼容 iOS < 11.2 */
padding-bottom: constant(safe-area-inset-bottom);
/* 兼容 iOS >= 11.2 */
padding-bottom: env(safe-area-inset-bottom);
}
&__indexes-fragment {}
&__indexes-anchor {
height: 48px;
font-size: 24px;
line-height: 48px;
padding: 0 24px;
color: @blColorG1;
background: #f2f5f7;
}
&__indexes-cell {
position: relative;
font-size: 28px;
padding: 30px 24px;
color: @blColor;
&::after {
content: '';
position: absolute;
border-bottom: 1rpx solid #eaeef1;
transform: scaleY(0.5);
bottom: 0;
right: 0;
left: 24px;
}
}
&__indexes-bar {
width: 44rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: fixed;
right: 10px;
}
&__indexes-bar-item {
font-size: 22px;
color: @blColor;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
}
&__indexes-index-alert {
position: absolute;
z-index: 20;
width: 160px;
height: 160px;
left: 50%;
top: 50%;
margin-left: -80px;
margin-top: -80px;
border-radius: 80px;
text-align: center;
line-height: 160px;
font-size: 70px;
color: #FFFFFF;
background-color: rgba(0, 0, 0, 0.5);
}
}

View File

@ -1,130 +1,29 @@
import { BaseEventOrig, InputProps, ScrollView } from '@tarojs/components';
import Taro, { useLoad } from '@tarojs/taro';
import { Search } from '@taroify/core';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import SearchCity from '@/components/search-city';
import { EventName, OpenSource } from '@/constants/app';
import { CITY_CODE_TO_NAME_MAP, CITY_INDEXES_LIST } from '@/constants/city';
import { logWithPrefix } from '@/utils/common';
import { getPageQuery, navigateBack } from '@/utils/route';
import './index.less';
interface Item {
cityCode: number | string;
cityName: string;
keyword: string;
}
const PREFIX = 'search-city-profile';
const HOT_CITY = [
{ cityCode: 110100, cityName: '北京' },
{ cityCode: 310100, cityName: '上海' },
{ cityCode: 440100, cityName: '广州' },
{ cityCode: 440300, cityName: '深圳' },
{ cityCode: 330100, cityName: '杭州' },
{ cityCode: 430100, cityName: '长沙' },
{ cityCode: 420100, cityName: '武汉' },
{ cityCode: 350200, cityName: '厦门' },
{ cityCode: 610100, cityName: '西安' },
{ cityCode: 410100, cityName: '郑州' },
{ cityCode: 510100, cityName: '成都' },
{ cityCode: 340100, cityName: '合肥' },
];
const OFFSET_INDEX_SIZE = 2;
const log = logWithPrefix(PREFIX);
const realtimeLogger = Taro.getRealtimeLogManager();
realtimeLogger.tag(PREFIX);
const useHeight = () => {
const [winHeight, setWinHeight] = useState(0);
const [indexItemHeight, setIndexItemHeight] = useState(0);
useEffect(() => {
const windowInfo = Taro.getWindowInfo();
const windowHeight = windowInfo.windowHeight;
setWinHeight(windowHeight);
// 上下预留两个选项高度的空白
setIndexItemHeight(Math.floor(windowHeight / (26 + OFFSET_INDEX_SIZE * 2)));
}, []);
return [winHeight, indexItemHeight];
};
export default function SearchCity() {
const [winHeight, indexItemHeight] = useHeight();
export default function SearchCityProfilePage() {
const [currentCity, setCurrentCity] = useState<string>('');
const [touchAnchor, setTouchAnchor] = useState<string | undefined>();
const [touchMoving, setTouchMoving] = useState(false);
const [searchResult, setSearchResult] = useState<Item[]>([]);
const openSourceRef = useRef<OpenSource>(OpenSource.None);
const showSearchList = searchResult.length > 0;
const handleSearchChange = useCallback((event: BaseEventOrig<InputProps.inputEventDetail>) => {
const value = event.detail.value;
log('handleSearchChange', value);
if (!value) {
setSearchResult([]);
return;
}
const result: Item[] = [];
CITY_INDEXES_LIST.forEach(obj => {
obj.data.forEach(city => {
if (city.keyword.includes(value.toLocaleUpperCase())) {
result.push({ ...city });
}
});
});
setSearchResult(result);
}, []);
const handleSelectCity = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const cityCode = e.currentTarget.dataset.code;
const handleSelectCity = useCallback(cityCode => {
realtimeLogger.info('handleSelectCity openSource', openSourceRef.current);
Taro.eventCenter.trigger(EventName.SELECT_CITY, { openSource: openSourceRef.current, cityCode: String(cityCode) });
navigateBack(1);
}, []);
const handleTouchStart = useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const pageY = e.touches[0].pageY;
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
if (index < 0 || index >= CITY_INDEXES_LIST.length) {
return;
}
const item = CITY_INDEXES_LIST[index];
if (item) {
setTouchMoving(true);
setTouchAnchor(item.letter);
}
},
[indexItemHeight]
);
const handleTouchMove = useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const pageY = e.touches[0].pageY;
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
if (index < 0 || index >= CITY_INDEXES_LIST.length) {
return;
}
const item = CITY_INDEXES_LIST[index];
item && setTouchAnchor(item.letter);
},
[indexItemHeight]
);
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLDivElement>) => {
e.stopPropagation();
setTouchMoving(false);
log('touch end');
}, []);
const handleClickAnchor = useCallback((anchor: string) => {
setTouchAnchor(anchor);
log('click anchor', anchor);
}, []);
useLoad(() => {
const query = getPageQuery<{ city: string; source: OpenSource }>();
log('query', query);
@ -139,94 +38,7 @@ export default function SearchCity() {
return (
<div className={PREFIX}>
<ScrollView scrollY style={{ height: winHeight }} scrollIntoView={touchAnchor}>
<Search
className={`${PREFIX}__search`}
placeholder="输入城市名称"
shape="rounded"
onChange={handleSearchChange}
/>
{showSearchList && (
<div className={`${PREFIX}__search-list`}>
{searchResult.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__indexes-cell`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
)}
{!showSearchList && (
<div>
<div className={`${PREFIX}__position-title`}></div>
<div className={`${PREFIX}__position-city`}>{CITY_CODE_TO_NAME_MAP.get(currentCity)}</div>
<div className={`${PREFIX}__hot-city-title`}></div>
<div className={`${PREFIX}__hot-city-container`}>
{HOT_CITY.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__hot-city-item`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
<div className={`${PREFIX}__indexes-list`}>
{CITY_INDEXES_LIST.map(item => {
return (
<div key={item.letter} className={`${PREFIX}__indexes-fragment`}>
<div className={`${PREFIX}__indexes-anchor`} id={item.letter}>
{item.letter}
</div>
{item.data.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__indexes-cell`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
);
})}
</div>
</div>
)}
</ScrollView>
<div>
{!showSearchList && (
<div
className={`${PREFIX}__indexes-bar`}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchEnd}
style={{ top: indexItemHeight * OFFSET_INDEX_SIZE }}
>
{CITY_INDEXES_LIST.map(item => {
return (
<div
key={item.letter}
className={`${PREFIX}__indexes-bar-item`}
style={{ height: indexItemHeight }}
onClick={() => handleClickAnchor(item.letter)}
>
{item.letter}
</div>
);
})}
</div>
)}
{touchAnchor && touchMoving && <div className={`${PREFIX}__indexes-index-alert`}>{touchAnchor}</div>}
</div>
<SearchCity onSelectCity={handleSelectCity} currentCity={currentCity} />
</div>
);
}

View File

@ -1,123 +1,4 @@
@import '@/styles/variables.less';
.search-city {
background: #FFF;
&__position-title {
font-size: 24px;
color: @blColorG1;
padding: 0 24px;
margin-top: 18px;
}
&__position-city {
font-size: 30px;
font-weight: bold;
color: @blColor;
padding: 0 24px;
margin-top: 18px;
}
&__hot-city-title {
height: 48px;
font-size: 24px;
line-height: 48px;
padding: 0 24px;
color: #999;
background: #f2f5f7;
margin-top: 18px;
}
&__hot-city-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: space-between;
width: 630px;
padding: 12px 90px 26px 30px;
background: #FFFFFF;
}
&__hot-city-item {
width: 140px;
height: 58px;
font-size: 28px;
line-height: 58px;
text-align: center;
border-radius: 58px;
border: 2px solid @blColorG1;
margin-top: 18px;
}
&__indexes-list {
width: 100%;
/* 兼容 iOS < 11.2 */
padding-bottom: constant(safe-area-inset-bottom);
/* 兼容 iOS >= 11.2 */
padding-bottom: env(safe-area-inset-bottom);
}
&__indexes-fragment {}
&__indexes-anchor {
height: 48px;
font-size: 24px;
line-height: 48px;
padding: 0 24px;
color: @blColorG1;
background: #f2f5f7;
}
&__indexes-cell {
position: relative;
font-size: 28px;
padding: 30px 24px;
color: @blColor;
&::after {
content: '';
position: absolute;
border-bottom: 1rpx solid #eaeef1;
transform: scaleY(0.5);
bottom: 0;
right: 0;
left: 24px;
}
}
&__indexes-bar {
width: 44rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: fixed;
right: 10px;
}
&__indexes-bar-item {
font-size: 22px;
color: @blColor;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
}
&__indexes-index-alert {
position: absolute;
z-index: 20;
width: 160px;
height: 160px;
left: 50%;
top: 50%;
margin-left: -80px;
margin-top: -80px;
border-radius: 80px;
text-align: center;
line-height: 160px;
font-size: 70px;
color: #FFFFFF;
background-color: rgba(0, 0, 0, 0.5);
}
}
.page.search-city {
}

View File

@ -1,129 +1,29 @@
import { BaseEventOrig, InputProps, ScrollView } from '@tarojs/components';
import Taro, { useLoad } from '@tarojs/taro';
import { Search } from '@taroify/core';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import SearchCity from '@/components/search-city';
import { EventName, OpenSource } from '@/constants/app';
import { CITY_CODE_TO_NAME_MAP, CITY_INDEXES_LIST } from '@/constants/city';
import { logWithPrefix } from '@/utils/common';
import { getPageQuery, navigateBack } from '@/utils/route';
import './index.less';
interface Item {
cityCode: number | string;
cityName: string;
keyword: string;
}
const PREFIX = 'page-search-city';
const PREFIX = 'search-city';
const HOT_CITY = [
{ cityCode: 110100, cityName: '北京' },
{ cityCode: 310100, cityName: '上海' },
{ cityCode: 440100, cityName: '广州' },
{ cityCode: 440300, cityName: '深圳' },
{ cityCode: 330100, cityName: '杭州' },
{ cityCode: 430100, cityName: '长沙' },
{ cityCode: 420100, cityName: '武汉' },
{ cityCode: 350200, cityName: '厦门' },
{ cityCode: 610100, cityName: '西安' },
{ cityCode: 410100, cityName: '郑州' },
{ cityCode: 510100, cityName: '成都' },
{ cityCode: 340100, cityName: '合肥' },
];
const OFFSET_INDEX_SIZE = 2;
const log = logWithPrefix(PREFIX);
const realtimeLogger = Taro.getRealtimeLogManager();
realtimeLogger.tag(PREFIX);
const useHeight = () => {
const [winHeight, setWinHeight] = useState(0);
const [indexItemHeight, setIndexItemHeight] = useState(0);
useEffect(() => {
const windowInfo = Taro.getWindowInfo();
const windowHeight = windowInfo.windowHeight;
setWinHeight(windowHeight);
// 上下预留两个选项高度的空白
setIndexItemHeight(Math.floor(windowHeight / (26 + OFFSET_INDEX_SIZE * 2)));
}, []);
return [winHeight, indexItemHeight];
};
export default function SearchCity() {
const [winHeight, indexItemHeight] = useHeight();
export default function SearchCityPage() {
const [currentCity, setCurrentCity] = useState<string>('');
const [touchAnchor, setTouchAnchor] = useState<string | undefined>();
const [touchMoving, setTouchMoving] = useState(false);
const [searchResult, setSearchResult] = useState<Item[]>([]);
const openSourceRef = useRef<OpenSource>(OpenSource.None);
const showSearchList = searchResult.length > 0;
const handleSearchChange = useCallback((event: BaseEventOrig<InputProps.inputEventDetail>) => {
const value = event.detail.value;
log('handleSearchChange', value);
if (!value) {
setSearchResult([]);
return;
}
const result: Item[] = [];
CITY_INDEXES_LIST.forEach(obj => {
obj.data.forEach(city => {
if (city.keyword.includes(value.toLocaleUpperCase())) {
result.push({ ...city });
}
});
});
setSearchResult(result);
}, []);
const handleSelectCity = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const cityCode = e.currentTarget.dataset.code;
const handleSelectCity = useCallback(cityCode => {
Taro.eventCenter.trigger(EventName.SELECT_CITY, { openSource: openSourceRef.current, cityCode: String(cityCode) });
navigateBack(1);
}, []);
const handleTouchStart = useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const pageY = e.touches[0].pageY;
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
if (index < 0 || index >= CITY_INDEXES_LIST.length) {
return;
}
const item = CITY_INDEXES_LIST[index];
if (item) {
setTouchMoving(true);
setTouchAnchor(item.letter);
}
},
[indexItemHeight]
);
const handleTouchMove = useCallback(
(e: React.TouchEvent<HTMLDivElement>) => {
const pageY = e.touches[0].pageY;
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
if (index < 0 || index >= CITY_INDEXES_LIST.length) {
return;
}
const item = CITY_INDEXES_LIST[index];
item && setTouchAnchor(item.letter);
},
[indexItemHeight]
);
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLDivElement>) => {
e.stopPropagation();
setTouchMoving(false);
log('touch end');
}, []);
const handleClickAnchor = useCallback((anchor: string) => {
setTouchAnchor(anchor);
log('click anchor', anchor);
}, []);
useLoad(() => {
const query = getPageQuery<{ city: string; source: OpenSource }>();
log('query', query);
@ -138,94 +38,7 @@ export default function SearchCity() {
return (
<div className={PREFIX}>
<ScrollView scrollY style={{ height: winHeight }} scrollIntoView={touchAnchor}>
<Search
className={`${PREFIX}__search`}
placeholder="输入城市名称"
shape="rounded"
onChange={handleSearchChange}
/>
{showSearchList && (
<div className={`${PREFIX}__search-list`}>
{searchResult.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__indexes-cell`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
)}
{!showSearchList && (
<div>
<div className={`${PREFIX}__position-title`}></div>
<div className={`${PREFIX}__position-city`}>{CITY_CODE_TO_NAME_MAP.get(currentCity)}</div>
<div className={`${PREFIX}__hot-city-title`}></div>
<div className={`${PREFIX}__hot-city-container`}>
{HOT_CITY.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__hot-city-item`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
<div className={`${PREFIX}__indexes-list`}>
{CITY_INDEXES_LIST.map(item => {
return (
<div key={item.letter} className={`${PREFIX}__indexes-fragment`}>
<div className={`${PREFIX}__indexes-anchor`} id={item.letter}>
{item.letter}
</div>
{item.data.map(city => (
<div
key={city.cityCode}
className={`${PREFIX}__indexes-cell`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
))}
</div>
);
})}
</div>
</div>
)}
</ScrollView>
<div>
{!showSearchList && (
<div
className={`${PREFIX}__indexes-bar`}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={handleTouchEnd}
style={{ top: indexItemHeight * OFFSET_INDEX_SIZE }}
>
{CITY_INDEXES_LIST.map(item => {
return (
<div
key={item.letter}
className={`${PREFIX}__indexes-bar-item`}
style={{ height: indexItemHeight }}
onClick={() => handleClickAnchor(item.letter)}
>
{item.letter}
</div>
);
})}
</div>
)}
{touchAnchor && touchMoving && <div className={`${PREFIX}__indexes-index-alert`}>{touchAnchor}</div>}
</div>
<SearchCity onSelectCity={handleSelectCity} currentCity={currentCity} />
</div>
);
}

View File

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

View File

@ -6,6 +6,12 @@
height: 100vh;
.flex-column();
&.color-bg {
background: linear-gradient(180deg, #efecff 0%, #f7f5ff 100%);
padding-top: 347px;
display: block;
}
&__app {
margin-top: 50%;
}
@ -22,4 +28,68 @@
color: @blColorG2;
margin-top: 32px;
}
}
&__role-app {
padding: 64px;
}
&__greet {
font-size: 28px;
line-height: 56px;
color: @blColorG1;
margin-bottom: 14px;
}
&__title {
font-weight: 500;
font-size: 48px;
line-height: 57px;
color: @blColor;
margin-bottom: 75px;
}
&__card {
background: rgba(255, 255, 255, 0.5);
border: 2px solid #ffffff;
border-radius: 16px;
padding: 56px 40px 56px;
.flex-row();
& + & {
margin-top: 64px;
}
}
&__avatar {
width: 140px;
height: 140px;
border-radius: 50%;
}
&__content {
padding-left: 34px;
.flex-column();
flex: 1;
align-items: start;
.title {
font-weight: 600;
font-size: 40px;
line-height: 48px;
color: @blColor;
margin-bottom: 16px;
}
.desc {
font-size: 28px;
line-height: 40px;
color: @blColorG1;
}
}
&__arrow {
width: 24px;
height: 24px;
}
}

View File

@ -1,23 +1,96 @@
import { Image } from '@tarojs/components';
import { useLoad } from '@tarojs/taro';
import { switchDefaultTab } from '@/utils/app';
// import { useEffect, useState } from 'react';
// import { AgreementPopup } from '@/components/agreement-popup';
// import Slogan from '@/components/slogan';
// import { PageUrl, RoleType } from '@/constants/app';
// import { ANCHOR_TAB_LIST, COMPANY_TAB_LIST } from '@/hooks/use-config';
// import store from '@/store';
// import { changeHomePage } from '@/store/actions';
import { RoleType } from '@/constants/app';
import { getRoleType, switchDefaultTab, switchRoleType } from '@/utils/app';
// import { switchTab } from '@/utils/route';
// import { getAgreementSigned, setAgreementSigned } from '@/utils/user';
import './index.less';
const PREFIX = 'page-start';
export default function Start() {
// const [open, setOpen] = useState(typeof getAgreementSigned() !== 'boolean');
const mode = getRoleType();
useLoad(() => {
switchDefaultTab();
if (!mode) {
switchRoleType(RoleType.Anchor).then(() => {
switchDefaultTab();
});
} else {
switchDefaultTab();
}
});
// const handleAnchor = async () => {
// await switchRoleType(RoleType.Anchor);
// store.dispatch(changeHomePage(ANCHOR_TAB_LIST[0].type));
// await switchTab(ANCHOR_TAB_LIST[0].pagePath as PageUrl);
// };
// const handleCompany = async () => {
// await switchRoleType(RoleType.Company);
// store.dispatch(changeHomePage(COMPANY_TAB_LIST[0].type));
// await switchTab(COMPANY_TAB_LIST[0].pagePath as PageUrl);
// };
// const handleCancel = () => {
// setOpen(false);
// setAgreementSigned(false);
// };
//
// const handleConfirm = () => {
// setOpen(false);
// setAgreementSigned(true);
// };
return (
<div className={PREFIX}>
<div className={`${PREFIX}`}>
{/*{mode && (*/}
<div className={`${PREFIX}__app`}>
<Image className={`${PREFIX}__icon`} mode="aspectFit" src={require('@/statics/svg/slogan.svg')} />
<div className={`${PREFIX}__text`}> </div>
</div>
{/*)}*/}
{!mode && (
<>
{/*<div className={`${PREFIX}__role-app`}>*/}
{/* <div className={`${PREFIX}__greet`}>Hi很高兴见到你</div>*/}
{/* <div className={`${PREFIX}__title`}>请选择您的身份</div>*/}
{/* <div className={`${PREFIX}__card`} onClick={handleAnchor}>*/}
{/* <Image*/}
{/* className={`${PREFIX}__avatar anchor`}*/}
{/* src="https://publiccdn.neighbourhood.com.cn/img/avatar_f.png"*/}
{/* mode="aspectFill"*/}
{/* />*/}
{/* <div className={`${PREFIX}__content`}>*/}
{/* <div className="title">我是主播</div>*/}
{/* <div className="desc">我要找工作</div>*/}
{/* </div>*/}
{/* <Image src={require('@/statics/svg/arrow-right.svg')} mode="aspectFill" className={`${PREFIX}__arrow`} />*/}
{/* </div>*/}
{/* <div className={`${PREFIX}__card`} onClick={handleCompany}>*/}
{/* <Image*/}
{/* className={`${PREFIX}__avatar company`}*/}
{/* src="https://publiccdn.neighbourhood.com.cn/img/avatar_m.png"*/}
{/* mode="aspectFill"*/}
{/* />*/}
{/* <div className={`${PREFIX}__content`}>*/}
{/* <div className="title">我是企业</div>*/}
{/* <div className="desc">我要招主播</div>*/}
{/* </div>*/}
{/* <Image src={require('@/statics/svg/arrow-right.svg')} mode="aspectFill" className={`${PREFIX}__arrow`} />*/}
{/* </div>*/}
{/*</div>*/}
{/*<Slogan />*/}
{/*<AgreementPopup open={open} onCancel={handleCancel} onConfirm={handleConfirm} />*/}
</>
)}
</div>
);
}

View File

@ -1,90 +1,139 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.page-user-batch-publish {
padding: 24px;
.page-biz-service {
padding-bottom: 200px;
&__tabs {
--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);
}
//.taroify-tabs__nav .taroify-tabs__tab:nth-child(2) .taroify-badge-wrapper {
// left: 18px;
//}
}
&__star {
width: 33px;
height: 38px;
}
&__header-image {
width: 100%;
height: 120px;
margin-top: 24px;
}
&__title {
font-size: 32px;
line-height: 48px;
font-weight: 500;
color: @blColor;
margin-top: 24px;
&:first-child {
margin-top: 0;
}
}
&__cell {
height: 100px;
padding-left: 32px;
padding-right: 32px;
border-radius: 16px;
margin-top: 24px;
}
&__cost-describe {
height: 100px;
padding: 0 32px;
border-radius: 16px;
.flex-row();
justify-content: space-between;
background: #FFFFFF;
margin-top: 24px;
&__price {
font-size: 48px;
line-height: 48px;
font-weight: 500;
color: @blHighlightColor;
&__delegate {
& {
padding: 24px;
padding-bottom: calc(120px + env(safe-area-inset-bottom));
}
&__original_price {
flex: 1;
font-size: 32px;
line-height: 34px;
font-weight: 400;
color: @blColorG1;
margin-left: 16px;
text-decoration: line-through;
&-fix {
width: 100%;
background: #f5f6fa;
padding-left: 24px;
padding-right: 24px;
position: fixed;
left: 0;
right: 0;
bottom: 110px;
box-sizing: border-box;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
}
}
&__illustrate {
padding: 24px 32px;
margin-top: 24px;
font-size: 28px;
line-height: 48px;
font-weight: 400;
color: @blColorG2;
background: #FFFFFF;
border-radius: 16px;
&__describe {
.flex-row();
font-size: 28px;
line-height: 48px;
font-weight: 400;
color: @blColorG2;
margin-top: 8px;
&__view {
color: @blHighlightColor;
margin-left: 4px;
&-image {
width: 100%;
height: 298px;
}
&-card {
background: #fff;
padding: 24px;
border-radius: 24px;
&.image {
padding: 1px;
justify-content: center;
align-items: center;
display: flex;
}
}
&-h5 {
font-style: normal;
font-weight: 400;
font-size: 28px;
line-height: 32px;
color: @blColor;
padding-top: 40px;
padding-bottom: 24px;
&:first-child {
padding-top: 0;
}
}
&-body {
font-weight: 400;
font-size: 28px;
line-height: 48px;
color: @blColorG2;
&.link {
margin-top: 16px;
color: @blHighlightColor;
}
}
&-btn {
.button(@width: 100%; @height: 80px; @fontSize: 32px);
margin-top: 32px;
}
}
&__buy-button {
.button(@width: 100%; @height: 80px; @fontSize: 32px);
margin-top: 40px;
&__recruitment {
padding: 24px;
&-card {
background: #fff;
padding: 32px;
border-radius: 24px;
}
&-h5 {
font-weight: 500;
font-size: 32px;
line-height: 40px;
color: #1d2129;
padding-bottom: 16px;
padding-top: 40px;
&:first-child {
padding-top: 0;
}
}
&-body {
font-size: 28px;
line-height: 40px;
& + & {
padding-top: 20px;
}
}
&-btn-group {
.flex-row();
justify-content: center;
margin-top: 40px;
}
&-btn {
.button(@height: 72px; @width: 384px; @fontSize: 28px; @fontWeight: 400; @borderRadius: 44px; @highlight: 0);
}
}
}
}

View File

@ -1,197 +1,149 @@
import { Button, Image, Text } from '@tarojs/components';
import Taro, { useLoad } from '@tarojs/taro';
import { Image } from '@tarojs/components';
import Taro, { useLoad, useShareAppMessage } from '@tarojs/taro';
import { Cell } from '@taroify/core';
import { Button, Tabs } from '@taroify/core';
import { useCallback, useState } from 'react';
import HomePage from '@/components/home-page';
import PageLoading from '@/components/page-loading';
import { PublishJobQrCodeDialog } from '@/components/product-dialog/publish-job';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { ISelectOption, PopupSelect } from '@/components/select';
import { PageUrl } from '@/constants/app';
import { OrderStatus, OrderType, ProductSpecId, ProductType } from '@/constants/product';
import { BatchPublishGroup } from '@/types/group';
import { logWithPrefix } from '@/utils/common';
import {
getOrderPrice,
isCancelPay,
requestAllBuyProduct,
requestCreatePayInfo,
requestOrderInfo,
requestPayment,
} from '@/utils/product';
import { navigateTo } from '@/utils/route';
import Toast from '@/utils/toast';
import SearchCity from '@/components/search-city';
import { PageType, PageUrl, RoleType } from '@/constants/app';
import { GROUPS } from '@/constants/group';
import useInviteCode from '@/hooks/use-invite-code';
import { switchRoleType } from '@/utils/app';
import { openCustomerServiceChat } from '@/utils/common';
import { getCurrentCityCode } from '@/utils/location';
import { getPageQuery, navigateTo } from '@/utils/route';
import { getCommonShareMessage } from '@/utils/share';
import { checkCityCode } from '@/utils/user';
import './index.less';
interface CityValue extends BatchPublishGroup {
cityName: string;
}
const PREFIX = 'page-biz-service';
const EXAMPLE_IMAGE = 'https://publiccdn.neighbourhood.com.cn/img/delegate-example.png';
const COMMENT_IMAGE = 'https://publiccdn.neighbourhood.com.cn/img/delegate-comments.png';
export default function BizService() {
const inviteCode = useInviteCode();
const [value, setValue] = useState('0');
interface CityOption extends ISelectOption<CityValue> {
value: CityValue;
}
const PREFIX = 'page-user-batch-publish';
const log = logWithPrefix(PREFIX);
const SERVICE_ILLUSTRATE = `群发次数每日一次连发3天
群发内容:仅限主播招聘通告,违规内容不发
联系方法:通告中留通告主联系方式,主播直接联系`;
const cityValues: CityValue[] = [
{ cityCode: '440100', cityName: '广州', count: 300 },
{ cityCode: '440300', cityName: '深圳', count: 100 },
{ cityCode: '330100', cityName: '杭州', count: 300 },
{ cityCode: '110100', cityName: '北京', count: 100 },
{ cityCode: '510100', cityName: '成都', count: 50 },
{ cityCode: '430100', cityName: '长沙', count: 50 },
{ cityCode: '350200', cityName: '厦门', count: 50 },
{ cityCode: '310100', cityName: '上海', count: 100 },
{ cityCode: '420100', cityName: '武汉', count: 50 },
{ cityCode: '610100', cityName: '西安', count: 50 },
{ cityCode: '410100', cityName: '郑州', count: 100 },
].sort((a, b) => b.count - a.count);
const MIN_GROUP_SIZE = 20;
const GROUP_OPTIONS = [
{ value: MIN_GROUP_SIZE, productSpecId: ProductSpecId.GroupBatchPublish20, label: '20', price: 18 },
{ value: 50, productSpecId: ProductSpecId.GroupBatchPublish50, label: '50', price: 40 },
{ value: 100, productSpecId: ProductSpecId.GroupBatchPublish100, label: '100', price: 68 },
{ value: 300, productSpecId: ProductSpecId.GroupBatchPublish300, label: '300', price: 128 },
{ value: 500, productSpecId: ProductSpecId.GroupBatchPublish500, label: '500', price: 188 },
{ value: 1000, productSpecId: ProductSpecId.GroupBatchPublish1000, label: '1000', price: 288 },
];
const calcPrice = (city: CityValue | null) => {
if (!city) {
return {};
}
const { count } = city;
const originalPrice = count * 1;
const price = GROUP_OPTIONS.find(o => o.value === count)?.price || 18;
const productSpecId = GROUP_OPTIONS.find(o => o.value === count)?.productSpecId || ProductSpecId.GroupBatchPublish20;
return { price, originalPrice, productSpecId };
};
export default function UserBatchPublish() {
const [loading, setLoading] = useState(true);
const [showCitySelect, setShowCitySelect] = useState(false);
const [showQrCode, setShowQrCode] = useState(false);
const [city, setCity] = useState<CityOption['value'] | null>(null);
const [cityOptions, setCityOptions] = useState<CityOption[]>([]);
const { price, originalPrice, productSpecId } = calcPrice(city);
const handleClickCity = useCallback(() => setShowCitySelect(true), []);
const handleSelectCity = useCallback(value => {
setCity(value);
setShowCitySelect(false);
const handleClickDelegate = useCallback(() => {
navigateTo(PageUrl.GroupDelegatePublish);
}, []);
const handleClickViewGroup = useCallback(() => navigateTo(PageUrl.GroupList, { city: city?.cityCode }), [city]);
const handleClickBuy = useCallback(async () => {
// if (1 < 2) {
// await new Promise(r => setTimeout(r, 3000));
// setShowQrCode(true);
// return;
// }
if (!price || !productSpecId) {
const handlePreview = (current: string) => {
Taro.previewImage({
current,
urls: [EXAMPLE_IMAGE, COMMENT_IMAGE],
});
};
const handleOpenService = useCallback(() => {
openCustomerServiceChat('https://work.weixin.qq.com/kfid/kfcd60708731367168d');
}, []);
const handleSelectCity = useCallback(cityCode => {
if (!checkCityCode(cityCode)) {
return;
}
try {
Taro.showLoading();
const allowBuy = await requestAllBuyProduct(ProductType.GroupBatchPublish);
if (!allowBuy) {
Taro.hideLoading();
Toast.info('您最近已购买过,可直接联系客服');
setShowQrCode(true);
return;
}
const { payOrderNo, createPayInfo } = await requestCreatePayInfo({
type: OrderType.GroupBatchPublish,
amt: getOrderPrice(price),
// amt: 1,
productCode: ProductType.GroupBatchPublish,
productSpecId: productSpecId,
});
log('handleBuy payInfo', payOrderNo, createPayInfo);
await requestPayment({
timeStamp: createPayInfo.timeStamp,
nonceStr: createPayInfo.nonceStr,
package: createPayInfo.packageVal,
signType: createPayInfo.signType,
paySign: createPayInfo.paySign,
});
const { status } = await requestOrderInfo({ payOrderNo });
log('handleBuy orderInfo', status);
if (status !== OrderStatus.Success) {
throw new Error('order status error');
}
Taro.hideLoading();
setShowQrCode(true);
} catch (e) {
Taro.hideLoading();
Toast.error(isCancelPay(e) ? '取消购买' : '购买失败请重试');
log('handleBuy error', e);
const group = GROUPS.find(g => String(g.cityCode) === cityCode);
if (group) {
openCustomerServiceChat(group.serviceUrl);
}
}, [price, productSpecId]);
}, []);
const handleChange = useCallback(v => {
setValue(v);
}, []);
useLoad(async () => {
try {
const cOptions: CityOption[] = cityValues.map(value => ({ value, label: value.cityName }));
const initCity = cOptions[0].value;
useLoad(() => {
switchRoleType(RoleType.Company);
setLoading(false);
setCity(initCity);
setCityOptions(cOptions);
log('init data done', cOptions);
} catch (e) {
Toast.error('加载失败请重试');
const query = getPageQuery<{ tab?: string }>();
if (query.tab) {
handleChange(query.tab);
}
});
if (loading) {
return <PageLoading />;
}
useShareAppMessage(() =>
getCommonShareMessage({
inviteCode,
path: PageUrl.UserBatchPublish,
title: '邀请你加入本地主播求职招聘群',
params: { tab: '1' },
})
);
return (
<HomePage>
<HomePage type={PageType.BatchPublish}>
<div className={PREFIX}>
<Image mode="widthFix" className={`${PREFIX}__header-image`} src="https://neighbourhood.cn/pubJob.png" />
<div className={`${PREFIX}__title`}></div>
<Cell isLink align="center" className={`${PREFIX}__cell`} title={city?.cityName} onClick={handleClickCity} />
<div className={`${PREFIX}__title`}></div>
<Cell align="center" className={`${PREFIX}__cell`} title={city?.count} />
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__cost-describe`}>
<div className={`${PREFIX}__cost-describe__price`}>{`${price}`}</div>
<div className={`${PREFIX}__cost-describe__original_price`}>{`原价:${originalPrice}`}</div>
</div>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__illustrate`}>
<Text>{SERVICE_ILLUSTRATE}</Text>
<div className={`${PREFIX}__illustrate__describe`}>
<div></div>
<div className={`${PREFIX}__illustrate__describe__view`} onClick={handleClickViewGroup}>
<Tabs className={`${PREFIX}__tabs`} value={value} onChange={handleChange}>
<Tabs.TabPane value="0" title="群代发">
<div className={`${PREFIX}__delegate`}>
<Image
mode="widthFix"
className={`${PREFIX}__header-image`}
src="https://publiccdn.neighbourhood.com.cn/img/pub-job.png"
/>
<div className={`${PREFIX}__delegate-h5`}></div>
<div className={`${PREFIX}__delegate-card`}>
<div className={`${PREFIX}__delegate-body`}></div>
<div className={`${PREFIX}__delegate-body`}>13</div>
<div className={`${PREFIX}__delegate-body`}></div>
<div className={`${PREFIX}__delegate-body`}></div>
</div>
<div className={`${PREFIX}__delegate-h5`}></div>
<div className={`${PREFIX}__delegate-card image`} onClick={() => handlePreview(EXAMPLE_IMAGE)}>
<Image className={`${PREFIX}__delegate-image`} src={EXAMPLE_IMAGE} mode="heightFix" />
</div>
<div className={`${PREFIX}__delegate-h5`}></div>
<div className={`${PREFIX}__delegate-card image`} onClick={() => handlePreview(COMMENT_IMAGE)}>
<Image className={`${PREFIX}__delegate-image`} src={COMMENT_IMAGE} mode="heightFix" />
</div>
<div className={`${PREFIX}__delegate-fix`}>
<Button className={`${PREFIX}__delegate-btn`} onClick={handleClickDelegate}>
</Button>
</div>
</div>
</div>
</div>
<Button className={`${PREFIX}__buy-button`} onClick={handleClickBuy}>
</Button>
<SafeBottomPadding />
<div>
<PopupSelect
value={city}
options={cityOptions}
open={showCitySelect}
onSelect={handleSelectCity}
onClose={() => setShowCitySelect(false)}
/>
<PublishJobQrCodeDialog onClose={() => setShowQrCode(false)} open={showQrCode} />
</div>
</Tabs.TabPane>
<Tabs.TabPane
value="1"
title={
<>
{/*<Image src={require('@/statics/svg/star.svg')} className={`${PREFIX}__star`} />*/}
</>
}
>
<SearchCity
onSelectCity={handleSelectCity}
currentCity={getCurrentCityCode()}
forGroup
offset={72}
banner="点击城市名称,进本地通告群,免费招主播"
/>
</Tabs.TabPane>
<Tabs.TabPane value="2" title="代招">
<div className={`${PREFIX}__recruitment`}>
<div className={`${PREFIX}__recruitment-card`}>
<div className={`${PREFIX}__recruitment-h5`}></div>
<div className={`${PREFIX}__recruitment-body`}>-</div>
<div className={`${PREFIX}__recruitment-body`}>-广</div>
<div className={`${PREFIX}__recruitment-body`}>-</div>
<div className={`${PREFIX}__recruitment-body`}>-西</div>
<div className={`${PREFIX}__recruitment-body`}>-</div>
<div className={`${PREFIX}__recruitment-h5`}></div>
<div className={`${PREFIX}__recruitment-body`}></div>
<div className={`${PREFIX}__recruitment-body`}>200</div>
<div className={`${PREFIX}__recruitment-body`}></div>
<div className={`${PREFIX}__recruitment-h5`}></div>
<div className={`${PREFIX}__recruitment-body`}>
</div>
<div className={`${PREFIX}__recruitment-btn-group`}>
<Button className={`${PREFIX}__recruitment-btn`} onClick={handleOpenService}>
</Button>
</div>
</div>
</div>
</Tabs.TabPane>
</Tabs>
</div>
</HomePage>
);

View File

@ -9,7 +9,7 @@ import LoginButton from '@/components/login-button';
import PartnerEntry from '@/components/partner-entry';
import Slogan from '@/components/slogan';
import SwitchBar from '@/components/switch-bar';
import { RoleType, PageUrl } from '@/constants/app';
import { RoleType, PageUrl, PageType } from '@/constants/app';
import AnchorFragment from '@/fragments/user/anchor';
import CompanyFragment from '@/fragments/user/company';
import useUserInfo from '@/hooks/use-user-info';
@ -38,10 +38,10 @@ export default function User() {
[]
);
useShareAppMessage(() => getCommonShareMessage(false));
useShareAppMessage(() => getCommonShareMessage({ useCapture: false }));
return (
<HomePage>
<HomePage type={PageType.User}>
<CustomNavigationBar className={`${PREFIX}__navigation-bar`}>
<SwitchBar title={roleType === RoleType.Anchor ? '切换为企业' : '切换为主播'} onClick={handleSwitchRoleType} />
</CustomNavigationBar>

View File

@ -2,4 +2,52 @@
@import '@/styles/variables.less';
.withdraw-record {
&__item {
height: 131px;
width: 100%;
background: #fff;
padding: 24px 32px 0 32px;
box-sizing: border-box;
font-size: 28px;
&-border {
border-bottom: 1px solid #e6e7e8;
}
&-content {
.flex-row();
width: 100%;
padding-bottom: 24px;
}
&-time {
padding-right: 8px;
line-height: 40px;
flex: 1;
.noWrap();
}
&-withdraw {
width: 200px;
text-align: right;
padding-left: 8px;
flex-shrink: 0;
}
&-money {
line-height: 40px;
padding-bottom: 8px;
.noWrap();
font-weight: 600;
font-size: 30px;
color: #ff5051;
}
&-status {
font-size: 24px;
line-height: 36px;
color: #999999;
.noWrap();
}
}
}

View File

@ -1,14 +1,119 @@
import { useShareAppMessage } from '@tarojs/taro';
import { List, PullRefresh } from '@taroify/core';
import { useCallback, useEffect, useRef, useState } from 'react';
import ListPlaceholder from '@/components/list-placeholder';
import { WithdrawStatusDescriptions } from '@/constants/partner';
import useInviteCode from '@/hooks/use-invite-code';
import { WithdrawRecord } from '@/types/partner';
import { logWithPrefix } from '@/utils/common';
import { formatMoney, formatTimestamp, getWithdrawList as requestData } from '@/utils/partner';
import { getCommonShareMessage } from '@/utils/share';
import './index.less';
const PREFIX = 'withdraw-record';
const log = logWithPrefix(PREFIX);
const FIRST_PAGE = 0;
export default function WithdrawRecord() {
export default function WithdrawRecords() {
const inviteCode = useInviteCode();
const [hasMore, setHasMore] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState(false);
const [dataList, setDataList] = useState<WithdrawRecord[]>([]);
const currentPage = useRef<number>(FIRST_PAGE);
const handleRefresh = useCallback(async () => {
log('start pull refresh');
try {
setRefreshing(true);
setLoadMoreError(false);
const { content, totalPages } = await requestData({ page: 1 });
setDataList(content);
currentPage.current = 1;
setHasMore(currentPage.current < totalPages);
log('pull refresh success');
} catch (e) {
setDataList([]);
setHasMore(false);
setLoadMoreError(true);
currentPage.current = FIRST_PAGE;
log('pull refresh failed');
} finally {
setRefreshing(false);
}
}, []);
const handleLoadMore = useCallback(async () => {
log('start load more', hasMore);
if (!hasMore) {
return;
}
setLoadMoreError(false);
setLoadingMore(true);
try {
const { totalPages, content } = await requestData({ page: currentPage.current + 1 });
setDataList([...dataList, ...content]);
currentPage.current = currentPage.current + 1;
setHasMore(currentPage.current < totalPages);
log('load more success');
} catch (e) {
setLoadMoreError(true);
log('load more failed');
} finally {
setLoadingMore(false);
}
}, [dataList, hasMore]);
// 初始化数据&配置变更后刷新数据
useEffect(() => {
const refresh = async () => {
log('visible changed, start refresh list data');
try {
setDataList([]);
setLoadingMore(true);
setLoadMoreError(false);
const { totalPages, content } = await requestData({ page: 1 });
setDataList(content);
currentPage.current = 1;
setHasMore(currentPage.current < totalPages);
} catch (e) {
setDataList([]);
setHasMore(false);
setLoadMoreError(true);
} finally {
log('visible changed, refresh list data end');
setLoadingMore(false);
}
};
refresh();
}, []);
useShareAppMessage(() => {
return getCommonShareMessage(false);
return getCommonShareMessage({ useCapture: false, inviteCode });
});
return <div className={PREFIX}></div>;
return (
<div className={PREFIX}>
<PullRefresh className={`${PREFIX}__pull-refresh`} loading={refreshing} onRefresh={handleRefresh}>
<List hasMore={hasMore} onLoad={handleLoadMore} loading={loadingMore || refreshing} disabled={loadMoreError}>
{dataList.map(item => (
<div className={`${PREFIX}__item`} key={item.id || item.userId}>
<div className={`${PREFIX}__item-border`}>
<div className={`${PREFIX}__item-content`}>
<div className={`${PREFIX}__item-time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item-withdraw`}>
<div className={`${PREFIX}__item-money`}>{formatMoney(item.amt)}</div>
<div className={`${PREFIX}__item-status`}>{WithdrawStatusDescriptions[item.status]}</div>
</div>
</div>
</div>
</div>
))}
<ListPlaceholder hasMore={false} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List>
</PullRefresh>
</div>
);
}

4
src/statics/svg/star.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="33" height="36" viewBox="0 0 33 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.57143 8.57143L12 0L15.4286 8.57143L24 12L15.4286 15.4286L12 24L8.57143 15.4286L0 12L8.57143 8.57143Z" fill="#FF5051"/>
<path opacity="0.24" d="M24.6429 27.6429L26.5 23L28.3571 27.6429L33 29.5L28.3571 31.3571L26.5 36L24.6429 31.3571L20 29.5L24.6429 27.6429Z" fill="#FF5051"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@ -1,3 +1,4 @@
export * from './app';
export * from './user';
export * from './message';
export * from './job';

7
src/store/actions/job.ts Normal file
View File

@ -0,0 +1,7 @@
import { CreateJobInfo } from '@/types/job';
import { CREATE_JOB } from '../constants';
export const cacheCreateJob = (value: CreateJobInfo) => ({ type: CREATE_JOB, value });
export const clearCachedJob = () => ({ type: CREATE_JOB, value: {} });

View File

@ -5,3 +5,4 @@ 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';
export const CREATE_JOB = 'CREATE_JOB';

View File

@ -18,10 +18,10 @@ const DEFAULT_LOCATION: LocationInfo = {
longitude: 113.280637,
};
const defaultAppMode = Taro.getStorageSync<RoleType>(CacheKey.APP_MODE_NEW) || RoleType.Anchor;
const defaultAppMode = Taro.getStorageSync<RoleType>(CacheKey.APP_MODE_NEW);
const INIT_STATE: AppState = {
roleType: defaultAppMode,
homePageType: defaultAppMode === RoleType.Anchor ? PageType.JOB : PageType.Anchor,
homePageType: defaultAppMode === RoleType.Company ? PageType.Anchor : PageType.JOB,
location: Taro.getStorageSync<LocationInfo>(CacheKey.CACHE_LOCATION_INFO) || DEFAULT_LOCATION,
};

View File

@ -1,6 +1,7 @@
import { combineReducers } from 'redux';
import appState from './app';
import jobState from './job';
import message from './message';
import partnerInfo from './partner';
import userInfo from './user';
@ -10,4 +11,5 @@ export default combineReducers({
userInfo,
message,
partnerInfo,
jobState,
});

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