feat: first commit

This commit is contained in:
eleanor.mao
2025-03-31 22:34:22 +08:00
commit d25187c9c8
390 changed files with 57031 additions and 0 deletions

View File

@ -0,0 +1,330 @@
import { Map, MapProps, Text, Image, Button } from '@tarojs/components';
import Taro, { useLoad, useShareAppMessage } from '@tarojs/taro';
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 LoginButton from '@/components/login-button';
import MaterialGuide from '@/components/material-guide';
import PageLoading from '@/components/page-loading';
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 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 { copy, logWithPrefix } from '@/utils/common';
import { collectEvent, reportEvent } from '@/utils/event';
import { getJobTitle, getJobSalary, postPublishJob, requestJobDetail } from '@/utils/job';
import { calcDistance, isValidLocation } from '@/utils/location';
import { requestProfileDetail } from '@/utils/material';
import { isChatWithSelf, postCreateChat } from '@/utils/message';
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 (needCreateMaterial) {
setShowMaterialGuide(true);
return;
}
if (data.isAuthed) {
const toUserId = data.userId;
if (isChatWithSelf(toUserId)) {
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 });
} 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), []);
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 && <MaterialGuide onClose={() => setShowMaterialGuide(false)} />}
<CommonDialog
content={errorTips}
confirm="确定"
visible={!!errorTips}
onClose={() => setErrorTips('')}
onClick={() => setErrorTips('')}
/>
</div>
</>
);
};
const CompanyFooter = (props: { data: JobDetails }) => {
const { data } = props;
const handleClickEdit = useCallback(() => navigateTo(PageUrl.JobPublish, { jobId: data.id }), [data]);
const handlePublishJob = useCallback(async () => {
try {
Taro.showLoading();
await postPublishJob(data.id);
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
Toast.success('发布成功');
Taro.hideLoading();
} catch (error) {
Taro.hideLoading();
const e = error as HttpError;
const errorCode = e.errorCode;
collectEvent(CollectEventName.PUBLISH_OPEN_JOB_FAILED, { jobId: data.id, error: e.info?.() || e.message });
if (errorCode === RESPONSE_ERROR_CODE.BOSS_VIP_EXPIRED) {
Toast.info('该通告已到期,请创建新通告', 3000);
} else {
Toast.error(e.message || '发布失败请重试', 3000);
}
console.error(e);
}
}, [data]);
return (
<>
<div className={`${PREFIX}__footer`}>
<Button className={`${PREFIX}__share-button`} onClick={handleClickEdit}>
</Button>
<Button className={`${PREFIX}__contact-publisher`} onClick={handlePublishJob}>
</Button>
</div>
</>
);
};
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 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'>>();
const jobId = query?.id;
if (!jobId) {
return;
}
try {
const res = await requestJobDetail(jobId);
setData(res);
} catch (e) {
console.error(e);
Toast.error('出错了,请重试');
}
});
useShareAppMessage(() => {
if (!data) {
return getCommonShareMessage();
}
return {
title: getJobTitle(data) || '',
path: getJumpUrl(PageUrl.JobDetail, { id: data.id, share: true }),
};
});
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>
<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>
);
}