72 Commits

Author SHA1 Message Date
fde2027588 feat: 2025-10-26 22:23:14 +08:00
a179654898 feat: update 2025-10-20 07:58:50 +08:00
bc141fcf1b feat: share ticket 2025-10-15 20:46:42 +08:00
3d2b121b92 feat: 2025-10-15 20:44:20 +08:00
7ba04b27ff feat:hhhh 2025-10-06 11:49:41 +08:00
7f866c51be fea: update 2025-09-24 08:15:52 +08:00
fe33d0493c feat: update 2025-09-20 20:00:44 +08:00
87f8e1f7e2 feat: update 2025-09-20 19:52:56 +08:00
afa94e2c48 Merge branch 'trunk' into feat/material-list-updat 2025-09-20 18:28:53 +08:00
b8b33841d4 feat: 2025-09-20 18:25:58 +08:00
828e9b7512 feat: update style 2025-09-19 23:53:17 +08:00
1d8a0d7a33 feat: update 2025-09-19 23:49:31 +08:00
8c5fd1a86b feat: update 2025-09-19 22:55:21 +08:00
a4e03ae1bd Merge branch 'trunk' into feat/share-coupon 2025-09-14 21:38:42 +08:00
8e42fef4f7 feat: ps 2025-09-11 21:28:01 +08:00
6f7e78896a feat: distance 2025-09-10 21:37:20 +08:00
b9cd0a3e6d feat: update material card 2025-09-08 23:28:17 +08:00
c0d27a5ab2 feat: update img 2025-09-05 01:44:42 +08:00
6d76c82e96 feat: update 2025-09-03 23:50:03 +08:00
f2e7fd9d85 feat: 2025-09-01 18:44:26 +08:00
087e05a9a0 feat: update 2025-09-01 18:43:49 +08:00
aea8323d95 feat: 2025-08-31 22:43:00 +08:00
61686950bd feat: update 2025-08-28 14:47:02 +08:00
eea89263f2 Merge branch 'trunk' into feat/share-coupon 2025-08-28 14:45:50 +08:00
285db3f445 feat: wenan update 2025-08-28 14:40:48 +08:00
d0369bab36 feat: ui 2025-08-28 10:37:40 +08:00
898c6ab6ca Merge branch 'trunk' into feat/share-coupon 2025-08-28 08:56:37 +08:00
763a6245d2 feat: update 2025-08-28 08:56:26 +08:00
670a389f12 feat: share 2025-08-26 22:55:19 +08:00
2cb532c3d7 feat: 2025-08-24 17:35:13 +08:00
6a2c94c7cf feat: build 2025-08-17 13:44:41 +08:00
34f9c8d0e6 feat: 2025-08-16 13:02:46 +08:00
943f921e56 feat: 加群+一些bug+样式+job list改default sort 2025-08-15 00:41:44 +08:00
33ef940d6e 实人认证 2025-08-07 00:53:00 +08:00
61336b5282 feat: update tonggao 2025-08-07 00:24:49 +08:00
10e7f39c92 feat: update 2025-07-29 23:00:57 +08:00
1c1bb146fc feat: job publish 2025-07-28 23:35:51 +08:00
260e543fe6 feat: 报单优化 2025-07-27 23:23:29 +08:00
0cd1a46762 revert ad 2025-07-27 15:59:28 +08:00
1ddc8b46c9 feat: 去掉login guide 2025-07-12 19:42:22 +08:00
5820fa8d25 feat: 2025-07-12 15:43:19 +08:00
80846d507f feat: 广告还有角色异常报错 2025-07-12 15:28:40 +08:00
42d1208ee4 feat: get is vip 2025-07-07 22:08:51 +08:00
2c48a70b6d feat: 去掉月会员,改文案 2025-07-07 15:13:35 +08:00
e8cf28b6e9 feat: 广告 2025-07-07 01:15:50 +08:00
de7f0e14fe feat:群代发默认选项修改 + 加群卡片 2025-07-02 23:36:34 +08:00
de2f380cd9 feat: update 2025-06-27 22:46:24 +08:00
0020eb8dbe feat: add start to tab 2025-06-27 11:06:23 +08:00
b0dd660dde feat: 群代发改版 2025-06-27 10:56:44 +08:00
56cf10c768 feat: 2025-06-25 22:47:53 +08:00
0ec366cc2e feat: 分享的文案 + 点击头像看模卡 + 提现文案 + 协议弹窗 + 进群限制 2025-06-17 00:03:58 +08:00
828497fcd6 feat: resume 2025-06-16 00:29:17 +08:00
fd5b3dab97 feat: fix conflict 2025-06-15 01:10:47 +08:00
1b782ee82c Merge branch 'trunk' into feat/update-login 2025-06-15 01:09:35 +08:00
80c0d71921 feat: login whatever 2025-06-15 01:09:30 +08:00
c76027b4d9 feat: support click to open material page 2025-06-15 00:19:18 +08:00
c08b0bef95 feat: support click avatar jump to resume page 2025-06-15 00:14:48 +08:00
96eb46821e feat: withdraw 2025-06-14 23:45:47 +08:00
5acc25c8c9 feat: login 2025-06-08 22:57:23 +08:00
082c5483c5 feat: invite capa page 2025-06-05 23:32:04 +08:00
xd
6805b590c7 wip 2025-06-05 22:47:41 +08:00
ed99c7b1ae feat:n 2025-05-29 01:04:31 +08:00
fa30ec2988 Merge branch 'trunk' into feat/partner
* trunk:
  feat: login
2025-05-22 18:13:41 +08:00
6762973e14 feat: login 2025-05-22 18:08:31 +08:00
c0a2c66280 Merge branch 'trunk' into feat/partner
* trunk:
  feat: update
  feat: update
  feat: search city
  feat: s
  新页面
  feat: biz-service
2025-05-20 23:45:59 +08:00
8ebd3363b8 feat: update 2025-05-20 23:22:35 +08:00
c92ad08148 feat: update 2025-05-20 23:22:00 +08:00
74579f09e3 feat: search city 2025-05-20 23:19:46 +08:00
276107722e feat: s 2025-05-20 22:55:48 +08:00
9d6c89e0b6 feat: 暂时不请求 2025-05-20 00:57:13 +08:00
fc37a612cc 新页面 2025-05-20 00:50:07 +08:00
f60a753e5a feat: biz-service 2025-05-19 23:53:57 +08:00
168 changed files with 12537 additions and 14897 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist/ dist/
dist-dev/
deploy_versions/ deploy_versions/
.temp/ .temp/
.rn_temp/ .rn_temp/

View File

@ -12,6 +12,20 @@ const MockPlugin: PluginItem = [
} }
]; ];
const CIPluginOpt = () => {
const brand = process.env.BRAND as 'prod' | 'dev'; // 取环境变量
const map = {
prod: { appid: 'wxf0724a83f8e377d2', privateKeyPath: 'keys/private.wxf0724a83f8e377d2.key' },
dev: { appid: 'wx2d56597fd541f1a6', privateKeyPath: 'keys/private.wx2d56597fd541f1a6.key' },
};
return {
weapp: map[brand] || map.prod,
version: process.env.VERSION || '1.0.0',
desc: `build for ${brand}`,
};
};
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 // https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
export default defineConfig(async (merge, _env: ConfigEnv) => { export default defineConfig(async (merge, _env: ConfigEnv) => {
const baseConfig: UserConfigExport = { const baseConfig: UserConfigExport = {
@ -25,13 +39,15 @@ export default defineConfig(async (merge, _env: ConfigEnv) => {
828: 1.81 / 2 828: 1.81 / 2
}, },
sourceRoot: 'src', sourceRoot: 'src',
outputRoot: 'dist', outputRoot: process.env.OUTPUT_ROOT || 'dist',
plugins: ['@tarojs/plugin-html'], plugins: [['@tarojs/plugin-mini-ci', CIPluginOpt()], '@tarojs/plugin-html'],
// plugins: ['@tarojs/plugin-html', MockPlugin], // plugins: ['@tarojs/plugin-html', MockPlugin],
defineConstants: { defineConstants: {
'process.env.BRAND': JSON.stringify(process.env.BRAND),
}, },
copy: { copy: {
patterns: [ patterns: [
{from: 'project.config.dev.json',to: 'dist-dev/project.config.json'},
], ],
options: { options: {
} }

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArjidDs/HGidBTzyOssyhX/Yvlr+BZxO3Qqe1iFBIJYWVXwiJ
KFVZw14rPKnh6ORB9qpMq3+fYJ7VsodmobnVlTaFXxJ8yi+tVZSZyZPtHabu2JVO
qu7LX709p5NTFcrmOY6+NJA827+hg6V0Nl3b4CWLsouPHgiIxpqnLeaOka3lbPMq
vQuDeMCbQNFe5Jz7C0gc6dXwEpDExNX4WiVfi4UP5bcYiUKlbdben5i6qbFrZ4QK
woYoAIhr60jQGfB6Jg7YkbbAyh42XR3BMkf2TPQqieZM1/IKNuw2evtA2BqLJuiz
cQ1qNBBXczbJv9KjBVAzAJgT4InUyJF+QfFJ6wIDAQABAoIBAQCf2kohIPvqP0Jp
VYMJA0C+L8CSbLxqu2hWvFKP9Dl+VAct30MZwtZNr8W0L5uceHMdxDv8z2iI2rPw
ztOYaE5UTmYpSWk+imhcMbC5iAT0RAq6pqQ7qkboXVaIv2BoinabMSHMUNvGNRoZ
FGKAz7XDADAhbnOudt4ofBYQw47kkko8vaDZBJS6UGN+QVJpyBtJKohaXKC6HRva
2c8zDwVZlKMrPl4WB4cg0+WPv6vUZymv8U7flwY0xJBBWwZUxqbmjqbTg8BGb5Mu
tpIZRvTGNxbDu9Ms7mdYdGGe/PFmqgj1hOomeXvBlnYz+fmWHu5ElyPGBHD92N7x
Hdcopp4BAoGBANqQUfxzjGcBvSSSowehSvgVCnUFsTxSE8c9HQRX4LtMnLCo6c6m
Hxf+l67FyY5704EW29PLu5ETdLBm6pCaIk8aJo467l8M9BOHmE/CwNciloHxXa4h
/gUKex6EsFJoT+1gaCaDJeu0gDJl49QnpYOrma2CZ8vyG3+NCotuYturAoGBAMwP
8G2ObLZUFPZAfhGZrobUom2dCtJXenYupxSvJ/rNd7zY+F3pcyi461eMIkKFaxQX
bRyDi/LRCQRKrSDuw3Dt4Ne9gkpdlI/LUDTUUbWdDuCXxfsXSRAYHC1ae0rsiGQv
PaTotkDSjdAIKdgd0bmeufFZ2Ozmid2lHzSZfwrBAoGAQXSVSSClvLeVmJ1du8w/
MnrCrfctdB40lCemSAW1ZQJ1rqZ3lw7DIyF49buAOG63EeLxpMVQAAuul6HKv9GZ
Us1HHLNjbg3oDu093S07KXLPVk0dvCer9oCEDMi6MYPR5ApKRrZiJevhtLY0+/V/
qSY8GO2D/l0chR+NN+KwMo8CgYB4Y07s04G/b4uOXC4EJC6azQems2eXAJT29D8O
LOfhRybE+xxMC1YtSCtP0xTDOgMaL1fn4pPGBRzUs1nAfBxBm5VyshMT3HGxA7ml
8TzjcG0+g0wB/pLUM+UrvJ3QbgtpT5WGnOQpCg8wFCDbp3v7jnEUunUIDlp3sNrg
JU6WAQKBgDjcfRDtnmSxSiejsZaZM7gCAYlZq6WBuvQ6WF+nD9mzEgzkw3F+1cHC
Y905VPGb/vCPAV+JdN7LxnMn2G3JoWPqrhOIT6KgPgAYbVAq1hmLqXMjGi53n9uY
39GL4UgJkywIdEkla/8zNFNKZgQulWPDJqb/MFcVo3/VegEfwdbk
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1Tl6FYIIWB8b+OjYY9s6cOJf71Vn0J/u/kdq5CH/LMOdNrUh
fq3shAwdDkXmuRIpHU7Noxisu71tB2EumigIwBDRVyrdwPu/p6QWyaMgrh0bdtaC
LKW/j2teNU1MCSWyCDyEeZ9PJteK5+ieX+ZPDg17HLV2dISpB71AGyM461DIHCAV
uaanUJ4MYCtp9t7OFgrD4dNzDMH0lBJBczRQAmnit0GpJD/y2rfgtL+HaKpDN8rc
w80+3HVfhKq3niy/vBbI6alqFb1Mh9O5q6/c1tEWQKPn3TzneGIDhMupGzHEzrVR
j87+1cyYi8PtBdNMPls+zPn7MoajjrASFyo1hQIDAQABAoIBABHu652zehAvTaL5
snlzjs6/u2HdxQV+7hLk3tChK10QdsCLK24EIshYXHk+19Do/hxA7HqzMJP5spnU
Sp8cjt/YikLSkCQTCGOJIYGD/tOmDxFHqVsn9ZySYzIIdkSurvKSZHH2PCoAJuaq
Yeee7zc3WwtnF7dlEPJdsG6X38mqslzaLOuiftkHJ8hQ+1rWM9FFEp/S3pLfLB+7
jEQhuVk0WjLTqmWmKvi0Nz5tUibBpF1OHtCfIhPjb1N8k/bcv1jumETx+2PDxA62
zpPNGespBaET+jeEqNCqLO7Zq8+iUO8d4XpZsFCR2cyE6zzXK2mhQ+2pW2BRU/DI
w/ZylmECgYEA6uPRJ4uk+NavoJlQCv7R9B8CGbwzbRaG8sg/NuwvTOO6D4FGUdcu
f8qA2kCd8bSCuWrS1mVgPpvzwHt5Bh3k7FC/0SfW5ZDC5Lp1IssI0jeka39vfPOD
ZdcvgJ3dUnHUrvb4L4PclgCa3KPXLTVDnK+4ExvaYOxNAaG6wROCGBsCgYEA6GMy
nCsPr21kKmhybCrhcTYoENb45hz6CCnXr4MGnYzF9c2ss2KXjR/boCdhF+3wxmmm
F4TKgcicOUfOJIXSIHBD9nphLa5CfrksLxfX0iIyNCG74aXIWl6M6QgQZdWqV4qC
xYTtFYACELIb8qK/mraMP28c9BzDpwd909ZlAt8CgYEAmpXCIa+CrA3GWgBsMEBR
9sKthDha8/E+cOuexsubuZOp8OhYWiYuwQWJ1ZlvGRJ+cf6+sUDYnXvPaJ8aIA9u
iKQACWuZv02NJjEcRACxVoAuIl0lB2vPzydWaOP8M3UczuVUoxi6BLJzOMBPLb5/
N9BhaZoMF3uMloVUvhrTpH8CgYA9sQfgeXYChjHPKEcn/AI5Qb1FKyyMOikOPb1q
dIgvDw4GgQFV5diBuHFfhNS7u4jtw2I6YCA6b/05H5zUCUqNHgCM0FQ4AkjYx2O5
ZMUAnd9iA4qTDqWvh+Y0ftWLkztTXgv1T6J40LC7qezNbvMaqVd2B5NSIsi18MEw
mXvbVQKBgQC0Rh5TJEfeBWRoIixwFuDUgHYRsdk3SV1hymYO3aKBr0uD8Arv4v1M
XO6DchFjcpKuwScjXX5yEot5PJOUTPG2T2uBPnJggtNig2dnRzQDLM+bK0vVecoz
QL9YMOYv6DNH6Z2yEfMZHZLto9Oaqm9J62hZahZ2GukFLOMBGZAVhA==
-----END RSA PRIVATE KEY-----

View File

@ -10,29 +10,10 @@
"framework": "React" "framework": "React"
}, },
"scripts": { "scripts": {
"build:weapp": "taro build --type weapp", "build:weapp": "BRAND=prod taro build --type weapp",
"build:swan": "taro build --type swan", "build:dev:weapp": "BRAND=dev OUTPUT_ROOT=dist-dev taro build --type weapp --upload",
"build:alipay": "taro build --type alipay", "dev:weapp": "BRAND=prod npm run build:weapp -- --watch",
"build:tt": "taro build --type tt", "dev:dev:weapp": "BRAND=dev OUTPUT_ROOT=dist-dev taro build --type weapp --watch"
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"build:qq": "taro build --type qq",
"build:jd": "taro build --type jd",
"build:quickapp": "taro build --type quickapp",
"build:harmony-hybrid": "taro build --type harmony-hybrid",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch",
"dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch",
"test": "jest",
"lint:format": "npx prettier --write",
"lint:format-all": "npx prettier --write 'src/**/*.{ts,js,css,jsx,html,vue,tsx}'"
}, },
"browserslist": [ "browserslist": [
"last 3 versions", "last 3 versions",
@ -76,6 +57,7 @@
"@babel/core": "^7.8.0", "@babel/core": "^7.8.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@tarojs/cli": "4.0.9", "@tarojs/cli": "4.0.9",
"@tarojs/plugin-mini-ci": "^4.1.5",
"@tarojs/plugin-mock": "^0.0.9", "@tarojs/plugin-mock": "^0.0.9",
"@tarojs/taro-loader": "4.0.9", "@tarojs/taro-loader": "4.0.9",
"@tarojs/test-utils-react": "^0.1.1", "@tarojs/test-utils-react": "^0.1.1",
@ -95,6 +77,7 @@
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"jest": "^29.3.1", "jest": "^29.3.1",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",
"miniprogram-ci": "^2.1.14",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"prettier": "^3.3.1", "prettier": "^3.3.1",
"react-refresh": "^0.11.0", "react-refresh": "^0.11.0",
@ -109,5 +92,6 @@
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
"@tarojs/binding" "@tarojs/binding"
] ]
} },
"packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
} }

6883
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

47
project.config.dev.json Normal file
View File

@ -0,0 +1,47 @@
{
"miniprogramRoot": "./",
"description": "boluo app",
"setting": {
"urlCheck": true,
"es6": false,
"enhance": false,
"compileHotReLoad": false,
"postcss": false,
"minified": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"coverView": false,
"showShadowRootInWxmlPanel": false,
"packNpmRelationList": [],
"ignoreUploadUnusedFiles": true,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"srcMiniprogramRoot": "dist/",
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"libVersion": "3.4.6",
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wx2d56597fd541f1a6",
"simulatorPluginLibVersion": {}
}

View File

@ -16,7 +16,19 @@
"coverView": false, "coverView": false,
"showShadowRootInWxmlPanel": false, "showShadowRootInWxmlPanel": false,
"packNpmRelationList": [], "packNpmRelationList": [],
"ignoreUploadUnusedFiles": true "ignoreUploadUnusedFiles": true,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
}, },
"compileType": "miniprogram", "compileType": "miniprogram",
"srcMiniprogramRoot": "dist/", "srcMiniprogramRoot": "dist/",
@ -30,5 +42,6 @@
"ignore": [], "ignore": [],
"include": [] "include": []
}, },
"appid": "wxf0724a83f8e377d2" "appid": "wxf0724a83f8e377d2",
} "simulatorPluginLibVersion": {}
}

View File

@ -12,4 +12,9 @@ page {
.taroify-tabs__wrap__scroll { .taroify-tabs__wrap__scroll {
.base-bg(); .base-bg();
} }
} }
@font-face {
font-family: 'Alimama ShuHeiTi';
src: url('https://publiccdn.neighbourhood.com.cn/font/almmheiti.ttf');
}

View File

@ -1,4 +1,4 @@
import { useLaunch } from '@tarojs/taro'; import Taro, { useDidShow, useLaunch } from '@tarojs/taro';
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
@ -6,17 +6,18 @@ import { Provider } from 'react-redux';
import { REFRESH_UNREAD_COUNT_TIME } from '@/constants/message'; import { REFRESH_UNREAD_COUNT_TIME } from '@/constants/message';
import http from '@/http'; import http from '@/http';
import store from '@/store'; import store from '@/store';
import { requestCityOperators } from '@/utils/location';
import { requestUnreadMessageCount } from '@/utils/message'; import { requestUnreadMessageCount } from '@/utils/message';
import { getInviteCode } from '@/utils/partner'; import { decryptOpenGid, getInviteCode, getInviteCodeFromQuery } from '@/utils/partner';
import qiniuUpload from '@/utils/qiniu-upload'; import qiniuUpload from '@/utils/qiniu-upload';
import { requestUserInfo, updateLastLoginTime } from '@/utils/user'; import { requestUserInfo, updateLastLoginTime } from '@/utils/user';
import './app.less'; import './app.less';
function App({ children }: PropsWithChildren<BL.Anything>) { function App({ children }: PropsWithChildren<BL.Anything>) {
useLaunch(async () => { useLaunch(async ({ query }) => {
console.log('App launched.'); console.log('App launched.');
await http.init(); await http.init(getInviteCodeFromQuery(query));
requestUserInfo().then(userInfo => { requestUserInfo().then(userInfo => {
if (userInfo.isPartner) { if (userInfo.isPartner) {
getInviteCode(); getInviteCode();
@ -28,6 +29,33 @@ function App({ children }: PropsWithChildren<BL.Anything>) {
setInterval(() => requestUnreadMessageCount(), REFRESH_UNREAD_COUNT_TIME); setInterval(() => requestUnreadMessageCount(), REFRESH_UNREAD_COUNT_TIME);
}); });
useDidShow(options => {
requestCityOperators();
console.log(options);
const inviteCode = getInviteCodeFromQuery(options?.query || {});
const authCode = options?.query?.authCode;
if (inviteCode || authCode) {
Taro.getGroupEnterInfo()
.then(info => {
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>; return <Provider store={store}>{children}</Provider>;
} }

View File

@ -0,0 +1,41 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.agreement-popup {
&__content {
.flex-column();
padding: 40px 32px;
}
&__title {
font-size: 32px;
font-weight: 500;
line-height: 48px;
color: @blColor;;
margin-bottom: 24px;
}
&__body {
font-weight: 400;
font-size: 28px;
line-height: 40px;
color: @blColor;
margin-bottom: 24px;
align-self: flex-start;
};
&__btn-group {
margin-top: 32px;
.flex-row();
gap: 30px;
}
&__confirm-button {
.button(@width: 230px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
}
&__cancel-button {
.button(@width: 230px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px, @highlight: 0);
}
}

View File

@ -0,0 +1,66 @@
import { Button, ITouchEvent } from '@tarojs/components';
import { Popup } from '@taroify/core';
import { useCallback } from 'react';
import { BindPhoneStatus } from '@/components/login-button';
import PhoneButton from '@/components/phone-button';
import { ProtocolPrivacy } from '@/components/protocol-privacy';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import './index.less';
interface IProps {
open: boolean;
onCancel: () => void;
onConfirm: (e: ITouchEvent) => void;
needPhone?: boolean;
needBindPhone?: boolean;
}
const PREFIX = 'agreement-popup';
export function AgreementPopup({ open, onCancel, onConfirm, needPhone, needBindPhone }: IProps) {
const handleBindPhone = useCallback(
(status: BindPhoneStatus, e: ITouchEvent) => {
if (status === BindPhoneStatus.Success) {
onConfirm(e);
} else {
onCancel();
}
},
[onCancel, onConfirm]
);
if (!open) {
return null;
}
return (
<Popup rounded className={PREFIX} placement="bottom" open={open} onClose={onCancel}>
<div className={`${PREFIX}__content`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__body`}>
<div></div>
<div>使</div>
</div>
<ProtocolPrivacy divider />
<div className={`${PREFIX}__btn-group`}>
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
</Button>
{needBindPhone ? (
<PhoneButton className={`${PREFIX}__confirm-button`} needPhone={needPhone} onBindPhone={handleBindPhone}>
</PhoneButton>
) : (
<Button className={`${PREFIX}__confirm-button`} onClick={onConfirm}>
</Button>
)}
</div>
</div>
<SafeBottomPadding />
</Popup>
);
}

View File

@ -9,7 +9,7 @@ import { PageUrl } from '@/constants/app';
import { MaterialViewSource, WORK_YEAR_LABELS } from '@/constants/material'; import { MaterialViewSource, WORK_YEAR_LABELS } from '@/constants/material';
import { AnchorInfo } from '@/types/material'; import { AnchorInfo } from '@/types/material';
import { calcDistance } from '@/utils/location'; import { calcDistance } from '@/utils/location';
import { getBasicInfo } from '@/utils/material'; import { getBasicInfo, getSalary } from '@/utils/material';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
import { activeDate } from '@/utils/time'; import { activeDate } from '@/utils/time';
@ -18,30 +18,23 @@ import './index.less';
interface IProps { interface IProps {
data: AnchorInfo; data: AnchorInfo;
jobId?: string; jobId?: string;
validator: (onSuccess: () => void) => void;
} }
const PREFIX = 'anchor-card'; const PREFIX = 'anchor-card';
const getSalary = (data: AnchorInfo) => {
const { fullTimeMinPrice, fullTimeMaxPrice, partyTimeMinPrice, partyTimeMaxPrice } = data;
const prices: string[] = [];
if (fullTimeMinPrice && fullTimeMaxPrice) {
prices.push(`${fullTimeMinPrice / 1000}-${fullTimeMaxPrice / 1000}K/月`);
}
if (partyTimeMinPrice && partyTimeMaxPrice) {
prices.push(`${partyTimeMinPrice}-${partyTimeMaxPrice}/小时`);
}
return prices.filter(Boolean).join(' ');
};
function AnchorCard(props: IProps) { function AnchorCard(props: IProps) {
const { data, jobId } = props; const { data, jobId, validator } = props;
const style = data.isRead ? ({ '--read-color': '#999999' } as React.CSSProperties) : {}; const style = data.isRead ? ({ '--read-color': '#999999' } as React.CSSProperties) : {};
const cover = (data.materialVideoInfoList.find(video => video.isDefault) || data.materialVideoInfoList[0])?.coverUrl; const cover = (data.materialVideoInfoList.find(video => video.isDefault) || data.materialVideoInfoList[0])?.coverUrl;
const handleClick = useCallback( const handleNavTo = useCallback(() => {
() => navigateTo(PageUrl.MaterialView, { jobId, resumeId: data.id, source: MaterialViewSource.AnchorList }), navigateTo(PageUrl.MaterialView, { jobId, resumeId: data.id, source: MaterialViewSource.AnchorList });
[data, jobId] }, [data, jobId]);
);
const handleClick = useCallback(() => {
validator(handleNavTo);
}, [handleNavTo, validator]);
return ( return (
<div className={PREFIX} style={style} onClick={handleClick}> <div className={PREFIX} style={style} onClick={handleClick}>

View File

@ -7,11 +7,16 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import AnchorCard from '@/components/anchor-card'; import AnchorCard from '@/components/anchor-card';
import ListPlaceholder from '@/components/list-placeholder'; import ListPlaceholder from '@/components/list-placeholder';
import LoginDialog from '@/components/login-dialog';
import { EventName } from '@/constants/app'; import { EventName } from '@/constants/app';
import { AnchorSortType } from '@/constants/material'; import { AnchorSortType } from '@/constants/material';
import useUserInfo from '@/hooks/use-user-info';
import { AnchorInfo, GetAnchorListRequest, IAnchorFilters } from '@/types/material'; import { AnchorInfo, GetAnchorListRequest, IAnchorFilters } from '@/types/material';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { requestAnchorList as requestData } from '@/utils/material'; import { requestAnchorList as requestData } from '@/utils/material';
import { getAgreementSigned, isNeedLogin, setAgreementSigned } from '@/utils/user';
import { AgreementPopup } from '../agreement-popup';
import './index.less'; import './index.less';
@ -55,6 +60,11 @@ function AnchorList(props: IAnchorListProps) {
const requestProps = useRef<IRequestProps>({}); const requestProps = useRef<IRequestProps>({});
const prevRequestProps = useRef<IRequestProps>({}); const prevRequestProps = useRef<IRequestProps>({});
const onListEmptyRef = useRef(onListEmpty); const onListEmptyRef = useRef(onListEmpty);
const [openLogin, setLoginOpen] = useState(false);
const [openAssignment, setAssignmentOpen] = useState(false);
const successCallback = useRef<() => void>(() => {});
const userInfo = useUserInfo();
const needLogin = isNeedLogin(userInfo);
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
log('start pull refresh'); log('start pull refresh');
@ -120,6 +130,37 @@ function AnchorList(props: IAnchorListProps) {
[dataList] [dataList]
); );
const handleCancel = useCallback(() => {
setAssignmentOpen(false);
setAgreementSigned(false);
}, []);
const handleConfirm = useCallback(() => {
setAssignmentOpen(false);
setAgreementSigned(true);
successCallback.current();
}, []);
const handleLoginSuccess = useCallback(() => {
setLoginOpen(false);
successCallback.current();
}, []);
const handleLoginCancel = useCallback(() => {
setLoginOpen(false);
}, []);
const validator = (onSuccess: () => void) => {
successCallback.current = onSuccess;
if (!getAgreementSigned()) {
setAssignmentOpen(true);
} else if (needLogin) {
setLoginOpen(true);
} else {
onSuccess();
}
};
useEffect(() => { useEffect(() => {
onListEmptyRef.current = onListEmpty; onListEmptyRef.current = onListEmpty;
}, [onListEmpty]); }, [onListEmpty]);
@ -183,26 +224,35 @@ function AnchorList(props: IAnchorListProps) {
}, [jobId, ready, filters, cityCode, sortType]); }, [jobId, ready, filters, cityCode, sortType]);
return ( return (
<PullRefresh <>
className={classNames(`${PREFIX}__pull-refresh`, className)} <PullRefresh
loading={refreshing} className={classNames(`${PREFIX}__pull-refresh`, className)}
onRefresh={handleRefresh} loading={refreshing}
disabled={refreshDisabled || !ready} onRefresh={handleRefresh}
> disabled={refreshDisabled || !ready}
<List
hasMore={hasMore}
onLoad={handleLoadMore}
loading={loadingMore || refreshing}
disabled={loadMoreError || !ready}
fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
> >
{dataList.map(item => ( <List
<AnchorCard data={item} jobId={jobId} key={item.id} /> hasMore={hasMore}
))} onLoad={handleLoadMore}
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} /> loading={loadingMore || refreshing}
</List> disabled={loadMoreError || !ready}
</PullRefresh> fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined}
>
{dataList.map(item => (
<AnchorCard data={item} jobId={jobId} key={item.id} validator={validator} />
))}
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List>
</PullRefresh>
<AgreementPopup
open={openAssignment}
onCancel={handleCancel}
onConfirm={handleConfirm}
needBindPhone={needLogin}
/>
{openLogin && <LoginDialog onCancel={handleLoginCancel} onSuccess={handleLoginSuccess} />}
</>
); );
} }

View File

@ -5,34 +5,19 @@ import { isEqual } from 'lodash-es';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import PickerToolbar from '@/components/picker-toolbar'; import PickerToolbar from '@/components/picker-toolbar';
import { import { FULL_PRICE_OPTIONS, PART_PRICE_OPTIONS } from '@/constants/job';
EmployType, import { ALL_ANCHOR_READ_TYPES, ANCHOR_READ_TITLE_MAP, AnchorReadType } from '@/constants/material';
ALL_EMPLOY_TYPES,
FULL_PRICE_OPTIONS,
PART_PRICE_OPTIONS,
EMPLOY_TYPE_TITLE_MAP,
} from '@/constants/job';
import {
ALL_ANCHOR_READ_TYPES,
ALL_GENDER_TYPES,
ANCHOR_READ_TITLE_MAP,
AnchorReadType,
GENDER_TYPE_TITLE_MAP,
GenderType,
} from '@/constants/material';
import { IAnchorFilters } from '@/types/material'; import { IAnchorFilters } from '@/types/material';
import { isUndefined } from '@/utils/common';
import './index.less'; import './index.less';
type MoreFilter = Omit<IAnchorFilters, 'employType' | 'gender'>;
interface IProps { interface IProps {
value: IAnchorFilters; value: MoreFilter;
onConfirm: (newValue: IAnchorFilters) => void; onConfirm: (newValue: MoreFilter) => void;
} }
const PREFIX = 'anchor-picker'; const PREFIX = 'anchor-picker';
const getDefaultGender = (value: IAnchorFilters) => value.gender;
const getDefaultEmploy = (value: IAnchorFilters) => value.employType;
const getDefaultReadType = (value: IAnchorFilters) => value.readType; const getDefaultReadType = (value: IAnchorFilters) => value.readType;
const getDefaultCategory = (value: IAnchorFilters) => value.category || ''; const getDefaultCategory = (value: IAnchorFilters) => value.category || '';
const getSalaryValue = (value: IAnchorFilters, full: boolean) => { const getSalaryValue = (value: IAnchorFilters, full: boolean) => {
@ -47,9 +32,7 @@ const getSalaryValue = (value: IAnchorFilters, full: boolean) => {
function AnchorPicker(props: IProps) { function AnchorPicker(props: IProps) {
const { value, onConfirm } = props; const { value, onConfirm } = props;
const [gender, setGender] = useState<GenderType | undefined>(getDefaultGender(value));
const [readType, setReadType] = useState<AnchorReadType | undefined>(getDefaultReadType(value)); const [readType, setReadType] = useState<AnchorReadType | undefined>(getDefaultReadType(value));
const [employType, setEmployType] = useState<EmployType | undefined>(getDefaultEmploy(value));
const [fullSalary, setFullSalary] = useState(getSalaryValue(value, true)); const [fullSalary, setFullSalary] = useState(getSalaryValue(value, true));
const [partSalary, setPartSalary] = useState(getSalaryValue(value, false)); const [partSalary, setPartSalary] = useState(getSalaryValue(value, false));
const [category, setCategory] = useState(getDefaultCategory(value)); const [category, setCategory] = useState(getDefaultCategory(value));
@ -59,9 +42,7 @@ function AnchorPicker(props: IProps) {
}, []); }, []);
const handleClickReset = useCallback(() => { const handleClickReset = useCallback(() => {
setGender(undefined);
setReadType(undefined); setReadType(undefined);
setEmployType(undefined);
setFullSalary(null); setFullSalary(null);
setPartSalary(null); setPartSalary(null);
setCategory(''); setCategory('');
@ -83,10 +64,6 @@ function AnchorPicker(props: IProps) {
const handleClickConfirm = useCallback(() => { const handleClickConfirm = useCallback(() => {
const filters: IAnchorFilters = {}; const filters: IAnchorFilters = {};
if (!isUndefined(gender)) {
filters.gender = gender === GenderType.All ? undefined : gender;
}
employType && (filters.employType = employType);
readType && (filters.readType = readType); readType && (filters.readType = readType);
category && (filters.category = category); category && (filters.category = category);
if (fullSalary) { if (fullSalary) {
@ -98,34 +75,10 @@ function AnchorPicker(props: IProps) {
filters.highPriceForPartyTime = partSalary.maxSalary; filters.highPriceForPartyTime = partSalary.maxSalary;
} }
onConfirm(filters); onConfirm(filters);
}, [gender, employType, readType, category, fullSalary, partSalary, onConfirm]); }, [readType, category, fullSalary, partSalary, onConfirm]);
return ( return (
<div className={PREFIX}> <div className={PREFIX}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__container`}>
{ALL_GENDER_TYPES.map((type: GenderType) => (
<div
key={type}
onClick={() => setGender(type)}
className={classNames(`${PREFIX}__item`, { selected: type === gender })}
>
{GENDER_TYPE_TITLE_MAP[type]}
</div>
))}
</div>
<div className={`${PREFIX}__title`}>/</div>
<div className={`${PREFIX}__container`}>
{ALL_EMPLOY_TYPES.map(type => (
<div
key={type}
onClick={() => setEmployType(type)}
className={classNames(`${PREFIX}__item`, { selected: type === employType })}
>
{EMPLOY_TYPE_TITLE_MAP[type]}
</div>
))}
</div>
<div className={`${PREFIX}__title`}>/</div> <div className={`${PREFIX}__title`}>/</div>
<div className={`${PREFIX}__container`}> <div className={`${PREFIX}__container`}>
{ALL_ANCHOR_READ_TYPES.map(type => ( {ALL_ANCHOR_READ_TYPES.map(type => (

View File

@ -0,0 +1,13 @@
import Select, { ISelectProps } from '@/components/select';
import { EmployType, JOB_EMPLOY_TYPE_OPTIONS } from '@/constants/job';
interface IProps extends Omit<ISelectProps<EmployType>, 'options'> {
value: EmployType;
}
function EmployTypeSelect(props: IProps) {
const { value: selectValue, onSelect } = props;
return <Select options={JOB_EMPLOY_TYPE_OPTIONS} title="类型" value={selectValue} onSelect={onSelect} />;
}
export default EmployTypeSelect;

View File

@ -0,0 +1,13 @@
import Select, { ISelectProps } from '@/components/select';
import { GENDER_OPTIONS, GenderType } from '@/constants/material';
interface IProps extends Omit<ISelectProps<GenderType>, 'options'> {
value: GenderType;
}
function GenderSelect(props: IProps) {
const { value: selectValue, onSelect } = props;
return <Select options={GENDER_OPTIONS} title="性别" value={selectValue} onSelect={onSelect} />;
}
export default GenderSelect;

View File

@ -0,0 +1,85 @@
@import '@/styles/common.less';
.group-certification-list {
min-height: calc(100vh - 98rpx);
&__banner {
font-weight: 400;
font-size: 24px;
height: 74px;
padding: 32px 32px 25px;
line-height: 36px;
color: #999999;
background: #F5F6FA;
position: fixed;
z-index: 1;
top: 99px;
left: 0;
right: 0;
}
&__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

@ -1,18 +1,21 @@
import React from 'react'; import React from 'react';
import BaseTabBar from '@/components/tab-bar'; import BaseTabBar from '@/components/tab-bar';
import { PageType } from '@/constants/app';
import './index.less'; import './index.less';
interface IProps extends React.PropsWithChildren {} interface IProps extends React.PropsWithChildren {
type: PageType;
}
export default function HomePage(props: IProps) { export default function HomePage(props: IProps) {
const { children } = props; const { children, type } = props;
return ( return (
<React.Fragment> <React.Fragment>
{children} {children}
<BaseTabBar /> <BaseTabBar type={type} />
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -1,6 +1,6 @@
@import '@/styles/common.less'; @import '@/styles/common.less';
@import '@/styles/variables.less'; @import '@/styles/variables.less';
.job-card { .job-card {
&__container { &__container {
width: 100%; width: 100%;
@ -11,6 +11,16 @@
border-radius: 16px; border-radius: 16px;
background: #FFFFFF; background: #FFFFFF;
margin-bottom: 24px; margin-bottom: 24px;
position: relative;
}
&__mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.4);
} }
&__header { &__header {
@ -160,4 +170,4 @@
color: @blColorG2; color: @blColorG2;
} }
} }

View File

@ -5,11 +5,11 @@ import React, { useCallback } from 'react';
import { CertificationStatusIcon } from '@/components/certification-status'; import { CertificationStatusIcon } from '@/components/certification-status';
import { PageUrl } from '@/constants/app'; import { PageUrl } from '@/constants/app';
import { CITY_CODE_TO_NAME_MAP, COUNTY_CODE_TO_NAME_MAP } from '@/constants/city'; // import { CITY_CODE_TO_NAME_MAP, COUNTY_CODE_TO_NAME_MAP } from '@/constants/city';
import { CertificationStatusType } from '@/constants/company'; import { CertificationStatusType } from '@/constants/company';
import { EMPLOY_TYPE_TITLE_MAP, EmployType } from '@/constants/job'; import { EMPLOY_TYPE_TITLE_MAP, EmployType } from '@/constants/job';
import { JobInfo } from '@/types/job'; import { JobInfo } from '@/types/job';
import { LocationInfo } from '@/types/location'; // import { LocationInfo } from '@/types/location';
import { getJobSalary, getJobTitle } from '@/utils/job'; import { getJobSalary, getJobTitle } from '@/utils/job';
import { calcDistance } from '@/utils/location'; import { calcDistance } from '@/utils/location';
import { navigateTo, redirectTo } from '@/utils/route'; import { navigateTo, redirectTo } from '@/utils/route';
@ -23,16 +23,16 @@ interface IProps {
} }
const PREFIX = 'job-card'; const PREFIX = 'job-card';
const getCityDes = (location: LocationInfo) => { // const getCityDes = (location: LocationInfo) => {
if (!location) { // if (!location) {
return ''; // return '';
} // }
let des = CITY_CODE_TO_NAME_MAP.get(location.cityCode); // let des = CITY_CODE_TO_NAME_MAP.get(location.cityCode);
if (location.countyCode) { // if (location.countyCode) {
des += `-${COUNTY_CODE_TO_NAME_MAP.get(location.countyCode)}`; // des += `-${COUNTY_CODE_TO_NAME_MAP.get(location.countyCode)}`;
} // }
return des; // return des;
}; // };
function JobCard(props: IProps) { function JobCard(props: IProps) {
const { className, data, redirectOpen } = props; const { className, data, redirectOpen } = props;
@ -102,8 +102,9 @@ function JobCard(props: IProps) {
/> />
<div className={`${PREFIX}__publisher-name`}>{publisher}</div> <div className={`${PREFIX}__publisher-name`}>{publisher}</div>
</div> </div>
<div className={`${PREFIX}__city`}>{getCityDes(jobLocation)}</div> {/*<div className={`${PREFIX}__city`}>{getCityDes(jobLocation)}</div>*/}
</div> </div>
{data.isRead && <div className={`${PREFIX}__mask`} />}
</div> </div>
); );
} }

View File

@ -1,3 +1,5 @@
import Taro from '@tarojs/taro';
import { List, PullRefresh } from '@taroify/core'; import { List, PullRefresh } from '@taroify/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
@ -5,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import JobCard from '@/components/job-card'; import JobCard from '@/components/job-card';
import ListPlaceholder from '@/components/list-placeholder'; import ListPlaceholder from '@/components/list-placeholder';
import { EventName } from '@/constants/app';
import { JobType, EmployType, SortType } from '@/constants/job'; import { JobType, EmployType, SortType } from '@/constants/job';
import { JobInfo, GetJobsRequest } from '@/types/job'; import { JobInfo, GetJobsRequest } from '@/types/job';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
@ -35,6 +38,7 @@ function JobList(props: IJobListProps) {
visible = true, visible = true,
cityCode = 'ALL', cityCode = 'ALL',
category = JobType.All, category = JobType.All,
sourceType,
employType = EmployType.All, employType = EmployType.All,
sortType = SortType.RECOMMEND, sortType = SortType.RECOMMEND,
isFollow = false, isFollow = false,
@ -57,6 +61,23 @@ function JobList(props: IJobListProps) {
const prevRequestProps = useRef<IRequestProps>({}); const prevRequestProps = useRef<IRequestProps>({});
const onListEmptyRef = useRef(onListEmpty); const onListEmptyRef = useRef(onListEmpty);
const handleReadJob = useCallback(
(jobId: string) => {
const index = dataList.findIndex(d => String(d.id) === jobId);
if (index < 0) {
return;
}
const job = dataList[index];
if (!job || job.isRead) {
return;
}
log('auto mark read', jobId);
dataList.splice(index, 1, { ...job, isRead: true });
setDataList([...dataList]);
},
[dataList]
);
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
log('start pull refresh'); log('start pull refresh');
try { try {
@ -108,12 +129,20 @@ function JobList(props: IJobListProps) {
onListEmptyRef.current = onListEmpty; onListEmptyRef.current = onListEmpty;
}, [onListEmpty]); }, [onListEmpty]);
useEffect(() => {
Taro.eventCenter.on(EventName.VIEW_JOB_SUCCESS, handleReadJob);
return () => {
Taro.eventCenter.off(EventName.VIEW_JOB_SUCCESS, handleReadJob);
};
}, [handleReadJob]);
useEffect(() => { useEffect(() => {
log('request params changed'); log('request params changed');
requestProps.current = { requestProps.current = {
category, category,
cityCode, cityCode,
employType, employType,
sourceType,
sortType, sortType,
isFollow, isFollow,
isOwner, isOwner,
@ -128,6 +157,7 @@ function JobList(props: IJobListProps) {
}, [ }, [
category, category,
cityCode, cityCode,
sourceType,
employType, employType,
sortType, sortType,
isFollow, isFollow,
@ -176,7 +206,7 @@ function JobList(props: IJobListProps) {
} }
}; };
refresh(); refresh();
}, [visible, cityCode, employType, sortType, keyWord, minSalary, maxSalary, blGroupId]); }, [visible, cityCode, employType, sourceType, sortType, keyWord, minSalary, maxSalary, blGroupId]);
// log('render', `hasMore: ${hasMore}, loadingMore: ${loadingMore}, refreshing: ${refreshing}`); // log('render', `hasMore: ${hasMore}, loadingMore: ${loadingMore}, refreshing: ${refreshing}`);

View File

@ -46,8 +46,8 @@ function JobRecommendList(props: IJobListProps) {
setDataList([]); setDataList([]);
setLoading(true); setLoading(true);
setLoadError(false); setLoadError(false);
const { jobResults = [] } = await requestMyRecommendJobList({ ...requestProps.current }); const { jobResults } = await requestMyRecommendJobList({ ...requestProps.current });
setDataList(jobResults); setDataList(jobResults || []);
} catch (e) { } catch (e) {
setDataList([]); setDataList([]);
setLoadError(true); setLoadError(true);

View File

@ -0,0 +1,41 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.join-group-hint {
.flex-row();
background: #FFFFFF;
border-radius: 16px;
padding: 32px 24px;
margin-top: 24px;
margin-bottom: 24px;
&__icon {
width: 80px;
height: 80px;
margin-right: 32px;
}
&__left {
flex: 1;
padding-left: 8px;
&-title {
font-weight: 500;
font-size: 32px;
line-height: 40px;
margin-bottom: 16px;
}
&-desc {
font-weight: 400;
font-size: 28px;
line-height: 40px;
}
}
&__right {
flex: 0 0 auto;
}
&__button {
.button(@width: 186px, @height: 64px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 32px, @highlight: 0);
}
}

View File

@ -0,0 +1,63 @@
import { Button, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Plus } from '@taroify/icons';
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 useCityOperators from '@/hooks/use-city-operators';
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';
const DEFAULT_GROUP = {
name: '播络主播招聘群',
serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc60ac7b6420787a8',
};
export function JoinGroupHint() {
const cityCode = getCurrentCityCode();
const roleType = getRoleTypeWithDefault();
const cityOperators = useCityOperators();
const group = cityOperators.find(g => String(g.cityCode) === cityCode);
const [clicked, setClicked] = useState(!!Taro.getStorageSync(CacheKey.JOIN_GROUP_CARD_CLICKED));
const handleClick = useCallback(() => {
if (group && !checkCityCode(cityCode)) {
return;
}
openCustomerServiceChat(group ? group.groupLink : DEFAULT_GROUP.serviceUrl);
Taro.setStorageSync(CacheKey.JOIN_GROUP_CARD_CLICKED, true);
setClicked(true);
}, [cityCode, group]);
if (!validCityCode(cityCode) || clicked) {
return null;
}
return (
<div className={PREFIX}>
<Image
className={`${PREFIX}__icon`}
src="https://publiccdn.neighbourhood.com.cn/img/group-avatar.png"
mode="aspectFit"
/>
<div className={`${PREFIX}__left`}>
<div className={`${PREFIX}__left-title`}>
{group ? `${CITY_CODE_TO_NAME_MAP.get(cityCode)}主播招聘群` : DEFAULT_GROUP.name}
</div>
<div className={`${PREFIX}__left-desc`}>{roleType === RoleType.Anchor ? '高薪工作早知道' : '免费招主播'}</div>
</div>
<div className={`${PREFIX}__right`}>
<Button className={`${PREFIX}__button`} onClick={handleClick}>
<Plus />
</Button>
</div>
</div>
);
}

View File

@ -1,11 +1,12 @@
import { Button, ButtonProps } from '@tarojs/components'; import { Button, ButtonProps, ITouchEvent } from '@tarojs/components';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { AgreementPopup } from '@/components/agreement-popup';
import LoginDialog from '@/components/login-dialog'; import LoginDialog from '@/components/login-dialog';
import useUserInfo from '@/hooks/use-user-info'; import useUserInfo from '@/hooks/use-user-info';
import { isNeedLogin } from '@/utils/user'; import { getAgreementSigned, isNeedLogin, setAgreementSigned } from '@/utils/user';
import './index.less'; import './index.less';
@ -17,6 +18,7 @@ export enum BindPhoneStatus {
export interface ILoginButtonProps extends ButtonProps { export interface ILoginButtonProps extends ButtonProps {
needPhone?: boolean; needPhone?: boolean;
needAssignment?: boolean;
} }
const PREFIX = 'login-button'; const PREFIX = 'login-button';
@ -24,12 +26,50 @@ const PREFIX = 'login-button';
function LoginButton(props: ILoginButtonProps) { function LoginButton(props: ILoginButtonProps) {
const { className, children, needPhone, onClick, ...otherProps } = props; const { className, children, needPhone, onClick, ...otherProps } = props;
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const [visible, setVisible] = useState(false); const [loginVisible, setLoginVisible] = useState(false);
const [assignVisible, setAssignVisible] = useState(false);
const needLogin = isNeedLogin(userInfo); const needLogin = isNeedLogin(userInfo);
// 点击按钮时,协议同意也手机也授权了 -> 下一步
//
// 点击按钮时,协议同意但是手机授权没给 -> 弹登录
// -> 如果授权了手机号就进行下一步
// -> 不给授权没下一步
//
// 点击按钮时,协议不同意 -> 弹协议窗口
// -> 如果同意就进行下一步
// -> 不同意没下一步
const handleClick = useCallback(
(e: ITouchEvent) => {
if (!getAgreementSigned()) {
setAssignVisible(true);
} else if (needLogin) {
setLoginVisible(true);
} else if (onClick) {
onClick(e);
}
},
[needLogin, onClick]
);
const onSuccess = useCallback( const handleConfirm = useCallback(
(e: ITouchEvent) => {
setAssignVisible(false);
setAgreementSigned(true);
if (onClick) {
onClick(e);
}
},
[onClick]
);
const handleCancel = useCallback(() => {
setAgreementSigned(false);
setAssignVisible(false);
}, []);
const handleLoginSuccess = useCallback(
e => { e => {
setVisible(false); setLoginVisible(false);
onClick?.(e); onClick?.(e);
}, },
[onClick] [onClick]
@ -37,14 +77,18 @@ function LoginButton(props: ILoginButtonProps) {
return ( return (
<> <>
<Button <Button {...otherProps} className={classNames(PREFIX, className)} onClick={handleClick}>
{...otherProps}
className={classNames(PREFIX, className)}
onClick={needLogin ? () => setVisible(true) : onClick}
>
{children} {children}
</Button> </Button>
{visible && <LoginDialog onCancel={() => setVisible(false)} onSuccess={onSuccess} needPhone={needPhone} />} <AgreementPopup
open={assignVisible}
onCancel={handleCancel}
onConfirm={handleConfirm}
needBindPhone={needLogin}
/>
{loginVisible && (
<LoginDialog onCancel={() => setLoginVisible(false)} onSuccess={handleLoginSuccess} needPhone={needPhone} />
)}
</> </>
); );
} }

View File

@ -17,22 +17,4 @@
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px); .button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
margin-top: 40px; margin-top: 40px;
} }
&__cancel-button {
min-width: fit-content;
font-size: 28px;
line-height: 32px;
color: @blHighlightColor;
background: transparent;
border: none;
margin-top: 40px;
&::after {
border-color: transparent
}
}
&__checkbox {
margin-top: 40px;
}
} }

View File

@ -1,11 +1,6 @@
import { Button } from '@tarojs/components';
import { Dialog } from '@taroify/core'; import { Dialog } from '@taroify/core';
import { useCallback, useState } from 'react';
import PhoneButton, { IPhoneButtonProps } from '@/components/phone-button'; import PhoneButton, { IPhoneButtonProps } from '@/components/phone-button';
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
import Toast from '@/utils/toast';
import './index.less'; import './index.less';
@ -21,36 +16,20 @@ const PREFIX = 'login-dialog';
export default function LoginDialog(props: IProps) { export default function LoginDialog(props: IProps) {
const { title = '使用播络服务前,请先登录', needPhone, onSuccess, onCancel, onBindPhone } = props; const { title = '使用播络服务前,请先登录', needPhone, onSuccess, onCancel, onBindPhone } = props;
const [checked, setChecked] = useState(false);
const handleTipCheck = useCallback(() => {
Toast.info('请先阅读并同意协议');
}, []);
return ( return (
<Dialog open onClose={onCancel}> <Dialog open onClose={onCancel} className={PREFIX}>
<Dialog.Content> <Dialog.Content>
<div className={`${PREFIX}__container`}> <div className={`${PREFIX}__container`}>
<div className={`${PREFIX}__title`}>{title}</div> <div className={`${PREFIX}__title`}>{title}</div>
{!checked && ( <PhoneButton
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}> className={`${PREFIX}__confirm-button`}
onSuccess={onSuccess}
</Button> onBindPhone={onBindPhone}
)} needPhone={needPhone}
{checked && ( >
<PhoneButton
className={`${PREFIX}__confirm-button`} </PhoneButton>
onSuccess={onSuccess}
onBindPhone={onBindPhone}
needPhone={needPhone}
>
</PhoneButton>
)}
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
</Button>
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
</div> </div>
</Dialog.Content> </Dialog.Content>
</Dialog> </Dialog>

View File

@ -4,7 +4,7 @@
.material-card { .material-card {
padding: 32px 24px; padding: 32px 24px;
border-radius: 16px; border-radius: 16px;
background: #FFFFFF; background: #ffffff;
box-sizing: border-box; box-sizing: border-box;
&__header { &__header {
@ -16,6 +16,10 @@
.flex-row(); .flex-row();
} }
&__right {
color: @blHighlightColor;
}
&__title { &__title {
font-size: 32px; font-size: 32px;
line-height: 40px; line-height: 40px;
@ -50,7 +54,7 @@
&__body { &__body {
width: 100%; width: 100%;
height: 156px; height: 160px;
margin-top: 24px; margin-top: 24px;
.flex-column(); .flex-column();
justify-content: center; justify-content: center;
@ -80,38 +84,44 @@
} }
} }
&__scroll-view { &__info {
position: relative; .flex-row();
gap: 24px;
width: 100%; width: 100%;
height: 100%;
.flex-row();
&::-webkit-scrollbar { &-left {
display: none; width: 125px;
height: 160px;
} }
&-cover {
&::after { width: 100%;
content: '';
position: absolute;
right: 0;
width: 102px;
height: 100%; height: 100%;
background: linear-gradient(91.41deg, rgba(255, 255, 255, 0) 1.86%, #FFFFFF 99.47%); border-radius: 12px;
}
&-right {
.name {
font-style: normal;
font-weight: 500;
font-size: 28px;
line-height: 32px;
color: @blColor;
margin-bottom: 12px;
}
.info,
.worked {
font-weight: 400;
font-size: 24px;
line-height: 36px;
color: @blColorG2;
margin-bottom: 8px;
}
.salary {
font-weight: 500;
font-size: 24px;
line-height: 36px;
color: @blColor;
}
} }
} }
}
&__cover-list {
height: 100%;
.flex-row();
}
&__cover-image {
width: 120px;
height: 100%;
margin-right: 24px;
// 不知道为啥高度不对,可能 scroll-view 默认底部是滚动条高度?
margin-top: 38px;
border-radius: 8px;
}
}

View File

@ -1,4 +1,4 @@
import { Image, ScrollView } from '@tarojs/components'; import { Image } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import { Loading } from '@taroify/core'; import { Loading } from '@taroify/core';
@ -9,11 +9,12 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import LoginButton from '@/components/login-button'; import LoginButton from '@/components/login-button';
import { EventName, PageUrl } from '@/constants/app'; import { EventName, PageUrl } from '@/constants/app';
import { CollectEventName, ReportEventId } from '@/constants/event'; import { CollectEventName, ReportEventId } from '@/constants/event';
import { WORK_YEAR_LABELS } from '@/constants/material';
import useUserInfo from '@/hooks/use-user-info'; import useUserInfo from '@/hooks/use-user-info';
import { MaterialProfile } from '@/types/material'; import { MaterialProfile } from '@/types/material';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { collectEvent, reportEvent } from '@/utils/event'; import { collectEvent, reportEvent } from '@/utils/event';
import { requestProfileDetail, sortVideos } from '@/utils/material'; import { getBasicInfo, getSalary, requestProfileDetail, sortVideos } from '@/utils/material';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
import Toast from '@/utils/toast'; import Toast from '@/utils/toast';
import { isValidUserInfo } from '@/utils/user'; import { isValidUserInfo } from '@/utils/user';
@ -34,8 +35,9 @@ function MaterialCard(props: IProps) {
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState<MaterialProfile | null>(null); const [profile, setProfile] = useState<MaterialProfile | null>(null);
const refreshRef = useRef((_f?: boolean) => { }); const refreshRef = useRef((_f?: boolean) => {});
const hasMaterial = !!profile; const hasMaterial = !!profile;
const firstCover = sortVideos(profile?.materialVideoInfoList || [])[0];
const handleGoCreateProfile = useCallback(() => { const handleGoCreateProfile = useCallback(() => {
reportEvent(ReportEventId.CLICK_GO_TO_CREATE_MATERIAL); reportEvent(ReportEventId.CLICK_GO_TO_CREATE_MATERIAL);
@ -44,7 +46,7 @@ function MaterialCard(props: IProps) {
const handleGoProfile = useCallback(() => { const handleGoProfile = useCallback(() => {
if (!hasMaterial) { if (!hasMaterial) {
realtimeLogger.info('handleGoProfile noMaterial') realtimeLogger.info('handleGoProfile noMaterial');
return; return;
} }
navigateTo(PageUrl.MaterialProfile).catch(err => { navigateTo(PageUrl.MaterialProfile).catch(err => {
@ -67,7 +69,7 @@ function MaterialCard(props: IProps) {
try { try {
const profileDetail = await requestProfileDetail(); const profileDetail = await requestProfileDetail();
if (!profileDetail) { if (!profileDetail) {
realtimeLogger.info('getProfileDetail no profileDetail') realtimeLogger.info('getProfileDetail no profileDetail');
} }
setProfile(profileDetail); setProfile(profileDetail);
} catch (e) { } catch (e) {
@ -90,8 +92,10 @@ function MaterialCard(props: IProps) {
refreshRef.current?.(true); refreshRef.current?.(true);
}; };
Taro.eventCenter.on(EventName.CREATE_PROFILE, callback); Taro.eventCenter.on(EventName.CREATE_PROFILE, callback);
Taro.eventCenter.on(EventName.UPDATE_PROFILE, callback);
return () => { return () => {
Taro.eventCenter.off(EventName.CREATE_PROFILE, callback); Taro.eventCenter.off(EventName.CREATE_PROFILE, callback);
Taro.eventCenter.on(EventName.UPDATE_PROFILE, callback);
}; };
}, [userInfo]); }, [userInfo]);
@ -100,16 +104,11 @@ function MaterialCard(props: IProps) {
<div className={`${PREFIX}__header`}> <div className={`${PREFIX}__header`}>
<div className={`${PREFIX}__header__left`}> <div className={`${PREFIX}__header__left`}>
<div className={`${PREFIX}__header__title`}></div> <div className={`${PREFIX}__header__title`}></div>
{/* {profile && (
<div
className={`${PREFIX}__header__progress`}
>{`完成度${Math.min((profile.progressBar || 0) * 100, 100)}%`}</div>
)} */}
</div> </div>
{profile && ( {profile && (
<div className={`${PREFIX}__header__right`}> <div className={`${PREFIX}__header__right`}>
{/* <div className={`${PREFIX}__header__status`}>{profile?.isOpen ? '开放中' : '关闭'}</div> */} <div></div>
<ArrowRight className={`${PREFIX}__header__icon`} /> <ArrowRight color="#6D3DF5" className={`${PREFIX}__header__icon`} />
</div> </div>
)} )}
</div> </div>
@ -123,18 +122,21 @@ function MaterialCard(props: IProps) {
</div> </div>
)} )}
{!loading && hasMaterial && ( {!loading && hasMaterial && (
<ScrollView className={`${PREFIX}__scroll-view`} showScrollbar={false} enableFlex enhanced scrollX> <div className={`${PREFIX}__info`}>
<div className={`${PREFIX}__cover-list`}> {firstCover && (
{sortVideos(profile?.materialVideoInfoList || []).map(video => ( <div className={`${PREFIX}__info-left`}>
<Image <Image className={`${PREFIX}__info-cover`} mode="aspectFill" src={firstCover.coverUrl} />
className={`${PREFIX}__cover-image`} </div>
mode="aspectFit" )}
key={video.coverUrl} <div className={`${PREFIX}__info-right`}>
src={video.coverUrl} <div className="name">{profile?.name}</div>
/> <div className="info">
))} {WORK_YEAR_LABELS[profile?.workedYear] || ''}·{getBasicInfo({ ...profile, shoeSize: null })}
</div>
<div className="worked"> {profile?.workedSecCategoryStr}</div>
<div className="salary">{getSalary(profile)}</div>
</div> </div>
</ScrollView> </div>
)} )}
{loading && <Loading />} {loading && <Loading />}
</div> </div>

View File

@ -2,13 +2,13 @@ import { Image } from '@tarojs/components';
import classNames from 'classnames'; import classNames from 'classnames';
import { PropsWithChildren, useEffect, useState, useCallback } from 'react'; import { PropsWithChildren, useEffect, useState, useCallback } from 'react';
import { MaterialViewSource } from '@/constants/material';
import { PageUrl } from '@/constants/app';
import { MaterialViewSource } from '@/constants/material';
import useUserInfo from '@/hooks/use-user-info'; import useUserInfo from '@/hooks/use-user-info';
import { IChatMessage } from '@/types/message'; import { IChatMessage } from '@/types/message';
import { getScrollItemId } from '@/utils/common'; import { getScrollItemId } from '@/utils/common';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
import { PageUrl } from '@/constants/app';
import './index.less'; import './index.less';
@ -19,12 +19,13 @@ export interface IBaseMessageProps {
export interface IUserMessageProps extends PropsWithChildren, IBaseMessageProps { export interface IUserMessageProps extends PropsWithChildren, IBaseMessageProps {
isRead?: boolean; isRead?: boolean;
resumeId?: string;
} }
const PREFIX = 'base-message'; const PREFIX = 'base-message';
function BaseMessage(props: IUserMessageProps) { function BaseMessage(props: IUserMessageProps) {
const { id, message, isRead: isReadProps, children } = props; const { id, message, isRead: isReadProps, children, resumeId } = props;
const { userId } = useUserInfo(); const { userId } = useUserInfo();
const [isRead, setIsRead] = useState(message.isRead); const [isRead, setIsRead] = useState(message.isRead);
const isSender = message.senderUserId === userId; const isSender = message.senderUserId === userId;
@ -37,10 +38,12 @@ function BaseMessage(props: IUserMessageProps) {
// const timer = setTimeout(() => setIsRead(true), 1200); // const timer = setTimeout(() => setIsRead(true), 1200);
// return () => clearTimeout(timer); // return () => clearTimeout(timer);
// }, [isSender]); // }, [isSender]);
const handleClick = useCallback( const handleClick = useCallback(() => {
() => navigateTo(PageUrl.MaterialView, { resumeId: message.jobId, source: MaterialViewSource.Chat }), if (!resumeId || isSender) {
[message.jobId] return;
); }
navigateTo(PageUrl.MaterialView, { resumeId: resumeId, source: MaterialViewSource.Chat });
}, [resumeId, isSender]);
useEffect(() => { useEffect(() => {
if (isRead) { if (isRead) {
return; return;
@ -53,6 +56,7 @@ function BaseMessage(props: IUserMessageProps) {
<Image <Image
mode="aspectFit" mode="aspectFit"
className={`${PREFIX}__avatar`} className={`${PREFIX}__avatar`}
onClick={handleClick}
src={message.senderAvatarUrl || require('@/statics/png/default_avatar.png')} src={message.senderAvatarUrl || require('@/statics/png/default_avatar.png')}
/> />
<div className={`${PREFIX}__content-container`}> <div className={`${PREFIX}__content-container`}>

View File

@ -80,4 +80,8 @@
margin-top: 24px; margin-top: 24px;
} }
} }
}
&__no-subscription {
}
}

View File

@ -57,7 +57,9 @@ export function MessageNoTimesDialog(props: INoTimesProps) {
<Dialog className={NO_TIMES} onClose={onClose} open={open}> <Dialog className={NO_TIMES} onClose={onClose} open={open}>
<Dialog.Content> <Dialog.Content>
<div className={`${NO_TIMES}__title`}></div> <div className={`${NO_TIMES}__title`}></div>
<div className={`${NO_TIMES}__tips`}><span className="highlight"></span></div> <div className={`${NO_TIMES}__tips`}>
<span className="highlight"></span>
</div>
<div className={`${NO_TIMES}__body`}> <div className={`${NO_TIMES}__body`}>
<div className={`${NO_TIMES}__times`}>{`未读消息提醒剩余:${times}`}</div> <div className={`${NO_TIMES}__times`}>{`未读消息提醒剩余:${times}`}</div>
<Button className={`${NO_TIMES}__btn`} onClick={onClick}> <Button className={`${NO_TIMES}__btn`} onClick={onClick}>

View File

@ -12,7 +12,7 @@ import './index.less';
const PREFIX = 'partner-fragment-banner'; const PREFIX = 'partner-fragment-banner';
export default function PartnerBanner() { export default function PartnerBanner({ isBoss = false }) {
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const needPhone = isNeedPhone(userInfo); const needPhone = isNeedPhone(userInfo);
const isPartner = userInfo.isPartner; const isPartner = userInfo.isPartner;
@ -50,7 +50,11 @@ export default function PartnerBanner() {
<div className={PREFIX} onClick={handleClick}> <div className={PREFIX} onClick={handleClick}>
<Image <Image
className={`${PREFIX}__image`} className={`${PREFIX}__image`}
src="https://publiccdn.neighbourhood.com.cn/img/partner_banner.png" src={
isBoss
? 'https://publiccdn.neighbourhood.com.cn/img/partner_banner.png'
: 'https://publiccdn.neighbourhood.com.cn/img/partner-banner-boss.png'
}
mode="scaleToFill" mode="scaleToFill"
/> />
{needPhone && ( {needPhone && (

View File

@ -2,26 +2,53 @@
@import '@/styles/common.less'; @import '@/styles/common.less';
.partner-intro { .partner-intro {
padding-top: 112px; background: #eae5fb;
padding-bottom: calc(112px + constant(safe-area-inset-bottom)); padding-top: 271px;
padding-bottom: calc(112px + env(safe-area-inset-bottom)); position: relative;
padding-bottom: calc(142px + constant(safe-area-inset-bottom));
padding-bottom: calc(142px + env(safe-area-inset-bottom));
&__banner { &__bg {
background: fade(@blHighlightBg, 8); width: 100%;
height: 88px; height: 705px;
line-height: 88px; position: absolute;
text-align: center; top: 0;
font-size: 28px;
position: fixed;
top: 98px;
left: 0; left: 0;
right: 0; }
width: 100vw; &__slogan {
top: 66px;
left: 44px;
position: absolute;
font-family: 'Alimama ShuHeiTi';
font-style: normal;
font-weight: 700;
font-size: 44px;
line-height: 62px;
color: #333333;
> div {
margin-bottom: 12px;
}
&-highlight {
display: inline-block;
padding-left: 12px;
padding-right: 9px;
font-size: 64px;
line-height: 62px;
background: linear-gradient(87.53deg, #683DE3 0.4%, #39227D 84.55%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
} }
&__main { &__main {
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
position: relative;
z-index: 1;
} }
&__highlight { &__highlight {
@ -34,14 +61,73 @@
} }
&__block { &__block {
margin-bottom: 40px; margin-bottom: 12px;
position: relative;
} }
&__swiper {
&-wrapper {
margin-bottom: 8px;
background: #fff;
border-radius: 24px;
position: relative;
border: 1px solid #6d3df5;
}
&-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
&__swiper-item {
box-sizing: border-box;
height: 130px;
padding: 24px 32px;
&-time {
font-style: normal;
font-size: 28px;
line-height: 40px;
color: #333333;
font-weight: 400;
position: relative;
}
&-details {
margin-top: 5px;
white-space: nowrap;
}
&-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 { &__card {
background: #fff; background: #fff;
border-radius: 24px; border-radius: 24px;
padding: 24px 32px; padding: 68px 32px 44px;
} }
&__h1 { &__h1 {
@ -51,29 +137,81 @@
color: #1d2129; color: #1d2129;
margin-bottom: 16px; margin-bottom: 16px;
margin-top: 32px; margin-top: 32px;
position: relative;
z-index: 0;
&.no-dot {
&:after {
background: transparent;
}
}
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
} }
&:after {
content: '';
width: 28px;
height: 28px;
background: #ffd236;
border-radius: 50%;
position: absolute;
left: -6px;
top: -3px;
z-index: -1;
}
} }
&__body { &__body {
font-weight: 400; font-weight: 400;
font-size: 28px; font-size: 28px;
line-height: 40px; line-height: 40px;
color: @blColor;
&.grey {
color: @blColorG2;
}
&.center {
text-align: center;
}
} }
&__title { &__title {
margin-bottom: 24px; margin-left: 48px;
margin-right: 48px;
font-weight: 500; font-weight: 500;
font-size: 32px; font-size: 32px;
line-height: 32px; line-height: 80px;
height: 80px;
transform: translateY(50%);
background: #ffd236;
box-shadow: 0px 12px 0px #ffa35d;
border-radius: 60px;
color: #1d2129; color: #1d2129;
text-align: center;
position: relative;
}
&__recommend {
display: inline-flex;
line-height: 36px;
padding: 0 8px;
height: 36px;
background: rgb(255, 80, 81);
border-radius: 4px;
font-size: 24px;
color: #fff;
align-items: center;
position: absolute;
gap: 6px;
right: -25px;
top: -18px;
} }
&__special { &__special {
padding: 32px; padding-top: 75px;
.flex-column(); .flex-column();
.partner-intro__body { .partner-intro__body {
@ -96,6 +234,7 @@
&__footer { &__footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
z-index: 2;
width: 100vw; width: 100vw;
background: #ffffff; background: #ffffff;
padding: 12px 32px; padding: 12px 32px;
@ -108,17 +247,20 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
box-sizing: border-box; box-sizing: border-box;
justify-content: center;
} }
&__download-button { &__download-button {
flex: 1 1; //flex: 1 1;
.button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px; @highlight: 0); .button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px; @highlight: 0);
} }
&__share-button { &__share-button {
flex: 2 2; //flex: 2 2;
flex: 0 0 406px;
width: 406px;
.button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;); .button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;);
margin-left: 32px; //margin-left: 32px;
} }
} }

View File

@ -1,166 +1,166 @@
import { Button, Canvas } from '@tarojs/components'; import { Button, Canvas, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useCallback, useState } 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 { openCustomerServiceChat } from '@/utils/common';
import { getPartnerQrcode } from '@/utils/partner'; import { formatMoney, formatTimestamp, formatUserId, getLastProfitList } from '@/utils/partner';
import { navigateTo } from '@/utils/route';
import './index.less'; import './index.less';
const PREFIX = 'partner-intro'; const PREFIX = 'partner-intro';
export default function PartnerIntro() { export default function PartnerIntro() {
const [posterPath, setPosterPath] = useState(''); const handleConfirm = useCallback(() => {
const getQrcode = async () => { navigateTo(PageUrl.GroupOwnerCertificate);
try { }, []);
const data = await getPartnerQrcode(); // 假设 getPartnerQrcode 返回 ArrayBuffer
const base64 = Taro.arrayBufferToBase64(data);
return `data:image/png;base64,${base64}`;
} catch (error) {
console.error('获取二维码失败', error);
Taro.showToast({ title: '获取二维码失败', icon: 'none' });
throw error;
}
};
const saveCanvasToTempFile = (): Promise<string> => {
return new Promise((resolve, reject) => {
try {
const query = Taro.createSelectorQuery().select('#posterCanvas');
query.fields({ node: true }).exec(async res => {
const canvas = res[0].node;
const tempFilePath = await Taro.canvasToTempFilePath({
canvas,
x: 0,
y: 0,
width: 1500, // 实际绘制宽度
height: 2668, // 实际绘制高度
destWidth: 750, // 目标显示宽度
destHeight: 1334, // 目标显示高度
fileType: 'jpg',
});
setPosterPath(tempFilePath.tempFilePath);
resolve(tempFilePath.tempFilePath);
});
} catch (error) {
console.error('保存 Canvas 到临时文件失败', error);
Taro.showToast({ title: '保存 Canvas 失败', icon: 'none' });
reject(error);
}
});
};
const drawCanvas = (qrCode: string): Promise<string> => {
const query = Taro.createSelectorQuery().select('#posterCanvas');
return new Promise(resolve => {
query.fields({ node: true, size: true }).exec(async res => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
canvas.width = 1500;
canvas.height = 2668;
ctx.scale(2, 2);
// 绘制背景图片
const bgImage = canvas.createImage();
const poster = 'https://publiccdn.neighbourhood.com.cn/img/poster.png'
bgImage.src = poster;
bgImage.onload = () => {
ctx.drawImage(bgImage, 0, 0, 750, 1334);
const qrCodeImage = canvas.createImage();
qrCodeImage.src = qrCode; // 假设 getQrcode() 返回的是二维码图片的路径
qrCodeImage.onload = () => {
ctx.drawImage(qrCodeImage, 235, 894, 280, 280); // 绘制二维码,位置和大小
saveCanvasToTempFile().then(tempPath => {
resolve(tempPath);
});
};
};
bgImage.onerror = err => {
console.error(err);
};
});
});
};
const savePoster = async () => {
let filePath = posterPath;
if (!filePath) {
Taro.showLoading({ title: '正在生成海报' });
const qrCode = await getQrcode();
filePath = await drawCanvas(qrCode);
Taro.hideLoading();
}
const res = await Taro.getSetting();
const hasPermission = res.authSetting['scope.writePhotosAlbum'];
if (hasPermission === false) {
Taro.showModal({
title: '提示',
content: '需要访问相册权限才能保存图片,请前往设置开启权限',
showCancel: false,
success() {
Taro.openSetting();
},
});
} else {
try {
await Taro.authorize({ scope: 'scope.writePhotosAlbum' });
await Taro.saveImageToPhotosAlbum({ filePath });
Taro.showToast({ title: '保存成功', icon: 'success' });
} catch (error) {
console.error(error);
Taro.showToast({ title: '保存失败', icon: 'none' });
}
}
};
const handleOpenService = useCallback(() => { const handleOpenService = useCallback(() => {
openCustomerServiceChat('https://work.weixin.qq.com/kfid/kfc4fcf6b109b3771d7'); openCustomerServiceChat('https://work.weixin.qq.com/kfid/kfc4fcf6b109b3771d7');
}, []); }, []);
const handleJump = useCallback(() => {
navigateTo(PageUrl.PartnerShareVip);
}, []);
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(list);
timerRef.current = setTimeout(
() => {
getBannerList();
},
3000 * (list.length || 10)
);
}, []);
useEffect(() => {
getBannerList();
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, [getBannerList]);
return ( return (
<div className={PREFIX}> <div className={PREFIX}>
<div className={`${PREFIX}__banner`}> <Image
<span className={`${PREFIX}__highlight`}>75%</span> src="https://publiccdn.neighbourhood.com.cn/img/partner-intro-bg.png"
className={`${PREFIX}__bg`}
mode="aspectFill"
/>
<div className={`${PREFIX}__slogan`}>
<div></div>
<div>
<div className={`${PREFIX}__slogan-highlight`}>75%</div>
</div>
</div> </div>
<div className={`${PREFIX}__main`}> <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={`${item.userId}-${index}`}>
<div className={`${PREFIX}__swiper-item-time`}>{formatTimestamp(item.updatedAt)}</div>
<div className={`${PREFIX}__swiper-item-details`}>
<div className={`${PREFIX}__swiper-item-id`}>{formatUserId(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.amount, 1)}</div>
</div>
<div className={`${PREFIX}__swiper-item-info`}>
<div className="money">{formatMoney(item.total, 1)}</div>
</div>
</div>
</Swiper.Item>
))}
</Swiper>
</div>
<div className={`${PREFIX}__block`}> <div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}>
<div className={`${PREFIX}__recommend`}>
<GoodJob />
</div>
</div>
<div className={`${PREFIX}__card ${PREFIX}__special`}>
<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`}>3</div>
<div className={`${PREFIX}__card`}> <div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__h1`}></div> <div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body`}> <div className={`${PREFIX}__body`}>
<span className={`${PREFIX}__highlight`}>20%</span> <span className={`${PREFIX}__highlight`}>20%</span>
</div> </div>
<div className={`${PREFIX}__h1`}></div> <div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body`}> <div className={`${PREFIX}__body`}>
<span className={`${PREFIX}__highlight`}>50%</span> <span className={`${PREFIX}__highlight`}>50%</span>
</div> </div>
<div className={`${PREFIX}__h1`}></div> <div className={`${PREFIX}__h1`}></div>
<div className={`${PREFIX}__body`}> <div className={`${PREFIX}__body`}>
<span className={`${PREFIX}__highlight`}>5%</span> <span className={`${PREFIX}__highlight`}>5%</span>
</div> </div>
</div> </div>
</div> </div>
<div className={`${PREFIX}__block`}> <div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div> <div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card`}>
<div className={`${PREFIX}__body`}>
<div className={`${PREFIX}__h1`}></div>
<div></div>
<div className={`${PREFIX}__h1`}></div>
<div></div>
</div>
</div>
</div>
<div className={`${PREFIX}__block`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__card ${PREFIX}__special`}> <div className={`${PREFIX}__card ${PREFIX}__special`}>
<div className={`${PREFIX}__body`}></div> <div className={`${PREFIX}__h1 no-dot`}></div>
<div className={`${PREFIX}__body`}></div>
<Button className={`${PREFIX}__service`} onClick={handleOpenService}> <Button className={`${PREFIX}__service`} onClick={handleOpenService}>
</Button> </Button>
</div> </div>
<div className={`${PREFIX}__tip`}></div>
</div> </div>
</div> </div>
<Canvas id="posterCanvas" canvas-id="posterCanvas" type="2d" style="width: 750px; height: 1334px;" /> <Canvas id="posterCanvas" canvas-id="posterCanvas" type="2d" style="width: 750px; height: 1334px;" />
<div className={`${PREFIX}__footer`}> <div className={`${PREFIX}__footer`}>
<Button className={`${PREFIX}__download-button`} onClick={savePoster}> <Button className={`${PREFIX}__share-button`} onClick={handleJump}>
</Button>
<Button className={`${PREFIX}__share-button`} openType="share">
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -41,14 +41,18 @@
height: 131px; height: 131px;
width: 100%; width: 100%;
background: #fff; background: #fff;
padding: 24px 32px; padding: 24px 32px 0 32px;
box-sizing: border-box; box-sizing: border-box;
font-size: 28px; font-size: 28px;
&-border {
border-bottom: 1px solid #e6e7e8;
}
&-content { &-content {
.flex-row(); .flex-row();
width: 100%; width: 100%;
border-bottom: 1px solid #e6e7e8; padding-bottom: 24px;
} }
&-time-id { &-time-id {

View File

@ -12,6 +12,8 @@ import './index.less';
const PREFIX = 'partner-invite-list'; const PREFIX = 'partner-invite-list';
const log = logWithPrefix(PREFIX); const log = logWithPrefix(PREFIX);
const FIRST_PAGE = 0;
function PartnerList(props: { function PartnerList(props: {
refreshDisabled?: boolean; refreshDisabled?: boolean;
visible?: boolean; visible?: boolean;
@ -20,10 +22,12 @@ function PartnerList(props: {
onListEmpty?: () => void; onListEmpty?: () => void;
}) { }) {
const { className, listHeight, refreshDisabled, visible = true, onListEmpty } = props; const { className, listHeight, refreshDisabled, visible = true, onListEmpty } = props;
const [hasMore, setHasMore] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState(false); const [loadMoreError, setLoadMoreError] = useState(false);
const [dataList, setDataList] = useState<InviteUserInfo[]>([]); const [dataList, setDataList] = useState<InviteUserInfo[]>([]);
const currentPage = useRef<number>(FIRST_PAGE);
const onListEmptyRef = useRef(onListEmpty); const onListEmptyRef = useRef(onListEmpty);
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
@ -31,19 +35,44 @@ function PartnerList(props: {
try { try {
setRefreshing(true); setRefreshing(true);
setLoadMoreError(false); setLoadMoreError(false);
const list = await requestData(); const { content, totalPages } = await requestData({ page: 1 });
setDataList(list); setDataList(content);
!list.length && onListEmptyRef.current?.(); currentPage.current = 1;
setHasMore(currentPage.current < totalPages);
!content.length && onListEmptyRef.current?.();
log('pull refresh success'); log('pull refresh success');
} catch (e) { } catch (e) {
setDataList([]); setDataList([]);
setHasMore(false);
setLoadMoreError(true); setLoadMoreError(true);
currentPage.current = FIRST_PAGE;
log('pull refresh failed'); log('pull refresh failed');
} finally { } finally {
setRefreshing(false); setRefreshing(false);
} }
}, []); }, []);
const handleLoadMore = useCallback(async () => {
log('start load more', hasMore);
if (!hasMore) {
return;
}
setLoadMoreError(false);
setLoadingMore(true);
try {
const { totalPages, content } = await requestData({ page: currentPage.current + 1 });
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(() => { useEffect(() => {
onListEmptyRef.current = onListEmpty; onListEmptyRef.current = onListEmpty;
}, [onListEmpty]); }, [onListEmpty]);
@ -62,11 +91,14 @@ function PartnerList(props: {
setDataList([]); setDataList([]);
setLoadingMore(true); setLoadingMore(true);
setLoadMoreError(false); setLoadMoreError(false);
const list = await requestData(); const { totalPages, content } = await requestData({ page: 1 });
setDataList(list); setDataList(content);
!list.length && onListEmptyRef.current?.(); currentPage.current = 1;
setHasMore(currentPage.current < totalPages);
!content.length && onListEmptyRef.current?.();
} catch (e) { } catch (e) {
setDataList([]); setDataList([]);
setHasMore(false);
setLoadMoreError(true); setLoadMoreError(true);
} finally { } finally {
log('visible changed, refresh list data end'); log('visible changed, refresh list data end');
@ -90,22 +122,24 @@ function PartnerList(props: {
disabled={refreshDisabled} disabled={refreshDisabled}
> >
<List <List
hasMore={false} hasMore={hasMore}
onLoad={() => {}} onLoad={handleLoadMore}
loading={loadingMore || refreshing} loading={loadingMore || refreshing}
disabled={loadMoreError} disabled={loadMoreError || !visible}
fixedHeight={typeof listHeight !== 'undefined'} fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined} style={listHeight ? { height: `${listHeight}px` } : undefined}
> >
{dataList.map(item => ( {dataList.map(item => (
<div className={`${PREFIX}__item`} key={item.id || item.userId}> <div className={`${PREFIX}__item`} key={item.id || item.userId}>
<div className={`${PREFIX}__item-content`}> <div className={`${PREFIX}__item-border`}>
<div className={`${PREFIX}__item-time-id`}> <div className={`${PREFIX}__item-content`}>
<div className={`${PREFIX}__item-time`}>{formatTimestamp(item.created)}</div> <div className={`${PREFIX}__item-time-id`}>
<div className={`${PREFIX}__item-id`}>{formatUserId(item.userId)}</div> <div className={`${PREFIX}__item-time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item-id`}>{formatUserId(item.userId)}</div>
</div>
<div className={`${PREFIX}__item-created`}>{item.isCreateResume ? '已创建' : '未创建'}</div>
<div className={`${PREFIX}__item-joined`}>{item.isPartner ? '已加入' : '未加入'}</div>
</div> </div>
<div className={`${PREFIX}__item-created`}>{item.isCreateResume ? '已创建' : '未创建'}</div>
<div className={`${PREFIX}__item-joined`}>{item.isPartner ? '已加入' : '未加入'}</div>
</div> </div>
</div> </div>
))} ))}

View File

@ -8,7 +8,51 @@
background: #6d3df5; background: #6d3df5;
color: #fff; color: #fff;
&__simple { &-simple {
padding: 39px 35px;
height: 120px;
box-sizing: border-box;
&__bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
border-radius: 16px;
height: 100%;
}
&__content {
.flex-row();
line-height: 42px;
position: relative;
z-index: 1;
}
&__title {
font-size: 26px;
color: #FFFFFF;
}
&__money {
font-weight: 800;
margin-left: 12px;
font-size: 44px;
}
&__button {
position: absolute;
z-index: 2;
top: 44px;
right: 24px;
font-size: 28px;
line-height: 32px;
&-image {
width: 24px;
height: 24px;
display: inline-block;
margin-left: 8px;
}
}
} }
&__bg { &__bg {
@ -28,20 +72,7 @@
z-index: 1; z-index: 1;
} }
&__button {
position: absolute;
z-index: 2;
top: 44px;
right: 56px;
font-size: 24px;
line-height: 24px;
&-image {
width: 20px;
height: 20px;
display: inline-block;
margin-left: 4px;
}
}
&__title { &__title {
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;

View File

@ -1,14 +1,16 @@
import { Button, Image } from '@tarojs/components'; import { Button, Image } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { Dialog } from '@taroify/core'; import { Dialog } from '@taroify/core';
import { Question } from '@taroify/icons'; import { Question } from '@taroify/icons';
import { useCallback, useState, useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { PageUrl } from '@/constants/app'; import { PageUrl } from '@/constants/app';
import { PartnerProfitsState } from '@/types/partner'; import { PartnerProfitsState } from '@/types/partner';
import { formatMoney, getPartnerProfitStat } from '@/utils/partner'; import { formatMoney, getPartnerProfitStat, withdrawMoney } from '@/utils/partner';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
import Toast from '@/utils/toast'; import Toast from '@/utils/toast';
import './index.less'; import './index.less';
const PREFIX = 'partner-kanban'; const PREFIX = 'partner-kanban';
@ -31,17 +33,48 @@ function TipDialog(props: { open: boolean; onClose: () => void }) {
} }
function WithdrawDialog(props: { open: boolean; onClose: () => void; count: number }) { function WithdrawDialog(props: { open: boolean; onClose: () => void; count: number }) {
const handleWithdraw = useCallback(() => {}, []); const handleWithdraw = useCallback(async () => {
if (Taro.canIUse('requestMerchantTransfer')) {
try {
const result = await withdrawMoney();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
wx.requestMerchantTransfer({
mchId: '1642470088',
appId: 'wxf0724a83f8e377d2',
package: result.packageInfo,
success: (res: never) => {
// res.err_msg将在页面展示成功后返回应用时返回ok并不代表付款成功
console.log('success:', res);
Toast.success('提现成功');
props.onClose();
},
fail: (res: never) => {
Toast.error('提现失败');
console.log('fail:', res);
},
});
} catch (e) {
Toast.error('提现订单创建失败');
console.log(e);
}
} else {
await Taro.showModal({
content: '你的微信版本过低,请更新至最新版本。',
showCancel: false,
});
}
}, [props]);
return ( return (
<Dialog open={props.open} onClose={props.onClose}> <Dialog open={props.open} onClose={props.onClose}>
<Dialog.Content> <Dialog.Content>
<div className={`${PREFIX}-withdraw-dialog__container`}> <div className={`${PREFIX}-withdraw-dialog__container`}>
<div className={`${PREFIX}-withdraw-dialog__title`}></div> <div className={`${PREFIX}-withdraw-dialog__title`}></div>
<div className={`${PREFIX}-withdraw-dialog__count`}> <div className={`${PREFIX}-withdraw-dialog__count`}>
{props.count} {+props.count}
<div className="yuan"></div> <div className="yuan"></div>
</div> </div>
<div className={`${PREFIX}-withdraw-dialog__hint`}>500</div> <div className={`${PREFIX}-withdraw-dialog__hint`}>200</div>
<Button className={`${PREFIX}-withdraw-dialog__confirm-button`} onClick={handleWithdraw}> <Button className={`${PREFIX}-withdraw-dialog__confirm-button`} onClick={handleWithdraw}>
</Button> </Button>
@ -86,41 +119,62 @@ export default function PartnerKanban({ simple }: PartnerKanbanProps) {
const handleTipClose = useCallback(() => { const handleTipClose = useCallback(() => {
setTipOpen(false); setTipOpen(false);
}, []); }, []);
const handleViewWithdraw = useCallback(() => {
if (stats.availableBalance < 10 * 1000) {
Toast.info('提现金额需大于等于10元');
return;
}
setWithdrawOpen(true);
}, []);
const handleWithdrawClose = useCallback(() => {
setWithdrawOpen(false);
}, []);
const getProfitStats = useCallback(async () => { const getProfitStats = useCallback(async () => {
const data = await getPartnerProfitStat(); const data = await getPartnerProfitStat();
setStats(data); setStats(data);
}, []); }, []);
const handleViewWithdraw = useCallback(() => {
if (stats.availableBalance < 0) {
Toast.info('提现金额需大于等于0元');
return;
}
// if (stats.availableBalance < 10 * 1000) {
// Toast.error('提现金额需大于等于10元');
// return;
// }
setWithdrawOpen(true);
}, [stats.availableBalance]);
const handleWithdrawClose = useCallback(() => {
setWithdrawOpen(false);
getProfitStats();
}, [getProfitStats]);
useDidShow(() => {
getProfitStats();
});
useEffect(() => { useEffect(() => {
getProfitStats(); getProfitStats();
}, []); }, []);
if (simple) {
return (
<div className={`${PREFIX} ${PREFIX}-simple`}>
<Image
className={`${PREFIX}-simple__bg`}
src="https://publiccdn.neighbourhood.com.cn/img/partner_bg.png"
mode="aspectFill"
/>
<div className={`${PREFIX}-simple__content`}>
<div className={`${PREFIX}-simple__title`}>()</div>
<div className={`${PREFIX}-simple__money`}>{formatMoney(total)}</div>
</div>
<div className={`${PREFIX}-simple__button`} onClick={handleNavigate}>
<Image
className={`${PREFIX}-simple__button-image`}
mode="aspectFit"
src={require('@/statics/svg/caret-right.svg')}
/>
</div>
</div>
);
}
return ( return (
<div className={`${PREFIX} ${simple ? `${PREFIX}__simple` : ''}`}> <div className={PREFIX}>
<Image <Image
className={`${PREFIX}__bg`} className={`${PREFIX}__bg`}
src="https://publiccdn.neighbourhood.com.cn/img/partner_bg.png" src="https://publiccdn.neighbourhood.com.cn/img/partner_bg.png"
mode="aspectFill" mode="aspectFill"
/> />
<div className={`${PREFIX}__content`}> <div className={`${PREFIX}__content`}>
{simple && (
<div className={`${PREFIX}__button`} onClick={handleNavigate}>
<Image
className={`${PREFIX}__button-image`}
mode="aspectFit"
src={require('@/statics/svg/caret-right.svg')}
/>
</div>
)}
<div className={`${PREFIX}__total`}> <div className={`${PREFIX}__total`}>
<div className={`${PREFIX}__title`}></div> <div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__money`}>{formatMoney(total)}</div> <div className={`${PREFIX}__money`}>{formatMoney(total)}</div>
@ -142,19 +196,21 @@ export default function PartnerKanban({ simple }: PartnerKanbanProps) {
<div className={`${PREFIX}__money`}>{formatMoney(stats.availableProfit)}</div> <div className={`${PREFIX}__money`}>{formatMoney(stats.availableProfit)}</div>
</div> </div>
</div> </div>
{!simple && ( <div className={`${PREFIX}__buttons`}>
<div className={`${PREFIX}__buttons`}> <Button className={`${PREFIX}__withdraw`} onClick={handleViewWithdraw}>
<Button className={`${PREFIX}__withdraw`} onClick={handleViewWithdraw}>
</Button>
</Button> <Button className={`${PREFIX}__record`} onClick={handleNavigateRecord}>
<Button className={`${PREFIX}__record`} onClick={handleNavigateRecord}>
</Button>
</Button> </div>
</div>
)}
</div> </div>
{!simple && <TipDialog open={tipOpen} onClose={handleTipClose} />} <TipDialog open={tipOpen} onClose={handleTipClose} />
{!simple && <WithdrawDialog count={350} open={withdrawOpen} onClose={handleWithdrawClose} />} <WithdrawDialog
count={Math.min(Number(formatMoney(stats.availableBalance)), 200)}
open={withdrawOpen}
onClose={handleWithdrawClose}
/>
</div> </div>
); );
} }

View File

@ -3,12 +3,12 @@ import classNames from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import ListPlaceholder from '@/components/list-placeholder'; import ListPlaceholder from '@/components/list-placeholder';
import { ProfitStatusDescriptions } from '@/constants/partner';
import { GetProfitRequest, PartnerProfitItem, ProfitType } from '@/types/partner'; import { GetProfitRequest, PartnerProfitItem, ProfitType } from '@/types/partner';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { formatMoney, formatTimestamp, getProfitList as requestData } from '@/utils/partner'; import { formatMoney, formatTimestamp, getProfitList as requestData } from '@/utils/partner';
import './index.less'; import './index.less';
import { PROFIT_STATUS_MAP, PROFIT_TYPE_MAP } from '@/constants/partner';
export interface IPartnerProfitListProps extends GetProfitRequest { export interface IPartnerProfitListProps extends GetProfitRequest {
visible?: boolean; visible?: boolean;
@ -22,7 +22,7 @@ const PREFIX = 'partner-profit';
const log = logWithPrefix(PREFIX); const log = logWithPrefix(PREFIX);
function ProfitList(props: IPartnerProfitListProps) { function ProfitList(props: IPartnerProfitListProps) {
const { className, listHeight, refreshDisabled, visible = true, profitType, onListEmpty } = props; const { className, listHeight, refreshDisabled, visible = true, type, onListEmpty } = props;
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState(false); const [loadMoreError, setLoadMoreError] = useState(false);
@ -34,7 +34,7 @@ function ProfitList(props: IPartnerProfitListProps) {
try { try {
setRefreshing(true); setRefreshing(true);
setLoadMoreError(false); setLoadMoreError(false);
const list = await requestData({ profitType }); const list = await requestData({ type });
setDataList(list); setDataList(list);
!list.length && onListEmptyRef.current?.(); !list.length && onListEmptyRef.current?.();
log('pull refresh success'); log('pull refresh success');
@ -65,7 +65,7 @@ function ProfitList(props: IPartnerProfitListProps) {
setDataList([]); setDataList([]);
setLoadingMore(true); setLoadingMore(true);
setLoadMoreError(false); setLoadMoreError(false);
const list = await requestData({ profitType }); const list = await requestData({ type });
setDataList(list); setDataList(list);
!list.length && onListEmptyRef.current?.(); !list.length && onListEmptyRef.current?.();
} catch (e) { } catch (e) {
@ -77,7 +77,7 @@ function ProfitList(props: IPartnerProfitListProps) {
} }
}; };
refresh(); refresh();
}, [visible, profitType]); }, [visible, type]);
return ( return (
<div className={`${PREFIX}__tab-content`}> <div className={`${PREFIX}__tab-content`}>
@ -95,18 +95,22 @@ function ProfitList(props: IPartnerProfitListProps) {
fixedHeight={typeof listHeight !== 'undefined'} fixedHeight={typeof listHeight !== 'undefined'}
style={listHeight ? { height: `${listHeight}px` } : undefined} style={listHeight ? { height: `${listHeight}px` } : undefined}
> >
{dataList.map(item => ( {dataList.map(item => {
<div className={`${PREFIX}__row`} key={item.id}> const isChat =
<div className={`${PREFIX}__row-content`}> type === ProfitType.CHAT_SHARE || item.earnType.toString().toLowerCase().indexOf('chat') > -1;
<div className={`${PREFIX}__item time`}>{formatTimestamp(item.created)}</div> return (
<div className={`${PREFIX}__item project`}>{PROFIT_TYPE_MAP[profitType]}</div> <div className={`${PREFIX}__row`} key={item.id}>
<div className={`${PREFIX}__item status`}> <div className={`${PREFIX}__row-content`}>
{profitType === ProfitType.Anchor ? '已结算' : PROFIT_STATUS_MAP[item.status]} <div className={`${PREFIX}__item time`}>{formatTimestamp(item.created)}</div>
<div className={`${PREFIX}__item project`}>{isChat ? '主播被开聊' : '会员支付'}</div>
<div className={`${PREFIX}__item status`}>
{isChat ? '已结算' : ProfitStatusDescriptions[item.status]}
</div>
<div className={`${PREFIX}__item income`}>+{formatMoney(item.amount)}</div>
</div> </div>
<div className={`${PREFIX}__item income`}>+{formatMoney(item.profit)}</div>
</div> </div>
</div> );
))} })}
<ListPlaceholder hasMore={false} loadingMore={loadingMore} loadMoreError={loadMoreError} /> <ListPlaceholder hasMore={false} loadingMore={loadingMore} loadMoreError={loadMoreError} />
</List> </List>
</PullRefresh> </PullRefresh>

View File

@ -28,6 +28,14 @@
} }
} }
&__help-icon {
width: 28px;
height: 28px;
margin-left: 2px;
position: relative;
top: 6px;
}
&__title { &__title {
height: 72px; height: 72px;
width: 100%; width: 100%;
@ -70,10 +78,13 @@
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }
&.time, &.time {
&.project {
flex: 2; flex: 2;
} }
&.project {
width: 150px;
flex-shrink: 0;
}
&.status { &.status {
width: 96px; width: 96px;
padding: 0 8px; padding: 0 8px;

View File

@ -1,7 +1,10 @@
import { Image } from '@tarojs/components';
import { Tabs } from '@taroify/core'; import { Tabs } from '@taroify/core';
import PartnerKanban from '@/components/partner-kanban'; import PartnerKanban from '@/components/partner-kanban';
import { ProfitType } from '@/types/partner'; import { ProfitType } from '@/types/partner';
import Toast from '@/utils/toast';
import ProfitList from './ProfitList'; import ProfitList from './ProfitList';
@ -21,24 +24,66 @@ function TableTitle() {
} }
export default function PartnerProfit() { export default function PartnerProfit() {
const handleClickHelpChat = () => {
Toast.info('主播被开聊14天后会显示收益');
};
const handleClickHelpPay = () => {
Toast.info('会员支付15日后结算收益');
};
const handleClickHelpInvite = () => {
Toast.info('所邀合伙人获得收益后自动获得收益');
};
return ( return (
<div className={PREFIX}> <div className={PREFIX}>
<div className={`${PREFIX}__top`}> <div className={`${PREFIX}__top`}>
<PartnerKanban /> <PartnerKanban />
</div> </div>
<div className={`${PREFIX}__main`}> <div className={`${PREFIX}__main`}>
<Tabs className={`${PREFIX}__tabs`}> <Tabs className={`${PREFIX}__tabs`} ellipsis={false}>
<Tabs.TabPane title="推荐主播收益"> <Tabs.TabPane
title={
<>
<Image
className={`${PREFIX}__help-icon`}
src={require('@/statics/svg/help.svg')}
onClick={handleClickHelpChat}
/>
</>
}
>
<TableTitle /> <TableTitle />
<ProfitList profitType={ProfitType.Anchor} /> <ProfitList type={ProfitType.CHAT_SHARE} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane title="推荐会员权益"> <Tabs.TabPane
title={
<>
<Image
className={`${PREFIX}__help-icon`}
src={require('@/statics/svg/help.svg')}
onClick={handleClickHelpPay}
/>
</>
}
>
<TableTitle /> <TableTitle />
<ProfitList profitType={ProfitType.Member} /> <ProfitList type={ProfitType.PAYMENT_SHARE} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane title="推荐合伙人收益"> <Tabs.TabPane
title={
<>
<Image
className={`${PREFIX}__help-icon`}
src={require('@/statics/svg/help.svg')}
onClick={handleClickHelpInvite}
/>
</>
}
>
<TableTitle /> <TableTitle />
<ProfitList profitType={ProfitType.Partner} /> <ProfitList type={ProfitType.INDIRECT_MEMBER_REFERRAL} />
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
</div> </div>

View File

@ -19,7 +19,7 @@ export interface IPhoneButtonProps extends ButtonProps {
message?: string; message?: string;
// 绑定后是否需要刷新接口获取手机号 // 绑定后是否需要刷新接口获取手机号
needPhone?: boolean; needPhone?: boolean;
onBindPhone?: (status: BindPhoneStatus) => void; onBindPhone?: (status: BindPhoneStatus, e: ITouchEvent) => void;
onSuccess?: (e: ITouchEvent) => void; onSuccess?: (e: ITouchEvent) => void;
} }
@ -44,17 +44,17 @@ export default function PhoneButton(props: IPhoneButtonProps) {
const encryptedData = e.detail.encryptedData; const encryptedData = e.detail.encryptedData;
const iv = e.detail.iv; const iv = e.detail.iv;
if (!encryptedData || !iv) { if (!encryptedData || !iv) {
onBindPhone?.(BindPhoneStatus.Cancel); onBindPhone?.(BindPhoneStatus.Cancel, e as ITouchEvent);
return Toast.error('取消授权'); return Toast.error('取消授权');
} }
try { try {
await setPhoneNumber({ encryptedData, iv }); await setPhoneNumber({ encryptedData, iv });
needPhone && (await requestUserInfo()); needPhone && (await requestUserInfo());
Toast.success(message); Toast.success(message);
onBindPhone?.(BindPhoneStatus.Success); onBindPhone?.(BindPhoneStatus.Success, e as ITouchEvent);
onSuccess?.(e as ITouchEvent); onSuccess?.(e as ITouchEvent);
} catch (err) { } catch (err) {
onBindPhone?.(BindPhoneStatus.Error); onBindPhone?.(BindPhoneStatus.Error, e as ITouchEvent);
Toast.error('绑定失败'); Toast.error('绑定失败');
log('bind phone fail', err); log('bind phone fail', err);
} }

View File

@ -0,0 +1,82 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.prejob-popup {
&__content {
padding: 40px 32px;
}
&__title {
font-weight: 500;
font-size: 32px;
line-height: 48px;
margin-bottom: 31px;
text-align: center;
}
&__body {
}
&__item {
.flex-row();
margin-bottom: 40px;
&:last-child {
margin-bottom: 0;
}
&-icon {
width: 88px;
height: 88px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
&.material {
background: #feba00;
> image {
width: 40px;
height: 46px;
}
}
&.vip {
background: #b094ff;
> image {
width: 48px;
height: 42px;
}
}
&.video {
background: #34a853;
> image {
width: 46px;
height: 44px;
}
}
}
&-main {
padding: 0 24px;
flex: 1;
.title {
font-weight: 500;
font-size: 28px;
line-height: 40px;
margin-bottom: 8px;
}
.desc {
font-size: 24px;
line-height: 36px;
color: @blColorG1;
}
}
&-action {
flex: 0 0 auto;
}
}
&__btn {
.button(@width: 136px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 43px, @highlight: 0);
background: #f2f2f2;
}
}

View File

@ -0,0 +1,89 @@
import { Button, Image } from '@tarojs/components';
import { Popup, Dialog } from '@taroify/core';
import { Fragment, useCallback, useState } from 'react';
import JobBuy from '@/components/product-dialog/steps-ui/job-buy';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { PageUrl } from '@/constants/app';
import { GET_CONTACT_TYPE } from '@/constants/job';
import { navigateTo } from '@/utils/route';
import './index.less';
interface IProps {
onCancel: () => void;
onConfirm: (type: GET_CONTACT_TYPE) => void;
}
const PREFIX = 'prejob-popup';
const GET_CONTACT_TYPE_OPTIONS = [
{
type: GET_CONTACT_TYPE.MATERIAL,
icon: 'https://publiccdn.neighbourhood.com.cn/img/file.svg',
title: '创建模卡(免费报单)',
desc: '免费报单,优先推荐给企业,机会更多',
btnText: '创建',
},
{
type: GET_CONTACT_TYPE.VIP,
icon: 'https://publiccdn.neighbourhood.com.cn/img/diamond.svg',
title: '播络会员',
desc: '开通会员每天可查看10个',
btnText: '开通',
},
];
export function PrejobPopup({ onCancel, onConfirm }: IProps) {
const [openPopup, setOpenPopup] = useState(true);
const [openDialog, setOpenDialog] = useState(false);
const handleClick = (type: GET_CONTACT_TYPE) => () => {
if (type === GET_CONTACT_TYPE.MATERIAL) {
navigateTo(PageUrl.MaterialUploadVideo);
onConfirm(type);
}
if (type === GET_CONTACT_TYPE.VIP) {
setOpenPopup(false);
setOpenDialog(true);
}
};
const handleAfterBuy = useCallback(async () => {
onConfirm(GET_CONTACT_TYPE.VIP);
}, [onConfirm]);
return (
<Fragment>
<Popup rounded className={PREFIX} placement="bottom" open={openPopup} onClose={onCancel}>
<div className={`${PREFIX}__content`}>
<div className={`${PREFIX}__title`}></div>
<div className={`${PREFIX}__body`}>
{GET_CONTACT_TYPE_OPTIONS.map(option => {
return (
<div className={`${PREFIX}__item`} key={option.type}>
<div className={`${PREFIX}__item-icon ${option.type}`}>
<Image mode="aspectFit" src={option.icon} />
</div>
<div className={`${PREFIX}__item-main`}>
<div className="title">{option.title}</div>
<div className="desc">{option.desc}</div>
</div>
<div className={`${PREFIX}__item-action`}>
<Button className={`${PREFIX}__btn`} onClick={handleClick(option.type)}>
{option.btnText}
</Button>
</div>
</div>
);
})}
</div>
</div>
<SafeBottomPadding />
</Popup>
<Dialog open={openDialog} onClose={onCancel}>
<Dialog.Content>
<JobBuy onConfirm={handleAfterBuy} buyOnly />
</Dialog.Content>
</Dialog>
</Fragment>
);
}

View File

@ -3,6 +3,7 @@ export const PREFIX = 'product-dialog';
export enum DialogStatus { export enum DialogStatus {
// 加载中 // 加载中
LOADING = 'loading', LOADING = 'loading',
PRE_ACTION = 'pre_action',
// 直接联系通告主 // 直接联系通告主
JOB_CONTACT_DIRECT = 'job_contact_direct', JOB_CONTACT_DIRECT = 'job_contact_direct',
// 联系客服去联系通告主 -> 订阅通知 // 联系客服去联系通告主 -> 订阅通知

View File

@ -82,7 +82,10 @@ function ProductGroupDialog(props: IProps) {
return; return;
} }
// 否则:如果有解锁次数,显示是否确定消费。无解锁次数,显示不无次数 UI // 否则:如果有解锁次数,显示是否确定消费。无解锁次数,显示不无次数 UI
const [time, detail] = await Promise.all([requestProductBalance(PRODUCT_CODE), requestGroupDetail(blGroupId)]); const [[time], detail] = await Promise.all([
requestProductBalance(PRODUCT_CODE),
requestGroupDetail(blGroupId),
]);
setGroupDetail(detail); setGroupDetail(detail);
if (time <= 0) { if (time <= 0) {
setStatus(DialogStatus.GROUP_NEED_BUY_ADD); setStatus(DialogStatus.GROUP_NEED_BUY_ADD);

View File

@ -133,6 +133,7 @@
&__header { &__header {
.flex-row(); .flex-row();
.header-font(); .header-font();
font-size: 32px;
.highlight { .highlight {
color: @blHighlightColor; color: @blHighlightColor;
@ -143,7 +144,8 @@
&__describe { &__describe {
.flex-row(); .flex-row();
.describe-font(); .describe-font();
margin-top: 24px; margin-top: 18px;
line-height: 40px;
.highlight { .highlight {
color: @blHighlightColor; color: @blHighlightColor;
@ -158,13 +160,13 @@
&__item { &__item {
position: relative; position: relative;
width: 170px; width: 182px;
height: 192px; height: 192px;
.flex-column(); .flex-column();
justify-content: center; justify-content: center;
border: 2px solid @blHighlightColor; border: 2px solid @blHighlightColor;
border-radius: 8px; border-radius: 8px;
margin-right: 24px; margin-right: 15px;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
@ -224,6 +226,20 @@
margin-top: 32px; margin-top: 32px;
} }
&__hint {
font-size: 24px;
line-height: 40px;
text-align: center;
color: @blColorG1;
margin-top: 24px;
.highlight {
color: @blHighlightColor;
display: inline;
}
}
} }
// ============================================= 群 ================================================= // // ============================================= 群 ================================================= //
@ -652,6 +668,11 @@
&.highlight { &.highlight {
color: @blHighlightColor; color: @blHighlightColor;
} }
.highlight-span {
color: @blHighlightColor;
display: inline;
}
} }
&__button { &__button {
@ -660,4 +681,4 @@
} }
} }
} }

View File

@ -102,7 +102,7 @@ function ProductJobWithGroupDialog(props: Omit<IProps, 'visible'>) {
return; return;
} }
// 否则:如果有解锁次数,显示是否确定消费。无解锁次数,显示不无次数 UI // 否则:如果有解锁次数,显示是否确定消费。无解锁次数,显示不无次数 UI
const time = await requestProductBalance(ProductType.AddGroup); const [time] = await requestProductBalance(ProductType.AddGroup);
if (time <= 0) { if (time <= 0) {
setStatus(DialogStatus.JOB_CONTACT_NEED_BUY_GROUP); setStatus(DialogStatus.JOB_CONTACT_NEED_BUY_GROUP);
} else { } else {
@ -119,7 +119,7 @@ function ProductJobWithGroupDialog(props: Omit<IProps, 'visible'>) {
return; return;
} }
// 自动报单 // 自动报单
const time = await requestProductBalance(ProductType.GetJob); const [time] = await requestProductBalance(ProductType.GetJob);
if (time <= 0) { if (time <= 0) {
setStatus(DialogStatus.JOB_UNABLE_UNLOCK); setStatus(DialogStatus.JOB_UNABLE_UNLOCK);
} else { } else {

View File

@ -41,7 +41,7 @@ function ProductJobDialog(props: Omit<IProps, 'visible'>) {
}, [onClose]); }, [onClose]);
const handleAfterBuy = useCallback(async () => { const handleAfterBuy = useCallback(async () => {
const time = await requestProductBalance(PRODUCT_CODE); const [time] = await requestProductBalance(PRODUCT_CODE);
if (time <= 0) { if (time <= 0) {
Toast.error('发生错误请重试'); Toast.error('发生错误请重试');
onClose(); onClose();
@ -83,7 +83,7 @@ function ProductJobDialog(props: Omit<IProps, 'visible'>) {
handleContact(result.declarationTypeResult); handleContact(result.declarationTypeResult);
return; return;
} }
const time = await requestProductBalance(PRODUCT_CODE); const [time] = await requestProductBalance(PRODUCT_CODE);
if (time <= 0) { if (time <= 0) {
const allowBuy = await requestAllBuyProduct(PRODUCT_CODE); const allowBuy = await requestAllBuyProduct(PRODUCT_CODE);
setStatus(allowBuy ? DialogStatus.JOB_BUY : DialogStatus.JOB_UNABLE_UNLOCK); setStatus(allowBuy ? DialogStatus.JOB_BUY : DialogStatus.JOB_UNABLE_UNLOCK);

View File

@ -99,12 +99,8 @@ export function CompanyPublishJobDialog(props: IProps) {
const handleNext = useCallback(async () => { const handleNext = useCallback(async () => {
onClose(true); onClose(true);
if (userInfo.bossAuthStatus) { navigateTo(PageUrl.JobPublish);
navigateTo(PageUrl.JobPublish); }, [onClose]);
} else {
navigateTo(PageUrl.CertificationStart);
}
}, [userInfo, onClose]);
useEffect(() => { useEffect(() => {
initRef.current = async () => { initRef.current = async () => {
@ -115,7 +111,7 @@ export function CompanyPublishJobDialog(props: IProps) {
try { try {
const productCode = ProductType.CompanyPublishJob; const productCode = ProductType.CompanyPublishJob;
Taro.showLoading(); Taro.showLoading();
const time = await requestProductBalance(productCode); const [time] = await requestProductBalance(productCode);
if (time <= 0) { if (time <= 0) {
setStatus(DialogStatus.COMPANY_PUBLISH_JOB_BUY); setStatus(DialogStatus.COMPANY_PUBLISH_JOB_BUY);
return; return;

View File

@ -26,53 +26,51 @@ interface Item {
title: string; title: string;
price: string; price: string;
amt: number; amt: number;
contents: { content: string; highlight?: boolean }[]; contents: { content: string; highlight?: boolean; inlineHighlight?: string }[];
badge?: string; badge?: string;
} }
const LIST: Item[] = [ const LIST: Item[] = [
{ // {
id: ProductSpecId.BOSS_VIP_NEW_1, // id: ProductSpecId.BOSS_VIP_NEW_1,
title: '展示一周', // title: '展示一周',
price: '480播豆', // price: '480播豆',
amt: 48, // amt: 48,
badge: '限时体验', // badge: '限时体验',
contents: [ // contents: [
{ content: '-1个通告' }, // { content: '-通告每日优先展示' },
{ content: '-每天可查看20个主播详情' }, // { content: '-每天可查看20个主播详情' },
{ content: '-每天可主联系10个主播' }, // { content: '-每天可主联系10个主播' },
// { content: '-播络可代为联系20个主播高成功率', highlight: true }, // // { content: '-播络可代为联系20个主播高成功率', highlight: true },
{ content: '-有效期一周' }, // { content: '-有效期一周' },
], // ],
}, // },
{ {
id: ProductSpecId.BOSS_VIP_NEW_2, id: ProductSpecId.BOSS_VIP_NEW_2,
title: '展示一月', title: '推荐一月',
price: '960播豆', price: '680播豆',
amt: 96, amt: 68,
badge: '五折', badge: '限时体验',
contents: [ contents: [
{ content: '-1个通告' }, { content: '-通告每日优先展示', highlight: true },
{ content: '-每天可查看20个主播详情' }, { content: '-每天可查看20个主播详情', inlineHighlight: '可主动联系10个' },
{ content: '-每天可自主联系10个主播' },
// { content: '-播络可代为联系20个主播高成功率', highlight: true }, // { content: '-播络可代为联系20个主播高成功率', highlight: true },
{ content: '-有效期一个月' }, { content: '-有效期一个月' },
], ],
}, },
// { {
// id: ProductSpecId.BOSS_VIP_NEW_3, id: ProductSpecId.BOSS_VIP_NEW_3,
// title: '季会员', title: '推荐一季',
// price: '2680播豆', price: '1360播豆',
// amt: 268, amt: 136,
// badge: '7折', badge: '6.7折',
// contents: [ contents: [
// { content: '-1个通告' }, { content: '-通告每日优先展示', highlight: true },
// { content: '-每天可查看20个主播详情' }, { content: '-每天可查看20个主播详情', inlineHighlight: '可主动联系10个' },
// { content: '-每天可自主联系10个主播' }, // { content: '-播络可代为联系60个主播(高成功率)', highlight: true },
// { content: '-播络可代为联系60个主播高成功率', highlight: true }, { content: '-有效期一个季度' },
// { content: '-有效期3个月' }, ],
// ], },
// },
]; ];
const subscribe = async () => { const subscribe = async () => {
@ -161,6 +159,7 @@ export default function CompanyPublishJobBuy(props: IProps) {
key={i.content} key={i.content}
> >
{i.content} {i.content}
{i.inlineHighlight && <div className="highlight-span">{i.inlineHighlight}</div>}
</div> </div>
))} ))}
</div> </div>

View File

@ -70,7 +70,7 @@ export default function GroupBuy(props: IProps) {
if (status !== OrderStatus.Success) { if (status !== OrderStatus.Success) {
throw new Error('order status error'); throw new Error('order status error');
} }
const time = await requestProductBalance(ProductType.AddGroup); const [time] = await requestProductBalance(ProductType.AddGroup);
log('handleBuy new addGroupTime', time); log('handleBuy new addGroupTime', time);
onConfirm(time); onConfirm(time);
} catch (e) { } catch (e) {

View File

@ -16,6 +16,7 @@ import { postSubscribe, subscribeMessage } from '@/utils/subscribe';
import Toast from '@/utils/toast'; import Toast from '@/utils/toast';
interface IProps { interface IProps {
buyOnly?: boolean;
onConfirm: () => void; onConfirm: () => void;
} }
@ -23,6 +24,7 @@ interface Item {
id: ProductSpecId; id: ProductSpecId;
title: string; title: string;
content: string; content: string;
buyOnlyContent?: string;
price: string; price: string;
amt: number; amt: number;
badge?: string; badge?: string;
@ -31,17 +33,19 @@ interface Item {
const LIST: Item[] = [ const LIST: Item[] = [
{ id: ProductSpecId.WeeklyVIP, title: '非会员', content: '每日2次', price: '免费', amt: 0 }, { id: ProductSpecId.WeeklyVIP, title: '非会员', content: '每日2次', price: '免费', amt: 0 },
{ {
id: ProductSpecId.WeeklyVIP, id: ProductSpecId.DailyVIP,
title: '会员', title: '会员',
content: '每日5次', content: '每日+10次',
buyOnlyContent: '每日12次',
price: '60播豆', price: '60播豆',
amt: 6, amt: 6,
badge: '限时体验', badge: '限时体验',
}, },
{ {
id: ProductSpecId.NewMonthlyVIP, id: ProductSpecId.WeeklyVIP,
title: '会员', title: '会员',
content: '每日5次', content: '每日+10次',
buyOnlyContent: '每日12次',
price: '180播豆', price: '180播豆',
amt: 18, amt: 18,
badge: ' 超值', badge: ' 超值',
@ -62,7 +66,7 @@ const subscribe = async () => {
}; };
export default function JobBuy(props: IProps) { export default function JobBuy(props: IProps) {
const { onConfirm } = props; const { onConfirm, buyOnly } = props;
const [selectItem, setSelectItem] = useState(LIST[1]); const [selectItem, setSelectItem] = useState(LIST[1]);
const handleClickItem = useCallback((newSelectItem: Item) => setSelectItem(newSelectItem), []); const handleClickItem = useCallback((newSelectItem: Item) => setSelectItem(newSelectItem), []);
@ -106,32 +110,50 @@ export default function JobBuy(props: IProps) {
return ( return (
<div className={`${PREFIX}__job-buy`}> <div className={`${PREFIX}__job-buy`}>
<div className={`${PREFIX}__job-buy__header`}> {buyOnly ? (
<div></div> <div className={`${PREFIX}__job-buy__header`}></div>
<div className="highlight"></div> ) : (
</div> <div className={`${PREFIX}__job-buy__header`}>
<div className={`${PREFIX}__job-buy__describe`}> <div></div>
<div></div> <div className="highlight"></div>
<div className="highlight"></div> </div>
<div> </div> )}
<div className="highlight"></div> {buyOnly ? (
</div> <div className={`${PREFIX}__job-buy__describe`}>12</div>
) : (
<div className={`${PREFIX}__job-buy__describe`}>
<div></div>
<div className="highlight"></div>
<div> </div>
<div className="highlight"></div>
</div>
)}
<div className={`${PREFIX}__job-buy__container`}> <div className={`${PREFIX}__job-buy__container`}>
{LIST.map(item => ( {LIST.map(item => {
<div if (buyOnly && !item.amt) {
key={item.price} return null;
className={classNames(`${PREFIX}__job-buy__item`, { }
selected: item.amt === selectItem.amt, return (
disabled: item.amt === 0, <div
})} key={item.price}
onClick={item.amt === 0 ? undefined : () => handleClickItem(item)} className={classNames(`${PREFIX}__job-buy__item`, {
> selected: item.amt === selectItem.amt,
<div className={classNames(`${PREFIX}__job-buy__item__title`, { free: item.amt === 0 })}>{item.title}</div> disabled: item.amt === 0,
<div className={`${PREFIX}__job-buy__item__content`}>{item.content}</div> })}
<div className={`${PREFIX}__job-buy__item__price`}>{item.price}</div> onClick={item.amt === 0 ? undefined : () => handleClickItem(item)}
{item.badge && <Badge className={`${PREFIX}__job-buy__item__badge`} text={item.badge} />} >
</div> <div className={classNames(`${PREFIX}__job-buy__item__title`, { free: item.amt === 0 })}>
))} {item.title}
</div>
<div className={`${PREFIX}__job-buy__item__content`}>{buyOnly ? item.buyOnlyContent : item.content}</div>
<div className={`${PREFIX}__job-buy__item__price`}>{item.price}</div>
{item.badge && <Badge className={`${PREFIX}__job-buy__item__badge`} text={item.badge} />}
</div>
);
})}
</div>
<div className={`${PREFIX}__job-buy__hint`}>
<div className="highlight">24</div>
</div> </div>
<Button className={`${PREFIX}__job-buy__button`} onClick={handleBuy}> <Button className={`${PREFIX}__job-buy__button`} onClick={handleBuy}>
{`支付 ${selectItem.amt}`} {`支付 ${selectItem.amt}`}

View File

@ -0,0 +1,162 @@
@import '@/styles/variables.less';
@import '@/styles/common.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);
}
&__search-wrapper {
&.group {
background: linear-gradient(2.75deg, #ffffff 7.9%, #f2edff 97.24%);
.search-city__search {
background-color: transparent;
.taroify-search__content--rounded {
border: @blHighlightColor 1px solid;
}
}
}
}
&__banner {
padding: 32px 24px 0;
text-align: center;
font-size: 28px;
line-height: 40px;
color: @blHighlightColor;
.flex-row();
.text {
padding: 0 12px;
flex: 0 1 auto;
}
.dash {
height: 1px;
flex: 1;
&:first-child {
background: linear-gradient(270deg, #6d3df5 0%, #f7f4ff 100%);
}
&:last-child {
background: linear-gradient(90deg, #6d3df5 0%, #f7f4ff 100%);
}
}
}
}

View File

@ -0,0 +1,259 @@
import { BaseEventOrig, InputProps, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Search } from '@taroify/core';
import { Fragment, useCallback, useEffect, useState } from 'react';
import { CITY_CODE_TO_NAME_MAP, CITY_INDEXES_LIST } from '@/constants/city';
import useCityOperators from '@/hooks/use-city-operators';
import { logWithPrefix } from '@/utils/common';
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 realtimeLogger = Taro.getRealtimeLogManager();
realtimeLogger.tag(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 interface SearchCityProps {
onSelectCity: (cityCode: string) => void;
currentCity?: string;
forGroup?: boolean;
banner?: string;
offset?: number;
}
export default function SearchCity({
onSelectCity,
currentCity = '',
banner = '',
forGroup = false,
offset = 0,
}: SearchCityProps) {
const [winHeight, indexItemHeight] = useHeight();
const [touchAnchor, setTouchAnchor] = useState<string | undefined>();
const [touchMoving, setTouchMoving] = useState(false);
const [searchResult, setSearchResult] = useState<Item[]>([]);
const cityOperators = useCityOperators();
const groupCityCodes = cityOperators.map(it => (it.groupLink ? it.cityCode : null)).filter(Boolean);
const showSearchList = searchResult.length > 0;
const CITY_LIST = forGroup
? CITY_INDEXES_LIST.map(({ letter, data }) => {
return {
letter,
data: data.filter(it => groupCityCodes.includes(it.cityCode)),
};
}).filter(item => item.data.length > 0)
: CITY_INDEXES_LIST;
const HOT_CITY_LIST = forGroup ? HOT_CITY.filter(it => groupCityCodes.includes(`${it.cityCode}`)) : HOT_CITY;
const handleSearchChange = useCallback((event: BaseEventOrig<InputProps.inputEventDetail>) => {
const value = event.detail.value;
log('handleSearchChange', value);
if (!value) {
setSearchResult([]);
return;
}
const result: Item[] = [];
CITY_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;
onSelectCity(String(cityCode));
},
[onSelectCity]
);
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_LIST.length) {
return;
}
const item = CITY_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_LIST.length) {
return;
}
const item = CITY_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);
}, []);
return (
<div className={PREFIX}>
<ScrollView scrollY style={{ height: winHeight - offset }} scrollIntoView={touchAnchor}>
<div className={`${PREFIX}__search-wrapper ${forGroup ? 'group' : ''}`}>
{forGroup && banner ? (
<div className={`${PREFIX}__banner`}>
<div className="dash"></div>
<div className="text">{banner}</div>
<div className="dash"></div>
</div>
) : null}
<Search
className={`${PREFIX}__search`}
placeholder="输入城市名称"
shape="rounded"
onChange={handleSearchChange}
/>
</div>
{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>
{!!HOT_CITY_LIST.length && (
<Fragment>
<div className={`${PREFIX}__hot-city-title`}></div>
<div className={`${PREFIX}__hot-city-container`}>
{HOT_CITY_LIST.map(city => {
return (
<div
key={city.cityCode}
className={`${PREFIX}__hot-city-item`}
data-code={city.cityCode}
onClick={handleSelectCity}
>
{city.cityName}
</div>
);
})}
</div>
</Fragment>
)}
<div className={`${PREFIX}__indexes-list`}>
{CITY_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 + (forGroup ? 72 : 0) }}
>
{CITY_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>
);
}

View File

@ -28,6 +28,7 @@
justify-content: space-between; justify-content: space-between;
box-sizing: border-box; box-sizing: border-box;
padding: 0 24px; padding: 0 24px;
flex: 0 0 96px;
&.selected { &.selected {
color: @blHighlightColor; color: @blHighlightColor;
@ -45,4 +46,4 @@
border-top-left-radius: 24px; border-top-left-radius: 24px;
border-top-right-radius: 24px; border-top-right-radius: 24px;
} }
} }

View File

@ -1,14 +1,11 @@
import { Tabbar } from '@taroify/core'; import { Tabbar } from '@taroify/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { APP_TAB_BAR_ID, RoleType, PageType, PageUrl } from '@/constants/app'; import { APP_TAB_BAR_ID, RoleType, PageType, PageUrl } from '@/constants/app';
import { ANCHOR_TAB_LIST, COMPANY_TAB_LIST, TabItemType } from '@/hooks/use-config'; import { ANCHOR_TAB_LIST, COMPANY_TAB_LIST, TabItemType } from '@/hooks/use-config';
import useMessage from '@/hooks/use-message'; import useMessage from '@/hooks/use-message';
import useRoleType from '@/hooks/user-role-type'; import useRoleType from '@/hooks/user-role-type';
import { changeHomePage } from '@/store/actions';
import { selectHomePageType } from '@/store/selector';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { switchTab } from '@/utils/route'; import { switchTab } from '@/utils/route';
@ -16,6 +13,7 @@ import './index.less';
interface IProps { interface IProps {
className?: string; className?: string;
type: PageType;
} }
const PREFIX = 'base-tab-bar'; const PREFIX = 'base-tab-bar';
@ -40,25 +38,21 @@ const TabItem = (props: { item: TabItemType }) => {
); );
}; };
function BaseTabBar(props: IProps) { function BaseTabBar({ className, type }: IProps) {
const { className } = props;
const roleType = useRoleType(); const roleType = useRoleType();
const currentPage = useSelector(selectHomePageType);
const dispatch = useDispatch();
const tabs = roleType === RoleType.Anchor ? ANCHOR_TAB_LIST : COMPANY_TAB_LIST; const tabs = roleType === RoleType.Anchor ? ANCHOR_TAB_LIST : COMPANY_TAB_LIST;
const handleTabClick = useCallback( const handleTabClick = useCallback(
(value: PageType) => { (value: PageType) => {
if (value === currentPage) { if (value === type) {
return; return;
} }
dispatch(changeHomePage(value));
const item = tabs.find((i: TabItemType) => i.type === value); const item = tabs.find((i: TabItemType) => i.type === value);
log('tab bar changed', value, item?.pagePath); log('tab bar changed', value, item?.pagePath);
item && switchTab(item.pagePath as PageUrl); item && switchTab(item.pagePath as PageUrl);
}, },
[tabs, currentPage, dispatch] [tabs, type]
); );
// useEffect(() => { // useEffect(() => {
@ -72,14 +66,14 @@ function BaseTabBar(props: IProps) {
return ( return (
<div className={classNames(`${PREFIX}__wrapper`, className)} id={APP_TAB_BAR_ID}> <div className={classNames(`${PREFIX}__wrapper`, className)} id={APP_TAB_BAR_ID}>
<Tabbar className={PREFIX} defaultValue={currentPage}> <Tabbar className={PREFIX} value={type}>
{tabs.map((item: TabItemType) => { {tabs.map((item: TabItemType) => {
return ( return (
<Tabbar.TabItem <Tabbar.TabItem
key={item.pagePath} key={item.pagePath}
value={item.type} value={item.type}
onClick={() => handleTabClick(item.type)} onClick={() => handleTabClick(item.type)}
className={classNames(`${PREFIX}__item`, { selected: item.type === currentPage })} className={classNames(`${PREFIX}__item`, { selected: item.type === type })}
> >
<TabItem item={item} /> <TabItem item={item} />
</Tabbar.TabItem> </Tabbar.TabItem>

View File

@ -0,0 +1,13 @@
import Select, { ISelectProps } from '@/components/select';
import { JOB_TYPE_SELECT_OPTIONS_WITH_ALL, JobType } from '@/constants/job';
interface IProps extends Omit<ISelectProps<JobType>, 'options'> {
value: JobType;
}
function TopCategorySelect(props: IProps) {
const { value: selectValue, onSelect } = props;
return <Select options={JOB_TYPE_SELECT_OPTIONS_WITH_ALL} title="品类" value={selectValue} onSelect={onSelect} />;
}
export default TopCategorySelect;

View File

@ -0,0 +1,89 @@
@import '@/styles/common.less';
@import '@/styles/variables.less';
.user-batch-publish {
padding: 16px 24px;
&__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;
}
}

View File

@ -0,0 +1,202 @@
import { Button, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Cell } from '@taroify/core';
import { useCallback, useState, useEffect } from 'react';
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 = 'user-batch-publish';
const log = logWithPrefix(PREFIX);
const SERVICE_ILLUSTRATE = `服务方式:帮您把招聘需求发到众多同城合作主播群
群发次数杭州、广州发3次其他城市1次
内容要求:仅限带货主播招聘需求,其他不发
主播联系:内容中留招聘方联系方式,主播直接联系`;
const cityValues: CityValue[] = [
{ cityCode: '440100', cityName: '广州', count: 800 },
{ cityCode: '440300', cityName: '深圳', count: 100 },
{ cityCode: '330100', cityName: '杭州', count: 750 },
{ cityCode: '110100', cityName: '北京', count: 150 },
{ cityCode: '510100', cityName: '成都', count: 100 },
{ cityCode: '500100', cityName: '重庆', count: 50 },
{ cityCode: '430100', cityName: '长沙', count: 50 },
{ cityCode: '350200', cityName: '厦门', count: 50 },
{ cityCode: '310100', cityName: '上海', count: 150 },
{ cityCode: '420100', cityName: '武汉', count: 80 },
{ cityCode: '610100', cityName: '西安', count: 60 },
{ cityCode: '410100', cityName: '郑州', count: 150 },
].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: 60, productSpecId: ProductSpecId.GroupBatchPublish60, label: '60', price: 48 },
{ value: 80, productSpecId: ProductSpecId.GroupBatchPublish80, label: '80', price: 58 },
{ value: 100, productSpecId: ProductSpecId.GroupBatchPublish100, label: '100', price: 68 },
{ value: 150, productSpecId: ProductSpecId.GroupBatchPublish150, label: '150', price: 98 },
{ value: 300, productSpecId: ProductSpecId.GroupBatchPublish300, label: '300', price: 128 },
{ value: 500, productSpecId: ProductSpecId.GroupBatchPublish500, label: '500', price: 168 },
{ value: 750, productSpecId: ProductSpecId.GroupBatchPublish750, label: '750', price: 188 },
{ value: 800, productSpecId: ProductSpecId.GroupBatchPublish800, label: '800', price: 198 },
{ 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]);
useEffect(() => {
try {
const cOptions: CityOption[] = cityValues.map(value => ({ value, label: value.cityName }));
const initCity = (cOptions.find(o => o.label === '重庆') || cOptions[0]).value;
setLoading(false);
setCity(initCity);
setCityOptions(cOptions);
log('init data done', cOptions);
} catch (e) {
Toast.error('加载失败请重试');
}
}, []);
if (loading) {
return <PageLoading />;
}
return (
<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>
);
}

View File

@ -24,6 +24,7 @@ export enum EventName {
SELECT_MY_PUBLISH_JOB = 'select_my_publish_job', SELECT_MY_PUBLISH_JOB = 'select_my_publish_job',
EXIT_CHAT_PAGE = 'exit_chat_page', EXIT_CHAT_PAGE = 'exit_chat_page',
VIEW_MATERIAL_SUCCESS = 'view_material_success', VIEW_MATERIAL_SUCCESS = 'view_material_success',
VIEW_JOB_SUCCESS = 'view_job_success',
} }
export enum OpenSource { export enum OpenSource {
@ -34,6 +35,7 @@ export enum OpenSource {
UserPage = 'user_page', UserPage = 'user_page',
AnchorPage = 'anchor_page', AnchorPage = 'anchor_page',
MaterialViewPage = 'material_view_page', MaterialViewPage = 'material_view_page',
GroupOwnerCertificate = 'group_owner_certificate',
} }
export enum PageUrl { export enum PageUrl {
@ -74,6 +76,10 @@ export enum PageUrl {
PrivacyWebview = 'pages/privacy-webview/index', PrivacyWebview = 'pages/privacy-webview/index',
Partner = 'pages/partner/index', Partner = 'pages/partner/index',
WithdrawRecord = 'pages/withdraw-record/index', WithdrawRecord = 'pages/withdraw-record/index',
GroupDelegatePublish = 'pages/group-delegate-publish/index',
GiveVip = 'pages/give-vip/index',
GroupOwnerCertificate = 'pages/group-owner-certification/index',
PartnerShareVip = 'pages/partner-share-vip/index',
} }
export enum PluginUrl { export enum PluginUrl {

View File

@ -10,4 +10,7 @@ export enum CacheKey {
LAST_SELECT_MY_JOB = '__last_select_my_job__', LAST_SELECT_MY_JOB = '__last_select_my_job__',
CLOSE_PARTNER_BANNER = '__close_partner_banner__', CLOSE_PARTNER_BANNER = '__close_partner_banner__',
INVITE_CODE = '__invite_code__', INVITE_CODE = '__invite_code__',
AGREEMENT_SIGNED = '__agreement_signed__',
CITY_CODES = '__city_codes__',
JOIN_GROUP_CARD_CLICKED = '__join_group_card_clicked__',
} }

View File

@ -5859,3 +5859,226 @@ export const CITY_INDEXES_LIST = [
], ],
}, },
]; ];
export const GROUP_CITY_INDEXES_LIST = [
{
letter: 'B',
data: [
{
cityCode: '110100',
cityName: '北京',
keyword: 'BEIJING北京',
},
],
},
{
letter: 'C',
data: [
{
cityCode: '500100',
cityName: '重庆',
keyword: 'CHONGQING重庆',
},
{
cityCode: '510100',
cityName: '成都',
keyword: 'CHENGDOU成都',
},
{
cityCode: '430100',
cityName: '长沙',
keyword: 'CHANGSHA长沙',
},
],
},
{
letter: 'D',
data: [
{
cityCode: '441900',
cityName: '东莞',
keyword: 'DONGGUAN东莞',
},
],
},
{
letter: 'F',
data: [
{
cityCode: '440600',
cityName: '佛山',
keyword: 'FUSHAN佛山',
},
{
cityCode: '350100',
cityName: '福州',
keyword: 'FUZHOU福州',
},
],
},
{
letter: 'G',
data: [
{
cityCode: '440100',
cityName: '广州',
keyword: 'GUANGZHOU广州',
},
],
},
{
letter: 'H',
data: [
{
cityCode: '330100',
cityName: '杭州',
keyword: 'HANGZHOU杭州',
},
{
cityCode: '230100',
cityName: '哈尔滨',
keyword: 'HAERBIN哈尔滨',
},
{
cityCode: '340100',
cityName: '合肥',
keyword: 'HEFEI合肥',
},
],
},
{
letter: 'J',
data: [
{
cityCode: '370100',
cityName: '济南',
keyword: 'JINAN济南',
},
],
},
{
letter: 'K',
data: [
{
cityCode: '530100',
cityName: '昆明',
keyword: 'KUNMING昆明',
},
],
},
{
letter: 'N',
data: [
{
cityCode: '320100',
cityName: '南京',
keyword: 'NANJING南京',
},
],
},
{
letter: 'Q',
data: [
{
cityCode: '370200',
cityName: '青岛',
keyword: 'QINGDAO青岛',
},
{
cityCode: '350500',
cityName: '泉州',
keyword: 'QUANZHOU泉州',
},
],
},
{
letter: 'S',
data: [
{
cityCode: '310100',
cityName: '上海',
keyword: 'SHANGHAI上海',
},
{
cityCode: '210100',
cityName: '沈阳',
keyword: 'SHENYANG沈阳',
},
{
cityCode: '440300',
cityName: '深圳',
keyword: 'SHENZHEN深圳',
},
{
cityCode: '130100',
cityName: '石家庄',
keyword: 'SHIJIAZHUANG石家庄',
},
{
cityCode: '320500',
cityName: '苏州',
keyword: 'SUZHOU苏州',
},
],
},
{
letter: 'T',
data: [
{
cityCode: '120100',
cityName: '天津',
keyword: 'TIANJIN天津',
},
{
cityCode: '140100',
cityName: '太原',
keyword: 'TAIYUAN太原',
},
],
},
{
letter: 'W',
data: [
{
cityCode: '330300',
cityName: '温州',
keyword: 'WENZHOU温州',
},
{
cityCode: '420100',
cityName: '武汉',
keyword: 'WUHAN武汉',
},
],
},
{
letter: 'X',
data: [
{
cityCode: '610100',
cityName: '西安',
keyword: 'XIAN西安',
},
{
cityCode: '350200',
cityName: '厦门',
keyword: 'XIAMEN厦门',
},
],
},
{
letter: 'Z',
data: [
{
cityCode: '410100',
cityName: '郑州',
keyword: 'ZHENGZHOU郑州',
},
{
cityCode: '430200',
cityName: '株洲',
keyword: 'ZHUZHOU株洲',
},
],
},
];

View File

@ -1,3 +1,5 @@
import { GroupItem } from '@/types/group';
export enum GroupType { export enum GroupType {
// 所有可加入的群 // 所有可加入的群
All = 'ALL', All = 'ALL',
@ -29,3 +31,36 @@ export const GROUP_STATUS_OPTIONS = [
{ label: '全部', value: GroupStatus.All }, { label: '全部', value: GroupStatus.All },
{ label: '已申请', value: GroupStatus.Requested }, { label: '已申请', value: GroupStatus.Requested },
]; ];
export const GROUPS: GroupItem[] = [
{ title: '【杭州】', cityCode: 330100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc223f495e159af95e' },
{ title: '【广州】', cityCode: 440100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb4b88b8abb7a7c8b' },
{ title: '【深圳】', cityCode: 440300, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfe70d8736e14bb64' },
{ title: '【北京】', cityCode: 110100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb119c94575e91262' },
{ title: '【上海】', cityCode: 310100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4189e68429cf07f8' },
{ title: '【郑州】', cityCode: 410100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd1c53b7bf8ecdb97' },
{ title: '【长沙】', cityCode: 430100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc76be8f2b3f8aa437' },
{ title: '【成都】', cityCode: 510100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf75cefbdc62946fa' },
{ title: '【重庆】', cityCode: 500100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd7008f747d545f83' },
{ title: '【武汉】', cityCode: 420100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc047c94f8c709b395' },
{ title: '【厦门】', cityCode: 350200, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc2007a895cb48464b' },
{ title: '【西安】', cityCode: 610100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc34768971b7354220' },
{ title: '【合肥】', cityCode: 340100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc41c9785cc2035277' },
{ title: '【南京】', cityCode: 320100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc6dc8d0a9692b70e' },
{ title: '【青岛】', cityCode: 370200, serviceUrl: 'https://work.weixin.qq.com/kfid/kfce8d7a68190f6a1d2' },
{ title: '【佛山】', cityCode: 440600, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcfac1132df386fac8' },
{ title: '【东莞】', cityCode: 441900, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb2b0e39026f7dddc' },
{ title: '【苏州】', cityCode: 320500, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4642f90a6e3528ff' },
{ title: '【福州】', cityCode: 350100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc126483dedadde82b' },
{ title: '【泉州】', cityCode: 350500, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc4c8c42b1a9337aaf' },
{ title: '【温州】', cityCode: 330300, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcb0ea5f197a18b335' },
{ title: '【天津】', cityCode: 120100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcda46c23dade6f6a3' },
{ title: '【昆明】', cityCode: 530100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcf2aebcbf3d46d9cd' },
{ title: '【石家庄】', cityCode: 130100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc7dedcc69aaa9cdae' },
{ title: '【太原】', cityCode: 140100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfccc8b59411baa7c24' },
{ title: '【株洲】', cityCode: 430200, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc8584f903f9263f85' },
{ title: '【哈尔滨】', cityCode: 230100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcaf757d0590d2bad0' },
{ title: '【沈阳】', cityCode: 210100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcbe96e71d87061f88' },
{ title: '【济南】', cityCode: 370100, serviceUrl: 'https://work.weixin.qq.com/kfid/kfc57d4d9bd032fe89c' },
{ title: '【全国】', cityCode: 440300, serviceUrl: 'https://work.weixin.qq.com/kfid/kfcc60ac7b6420787a8' },
];

View File

@ -7,6 +7,7 @@ export enum JobType {
Jewelry = 'JEWELRY', // 珠宝 Jewelry = 'JEWELRY', // 珠宝
Appliance = 'APPLIANCE', // 家电 Appliance = 'APPLIANCE', // 家电
Furniture = 'FURNITURE', // 日用家具 Furniture = 'FURNITURE', // 日用家具
OutdoorSports = 'OUTDOOR_SPORTS',
PetFamily = 'PET_FAMILY', // 母婴宠物 PetFamily = 'PET_FAMILY', // 母婴宠物
Luxury = 'LUXURY', // 奢品 Luxury = 'LUXURY', // 奢品
LocalLive = 'LOCAL_LIVE', // 本地生活 LocalLive = 'LOCAL_LIVE', // 本地生活
@ -21,6 +22,12 @@ export enum EmployType {
Part = 'PARTY_TIME', Part = 'PARTY_TIME',
} }
export enum JobSourceType {
All = 'All',
VX = 'vx',
BL = 'bl',
}
export enum SortType { export enum SortType {
RECOMMEND = 'RECOMMEND', RECOMMEND = 'RECOMMEND',
DISTANCE = 'DISTANCE', DISTANCE = 'DISTANCE',
@ -66,6 +73,12 @@ export const EMPLOY_TYPE_TITLE_MAP = {
[EmployType.Full]: '全职', [EmployType.Full]: '全职',
}; };
export const JOB_SOURCE_TYPE_TITLE_MAP = {
[JobSourceType.All]: '全部',
[JobSourceType.BL]: '急招',
[JobSourceType.VX]: '群通告',
};
export const JOB_TYPE_TITLE_MAP: { [key in JobType]: string } = { export const JOB_TYPE_TITLE_MAP: { [key in JobType]: string } = {
[JobType.All]: '全部', [JobType.All]: '全部',
[JobType.Finery]: '服饰', [JobType.Finery]: '服饰',
@ -75,6 +88,7 @@ export const JOB_TYPE_TITLE_MAP: { [key in JobType]: string } = {
[JobType.Jewelry]: '珠宝', [JobType.Jewelry]: '珠宝',
[JobType.Appliance]: '家电', [JobType.Appliance]: '家电',
[JobType.Furniture]: '日用家具', [JobType.Furniture]: '日用家具',
[JobType.OutdoorSports]: '户外运动',
[JobType.PetFamily]: '母婴宠物', [JobType.PetFamily]: '母婴宠物',
[JobType.Luxury]: '奢品', [JobType.Luxury]: '奢品',
[JobType.LocalLive]: '本地生活', [JobType.LocalLive]: '本地生活',
@ -101,16 +115,31 @@ export const JOB_MANAGE_STATUS_TITLE_MAP = {
export const JOB_PAGE_TABS = [ export const JOB_PAGE_TABS = [
{ {
type: EmployType.All, type: JobSourceType.All,
title: EMPLOY_TYPE_TITLE_MAP[EmployType.All], title: JOB_SOURCE_TYPE_TITLE_MAP[JobSourceType.All],
}, },
{ {
type: EmployType.Part, type: JobSourceType.BL,
title: EMPLOY_TYPE_TITLE_MAP[EmployType.Part], title: JOB_SOURCE_TYPE_TITLE_MAP[JobSourceType.BL],
},
// {
// type: JobSourceType.VX,
// title: JOB_SOURCE_TYPE_TITLE_MAP[JobSourceType.VX],
// },
];
export const JOB_EMPLOY_TYPE_OPTIONS = [
{
value: EmployType.All,
label: EMPLOY_TYPE_TITLE_MAP[EmployType.All],
}, },
{ {
type: EmployType.Full, value: EmployType.Part,
title: EMPLOY_TYPE_TITLE_MAP[EmployType.Full], label: EMPLOY_TYPE_TITLE_MAP[EmployType.Part],
},
{
value: EmployType.Full,
label: EMPLOY_TYPE_TITLE_MAP[EmployType.Full],
}, },
]; ];
@ -146,6 +175,7 @@ export const JOB_TYPE_SELECT_OPTIONS = [
{ label: JOB_TYPE_TITLE_MAP[JobType.Jewelry], value: JobType.Jewelry }, { label: JOB_TYPE_TITLE_MAP[JobType.Jewelry], value: JobType.Jewelry },
{ label: JOB_TYPE_TITLE_MAP[JobType.Appliance], value: JobType.Appliance }, { label: JOB_TYPE_TITLE_MAP[JobType.Appliance], value: JobType.Appliance },
{ label: JOB_TYPE_TITLE_MAP[JobType.Furniture], value: JobType.Furniture }, { label: JOB_TYPE_TITLE_MAP[JobType.Furniture], value: JobType.Furniture },
{ label: JOB_TYPE_TITLE_MAP[JobType.OutdoorSports], value: JobType.OutdoorSports },
{ label: JOB_TYPE_TITLE_MAP[JobType.PetFamily], value: JobType.PetFamily }, { label: JOB_TYPE_TITLE_MAP[JobType.PetFamily], value: JobType.PetFamily },
{ label: JOB_TYPE_TITLE_MAP[JobType.Luxury], value: JobType.Luxury }, { label: JOB_TYPE_TITLE_MAP[JobType.Luxury], value: JobType.Luxury },
{ label: JOB_TYPE_TITLE_MAP[JobType.LocalLive], value: JobType.LocalLive }, { label: JOB_TYPE_TITLE_MAP[JobType.LocalLive], value: JobType.LocalLive },
@ -154,6 +184,11 @@ export const JOB_TYPE_SELECT_OPTIONS = [
{ label: JOB_TYPE_TITLE_MAP[JobType.Other], value: JobType.Other }, { label: JOB_TYPE_TITLE_MAP[JobType.Other], value: JobType.Other },
]; ];
export const JOB_TYPE_SELECT_OPTIONS_WITH_ALL = [
{ label: JOB_TYPE_TITLE_MAP[JobType.All], value: JobType.All },
...JOB_TYPE_SELECT_OPTIONS,
];
const MAX_SALARY = 10000000; const MAX_SALARY = 10000000;
export const PART_EMPLOY_SALARY_OPTIONS = [ export const PART_EMPLOY_SALARY_OPTIONS = [
{ label: '不限' }, { label: '不限' },
@ -174,3 +209,8 @@ export const FULL_EMPLOY_SALARY_OPTIONS = [
export const PART_PRICE_OPTIONS = PART_EMPLOY_SALARY_OPTIONS.filter(o => !!o.value); export const PART_PRICE_OPTIONS = PART_EMPLOY_SALARY_OPTIONS.filter(o => !!o.value);
export const FULL_PRICE_OPTIONS = FULL_EMPLOY_SALARY_OPTIONS.filter(o => !!o.value); export const FULL_PRICE_OPTIONS = FULL_EMPLOY_SALARY_OPTIONS.filter(o => !!o.value);
export enum GET_CONTACT_TYPE {
VIP = 'vip',
MATERIAL = 'material',
}

View File

@ -84,14 +84,25 @@ export const WORK_YEAR_OPTIONS = [
{ label: WORK_YEAR_LABELS[WorkedYears.MoreThreeYear], value: WorkedYears.MoreThreeYear }, { label: WORK_YEAR_LABELS[WorkedYears.MoreThreeYear], value: WorkedYears.MoreThreeYear },
]; ];
export const ALL_GENDER_TYPES = [GenderType.All, GenderType.MEN, GenderType.WOMEN];
export const GENDER_TYPE_TITLE_MAP = { export const GENDER_TYPE_TITLE_MAP = {
[GenderType.All]: '不限', [GenderType.All]: '不限',
[GenderType.WOMEN]: '女', [GenderType.WOMEN]: '女',
[GenderType.MEN]: '男', [GenderType.MEN]: '男',
}; };
export const GENDER_OPTIONS = [
{
value: GenderType.All,
label: GENDER_TYPE_TITLE_MAP[GenderType.All],
},
{
value: GenderType.WOMEN,
label: GENDER_TYPE_TITLE_MAP[GenderType.WOMEN],
},
{
value: GenderType.MEN,
label: GENDER_TYPE_TITLE_MAP[GenderType.MEN],
},
];
export const ALL_ANCHOR_READ_TYPES = Object.values(AnchorReadType); export const ALL_ANCHOR_READ_TYPES = Object.values(AnchorReadType);
export const ANCHOR_READ_TITLE_MAP = { export const ANCHOR_READ_TITLE_MAP = {

View File

@ -1,21 +1,60 @@
export enum ProfitType {
Anchor = '1',
Member = '2',
Partner = '3',
}
export enum ProfitStatus { export enum ProfitStatus {
AVAILABLE = '1', /**
WITHDRAWING = '2', * 待处理/待计算 (例如,等待上游数据或条件满足)
WITHDRAW = '3', */
PENDING_CALCULATION = 'PENDING_CALCULATION',
/**
* 待直接结算/待直接分账 (例如T+7 到账)
* 这种类型的佣金会计入银行账户,而非平台余额
*/
DIRECT_SETTLEMENT_PENDING = 'DIRECT_SETTLEMENT_PENDING',
/**
* 直接结算已完成/直接分账已完成
*/
DIRECT_SETTLEMENT_PROCESSING = 'DIRECT_SETTLEMENT_PROCESSING',
/**
* 间接收益已结算到合伙人余额 (例如,主播推荐奖金进入可提现余额)
*/
INDIRECT_SETTLED_TO_BALANCE = 'INDIRECT_SETTLED_TO_BALANCE',
/**
* 佣金已取消 (例如,订单退款,不满足条件等)
*/
CANCELLED = 'CANCELLED',
/**
* 佣金处理失败
*/
FAILED = 'FAILED',
/**
* 其他状态
*/
OTHER = 'OTHER',
/**
* 已完成
*/
FINISHED = 'FINISHED',
} }
export const PROFIT_TYPE_MAP = { // 如果需要为每个枚举值添加描述,可以使用一个单独的映射对象
[ProfitType.Anchor]: '主播被开聊', export const ProfitStatusDescriptions = {
[ProfitType.Member]: '会员支付', PENDING_CALCULATION: '',
[ProfitType.Partner]: '合伙人收益分成', DIRECT_SETTLEMENT_PENDING: '待分账',
DIRECT_SETTLEMENT_PROCESSING: '',
INDIRECT_SETTLED_TO_BALANCE: '',
CANCELLED: '',
FAILED: '',
OTHER: '',
FINISHED: '已分账',
}; };
export const PROFIT_STATUS_MAP = {
[ProfitStatus.AVAILABLE]: '可提现', export const WithdrawStatusDescriptions = {
[ProfitStatus.WITHDRAWING]: '提现中', 0: '提现中',
[ProfitStatus.WITHDRAW]: '已提现', 1: '已提现',
2: '失败',
}; };

View File

@ -27,14 +27,20 @@ export enum ProductSpecId {
AddGroup2 = 'ADDGROUP_2', AddGroup2 = 'ADDGROUP_2',
AddGroup3 = 'ADDGROUP_3', AddGroup3 = 'ADDGROUP_3',
BossVip = 'BOSSVIP', BossVip = 'BOSSVIP',
DailyVIP = 'VIP_D',
WeeklyVIP = 'VIP_W', WeeklyVIP = 'VIP_W',
MonthlyVIP = 'VIP_M', // 30 每天十次 MonthlyVIP = 'VIP_M', // 30 每天十次
NewMonthlyVIP = 'VIP_M_NEW', // 18 每天五次 NewMonthlyVIP = 'VIP_M_NEW', // 18 每天五次
GroupBatchPublish20 = 'GROUP_BATCH_PUSH_20', GroupBatchPublish20 = 'GROUP_BATCH_PUSH_20',
GroupBatchPublish50 = 'GROUP_BATCH_PUSH_50', GroupBatchPublish50 = 'GROUP_BATCH_PUSH_50',
GroupBatchPublish60 = 'GROUP_BATCH_PUSH_60',
GroupBatchPublish80 = 'GROUP_BATCH_PUSH_80',
GroupBatchPublish100 = 'GROUP_BATCH_PUSH_100', GroupBatchPublish100 = 'GROUP_BATCH_PUSH_100',
GroupBatchPublish150 = 'GROUP_BATCH_PUSH_150',
GroupBatchPublish300 = 'GROUP_BATCH_PUSH_300', GroupBatchPublish300 = 'GROUP_BATCH_PUSH_300',
GroupBatchPublish500 = 'GROUP_BATCH_PUSH_500', GroupBatchPublish500 = 'GROUP_BATCH_PUSH_500',
GroupBatchPublish750 = 'GROUP_BATCH_PUSH_750',
GroupBatchPublish800 = 'GROUP_BATCH_PUSH_800',
GroupBatchPublish1000 = 'GROUP_BATCH_PUSH_1000', GroupBatchPublish1000 = 'GROUP_BATCH_PUSH_1000',
BOSS_PUB_JOB_1 = 'BOSS_PUB_JOB_1', // 旧版企业发通告会员 BOSS_PUB_JOB_1 = 'BOSS_PUB_JOB_1', // 旧版企业发通告会员
BOSS_VIP_NEW_1 = 'BOSS_VIP_NEW_1', // 新版企业发通告会员 - 周 BOSS_VIP_NEW_1 = 'BOSS_VIP_NEW_1', // 新版企业发通告会员 - 周

View File

@ -54,6 +54,7 @@
flex: 1 0; flex: 1 0;
} }
&__employType-select,
&__salary-select, &__salary-select,
&__search-city { &__search-city {
display: flex; display: flex;

View File

@ -2,10 +2,11 @@ import { Image } from '@tarojs/components';
import { NodesRef, useDidHide } from '@tarojs/taro'; import { NodesRef, useDidHide } from '@tarojs/taro';
import { Tabs } from '@taroify/core'; import { Tabs } from '@taroify/core';
import { ArrowUp, ArrowDown } from '@taroify/icons'; import { ArrowDown, ArrowUp } from '@taroify/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import EmployTypeSelect from '@/components/employ-type-select';
import JobList, { IJobListProps } from '@/components/job-list'; import JobList, { IJobListProps } from '@/components/job-list';
import Overlay from '@/components/overlay'; import Overlay from '@/components/overlay';
import PartnerBanner from '@/components/partner-banner'; import PartnerBanner from '@/components/partner-banner';
@ -13,7 +14,16 @@ import SalarySelect from '@/components/salary-select';
import SearchInput from '@/components/search'; import SearchInput from '@/components/search';
import { APP_TAB_BAR_ID, PageUrl } from '@/constants/app'; import { APP_TAB_BAR_ID, PageUrl } from '@/constants/app';
import { CITY_CODE_TO_NAME_MAP } from '@/constants/city'; import { CITY_CODE_TO_NAME_MAP } from '@/constants/city';
import { JOB_TABS, JobType, EmployType, ALL_SORT_TYPES, SORT_TYPE_TITLE_MAP, SortType } from '@/constants/job'; import {
ALL_SORT_TYPES,
EMPLOY_TYPE_TITLE_MAP,
EmployType,
JOB_TABS,
JobSourceType,
JobType,
SORT_TYPE_TITLE_MAP,
SortType,
} from '@/constants/job';
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height'; import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
import { SalaryRange } from '@/types/job'; import { SalaryRange } from '@/types/job';
import { Coordinate } from '@/types/location'; import { Coordinate } from '@/types/location';
@ -24,9 +34,11 @@ import './index.less';
interface IProps { interface IProps {
cityCode: string; cityCode: string;
sortType: SortType; sortType: SortType;
sourceType: JobSourceType;
employType: EmployType; employType: EmployType;
coordinate: Coordinate; coordinate: Coordinate;
onClickCity: () => void; onClickCity: () => void;
onClickEmployType: (type: EmployType) => void;
onClickSort: (type: SortType) => void; onClickSort: (type: SortType) => void;
} }
@ -74,20 +86,35 @@ function ListWrapper(props: IJobListProps) {
} }
function JobFragment(props: IProps) { function JobFragment(props: IProps) {
const { cityCode, employType, sortType = SortType.RECOMMEND, coordinate, onClickCity, onClickSort } = props; const {
cityCode,
employType,
sourceType,
sortType = SortType.RECOMMEND,
coordinate,
onClickEmployType,
onClickCity,
onClickSort,
} = props;
const listHeight = useListHeight(CALC_LIST_PROPS); const listHeight = useListHeight(CALC_LIST_PROPS);
const [tabType, setTabType] = useState<JobType>(JobType.All); const [tabType, setTabType] = useState<JobType>(JobType.All);
const [salaryRange, setSalaryRange] = useState<SalaryRange | undefined>(); const [salaryRange, setSalaryRange] = useState<SalaryRange | undefined>();
const [showSalarySelect, setShowSalarySelect] = useState<boolean>(false); const [showSalarySelect, setShowSalarySelect] = useState<boolean>(false);
const [showEmployTypeSelect, setShowEmployTypeSelect] = useState<boolean>(false);
const { latitude, longitude } = coordinate; const { latitude, longitude } = coordinate;
const handleClickSearch = useCallback(() => navigateTo(PageUrl.JobSearch, { city: cityCode }), [cityCode]); const handleClickSearch = useCallback(() => navigateTo(PageUrl.JobSearch, { city: cityCode }), [cityCode]);
const handleClickSalarySelect = useCallback(() => { const handleClickSalarySelect = useCallback(() => {
setShowSalarySelect(!showSalarySelect); setShowSalarySelect(s => !s);
}, [showSalarySelect]); }, []);
const handleClickEmployTypeSelect = useCallback(() => {
setShowEmployTypeSelect(s => !s);
}, []);
const handleHideSalarySelect = useCallback(() => setShowSalarySelect(false), []); const handleHideSalarySelect = useCallback(() => setShowSalarySelect(false), []);
const handleHideEmployTypeSelect = useCallback(() => setShowEmployTypeSelect(false), []);
const handleSelectSalary = useCallback((value?: SalaryRange) => { const handleSelectSalary = useCallback((value?: SalaryRange) => {
log('handleSelectSalary', value); log('handleSelectSalary', value);
@ -95,6 +122,15 @@ function JobFragment(props: IProps) {
setShowSalarySelect(false); setShowSalarySelect(false);
}, []); }, []);
const handleSelectEmployType = useCallback(
(value?: EmployType) => {
console.log('哈哈哈', value);
setShowEmployTypeSelect(false);
onClickEmployType(value!);
},
[onClickEmployType]
);
const onTypeChange = useCallback( const onTypeChange = useCallback(
value => { value => {
log('onTypeChange', value); log('onTypeChange', value);
@ -109,6 +145,12 @@ function JobFragment(props: IProps) {
<div className={`${PREFIX}__container`}> <div className={`${PREFIX}__container`}>
<div className={`${PREFIX}__top-search-bar`}> <div className={`${PREFIX}__top-search-bar`}>
<SearchInput disabled className={`${PREFIX}__search`} placeholder="试试 女装" onClick={handleClickSearch} /> <SearchInput disabled className={`${PREFIX}__search`} placeholder="试试 女装" onClick={handleClickSearch} />
<div className={classNames(`${PREFIX}__employType-select`)} onClick={handleClickEmployTypeSelect}>
<div className="title">
{employType && employType !== EmployType.All ? EMPLOY_TYPE_TITLE_MAP[employType] : '类型'}
</div>
{showEmployTypeSelect ? <ArrowUp /> : <ArrowDown />}
</div>
<div className={classNames(`${PREFIX}__search-city`)} onClick={onClickCity}> <div className={classNames(`${PREFIX}__search-city`)} onClick={onClickCity}>
<div className="title">{CITY_CODE_TO_NAME_MAP.get(cityCode)}</div> <div className="title">{CITY_CODE_TO_NAME_MAP.get(cityCode)}</div>
<ArrowDown /> <ArrowDown />
@ -139,6 +181,7 @@ function JobFragment(props: IProps) {
sortType={sortType} sortType={sortType}
latitude={latitude} latitude={latitude}
longitude={longitude} longitude={longitude}
sourceType={sourceType}
employType={employType} employType={employType}
minSalary={salaryRange?.minSalary} minSalary={salaryRange?.minSalary}
maxSalary={salaryRange?.maxSalary} maxSalary={salaryRange?.maxSalary}
@ -157,6 +200,15 @@ function JobFragment(props: IProps) {
> >
<SalarySelect type={employType} value={salaryRange} onSelect={handleSelectSalary} /> <SalarySelect type={employType} value={salaryRange} onSelect={handleSelectSalary} />
</Overlay> </Overlay>
<Overlay
visible={showEmployTypeSelect}
onClickOuter={handleHideEmployTypeSelect}
outerClassName={`${PREFIX}__overlay-outer`}
innerClassName={`${PREFIX}__overlay-inner`}
>
<EmployTypeSelect value={employType} onSelect={handleSelectEmployType} />
</Overlay>
</div> </div>
); );
} }

View File

@ -12,8 +12,8 @@ import { EmployType, EMPLOY_TYPE_TITLE_MAP, FULL_PRICE_OPTIONS, PART_PRICE_OPTIO
import { SalaryRange } from '@/types/job'; import { SalaryRange } from '@/types/job';
import { MaterialProfile } from '@/types/material'; import { MaterialProfile } from '@/types/material';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job' import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
import { getCurrentCity } from '@/utils/location'; import { getCurrentCityCode } from '@/utils/location';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
import './index.less'; import './index.less';
@ -27,7 +27,7 @@ const log = logWithPrefix(PREFIX);
const realtimeLogger = Taro.getRealtimeLogManager(); const realtimeLogger = Taro.getRealtimeLogManager();
realtimeLogger.tag(PREFIX); realtimeLogger.tag(PREFIX);
const calcInitCityCodes = (codeString: string = '') => { const calcInitCityCodes = (codeString: string = '') => {
const codes = codeString.split('、'); const codes = (codeString || '').split('、');
return codes.filter(code => !!CITY_CODE_TO_NAME_MAP.get(code)); return codes.filter(code => !!CITY_CODE_TO_NAME_MAP.get(code));
}; };
@ -62,7 +62,7 @@ const getSalaryPrice = (fullSalary?: SalaryRange, partSalary?: SalaryRange) => {
function ProfileIntentionFragment(props: IProps, ref) { function ProfileIntentionFragment(props: IProps, ref) {
const { profile } = props; const { profile } = props;
const [cityCodes, setCityCodes] = useState(calcInitCityCodes(profile.cityCodes)); const [cityCodes, setCityCodes] = useState(calcInitCityCodes(profile.cityCodes || ''));
const [employType, setEmployType] = useState(profile.employType); const [employType, setEmployType] = useState(profile.employType);
const [fullSalary, setFullSalary] = useState(calcSalarySelect(profile, true)); const [fullSalary, setFullSalary] = useState(calcSalarySelect(profile, true));
const [partSalary, setPartSalary] = useState(calcSalarySelect(profile, false)); const [partSalary, setPartSalary] = useState(calcSalarySelect(profile, false));
@ -76,9 +76,9 @@ function ProfileIntentionFragment(props: IProps, ref) {
); );
const handleClickAddCity = useCallback(() => { const handleClickAddCity = useCallback(() => {
const currentCity = getCurrentCity(); const currentCityCode = getCurrentCityCode();
realtimeLogger.info('handleClickAddCity', OpenSource.AddIndentCity); realtimeLogger.info('handleClickAddCity', OpenSource.AddIndentCity);
navigateTo(PageUrl.CitySearchProfile, { city: currentCity, source: OpenSource.AddIndentCity }); navigateTo(PageUrl.CitySearchProfile, { city: currentCityCode, source: OpenSource.AddIndentCity });
}, []); }, []);
const handleSelectCity = useCallback( const handleSelectCity = useCallback(
@ -98,10 +98,10 @@ function ProfileIntentionFragment(props: IProps, ref) {
const handleEmployTypeChange = useCallback((value: EmployType) => { const handleEmployTypeChange = useCallback((value: EmployType) => {
setEmployType(value); setEmployType(value);
if (value === EmployType.Full) { if (value === EmployType.Full) {
setPartSalary({ minSalary: 0, maxSalary: 0 }) setPartSalary({ minSalary: 0, maxSalary: 0 });
} }
if (value === EmployType.Part) { if (value === EmployType.Part) {
setFullSalary({ minSalary: 0, maxSalary: 0 }) setFullSalary({ minSalary: 0, maxSalary: 0 });
} }
}, []); }, []);
@ -173,24 +173,26 @@ function ProfileIntentionFragment(props: IProps, ref) {
<BlFormRadio name={EmployType.All} text={EMPLOY_TYPE_TITLE_MAP[EmployType.All]} value={employType} /> <BlFormRadio name={EmployType.All} text={EMPLOY_TYPE_TITLE_MAP[EmployType.All]} value={employType} />
</BlFormRadioGroup> </BlFormRadioGroup>
</BlFormItem> </BlFormItem>
{isFullTimePriceRequired(employType) && {isFullTimePriceRequired(employType) && (
(<BlFormItem title="期望全职薪资"> <BlFormItem title="期望全职薪资">
<BlFormSelect <BlFormSelect
title="期望全职薪资" title="期望全职薪资"
value={fullSalary} value={fullSalary}
options={FULL_PRICE_OPTIONS} options={FULL_PRICE_OPTIONS}
onSelect={handleSelectFullSalary} onSelect={handleSelectFullSalary}
/> />
</BlFormItem>)} </BlFormItem>
{isPartTimePriceRequired(employType) && )}
(<BlFormItem title="期望兼职薪资"> {isPartTimePriceRequired(employType) && (
<BlFormItem title="期望兼职薪资">
<BlFormSelect <BlFormSelect
title="期望兼职薪资" title="期望兼职薪资"
value={partSalary} value={partSalary}
options={PART_PRICE_OPTIONS} options={PART_PRICE_OPTIONS}
onSelect={handleSelectPartSalary} onSelect={handleSelectPartSalary}
/> />
</BlFormItem>)} </BlFormItem>
)}
<BlFormItem title="是否接受坐班"> <BlFormItem title="是否接受坐班">
<BlFormRadioGroup direction="horizontal" value={acceptWorkForSit} onChange={handleAcceptSittingChange}> <BlFormRadioGroup direction="horizontal" value={acceptWorkForSit} onChange={handleAcceptSittingChange}>
<BlFormRadio name text="接受" value={acceptWorkForSit} /> <BlFormRadio name text="接受" value={acceptWorkForSit} />

View File

@ -44,7 +44,7 @@
font-size: 28px; font-size: 28px;
font-weight: 400; font-weight: 400;
color: #ffffff; color: #ffffff;
background: #0000005c; background: #6D3DF5B2;
border-radius: 48px; border-radius: 48px;
} }
@ -232,4 +232,8 @@
} }
} }
} }
.join-group-hint {
margin: 24px;
}
} }

View File

@ -17,6 +17,7 @@ import { navigateTo } from '@/utils/route';
import './index.less'; import './index.less';
import onChangeEventDetail = SwiperProps.onChangeEventDetail; import onChangeEventDetail = SwiperProps.onChangeEventDetail;
import { JoinGroupHint } from '@/components/join-group-hint';
interface IProps { interface IProps {
editable: boolean; editable: boolean;
@ -29,7 +30,7 @@ const log = logWithPrefix(PREFIX);
const DEFAULT_TEXT = '未填写'; const DEFAULT_TEXT = '未填写';
const getIndentCity = (codeString: string = '') => { const getIndentCity = (codeString: string = '') => {
const codes = codeString.split('、'); const codes = (codeString||'').split('、');
const cityNames: string[] = []; const cityNames: string[] = [];
codes.forEach(code => { codes.forEach(code => {
const cityName = CITY_CODE_TO_NAME_MAP.get(code); const cityName = CITY_CODE_TO_NAME_MAP.get(code);
@ -207,6 +208,7 @@ export default function ProfileViewFragment(props: IProps) {
>{`${coverIndex + 1}/${profile.materialVideoInfoList.length}`}</div> >{`${coverIndex + 1}/${profile.materialVideoInfoList.length}`}</div>
</div> </div>
</div> </div>
{!editable && <JoinGroupHint />}
<div className={`${PREFIX}__body`}> <div className={`${PREFIX}__body`}>
<div className={`${PREFIX}__basic-info`}> <div className={`${PREFIX}__basic-info`}>
<div className={`${PREFIX}__basic-info__name-container`}> <div className={`${PREFIX}__basic-info__name-container`}>
@ -228,7 +230,9 @@ export default function ProfileViewFragment(props: IProps) {
{index ? ( {index ? (
<div className={`${PREFIX}__info-group__header__title`}>{data.title}</div> <div className={`${PREFIX}__info-group__header__title`}>{data.title}</div>
) : ( ) : (
<DevDiv className={`${PREFIX}__info-group__header__title`} OnDev={onDev}>{data.title}</DevDiv> <DevDiv className={`${PREFIX}__info-group__header__title`} OnDev={onDev}>
{data.title}
</DevDiv>
)} )}
{editable && ( {editable && (
<div className={`${PREFIX}__info-group__header__edit`} onClick={() => handleEditGroupItem(data.type)}> <div className={`${PREFIX}__info-group__header__edit`} onClick={() => handleEditGroupItem(data.type)}>

View File

@ -13,6 +13,21 @@
.taroify-cell__value { .taroify-cell__value {
color: @blColor; color: @blColor;
} }
&-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 80, 81, 0.16);
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 16px;
> image {
width: 18px;
height: 24px;
}
}
} }
&__go-publish-cell { &__go-publish-cell {
@ -24,4 +39,6 @@
color: @blHighlightColor; color: @blHighlightColor;
} }
} }
} }

View File

@ -1,3 +1,5 @@
import { Image } from '@tarojs/components';
import { Cell } from '@taroify/core'; import { Cell } from '@taroify/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
@ -5,7 +7,6 @@ import { useCallback } from 'react';
import CertificationStatus from '@/components/certification-status'; import CertificationStatus from '@/components/certification-status';
import WechatCell from '@/components/wx-cell'; import WechatCell from '@/components/wx-cell';
import { PageUrl } from '@/constants/app'; import { PageUrl } from '@/constants/app';
import { CertificationStatusType } from '@/constants/company';
import useUserInfo from '@/hooks/use-user-info'; import useUserInfo from '@/hooks/use-user-info';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
@ -18,10 +19,6 @@ export default function CompanyFragment() {
// const [showPublish, setShowPublish] = useState(false); // const [showPublish, setShowPublish] = useState(false);
const handlePublishJob = useCallback(async () => { const handlePublishJob = useCallback(async () => {
if (userInfo.bossAuthStatus !== CertificationStatusType.Success) {
navigateTo(PageUrl.CertificationStart);
return;
}
navigateTo(PageUrl.CertificationManage); navigateTo(PageUrl.CertificationManage);
}, [userInfo]); }, [userInfo]);
@ -32,7 +29,14 @@ export default function CompanyFragment() {
<Cell <Cell
isLink isLink
align="center" align="center"
title="发通告,让主播主动报单" title={
<div>
<div className={`${PREFIX}__cell-icon`}>
<Image src="https://publiccdn.neighbourhood.com.cn/img/lightning.svg" />
</div>
</div>
}
className={classNames(`${PREFIX}__cell`, `${PREFIX}__go-publish-cell`)} className={classNames(`${PREFIX}__cell`, `${PREFIX}__go-publish-cell`)}
onClick={handlePublishJob} onClick={handlePublishJob}
> >

View File

@ -0,0 +1,10 @@
import { useSelector } from 'react-redux';
import { selectCreatedJob } from '@/store/selector/job';
function useCachedJobId() {
const data = useSelector(selectCreatedJob);
return data;
}
export default useCachedJobId;

View File

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

View File

@ -48,7 +48,7 @@ const CompanyTabs: TabItemType[] = [
{ {
type: PageType.BatchPublish, type: PageType.BatchPublish,
pagePath: PageUrl.UserBatchPublish, pagePath: PageUrl.UserBatchPublish,
text: '代发', text: '代招代发',
}, },
]; ];
@ -97,6 +97,10 @@ export const APP_CONFIG: AppConfigType = {
PageUrl.PrivacyWebview, PageUrl.PrivacyWebview,
PageUrl.Partner, PageUrl.Partner,
PageUrl.WithdrawRecord, PageUrl.WithdrawRecord,
PageUrl.GroupDelegatePublish,
PageUrl.GiveVip,
PageUrl.GroupOwnerCertificate,
PageUrl.PartnerShareVip,
// PageUrl.DevDebug, // PageUrl.DevDebug,
], ],
window: { window: {

13
src/hooks/use-previous.ts Normal file
View File

@ -0,0 +1,13 @@
import { useEffect, useRef } from 'react';
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export default usePrevious;

View File

@ -1,9 +1,10 @@
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RoleType } from '@/constants/app';
import { selectRoleType } from '@/store/selector'; import { selectRoleType } from '@/store/selector';
function useRoleType() { function useRoleType() {
return useSelector(selectRoleType); return useSelector(selectRoleType) || RoleType.Anchor;
} }
export default useRoleType; export default useRoleType;

View File

@ -1,5 +1,6 @@
// export const DOMAIN = 'http://192.168.60.120:8082'; // export const DOMAIN = 'http://192.168.60.116:8082';
export const DOMAIN = 'https://neighbourhood.cn'; export const DOMAIN = process.env.BRAND === 'dev' ? 'https://dev.neighbourhood.cn' : 'https://neighbourhood.cn';
// export const DOMAIN = 'https://dev.neighbourhood.cn';
export const BASE_URL = `${DOMAIN}/api`; export const BASE_URL = `${DOMAIN}/api`;
export enum API { export enum API {
@ -78,8 +79,21 @@ export enum API {
// partner // partner
PARTNER_QRCODE = '/user/getInviteQrCode', PARTNER_QRCODE = '/user/getInviteQrCode',
GET_INVITE_CODE = '/user/getUserInviteCode', GET_INVITE_CODE = '/user/getUserInviteCode',
GET_INVITE_LIST = '/user/inviteUsers', GET_INVITE_LIST = '/user/inviteUsers/list',
BECOME_PARTNER = '/user/becomePartner', BECOME_PARTNER = '/user/becomePartner',
GET_PROFIT_LIST = '/profit/profits', GET_PROFIT_LIST = '/user/profit/list',
GET_PROFIT_STAT = '/user/profits', GET_PROFIT_STAT = '/user/profits',
WITHDRAW = '/user/withdraw',
GET_WITHDRAW_LIST = '/user/withdraw/records',
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

@ -26,6 +26,7 @@ export enum RESPONSE_ERROR_CODE {
INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE', // 聊天或者模卡查看超出限制 INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE', // 聊天或者模卡查看超出限制
INSUFFICIENT_FREE_BALANCE = 'INSUFFICIENT_FREE_BALANCE', // 免费查看次数(未购买会员)超限 INSUFFICIENT_FREE_BALANCE = 'INSUFFICIENT_FREE_BALANCE', // 免费查看次数(未购买会员)超限
BOSS_VIP_EXPIRED = 'BOSS_VIP_EXPIRED', // 会员过期 BOSS_VIP_EXPIRED = 'BOSS_VIP_EXPIRED', // 会员过期
CHAT_MSG_SEND_NOT_ALLOW = 'CHAT_MSG_SEND_NOT_ALLOW',
} }
export const RESPONSE_ERROR_INFO: { [key in RESPONSE_ERROR_CODE]?: string } = { export const RESPONSE_ERROR_INFO: { [key in RESPONSE_ERROR_CODE]?: string } = {

View File

@ -107,14 +107,14 @@ class Http {
url: BASE_URL + url, url: BASE_URL + url,
data, data,
method: method, method: method,
header: { 'content-type': contentType /*, 'user-Id': '588002047871053824' */ }, header: { 'content-type': contentType /*,'user-Id': '588002047871053824'*/ },
}; };
return this.request(option); return this.request(option);
}; };
async init() { async init(inviteCode?: string) {
if (isTokenExpired()) { if (isTokenExpired()) {
await refreshToken(); await refreshToken(inviteCode);
} }
} }

View File

@ -1,6 +1,6 @@
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import { getRoleType } from '@/utils/app'; import { getRoleTypeWithDefault } from '@/utils/app';
import { isDev } from '@/utils/common'; import { isDev } from '@/utils/common';
import { getToken } from './utils'; import { getToken } from './utils';
@ -20,7 +20,7 @@ const tokenInterceptor: Taro.interceptor = (chain: Taro.Chain) => {
const roleInterceptor: Taro.interceptor = (chain: Taro.Chain) => { const roleInterceptor: Taro.interceptor = (chain: Taro.Chain) => {
const requestParams = chain.requestParams; const requestParams = chain.requestParams;
const roleType = getRoleType(); const roleType = getRoleTypeWithDefault();
requestParams.header = { requestParams.header = {
...requestParams.header, ...requestParams.header,
'role-type': roleType, 'role-type': roleType,

View File

@ -24,10 +24,10 @@ const clearToken = () => {
Taro.setStorageSync(TOKEN_EXPIRES_TIME, 0); Taro.setStorageSync(TOKEN_EXPIRES_TIME, 0);
}; };
const requestToken = (): Promise<string> => { const requestToken = (inviteCode?: string): Promise<string> => {
return getCode() return getCode()
.then(code => { .then(code => {
return http.post<LoginResponse>(API.LOGIN, { data: { code } }).then(data => { return http.post<LoginResponse>(API.LOGIN, { data: { code, inviteCode } }).then(data => {
const newToken = data?.token || ''; const newToken = data?.token || '';
const expires = data?.expires || 0; const expires = data?.expires || 0;
if (newToken) { if (newToken) {
@ -47,12 +47,12 @@ const requestToken = (): Promise<string> => {
export const isTokenExpired = () => (Taro.getStorageSync(TOKEN_EXPIRES_TIME) || 0) < Date.now(); export const isTokenExpired = () => (Taro.getStorageSync(TOKEN_EXPIRES_TIME) || 0) < Date.now();
export const refreshToken = () => { export const refreshToken = (inviteCode?: string) => {
if (_fetchTokenPromise) { if (_fetchTokenPromise) {
return _fetchTokenPromise; return _fetchTokenPromise;
} }
clearToken(); clearToken();
_fetchTokenPromise = requestToken(); _fetchTokenPromise = requestToken(inviteCode);
return _fetchTokenPromise; return _fetchTokenPromise;
}; };

View File

@ -52,7 +52,11 @@
font-size: 28px; font-size: 28px;
line-height: 32px; line-height: 32px;
color: @blColor; color: @blColor;
gap: 20px;
&-item {
display: flex;
}
.title { .title {
margin-right: 5px; margin-right: 5px;
} }
@ -61,11 +65,17 @@
padding: 0 24px; padding: 0 24px;
} }
&__overlay-outer { &__overlay-outer {
top: 82px; top: 72px;
} }
&__overlay-inner { &__overlay-inner {
width: 100%; width: 100%;
max-height: 100%;
overflow: hidden;
.bl-select__items-container {
max-height: calc(100vh - 610rpx);
overflow-y: auto;
}
} }
&__tips-container { &__tips-container {
@ -90,4 +100,23 @@
color: @blColor; color: @blColor;
margin-top: 50px; margin-top: 50px;
} }
&__popup {
&-content {
.flex-column();
padding: 40px 32px 64px 32px;
}
&-title {
font-weight: 500;
font-size: 36px;
line-height: 57px;
text-align: center;
margin-bottom: 48px;
}
&-confirm {
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
}
}
} }

View File

@ -1,28 +1,39 @@
import { Image } from '@tarojs/components'; import { Button, Image } from '@tarojs/components';
import Taro, { NodesRef, useDidShow, useLoad, useShareAppMessage } from '@tarojs/taro'; import Taro, { NodesRef, useDidShow, useLoad, useShareAppMessage } from '@tarojs/taro';
import { ArrowUp, ArrowDown } from '@taroify/icons'; import { Popup } from '@taroify/core';
import { ArrowDown, ArrowUp } from '@taroify/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import AnchorList, { IAnchorListProps } from '@/components/anchor-list'; import AnchorList, { IAnchorListProps } from '@/components/anchor-list';
import AnchorPicker from '@/components/anchor-picker'; import AnchorPicker from '@/components/anchor-picker';
import CustomNavigationBar from '@/components/custom-navigation-bar'; import CustomNavigationBar from '@/components/custom-navigation-bar';
import EmployTypeSelect from '@/components/employ-type-select';
import GenderSelect from '@/components/gender-select';
import HomePage from '@/components/home-page'; import HomePage from '@/components/home-page';
import Overlay from '@/components/overlay'; import Overlay from '@/components/overlay';
import PageLoading from '@/components/page-loading'; import PageLoading from '@/components/page-loading';
import PartnerBanner from '@/components/partner-banner'; import PartnerBanner from '@/components/partner-banner';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import SwitchBar from '@/components/switch-bar'; import SwitchBar from '@/components/switch-bar';
import { APP_TAB_BAR_ID, EventName, OpenSource, PageUrl } from '@/constants/app'; import { APP_TAB_BAR_ID, EventName, OpenSource, PageType, PageUrl, RoleType } from '@/constants/app';
import { EmployType, JobManageStatus } from '@/constants/job'; import { EMPLOY_TYPE_TITLE_MAP, EmployType, JOB_TYPE_TITLE_MAP, JobType } from '@/constants/job';
import { ALL_ANCHOR_SORT_TYPES, ANCHOR_SORT_TYPE_TITLE_MAP, AnchorSortType } from '@/constants/material'; import {
ALL_ANCHOR_SORT_TYPES,
ANCHOR_SORT_TYPE_TITLE_MAP,
AnchorSortType,
GENDER_TYPE_TITLE_MAP,
GenderType,
} from '@/constants/material';
import useInviteCode from '@/hooks/use-invite-code'; import useInviteCode from '@/hooks/use-invite-code';
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height'; import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
import useLocation from '@/hooks/use-location'; import useLocation from '@/hooks/use-location';
import { JobManageInfo } from '@/types/job'; import { JobManageInfo } from '@/types/job';
import { Coordinate } from '@/types/location'; import { Coordinate } from '@/types/location';
import { IAnchorFilters } from '@/types/material'; import { IAnchorFilters } from '@/types/material';
import { switchRoleType } from '@/utils/app';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { getLastSelectMyJobId, requestJobManageList, setLastSelectMyJobId } from '@/utils/job'; import { getLastSelectMyJobId, requestJobManageList, setLastSelectMyJobId } from '@/utils/job';
import { getWxLocation } from '@/utils/location'; import { getWxLocation } from '@/utils/location';
@ -31,7 +42,9 @@ import { getInviteCodeFromQueryAndUpdate } from '@/utils/partner';
import { getPageQuery, navigateTo } from '@/utils/route'; import { getPageQuery, navigateTo } from '@/utils/route';
import { getCommonShareMessage } from '@/utils/share'; import { getCommonShareMessage } from '@/utils/share';
import Toast from '@/utils/toast'; import Toast from '@/utils/toast';
import './index.less'; import './index.less';
import TopCategorySelect from '@/components/top-category-select';
const PREFIX = 'page-anchor'; const PREFIX = 'page-anchor';
const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`; const LIST_CONTAINER_CLASS = `${PREFIX}__list-container`;
@ -74,13 +87,24 @@ function ListWrapper(props: IAnchorListProps) {
return <AnchorList listHeight={listHeight} {...props} onListEmpty={handleListEmpty} />; return <AnchorList listHeight={listHeight} {...props} onListEmpty={handleListEmpty} />;
} }
enum FilterType {
gender,
employType,
topCategory,
more,
}
export default function AnchorPage() { export default function AnchorPage() {
const location = useLocation(); const location = useLocation();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectJob, setSelectJob] = useState<JobManageInfo | undefined>(); const [selectJob, setSelectJob] = useState<JobManageInfo | undefined>();
const [filters, setFilters] = useState<IAnchorFilters>({ employType: EmployType.All }); const [moreFilters, setMoreFilters] = useState<IAnchorFilters>({});
const [showFilter, setShowFilter] = useState<boolean>(false); const [gender, setGender] = useState<GenderType>(GenderType.All);
const [employType, setEmployType] = useState<EmployType>(EmployType.All);
const [topCategory, setTopCategory] = useState<JobType>(JobType.All);
const [showFilter, setShowFilter] = useState<FilterType | null>(null);
const [sortType, setSortType] = useState<AnchorSortType>(AnchorSortType.Active); const [sortType, setSortType] = useState<AnchorSortType>(AnchorSortType.Active);
const [openPopup, setOpenPopup] = useState<boolean>(false);
const [coordinate, setCoordinate] = useState<Coordinate>({ const [coordinate, setCoordinate] = useState<Coordinate>({
latitude: location.latitude, latitude: location.latitude,
longitude: location.longitude, longitude: location.longitude,
@ -91,6 +115,8 @@ export default function AnchorPage() {
const handleChangeSelectJob = useCallback((select?: JobManageInfo) => { const handleChangeSelectJob = useCallback((select?: JobManageInfo) => {
log('select job change', select); log('select job change', select);
setSelectJob(select); setSelectJob(select);
console.log('哈哈哈', !select);
setOpenPopup(!select);
setLastSelectMyJobId(select?.id || ''); setLastSelectMyJobId(select?.id || '');
}, []); }, []);
@ -99,20 +125,35 @@ export default function AnchorPage() {
[selectJob] [selectJob]
); );
const handleClickSalarySelect = useCallback(() => { const handleClickFilterSelect = (type: FilterType) => () => {
setShowFilter(!showFilter); setShowFilter(type === showFilter ? null : type);
}, [showFilter]); };
const handleHideFilter = useCallback(() => setShowFilter(false), []); const handleHideFilter = useCallback(() => setShowFilter(null), []);
const handleFilterChange = useCallback( const handleFilterChange = useCallback(
(newFilters: IAnchorFilters) => { (newFilters: IAnchorFilters) => {
!isEqual(newFilters, filters) && setFilters(newFilters); !isEqual(newFilters, moreFilters) && setMoreFilters(newFilters);
setShowFilter(false); setShowFilter(null);
}, },
[filters] [moreFilters]
); );
const handleChangeSelectGender = useCallback((value: GenderType) => {
setGender(value);
setShowFilter(null);
}, []);
const handleChangeSelectEmployType = useCallback((value: EmployType) => {
setEmployType(value);
setShowFilter(null);
}, []);
const handleChangeSelectTopCategory = useCallback((value: JobType) => {
setTopCategory(value);
setShowFilter(null);
}, []);
const handleClickSortType = useCallback(async (type: AnchorSortType) => setSortType(type), []); const handleClickSortType = useCallback(async (type: AnchorSortType) => setSortType(type), []);
const handleJobChange = useCallback( const handleJobChange = useCallback(
@ -124,7 +165,7 @@ export default function AnchorPage() {
); );
const handlePublishJobChange = useCallback(async () => { const handlePublishJobChange = useCallback(async () => {
const { jobResults = [] } = await requestJobManageList({ status: JobManageStatus.Open }); const { jobResults = [] } = await requestJobManageList();
if (!selectJob) { if (!selectJob) {
// 之前没有开发中的通告,自动选中第一个开放中的通告 // 之前没有开发中的通告,自动选中第一个开放中的通告
handleChangeSelectJob(jobResults[0]); handleChangeSelectJob(jobResults[0]);
@ -140,6 +181,10 @@ export default function AnchorPage() {
} }
}, [selectJob, handleChangeSelectJob]); }, [selectJob, handleChangeSelectJob]);
const handleGoCreate = useCallback(() => {
navigateTo(PageUrl.JobPublish);
}, []);
useEffect(() => { useEffect(() => {
Taro.eventCenter.on(EventName.SELECT_MY_PUBLISH_JOB, handleJobChange); Taro.eventCenter.on(EventName.SELECT_MY_PUBLISH_JOB, handleJobChange);
Taro.eventCenter.on(EventName.COMPANY_JOB_PUBLISH_CHANGED, handlePublishJobChange); Taro.eventCenter.on(EventName.COMPANY_JOB_PUBLISH_CHANGED, handlePublishJobChange);
@ -165,13 +210,16 @@ export default function AnchorPage() {
}, [location]); }, [location]);
useLoad(async () => { useLoad(async () => {
switchRoleType(RoleType.Company);
const query = getPageQuery(); const query = getPageQuery();
getInviteCodeFromQueryAndUpdate(query); getInviteCodeFromQueryAndUpdate(query);
try { try {
const { jobResults = [] } = await requestJobManageList({ status: JobManageStatus.Open }); const { jobResults = [] } = await requestJobManageList();
if (!jobResults.length) { if (!jobResults.length) {
Toast.info('当前是根据定位为您展示主播'); setOpenPopup(true);
// Toast.info('当前是根据定位为您展示主播');
return; return;
} }
const lastSelectJobId = getLastSelectMyJobId(); const lastSelectJobId = getLastSelectMyJobId();
@ -185,14 +233,27 @@ export default function AnchorPage() {
} }
}); });
Taro.showShareMenu({
withShareTicket: true,
});
useShareAppMessage(() => { useShareAppMessage(() => {
return getCommonShareMessage(true, inviteCode); return getCommonShareMessage({ inviteCode, title: '数万名优质主播等你来挑', path: PageUrl.Anchor });
}); });
useDidShow(() => requestUnreadMessageCount()); useDidShow(() => requestUnreadMessageCount());
const filters = useMemo(
() => ({
...moreFilters,
topCategory: topCategory === JobType.All ? undefined : topCategory,
gender: gender === -1 ? undefined : gender,
employType,
}),
[moreFilters, gender, employType, topCategory]
);
return ( return (
<HomePage> <HomePage type={PageType.Anchor}>
{!!loading && <PageLoading className={`${PREFIX}__loading`} />} {!!loading && <PageLoading className={`${PREFIX}__loading`} />}
<CustomNavigationBar className={`${PREFIX}__navigation-bar`}> <CustomNavigationBar className={`${PREFIX}__navigation-bar`}>
{selectJob && <SwitchBar title={selectJob.title.substring(0, 4)} onClick={handleClickSwitch} />} {selectJob && <SwitchBar title={selectJob.title.substring(0, 4)} onClick={handleClickSwitch} />}
@ -210,13 +271,27 @@ export default function AnchorPage() {
</div> </div>
))} ))}
</div> </div>
<div className={classNames(`${PREFIX}__filter`)} onClick={handleClickSalarySelect}> <div className={classNames(`${PREFIX}__filter`)}>
<div className="title"></div> <div className={`${PREFIX}__filter-item`} onClick={handleClickFilterSelect(FilterType.gender)}>
{showFilter ? <ArrowUp /> : <ArrowDown />} <div className="title">{gender === GenderType.All ? '性别' : GENDER_TYPE_TITLE_MAP[gender]}</div>
{showFilter === FilterType.gender ? <ArrowUp /> : <ArrowDown />}
</div>
<div className={`${PREFIX}__filter-item`} onClick={handleClickFilterSelect(FilterType.employType)}>
<div className="title">{employType === EmployType.All ? '类型' : EMPLOY_TYPE_TITLE_MAP[employType]}</div>
{showFilter === FilterType.employType ? <ArrowUp /> : <ArrowDown />}
</div>
<div className={`${PREFIX}__filter-item`} onClick={handleClickFilterSelect(FilterType.topCategory)}>
<div className="title">{topCategory === JobType.All ? '品类' : JOB_TYPE_TITLE_MAP[topCategory]}</div>
{showFilter === FilterType.topCategory ? <ArrowUp /> : <ArrowDown />}
</div>
<div className={`${PREFIX}__filter-item`} onClick={handleClickFilterSelect(FilterType.more)}>
<div className="title"></div>
{showFilter === FilterType.more ? <ArrowUp /> : <ArrowDown />}
</div>
</div> </div>
</div> </div>
<div className={`${PREFIX}__banner`}> <div className={`${PREFIX}__banner`}>
<PartnerBanner /> <PartnerBanner isBoss />
</div> </div>
<ListWrapper <ListWrapper
filters={filters} filters={filters}
@ -229,14 +304,53 @@ export default function AnchorPage() {
className={LIST_CONTAINER_CLASS} className={LIST_CONTAINER_CLASS}
/> />
<Overlay <Overlay
visible={showFilter} visible={showFilter === FilterType.gender}
onClickOuter={handleHideFilter} onClickOuter={handleHideFilter}
outerClassName={`${PREFIX}__overlay-outer`} outerClassName={`${PREFIX}__overlay-outer`}
innerClassName={`${PREFIX}__overlay-inner`} innerClassName={`${PREFIX}__overlay-inner`}
> >
<AnchorPicker value={filters} onConfirm={handleFilterChange} /> <GenderSelect value={gender} onSelect={handleChangeSelectGender} />
</Overlay>
<Overlay
visible={showFilter === FilterType.employType}
onClickOuter={handleHideFilter}
outerClassName={`${PREFIX}__overlay-outer`}
innerClassName={`${PREFIX}__overlay-inner`}
>
<EmployTypeSelect value={employType} onSelect={handleChangeSelectEmployType} />
</Overlay>
<Overlay
visible={showFilter === FilterType.topCategory}
onClickOuter={handleHideFilter}
outerClassName={`${PREFIX}__overlay-outer`}
innerClassName={`${PREFIX}__overlay-inner`}
>
<TopCategorySelect value={topCategory} onSelect={handleChangeSelectTopCategory} />
</Overlay>
<Overlay
visible={showFilter === FilterType.more}
onClickOuter={handleHideFilter}
outerClassName={`${PREFIX}__overlay-outer`}
innerClassName={`${PREFIX}__overlay-inner`}
>
<AnchorPicker value={moreFilters} onConfirm={handleFilterChange} />
</Overlay> </Overlay>
</div> </div>
<Popup
rounded
open={openPopup}
placement="bottom"
className={`${PREFIX}__popup`}
onClose={() => setOpenPopup(false)}
>
<div className={`${PREFIX}__popup-content`}>
<div className={`${PREFIX}__popup-title`}></div>
<Button className={`${PREFIX}__popup-confirm`} onClick={handleGoCreate}>
</Button>
</div>
<SafeBottomPadding />
</Popup>
</HomePage> </HomePage>
); );
} }

View File

@ -6,14 +6,15 @@ import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import JobManageList, { IJobManageListProps } from '@/components/job-manage-list'; import JobManageList, { IJobManageListProps } from '@/components/job-manage-list';
import { CompanyPublishJobDialog } from '@/components/product-dialog/publish-job';
import SafeBottomPadding from '@/components/safe-bottom-padding'; import SafeBottomPadding from '@/components/safe-bottom-padding';
import { PageUrl } from '@/constants/app';
import { ReportEventId } from '@/constants/event'; import { ReportEventId } from '@/constants/event';
import { JOB_MANAGE_TABS, JobManageStatus, JobManageType } from '@/constants/job'; import { JOB_MANAGE_TABS, JobManageStatus, JobManageType } from '@/constants/job';
import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height'; import useListHeight, { IUseListHeightProps } from '@/hooks/use-list-height';
import useUserInfo from '@/hooks/use-user-info'; import useUserInfo from '@/hooks/use-user-info';
import { logWithPrefix } from '@/utils/common'; import { logWithPrefix } from '@/utils/common';
import { reportEvent } from '@/utils/event'; import { reportEvent } from '@/utils/event';
import { navigateTo } from '@/utils/route';
import { ensureUserInfo } from '@/utils/user'; import { ensureUserInfo } from '@/utils/user';
import './index.less'; import './index.less';
@ -91,7 +92,6 @@ export default function CertificationManage() {
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const listHeight = useListHeight(CALC_LIST_PROPS); const listHeight = useListHeight(CALC_LIST_PROPS);
const [tabType, setTabType] = useState<JobManageType>(JobManageType.All); const [tabType, setTabType] = useState<JobManageType>(JobManageType.All);
const [showPublish, setShowPublish] = useState(false);
const handleTypeChange = useCallback(value => setTabType(value), []); const handleTypeChange = useCallback(value => setTabType(value), []);
@ -101,7 +101,7 @@ export default function CertificationManage() {
if (!(await ensureUserInfo(userInfo))) { if (!(await ensureUserInfo(userInfo))) {
return; return;
} }
setShowPublish(true); navigateTo(PageUrl.JobPublish);
}, [userInfo]); }, [userInfo]);
return ( return (
@ -124,7 +124,6 @@ export default function CertificationManage() {
</Button> </Button>
</div> </div>
<SafeBottomPadding className={SAFE_BOTTOM_PADDING_CLASS} /> <SafeBottomPadding className={SAFE_BOTTOM_PADDING_CLASS} />
<div>{showPublish && <CompanyPublishJobDialog userInfo={userInfo} onClose={() => setShowPublish(false)} />}</div>
</div> </div>
); );
} }

View File

@ -1,21 +1,29 @@
import { BaseEventOrig, Button, ButtonProps, Image, InputProps } from '@tarojs/components'; import { BaseEventOrig, Button, ButtonProps, Image, InputProps } from '@tarojs/components';
import Taro, { UploadTask } from '@tarojs/taro'; import Taro, { UploadTask } from '@tarojs/taro';
import { useCallback, useEffect, useState } from 'react'; import { Dialog } from '@taroify/core';
import { useCallback, useEffect, useState } from 'react';
import BlFormInput from '@/components/bl-form-input'; import BlFormInput from '@/components/bl-form-input';
import BlFormItem from '@/components/bl-form-item'; import BlFormItem from '@/components/bl-form-item';
import LoadingDialog from '@/components/loading-dialog'; import LoadingDialog from '@/components/loading-dialog';
import CompanyPublishJobBuy from '@/components/product-dialog/steps-ui/company-publish-job-buy';
import SafeBottomPadding from '@/components/safe-bottom-padding'; import SafeBottomPadding from '@/components/safe-bottom-padding';
import { PageUrl } from '@/constants/app'; import { EventName, PageUrl } from '@/constants/app';
import { CertificationStatusType } from '@/constants/company'; import { CertificationStatusType } from '@/constants/company';
import { CollectEventName, ReportEventId } from '@/constants/event'; import { CollectEventName, ReportEventId } from '@/constants/event';
import { ProductType } from '@/constants/product';
import useCachedJobId from '@/hooks/use-cached-job';
import useUserInfo from '@/hooks/use-user-info'; import useUserInfo from '@/hooks/use-user-info';
import store from '@/store';
import { clearCachedJobId } from '@/store/actions';
import { ICertificationRequest } from '@/types/company'; import { ICertificationRequest } from '@/types/company';
import { isValidIdCard, isValidPhone, logWithPrefix } from '@/utils/common'; import { isValidIdCard, isValidPhone, logWithPrefix } from '@/utils/common';
import { postCertification, getPhone } from '@/utils/company'; import { postCertification, getPhone } from '@/utils/company';
import { collectEvent, reportEvent } from '@/utils/event'; import { collectEvent, reportEvent } from '@/utils/event';
import { postPublishJob } from '@/utils/job';
import { chooseMedia } from '@/utils/material'; import { chooseMedia } from '@/utils/material';
import { requestProductBalance } from '@/utils/product';
import { redirectTo } from '@/utils/route'; import { redirectTo } from '@/utils/route';
import Toast from '@/utils/toast'; import Toast from '@/utils/toast';
import { dispatchUpdateUser, requestUserInfo } from '@/utils/user'; import { dispatchUpdateUser, requestUserInfo } from '@/utils/user';
@ -23,6 +31,7 @@ import { uploadVideo } from '@/utils/video';
import './index.less'; import './index.less';
const PREFIX = 'page-certification'; const PREFIX = 'page-certification';
const log = logWithPrefix(PREFIX); const log = logWithPrefix(PREFIX);
const needIdCard = false; const needIdCard = false;
@ -100,6 +109,8 @@ export default function Certification() {
// const [code, setCode] = useState(''); // const [code, setCode] = useState('');
const [company, setCompany] = useState(''); const [company, setCompany] = useState('');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [showBuy, setShowBuy] = useState(false);
const cachedJobId = useCachedJobId();
const handleClickIdCardLeft = useCallback(async () => { const handleClickIdCardLeft = useCallback(async () => {
reportEvent(ReportEventId.CLICK_UPLOAD_ID_CARD, { type: 'left' }); reportEvent(ReportEventId.CLICK_UPLOAD_ID_CARD, { type: 'left' });
@ -133,6 +144,29 @@ export default function Certification() {
setCompany(value); setCompany(value);
}, []); }, []);
const handleCloseBuy = useCallback(() => {
setShowBuy(false);
redirectTo(PageUrl.CertificationManage);
}, []);
const handlePublishJob = useCallback(async () => {
try {
Taro.showLoading();
await postPublishJob(cachedJobId!);
await Toast.success('通告发布成功', 1500, true);
store.dispatch(clearCachedJobId());
Taro.eventCenter.trigger(EventName.COMPANY_JOB_PUBLISH_CHANGED);
redirectTo(PageUrl.CertificationManage);
} catch (e) {
console.error('submit error', e);
Toast.error('通告发布失败');
collectEvent(CollectEventName.PUBLISH_JOB_FAILED, e);
redirectTo(PageUrl.CertificationManage);
} finally {
Taro.hideLoading();
}
}, [cachedJobId]);
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {
reportEvent(ReportEventId.CLICK_CERTIFICATION_SUBMIT); reportEvent(ReportEventId.CLICK_CERTIFICATION_SUBMIT);
const data: ICertificationRequest = { const data: ICertificationRequest = {
@ -157,7 +191,14 @@ export default function Certification() {
return; return;
} }
dispatchUpdateUser({ bossAuthStatus: CertificationStatusType.Success }); dispatchUpdateUser({ bossAuthStatus: CertificationStatusType.Success });
redirectTo(PageUrl.CertificationManage);
const [time] = await requestProductBalance(ProductType.CompanyPublishJob);
if (time <= 0) {
setShowBuy(true);
Taro.hideLoading();
} else {
await handlePublishJob();
}
} catch (e) { } catch (e) {
console.error('submit error', e); console.error('submit error', e);
Toast.error('认证失败请重试'); Toast.error('认证失败请重试');
@ -173,6 +214,7 @@ export default function Certification() {
try { try {
const { phoneNumber } = await getPhone(e.detail.code); const { phoneNumber } = await getPhone(e.detail.code);
setRevisedPhone(phoneNumber); setRevisedPhone(phoneNumber);
// eslint-disable-next-line @typescript-eslint/no-shadow
} catch (e) { } catch (e) {
Toast.error('获取手机号失败'); Toast.error('获取手机号失败');
} }
@ -262,6 +304,11 @@ export default function Certification() {
<div> <div>
<LoadingDialog open={open} text="认证中" /> <LoadingDialog open={open} text="认证中" />
</div> </div>
<Dialog open={showBuy} onClose={handleCloseBuy}>
<Dialog.Content>
<CompanyPublishJobBuy onNext={handlePublishJob} />
</Dialog.Content>
</Dialog>
</div> </div>
); );
} }

View File

@ -4,7 +4,7 @@ import { Button } from '@taroify/core';
import { useCallback } from 'react'; import { useCallback } from 'react';
import HomePage from '@/components/home-page'; import HomePage from '@/components/home-page';
import { PageUrl } from '@/constants/app'; import { PageType, PageUrl } from '@/constants/app';
import { copy, logWithPrefix } from '@/utils/common'; import { copy, logWithPrefix } from '@/utils/common';
import { navigateTo } from '@/utils/route'; import { navigateTo } from '@/utils/route';
@ -136,7 +136,7 @@ export default function DevDebug() {
}); });
return ( return (
<HomePage> <HomePage type={PageType.DEV}>
<div className={PREFIX}> <div className={PREFIX}>
{/* <div>{`最近一次的订单 ID: ${lastOrderNo}`}</div> {/* <div>{`最近一次的订单 ID: ${lastOrderNo}`}</div>
<Button onClick={() => handleCreateOrder(OrderType.Group)} style={marginTopStyle} color="primary"> <Button onClick={() => handleCreateOrder(OrderType.Group)} style={marginTopStyle} color="primary">

View File

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '赠送会员',
});

View File

@ -0,0 +1,130 @@
@import '@/styles/variables.less';
@import '@/styles/common.less';
.give-vip {
padding: 40px 24px 200px 24px;
&__hint {
font-size: 28px;
line-height: 48px;
color: @blColorG2;
gap: 12px;
.flex-row();
justify-content: center;
}
&__icon {
width: 65px;
height: 65px;
}
&__title {
margin-top: 24px;
font-weight: 500;
font-size: 40px;
line-height: 48px;
color: @blColor;
text-align: center;
flex: 0 1 auto;
gap: 12px;
.flex-row();
justify-content: center;
.highlight {
margin-left: -12px;
display: inline-block;
color: @blHighlightColor;
}
}
&__bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
&__info {
&-block {
margin-top: 48px;
}
&-title {
font-weight: 500;
font-size: 32px;
line-height: 32px;
color: #1d2129;
margin-bottom: 24px;
}
&-card {
box-sizing: border-box;
background: #ffffff;
border-radius: 24px;
padding: 32px;
}
&-body {
font-weight: 400;
font-size: 28px;
line-height: 40px;
color: #333333;
margin-bottom: 32px;
}
&-img {
width: 100%;
height: 614px;
}
}
&__footer {
position: fixed;
bottom: 0;
width: 100%;
padding: 12px 32px;
box-shadow: 0px -4px 20px 0px #00000014;
box-sizing: border-box;
background: #ffffff;
}
&__footer-body {
.flex-row();
justify-content: center;
}
&__button {
.button(@height: 88px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;);
width: 400px;
}
&__coupon {
&-info {
position: relative;
margin-top: 32px;
padding: 24px;
box-sizing: border-box;
border-radius: 24px;
.flex-column();
justify-content: center;
}
&-title {
font-weight: 500;
font-size: 32px;
line-height: 48px;
color: @blHighlightColor;
margin-bottom: 8px;
}
&-intro {
font-size: 28px;
line-height: 40px;
margin-bottom: 12px;
color: @blColor;
gap: 4px;
.flex-row();
.highlight {
font-weight: 600;
font-size: 32px;
line-height: 56px;
color: @blHighlightColor;
}
}
&-valid {
font-size: 28px;
line-height: 40px;
color: @blColorG2;
}
}
}

View File

@ -0,0 +1,103 @@
import { Image, Button } from '@tarojs/components';
import { useLoad } from '@tarojs/taro';
import { Fragment, useCallback, useState } from 'react';
import SafeBottomPadding from '@/components/safe-bottom-padding';
import { PageUrl } from '@/constants/app';
import { logWithPrefix } from '@/utils/common';
import { claimMembershipCoupon, getCouponCodeFromQuery } from '@/utils/coupon';
import { getPageQuery, switchTab } from '@/utils/route';
import { formatTime } from '@/utils/time';
import './index.less';
const PREFIX = 'give-vip';
const log = logWithPrefix(PREFIX);
enum State {
PENDING,
SUCCESS,
FAILED,
}
export default function GiveVip() {
const [state, setState] = useState<State>(State.PENDING);
const [expireAt, setExpireAt] = useState('');
const [usedBefore, setUsedBefore] = useState(false);
const handleNavigate = useCallback(() => {
switchTab(PageUrl.Job);
}, []);
useLoad(() => {
const query = getPageQuery<{ d: string }>();
const code = getCouponCodeFromQuery(query);
claimMembershipCoupon(code!)
.then(res => {
setUsedBefore(res.usedBefore);
setExpireAt(res.usageExpireAt);
setState(Date.now() > new Date(res.usageExpireAt).getTime() ? State.FAILED : State.SUCCESS);
})
.catch(err => {
console.error(err);
setState(State.FAILED);
});
});
return (
<div className={PREFIX}>
{state === State.SUCCESS && (
<Fragment>
<div className={`${PREFIX}__title`}>
<Image src="https://publiccdn.neighbourhood.com.cn/img/partner-yes.svg" className={`${PREFIX}__icon`} />
<div>{usedBefore ? '宝子,你已经领过了,下周再来' : '宝子,播络会员领取成功'}</div>
</div>
<div className={`${PREFIX}__coupon-info`}>
<Image src="https://publiccdn.neighbourhood.com.cn/img/parnet-share-bg.png" className={`${PREFIX}__bg`} />
<div className={`${PREFIX}__coupon-title`}></div>
<div className={`${PREFIX}__coupon-intro`}>
<div className="highlight">10</div>
</div>
<div className={`${PREFIX}__coupon-valid`}>
{formatTime(expireAt, 'YYYY/MM/DD HH:mm:ss', false)}
</div>
</div>
</Fragment>
)}
{state === State.FAILED && (
<Fragment>
<div className={`${PREFIX}__hint`}>
<Image
src="https://publiccdn.neighbourhood.com.cn/img/link-invalid.svg"
mode="aspectFit"
className={`${PREFIX}__icon`}
/>
<div className={`${PREFIX}__hint-title`}></div>
</div>
<div className={`${PREFIX}__title`}>
<div className="highlight"></div>
</div>
</Fragment>
)}
<div className={`${PREFIX}__info-block`}>
<div className={`${PREFIX}__info-title`}></div>
<div className={`${PREFIX}__info-card`}>
<div className={`${PREFIX}__info-body`}>
便
</div>
<Image
className={`${PREFIX}__info-img`}
src="https://publiccdn.neighbourhood.com.cn/img/partner-share-coupon-img.png"
mode="heightFix"
/>
</div>
</div>
<div className={`${PREFIX}__footer`}>
<div className={`${PREFIX}__footer-body`}>
<Button className={`${PREFIX}__button`} onClick={handleNavigate}>
使
</Button>
</div>
<SafeBottomPadding />
</div>
</div>
);
}

View File

@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '群代发',
});

View File

@ -0,0 +1,12 @@
import UserBatchPublish from '@/components/user-batch-publish';
import './index.less';
const PREFIX = 'group-delegate-publish';
export default function Partner() {
return (
<div className={PREFIX}>
<UserBatchPublish />
</div>
);
}

View File

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

Some files were not shown because too many files have changed in this diff Show More