400 lines
14 KiB
TypeScript
400 lines
14 KiB
TypeScript
import { Button, Image, Map, MapProps, Text } from '@tarojs/components';
|
||
import Taro, { useLoad, useShareAppMessage } from '@tarojs/taro';
|
||
|
||
import { Dialog } from '@taroify/core';
|
||
import React, { useCallback, useEffect, useState } from 'react';
|
||
|
||
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 PageLoading from '@/components/page-loading';
|
||
import { PrejobPopup } from '@/components/prejob-popup';
|
||
import ProductJobDialog from '@/components/product-dialog/job';
|
||
import CompanyPublishJobBuy from '@/components/product-dialog/steps-ui/company-publish-job-buy';
|
||
import { EventName, PageUrl, RoleType } from '@/constants/app';
|
||
import { CertificationStatusType } from '@/constants/company';
|
||
import { CollectEventName, ReportEventId } from '@/constants/event';
|
||
import { EMPLOY_TYPE_TITLE_MAP, GET_CONTACT_TYPE, JobManageStatus } from '@/constants/job';
|
||
import { ProductType } from '@/constants/product';
|
||
import useInviteCode from '@/hooks/use-invite-code';
|
||
import useUserInfo from '@/hooks/use-user-info';
|
||
import useRoleType from '@/hooks/user-role-type';
|
||
import { RESPONSE_ERROR_CODE } from '@/http/constant';
|
||
import { HttpError } from '@/http/error';
|
||
import store from '@/store';
|
||
import { cacheJobId } from '@/store/actions';
|
||
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 { getJobSalary, getJobTitle, postPublishJob, requestJobDetail } from '@/utils/job';
|
||
import { calcDistance, isValidLocation } from '@/utils/location';
|
||
import { requestProfileDetail } from '@/utils/material';
|
||
import { isChatWithSelf, postCreateChat } from '@/utils/message';
|
||
import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner';
|
||
import { requestProductBalance, requestProductUseRecord } from '@/utils/product';
|
||
import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route';
|
||
import { getCommonShareMessage } from '@/utils/share';
|
||
import { formatDate } from '@/utils/time';
|
||
import Toast from '@/utils/toast';
|
||
import { isNeedCreateMaterial } from '@/utils/user';
|
||
|
||
import './index.less';
|
||
|
||
const PREFIX = 'job-detail';
|
||
const log = logWithPrefix(PREFIX);
|
||
|
||
const getMapCallout = (data: JobDetails): MapProps.callout | undefined => {
|
||
if (!data.jobLocation?.address) {
|
||
return;
|
||
}
|
||
return {
|
||
display: 'ALWAYS',
|
||
content: data.jobLocation.address,
|
||
color: '#000000',
|
||
bgColor: '#FFFFFF',
|
||
fontSize: 12,
|
||
textAlign: 'center',
|
||
anchorX: 0,
|
||
anchorY: 0,
|
||
borderRadius: 4,
|
||
borderWidth: 0,
|
||
borderColor: '#FFFFFF',
|
||
padding: 4,
|
||
};
|
||
};
|
||
|
||
const AnchorFooter = (props: { data: JobDetails }) => {
|
||
const { data } = props;
|
||
const [errorTips, setErrorTips] = useState<string>('');
|
||
const [dialogVisible, setDialogVisible] = useState(false);
|
||
const [showMaterialGuide, setShowMaterialGuide] = useState(false);
|
||
|
||
const handleClickContact = useCallback(async () => {
|
||
log('handleClickContact');
|
||
if (!data) {
|
||
return;
|
||
}
|
||
reportEvent(ReportEventId.CLICK_JOB_CONTACT);
|
||
try {
|
||
const needCreateMaterial = await isNeedCreateMaterial();
|
||
|
||
if (data.sourcePlat !== 'bl') {
|
||
if (needCreateMaterial) {
|
||
const result = await requestProductUseRecord(ProductType.VIP, { jobId: data.id });
|
||
if (!result) {
|
||
const [time, isPaidVip] = await requestProductBalance(ProductType.VIP);
|
||
if (time <= 0 || !isPaidVip) {
|
||
setShowMaterialGuide(true);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (data.isAuthed) {
|
||
const toUserId = data.userId;
|
||
if (isChatWithSelf(toUserId)) {
|
||
Toast.error('不能与自己聊天');
|
||
return;
|
||
}
|
||
const chat = await postCreateChat(toUserId);
|
||
let materialMessage: null | IMaterialMessage = null;
|
||
if (!needCreateMaterial) {
|
||
const profile = await requestProfileDetail();
|
||
if (profile) {
|
||
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);
|
||
}
|
||
} catch (error) {
|
||
const e = error as HttpError;
|
||
const errorCode = e.errorCode;
|
||
if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE) {
|
||
setErrorTips('今日开聊次数已用完,请明日再来');
|
||
} else {
|
||
Toast.error('请求失败请重试');
|
||
}
|
||
}
|
||
}, [data]);
|
||
|
||
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`}>
|
||
<Button className={`${PREFIX}__share-button`} openType="share">
|
||
分享
|
||
</Button>
|
||
<LoginButton className={`${PREFIX}__contact-publisher`} onClick={handleClickContact}>
|
||
{data.isAuthed ? '在线沟通' : '立即联系'}
|
||
</LoginButton>
|
||
</div>
|
||
<div>
|
||
{dialogVisible && <ProductJobDialog data={data} onClose={handleDialogHidden} />}
|
||
{showMaterialGuide && (
|
||
<PrejobPopup onCancel={() => setShowMaterialGuide(false)} onConfirm={handleConfirmPrejob} />
|
||
)}
|
||
<CommonDialog
|
||
content={errorTips}
|
||
confirm="确定"
|
||
visible={!!errorTips}
|
||
onClose={() => setErrorTips('')}
|
||
onClick={() => setErrorTips('')}
|
||
/>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const CompanyFooter = (props: { data: JobDetails }) => {
|
||
const { data } = props;
|
||
const [showBuy, setShowBuy] = useState(false);
|
||
const userInfo = useUserInfo();
|
||
|
||
const handleClickEdit = useCallback(() => navigateTo(PageUrl.JobPublish, { jobId: data.id }), [data]);
|
||
|
||
const handlePublishJob = useCallback(async () => {
|
||
try {
|
||
if (userInfo.bossAuthStatus !== CertificationStatusType.Success) {
|
||
store.dispatch(cacheJobId(data.id));
|
||
navigateTo(PageUrl.CertificationStart);
|
||
return;
|
||
}
|
||
Taro.showLoading();
|
||
await postPublishJob(data.id);
|
||
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
|
||
setShowBuy(false);
|
||
Toast.success('发布成功');
|
||
Taro.hideLoading();
|
||
} catch (error) {
|
||
Taro.hideLoading();
|
||
const e = error as HttpError;
|
||
const errorCode = e.errorCode;
|
||
const errorMsg = e.info?.() || e.message;
|
||
collectEvent(CollectEventName.PUBLISH_OPEN_JOB_FAILED, { jobId: data.id, error: e.info?.() || e.message });
|
||
if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE) {
|
||
Toast.info('您购买的产品已耗尽使用次数');
|
||
setShowBuy(true);
|
||
} else if (errorCode === RESPONSE_ERROR_CODE.BOSS_VIP_EXPIRED) {
|
||
Toast.info('该通告已到期,请创建新通告', 3000);
|
||
} else {
|
||
Toast.error(errorMsg || '发布失败请重试', 3000);
|
||
}
|
||
console.error(e);
|
||
}
|
||
}, [data]);
|
||
|
||
return (
|
||
<>
|
||
<div className={`${PREFIX}__footer`}>
|
||
<Button className={`${PREFIX}__share-button`} onClick={handleClickEdit}>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
disabled={data.status === JobManageStatus.Open}
|
||
className={`${PREFIX}__contact-publisher`}
|
||
onClick={handlePublishJob}
|
||
>
|
||
发布通告
|
||
</Button>
|
||
</div>
|
||
<Dialog open={showBuy} onClose={() => setShowBuy(false)}>
|
||
<Dialog.Content>
|
||
<CompanyPublishJobBuy onNext={handlePublishJob} />
|
||
</Dialog.Content>
|
||
</Dialog>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default function JobDetail() {
|
||
const roleType = useRoleType();
|
||
const userInfo = useUserInfo();
|
||
const [data, setData] = useState<JobDetails | null>(null);
|
||
const isOwner = roleType === RoleType.Company && userInfo.userId === data?.userId;
|
||
const inviteCode = useInviteCode();
|
||
|
||
const onDev = useCallback(async () => data && copy(data.id), [data]);
|
||
|
||
const handleClickMap = useCallback(
|
||
(e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
|
||
if (!data?.jobLocation) {
|
||
return;
|
||
}
|
||
Taro.openLocation({
|
||
longitude: Number(data.jobLocation.longitude),
|
||
latitude: Number(data.jobLocation.latitude),
|
||
address: data.jobLocation.address,
|
||
});
|
||
},
|
||
[data]
|
||
);
|
||
|
||
useEffect(() => {
|
||
const callback = async (jobId: string) => {
|
||
try {
|
||
const res = await requestJobDetail(jobId);
|
||
setData(res);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
Taro.eventCenter.on(EventName.JOB_UPDATE, callback);
|
||
return () => {
|
||
Taro.eventCenter.off(EventName.JOB_UPDATE, callback);
|
||
};
|
||
}, []);
|
||
|
||
useLoad(async () => {
|
||
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) {
|
||
return;
|
||
}
|
||
Taro.eventCenter.trigger(EventName.VIEW_JOB_SUCCESS, jobId);
|
||
try {
|
||
const res = await requestJobDetail(jobId);
|
||
setData(res);
|
||
} catch (e) {
|
||
console.error(e);
|
||
Toast.error('出错了,请重试');
|
||
}
|
||
});
|
||
|
||
useShareAppMessage(() => {
|
||
if (!data) {
|
||
return getCommonShareMessage({ inviteCode });
|
||
}
|
||
|
||
return {
|
||
title: getJobTitle(data) || '',
|
||
path: getJumpUrl(PageUrl.JobDetail, { id: data.id, share: true, c: inviteCode }),
|
||
};
|
||
});
|
||
|
||
if (!data) {
|
||
return <PageLoading />;
|
||
}
|
||
|
||
return (
|
||
<div className={PREFIX}>
|
||
<div className={`${PREFIX}__container`}>
|
||
<div className={`${PREFIX}__header`}>
|
||
<div className={`${PREFIX}__header-info`}>
|
||
<DevDiv className={`${PREFIX}__header-title`} OnDev={onDev}>
|
||
{getJobTitle(data)}
|
||
</DevDiv>
|
||
<div className={`${PREFIX}__employ-type`}>{EMPLOY_TYPE_TITLE_MAP[data.employType]}</div>
|
||
</div>
|
||
<div className={`${PREFIX}__salary`}>{getJobSalary(data)}</div>
|
||
<div className={`${PREFIX}__update-time`}>{`${formatDate(data.updated)}更新`}</div>
|
||
<div className={`${PREFIX}__tips`}>请注意甄别通告真假,谨防上当</div>
|
||
<div className={`${PREFIX}__publisher`}>
|
||
<div className={`${PREFIX}__publisher-name`}>{`发布人:${data.publisher}`}</div>
|
||
{data.isAuthed && (
|
||
<CertificationStatusIcon
|
||
className={`${PREFIX}__certification-type`}
|
||
status={CertificationStatusType.Success}
|
||
small
|
||
/>
|
||
)}
|
||
<Image
|
||
mode="aspectFit"
|
||
className={`${PREFIX}__publisher-avatar`}
|
||
src={data.publisherAvatar || require('@/statics/svg/wechat.svg')}
|
||
/>
|
||
</div>
|
||
{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`}>
|
||
{(data.tags || []).map((keyword: string, index) => (
|
||
<div className={`${PREFIX}__tag`} key={index}>
|
||
{keyword}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<Text className={`${PREFIX}__description`}>{data.sourceText}</Text>
|
||
<div className={`${PREFIX}__address-wrapper`}>
|
||
<Image className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||
<div className={`${PREFIX}__detailed-address`}>{data.jobLocation?.address}</div>
|
||
</div>
|
||
{data.distance && (
|
||
<div className={`${PREFIX}__distance-wrapper`}>
|
||
<Image className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||
<div className={`${PREFIX}__distance`}>{calcDistance(data.distance)}</div>
|
||
</div>
|
||
)}
|
||
{isValidLocation(data.jobLocation) && (
|
||
<div className={`${PREFIX}__map__wrapper`} onClick={handleClickMap}>
|
||
<Map
|
||
enableZoom={false}
|
||
enableScroll={false}
|
||
className={`${PREFIX}__map`}
|
||
latitude={Number(data.jobLocation.latitude)}
|
||
longitude={Number(data.jobLocation.longitude)}
|
||
markers={[
|
||
{
|
||
id: 0,
|
||
latitude: Number(data.jobLocation.latitude),
|
||
longitude: Number(data.jobLocation.longitude),
|
||
callout: getMapCallout(data),
|
||
iconPath: '',
|
||
width: 20,
|
||
height: 36,
|
||
},
|
||
]}
|
||
onError={() => Toast.error('地图加载错误')}
|
||
/>
|
||
<div className={`${PREFIX}__map__mask`} onClick={handleClickMap} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
{!isOwner && <JobRecommendList />}
|
||
<div className={`${PREFIX}__bottom-space`} />
|
||
</div>
|
||
{!isOwner && <AnchorFooter data={data} />}
|
||
{isOwner && <CompanyFooter data={data} />}
|
||
</div>
|
||
);
|
||
}
|