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,209 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.profile-view-fragment {
padding-bottom: 88rpx;
&__header {
position: relative;
width: 100%;
&__swiper {
width: 100%;
height: 1080px;
}
&__swiper-item {
width: 100%;
height: 1080px;
position: relative;
}
&__cover {
width: 100%;
height: 100%;
}
&__cover-preview {
width: 70px;
height: 70px;
position: absolute;
top: 50%;
right: 50%;
transform: translate3d(50%, -50%, 0);
}
&__edit-video {
position: absolute;
top: 32px;
right: 24px;
width: 176px;
height: 56px;
line-height: 56px;
text-align: center;
font-size: 28px;
font-weight: 400;
color: #FFFFFF;
background: #0000005C;
border-radius: 48px;
}
&__banner {
position: absolute;
bottom: 0;
width: 100%;
height: 72px;
padding: 0 24px;
.flex-row();
justify-content: space-between;
font-size: 32px;
line-height: 48px;
font-weight: 500;
color: #FFFFFF;
box-sizing: border-box;
background: linear-gradient(349.14deg, rgba(0, 0, 0, 0.3) 13.62%, rgba(0, 0, 0, 0.195) 126.8%);
}
&__video-size {
font-size: 24px;
font-weight: 400;
}
}
&__body {
margin: 24px;
// 底部按钮栏高度 + 边距
margin-bottom: calc(24px + 24px);
// margin-bottom: calc(112px + 24px);
width: calc(100% - 48px);
background: #FFFFFF;
border-radius: 16px;
}
&__divider {
position: absolute;
left: 24px;
right: 24px;
bottom: 0;
height: 1px;
background: #E0E0E0;
}
&__basic-info {
position: relative;
padding: 40px;
&__name-container {
.flex-row();
}
&__name {
font-size: 36px;
line-height: 48px;
font-weight: 500;
color: @blColor;
}
&__edit {
font-size: 28px;
line-height: 32px;
font-weight: 400;
color: @blHighlightColor;
margin-left: 8px;
}
&__content {
font-size: 24px;
line-height: 40px;
font-weight: 400;
color: @blColor;
margin-top: 12px;
}
}
&__info-group {
position: relative;
width: 100%;
padding: 40px;
box-sizing: border-box;
}
&__info-group__header {
.flex-row();
justify-content: space-between;
&__title {
font-size: 28px;
line-height: 40px;
font-weight: 500;
color: @blColor;
}
&__edit {
font-size: 28px;
line-height: 32px;
font-weight: 400;
color: @blHighlightColor;
}
}
&__info-group__items {
.flex-row();
align-items: flex-start;
flex-wrap: wrap;
}
&__info-group__item {
width: 50%;
margin-top: 32px;
&.full-line {
width: 100%;
}
&__title {
font-size: 24px;
line-height: 36px;
font-weight: 400;
color: @blColorG1;
}
&__value {
font-size: 28px;
line-height: 40px;
font-weight: 500;
color: @blColor;
margin-top: 4px;
white-space: pre-wrap;
word-break: break-all;
}
}
&__footer {
position: fixed;
bottom: 0;
width: 100%;
background: #FFFFFF;
padding: 12px 32px;
box-shadow: 0px -4px 20px 0px #00000014;
box-sizing: border-box;
&__buttons {
.flex-row();
&__share {
.button(@height: 88px, @fontSize: 32px, @fontWeight: 500, @borderRadius: 48px);
flex: 1 1;
color: @blHighlightColor;
background: @blHighlightBg;
}
&__manager {
.button(@height: 88px, @fontSize: 32px, @fontWeight: 500, @borderRadius: 48px);
flex: 2 2;
margin-left: 32px;
}
}
}
}

View File

@ -0,0 +1,223 @@
import { Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Swiper } from '@taroify/core';
import classNames from 'classnames';
import { useCallback, useState } from 'react';
import DevDiv from '@/components/dev-div';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { OpenSource, PageUrl } from '@/constants/app';
import { CITY_CODE_TO_NAME_MAP } from '@/constants/city';
import { FULL_PRICE_OPTIONS, PART_PRICE_OPTIONS } from '@/constants/job';
import { ProfileGroupType, ProfileTitleMap, WORK_YEAR_OPTIONS } from '@/constants/material';
import { MaterialProfile } from '@/types/material';
import { logWithPrefix, isDesktop } from '@/utils/common';
import { getBasicInfo, sortVideos } from '@/utils/material';
import { navigateTo } from '@/utils/route';
import './index.less';
interface IProps {
editable: boolean;
profile: MaterialProfile;
onDev?: () => void;
}
const PREFIX = 'profile-view-fragment';
const log = logWithPrefix(PREFIX);
const DEFAULT_TEXT = '未填写';
const getIndentCity = (codeString: string = '') => {
const codes = codeString.split('、');
const cityNames: string[] = [];
codes.forEach(code => {
const cityName = CITY_CODE_TO_NAME_MAP.get(code);
cityName && cityNames.push(cityName);
});
return cityNames.join('、');
};
const getIndentPrice = (min: number, max: number, full: boolean) => {
if (!min && !max) {
return DEFAULT_TEXT;
}
const options = full ? FULL_PRICE_OPTIONS : PART_PRICE_OPTIONS;
const option = options.find(o => o.value.minSalary === min && o.value.maxSalary === max);
if (option) {
return option.label;
}
const unit = full ? 'K' : '';
const prices: string[] = [];
min && prices.push(`${full ? Number(min) / 1000 : min}${unit}`);
max && prices.push(`${full ? Number(max) / 1000 : max}${unit}`);
return prices.join(' - ');
};
const getWorkYear = (year: number) => {
if (!year) {
return DEFAULT_TEXT;
}
const y = Math.min(year, 3);
return WORK_YEAR_OPTIONS.find(option => option.value === y)?.label || `${y}`;
};
const getDataGroup = (profile: MaterialProfile | null) => {
if (!profile) {
return [];
}
return [
{
title: ProfileTitleMap[ProfileGroupType.Intention],
type: ProfileGroupType.Intention,
items: [
{ title: '全职薪资', value: getIndentPrice(profile.fullTimeMinPrice, profile.fullTimeMaxPrice, true) || '' },
{ title: '兼职薪资', value: getIndentPrice(profile.partyTimeMinPrice, profile.partyTimeMaxPrice, false) || '' },
{ title: '意向城市', value: getIndentCity(profile.cityCodes) || DEFAULT_TEXT },
{ title: '是否接受坐班', value: profile.acceptWorkForSit ? '是' : '否' },
],
},
{
title: ProfileTitleMap[ProfileGroupType.Experience],
type: ProfileGroupType.Experience,
items: [
{ title: '直播年限', value: getWorkYear(profile.workedYear) || DEFAULT_TEXT },
{ title: '直播过的账号', value: profile.workedAccounts || DEFAULT_TEXT },
{ title: '自然流起号经验', value: Boolean(profile.newAccountExperience) ? '有' : '无' },
{ title: '直播过的品类', value: profile.workedSecCategoryStr || DEFAULT_TEXT },
{ title: '最高GMV', value: profile.maxGmv ? `${profile.maxGmv / 10000}` : DEFAULT_TEXT },
{ title: '最高在线人数', value: profile.maxOnline || DEFAULT_TEXT },
],
},
{
title: ProfileTitleMap[ProfileGroupType.Advantages],
type: ProfileGroupType.Advantages,
items: [{ title: '自身优势', value: profile.advantages || DEFAULT_TEXT, fullLine: true }],
},
];
};
export default function ProfileViewFragment(props: IProps) {
const { profile, editable, onDev } = props;
const [coverIndex, setCoverIndex] = useState(0);
const dataGroup = getDataGroup(profile);
const videos = sortVideos(profile?.materialVideoInfoList || []);
const handleClickVideo = useCallback(
(index: number) => {
log('handleClickVideo', index);
if (isDesktop) {
navigateTo(PageUrl.MaterialWebview, {
source: encodeURIComponent(videos[index].url)
})
} else {
Taro.previewMedia({
sources: videos.map(video => ({ url: video.url, type: video.type })),
current: index,
});
}
},
[videos]
);
const handleEditVideo = useCallback(() => {
log('handleEditBasic');
navigateTo(PageUrl.MaterialUploadVideo, { source: OpenSource.UserPage });
}, []);
const handleEditBasic = useCallback(() => {
log('handleEditBasic');
navigateTo(PageUrl.MaterialEditProfile, { type: ProfileGroupType.Basic });
}, []);
const handleEditGroupItem = useCallback((groupType: ProfileGroupType) => {
log('handleEditGroupItem', groupType);
navigateTo(PageUrl.MaterialEditProfile, { type: groupType });
}, []);
return (
<div className={PREFIX}>
<div className={`${PREFIX}__header`}>
<Swiper className={`${PREFIX}__header__swiper`} onChange={setCoverIndex} stopPropagation={false} lazyRender>
{videos.map((cover, index) => (
<Swiper.Item key={cover.coverUrl}>
<div className={`${PREFIX}__header__swiper-item`}>
<Image
mode="aspectFill"
src={cover.coverUrl}
className={`${PREFIX}__header__cover`}
onClick={() => handleClickVideo(index)}
/>
{cover.type === 'video' && (
<Image
className={`${PREFIX}__header__cover-preview`}
mode="aspectFit"
src={require('@/statics/svg/preview_video.svg')}
onClick={() => handleClickVideo(index)}
/>
)}
</div>
</Swiper.Item>
))}
</Swiper>
{editable && (
<div className={`${PREFIX}__header__edit-video`} onClick={handleEditVideo}>
</div>
)}
<div className={`${PREFIX}__header__banner`}>
<div>{videos[coverIndex]?.title || ''}</div>
<div
className={`${PREFIX}__header__video-size`}
>{`${coverIndex + 1}/${profile.materialVideoInfoList.length}`}</div>
</div>
</div>
<div className={`${PREFIX}__body`}>
<div className={`${PREFIX}__basic-info`}>
<div className={`${PREFIX}__basic-info__name-container`}>
<DevDiv className={`${PREFIX}__basic-info__name`} OnDev={onDev}>
{profile.name}
</DevDiv>
{editable && (
<div className={`${PREFIX}__basic-info__edit`} onClick={handleEditBasic}>
</div>
)}
</div>
<div className={`${PREFIX}__basic-info__content`}>{getBasicInfo(profile)}</div>
<div className={`${PREFIX}__divider`} />
</div>
{dataGroup.map((data, index: number) => (
<div className={`${PREFIX}__info-group`} key={data.type}>
<div className={`${PREFIX}__info-group__header`}>
<div className={`${PREFIX}__info-group__header__title`}>{data.title}</div>
{editable && (
<div className={`${PREFIX}__info-group__header__edit`} onClick={() => handleEditGroupItem(data.type)}>
</div>
)}
</div>
<div className={`${PREFIX}__info-group__items`}>
{data.items.map(item => (
<div
key={String(item.value)}
className={classNames(`${PREFIX}__info-group__item`, { 'full-line': item.fullLine })}
>
<div className={`${PREFIX}__info-group__item__title`}>{item.title}</div>
<div className={`${PREFIX}__info-group__item__value`}>{item.value}</div>
</div>
))}
</div>
{index !== dataGroup.length - 1 && <div className={`${PREFIX}__divider`} />}
</div>
))}
</div>
<SafeBottomPadding />
</div>
);
}