2 Commits

Author SHA1 Message Date
3d2b121b92 feat: 2025-10-15 20:44:20 +08:00
7ba04b27ff feat:hhhh 2025-10-06 11:49:41 +08:00
26 changed files with 1668 additions and 140 deletions

900
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { useLaunch } from '@tarojs/taro';
import Taro, { useDidShow, useLaunch } from '@tarojs/taro';
import { PropsWithChildren } from 'react';
import { Provider } from 'react-redux';
@ -6,8 +6,9 @@ import { Provider } from 'react-redux';
import { REFRESH_UNREAD_COUNT_TIME } from '@/constants/message';
import http from '@/http';
import store from '@/store';
import { requestServiceUrls } from '@/utils/location';
import { requestUnreadMessageCount } from '@/utils/message';
import { getInviteCode, getInviteCodeFromQuery } from '@/utils/partner';
import { decryptOpenGid, getInviteCode, getInviteCodeFromQuery } from '@/utils/partner';
import qiniuUpload from '@/utils/qiniu-upload';
import { requestUserInfo, updateLastLoginTime } from '@/utils/user';
@ -28,6 +29,31 @@ function App({ children }: PropsWithChildren<BL.Anything>) {
setInterval(() => requestUnreadMessageCount(), REFRESH_UNREAD_COUNT_TIME);
});
useDidShow(options => {
requestServiceUrls();
console.log(options);
Taro.getGroupEnterInfo()
.then(info => {
const inviteCode = getInviteCodeFromQuery(options?.query || {});
const authCode = options?.query?.authCode;
decryptOpenGid({
inviteCode,
authCode,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
iv: info.iv,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
encryptedData: info.encryptedData,
});
console.log('哈哈哈', info);
})
.catch(() => {
console.log('没有解析到群', options?.scene);
});
});
return <Provider store={store}>{children}</Provider>;
}

View File

@ -0,0 +1,79 @@
@import '@/styles/common.less';
.group-certification-list {
min-height: calc(100vh - 98rpx);
&__banner {
font-weight: 400;
font-size: 24px;
height: 72px;
padding: 32px 32px 25px;
line-height: 36px;
color: #999999;
}
&__title {
height: 72px;
width: 100%;
padding: 0 24px;
box-sizing: border-box;
line-height: 72px;
font-size: 24px;
color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 227rpx;
left: 0;
z-index: 1;
background: #fff;
&-border {
border-bottom: 1px solid #e6e7e8;
.flex-row();
}
&-time {
padding: 0 8px;
flex: 0 0 120px;
width: 120px;
flex-shrink: 0;
}
&-name {
text-align: right;
flex: 1;
}
}
&__pull-refresh {
margin-top: 72px;
}
&__item {
height: 100px;
width: 100%;
padding: 24px 32px 0 32px;
box-sizing: border-box;
font-size: 28px;
background: #fff;
&-border {
border-bottom: 1px solid #e6e7e8;
}
&-content {
.flex-row();
width: 100%;
padding-bottom: 24px;
}
&-time {
padding: 0 8px;
flex: 0 0 120px;
width: 120px;
flex-shrink: 0;
}
&-name {
text-align: right;
flex: 1;
}
}
}

View File

@ -0,0 +1,153 @@
import { List, PullRefresh } from '@taroify/core';
import classNames from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'react';
import ListPlaceholder from '@/components/list-placeholder';
import { AuthedGroupInfo } from '@/types/partner';
import { logWithPrefix } from '@/utils/common';
import { formatTimestamp, getAuthedGroupList as requestData } from '@/utils/partner';
import './index.less';
const PREFIX = 'group-certification-list';
const log = logWithPrefix(PREFIX);
const FIRST_PAGE = 0;
function GroupCertificationList(props: {
refreshDisabled?: boolean;
visible?: boolean;
listHeight?: number;
className?: string;
onListEmpty?: () => void;
}) {
const { className, listHeight, refreshDisabled, visible = true, onListEmpty } = props;
const [hasMore, setHasMore] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState(false);
const [dataList, setDataList] = useState<AuthedGroupInfo[]>([]);
const currentPage = useRef<number>(FIRST_PAGE);
const onListEmptyRef = useRef(onListEmpty);
const handleRefresh = useCallback(async () => {
log('start pull refresh');
try {
setRefreshing(true);
setLoadMoreError(false);
const content = await requestData();
setDataList(content);
currentPage.current = 1;
// setHasMore(currentPage.current < totalPages);
!content.length && onListEmptyRef.current?.();
log('pull refresh success');
} catch (e) {
setDataList([]);
setHasMore(false);
setLoadMoreError(true);
currentPage.current = FIRST_PAGE;
log('pull refresh failed');
} finally {
setRefreshing(false);
}
}, []);
const handleLoadMore = useCallback(async () => {
log('start load more', hasMore);
if (!hasMore) {
return;
}
setLoadMoreError(false);
setLoadingMore(true);
try {
const content = await requestData();
setDataList([...dataList, ...content]);
currentPage.current = currentPage.current + 1;
// setHasMore(currentPage.current < totalPages);
log('load more success');
} catch (e) {
setLoadMoreError(true);
log('load more failed');
} finally {
setLoadingMore(false);
}
}, [dataList, hasMore]);
useEffect(() => {
onListEmptyRef.current = onListEmpty;
}, [onListEmpty]);
// 初始化数据&配置变更后刷新数据
useEffect(() => {
// 列表不可见时,先不做处理
if (!visible) {
log('visible changed, but is not visible, only clear list');
return;
}
const refresh = async () => {
log('visible changed, start refresh list data');
try {
setDataList([]);
setLoadingMore(true);
setLoadMoreError(false);
const content = await requestData();
setDataList(content);
currentPage.current = 1;
// setHasMore(currentPage.current < totalPages);
!content.length && onListEmptyRef.current?.();
} catch (e) {
setDataList([]);
setHasMore(false);
setLoadMoreError(true);
} finally {
log('visible changed, refresh list data end');
setLoadingMore(false);
}
};
refresh();
}, [visible]);
return (
<div className={PREFIX}>
<div className={`${PREFIX}__banner`}>
</div>
<div className={`${PREFIX}__title`}>
<div className={`${PREFIX}__title-border`}>
<div className={`${PREFIX}__title-time`}></div>
<div className={`${PREFIX}__title-name`}></div>
</div>
</div>
<PullRefresh
className={classNames(`${PREFIX}__pull-refresh`, className)}
loading={refreshing}
onRefresh={handleRefresh}
disabled={refreshDisabled}
>
<List
hasMore={hasMore}
onLoad={handleLoadMore}
loading={loadingMore || refreshing}
disabled={loadMoreError || !visible}
fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
>
{dataList.map(item => (
<div className={`${PREFIX}__item`} key={item.openGid}>
<div className={`${PREFIX}__item-border`}>
<div className={`${PREFIX}__item-content`}>
<div className={`${PREFIX}__item-time`}>{formatTimestamp(item.authDate, true)}</div>
<div className={`${PREFIX}__item-name`}>{item.groupName}</div>
</div>
</div>
</div>
))}
<ListPlaceholder hasMore={false} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List>
</PullRefresh>
</div>
);
}
export default GroupCertificationList;

View File

@ -7,12 +7,11 @@ import { useCallback, useState } from 'react';
import { RoleType } from '@/constants/app';
import { CacheKey } from '@/constants/cache-key';
import { CITY_CODE_TO_NAME_MAP } from '@/constants/city';
import { GROUPS } from '@/constants/group';
import useServiceUrls from '@/hooks/use-service-urls';
import { getRoleTypeWithDefault } from '@/utils/app';
import { openCustomerServiceChat } from '@/utils/common';
import { getCurrentCityCode } from '@/utils/location';
import { checkCityCode, validCityCode } from '@/utils/user';
import './index.less';
const PREFIX = 'join-group-hint';
@ -25,7 +24,8 @@ const DEFAULT_GROUP = {
export function JoinGroupHint() {
const cityCode = getCurrentCityCode();
const roleType = getRoleTypeWithDefault();
const group = GROUPS.find(g => String(g.cityCode) === cityCode);
const serviceUrls = useServiceUrls();
const group = serviceUrls.find(g => String(g.cityCode) === cityCode);
const [clicked, setClicked] = useState(!!Taro.getStorageSync(CacheKey.JOIN_GROUP_CARD_CLICKED));
const handleClick = useCallback(() => {
if (group && !checkCityCode(cityCode)) {

View File

@ -7,7 +7,7 @@
padding-bottom: calc(112px + env(safe-area-inset-bottom));
&__banner {
background: fade(@blHighlightBg, 8);
background: rgb(229, 225, 248);
height: 88px;
line-height: 88px;
text-align: center;
@ -37,6 +37,63 @@
margin-bottom: 40px;
}
&__swiper {
margin-bottom: 48px;
&-wrapper {
background: #fff;
border-radius: 24px;
position: relative;
}
&-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
&__swiper-item {
box-sizing: border-box;
height: 130px;
padding: 24px 32px;
&-item {
font-style: normal;
font-size: 28px;
line-height: 40px;
color: #333333;
font-weight: 400;
position: relative;
}
&-details {
margin-top: 5px;
}
&-id {
font-size: 24px;
line-height: 36px;
color: #999999;
padding-right: 22px;
display: inline-block;
}
&-info {
font-size: 28px;
line-height: 40px;
color: #333333;
margin-right: 16px;
display: inline-block;
&:last-child {
margin-right: 0;
}
.money {
color: #ff5051;
display: inline-block;
padding-left: 8px;
}
}
}
&__card {
background: #fff;
@ -61,9 +118,14 @@
font-weight: 400;
font-size: 28px;
line-height: 40px;
color: @blColor;
&.grey {
color: @blColorG2
color: @blColorG2;
}
&.center {
text-align: center;
}
}
@ -76,6 +138,20 @@
color: #1d2129;
}
&__recommend {
display: inline-flex;
line-height: 36px;
padding: 0 8px;
height: 36px;
margin-left: 16px;
background: rgba(255, 80, 81, 0.12);
border-radius: 4px;
font-size: 24px;
color: #ff5051;
align-items: center;
gap: 6px;
}
&__special {
padding: 32px;
.flex-column();

View File

@ -1,10 +1,16 @@
import { Button, Canvas } from '@tarojs/components';
import { Button, Canvas, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useCallback } from 'react';
import { Swiper } from '@taroify/core';
import { GoodJob } from '@taroify/icons';
import { useCallback, useEffect, useRef, useState } from 'react';
import { PageUrl } from '@/constants/app';
import { EarnType, UserProfitListItem } from '@/types/partner';
import { openCustomerServiceChat } from '@/utils/common';
import { getCouponQrCode, generateMembershipCoupon } from '@/utils/coupon';
import { generateMembershipCoupon, getCouponQrCode } from '@/utils/coupon';
import { formatMoney, formatTimestamp, getLastProfitList } from '@/utils/partner';
import { navigateTo } from '@/utils/route';
import './index.less';
const PREFIX = 'partner-intro';
@ -110,50 +116,131 @@ export default function PartnerIntro() {
}
};
const handleConfirm = useCallback(() => {
navigateTo(PageUrl.GroupOwnerCertificate);
}, []);
const handleOpenService = useCallback(() => {
openCustomerServiceChat('https://work.weixin.qq.com/kfid/kfc4fcf6b109b3771d7');
}, []);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const [bannerList, setBannerList] = useState<UserProfitListItem[]>([]);
const getBannerList = useCallback(async () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
const list = await getLastProfitList();
setBannerList(s => [...s, ...list]);
timerRef.current = setTimeout(
() => {
getBannerList();
},
3000 * (list.length || 10)
);
}, []);
useEffect(() => {
getBannerList();
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, [getBannerList]);
return (
<div className={PREFIX}>
<div className={`${PREFIX}__banner`}>
<span className={`${PREFIX}__highlight`}>75%</span>
</div>
<div className={`${PREFIX}__main`}>
<div className={`${PREFIX}__swiper-wrapper`}>
<Image
className={`${PREFIX}__swiper-bg`}
src="https://publiccdn.neighbourhood.com.cn/img/partner-swipe-item.png"
mode="aspectFill"
/>
<Swiper className={`${PREFIX}__swiper`} autoplay={3000} touchable={false}>
{bannerList.map((item, index) => (
<Swiper.Item className={`${PREFIX}__swiper-item`} key={index}>
<div className={`${PREFIX}__swiper-item-time`}>{formatTimestamp(item.updatedAt)}</div>
<div className={`${PREFIX}__swiper-item-details`}>
<div className={`${PREFIX}__swiper-item-id`}>{item.userId}</div>
<div className={`${PREFIX}__swiper-item-info`}>
{[EarnType.CHAT_ACTIVITY_SHARE_L1, EarnType.CHAT_ACTIVITY_SHARE_L2].includes(item.earnType)
? '主播被开聊'
: '会员支付'}
<div className="money">+{formatMoney(item.total)}</div>
</div>
<div className={`${PREFIX}__swiper-item-info`}>
<div className="money">{formatMoney(item.amount)}</div>
</div>
</div>
</Swiper.Item>
))}
</Swiper>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}>3</div>
<div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body`}>
<span className={`${PREFIX}__highlight`}>20%</span>
</div>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body`}>
<span className={`${PREFIX}__highlight`}>50%</span>
</div>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body`}>
<span className={`${PREFIX}__highlight`}>5%</span>
<span className={`${PREFIX}__highlight`}>5%</span>
</div>
</div>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__body`}>
<div>1.</div>
<div>2.</div>
</div>
</div>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__title`}>
<div className={`${PREFIX}__recommend`}>
<GoodJob />
</div>
</div>
<div className={`${PREFIX}__card ${PREFIX}__special`}>
<div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body grey`}></div>
<div className={`${PREFIX}__body`}>
<div className="center">
访
</div>
</div>
<Button className={`${PREFIX}__service`} onClick={handleConfirm}>
</Button>
</div>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card ${PREFIX}__special`}>
<div className={`${PREFIX}__h1`}></div>
<Button className={`${PREFIX}__service`} onClick={handleOpenService}>
</Button>
</div>
<div className={`${PREFIX}__tip`}></div>
</div>
</div>
<Canvas id="posterCanvas" canvas-id="posterCanvas" type="2d" style="width: 750px; height: 1334px;" />

View File

@ -35,6 +35,7 @@ export enum OpenSource {
UserPage = 'user_page',
AnchorPage = 'anchor_page',
MaterialViewPage = 'material_view_page',
GroupOwnerCertificate = 'group_owner_certificate',
}
export enum PageUrl {
@ -77,6 +78,7 @@ export enum PageUrl {
WithdrawRecord = 'pages/withdraw-record/index',
GroupDelegatePublish = 'pages/group-delegate-publish/index',
GiveVip = 'pages/give-vip/index',
GroupOwnerCertificate = 'pages/group-owner-certification/index',
}
export enum PluginUrl {

View File

@ -99,6 +99,7 @@ export const APP_CONFIG: AppConfigType = {
PageUrl.WithdrawRecord,
PageUrl.GroupDelegatePublish,
PageUrl.GiveVip,
PageUrl.GroupOwnerCertificate,
// PageUrl.DevDebug,
],
window: {

View File

@ -0,0 +1,10 @@
import { useSelector } from 'react-redux';
import { selectServiceUrls } from '@/store/selector';
function useServiceUrls() {
const data = useSelector(selectServiceUrls);
return data || [];
}
export default useServiceUrls;

View File

@ -88,4 +88,12 @@ export enum API {
GENERATE_MEMBERSHIP_COUPON = '/coupon/membership/generate',
CLAIM_MEMBERSHIP_COUPON = '/coupon/membership/claim',
GET_VIP_QRCODE = '/user/getVipQrCode',
// 群认证
GENERATE_GROUP_AUTH_CODE = '/partner/generateGroupAuthCode',
DECRYPT_OPEN_GID = '/partner/decryptOpenGid',
GET_USER_PROFIT_LIST = '/partner/getLatestUserProfitList',
GET_AUTHED_GROUP_LIST = '/partner/getAuthedGroupList',
GET_STAFF_CODE = '/partner/staff/{cityCode}',
// 所有城市运营
GET_ALL_CITY_OPERATOR = '/group/getAllGroup',
}

View File

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '群主认证',
enableShareAppMessage: true,
});

View File

@ -0,0 +1,127 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.group-owner-certification {
&__tabs {
--tabs-active-color: @blHighlightColor;
--tabs-nav-background-color: #fff;
--tabs-wrap-height: 98px;
> .taroify-tabs__wrap {
position: fixed;
width: 100vw;
top: 0;
left: 0;
z-index: 2;
}
> .taroify-tabs__content {
padding-top: var(--tabs-wrap-height);
}
}
&__main {
padding-left: 24px;
padding-right: 24px;
padding-top: 48px;
}
&__block {
margin-bottom: 40px;
}
&__card {
background: #fff;
border-radius: 24px;
padding: 24px 32px;
margin-bottom: 48px;
}
&__bold {
font-weight: 500;
margin-bottom: 8px;
}
&__body {
font-weight: 400;
font-size: 28px;
line-height: 40px;
color: @blColor;
.highlight {
color: @blHighlightColor;
display: inline;
}
&.center {
text-align: center;
}
}
&__title {
margin-bottom: 24px;
font-weight: 500;
font-size: 32px;
line-height: 32px;
color: #1d2129;
}
&__share {
.button(@height: 72px; @width: 384px; @fontSize: 28px; @fontWeight: 400; @borderRadius: 44px; @highlight: 0);
margin-top: 32px;
margin-left: auto;
margin-right: auto;
}
&__lined-wrapper {
text-align: center;
margin-top: 48px;
margin-bottom: 24px;
}
&__lined-title {
text-align: center;
font-weight: 400;
font-size: 28px;
line-height: 40px;
color: #333333;
position: relative;
display: inline-block;
&:before {
content: '';
position: absolute;
left: -68px;
width: 56px;
height: 1px;
background: #ccc;
top: 50%;
}
&:after {
content: '';
position: absolute;
right: -68px;
width: 56px;
height: 1px;
background: #ccc;
top: 50%;
}
}
&__city-select {
background: #F7F7F7;
border-radius: 16px;
padding: 34px 32px;
font-weight: 400;
font-size: 32px;
line-height: 32px;
color: #333333;
display: flex;
justify-content: space-between;
}
&__qrcode {
width: 280px;
height: 280px;
background: #6F7686;
margin: auto auto 24px;
}
}

View File

@ -0,0 +1,123 @@
import { Button, Image } from '@tarojs/components';
import Taro, { useShareAppMessage } from '@tarojs/taro';
import { Tabs } from '@taroify/core';
import { Arrow } from '@taroify/icons';
import { useCallback, useEffect, useRef, useState } from 'react';
import GroupCertificationList from '@/components/group-certification-list';
import { EventName, OpenSource, PageUrl } from '@/constants/app';
import { CITY_CODE_TO_NAME_MAP } from '@/constants/city';
import useInviteCode from '@/hooks/use-invite-code';
import useLocation from '@/hooks/use-location';
import { StaffInfo } from '@/types/partner';
import { generateGroupAuthCode, getStaffInfo } from '@/utils/partner';
import { navigateTo } from '@/utils/route';
import { getCommonShareMessage } from '@/utils/share';
import './index.less';
const PREFIX = 'group-owner-certification';
export default function GroupOwnerCertification() {
const location = useLocation();
const inviteCode = useInviteCode();
const [cityCode, setCityCode] = useState<string>(location.cityCode);
const cityValuesChangedRef = useRef(false);
Taro.showShareMenu({
withShareTicket: true,
});
useShareAppMessage(async () => {
const { authCode } = await generateGroupAuthCode();
return getCommonShareMessage({
useCapture: false,
title: `群主测试,${authCode}`,
inviteCode,
params: { authCode },
path: PageUrl.GroupOwnerCertificate,
imageUrl: 'https://publiccdn.neighbourhood.com.cn/img/share-group-owner-certificate.png',
});
});
const handleClickCityMenu = useCallback(() => {
navigateTo(PageUrl.CitySearch, { city: cityCode, source: OpenSource.GroupOwnerCertificate });
}, [cityCode]);
const handleCityChange = useCallback(data => {
console.log('handleCityChange', data);
const { openSource, cityCode: cCode } = data;
if (openSource !== OpenSource.GroupOwnerCertificate) {
return;
}
cityValuesChangedRef.current = true;
setCityCode(cCode);
}, []);
useEffect(() => {
if (cityValuesChangedRef.current) {
return;
}
setCityCode(location.cityCode);
}, [location]);
useEffect(() => {
Taro.eventCenter.on(EventName.SELECT_CITY, handleCityChange);
return () => {
Taro.eventCenter.off(EventName.SELECT_CITY, handleCityChange);
};
}, [handleCityChange]);
const [staffInfo, setStaffInfo] = useState<StaffInfo | null>(null);
useEffect(() => {
getStaffInfo(cityCode)
.then(data => {
setStaffInfo(data);
})
.catch(() => {
setStaffInfo(null);
});
}, [cityCode]);
return (
<div className={PREFIX}>
<Tabs className={`${PREFIX}__tabs`}>
<Tabs.TabPane value={0} title="认证方法">
<div className={`${PREFIX}__main`}>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__body`}>
<div></div>
<div></div>
</div>
<div className={`${PREFIX}__lined-wrapper`}>
<div className={`${PREFIX}__lined-title`}></div>
</div>
<div className={`${PREFIX}__city-select`} onClick={handleClickCityMenu}>
{CITY_CODE_TO_NAME_MAP.get(cityCode)}
<Arrow size={16} />
</div>
<div className={`${PREFIX}__lined-wrapper`}>
<div className={`${PREFIX}__lined-title`}></div>
</div>
{staffInfo && <Image className={`${PREFIX}__qrcode`} src={staffInfo.staffQrCode} mode="aspectFill" />}
</div>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__h1 ${PREFIX}__bold`}></div>
<div className={`${PREFIX}__body center`}>
<div className="highlight">1</div>
1
</div>
<Button className={`${PREFIX}__share`} openType="share">
</Button>
</div>
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane value={1} title="已认证的群">
<GroupCertificationList />
</Tabs.TabPane>
</Tabs>
</div>
);
}

View File

@ -5,8 +5,8 @@ import { useCallback } from 'react';
import HomePage from '@/components/home-page';
import SearchCity from '@/components/search-city';
import { PageType, PageUrl, RoleType } from '@/constants/app';
import { GROUPS } from '@/constants/group';
import useInviteCode from '@/hooks/use-invite-code';
import useServiceUrls from '@/hooks/use-service-urls';
import { switchRoleType } from '@/utils/app';
import { openCustomerServiceChat } from '@/utils/common';
import { getCurrentCityCode } from '@/utils/location';
@ -20,6 +20,7 @@ const PREFIX = 'group-v2-page';
export default function GroupV2() {
const inviteCode = useInviteCode();
const serviceUrls = useServiceUrls();
useLoad(() => {
switchRoleType(RoleType.Anchor);
@ -32,15 +33,18 @@ export default function GroupV2() {
getCommonShareMessage({ inviteCode, title: '邀请你加入本地主播求职招聘群', path: PageUrl.GroupV2 })
);
const handleSelectCity = useCallback(cityCode => {
const handleSelectCity = useCallback(
cityCode => {
if (!checkCityCode(cityCode)) {
return;
}
const group = GROUPS.find(g => String(g.cityCode) === cityCode);
const group = serviceUrls.find(g => String(g.cityCode) === cityCode);
if (group) {
openCustomerServiceChat(group.serviceUrl);
}
}, []);
},
[serviceUrls]
);
return (
<HomePage type={PageType.GroupV2}>

View File

@ -7,8 +7,8 @@ import { useCallback, useState } from 'react';
import HomePage from '@/components/home-page';
import SearchCity from '@/components/search-city';
import { PageType, PageUrl, RoleType } from '@/constants/app';
import { GROUPS } from '@/constants/group';
import useInviteCode from '@/hooks/use-invite-code';
import useServiceUrls from '@/hooks/use-service-urls';
import { switchRoleType } from '@/utils/app';
import { openCustomerServiceChat } from '@/utils/common';
import { getCurrentCityCode } from '@/utils/location';
@ -23,6 +23,7 @@ const EXAMPLE_IMAGE = 'https://publiccdn.neighbourhood.com.cn/img/delegate-examp
const COMMENT_IMAGE = 'https://publiccdn.neighbourhood.com.cn/img/delegate-comments.png';
export default function BizService() {
const inviteCode = useInviteCode();
const serviceUrls = useServiceUrls();
const [value, setValue] = useState('0');
const handleClickDelegate = useCallback(() => {
@ -37,15 +38,18 @@ export default function BizService() {
const handleOpenService = useCallback(() => {
openCustomerServiceChat('https://work.weixin.qq.com/kfid/kfcd60708731367168d');
}, []);
const handleSelectCity = useCallback(cityCode => {
const handleSelectCity = useCallback(
cityCode => {
if (!checkCityCode(cityCode)) {
return;
}
const group = GROUPS.find(g => String(g.cityCode) === cityCode);
const group = serviceUrls.find(g => String(g.cityCode) === cityCode);
if (group) {
openCustomerServiceChat(group.serviceUrl);
}
}, []);
},
[serviceUrls]
);
const handleChange = useCallback(v => {
setValue(v);
}, []);

View File

@ -1,10 +1,13 @@
import { RoleType, PageType } from '@/constants/app';
import { LocationInfo } from '@/types/location';
import { AppState } from '@/types/store';
import { CHANGE_ROLE_TYPE, CHANGE_HOME_PAGE, SET_LOCATION_INFO } from '../constants';
import { CHANGE_ROLE_TYPE, CHANGE_HOME_PAGE, SET_LOCATION_INFO, SET_SERVICE_URLS } from '../constants';
export const changeRoleType = (value: RoleType) => ({ type: CHANGE_ROLE_TYPE, value });
export const changeHomePage = (value: PageType) => ({ type: CHANGE_HOME_PAGE, value });
export const setLocationInfo = (value: LocationInfo) => ({ type: SET_LOCATION_INFO, value });
export const setServiceUrls = (value: AppState['serviceUrls']) => ({ type: SET_SERVICE_URLS, value });

View File

@ -6,3 +6,4 @@ export const SET_BIND_PHONE = 'SET_BIND_PHONE';
export const SET_USER_MESSAGE = 'SET_USER_MESSAGE';
export const SET_INVITE_CODE = 'SET_INVITE_CODE';
export const SET_JOB_ID = 'SET_JOB_ID';
export const SET_SERVICE_URLS = 'SET_SERVICE_URLS';

View File

@ -7,7 +7,7 @@ import { CacheKey } from '@/constants/cache-key';
import { LocationInfo } from '@/types/location';
import { AppState } from '@/types/store';
import { CHANGE_ROLE_TYPE, CHANGE_HOME_PAGE, SET_LOCATION_INFO } from '../constants';
import { CHANGE_ROLE_TYPE, CHANGE_HOME_PAGE, SET_LOCATION_INFO, SET_SERVICE_URLS } from '../constants';
const DEFAULT_LOCATION: LocationInfo = {
provinceCode: '440000',
@ -23,6 +23,7 @@ const INIT_STATE: AppState = {
roleType: defaultAppMode,
homePageType: defaultAppMode === RoleType.Company ? PageType.Anchor : PageType.JOB,
location: Taro.getStorageSync<LocationInfo>(CacheKey.CACHE_LOCATION_INFO) || DEFAULT_LOCATION,
serviceUrls: [],
};
const appState = (state: AppState = INIT_STATE, action: Action): AppState => {
@ -33,6 +34,8 @@ const appState = (state: AppState = INIT_STATE, action: Action): AppState => {
return { ...state, roleType: value };
case CHANGE_HOME_PAGE:
return { ...state, homePageType: value };
case SET_SERVICE_URLS:
return { ...state, serviceUrls: value };
case SET_LOCATION_INFO:
Taro.setStorageSync(CacheKey.CACHE_LOCATION_INFO, value);
return { ...state, location: value };

View File

@ -5,3 +5,5 @@ export const selectRoleType = (state: IState) => state.appState.roleType;
export const selectHomePageType = (state: IState) => state.appState.homePageType;
export const selectLocation = (state: IState) => state.appState.location;
export const selectServiceUrls = (state: IState) => state.appState.serviceUrls || {};

View File

@ -20,3 +20,14 @@ export interface GetCityCodeRequest {
latitude: number; // 纬度,浮点数,范围为-90~90负数表示南纬
longitude: number; // 经度,范围为-180~180负数表示西经
}
export interface CityOperatorListItem {
id: number;
staffId: number;
staffName: string;
cityName: string;
cityCode: string;
groupLink: string;
created: string;
updated: string;
}

View File

@ -87,3 +87,41 @@ export interface PartnerPagination<T> {
content: T[];
totalPages: number;
}
export enum EarnType {
ORDER_PAYMENT_SHARE_L1 = 'ORDER_PAYMENT_SHARE_L1',
ORDER_PAYMENT_SHARE_L2 = 'ORDER_PAYMENT_SHARE_L2',
CHAT_ACTIVITY_SHARE_L1 = 'CHAT_ACTIVITY_SHARE_L1',
CHAT_ACTIVITY_SHARE_L2 = 'CHAT_ACTIVITY_SHARE_L2',
OTHER = 'OTHER',
}
export interface UserProfitListItem {
userId: string;
total: number;
earnType: EarnType;
amount: number;
updatedAt: string;
}
export interface GroupAuthCode {
shareUserId: string;
authCode: string;
}
export interface AuthedGroupInfo {
userId: string;
openGid: string;
groupId: string;
groupName: string;
groupAvatar: string;
authDate: string;
}
export interface DecryptOpenGidBody {
authCode?: string;
inviteCode?: string;
encryptedData: string;
iv: string;
}
export interface StaffInfo {
id: number;
staffName: string;
staffQrCode: string;
isDefault: 0 | 1;
}

View File

@ -17,4 +17,9 @@ export interface AppState {
roleType: RoleType;
homePageType: PageType;
location: LocationInfo;
serviceUrls: Array<{
title: string;
cityCode: number;
serviceUrl: string;
}>;
}

View File

@ -1,5 +1,7 @@
import Taro from '@tarojs/taro';
import { API } from '@/http/api';
export const isDev = () => process.env.NODE_ENV === 'development';
// export const isDev = () => true;
@ -13,7 +15,6 @@ export const isDesktop = (() => {
return info.platform === 'windows' || info.platform === 'mac';
})();
export const logWithPrefix = isDev()
? (prefix: string) =>
(...args: BL.Anything[]) =>
@ -87,3 +88,9 @@ export const isValidIdCard = (idCard: string) =>
export const isValidPhone = (phone: string) => /^1[3-9]\d{9}$/.test(phone);
export const getScrollItemId = (id?: string) => (id ? `sid-${id}` : id);
export function buildUrl(url: API, params: Record<string, string | number>): API {
return Object.entries(params).reduce((result, [key, value]) => {
return result.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value));
}, url) as API;
}

View File

@ -6,9 +6,9 @@ import { CITY_CODE_TO_NAME_MAP, COUNTY_CODE_TO_NAME_MAP, PROVINCE_CODE_TO_NAME_M
import http from '@/http';
import { API } from '@/http/api';
import store from '@/store';
import { setLocationInfo } from '@/store/actions';
import { setLocationInfo, setServiceUrls } from '@/store/actions';
import { selectLocation } from '@/store/selector';
import { GetCityCodeRequest, LocationInfo } from '@/types/location';
import { CityOperatorListItem, GetCityCodeRequest, LocationInfo } from '@/types/location';
import { authorize, getWxSetting } from './wx';
@ -134,3 +134,16 @@ export async function requestLocation(force: boolean = false) {
store.dispatch(setLocationInfo(location));
return location;
}
export async function requestServiceUrls() {
const list = await http.post<CityOperatorListItem[]>(API.GET_ALL_CITY_OPERATOR);
store.dispatch(
setServiceUrls(
(list || []).map(it => ({
title: `${it.cityName}`,
cityCode: Number(it.cityCode),
serviceUrl: it.groupLink,
}))
)
);
}

View File

@ -7,16 +7,22 @@ import store from '@/store';
import { setInviteCode } from '@/store/actions/partner';
import { IPaginationRequest } from '@/types/common';
import {
AuthedGroupInfo,
DecryptOpenGidBody,
GetProfitRequest,
GroupAuthCode,
InviteUserInfo,
PartnerInviteCode,
PartnerPagination,
PartnerProfitItem,
PartnerProfitsState,
StaffInfo,
UserProfitListItem,
WithdrawRecord,
WithdrawResponse,
} from '@/types/partner';
import { requestUserInfo } from '@/utils/user';
import { buildUrl } from '@/utils/common';
export const getInviteCodeFromQuery = (query: Record<string, string>): string | undefined => {
if (query) {
@ -82,7 +88,7 @@ export const formatMoney = (cents: number) => {
const yuan = cents / 100;
return yuan.toFixed(2);
};
export function formatTimestamp(timestamp: string): string {
export function formatTimestamp(timestamp: string, dateOnly?: boolean): string {
// 创建 Date 对象
const date = new Date(/^\d+$/.test(timestamp) ? Number(timestamp) : timestamp);
@ -94,7 +100,7 @@ export function formatTimestamp(timestamp: string): string {
const mm = String(date.getMinutes()).padStart(2, '0');
// 拼接成所需的格式
return `${YYYY}.${MM}.${DD} ${HH}:${mm}`;
return dateOnly ? `${YYYY}.${MM}.${DD}` : `${YYYY}.${MM}.${DD} ${HH}:${mm}`;
}
export function formatUserId(input: string): string {
@ -127,3 +133,24 @@ export async function getWithdrawList(data: IPaginationRequest) {
contentType: 'application/x-www-form-urlencoded',
});
}
export async function getLastProfitList() {
const result = await http.get<UserProfitListItem[]>(API.GET_PROFIT_LIST);
return Array.isArray(result) ? result : [];
}
export async function generateGroupAuthCode() {
return await http.get<GroupAuthCode>(API.GENERATE_GROUP_AUTH_CODE);
}
export async function getAuthedGroupList() {
return await http.get<AuthedGroupInfo[]>(API.GET_AUTHED_GROUP_LIST);
}
export async function decryptOpenGid(data: DecryptOpenGidBody) {
return await http.post(API.DECRYPT_OPEN_GID, {
data,
});
}
export async function getStaffInfo(cityCode: string) {
const result = await http.post<StaffInfo[]>(buildUrl(API.GET_STAFF_CODE, { cityCode }));
return Array.isArray(result) && result.length ? result[0] : null;
}