Files
boluo-app-main/src/pages/job-detail/index.tsx
chashaobao f6cec5dd43 feat:
2025-12-27 16:00:25 +08:00

452 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ProductJobContactDialog from '@/components/product-dialog/job-contact';
import CompanyPublishJobBuy from '@/components/product-dialog/steps-ui/company-publish-job-buy';
import { EventName, PageUrl, RoleType } from '@/constants/app';
import { CertificationStatusType } from '@/constants/company';
import { 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 { usePublishJob } from '@/hooks/use-publish-job';
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 { JobDetails } from '@/types/job';
import { IMaterialMessage } from '@/types/message';
import { GetProductIsUnlockResponse, ProductInfo } from '@/types/product';
import { switchRoleType } from '@/utils/app';
import { copy, logWithPrefix } from '@/utils/common';
import { reportEvent } from '@/utils/event';
import { getJobSalary, getJobTitle, 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 { isNeedPhone } 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 [showJobContactDialog, setShowJobContactDialog] = useState(false);
const [showMaterialGuide, setShowMaterialGuide] = useState(false);
const [productInfo, setProductInfo] = useState<undefined | ProductInfo>();
const [productRecord, setProductRecord] = useState<undefined | GetProductIsUnlockResponse>();
const userInfo = useUserInfo();
const needPhone = isNeedPhone(userInfo);
const getProductRecord = useCallback(async () => {
const result = await requestProductUseRecord(ProductType.VIP, { jobId: data.id });
setProductRecord(result);
}, [data.id]);
const getProductBalance = useCallback(async (loading?: boolean) => {
if (loading) {
Taro.showLoading();
}
const [, resp] = await requestProductBalance(ProductType.VIP);
setProductInfo(resp);
console.log(resp);
if (loading) {
Taro.hideLoading();
}
return resp;
}, []);
const handleClickContact = useCallback(async () => {
log('handleClickContact');
if (!data) {
return;
}
reportEvent(ReportEventId.CLICK_JOB_CONTACT);
try {
if (data.isAuthed) {
const toUserId = data.userId;
if (isChatWithSelf(toUserId)) {
Toast.error('不能与自己聊天');
return;
}
const chat = await postCreateChat(toUserId);
let materialMessage: null | IMaterialMessage = null;
if (!!productInfo?.isCreateResume) {
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 {
// Show material guide if no resume and no VIP and no free balance
if (!productRecord && !productInfo?.isCreateResume && !productInfo?.isPaidVip && !productInfo?.freeBalance) {
setShowMaterialGuide(true);
return;
}
// Open integrated dialog - it handles buy + contact internally
setShowJobContactDialog(true);
}
} catch (error) {
const e = error as HttpError;
const errorCode = e.errorCode;
if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE) {
setErrorTips('今日开聊次数已用完,请明日再来');
} else {
Toast.error('请求失败请重试');
}
}
}, [data, productInfo?.freeBalance, productInfo?.isCreateResume, productInfo?.isPaidVip]);
const handleDialogClose = useCallback(() => {
setShowJobContactDialog(false);
// Refresh data after dialog closes
getProductRecord();
}, [getProductRecord]);
const handleConfirmPrejob = useCallback(
(type: GET_CONTACT_TYPE) => {
setShowMaterialGuide(false);
if (GET_CONTACT_TYPE.VIP === type) {
getProductBalance().then(() => {
setShowJobContactDialog(true);
});
}
},
[getProductBalance]
);
// const unAuthedButtonText = useMemo(() => {
// if (haveSeen) {
// return '查看联系方式';
// }
//
// if (productInfo?.isPaidVip) {
// return '您是会员,可直接查看';
// }
//
// if (productInfo?.freeBalance) {
// return `还剩${productInfo.freeBalance}次查看次数`;
// }
//
// return productInfo?.isCreateResume? '升级会员即可查看': '创建模卡,免费查看';
// }, [productInfo, haveSeen]);
const handleRefresh = useCallback(async () => {
await getProductBalance(true);
}, [getProductBalance]);
useEffect(() => {
Taro.eventCenter.on(EventName.CREATE_PROFILE, getProductBalance);
return () => {
Taro.eventCenter.off(EventName.CREATE_PROFILE);
};
}, [getProductBalance]);
useEffect(() => {
getProductBalance();
}, [getProductBalance]);
useEffect(() => {
getProductRecord();
}, [getProductRecord]);
return (
<>
<div className={`${PREFIX}__footer`}>
<Button className={`${PREFIX}__share-button`} openType="share">
</Button>
<LoginButton
needRefresh
onRefresh={handleRefresh}
className={`${PREFIX}__contact-publisher`}
onClick={handleClickContact}
>
{data.isAuthed ? '在线沟通' : '查看联系方式'}
{needPhone ? (
<div className={`${PREFIX}__contact-publisher-tag`}></div>
) : !productRecord && (data.isAuthed || productInfo?.content) ? (
<div className={`${PREFIX}__contact-publisher-tag`}>
{data.isAuthed ? '急招岗位可免费查看' : productInfo?.content}
</div>
) : null}
</LoginButton>
</div>
<div>
{showJobContactDialog && (
<ProductJobContactDialog
data={data}
productRecord={productRecord}
productInfo={productInfo}
onRefreshBalance={getProductBalance}
onClose={handleDialogClose}
/>
)}
{showMaterialGuide && (
<PrejobPopup
isCreateResume={productInfo?.isCreateResume}
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, handlePublishJob] = usePublishJob(data.id);
const handleClickEdit = useCallback(() => navigateTo(PageUrl.JobPublish, { jobId: data.id }), [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('出错了,请重试');
}
});
Taro.showShareMenu({
withShareTicket: true,
});
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>
);
}