Files
boluo-app-main/src/pages/job-detail/index.tsx
chashaobao 2cb532c3d7 feat:
2025-08-24 17:35:13 +08:00

400 lines
14 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 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>
);
}