feat: first commit
This commit is contained in:
3
src/app.config.ts
Normal file
3
src/app.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { APP_CONFIG } from './hooks/use-config';
|
||||
|
||||
export default defineAppConfig(APP_CONFIG);
|
15
src/app.less
Normal file
15
src/app.less
Normal file
@ -0,0 +1,15 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.base-bg {
|
||||
background: @pageBg;
|
||||
}
|
||||
|
||||
page {
|
||||
.base-bg();
|
||||
// 全部覆盖 taroify tabs 的背景色
|
||||
--tabs-nav-background-color: @pageBg;
|
||||
|
||||
.taroify-tabs__wrap__scroll {
|
||||
.base-bg();
|
||||
}
|
||||
}
|
29
src/app.tsx
Normal file
29
src/app.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useLaunch } from '@tarojs/taro';
|
||||
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { REFRESH_UNREAD_COUNT_TIME } from '@/constants/message';
|
||||
import http from '@/http';
|
||||
import store from '@/store';
|
||||
import { requestUnreadMessageCount } from '@/utils/message';
|
||||
import qiniuUpload from '@/utils/qiniu-upload';
|
||||
import { requestUserInfo, updateLastLoginTime } from '@/utils/user';
|
||||
|
||||
import './app.less';
|
||||
|
||||
function App({ children }: PropsWithChildren<BL.Anything>) {
|
||||
useLaunch(async () => {
|
||||
console.log('App launched.');
|
||||
await http.init();
|
||||
requestUserInfo();
|
||||
updateLastLoginTime();
|
||||
qiniuUpload.init();
|
||||
requestUnreadMessageCount();
|
||||
setInterval(() => requestUnreadMessageCount(), REFRESH_UNREAD_COUNT_TIME);
|
||||
});
|
||||
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
|
||||
export default App;
|
123
src/components/anchor-card/index.less
Normal file
123
src/components/anchor-card/index.less
Normal file
@ -0,0 +1,123 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.anchor-card {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
background: #FFFFFF;
|
||||
|
||||
.color(@defaultColor) {
|
||||
color: var(--read-color, @defaultColor);
|
||||
}
|
||||
|
||||
&__cover {
|
||||
position: relative;
|
||||
width: 188px;
|
||||
min-width: 188px;
|
||||
max-width: 188px;
|
||||
height: 242px;
|
||||
min-height: 242px;
|
||||
max-height: 242px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__cover-skeleton {
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__info-container {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin-right: 10px;
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
.color(@blColor);
|
||||
}
|
||||
|
||||
@maxTextWidth: 49vw;
|
||||
|
||||
&__basic {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG2);
|
||||
margin-top: 16px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__categories {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG2);
|
||||
margin-top: 12px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__year {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG2);
|
||||
margin-top: 12px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__salary {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
.color(@blHighlightColor);
|
||||
margin-top: 20px;
|
||||
.noWrap();
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
height: 242px;
|
||||
.flex-column();
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG1);
|
||||
}
|
||||
|
||||
&__distance-wrapper {
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__distance-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&__distance {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG1);
|
||||
}
|
||||
}
|
82
src/components/anchor-card/index.tsx
Normal file
82
src/components/anchor-card/index.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Image as TaroImage } from '@tarojs/components';
|
||||
|
||||
import { Image } from '@taroify/core';
|
||||
import { PhotoFail } from '@taroify/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import SkeletonLoading from '@/components/skeleton-loading';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { MaterialViewSource, WORK_YEAR_LABELS } from '@/constants/material';
|
||||
import { AnchorInfo } from '@/types/material';
|
||||
import { calcDistance } from '@/utils/location';
|
||||
import { getBasicInfo } from '@/utils/material';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import { activeDate } from '@/utils/time';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: AnchorInfo;
|
||||
jobId?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'anchor-card';
|
||||
const getSalary = (data: AnchorInfo) => {
|
||||
const { fullTimeMinPrice, fullTimeMaxPrice, partyTimeMinPrice, partyTimeMaxPrice } = data;
|
||||
const prices: string[] = [];
|
||||
if (fullTimeMinPrice && fullTimeMaxPrice) {
|
||||
prices.push(`${fullTimeMinPrice / 1000}-${fullTimeMaxPrice / 1000}K/月`);
|
||||
}
|
||||
if (partyTimeMinPrice && partyTimeMaxPrice) {
|
||||
prices.push(`${partyTimeMinPrice}-${partyTimeMaxPrice}/小时`);
|
||||
}
|
||||
return prices.filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
function AnchorCard(props: IProps) {
|
||||
const { data, jobId } = props;
|
||||
const style = data.isRead ? ({ '--read-color': '#999999' } as React.CSSProperties) : {};
|
||||
const cover = (data.materialVideoInfoList.find(video => video.isDefault) || data.materialVideoInfoList[0])?.coverUrl;
|
||||
|
||||
const handleClick = useCallback(
|
||||
() => navigateTo(PageUrl.MaterialView, { jobId, resumeId: data.id, source: MaterialViewSource.AnchorList }),
|
||||
[data, jobId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} style={style} onClick={handleClick}>
|
||||
<Image
|
||||
lazyLoad
|
||||
src={cover}
|
||||
width={188}
|
||||
height={242}
|
||||
mode="aspectFill"
|
||||
fallback={<PhotoFail />}
|
||||
className={`${PREFIX}__cover`}
|
||||
placeholder={<SkeletonLoading customName={`${PREFIX}__cover-skeleton`} />}
|
||||
/>
|
||||
<div className={`${PREFIX}__info-container`}>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
<div className={`${PREFIX}__info__title`}>{data.name}</div>
|
||||
<div className={`${PREFIX}__info__basic`}>{getBasicInfo(data)}</div>
|
||||
<div className={`${PREFIX}__info__year`}>{WORK_YEAR_LABELS[data.workedYear] || ''}</div>
|
||||
{data.workedSecCategoryStr && (
|
||||
<div className={`${PREFIX}__info__categories`}>{`播过 ${data.workedSecCategoryStr}`}</div>
|
||||
)}
|
||||
<div className={`${PREFIX}__info__salary`}>{getSalary(data)}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__right`}>
|
||||
<div className={`${PREFIX}__active-time`}>{activeDate(data.sortTime)}</div>
|
||||
{typeof data.distance !== 'undefined' && (
|
||||
<div className={`${PREFIX}__distance-wrapper`}>
|
||||
<TaroImage className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||||
<div className={`${PREFIX}__distance`}>{calcDistance(data.distance, 1)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnchorCard;
|
0
src/components/anchor-list/index.less
Normal file
0
src/components/anchor-list/index.less
Normal file
209
src/components/anchor-list/index.tsx
Normal file
209
src/components/anchor-list/index.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import AnchorCard from '@/components/anchor-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { AnchorSortType } from '@/constants/material';
|
||||
import { AnchorInfo, GetAnchorListRequest, IAnchorFilters } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestAnchorList as requestData } from '@/utils/material';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetAnchorListRequest> {
|
||||
filters?: IAnchorFilters;
|
||||
}
|
||||
|
||||
export interface IAnchorListProps extends IRequestProps {
|
||||
ready?: boolean;
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 10;
|
||||
const PREFIX = 'anchor-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function AnchorList(props: IAnchorListProps) {
|
||||
const {
|
||||
className,
|
||||
listHeight,
|
||||
refreshDisabled,
|
||||
jobId,
|
||||
filters,
|
||||
cityCode = 'ALL',
|
||||
sortType = AnchorSortType.Recommend,
|
||||
latitude,
|
||||
longitude,
|
||||
ready,
|
||||
onListEmpty,
|
||||
} = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<AnchorInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
const prevRequestProps = useRef<IRequestProps>({});
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, data: anchorResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(anchorResults);
|
||||
currentPage.current = page;
|
||||
!anchorResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
data: anchorResults,
|
||||
} = await requestData({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...anchorResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, hasMore]);
|
||||
|
||||
const handleReadMaterial = useCallback(
|
||||
(materialId: string) => {
|
||||
const index = dataList.findIndex(d => String(d.id) === materialId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
const material = dataList[index];
|
||||
if (!material || material.isRead) {
|
||||
return;
|
||||
}
|
||||
log('auto mark read', materialId);
|
||||
dataList.splice(index, 1, { ...material, isRead: true });
|
||||
setDataList([...dataList]);
|
||||
},
|
||||
[dataList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = {
|
||||
...filters,
|
||||
jobId,
|
||||
cityCode,
|
||||
sortType,
|
||||
latitude,
|
||||
longitude,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
}, [jobId, filters, cityCode, sortType, latitude, longitude]);
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.VIEW_MATERIAL_SUCCESS, handleReadMaterial);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.VIEW_MATERIAL_SUCCESS, handleReadMaterial);
|
||||
};
|
||||
}, [handleReadMaterial]);
|
||||
|
||||
// 初始化数据&配置变更后刷新数据
|
||||
useEffect(() => {
|
||||
// 相比前一次可见时没有数据变更时,不再重新请求
|
||||
if (isEqual(prevRequestProps.current, requestProps.current)) {
|
||||
log('visible/city changed, but request params not change, ignore');
|
||||
return;
|
||||
}
|
||||
// 列表不可见时,先不做处理
|
||||
if (!ready) {
|
||||
log('visible/city changed, but is not ready, only refresh list');
|
||||
return;
|
||||
}
|
||||
|
||||
prevRequestProps.current = requestProps.current;
|
||||
const refresh = async () => {
|
||||
log('visible/city changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, data: anchorResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(anchorResults);
|
||||
currentPage.current = page;
|
||||
!anchorResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('visible/city changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [jobId, ready, filters, cityCode, sortType]);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled || !ready}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError || !ready}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<AnchorCard data={item} jobId={jobId} key={item.id} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnchorList;
|
69
src/components/anchor-picker/index.less
Normal file
69
src/components/anchor-picker/index.less
Normal file
@ -0,0 +1,69 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.anchor-picker {
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
padding: 24px;
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
.flex-row();
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
min-width: 164px;
|
||||
height: 64px;
|
||||
font-size: 28px;
|
||||
line-height: 64px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
color: @blColor;
|
||||
background: #F6F6F6;
|
||||
margin-left: 16px;
|
||||
border-radius: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 344px;
|
||||
height: 72px;
|
||||
font-size: 28px;
|
||||
line-height: 72px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
background: #F6F6F6;
|
||||
padding: 0 24px;
|
||||
margin-top: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
186
src/components/anchor-picker/index.tsx
Normal file
186
src/components/anchor-picker/index.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { BaseEventOrig, Input, InputProps } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import PickerToolbar from '@/components/picker-toolbar';
|
||||
import {
|
||||
EmployType,
|
||||
ALL_EMPLOY_TYPES,
|
||||
FULL_PRICE_OPTIONS,
|
||||
PART_PRICE_OPTIONS,
|
||||
EMPLOY_TYPE_TITLE_MAP,
|
||||
} from '@/constants/job';
|
||||
import {
|
||||
ALL_ANCHOR_READ_TYPES,
|
||||
ALL_GENDER_TYPES,
|
||||
ANCHOR_READ_TITLE_MAP,
|
||||
AnchorReadType,
|
||||
GENDER_TYPE_TITLE_MAP,
|
||||
GenderType,
|
||||
} from '@/constants/material';
|
||||
import { IAnchorFilters } from '@/types/material';
|
||||
import { isUndefined } from '@/utils/common';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
value: IAnchorFilters;
|
||||
onConfirm: (newValue: IAnchorFilters) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'anchor-picker';
|
||||
const getDefaultGender = (value: IAnchorFilters) => value.gender;
|
||||
const getDefaultEmploy = (value: IAnchorFilters) => value.employType;
|
||||
const getDefaultReadType = (value: IAnchorFilters) => value.readType;
|
||||
const getDefaultCategory = (value: IAnchorFilters) => value.category || '';
|
||||
const getSalaryValue = (value: IAnchorFilters, full: boolean) => {
|
||||
const min = full ? value.lowPriceForFullTime : value.lowPriceForPartyTime;
|
||||
const max = full ? value.highPriceForFullTime : value.highPriceForPartyTime;
|
||||
if (!min || !max) {
|
||||
return null;
|
||||
}
|
||||
const options = full ? FULL_PRICE_OPTIONS : PART_PRICE_OPTIONS;
|
||||
return options.find(v => v.value && v.value.minSalary <= min && v.value.maxSalary >= max)?.value;
|
||||
};
|
||||
|
||||
function AnchorPicker(props: IProps) {
|
||||
const { value, onConfirm } = props;
|
||||
const [gender, setGender] = useState<GenderType | undefined>(getDefaultGender(value));
|
||||
const [readType, setReadType] = useState<AnchorReadType | undefined>(getDefaultReadType(value));
|
||||
const [employType, setEmployType] = useState<EmployType | undefined>(getDefaultEmploy(value));
|
||||
const [fullSalary, setFullSalary] = useState(getSalaryValue(value, true));
|
||||
const [partSalary, setPartSalary] = useState(getSalaryValue(value, false));
|
||||
const [category, setCategory] = useState(getDefaultCategory(value));
|
||||
|
||||
const handleInputCategory = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
setCategory(e.detail.value || '');
|
||||
}, []);
|
||||
|
||||
const handleClickReset = useCallback(() => {
|
||||
setGender(undefined);
|
||||
setReadType(undefined);
|
||||
setEmployType(undefined);
|
||||
setFullSalary(null);
|
||||
setPartSalary(null);
|
||||
setCategory('');
|
||||
}, []);
|
||||
|
||||
const handleSelectFull = useCallback(
|
||||
(newSalary?: { minSalary: number; maxSalary: number }) => {
|
||||
setFullSalary(newSalary === fullSalary ? null : newSalary);
|
||||
},
|
||||
[fullSalary]
|
||||
);
|
||||
|
||||
const handleSelectPart = useCallback(
|
||||
(newSalary?: { minSalary: number; maxSalary: number }) => {
|
||||
setPartSalary(newSalary === partSalary ? null : newSalary);
|
||||
},
|
||||
[partSalary]
|
||||
);
|
||||
|
||||
const handleClickConfirm = useCallback(() => {
|
||||
const filters: IAnchorFilters = {};
|
||||
if (!isUndefined(gender)) {
|
||||
filters.gender = gender === GenderType.All ? undefined : gender;
|
||||
}
|
||||
employType && (filters.employType = employType);
|
||||
readType && (filters.readType = readType);
|
||||
category && (filters.category = category);
|
||||
if (fullSalary) {
|
||||
filters.lowPriceForFullTime = fullSalary.minSalary;
|
||||
filters.highPriceForFullTime = fullSalary.maxSalary;
|
||||
}
|
||||
if (partSalary) {
|
||||
filters.lowPriceForPartyTime = partSalary.minSalary;
|
||||
filters.highPriceForPartyTime = partSalary.maxSalary;
|
||||
}
|
||||
onConfirm(filters);
|
||||
}, [gender, employType, readType, category, fullSalary, partSalary, onConfirm]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__title`}>性别</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{ALL_GENDER_TYPES.map((type: GenderType) => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => setGender(type)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: type === gender })}
|
||||
>
|
||||
{GENDER_TYPE_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>全职/兼职</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{ALL_EMPLOY_TYPES.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => setEmployType(type)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: type === employType })}
|
||||
>
|
||||
{EMPLOY_TYPE_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>已读/未读</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{ALL_ANCHOR_READ_TYPES.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => setReadType(type)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: type === readType })}
|
||||
>
|
||||
{ANCHOR_READ_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>全职薪资范围(每月)</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{FULL_PRICE_OPTIONS.map(option => (
|
||||
<div
|
||||
key={option.label}
|
||||
onClick={() => handleSelectFull(option.value)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: isEqual(option.value, fullSalary) })}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>兼职薪资范围(每小时)</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{PART_PRICE_OPTIONS.map(option => (
|
||||
<div
|
||||
key={option.label}
|
||||
onClick={() => handleSelectPart(option.value)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: isEqual(option.value, partSalary) })}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>播过的品类(模糊匹配)</div>
|
||||
<Input
|
||||
maxlength={20}
|
||||
value={category}
|
||||
confirmType="done"
|
||||
placeholder="如 服装"
|
||||
onInput={handleInputCategory}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<PickerToolbar
|
||||
cancelText="重置"
|
||||
confirmText="确定"
|
||||
className={`${PREFIX}__toolbar`}
|
||||
onClickCancel={handleClickReset}
|
||||
onClickConfirm={handleClickConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnchorPicker;
|
13
src/components/badge/index.less
Normal file
13
src/components/badge/index.less
Normal file
@ -0,0 +1,13 @@
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #FFFFFF;
|
||||
background: #FF5051;
|
||||
font-size: 24px;
|
||||
line-height: 34px;
|
||||
padding: 0 8px;
|
||||
border-radius: 10px;
|
||||
border-bottom-left-radius: 0;
|
||||
transform: translate3d(30%, -50%, 0);
|
||||
}
|
16
src/components/badge/index.tsx
Normal file
16
src/components/badge/index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import classNames from 'classnames';
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'badge';
|
||||
|
||||
function Badge(props: IProps) {
|
||||
const { className, text } = props;
|
||||
return <div className={classNames(PREFIX, className)}>{text}</div>;
|
||||
}
|
||||
|
||||
export default Badge;
|
21
src/components/bl-checkbox/index.less
Normal file
21
src/components/bl-checkbox/index.less
Normal file
@ -0,0 +1,21 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.profile-checkbox {
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
43
src/components/bl-checkbox/index.tsx
Normal file
43
src/components/bl-checkbox/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Checkbox } from '@taroify/core';
|
||||
import { CheckboxProps, CheckboxGroupProps } from '@taroify/core/checkbox';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends CheckboxProps {
|
||||
text: string;
|
||||
value: BL.Anything[];
|
||||
}
|
||||
|
||||
interface IGroupProps extends CheckboxGroupProps {}
|
||||
|
||||
const PREFIX = 'profile-checkbox';
|
||||
|
||||
export function BlCheckboxGroup(props: IGroupProps) {
|
||||
return <Checkbox.Group className={`${PREFIX}__group`} direction="horizontal" {...props} />;
|
||||
}
|
||||
|
||||
export function BlCheckbox(props: IProps) {
|
||||
const { name, text, value } = props;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
className={`${PREFIX}__item`}
|
||||
name={name}
|
||||
icon={
|
||||
<Image
|
||||
className={`${PREFIX}__icon`}
|
||||
mode="aspectFit"
|
||||
src={
|
||||
value.includes(name)
|
||||
? require('@/statics/svg/radio-checked.svg')
|
||||
: require('@/statics/svg/radio-uncheck.svg')
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
28
src/components/bl-form-cell/index.less
Normal file
28
src/components/bl-form-cell/index.less
Normal file
@ -0,0 +1,28 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-cell {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: #CCCCCC;
|
||||
.noWrap();
|
||||
|
||||
&.hasText {
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__right-icon {
|
||||
.flex-row();
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
25
src/components/bl-form-cell/index.tsx
Normal file
25
src/components/bl-form-cell/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { ArrowRight } from '@taroify/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
text: string;
|
||||
placeholder?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-form-cell';
|
||||
|
||||
function BlFormCell(props: IProps) {
|
||||
const { text, placeholder, onClick } = props;
|
||||
|
||||
return (
|
||||
<div className={PREFIX} onClick={onClick}>
|
||||
<div className={classNames(`${PREFIX}__text`, { hasText: !!text })}>{text || placeholder}</div>
|
||||
<ArrowRight className={`${PREFIX}__right-icon`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormCell;
|
36
src/components/bl-form-input/index.less
Normal file
36
src/components/bl-form-input/index.less
Normal file
@ -0,0 +1,36 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-input {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
|
||||
&__input {
|
||||
height: 60px;
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 60px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__right-text {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: @blColor;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__max-length-tips {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blColorG1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
}
|
35
src/components/bl-form-input/index.tsx
Normal file
35
src/components/bl-form-input/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Input, InputProps } from '@tarojs/components';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends InputProps {
|
||||
rightText?: string;
|
||||
maxLengthTips?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-form-input';
|
||||
|
||||
function BlFormInput(props: IProps) {
|
||||
const { value, maxlength = 140, maxLengthTips, rightText, onInput, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<Input
|
||||
value={value}
|
||||
maxlength={maxlength}
|
||||
confirmType="done"
|
||||
placeholder="请输入"
|
||||
onInput={onInput}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
{...otherProps}
|
||||
/>
|
||||
{rightText && <div className={`${PREFIX}__right-text`}>{rightText}</div>}
|
||||
{maxLengthTips && maxlength && (
|
||||
<div className={`${PREFIX}__max-length-tips`}>{`${(value || '').length}/${maxlength}`}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormInput;
|
45
src/components/bl-form-item/index.less
Normal file
45
src/components/bl-form-item/index.less
Normal file
@ -0,0 +1,45 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-item {
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
.flex-row();
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__type {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
.flex-row();
|
||||
margin-top: 24px;
|
||||
padding: 0 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.dynamicHeight {
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
42
src/components/bl-form-item/index.tsx
Normal file
42
src/components/bl-form-item/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {
|
||||
title: string;
|
||||
subTitle?: string | boolean;
|
||||
optional?: boolean;
|
||||
dynamicHeight?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-form-item';
|
||||
|
||||
function BlFormItem(props: IProps) {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
title,
|
||||
subTitle = true,
|
||||
optional = false,
|
||||
dynamicHeight = false,
|
||||
} = props;
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header__title`}>{title}</div>
|
||||
{subTitle !== false && (
|
||||
<div
|
||||
className={`${PREFIX}__header__type`}
|
||||
>{`(${typeof subTitle === 'string' ? subTitle : optional ? '建议填写' : '必填'})`}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames(`${PREFIX}__content`, contentClassName, { dynamicHeight })}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormItem;
|
19
src/components/bl-form-radio/index.less
Normal file
19
src/components/bl-form-radio/index.less
Normal file
@ -0,0 +1,19 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-radio {
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
39
src/components/bl-form-radio/index.tsx
Normal file
39
src/components/bl-form-radio/index.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Radio } from '@taroify/core';
|
||||
import { RadioGroupProps, RadioProps } from '@taroify/core/radio';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends RadioProps {
|
||||
text: string;
|
||||
value: BL.Anything;
|
||||
}
|
||||
|
||||
interface IGroupProps extends RadioGroupProps {}
|
||||
|
||||
const PREFIX = 'bl-form-radio';
|
||||
|
||||
export function BlFormRadioGroup(props: IGroupProps) {
|
||||
return <Radio.Group className={`${PREFIX}__group`} {...props} />;
|
||||
}
|
||||
|
||||
export function BlFormRadio(props: IProps) {
|
||||
const { name, text, value } = props;
|
||||
|
||||
return (
|
||||
<Radio
|
||||
className={`${PREFIX}__item`}
|
||||
name={name}
|
||||
icon={
|
||||
<Image
|
||||
className={`${PREFIX}__icon`}
|
||||
mode="aspectFit"
|
||||
src={value === name ? require('@/statics/svg/radio-checked.svg') : require('@/statics/svg/radio-uncheck.svg')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Radio>
|
||||
);
|
||||
}
|
30
src/components/bl-form-select/index.less
Normal file
30
src/components/bl-form-select/index.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-select {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
|
||||
&__input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__right-icon {
|
||||
.flex-row();
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
50
src/components/bl-form-select/index.tsx
Normal file
50
src/components/bl-form-select/index.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Input } from '@tarojs/components';
|
||||
|
||||
import { ArrowRight } from '@taroify/icons';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ISelectProps, PopupSelect } from '@/components/select';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends ISelectProps {}
|
||||
|
||||
const PREFIX = 'bl-form-select';
|
||||
|
||||
function BlFormSelect(props: IProps) {
|
||||
const { value, options, onSelect, ...otherProps } = props;
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
newValue => {
|
||||
setShowSelect(false);
|
||||
onSelect(newValue);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={PREFIX} onClick={() => setShowSelect(true)}>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="请选择"
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
value={options.find(i => i.value === value)?.label || ''}
|
||||
/>
|
||||
<ArrowRight className={`${PREFIX}__right-icon`} />
|
||||
</div>
|
||||
<PopupSelect
|
||||
value={value}
|
||||
options={options}
|
||||
open={showSelect}
|
||||
onSelect={handleSelect}
|
||||
onClose={() => setShowSelect(false)}
|
||||
{...otherProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormSelect;
|
71
src/components/bl-salary-input/index.less
Normal file
71
src/components/bl-salary-input/index.less
Normal file
@ -0,0 +1,71 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-salary-input {
|
||||
|
||||
&__item {
|
||||
height: 100px;
|
||||
.flex-row();
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
background: #00000026;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: -32px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text(@fontSize: 28px) {
|
||||
font-size: @fontSize;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
&__title {
|
||||
.text(@fontSize: 32px);
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
&__input-container {
|
||||
height: 72px;
|
||||
flex: 1;
|
||||
.flex-row();
|
||||
border-radius: 16px;
|
||||
background: #F6F6F6;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
.text();
|
||||
flex: 1;
|
||||
height: 72px;
|
||||
line-height: 72px;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
|
||||
&__unit {
|
||||
.text();
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&__center-divider {
|
||||
.text();
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
155
src/components/bl-salary-input/index.tsx
Normal file
155
src/components/bl-salary-input/index.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { BaseEventOrig, Input, InputProps } from '@tarojs/components';
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { logWithPrefix, string2Number } from '@/utils/common';
|
||||
import { EmployType } from '@/constants/job';
|
||||
|
||||
import './index.less';
|
||||
import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
|
||||
|
||||
export type BlSalaryValue = [number, number, number, number];
|
||||
|
||||
interface IProps {
|
||||
value: BlSalaryValue;
|
||||
employType?: EmployType;
|
||||
onChange: (result: BlSalaryValue) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-salary-input';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const MAX_FULL_PRICE = 1000;
|
||||
const MAX_PART_PRICE = 2000;
|
||||
|
||||
function BlSalaryInput(props: IProps) {
|
||||
const { value: initValue = [], onChange, employType } = props;
|
||||
const [minFull = '', maxFull = '', minPart = '', maxPart = ''] = initValue;
|
||||
|
||||
const onInput = useCallback(
|
||||
(value: number, index: number) => {
|
||||
const newValue = [...initValue] as BlSalaryValue;
|
||||
newValue.splice(index, 1, value);
|
||||
log('onInput', newValue);
|
||||
onChange(newValue);
|
||||
},
|
||||
[initValue, onChange]
|
||||
);
|
||||
|
||||
const handleInputMinFull = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(string2Number(value), 0);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
const handleInputMaxFull = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(Math.min(string2Number(value), MAX_FULL_PRICE), 1);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
const handleInputMinPart = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(string2Number(value), 2);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
const handleInputMaxPart = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(Math.min(string2Number(value), MAX_PART_PRICE), 3);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange([minFull, maxFull, minPart, maxPart] as BlSalaryValue);
|
||||
}, [minFull, maxFull, minPart, maxPart, onChange]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{isFullTimePriceRequired(employType) && (
|
||||
<div className={`${PREFIX}__item`}>
|
||||
<div className={`${PREFIX}__title`}>全职</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最低值"
|
||||
value={String(minFull)}
|
||||
onInput={handleInputMinFull}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>K/月</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__center-divider`}>-</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最高值"
|
||||
value={String(maxFull)}
|
||||
onInput={handleInputMaxFull}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>K/月</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isPartTimePriceRequired(employType) && (
|
||||
<div className={`${PREFIX}__item`}>
|
||||
<div className={`${PREFIX}__title`}>兼职</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最低值"
|
||||
value={String(minPart)}
|
||||
onInput={handleInputMinPart}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>元/小时</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__center-divider`}>-</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最高值"
|
||||
value={String(maxPart)}
|
||||
onInput={handleInputMaxPart}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>元/小时</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlSalaryInput;
|
12
src/components/cell/index.less
Normal file
12
src/components/cell/index.less
Normal file
@ -0,0 +1,12 @@
|
||||
.bl-cell {
|
||||
height: 112px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-radius: 16px;
|
||||
|
||||
&__right-text {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
22
src/components/cell/index.tsx
Normal file
22
src/components/cell/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Cell } from '@taroify/core';
|
||||
import { CellProps } from '@taroify/core/cell';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends CellProps {
|
||||
rightText?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-cell';
|
||||
|
||||
function BlCell(props: IProps) {
|
||||
const { className, rightText, ...otherProps } = props;
|
||||
return (
|
||||
<Cell className={classNames(PREFIX, className)} {...otherProps}>
|
||||
{rightText && <div className={`${PREFIX}__right-text`}>{rightText}</div>}
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlCell;
|
74
src/components/certification-status/index.less
Normal file
74
src/components/certification-status/index.less
Normal file
@ -0,0 +1,74 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.certification-status {
|
||||
|
||||
.taroify-cell__title {
|
||||
flex: unset;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__right-text {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
||||
|
||||
.certification-status-icon {
|
||||
height: 38px;
|
||||
.flex-row();
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.none {
|
||||
color: @blColorG1;
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #117264;
|
||||
background: #DCF7F0;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
color: #FF5051;
|
||||
background: #FF50511F;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&__describe {
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: 30px;
|
||||
padding: 0 6px;
|
||||
|
||||
&__icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
&__describe {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
109
src/components/certification-status/index.tsx
Normal file
109
src/components/certification-status/index.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Cell } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { CertificationStatusType } from '@/constants/company';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IIconProps {
|
||||
status: CertificationStatusType;
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'certification-status';
|
||||
const ICON_PREFIX = 'certification-status-icon';
|
||||
const getStatusClassName = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return 'none';
|
||||
case CertificationStatusType.Success:
|
||||
return 'success';
|
||||
case CertificationStatusType.Fail:
|
||||
return 'fail';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
const getStatusIconUrl = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return require('@/statics/svg/certification-status-none.svg');
|
||||
case CertificationStatusType.Success:
|
||||
return require('@/statics/svg/certification-status-success.svg');
|
||||
case CertificationStatusType.Fail:
|
||||
return require('@/statics/svg/certification-status-fail.svg');
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
const getStatusDescribe = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return '未认证';
|
||||
case CertificationStatusType.Success:
|
||||
return '实人认证';
|
||||
case CertificationStatusType.Fail:
|
||||
return '认证失败';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
const getRightText = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return '去认证';
|
||||
case CertificationStatusType.Fail:
|
||||
return '重新认证';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export function CertificationStatusIcon(props: IIconProps) {
|
||||
const { status, className, small = false } = props;
|
||||
return (
|
||||
<div className={classNames(ICON_PREFIX, className, { [getStatusClassName(status)]: true, small })}>
|
||||
<Image mode="aspectFit" className={`${ICON_PREFIX}__icon`} src={getStatusIconUrl(status)} />
|
||||
<div className={`${ICON_PREFIX}__describe`}>{getStatusDescribe(status)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CertificationStatus(props: IProps) {
|
||||
const { className } = props;
|
||||
const { bossAuthStatus: status = CertificationStatusType.None } = useUserInfo();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (status !== CertificationStatusType.Success) {
|
||||
navigateTo(PageUrl.CertificationStart);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Cell
|
||||
align="center"
|
||||
title="认证状态"
|
||||
className={classNames(PREFIX, className)}
|
||||
isLink={status !== CertificationStatusType.Success}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={`${PREFIX}__content`}>
|
||||
<CertificationStatusIcon status={status} />
|
||||
<div className={`${PREFIX}__right-text`}>{getRightText(status)}</div>
|
||||
</div>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
export default CertificationStatus;
|
4
src/components/city-picker/index.less
Normal file
4
src/components/city-picker/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.city-picker {
|
||||
}
|
70
src/components/city-picker/index.tsx
Normal file
70
src/components/city-picker/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { AreaPicker, Popup } from '@taroify/core';
|
||||
import { AreaData } from '@taroify/core/area-picker/area-picker.shared';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { CITY_LIST, COUNTY_LIST, PROVINCE_LIST } from '@/constants/city';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
depth?: 1 | 2 | 3;
|
||||
areaValues?: string[];
|
||||
onCancel: () => void;
|
||||
onConfirm: (areaValues: string[]) => void;
|
||||
}
|
||||
|
||||
interface IPopupProps extends IProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'city-picker';
|
||||
const areaList: AreaData = {
|
||||
province_list: PROVINCE_LIST,
|
||||
city_list: CITY_LIST,
|
||||
county_list: COUNTY_LIST,
|
||||
};
|
||||
const DEFAULT_AREA = ['110000', '110100', '110101'];
|
||||
|
||||
function CityPicker(props: IProps) {
|
||||
const { areaValues = DEFAULT_AREA, depth = 3, onCancel, onConfirm } = props;
|
||||
const provinceCodeRef = useRef(areaValues[0]);
|
||||
const cityCodeRef = useRef(areaValues[1]);
|
||||
const countyCodeRef = useRef(areaValues[2]);
|
||||
|
||||
const handleClickConfirm = useCallback(() => {
|
||||
onConfirm([provinceCodeRef.current, cityCodeRef.current, countyCodeRef.current]);
|
||||
}, [onConfirm]);
|
||||
|
||||
const handleChange = useCallback((values: string[]) => {
|
||||
provinceCodeRef.current = values[0];
|
||||
cityCodeRef.current = values[1];
|
||||
countyCodeRef.current = values[2];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<AreaPicker
|
||||
depth={depth}
|
||||
className={`${PREFIX}__area-picker`}
|
||||
areaList={areaList}
|
||||
defaultValue={areaValues}
|
||||
onCancel={onCancel}
|
||||
onChange={handleChange}
|
||||
onConfirm={handleClickConfirm}
|
||||
></AreaPicker>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CityPickerPopup(props: IPopupProps) {
|
||||
const { open, onCancel } = props;
|
||||
return (
|
||||
<Popup className={PREFIX} placement="bottom" open={open} onClose={onCancel}>
|
||||
<CityPicker {...props} />
|
||||
<SafeBottomPadding />
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default CityPicker;
|
32
src/components/common-dialog/index.less
Normal file
32
src/components/common-dialog/index.less
Normal file
@ -0,0 +1,32 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.common-dialog {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
line-height: 58px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button,
|
||||
&__cancel-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
&__cancel-button {
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
47
src/components/common-dialog/index.tsx
Normal file
47
src/components/common-dialog/index.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends PropsWithChildren {
|
||||
visible: boolean;
|
||||
content: string;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
confirm?: string;
|
||||
cancel?: string;
|
||||
onClick?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'common-dialog';
|
||||
|
||||
function CommonDialog(props: IProps) {
|
||||
const { visible, content, confirm, cancel, className, children, onClose, onClick, onCancel } = props;
|
||||
|
||||
return (
|
||||
<Dialog className={classNames(PREFIX, className)} open={visible} onClose={onClose}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>{content}</div>
|
||||
{children}
|
||||
{confirm && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={onClick}>
|
||||
{confirm}
|
||||
</Button>
|
||||
)}
|
||||
{cancel && (
|
||||
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
|
||||
{cancel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommonDialog;
|
8
src/components/custom-navigation-bar/index.less
Normal file
8
src/components/custom-navigation-bar/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
|
||||
.bl-navigation-bar {
|
||||
.flex-row();
|
||||
padding: 0 24px;
|
||||
}
|
30
src/components/custom-navigation-bar/index.tsx
Normal file
30
src/components/custom-navigation-bar/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import useNavigation from '@/hooks/use-navigation';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends PropsWithChildren {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-navigation-bar';
|
||||
|
||||
function CustomNavigationBar(props: IProps) {
|
||||
const { className, children } = props;
|
||||
const { barHeight, statusBarHeight } = useNavigation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(PREFIX, className)}
|
||||
style={{ height: Taro.pxTransform(barHeight.current), paddingTop: statusBarHeight.current }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomNavigationBar;
|
43
src/components/dev-div/index.tsx
Normal file
43
src/components/dev-div/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
OnDev?: () => void;
|
||||
}
|
||||
|
||||
const CLICK_COUNT = 5;
|
||||
|
||||
function DevDiv(props: IProps) {
|
||||
const { OnDev, onClick, ...otherProps } = props;
|
||||
const lastClickTime = useRef(0);
|
||||
const clickCount = useRef(0);
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
onClick?.(e);
|
||||
if (!OnDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - lastClickTime.current;
|
||||
|
||||
if (timeDiff < 300) {
|
||||
clickCount.current = clickCount.current + 1;
|
||||
|
||||
if (clickCount.current >= CLICK_COUNT) {
|
||||
OnDev?.();
|
||||
clickCount.current = 0;
|
||||
}
|
||||
} else {
|
||||
clickCount.current = 1;
|
||||
}
|
||||
|
||||
lastClickTime.current = currentTime;
|
||||
},
|
||||
[OnDev, onClick]
|
||||
);
|
||||
|
||||
return <div onClick={handleClick} {...otherProps}></div>;
|
||||
}
|
||||
|
||||
export default DevDiv;
|
92
src/components/group-card/index.less
Normal file
92
src/components/group-card/index.less
Normal file
@ -0,0 +1,92 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.group-card {
|
||||
width: 100%;
|
||||
padding: 32px;
|
||||
background: #FFF;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__group-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__group-avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
&__group-info {
|
||||
width: 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-left: 32px;
|
||||
|
||||
&.full {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__group-title-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__group-full-icon {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
border: 2px solid @blHighlightColor;
|
||||
border-radius: 4px;
|
||||
padding: 0 6px;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__group-desc {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__group-job-count {
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__group-view {
|
||||
color: @blHighlightColor;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__group-button {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @fontWeight: 500; @borderRadius: 48px;);
|
||||
}
|
||||
}
|
61
src/components/group-card/index.tsx
Normal file
61
src/components/group-card/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import LoginButton from '@/components/login-button';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { GroupType } from '@/constants/group';
|
||||
import { GroupInfo } from '@/types/group';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
type: GroupType;
|
||||
data: GroupInfo;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'group-card';
|
||||
|
||||
function GroupCard(props: IProps) {
|
||||
const { type, data, onClick } = props;
|
||||
const showButton = type === GroupType.All;
|
||||
const isFull = (data.groupMemberCount || 0) >= 500;
|
||||
|
||||
const handleClickView = useCallback(() => {
|
||||
navigateTo(PageUrl.GroupJob, { groupId: data.blGroupId, title: data.imGroupNick });
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__group-container`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__group-avatar`}
|
||||
src={data.groupAvatar || require('@/statics/svg/wechat.svg')}
|
||||
/>
|
||||
<div className={classNames(`${PREFIX}__group-info`, { full: !showButton })}>
|
||||
<div className={`${PREFIX}__group-title-container`}>
|
||||
<div className={`${PREFIX}__group-title`}>{data.imGroupNick}</div>
|
||||
{isFull && <div className={`${PREFIX}__group-full-icon`}>已满</div>}
|
||||
</div>
|
||||
<div className={`${PREFIX}__group-desc`}>
|
||||
<div className={`${PREFIX}__group-job-count`}>{`${data.allJobs}条通告`}</div>
|
||||
<div className={`${PREFIX}__group-view`} onClick={handleClickView}>
|
||||
查看
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showButton && (
|
||||
<LoginButton className={`${PREFIX}__group-button`} onClick={onClick}>
|
||||
我要进群
|
||||
</LoginButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupCard;
|
3
src/components/group-list/index.less
Normal file
3
src/components/group-list/index.less
Normal file
@ -0,0 +1,3 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.group-list {}
|
149
src/components/group-list/index.tsx
Normal file
149
src/components/group-list/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import GroupCard from '@/components/group-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { GroupInfo, GetGroupsRequest } from '@/types/group';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestGroupList } from '@/utils/group';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends GetGroupsRequest {}
|
||||
|
||||
export interface IGroupListProps extends IRequestProps {
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
onClickInvite: (data: GroupInfo) => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 40;
|
||||
const PREFIX = 'group-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobList(props: IGroupListProps) {
|
||||
const { className, listHeight, refreshDisabled, type, imGroupNick, status, onListEmpty, onClickInvite } = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<GroupInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({ type });
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, groupResults } = await requestGroupList({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(groupResults);
|
||||
currentPage.current = page;
|
||||
!groupResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
groupResults,
|
||||
} = await requestGroupList({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...groupResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, currentPage, hasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = {
|
||||
type,
|
||||
status,
|
||||
imGroupNick: imGroupNick ? imGroupNick.trim() : undefined,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
}, [type, status, imGroupNick]);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = async () => {
|
||||
log('props changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, groupResults } = await requestGroupList({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(groupResults);
|
||||
currentPage.current = page;
|
||||
!groupResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('props changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [type, status, imGroupNick]);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<GroupCard type={type} data={item} key={item.blGroupId} onClick={() => onClickInvite(item)} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobList;
|
0
src/components/home-page/index.less
Normal file
0
src/components/home-page/index.less
Normal file
18
src/components/home-page/index.tsx
Normal file
18
src/components/home-page/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import BaseTabBar from '@/components/tab-bar';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {}
|
||||
|
||||
export default function HomePage(props: IProps) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<BaseTabBar />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
163
src/components/job-card/index.less
Normal file
163
src/components/job-card/index.less
Normal file
@ -0,0 +1,163 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-card {
|
||||
&__container {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__employment-type {
|
||||
width: fit-content;
|
||||
padding: 3px 6px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background: @blHighlightBg;
|
||||
color: @blHighlightColor;
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
|
||||
&__wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__certification-type {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
overflow: hidden;
|
||||
margin-top: 32px;
|
||||
// 抵消最后一行的 margin-bottom
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
padding: 3px 6px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background: #F2F2F2;
|
||||
color: @blColorG2;
|
||||
white-space: nowrap;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__salary {
|
||||
font-size: 30px;
|
||||
line-height: 32px;
|
||||
font: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__summary {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__distance-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__detailed-address {
|
||||
flex: 1 1;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__distance-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-left: 15px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&__distance {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #E0E0E0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__publisher {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__publisher-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
&__publisher-name,
|
||||
&__city {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
}
|
||||
|
||||
}
|
111
src/components/job-card/index.tsx
Normal file
111
src/components/job-card/index.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { CertificationStatusIcon } from '@/components/certification-status';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { CITY_CODE_TO_NAME_MAP, COUNTY_CODE_TO_NAME_MAP } from '@/constants/city';
|
||||
import { CertificationStatusType } from '@/constants/company';
|
||||
import { EMPLOY_TYPE_TITLE_MAP, EmployType } from '@/constants/job';
|
||||
import { JobInfo } from '@/types/job';
|
||||
import { LocationInfo } from '@/types/location';
|
||||
import { getJobSalary, getJobTitle } from '@/utils/job';
|
||||
import { calcDistance } from '@/utils/location';
|
||||
import { navigateTo, redirectTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: JobInfo;
|
||||
redirectOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'job-card';
|
||||
const getCityDes = (location: LocationInfo) => {
|
||||
if (!location) {
|
||||
return '';
|
||||
}
|
||||
let des = CITY_CODE_TO_NAME_MAP.get(location.cityCode);
|
||||
if (location.countyCode) {
|
||||
des += `-${COUNTY_CODE_TO_NAME_MAP.get(location.countyCode)}`;
|
||||
}
|
||||
return des;
|
||||
};
|
||||
|
||||
function JobCard(props: IProps) {
|
||||
const { className, data, redirectOpen } = props;
|
||||
const {
|
||||
id,
|
||||
tags = [],
|
||||
employType = EmployType.All,
|
||||
jobDescription,
|
||||
sourceText,
|
||||
publisher,
|
||||
publisherAvatar,
|
||||
jobLocation,
|
||||
distance,
|
||||
isAuthed = false,
|
||||
} = data;
|
||||
|
||||
const handleClickCard = useCallback(() => {
|
||||
if (redirectOpen) {
|
||||
redirectTo(PageUrl.JobDetail, { id });
|
||||
} else {
|
||||
navigateTo(PageUrl.JobDetail, { id });
|
||||
}
|
||||
}, [id, redirectOpen]);
|
||||
|
||||
return (
|
||||
<div className={classNames(`${PREFIX}__container`, className)} onClick={handleClickCard}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__title`}>{getJobTitle(data)}</div>
|
||||
<div className={`${PREFIX}__employment-type__wrapper`}>
|
||||
<div className={`${PREFIX}__employment-type`}>{EMPLOY_TYPE_TITLE_MAP[employType]}</div>
|
||||
</div>
|
||||
{isAuthed && (
|
||||
<CertificationStatusIcon
|
||||
className={`${PREFIX}__certification-type`}
|
||||
status={CertificationStatusType.Success}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__tags`}>
|
||||
{tags.map((keyword: string, index) => (
|
||||
<div className={`${PREFIX}__tag`} key={index}>
|
||||
{keyword}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__salary`}>{getJobSalary(data) || '见描述'}</div>
|
||||
<div className={`${PREFIX}__content`}>
|
||||
<div className={`${PREFIX}__summary`}>{jobDescription || sourceText}</div>
|
||||
<div className={`${PREFIX}__distance-wrapper`}>
|
||||
<div className={`${PREFIX}__detailed-address`}>{jobLocation?.address}</div>
|
||||
{distance && (
|
||||
<>
|
||||
<Image className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||||
<div className={`${PREFIX}__distance`}>{calcDistance(distance)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__divider`} />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<div className={`${PREFIX}__publisher`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__publisher-avatar`}
|
||||
src={publisherAvatar || require('@/statics/svg/wechat.svg')}
|
||||
/>
|
||||
<div className={`${PREFIX}__publisher-name`}>{publisher}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__city`}>{getCityDes(jobLocation)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(JobCard);
|
0
src/components/job-list/index.less
Normal file
0
src/components/job-list/index.less
Normal file
207
src/components/job-list/index.tsx
Normal file
207
src/components/job-list/index.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobCard from '@/components/job-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { JobType, EmployType, SortType } from '@/constants/job';
|
||||
import { JobInfo, GetJobsRequest } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestJobList as requestData } from '@/utils/job';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetJobsRequest> {}
|
||||
|
||||
export interface IJobListProps extends IRequestProps {
|
||||
visible?: boolean;
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 40;
|
||||
const PREFIX = 'job-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobList(props: IJobListProps) {
|
||||
const {
|
||||
className,
|
||||
listHeight,
|
||||
refreshDisabled,
|
||||
visible = true,
|
||||
cityCode = 'ALL',
|
||||
category = JobType.All,
|
||||
employType = EmployType.All,
|
||||
sortType = SortType.RECOMMEND,
|
||||
isFollow = false,
|
||||
isOwner = false,
|
||||
keyWord,
|
||||
latitude,
|
||||
longitude,
|
||||
minSalary,
|
||||
maxSalary,
|
||||
blGroupId,
|
||||
onListEmpty,
|
||||
} = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<JobInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
const prevRequestProps = useRef<IRequestProps>({});
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
jobResults,
|
||||
} = await requestData({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...jobResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, currentPage, hasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = {
|
||||
category,
|
||||
cityCode,
|
||||
employType,
|
||||
sortType,
|
||||
isFollow,
|
||||
isOwner,
|
||||
keyWord,
|
||||
latitude,
|
||||
longitude,
|
||||
minSalary,
|
||||
maxSalary,
|
||||
blGroupId,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
}, [
|
||||
category,
|
||||
cityCode,
|
||||
employType,
|
||||
sortType,
|
||||
isFollow,
|
||||
isOwner,
|
||||
keyWord,
|
||||
latitude,
|
||||
longitude,
|
||||
minSalary,
|
||||
maxSalary,
|
||||
blGroupId,
|
||||
]);
|
||||
|
||||
// 初始化数据&配置变更后刷新数据
|
||||
useEffect(() => {
|
||||
// 相比前一次可见时没有数据变更时,不再重新请求
|
||||
if (isEqual(prevRequestProps.current, requestProps.current)) {
|
||||
log('visible/city changed, but request params not change, ignore');
|
||||
return;
|
||||
}
|
||||
// 列表不可见时,先不做处理
|
||||
if (!visible) {
|
||||
log('visible/city changed, but is not visible, only clear list');
|
||||
return;
|
||||
}
|
||||
|
||||
prevRequestProps.current = requestProps.current;
|
||||
const refresh = async () => {
|
||||
log('visible/city changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('visible/city changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [visible, cityCode, employType, sortType, keyWord, minSalary, maxSalary, blGroupId]);
|
||||
|
||||
// log('render', `hasMore: ${hasMore}, loadingMore: ${loadingMore}, refreshing: ${refreshing}`);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<JobCard data={item} key={item.id} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobList;
|
69
src/components/job-manage-card/index.less
Normal file
69
src/components/job-manage-card/index.less
Normal file
@ -0,0 +1,69 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-manage-card {
|
||||
width: 100%;
|
||||
height: 152px;
|
||||
.flex-row();
|
||||
padding: 24px 32px;
|
||||
background: #FFF;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
background: #00000026;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 32px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
&__title {
|
||||
max-width: 75vw;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__location {
|
||||
max-width: 75vw;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-top: 16px;
|
||||
.noWrap();
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
|
||||
&.open {
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #FF5051;
|
||||
}
|
||||
}
|
||||
}
|
49
src/components/job-manage-card/index.tsx
Normal file
49
src/components/job-manage-card/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { JOB_MANAGE_STATUS_TITLE_MAP, JobManageStatus } from '@/constants/job';
|
||||
import { JobManageInfo } from '@/types/job';
|
||||
import { getJobLocation } from '@/utils/job';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: JobManageInfo;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'job-manage-card';
|
||||
|
||||
const STATUS_CLASS_MAP = {
|
||||
[JobManageStatus.WaitVerify]: 'pending',
|
||||
[JobManageStatus.Open]: 'open',
|
||||
[JobManageStatus.Pending]: 'pending',
|
||||
[JobManageStatus.Error]: 'error',
|
||||
[JobManageStatus.Close]: 'close',
|
||||
[JobManageStatus.Expire]: 'close',
|
||||
};
|
||||
|
||||
function JobManageCard(props: IProps) {
|
||||
const { data = {} } = props;
|
||||
const { id, title, status } = data as JobManageInfo;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigateTo(PageUrl.JobDetail, { id });
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} onClick={handleClick}>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
<div className={`${PREFIX}__info__title`}>{title}</div>
|
||||
<div className={`${PREFIX}__info__location`}>{getJobLocation(data as JobManageInfo)}</div>
|
||||
</div>
|
||||
<div className={classNames(`${PREFIX}__status`, { [STATUS_CLASS_MAP[status]]: true })}>
|
||||
<div>{JOB_MANAGE_STATUS_TITLE_MAP[status]}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(JobManageCard);
|
2
src/components/job-manage-list/index.less
Normal file
2
src/components/job-manage-list/index.less
Normal file
@ -0,0 +1,2 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
171
src/components/job-manage-list/index.tsx
Normal file
171
src/components/job-manage-list/index.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobManageCard from '@/components/job-manage-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { GetJobManagesRequest, JobManageInfo } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestJobManageList as requestData } from '@/utils/job';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetJobManagesRequest> {}
|
||||
|
||||
export interface IJobManageListProps extends IRequestProps {
|
||||
visible?: boolean;
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 40;
|
||||
const PREFIX = 'job-manage-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobManageList(props: IJobManageListProps) {
|
||||
const { className, listHeight, refreshDisabled, visible = true, status, onListEmpty } = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<JobManageInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
const prevRequestProps = useRef<IRequestProps>({});
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
jobResults,
|
||||
} = await requestData({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...jobResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, currentPage, hasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = { status: status, pageSize: PAGE_SIZE };
|
||||
}, [status]);
|
||||
|
||||
// 初始化数据&配置变更后刷新数据
|
||||
useEffect(() => {
|
||||
// 相比前一次可见时没有数据变更时,不再重新请求
|
||||
if (isEqual(prevRequestProps.current, requestProps.current)) {
|
||||
log('visible/city changed, but request params not change, ignore');
|
||||
return;
|
||||
}
|
||||
// 列表不可见时,先不做处理
|
||||
if (!visible) {
|
||||
log('visible/city changed, but is not visible, only clear list');
|
||||
return;
|
||||
}
|
||||
|
||||
prevRequestProps.current = requestProps.current;
|
||||
const refresh = async () => {
|
||||
log('visible/city changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('visible/city changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [visible, status]);
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.COMPANY_JOB_PUBLISH_CHANGED, handleRefresh);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.COMPANY_JOB_PUBLISH_CHANGED, handleRefresh);
|
||||
};
|
||||
}, [handleRefresh]);
|
||||
|
||||
// log('render', `hasMore: ${hasMore}, loadingMore: ${loadingMore}, refreshing: ${refreshing}`);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<JobManageCard data={item} key={item.id} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} noMoreText="" />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobManageList;
|
47
src/components/job-picker/index.less
Normal file
47
src/components/job-picker/index.less
Normal file
@ -0,0 +1,47 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-type-picker {
|
||||
background: #FFFFFF;
|
||||
|
||||
&__groups-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&__group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__group-items-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__group-item {
|
||||
min-width: 158px;
|
||||
height: 64px;
|
||||
font-size: 28px;
|
||||
padding: 0 19px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 24px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 34px;
|
||||
line-height: 64px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
color: @blColor;
|
||||
background: #F6F6F6;
|
||||
|
||||
&.selected {
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
}
|
||||
}
|
90
src/components/job-picker/index.tsx
Normal file
90
src/components/job-picker/index.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
|
||||
import PickerToolbar from '@/components/picker-toolbar';
|
||||
import { EMPLOY_TYPE_TITLE_MAP, EmployType } from '@/constants/job';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
onConfirm: (employType: EmployType) => void;
|
||||
}
|
||||
|
||||
interface IGroupProps {
|
||||
name: string;
|
||||
types: string[];
|
||||
typeTitleMap: Record<string, string>;
|
||||
selectTypes: string[];
|
||||
onClickItem: (item: string) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'job-type-picker';
|
||||
|
||||
const TypeGroup = (props: IGroupProps) => {
|
||||
const { name, types, selectTypes, typeTitleMap, onClickItem } = props;
|
||||
return (
|
||||
<div className={`${PREFIX}__group`}>
|
||||
<div className={`${PREFIX}__group-title`}>{name}</div>
|
||||
<div className={`${PREFIX}__group-items-container`}>
|
||||
{types.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => onClickItem(type)}
|
||||
className={classNames(`${PREFIX}__group-item`, { selected: selectTypes.includes(type) })}
|
||||
>
|
||||
{typeTitleMap[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function JobPicker(props: IProps) {
|
||||
const [selectedEmployTypes, setSelectedEmployTypes] = useState<EmployType[]>([EmployType.Full, EmployType.Part]);
|
||||
const { onConfirm } = props;
|
||||
|
||||
const handleClickEmployType = (clickType: EmployType) => {
|
||||
if (selectedEmployTypes.includes(clickType)) {
|
||||
setSelectedEmployTypes(selectedEmployTypes.filter(type => type !== clickType));
|
||||
} else {
|
||||
setSelectedEmployTypes(selectedEmployTypes.concat([clickType]));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickReset = () => {
|
||||
setSelectedEmployTypes([EmployType.Full, EmployType.Part]);
|
||||
};
|
||||
|
||||
const handleClickConfirm = () => {
|
||||
if (!selectedEmployTypes.length) {
|
||||
Toast.error('至少选择一个');
|
||||
return;
|
||||
}
|
||||
const newEmployType = selectedEmployTypes.length === 1 ? selectedEmployTypes[0] : EmployType.All;
|
||||
onConfirm(newEmployType);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__groups-container`}>
|
||||
<TypeGroup
|
||||
name="职位类型"
|
||||
types={[EmployType.Full, EmployType.Part]}
|
||||
typeTitleMap={EMPLOY_TYPE_TITLE_MAP}
|
||||
selectTypes={selectedEmployTypes}
|
||||
onClickItem={handleClickEmployType}
|
||||
/>
|
||||
</div>
|
||||
<PickerToolbar
|
||||
cancelText="重置"
|
||||
confirmText="确定"
|
||||
onClickCancel={handleClickReset}
|
||||
onClickConfirm={handleClickConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobPicker;
|
44
src/components/job-recommend-list/index.less
Normal file
44
src/components/job-recommend-list/index.less
Normal file
@ -0,0 +1,44 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-recommend-list {
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
&__header-left-line,
|
||||
&__header-right-line {
|
||||
width: 88px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&__header-left-line {
|
||||
background: linear-gradient(90deg, rgba(109, 61, 245, 0) 0%, #6D3DF5 100%);
|
||||
}
|
||||
|
||||
&__header-right-line {
|
||||
background: linear-gradient(90deg, #6D3DF5 0%, rgba(109, 61, 245, 0) 100%);
|
||||
}
|
||||
|
||||
&__header-title {
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 500;
|
||||
color: @blHighlightColor;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&__header-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&__more-button {
|
||||
.button(@height: 80px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;);
|
||||
}
|
||||
}
|
83
src/components/job-recommend-list/index.tsx
Normal file
83
src/components/job-recommend-list/index.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Button, Image } from '@tarojs/components';
|
||||
|
||||
import { List } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobCard from '@/components/job-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { JobInfo, GetJobsRequest } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestMyRecommendJobList } from '@/utils/job';
|
||||
import { switchTab } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetJobsRequest> {}
|
||||
|
||||
export interface IJobListProps extends IRequestProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const PREFIX = 'job-recommend-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobRecommendList(props: IJobListProps) {
|
||||
const { className } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [dataList, setDataList] = useState<JobInfo[]>([]);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
|
||||
const handleClickAllJob = useCallback(() => {
|
||||
switchTab(PageUrl.Job);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestProps.current = { page: 1, pageSize: PAGE_SIZE };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = async () => {
|
||||
log('start request list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoading(true);
|
||||
setLoadError(false);
|
||||
const { jobResults = [] } = await requestMyRecommendJobList({ ...requestProps.current });
|
||||
setDataList(jobResults);
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setLoadError(true);
|
||||
} finally {
|
||||
log('request list data end');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header-left-line`} />
|
||||
<Image className={`${PREFIX}__header-icon`} src={require('@/statics/svg/like.svg')} />
|
||||
<div className={`${PREFIX}__header-title`}>为你推荐附近通告</div>
|
||||
<div className={`${PREFIX}__header-right-line`} />
|
||||
</div>
|
||||
<List disabled>
|
||||
{dataList.map(item => (
|
||||
<JobCard data={item} key={item.id} redirectOpen />
|
||||
))}
|
||||
<ListPlaceholder noMoreText="" loadingMore={loading} loadMoreError={loadError} loadMoreErrorText="加载失败" />
|
||||
</List>
|
||||
<Button className={`${PREFIX}__more-button`} onClick={handleClickAllJob}>
|
||||
查看更多通告
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobRecommendList;
|
0
src/components/list-placeholder/index.less
Normal file
0
src/components/list-placeholder/index.less
Normal file
33
src/components/list-placeholder/index.tsx
Normal file
33
src/components/list-placeholder/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { List, Loading } from '@taroify/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IPlaceholderProps {
|
||||
hasMore: boolean;
|
||||
loadingMore: boolean;
|
||||
loadMoreError: boolean;
|
||||
noMoreText: ReactNode;
|
||||
loadMoreErrorText: ReactNode;
|
||||
}
|
||||
|
||||
function ListPlaceholder(props: Partial<IPlaceholderProps>) {
|
||||
const { hasMore, loadingMore, loadMoreError, noMoreText, loadMoreErrorText } = props;
|
||||
|
||||
let content: ReactNode = '';
|
||||
if (loadingMore) {
|
||||
content = <Loading>加载中...</Loading>;
|
||||
} else if (loadMoreError) {
|
||||
content = loadMoreErrorText ?? '加载失败,请下拉刷新重试';
|
||||
} else if (!hasMore) {
|
||||
content = noMoreText ?? '没有更多了';
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <List.Placeholder>{content}</List.Placeholder>;
|
||||
}
|
||||
|
||||
export default ListPlaceholder;
|
52
src/components/loading-dialog/index.less
Normal file
52
src/components/loading-dialog/index.less
Normal file
@ -0,0 +1,52 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.loading-dialog {
|
||||
|
||||
&__dialog-content {
|
||||
.flex-column();
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
position: relative;
|
||||
width: 188px;
|
||||
height: 188px;
|
||||
}
|
||||
|
||||
&__icon-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
background: conic-gradient(#6D3DF5, 30%, #ECE5FF);
|
||||
border-radius: 50%;
|
||||
animation: spin 1.5s linear infinite reverse;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 188px;
|
||||
height: 188px;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F2;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 58px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
src/components/loading-dialog/index.tsx
Normal file
35
src/components/loading-dialog/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'loading-dialog';
|
||||
|
||||
function LoadingDialog(props: IProps) {
|
||||
const { open, text } = props;
|
||||
return (
|
||||
<Dialog className={PREFIX} open={open}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__dialog-content`}>
|
||||
<div className={`${PREFIX}__dialog-content__icon-wrapper`}>
|
||||
<div className={`${PREFIX}__dialog-content__icon-bg`} />
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__dialog-content__icon`}
|
||||
src={require('@/statics/svg/certification-tips-icon.svg')}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${PREFIX}__dialog-content__title`}>{text}</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingDialog;
|
38
src/components/location-dialog/index.less
Normal file
38
src/components/location-dialog/index.less
Normal file
@ -0,0 +1,38 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.location-dialog {
|
||||
&__container {
|
||||
.flex-column();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 56px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
// &__cancel-button {
|
||||
// min-width: fit-content;
|
||||
// font-size: 28px;
|
||||
// line-height: 32px;
|
||||
// color: @blHighlightColor;
|
||||
// background: transparent;
|
||||
// border: none;
|
||||
// margin-top: 40px;
|
||||
|
||||
// &::after {
|
||||
// border-color: transparent
|
||||
// }
|
||||
// }
|
||||
|
||||
&__checkbox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
47
src/components/location-dialog/index.tsx
Normal file
47
src/components/location-dialog/index.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClick: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'location-dialog';
|
||||
|
||||
export default function LocationDialog(props: IProps) {
|
||||
const { open, onClick, onClose } = props;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleTipCheck = useCallback(() => {
|
||||
Toast.info('请先阅读并同意协议');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>{`我们需要获取您的位置信息\n以便推荐附近的通告或主播`}</div>
|
||||
{!checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
|
||||
授权
|
||||
</Button>
|
||||
)}
|
||||
{checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={onClick}>
|
||||
授权
|
||||
</Button>
|
||||
)}
|
||||
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
4
src/components/login-button/index.less
Normal file
4
src/components/login-button/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
.login-button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
52
src/components/login-button/index.tsx
Normal file
52
src/components/login-button/index.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Button, ButtonProps } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import LoginDialog from '@/components/login-dialog';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { isNeedLogin } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export enum BindPhoneStatus {
|
||||
Success,
|
||||
Cancel,
|
||||
Error,
|
||||
}
|
||||
|
||||
export interface ILoginButtonProps extends ButtonProps {
|
||||
needPhone?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'login-button';
|
||||
|
||||
function LoginButton(props: ILoginButtonProps) {
|
||||
const { className, children, needPhone, onClick, ...otherProps } = props;
|
||||
const userInfo = useUserInfo();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const needLogin = isNeedLogin(userInfo);
|
||||
|
||||
const onSuccess = useCallback(
|
||||
e => {
|
||||
setVisible(false);
|
||||
onClick?.(e);
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{...otherProps}
|
||||
className={classNames(PREFIX, className)}
|
||||
onClick={needLogin ? () => setVisible(true) : onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{visible && <LoginDialog onCancel={() => setVisible(false)} onSuccess={onSuccess} needPhone={needPhone} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginButton;
|
38
src/components/login-dialog/index.less
Normal file
38
src/components/login-dialog/index.less
Normal file
@ -0,0 +1,38 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.login-dialog {
|
||||
&__container {
|
||||
.flex-column();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 56px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&__cancel-button {
|
||||
min-width: fit-content;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin-top: 40px;
|
||||
|
||||
&::after {
|
||||
border-color: transparent
|
||||
}
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
58
src/components/login-dialog/index.tsx
Normal file
58
src/components/login-dialog/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import PhoneButton, { IPhoneButtonProps } from '@/components/phone-button';
|
||||
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
onCancel: () => void;
|
||||
needPhone?: IPhoneButtonProps['needPhone'];
|
||||
onSuccess?: IPhoneButtonProps['onSuccess'];
|
||||
onBindPhone?: IPhoneButtonProps['onBindPhone'];
|
||||
}
|
||||
|
||||
const PREFIX = 'login-dialog';
|
||||
|
||||
export default function LoginDialog(props: IProps) {
|
||||
const { title = '使用播络服务前,请先登录', needPhone, onSuccess, onCancel, onBindPhone } = props;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleTipCheck = useCallback(() => {
|
||||
Toast.info('请先阅读并同意协议');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onCancel}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>{title}</div>
|
||||
{!checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
{checked && (
|
||||
<PhoneButton
|
||||
className={`${PREFIX}__confirm-button`}
|
||||
onSuccess={onSuccess}
|
||||
onBindPhone={onBindPhone}
|
||||
needPhone={needPhone}
|
||||
>
|
||||
登录
|
||||
</PhoneButton>
|
||||
)}
|
||||
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
|
||||
跳过,暂不登录
|
||||
</Button>
|
||||
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
50
src/components/login-guide/index.less
Normal file
50
src/components/login-guide/index.less
Normal file
@ -0,0 +1,50 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.login-content {
|
||||
&__container {
|
||||
.flex-column();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 56px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&__cancel-button {
|
||||
min-width: fit-content;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin-top: 40px;
|
||||
|
||||
&::after {
|
||||
border-color: transparent
|
||||
}
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1001;
|
||||
background: #FFFFFF;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
padding-top: 48px;
|
||||
padding-bottom: 64px;
|
||||
}
|
119
src/components/login-guide/index.tsx
Normal file
119
src/components/login-guide/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Popup } from '@taroify/core';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import PhoneButton, { BindPhoneStatus, IPhoneButtonProps } from '@/components/phone-button';
|
||||
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { logWithPrefix, sleep } from '@/utils/common';
|
||||
import { waitLocationAuthorizeHidden } from '@/utils/location';
|
||||
import Toast from '@/utils/toast';
|
||||
import { shouldShowLoginGuide } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IContentProps extends Pick<IPhoneButtonProps, 'onBindPhone'> {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
disabled: boolean;
|
||||
onAfterBind?: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'login-content';
|
||||
const PREFIX_GUIDE = 'login-guide';
|
||||
const log = logWithPrefix(PREFIX_GUIDE);
|
||||
|
||||
function LoginContent(props: IContentProps) {
|
||||
const { onCancel, onBindPhone } = props;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleClose = useCallback(() => onCancel(), [onCancel]);
|
||||
|
||||
const handleTipCheck = useCallback(() => {
|
||||
Toast.info('请先阅读并同意协议');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>使用播络服务前,请先登录</div>
|
||||
{!checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
{checked && (
|
||||
<PhoneButton
|
||||
className={`${PREFIX}__confirm-button`}
|
||||
onSuccess={handleClose}
|
||||
onBindPhone={onBindPhone}
|
||||
needPhone
|
||||
>
|
||||
登录
|
||||
</PhoneButton>
|
||||
)}
|
||||
<Button className={`${PREFIX}__cancel-button`} onClick={handleClose}>
|
||||
跳过,暂不登录
|
||||
</Button>
|
||||
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoginGuide(props: IProps) {
|
||||
const { disabled = false, onAfterBind } = props;
|
||||
const userInfo = useUserInfo();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleBind = useCallback(
|
||||
(status: BindPhoneStatus) => {
|
||||
status === BindPhoneStatus.Success && onAfterBind?.();
|
||||
status === BindPhoneStatus.Success && setOpen(false);
|
||||
},
|
||||
[onAfterBind]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => setOpen(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled || !shouldShowLoginGuide(userInfo)) {
|
||||
return;
|
||||
}
|
||||
let effectCleaned = false;
|
||||
const showGuide = async () => {
|
||||
await sleep(1);
|
||||
await waitLocationAuthorizeHidden();
|
||||
if (effectCleaned) {
|
||||
log('ignore login guide, effect changed');
|
||||
return;
|
||||
}
|
||||
setOpen(true);
|
||||
log('open login guide');
|
||||
};
|
||||
showGuide();
|
||||
return () => {
|
||||
effectCleaned = true;
|
||||
};
|
||||
}, [disabled, userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
log('hide login guide by disabled');
|
||||
setOpen(false);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup className={PREFIX_GUIDE} placement="bottom" open={open} onClose={handleClose}>
|
||||
<LoginContent onCancel={handleClose} onBindPhone={handleBind} />
|
||||
<SafeBottomPadding />
|
||||
</Popup>
|
||||
);
|
||||
}
|
117
src/components/material-card/index.less
Normal file
117
src/components/material-card/index.less
Normal file
@ -0,0 +1,117 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-card {
|
||||
padding: 32px 24px;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__header {
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
|
||||
&__left,
|
||||
&__right {
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
.flex-row();
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
width: 100%;
|
||||
height: 156px;
|
||||
margin-top: 24px;
|
||||
.flex-column();
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
height: 100%;
|
||||
.flex-column();
|
||||
justify-content: center;
|
||||
|
||||
&__tips {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__create-button {
|
||||
.button();
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 22px;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 102px;
|
||||
height: 100%;
|
||||
background: linear-gradient(91.41deg, rgba(255, 255, 255, 0) 1.86%, #FFFFFF 99.47%);
|
||||
}
|
||||
}
|
||||
|
||||
&__cover-list {
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__cover-image {
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
// 不知道为啥高度不对,可能 scroll-view 默认底部是滚动条高度?
|
||||
margin-top: 38px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
}
|
145
src/components/material-card/index.tsx
Normal file
145
src/components/material-card/index.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { Image, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { Loading } from '@taroify/core';
|
||||
import { ArrowRight } from '@taroify/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import LoginButton from '@/components/login-button';
|
||||
import { EventName, PageUrl } from '@/constants/app';
|
||||
import { CollectEventName, ReportEventId } from '@/constants/event';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent, reportEvent } from '@/utils/event';
|
||||
import { requestProfileDetail, sortVideos } from '@/utils/material';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
import { isValidUserInfo } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-card';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const realtimeLogger = Taro.getRealtimeLogManager();
|
||||
realtimeLogger.tag(PREFIX);
|
||||
|
||||
function MaterialCard(props: IProps) {
|
||||
const { className } = props;
|
||||
const userInfo = useUserInfo();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profile, setProfile] = useState<MaterialProfile | null>(null);
|
||||
const refreshRef = useRef((_f?: boolean) => { });
|
||||
const hasMaterial = !!profile;
|
||||
|
||||
const handleGoCreateProfile = useCallback(() => {
|
||||
reportEvent(ReportEventId.CLICK_GO_TO_CREATE_MATERIAL);
|
||||
navigateTo(PageUrl.MaterialUploadVideo);
|
||||
}, []);
|
||||
|
||||
const handleGoProfile = useCallback(() => {
|
||||
if (!hasMaterial) {
|
||||
realtimeLogger.info('handleGoProfile noMaterial')
|
||||
return;
|
||||
}
|
||||
navigateTo(PageUrl.MaterialProfile).catch(err => {
|
||||
realtimeLogger.error('handleGoProfile Failed', err);
|
||||
});
|
||||
}, [hasMaterial]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshRef.current = async (force: boolean = false) => {
|
||||
collectEvent(CollectEventName.MATERIAL_CARD_VIEW, {
|
||||
status: 'refresh',
|
||||
info: { force, isCreateResume: userInfo.isCreateResume },
|
||||
});
|
||||
setLoading(true);
|
||||
if (!userInfo.isCreateResume && !force) {
|
||||
log('refresh break by is not create resume');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const profileDetail = await requestProfileDetail();
|
||||
if (!profileDetail) {
|
||||
realtimeLogger.info('getProfileDetail no profileDetail')
|
||||
}
|
||||
setProfile(profileDetail);
|
||||
} catch (e) {
|
||||
realtimeLogger.error('getProfileDetail Failed', e);
|
||||
Toast.error('加载失败');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
}, [userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidUserInfo(userInfo)) {
|
||||
return;
|
||||
}
|
||||
refreshRef.current?.(true);
|
||||
}, [userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async () => {
|
||||
refreshRef.current?.(true);
|
||||
};
|
||||
Taro.eventCenter.on(EventName.CREATE_PROFILE, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.CREATE_PROFILE, callback);
|
||||
};
|
||||
}, [userInfo]);
|
||||
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)} onClick={handleGoProfile}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header__left`}>
|
||||
<div className={`${PREFIX}__header__title`}>我的模卡</div>
|
||||
{/* {profile && (
|
||||
<div
|
||||
className={`${PREFIX}__header__progress`}
|
||||
>{`完成度${Math.min((profile.progressBar || 0) * 100, 100)}%`}</div>
|
||||
)} */}
|
||||
</div>
|
||||
{profile && (
|
||||
<div className={`${PREFIX}__header__right`}>
|
||||
{/* <div className={`${PREFIX}__header__status`}>{profile?.isOpen ? '开放中' : '关闭'}</div> */}
|
||||
<ArrowRight className={`${PREFIX}__header__icon`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__body`}>
|
||||
{!loading && !hasMaterial && (
|
||||
<div className={`${PREFIX}__placeholder`}>
|
||||
<div className={`${PREFIX}__placeholder__tips`}>创建模卡更容易被老板挑中哦</div>
|
||||
<LoginButton className={`${PREFIX}__placeholder__create-button`} onClick={handleGoCreateProfile}>
|
||||
去创建
|
||||
</LoginButton>
|
||||
</div>
|
||||
)}
|
||||
{!loading && hasMaterial && (
|
||||
<ScrollView className={`${PREFIX}__scroll-view`} showScrollbar={false} enableFlex enhanced scrollX>
|
||||
<div className={`${PREFIX}__cover-list`}>
|
||||
{sortVideos(profile?.materialVideoInfoList || []).map(video => (
|
||||
<Image
|
||||
className={`${PREFIX}__cover-image`}
|
||||
mode="aspectFit"
|
||||
key={video.coverUrl}
|
||||
src={video.coverUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollView>
|
||||
)}
|
||||
{loading && <Loading />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialCard;
|
0
src/components/material-guide/index.less
Normal file
0
src/components/material-guide/index.less
Normal file
43
src/components/material-guide/index.tsx
Normal file
43
src/components/material-guide/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import CommonDialog from '@/components/common-dialog';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { ReportEventId } from '@/constants/event';
|
||||
import { reportEvent } from '@/utils/event';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-guide';
|
||||
|
||||
function MaterialGuide(props: IProps) {
|
||||
const { onClose } = props;
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
reportEvent(ReportEventId.VIEW_MATERIAL_GUIDE);
|
||||
navigateTo(PageUrl.MaterialUploadVideo);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
// useEffect(() => {
|
||||
// updateLastMaterialGuideTime();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<CommonDialog
|
||||
className={`${PREFIX}__dialog`}
|
||||
visible
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onClick={handleConfirm}
|
||||
content="完善模卡更容易获得老板青睐"
|
||||
confirm="立刻完善"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialGuide;
|
42
src/components/material-manage-popup/index.less
Normal file
42
src/components/material-manage-popup/index.less
Normal file
@ -0,0 +1,42 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-manage-popup {
|
||||
.flex-column();
|
||||
|
||||
&__popup {
|
||||
background: #FFFFFF;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
.flex-column();
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__tips {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
.button(@width: 360px; @height: 72px; @fontSize: 28px; @borderRadius: 44px);
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
58
src/components/material-manage-popup/index.tsx
Normal file
58
src/components/material-manage-popup/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Popup } from '@taroify/core';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import Select from '@/components/select';
|
||||
import { MaterialStatus } from '@/constants/material';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
value: MaterialStatus;
|
||||
onSave: (newValue: MaterialProfile['isOpen']) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-manage-popup';
|
||||
const OPTIONS = [
|
||||
{ label: '开放', value: MaterialStatus.Open },
|
||||
{ label: '关闭', value: MaterialStatus.Close },
|
||||
];
|
||||
|
||||
function MaterialManagePopup(props: IProps) {
|
||||
const { open, value = MaterialStatus.Open, onSave, onClose } = props;
|
||||
const [currentValue, setCurrentValue] = useState<MaterialStatus>(value);
|
||||
|
||||
const handleSelect = useCallback((v: MaterialStatus) => setCurrentValue(v), []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(currentValue === MaterialStatus.Open);
|
||||
onClose();
|
||||
}, [currentValue, onSave, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Popup className={`${PREFIX}__popup`} placement="bottom" open={open} onClose={onClose}>
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__title`}>模卡开放状态</div>
|
||||
<div className={`${PREFIX}__tips`}>开放模卡给企业查看后,可以获得更多求职机会</div>
|
||||
</div>
|
||||
<Select className={`${PREFIX}__select`} options={OPTIONS} value={currentValue} onSelect={handleSelect} />
|
||||
<Button className={`${PREFIX}__btn`} onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
<SafeBottomPadding />
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialManagePopup;
|
96
src/components/material-video-card/index.less
Normal file
96
src/components/material-video-card/index.less
Normal file
@ -0,0 +1,96 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-video-card {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
|
||||
&__cover {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__cover__image,
|
||||
&__cover__placeholder {
|
||||
width: 150px;
|
||||
height: 200px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
&__cover__placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
&__cover__placeholder__image {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&__cover__preview-video {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 50%;
|
||||
transform: translate3d(50%, -50%, 0);
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__info__title {
|
||||
width: 100%;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
|
||||
&__info__title__input {
|
||||
font-size: 32px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__info__title__placeholder {
|
||||
font-size: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__info__operate {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__info__operate__checkbox {
|
||||
--checkbox-label-color: @blColor;
|
||||
--checkbox-checked-icon-border-color: @blHighlightColor;
|
||||
--checkbox-checked-icon-background-color: @blHighlightColor;
|
||||
}
|
||||
|
||||
&__info__operate__delete {
|
||||
color: @blHighlightColor;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__info__temp-tips {
|
||||
font-size: 28px;
|
||||
line-height: 50px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
}
|
149
src/components/material-video-card/index.tsx
Normal file
149
src/components/material-video-card/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { BaseEventOrig, Image, Input, InputProps, Text } from '@tarojs/components';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { Checkbox } from '@taroify/core';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { MaterialVideoInfo } from '@/types/material';
|
||||
import { logWithPrefix, isDesktop } from '@/utils/common';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
isTemp?: boolean;
|
||||
videoInfo: MaterialVideoInfo;
|
||||
onClickUpload?: () => void;
|
||||
onClickDelete?: () => void;
|
||||
onClickSetDefault?: () => void;
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-video-card';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function MaterialVideoCard(props: IProps) {
|
||||
const { videoInfo, isTemp = false, onClickUpload, onClickDelete, onClickSetDefault, onTitleChange } = props;
|
||||
const isVideo = videoInfo.type === 'video';
|
||||
|
||||
const handleInput = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputValueEventDetail>) => {
|
||||
const value = e.detail?.value || '';
|
||||
log('handleInput value', value);
|
||||
onTitleChange?.(value);
|
||||
},
|
||||
[onTitleChange]
|
||||
);
|
||||
|
||||
// const handleInputBlurOrConfirm = useCallback(() => {
|
||||
// log('newVideoTitle', title);
|
||||
// if (!title) {
|
||||
// return;
|
||||
// }
|
||||
// onTitleChange?.(videoInfo);
|
||||
// // ...
|
||||
// }, [title, videoInfo, onTitleChange]);
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
log('handleCheckboxChange', checked);
|
||||
if (videoInfo.isDefault) {
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
onClickSetDefault?.();
|
||||
},
|
||||
[videoInfo, onClickSetDefault]
|
||||
);
|
||||
|
||||
const handleClickVideo = useCallback(() => {
|
||||
log('handleClickVideo', videoInfo);
|
||||
if (!videoInfo.url) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
navigateTo(PageUrl.MaterialWebview, {
|
||||
source: encodeURIComponent(videoInfo.url)
|
||||
})
|
||||
} else {
|
||||
Taro.previewMedia({
|
||||
sources: [{ url: videoInfo.url, type: videoInfo.type }],
|
||||
});
|
||||
}
|
||||
|
||||
}, [videoInfo]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__cover`}>
|
||||
{!isTemp && (
|
||||
<>
|
||||
<Image
|
||||
className={`${PREFIX}__cover__image`}
|
||||
mode="aspectFit"
|
||||
src={videoInfo.coverUrl}
|
||||
onClick={handleClickVideo}
|
||||
/>
|
||||
{isVideo && (
|
||||
<Image
|
||||
className={`${PREFIX}__cover__preview-video`}
|
||||
mode="aspectFit"
|
||||
src={require('@/statics/svg/preview_video.svg')}
|
||||
onClick={handleClickVideo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isTemp && (
|
||||
<div className={`${PREFIX}__cover__placeholder`} onClick={onClickUpload}>
|
||||
<Image
|
||||
className={`${PREFIX}__cover__placeholder__image`}
|
||||
mode="aspectFit"
|
||||
src={require('@/statics/svg/add.svg')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
{!isTemp && (
|
||||
<>
|
||||
<div className={`${PREFIX}__info__title`}>
|
||||
<Input
|
||||
value={videoInfo.title}
|
||||
maxlength={20}
|
||||
confirmType="done"
|
||||
placeholder="请填写直播产品名称"
|
||||
onInput={handleInput}
|
||||
// onBlur={handleInputBlurOrConfirm}
|
||||
// onConfirm={handleInputBlurOrConfirm}
|
||||
className={`${PREFIX}__info__title__input`}
|
||||
placeholderClass={`${PREFIX}__info__title__placeholder`}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${PREFIX}__info__operate`}>
|
||||
<Checkbox
|
||||
checked={videoInfo.isDefault}
|
||||
onChange={handleCheckboxChange}
|
||||
className={`${PREFIX}__info__operate__checkbox`}
|
||||
>
|
||||
选为封面
|
||||
</Checkbox>
|
||||
<div className={`${PREFIX}__info__operate__delete`} onClick={onClickDelete}>
|
||||
删除
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isTemp && (
|
||||
<Text className={`${PREFIX}__info__temp-tips`}>
|
||||
{`视频不能超过1000M
|
||||
视频若太大加载较慢,请耐心等待`}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialVideoCard;
|
85
src/components/message-card/index.less
Normal file
85
src/components/message-card/index.less
Normal file
@ -0,0 +1,85 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.message-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
padding: 20px 32px;
|
||||
box-sizing: border-box;
|
||||
background: #FFF;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
background: #00000026;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(32px + 90px + 24px);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__unread {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: calc(32px - 8px);
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
padding: 4px 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
background: #EB5953;
|
||||
transform: translate3d(20%, -50%, 0);
|
||||
}
|
||||
|
||||
&__body-container {
|
||||
flex: 1;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: 78vw;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: #8D8E99;
|
||||
margin-top: 4px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: #8D8E99;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
42
src/components/message-card/index.tsx
Normal file
42
src/components/message-card/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { MainMessage } from '@/types/message';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import { formatTime } from '@/utils/time';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: MainMessage;
|
||||
}
|
||||
|
||||
const PREFIX = 'message-card';
|
||||
|
||||
function MessageCard(props: IProps) {
|
||||
const { data } = props;
|
||||
|
||||
const handleClick = useCallback(() => navigateTo(PageUrl.MessageChat, { chatId: data.chatId }), [data]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} onClick={handleClick}>
|
||||
<div className={`${PREFIX}__avatar-container`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__avatar`}
|
||||
src={data.toUserAvatarUrl || require('@/statics/png/default_avatar.png')}
|
||||
/>
|
||||
{!!data.unReadMsgCount && <div className={`${PREFIX}__unread`}>{Math.min(data.unReadMsgCount, 999)}</div>}
|
||||
</div>
|
||||
<div className={`${PREFIX}__body-container`}>
|
||||
<div className={`${PREFIX}__name`}>{data.toUserName}</div>
|
||||
<div className={`${PREFIX}__content`}>{data.lastContactMsgContent}</div>
|
||||
<div className={`${PREFIX}__time`}>{formatTime(data.lastContactTime, 'MM-DD')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageCard;
|
54
src/components/message-chat/base/index.less
Normal file
54
src/components/message-chat/base/index.less
Normal file
@ -0,0 +1,54 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.base-message {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
margin-top: 40px;
|
||||
padding: 0 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.is-sender {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__content-container {
|
||||
flex: 1;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin: 0 16px;
|
||||
|
||||
.is-sender & {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
background: #D9D9D9;
|
||||
padding: 20px 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 8px;
|
||||
|
||||
&.done {
|
||||
color: #8D8E99;
|
||||
}
|
||||
}
|
||||
}
|
66
src/components/message-chat/base/index.tsx
Normal file
66
src/components/message-chat/base/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren, useEffect, useState, useCallback } from 'react';
|
||||
import { MaterialViewSource } from '@/constants/material';
|
||||
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { IChatMessage } from '@/types/message';
|
||||
import { getScrollItemId } from '@/utils/common';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export interface IBaseMessageProps {
|
||||
id: string;
|
||||
message: IChatMessage;
|
||||
}
|
||||
|
||||
export interface IUserMessageProps extends PropsWithChildren, IBaseMessageProps {
|
||||
isRead?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'base-message';
|
||||
|
||||
function BaseMessage(props: IUserMessageProps) {
|
||||
const { id, message, isRead: isReadProps, children } = props;
|
||||
const { userId } = useUserInfo();
|
||||
const [isRead, setIsRead] = useState(message.isRead);
|
||||
const isSender = message.senderUserId === userId;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isSender) {
|
||||
// return;
|
||||
// }
|
||||
// // 对方发的消息,拉取到消息后,后端会主动已读,这里延迟模拟下
|
||||
// const timer = setTimeout(() => setIsRead(true), 1200);
|
||||
// return () => clearTimeout(timer);
|
||||
// }, [isSender]);
|
||||
const handleClick = useCallback(
|
||||
() => navigateTo(PageUrl.MaterialView, { resumeId: message.jobId, source: MaterialViewSource.Chat }),
|
||||
[message.jobId]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isRead) {
|
||||
return;
|
||||
}
|
||||
isReadProps && setIsRead(true);
|
||||
}, [isRead, isReadProps]);
|
||||
|
||||
return (
|
||||
<div className={classNames(PREFIX, { 'is-sender': isSender })} id={getScrollItemId(id)}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__avatar`}
|
||||
src={message.senderAvatarUrl || require('@/statics/png/default_avatar.png')}
|
||||
/>
|
||||
<div className={`${PREFIX}__content-container`}>
|
||||
{children}
|
||||
<div className={classNames(`${PREFIX}__status`, { done: isRead })}>{isRead ? '已读' : '未读'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BaseMessage;
|
50
src/components/message-chat/contact/index.less
Normal file
50
src/components/message-chat/contact/index.less
Normal file
@ -0,0 +1,50 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.exchange-message {
|
||||
width: 100%;
|
||||
.flex-column();
|
||||
margin-top: 40px;
|
||||
|
||||
&__content {
|
||||
padding: 24px 60px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
.flex-row();
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
&__reject {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @borderRadius: 48px);
|
||||
border: 2px solid #E0E0E0;
|
||||
color: @blColor;
|
||||
background: #FFF;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&__agree {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @borderRadius: 48px);
|
||||
}
|
||||
|
||||
&__disable-btn {
|
||||
.button(@height: 56px; @fontSize: 28px; @borderRadius: 48px);
|
||||
padding: 0 74px;
|
||||
color: #C0C0C0;
|
||||
background: #F0F0F0;
|
||||
|
||||
&:active {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
}
|
||||
}
|
91
src/components/message-chat/contact/index.tsx
Normal file
91
src/components/message-chat/contact/index.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { IBaseMessageProps } from '@/components/message-chat/base';
|
||||
import { MessageActionStatus } from '@/constants/message';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { getScrollItemId } from '@/utils/common';
|
||||
import { posConfirmAction, postAddMessageTimes, requestChatActionStatus } from '@/utils/message';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends IBaseMessageProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'exchange-message';
|
||||
|
||||
function ContactMessage(props: IProps) {
|
||||
const { id, message, onClick } = props;
|
||||
const { userId } = useUserInfo();
|
||||
const [status, setStatus] = useState<MessageActionStatus>();
|
||||
const isSender = message.senderUserId === userId;
|
||||
|
||||
const handleClickReject = useCallback(async () => {
|
||||
if (isSender || status !== MessageActionStatus.Send) {
|
||||
return;
|
||||
}
|
||||
postAddMessageTimes('click_reject_exchange_contact');
|
||||
await posConfirmAction({ actionId: message.actionId, status: false });
|
||||
setStatus(MessageActionStatus.Reject);
|
||||
onClick();
|
||||
}, [isSender, status, message.actionId, onClick]);
|
||||
|
||||
const handleClickAgree = useCallback(async () => {
|
||||
if (isSender || status !== MessageActionStatus.Send) {
|
||||
return;
|
||||
}
|
||||
postAddMessageTimes('click_agree_exchange_contact');
|
||||
await posConfirmAction({ actionId: message.actionId, status: true });
|
||||
setStatus(MessageActionStatus.Agree);
|
||||
onClick();
|
||||
}, [isSender, status, message.actionId, onClick]);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const res = await requestChatActionStatus(message.actionId);
|
||||
setStatus(res);
|
||||
};
|
||||
init();
|
||||
}, [message.actionId]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} id={getScrollItemId(id)}>
|
||||
<div className={`${PREFIX}__content`}>
|
||||
<div className={`${PREFIX}__title`}>
|
||||
{isSender ? '您已向对方请求交换联系方式' : `${message.senderName}申请交换联系方式,沟通面试`}
|
||||
</div>
|
||||
<div className={`${PREFIX}__buttons`}>
|
||||
{isSender && (
|
||||
<Button className={`${PREFIX}__disable-btn`} onClick={handleClickAgree}>
|
||||
{status === MessageActionStatus.Agree
|
||||
? '对方已同意'
|
||||
: status === MessageActionStatus.Reject
|
||||
? '对方已拒绝'
|
||||
: '等待对方同意'}
|
||||
</Button>
|
||||
)}
|
||||
{!isSender && (
|
||||
<>
|
||||
{(status === MessageActionStatus.Send || !status) && (
|
||||
<>
|
||||
<Button className={`${PREFIX}__reject`} onClick={handleClickReject}>
|
||||
拒绝
|
||||
</Button>
|
||||
<Button className={`${PREFIX}__agree`} onClick={handleClickAgree}>
|
||||
同意
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{status === MessageActionStatus.Agree && <Button className={`${PREFIX}__disable-btn`}>你已同意</Button>}
|
||||
{status === MessageActionStatus.Reject && <Button className={`${PREFIX}__disable-btn`}>你已拒绝</Button>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContactMessage;
|
8
src/components/message-chat/index.ts
Normal file
8
src/components/message-chat/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import ContactMessage from './contact';
|
||||
import JobMessage from './job';
|
||||
import LocationMessage from './location';
|
||||
import MaterialMessage from './material';
|
||||
import TextMessage from './text';
|
||||
import TimeMessage from './time';
|
||||
|
||||
export { ContactMessage, JobMessage, LocationMessage, MaterialMessage, TextMessage, TimeMessage };
|
29
src/components/message-chat/job/index.less
Normal file
29
src/components/message-chat/job/index.less
Normal file
@ -0,0 +1,29 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-message {
|
||||
width: calc(100% - 64px);
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin: 40px 32px 0;
|
||||
padding: 20px 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__salary {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
}
|
38
src/components/message-chat/job/index.tsx
Normal file
38
src/components/message-chat/job/index.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { IBaseMessageProps } from '@/components/message-chat/base';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { IJobMessage } from '@/types/message';
|
||||
import { getScrollItemId, safeJsonParse } from '@/utils/common';
|
||||
import { getJobSalary } from '@/utils/job';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends IBaseMessageProps {}
|
||||
|
||||
const PREFIX = 'job-message';
|
||||
|
||||
function JobMessage(props: IProps) {
|
||||
const { id, message } = props;
|
||||
const data = safeJsonParse<IJobMessage>(message.actionObject, null);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
navigateTo(PageUrl.JobDetail, { id: data.id });
|
||||
}, [data]);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={PREFIX} id={getScrollItemId(id)} onClick={handleClick}>
|
||||
<div className={`${PREFIX}__title`}>{data.title}</div>
|
||||
<div className={`${PREFIX}__salary`}>{getJobSalary(data)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobMessage;
|
45
src/components/message-chat/location/index.less
Normal file
45
src/components/message-chat/location/index.less
Normal file
@ -0,0 +1,45 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.location-message {
|
||||
&__map-container {
|
||||
width: 440px;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
background: #FFF;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin: 20px 20px 0;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__address {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin: 4px 20px 20px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__map {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
}
|
69
src/components/message-chat/location/index.tsx
Normal file
69
src/components/message-chat/location/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Map } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import BaseMessage, { IUserMessageProps } from '@/components/message-chat/base';
|
||||
import { ILocationMessage } from '@/types/message';
|
||||
import { safeJsonParse } from '@/utils/common';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends IUserMessageProps {}
|
||||
|
||||
const PREFIX = 'location-message';
|
||||
|
||||
function LocationMessage(props: IProps) {
|
||||
const { message } = props;
|
||||
const data = safeJsonParse<ILocationMessage>(message.actionObject, null);
|
||||
|
||||
const handleClickMap = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
Taro.openLocation({
|
||||
name: data.name,
|
||||
address: data.address,
|
||||
longitude: Number(data.longitude),
|
||||
latitude: Number(data.latitude),
|
||||
scale: 18,
|
||||
});
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseMessage {...props}>
|
||||
<div className={`${PREFIX}__map-container`} onClick={handleClickMap}>
|
||||
<div className={`${PREFIX}__name`}>{data.name}</div>
|
||||
<div className={`${PREFIX}__address`}>{data.address}</div>
|
||||
<Map
|
||||
scale={15}
|
||||
enableZoom={false}
|
||||
enableScroll={false}
|
||||
className={`${PREFIX}__map`}
|
||||
latitude={Number(data.latitude)}
|
||||
longitude={Number(data.longitude)}
|
||||
markers={[
|
||||
{
|
||||
id: 0,
|
||||
latitude: Number(data.latitude),
|
||||
longitude: Number(data.longitude),
|
||||
iconPath: '',
|
||||
width: 20,
|
||||
height: 28,
|
||||
},
|
||||
]}
|
||||
onError={() => Toast.error('地图加载错误')}
|
||||
/>
|
||||
<div className={`${PREFIX}__mask`} onClick={handleClickMap} />
|
||||
</div>
|
||||
</BaseMessage>
|
||||
);
|
||||
}
|
||||
|
||||
export default LocationMessage;
|
30
src/components/message-chat/material/index.less
Normal file
30
src/components/message-chat/material/index.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-message {
|
||||
width: calc(100% - 64px);
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin: 40px 32px 0;
|
||||
padding: 20px 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__name {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__basic,
|
||||
&__categories {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
}
|
41
src/components/message-chat/material/index.tsx
Normal file
41
src/components/message-chat/material/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { IBaseMessageProps } from '@/components/message-chat/base';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { MaterialViewSource } from '@/constants/material';
|
||||
import { IMaterialMessage } from '@/types/message';
|
||||
import { getScrollItemId, safeJsonParse } from '@/utils/common';
|
||||
import { getBasicInfo } from '@/utils/material';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends IBaseMessageProps {}
|
||||
|
||||
const PREFIX = 'material-message';
|
||||
|
||||
function MaterialMessage(props: IProps) {
|
||||
const { id, message } = props;
|
||||
const data = safeJsonParse<IMaterialMessage>(message.actionObject, null);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
navigateTo(PageUrl.MaterialView, { resumeId: data.id, source: MaterialViewSource.Chat });
|
||||
}, [data]);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX} id={getScrollItemId(id)} onClick={handleClick}>
|
||||
<div className={`${PREFIX}__name`}>{data.name}</div>
|
||||
<div className={`${PREFIX}__basic`}>{getBasicInfo(data)}</div>
|
||||
<div className={`${PREFIX}__categories`}>{`播过 ${data.workedSecCategoryStr}`}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialMessage;
|
14
src/components/message-chat/text/index.less
Normal file
14
src/components/message-chat/text/index.less
Normal file
@ -0,0 +1,14 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.text-message {
|
||||
&__content {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
background: #D9D9D9;
|
||||
padding: 20px 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
32
src/components/message-chat/text/index.tsx
Normal file
32
src/components/message-chat/text/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Text } from '@tarojs/components';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import BaseMessage, { IUserMessageProps } from '@/components/message-chat/base';
|
||||
import { copy } from '@/utils/common';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends IUserMessageProps {}
|
||||
|
||||
const PREFIX = 'text-message';
|
||||
|
||||
function TextMessage(props: IProps) {
|
||||
const { message } = props;
|
||||
|
||||
const handleLongPress = useCallback(async () => {
|
||||
await copy(message.content);
|
||||
Toast.success('复制成功');
|
||||
}, [message.content]);
|
||||
|
||||
return (
|
||||
<BaseMessage {...props}>
|
||||
<Text className={`${PREFIX}__content`} onLongPress={handleLongPress}>
|
||||
{message.content}
|
||||
</Text>
|
||||
</BaseMessage>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextMessage;
|
12
src/components/message-chat/time/index.less
Normal file
12
src/components/message-chat/time/index.less
Normal file
@ -0,0 +1,12 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.time-message {
|
||||
width: 100%;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
color: #8D8E99;
|
||||
margin-top: 40px;
|
||||
}
|
20
src/components/message-chat/time/index.tsx
Normal file
20
src/components/message-chat/time/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { IBaseMessageProps } from '@/components/message-chat/base';
|
||||
import { getScrollItemId } from '@/utils/common';
|
||||
import { formatTime } from '@/utils/time';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends IBaseMessageProps {}
|
||||
|
||||
const PREFIX = 'time-message';
|
||||
|
||||
function TimeMessage(props: IProps) {
|
||||
const { id, message } = props;
|
||||
return (
|
||||
<div className={PREFIX} id={getScrollItemId(id)}>
|
||||
{formatTime(message.content, 'MM-DD HH:mm')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeMessage;
|
83
src/components/message-dialog/index.less
Normal file
83
src/components/message-dialog/index.less
Normal file
@ -0,0 +1,83 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.message-dialog {
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
line-height: 58px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 28px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
color: @blColorG2;
|
||||
|
||||
.highlight {
|
||||
display: inline;
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__help {
|
||||
.flex-column();
|
||||
|
||||
&__title {
|
||||
.title();
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 24px
|
||||
}
|
||||
|
||||
&__tips {
|
||||
.tips();
|
||||
|
||||
.highlight {
|
||||
display: inline;
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn {
|
||||
.button(@width: 360px; @height: 72px; @fontSize: 28px; @borderRadius: 36px);
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__no-times {
|
||||
&__title {
|
||||
.title();
|
||||
}
|
||||
|
||||
&__tips {
|
||||
.tips();
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
width: 100%;
|
||||
.flex-column();
|
||||
padding: 40px 0;
|
||||
background: @blHighlightBg;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__times {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
|
||||
&__btn {
|
||||
.button(@width: 360px; @height: 72px; @fontSize: 28px; @borderRadius: 36px);
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
70
src/components/message-dialog/index.tsx
Normal file
70
src/components/message-dialog/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'message-dialog';
|
||||
const HELP = `${PREFIX}__help`;
|
||||
const NO_TIMES = `${PREFIX}__no-times`;
|
||||
|
||||
interface IHelpProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface INoTimesProps {
|
||||
open: boolean;
|
||||
times: number;
|
||||
onClick: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MessageHelpDialog(props: IHelpProps) {
|
||||
const { open, onClose } = props;
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} open={open}>
|
||||
<Dialog.Content>
|
||||
<div className={HELP}>
|
||||
<div className={`${HELP}__title`}>消息通知次数说明</div>
|
||||
<div className={`${HELP}__body`}>
|
||||
<div className={`${HELP}__tips`}>
|
||||
{`离开小程序后,如果有用户向你发送消息,我们将通过微信的"服务通知"提醒你。\n由于微信服务通知有次数限制,次数使用完则无法收到通知。`}
|
||||
</div>
|
||||
<div className={`${HELP}__tips`}>
|
||||
<div className="highlight">通知次数</div>
|
||||
代表你还能接收服务通知的次数
|
||||
</div>
|
||||
<div className={`${HELP}__tips`}>
|
||||
<div className="highlight">点击增加</div>
|
||||
可以为你增加通知次数
|
||||
</div>
|
||||
</div>
|
||||
<Button className={`${HELP}__btn`} onClick={onClose}>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageNoTimesDialog(props: INoTimesProps) {
|
||||
const { open, times = 0, onClick, onClose } = props;
|
||||
|
||||
return (
|
||||
<Dialog className={NO_TIMES} onClose={onClose} open={open}>
|
||||
<Dialog.Content>
|
||||
<div className={`${NO_TIMES}__title`}>未读消息提醒次数不够了!</div>
|
||||
<div className={`${NO_TIMES}__tips`}>有通知次数才能<span className="highlight">及时收到</span>招聘邀请,快点击“点我增加”吧~</div>
|
||||
<div className={`${NO_TIMES}__body`}>
|
||||
<div className={`${NO_TIMES}__times`}>{`未读消息提醒剩余:${times}次`}</div>
|
||||
<Button className={`${NO_TIMES}__btn`} onClick={onClick}>
|
||||
点我增加
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
18
src/components/overlay/index.less
Normal file
18
src/components/overlay/index.less
Normal file
@ -0,0 +1,18 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.overlay {
|
||||
&__container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
background: @blMaskBg;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
33
src/components/overlay/index.tsx
Normal file
33
src/components/overlay/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren, useCallback } from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends PropsWithChildren {
|
||||
visible: boolean;
|
||||
onClickOuter: () => void;
|
||||
outerClassName?: string;
|
||||
innerClassName?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'overlay';
|
||||
|
||||
function Overlay(props: IProps) {
|
||||
const { visible, outerClassName, innerClassName, onClickOuter } = props;
|
||||
|
||||
const onClickContent = useCallback((e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation(), []);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(`${PREFIX}__container`, outerClassName)} onClick={onClickOuter}>
|
||||
<div className={classNames(`${PREFIX}__content`, innerClassName)} onClick={onClickContent}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Overlay;
|
7
src/components/page-loading/index.less
Normal file
7
src/components/page-loading/index.less
Normal file
@ -0,0 +1,7 @@
|
||||
.page-loading {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
21
src/components/page-loading/index.tsx
Normal file
21
src/components/page-loading/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Loading } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'page-loading';
|
||||
|
||||
function PageLoading(props: IProps) {
|
||||
const { className } = props;
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)}>
|
||||
<Loading direction="vertical">加载中...</Loading>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageLoading;
|
4
src/components/phone-button/index.less
Normal file
4
src/components/phone-button/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
.phone-button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
75
src/components/phone-button/index.tsx
Normal file
75
src/components/phone-button/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { BaseEventOrig, Button, ButtonProps, ITouchEvent } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import Toast from '@/utils/toast';
|
||||
import { requestUserInfo, setPhoneNumber } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export enum BindPhoneStatus {
|
||||
Success,
|
||||
Cancel,
|
||||
Error,
|
||||
}
|
||||
|
||||
export interface IPhoneButtonProps extends ButtonProps {
|
||||
message?: string;
|
||||
// 绑定后是否需要刷新接口获取手机号
|
||||
needPhone?: boolean;
|
||||
onBindPhone?: (status: BindPhoneStatus) => void;
|
||||
onSuccess?: (e: ITouchEvent) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'phone-button';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function PhoneButton(props: IPhoneButtonProps) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
message = '绑定成功',
|
||||
openType,
|
||||
needPhone,
|
||||
onSuccess,
|
||||
onBindPhone,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const handleGetPhoneNumber = useCallback(
|
||||
async (e: BaseEventOrig<ButtonProps.onGetPhoneNumberEventDetail>) => {
|
||||
log('handleGetPhoneNumber', e.detail.code, e.detail.iv, e.detail.encryptedData);
|
||||
const encryptedData = e.detail.encryptedData;
|
||||
const iv = e.detail.iv;
|
||||
if (!encryptedData || !iv) {
|
||||
onBindPhone?.(BindPhoneStatus.Cancel);
|
||||
return Toast.error('取消授权');
|
||||
}
|
||||
try {
|
||||
await setPhoneNumber({ encryptedData, iv });
|
||||
needPhone && (await requestUserInfo());
|
||||
Toast.success(message);
|
||||
onBindPhone?.(BindPhoneStatus.Success);
|
||||
onSuccess?.(e as ITouchEvent);
|
||||
} catch (err) {
|
||||
onBindPhone?.(BindPhoneStatus.Error);
|
||||
Toast.error('绑定失败');
|
||||
log('bind phone fail', err);
|
||||
}
|
||||
},
|
||||
[message, needPhone, onSuccess, onBindPhone]
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...otherProps}
|
||||
className={classNames(PREFIX, className)}
|
||||
openType="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
37
src/components/picker-toolbar/index.less
Normal file
37
src/components/picker-toolbar/index.less
Normal file
@ -0,0 +1,37 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.picker-toolbar {
|
||||
width: 100%;
|
||||
padding: 16px 24px;
|
||||
border-top: solid 1px #E0E0E0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.button(@borderColor) {
|
||||
width: calc(50% - 36px);
|
||||
height: 80px;
|
||||
line-height: 78px;
|
||||
border-radius: 48px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: solid 1px @borderColor;
|
||||
}
|
||||
|
||||
&__button-cancel {
|
||||
.button(#E0E0E0);
|
||||
margin-right: 24px;
|
||||
color: @blColor;
|
||||
background: #FFFFFF;
|
||||
|
||||
&::after {
|
||||
border-color: transparent
|
||||
}
|
||||
}
|
||||
|
||||
&__button-confirm {
|
||||
.button(@blHighlightColor);
|
||||
color: #FFFFFF;
|
||||
background: @blHighlightColor;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user