feat: first commit
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
2
.env.development
Normal file
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config
|
||||
TARO_APP_ID="wxf0724a83f8e377d2"
|
1
.env.production
Normal file
1
.env.production
Normal file
@ -0,0 +1 @@
|
||||
TARO_APP_ID="wxf0724a83f8e377d2"
|
11
.eslintignore
Normal file
11
.eslintignore
Normal file
@ -0,0 +1,11 @@
|
||||
build/*.js
|
||||
public
|
||||
dist
|
||||
node_modules
|
||||
config
|
||||
config-overrides.js
|
||||
prettierrc
|
||||
.DS_Store
|
||||
.eslintrc.json
|
||||
.env.development
|
||||
.env.production
|
26
.eslintrc
Normal file
26
.eslintrc
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": ["taro/react", "prettier", "plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["prettier", "import", "@typescript-eslint"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": ["builtin", "external", ["internal", "parent", "sibling", "index"], "unknown"],
|
||||
"pathGroups": [{
|
||||
"pattern": "@/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
dist/
|
||||
deploy_versions/
|
||||
.temp/
|
||||
.rn_temp/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.swc
|
||||
project.private.config.json
|
||||
node_test.js
|
11
.prettierignore
Normal file
11
.prettierignore
Normal file
@ -0,0 +1,11 @@
|
||||
build/*.js
|
||||
public
|
||||
dist
|
||||
node_modules
|
||||
config
|
||||
config-overrides.js
|
||||
prettierrc
|
||||
.DS_Store
|
||||
.eslintrc.json
|
||||
.env.development
|
||||
.env.production
|
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve",
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "auto",
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5"
|
||||
}
|
6
README.md
Normal file
6
README.md
Normal file
@ -0,0 +1,6 @@
|
||||
播络 App
|
||||
|
||||
### 开发指引
|
||||
1. yarn
|
||||
2. yarn dev:weapp
|
||||
3. 下载并打开[微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html),然后**选择项目根目录**进行预览。
|
12
__tests__/index.test.js
Normal file
12
__tests__/index.test.js
Normal file
@ -0,0 +1,12 @@
|
||||
import TestUtils from '@tarojs/test-utils-react'
|
||||
|
||||
describe('Testing', () => {
|
||||
|
||||
test('Test', async () => {
|
||||
const testUtils = new TestUtils()
|
||||
await testUtils.createApp()
|
||||
await testUtils.PageLifecycle.onShow('pages/home/index')
|
||||
expect(testUtils.html()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
})
|
34
babel.config.js
Normal file
34
babel.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
// babel-preset-taro 更多选项和默认值:
|
||||
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'taro',
|
||||
{
|
||||
framework: 'react',
|
||||
ts: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
'import',
|
||||
{
|
||||
libraryName: '@taroify/core',
|
||||
libraryDirectory: '',
|
||||
style: true,
|
||||
},
|
||||
'@taroify/core',
|
||||
],
|
||||
[
|
||||
'import',
|
||||
{
|
||||
libraryName: '@taroify/icons',
|
||||
libraryDirectory: '',
|
||||
camel2DashComponentName: false,
|
||||
style: () => '@taroify/icons/style',
|
||||
},
|
||||
'@taroify/icons',
|
||||
],
|
||||
],
|
||||
};
|
9
config/dev.ts
Normal file
9
config/dev.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli";
|
||||
export default {
|
||||
logger: {
|
||||
quiet: false,
|
||||
stats: true
|
||||
},
|
||||
mini: {},
|
||||
h5: {}
|
||||
} satisfies UserConfigExport
|
124
config/index.ts
Normal file
124
config/index.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { ConfigEnv, defineConfig, type UserConfigExport } from '@tarojs/cli';
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||
import { PluginItem } from '@tarojs/taro/types/compile/config';
|
||||
import devConfig from './dev';
|
||||
import prodConfig from './prod';
|
||||
|
||||
const MockPlugin: PluginItem = [
|
||||
'@tarojs/plugin-mock',
|
||||
{
|
||||
host: 'localhost',
|
||||
port: 9527,
|
||||
}
|
||||
];
|
||||
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
export default defineConfig(async (merge, _env: ConfigEnv) => {
|
||||
const baseConfig: UserConfigExport = {
|
||||
projectName: 'boluo-app',
|
||||
date: '2024-6-7',
|
||||
designWidth: 750,
|
||||
deviceRatio: {
|
||||
640: 2.34 / 2,
|
||||
750: 1,
|
||||
375: 2,
|
||||
828: 1.81 / 2
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
plugins: ['@tarojs/plugin-html'],
|
||||
// plugins: ['@tarojs/plugin-html', MockPlugin],
|
||||
defineConstants: {
|
||||
},
|
||||
copy: {
|
||||
patterns: [
|
||||
],
|
||||
options: {
|
||||
}
|
||||
},
|
||||
framework: 'react',
|
||||
compiler: {
|
||||
type: 'webpack5',
|
||||
prebundle: {
|
||||
exclude: ['@taroify/icons']
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
|
||||
},
|
||||
mini: {
|
||||
// debugReact: true,
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true,
|
||||
},
|
||||
postcss: {
|
||||
pxtransform: {
|
||||
enable: true,
|
||||
config: {
|
||||
|
||||
}
|
||||
},
|
||||
url: {
|
||||
enable: true,
|
||||
config: {
|
||||
limit: 1024 // 设定转换尺寸上限
|
||||
}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin);
|
||||
// chain.plugin('analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []);
|
||||
}
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
output: {
|
||||
filename: 'js/[name].[hash:8].js',
|
||||
chunkFilename: 'js/[name].[chunkhash:8].js'
|
||||
},
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true,
|
||||
filename: 'css/[name].[hash].css',
|
||||
chunkFilename: 'css/[name].[chunkhash].css'
|
||||
},
|
||||
postcss: {
|
||||
autoprefixer: {
|
||||
enable: true,
|
||||
config: {}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
||||
}
|
||||
},
|
||||
rn: {
|
||||
appName: 'taroDemo',
|
||||
postcss: {
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 本地开发构建配置(不混淆压缩)
|
||||
return merge({}, baseConfig, devConfig)
|
||||
}
|
||||
// 生产构建配置(默认开启压缩混淆等)
|
||||
return merge({}, baseConfig, prodConfig)
|
||||
})
|
32
config/prod.ts
Normal file
32
config/prod.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli";
|
||||
export default {
|
||||
mini: {},
|
||||
h5: {
|
||||
/**
|
||||
* WebpackChain 插件配置
|
||||
* @docs https://github.com/neutrinojs/webpack-chain
|
||||
*/
|
||||
// webpackChain (chain) {
|
||||
// /**
|
||||
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
|
||||
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
|
||||
// */
|
||||
// chain.plugin('analyzer')
|
||||
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
|
||||
// /**
|
||||
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
|
||||
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
|
||||
// */
|
||||
// const path = require('path')
|
||||
// const Prerender = require('prerender-spa-plugin')
|
||||
// const staticDir = path.join(__dirname, '..', 'dist')
|
||||
// chain
|
||||
// .plugin('prerender')
|
||||
// .use(new Prerender({
|
||||
// staticDir,
|
||||
// routes: [ '/pages/index/index' ],
|
||||
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
} satisfies UserConfigExport
|
6
jest.config.ts
Normal file
6
jest.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
const defineJestConfig = require('@tarojs/test-utils-react/dist/jest.js').default
|
||||
|
||||
module.exports = defineJestConfig({
|
||||
testEnvironment: 'jsdom',
|
||||
testMatch: ['<rootDir>/__tests__/**/*.(spec|test).[jt]s?(x)']
|
||||
})
|
194
mock/index.ts
Normal file
194
mock/index.ts
Normal file
@ -0,0 +1,194 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const wrapperData = (data: any, code: number = 200, message: string = 'success') => {
|
||||
if (code === 200) {
|
||||
return data;
|
||||
}
|
||||
return {
|
||||
data,
|
||||
code,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
let jobId = 0;
|
||||
const getJobData = (size: number) =>
|
||||
new Array(size).fill(1).map(() => ({
|
||||
id: `${jobId++}`,
|
||||
title: '滨江品牌淑女装兼职主播滨江品牌淑女装兼职主播滨江品牌淑女装兼职主播',
|
||||
tags: ['女装', '抖音', '快手', '视频号', '直播 4-5h'],
|
||||
employType: 'ALL',
|
||||
salary: '时薪 200-300',
|
||||
jobDescription: 'TOP级账号!5年直播团队体系成熟!',
|
||||
publisher: '杭州主播群3-甲子',
|
||||
publisherAvatar: '',
|
||||
jobLocation: {
|
||||
latitude: 39.961,
|
||||
longitude: 116.4551,
|
||||
cityCode: '430100',
|
||||
countyCode: '430121',
|
||||
address: '朝阳区望京街道',
|
||||
},
|
||||
}));
|
||||
|
||||
const JOB_DETAIL_DATA = {
|
||||
id: `abcs`,
|
||||
title: '滨江品牌淑女装兼职主播滨江品牌淑女装兼职主播滨江品牌淑女装兼职主播',
|
||||
tags: ['女装', '抖音', '快手', '视频号', '直播 4-5h'],
|
||||
employType: 'ALL',
|
||||
salary: '时薪 200-300',
|
||||
jobDescription: 'TOP级账号!5年直播团队体系成熟!',
|
||||
publisher: '杭州主播群3-甲子',
|
||||
publisherAvatar: '',
|
||||
jobLocation: {
|
||||
latitude: 39.961,
|
||||
longitude: 116.4551,
|
||||
cityCode: '430100',
|
||||
countyCode: '430121',
|
||||
address: '朝阳区望京街道',
|
||||
},
|
||||
created: 1719122520631,
|
||||
updated: 1719122520631,
|
||||
imGroupId: '12345',
|
||||
imGroupNick: '服装设计交流群',
|
||||
isFollow: false,
|
||||
sourceText: `可兼职可全职\n账号:FAIRWHALE时尚旗旗舰店\nTOP级账号!5年直播团队体系成熟!有成熟晋升机制!中少淑穿版好经验不足也可以报名!\n各路女装大神可以来无责分红机制!公司背景强大!资金不是问题!!\n时薪:3-800/H(能给),薪资:2W-8W+1-3%提成\n要求:
|
||||
1.身高160-168。亲和力强,站播,貌美口齿清晰
|
||||
2.会控场能带动直播间气氛会拉流量会玩免费流
|
||||
3.中少淑穿版效果要好
|
||||
4.场均20-50w的都可以报名,都有机会晋升大号
|
||||
`,
|
||||
};
|
||||
|
||||
let groupId = 0;
|
||||
const getGroupList = (size: number) =>
|
||||
new Array(size).fill(1).map(() => ({
|
||||
id: `${groupId++}`,
|
||||
imGroupId: '1',
|
||||
imGroupNick: '广州主播主播🍒经纪群🍒经纪群经纪群',
|
||||
joinedTime: 1719119060767,
|
||||
allJobs: Math.floor(Math.random() * 1000),
|
||||
groupAvatar: 'https://neighbourhood.cn/p_d.png',
|
||||
}));
|
||||
|
||||
const getJobListData = (emptyList: boolean = false) => {
|
||||
if (emptyList) {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: false,
|
||||
jobResults: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
jobResults: getJobData(10),
|
||||
};
|
||||
};
|
||||
|
||||
const getUserJobListData = (emptyList: boolean = false) => {
|
||||
if (emptyList) {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: false,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
data: {
|
||||
'2024-07-23': getJobData(10),
|
||||
'2024-07-22': getJobData(10),
|
||||
'2024-07-18': getJobData(10),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getGroupListData = (emptyList: boolean = false) => {
|
||||
if (emptyList) {
|
||||
return {
|
||||
allGroups: [],
|
||||
myJoinedGroups: [],
|
||||
myCreatedGroups: [],
|
||||
myFollowedGroups: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
allGroups: getGroupList(10),
|
||||
myJoinedGroups: getGroupList(10),
|
||||
myCreatedGroups: getGroupList(10),
|
||||
myFollowedGroups: getGroupList(10),
|
||||
};
|
||||
};
|
||||
|
||||
const getProductListData = (emptyList: boolean = false) => {
|
||||
if (emptyList) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
productId: 'job',
|
||||
balance: 3,
|
||||
created: 1719109060767,
|
||||
updated: 1719109060767,
|
||||
},
|
||||
{
|
||||
productId: 'group',
|
||||
balance: 2,
|
||||
created: 1719109060767,
|
||||
updated: 1719109060767,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const mockData = {
|
||||
'POST /api/user/login': wrapperData({
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
||||
expires: 3600,
|
||||
}),
|
||||
'POST /api/location/get': wrapperData({
|
||||
provinceCode: '430000',
|
||||
provinceDesc: '湖南省',
|
||||
cityCode: '430100',
|
||||
cityDesc: '长沙市',
|
||||
}),
|
||||
'POST /api/user/get': wrapperData({
|
||||
userId: 'abc',
|
||||
nickname: '微信用户9627',
|
||||
avatarUrl: '',
|
||||
isDefaultAvatar: true,
|
||||
isDefaultNickname: true,
|
||||
isBindPhone: false,
|
||||
// isBindPhone: true,
|
||||
}),
|
||||
'POST /api/user/setPhone': wrapperData({}),
|
||||
'POST /api/userGroup/follow': wrapperData({}),
|
||||
'POST /api/userGroup/list': wrapperData(getGroupListData(false)),
|
||||
'POST /api/job/search': wrapperData(getJobListData(false)),
|
||||
'POST /api/job/searchMyJobs': wrapperData(getJobListData(true)),
|
||||
'POST /api/job/get': wrapperData(JOB_DETAIL_DATA),
|
||||
'POST /api/job/user/searchMyDeclared': wrapperData(getUserJobListData(false)),
|
||||
'POST /api/product/listMyProduct': wrapperData(getProductListData(false)),
|
||||
'POST /api/product/getMyProductDetail': wrapperData({ balance: 2 }),
|
||||
'POST /api/product/getProductUseRecord': wrapperData(false),
|
||||
// 'POST /api/product/getProductUseRecord': wrapperData({}),
|
||||
// 'POST /api/product/use': wrapperData({}),
|
||||
// 'POST /api/product/getProductUseRecord': wrapperData({
|
||||
// declarationTypeResult: {
|
||||
// type: 0,
|
||||
// publisherAcctNo: 'yeruth',
|
||||
// },
|
||||
// }),
|
||||
'POST /api/product/use': wrapperData({
|
||||
declarationTypeResult: {
|
||||
type: 0,
|
||||
publisherAcctNo: 'yeruth',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default mockData;
|
108
package.json
Normal file
108
package.json
Normal file
@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "boluo-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "boluo app",
|
||||
"templateInfo": {
|
||||
"name": "default",
|
||||
"typescript": true,
|
||||
"css": "Less",
|
||||
"framework": "React"
|
||||
},
|
||||
"scripts": {
|
||||
"build:weapp": "taro build --type weapp",
|
||||
"build:swan": "taro build --type swan",
|
||||
"build:alipay": "taro build --type alipay",
|
||||
"build:tt": "taro build --type tt",
|
||||
"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": [
|
||||
"last 3 versions",
|
||||
"Android >= 4.1",
|
||||
"ios >= 8"
|
||||
],
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.5",
|
||||
"@taroify/core": "^0.3.2-alpha.0",
|
||||
"@taroify/icons": "0.6.4-alpha.0",
|
||||
"@tarojs/components": "3.6.31",
|
||||
"@tarojs/helper": "3.6.31",
|
||||
"@tarojs/plugin-framework-react": "3.6.31",
|
||||
"@tarojs/plugin-html": "^3.6.31",
|
||||
"@tarojs/plugin-platform-alipay": "3.6.31",
|
||||
"@tarojs/plugin-platform-h5": "3.6.31",
|
||||
"@tarojs/plugin-platform-harmony-hybrid": "3.6.31",
|
||||
"@tarojs/plugin-platform-jd": "3.6.31",
|
||||
"@tarojs/plugin-platform-qq": "3.6.31",
|
||||
"@tarojs/plugin-platform-swan": "3.6.31",
|
||||
"@tarojs/plugin-platform-tt": "3.6.31",
|
||||
"@tarojs/plugin-platform-weapp": "3.6.31",
|
||||
"@tarojs/react": "3.6.31",
|
||||
"@tarojs/runtime": "3.6.31",
|
||||
"@tarojs/shared": "3.6.31",
|
||||
"@tarojs/taro": "3.6.31",
|
||||
"classnames": "^2.5.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-redux": "^8.1.2",
|
||||
"redux": "^4.2.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
|
||||
"@tarojs/cli": "3.6.31",
|
||||
"@tarojs/plugin-mock": "^0.0.9",
|
||||
"@tarojs/taro-loader": "3.6.31",
|
||||
"@tarojs/test-utils-react": "^0.1.1",
|
||||
"@tarojs/webpack5-runner": "3.6.31",
|
||||
"@types/jest": "^29.3.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/webpack-env": "^1.13.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"babel-plugin-import": "^1.13.8",
|
||||
"babel-preset-taro": "3.6.31",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-taro": "3.6.31",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-react": "^7.8.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"postcss": "^8.4.18",
|
||||
"prettier": "^3.3.1",
|
||||
"react-refresh": "^0.11.0",
|
||||
"stylelint": "^14.4.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"typescript": "^5.1.0",
|
||||
"webpack": "5.78.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
16524
pnpm-lock.yaml
generated
Normal file
16524
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
project.config.json
Normal file
34
project.config.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"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
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"srcMiniprogramRoot": "dist/",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
},
|
||||
"libVersion": "3.4.6",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wxf0724a83f8e377d2"
|
||||
}
|
9
project.tt.json
Normal file
9
project.tt.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "boluo-app",
|
||||
"appid": "testAppId",
|
||||
"setting": {
|
||||
"es6": false,
|
||||
"minified": false
|
||||
}
|
||||
}
|
3
src/app.config.ts
Normal file
3
src/app.config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { APP_CONFIG } from './hooks/use-config';
|
||||
|
||||
export default defineAppConfig(APP_CONFIG);
|
15
src/app.less
Normal file
15
src/app.less
Normal file
@ -0,0 +1,15 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.base-bg {
|
||||
background: @pageBg;
|
||||
}
|
||||
|
||||
page {
|
||||
.base-bg();
|
||||
// 全部覆盖 taroify tabs 的背景色
|
||||
--tabs-nav-background-color: @pageBg;
|
||||
|
||||
.taroify-tabs__wrap__scroll {
|
||||
.base-bg();
|
||||
}
|
||||
}
|
29
src/app.tsx
Normal file
29
src/app.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useLaunch } from '@tarojs/taro';
|
||||
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { REFRESH_UNREAD_COUNT_TIME } from '@/constants/message';
|
||||
import http from '@/http';
|
||||
import store from '@/store';
|
||||
import { requestUnreadMessageCount } from '@/utils/message';
|
||||
import qiniuUpload from '@/utils/qiniu-upload';
|
||||
import { requestUserInfo, updateLastLoginTime } from '@/utils/user';
|
||||
|
||||
import './app.less';
|
||||
|
||||
function App({ children }: PropsWithChildren<BL.Anything>) {
|
||||
useLaunch(async () => {
|
||||
console.log('App launched.');
|
||||
await http.init();
|
||||
requestUserInfo();
|
||||
updateLastLoginTime();
|
||||
qiniuUpload.init();
|
||||
requestUnreadMessageCount();
|
||||
setInterval(() => requestUnreadMessageCount(), REFRESH_UNREAD_COUNT_TIME);
|
||||
});
|
||||
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
|
||||
export default App;
|
123
src/components/anchor-card/index.less
Normal file
123
src/components/anchor-card/index.less
Normal file
@ -0,0 +1,123 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.anchor-card {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
background: #FFFFFF;
|
||||
|
||||
.color(@defaultColor) {
|
||||
color: var(--read-color, @defaultColor);
|
||||
}
|
||||
|
||||
&__cover {
|
||||
position: relative;
|
||||
width: 188px;
|
||||
min-width: 188px;
|
||||
max-width: 188px;
|
||||
height: 242px;
|
||||
min-height: 242px;
|
||||
max-height: 242px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__cover-skeleton {
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__info-container {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin-right: 10px;
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
.color(@blColor);
|
||||
}
|
||||
|
||||
@maxTextWidth: 49vw;
|
||||
|
||||
&__basic {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG2);
|
||||
margin-top: 16px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__categories {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG2);
|
||||
margin-top: 12px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__year {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG2);
|
||||
margin-top: 12px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__salary {
|
||||
max-width: @maxTextWidth;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
.color(@blHighlightColor);
|
||||
margin-top: 20px;
|
||||
.noWrap();
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
height: 242px;
|
||||
.flex-column();
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG1);
|
||||
}
|
||||
|
||||
&__distance-wrapper {
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__distance-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&__distance {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
.color(@blColorG1);
|
||||
}
|
||||
}
|
82
src/components/anchor-card/index.tsx
Normal file
82
src/components/anchor-card/index.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Image as TaroImage } from '@tarojs/components';
|
||||
|
||||
import { Image } from '@taroify/core';
|
||||
import { PhotoFail } from '@taroify/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import SkeletonLoading from '@/components/skeleton-loading';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { MaterialViewSource, WORK_YEAR_LABELS } from '@/constants/material';
|
||||
import { AnchorInfo } from '@/types/material';
|
||||
import { calcDistance } from '@/utils/location';
|
||||
import { getBasicInfo } from '@/utils/material';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import { activeDate } from '@/utils/time';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: AnchorInfo;
|
||||
jobId?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'anchor-card';
|
||||
const getSalary = (data: AnchorInfo) => {
|
||||
const { fullTimeMinPrice, fullTimeMaxPrice, partyTimeMinPrice, partyTimeMaxPrice } = data;
|
||||
const prices: string[] = [];
|
||||
if (fullTimeMinPrice && fullTimeMaxPrice) {
|
||||
prices.push(`${fullTimeMinPrice / 1000}-${fullTimeMaxPrice / 1000}K/月`);
|
||||
}
|
||||
if (partyTimeMinPrice && partyTimeMaxPrice) {
|
||||
prices.push(`${partyTimeMinPrice}-${partyTimeMaxPrice}/小时`);
|
||||
}
|
||||
return prices.filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
function AnchorCard(props: IProps) {
|
||||
const { data, jobId } = props;
|
||||
const style = data.isRead ? ({ '--read-color': '#999999' } as React.CSSProperties) : {};
|
||||
const cover = (data.materialVideoInfoList.find(video => video.isDefault) || data.materialVideoInfoList[0])?.coverUrl;
|
||||
|
||||
const handleClick = useCallback(
|
||||
() => navigateTo(PageUrl.MaterialView, { jobId, resumeId: data.id, source: MaterialViewSource.AnchorList }),
|
||||
[data, jobId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} style={style} onClick={handleClick}>
|
||||
<Image
|
||||
lazyLoad
|
||||
src={cover}
|
||||
width={188}
|
||||
height={242}
|
||||
mode="aspectFill"
|
||||
fallback={<PhotoFail />}
|
||||
className={`${PREFIX}__cover`}
|
||||
placeholder={<SkeletonLoading customName={`${PREFIX}__cover-skeleton`} />}
|
||||
/>
|
||||
<div className={`${PREFIX}__info-container`}>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
<div className={`${PREFIX}__info__title`}>{data.name}</div>
|
||||
<div className={`${PREFIX}__info__basic`}>{getBasicInfo(data)}</div>
|
||||
<div className={`${PREFIX}__info__year`}>{WORK_YEAR_LABELS[data.workedYear] || ''}</div>
|
||||
{data.workedSecCategoryStr && (
|
||||
<div className={`${PREFIX}__info__categories`}>{`播过 ${data.workedSecCategoryStr}`}</div>
|
||||
)}
|
||||
<div className={`${PREFIX}__info__salary`}>{getSalary(data)}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__right`}>
|
||||
<div className={`${PREFIX}__active-time`}>{activeDate(data.sortTime)}</div>
|
||||
{typeof data.distance !== 'undefined' && (
|
||||
<div className={`${PREFIX}__distance-wrapper`}>
|
||||
<TaroImage className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||||
<div className={`${PREFIX}__distance`}>{calcDistance(data.distance, 1)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnchorCard;
|
0
src/components/anchor-list/index.less
Normal file
0
src/components/anchor-list/index.less
Normal file
209
src/components/anchor-list/index.tsx
Normal file
209
src/components/anchor-list/index.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import AnchorCard from '@/components/anchor-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { AnchorSortType } from '@/constants/material';
|
||||
import { AnchorInfo, GetAnchorListRequest, IAnchorFilters } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestAnchorList as requestData } from '@/utils/material';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetAnchorListRequest> {
|
||||
filters?: IAnchorFilters;
|
||||
}
|
||||
|
||||
export interface IAnchorListProps extends IRequestProps {
|
||||
ready?: boolean;
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 10;
|
||||
const PREFIX = 'anchor-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function AnchorList(props: IAnchorListProps) {
|
||||
const {
|
||||
className,
|
||||
listHeight,
|
||||
refreshDisabled,
|
||||
jobId,
|
||||
filters,
|
||||
cityCode = 'ALL',
|
||||
sortType = AnchorSortType.Recommend,
|
||||
latitude,
|
||||
longitude,
|
||||
ready,
|
||||
onListEmpty,
|
||||
} = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<AnchorInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
const prevRequestProps = useRef<IRequestProps>({});
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, data: anchorResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(anchorResults);
|
||||
currentPage.current = page;
|
||||
!anchorResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
data: anchorResults,
|
||||
} = await requestData({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...anchorResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, hasMore]);
|
||||
|
||||
const handleReadMaterial = useCallback(
|
||||
(materialId: string) => {
|
||||
const index = dataList.findIndex(d => String(d.id) === materialId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
const material = dataList[index];
|
||||
if (!material || material.isRead) {
|
||||
return;
|
||||
}
|
||||
log('auto mark read', materialId);
|
||||
dataList.splice(index, 1, { ...material, isRead: true });
|
||||
setDataList([...dataList]);
|
||||
},
|
||||
[dataList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = {
|
||||
...filters,
|
||||
jobId,
|
||||
cityCode,
|
||||
sortType,
|
||||
latitude,
|
||||
longitude,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
}, [jobId, filters, cityCode, sortType, latitude, longitude]);
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.VIEW_MATERIAL_SUCCESS, handleReadMaterial);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.VIEW_MATERIAL_SUCCESS, handleReadMaterial);
|
||||
};
|
||||
}, [handleReadMaterial]);
|
||||
|
||||
// 初始化数据&配置变更后刷新数据
|
||||
useEffect(() => {
|
||||
// 相比前一次可见时没有数据变更时,不再重新请求
|
||||
if (isEqual(prevRequestProps.current, requestProps.current)) {
|
||||
log('visible/city changed, but request params not change, ignore');
|
||||
return;
|
||||
}
|
||||
// 列表不可见时,先不做处理
|
||||
if (!ready) {
|
||||
log('visible/city changed, but is not ready, only refresh list');
|
||||
return;
|
||||
}
|
||||
|
||||
prevRequestProps.current = requestProps.current;
|
||||
const refresh = async () => {
|
||||
log('visible/city changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, data: anchorResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(anchorResults);
|
||||
currentPage.current = page;
|
||||
!anchorResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('visible/city changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [jobId, ready, filters, cityCode, sortType]);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled || !ready}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError || !ready}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<AnchorCard data={item} jobId={jobId} key={item.id} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnchorList;
|
69
src/components/anchor-picker/index.less
Normal file
69
src/components/anchor-picker/index.less
Normal file
@ -0,0 +1,69 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.anchor-picker {
|
||||
width: 100%;
|
||||
background: #FFFFFF;
|
||||
padding: 24px;
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
margin-top: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
.flex-row();
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
min-width: 164px;
|
||||
height: 64px;
|
||||
font-size: 28px;
|
||||
line-height: 64px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
color: @blColor;
|
||||
background: #F6F6F6;
|
||||
margin-left: 16px;
|
||||
border-radius: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 344px;
|
||||
height: 72px;
|
||||
font-size: 28px;
|
||||
line-height: 72px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
background: #F6F6F6;
|
||||
padding: 0 24px;
|
||||
margin-top: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
186
src/components/anchor-picker/index.tsx
Normal file
186
src/components/anchor-picker/index.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { BaseEventOrig, Input, InputProps } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import PickerToolbar from '@/components/picker-toolbar';
|
||||
import {
|
||||
EmployType,
|
||||
ALL_EMPLOY_TYPES,
|
||||
FULL_PRICE_OPTIONS,
|
||||
PART_PRICE_OPTIONS,
|
||||
EMPLOY_TYPE_TITLE_MAP,
|
||||
} from '@/constants/job';
|
||||
import {
|
||||
ALL_ANCHOR_READ_TYPES,
|
||||
ALL_GENDER_TYPES,
|
||||
ANCHOR_READ_TITLE_MAP,
|
||||
AnchorReadType,
|
||||
GENDER_TYPE_TITLE_MAP,
|
||||
GenderType,
|
||||
} from '@/constants/material';
|
||||
import { IAnchorFilters } from '@/types/material';
|
||||
import { isUndefined } from '@/utils/common';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
value: IAnchorFilters;
|
||||
onConfirm: (newValue: IAnchorFilters) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'anchor-picker';
|
||||
const getDefaultGender = (value: IAnchorFilters) => value.gender;
|
||||
const getDefaultEmploy = (value: IAnchorFilters) => value.employType;
|
||||
const getDefaultReadType = (value: IAnchorFilters) => value.readType;
|
||||
const getDefaultCategory = (value: IAnchorFilters) => value.category || '';
|
||||
const getSalaryValue = (value: IAnchorFilters, full: boolean) => {
|
||||
const min = full ? value.lowPriceForFullTime : value.lowPriceForPartyTime;
|
||||
const max = full ? value.highPriceForFullTime : value.highPriceForPartyTime;
|
||||
if (!min || !max) {
|
||||
return null;
|
||||
}
|
||||
const options = full ? FULL_PRICE_OPTIONS : PART_PRICE_OPTIONS;
|
||||
return options.find(v => v.value && v.value.minSalary <= min && v.value.maxSalary >= max)?.value;
|
||||
};
|
||||
|
||||
function AnchorPicker(props: IProps) {
|
||||
const { value, onConfirm } = props;
|
||||
const [gender, setGender] = useState<GenderType | undefined>(getDefaultGender(value));
|
||||
const [readType, setReadType] = useState<AnchorReadType | undefined>(getDefaultReadType(value));
|
||||
const [employType, setEmployType] = useState<EmployType | undefined>(getDefaultEmploy(value));
|
||||
const [fullSalary, setFullSalary] = useState(getSalaryValue(value, true));
|
||||
const [partSalary, setPartSalary] = useState(getSalaryValue(value, false));
|
||||
const [category, setCategory] = useState(getDefaultCategory(value));
|
||||
|
||||
const handleInputCategory = useCallback((e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
setCategory(e.detail.value || '');
|
||||
}, []);
|
||||
|
||||
const handleClickReset = useCallback(() => {
|
||||
setGender(undefined);
|
||||
setReadType(undefined);
|
||||
setEmployType(undefined);
|
||||
setFullSalary(null);
|
||||
setPartSalary(null);
|
||||
setCategory('');
|
||||
}, []);
|
||||
|
||||
const handleSelectFull = useCallback(
|
||||
(newSalary?: { minSalary: number; maxSalary: number }) => {
|
||||
setFullSalary(newSalary === fullSalary ? null : newSalary);
|
||||
},
|
||||
[fullSalary]
|
||||
);
|
||||
|
||||
const handleSelectPart = useCallback(
|
||||
(newSalary?: { minSalary: number; maxSalary: number }) => {
|
||||
setPartSalary(newSalary === partSalary ? null : newSalary);
|
||||
},
|
||||
[partSalary]
|
||||
);
|
||||
|
||||
const handleClickConfirm = useCallback(() => {
|
||||
const filters: IAnchorFilters = {};
|
||||
if (!isUndefined(gender)) {
|
||||
filters.gender = gender === GenderType.All ? undefined : gender;
|
||||
}
|
||||
employType && (filters.employType = employType);
|
||||
readType && (filters.readType = readType);
|
||||
category && (filters.category = category);
|
||||
if (fullSalary) {
|
||||
filters.lowPriceForFullTime = fullSalary.minSalary;
|
||||
filters.highPriceForFullTime = fullSalary.maxSalary;
|
||||
}
|
||||
if (partSalary) {
|
||||
filters.lowPriceForPartyTime = partSalary.minSalary;
|
||||
filters.highPriceForPartyTime = partSalary.maxSalary;
|
||||
}
|
||||
onConfirm(filters);
|
||||
}, [gender, employType, readType, category, fullSalary, partSalary, onConfirm]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__title`}>性别</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{ALL_GENDER_TYPES.map((type: GenderType) => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => setGender(type)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: type === gender })}
|
||||
>
|
||||
{GENDER_TYPE_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>全职/兼职</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{ALL_EMPLOY_TYPES.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => setEmployType(type)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: type === employType })}
|
||||
>
|
||||
{EMPLOY_TYPE_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>已读/未读</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{ALL_ANCHOR_READ_TYPES.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => setReadType(type)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: type === readType })}
|
||||
>
|
||||
{ANCHOR_READ_TITLE_MAP[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>全职薪资范围(每月)</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{FULL_PRICE_OPTIONS.map(option => (
|
||||
<div
|
||||
key={option.label}
|
||||
onClick={() => handleSelectFull(option.value)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: isEqual(option.value, fullSalary) })}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>兼职薪资范围(每小时)</div>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
{PART_PRICE_OPTIONS.map(option => (
|
||||
<div
|
||||
key={option.label}
|
||||
onClick={() => handleSelectPart(option.value)}
|
||||
className={classNames(`${PREFIX}__item`, { selected: isEqual(option.value, partSalary) })}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__title`}>播过的品类(模糊匹配)</div>
|
||||
<Input
|
||||
maxlength={20}
|
||||
value={category}
|
||||
confirmType="done"
|
||||
placeholder="如 服装"
|
||||
onInput={handleInputCategory}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<PickerToolbar
|
||||
cancelText="重置"
|
||||
confirmText="确定"
|
||||
className={`${PREFIX}__toolbar`}
|
||||
onClickCancel={handleClickReset}
|
||||
onClickConfirm={handleClickConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnchorPicker;
|
13
src/components/badge/index.less
Normal file
13
src/components/badge/index.less
Normal file
@ -0,0 +1,13 @@
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #FFFFFF;
|
||||
background: #FF5051;
|
||||
font-size: 24px;
|
||||
line-height: 34px;
|
||||
padding: 0 8px;
|
||||
border-radius: 10px;
|
||||
border-bottom-left-radius: 0;
|
||||
transform: translate3d(30%, -50%, 0);
|
||||
}
|
16
src/components/badge/index.tsx
Normal file
16
src/components/badge/index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import classNames from 'classnames';
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'badge';
|
||||
|
||||
function Badge(props: IProps) {
|
||||
const { className, text } = props;
|
||||
return <div className={classNames(PREFIX, className)}>{text}</div>;
|
||||
}
|
||||
|
||||
export default Badge;
|
21
src/components/bl-checkbox/index.less
Normal file
21
src/components/bl-checkbox/index.less
Normal file
@ -0,0 +1,21 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.profile-checkbox {
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
43
src/components/bl-checkbox/index.tsx
Normal file
43
src/components/bl-checkbox/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Checkbox } from '@taroify/core';
|
||||
import { CheckboxProps, CheckboxGroupProps } from '@taroify/core/checkbox';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends CheckboxProps {
|
||||
text: string;
|
||||
value: BL.Anything[];
|
||||
}
|
||||
|
||||
interface IGroupProps extends CheckboxGroupProps {}
|
||||
|
||||
const PREFIX = 'profile-checkbox';
|
||||
|
||||
export function BlCheckboxGroup(props: IGroupProps) {
|
||||
return <Checkbox.Group className={`${PREFIX}__group`} direction="horizontal" {...props} />;
|
||||
}
|
||||
|
||||
export function BlCheckbox(props: IProps) {
|
||||
const { name, text, value } = props;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
className={`${PREFIX}__item`}
|
||||
name={name}
|
||||
icon={
|
||||
<Image
|
||||
className={`${PREFIX}__icon`}
|
||||
mode="aspectFit"
|
||||
src={
|
||||
value.includes(name)
|
||||
? require('@/statics/svg/radio-checked.svg')
|
||||
: require('@/statics/svg/radio-uncheck.svg')
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
28
src/components/bl-form-cell/index.less
Normal file
28
src/components/bl-form-cell/index.less
Normal file
@ -0,0 +1,28 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-cell {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: #CCCCCC;
|
||||
.noWrap();
|
||||
|
||||
&.hasText {
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
||||
|
||||
&__right-icon {
|
||||
.flex-row();
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
25
src/components/bl-form-cell/index.tsx
Normal file
25
src/components/bl-form-cell/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { ArrowRight } from '@taroify/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
text: string;
|
||||
placeholder?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-form-cell';
|
||||
|
||||
function BlFormCell(props: IProps) {
|
||||
const { text, placeholder, onClick } = props;
|
||||
|
||||
return (
|
||||
<div className={PREFIX} onClick={onClick}>
|
||||
<div className={classNames(`${PREFIX}__text`, { hasText: !!text })}>{text || placeholder}</div>
|
||||
<ArrowRight className={`${PREFIX}__right-icon`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormCell;
|
36
src/components/bl-form-input/index.less
Normal file
36
src/components/bl-form-input/index.less
Normal file
@ -0,0 +1,36 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-input {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
|
||||
&__input {
|
||||
height: 60px;
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 60px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__right-text {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: @blColor;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__max-length-tips {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blColorG1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
}
|
35
src/components/bl-form-input/index.tsx
Normal file
35
src/components/bl-form-input/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Input, InputProps } from '@tarojs/components';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends InputProps {
|
||||
rightText?: string;
|
||||
maxLengthTips?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-form-input';
|
||||
|
||||
function BlFormInput(props: IProps) {
|
||||
const { value, maxlength = 140, maxLengthTips, rightText, onInput, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<Input
|
||||
value={value}
|
||||
maxlength={maxlength}
|
||||
confirmType="done"
|
||||
placeholder="请输入"
|
||||
onInput={onInput}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
{...otherProps}
|
||||
/>
|
||||
{rightText && <div className={`${PREFIX}__right-text`}>{rightText}</div>}
|
||||
{maxLengthTips && maxlength && (
|
||||
<div className={`${PREFIX}__max-length-tips`}>{`${(value || '').length}/${maxlength}`}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormInput;
|
45
src/components/bl-form-item/index.less
Normal file
45
src/components/bl-form-item/index.less
Normal file
@ -0,0 +1,45 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-item {
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
.flex-row();
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__type {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
.flex-row();
|
||||
margin-top: 24px;
|
||||
padding: 0 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.dynamicHeight {
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
42
src/components/bl-form-item/index.tsx
Normal file
42
src/components/bl-form-item/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {
|
||||
title: string;
|
||||
subTitle?: string | boolean;
|
||||
optional?: boolean;
|
||||
dynamicHeight?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-form-item';
|
||||
|
||||
function BlFormItem(props: IProps) {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
title,
|
||||
subTitle = true,
|
||||
optional = false,
|
||||
dynamicHeight = false,
|
||||
} = props;
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header__title`}>{title}</div>
|
||||
{subTitle !== false && (
|
||||
<div
|
||||
className={`${PREFIX}__header__type`}
|
||||
>{`(${typeof subTitle === 'string' ? subTitle : optional ? '建议填写' : '必填'})`}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames(`${PREFIX}__content`, contentClassName, { dynamicHeight })}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormItem;
|
19
src/components/bl-form-radio/index.less
Normal file
19
src/components/bl-form-radio/index.less
Normal file
@ -0,0 +1,19 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-radio {
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
39
src/components/bl-form-radio/index.tsx
Normal file
39
src/components/bl-form-radio/index.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Radio } from '@taroify/core';
|
||||
import { RadioGroupProps, RadioProps } from '@taroify/core/radio';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends RadioProps {
|
||||
text: string;
|
||||
value: BL.Anything;
|
||||
}
|
||||
|
||||
interface IGroupProps extends RadioGroupProps {}
|
||||
|
||||
const PREFIX = 'bl-form-radio';
|
||||
|
||||
export function BlFormRadioGroup(props: IGroupProps) {
|
||||
return <Radio.Group className={`${PREFIX}__group`} {...props} />;
|
||||
}
|
||||
|
||||
export function BlFormRadio(props: IProps) {
|
||||
const { name, text, value } = props;
|
||||
|
||||
return (
|
||||
<Radio
|
||||
className={`${PREFIX}__item`}
|
||||
name={name}
|
||||
icon={
|
||||
<Image
|
||||
className={`${PREFIX}__icon`}
|
||||
mode="aspectFit"
|
||||
src={value === name ? require('@/statics/svg/radio-checked.svg') : require('@/statics/svg/radio-uncheck.svg')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Radio>
|
||||
);
|
||||
}
|
30
src/components/bl-form-select/index.less
Normal file
30
src/components/bl-form-select/index.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-form-select {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
|
||||
&__input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__right-icon {
|
||||
.flex-row();
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
50
src/components/bl-form-select/index.tsx
Normal file
50
src/components/bl-form-select/index.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Input } from '@tarojs/components';
|
||||
|
||||
import { ArrowRight } from '@taroify/icons';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ISelectProps, PopupSelect } from '@/components/select';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends ISelectProps {}
|
||||
|
||||
const PREFIX = 'bl-form-select';
|
||||
|
||||
function BlFormSelect(props: IProps) {
|
||||
const { value, options, onSelect, ...otherProps } = props;
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
newValue => {
|
||||
setShowSelect(false);
|
||||
onSelect(newValue);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={PREFIX} onClick={() => setShowSelect(true)}>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="请选择"
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
value={options.find(i => i.value === value)?.label || ''}
|
||||
/>
|
||||
<ArrowRight className={`${PREFIX}__right-icon`} />
|
||||
</div>
|
||||
<PopupSelect
|
||||
value={value}
|
||||
options={options}
|
||||
open={showSelect}
|
||||
onSelect={handleSelect}
|
||||
onClose={() => setShowSelect(false)}
|
||||
{...otherProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlFormSelect;
|
71
src/components/bl-salary-input/index.less
Normal file
71
src/components/bl-salary-input/index.less
Normal file
@ -0,0 +1,71 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.bl-salary-input {
|
||||
|
||||
&__item {
|
||||
height: 100px;
|
||||
.flex-row();
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
background: #00000026;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: -32px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text(@fontSize: 28px) {
|
||||
font-size: @fontSize;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
&__title {
|
||||
.text(@fontSize: 32px);
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
&__input-container {
|
||||
height: 72px;
|
||||
flex: 1;
|
||||
.flex-row();
|
||||
border-radius: 16px;
|
||||
background: #F6F6F6;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
.text();
|
||||
flex: 1;
|
||||
height: 72px;
|
||||
line-height: 72px;
|
||||
}
|
||||
|
||||
&__input-placeholder {
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
|
||||
&__unit {
|
||||
.text();
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&__center-divider {
|
||||
.text();
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
155
src/components/bl-salary-input/index.tsx
Normal file
155
src/components/bl-salary-input/index.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { BaseEventOrig, Input, InputProps } from '@tarojs/components';
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { logWithPrefix, string2Number } from '@/utils/common';
|
||||
import { EmployType } from '@/constants/job';
|
||||
|
||||
import './index.less';
|
||||
import { isFullTimePriceRequired, isPartTimePriceRequired } from '@/utils/job';
|
||||
|
||||
export type BlSalaryValue = [number, number, number, number];
|
||||
|
||||
interface IProps {
|
||||
value: BlSalaryValue;
|
||||
employType?: EmployType;
|
||||
onChange: (result: BlSalaryValue) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-salary-input';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const MAX_FULL_PRICE = 1000;
|
||||
const MAX_PART_PRICE = 2000;
|
||||
|
||||
function BlSalaryInput(props: IProps) {
|
||||
const { value: initValue = [], onChange, employType } = props;
|
||||
const [minFull = '', maxFull = '', minPart = '', maxPart = ''] = initValue;
|
||||
|
||||
const onInput = useCallback(
|
||||
(value: number, index: number) => {
|
||||
const newValue = [...initValue] as BlSalaryValue;
|
||||
newValue.splice(index, 1, value);
|
||||
log('onInput', newValue);
|
||||
onChange(newValue);
|
||||
},
|
||||
[initValue, onChange]
|
||||
);
|
||||
|
||||
const handleInputMinFull = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(string2Number(value), 0);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
const handleInputMaxFull = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(Math.min(string2Number(value), MAX_FULL_PRICE), 1);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
const handleInputMinPart = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(string2Number(value), 2);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
const handleInputMaxPart = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputEventDetail>) => {
|
||||
const value = e.detail.value || '';
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
onInput(Math.min(string2Number(value), MAX_PART_PRICE), 3);
|
||||
},
|
||||
[onInput]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange([minFull, maxFull, minPart, maxPart] as BlSalaryValue);
|
||||
}, [minFull, maxFull, minPart, maxPart, onChange]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
{isFullTimePriceRequired(employType) && (
|
||||
<div className={`${PREFIX}__item`}>
|
||||
<div className={`${PREFIX}__title`}>全职</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最低值"
|
||||
value={String(minFull)}
|
||||
onInput={handleInputMinFull}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>K/月</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__center-divider`}>-</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最高值"
|
||||
value={String(maxFull)}
|
||||
onInput={handleInputMaxFull}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>K/月</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isPartTimePriceRequired(employType) && (
|
||||
<div className={`${PREFIX}__item`}>
|
||||
<div className={`${PREFIX}__title`}>兼职</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最低值"
|
||||
value={String(minPart)}
|
||||
onInput={handleInputMinPart}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>元/小时</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__center-divider`}>-</div>
|
||||
<div className={`${PREFIX}__input-container`}>
|
||||
<Input
|
||||
type="number"
|
||||
maxlength={5}
|
||||
confirmType="done"
|
||||
placeholder="输入最高值"
|
||||
value={String(maxPart)}
|
||||
onInput={handleInputMaxPart}
|
||||
className={`${PREFIX}__input`}
|
||||
placeholderClass={`${PREFIX}__input-placeholder`}
|
||||
/>
|
||||
<div className={`${PREFIX}__unit`}>元/小时</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlSalaryInput;
|
12
src/components/cell/index.less
Normal file
12
src/components/cell/index.less
Normal file
@ -0,0 +1,12 @@
|
||||
.bl-cell {
|
||||
height: 112px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-radius: 16px;
|
||||
|
||||
&__right-text {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
22
src/components/cell/index.tsx
Normal file
22
src/components/cell/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Cell } from '@taroify/core';
|
||||
import { CellProps } from '@taroify/core/cell';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends CellProps {
|
||||
rightText?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-cell';
|
||||
|
||||
function BlCell(props: IProps) {
|
||||
const { className, rightText, ...otherProps } = props;
|
||||
return (
|
||||
<Cell className={classNames(PREFIX, className)} {...otherProps}>
|
||||
{rightText && <div className={`${PREFIX}__right-text`}>{rightText}</div>}
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlCell;
|
74
src/components/certification-status/index.less
Normal file
74
src/components/certification-status/index.less
Normal file
@ -0,0 +1,74 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.certification-status {
|
||||
|
||||
.taroify-cell__title {
|
||||
flex: unset;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__right-text {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
}
|
||||
|
||||
.certification-status-icon {
|
||||
height: 38px;
|
||||
.flex-row();
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.none {
|
||||
color: @blColorG1;
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #117264;
|
||||
background: #DCF7F0;
|
||||
}
|
||||
|
||||
&.fail {
|
||||
color: #FF5051;
|
||||
background: #FF50511F;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&__describe {
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: 30px;
|
||||
padding: 0 6px;
|
||||
|
||||
&__icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
&__describe {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
109
src/components/certification-status/index.tsx
Normal file
109
src/components/certification-status/index.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Cell } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { CertificationStatusType } from '@/constants/company';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IIconProps {
|
||||
status: CertificationStatusType;
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'certification-status';
|
||||
const ICON_PREFIX = 'certification-status-icon';
|
||||
const getStatusClassName = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return 'none';
|
||||
case CertificationStatusType.Success:
|
||||
return 'success';
|
||||
case CertificationStatusType.Fail:
|
||||
return 'fail';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
const getStatusIconUrl = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return require('@/statics/svg/certification-status-none.svg');
|
||||
case CertificationStatusType.Success:
|
||||
return require('@/statics/svg/certification-status-success.svg');
|
||||
case CertificationStatusType.Fail:
|
||||
return require('@/statics/svg/certification-status-fail.svg');
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
const getStatusDescribe = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return '未认证';
|
||||
case CertificationStatusType.Success:
|
||||
return '实人认证';
|
||||
case CertificationStatusType.Fail:
|
||||
return '认证失败';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
const getRightText = (status: CertificationStatusType) => {
|
||||
switch (status) {
|
||||
case CertificationStatusType.None:
|
||||
return '去认证';
|
||||
case CertificationStatusType.Fail:
|
||||
return '重新认证';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export function CertificationStatusIcon(props: IIconProps) {
|
||||
const { status, className, small = false } = props;
|
||||
return (
|
||||
<div className={classNames(ICON_PREFIX, className, { [getStatusClassName(status)]: true, small })}>
|
||||
<Image mode="aspectFit" className={`${ICON_PREFIX}__icon`} src={getStatusIconUrl(status)} />
|
||||
<div className={`${ICON_PREFIX}__describe`}>{getStatusDescribe(status)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CertificationStatus(props: IProps) {
|
||||
const { className } = props;
|
||||
const { bossAuthStatus: status = CertificationStatusType.None } = useUserInfo();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (status !== CertificationStatusType.Success) {
|
||||
navigateTo(PageUrl.CertificationStart);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Cell
|
||||
align="center"
|
||||
title="认证状态"
|
||||
className={classNames(PREFIX, className)}
|
||||
isLink={status !== CertificationStatusType.Success}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={`${PREFIX}__content`}>
|
||||
<CertificationStatusIcon status={status} />
|
||||
<div className={`${PREFIX}__right-text`}>{getRightText(status)}</div>
|
||||
</div>
|
||||
</Cell>
|
||||
);
|
||||
}
|
||||
|
||||
export default CertificationStatus;
|
4
src/components/city-picker/index.less
Normal file
4
src/components/city-picker/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.city-picker {
|
||||
}
|
70
src/components/city-picker/index.tsx
Normal file
70
src/components/city-picker/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { AreaPicker, Popup } from '@taroify/core';
|
||||
import { AreaData } from '@taroify/core/area-picker/area-picker.shared';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import { CITY_LIST, COUNTY_LIST, PROVINCE_LIST } from '@/constants/city';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
depth?: 1 | 2 | 3;
|
||||
areaValues?: string[];
|
||||
onCancel: () => void;
|
||||
onConfirm: (areaValues: string[]) => void;
|
||||
}
|
||||
|
||||
interface IPopupProps extends IProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'city-picker';
|
||||
const areaList: AreaData = {
|
||||
province_list: PROVINCE_LIST,
|
||||
city_list: CITY_LIST,
|
||||
county_list: COUNTY_LIST,
|
||||
};
|
||||
const DEFAULT_AREA = ['110000', '110100', '110101'];
|
||||
|
||||
function CityPicker(props: IProps) {
|
||||
const { areaValues = DEFAULT_AREA, depth = 3, onCancel, onConfirm } = props;
|
||||
const provinceCodeRef = useRef(areaValues[0]);
|
||||
const cityCodeRef = useRef(areaValues[1]);
|
||||
const countyCodeRef = useRef(areaValues[2]);
|
||||
|
||||
const handleClickConfirm = useCallback(() => {
|
||||
onConfirm([provinceCodeRef.current, cityCodeRef.current, countyCodeRef.current]);
|
||||
}, [onConfirm]);
|
||||
|
||||
const handleChange = useCallback((values: string[]) => {
|
||||
provinceCodeRef.current = values[0];
|
||||
cityCodeRef.current = values[1];
|
||||
countyCodeRef.current = values[2];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<AreaPicker
|
||||
depth={depth}
|
||||
className={`${PREFIX}__area-picker`}
|
||||
areaList={areaList}
|
||||
defaultValue={areaValues}
|
||||
onCancel={onCancel}
|
||||
onChange={handleChange}
|
||||
onConfirm={handleClickConfirm}
|
||||
></AreaPicker>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CityPickerPopup(props: IPopupProps) {
|
||||
const { open, onCancel } = props;
|
||||
return (
|
||||
<Popup className={PREFIX} placement="bottom" open={open} onClose={onCancel}>
|
||||
<CityPicker {...props} />
|
||||
<SafeBottomPadding />
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default CityPicker;
|
32
src/components/common-dialog/index.less
Normal file
32
src/components/common-dialog/index.less
Normal file
@ -0,0 +1,32 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.common-dialog {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
line-height: 58px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button,
|
||||
&__cancel-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
&__cancel-button {
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
47
src/components/common-dialog/index.tsx
Normal file
47
src/components/common-dialog/index.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends PropsWithChildren {
|
||||
visible: boolean;
|
||||
content: string;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
confirm?: string;
|
||||
cancel?: string;
|
||||
onClick?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'common-dialog';
|
||||
|
||||
function CommonDialog(props: IProps) {
|
||||
const { visible, content, confirm, cancel, className, children, onClose, onClick, onCancel } = props;
|
||||
|
||||
return (
|
||||
<Dialog className={classNames(PREFIX, className)} open={visible} onClose={onClose}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>{content}</div>
|
||||
{children}
|
||||
{confirm && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={onClick}>
|
||||
{confirm}
|
||||
</Button>
|
||||
)}
|
||||
{cancel && (
|
||||
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
|
||||
{cancel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommonDialog;
|
8
src/components/custom-navigation-bar/index.less
Normal file
8
src/components/custom-navigation-bar/index.less
Normal file
@ -0,0 +1,8 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
|
||||
.bl-navigation-bar {
|
||||
.flex-row();
|
||||
padding: 0 24px;
|
||||
}
|
30
src/components/custom-navigation-bar/index.tsx
Normal file
30
src/components/custom-navigation-bar/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import useNavigation from '@/hooks/use-navigation';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends PropsWithChildren {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'bl-navigation-bar';
|
||||
|
||||
function CustomNavigationBar(props: IProps) {
|
||||
const { className, children } = props;
|
||||
const { barHeight, statusBarHeight } = useNavigation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(PREFIX, className)}
|
||||
style={{ height: Taro.pxTransform(barHeight.current), paddingTop: statusBarHeight.current }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomNavigationBar;
|
43
src/components/dev-div/index.tsx
Normal file
43
src/components/dev-div/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
OnDev?: () => void;
|
||||
}
|
||||
|
||||
const CLICK_COUNT = 5;
|
||||
|
||||
function DevDiv(props: IProps) {
|
||||
const { OnDev, onClick, ...otherProps } = props;
|
||||
const lastClickTime = useRef(0);
|
||||
const clickCount = useRef(0);
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
onClick?.(e);
|
||||
if (!OnDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - lastClickTime.current;
|
||||
|
||||
if (timeDiff < 300) {
|
||||
clickCount.current = clickCount.current + 1;
|
||||
|
||||
if (clickCount.current >= CLICK_COUNT) {
|
||||
OnDev?.();
|
||||
clickCount.current = 0;
|
||||
}
|
||||
} else {
|
||||
clickCount.current = 1;
|
||||
}
|
||||
|
||||
lastClickTime.current = currentTime;
|
||||
},
|
||||
[OnDev, onClick]
|
||||
);
|
||||
|
||||
return <div onClick={handleClick} {...otherProps}></div>;
|
||||
}
|
||||
|
||||
export default DevDiv;
|
92
src/components/group-card/index.less
Normal file
92
src/components/group-card/index.less
Normal file
@ -0,0 +1,92 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.group-card {
|
||||
width: 100%;
|
||||
padding: 32px;
|
||||
background: #FFF;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__group-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__group-avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
&__group-info {
|
||||
width: 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-left: 32px;
|
||||
|
||||
&.full {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__group-title-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
flex: 1;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__group-full-icon {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
border: 2px solid @blHighlightColor;
|
||||
border-radius: 4px;
|
||||
padding: 0 6px;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__group-desc {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__group-job-count {
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__group-view {
|
||||
color: @blHighlightColor;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__group-button {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @fontWeight: 500; @borderRadius: 48px;);
|
||||
}
|
||||
}
|
61
src/components/group-card/index.tsx
Normal file
61
src/components/group-card/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import LoginButton from '@/components/login-button';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { GroupType } from '@/constants/group';
|
||||
import { GroupInfo } from '@/types/group';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
type: GroupType;
|
||||
data: GroupInfo;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'group-card';
|
||||
|
||||
function GroupCard(props: IProps) {
|
||||
const { type, data, onClick } = props;
|
||||
const showButton = type === GroupType.All;
|
||||
const isFull = (data.groupMemberCount || 0) >= 500;
|
||||
|
||||
const handleClickView = useCallback(() => {
|
||||
navigateTo(PageUrl.GroupJob, { groupId: data.blGroupId, title: data.imGroupNick });
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__group-container`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__group-avatar`}
|
||||
src={data.groupAvatar || require('@/statics/svg/wechat.svg')}
|
||||
/>
|
||||
<div className={classNames(`${PREFIX}__group-info`, { full: !showButton })}>
|
||||
<div className={`${PREFIX}__group-title-container`}>
|
||||
<div className={`${PREFIX}__group-title`}>{data.imGroupNick}</div>
|
||||
{isFull && <div className={`${PREFIX}__group-full-icon`}>已满</div>}
|
||||
</div>
|
||||
<div className={`${PREFIX}__group-desc`}>
|
||||
<div className={`${PREFIX}__group-job-count`}>{`${data.allJobs}条通告`}</div>
|
||||
<div className={`${PREFIX}__group-view`} onClick={handleClickView}>
|
||||
查看
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showButton && (
|
||||
<LoginButton className={`${PREFIX}__group-button`} onClick={onClick}>
|
||||
我要进群
|
||||
</LoginButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupCard;
|
3
src/components/group-list/index.less
Normal file
3
src/components/group-list/index.less
Normal file
@ -0,0 +1,3 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.group-list {}
|
149
src/components/group-list/index.tsx
Normal file
149
src/components/group-list/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import GroupCard from '@/components/group-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { GroupInfo, GetGroupsRequest } from '@/types/group';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestGroupList } from '@/utils/group';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends GetGroupsRequest {}
|
||||
|
||||
export interface IGroupListProps extends IRequestProps {
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
onClickInvite: (data: GroupInfo) => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 40;
|
||||
const PREFIX = 'group-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobList(props: IGroupListProps) {
|
||||
const { className, listHeight, refreshDisabled, type, imGroupNick, status, onListEmpty, onClickInvite } = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<GroupInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({ type });
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, groupResults } = await requestGroupList({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(groupResults);
|
||||
currentPage.current = page;
|
||||
!groupResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
groupResults,
|
||||
} = await requestGroupList({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...groupResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, currentPage, hasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = {
|
||||
type,
|
||||
status,
|
||||
imGroupNick: imGroupNick ? imGroupNick.trim() : undefined,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
}, [type, status, imGroupNick]);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = async () => {
|
||||
log('props changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, groupResults } = await requestGroupList({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(groupResults);
|
||||
currentPage.current = page;
|
||||
!groupResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('props changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [type, status, imGroupNick]);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<GroupCard type={type} data={item} key={item.blGroupId} onClick={() => onClickInvite(item)} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobList;
|
0
src/components/home-page/index.less
Normal file
0
src/components/home-page/index.less
Normal file
18
src/components/home-page/index.tsx
Normal file
18
src/components/home-page/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import BaseTabBar from '@/components/tab-bar';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {}
|
||||
|
||||
export default function HomePage(props: IProps) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<BaseTabBar />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
163
src/components/job-card/index.less
Normal file
163
src/components/job-card/index.less
Normal file
@ -0,0 +1,163 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-card {
|
||||
&__container {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__employment-type {
|
||||
width: fit-content;
|
||||
padding: 3px 6px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background: @blHighlightBg;
|
||||
color: @blHighlightColor;
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
|
||||
&__wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__certification-type {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
overflow: hidden;
|
||||
margin-top: 32px;
|
||||
// 抵消最后一行的 margin-bottom
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
padding: 3px 6px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
background: #F2F2F2;
|
||||
color: @blColorG2;
|
||||
white-space: nowrap;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__salary {
|
||||
font-size: 30px;
|
||||
line-height: 32px;
|
||||
font: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__summary {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__distance-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__detailed-address {
|
||||
flex: 1 1;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__distance-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-left: 15px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&__distance {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #E0E0E0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__publisher {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__publisher-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
&__publisher-name,
|
||||
&__city {
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG2;
|
||||
}
|
||||
|
||||
}
|
111
src/components/job-card/index.tsx
Normal file
111
src/components/job-card/index.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { CertificationStatusIcon } from '@/components/certification-status';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { CITY_CODE_TO_NAME_MAP, COUNTY_CODE_TO_NAME_MAP } from '@/constants/city';
|
||||
import { CertificationStatusType } from '@/constants/company';
|
||||
import { EMPLOY_TYPE_TITLE_MAP, EmployType } from '@/constants/job';
|
||||
import { JobInfo } from '@/types/job';
|
||||
import { LocationInfo } from '@/types/location';
|
||||
import { getJobSalary, getJobTitle } from '@/utils/job';
|
||||
import { calcDistance } from '@/utils/location';
|
||||
import { navigateTo, redirectTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: JobInfo;
|
||||
redirectOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'job-card';
|
||||
const getCityDes = (location: LocationInfo) => {
|
||||
if (!location) {
|
||||
return '';
|
||||
}
|
||||
let des = CITY_CODE_TO_NAME_MAP.get(location.cityCode);
|
||||
if (location.countyCode) {
|
||||
des += `-${COUNTY_CODE_TO_NAME_MAP.get(location.countyCode)}`;
|
||||
}
|
||||
return des;
|
||||
};
|
||||
|
||||
function JobCard(props: IProps) {
|
||||
const { className, data, redirectOpen } = props;
|
||||
const {
|
||||
id,
|
||||
tags = [],
|
||||
employType = EmployType.All,
|
||||
jobDescription,
|
||||
sourceText,
|
||||
publisher,
|
||||
publisherAvatar,
|
||||
jobLocation,
|
||||
distance,
|
||||
isAuthed = false,
|
||||
} = data;
|
||||
|
||||
const handleClickCard = useCallback(() => {
|
||||
if (redirectOpen) {
|
||||
redirectTo(PageUrl.JobDetail, { id });
|
||||
} else {
|
||||
navigateTo(PageUrl.JobDetail, { id });
|
||||
}
|
||||
}, [id, redirectOpen]);
|
||||
|
||||
return (
|
||||
<div className={classNames(`${PREFIX}__container`, className)} onClick={handleClickCard}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__title`}>{getJobTitle(data)}</div>
|
||||
<div className={`${PREFIX}__employment-type__wrapper`}>
|
||||
<div className={`${PREFIX}__employment-type`}>{EMPLOY_TYPE_TITLE_MAP[employType]}</div>
|
||||
</div>
|
||||
{isAuthed && (
|
||||
<CertificationStatusIcon
|
||||
className={`${PREFIX}__certification-type`}
|
||||
status={CertificationStatusType.Success}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__tags`}>
|
||||
{tags.map((keyword: string, index) => (
|
||||
<div className={`${PREFIX}__tag`} key={index}>
|
||||
{keyword}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${PREFIX}__salary`}>{getJobSalary(data) || '见描述'}</div>
|
||||
<div className={`${PREFIX}__content`}>
|
||||
<div className={`${PREFIX}__summary`}>{jobDescription || sourceText}</div>
|
||||
<div className={`${PREFIX}__distance-wrapper`}>
|
||||
<div className={`${PREFIX}__detailed-address`}>{jobLocation?.address}</div>
|
||||
{distance && (
|
||||
<>
|
||||
<Image className={`${PREFIX}__distance-icon`} src={require('@/statics/svg/location.svg')} />
|
||||
<div className={`${PREFIX}__distance`}>{calcDistance(distance)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__divider`} />
|
||||
<div className={`${PREFIX}__footer`}>
|
||||
<div className={`${PREFIX}__publisher`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__publisher-avatar`}
|
||||
src={publisherAvatar || require('@/statics/svg/wechat.svg')}
|
||||
/>
|
||||
<div className={`${PREFIX}__publisher-name`}>{publisher}</div>
|
||||
</div>
|
||||
<div className={`${PREFIX}__city`}>{getCityDes(jobLocation)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(JobCard);
|
0
src/components/job-list/index.less
Normal file
0
src/components/job-list/index.less
Normal file
207
src/components/job-list/index.tsx
Normal file
207
src/components/job-list/index.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobCard from '@/components/job-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { JobType, EmployType, SortType } from '@/constants/job';
|
||||
import { JobInfo, GetJobsRequest } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestJobList as requestData } from '@/utils/job';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetJobsRequest> {}
|
||||
|
||||
export interface IJobListProps extends IRequestProps {
|
||||
visible?: boolean;
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 40;
|
||||
const PREFIX = 'job-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobList(props: IJobListProps) {
|
||||
const {
|
||||
className,
|
||||
listHeight,
|
||||
refreshDisabled,
|
||||
visible = true,
|
||||
cityCode = 'ALL',
|
||||
category = JobType.All,
|
||||
employType = EmployType.All,
|
||||
sortType = SortType.RECOMMEND,
|
||||
isFollow = false,
|
||||
isOwner = false,
|
||||
keyWord,
|
||||
latitude,
|
||||
longitude,
|
||||
minSalary,
|
||||
maxSalary,
|
||||
blGroupId,
|
||||
onListEmpty,
|
||||
} = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<JobInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
const prevRequestProps = useRef<IRequestProps>({});
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
jobResults,
|
||||
} = await requestData({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...jobResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, currentPage, hasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = {
|
||||
category,
|
||||
cityCode,
|
||||
employType,
|
||||
sortType,
|
||||
isFollow,
|
||||
isOwner,
|
||||
keyWord,
|
||||
latitude,
|
||||
longitude,
|
||||
minSalary,
|
||||
maxSalary,
|
||||
blGroupId,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
}, [
|
||||
category,
|
||||
cityCode,
|
||||
employType,
|
||||
sortType,
|
||||
isFollow,
|
||||
isOwner,
|
||||
keyWord,
|
||||
latitude,
|
||||
longitude,
|
||||
minSalary,
|
||||
maxSalary,
|
||||
blGroupId,
|
||||
]);
|
||||
|
||||
// 初始化数据&配置变更后刷新数据
|
||||
useEffect(() => {
|
||||
// 相比前一次可见时没有数据变更时,不再重新请求
|
||||
if (isEqual(prevRequestProps.current, requestProps.current)) {
|
||||
log('visible/city changed, but request params not change, ignore');
|
||||
return;
|
||||
}
|
||||
// 列表不可见时,先不做处理
|
||||
if (!visible) {
|
||||
log('visible/city changed, but is not visible, only clear list');
|
||||
return;
|
||||
}
|
||||
|
||||
prevRequestProps.current = requestProps.current;
|
||||
const refresh = async () => {
|
||||
log('visible/city changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('visible/city changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [visible, cityCode, employType, sortType, keyWord, minSalary, maxSalary, blGroupId]);
|
||||
|
||||
// log('render', `hasMore: ${hasMore}, loadingMore: ${loadingMore}, refreshing: ${refreshing}`);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<JobCard data={item} key={item.id} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobList;
|
69
src/components/job-manage-card/index.less
Normal file
69
src/components/job-manage-card/index.less
Normal file
@ -0,0 +1,69 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-manage-card {
|
||||
width: 100%;
|
||||
height: 152px;
|
||||
.flex-row();
|
||||
padding: 24px 32px;
|
||||
background: #FFF;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
background: #00000026;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 32px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
&__title {
|
||||
max-width: 75vw;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__location {
|
||||
max-width: 75vw;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-top: 16px;
|
||||
.noWrap();
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
|
||||
&.open {
|
||||
color: @blHighlightColor;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #FF5051;
|
||||
}
|
||||
}
|
||||
}
|
49
src/components/job-manage-card/index.tsx
Normal file
49
src/components/job-manage-card/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { JOB_MANAGE_STATUS_TITLE_MAP, JobManageStatus } from '@/constants/job';
|
||||
import { JobManageInfo } from '@/types/job';
|
||||
import { getJobLocation } from '@/utils/job';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: JobManageInfo;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'job-manage-card';
|
||||
|
||||
const STATUS_CLASS_MAP = {
|
||||
[JobManageStatus.WaitVerify]: 'pending',
|
||||
[JobManageStatus.Open]: 'open',
|
||||
[JobManageStatus.Pending]: 'pending',
|
||||
[JobManageStatus.Error]: 'error',
|
||||
[JobManageStatus.Close]: 'close',
|
||||
[JobManageStatus.Expire]: 'close',
|
||||
};
|
||||
|
||||
function JobManageCard(props: IProps) {
|
||||
const { data = {} } = props;
|
||||
const { id, title, status } = data as JobManageInfo;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
navigateTo(PageUrl.JobDetail, { id });
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} onClick={handleClick}>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
<div className={`${PREFIX}__info__title`}>{title}</div>
|
||||
<div className={`${PREFIX}__info__location`}>{getJobLocation(data as JobManageInfo)}</div>
|
||||
</div>
|
||||
<div className={classNames(`${PREFIX}__status`, { [STATUS_CLASS_MAP[status]]: true })}>
|
||||
<div>{JOB_MANAGE_STATUS_TITLE_MAP[status]}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(JobManageCard);
|
2
src/components/job-manage-list/index.less
Normal file
2
src/components/job-manage-list/index.less
Normal file
@ -0,0 +1,2 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
171
src/components/job-manage-list/index.tsx
Normal file
171
src/components/job-manage-list/index.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { List, PullRefresh } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobManageCard from '@/components/job-manage-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { EventName } from '@/constants/app';
|
||||
import { GetJobManagesRequest, JobManageInfo } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestJobManageList as requestData } from '@/utils/job';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetJobManagesRequest> {}
|
||||
|
||||
export interface IJobManageListProps extends IRequestProps {
|
||||
visible?: boolean;
|
||||
refreshDisabled?: boolean;
|
||||
listHeight?: number;
|
||||
className?: string;
|
||||
onListEmpty?: () => void;
|
||||
}
|
||||
|
||||
const FIRST_PAGE = 0;
|
||||
const PAGE_SIZE = 40;
|
||||
const PREFIX = 'job-manage-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobManageList(props: IJobManageListProps) {
|
||||
const { className, listHeight, refreshDisabled, visible = true, status, onListEmpty } = props;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [loadMoreError, setLoadMoreError] = useState(false);
|
||||
const [dataList, setDataList] = useState<JobManageInfo[]>([]);
|
||||
const currentPage = useRef<number>(FIRST_PAGE);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
const prevRequestProps = useRef<IRequestProps>({});
|
||||
const onListEmptyRef = useRef(onListEmpty);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
log('start pull refresh');
|
||||
try {
|
||||
setRefreshing(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
log('pull refresh success');
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
log('pull refresh failed');
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
log('start load more', hasMore);
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
setLoadMoreError(false);
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const {
|
||||
page,
|
||||
hasMore: more,
|
||||
jobResults,
|
||||
} = await requestData({ ...requestProps.current, page: currentPage.current + 1 });
|
||||
setDataList([...dataList, ...jobResults]);
|
||||
setHasMore(more);
|
||||
currentPage.current = page;
|
||||
log('load more success');
|
||||
} catch (e) {
|
||||
setLoadMoreError(true);
|
||||
log('load more failed');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [dataList, currentPage, hasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
onListEmptyRef.current = onListEmpty;
|
||||
}, [onListEmpty]);
|
||||
|
||||
useEffect(() => {
|
||||
log('request params changed');
|
||||
requestProps.current = { status: status, pageSize: PAGE_SIZE };
|
||||
}, [status]);
|
||||
|
||||
// 初始化数据&配置变更后刷新数据
|
||||
useEffect(() => {
|
||||
// 相比前一次可见时没有数据变更时,不再重新请求
|
||||
if (isEqual(prevRequestProps.current, requestProps.current)) {
|
||||
log('visible/city changed, but request params not change, ignore');
|
||||
return;
|
||||
}
|
||||
// 列表不可见时,先不做处理
|
||||
if (!visible) {
|
||||
log('visible/city changed, but is not visible, only clear list');
|
||||
return;
|
||||
}
|
||||
|
||||
prevRequestProps.current = requestProps.current;
|
||||
const refresh = async () => {
|
||||
log('visible/city changed, start refresh list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoadingMore(true);
|
||||
setLoadMoreError(false);
|
||||
const { page, hasMore: more, jobResults } = await requestData({ ...requestProps.current, page: 1 });
|
||||
setHasMore(more);
|
||||
setDataList(jobResults);
|
||||
currentPage.current = page;
|
||||
!jobResults.length && onListEmptyRef.current?.();
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setHasMore(false);
|
||||
setLoadMoreError(true);
|
||||
currentPage.current = FIRST_PAGE;
|
||||
} finally {
|
||||
log('visible/city changed, refresh list data end');
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, [visible, status]);
|
||||
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.on(EventName.COMPANY_JOB_PUBLISH_CHANGED, handleRefresh);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.COMPANY_JOB_PUBLISH_CHANGED, handleRefresh);
|
||||
};
|
||||
}, [handleRefresh]);
|
||||
|
||||
// log('render', `hasMore: ${hasMore}, loadingMore: ${loadingMore}, refreshing: ${refreshing}`);
|
||||
|
||||
return (
|
||||
<PullRefresh
|
||||
className={classNames(`${PREFIX}__pull-refresh`, className)}
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshDisabled}
|
||||
>
|
||||
<List
|
||||
hasMore={hasMore}
|
||||
onLoad={handleLoadMore}
|
||||
loading={loadingMore || refreshing}
|
||||
disabled={loadMoreError}
|
||||
fixedHeight={typeof listHeight !== 'undefined'}
|
||||
style={listHeight ? { height: `${listHeight}px` } : undefined}
|
||||
>
|
||||
{dataList.map(item => (
|
||||
<JobManageCard data={item} key={item.id} />
|
||||
))}
|
||||
<ListPlaceholder hasMore={hasMore} loadingMore={loadingMore} loadMoreError={loadMoreError} noMoreText="" />
|
||||
</List>
|
||||
</PullRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobManageList;
|
47
src/components/job-picker/index.less
Normal file
47
src/components/job-picker/index.less
Normal file
@ -0,0 +1,47 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-type-picker {
|
||||
background: #FFFFFF;
|
||||
|
||||
&__groups-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&__group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__group-items-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__group-item {
|
||||
min-width: 158px;
|
||||
height: 64px;
|
||||
font-size: 28px;
|
||||
padding: 0 19px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 24px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 34px;
|
||||
line-height: 64px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
color: @blColor;
|
||||
background: #F6F6F6;
|
||||
|
||||
&.selected {
|
||||
color: @blHighlightColor;
|
||||
background: @blHighlightBg;
|
||||
}
|
||||
}
|
||||
}
|
90
src/components/job-picker/index.tsx
Normal file
90
src/components/job-picker/index.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
|
||||
import PickerToolbar from '@/components/picker-toolbar';
|
||||
import { EMPLOY_TYPE_TITLE_MAP, EmployType } from '@/constants/job';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
onConfirm: (employType: EmployType) => void;
|
||||
}
|
||||
|
||||
interface IGroupProps {
|
||||
name: string;
|
||||
types: string[];
|
||||
typeTitleMap: Record<string, string>;
|
||||
selectTypes: string[];
|
||||
onClickItem: (item: string) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'job-type-picker';
|
||||
|
||||
const TypeGroup = (props: IGroupProps) => {
|
||||
const { name, types, selectTypes, typeTitleMap, onClickItem } = props;
|
||||
return (
|
||||
<div className={`${PREFIX}__group`}>
|
||||
<div className={`${PREFIX}__group-title`}>{name}</div>
|
||||
<div className={`${PREFIX}__group-items-container`}>
|
||||
{types.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
onClick={() => onClickItem(type)}
|
||||
className={classNames(`${PREFIX}__group-item`, { selected: selectTypes.includes(type) })}
|
||||
>
|
||||
{typeTitleMap[type]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function JobPicker(props: IProps) {
|
||||
const [selectedEmployTypes, setSelectedEmployTypes] = useState<EmployType[]>([EmployType.Full, EmployType.Part]);
|
||||
const { onConfirm } = props;
|
||||
|
||||
const handleClickEmployType = (clickType: EmployType) => {
|
||||
if (selectedEmployTypes.includes(clickType)) {
|
||||
setSelectedEmployTypes(selectedEmployTypes.filter(type => type !== clickType));
|
||||
} else {
|
||||
setSelectedEmployTypes(selectedEmployTypes.concat([clickType]));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickReset = () => {
|
||||
setSelectedEmployTypes([EmployType.Full, EmployType.Part]);
|
||||
};
|
||||
|
||||
const handleClickConfirm = () => {
|
||||
if (!selectedEmployTypes.length) {
|
||||
Toast.error('至少选择一个');
|
||||
return;
|
||||
}
|
||||
const newEmployType = selectedEmployTypes.length === 1 ? selectedEmployTypes[0] : EmployType.All;
|
||||
onConfirm(newEmployType);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__groups-container`}>
|
||||
<TypeGroup
|
||||
name="职位类型"
|
||||
types={[EmployType.Full, EmployType.Part]}
|
||||
typeTitleMap={EMPLOY_TYPE_TITLE_MAP}
|
||||
selectTypes={selectedEmployTypes}
|
||||
onClickItem={handleClickEmployType}
|
||||
/>
|
||||
</div>
|
||||
<PickerToolbar
|
||||
cancelText="重置"
|
||||
confirmText="确定"
|
||||
onClickCancel={handleClickReset}
|
||||
onClickConfirm={handleClickConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobPicker;
|
44
src/components/job-recommend-list/index.less
Normal file
44
src/components/job-recommend-list/index.less
Normal file
@ -0,0 +1,44 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.job-recommend-list {
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
&__header-left-line,
|
||||
&__header-right-line {
|
||||
width: 88px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&__header-left-line {
|
||||
background: linear-gradient(90deg, rgba(109, 61, 245, 0) 0%, #6D3DF5 100%);
|
||||
}
|
||||
|
||||
&__header-right-line {
|
||||
background: linear-gradient(90deg, #6D3DF5 0%, rgba(109, 61, 245, 0) 100%);
|
||||
}
|
||||
|
||||
&__header-title {
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 500;
|
||||
color: @blHighlightColor;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&__header-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&__more-button {
|
||||
.button(@height: 80px; @fontSize: 32px; @fontWeight: 500; @borderRadius: 44px;);
|
||||
}
|
||||
}
|
83
src/components/job-recommend-list/index.tsx
Normal file
83
src/components/job-recommend-list/index.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Button, Image } from '@tarojs/components';
|
||||
|
||||
import { List } from '@taroify/core';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import JobCard from '@/components/job-card';
|
||||
import ListPlaceholder from '@/components/list-placeholder';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { JobInfo, GetJobsRequest } from '@/types/job';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { requestMyRecommendJobList } from '@/utils/job';
|
||||
import { switchTab } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IRequestProps extends Partial<GetJobsRequest> {}
|
||||
|
||||
export interface IJobListProps extends IRequestProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const PREFIX = 'job-recommend-list';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function JobRecommendList(props: IJobListProps) {
|
||||
const { className } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [dataList, setDataList] = useState<JobInfo[]>([]);
|
||||
const requestProps = useRef<IRequestProps>({});
|
||||
|
||||
const handleClickAllJob = useCallback(() => {
|
||||
switchTab(PageUrl.Job);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestProps.current = { page: 1, pageSize: PAGE_SIZE };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = async () => {
|
||||
log('start request list data');
|
||||
try {
|
||||
setDataList([]);
|
||||
setLoading(true);
|
||||
setLoadError(false);
|
||||
const { jobResults = [] } = await requestMyRecommendJobList({ ...requestProps.current });
|
||||
setDataList(jobResults);
|
||||
} catch (e) {
|
||||
setDataList([]);
|
||||
setLoadError(true);
|
||||
} finally {
|
||||
log('request list data end');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header-left-line`} />
|
||||
<Image className={`${PREFIX}__header-icon`} src={require('@/statics/svg/like.svg')} />
|
||||
<div className={`${PREFIX}__header-title`}>为你推荐附近通告</div>
|
||||
<div className={`${PREFIX}__header-right-line`} />
|
||||
</div>
|
||||
<List disabled>
|
||||
{dataList.map(item => (
|
||||
<JobCard data={item} key={item.id} redirectOpen />
|
||||
))}
|
||||
<ListPlaceholder noMoreText="" loadingMore={loading} loadMoreError={loadError} loadMoreErrorText="加载失败" />
|
||||
</List>
|
||||
<Button className={`${PREFIX}__more-button`} onClick={handleClickAllJob}>
|
||||
查看更多通告
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobRecommendList;
|
0
src/components/list-placeholder/index.less
Normal file
0
src/components/list-placeholder/index.less
Normal file
33
src/components/list-placeholder/index.tsx
Normal file
33
src/components/list-placeholder/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { List, Loading } from '@taroify/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IPlaceholderProps {
|
||||
hasMore: boolean;
|
||||
loadingMore: boolean;
|
||||
loadMoreError: boolean;
|
||||
noMoreText: ReactNode;
|
||||
loadMoreErrorText: ReactNode;
|
||||
}
|
||||
|
||||
function ListPlaceholder(props: Partial<IPlaceholderProps>) {
|
||||
const { hasMore, loadingMore, loadMoreError, noMoreText, loadMoreErrorText } = props;
|
||||
|
||||
let content: ReactNode = '';
|
||||
if (loadingMore) {
|
||||
content = <Loading>加载中...</Loading>;
|
||||
} else if (loadMoreError) {
|
||||
content = loadMoreErrorText ?? '加载失败,请下拉刷新重试';
|
||||
} else if (!hasMore) {
|
||||
content = noMoreText ?? '没有更多了';
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <List.Placeholder>{content}</List.Placeholder>;
|
||||
}
|
||||
|
||||
export default ListPlaceholder;
|
52
src/components/loading-dialog/index.less
Normal file
52
src/components/loading-dialog/index.less
Normal file
@ -0,0 +1,52 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.loading-dialog {
|
||||
|
||||
&__dialog-content {
|
||||
.flex-column();
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
position: relative;
|
||||
width: 188px;
|
||||
height: 188px;
|
||||
}
|
||||
|
||||
&__icon-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
background: conic-gradient(#6D3DF5, 30%, #ECE5FF);
|
||||
border-radius: 50%;
|
||||
animation: spin 1.5s linear infinite reverse;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 188px;
|
||||
height: 188px;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F2;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 58px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
src/components/loading-dialog/index.tsx
Normal file
35
src/components/loading-dialog/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'loading-dialog';
|
||||
|
||||
function LoadingDialog(props: IProps) {
|
||||
const { open, text } = props;
|
||||
return (
|
||||
<Dialog className={PREFIX} open={open}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__dialog-content`}>
|
||||
<div className={`${PREFIX}__dialog-content__icon-wrapper`}>
|
||||
<div className={`${PREFIX}__dialog-content__icon-bg`} />
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__dialog-content__icon`}
|
||||
src={require('@/statics/svg/certification-tips-icon.svg')}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${PREFIX}__dialog-content__title`}>{text}</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingDialog;
|
38
src/components/location-dialog/index.less
Normal file
38
src/components/location-dialog/index.less
Normal file
@ -0,0 +1,38 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.location-dialog {
|
||||
&__container {
|
||||
.flex-column();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 56px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
// &__cancel-button {
|
||||
// min-width: fit-content;
|
||||
// font-size: 28px;
|
||||
// line-height: 32px;
|
||||
// color: @blHighlightColor;
|
||||
// background: transparent;
|
||||
// border: none;
|
||||
// margin-top: 40px;
|
||||
|
||||
// &::after {
|
||||
// border-color: transparent
|
||||
// }
|
||||
// }
|
||||
|
||||
&__checkbox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
47
src/components/location-dialog/index.tsx
Normal file
47
src/components/location-dialog/index.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClick: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'location-dialog';
|
||||
|
||||
export default function LocationDialog(props: IProps) {
|
||||
const { open, onClick, onClose } = props;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleTipCheck = useCallback(() => {
|
||||
Toast.info('请先阅读并同意协议');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>{`我们需要获取您的位置信息\n以便推荐附近的通告或主播`}</div>
|
||||
{!checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
|
||||
授权
|
||||
</Button>
|
||||
)}
|
||||
{checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={onClick}>
|
||||
授权
|
||||
</Button>
|
||||
)}
|
||||
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
4
src/components/login-button/index.less
Normal file
4
src/components/login-button/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
.login-button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
52
src/components/login-button/index.tsx
Normal file
52
src/components/login-button/index.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Button, ButtonProps } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import LoginDialog from '@/components/login-dialog';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { isNeedLogin } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export enum BindPhoneStatus {
|
||||
Success,
|
||||
Cancel,
|
||||
Error,
|
||||
}
|
||||
|
||||
export interface ILoginButtonProps extends ButtonProps {
|
||||
needPhone?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'login-button';
|
||||
|
||||
function LoginButton(props: ILoginButtonProps) {
|
||||
const { className, children, needPhone, onClick, ...otherProps } = props;
|
||||
const userInfo = useUserInfo();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const needLogin = isNeedLogin(userInfo);
|
||||
|
||||
const onSuccess = useCallback(
|
||||
e => {
|
||||
setVisible(false);
|
||||
onClick?.(e);
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{...otherProps}
|
||||
className={classNames(PREFIX, className)}
|
||||
onClick={needLogin ? () => setVisible(true) : onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{visible && <LoginDialog onCancel={() => setVisible(false)} onSuccess={onSuccess} needPhone={needPhone} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginButton;
|
38
src/components/login-dialog/index.less
Normal file
38
src/components/login-dialog/index.less
Normal file
@ -0,0 +1,38 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.login-dialog {
|
||||
&__container {
|
||||
.flex-column();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 56px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&__cancel-button {
|
||||
min-width: fit-content;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin-top: 40px;
|
||||
|
||||
&::after {
|
||||
border-color: transparent
|
||||
}
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
58
src/components/login-dialog/index.tsx
Normal file
58
src/components/login-dialog/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Dialog } from '@taroify/core';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import PhoneButton, { IPhoneButtonProps } from '@/components/phone-button';
|
||||
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
|
||||
import Toast from '@/utils/toast';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
onCancel: () => void;
|
||||
needPhone?: IPhoneButtonProps['needPhone'];
|
||||
onSuccess?: IPhoneButtonProps['onSuccess'];
|
||||
onBindPhone?: IPhoneButtonProps['onBindPhone'];
|
||||
}
|
||||
|
||||
const PREFIX = 'login-dialog';
|
||||
|
||||
export default function LoginDialog(props: IProps) {
|
||||
const { title = '使用播络服务前,请先登录', needPhone, onSuccess, onCancel, onBindPhone } = props;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleTipCheck = useCallback(() => {
|
||||
Toast.info('请先阅读并同意协议');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onCancel}>
|
||||
<Dialog.Content>
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>{title}</div>
|
||||
{!checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
{checked && (
|
||||
<PhoneButton
|
||||
className={`${PREFIX}__confirm-button`}
|
||||
onSuccess={onSuccess}
|
||||
onBindPhone={onBindPhone}
|
||||
needPhone={needPhone}
|
||||
>
|
||||
登录
|
||||
</PhoneButton>
|
||||
)}
|
||||
<Button className={`${PREFIX}__cancel-button`} onClick={onCancel}>
|
||||
跳过,暂不登录
|
||||
</Button>
|
||||
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
50
src/components/login-guide/index.less
Normal file
50
src/components/login-guide/index.less
Normal file
@ -0,0 +1,50 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.login-content {
|
||||
&__container {
|
||||
.flex-column();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
line-height: 56px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__confirm-button {
|
||||
.button(@width: 360px, @height: 72px, @fontSize: 28px, @fontWeight: 400, @borderRadius: 44px);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&__cancel-button {
|
||||
min-width: fit-content;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
color: @blHighlightColor;
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin-top: 40px;
|
||||
|
||||
&::after {
|
||||
border-color: transparent
|
||||
}
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1001;
|
||||
background: #FFFFFF;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
padding-top: 48px;
|
||||
padding-bottom: 64px;
|
||||
}
|
119
src/components/login-guide/index.tsx
Normal file
119
src/components/login-guide/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Popup } from '@taroify/core';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import PhoneButton, { BindPhoneStatus, IPhoneButtonProps } from '@/components/phone-button';
|
||||
import { ProtocolPrivacyCheckbox } from '@/components/protocol-privacy';
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { logWithPrefix, sleep } from '@/utils/common';
|
||||
import { waitLocationAuthorizeHidden } from '@/utils/location';
|
||||
import Toast from '@/utils/toast';
|
||||
import { shouldShowLoginGuide } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IContentProps extends Pick<IPhoneButtonProps, 'onBindPhone'> {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
disabled: boolean;
|
||||
onAfterBind?: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'login-content';
|
||||
const PREFIX_GUIDE = 'login-guide';
|
||||
const log = logWithPrefix(PREFIX_GUIDE);
|
||||
|
||||
function LoginContent(props: IContentProps) {
|
||||
const { onCancel, onBindPhone } = props;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleClose = useCallback(() => onCancel(), [onCancel]);
|
||||
|
||||
const handleTipCheck = useCallback(() => {
|
||||
Toast.info('请先阅读并同意协议');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${PREFIX}__container`}>
|
||||
<div className={`${PREFIX}__title`}>使用播络服务前,请先登录</div>
|
||||
{!checked && (
|
||||
<Button className={`${PREFIX}__confirm-button`} onClick={handleTipCheck}>
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
{checked && (
|
||||
<PhoneButton
|
||||
className={`${PREFIX}__confirm-button`}
|
||||
onSuccess={handleClose}
|
||||
onBindPhone={onBindPhone}
|
||||
needPhone
|
||||
>
|
||||
登录
|
||||
</PhoneButton>
|
||||
)}
|
||||
<Button className={`${PREFIX}__cancel-button`} onClick={handleClose}>
|
||||
跳过,暂不登录
|
||||
</Button>
|
||||
<ProtocolPrivacyCheckbox checked={checked} onChange={setChecked} className={`${PREFIX}__checkbox`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoginGuide(props: IProps) {
|
||||
const { disabled = false, onAfterBind } = props;
|
||||
const userInfo = useUserInfo();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleBind = useCallback(
|
||||
(status: BindPhoneStatus) => {
|
||||
status === BindPhoneStatus.Success && onAfterBind?.();
|
||||
status === BindPhoneStatus.Success && setOpen(false);
|
||||
},
|
||||
[onAfterBind]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => setOpen(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled || !shouldShowLoginGuide(userInfo)) {
|
||||
return;
|
||||
}
|
||||
let effectCleaned = false;
|
||||
const showGuide = async () => {
|
||||
await sleep(1);
|
||||
await waitLocationAuthorizeHidden();
|
||||
if (effectCleaned) {
|
||||
log('ignore login guide, effect changed');
|
||||
return;
|
||||
}
|
||||
setOpen(true);
|
||||
log('open login guide');
|
||||
};
|
||||
showGuide();
|
||||
return () => {
|
||||
effectCleaned = true;
|
||||
};
|
||||
}, [disabled, userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
log('hide login guide by disabled');
|
||||
setOpen(false);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup className={PREFIX_GUIDE} placement="bottom" open={open} onClose={handleClose}>
|
||||
<LoginContent onCancel={handleClose} onBindPhone={handleBind} />
|
||||
<SafeBottomPadding />
|
||||
</Popup>
|
||||
);
|
||||
}
|
117
src/components/material-card/index.less
Normal file
117
src/components/material-card/index.less
Normal file
@ -0,0 +1,117 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-card {
|
||||
padding: 32px 24px;
|
||||
border-radius: 16px;
|
||||
background: #FFFFFF;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__header {
|
||||
.flex-row();
|
||||
justify-content: space-between;
|
||||
|
||||
&__left,
|
||||
&__right {
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
.flex-row();
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
width: 100%;
|
||||
height: 156px;
|
||||
margin-top: 24px;
|
||||
.flex-column();
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
height: 100%;
|
||||
.flex-column();
|
||||
justify-content: center;
|
||||
|
||||
&__tips {
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
|
||||
&__create-button {
|
||||
.button();
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 22px;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 102px;
|
||||
height: 100%;
|
||||
background: linear-gradient(91.41deg, rgba(255, 255, 255, 0) 1.86%, #FFFFFF 99.47%);
|
||||
}
|
||||
}
|
||||
|
||||
&__cover-list {
|
||||
height: 100%;
|
||||
.flex-row();
|
||||
}
|
||||
|
||||
&__cover-image {
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
// 不知道为啥高度不对,可能 scroll-view 默认底部是滚动条高度?
|
||||
margin-top: 38px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
}
|
145
src/components/material-card/index.tsx
Normal file
145
src/components/material-card/index.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { Image, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { Loading } from '@taroify/core';
|
||||
import { ArrowRight } from '@taroify/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import LoginButton from '@/components/login-button';
|
||||
import { EventName, PageUrl } from '@/constants/app';
|
||||
import { CollectEventName, ReportEventId } from '@/constants/event';
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
import { logWithPrefix } from '@/utils/common';
|
||||
import { collectEvent, reportEvent } from '@/utils/event';
|
||||
import { requestProfileDetail, sortVideos } from '@/utils/material';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import Toast from '@/utils/toast';
|
||||
import { isValidUserInfo } from '@/utils/user';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-card';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
const realtimeLogger = Taro.getRealtimeLogManager();
|
||||
realtimeLogger.tag(PREFIX);
|
||||
|
||||
function MaterialCard(props: IProps) {
|
||||
const { className } = props;
|
||||
const userInfo = useUserInfo();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profile, setProfile] = useState<MaterialProfile | null>(null);
|
||||
const refreshRef = useRef((_f?: boolean) => { });
|
||||
const hasMaterial = !!profile;
|
||||
|
||||
const handleGoCreateProfile = useCallback(() => {
|
||||
reportEvent(ReportEventId.CLICK_GO_TO_CREATE_MATERIAL);
|
||||
navigateTo(PageUrl.MaterialUploadVideo);
|
||||
}, []);
|
||||
|
||||
const handleGoProfile = useCallback(() => {
|
||||
if (!hasMaterial) {
|
||||
realtimeLogger.info('handleGoProfile noMaterial')
|
||||
return;
|
||||
}
|
||||
navigateTo(PageUrl.MaterialProfile).catch(err => {
|
||||
realtimeLogger.error('handleGoProfile Failed', err);
|
||||
});
|
||||
}, [hasMaterial]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshRef.current = async (force: boolean = false) => {
|
||||
collectEvent(CollectEventName.MATERIAL_CARD_VIEW, {
|
||||
status: 'refresh',
|
||||
info: { force, isCreateResume: userInfo.isCreateResume },
|
||||
});
|
||||
setLoading(true);
|
||||
if (!userInfo.isCreateResume && !force) {
|
||||
log('refresh break by is not create resume');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const profileDetail = await requestProfileDetail();
|
||||
if (!profileDetail) {
|
||||
realtimeLogger.info('getProfileDetail no profileDetail')
|
||||
}
|
||||
setProfile(profileDetail);
|
||||
} catch (e) {
|
||||
realtimeLogger.error('getProfileDetail Failed', e);
|
||||
Toast.error('加载失败');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
}, [userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidUserInfo(userInfo)) {
|
||||
return;
|
||||
}
|
||||
refreshRef.current?.(true);
|
||||
}, [userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async () => {
|
||||
refreshRef.current?.(true);
|
||||
};
|
||||
Taro.eventCenter.on(EventName.CREATE_PROFILE, callback);
|
||||
return () => {
|
||||
Taro.eventCenter.off(EventName.CREATE_PROFILE, callback);
|
||||
};
|
||||
}, [userInfo]);
|
||||
|
||||
return (
|
||||
<div className={classNames(PREFIX, className)} onClick={handleGoProfile}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__header__left`}>
|
||||
<div className={`${PREFIX}__header__title`}>我的模卡</div>
|
||||
{/* {profile && (
|
||||
<div
|
||||
className={`${PREFIX}__header__progress`}
|
||||
>{`完成度${Math.min((profile.progressBar || 0) * 100, 100)}%`}</div>
|
||||
)} */}
|
||||
</div>
|
||||
{profile && (
|
||||
<div className={`${PREFIX}__header__right`}>
|
||||
{/* <div className={`${PREFIX}__header__status`}>{profile?.isOpen ? '开放中' : '关闭'}</div> */}
|
||||
<ArrowRight className={`${PREFIX}__header__icon`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__body`}>
|
||||
{!loading && !hasMaterial && (
|
||||
<div className={`${PREFIX}__placeholder`}>
|
||||
<div className={`${PREFIX}__placeholder__tips`}>创建模卡更容易被老板挑中哦</div>
|
||||
<LoginButton className={`${PREFIX}__placeholder__create-button`} onClick={handleGoCreateProfile}>
|
||||
去创建
|
||||
</LoginButton>
|
||||
</div>
|
||||
)}
|
||||
{!loading && hasMaterial && (
|
||||
<ScrollView className={`${PREFIX}__scroll-view`} showScrollbar={false} enableFlex enhanced scrollX>
|
||||
<div className={`${PREFIX}__cover-list`}>
|
||||
{sortVideos(profile?.materialVideoInfoList || []).map(video => (
|
||||
<Image
|
||||
className={`${PREFIX}__cover-image`}
|
||||
mode="aspectFit"
|
||||
key={video.coverUrl}
|
||||
src={video.coverUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollView>
|
||||
)}
|
||||
{loading && <Loading />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialCard;
|
0
src/components/material-guide/index.less
Normal file
0
src/components/material-guide/index.less
Normal file
43
src/components/material-guide/index.tsx
Normal file
43
src/components/material-guide/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import CommonDialog from '@/components/common-dialog';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { ReportEventId } from '@/constants/event';
|
||||
import { reportEvent } from '@/utils/event';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-guide';
|
||||
|
||||
function MaterialGuide(props: IProps) {
|
||||
const { onClose } = props;
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
reportEvent(ReportEventId.VIEW_MATERIAL_GUIDE);
|
||||
navigateTo(PageUrl.MaterialUploadVideo);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
// useEffect(() => {
|
||||
// updateLastMaterialGuideTime();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<CommonDialog
|
||||
className={`${PREFIX}__dialog`}
|
||||
visible
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onClick={handleConfirm}
|
||||
content="完善模卡更容易获得老板青睐"
|
||||
confirm="立刻完善"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialGuide;
|
42
src/components/material-manage-popup/index.less
Normal file
42
src/components/material-manage-popup/index.less
Normal file
@ -0,0 +1,42 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-manage-popup {
|
||||
.flex-column();
|
||||
|
||||
&__popup {
|
||||
background: #FFFFFF;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
.flex-column();
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__tips {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
.button(@width: 360px; @height: 72px; @fontSize: 28px; @borderRadius: 44px);
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
58
src/components/material-manage-popup/index.tsx
Normal file
58
src/components/material-manage-popup/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Button } from '@tarojs/components';
|
||||
|
||||
import { Popup } from '@taroify/core';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import SafeBottomPadding from '@/components/safe-bottom-padding';
|
||||
import Select from '@/components/select';
|
||||
import { MaterialStatus } from '@/constants/material';
|
||||
import { MaterialProfile } from '@/types/material';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
value: MaterialStatus;
|
||||
onSave: (newValue: MaterialProfile['isOpen']) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-manage-popup';
|
||||
const OPTIONS = [
|
||||
{ label: '开放', value: MaterialStatus.Open },
|
||||
{ label: '关闭', value: MaterialStatus.Close },
|
||||
];
|
||||
|
||||
function MaterialManagePopup(props: IProps) {
|
||||
const { open, value = MaterialStatus.Open, onSave, onClose } = props;
|
||||
const [currentValue, setCurrentValue] = useState<MaterialStatus>(value);
|
||||
|
||||
const handleSelect = useCallback((v: MaterialStatus) => setCurrentValue(v), []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(currentValue === MaterialStatus.Open);
|
||||
onClose();
|
||||
}, [currentValue, onSave, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Popup className={`${PREFIX}__popup`} placement="bottom" open={open} onClose={onClose}>
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__header`}>
|
||||
<div className={`${PREFIX}__title`}>模卡开放状态</div>
|
||||
<div className={`${PREFIX}__tips`}>开放模卡给企业查看后,可以获得更多求职机会</div>
|
||||
</div>
|
||||
<Select className={`${PREFIX}__select`} options={OPTIONS} value={currentValue} onSelect={handleSelect} />
|
||||
<Button className={`${PREFIX}__btn`} onClick={handleSave}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
<SafeBottomPadding />
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialManagePopup;
|
96
src/components/material-video-card/index.less
Normal file
96
src/components/material-video-card/index.less
Normal file
@ -0,0 +1,96 @@
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.material-video-card {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16px;
|
||||
|
||||
&__cover {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__cover__image,
|
||||
&__cover__placeholder {
|
||||
width: 150px;
|
||||
height: 200px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
&__cover__placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
&__cover__placeholder__image {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&__cover__preview-video {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 50%;
|
||||
transform: translate3d(50%, -50%, 0);
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__info__title {
|
||||
width: 100%;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
|
||||
&__info__title__input {
|
||||
font-size: 32px;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__info__title__placeholder {
|
||||
font-size: 32px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
&__info__operate {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__info__operate__checkbox {
|
||||
--checkbox-label-color: @blColor;
|
||||
--checkbox-checked-icon-border-color: @blHighlightColor;
|
||||
--checkbox-checked-icon-background-color: @blHighlightColor;
|
||||
}
|
||||
|
||||
&__info__operate__delete {
|
||||
color: @blHighlightColor;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__info__temp-tips {
|
||||
font-size: 28px;
|
||||
line-height: 50px;
|
||||
font-weight: 400;
|
||||
color: @blColorG1;
|
||||
}
|
||||
}
|
149
src/components/material-video-card/index.tsx
Normal file
149
src/components/material-video-card/index.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { BaseEventOrig, Image, Input, InputProps, Text } from '@tarojs/components';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
import { Checkbox } from '@taroify/core';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { MaterialVideoInfo } from '@/types/material';
|
||||
import { logWithPrefix, isDesktop } from '@/utils/common';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
isTemp?: boolean;
|
||||
videoInfo: MaterialVideoInfo;
|
||||
onClickUpload?: () => void;
|
||||
onClickDelete?: () => void;
|
||||
onClickSetDefault?: () => void;
|
||||
onTitleChange?: (newTitle: string) => void;
|
||||
}
|
||||
|
||||
const PREFIX = 'material-video-card';
|
||||
const log = logWithPrefix(PREFIX);
|
||||
|
||||
function MaterialVideoCard(props: IProps) {
|
||||
const { videoInfo, isTemp = false, onClickUpload, onClickDelete, onClickSetDefault, onTitleChange } = props;
|
||||
const isVideo = videoInfo.type === 'video';
|
||||
|
||||
const handleInput = useCallback(
|
||||
(e: BaseEventOrig<InputProps.inputValueEventDetail>) => {
|
||||
const value = e.detail?.value || '';
|
||||
log('handleInput value', value);
|
||||
onTitleChange?.(value);
|
||||
},
|
||||
[onTitleChange]
|
||||
);
|
||||
|
||||
// const handleInputBlurOrConfirm = useCallback(() => {
|
||||
// log('newVideoTitle', title);
|
||||
// if (!title) {
|
||||
// return;
|
||||
// }
|
||||
// onTitleChange?.(videoInfo);
|
||||
// // ...
|
||||
// }, [title, videoInfo, onTitleChange]);
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
log('handleCheckboxChange', checked);
|
||||
if (videoInfo.isDefault) {
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
onClickSetDefault?.();
|
||||
},
|
||||
[videoInfo, onClickSetDefault]
|
||||
);
|
||||
|
||||
const handleClickVideo = useCallback(() => {
|
||||
log('handleClickVideo', videoInfo);
|
||||
if (!videoInfo.url) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
navigateTo(PageUrl.MaterialWebview, {
|
||||
source: encodeURIComponent(videoInfo.url)
|
||||
})
|
||||
} else {
|
||||
Taro.previewMedia({
|
||||
sources: [{ url: videoInfo.url, type: videoInfo.type }],
|
||||
});
|
||||
}
|
||||
|
||||
}, [videoInfo]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX}>
|
||||
<div className={`${PREFIX}__cover`}>
|
||||
{!isTemp && (
|
||||
<>
|
||||
<Image
|
||||
className={`${PREFIX}__cover__image`}
|
||||
mode="aspectFit"
|
||||
src={videoInfo.coverUrl}
|
||||
onClick={handleClickVideo}
|
||||
/>
|
||||
{isVideo && (
|
||||
<Image
|
||||
className={`${PREFIX}__cover__preview-video`}
|
||||
mode="aspectFit"
|
||||
src={require('@/statics/svg/preview_video.svg')}
|
||||
onClick={handleClickVideo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isTemp && (
|
||||
<div className={`${PREFIX}__cover__placeholder`} onClick={onClickUpload}>
|
||||
<Image
|
||||
className={`${PREFIX}__cover__placeholder__image`}
|
||||
mode="aspectFit"
|
||||
src={require('@/statics/svg/add.svg')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${PREFIX}__info`}>
|
||||
{!isTemp && (
|
||||
<>
|
||||
<div className={`${PREFIX}__info__title`}>
|
||||
<Input
|
||||
value={videoInfo.title}
|
||||
maxlength={20}
|
||||
confirmType="done"
|
||||
placeholder="请填写直播产品名称"
|
||||
onInput={handleInput}
|
||||
// onBlur={handleInputBlurOrConfirm}
|
||||
// onConfirm={handleInputBlurOrConfirm}
|
||||
className={`${PREFIX}__info__title__input`}
|
||||
placeholderClass={`${PREFIX}__info__title__placeholder`}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${PREFIX}__info__operate`}>
|
||||
<Checkbox
|
||||
checked={videoInfo.isDefault}
|
||||
onChange={handleCheckboxChange}
|
||||
className={`${PREFIX}__info__operate__checkbox`}
|
||||
>
|
||||
选为封面
|
||||
</Checkbox>
|
||||
<div className={`${PREFIX}__info__operate__delete`} onClick={onClickDelete}>
|
||||
删除
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isTemp && (
|
||||
<Text className={`${PREFIX}__info__temp-tips`}>
|
||||
{`视频不能超过1000M
|
||||
视频若太大加载较慢,请耐心等待`}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MaterialVideoCard;
|
85
src/components/message-card/index.less
Normal file
85
src/components/message-card/index.less
Normal file
@ -0,0 +1,85 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.message-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
padding: 20px 32px;
|
||||
box-sizing: border-box;
|
||||
background: #FFF;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
background: #00000026;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(32px + 90px + 24px);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__unread {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: calc(32px - 8px);
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
padding: 4px 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
background: #EB5953;
|
||||
transform: translate3d(20%, -50%, 0);
|
||||
}
|
||||
|
||||
&__body-container {
|
||||
flex: 1;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 32px;
|
||||
line-height: 48px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: 78vw;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: #8D8E99;
|
||||
margin-top: 4px;
|
||||
.noWrap();
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
color: #8D8E99;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
42
src/components/message-card/index.tsx
Normal file
42
src/components/message-card/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { PageUrl } from '@/constants/app';
|
||||
import { MainMessage } from '@/types/message';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import { formatTime } from '@/utils/time';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface IProps {
|
||||
data: MainMessage;
|
||||
}
|
||||
|
||||
const PREFIX = 'message-card';
|
||||
|
||||
function MessageCard(props: IProps) {
|
||||
const { data } = props;
|
||||
|
||||
const handleClick = useCallback(() => navigateTo(PageUrl.MessageChat, { chatId: data.chatId }), [data]);
|
||||
|
||||
return (
|
||||
<div className={PREFIX} onClick={handleClick}>
|
||||
<div className={`${PREFIX}__avatar-container`}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__avatar`}
|
||||
src={data.toUserAvatarUrl || require('@/statics/png/default_avatar.png')}
|
||||
/>
|
||||
{!!data.unReadMsgCount && <div className={`${PREFIX}__unread`}>{Math.min(data.unReadMsgCount, 999)}</div>}
|
||||
</div>
|
||||
<div className={`${PREFIX}__body-container`}>
|
||||
<div className={`${PREFIX}__name`}>{data.toUserName}</div>
|
||||
<div className={`${PREFIX}__content`}>{data.lastContactMsgContent}</div>
|
||||
<div className={`${PREFIX}__time`}>{formatTime(data.lastContactTime, 'MM-DD')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageCard;
|
54
src/components/message-chat/base/index.less
Normal file
54
src/components/message-chat/base/index.less
Normal file
@ -0,0 +1,54 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.base-message {
|
||||
width: 100%;
|
||||
.flex-row();
|
||||
align-items: flex-start;
|
||||
margin-top: 40px;
|
||||
padding: 0 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.is-sender {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__content-container {
|
||||
flex: 1;
|
||||
.flex-column();
|
||||
align-items: flex-start;
|
||||
margin: 0 16px;
|
||||
|
||||
.is-sender & {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
background: #D9D9D9;
|
||||
padding: 20px 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
color: @blHighlightColor;
|
||||
margin-top: 8px;
|
||||
|
||||
&.done {
|
||||
color: #8D8E99;
|
||||
}
|
||||
}
|
||||
}
|
66
src/components/message-chat/base/index.tsx
Normal file
66
src/components/message-chat/base/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Image } from '@tarojs/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { PropsWithChildren, useEffect, useState, useCallback } from 'react';
|
||||
import { MaterialViewSource } from '@/constants/material';
|
||||
|
||||
import useUserInfo from '@/hooks/use-user-info';
|
||||
import { IChatMessage } from '@/types/message';
|
||||
import { getScrollItemId } from '@/utils/common';
|
||||
import { navigateTo } from '@/utils/route';
|
||||
import { PageUrl } from '@/constants/app';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export interface IBaseMessageProps {
|
||||
id: string;
|
||||
message: IChatMessage;
|
||||
}
|
||||
|
||||
export interface IUserMessageProps extends PropsWithChildren, IBaseMessageProps {
|
||||
isRead?: boolean;
|
||||
}
|
||||
|
||||
const PREFIX = 'base-message';
|
||||
|
||||
function BaseMessage(props: IUserMessageProps) {
|
||||
const { id, message, isRead: isReadProps, children } = props;
|
||||
const { userId } = useUserInfo();
|
||||
const [isRead, setIsRead] = useState(message.isRead);
|
||||
const isSender = message.senderUserId === userId;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isSender) {
|
||||
// return;
|
||||
// }
|
||||
// // 对方发的消息,拉取到消息后,后端会主动已读,这里延迟模拟下
|
||||
// const timer = setTimeout(() => setIsRead(true), 1200);
|
||||
// return () => clearTimeout(timer);
|
||||
// }, [isSender]);
|
||||
const handleClick = useCallback(
|
||||
() => navigateTo(PageUrl.MaterialView, { resumeId: message.jobId, source: MaterialViewSource.Chat }),
|
||||
[message.jobId]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isRead) {
|
||||
return;
|
||||
}
|
||||
isReadProps && setIsRead(true);
|
||||
}, [isRead, isReadProps]);
|
||||
|
||||
return (
|
||||
<div className={classNames(PREFIX, { 'is-sender': isSender })} id={getScrollItemId(id)}>
|
||||
<Image
|
||||
mode="aspectFit"
|
||||
className={`${PREFIX}__avatar`}
|
||||
src={message.senderAvatarUrl || require('@/statics/png/default_avatar.png')}
|
||||
/>
|
||||
<div className={`${PREFIX}__content-container`}>
|
||||
{children}
|
||||
<div className={classNames(`${PREFIX}__status`, { done: isRead })}>{isRead ? '已读' : '未读'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BaseMessage;
|
50
src/components/message-chat/contact/index.less
Normal file
50
src/components/message-chat/contact/index.less
Normal file
@ -0,0 +1,50 @@
|
||||
@import '@/styles/common.less';
|
||||
@import '@/styles/variables.less';
|
||||
|
||||
.exchange-message {
|
||||
width: 100%;
|
||||
.flex-column();
|
||||
margin-top: 40px;
|
||||
|
||||
&__content {
|
||||
padding: 24px 60px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 400;
|
||||
color: @blColor;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
.flex-row();
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
&__reject {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @borderRadius: 48px);
|
||||
border: 2px solid #E0E0E0;
|
||||
color: @blColor;
|
||||
background: #FFF;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&__agree {
|
||||
.button(@width: 176px; @height: 56px; @fontSize: 28px; @borderRadius: 48px);
|
||||
}
|
||||
|
||||
&__disable-btn {
|
||||
.button(@height: 56px; @fontSize: 28px; @borderRadius: 48px);
|
||||
padding: 0 74px;
|
||||
color: #C0C0C0;
|
||||
background: #F0F0F0;
|
||||
|
||||
&:active {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user