feat: first commit
This commit is contained in:
7
src/pages/anchor/index.config.ts
Normal file
7
src/pages/anchor/index.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '',
|
||||
disableScroll: true,
|
||||
enableShareAppMessage: true,
|
||||
usingComponents: {},
|
||||
});
|
||||
91
src/pages/anchor/index.less
Normal file
91
src/pages/anchor/index.less
Normal file
@ -0,0 +1,91 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-anchor {
|
||||
position: relative;
|
||||
|
||||
&__loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: @pageBg;
|
||||
}
|
||||
|
||||
&__top-search-bar {
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
margin-top: 34px;
|
||||
margin-bottom: 42px;
|
||||
}
|
||||
|
||||
&__sort-type {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blColor;
|
||||
|
||||
.selected {
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__sort-item {
|
||||
margin-left: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blColor;
|
||||
|
||||
.title {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__overlay-outer {
|
||||
top: 82px;
|
||||
}
|
||||
|
||||
&__overlay-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__tips-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding-top: 218px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__empty-box {
|
||||
width: 386px;
|
||||
height: 278px;
|
||||
}
|
||||
|
||||
&__tips-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
color: @blColor;
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
228
src/pages/anchor/index.tsx
Normal file
228
src/pages/anchor/index.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
import Taro, { NodesRef, useDidShow, useLoad } from '@tarojs/taro';
|
||||
|
||||
import { ArrowUp, ArrowDown } from '@taroify/icons';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import AnchorList, { IAnchorListProps } from '@/components/anchor-list';
|
||||
import AnchorPicker from '@/components/anchor-picker';
|
||||
import CustomNavigationBar from '@/components/custom-navigation-bar';
|
||||
import HomePage from '@/components/home-page';
|
||||
import Overlay from '@/components/overlay';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SwitchBar from '@/components/switch-bar';
|
||||
import { APP_TAB_BAR_ID, EventName, OpenSource, PageUrl } from '@/constants/app';
|
||||
import { EmployType, JobManageStatus } from '@/constants/job';
|
||||
import { ALL_ANCHOR_SORT_TYPES, ANCHOR_SORT_TYPE_TITLE_MAP, AnchorSortType } from '@/constants/material';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import useLocation from '@/hooks/use-location';
|
||||
import { JobManageInfo } from '@/types/job';
|
||||
import { Coordinate } from '@/types/location';
|
||||
import { IAnchorFilters } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { getLastSelectMyJobId, requestJobManageList, setLastSelectMyJobId } from '@/utils/job';
|
||||
import { getWxLocation } from '@/utils/location';
|
||||
import { requestUnreadMessageCount } from '@/utils/message';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-anchor';
|
||||
const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${LIST_CONTAINER_CLASS}`, `#${APP_TAB_BAR_ID}`],
|
||||
calc: (rects: [NodesRef.BoundingClientRectCallbackResult, NodesRef.BoundingClientRectCallbackResult]) => {
|
||||
const [rect, diffRect] = rects;
|
||||
return diffRect.top - rect.top;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
const EmptyTips = (props: { className?: string; height?: number }) => {
|
||||
const { className, height } = props;
|
||||
return (
|
||||
<div className={classNames(`${PREFIX}__tips-container`, className)} style={height ? { height } : undefined}>
|
||||
<Image className={`${PREFIX}__empty-box`} src={require('@/statics/svg/empty-box.svg')} mode="aspectFit" />
|
||||
<div className={`${PREFIX}__tips-title`}>该条件下还没有主播</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function ListWrapper(props: IAnchorListProps) {
|
||||
const { className, jobId, filters, cityCode, sortType, latitude, longitude } = props;
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
|
||||
const handleListEmpty = useCallback(() => {
|
||||
setIsEmpty(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsEmpty(false);
|
||||
}, [jobId, filters, cityCode, sortType, latitude, longitude]);
|
||||
|
||||
if (isEmpty) {
|
||||
return <EmptyTips className={className} height={listHeight} />;
|
||||
}
|
||||
|
||||
return <AnchorList listHeight={listHeight} {...props} onListEmpty={handleListEmpty} />;
|
||||
}
|
||||
|
||||
export default function AnchorPage() {
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectJob, setSelectJob] = useState<JobManageInfo | undefined>();
|
||||
const [filters, setFilters] = useState<IAnchorFilters>({ employType: EmployType.All });
|
||||
const [showFilter, setShowFilter] = useState<boolean>(false);
|
||||
const [sortType, setSortType] = useState<AnchorSortType>(AnchorSortType.Active);
|
||||
const [coordinate, setCoordinate] = useState<Coordinate>({
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
});
|
||||
log('jobId', selectJob);
|
||||
|
||||
const handleChangeSelectJob = useCallback((select?: JobManageInfo) => {
|
||||
log('select job change', select);
|
||||
setSelectJob(select);
|
||||
setLastSelectMyJobId(select?.id || '');
|
||||
}, []);
|
||||
|
||||
const handleClickSwitch = useCallback(
|
||||
() => navigateTo(PageUrl.JobSelectMyPublish, { id: selectJob?.id, source: OpenSource.AnchorPage }),
|
||||
[selectJob]
|
||||
);
|
||||
|
||||
const handleClickSalarySelect = useCallback(() => {
|
||||
setShowFilter(!showFilter);
|
||||
}, [showFilter]);
|
||||
|
||||
const handleHideFilter = useCallback(() => setShowFilter(false), []);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(newFilters: IAnchorFilters) => {
|
||||
!isEqual(newFilters, filters) && setFilters(newFilters);
|
||||
setShowFilter(false);
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
|
||||
const handleClickSortType = useCallback(async (type: AnchorSortType) => setSortType(type), []);
|
||||
|
||||
const handleJobChange = useCallback(
|
||||
(select: JobManageInfo, source: OpenSource) => {
|
||||
log('handleJobChange', select, source);
|
||||
source === OpenSource.AnchorPage && handleChangeSelectJob(select);
|
||||
},
|
||||
[handleChangeSelectJob]
|
||||
);
|
||||
|
||||
const handlePublishJobChange = useCallback(async () => {
|
||||
const { jobResults = [] } = await requestJobManageList({ status: JobManageStatus.Open });
|
||||
if (!selectJob) {
|
||||
// 之前没有开发中的通告,自动选中第一个开放中的通告
|
||||
handleChangeSelectJob(jobResults[0]);
|
||||
return;
|
||||
}
|
||||
const curJob = jobResults.find(j => j.id === selectJob.id);
|
||||
if (!curJob) {
|
||||
// 之前选中的通告不再开放了,自动切到第一个开放中的通告
|
||||
handleChangeSelectJob(jobResults[0]);
|
||||
} else if (!isEqual(curJob, selectJob)) {
|
||||
// 之前选中的通告发生了变化,尝试更新
|
||||
handleChangeSelectJob(curJob);
|
||||
}
|
||||
}, [selectJob, handleChangeSelectJob]);
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.SELECT_MY_PUBLISH_JOB, handleJobChange);
|
||||
Taro.eventCenter.on(EventName.COMPANY_JOB_PUBLISH_CHANGED, handlePublishJobChange);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.SELECT_MY_PUBLISH_JOB, handleJobChange);
|
||||
Taro.eventCenter.off(EventName.COMPANY_JOB_PUBLISH_CHANGED, handlePublishJobChange);
|
||||
};
|
||||
}, [handleJobChange, handlePublishJobChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const ensureLocation = async () => {
|
||||
if (location.latitude || !location.longitude) {
|
||||
const res = await getWxLocation();
|
||||
if (!res) {
|
||||
Toast.info('获取位置信息失败,请重试');
|
||||
return;
|
||||
}
|
||||
const { latitude, longitude } = res;
|
||||
setCoordinate({ latitude, longitude });
|
||||
}
|
||||
};
|
||||
ensureLocation();
|
||||
}, [location]);
|
||||
|
||||
useLoad(async () => {
|
||||
try {
|
||||
const { jobResults = [] } = await requestJobManageList({ status: JobManageStatus.Open });
|
||||
if (!jobResults.length) {
|
||||
Toast.info('当前是根据定位为您展示主播');
|
||||
return;
|
||||
}
|
||||
const lastSelectJobId = getLastSelectMyJobId();
|
||||
const lastJob = jobResults.find(job => job.id === lastSelectJobId) || jobResults[0];
|
||||
log('lastJob', lastSelectJobId, lastJob);
|
||||
handleChangeSelectJob(lastJob);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useDidShow(() => requestUnreadMessageCount());
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
{!!loading && <PageLoading className={`${PREFIX}__loading`} />}
|
||||
<CustomNavigationBar className={`${PREFIX}__navigation-bar`}>
|
||||
{selectJob && <SwitchBar title={selectJob.title.substring(0, 4)} onClick={handleClickSwitch} />}
|
||||
</CustomNavigationBar>
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__top-search-bar`}>
|
||||
<div className={classNames(`${PREFIX}__sort-type`)}>
|
||||
{ALL_ANCHOR_SORT_TYPES.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
className={classNames(`${PREFIX}__sort-item`, { selected: sortType === type })}
|
||||
onClick={() => handleClickSortType(type)}
|
||||
>
|
||||
{ANCHOR_SORT_TYPE_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={classNames(`${PREFIX}__filter`)} onClick={handleClickSalarySelect}>
|
||||
<div className="title">筛选</div>
|
||||
{showFilter ? <ArrowUp /> : <ArrowDown />}
|
||||
</div>
|
||||
</div>
|
||||
<ListWrapper
|
||||
filters={filters}
|
||||
ready={!loading}
|
||||
sortType={sortType}
|
||||
jobId={selectJob?.id}
|
||||
cityCode={selectJob?.cityCode ?? location.cityCode}
|
||||
latitude={coordinate.latitude}
|
||||
longitude={coordinate.longitude}
|
||||
className={LIST_CONTAINER_CLASS}
|
||||
/>
|
||||
<Overlay
|
||||
visible={showFilter}
|
||||
onClickOuter={handleHideFilter}
|
||||
outerClassName={`${PREFIX}__overlay-outer`}
|
||||
innerClassName={`${PREFIX}__overlay-inner`}
|
||||
>
|
||||
<AnchorPicker value={filters} onConfirm={handleFilterChange} />
|
||||
</Overlay>
|
||||
</div>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
5
src/pages/certification-manage/index.config.ts
Normal file
5
src/pages/certification-manage/index.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '通告管理',
|
||||
disableScroll: true,
|
||||
usingComponents: {},
|
||||
});
|
||||
60
src/pages/certification-manage/index.less
Normal file
60
src/pages/certification-manage/index.less
Normal file
@ -0,0 +1,60 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-certification-manage {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
.flex-column();
|
||||
|
||||
&__tabs {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
.taroify-tabs__wrap__scroll {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.taroify-tabs__tab {
|
||||
--tab-color: @blColorG2;
|
||||
--tabs-active-color: @blColor;
|
||||
}
|
||||
|
||||
.taroify-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty-tips {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding-top: 218px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__icon {
|
||||
width: 386px;
|
||||
height: 278px;
|
||||
}
|
||||
|
||||
&__describe {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
color: @blColor;
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
.button(@width: calc(100% - 48px); @height: 80px);
|
||||
margin: 0 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
130
src/pages/certification-manage/index.tsx
Normal file
130
src/pages/certification-manage/index.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import { Button, Image } from '@tarojs/components';
|
||||
import { NodesRef } from '@tarojs/taro';
|
||||
|
||||
import { Tabs } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import JobManageList, { IJobManageListProps } from '@/components/job-manage-list';
|
||||
import { CompanyPublishJobDialog } from '@/components/product-dialog/publish-job';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { ReportEventId } from '@/constants/event';
|
||||
import { JOB_MANAGE_TABS, JobManageStatus, JobManageType } from '@/constants/job';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { reportEvent } from '@/utils/event';
|
||||
import { ensureUserInfo } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-certification-manage';
|
||||
const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`;
|
||||
const BUTTON_CLASS = `${PREFIX}__button`;
|
||||
const SAFE_BOTTOM_PADDING_CLASS = `${PREFIX}__sbpc`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [
|
||||
`.${PREFIX}`,
|
||||
`.${PREFIX} .taroify-tabs__wrap__scroll`,
|
||||
`.${BUTTON_CLASS}`,
|
||||
`.${SAFE_BOTTOM_PADDING_CLASS}`,
|
||||
],
|
||||
calc: (
|
||||
rects: [
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
]
|
||||
) => {
|
||||
const [page, tabs, button, safePadding] = rects;
|
||||
return page.height - tabs.height - button.height - safePadding.height - 10;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const tab2Status = (tabType: JobManageType) => {
|
||||
switch (tabType) {
|
||||
case JobManageType.Open:
|
||||
return JobManageStatus.Open;
|
||||
case JobManageType.Pending:
|
||||
return JobManageStatus.Pending;
|
||||
case JobManageType.Error:
|
||||
return JobManageStatus.Error;
|
||||
case JobManageType.All:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const EmptyTips = (props: { className?: string; height?: number }) => {
|
||||
const { className, height } = props;
|
||||
return (
|
||||
<div className={classNames(`${PREFIX}__empty-tips`, className)} style={height ? { height } : undefined}>
|
||||
<Image className={`${PREFIX}__empty-tips__icon`} src={require('@/statics/svg/empty-box.svg')} mode="aspectFit" />
|
||||
<div className={`${PREFIX}__empty-tips__describe`}>当前还没有通告</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function ListWrapper(props: IJobManageListProps) {
|
||||
const { className, listHeight, visible } = props;
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
|
||||
const handleListEmpty = useCallback(() => {
|
||||
setIsEmpty(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setIsEmpty(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
if (isEmpty) {
|
||||
return <EmptyTips className={className} height={listHeight} />;
|
||||
}
|
||||
|
||||
return <JobManageList {...props} onListEmpty={handleListEmpty} />;
|
||||
}
|
||||
|
||||
export default function CertificationManage() {
|
||||
const userInfo = useUserInfo();
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [tabType, setTabType] = useState<JobManageType>(JobManageType.All);
|
||||
const [showPublish, setShowPublish] = useState(false);
|
||||
|
||||
const handleTypeChange = useCallback(value => setTabType(value), []);
|
||||
|
||||
const handlePublishJob = useCallback(async () => {
|
||||
log('handlePublishJob');
|
||||
reportEvent(ReportEventId.CLICK_GO_TO_PUBLISH_JOB);
|
||||
if (!(await ensureUserInfo(userInfo))) {
|
||||
return;
|
||||
}
|
||||
setShowPublish(true);
|
||||
}, [userInfo]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<Tabs swipeable value={tabType} className={`${PREFIX}__tabs`} onChange={handleTypeChange}>
|
||||
{JOB_MANAGE_TABS.map(tab => (
|
||||
<Tabs.TabPane value={tab.type} title={tab.title} key={tab.type}>
|
||||
<ListWrapper
|
||||
status={tab2Status(tab.type)}
|
||||
listHeight={listHeight}
|
||||
className={LIST_CONTAINER_CLASS}
|
||||
visible={tabType === tab.type}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={BUTTON_CLASS} onClick={handlePublishJob}>
|
||||
创建新通告
|
||||
</Button>
|
||||
</div>
|
||||
<SafeBottomPadding className={SAFE_BOTTOM_PADDING_CLASS} />
|
||||
<div>{showPublish && <CompanyPublishJobDialog userInfo={userInfo} onClose={() => setShowPublish(false)} />}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/certification-start/index.config.ts
Normal file
3
src/pages/certification-start/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '认证',
|
||||
});
|
||||
47
src/pages/certification-start/index.less
Normal file
47
src/pages/certification-start/index.less
Normal file
@ -0,0 +1,47 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-certification-start {
|
||||
.flex-column();
|
||||
padding: 0 24px;
|
||||
|
||||
&__icon {
|
||||
width: 224px;
|
||||
height: 224px;
|
||||
border-radius: 50%;
|
||||
background: #FFF;
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
&__tips {
|
||||
position: relative;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 24px;
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 5px;
|
||||
background: #6D3DF54D;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
.button(@width: 100%; @height: 80px);
|
||||
margin-top: 48px;
|
||||
}
|
||||
}
|
||||
33
src/pages/certification-start/index.tsx
Normal file
33
src/pages/certification-start/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import LoginButton from '@/components/login-button';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { ReportEventId } from '@/constants/event';
|
||||
import { reportEvent } from '@/utils/event';
|
||||
import { redirectTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-certification-start';
|
||||
|
||||
export default function CertificationStart() {
|
||||
const handleClick = () => {
|
||||
reportEvent(ReportEventId.CLICK_START_CERTIFICATION);
|
||||
redirectTo(PageUrl.Certification);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__icon`}
|
||||
src={require('@/statics/svg/certification-tips-icon.svg')}
|
||||
/>
|
||||
<div className={`${PREFIX}__title`}>为了保障信息真实性,请进行实人认证,完成后即可</div>
|
||||
<div className={`${PREFIX}__tips`}>发布认证通告,优先展示</div>
|
||||
<LoginButton className={`${PREFIX}__button`} onClick={handleClick} needPhone>
|
||||
开始认证
|
||||
</LoginButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/certification/index.config.ts
Normal file
3
src/pages/certification/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '个人认证',
|
||||
});
|
||||
79
src/pages/certification/index.less
Normal file
79
src/pages/certification/index.less
Normal file
@ -0,0 +1,79 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-certification {
|
||||
padding: 40px 24px;
|
||||
|
||||
&__id-card-container {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__id-card {
|
||||
flex: 1;
|
||||
height: 178px;
|
||||
.flex-column();
|
||||
justify-content: center;
|
||||
background: #F2F2F2;
|
||||
border-radius: 12px;
|
||||
margin: 24px 0;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
&__describe {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__verify {
|
||||
.flex-column();
|
||||
|
||||
&__input {
|
||||
flex: 1;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
&__code-container {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__send {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
.button(@width: 100%; @height: 80px);
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
}
|
||||
252
src/pages/certification/index.tsx
Normal file
252
src/pages/certification/index.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import { BaseEventOrig, Button, Image, InputProps } from '@tarojs/components';
|
||||
import Taro, { UploadTask } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import BlFormInput from '@/components/bl-form-input';
|
||||
import BlFormItem from '@/components/bl-form-item';
|
||||
import LoadingDialog from '@/components/loading-dialog';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { CertificationStatusType } from '@/constants/company';
|
||||
import { CollectEventName, ReportEventId } from '@/constants/event';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { ICertificationRequest } from '@/types/company';
|
||||
import { isValidIdCard, isValidPhone, logWithPrefix } from '@/utils/common';
|
||||
import { postCertification } from '@/utils/company';
|
||||
import { collectEvent, reportEvent } from '@/utils/event';
|
||||
import { chooseMedia } from '@/utils/material';
|
||||
import { redirectTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
import { dispatchUpdateUser, requestUserInfo } from '@/utils/user';
|
||||
import { uploadVideo } from '@/utils/video';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-certification';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const needIdCard = false;
|
||||
|
||||
const isValidCertificationInfo = (data: ICertificationRequest) => {
|
||||
const {
|
||||
name,
|
||||
// code,
|
||||
phone,
|
||||
idCardNo: idNumber,
|
||||
companyName: company,
|
||||
// idCardSideAUrl: leftIdCardUrl,
|
||||
// idCardSideBUrl: rightIdCardUrl,
|
||||
} = data;
|
||||
// if (!leftIdCardUrl || !rightIdCardUrl) {
|
||||
// return '请上传身份证照片';
|
||||
// }
|
||||
if (!name) {
|
||||
return '请输入姓名';
|
||||
}
|
||||
if (!idNumber || !isValidIdCard(idNumber)) {
|
||||
return '请输入正确的身份证';
|
||||
}
|
||||
if (!phone || !isValidPhone(phone)) {
|
||||
return '请输入正确的手机号';
|
||||
}
|
||||
// if (!code) {
|
||||
// return '验证码不能为空';
|
||||
// }
|
||||
if (!company) {
|
||||
return '请输入公司名称';
|
||||
}
|
||||
};
|
||||
|
||||
const uploadIdCard = async () => {
|
||||
let showLoading = false;
|
||||
try {
|
||||
const media = await chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
});
|
||||
if (!media) {
|
||||
return;
|
||||
}
|
||||
const { tempFiles } = media;
|
||||
const tempFile = tempFiles[0];
|
||||
if (!tempFile) {
|
||||
throw new Error('tempFile is not exist');
|
||||
}
|
||||
showLoading = true;
|
||||
Taro.showLoading({ title: '上传0%' });
|
||||
const onProgress: UploadTask.OnProgressUpdateCallback = res => {
|
||||
log('上传视频进度', res.progress, '总长度', res.totalBytesExpectedToSend, '已上传的长度', res.totalBytesSent);
|
||||
Taro.showLoading({ title: `上传${res.progress}%` });
|
||||
};
|
||||
const { url } = await uploadVideo(tempFile.tempFilePath, tempFile.fileType, onProgress, 'id-card');
|
||||
return url;
|
||||
} catch (e) {
|
||||
console.error('upload fail', e);
|
||||
Toast.error('上传失败');
|
||||
collectEvent(CollectEventName.UPDATE_ID_CARD_FAILED, e);
|
||||
} finally {
|
||||
showLoading && Taro.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
export default function Certification() {
|
||||
const { phone } = useUserInfo();
|
||||
const [leftIdCardUrl, setLeftIdCardUrl] = useState('');
|
||||
const [rightIdCardUrl, setRightIdCardUrl] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [idNumber, setIdNumber] = useState('');
|
||||
// const [code, setCode] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClickIdCardLeft = useCallback(async () => {
|
||||
reportEvent(ReportEventId.CLICK_UPLOAD_ID_CARD, { type: 'left' });
|
||||
const url = await uploadIdCard();
|
||||
url && setLeftIdCardUrl(url);
|
||||
}, []);
|
||||
|
||||
const handleClickIdCardRight = useCallback(async () => {
|
||||
reportEvent(ReportEventId.CLICK_START_CERTIFICATION, { type: 'right' });
|
||||
const url = await uploadIdCard();
|
||||
url && setRightIdCardUrl(url);
|
||||
}, []);
|
||||
|
||||
const handleInputName = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setName(value);
|
||||
}, []);
|
||||
|
||||
const handleInputIdNumber = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setIdNumber(value);
|
||||
}, []);
|
||||
|
||||
// const handleInputCode = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
// const value = e.detail.value || '';
|
||||
// setCode(value);
|
||||
// }, []);
|
||||
|
||||
const handleInputCompany = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setCompany(value);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
reportEvent(ReportEventId.CLICK_CERTIFICATION_SUBMIT);
|
||||
const data: ICertificationRequest = {
|
||||
name,
|
||||
// code,
|
||||
phone,
|
||||
idCardNo: idNumber,
|
||||
companyName: company,
|
||||
// idCardSideAUrl: leftIdCardUrl,
|
||||
// idCardSideBUrl: rightIdCardUrl,
|
||||
};
|
||||
const errMsg = isValidCertificationInfo(data);
|
||||
if (errMsg) {
|
||||
Toast.info(errMsg);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setOpen(true);
|
||||
const { authSuc, msg } = await postCertification(data);
|
||||
if (!authSuc) {
|
||||
Toast.info(msg || '认证失败');
|
||||
return;
|
||||
}
|
||||
dispatchUpdateUser({ bossAuthStatus: CertificationStatusType.Success });
|
||||
redirectTo(PageUrl.CertificationManage);
|
||||
} catch (e) {
|
||||
console.error('submit error', e);
|
||||
Toast.error('认证失败请重试');
|
||||
collectEvent(CollectEventName.SUBMIT_CERTIFICATION_FAILED, e);
|
||||
} finally {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [name, idNumber, phone, company]);
|
||||
// }, [leftIdCardUrl, rightIdCardUrl, name, idNumber, phone, company]);
|
||||
|
||||
useEffect(() => {
|
||||
if (phone) {
|
||||
return;
|
||||
}
|
||||
const requestPhone = async () => {
|
||||
collectEvent(CollectEventName.CERTIFICATION_PAGE, { info: 'start requestPhone' });
|
||||
const userInfo = await requestUserInfo();
|
||||
collectEvent(CollectEventName.CERTIFICATION_PAGE, { info: 'requestPhone success', phone: userInfo.phone });
|
||||
};
|
||||
requestPhone();
|
||||
}, [phone]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{needIdCard && (
|
||||
<BlFormItem title="上传身份证照片" subTitle={false} dynamicHeight>
|
||||
<div className={`${PREFIX}__id-card-container`}>
|
||||
<div className={`${PREFIX}__id-card`} onClick={handleClickIdCardLeft}>
|
||||
{leftIdCardUrl && <Image mode="aspectFit" className={`${PREFIX}__id-card__image`} src={leftIdCardUrl} />}
|
||||
{!leftIdCardUrl && (
|
||||
<>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__id-card__icon`}
|
||||
src={require('@/statics/svg/upload-id-card-icon.svg')}
|
||||
/>
|
||||
<div className={`${PREFIX}__id-card__describe`}>身份证人像面</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__id-card`} onClick={handleClickIdCardRight}>
|
||||
{rightIdCardUrl && (
|
||||
<Image mode="aspectFit" className={`${PREFIX}__id-card__image`} src={rightIdCardUrl} />
|
||||
)}
|
||||
{!rightIdCardUrl && (
|
||||
<>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__id-card__icon`}
|
||||
src={require('@/statics/svg/upload-id-card-icon.svg')}
|
||||
/>
|
||||
<div className={`${PREFIX}__id-card__describe`}>身份证国徽面</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BlFormItem>
|
||||
)}
|
||||
<BlFormItem title="姓名" subTitle={false}>
|
||||
<BlFormInput value={name} onInput={handleInputName} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="身份证号" subTitle={false}>
|
||||
<BlFormInput value={idNumber} onInput={handleInputIdNumber} type="idcard" maxlength={18} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="手机号" subTitle="请使用本人名下的手机号" contentClassName={`${PREFIX}__verify`} dynamicHeight>
|
||||
<BlFormInput className={`${PREFIX}__verify__input`} value={phone} type="number" maxlength={11} disabled />
|
||||
{/* <div className={`${PREFIX}__verify__code-container`}>
|
||||
<BlFormInput
|
||||
className={`${PREFIX}__verify__input`}
|
||||
value={code}
|
||||
onInput={handleInputCode}
|
||||
type="number"
|
||||
maxlength={8}
|
||||
/>
|
||||
<div className={`${PREFIX}__verify__send`}>获取验证码</div>
|
||||
</div> */}
|
||||
</BlFormItem>
|
||||
<BlFormItem title="公司全称" subTitle={false}>
|
||||
<BlFormInput maxlength={200} value={company} onInput={handleInputCompany} />
|
||||
</BlFormItem>
|
||||
<SafeBottomPadding />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__submit`} onClick={handleSubmit}>
|
||||
提交
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
<div>
|
||||
<LoadingDialog open={open} text="认证中" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/dev-debug/index.config.ts
Normal file
3
src/pages/dev-debug/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: 'debug',
|
||||
});
|
||||
6
src/pages/dev-debug/index.less
Normal file
6
src/pages/dev-debug/index.less
Normal file
@ -0,0 +1,6 @@
|
||||
.dev-debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
172
src/pages/dev-debug/index.tsx
Normal file
172
src/pages/dev-debug/index.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { Button } from '@taroify/core';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import HomePage from '@/components/home-page';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { copy, logWithPrefix } from '@/utils/common';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'dev-debug';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const marginTopStyle = { marginTop: 20 };
|
||||
|
||||
export default function DevDebug() {
|
||||
// const [lastOrderNo, setLastOrderNo] = useState<string>('');
|
||||
const handleLogin = useCallback(() => {
|
||||
Taro.login({
|
||||
success: res => {
|
||||
log('login code:', res.code);
|
||||
copy(res.code);
|
||||
},
|
||||
fail: e => log('login fail', e),
|
||||
});
|
||||
}, []);
|
||||
|
||||
// const handleGetPhoneNumber = useCallback((e: BaseEventOrig<ButtonProps.onGetPhoneNumberEventDetail>) => {
|
||||
// log(
|
||||
// 'handleGetPhoneNumber',
|
||||
// `code: ${e.detail.code}`,
|
||||
// `\niv: ${e.detail.iv}`,
|
||||
// `\nencryptedData:${e.detail.encryptedData}`
|
||||
// );
|
||||
// const encryptedData = e.detail.encryptedData;
|
||||
// const iv = e.detail.iv;
|
||||
// if (!encryptedData || !iv) {
|
||||
// Toast.error('取消授权');
|
||||
// return;
|
||||
// }
|
||||
// Taro.setClipboardData({
|
||||
// data: `code: ${e.detail.code}, iv: ${e.detail.iv}, encryptedData:${e.detail.encryptedData}`,
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// const handleCopyAndContact = async () => {
|
||||
// openCustomerServiceChat();
|
||||
// await copy('测试复制');
|
||||
// };
|
||||
|
||||
// const handleGetLocationInfo = () => {
|
||||
// requestLocation();
|
||||
// };
|
||||
|
||||
// const handleCreateOrder = async (type: OrderType) => {
|
||||
// try {
|
||||
// const { payOrderNo, createPayInfo: payInfo } = await requestCreatePayInfo({
|
||||
// type,
|
||||
// amt: 1,
|
||||
// productCode: type === OrderType.Group ? ProductType.AddGroup : ProductType.BossVip,
|
||||
// productSpecId: type === OrderType.Group ? ProductSpecId.AddGroup1 : ProductSpecId.BossVip,
|
||||
// });
|
||||
// log('handlePay data', payOrderNo, payInfo);
|
||||
// const res = await Taro.requestPayment({
|
||||
// timeStamp: payInfo.timeStamp,
|
||||
// nonceStr: payInfo.nonceStr,
|
||||
// package: payInfo.packageVal,
|
||||
// signType: payInfo.signType,
|
||||
// paySign: payInfo.paySign,
|
||||
// });
|
||||
// setLastOrderNo(payOrderNo);
|
||||
// log('handleBuy requestPayment res', res);
|
||||
// } catch (e) {
|
||||
// Toast.error('出错了,请重试');
|
||||
// log('handleBuy error', e);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleGetOrder = async () => {
|
||||
// if (!lastOrderNo) {
|
||||
// return;
|
||||
// }
|
||||
// try {
|
||||
// const result = await requestOrderInfo({
|
||||
// payOrderNo: lastOrderNo,
|
||||
// });
|
||||
// Taro.showToast({ title: JSON.stringify(result), icon: 'none' });
|
||||
// log('handleGetOrder data', result);
|
||||
// } catch (e) {
|
||||
// Toast.error('出错了,请重试');
|
||||
// log('handleGetOrder error', e);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleJumpPage = () => navigateTo(PageUrl.CertificationStart);
|
||||
|
||||
// const handleSubscribeJob = async () => {
|
||||
// const result = await subscribeMessage([SubscribeTempId.SUBSCRIBE_JOB]);
|
||||
// log('handleSubscribeMessage result', result);
|
||||
// Toast.info(`订阅结果: ${result[SubscribeTempId.SUBSCRIBE_JOB]}`);
|
||||
// };
|
||||
|
||||
// const handleSubscribeVip = async () => {
|
||||
// const result = await subscribeMessage([SubscribeTempId.SUBSCRIBE_VIP]);
|
||||
// log('handleSubscribeMessage result', result);
|
||||
// Toast.info(`订阅结果: ${result[SubscribeTempId.SUBSCRIBE_VIP]}`);
|
||||
// };
|
||||
|
||||
// const handleQiniu = async () => {
|
||||
// try {
|
||||
// const media = await chooseMedia();
|
||||
// if (!media) {
|
||||
// return;
|
||||
// }
|
||||
// const { tempFiles } = media;
|
||||
// const tempFile = tempFiles[0];
|
||||
// if (!tempFile) {
|
||||
// throw new Error('tempFile is not exist');
|
||||
// }
|
||||
// const onProgress: UploadTask.OnProgressUpdateCallback = res => {
|
||||
// log('上传视频进度', res.progress, '总长度', res.totalBytesExpectedToSend, '已上传的长度', res.totalBytesSent);
|
||||
// Taro.showLoading({ title: `上传${res.progress}%` });
|
||||
// };
|
||||
// await qiniuUpload.upload(tempFile.tempFilePath, onProgress);
|
||||
// } catch (e) {
|
||||
// console.error('upload fail', e);
|
||||
// Toast.error('上传失败');
|
||||
// } finally {
|
||||
// Taro.hideLoading();
|
||||
// }
|
||||
// };
|
||||
|
||||
useLoad(() => {
|
||||
console.log('Page loaded.');
|
||||
});
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
<div className={PREFIX}>
|
||||
{/* <div>{`最近一次的订单 ID: ${lastOrderNo}`}</div>
|
||||
<Button onClick={() => handleCreateOrder(OrderType.Group)} style={marginTopStyle} color="primary">
|
||||
下单群
|
||||
</Button>
|
||||
<Button onClick={() => handleCreateOrder(OrderType.BossVip)} style={marginTopStyle} color="primary">
|
||||
下单 BossVip
|
||||
</Button>
|
||||
<Button onClick={handleGetOrder} style={marginTopStyle} color="primary">
|
||||
查询最近一次订单
|
||||
</Button> */}
|
||||
{/* <Button onClick={() => navigateTo(PageUrl.JobPublish)} style={marginTopStyle} color="primary">
|
||||
跳转到通告发布
|
||||
</Button> */}
|
||||
<Button onClick={handleJumpPage} style={marginTopStyle} color="primary">
|
||||
跳转到开发页面
|
||||
</Button>
|
||||
{/* <Button onClick={handleSubscribeJob} style={marginTopStyle} color="primary">
|
||||
订阅通告推送
|
||||
</Button>
|
||||
<Button onClick={handleSubscribeVip} style={marginTopStyle} color="primary">
|
||||
订阅主播会员推送
|
||||
</Button> */}
|
||||
{/* <Button onClick={handleQiniu} style={marginTopStyle} color="primary">
|
||||
上传到七牛
|
||||
</Button> */}
|
||||
<Button onClick={handleLogin} style={marginTopStyle} color="primary">
|
||||
获取登录 code
|
||||
</Button>
|
||||
</div>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
3
src/pages/follow-group/index.config.ts
Normal file
3
src/pages/follow-group/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我关注群的通告',
|
||||
});
|
||||
29
src/pages/follow-group/index.less
Normal file
29
src/pages/follow-group/index.less
Normal file
@ -0,0 +1,29 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.follow-group {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
padding: 0 20px 20px;
|
||||
|
||||
&__empty-container {
|
||||
width: 100%;
|
||||
padding-top: 338px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__empty-box {
|
||||
width: 386px;
|
||||
height: 278px;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 34px;
|
||||
}
|
||||
}
|
||||
60
src/pages/follow-group/index.tsx
Normal file
60
src/pages/follow-group/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
import { NodesRef } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import JobList from '@/components/job-list';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'follow-group';
|
||||
const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`;
|
||||
const SAFE_PADDING_BOTTOM_CLASS = `${PREFIX}__safe-padding-bottom`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${PREFIX}`, `.${SAFE_PADDING_BOTTOM_CLASS}`],
|
||||
calc: (rects: [NodesRef.BoundingClientRectCallbackResult, NodesRef.BoundingClientRectCallbackResult]) => {
|
||||
const [pageRect, safePaddingRect] = rects;
|
||||
return pageRect.height - safePaddingRect.height;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
const NoFollowTips = () => {
|
||||
return (
|
||||
<div className={`${PREFIX}__empty-container`}>
|
||||
<Image className={`${PREFIX}__empty-box`} src={require('@/statics/svg/empty-box.svg')} mode="aspectFit" />
|
||||
<div className={`${PREFIX}__empty-text`}>暂无关注的群</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function FollowGroup() {
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
log('list height', listHeight);
|
||||
|
||||
const handleListEmpty = useCallback(() => {
|
||||
setIsEmpty(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{!isEmpty && (
|
||||
<JobList
|
||||
visible
|
||||
isFollow
|
||||
className={LIST_CONTAINER_CLASS}
|
||||
listHeight={listHeight}
|
||||
onListEmpty={handleListEmpty}
|
||||
/>
|
||||
)}
|
||||
{isEmpty && <NoFollowTips />}
|
||||
<SafeBottomPadding className={SAFE_PADDING_BOTTOM_CLASS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FollowGroup;
|
||||
4
src/pages/group-job/index.config.ts
Normal file
4
src/pages/group-job/index.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
disableScroll: true,
|
||||
});
|
||||
42
src/pages/group-job/index.less
Normal file
42
src/pages/group-job/index.less
Normal file
@ -0,0 +1,42 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-group-job {
|
||||
height: 100vh;
|
||||
padding: 0 24px;
|
||||
|
||||
.page-group-job__type-tabs {
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
.taroify-tabs__wrap {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.taroify-tabs__wrap__scroll {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.taroify-tabs__tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto !important;
|
||||
font-size: 28px;
|
||||
--tab-color: @blColorG1;
|
||||
--tabs-active-color: @blColor;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.taroify-tabs__line {
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.taroify-tabs__content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/pages/group-job/index.tsx
Normal file
95
src/pages/group-job/index.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import Taro, { NodesRef, useLoad } from '@tarojs/taro';
|
||||
|
||||
import { Tabs } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import JobList from '@/components/job-list';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import SearchInput from '@/components/search';
|
||||
import { JOB_TABS, JobType } from '@/constants/job';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { getPageQuery } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-group-job';
|
||||
const LIST_CLASS = `${PREFIX}__list-container`;
|
||||
const SAFE_PADDING_BOTTOM_CLASS = `${PREFIX}__safe-padding-bottom`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${LIST_CLASS}`, `.${PREFIX}`, `.${SAFE_PADDING_BOTTOM_CLASS}`],
|
||||
calc: (
|
||||
rects: [
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
]
|
||||
) => {
|
||||
const [listRect, pageRect, safePaddingRect] = rects;
|
||||
return pageRect.bottom - listRect.top - safePaddingRect.height;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function GroupJob() {
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [tabType, setTabType] = useState<JobType>(JobType.All);
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [keyWord, setKeyWord] = useState<string>('');
|
||||
const [groupId, setGroupId] = useState<string | undefined>();
|
||||
|
||||
const handleClickSearch = useCallback(() => {
|
||||
if (value === keyWord) {
|
||||
return;
|
||||
}
|
||||
setKeyWord(value);
|
||||
}, [value, keyWord]);
|
||||
|
||||
const handleSearchClear = useCallback(() => {
|
||||
setValue('');
|
||||
setKeyWord('');
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = useCallback(e => setValue(e.detail.value), []);
|
||||
|
||||
const onTypeChange = useCallback(type => setTabType(type), [setTabType]);
|
||||
|
||||
useLoad(() => {
|
||||
const query = getPageQuery<{ groupId: string; title: string }>();
|
||||
log('query', query);
|
||||
const title = query.title || '群通告';
|
||||
Taro.setNavigationBarTitle({ title });
|
||||
setGroupId(query.groupId);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<SearchInput
|
||||
value={value}
|
||||
placeholder="搜索通告"
|
||||
className={`${PREFIX}__search`}
|
||||
onClear={handleSearchClear}
|
||||
onSearch={handleClickSearch}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<Tabs className={`${PREFIX}__type-tabs`} value={tabType} onChange={onTypeChange}>
|
||||
{JOB_TABS.map(tab => (
|
||||
<Tabs.TabPane title={tab.title} key={tab.type} value={tab.type}>
|
||||
{!groupId && <div className={LIST_CLASS} />}
|
||||
{groupId && (
|
||||
<JobList
|
||||
category={tab.type}
|
||||
keyWord={keyWord}
|
||||
blGroupId={groupId}
|
||||
className={LIST_CLASS}
|
||||
listHeight={listHeight}
|
||||
visible={tabType === tab.type}
|
||||
/>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<SafeBottomPadding className={SAFE_PADDING_BOTTOM_CLASS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/group-list/index.config.ts
Normal file
3
src/pages/group-list/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '合作群列表',
|
||||
});
|
||||
24
src/pages/group-list/index.less
Normal file
24
src/pages/group-list/index.less
Normal file
@ -0,0 +1,24 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-group-list {
|
||||
padding: 24px;
|
||||
|
||||
&__group-card {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
font-size: 32px;
|
||||
line-height: 150px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 40px;
|
||||
margin-top: 24px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/pages/group-list/index.tsx
Normal file
49
src/pages/group-list/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { SimpleGroupInfo } from '@/types/group';
|
||||
import { requestSimpleGroupList } from '@/utils/group';
|
||||
import { getPageQuery } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-group-list';
|
||||
|
||||
export default function GroupList() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groupList, setGroupList] = useState<SimpleGroupInfo[]>([]);
|
||||
|
||||
useLoad(async () => {
|
||||
try {
|
||||
const query = getPageQuery<{ city: string }>();
|
||||
const { city: cityCode } = query;
|
||||
if (!cityCode) {
|
||||
return;
|
||||
}
|
||||
const groups = await requestSimpleGroupList(cityCode);
|
||||
setLoading(false);
|
||||
setGroupList(groups);
|
||||
} catch (e) {
|
||||
Toast.error('加载失败请重试');
|
||||
}
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{groupList.map(group => (
|
||||
<div className={`${PREFIX}__group-card`} key={group.blGroupId}>
|
||||
{group.imGroupNick}
|
||||
</div>
|
||||
))}
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
src/pages/group-v2/index.config.ts
Normal file
6
src/pages/group-v2/index.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '通告群',
|
||||
disableScroll: true,
|
||||
enableShareAppMessage: true,
|
||||
usingComponents: {},
|
||||
});
|
||||
78
src/pages/group-v2/index.less
Normal file
78
src/pages/group-v2/index.less
Normal file
@ -0,0 +1,78 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.group-v2-page {
|
||||
padding: 24px;
|
||||
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
&__left-line,
|
||||
&__right-line {
|
||||
width: 88px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&__left-line {
|
||||
background: linear-gradient(270deg, #CCCCCC -0.05%, rgba(204, 204, 204, 0) 99.95%);
|
||||
}
|
||||
|
||||
&__right-line {
|
||||
background: linear-gradient(90deg, #CCCCCC 0%, rgba(204, 204, 204, 0) 100%);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__group-card {
|
||||
.flex-row();
|
||||
width: 100%;
|
||||
padding: 32px;
|
||||
background: #FFF;
|
||||
border-radius: 16px;
|
||||
margin-top: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
&__avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 6px;
|
||||
border: 4px solid #D9D9D9;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
align-self: flex-start;
|
||||
margin-left: 36px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @fontWeight: 500);
|
||||
}
|
||||
}
|
||||
|
||||
&__bottom-padding {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
90
src/pages/group-v2/index.tsx
Normal file
90
src/pages/group-v2/index.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
import { NodesRef, useShareAppMessage } from '@tarojs/taro';
|
||||
|
||||
import { List } from '@taroify/core';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import HomePage from '@/components/home-page';
|
||||
import LoginButton from '@/components/login-button';
|
||||
import { APP_TAB_BAR_ID } from '@/constants/app';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import { openCustomerServiceChat } from '@/utils/common';
|
||||
import { getCommonShareMessage } from '@/utils/share';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface GroupItem {
|
||||
title: string;
|
||||
serviceUrl: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'group-v2-page';
|
||||
const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${LIST_CONTAINER_CLASS}`, `#${APP_TAB_BAR_ID}`],
|
||||
calc: (rects: [NodesRef.BoundingClientRectCallbackResult, NodesRef.BoundingClientRectCallbackResult]) => {
|
||||
const [rect, diffRect] = rects;
|
||||
return diffRect.top - rect.top;
|
||||
},
|
||||
};
|
||||
const GROUPS: GroupItem[] = [
|
||||
{ title: '【广州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb4b88b8abb7a7c8b' },
|
||||
{ title: '【深圳】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfe70d8736e14bb64' },
|
||||
{ title: '【佛山】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfac1132df386fac8' },
|
||||
{ title: '【东莞】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb2b0e39026f7dddc' },
|
||||
{ title: '【杭州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc223f495e159af95e' },
|
||||
{ title: '【温州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb0ea5f197a18b335' },
|
||||
{ title: '【上海】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4189e68429cf07f8' },
|
||||
{ title: '【厦门】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc2007a895cb48464b' },
|
||||
{ title: '【福州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc126483dedadde82b' },
|
||||
{ title: '【泉州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4c8c42b1a9337aaf' },
|
||||
{ title: '【长沙】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc76be8f2b3f8aa437' },
|
||||
{ title: '【成都】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf75cefbdc62946fa' },
|
||||
{ title: '【重庆】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd7008f747d545f83' },
|
||||
{ title: '【郑州】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd1c53b7bf8ecdb97' },
|
||||
{ title: '【西安】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc34768971b7354220' },
|
||||
{ title: '【武汉】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc047c94f8c709b395' },
|
||||
{ title: '【南京】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc6dc8d0a9692b70e' },
|
||||
{ title: '【合肥】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfc41c9785cc2035277' },
|
||||
{ title: '【北京】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb119c94575e91262' },
|
||||
{ title: '【青岛】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfce8d7a68190f6a1d2' },
|
||||
{ title: '【其他】', serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc60ac7b6420787a8' },
|
||||
];
|
||||
|
||||
export default function GroupV2() {
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
|
||||
const handleClick = useCallback((group: GroupItem) => openCustomerServiceChat(group.serviceUrl), []);
|
||||
|
||||
useShareAppMessage(() => getCommonShareMessage());
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header__left-line`} />
|
||||
<div className={`${PREFIX}__header__title`}>进本地通告群,高薪通告早知道</div>
|
||||
<div className={`${PREFIX}__header__right-line`} />
|
||||
</div>
|
||||
<div className={LIST_CONTAINER_CLASS}>
|
||||
<List style={{ height: `${listHeight}px` }} disabled fixedHeight>
|
||||
{GROUPS.map(group => (
|
||||
<div className={`${PREFIX}__group-card`} key={group.serviceUrl}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__group-card__avatar`}
|
||||
src="https://neighbourhood.cn/addGroup.jpg"
|
||||
/>
|
||||
<div className={`${PREFIX}__group-card__title`}>{group.title}</div>
|
||||
<LoginButton className={`${PREFIX}__group-card__button`} onClick={() => handleClick(group)}>
|
||||
我要进群
|
||||
</LoginButton>
|
||||
</div>
|
||||
))}
|
||||
<div className={`${PREFIX}__bottom-padding`} />
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
7
src/pages/group/index.config.ts
Normal file
7
src/pages/group/index.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '',
|
||||
disableScroll: true,
|
||||
enableShareAppMessage: true,
|
||||
usingComponents: {},
|
||||
});
|
||||
27
src/pages/group/index.less
Normal file
27
src/pages/group/index.less
Normal file
@ -0,0 +1,27 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.group-page {
|
||||
&__tabs {
|
||||
.taroify-tabs__wrap__scroll {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.taroify-tabs__tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto !important;
|
||||
font-size: 40px;
|
||||
--tab-color: @blColorG2;
|
||||
--tabs-active-color: @blColor;
|
||||
}
|
||||
|
||||
.taroify-tabs__line {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-top: -5px;
|
||||
height: 8px;
|
||||
background-color: @blTabLineColor;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/pages/group/index.tsx
Normal file
41
src/pages/group/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useShareAppMessage } from '@tarojs/taro';
|
||||
|
||||
import { Tabs } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import HomePage from '@/components/home-page';
|
||||
import { GroupType, GROUP_PAGE_TABS } from '@/constants/group';
|
||||
import GroupFragment from '@/fragments/group';
|
||||
import useNavigation from '@/hooks/use-navigation';
|
||||
import { getCommonShareMessage } from '@/utils/share';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'group-page';
|
||||
|
||||
export default function Group() {
|
||||
const { barHeight, statusBarHeight } = useNavigation();
|
||||
const [tabType, setTabType] = useState<GroupType>(GroupType.All);
|
||||
|
||||
const handleTypeChange = useCallback(value => setTabType(value), []);
|
||||
|
||||
useShareAppMessage(() => getCommonShareMessage());
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
<Tabs
|
||||
swipeable
|
||||
value={tabType}
|
||||
className={`${PREFIX}__tabs`}
|
||||
onChange={handleTypeChange}
|
||||
style={{ height: barHeight.current, paddingTop: statusBarHeight.current }}
|
||||
>
|
||||
{GROUP_PAGE_TABS.map(tab => (
|
||||
<Tabs.TabPane value={tab.type} title={tab.title} key={tab.type}>
|
||||
<GroupFragment type={tab.type} />
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
4
src/pages/job-detail/index.config.ts
Normal file
4
src/pages/job-detail/index.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
enableShareAppMessage: true,
|
||||
});
|
||||
274
src/pages/job-detail/index.less
Normal file
274
src/pages/job-detail/index.less
Normal file
@ -0,0 +1,274 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-detail {
|
||||
&__container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
&__header-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
&__header-title {
|
||||
font-size: 36px;
|
||||
line-height: 48px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
display: -webkit-box;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
&__employ-type {
|
||||
width: fit-content;
|
||||
padding: 3px 6px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background: @blHighlightBg;
|
||||
color: @blHighlightColor;
|
||||
white-space: nowrap;
|
||||
margin-top: 6px;
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
&__salary {
|
||||
font-size: 36px;
|
||||
font: 500;
|
||||
color: @blHighlightColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__update-time {
|
||||
margin: 16px 0;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__tips {
|
||||
padding: 18px 32px;
|
||||
font-size: 26px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
color: #946724;
|
||||
background: #FFF4F0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&__group,
|
||||
&__publisher,
|
||||
&__company {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin-top: 16px;
|
||||
.flex-row();
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__group-name,
|
||||
&__publisher-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
&__group__not-follow {
|
||||
height: 36px;
|
||||
padding: 0 12rpx;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
border: 2px solid @blHighlightColor;
|
||||
border-radius: 32px;
|
||||
margin-left: 14px;
|
||||
|
||||
&::after {
|
||||
border-color: transparent
|
||||
}
|
||||
}
|
||||
|
||||
&__group__followed {
|
||||
height: 36px;
|
||||
padding: 0 12rpx;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
border-radius: 4px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
&__certification-type {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__publisher-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
&__content-title {
|
||||
font-size: 36px;
|
||||
line-height: 48px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 23px;
|
||||
// 抵消最后一行的 margin-bottom
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
padding: 3px 6px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background: #F2F2F2;
|
||||
white-space: nowrap;
|
||||
color: @blColorG2;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 28px;
|
||||
line-height: 56px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__address-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&__detailed-address {
|
||||
font-size: 28px;
|
||||
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;
|
||||
}
|
||||
|
||||
&__distance-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&__distance {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__map__wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__map__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&__bottom-space {
|
||||
// 按钮的高度 + 底部边距
|
||||
height: calc(88px + 26px);
|
||||
padding-bottom: calc(constant(safe-area-inset-bottom) + 12px);
|
||||
/* 兼容 iOS < 11.2 */
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 12px);
|
||||
/* 兼容 iOS >= 11.2 */
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
padding: 12px 32px;
|
||||
padding-bottom: calc(constant(safe-area-inset-bottom) + 12px);
|
||||
/* 兼容 iOS < 11.2 */
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 12px);
|
||||
/* 兼容 iOS >= 11.2 */
|
||||
box-shadow: 0px -4px 20px 0px #00000014;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__share-button {
|
||||
flex: 1 1;
|
||||
.button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px; @highlight: 0);
|
||||
}
|
||||
|
||||
&__contact-publisher {
|
||||
flex: 2 2;
|
||||
.button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;);
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
}
|
||||
330
src/pages/job-detail/index.tsx
Normal file
330
src/pages/job-detail/index.tsx
Normal file
@ -0,0 +1,330 @@
|
||||
import { Map, MapProps, Text, Image, Button } from '@tarojs/components';
|
||||
import Taro, { useLoad, useShareAppMessage } from '@tarojs/taro';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { CertificationStatusIcon } from '@/components/certification-status';
|
||||
import CommonDialog from '@/components/common-dialog';
|
||||
import DevDiv from '@/components/dev-div';
|
||||
import JobRecommendList from '@/components/job-recommend-list';
|
||||
import LoginButton from '@/components/login-button';
|
||||
import MaterialGuide from '@/components/material-guide';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import ProductJobDialog from '@/components/product-dialog/job';
|
||||
import { RoleType, EventName, PageUrl } from '@/constants/app';
|
||||
import { CertificationStatusType } from '@/constants/company';
|
||||
import { CollectEventName, ReportEventId } from '@/constants/event';
|
||||
import { EMPLOY_TYPE_TITLE_MAP } from '@/constants/job';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import useRoleType from '@/hooks/user-role-type';
|
||||
import { RESPONSE_ERROR_CODE } from '@/http/constant';
|
||||
import { HttpError } from '@/http/error';
|
||||
import { JobDetails } from '@/types/job';
|
||||
import { IMaterialMessage } from '@/types/message';
|
||||
import { copy, logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent, reportEvent } from '@/utils/event';
|
||||
import { getJobTitle, getJobSalary, postPublishJob, requestJobDetail } from '@/utils/job';
|
||||
import { calcDistance, isValidLocation } from '@/utils/location';
|
||||
import { requestProfileDetail } from '@/utils/material';
|
||||
import { isChatWithSelf, postCreateChat } from '@/utils/message';
|
||||
import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route';
|
||||
import { getCommonShareMessage } from '@/utils/share';
|
||||
import { formatDate } from '@/utils/time';
|
||||
import Toast from '@/utils/toast';
|
||||
import { isNeedCreateMaterial } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'job-detail';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
const getMapCallout = (data: JobDetails): MapProps.callout | undefined => {
|
||||
if (!data.jobLocation?.address) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
display: 'ALWAYS',
|
||||
content: data.jobLocation.address,
|
||||
color: '#000000',
|
||||
bgColor: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
anchorX: 0,
|
||||
anchorY: 0,
|
||||
borderRadius: 4,
|
||||
borderWidth: 0,
|
||||
borderColor: '#FFFFFF',
|
||||
padding: 4,
|
||||
};
|
||||
};
|
||||
|
||||
const AnchorFooter = (props: { data: JobDetails }) => {
|
||||
const { data } = props;
|
||||
const [errorTips, setErrorTips] = useState<string>('');
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const [showMaterialGuide, setShowMaterialGuide] = useState(false);
|
||||
|
||||
const handleClickContact = useCallback(async () => {
|
||||
log('handleClickContact');
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
reportEvent(ReportEventId.CLICK_JOB_CONTACT);
|
||||
try {
|
||||
const needCreateMaterial = await isNeedCreateMaterial();
|
||||
if (needCreateMaterial) {
|
||||
setShowMaterialGuide(true);
|
||||
return;
|
||||
}
|
||||
if (data.isAuthed) {
|
||||
const toUserId = data.userId;
|
||||
if (isChatWithSelf(toUserId)) {
|
||||
Toast.error('不能与自己聊天');
|
||||
return;
|
||||
}
|
||||
const profile = await requestProfileDetail();
|
||||
const chat = await postCreateChat(toUserId);
|
||||
const materialMessage: IMaterialMessage = {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
age: profile.age,
|
||||
height: profile.height,
|
||||
weight: profile.weight,
|
||||
shoeSize: profile.shoeSize,
|
||||
gender: profile.gender,
|
||||
workedSecCategoryStr: profile.workedSecCategoryStr,
|
||||
};
|
||||
navigateTo(PageUrl.MessageChat, { chatId: chat.chatId, material: materialMessage, jobId: data.id });
|
||||
} else {
|
||||
setDialogVisible(true);
|
||||
}
|
||||
} catch (error) {
|
||||
const e = error as HttpError;
|
||||
const errorCode = e.errorCode;
|
||||
if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE) {
|
||||
setErrorTips('今日开聊次数已用完,请明日再来');
|
||||
} else {
|
||||
Toast.error('请求失败请重试');
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleDialogHidden = useCallback(() => setDialogVisible(false), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__share-button`} openType="share">
|
||||
分享
|
||||
</Button>
|
||||
<LoginButton className={`${PREFIX}__contact-publisher`} onClick={handleClickContact}>
|
||||
{data.isAuthed ? '在线沟通' : '立即联系'}
|
||||
</LoginButton>
|
||||
</div>
|
||||
<div>
|
||||
{dialogVisible && <ProductJobDialog data={data} onClose={handleDialogHidden} />}
|
||||
{showMaterialGuide && <MaterialGuide onClose={() => setShowMaterialGuide(false)} />}
|
||||
<CommonDialog
|
||||
content={errorTips}
|
||||
confirm="确定"
|
||||
visible={!!errorTips}
|
||||
onClose={() => setErrorTips('')}
|
||||
onClick={() => setErrorTips('')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CompanyFooter = (props: { data: JobDetails }) => {
|
||||
const { data } = props;
|
||||
|
||||
const handleClickEdit = useCallback(() => navigateTo(PageUrl.JobPublish, { jobId: data.id }), [data]);
|
||||
|
||||
const handlePublishJob = useCallback(async () => {
|
||||
try {
|
||||
Taro.showLoading();
|
||||
await postPublishJob(data.id);
|
||||
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
|
||||
Toast.success('发布成功');
|
||||
Taro.hideLoading();
|
||||
} catch (error) {
|
||||
Taro.hideLoading();
|
||||
const e = error as HttpError;
|
||||
const errorCode = e.errorCode;
|
||||
collectEvent(CollectEventName.PUBLISH_OPEN_JOB_FAILED, { jobId: data.id, error: e.info?.() || e.message });
|
||||
if (errorCode === RESPONSE_ERROR_CODE.BOSS_VIP_EXPIRED) {
|
||||
Toast.info('该通告已到期,请创建新通告', 3000);
|
||||
} else {
|
||||
Toast.error(e.message || '发布失败请重试', 3000);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__share-button`} onClick={handleClickEdit}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button className={`${PREFIX}__contact-publisher`} onClick={handlePublishJob}>
|
||||
发布通告
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function JobDetail() {
|
||||
const roleType = useRoleType();
|
||||
const userInfo = useUserInfo();
|
||||
const [data, setData] = useState<JobDetails | null>(null);
|
||||
const isOwner = roleType === RoleType.Company && userInfo.userId === data?.userId;
|
||||
|
||||
const onDev = useCallback(async () => data && copy(data.id), [data]);
|
||||
|
||||
const handleClickMap = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!data?.jobLocation) {
|
||||
return;
|
||||
}
|
||||
Taro.openLocation({
|
||||
longitude: Number(data.jobLocation.longitude),
|
||||
latitude: Number(data.jobLocation.latitude),
|
||||
address: data.jobLocation.address,
|
||||
});
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async (jobId: string) => {
|
||||
try {
|
||||
const res = await requestJobDetail(jobId);
|
||||
setData(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
Taro.eventCenter.on(EventName.JOB_UPDATE, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.JOB_UPDATE, callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLoad(async () => {
|
||||
const query = getPageQuery<Pick<JobDetails, 'id'>>();
|
||||
const jobId = query?.id;
|
||||
if (!jobId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await requestJobDetail(jobId);
|
||||
setData(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toast.error('出错了,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
if (!data) {
|
||||
return getCommonShareMessage();
|
||||
}
|
||||
return {
|
||||
title: getJobTitle(data) || '',
|
||||
path: getJumpUrl(PageUrl.JobDetail, { id: data.id, share: true }),
|
||||
};
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header-info`}>
|
||||
<DevDiv className={`${PREFIX}__header-title`} OnDev={onDev}>
|
||||
{getJobTitle(data)}
|
||||
</DevDiv>
|
||||
<div className={`${PREFIX}__employ-type`}>{EMPLOY_TYPE_TITLE_MAP[data.employType]}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__salary`}>{getJobSalary(data)}</div>
|
||||
<div className={`${PREFIX}__update-time`}>{`${formatDate(data.updated)}更新`}</div>
|
||||
<div className={`${PREFIX}__tips`}>请注意甄别通告真假,谨防上当</div>
|
||||
<div className={`${PREFIX}__publisher`}>
|
||||
<div className={`${PREFIX}__publisher-name`}>{`发布人:${data.publisher}`}</div>
|
||||
{data.isAuthed && (
|
||||
<CertificationStatusIcon
|
||||
className={`${PREFIX}__certification-type`}
|
||||
status={CertificationStatusType.Success}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__publisher-avatar`}
|
||||
src={data.publisherAvatar || require('@/statics/svg/wechat.svg')}
|
||||
/>
|
||||
</div>
|
||||
{data.companyName && <div className={`${PREFIX}__company`}>{`公司:${data.companyName}`}</div>}
|
||||
</div>
|
||||
|
||||
<div className={`${PREFIX}__content`}>
|
||||
<div className={`${PREFIX}__content-title`}>职位描述</div>
|
||||
<div className={`${PREFIX}__tags`}>
|
||||
{(data.tags || []).map((keyword: string, index) => (
|
||||
<div className={`${PREFIX}__tag`} key={index}>
|
||||
{keyword}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Text className={`${PREFIX}__description`}>{data.sourceText}</Text>
|
||||
<div className={`${PREFIX}__address-wrapper`}>
|
||||
<Image className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||||
<div className={`${PREFIX}__detailed-address`}>{data.jobLocation?.address}</div>
|
||||
</div>
|
||||
{data.distance && (
|
||||
<div className={`${PREFIX}__distance-wrapper`}>
|
||||
<Image className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||||
<div className={`${PREFIX}__distance`}>{calcDistance(data.distance)}</div>
|
||||
</div>
|
||||
)}
|
||||
{isValidLocation(data.jobLocation) && (
|
||||
<div className={`${PREFIX}__map__wrapper`} onClick={handleClickMap}>
|
||||
<Map
|
||||
enableZoom={false}
|
||||
enableScroll={false}
|
||||
className={`${PREFIX}__map`}
|
||||
latitude={Number(data.jobLocation.latitude)}
|
||||
longitude={Number(data.jobLocation.longitude)}
|
||||
markers={[
|
||||
{
|
||||
id: 0,
|
||||
latitude: Number(data.jobLocation.latitude),
|
||||
longitude: Number(data.jobLocation.longitude),
|
||||
callout: getMapCallout(data),
|
||||
iconPath: '',
|
||||
width: 20,
|
||||
height: 36,
|
||||
},
|
||||
]}
|
||||
onError={() => Toast.error('地图加载错误')}
|
||||
/>
|
||||
<div className={`${PREFIX}__map__mask`} onClick={handleClickMap} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isOwner && <JobRecommendList />}
|
||||
<div className={`${PREFIX}__bottom-space`} />
|
||||
</div>
|
||||
{!isOwner && <AnchorFooter data={data} />}
|
||||
{isOwner && <CompanyFooter data={data} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/job-publish-address/index.config.ts
Normal file
3
src/pages/job-publish-address/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '工作地址',
|
||||
});
|
||||
32
src/pages/job-publish-address/index.less
Normal file
32
src/pages/job-publish-address/index.less
Normal file
@ -0,0 +1,32 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-job-publish-address {
|
||||
padding: 24px;
|
||||
|
||||
&__tips {
|
||||
width: 100%;;
|
||||
padding: 16px 32px;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: #946724;
|
||||
background: #FFF4F0;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
.flex-column();
|
||||
|
||||
&__submit {
|
||||
.button(@width: 100%, @height: 80px, @fontSize: 32px, @fontWeight: 400, @borderRadius: 48px);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/pages/job-publish-address/index.tsx
Normal file
57
src/pages/job-publish-address/index.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { BaseEventOrig, Button, InputProps } from '@tarojs/components';
|
||||
import { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import BlFormCell from '@/components/bl-form-cell';
|
||||
import BlFormInput from '@/components/bl-form-input';
|
||||
import BlFormItem from '@/components/bl-form-item';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-job-publish-address';
|
||||
|
||||
export default function JobPublishAddress() {
|
||||
const [city, setCity] = useState('');
|
||||
const [county, setCounty] = useState('');
|
||||
const [detail, setDetail] = useState('');
|
||||
|
||||
const handleClickEditDescribe = useCallback(() => navigateTo(PageUrl.JobPublishDescribe), []);
|
||||
|
||||
const handleClickEditAddress = useCallback(() => navigateTo(PageUrl.JobPublishAddress), []);
|
||||
|
||||
const handleInputAddressDetail = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setDetail(value);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {}, []);
|
||||
|
||||
useLoad(() => {
|
||||
console.log('Page loaded.');
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__tips`}>请提供真实有效地址,若查实造假,将受到处罚</div>
|
||||
<BlFormItem title="城市">
|
||||
<BlFormCell text={city} placeholder="请选择" onClick={handleClickEditDescribe} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="区">
|
||||
<BlFormCell text={county} placeholder="请选择" onClick={handleClickEditAddress} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="区">
|
||||
<BlFormInput value={detail} onInput={handleInputAddressDetail} />
|
||||
</BlFormItem>
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__footer__submit`} onClick={handleSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/job-publish-describe/index.config.ts
Normal file
3
src/pages/job-publish-describe/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '通告描述',
|
||||
});
|
||||
68
src/pages/job-publish-describe/index.less
Normal file
68
src/pages/job-publish-describe/index.less
Normal file
@ -0,0 +1,68 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-job-publish-describe {
|
||||
padding: 24px;
|
||||
|
||||
&__tips {
|
||||
width: 100%;;
|
||||
padding: 16px 32px;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: #946724;
|
||||
background: #FFF4F0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 34px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
&__input-container {
|
||||
.flex-column();
|
||||
padding: 32px;
|
||||
margin-top: 24px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
font-size: 28px;
|
||||
line-height: 56px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__paste-btn {
|
||||
margin-top: 40px;
|
||||
align-self: flex-end;
|
||||
.button(@width: 176px, @height: 56px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 48px);
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
.flex-column();
|
||||
|
||||
&__submit {
|
||||
.button(@width: 100%, @height: 80px, @fontSize: 32px, @fontWeight: 400, @borderRadius: 48px);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/pages/job-publish-describe/index.tsx
Normal file
79
src/pages/job-publish-describe/index.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { BaseEventOrig, Button, Textarea, TextareaProps } from '@tarojs/components';
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { getPageQuery, navigateBack } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-job-publish-describe';
|
||||
|
||||
const TEMPLATE = `直播品类:
|
||||
直播时长:
|
||||
岗位要求:
|
||||
其他说明:`;
|
||||
|
||||
export default function JobPublishDetail() {
|
||||
const [info, setInfo] = useState(TEMPLATE);
|
||||
|
||||
const handleInput = useCallback((e: BaseEventOrig<TextareaProps.onInputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setInfo(value);
|
||||
}, []);
|
||||
|
||||
const handleClickPaste = useCallback(async () => {
|
||||
try {
|
||||
const { data } = await Taro.getClipboardData();
|
||||
data && setInfo(data);
|
||||
} catch (e) {
|
||||
Toast.error('读取剪切板失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
Taro.eventCenter.trigger(EventName.EDIT_JOB_DESCRIBE, info);
|
||||
navigateBack();
|
||||
}, [info]);
|
||||
|
||||
useLoad(() => {
|
||||
const query = getPageQuery<{ describe: string }>();
|
||||
if (!query.describe) {
|
||||
return;
|
||||
}
|
||||
setInfo(query.describe);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__tips`}>
|
||||
请勿填写微信、电话等联系方式及违反劳动法相关内容,否则有可能导致您的账号被封禁
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>请提供以下信息</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Textarea
|
||||
maxlength={-1}
|
||||
value={info}
|
||||
confirmType="return"
|
||||
onInput={handleInput}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholder="介绍工作内容、通告要求、加分项"
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
cursorSpacing={100}
|
||||
/>
|
||||
<Button className={`${PREFIX}__paste-btn`} onClick={handleClickPaste}>
|
||||
粘贴
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__footer__submit`} onClick={handleSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/job-publish/index.config.ts
Normal file
3
src/pages/job-publish/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '发布通告',
|
||||
});
|
||||
60
src/pages/job-publish/index.less
Normal file
60
src/pages/job-publish/index.less
Normal file
@ -0,0 +1,60 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-job-publish {
|
||||
padding: 24px;
|
||||
// 底部高度(paddingTop: 30 + tips: 30 + margin: 40 + button: 80 + margin: 40);
|
||||
padding-bottom: 220px;
|
||||
|
||||
&__reason {
|
||||
width: 100%;;
|
||||
padding: 16px 32px;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: #946724;
|
||||
background: #FFF4F0;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
padding-top: 30px;
|
||||
.flex-column();
|
||||
|
||||
&__tips {
|
||||
font-size: 28px;
|
||||
line-height: 30px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
.button(@width: 100%, @height: 80px, @fontSize: 32px, @fontWeight: 400, @borderRadius: 48px);
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.lowLight {
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
312
src/pages/job-publish/index.tsx
Normal file
312
src/pages/job-publish/index.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
import { BaseEventOrig, Button, InputProps } from '@tarojs/components';
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import BlFormCell from '@/components/bl-form-cell';
|
||||
import BlFormInput from '@/components/bl-form-input';
|
||||
import BlFormItem from '@/components/bl-form-item';
|
||||
import { BlFormRadio, BlFormRadioGroup } from '@/components/bl-form-radio';
|
||||
import BlFormSelect from '@/components/bl-form-select';
|
||||
import BlSalaryInput, { BlSalaryValue } from '@/components/bl-salary-input';
|
||||
import { CityPickerPopup } from '@/components/city-picker';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName, PageUrl } from '@/constants/app';
|
||||
import { CollectEventName } from '@/constants/event';
|
||||
import { EMPLOY_TYPE_TITLE_MAP, EmployType, JOB_TYPE_SELECT_OPTIONS, JobType } from '@/constants/job';
|
||||
import { CreateJobInfo, JobDetails } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent } from '@/utils/event';
|
||||
import { postCloseJob, postCreateJob, postUpdateJob, postPublishJob, requestJobDetail, isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
|
||||
import { getCityValues } from '@/utils/location';
|
||||
import { getPageQuery, navigateBack, navigateTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-job-publish';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
const isInvalidCreateJobInfo = (data: CreateJobInfo) => {
|
||||
const {
|
||||
title,
|
||||
category,
|
||||
employType,
|
||||
provinceCode,
|
||||
cityCode,
|
||||
countyCode,
|
||||
address,
|
||||
jobDescription,
|
||||
lowPriceForFullTime,
|
||||
highPriceForFullTime,
|
||||
lowPriceForPartyTime,
|
||||
highPriceForPartyTime,
|
||||
} = data;
|
||||
if (!category) {
|
||||
return '请选择通告品类';
|
||||
}
|
||||
if (!title) {
|
||||
return '请选择通告标题';
|
||||
}
|
||||
if (!jobDescription) {
|
||||
return '请输入通告描述';
|
||||
}
|
||||
if (isFullTimePriceRequired(employType)) {
|
||||
if (!lowPriceForFullTime || !highPriceForFullTime) {
|
||||
return '薪资范围不能为空';
|
||||
}
|
||||
if (lowPriceForFullTime > highPriceForFullTime) {
|
||||
return '薪资最高值不能低于最低值';
|
||||
}
|
||||
}
|
||||
if (isPartTimePriceRequired(employType)) {
|
||||
if (!lowPriceForPartyTime || !highPriceForPartyTime) {
|
||||
return '薪资范围不能为空';
|
||||
}
|
||||
if (lowPriceForPartyTime > highPriceForPartyTime) {
|
||||
return '薪资最高值不能低于最低值';
|
||||
}
|
||||
}
|
||||
|
||||
if (!provinceCode || !cityCode || !countyCode) {
|
||||
return '请输入工作地址';
|
||||
}
|
||||
if (!address) {
|
||||
return '请输入详细地址';
|
||||
}
|
||||
};
|
||||
|
||||
export default function JobPublish() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isUpdate, setIsUpdate] = useState(false);
|
||||
const [job, setJob] = useState<JobDetails | null>(null);
|
||||
const [reason, setReason] = useState('');
|
||||
const [employType, setEmployType] = useState(EmployType.Full);
|
||||
const [category, setCategory] = useState<JobType>(JobType.Finery);
|
||||
const [title, setTitle] = useState('');
|
||||
const [describe, setDescribe] = useState('');
|
||||
const [salary, setSalary] = useState<BlSalaryValue>(['', '', '', ''] as unknown as BlSalaryValue);
|
||||
const [city, setCity] = useState<string[] | undefined>();
|
||||
const [showCityPicker, setShowCityPicker] = useState(false);
|
||||
const [address, setAddress] = useState('');
|
||||
|
||||
const handleEmployTypeChange = useCallback((value: EmployType) => {
|
||||
setEmployType(value);
|
||||
}, []);
|
||||
|
||||
const handleSelectCategory = useCallback((value: JobType) => {
|
||||
setCategory(value);
|
||||
}, []);
|
||||
|
||||
const handleInputTitle = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setTitle(value);
|
||||
}, []);
|
||||
|
||||
const handleClickEditDescribe = useCallback(() => navigateTo(PageUrl.JobPublishDescribe, { describe }), [describe]);
|
||||
|
||||
const handleSalaryChange = useCallback((value: BlSalaryValue) => {
|
||||
setSalary(value);
|
||||
}, []);
|
||||
|
||||
const handleClickCity = useCallback(() => setShowCityPicker(true), []);
|
||||
|
||||
const handleConfirmCityPicker = useCallback((areaValues: string[]) => {
|
||||
log('handleConfirmCityPicker', areaValues);
|
||||
setShowCityPicker(false);
|
||||
setCity(areaValues);
|
||||
setAddress('');
|
||||
}, []);
|
||||
|
||||
const handleInputAddress = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setAddress(value);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(async () => {
|
||||
if (!job) {
|
||||
Toast.error('数据出错请重试');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Taro.showLoading();
|
||||
await postCloseJob(job.id);
|
||||
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
|
||||
navigateBack();
|
||||
} catch (e) {
|
||||
console.error('submit error', e);
|
||||
Toast.error('关闭失败请重试');
|
||||
collectEvent(CollectEventName.CLOSE_JOB_FAILED, e);
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
}
|
||||
}, [job]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const cityCodes = city || [];
|
||||
const data: CreateJobInfo = {
|
||||
title,
|
||||
employType,
|
||||
category: category!,
|
||||
jobDescription: describe,
|
||||
provinceCode: cityCodes[0],
|
||||
cityCode: cityCodes[1],
|
||||
countyCode: cityCodes[2],
|
||||
lowPriceForFullTime: !isFullTimePriceRequired(employType) ? 0 : salary[0] * 1000,
|
||||
highPriceForFullTime: !isFullTimePriceRequired(employType) ? 0 : salary[1] * 1000,
|
||||
lowPriceForPartyTime: !isPartTimePriceRequired(employType) ? 0 : salary[2],
|
||||
highPriceForPartyTime: !isPartTimePriceRequired(employType) ? 0 : salary[3],
|
||||
address: address,
|
||||
};
|
||||
const errMsg = isInvalidCreateJobInfo(data);
|
||||
if (errMsg) {
|
||||
Toast.info(errMsg);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Taro.showLoading();
|
||||
// 将省市区拼到最前面
|
||||
const cityValues = getCityValues(cityCodes);
|
||||
if (!data.address.startsWith(cityValues)) {
|
||||
data.address = `${cityValues}${data.address}`;
|
||||
}
|
||||
const isUpdateJob = isUpdate && job;
|
||||
if (isUpdateJob) {
|
||||
data.jobId = job!.id;
|
||||
await postUpdateJob(data);
|
||||
Taro.eventCenter.trigger(EventName.JOB_UPDATE, job!.id);
|
||||
} else {
|
||||
await postCreateJob(data);
|
||||
}
|
||||
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
|
||||
await Toast.success(isUpdateJob ? '更新成功' : '创建成功', 1500, true);
|
||||
navigateBack();
|
||||
} catch (e) {
|
||||
console.error('submit error', e);
|
||||
Toast.error('审核失败请重试');
|
||||
collectEvent(CollectEventName.PUBLISH_JOB_FAILED, e);
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
}
|
||||
}, [isUpdate, job, title, employType, category, describe, city, salary, address]);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = (d: string) => setDescribe(d);
|
||||
Taro.eventCenter.on(EventName.EDIT_JOB_DESCRIBE, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.EDIT_JOB_DESCRIBE, callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLoad(async () => {
|
||||
const query = getPageQuery<{ jobId: string }>();
|
||||
const jobId = query?.jobId;
|
||||
if (!jobId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
setIsUpdate(true);
|
||||
const details = await requestJobDetail(jobId);
|
||||
setJob(details);
|
||||
details.title && setTitle(details.title);
|
||||
details.employType && setEmployType(details.employType);
|
||||
details.category && setCategory(details.category);
|
||||
details.jobDescription && setDescribe(details.jobDescription);
|
||||
details.jobLocation.provinceCode &&
|
||||
details.jobLocation.cityCode &&
|
||||
details.jobLocation.countyCode &&
|
||||
setCity([details.jobLocation.provinceCode, details.jobLocation.cityCode, details.jobLocation.countyCode]);
|
||||
details.lowPriceForFullTime &&
|
||||
details.highPriceForFullTime &&
|
||||
details.lowPriceForPartyTime &&
|
||||
details.highPriceForPartyTime &&
|
||||
setSalary([
|
||||
details.lowPriceForFullTime / 1000,
|
||||
details.highPriceForFullTime / 1000,
|
||||
details.lowPriceForPartyTime,
|
||||
details.highPriceForPartyTime,
|
||||
]);
|
||||
details.jobLocation.address && setAddress(details.jobLocation.address);
|
||||
details.verifyFailReason && setReason(details.verifyFailReason);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toast.error('出错了,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{reason && <div className={`${PREFIX}__reason`}>{reason}</div>}
|
||||
<BlFormItem title="通告标题" subTitle="不能填写微信、电话等联系方式">
|
||||
<BlFormInput
|
||||
value={title}
|
||||
maxlength={20}
|
||||
placeholder="如“招聘女装主播”"
|
||||
onInput={handleInputTitle}
|
||||
maxLengthTips
|
||||
/>
|
||||
</BlFormItem>
|
||||
<BlFormItem title="工作类型" subTitle={false}>
|
||||
<BlFormRadioGroup direction="horizontal" value={employType} onChange={handleEmployTypeChange}>
|
||||
<BlFormRadio name={EmployType.Full} text={EMPLOY_TYPE_TITLE_MAP[EmployType.Full]} value={employType} />
|
||||
<BlFormRadio name={EmployType.Part} text={EMPLOY_TYPE_TITLE_MAP[EmployType.Part]} value={employType} />
|
||||
<BlFormRadio name={EmployType.All} text={EMPLOY_TYPE_TITLE_MAP[EmployType.All]} value={employType} />
|
||||
</BlFormRadioGroup>
|
||||
</BlFormItem>
|
||||
<BlFormItem title="品类" subTitle={false}>
|
||||
<BlFormSelect title="品类" value={category} options={JOB_TYPE_SELECT_OPTIONS} onSelect={handleSelectCategory} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="薪资范围" subTitle={false} dynamicHeight>
|
||||
<BlSalaryInput value={salary} employType={employType} onChange={handleSalaryChange} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="通告描述" subTitle={false}>
|
||||
<BlFormCell text={describe} placeholder="介绍工作内容、通告要求、加分项" onClick={handleClickEditDescribe} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="工作城市" subTitle={false}>
|
||||
<BlFormCell text={getCityValues(city, '-')} placeholder="工作所在省-城-区" onClick={handleClickCity} />
|
||||
</BlFormItem>
|
||||
<BlFormItem title="详细地址" subTitle={false}>
|
||||
<BlFormInput value={address} placeholder="请填写详细地址" onInput={handleInputAddress} />
|
||||
</BlFormItem>
|
||||
<SafeBottomPadding />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<div className={`${PREFIX}__footer__tips`}>注:通告标题和工作城市发布后不可修改</div>
|
||||
<div className={`${PREFIX}__footer__buttons`}>
|
||||
{!isUpdate && (
|
||||
<Button className={`${PREFIX}__footer__button`} onClick={handleSubmit}>
|
||||
发布
|
||||
</Button>
|
||||
)}
|
||||
{isUpdate && (
|
||||
<>
|
||||
<Button className={classNames(`${PREFIX}__footer__button`, 'lowLight')} onClick={handleClose}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button className={`${PREFIX}__footer__button`} onClick={handleSubmit}>
|
||||
更新
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
<div>
|
||||
<CityPickerPopup
|
||||
areaValues={city}
|
||||
open={showCityPicker}
|
||||
onConfirm={handleConfirmCityPicker}
|
||||
onCancel={() => setShowCityPicker(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
src/pages/job-search/index.config.ts
Normal file
4
src/pages/job-search/index.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '搜索',
|
||||
disableScroll: true,
|
||||
});
|
||||
137
src/pages/job-search/index.less
Normal file
137
src/pages/job-search/index.less
Normal file
@ -0,0 +1,137 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-search-job {
|
||||
height: 100vh;
|
||||
padding: 0 24px;
|
||||
|
||||
.page-search-job__type-tabs {
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
.taroify-tabs__wrap {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.taroify-tabs__wrap__scroll {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.taroify-tabs__tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto !important;
|
||||
font-size: 28px;
|
||||
--tab-color: @blColorG1;
|
||||
--tabs-active-color: @blColor;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.taroify-tabs__line {
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.taroify-tabs__content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
color: @blColor;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: @blColorG1;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-filter {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&__search-filter-menu {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #FFFFFF;
|
||||
height: 64px;
|
||||
padding: 0 24px;
|
||||
margin-top: 24px;
|
||||
border-radius: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&.show {
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__overlay-outer {
|
||||
top: 180px;
|
||||
}
|
||||
|
||||
&__overlay-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__search-history {
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
&__search-history-title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
&__search-history-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// 24 - 16
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__search-history-item {
|
||||
height: 56px;
|
||||
font-size: 24px;
|
||||
line-height: 56px;
|
||||
color: @blColorG2;
|
||||
border-radius: 36px;
|
||||
padding: 0 24px;
|
||||
margin-right: 12px;
|
||||
margin-top: 16px;
|
||||
border: 2px solid @blBorderColor;
|
||||
}
|
||||
}
|
||||
217
src/pages/job-search/index.tsx
Normal file
217
src/pages/job-search/index.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
import Taro, { NodesRef, useLoad } from '@tarojs/taro';
|
||||
|
||||
import { Tabs } from '@taroify/core';
|
||||
import { ArrowDown, ArrowUp } from '@taroify/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobList from '@/components/job-list';
|
||||
import JobPicker from '@/components/job-picker';
|
||||
import Overlay from '@/components/overlay';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import SearchInput from '@/components/search';
|
||||
import { EventName, OpenSource, PageUrl } from '@/constants/app';
|
||||
import { CacheKey } from '@/constants/cache-key';
|
||||
import { CITY_CODE_TO_NAME_MAP } from '@/constants/city';
|
||||
import { JOB_TABS, JobType, EmployType } from '@/constants/job';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { getPageQuery, navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-search-job';
|
||||
const LIST_CLASS = 'search-job-list-container';
|
||||
const SAFE_PADDING_BOTTOM_CLASS = `${PREFIX}__safe-padding-bottom`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${LIST_CLASS}`, `.${PREFIX}`, `.${SAFE_PADDING_BOTTOM_CLASS}`],
|
||||
calc: (
|
||||
rects: [
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
]
|
||||
) => {
|
||||
const [listRect, pageRect, safePaddingRect] = rects;
|
||||
return pageRect.bottom - listRect.top - safePaddingRect.height;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function JobSearch() {
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [focus, setFocus] = useState(true);
|
||||
const [employType, setEmployType] = useState(EmployType.All);
|
||||
const [tabType, setTabType] = useState<JobType>(JobType.All);
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [keyWord, setKeyWord] = useState<string>('');
|
||||
const [cityCode, setCityCode] = useState<string>('');
|
||||
const [showMenu, setShowMenu] = useState<boolean>(false);
|
||||
const historyRef = useRef<string[]>([]);
|
||||
const showContent = keyWord.length > 0;
|
||||
|
||||
const handleUpdateHistory = useCallback((historyKeyword: string) => {
|
||||
const newHistory = [...new Set([historyKeyword, ...historyRef.current])].slice(0, 20);
|
||||
historyRef.current = newHistory;
|
||||
Taro.setStorageSync(CacheKey.JOB_SEARCH_HISTORY, newHistory);
|
||||
log('handleUpdateHistory', newHistory);
|
||||
}, []);
|
||||
|
||||
const handleClickHistoryItem = useCallback(
|
||||
(historyKeyword: string) => {
|
||||
setValue(historyKeyword);
|
||||
setKeyWord(historyKeyword);
|
||||
handleUpdateHistory(historyKeyword);
|
||||
},
|
||||
[handleUpdateHistory]
|
||||
);
|
||||
|
||||
const handleClickSearch = useCallback(() => {
|
||||
if (value === keyWord) {
|
||||
return;
|
||||
}
|
||||
setFocus(false);
|
||||
setKeyWord(value);
|
||||
handleUpdateHistory(value);
|
||||
}, [value, keyWord, handleUpdateHistory]);
|
||||
|
||||
const handleSearchClear = useCallback(() => {
|
||||
setValue('');
|
||||
setKeyWord('');
|
||||
}, []);
|
||||
|
||||
const handleSearchBlur = useCallback(() => setFocus(false), []);
|
||||
|
||||
const handleSearchFocus = useCallback(() => setFocus(true), []);
|
||||
|
||||
const handleSearchChange = useCallback(e => setValue(e.detail.value), []);
|
||||
|
||||
const handleClickCityMenu = useCallback(() => {
|
||||
navigateTo(PageUrl.CitySearch, { city: cityCode, source: OpenSource.JobSearch });
|
||||
}, [cityCode]);
|
||||
|
||||
const handleClickTypeMenu = useCallback(() => {
|
||||
setShowMenu(!showMenu);
|
||||
}, [showMenu]);
|
||||
|
||||
const handleHideMenu = useCallback(() => setShowMenu(false), []);
|
||||
|
||||
const handleCityChange = useCallback(data => {
|
||||
log('handleCityChange', data);
|
||||
const { openSource, cityCode: code } = data;
|
||||
if (openSource !== OpenSource.JobSearch) {
|
||||
return;
|
||||
}
|
||||
setCityCode(code);
|
||||
}, []);
|
||||
|
||||
const handleTypePickerConfirm = useCallback((newEmployType: EmployType) => {
|
||||
log('picker confirm', newEmployType);
|
||||
setEmployType(newEmployType);
|
||||
setShowMenu(false);
|
||||
}, []);
|
||||
|
||||
const onTypeChange = useCallback(type => setTabType(type), [setTabType]);
|
||||
|
||||
useLoad(() => {
|
||||
const query = getPageQuery<{ city: string }>();
|
||||
const currentCity = query.city;
|
||||
if (!currentCity) {
|
||||
return;
|
||||
}
|
||||
setCityCode(currentCity);
|
||||
});
|
||||
|
||||
useLoad(() => {
|
||||
const cacheHistory = Taro.getStorageSync(CacheKey.JOB_SEARCH_HISTORY);
|
||||
log('useLoad', cacheHistory);
|
||||
if (!cacheHistory) {
|
||||
return;
|
||||
}
|
||||
historyRef.current = cacheHistory;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.SELECT_CITY, handleCityChange);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.SELECT_CITY, handleCityChange);
|
||||
};
|
||||
}, [handleCityChange]);
|
||||
|
||||
const searchAction = (
|
||||
<Button className={`${PREFIX}__search-button`} hoverClass="none" onClick={handleClickSearch}>
|
||||
搜索
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<SearchInput
|
||||
focus={focus}
|
||||
value={value}
|
||||
placeholder="你想搜什么"
|
||||
searchAction={searchAction}
|
||||
className={`${PREFIX}__search`}
|
||||
onClear={handleSearchClear}
|
||||
onBlur={handleSearchBlur}
|
||||
onFocus={handleSearchFocus}
|
||||
onSearch={handleClickSearch}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<div className={`${PREFIX}__page-content`} style={{ visibility: showContent ? 'visible' : 'hidden' }}>
|
||||
<div className={`${PREFIX}__search-filter`}>
|
||||
<div className={classNames(`${PREFIX}__search-filter-menu`)} onClick={handleClickCityMenu}>
|
||||
<div className="title">{CITY_CODE_TO_NAME_MAP.get(cityCode)}</div>
|
||||
<ArrowDown />
|
||||
</div>
|
||||
<div
|
||||
className={classNames(`${PREFIX}__search-filter-menu`, { show: showMenu })}
|
||||
onClick={handleClickTypeMenu}
|
||||
>
|
||||
<div className="title">筛选</div>
|
||||
{showMenu ? <ArrowUp /> : <ArrowDown />}
|
||||
</div>
|
||||
</div>
|
||||
<Overlay
|
||||
visible={showMenu}
|
||||
onClickOuter={handleHideMenu}
|
||||
outerClassName={`${PREFIX}__overlay-outer`}
|
||||
innerClassName={`${PREFIX}__overlay-inner`}
|
||||
>
|
||||
<JobPicker onConfirm={handleTypePickerConfirm} />
|
||||
</Overlay>
|
||||
<Tabs className={`${PREFIX}__type-tabs`} value={tabType} onChange={onTypeChange}>
|
||||
{JOB_TABS.map(tab => (
|
||||
<Tabs.TabPane title={tab.title} key={tab.type} value={tab.type}>
|
||||
<JobList
|
||||
cityCode={cityCode}
|
||||
category={tab.type}
|
||||
employType={employType}
|
||||
keyWord={keyWord}
|
||||
visible={tabType === tab.type}
|
||||
className={LIST_CLASS}
|
||||
listHeight={listHeight}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className={`${PREFIX}__search-history`} style={{ visibility: showContent ? 'hidden' : 'visible' }}>
|
||||
<div className={`${PREFIX}__search-history-title`}>历史搜索</div>
|
||||
<div className={`${PREFIX}__search-history-items`}>
|
||||
{historyRef.current.map(historyKeyword => (
|
||||
<div
|
||||
key={historyKeyword}
|
||||
className={`${PREFIX}__search-history-item`}
|
||||
onClick={() => handleClickHistoryItem(historyKeyword)}
|
||||
>
|
||||
{historyKeyword}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<SafeBottomPadding className={SAFE_PADDING_BOTTOM_CLASS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/job-select-my-publish/index.config.ts
Normal file
3
src/pages/job-select-my-publish/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '选择沟通通告',
|
||||
});
|
||||
62
src/pages/job-select-my-publish/index.less
Normal file
62
src/pages/job-select-my-publish/index.less
Normal file
@ -0,0 +1,62 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-job-select-my-publish {
|
||||
width: 100%;
|
||||
|
||||
&__card {
|
||||
width: 100%;
|
||||
height: 144px;
|
||||
.flex-row();
|
||||
padding: 24px;
|
||||
background: #FFF;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.selected {
|
||||
background: #6D3DF514;
|
||||
}
|
||||
}
|
||||
|
||||
&__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: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin-top: 8px;
|
||||
.noWrap();
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
height: 100%;
|
||||
.flex-column();
|
||||
align-items: flex-end;
|
||||
|
||||
&__time {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/pages/job-select-my-publish/index.tsx
Normal file
74
src/pages/job-select-my-publish/index.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { List } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { EventName, OpenSource } from '@/constants/app';
|
||||
import { JobManageStatus } from '@/constants/job';
|
||||
import { JobManageInfo } from '@/types/job';
|
||||
import { getJobLocation, requestJobManageList } from '@/utils/job';
|
||||
import { getPageQuery, navigateBack } from '@/utils/route';
|
||||
import { formatTime } from '@/utils/time';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-job-select-my-publish';
|
||||
|
||||
export default function JobSelectMyPublish() {
|
||||
const [list, setList] = useState<JobManageInfo[]>([]);
|
||||
const [jobId, setJobId] = useState<string | null>('1');
|
||||
const [source, setSource] = useState<OpenSource>();
|
||||
|
||||
const handleClick = useCallback(
|
||||
(info: JobManageInfo) => {
|
||||
Taro.eventCenter.trigger(EventName.SELECT_MY_PUBLISH_JOB, info, source);
|
||||
navigateBack();
|
||||
},
|
||||
[source]
|
||||
);
|
||||
|
||||
useLoad(async () => {
|
||||
const query = getPageQuery<{ id: string; source: OpenSource }>();
|
||||
query?.id && setJobId(query.id);
|
||||
try {
|
||||
const res = await requestJobManageList({ status: JobManageStatus.Open });
|
||||
setList(res.jobResults);
|
||||
setSource(query.source);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toast.error('出错了,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<List disabled>
|
||||
{list.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleClick(item)}
|
||||
className={classNames(`${PREFIX}__card`, { selected: item.id === jobId })}
|
||||
>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
<div className={`${PREFIX}__info__title`}>{item.title}</div>
|
||||
<div className={`${PREFIX}__info__location`}>{getJobLocation(item)}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__right`}>
|
||||
<div className={`${PREFIX}__right__time`}>{formatTime(item.updated)}</div>
|
||||
{item.id === jobId && (
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__right__icon`}
|
||||
src={require('@/statics/svg/success.svg')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/pages/job/index.config.ts
Normal file
7
src/pages/job/index.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '',
|
||||
disableScroll: true,
|
||||
enableShareAppMessage: true,
|
||||
usingComponents: {},
|
||||
});
|
||||
27
src/pages/job/index.less
Normal file
27
src/pages/job/index.less
Normal file
@ -0,0 +1,27 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job {
|
||||
&__tabs {
|
||||
.taroify-tabs__wrap__scroll {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.taroify-tabs__tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto !important;
|
||||
font-size: 40px;
|
||||
--tab-color: @blColorG2;
|
||||
--tabs-active-color: @blColor;
|
||||
}
|
||||
|
||||
.taroify-tabs__line {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-top: -5px;
|
||||
height: 8px;
|
||||
background-color: @blTabLineColor;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/pages/job/index.tsx
Normal file
159
src/pages/job/index.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import Taro, { useDidShow, useLoad, useShareAppMessage } from '@tarojs/taro';
|
||||
|
||||
import { Tabs } from '@taroify/core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import HomePage from '@/components/home-page';
|
||||
import LocationDialog from '@/components/location-dialog';
|
||||
import { LoginGuide } from '@/components/login-guide';
|
||||
import MaterialGuide from '@/components/material-guide';
|
||||
import { EventName, OpenSource, PageUrl } from '@/constants/app';
|
||||
import { EmployType, JOB_PAGE_TABS, SortType } from '@/constants/job';
|
||||
import JobFragment from '@/fragments/job/base';
|
||||
import useLocation from '@/hooks/use-location';
|
||||
import useNavigation from '@/hooks/use-navigation';
|
||||
import { Coordinate } from '@/types/location';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { getWxLocation, isNotNeedAuthorizeLocation, requestLocation } from '@/utils/location';
|
||||
import { requestUnreadMessageCount } from '@/utils/message';
|
||||
import { getJumpUrl, getPageQuery, navigateTo } from '@/utils/route';
|
||||
import { getCommonShareMessage } from '@/utils/share';
|
||||
import Toast from '@/utils/toast';
|
||||
import { isNeedCreateMaterial } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'job';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function Job() {
|
||||
const location = useLocation();
|
||||
const { barHeight, statusBarHeight } = useNavigation();
|
||||
const [tabType, setTabType] = useState<EmployType>(EmployType.All);
|
||||
const [sortType, setSortType] = useState<SortType>(SortType.RECOMMEND);
|
||||
const [cityCode, setCityCode] = useState<string>(location.cityCode);
|
||||
const [coordinate, setCoordinate] = useState<Coordinate>({
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
});
|
||||
const [showMaterialGuide, setShowMaterialGuide] = useState(false);
|
||||
const [showAuthorize, setShowAuthorize] = useState(false);
|
||||
const cityValuesChangedRef = useRef(false);
|
||||
|
||||
const handleTypeChange = useCallback(value => setTabType(value), []);
|
||||
|
||||
const handleClickCity = useCallback(
|
||||
() => navigateTo(PageUrl.CitySearch, { city: cityCode, source: OpenSource.JobPage }),
|
||||
[cityCode]
|
||||
);
|
||||
|
||||
const handleClickSortType = useCallback(
|
||||
async (type: SortType) => {
|
||||
if (type === SortType.DISTANCE && (!location.latitude || !location.longitude)) {
|
||||
const res = await getWxLocation();
|
||||
if (!res) {
|
||||
Toast.info('获取位置信息失败,请重试');
|
||||
return;
|
||||
}
|
||||
const { latitude, longitude } = res;
|
||||
setCoordinate({ latitude, longitude });
|
||||
}
|
||||
setSortType(type);
|
||||
},
|
||||
[location]
|
||||
);
|
||||
|
||||
const handleCityChange = useCallback(data => {
|
||||
log('handleCityChange', data);
|
||||
const { openSource, cityCode: code } = data;
|
||||
if (openSource !== OpenSource.JobPage) {
|
||||
return;
|
||||
}
|
||||
cityValuesChangedRef.current = true;
|
||||
setCityCode(code);
|
||||
}, []);
|
||||
|
||||
const handleAfterBindPhone = useCallback(async () => {
|
||||
if (await isNeedCreateMaterial()) {
|
||||
setShowMaterialGuide(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseAuthorize = useCallback(() => setShowAuthorize(false), []);
|
||||
|
||||
const handleClickAuthorize = useCallback(() => {
|
||||
requestLocation(true);
|
||||
setShowAuthorize(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.SELECT_CITY, handleCityChange);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.SELECT_CITY, handleCityChange);
|
||||
};
|
||||
}, [handleCityChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cityValuesChangedRef.current) {
|
||||
return;
|
||||
}
|
||||
setCityCode(location.cityCode);
|
||||
}, [location]);
|
||||
|
||||
useLoad(async () => {
|
||||
const query = getPageQuery<{ sortType: SortType }>();
|
||||
const type = query.sortType;
|
||||
if (type === SortType.CREATE_TIME) {
|
||||
setSortType(type);
|
||||
}
|
||||
if (await isNotNeedAuthorizeLocation()) {
|
||||
log('not need authorize location');
|
||||
requestLocation();
|
||||
} else {
|
||||
log('show authorize location dialog');
|
||||
setShowAuthorize(true);
|
||||
}
|
||||
});
|
||||
|
||||
useDidShow(() => requestUnreadMessageCount());
|
||||
|
||||
useShareAppMessage(() => {
|
||||
if (sortType === SortType.CREATE_TIME) {
|
||||
return {
|
||||
title: '这里有今日全城新增通告,快来看看',
|
||||
path: getJumpUrl(PageUrl.Job, { sortType }),
|
||||
};
|
||||
}
|
||||
return getCommonShareMessage();
|
||||
});
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
<Tabs
|
||||
swipeable
|
||||
value={tabType}
|
||||
className={`${PREFIX}__tabs`}
|
||||
onChange={handleTypeChange}
|
||||
style={{ height: barHeight.current, paddingTop: statusBarHeight.current }}
|
||||
>
|
||||
{JOB_PAGE_TABS.map(tab => (
|
||||
<Tabs.TabPane value={tab.type} title={tab.title} key={tab.type}>
|
||||
<JobFragment
|
||||
cityCode={cityCode}
|
||||
sortType={sortType}
|
||||
employType={tab.type}
|
||||
coordinate={coordinate}
|
||||
onClickCity={handleClickCity}
|
||||
onClickSort={handleClickSortType}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<div>
|
||||
<LocationDialog open={showAuthorize} onClose={handleCloseAuthorize} onClick={handleClickAuthorize} />
|
||||
<LoginGuide disabled={showAuthorize} onAfterBind={handleAfterBindPhone} />
|
||||
{showMaterialGuide && <MaterialGuide onClose={() => setShowMaterialGuide(false)} />}
|
||||
</div>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
3
src/pages/material-create-profile/index.config.ts
Normal file
3
src/pages/material-create-profile/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
});
|
||||
20
src/pages/material-create-profile/index.less
Normal file
20
src/pages/material-create-profile/index.less
Normal file
@ -0,0 +1,20 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-material-create-profile {
|
||||
// bottom: 24px + 100px + 20px 内边距+按钮高度+按钮上边距
|
||||
padding: 24px 24px 144px;
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
}
|
||||
|
||||
&__submit-btn {
|
||||
.button(@width: 100%, @height: 80px, @fontSize: 32px, @fontWeight: 400, @borderRadius: 48px);
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
134
src/pages/material-create-profile/index.tsx
Normal file
134
src/pages/material-create-profile/index.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { CollectEventName } from '@/constants/event';
|
||||
import { ProfileGroupType, ProfileTitleMap } from '@/constants/material';
|
||||
import ProfileAdvantagesFragment from '@/fragments/profile/advantages';
|
||||
import ProfileBasicFragment from '@/fragments/profile/basic';
|
||||
import ProfileExperienceFragment from '@/fragments/profile/experience';
|
||||
import ProfileIntentionFragment from '@/fragments/profile/intention';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent } from '@/utils/event';
|
||||
import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
|
||||
import { updateProfile, subscribeMaterialMessage } from '@/utils/material';
|
||||
import { navigateBack } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-material-create-profile';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
const REQUIRE_KEYS = {
|
||||
[ProfileGroupType.Basic]: ['name', 'gender', 'age', 'height', 'weight'],
|
||||
[ProfileGroupType.Intention]: [
|
||||
'cityCodes',
|
||||
'employType',
|
||||
'acceptWorkForSit',
|
||||
'fullTimeMinPrice',
|
||||
'fullTimeMaxPrice',
|
||||
'partyTimeMinPrice',
|
||||
'partyTimeMaxPrice',
|
||||
],
|
||||
[ProfileGroupType.Experience]: ['workedYear'],
|
||||
[ProfileGroupType.Advantages]: [],
|
||||
};
|
||||
|
||||
const CONDITIONAL_REQUIRED_KEYS = {
|
||||
[ProfileGroupType.Intention]: [
|
||||
['fullTimeMinPrice', (data) => isFullTimePriceRequired(data.employType)],
|
||||
['fullTimeMaxPrice', (data) => isFullTimePriceRequired(data.employType)],
|
||||
['partyTimeMinPrice', (data) => isPartTimePriceRequired(data.employType)],
|
||||
['partyTimeMaxPrice', (data) => isPartTimePriceRequired(data.employType)],
|
||||
],
|
||||
}
|
||||
|
||||
const getNextStepGroupType = (curType: ProfileGroupType) => {
|
||||
switch (curType) {
|
||||
case ProfileGroupType.Basic:
|
||||
return ProfileGroupType.Advantages;
|
||||
case ProfileGroupType.Intention:
|
||||
return ProfileGroupType.Experience;
|
||||
case ProfileGroupType.Experience:
|
||||
return ProfileGroupType.Basic;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const isValidFormData = (type: ProfileGroupType, data: Partial<MaterialProfile>) => {
|
||||
const requireKeys = REQUIRE_KEYS[type] || [];
|
||||
const conditionalKeys = CONDITIONAL_REQUIRED_KEYS[type] || []
|
||||
|
||||
const requiredValidator = (key: any) => typeof data[key] !== 'undefined' && data[key] !== ''
|
||||
return requireKeys.every(requiredValidator) && conditionalKeys.every(([key, validator]) => {
|
||||
return !validator(data) || requiredValidator(key)
|
||||
});
|
||||
};
|
||||
|
||||
export default function MaterialCreateProfile() {
|
||||
const [groupType, setGroupType] = useState<ProfileGroupType>(ProfileGroupType.Intention);
|
||||
const ref = useRef<{ getData: () => Partial<MaterialProfile> } | null>(null);
|
||||
const ProfileFragment =
|
||||
groupType === ProfileGroupType.Basic
|
||||
? ProfileBasicFragment
|
||||
: groupType === ProfileGroupType.Intention
|
||||
? ProfileIntentionFragment
|
||||
: groupType === ProfileGroupType.Experience
|
||||
? ProfileExperienceFragment
|
||||
: groupType === ProfileGroupType.Advantages
|
||||
? ProfileAdvantagesFragment
|
||||
: Fragment;
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
const data = ref.current?.getData();
|
||||
log('handleSubmit data:', data);
|
||||
if (!data) {
|
||||
throw new Error('数据异常');
|
||||
}
|
||||
if (!isValidFormData(groupType, data)) {
|
||||
Toast.error('重要选项必填!');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextType = getNextStepGroupType(groupType);
|
||||
log('handleSubmit nextType:', nextType);
|
||||
if (nextType) {
|
||||
await updateProfile(data);
|
||||
} else {
|
||||
// 发起订阅不能在异步任务中,保证是第一个
|
||||
await Promise.all([subscribeMaterialMessage(), updateProfile(data)]);
|
||||
}
|
||||
Taro.eventCenter.trigger(EventName.CREATE_PROFILE);
|
||||
nextType ? setGroupType(nextType) : navigateBack(2);
|
||||
} catch (e) {
|
||||
Toast.error('保存失败请重试');
|
||||
collectEvent(CollectEventName.CREATE_MATERIAL_FAILED, e);
|
||||
}
|
||||
}, [ref, groupType]);
|
||||
|
||||
useEffect(() => {
|
||||
const title = ProfileTitleMap[groupType];
|
||||
Taro.setNavigationBarTitle({ title });
|
||||
}, [groupType]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<ProfileFragment ref={ref} profile={{}} />
|
||||
<SafeBottomPadding />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__submit-btn`} onClick={handleSubmit}>
|
||||
{groupType === ProfileGroupType.Advantages ? '完成' : '下一步'}
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/material-edit-profile/index.config.ts
Normal file
3
src/pages/material-edit-profile/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
});
|
||||
20
src/pages/material-edit-profile/index.less
Normal file
20
src/pages/material-edit-profile/index.less
Normal file
@ -0,0 +1,20 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-material-edit-profile {
|
||||
// bottom: 24px + 100px + 20px 内边距+按钮高度+按钮上边距
|
||||
padding: 24px 24px 144px;
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
}
|
||||
|
||||
&__submit-btn {
|
||||
.button(@width: 100%, @height: 80px, @fontSize: 32px, @fontWeight: 400, @borderRadius: 48px);
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
98
src/pages/material-edit-profile/index.tsx
Normal file
98
src/pages/material-edit-profile/index.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { Fragment, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { CollectEventName } from '@/constants/event';
|
||||
import { ProfileGroupType, ProfileTitleMap } from '@/constants/material';
|
||||
import ProfileAdvantagesFragment from '@/fragments/profile/advantages';
|
||||
import ProfileBasicFragment from '@/fragments/profile/basic';
|
||||
import ProfileExperienceFragment from '@/fragments/profile/experience';
|
||||
import ProfileIntentionFragment from '@/fragments/profile/intention';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent } from '@/utils/event';
|
||||
import { isProfileNotChange, requestProfileDetail, updateProfile } from '@/utils/material';
|
||||
import { getPageQuery, navigateBack } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-material-edit-profile';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function MaterialEdit() {
|
||||
const [profile, setProfile] = useState<MaterialProfile | null>(null);
|
||||
const [groupType, setGroupType] = useState<ProfileGroupType | null>(null);
|
||||
const ref = useRef<{ getData: () => Partial<MaterialProfile> } | null>(null);
|
||||
const ProfileFragment =
|
||||
groupType === ProfileGroupType.Basic
|
||||
? ProfileBasicFragment
|
||||
: groupType === ProfileGroupType.Intention
|
||||
? ProfileIntentionFragment
|
||||
: groupType === ProfileGroupType.Experience
|
||||
? ProfileExperienceFragment
|
||||
: groupType === ProfileGroupType.Advantages
|
||||
? ProfileAdvantagesFragment
|
||||
: Fragment;
|
||||
log('MaterialEdit', groupType, ref);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
const data = ref.current?.getData();
|
||||
if (!data || !profile) {
|
||||
throw new Error('数据异常');
|
||||
}
|
||||
if (isProfileNotChange(profile, data)) {
|
||||
log('profile not change');
|
||||
navigateBack();
|
||||
return;
|
||||
}
|
||||
data.id = profile.id;
|
||||
await updateProfile(data);
|
||||
Taro.eventCenter.trigger(EventName.UPDATE_PROFILE);
|
||||
Toast.success('保存成功');
|
||||
navigateBack();
|
||||
log('handleSubmit', data);
|
||||
} catch (e) {
|
||||
Toast.error('保存失败请重试');
|
||||
collectEvent(CollectEventName.UPDATE_MATERIAL_FAILED, e);
|
||||
}
|
||||
}, [ref, profile]);
|
||||
|
||||
useLoad(async () => {
|
||||
const query = getPageQuery<{ type: ProfileGroupType }>();
|
||||
const type = query.type || ProfileGroupType.Intention;
|
||||
const title = ProfileTitleMap[type];
|
||||
Taro.setNavigationBarTitle({ title });
|
||||
setGroupType(type);
|
||||
|
||||
try {
|
||||
const profileInfo = await requestProfileDetail();
|
||||
setProfile(profileInfo);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toast.error('出错了,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
if (!groupType || !profile) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<ProfileFragment ref={ref} profile={profile} />
|
||||
<SafeBottomPadding />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__submit-btn`} onClick={handleSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/material-profile/index.config.ts
Normal file
3
src/pages/material-profile/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的模卡',
|
||||
});
|
||||
31
src/pages/material-profile/index.less
Normal file
31
src/pages/material-profile/index.less
Normal file
@ -0,0 +1,31 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-material-profile {
|
||||
&__footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
padding: 12px 32px;
|
||||
box-shadow: 0px -4px 20px 0px #00000014;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__buttons {
|
||||
.flex-row();
|
||||
|
||||
&__share {
|
||||
.button(@height: 88px, @fontSize: 32px, @fontWeight: 500, @borderRadius: 48px);
|
||||
flex: 1 1;
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
&__manager {
|
||||
.button(@height: 88px, @fontSize: 32px, @fontWeight: 500, @borderRadius: 48px);
|
||||
flex: 2 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/pages/material-profile/index.tsx
Normal file
104
src/pages/material-profile/index.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
import Taro, { useLoad, useShareAppMessage } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import MaterialManagePopup from '@/components/material-manage-popup';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { CollectEventName } from '@/constants/event';
|
||||
import { MaterialStatus } from '@/constants/material';
|
||||
import ProfileViewFragment from '@/fragments/profile/view';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
import { collectEvent } from '@/utils/event';
|
||||
import { getMaterialShareMessage, requestProfileDetail, updateProfileStatus } from '@/utils/material';
|
||||
import { getCommonShareMessage } from '@/utils/share';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-material-profile';
|
||||
|
||||
export default function MaterialProfilePage() {
|
||||
const [profile, setProfile] = useState<MaterialProfile | null>(null);
|
||||
const [showManage, setShowManage] = useState(false);
|
||||
|
||||
const handleClickManage = useCallback(() => setShowManage(true), []);
|
||||
|
||||
const handleChangeStatus = useCallback(
|
||||
async (newStatus: boolean) => {
|
||||
if (!profile || newStatus === profile.isOpen) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateProfileStatus({ resumeId: profile.id, userOpen: newStatus });
|
||||
profile.isOpen = newStatus;
|
||||
} catch (e) {
|
||||
Toast.error('保存失败请重试');
|
||||
collectEvent(CollectEventName.UPDATE_MATERIAL_FAILED, e);
|
||||
}
|
||||
},
|
||||
[profile]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async () => {
|
||||
try {
|
||||
const profileDetail = await requestProfileDetail();
|
||||
setProfile(profileDetail);
|
||||
} catch (e) {
|
||||
Toast.error('加载失败');
|
||||
}
|
||||
};
|
||||
Taro.eventCenter.on(EventName.CREATE_PROFILE, callback);
|
||||
Taro.eventCenter.on(EventName.UPDATE_PROFILE, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.CREATE_PROFILE, callback);
|
||||
Taro.eventCenter.off(EventName.UPDATE_PROFILE, callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLoad(async () => {
|
||||
try {
|
||||
const profileDetail = await requestProfileDetail();
|
||||
setProfile(profileDetail);
|
||||
} catch (e) {
|
||||
Toast.error('加载失败');
|
||||
}
|
||||
});
|
||||
|
||||
useShareAppMessage(async () => {
|
||||
const shareMessage = await getMaterialShareMessage(profile, false);
|
||||
return shareMessage || getCommonShareMessage(false);
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<ProfileViewFragment profile={profile} editable />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<div className={`${PREFIX}__footer__buttons`}>
|
||||
<Button className={`${PREFIX}__footer__buttons__share`} openType="share">
|
||||
分享
|
||||
</Button>
|
||||
<Button className={`${PREFIX}__footer__buttons__manager`} onClick={handleClickManage}>
|
||||
管理
|
||||
</Button>
|
||||
</div>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
<div>
|
||||
<MaterialManagePopup
|
||||
open={showManage}
|
||||
onSave={handleChangeStatus}
|
||||
onClose={() => setShowManage(false)}
|
||||
value={profile.userOpen ? MaterialStatus.Open : MaterialStatus.Close}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/material-upload-video/index.config.ts
Normal file
3
src/pages/material-upload-video/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '上传录屏',
|
||||
});
|
||||
39
src/pages/material-upload-video/index.less
Normal file
39
src/pages/material-upload-video/index.less
Normal file
@ -0,0 +1,39 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-material-upload-video {
|
||||
// bottom: 40px + 80px + 20px 内边距+按钮高度+按钮上边距
|
||||
padding: 40px 24px 140px;
|
||||
|
||||
&__header-title {
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__header-tips {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__video-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
background: #F5F6FA;
|
||||
}
|
||||
|
||||
&__submit-btn {
|
||||
.button(@width: 100%, @height: 80px, @fontSize: 32px, @fontWeight: 400, @borderRadius: 48px);
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
204
src/pages/material-upload-video/index.tsx
Normal file
204
src/pages/material-upload-video/index.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
import Taro, { UploadTask, useDidHide, useLoad, useUnload } from '@tarojs/taro';
|
||||
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import MaterialVideoCard from '@/components/material-video-card';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName, OpenSource, PageUrl } from '@/constants/app';
|
||||
import { CollectEventName, ReportEventId } from '@/constants/event';
|
||||
import { MaterialVideoInfo } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent, reportEvent } from '@/utils/event';
|
||||
import { chooseMedia, postVideos, requestVideoList } from '@/utils/material';
|
||||
import { getPageQuery, navigateBack, navigateTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
import { uploadVideo } from '@/utils/video';
|
||||
|
||||
import './index.less';
|
||||
|
||||
// 限制 500M
|
||||
const MAX_FILE_SIZE_LIMIT = 1024 * 1024 * 1000;
|
||||
const PREFIX = 'page-material-upload-video';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const TEMP_DATA: MaterialVideoInfo = { url: '', coverUrl: '', title: '', type: 'image', isDefault: false };
|
||||
|
||||
export default function MaterialUploadVideo() {
|
||||
const [source, setSource] = useState(OpenSource.None);
|
||||
const [videoList, setVideoList] = useState<MaterialVideoInfo[]>([]);
|
||||
const saveRef = useRef<(videos: MaterialVideoInfo[]) => Promise<void>>();
|
||||
const lastSaveVideosRef = useRef<MaterialVideoInfo[]>([]);
|
||||
|
||||
const initVideoList = useCallback(async () => {
|
||||
try {
|
||||
const res = await requestVideoList();
|
||||
lastSaveVideosRef.current = res;
|
||||
setVideoList(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toast.error('加载失败请重试');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClickDelete = useCallback(
|
||||
(video: MaterialVideoInfo) => {
|
||||
log('handleClickDelete', video);
|
||||
const newVideoList = videoList.filter(v => v.coverUrl !== video.coverUrl);
|
||||
setVideoList(newVideoList);
|
||||
},
|
||||
[videoList]
|
||||
);
|
||||
|
||||
const handleDefaultChange = useCallback(
|
||||
(video: MaterialVideoInfo) => {
|
||||
log('handleDefaultChange', video);
|
||||
const newVideoList = videoList.map(v => ({ ...v, isDefault: v.coverUrl === video.coverUrl }));
|
||||
setVideoList(newVideoList);
|
||||
},
|
||||
[videoList]
|
||||
);
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
(video: MaterialVideoInfo, newTitle: string) => {
|
||||
// log('handleTitleChange', video, newTitle);
|
||||
const newVideoList = [...videoList];
|
||||
const index = newVideoList.findIndex(v => v.coverUrl === video.coverUrl);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
newVideoList.splice(index, 1, { ...video, title: newTitle });
|
||||
setVideoList(newVideoList);
|
||||
},
|
||||
[videoList]
|
||||
);
|
||||
|
||||
const handleClickUpload = useCallback(async () => {
|
||||
log('click upload');
|
||||
let showLoading = false;
|
||||
try {
|
||||
reportEvent(ReportEventId.CLICK_UPLOAD_VIDEO);
|
||||
const media = await chooseMedia();
|
||||
if (!media) {
|
||||
return;
|
||||
}
|
||||
const { type, tempFiles } = media;
|
||||
log('upload result', type, tempFiles);
|
||||
const tempFile = tempFiles[0];
|
||||
if (!tempFile) {
|
||||
throw new Error('tempFile is not exist');
|
||||
}
|
||||
if (tempFile.size > MAX_FILE_SIZE_LIMIT) {
|
||||
Toast.info('视频超过1000m,请更换', 3000);
|
||||
collectEvent(CollectEventName.VIDEO_EXCEEDING_LIMITS);
|
||||
return;
|
||||
}
|
||||
showLoading = true;
|
||||
Taro.showLoading({ title: '准备上传' });
|
||||
const onProgress: UploadTask.OnProgressUpdateCallback = res => {
|
||||
log('上传视频进度', res.progress, '总长度', res.totalBytesExpectedToSend, '已上传的长度', res.totalBytesSent);
|
||||
Taro.showLoading({ title: `上传${res.progress}%` });
|
||||
};
|
||||
const { url, coverUrl } = await uploadVideo(tempFile.tempFilePath, tempFile.fileType, onProgress);
|
||||
const newVideo: MaterialVideoInfo = {
|
||||
title: '',
|
||||
isDefault: false,
|
||||
url: url,
|
||||
coverUrl: coverUrl,
|
||||
type: tempFile.fileType === 'video' ? 'video' : 'image',
|
||||
};
|
||||
setVideoList([...videoList, newVideo]);
|
||||
} catch (e) {
|
||||
console.error('upload fail', e);
|
||||
Toast.error('上传失败');
|
||||
collectEvent(CollectEventName.UPLOAD_VIDEO_FAILED, e);
|
||||
} finally {
|
||||
showLoading && Taro.hideLoading();
|
||||
}
|
||||
}, [videoList]);
|
||||
|
||||
const handleClickSubmit = useCallback(async () => {
|
||||
log('handleClickSubmit', videoList);
|
||||
reportEvent(ReportEventId.CLICK_SAVE_VIDEOS);
|
||||
if (videoList.length < 2) {
|
||||
Toast.info('请至少上传 2 个录屏');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Taro.showLoading();
|
||||
await saveRef.current?.(videoList);
|
||||
Taro.eventCenter.trigger(EventName.CREATE_PROFILE);
|
||||
if (source === OpenSource.None) {
|
||||
navigateTo(PageUrl.MaterialCreateProfile);
|
||||
} else {
|
||||
navigateBack();
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error('保存失败请重试');
|
||||
collectEvent(CollectEventName.SAVE_VIDEO_LIST_FAILED, e);
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
}
|
||||
}, [videoList, source]);
|
||||
|
||||
useEffect(() => {
|
||||
saveRef.current = async (videos: MaterialVideoInfo[]) => {
|
||||
if (!videos.length) {
|
||||
log('长度为空不保存');
|
||||
return;
|
||||
}
|
||||
if (isEqual(lastSaveVideosRef.current, videos)) {
|
||||
log('没变化不保存');
|
||||
return;
|
||||
}
|
||||
lastSaveVideosRef.current = videos;
|
||||
await postVideos(videos);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLoad(() => {
|
||||
const query = getPageQuery<{ source: OpenSource }>();
|
||||
log('query', query);
|
||||
const { source: openSource } = query;
|
||||
openSource && setSource(openSource);
|
||||
initVideoList();
|
||||
});
|
||||
|
||||
useDidHide(() => {
|
||||
log('didHide', videoList);
|
||||
saveRef.current?.(videoList);
|
||||
});
|
||||
|
||||
useUnload(() => {
|
||||
log('unload', videoList);
|
||||
saveRef.current?.(videoList);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header-title`}>上传录屏更易获得企业青睐</div>
|
||||
<div className={`${PREFIX}__header-tips`}>录屏是企业最关注的资料,建议提供多个风格和品类</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__video-list`}>
|
||||
{videoList.map(video => (
|
||||
<MaterialVideoCard
|
||||
key={video.coverUrl}
|
||||
videoInfo={video}
|
||||
onClickDelete={() => handleClickDelete(video)}
|
||||
onClickSetDefault={() => handleDefaultChange(video)}
|
||||
onTitleChange={(newTitle: string) => handleTitleChange(video, newTitle)}
|
||||
/>
|
||||
))}
|
||||
{videoList.length < 6 && <MaterialVideoCard videoInfo={TEMP_DATA} onClickUpload={handleClickUpload} isTemp />}
|
||||
</div>
|
||||
<SafeBottomPadding />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<Button className={`${PREFIX}__submit-btn`} onClick={handleClickSubmit}>
|
||||
{source === OpenSource.None ? '下一步' : '保存'}
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/material-view/index.config.ts
Normal file
3
src/pages/material-view/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
});
|
||||
39
src/pages/material-view/index.less
Normal file
39
src/pages/material-view/index.less
Normal file
@ -0,0 +1,39 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-material-view {
|
||||
|
||||
&__no-time-tips {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
padding: 12px 32px;
|
||||
box-shadow: 0px -4px 20px 0px #00000014;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__buttons {
|
||||
.flex-row();
|
||||
|
||||
&__share {
|
||||
.button(@height: 88px, @fontSize: 32px, @fontWeight: 500, @borderRadius: 48px);
|
||||
flex: 1 1;
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
|
||||
&__contact {
|
||||
.button(@height: 88px, @fontSize: 32px, @fontWeight: 500, @borderRadius: 48px);
|
||||
flex: 2 2;
|
||||
margin-left: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/pages/material-view/index.tsx
Normal file
259
src/pages/material-view/index.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
import Taro, { useLoad, useShareAppMessage } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import CommonDialog from '@/components/common-dialog';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName, OpenSource, PageUrl } from '@/constants/app';
|
||||
import { CollectEventName } from '@/constants/event';
|
||||
import { MaterialViewSource } from '@/constants/material';
|
||||
import ProfileViewFragment from '@/fragments/profile/view';
|
||||
import { RESPONSE_ERROR_CODE } from '@/http/constant';
|
||||
import { HttpError } from '@/http/error';
|
||||
import { JobManageInfo } from '@/types/job';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
import { IJobMessage } from '@/types/message';
|
||||
import { copy } from '@/utils/common';
|
||||
import { collectEvent } from '@/utils/event';
|
||||
import { requestHasPublishedJob, requestJobDetail } from '@/utils/job';
|
||||
import { getMaterialShareMessage, requestReadProfile, requestShareProfile } from '@/utils/material';
|
||||
import { isChatWithSelf, postCreateChat } from '@/utils/message';
|
||||
import { getPageQuery, navigateBack, navigateTo, redirectTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-material-view';
|
||||
|
||||
interface IViewContext {
|
||||
resumeId: string;
|
||||
source: MaterialViewSource.AnchorList | MaterialViewSource.Chat;
|
||||
jobId?: string;
|
||||
}
|
||||
|
||||
interface IShareContext {
|
||||
resumeId: string;
|
||||
source: MaterialViewSource.Share;
|
||||
shareCode: string;
|
||||
}
|
||||
|
||||
const isShareContext = (context: IViewContext | IShareContext): context is IShareContext => {
|
||||
return !!(context as IShareContext).shareCode;
|
||||
};
|
||||
|
||||
const requestProfile = async (context: IViewContext | IShareContext) => {
|
||||
if (!context.resumeId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
if (isShareContext(context)) {
|
||||
const { resumeId, shareCode } = context;
|
||||
const profileDetail = await requestShareProfile({ resumeId, shareCode });
|
||||
return profileDetail;
|
||||
} else {
|
||||
const { resumeId, jobId } = context;
|
||||
const profileDetail = await requestReadProfile({ resumeId, jobId });
|
||||
return profileDetail;
|
||||
}
|
||||
};
|
||||
|
||||
export default function MaterialViewPage() {
|
||||
const [contactEnable, setContactEnable] = useState(true);
|
||||
const [profile, setProfile] = useState<MaterialProfile | null>(null);
|
||||
const [jobId, setJobId] = useState<string>();
|
||||
const [errorTips, setErrorTips] = useState<string>('');
|
||||
const [publishDialogVisible, setPublishDialogVisible] = useState(false);
|
||||
const [certificationDialogVisible, setCertificationDialogVisible] = useState(false);
|
||||
const [noTimeDialogVisible, setNoTimeDialogVisible] = useState(false);
|
||||
const [noVipLimitVisible, setNoVipLimitVisible] = useState(false);
|
||||
const [vipExpiredVisible, setVipExpiredVisible] = useState(false);
|
||||
|
||||
const onDev = useCallback(async () => profile && copy(profile.userId), [profile]);
|
||||
|
||||
const handleClickContact = useCallback(async () => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (jobId) {
|
||||
const toUserId = profile.userId;
|
||||
if (isChatWithSelf(toUserId)) {
|
||||
Toast.error('不能与自己聊天');
|
||||
return;
|
||||
}
|
||||
const jobDetail = await requestJobDetail(jobId);
|
||||
const chat = await postCreateChat(toUserId);
|
||||
const jobMessage: IJobMessage = {
|
||||
id: jobDetail.id,
|
||||
title: jobDetail.title,
|
||||
employType: jobDetail.employType,
|
||||
salary: jobDetail.salary,
|
||||
lowPriceForFullTime: jobDetail.lowPriceForFullTime,
|
||||
highPriceForFullTime: jobDetail.highPriceForFullTime,
|
||||
lowPriceForPartyTime: jobDetail.lowPriceForPartyTime,
|
||||
highPriceForPartyTime: jobDetail.highPriceForPartyTime,
|
||||
};
|
||||
navigateTo(PageUrl.MessageChat, { chatId: chat.chatId, job: jobMessage, jobId });
|
||||
return;
|
||||
}
|
||||
if (!(await requestHasPublishedJob())) {
|
||||
setPublishDialogVisible(true);
|
||||
return;
|
||||
}
|
||||
navigateTo(PageUrl.JobSelectMyPublish, { source: OpenSource.MaterialViewPage });
|
||||
} catch (error) {
|
||||
const e = error as HttpError;
|
||||
const errorCode = e.errorCode;
|
||||
if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE) {
|
||||
setErrorTips('今日10次开聊次数已用完,请明日再来');
|
||||
} else {
|
||||
Toast.error('请求失败请重试');
|
||||
}
|
||||
}
|
||||
}, [profile, jobId]);
|
||||
|
||||
const handleClickNoViewTimes = useCallback(() => {
|
||||
setNoTimeDialogVisible(false);
|
||||
navigateBack();
|
||||
}, []);
|
||||
|
||||
const handleClickGoPublish = useCallback(() => {
|
||||
setPublishDialogVisible(false);
|
||||
redirectTo(PageUrl.CertificationManage);
|
||||
}, []);
|
||||
|
||||
const handleClickGoCertification = useCallback(() => {
|
||||
setCertificationDialogVisible(false);
|
||||
redirectTo(PageUrl.CertificationStart);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = (select: JobManageInfo, source: OpenSource) =>
|
||||
source === OpenSource.MaterialViewPage && setJobId(select.id);
|
||||
Taro.eventCenter.on(EventName.SELECT_MY_PUBLISH_JOB, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.SELECT_MY_PUBLISH_JOB, callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLoad(async () => {
|
||||
const context = getPageQuery<IViewContext | IShareContext>();
|
||||
try {
|
||||
const profileDetail = await requestProfile(context);
|
||||
setProfile(profileDetail);
|
||||
if (!isShareContext(context)) {
|
||||
setJobId(context.jobId);
|
||||
Taro.eventCenter.trigger(EventName.VIEW_MATERIAL_SUCCESS, profileDetail.id);
|
||||
}
|
||||
if (context.source === MaterialViewSource.Chat) {
|
||||
setContactEnable(false);
|
||||
}
|
||||
Taro.setNavigationBarTitle({ title: profileDetail.name || '主播模卡' });
|
||||
} catch (error) {
|
||||
const e = error as HttpError;
|
||||
const errorCode = e.errorCode;
|
||||
collectEvent(CollectEventName.VIEW_MATERIAL_FAILED, { context, error: e.info?.() || e.message });
|
||||
console.error(e);
|
||||
if (errorCode === RESPONSE_ERROR_CODE.BOSS_NOT_AUTH) {
|
||||
setCertificationDialogVisible(true);
|
||||
} else if (errorCode === RESPONSE_ERROR_CODE.NO_PUBLISHED_JOB) {
|
||||
setPublishDialogVisible(true);
|
||||
} else if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE) {
|
||||
setNoTimeDialogVisible(true);
|
||||
} else if (errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_FREE_BALANCE) {
|
||||
setNoVipLimitVisible(true);
|
||||
} else if (errorCode === RESPONSE_ERROR_CODE.BOSS_VIP_EXPIRED) {
|
||||
setVipExpiredVisible(true);
|
||||
} else {
|
||||
Toast.error(e.message || '加载失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useShareAppMessage(async () => {
|
||||
const shareMessage = await getMaterialShareMessage(profile);
|
||||
return shareMessage as BL.Anything;
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<>
|
||||
<PageLoading />
|
||||
<CommonDialog
|
||||
content="要查看主播详情,请先完成实人认证"
|
||||
confirm="去认证"
|
||||
visible={certificationDialogVisible}
|
||||
onClose={() => setCertificationDialogVisible(false)}
|
||||
onClick={handleClickGoCertification}
|
||||
/>
|
||||
<CommonDialog
|
||||
content="请先发布一个认证通告"
|
||||
confirm="去发布"
|
||||
visible={publishDialogVisible}
|
||||
onClose={() => setPublishDialogVisible(false)}
|
||||
onClick={handleClickGoPublish}
|
||||
/>
|
||||
<CommonDialog
|
||||
content="请先发布一个认证通告"
|
||||
confirm="去发布"
|
||||
visible={noVipLimitVisible}
|
||||
onClose={() => setNoVipLimitVisible(false)}
|
||||
onClick={handleClickGoPublish}
|
||||
>
|
||||
<div className={`${PREFIX}__no-time-tips`}>免费查看模卡详情次数已用完</div>
|
||||
</CommonDialog>
|
||||
<CommonDialog
|
||||
content="请先发布一个认证通告"
|
||||
confirm="去发布"
|
||||
visible={vipExpiredVisible}
|
||||
onClose={() => setVipExpiredVisible(false)}
|
||||
onClick={handleClickGoPublish}
|
||||
/>
|
||||
<CommonDialog
|
||||
content="今日查看模卡详情次数已用完"
|
||||
confirm="确定"
|
||||
visible={noTimeDialogVisible}
|
||||
onClick={handleClickNoViewTimes}
|
||||
>
|
||||
<div className={`${PREFIX}__no-time-tips`}> 当前每个通告单日查看次数为 20 次</div>
|
||||
</CommonDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<ProfileViewFragment profile={profile} editable={false} onDev={onDev} />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<div className={`${PREFIX}__footer__buttons`}>
|
||||
<Button className={`${PREFIX}__footer__buttons__share`} openType="share">
|
||||
分享
|
||||
</Button>
|
||||
{contactEnable && (
|
||||
<Button className={`${PREFIX}__footer__buttons__contact`} onClick={handleClickContact}>
|
||||
立即联系
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
<div>
|
||||
<CommonDialog
|
||||
content="请先发布一个认证通告"
|
||||
confirm="去发布"
|
||||
visible={publishDialogVisible}
|
||||
onClose={() => setPublishDialogVisible(false)}
|
||||
onClick={handleClickGoPublish}
|
||||
/>
|
||||
<CommonDialog
|
||||
content={errorTips}
|
||||
confirm="确定"
|
||||
visible={!!errorTips}
|
||||
onClose={() => setErrorTips('')}
|
||||
onClick={() => setErrorTips('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/material-webview/index.config.ts
Normal file
3
src/pages/material-webview/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '模卡录屏',
|
||||
});
|
||||
0
src/pages/material-webview/index.less
Normal file
0
src/pages/material-webview/index.less
Normal file
26
src/pages/material-webview/index.tsx
Normal file
26
src/pages/material-webview/index.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { WebView } from '@tarojs/components';
|
||||
import { getPageQuery } from '@/utils/route';
|
||||
import React, { useState } from 'react';
|
||||
import { useLoad } from '@tarojs/taro';
|
||||
|
||||
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export default function MaterialWebview() {
|
||||
const [src, setSrc] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
const { source } = getPageQuery<{ source: string }>()
|
||||
setSrc(`https://neighbourhood.cn/material-preview.html?source=${source}`)
|
||||
})
|
||||
|
||||
if (src) {
|
||||
|
||||
return <WebView src={src} onError={() => Toast.error('加载失败请重试')} />;
|
||||
}
|
||||
|
||||
return '加载中...'
|
||||
|
||||
}
|
||||
4
src/pages/message-chat/index.config.ts
Normal file
4
src/pages/message-chat/index.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
disableScroll: true,
|
||||
});
|
||||
123
src/pages/message-chat/index.less
Normal file
123
src/pages/message-chat/index.less
Normal file
@ -0,0 +1,123 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-message-chat {
|
||||
height: 100vh;
|
||||
background: #FFFFFF;
|
||||
|
||||
&__loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: #F5F6FA;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
.flex-row();
|
||||
padding: 0 32px;
|
||||
box-sizing: border-box;
|
||||
background: #FFFFFF;
|
||||
|
||||
&__reject {
|
||||
.button(@height: 72px; @fontSize: 28px; @borderRadius: 48px; @highlight: 0);
|
||||
flex: 1;
|
||||
margin-right: 26px;
|
||||
|
||||
&.highlight {
|
||||
color: @blColorG1;
|
||||
background: #F7F7F7;
|
||||
}
|
||||
}
|
||||
|
||||
&__exchange {
|
||||
.button(@height: 72px; @fontSize: 28px; @borderRadius: 48px;);
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__chat {
|
||||
width: 100%;
|
||||
height: calc(100vh - 120px - 112px - env(safe-area-inset-bottom));
|
||||
background: #F5F6FA;
|
||||
margin-top: 120px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__chat-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__input-container {
|
||||
.flex-row();
|
||||
padding: 20px 32px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
&__expand-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
min-height: 40px;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
padding: 16px;
|
||||
color: @blColor;
|
||||
background: #F7F7F8;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
&__send-button {
|
||||
.button(@width: 120px; @height: 64px; @fontSize: 28px; @borderRadius: 48px;);
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
&__more {
|
||||
.flex-row();
|
||||
padding: 12px 32px 32px;
|
||||
background: #FFFFFF;
|
||||
|
||||
&__item {
|
||||
.flex-column();
|
||||
|
||||
&__icon-wrapper {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
.flex-row();
|
||||
justify-content: center;
|
||||
background: #F3F3F5;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 24px;
|
||||
line-height: 34px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
430
src/pages/message-chat/index.tsx
Normal file
430
src/pages/message-chat/index.tsx
Normal file
@ -0,0 +1,430 @@
|
||||
import { BaseEventOrig, Button, Image, ScrollView, ScrollViewProps, Textarea, TextareaProps } from '@tarojs/components';
|
||||
import Taro, { NodesRef, useDidHide, useDidShow, useLoad, useUnload } from '@tarojs/taro';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
ContactMessage,
|
||||
JobMessage,
|
||||
LocationMessage,
|
||||
MaterialMessage,
|
||||
TextMessage,
|
||||
TimeMessage,
|
||||
} from '@/components/message-chat';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { CollectEventName } from '@/constants/event';
|
||||
import { ChatWatchType, MessageType, PULL_NEW_MESSAGES_TIME } from '@/constants/message';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import { RESPONSE_ERROR_CODE } from '@/http/constant';
|
||||
import { HttpError } from '@/http/error';
|
||||
import {
|
||||
IChatUser,
|
||||
IChatInfo,
|
||||
IChatMessage,
|
||||
IJobMessage,
|
||||
ILocationMessage,
|
||||
IMaterialMessage,
|
||||
IMessageStatus,
|
||||
PostMessageRequest,
|
||||
} from '@/types/message';
|
||||
import { isAnchorMode } from '@/utils/app';
|
||||
import { getScrollItemId, last, logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent } from '@/utils/event';
|
||||
import {
|
||||
isExchangeMessage,
|
||||
isJobMessage,
|
||||
isLocationMessage,
|
||||
isMaterialMessage,
|
||||
isTextMessage,
|
||||
isTimeMessage,
|
||||
openLocationSelect,
|
||||
postAddMessageTimes,
|
||||
postChatRejectWatch,
|
||||
postSendMessage,
|
||||
requestActionDetail,
|
||||
requestChatDetail,
|
||||
requestChatWatch,
|
||||
requestMessageStatusList,
|
||||
requestNewChatMessages,
|
||||
} from '@/utils/message';
|
||||
import { getPageQuery, parseQuery } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
import { getUserId } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-message-chat';
|
||||
const LIST_CONTAINER_CLASS = `${PREFIX}__chat-list`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${LIST_CONTAINER_CLASS}`],
|
||||
calc: (rects: [NodesRef.BoundingClientRectCallbackResult]) => {
|
||||
const [rect] = rects;
|
||||
return rect.height;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const chooseLocation = Taro.requirePlugin('chooseLocation');
|
||||
|
||||
interface ILoadProps {
|
||||
chatId: string;
|
||||
jobId?: string;
|
||||
job?: string;
|
||||
material?: string;
|
||||
}
|
||||
|
||||
const getHeaderLeftButtonText = (job?: IJobMessage, material?: IMaterialMessage) => {
|
||||
if (job) {
|
||||
return '不感兴趣';
|
||||
}
|
||||
if (material) {
|
||||
return '标记为不合适';
|
||||
}
|
||||
return isAnchorMode() ? '不感兴趣' : '标记为不合适';
|
||||
};
|
||||
|
||||
export default function MessageChat() {
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [input, setInput] = useState('');
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [chat, setChat] = useState<IChatInfo | null>(null);
|
||||
const [reject, setReject] = useState<boolean>(false);
|
||||
const [receiver, setReceiver] = useState<IChatUser | null>(null);
|
||||
const [messages, setMessages] = useState<IChatMessage[]>([]);
|
||||
const [messageStatusList, setMessageStatusList] = useState<IMessageStatus[]>([]);
|
||||
const [jobId, setJobId] = useState<string>();
|
||||
const [job, setJob] = useState<IJobMessage>();
|
||||
const [material, setMaterial] = useState<IMaterialMessage>();
|
||||
const [scrollItemId, setScrollItemId] = useState<string>();
|
||||
const scrollToLowerRef = useRef(false);
|
||||
const autoSendRef = useRef({ sendJob: false, sendMaterial: false });
|
||||
const loadMoreRef = useRef(async (chatId: string, currentMessages: IChatMessage[], forceScroll?: boolean) => {
|
||||
try {
|
||||
const lastMsgId = last(currentMessages)?.msgId;
|
||||
const newMessages = await requestNewChatMessages({ chatId: chatId, lastMsgId });
|
||||
log('requestNewChatMessages', newMessages, forceScroll);
|
||||
if (newMessages.length) {
|
||||
setMessages([...currentMessages, ...newMessages]);
|
||||
(forceScroll || scrollToLowerRef.current) && setScrollItemId(getScrollItemId(last(newMessages)?.msgId));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
const handleInput = useCallback((e: BaseEventOrig<TextareaProps.onInputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
setInput(value);
|
||||
}, []);
|
||||
|
||||
const handleClickExpand = useCallback(() => setShowMore(true), []);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(e: BaseEventOrig<ScrollViewProps.onScrollDetail>) => {
|
||||
// log('handleScroll', e);
|
||||
const { scrollTop, scrollHeight } = e.detail;
|
||||
scrollToLowerRef.current = listHeight + scrollTop >= scrollHeight - 40;
|
||||
},
|
||||
[listHeight]
|
||||
);
|
||||
|
||||
const handleClickSendLocation = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
openLocationSelect();
|
||||
}, []);
|
||||
|
||||
const handleClickMoreOuter = () => showMore && setShowMore(false);
|
||||
|
||||
const handleClickContactButton = useCallback(async () => {
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
await loadMoreRef.current(chat.chatId, messages, true);
|
||||
}, [chat, messages]);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (newMessage: Omit<PostMessageRequest, 'chatId' | 'bizId'>) => {
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Taro.showLoading();
|
||||
await postSendMessage({ chatId: chat.chatId, bizId: jobId || chat.lastJobId, ...newMessage });
|
||||
await loadMoreRef.current(chat.chatId, messages, true);
|
||||
Taro.hideLoading();
|
||||
} catch (error) {
|
||||
const e = error as HttpError;
|
||||
const errorCode = e.errorCode;
|
||||
collectEvent(CollectEventName.MESSAGE_DEV_LOG, { action: 'send-message', e, message: newMessage });
|
||||
let tips = '发送失败请重试';
|
||||
let duration = 1500;
|
||||
if (
|
||||
errorCode === RESPONSE_ERROR_CODE.INSUFFICIENT_BALANCE &&
|
||||
newMessage.type === MessageType.RequestCompanyContact
|
||||
) {
|
||||
tips = '今日申请交换联系方式次数已用完,当前每日限制为5次';
|
||||
duration = 3000;
|
||||
}
|
||||
tips.length > 7 ? Toast.info(tips, duration) : Toast.error(tips, duration);
|
||||
}
|
||||
},
|
||||
[chat, jobId, messages]
|
||||
);
|
||||
|
||||
const handleClickReject = useCallback(async () => {
|
||||
if (!chat || !receiver || reject) {
|
||||
return;
|
||||
}
|
||||
const watchType = isAnchorMode() ? ChatWatchType.AnchorReject : ChatWatchType.CompanyReject;
|
||||
await postChatRejectWatch({
|
||||
type: watchType,
|
||||
toUserId: receiver.userId,
|
||||
jobId: jobId || chat.lastJobId,
|
||||
status: false,
|
||||
});
|
||||
setReject(true);
|
||||
}, [jobId, chat, receiver, reject]);
|
||||
|
||||
const handleSendExchangeContact = useCallback(async () => {
|
||||
postAddMessageTimes('click_request_exchange_contact');
|
||||
const type = isAnchorMode() ? MessageType.RequestCompanyContact : MessageType.RequestAnchorContact;
|
||||
handleSendMessage({ type, actionObject: '' });
|
||||
}, [handleSendMessage]);
|
||||
|
||||
const handleSendJobMessage = useCallback(async () => {
|
||||
if (!job || !receiver || autoSendRef.current.sendJob) {
|
||||
return;
|
||||
}
|
||||
const detail = await requestActionDetail({ type: MessageType.Job, bizId: job.id, toUserId: receiver.userId });
|
||||
if (!detail) {
|
||||
handleSendMessage({ type: MessageType.Job, actionObject: JSON.stringify(job) });
|
||||
}
|
||||
autoSendRef.current.sendJob = true;
|
||||
}, [job, receiver, handleSendMessage]);
|
||||
|
||||
const handleSendMaterialMessage = useCallback(async () => {
|
||||
if (!material || !receiver || autoSendRef.current.sendMaterial) {
|
||||
return;
|
||||
}
|
||||
const detail = await requestActionDetail({
|
||||
type: MessageType.Material,
|
||||
bizId: material.id,
|
||||
toUserId: receiver.userId,
|
||||
});
|
||||
if (!detail) {
|
||||
handleSendMessage({ type: MessageType.Material, actionObject: JSON.stringify(material) });
|
||||
}
|
||||
autoSendRef.current.sendMaterial = true;
|
||||
}, [material, receiver, handleSendMessage]);
|
||||
|
||||
const handleSendLocationMessage = useCallback(
|
||||
(location: Omit<ILocationMessage, 'id'>) => {
|
||||
setShowMore(false);
|
||||
handleSendMessage({ type: MessageType.Location, actionObject: JSON.stringify(location) });
|
||||
},
|
||||
[handleSendMessage]
|
||||
);
|
||||
|
||||
const handleSendTextMessage = useCallback(async () => {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
postAddMessageTimes('send_message_button');
|
||||
await handleSendMessage({ type: MessageType.Text, content: input });
|
||||
setInput('');
|
||||
}, [input, handleSendMessage]);
|
||||
|
||||
// useEffect(() => {
|
||||
// loadMoreRef.current = async (chatId: string, currentMessages: IChatMessage[], forceScroll: boolean) => {
|
||||
// try {
|
||||
// const lastMsgId = last(currentMessages)?.msgId;
|
||||
// const newMessages = await requestNewChatMessages({ chatId: chatId, lastMsgId });
|
||||
// log('requestNewChatMessages', newMessages);
|
||||
// if (newMessages.length) {
|
||||
// setMessages([...currentMessages, ...newMessages]);
|
||||
// (forceScroll || scrollToLowerRef.current) && setScrollItemId(getScrollItemId(last(newMessages)?.msgId));
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// }
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
const intervalId = setInterval(async () => {
|
||||
loadMoreRef.current(chat.chatId, messages);
|
||||
const statusList = await requestMessageStatusList(chat.chatId);
|
||||
setMessageStatusList(statusList);
|
||||
}, PULL_NEW_MESSAGES_TIME);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [chat, messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
job && handleSendJobMessage();
|
||||
material && handleSendMaterialMessage();
|
||||
}, [chat, job, material, handleSendJobMessage, handleSendMaterialMessage]);
|
||||
|
||||
useLoad(async () => {
|
||||
const query = getPageQuery<ILoadProps>();
|
||||
const chatId = query.chatId;
|
||||
if (!chatId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const currentUserId = getUserId();
|
||||
const watchType = isAnchorMode() ? ChatWatchType.AnchorReject : ChatWatchType.CompanyReject;
|
||||
const chatDetail = await requestChatDetail(chatId);
|
||||
const toUserInfo = chatDetail.participants.find(u => u.userId !== currentUserId);
|
||||
if (!toUserInfo) {
|
||||
throw new Error('not receiver');
|
||||
}
|
||||
const watchStatus = await requestChatWatch({
|
||||
type: watchType,
|
||||
toUserId: toUserInfo.userId,
|
||||
jobId: query.jobId || chatDetail.lastJobId,
|
||||
});
|
||||
const parseJob = query.job ? parseQuery<IJobMessage>(query.job) : null;
|
||||
const parseMaterial = query.material ? parseQuery<IMaterialMessage>(query.material) : null;
|
||||
// log('requestChatDetail', chatDetail, parseJob, parseMaterial);
|
||||
setChat(chatDetail);
|
||||
setJobId(query.jobId);
|
||||
setMessages(chatDetail.messages);
|
||||
setScrollItemId(getScrollItemId(last(chatDetail.messages)?.msgId));
|
||||
parseJob && setJob(parseJob);
|
||||
parseMaterial && setMaterial(parseMaterial);
|
||||
Taro.setNavigationBarTitle({ title: toUserInfo.nickName });
|
||||
setReceiver(toUserInfo);
|
||||
setReject(!watchStatus);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
collectEvent(CollectEventName.MESSAGE_DEV_LOG, { action: 'init-chat-message', e });
|
||||
Toast.error('加载失败请重试');
|
||||
}
|
||||
});
|
||||
|
||||
useDidShow(() => {
|
||||
const location = chooseLocation?.getLocation() as Omit<ILocationMessage, 'id'>;
|
||||
log('useDidShow', location);
|
||||
if (!location) {
|
||||
return;
|
||||
}
|
||||
// 发送定位消息
|
||||
handleSendLocationMessage(location);
|
||||
chooseLocation?.setLocation(null);
|
||||
});
|
||||
|
||||
useDidHide(() => chooseLocation?.setLocation(null));
|
||||
|
||||
useUnload(() => {
|
||||
chooseLocation?.setLocation(null);
|
||||
Taro.eventCenter.trigger(EventName.EXIT_CHAT_PAGE);
|
||||
});
|
||||
|
||||
log('render', scrollItemId, scrollToLowerRef.current);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{!chat && <PageLoading className={`${PREFIX}__loading`} />}
|
||||
<div className={`${PREFIX}__header`} onTouchStart={handleClickMoreOuter}>
|
||||
<Button className={classNames(`${PREFIX}__header__reject`, { highlight: reject })} onClick={handleClickReject}>
|
||||
{getHeaderLeftButtonText(job, material)}
|
||||
</Button>
|
||||
<Button className={`${PREFIX}__header__exchange`} onClick={handleSendExchangeContact}>
|
||||
交换联系方式
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`${PREFIX}__chat`} onTouchStart={handleClickMoreOuter}>
|
||||
<ScrollView className={LIST_CONTAINER_CLASS} scrollIntoView={scrollItemId} onScroll={handleScroll} scrollY>
|
||||
{messages.map((message: IChatMessage) => {
|
||||
if (isTextMessage(message)) {
|
||||
return (
|
||||
<TextMessage
|
||||
id={message.msgId}
|
||||
key={message.msgId}
|
||||
message={message}
|
||||
isRead={messageStatusList.some(m => m.msgId === message.msgId && !!m.isRead)}
|
||||
/>
|
||||
);
|
||||
} else if (isTimeMessage(message)) {
|
||||
return <TimeMessage key={message.msgId} id={message.msgId} message={message} />;
|
||||
} else if (isJobMessage(message)) {
|
||||
return <JobMessage key={message.msgId} id={message.msgId} message={message} />;
|
||||
} else if (isMaterialMessage(message)) {
|
||||
return <MaterialMessage key={message.msgId} id={message.msgId} message={message} />;
|
||||
} else if (isExchangeMessage(message)) {
|
||||
return (
|
||||
<ContactMessage
|
||||
key={message.msgId}
|
||||
id={message.msgId}
|
||||
message={message}
|
||||
onClick={handleClickContactButton}
|
||||
/>
|
||||
);
|
||||
} else if (isLocationMessage(message)) {
|
||||
return (
|
||||
<LocationMessage
|
||||
id={message.msgId}
|
||||
key={message.msgId}
|
||||
message={message}
|
||||
isRead={messageStatusList.some(m => m.msgId === message.msgId && !!m.isRead)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<div className={`${PREFIX}__input-container`} onTouchStart={handleClickMoreOuter}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__expand-icon`}
|
||||
src={require('@/statics/svg/chat_expand.svg')}
|
||||
onTouchStart={e => e.stopPropagation()}
|
||||
onClick={handleClickExpand}
|
||||
/>
|
||||
<Textarea
|
||||
fixed
|
||||
autoHeight
|
||||
value={input}
|
||||
maxlength={100}
|
||||
cursorSpacing={20}
|
||||
confirmType="return"
|
||||
onInput={handleInput}
|
||||
showConfirmBar={false}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<Button className={`${PREFIX}__send-button`} onClick={handleSendTextMessage}>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
{showMore && (
|
||||
<div className={`${PREFIX}__more`}>
|
||||
<div className={`${PREFIX}__more__item`} onClick={handleClickSendLocation}>
|
||||
<div className={`${PREFIX}__more__item__icon-wrapper`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__more__item__icon`}
|
||||
src={require('@/statics/svg/location_black.svg')}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${PREFIX}__more__item__text`}>定位</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SafeBottomPadding />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
src/pages/message/index.config.ts
Normal file
5
src/pages/message/index.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '消息',
|
||||
usingComponents: {},
|
||||
disableScroll: true,
|
||||
});
|
||||
35
src/pages/message/index.less
Normal file
35
src/pages/message/index.less
Normal file
@ -0,0 +1,35 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-message {
|
||||
width: 100vw;
|
||||
|
||||
&__header {
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
padding: 16px 32px;
|
||||
background: @blHighlightBg;
|
||||
|
||||
&__times {
|
||||
.flex-row();
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: #6F7686;
|
||||
|
||||
.highlight {
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__help-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
.button(@width: 146px; @height: 60px; @fontSize: 24px; @borderRadius: 44px);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
137
src/pages/message/index.tsx
Normal file
137
src/pages/message/index.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { Button, Image } from '@tarojs/components';
|
||||
import Taro, { NodesRef, useDidHide, useDidShow, useLoad } from '@tarojs/taro';
|
||||
|
||||
import { List } from '@taroify/core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import HomePage from '@/components/home-page';
|
||||
import MessageCard from '@/components/message-card';
|
||||
import { MessageHelpDialog, MessageNoTimesDialog } from '@/components/message-dialog';
|
||||
import { APP_TAB_BAR_ID, EventName } from '@/constants/app';
|
||||
import { REFRESH_CHAT_LIST_TIME } from '@/constants/message';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
import useRoleType from '@/hooks/user-role-type';
|
||||
import { MainMessage } from '@/types/message';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import {
|
||||
postAddMessageTimes,
|
||||
requestMessageList,
|
||||
requestRemainPushTime,
|
||||
requestUnreadMessageCount,
|
||||
} from '@/utils/message';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-message';
|
||||
const HEADER_CLASS = `${PREFIX}__header`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${HEADER_CLASS}`, `#${APP_TAB_BAR_ID}`],
|
||||
calc: (rects: [NodesRef.BoundingClientRectCallbackResult, NodesRef.BoundingClientRectCallbackResult]) => {
|
||||
const [headerRect, tabBarRect] = rects;
|
||||
return tabBarRect.top - headerRect.bottom;
|
||||
},
|
||||
};
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function Message() {
|
||||
const roleType = useRoleType();
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [times, setTimes] = useState(0);
|
||||
const [messages, setMessages] = useState<MainMessage[]>([]);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [showTips, setShowTips] = useState(false);
|
||||
const pageVisibleRef = useRef(true);
|
||||
const refreshRef = useRef(async (needTips: boolean = false) => {
|
||||
try {
|
||||
const [list, remain] = await Promise.all([
|
||||
requestMessageList(),
|
||||
requestRemainPushTime(),
|
||||
requestUnreadMessageCount(),
|
||||
]);
|
||||
setMessages(list);
|
||||
setTimes(Number(remain));
|
||||
needTips && remain >= 0 && remain <= 3 && setShowTips(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClickHelp = useCallback(() => setShowHelp(true), []);
|
||||
|
||||
const handleClickAddMessageTimes = useCallback(async () => {
|
||||
await postAddMessageTimes('message_page');
|
||||
const remain = await requestRemainPushTime();
|
||||
setTimes(remain);
|
||||
}, []);
|
||||
|
||||
useDidHide(() => (pageVisibleRef.current = false));
|
||||
|
||||
useDidShow(() => {
|
||||
pageVisibleRef.current = true;
|
||||
refreshRef.current();
|
||||
});
|
||||
|
||||
useLoad(async () => {
|
||||
refreshRef.current(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(async () => {
|
||||
if (!pageVisibleRef.current) {
|
||||
log('ignore refresh message list by page hidden');
|
||||
return;
|
||||
}
|
||||
refreshRef.current();
|
||||
}, REFRESH_CHAT_LIST_TIME);
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = () => refreshRef.current();
|
||||
Taro.eventCenter.on(EventName.EXIT_CHAT_PAGE, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.EXIT_CHAT_PAGE, callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshRef.current();
|
||||
}, [roleType]);
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
<div className={PREFIX}>
|
||||
<div className={HEADER_CLASS}>
|
||||
<div className={`${HEADER_CLASS}__times`}>
|
||||
消息通知次数还剩:
|
||||
<div className="highlight">{`${times}次`}</div>
|
||||
<Image
|
||||
className={`${HEADER_CLASS}__help-icon`}
|
||||
src={require('@/statics/svg/help.svg')}
|
||||
onClick={handleClickHelp}
|
||||
/>
|
||||
</div>
|
||||
<Button className={`${HEADER_CLASS}__btn`} onClick={handleClickAddMessageTimes}>
|
||||
点击增加
|
||||
</Button>
|
||||
</div>
|
||||
<List className={`${PREFIX}__message-list`} style={{ height: `${listHeight}px` }} disabled fixedHeight>
|
||||
{messages.map(message => (
|
||||
<MessageCard key={message.toUserName} data={message} />
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
<div>
|
||||
<MessageHelpDialog open={showHelp} onClose={() => setShowHelp(false)} />
|
||||
<MessageNoTimesDialog
|
||||
times={times}
|
||||
open={showTips}
|
||||
onClose={() => setShowTips(false)}
|
||||
onClick={handleClickAddMessageTimes}
|
||||
/>
|
||||
</div>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
3
src/pages/my-declaration/index.config.ts
Normal file
3
src/pages/my-declaration/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我应聘的',
|
||||
});
|
||||
8
src/pages/my-declaration/index.less
Normal file
8
src/pages/my-declaration/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
.my-declaration {
|
||||
height: 100vh;
|
||||
padding: 0 24px;
|
||||
|
||||
&__search {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
72
src/pages/my-declaration/index.tsx
Normal file
72
src/pages/my-declaration/index.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { NodesRef } from '@tarojs/taro';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import SearchInput from '@/components/search';
|
||||
import UserJobList from '@/components/user-job-list';
|
||||
import { UserJobType } from '@/constants/job';
|
||||
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'my-declaration';
|
||||
const SEARCH_CLASS = `${PREFIX}__search`;
|
||||
const SAFE_PADDING_BOTTOM_CLASS = `${PREFIX}__safe-padding-bottom`;
|
||||
const CALC_LIST_PROPS: IUseListHeightProps = {
|
||||
selectors: [`.${SEARCH_CLASS}`, `.${PREFIX}`, `.${SAFE_PADDING_BOTTOM_CLASS}`],
|
||||
calc: (
|
||||
rects: [
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
NodesRef.BoundingClientRectCallbackResult,
|
||||
]
|
||||
) => {
|
||||
const [searchRect, pageRect, safePaddingRect] = rects;
|
||||
return pageRect.bottom - searchRect.height - safePaddingRect.height;
|
||||
},
|
||||
};
|
||||
|
||||
export default function Declaration() {
|
||||
const listHeight = useListHeight(CALC_LIST_PROPS);
|
||||
const [focus, setFocus] = useState(false);
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [keyWord, setKeyWord] = useState<string>('');
|
||||
|
||||
const handleClickSearch = useCallback(() => {
|
||||
if (value === keyWord) {
|
||||
return;
|
||||
}
|
||||
setFocus(false);
|
||||
setKeyWord(value);
|
||||
}, [value, keyWord]);
|
||||
|
||||
const handleSearchClear = useCallback(() => {
|
||||
setValue('');
|
||||
setKeyWord('');
|
||||
}, []);
|
||||
|
||||
const handleSearchBlur = useCallback(() => setFocus(false), []);
|
||||
|
||||
const handleSearchFocus = useCallback(() => setFocus(true), []);
|
||||
|
||||
const handleSearchChange = useCallback(e => setValue(e.detail.value), []);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<SearchInput
|
||||
focus={focus}
|
||||
value={value}
|
||||
placeholder="搜索我应聘的"
|
||||
className={SEARCH_CLASS}
|
||||
onClear={handleSearchClear}
|
||||
onBlur={handleSearchBlur}
|
||||
onFocus={handleSearchFocus}
|
||||
onSearch={handleClickSearch}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<UserJobList type={UserJobType.MyDeclared} keyWord={keyWord} listHeight={listHeight} />
|
||||
<SafeBottomPadding className={SAFE_PADDING_BOTTOM_CLASS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/my-publish/index.config.ts
Normal file
3
src/pages/my-publish/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页'
|
||||
})
|
||||
0
src/pages/my-publish/index.less
Normal file
0
src/pages/my-publish/index.less
Normal file
16
src/pages/my-publish/index.tsx
Normal file
16
src/pages/my-publish/index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { useLoad } from '@tarojs/taro'
|
||||
import './index.less'
|
||||
|
||||
export default function MyPublish() {
|
||||
|
||||
useLoad(() => {
|
||||
console.log('Page loaded.')
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='my-publish'>
|
||||
<Text>Hello world!</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
3
src/pages/privacy-webview/index.config.ts
Normal file
3
src/pages/privacy-webview/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '隐私协议',
|
||||
});
|
||||
0
src/pages/privacy-webview/index.less
Normal file
0
src/pages/privacy-webview/index.less
Normal file
9
src/pages/privacy-webview/index.tsx
Normal file
9
src/pages/privacy-webview/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { WebView } from '@tarojs/components';
|
||||
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export default function PrivacyWebview() {
|
||||
return <WebView src="https://neighbourhood.cn/protocol.html" onError={() => Toast.error('加载失败请重试')} />;
|
||||
}
|
||||
3
src/pages/protocol-webview/index.config.ts
Normal file
3
src/pages/protocol-webview/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '用户协议',
|
||||
});
|
||||
0
src/pages/protocol-webview/index.less
Normal file
0
src/pages/protocol-webview/index.less
Normal file
9
src/pages/protocol-webview/index.tsx
Normal file
9
src/pages/protocol-webview/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { WebView } from '@tarojs/components';
|
||||
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export default function ProtocolWebview() {
|
||||
return <WebView src="https://neighbourhood.cn/user-agreement.html" onError={() => Toast.error('加载失败请重试')} />;
|
||||
}
|
||||
4
src/pages/search-city/index.config.ts
Normal file
4
src/pages/search-city/index.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '选择城市',
|
||||
disableScroll: true,
|
||||
});
|
||||
123
src/pages/search-city/index.less
Normal file
123
src/pages/search-city/index.less
Normal file
@ -0,0 +1,123 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.search-city {
|
||||
background: #FFF;
|
||||
|
||||
&__position-title {
|
||||
font-size: 24px;
|
||||
color: @blColorG1;
|
||||
padding: 0 24px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
&__position-city {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: @blColor;
|
||||
padding: 0 24px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
&__hot-city-title {
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
padding: 0 24px;
|
||||
color: #999;
|
||||
background: #f2f5f7;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
&__hot-city-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-content: space-between;
|
||||
width: 630px;
|
||||
padding: 12px 90px 26px 30px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
&__hot-city-item {
|
||||
width: 140px;
|
||||
height: 58px;
|
||||
font-size: 28px;
|
||||
line-height: 58px;
|
||||
text-align: center;
|
||||
border-radius: 58px;
|
||||
border: 2px solid @blColorG1;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
&__indexes-list {
|
||||
width: 100%;
|
||||
/* 兼容 iOS < 11.2 */
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
/* 兼容 iOS >= 11.2 */
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
&__indexes-fragment {}
|
||||
|
||||
&__indexes-anchor {
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
padding: 0 24px;
|
||||
color: @blColorG1;
|
||||
background: #f2f5f7;
|
||||
}
|
||||
|
||||
&__indexes-cell {
|
||||
position: relative;
|
||||
font-size: 28px;
|
||||
padding: 30px 24px;
|
||||
color: @blColor;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-bottom: 1rpx solid #eaeef1;
|
||||
transform: scaleY(0.5);
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__indexes-bar {
|
||||
width: 44rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
&__indexes-bar-item {
|
||||
font-size: 22px;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__indexes-index-alert {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -80px;
|
||||
margin-top: -80px;
|
||||
border-radius: 80px;
|
||||
text-align: center;
|
||||
line-height: 160px;
|
||||
font-size: 70px;
|
||||
color: #FFFFFF;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
229
src/pages/search-city/index.tsx
Normal file
229
src/pages/search-city/index.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
import { BaseEventOrig, InputProps, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { Search } from '@taroify/core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { EventName, OpenSource } from '@/constants/app';
|
||||
import { CITY_CODE_TO_NAME_MAP, CITY_INDEXES_LIST } from '@/constants/city';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { getPageQuery, navigateBack } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface Item {
|
||||
cityCode: number | string;
|
||||
cityName: string;
|
||||
keyword: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'search-city';
|
||||
const HOT_CITY = [
|
||||
{ cityCode: 110100, cityName: '北京' },
|
||||
{ cityCode: 310100, cityName: '上海' },
|
||||
{ cityCode: 440100, cityName: '广州' },
|
||||
{ cityCode: 440300, cityName: '深圳' },
|
||||
{ cityCode: 330100, cityName: '杭州' },
|
||||
{ cityCode: 430100, cityName: '长沙' },
|
||||
{ cityCode: 420100, cityName: '武汉' },
|
||||
{ cityCode: 350200, cityName: '厦门' },
|
||||
{ cityCode: 610100, cityName: '西安' },
|
||||
{ cityCode: 410100, cityName: '郑州' },
|
||||
{ cityCode: 510100, cityName: '成都' },
|
||||
{ cityCode: 340100, cityName: '合肥' },
|
||||
];
|
||||
const OFFSET_INDEX_SIZE = 2;
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
const useHeight = () => {
|
||||
const [winHeight, setWinHeight] = useState(0);
|
||||
const [indexItemHeight, setIndexItemHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const windowInfo = Taro.getWindowInfo();
|
||||
const windowHeight = windowInfo.windowHeight;
|
||||
setWinHeight(windowHeight);
|
||||
// 上下预留两个选项高度的空白
|
||||
setIndexItemHeight(Math.floor(windowHeight / (26 + OFFSET_INDEX_SIZE * 2)));
|
||||
}, []);
|
||||
|
||||
return [winHeight, indexItemHeight];
|
||||
};
|
||||
|
||||
export default function SearchCity() {
|
||||
const [winHeight, indexItemHeight] = useHeight();
|
||||
const [currentCity, setCurrentCity] = useState<string>('');
|
||||
const [touchAnchor, setTouchAnchor] = useState<string | undefined>();
|
||||
const [touchMoving, setTouchMoving] = useState(false);
|
||||
const [searchResult, setSearchResult] = useState<Item[]>([]);
|
||||
const openSourceRef = useRef<OpenSource>(OpenSource.None);
|
||||
const showSearchList = searchResult.length > 0;
|
||||
|
||||
const handleSearchChange = useCallback((event: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = event.detail.value;
|
||||
log('handleSearchChange', value);
|
||||
if (!value) {
|
||||
setSearchResult([]);
|
||||
return;
|
||||
}
|
||||
const result: Item[] = [];
|
||||
CITY_INDEXES_LIST.forEach(obj => {
|
||||
obj.data.forEach(city => {
|
||||
if (city.keyword.includes(value.toLocaleUpperCase())) {
|
||||
result.push({ ...city });
|
||||
}
|
||||
});
|
||||
});
|
||||
setSearchResult(result);
|
||||
}, []);
|
||||
|
||||
const handleSelectCity = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const cityCode = e.currentTarget.dataset.code;
|
||||
Taro.eventCenter.trigger(EventName.SELECT_CITY, { openSource: openSourceRef.current, cityCode: String(cityCode) });
|
||||
navigateBack(1);
|
||||
}, []);
|
||||
|
||||
const handleTouchStart = useCallback(
|
||||
(e: React.TouchEvent<HTMLDivElement>) => {
|
||||
const pageY = e.touches[0].pageY;
|
||||
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
|
||||
if (index < 0 || index >= CITY_INDEXES_LIST.length) {
|
||||
return;
|
||||
}
|
||||
const item = CITY_INDEXES_LIST[index];
|
||||
if (item) {
|
||||
setTouchMoving(true);
|
||||
setTouchAnchor(item.letter);
|
||||
}
|
||||
},
|
||||
[indexItemHeight]
|
||||
);
|
||||
|
||||
const handleTouchMove = useCallback(
|
||||
(e: React.TouchEvent<HTMLDivElement>) => {
|
||||
const pageY = e.touches[0].pageY;
|
||||
const index = Math.floor(pageY / indexItemHeight) - OFFSET_INDEX_SIZE;
|
||||
if (index < 0 || index >= CITY_INDEXES_LIST.length) {
|
||||
return;
|
||||
}
|
||||
const item = CITY_INDEXES_LIST[index];
|
||||
item && setTouchAnchor(item.letter);
|
||||
},
|
||||
[indexItemHeight]
|
||||
);
|
||||
|
||||
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
setTouchMoving(false);
|
||||
log('touch end');
|
||||
}, []);
|
||||
|
||||
const handleClickAnchor = useCallback((anchor: string) => {
|
||||
setTouchAnchor(anchor);
|
||||
log('click anchor', anchor);
|
||||
}, []);
|
||||
|
||||
useLoad(() => {
|
||||
const query = getPageQuery<{ city: string; source: OpenSource }>();
|
||||
log('query', query);
|
||||
const { city: cityCode, source: openSource } = query;
|
||||
if (!cityCode) {
|
||||
return;
|
||||
}
|
||||
setCurrentCity(cityCode);
|
||||
openSourceRef.current = openSource || OpenSource.None;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<ScrollView scrollY style={{ height: winHeight }} scrollIntoView={touchAnchor}>
|
||||
<Search
|
||||
className={`${PREFIX}__search`}
|
||||
placeholder="输入城市名称"
|
||||
shape="rounded"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{showSearchList && (
|
||||
<div className={`${PREFIX}__search-list`}>
|
||||
{searchResult.map(city => (
|
||||
<div
|
||||
key={city.cityCode}
|
||||
className={`${PREFIX}__indexes-cell`}
|
||||
data-code={city.cityCode}
|
||||
onClick={handleSelectCity}
|
||||
>
|
||||
{city.cityName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!showSearchList && (
|
||||
<div>
|
||||
<div className={`${PREFIX}__position-title`}>当前城市</div>
|
||||
<div className={`${PREFIX}__position-city`}>{CITY_CODE_TO_NAME_MAP.get(currentCity)}</div>
|
||||
<div className={`${PREFIX}__hot-city-title`}>热门城市</div>
|
||||
<div className={`${PREFIX}__hot-city-container`}>
|
||||
{HOT_CITY.map(city => (
|
||||
<div
|
||||
key={city.cityCode}
|
||||
className={`${PREFIX}__hot-city-item`}
|
||||
data-code={city.cityCode}
|
||||
onClick={handleSelectCity}
|
||||
>
|
||||
{city.cityName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__indexes-list`}>
|
||||
{CITY_INDEXES_LIST.map(item => {
|
||||
return (
|
||||
<div key={item.letter} className={`${PREFIX}__indexes-fragment`}>
|
||||
<div className={`${PREFIX}__indexes-anchor`} id={item.letter}>
|
||||
{item.letter}
|
||||
</div>
|
||||
{item.data.map(city => (
|
||||
<div
|
||||
key={city.cityCode}
|
||||
className={`${PREFIX}__indexes-cell`}
|
||||
data-code={city.cityCode}
|
||||
onClick={handleSelectCity}
|
||||
>
|
||||
{city.cityName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ScrollView>
|
||||
<div>
|
||||
{!showSearchList && (
|
||||
<div
|
||||
className={`${PREFIX}__indexes-bar`}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
style={{ top: indexItemHeight * OFFSET_INDEX_SIZE }}
|
||||
>
|
||||
{CITY_INDEXES_LIST.map(item => {
|
||||
return (
|
||||
<div
|
||||
key={item.letter}
|
||||
className={`${PREFIX}__indexes-bar-item`}
|
||||
style={{ height: indexItemHeight }}
|
||||
onClick={() => handleClickAnchor(item.letter)}
|
||||
>
|
||||
{item.letter}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{touchAnchor && touchMoving && <div className={`${PREFIX}__indexes-index-alert`}>{touchAnchor}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/start/index.config.ts
Normal file
3
src/pages/start/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
});
|
||||
25
src/pages/start/index.less
Normal file
25
src/pages/start/index.less
Normal file
@ -0,0 +1,25 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-start {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
.flex-column();
|
||||
|
||||
&__app {
|
||||
margin-top: 50%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 312px;
|
||||
height: 152px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 30px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
23
src/pages/start/index.tsx
Normal file
23
src/pages/start/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
import { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { switchDefaultTab } from '@/utils/app';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'page-start';
|
||||
|
||||
export default function Start() {
|
||||
useLoad(() => {
|
||||
switchDefaultTab();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__app`}>
|
||||
<Image className={`${PREFIX}__icon`} mode="aspectFit" src={require('@/statics/svg/slogan.svg')} />
|
||||
<div className={`${PREFIX}__text`}>每天推荐海量高薪通告 </div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/pages/user-batch-publish/index.config.ts
Normal file
3
src/pages/user-batch-publish/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我要群发',
|
||||
});
|
||||
90
src/pages/user-batch-publish/index.less
Normal file
90
src/pages/user-batch-publish/index.less
Normal file
@ -0,0 +1,90 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.page-user-batch-publish {
|
||||
padding: 24px;
|
||||
padding-bottom: 200px;
|
||||
|
||||
&__header-image {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
margin-top: 24px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__cell {
|
||||
height: 100px;
|
||||
padding-left: 32px;
|
||||
padding-right: 32px;
|
||||
border-radius: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&__cost-describe {
|
||||
height: 100px;
|
||||
padding: 0 32px;
|
||||
border-radius: 16px;
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
background: #FFFFFF;
|
||||
margin-top: 24px;
|
||||
|
||||
&__price {
|
||||
font-size: 48px;
|
||||
line-height: 48px;
|
||||
font-weight: 500;
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
|
||||
&__original_price {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 34px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-left: 16px;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
&__illustrate {
|
||||
padding: 24px 32px;
|
||||
margin-top: 24px;
|
||||
font-size: 28px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
|
||||
&__describe {
|
||||
.flex-row();
|
||||
font-size: 28px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
margin-top: 8px;
|
||||
|
||||
&__view {
|
||||
color: @blHighlightColor;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__buy-button {
|
||||
.button(@width: 100%; @height: 80px; @fontSize: 32px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
198
src/pages/user-batch-publish/index.tsx
Normal file
198
src/pages/user-batch-publish/index.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import { Button, Image, Text } from '@tarojs/components';
|
||||
import Taro, { useLoad } from '@tarojs/taro';
|
||||
|
||||
import { Cell } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import HomePage from '@/components/home-page';
|
||||
import PageLoading from '@/components/page-loading';
|
||||
import { PublishJobQrCodeDialog } from '@/components/product-dialog/publish-job';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { ISelectOption, PopupSelect } from '@/components/select';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { OrderStatus, OrderType, ProductSpecId, ProductType } from '@/constants/product';
|
||||
import { BatchPublishGroup } from '@/types/group';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import {
|
||||
getOrderPrice,
|
||||
isCancelPay,
|
||||
requestAllBuyProduct,
|
||||
requestCreatePayInfo,
|
||||
requestOrderInfo,
|
||||
requestPayment,
|
||||
} from '@/utils/product';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface CityValue extends BatchPublishGroup {
|
||||
cityName: string;
|
||||
}
|
||||
|
||||
interface CityOption extends ISelectOption<CityValue> {
|
||||
value: CityValue;
|
||||
}
|
||||
|
||||
const PREFIX = 'page-user-batch-publish';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const SERVICE_ILLUSTRATE = `群发次数:每日一次,连发3天
|
||||
群发内容:仅限主播招聘通告,违规内容不发
|
||||
联系方法:通告中留通告主联系方式,主播直接联系`;
|
||||
const cityValues: CityValue[] = [
|
||||
{ cityCode: '440100', cityName: '广州', count: 300 },
|
||||
{ cityCode: '440300', cityName: '深圳', count: 100 },
|
||||
{ cityCode: '330100', cityName: '杭州', count: 300 },
|
||||
{ cityCode: '110100', cityName: '北京', count: 100 },
|
||||
{ cityCode: '510100', cityName: '成都', count: 50 },
|
||||
{ cityCode: '430100', cityName: '长沙', count: 50 },
|
||||
{ cityCode: '350200', cityName: '厦门', count: 50 },
|
||||
{ cityCode: '310100', cityName: '上海', count: 100 },
|
||||
{ cityCode: '420100', cityName: '武汉', count: 50 },
|
||||
{ cityCode: '610100', cityName: '西安', count: 50 },
|
||||
{ cityCode: '410100', cityName: '郑州', count: 100 },
|
||||
].sort((a, b) => b.count - a.count);
|
||||
const MIN_GROUP_SIZE = 20;
|
||||
const GROUP_OPTIONS = [
|
||||
{ value: MIN_GROUP_SIZE, productSpecId: ProductSpecId.GroupBatchPublish20, label: '20', price: 18 },
|
||||
{ value: 50, productSpecId: ProductSpecId.GroupBatchPublish50, label: '50', price: 40 },
|
||||
{ value: 100, productSpecId: ProductSpecId.GroupBatchPublish100, label: '100', price: 68 },
|
||||
{ value: 300, productSpecId: ProductSpecId.GroupBatchPublish300, label: '300', price: 128 },
|
||||
{ value: 500, productSpecId: ProductSpecId.GroupBatchPublish500, label: '500', price: 188 },
|
||||
{ value: 1000, productSpecId: ProductSpecId.GroupBatchPublish1000, label: '1000', price: 288 },
|
||||
];
|
||||
|
||||
const calcPrice = (city: CityValue | null) => {
|
||||
if (!city) {
|
||||
return {};
|
||||
}
|
||||
const { count } = city;
|
||||
const originalPrice = count * 1;
|
||||
const price = GROUP_OPTIONS.find(o => o.value === count)?.price || 18;
|
||||
const productSpecId = GROUP_OPTIONS.find(o => o.value === count)?.productSpecId || ProductSpecId.GroupBatchPublish20;
|
||||
return { price, originalPrice, productSpecId };
|
||||
};
|
||||
|
||||
export default function UserBatchPublish() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCitySelect, setShowCitySelect] = useState(false);
|
||||
const [showQrCode, setShowQrCode] = useState(false);
|
||||
const [city, setCity] = useState<CityOption['value'] | null>(null);
|
||||
const [cityOptions, setCityOptions] = useState<CityOption[]>([]);
|
||||
const { price, originalPrice, productSpecId } = calcPrice(city);
|
||||
|
||||
const handleClickCity = useCallback(() => setShowCitySelect(true), []);
|
||||
|
||||
const handleSelectCity = useCallback(value => {
|
||||
setCity(value);
|
||||
setShowCitySelect(false);
|
||||
}, []);
|
||||
|
||||
const handleClickViewGroup = useCallback(() => navigateTo(PageUrl.GroupList, { city: city?.cityCode }), [city]);
|
||||
|
||||
const handleClickBuy = useCallback(async () => {
|
||||
// if (1 < 2) {
|
||||
// await new Promise(r => setTimeout(r, 3000));
|
||||
// setShowQrCode(true);
|
||||
// return;
|
||||
// }
|
||||
if (!price || !productSpecId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Taro.showLoading();
|
||||
const allowBuy = await requestAllBuyProduct(ProductType.GroupBatchPublish);
|
||||
if (!allowBuy) {
|
||||
Taro.hideLoading();
|
||||
Toast.info('您最近已购买过,可直接联系客服');
|
||||
setShowQrCode(true);
|
||||
return;
|
||||
}
|
||||
const { payOrderNo, createPayInfo } = await requestCreatePayInfo({
|
||||
type: OrderType.GroupBatchPublish,
|
||||
amt: getOrderPrice(price),
|
||||
// amt: 1,
|
||||
productCode: ProductType.GroupBatchPublish,
|
||||
productSpecId: productSpecId,
|
||||
});
|
||||
log('handleBuy payInfo', payOrderNo, createPayInfo);
|
||||
await requestPayment({
|
||||
timeStamp: createPayInfo.timeStamp,
|
||||
nonceStr: createPayInfo.nonceStr,
|
||||
package: createPayInfo.packageVal,
|
||||
signType: createPayInfo.signType,
|
||||
paySign: createPayInfo.paySign,
|
||||
});
|
||||
const { status } = await requestOrderInfo({ payOrderNo });
|
||||
log('handleBuy orderInfo', status);
|
||||
if (status !== OrderStatus.Success) {
|
||||
throw new Error('order status error');
|
||||
}
|
||||
Taro.hideLoading();
|
||||
setShowQrCode(true);
|
||||
} catch (e) {
|
||||
Taro.hideLoading();
|
||||
Toast.error(isCancelPay(e) ? '取消购买' : '购买失败请重试');
|
||||
log('handleBuy error', e);
|
||||
}
|
||||
}, [price, productSpecId]);
|
||||
|
||||
useLoad(async () => {
|
||||
try {
|
||||
const cOptions: CityOption[] = cityValues.map(value => ({ value, label: value.cityName }));
|
||||
const initCity = cOptions[0].value;
|
||||
|
||||
setLoading(false);
|
||||
setCity(initCity);
|
||||
setCityOptions(cOptions);
|
||||
log('init data done', cOptions);
|
||||
} catch (e) {
|
||||
Toast.error('加载失败请重试');
|
||||
}
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<HomePage>
|
||||
<div className={PREFIX}>
|
||||
<Image mode="widthFix" className={`${PREFIX}__header-image`} src="https://neighbourhood.cn/pubJob.png" />
|
||||
<div className={`${PREFIX}__title`}>请选择城市</div>
|
||||
<Cell isLink align="center" className={`${PREFIX}__cell`} title={city?.cityName} onClick={handleClickCity} />
|
||||
<div className={`${PREFIX}__title`}>可购买群数</div>
|
||||
<Cell align="center" className={`${PREFIX}__cell`} title={city?.count} />
|
||||
<div className={`${PREFIX}__title`}>服务费用</div>
|
||||
<div className={`${PREFIX}__cost-describe`}>
|
||||
<div className={`${PREFIX}__cost-describe__price`}>{`${price}元`}</div>
|
||||
<div className={`${PREFIX}__cost-describe__original_price`}>{`原价:${originalPrice}元`}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>服务说明</div>
|
||||
<div className={`${PREFIX}__illustrate`}>
|
||||
<Text>{SERVICE_ILLUSTRATE}</Text>
|
||||
<div className={`${PREFIX}__illustrate__describe`}>
|
||||
<div>附:</div>
|
||||
<div className={`${PREFIX}__illustrate__describe__view`} onClick={handleClickViewGroup}>
|
||||
播络合作群列表
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className={`${PREFIX}__buy-button`} onClick={handleClickBuy}>
|
||||
立即购买
|
||||
</Button>
|
||||
<SafeBottomPadding />
|
||||
<div>
|
||||
<PopupSelect
|
||||
value={city}
|
||||
options={cityOptions}
|
||||
open={showCitySelect}
|
||||
onSelect={handleSelectCity}
|
||||
onClose={() => setShowCitySelect(false)}
|
||||
/>
|
||||
<PublishJobQrCodeDialog onClose={() => setShowQrCode(false)} open={showQrCode} />
|
||||
</div>
|
||||
</div>
|
||||
</HomePage>
|
||||
);
|
||||
}
|
||||
3
src/pages/user-info/index.config.ts
Normal file
3
src/pages/user-info/index.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '个人信息',
|
||||
});
|
||||
21
src/pages/user-info/index.less
Normal file
21
src/pages/user-info/index.less
Normal file
@ -0,0 +1,21 @@
|
||||
.user-info {
|
||||
|
||||
&__avatar-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__avatar-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
77
src/pages/user-info/index.tsx
Normal file
77
src/pages/user-info/index.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { BaseEventOrig, Button, Input, InputProps, Image } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { Cell } from '@taroify/core';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import Toast from '@/utils/toast';
|
||||
import { updateUserInfo } from '@/utils/user';
|
||||
import { commonUploadProgress, uploadVideo } from '@/utils/video';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const PREFIX = 'user-info';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
export default function UserInfo() {
|
||||
const userInfo = useUserInfo();
|
||||
const nameRef = useRef(userInfo.nickName);
|
||||
|
||||
const handleChooseAvatar = useCallback(async (e: BaseEventOrig) => {
|
||||
// const { avatarUrl } = e.detail;
|
||||
// log('handleChooseAvatar', avatarUrl, e.detail);
|
||||
// const { url } = await uploadVideo(avatarUrl, 'image', commonUploadProgress, 'user-avatar');
|
||||
// url && updateUserInfo({ avatarUrl: url });
|
||||
Taro.chooseMedia({
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album'],
|
||||
count: 1,
|
||||
success: async ({ tempFiles }) => {
|
||||
log('handleChooseAvatar', tempFiles[0]);
|
||||
const { url } = await uploadVideo(tempFiles[0].tempFilePath, 'image', commonUploadProgress, 'user-avatar');
|
||||
url && updateUserInfo({ avatarUrl: url });
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
const handleInput = useCallback((e: BaseEventOrig<InputProps.inputValueEventDetail>) => {
|
||||
const value = e.detail?.value || '';
|
||||
nameRef.current = value;
|
||||
}, []);
|
||||
|
||||
const handleInputBlurOrConfirm = useCallback(() => {
|
||||
const newNickName = nameRef.current;
|
||||
if (!newNickName) {
|
||||
Toast.error('昵称不能为空');
|
||||
}
|
||||
|
||||
log('confirm nickname changed:', newNickName, userInfo.nickName);
|
||||
newNickName !== userInfo.nickName && updateUserInfo({ nickName: newNickName });
|
||||
}, [userInfo]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<Cell className={`${PREFIX}__avatar-cell`} title="头像" align="center" isLink>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__avatar`}
|
||||
src={userInfo.avatarUrl || require('@/statics/png/default_avatar.png')}
|
||||
/>
|
||||
<Button className={`${PREFIX}__avatar-button`} onClick={handleChooseAvatar} />
|
||||
</Cell>
|
||||
<Cell title="昵称" align="center" isLink>
|
||||
<Input
|
||||
type="nickname"
|
||||
confirmType="done"
|
||||
placeholder="请输入昵称"
|
||||
value={nameRef.current}
|
||||
onInput={handleInput}
|
||||
onBlur={handleInputBlurOrConfirm}
|
||||
onConfirm={handleInputBlurOrConfirm}
|
||||
/>
|
||||
</Cell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
src/pages/user/index.config.ts
Normal file
6
src/pages/user/index.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '',
|
||||
enableShareAppMessage: true,
|
||||
usingComponents: {},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user