feat: first commit

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

46
src/utils/app.ts Normal file
View File

@ -0,0 +1,46 @@
import { RoleType, PageUrl } from '@/constants/app';
import { CollectEventName } from '@/constants/event';
import { ANCHOR_TAB_LIST, COMPANY_TAB_LIST } from '@/hooks/use-config';
import http from '@/http';
import { API } from '@/http/api';
import store from '@/store';
import { changeRoleType, changeHomePage } from '@/store/actions';
import { selectRoleType } from '@/store/selector';
import { sleep } from '@/utils/common';
import { collectEvent } from '@/utils/event';
import { switchTab } from '@/utils/route';
import Toast from '@/utils/toast';
const postSwitchRoleType = (appMode: RoleType) => {
const data = { roleType: appMode };
return http.post(API.APP_MODE_SWITCH, { data });
};
export const getRoleType = () => selectRoleType(store.getState());
export const isAnchorMode = () => getRoleType() === RoleType.Anchor;
export const isCompanyMode = () => getRoleType() === RoleType.Company;
export const switchDefaultTab = async () => {
await sleep(1);
const mode = getRoleType();
const tabList = mode === RoleType.Anchor ? ANCHOR_TAB_LIST : COMPANY_TAB_LIST;
const item = tabList[0];
store.dispatch(changeHomePage(item.type));
switchTab(item.pagePath as PageUrl);
};
export const switchRoleType = async (appMode?: RoleType) => {
if (!appMode) {
const curMode = getRoleType();
appMode = curMode === RoleType.Anchor ? RoleType.Company : RoleType.Anchor;
}
try {
await postSwitchRoleType(appMode);
store.dispatch(changeRoleType(appMode));
} catch (e) {
collectEvent(CollectEventName.SWITCH_APP_MODE_FAILED);
Toast.error('切换失败请重试');
}
};

88
src/utils/common.ts Normal file
View File

@ -0,0 +1,88 @@
import Taro from '@tarojs/taro';
export const isDev = () => process.env.NODE_ENV === 'development';
export const isIPhone = (() => {
const info = Taro.getSystemInfoSync();
return info.platform === 'ios';
})();
export const isDesktop = (() => {
const info = Taro.getSystemInfoSync();
return info.platform === 'windows' || info.platform === 'mac';
})();
export const logWithPrefix = isDev()
? (prefix: string) =>
(...args: BL.Anything[]) =>
console.log(`[${prefix}]`, ...args)
: (_prefix: string) =>
(..._args: BL.Anything[]) => {};
export const safeJsonParse = <T = BL.Anything>(str: string, defaultValue: BL.Anything = {}): T => {
try {
return JSON.parse(str);
} catch (e) {
return defaultValue;
}
};
export const string2Number = (str: string, defaultValue: number = 0) => {
if (!str) {
return defaultValue;
}
const num = Number(str);
return Number.isNaN(num) ? defaultValue : num;
};
export const isUndefined = (v: BL.Anything) => typeof v === 'undefined';
export const sleep = (timeout: number = 3) => new Promise(resolve => setTimeout(resolve, timeout * 1000));
export const last = <T = BL.Anything>(list: T[]) => list[list.length - 1];
export function oncePromise<T extends BL.Anything[], R extends BL.Anything>(
func: AsyncFunction<T, R>
): AsyncFunction<T, R> {
let task: Promise<R> | null = null;
return async (...args: T): Promise<R> => {
if (task) {
console.log('[once promise]: has task pending');
return task;
}
try {
task = func(...args);
const result = await task;
return result;
} catch (e) {
throw e;
} finally {
task = null;
}
};
}
export const copy = (content: string): Promise<TaroGeneral.CallbackResult> => {
return new Promise((resolve, reject) => {
Taro.setClipboardData({
data: content,
success: resolve,
fail: reject,
});
});
};
export const openCustomerServiceChat = (url: string = 'https://work.weixin.qq.com/kfid/kfc291d0b01ecda3088') => {
Taro.openCustomerServiceChat({
extInfo: { url },
corpId: 'ww4f2f2888bf9a4f95',
});
};
export const isValidIdCard = (idCard: string) =>
/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(idCard);
export const isValidPhone = (phone: string) => /^1[3-9]\d{9}$/.test(phone);
export const getScrollItemId = (id?: string) => (id ? `sid-${id}` : id);

10
src/utils/company.ts Normal file
View File

@ -0,0 +1,10 @@
import http from '@/http';
import { API } from '@/http/api';
import { ICertificationRequest, ICertificationResponse } from '@/types/company';
import { requestUserInfo } from '@/utils/user';
export const postCertification = async (data: ICertificationRequest) => {
const result = await http.post<ICertificationResponse>(API.CERTIFICATION, { data });
await requestUserInfo();
return result;
};

26
src/utils/event.ts Normal file
View File

@ -0,0 +1,26 @@
import Taro from '@tarojs/taro';
import { CollectEventName, ReportEventId } from '@/constants/event';
import { logWithPrefix } from '@/utils/common';
import { getUserId } from '@/utils/user';
const log = logWithPrefix('event');
const logManager = Taro.getRealtimeLogManager();
const deviceInfo = Taro.getDeviceInfo();
export const collectEvent = (eventName: CollectEventName, params: BL.Anything = {}) => {
if (!logManager) return;
const collectInfo = {
...deviceInfo,
...params,
eventName,
userId: getUserId(),
};
logManager.info(collectInfo);
log('devEvent', collectInfo);
};
export const reportEvent = (eventId: ReportEventId, params: BL.Anything = {}) => {
Taro.reportEvent(eventId, params);
log('reportEvent', eventId, params);
};

55
src/utils/group.ts Normal file
View File

@ -0,0 +1,55 @@
import { GroupType } from '@/constants/group';
import http from '@/http';
import { API } from '@/http/api';
import {
BatchPublishGroup,
GetGroupDetailsRequest,
GetGroupsRequest,
GetGroupsResponse,
GroupDetail,
SimpleGroupInfo,
} from '@/types/group';
import { JobDetails } from '@/types/job';
import { getUserId } from '@/utils/user';
const typeToKeyMap = {
[GroupType.All]: 'allGroups',
[GroupType.Joined]: 'myJoinedGroups',
[GroupType.Created]: 'myCreatedGroups',
[GroupType.Followed]: 'myFollowedGroups',
};
export const getInviteGroupText = (groupDetail: GroupDetail) => {
return `请邀请我进群:${getUserId()}|${groupDetail.blGroupId}|${groupDetail.imGroupNick}|${groupDetail.robotId}`;
};
export const getConnectCustomerServiceText = (jobDetail: JobDetails, groupDetail: GroupDetail) => {
return `请帮我对接该通告:${getUserId()}|${groupDetail.blGroupId}|${groupDetail.robotId}|${jobDetail.publisher}|${jobDetail.id}`;
};
export async function requestGroupList(data: GetGroupsRequest) {
const type = data.type;
const result = await http.post<GetGroupsResponse>(API.GROUPS, { data });
const groupResults = result[typeToKeyMap[type]] || [];
return {
page: 1,
pageSize: groupResults.length,
hasMore: false,
groupResults,
};
}
export async function requestGroupDetail(blGroupId: GetGroupDetailsRequest['blGroupId']) {
return await http.post<GroupDetail>(API.GROUP_DETAIL, {
data: { blGroupId },
contentType: 'application/x-www-form-urlencoded',
});
}
export const requestBatchPublishGroups = async () => {
return http.post<BatchPublishGroup[]>(API.BATCH_PUBLISH_GROUP_LIST);
};
export const requestSimpleGroupList = async (cityCode?: string) => {
return http.post<SimpleGroupInfo[]>(API.SIMPLE_GROUP_LIST, { data: { cityCode } });
};

133
src/utils/job.ts Normal file
View File

@ -0,0 +1,133 @@
import Taro from '@tarojs/taro';
import { CacheKey } from '@/constants/cache-key';
import { CollectEventName } from '@/constants/event';
import { EmployType, JobManageStatus, UserJobType } from '@/constants/job';
import http from '@/http';
import { API } from '@/http/api';
import store from '@/store';
import { selectLocation } from '@/store/selector';
import {
JobDetails,
GetJobsDetailsRequest,
GetJobsRequest,
GetJobsResponse,
JobInfo,
GetUserJobRequest,
GetUserJobResponse,
MyDeclaredJobInfo,
MyBrowsedJobInfo,
GetMyRecommendJobRequest,
GetJobManagesRequest,
GetJobManagesResponse,
CreateJobInfo,
JobManageInfo,
} from '@/types/job';
import { collectEvent } from '@/utils/event';
import { getCityValues } from '@/utils/location';
export const isFullTimePriceRequired = (employType?: JobDetails['employType']) => {
return employType === EmployType.Full || employType === EmployType.All
}
export const isPartTimePriceRequired = (employType?: JobDetails['employType']) => {
return employType === EmployType.Part || employType === EmployType.All
}
export const getJobSalary = (data: Partial<JobDetails>) => {
const { salary, employType, lowPriceForFullTime, highPriceForFullTime, lowPriceForPartyTime, highPriceForPartyTime } =
data;
if (salary) {
return salary;
}
const fullSalary =
lowPriceForFullTime && highPriceForFullTime
? `${lowPriceForFullTime / 1000} - ${highPriceForFullTime / 1000}K/月`
: '';
const partSalary =
lowPriceForPartyTime && highPriceForPartyTime ? `${lowPriceForPartyTime} - ${highPriceForPartyTime}/小时` : '';
const salaries: string[] = [];
if (employType === EmployType.All) {
salaries.push(fullSalary, partSalary);
} else if (employType === EmployType.Full) {
salaries.push(fullSalary);
} else if (employType === EmployType.Part) {
salaries.push(partSalary);
}
return salaries.filter(Boolean).join('');
};
export const setLastSelectMyJobId = (jobId: string) => Taro.setStorageSync(CacheKey.LAST_SELECT_MY_JOB, jobId);
export const getLastSelectMyJobId = () => Taro.getStorageSync<string>(CacheKey.LAST_SELECT_MY_JOB);
export const getJobLocation = (data: JobManageInfo) => {
const cityValues = getCityValues([data.provinceCode, data.cityCode, data.countyCode], '-');
return cityValues ? cityValues : data.jobLocation;
};
export const getJobTitle = (data: JobInfo) => data.title || (data.sourceText || '').substring(0, 20);
async function requestMyDeclaredJobList(data: GetUserJobRequest) {
const result = await http.post<GetUserJobResponse<MyDeclaredJobInfo>>(API.MY_DECLARED_JOB_LIST, { data });
return result;
}
async function requestMyBrowsedJobList(data: GetUserJobRequest) {
const result = await http.post<GetUserJobResponse<MyBrowsedJobInfo>>(API.MY_BROWSED_JOB_LIST, { data });
return result;
}
export async function requestJobList(data: GetJobsRequest = {}) {
const isGetMyJob = data.isOwner || data.isFollow;
const url = isGetMyJob ? API.GET_MY_JOB_LIST_V2 : API.GET_JOB_LIST;
return await http.post<GetJobsResponse>(url, { data });
}
export async function requestJobDetail(jobId: GetJobsDetailsRequest['jobId']) {
const { longitude, latitude } = selectLocation(store.getState());
const data: GetJobsDetailsRequest = { jobId, longitude, latitude };
const detail = await http.post<JobDetails>(API.GET_JOB_DETAIL, { data });
if (!detail.publisher || !detail.sourceText) {
collectEvent(CollectEventName.DEBUG, { action: 'invalid_job_detail', response: detail, request: data });
}
return detail;
}
export async function requestUserJobList(data: GetUserJobRequest) {
const request = data.type === UserJobType.MyDeclared ? requestMyDeclaredJobList : requestMyBrowsedJobList;
const result = await request(data);
const dataMap = new Map();
Object.entries(result.data).forEach(([k, v]) => dataMap.set(k, v));
result.dataMap = dataMap;
return result;
}
export async function requestMyRecommendJobList(data: GetMyRecommendJobRequest) {
return await http.post<GetJobsResponse>(API.MY_RECOMMEND_JOB_LIST, { data });
}
export async function requestJobManageList(data: Partial<GetJobManagesRequest> = {}) {
return await http.post<GetJobManagesResponse>(API.GET_JOB_MANAGE_LIST, { data });
}
export async function requestHasPublishedJob() {
const data = await requestJobManageList({ status: JobManageStatus.Open });
return data.jobResults.length > 0;
}
export function postCreateJob(data: CreateJobInfo) {
return http.post(API.CREATE_JOB, { data });
}
export function postUpdateJob(data: CreateJobInfo) {
return http.post(API.UPDATE_JOB, { data });
}
export function postPublishJob(jobId: string) {
return http.post(API.PUBLISH_JOB, { data: { jobId }, contentType: 'application/x-www-form-urlencoded' });
}
export function postCloseJob(jobId: string) {
return http.post(API.CLOSE_JOB, { data: { jobId }, contentType: 'application/x-www-form-urlencoded' });
}

133
src/utils/location.ts Normal file
View File

@ -0,0 +1,133 @@
import Taro from '@tarojs/taro';
import { isNaN } from 'lodash-es';
import { CITY_CODE_TO_NAME_MAP, COUNTY_CODE_TO_NAME_MAP, PROVINCE_CODE_TO_NAME_MAP } from '@/constants/city';
import http from '@/http';
import { API } from '@/http/api';
import store from '@/store';
import { setLocationInfo } from '@/store/actions';
import { selectLocation } from '@/store/selector';
import { GetCityCodeRequest, LocationInfo } from '@/types/location';
import { authorize, getWxSetting } from './wx';
let locationInfo: Taro.getLocation.SuccessCallbackResult | null = null;
let waitAuthorizeLocationPromise: ReturnType<typeof authorize> | null = null;
const VALID_DISTANCE_KM = 999;
export const isValidLocation = (location: LocationInfo) => {
if (!location) {
return false;
}
return !isNaN(Number(location.latitude)) && !isNaN(Number(location.longitude));
};
export const isValidDistance = (distance: number) => distance < 1000 * VALID_DISTANCE_KM;
export const isNotNeedAuthorizeLocation = () => getWxSetting('scope.userLocation');
export const calcDistance = (distance: number, fractionDigits = 2) => {
if (isValidDistance(distance)) {
return `${(distance / 1000).toFixed(fractionDigits)}km`;
}
return '999km';
};
export const getCurrentCity = () => {
const cityCode = selectLocation(store.getState()).cityCode;
return CITY_CODE_TO_NAME_MAP.get(cityCode) || '';
};
export const getCityValues = (codes: string[] = [], separator: string = '') => {
const [province, city, county] = codes;
const values: string[] = [];
if (province) {
const v = PROVINCE_CODE_TO_NAME_MAP.get(province);
v && values.push(v);
}
if (city) {
const v = CITY_CODE_TO_NAME_MAP.get(city);
v && values.push(v);
}
if (county) {
const v = COUNTY_CODE_TO_NAME_MAP.get(county);
v && values.push(v);
}
return values.join(separator);
};
export async function waitLocationAuthorizeHidden() {
if (!waitAuthorizeLocationPromise) {
return;
}
await waitAuthorizeLocationPromise;
}
/**
* 获取经纬度信息
* @param opts
* @returns
*/
export async function getWxLocation(
opts?: Taro.getLocation.Option,
force = true
): Promise<Taro.getLocation.SuccessCallbackResult | null> {
if (locationInfo?.latitude) return Promise.resolve(locationInfo);
const notNeedAuthorize = await isNotNeedAuthorizeLocation();
if (!notNeedAuthorize) {
waitAuthorizeLocationPromise = authorize('scope.userLocation', '请授权获取位置权限', force);
if (!(await waitAuthorizeLocationPromise)) {
return null;
}
}
// 重新获取位置信息
return new Promise(resolve => {
try {
Taro.getLocation({
type: 'wgs84',
...opts,
success: res => {
setTimeout(() => {
locationInfo = null;
}, 31000);
if (res?.latitude) {
locationInfo = res;
return resolve(res);
}
resolve(null);
},
fail: () => resolve(null),
});
} catch (e) {
console.error(e);
}
});
}
export async function requestLocation(force: boolean = false) {
const data = {} as GetCityCodeRequest;
const lgInfo = await getWxLocation({}, force);
if (!lgInfo) {
return;
}
if (lgInfo?.latitude && lgInfo?.longitude) {
data.latitude = lgInfo.latitude;
data.longitude = lgInfo.longitude;
}
const location = await http.post<LocationInfo>(API.LOCATION, {
data,
contentType: 'application/x-www-form-urlencoded',
});
if (!location.latitude) {
location.latitude = data.latitude;
}
if (!location.longitude) {
location.longitude = data.longitude;
}
store.dispatch(setLocationInfo(location));
return location;
}

165
src/utils/material.ts Normal file
View File

@ -0,0 +1,165 @@
import Taro from '@tarojs/taro';
import { PageUrl } from '@/constants/app';
import { CollectEventName } from '@/constants/event';
import { GenderType, MaterialViewSource } from '@/constants/material';
import { MessageSubscribeIds } from '@/constants/subscribe';
import http from '@/http';
import { API } from '@/http/api';
import { RESPONSE_ERROR_INFO } from '@/http/constant';
import { HttpError } from '@/http/error';
import {
GetAnchorListRequest,
GetAnchorListResponse,
GetReadProfileRequest,
GetShareProfileRequest,
MaterialProfile,
MaterialVideoInfo,
UpdateProfileStatusRequest,
} from '@/types/material';
import { oncePromise } from '@/utils/common';
import { collectEvent } from '@/utils/event';
import { getJumpUrl } from '@/utils/route';
import { postSubscribe, subscribeMessage } from '@/utils/subscribe';
import Toast from '@/utils/toast';
import { getUserId, getUserInfo, requestUserInfo } from '@/utils/user';
export const sortVideos = (videos: MaterialVideoInfo[]) => {
return [...videos].sort((a, b) => {
const num1 = Number(!a.isDefault);
const num2 = Number(!b.isDefault);
return num1 - num2;
});
};
export const isProfileNotChange = (profile: MaterialProfile, newProfile: Partial<MaterialProfile>) => {
return Object.keys(newProfile).every(key => newProfile[key] === profile[key]);
};
const updateUserIfNeed = async () => {
const user = getUserInfo();
if (user.isCreateResume) {
return;
}
await requestUserInfo();
};
export const getBasicInfo = (profile: Pick<MaterialProfile, 'age' | 'height' | 'weight' | 'shoeSize' | 'gender'>) => {
const result: string[] = [];
if (typeof profile.age !== 'undefined' && profile.age !== null) {
result.push(`${profile.age}`);
}
if (typeof profile.height !== 'undefined' && profile.height !== null) {
result.push(`${profile.height}cm`);
}
if (typeof profile.weight !== 'undefined' && profile.weight !== null) {
result.push(`${profile.weight}kg`);
}
if (typeof profile.shoeSize !== 'undefined' && profile.shoeSize !== null) {
result.push(`${profile.shoeSize}`);
}
result.push(profile.gender === GenderType.MEN ? '男' : '女');
return result.join('·');
};
export const chooseMedia = async (option: Taro.chooseMedia.Option = {}) => {
try {
const result = await Taro.chooseMedia({
count: 1,
mediaType: ['mix'],
sourceType: ['album'],
...option,
});
return result;
} catch (e) {
return null;
}
};
export const requestVideoList = async () => {
const profile = await requestProfileDetail();
return profile?.materialVideoInfoList || [];
};
export const requestProfileDetail = oncePromise(async () => {
return http.post<MaterialProfile>(API.GET_PROFILE);
});
export const requestReadProfile = async (data: GetReadProfileRequest) => {
return http.post<MaterialProfile>(API.READ_PROFILE, { data });
};
export const requestProfileShareCode = async (resumeId: string) => {
return http.post<MaterialProfile>(API.GET_PROFILE_SHARE_CODE, {
data: { resumeId },
contentType: 'application/x-www-form-urlencoded',
});
};
export const requestShareProfile = async (data: GetShareProfileRequest) => {
return http.post<MaterialProfile>(API.VIEW_SHARE_PROFILE, { data, contentType: 'application/x-www-form-urlencoded' });
};
export const postVideos = async (materialVideoInfos: MaterialVideoInfo[]) => {
await http.post(API.SAVE_VIDEOS, { data: { materialVideoInfos } });
updateUserIfNeed();
};
export const postResumeText = async (resumeText: string) => {
await http.post(API.CREATE_PROFILE, { data: { resumeText } });
updateUserIfNeed();
};
export const updateProfile = async (profile: Partial<MaterialProfile>) => {
await http.post(API.UPDATE_PROFILE, { data: profile });
updateUserIfNeed();
};
export const updateProfileStatus = async (params: Omit<UpdateProfileStatusRequest, 'userId'>) => {
const data = {
...params,
userId: getUserId(),
};
return http.post(API.UPDATE_PROFILE_STATUS, { data, contentType: 'application/x-www-form-urlencoded' });
};
export const subscribeMaterialMessage = async () => {
try {
const tempIds = MessageSubscribeIds;
const result = await subscribeMessage(tempIds);
const acceptTempIds = tempIds.filter(id => result[id] && result[id] === 'accept');
postSubscribe(tempIds, acceptTempIds);
return result;
} catch (e) {
console.error('subscribe message fail', e);
}
};
export const requestAnchorList = async (data: GetAnchorListRequest) => {
return http.post<GetAnchorListResponse>(API.GET_ANCHOR_LIST, { data });
};
export const getMaterialShareMessage = async (profile?: MaterialProfile | null, needShareCode: boolean = true) => {
if (!profile) {
return null;
}
try {
const { id, name = '', workedSecCategoryStr } = profile;
const shareCode = needShareCode ? await requestProfileShareCode(id) : undefined;
const title = `${name} ${workedSecCategoryStr ? `播过 ${workedSecCategoryStr}` : ''}`.trim();
return {
title,
path: getJumpUrl(PageUrl.MaterialView, { shareCode, resumeId: id, source: MaterialViewSource.Share }),
};
} catch (error: unknown) {
const e = error as HttpError;
const msg = RESPONSE_ERROR_INFO[e.errorCode!];
msg && Toast.info(msg);
// console.error('fetch share code failed', e.info?.());
collectEvent(CollectEventName.REQUEST_MATERIAL_SHARE_CODE_FAILED, {
profileId: profile.id,
error: e.info?.() || e.message,
});
return null;
}
};

131
src/utils/message.ts Normal file
View File

@ -0,0 +1,131 @@
import { PluginUrl } from '@/constants/app';
import { CollectEventName } from '@/constants/event';
import { MessageActionStatus, MessageType } from '@/constants/message';
import { MessageSubscribeIds, SubscribeTempId } from '@/constants/subscribe';
import http from '@/http';
import { API } from '@/http/api';
import store from '@/store';
import { setMessageInfo } from '@/store/actions';
import {
IChatMessage,
MainMessage,
IChatInfo,
GetNewChatMessagesRequest,
PostMessageRequest,
PostConfirmActionRequest,
IMessageStatus,
PostActionDetailRequest,
IChatActionDetail,
ChatWatchRequest,
} from '@/types/message';
import { getRoleType } from '@/utils/app';
import { logWithPrefix, oncePromise } from '@/utils/common';
import { collectEvent } from '@/utils/event';
import { navigateTo } from '@/utils/route';
import { postSubscribe, subscribeMessage } from '@/utils/subscribe';
import { getUserId } from '@/utils/user';
const log = logWithPrefix('message-utils');
export const isTextMessage = (message: IChatMessage) => message.type === MessageType.Text;
export const isTimeMessage = (message: IChatMessage) => message.type === MessageType.Time;
export const isJobMessage = (message: IChatMessage) => message.type === MessageType.Job;
export const isMaterialMessage = (message: IChatMessage) => message.type === MessageType.Material;
export const isExchangeMessage = (message: IChatMessage) =>
[MessageType.RequestAnchorContact, MessageType.RequestCompanyContact].includes(message.type);
export const isLocationMessage = (message: IChatMessage) => message.type === MessageType.Location;
export const requestRemainPushTime = oncePromise(() => {
// if (isDev) {
// return Promise.resolve(32);
// }
return http.post<number>(API.MESSAGE_REMAIN_PUSH_TIMES);
});
export const requestUnreadMessageCount = oncePromise(async () => {
const count = await http.post<number>(API.MESSAGE_UNREAD_COUNT);
log('request unread message success, count:', count);
store.dispatch(setMessageInfo({ count }));
return count;
});
export const requestMessageList = oncePromise(async () => {
const { data } = await http.post<{ data: MainMessage[] }>(API.MESSAGE_CHAT_LIST, { data: { userId: getUserId() } });
return data;
});
export const requestChatDetail = (chatId: string) => {
const data = { chatId, roleType: getRoleType() };
return http.post<IChatInfo>(API.MESSAGE_CHAT, { data, contentType: 'application/x-www-form-urlencoded' });
};
export const requestNewChatMessages = (params: GetNewChatMessagesRequest) => {
const data = { ...params, roleType: getRoleType() };
return http.post<IChatMessage[]>(API.MESSAGE_CHAT_NEW, { data });
};
export const requestChatActionStatus = (actionId: string) => {
return http.post<MessageActionStatus>(API.MESSAGE_GET_ACTION, {
data: { actionId },
contentType: 'application/x-www-form-urlencoded',
});
};
export const requestActionDetail = (data: PostActionDetailRequest) => {
return http.post<IChatActionDetail | null>(API.MESSAGE_GET_ACTION_DETAIL, { data });
};
export const requestMessageStatusList = async (chatId: string) => {
return http.post<IMessageStatus[]>(API.MESSAGE_LIST_STATUS, {
data: { chatId },
contentType: 'application/x-www-form-urlencoded',
});
};
export const requestChatWatch = (data: Omit<ChatWatchRequest, 'status'>) => {
return http.post<boolean>(API.MESSAGE_CHAT_WATCH_GET, { data });
};
export const postAddMessageTimes = async (source: string) => {
const result = await subscribeMessage(MessageSubscribeIds);
const successIds: SubscribeTempId[] = [];
MessageSubscribeIds.forEach(id => {
result[id] === 'accept' && successIds.push(id);
});
collectEvent(CollectEventName.MESSAGE_DEV_LOG, { action: 'subscribe_new_message_reminder', source, successIds });
await postSubscribe(MessageSubscribeIds, successIds);
};
export const postCreateChat = (toUserId: string) => {
return http.post<IChatInfo>(API.MESSAGE_CREATE_CHAT, { data: { toUserId } });
};
export const postSendMessage = (data: PostMessageRequest) => {
if (data.type === MessageType.Text) {
return http.post(API.MESSAGE_SEND_TEXT, { data });
}
return http.post(API.MESSAGE_SEND_ACTION, { data });
};
export const posConfirmAction = (data: PostConfirmActionRequest) => {
return http.post<IChatInfo>(API.MESSAGE_CONFIRM_ACTION, { data });
};
export const postChatRejectWatch = (data: ChatWatchRequest) => {
return http.post<IChatInfo>(API.MESSAGE_CHAT_WATCH, { data });
};
export const isChatWithSelf = (toUserId: string) => {
return getUserId() === toUserId;
};
export const openLocationSelect = () => {
const key = 'UNCBZ-ZCSLZ-HRJX4-7P4XS-76G5H-6WF2Z';
const referer = '播络';
navigateTo(PluginUrl.LocationSelect, { key, referer });
};

108
src/utils/product.ts Normal file
View File

@ -0,0 +1,108 @@
import Taro from '@tarojs/taro';
import { ProductType, QrCodeType } from '@/constants/product';
import http from '@/http';
import { API, DOMAIN } from '@/http/api';
import {
ProductInfo,
GetProductDetailRequest,
GetProductIsUnlockRequest,
PostUseProductRequest,
GetProductIsUnlockResponse,
CustomerServiceInfo,
CreatePayInfoRequest,
CreatePayInfoResponse,
CreatePayOrderParams,
GetOrderInfoRequest,
OrderInfo,
} from '@/types/product';
import { getUserId } from '@/utils/user';
export const isCancelPay = err => err?.errMsg === 'requestPayment:fail cancel';
export const getOrderPrice = (price: number) => {
// return 1;
return price * 100;
};
export async function requestProductList() {
const data = { userId: getUserId() };
const list = await http.post<ProductInfo[]>(API.GET_PRODUCT_LIST, { data });
return list;
}
// 判断某个产品是否已经解锁
export async function requestProductUseRecord(
productCode: ProductType,
params: Omit<PostUseProductRequest, 'productCode' | 'userId'> = {}
) {
const data: GetProductIsUnlockRequest = { ...params, productCode, userId: getUserId() };
// 返回结果不是空对象则是已经解锁
return await http.post<GetProductIsUnlockResponse>(API.PRODUCT_USE_RECORD, {
data,
contentType: 'application/x-www-form-urlencoded',
});
}
// 使用某一个产品
export async function requestUseProduct(
productCode: ProductType,
params: Omit<PostUseProductRequest, 'productCode' | 'userId'> = {}
) {
const data: PostUseProductRequest = { ...params, productCode, userId: getUserId() };
return await http.post<ProductInfo>(API.USE_PRODUCT, { data });
}
// 获取某个产品的剩余解锁次数
export async function requestProductBalance(productCode: ProductType) {
const data: GetProductDetailRequest = { productCode, userId: getUserId() };
const { balance } = await http.post<ProductInfo>(API.GET_PRODUCT_DETAIL, {
data,
contentType: 'application/x-www-form-urlencoded',
});
return balance;
}
// 是否可以购买某一个产品
export async function requestAllBuyProduct(productCode: ProductType) {
const data: GetProductDetailRequest = { productCode, userId: getUserId() };
const enable = await http.post<ProductInfo>(API.ALLOW_BUY_PRODUCT, {
data,
contentType: 'application/x-www-form-urlencoded',
});
return enable;
}
export async function requestCsQrCode(_type: QrCodeType) {
const result = await http.post<CustomerServiceInfo>(API.CS_QR_CODE);
return `${DOMAIN}/${result.vxQrCode}`;
}
export async function requestCreatePayInfo(params: Omit<CreatePayInfoRequest, 'payChannel' | 'payType' | 'userId'>) {
const data: CreatePayInfoRequest = { ...params, payChannel: 1, payType: 1, userId: getUserId() };
const result = await http.post<CreatePayInfoResponse>(API.CREATE_PAY_ORDER, { data });
const createPayInfo = JSON.parse(result.createPayInfo) as CreatePayOrderParams;
return { ...result, createPayInfo } as CreatePayInfoResponse & { createPayInfo: CreatePayOrderParams };
}
export async function requestOrderInfo(params: Omit<GetOrderInfoRequest, 'userId'>) {
const data: GetOrderInfoRequest = { ...params, userId: getUserId() };
const result = await http.post<OrderInfo>(API.GET_PAY_ORDER, {
data,
contentType: 'application/x-www-form-urlencoded',
});
return result;
}
export async function requestPayment(params: Taro.requestPayment.Option) {
return new Promise((resolve, reject) =>
Taro.requestPayment({
...params,
fail: reject,
success: res => {
params.success?.(res);
resolve(res);
},
})
);
}

106
src/utils/qiniu-upload.ts Normal file
View File

@ -0,0 +1,106 @@
import { UploadTask } from '@tarojs/taro';
import http from '@/http';
import { API } from '@/http/api';
import { logWithPrefix } from '@/utils/common';
type RegionCode = 'ECN' | 'NCN' | 'SCN' | 'NA' | 'ASG' | '';
interface Config {
token: string;
bucket: string;
path: string;
uploadUrl?: string;
}
const log = logWithPrefix('qiniu-upload');
// https://github.com/gpake/qiniu-wxapp-sdk
class QiniuUpload {
// bucket 所在区域。ECN, SCN, NCN, NA, ASG分别对应七牛云的华东华南华北北美新加坡 5 个区域
private qiniuRegion: RegionCode = 'ECN';
private config: Config | null = null;
private getUploadUrl() {
let uploadURL: string;
switch (this.qiniuRegion) {
case 'NCN':
uploadURL = 'https://up-z1.qiniup.com';
break;
case 'SCN':
uploadURL = 'https://up-z2.qiniup.com';
break;
case 'NA':
uploadURL = 'https://up-na0.qiniup.com';
break;
case 'ASG':
uploadURL = 'https://up-as0.qiniup.com';
break;
case 'ECN':
default:
uploadURL = 'https://up-z0.qiniup.com';
break;
}
return uploadURL;
}
private requestUploadConfig = async () => {
this.config = await http.post<Config>(API.GET_QI_NIU_TOKEN);
return this.config;
};
private getRandomFileName = (length: number = 32) => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
// 结合时间戳
const timestamp = Date.now().toString(36); // 将时间戳转换为36进制
result += timestamp;
// 随机补全到指定长度
while (result.length < length) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters[randomIndex];
}
// 截取到指定长度
return result.substring(0, length);
};
init = () => {
this.requestUploadConfig();
};
upload = async (filePath: string, onProgress?: UploadTask.OnProgressUpdateCallback, prefixName?: string) => {
let config = this.config;
if (!config) {
config = await this.requestUploadConfig();
}
const { token, bucket, path: folderPath } = config;
const fileName = `${prefixName ? `${prefixName}-` : ''}${this.getRandomFileName()}`;
const url = config.uploadUrl || this.getUploadUrl();
const formData = {
token,
bucket,
key: `${folderPath}${fileName}`,
};
log('upload fileName', fileName);
const result = await http.upload<{ hash: string; key: string }>(url, {
filePath,
name: 'file',
formData,
onProgress,
});
log('upload result:', result);
return result.key;
};
}
const qiniuUpload = new QiniuUpload();
export default qiniuUpload;

63
src/utils/route.ts Normal file
View File

@ -0,0 +1,63 @@
import Taro from '@tarojs/taro';
import { PageUrl, PluginUrl } from '@/constants/app';
import { logWithPrefix, safeJsonParse } from '@/utils/common';
type ValidUrl = PageUrl | PluginUrl;
const log = logWithPrefix('route-utils');
function completeUrl(path: string) {
if (path.startsWith('pages')) {
return `/${path}`;
}
return path;
}
export function getJumpUrl(page: ValidUrl, params: Record<string, BL.Anything> = {}) {
const query = Object.entries(params)
.filter(([_k, v]) => typeof v !== 'undefined')
.map(([k, v]) => `${k}=${typeof v === 'object' ? encodeURIComponent(JSON.stringify(v)) : v}`)
.join('&');
const url = completeUrl(`${page}${query ? `?${query}` : ''}`);
return url;
}
export function switchTab(page: PageUrl, params: Record<string, BL.Anything> = {}) {
const url = getJumpUrl(page, params);
log('switchTab', url);
return new Promise((resolve, reject) => {
Taro.switchTab({ url, success: resolve, fail: reject });
});
}
export function redirectTo(page: PageUrl, params: Record<string, BL.Anything> = {}) {
const url = getJumpUrl(page, params);
log('redirectTo', url);
return new Promise((resolve, reject) => {
Taro.redirectTo({ url, success: resolve, fail: reject });
});
}
export function navigateTo(page: ValidUrl, params: Record<string, BL.Anything> = {}) {
const url = getJumpUrl(page, params);
log('navigateTo', url);
return new Promise((resolve, reject) => {
Taro.navigateTo({ url, success: resolve, fail: reject });
});
}
export function navigateBack(delta: number = 1) {
return new Promise((resolve, reject) => {
Taro.navigateBack({ delta, success: resolve, fail: reject });
});
}
export function getPageQuery<T = Record<string, BL.Anything>>() {
const params = (Taro.getCurrentInstance().router?.params || {}) as T;
return params;
}
export function parseQuery<T = BL.Anything>(obj: string, defaultValue: BL.Anything = null) {
return safeJsonParse<T>(decodeURIComponent(obj), defaultValue);
}

24
src/utils/share.ts Normal file
View File

@ -0,0 +1,24 @@
import type { ShareAppMessageReturn } from '@tarojs/taro';
import { PageUrl } from '@/constants/app';
import { getJumpUrl } from './route';
const imageUrl = 'https://neighbourhood.cn/share_d.jpg';
const getRandomCount = () => {
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const seed = (year + month + day) * day;
return (seed % 300) + 500;
};
export const getCommonShareMessage = (useCapture: boolean = true): ShareAppMessageReturn => {
return {
title: `昨天新增了${getRandomCount()}条主播通告,宝子快来看看`,
path: getJumpUrl(PageUrl.Job),
imageUrl: useCapture ? undefined : imageUrl,
};
};

41
src/utils/subscribe.ts Normal file
View File

@ -0,0 +1,41 @@
import Taro from '@tarojs/taro';
import { SubscribeTempId } from '@/constants/subscribe';
import http from '@/http';
import { API } from '@/http/api';
import { logWithPrefix } from '@/utils/common';
const log = logWithPrefix('subscribe-utils');
export const isSubscribeRefused = async (tempId: SubscribeTempId | SubscribeTempId[]) => {
tempId = Array.isArray(tempId) ? tempId : [tempId];
const { subscriptionsSetting } = await Taro.getSetting({ withSubscriptions: true });
log('isSubscribeRefuse subscriptionsSetting:', subscriptionsSetting);
if (!subscriptionsSetting) {
return false;
}
const { mainSwitch, itemSettings = {} } = subscriptionsSetting;
if (!mainSwitch) {
return true;
}
return tempId.some(id => {
const item = itemSettings[id];
if (!item) {
return false;
}
return item === 'reject';
});
};
export const subscribeMessage = async (tempIds: SubscribeTempId[]) => {
return Taro.requestSubscribeMessage({
tmplIds: tempIds,
entityIds: [],
});
};
export const postSubscribe = (tempIds: SubscribeTempId[], acceptTempIds: SubscribeTempId[]) => {
const data = { templateIds: tempIds, subscribeIds: acceptTempIds };
log('postSubscribe', data);
return http.post(API.SUBSCRIBE, { data });
};

50
src/utils/time.ts Normal file
View File

@ -0,0 +1,50 @@
import dayjs from 'dayjs';
export function formatTime(time: number | string, template = 'YYYY-MM-DD'): string {
if (!time) {
return '';
}
time = Number(time);
return dayjs(time).format(template);
}
export function formatDate(time: number | string, template = 'YYYY-MM-DD'): string {
const now = dayjs();
const oneHour = now.subtract(1, 'hour').valueOf();
time = Number(time);
if (time > oneHour) {
return '刚刚';
}
const todayStart = now.startOf('date').valueOf();
if (time > todayStart) {
return '今天';
}
const yesterdayStart = now.subtract(1, 'days').startOf('date').valueOf();
if (time > yesterdayStart) {
return '昨天';
}
return dayjs(time).format(template);
}
export function activeDate(time: number | string): string {
const now = dayjs();
time = Number(time);
// const oneHour = now.subtract(1, 'hour').valueOf();
// if (time > oneHour) {
// return '刚刚活跃';
// }
// const todayStart = now.startOf('date').valueOf();
// if (time > todayStart) {
// return '今天活跃';
// }
const yesterdayStart = now.subtract(3, 'days').startOf('date').valueOf();
if (time > yesterdayStart) {
return '3日内活跃';
}
const weakStart = now.subtract(1, 'weeks').startOf('date').valueOf();
if (time > weakStart) {
return '7日内活跃';
}
return '';
}

24
src/utils/toast.ts Normal file
View File

@ -0,0 +1,24 @@
import Taro from '@tarojs/taro';
import { sleep } from '@/utils/common';
const Toast = {
info: async (title: string, duration: number = 1500, wait: boolean = false) => {
Taro.showToast({ title, icon: 'none', duration });
wait && (await sleep(duration / 1000));
},
success: async (title: string, duration: number = 1500, wait: boolean = false) => {
Taro.showToast({ title, icon: 'success', duration });
wait && (await sleep(duration / 1000));
},
error: async (title: string, duration: number = 1500, wait: boolean = false) => {
Taro.showToast({ title, icon: 'error', duration });
wait && (await sleep(duration / 1000));
},
loading: async (title: string, duration: number = 1500, wait: boolean = false) => {
Taro.showToast({ title, icon: 'loading', duration });
wait && (await sleep(duration / 1000));
},
};
export default Toast;

148
src/utils/user.ts Normal file
View File

@ -0,0 +1,148 @@
import Taro from '@tarojs/taro';
import { CacheKey } from '@/constants/cache-key';
import http from '@/http';
import { API } from '@/http/api';
import store from '@/store';
import { setUserInfo, setBindPhone } from '@/store/actions';
import { selectUserInfo } from '@/store/selector';
import {
FollowGroupRequest,
SetPhoneRequest,
UpdateUserInfoRequest,
UpdateUserInfoResponse,
UserInfo,
} from '@/types/user';
import { logWithPrefix } from '@/utils/common';
import Toast from '@/utils/toast';
let lastOpenMiniProgramTime: number | null = null;
const SHOW_LOGIN_GUIDE_OFFSET = 1 * 60 * 60 * 1000;
// const SHOW_LOGIN_GUIDE_OFFSET = 60 * 1000;
// const SHOW_MATERIAL_GUIDE_OFFSET = 1 * 1000;
const log = logWithPrefix('user-utils');
let showLoginGuide: boolean | null = null;
export const getUserInfo = () => selectUserInfo(store.getState());
export const getUserId = () => getUserInfo().userId;
// 无效则说明还没拉到数据
export const isValidUserInfo = (info: UserInfo) => !!info.userId;
export const isNeedLogin = (info: UserInfo) => !info.isBindPhone;
// export const isNeedLogin = (info: UserInfo) => !info.isBindPhone || info.userId === '534740874077898752';
export const updateLastLoginTime = () => {
lastOpenMiniProgramTime = Taro.getStorageSync<number>(CacheKey.LAST_OPEN_MINI_PROGRAM_TIME) ?? null;
const now = Date.now();
log(`updateLastLoginTime: lastOpenMiniProgramTime=${lastOpenMiniProgramTime}, now=${now}`);
Taro.setStorageSync(CacheKey.LAST_OPEN_MINI_PROGRAM_TIME, now);
};
/**
* 登录引导
* 非首次打开小程序后,如果没有绑定手机号,在打开小程序时自动弹出一次
* 指定时间间隔内只出现一次
* @param info
* @returns
*/
export const shouldShowLoginGuide = (info: UserInfo) => {
// if (1 < 2) {
// return true;
// }
if (!isValidUserInfo(info) || !isNeedLogin(info)) {
return false;
}
if (typeof showLoginGuide === 'boolean') {
return showLoginGuide;
}
if (!lastOpenMiniProgramTime) {
// 第一次打开不强制提示
showLoginGuide = false;
} else {
showLoginGuide = Date.now() - lastOpenMiniProgramTime > SHOW_LOGIN_GUIDE_OFFSET;
}
return showLoginGuide;
};
// export const shouldShowLoginDialog = (info: UserInfo) => {
// if (!isValidUserInfo(info) || !isNeedLogin(info)) {
// return false;
// }
// const cache = Taro.getStorageSync<boolean>(CacheKey.SHOW_LOGIN_DIALOG);
// return !cache;
// };
// export const setAlreadyShowLoginDialog = () => Taro.setStorageSync(CacheKey.SHOW_LOGIN_DIALOG, '1');
export const isNeedCreateMaterial = async () => {
let info = getUserInfo();
if (!isValidUserInfo(info)) {
info = await requestUserInfo();
}
return !info.isCreateResume;
};
export const ensureUserInfo = async (info: UserInfo, toast = true) => {
if (!isValidUserInfo(info)) {
try {
await requestUserInfo();
} catch (e) {
toast && Toast.error('请稍后再试');
return false;
}
}
return true;
};
export const dispatchUpdateUser = (userInfo: Partial<UserInfo>) => store.dispatch(setUserInfo(userInfo));
export async function requestUserInfo() {
const userInfo = await http.post<UserInfo>(API.USER);
dispatchUpdateUser(userInfo);
return userInfo;
}
export async function setPhoneNumber(params: SetPhoneRequest) {
try {
await http.post<string>(API.SET_PHONE, { data: params });
store.dispatch(setBindPhone(true));
} catch (e) {
Taro.showToast({ title: '绑定失败', icon: 'error' });
}
}
export async function updateUserInfo(params: Omit<UpdateUserInfoRequest, 'userId'>) {
try {
const data = { ...params, userId: getUserId() };
const {
avatarUrl,
nickName,
imAcctNo = params.imAcctNo,
} = await http.post<UpdateUserInfoResponse>(API.USER_UPDATE, { data });
const userInfoPayload: Partial<UserInfo> = { imAcctNo };
if (avatarUrl) {
userInfoPayload.avatarUrl = avatarUrl;
userInfoPayload.isDefaultAvatar = false;
}
if (nickName) {
userInfoPayload.nickName = nickName;
userInfoPayload.isDefaultNickname = false;
}
store.dispatch(setUserInfo(userInfoPayload));
} catch (e) {
Taro.showToast({ title: '更新失败', icon: 'error' });
}
}
export async function followGroup(blGroupId: FollowGroupRequest['blGroupId']) {
try {
const data: FollowGroupRequest = { blGroupId, userId: getUserId() };
await http.post(API.FOLLOW_GROUP, { data });
return true;
} catch (e) {
Taro.showToast({ title: '出错了,请重试', icon: 'error' });
return false;
}
}

25
src/utils/video.ts Normal file
View File

@ -0,0 +1,25 @@
import Taro, { UploadTask } from '@tarojs/taro';
import http from '@/http';
import { API } from '@/http/api';
import { GetVideoInfoRequest, UploadVideoResult } from '@/types/material';
import qiniuUpload from '@/utils/qiniu-upload';
export const commonUploadProgress: UploadTask.OnProgressUpdateCallback = res => {
if (res.progress >= 100) {
Taro.hideLoading();
return;
}
Taro.showLoading({ title: `上传${res.progress}%` });
};
export const uploadVideo = async (
filePath: string,
type: string,
onProgress?: UploadTask.OnProgressUpdateCallback,
prefixName?: string
) => {
const qiniuKey = await qiniuUpload.upload(filePath, onProgress, prefixName);
const data: GetVideoInfoRequest = { sourcePath: qiniuKey, type: type === 'video' ? 'VIDEO' : 'IMAGE' };
return http.post<UploadVideoResult>(API.GET_VIDEO_INFO, { data, contentType: 'application/x-www-form-urlencoded' });
};

54
src/utils/wx.ts Normal file
View File

@ -0,0 +1,54 @@
import Taro from '@tarojs/taro';
/**
* 获取微信配置
* @param scope
* @param tips
* @param force
* @returns
*/
export function getWxSetting(scope: keyof Taro.AuthSetting) {
return new Promise<boolean>(resolve => {
Taro.getSetting({
success(res) {
if (!res?.authSetting[scope]) {
resolve(false);
} else {
resolve(true);
}
},
fail() {
resolve(false);
},
});
});
}
export function authorize(scope: keyof Taro.AuthSetting, tips: string, force = true) {
return new Promise<boolean>(resolve => {
Taro.authorize({
scope,
success() {
return resolve(true);
},
fail() {
if (!force) return resolve(false);
Taro.showModal({
title: '提示',
content: tips,
showCancel: false,
success: () => {
Taro.openSetting({
success(result) {
if (!result.authSetting[scope]) resolve(false);
else resolve(true);
},
});
},
fail: () => resolve(false),
});
},
});
});
}