feat: 联调

This commit is contained in:
秋水浮尘
2023-11-12 16:54:55 +08:00
parent 8a9d22e0aa
commit 5a17d2da74
14 changed files with 744 additions and 699 deletions

View File

@@ -1,2 +1,2 @@
VITE_API_HOST=http://117.72.10.84:3010 VITE_API_HOST=http://117.72.10.84:5000
VITE_IMG_HOST=http://117.72.14.166:9000 VITE_IMG_HOST=http://117.72.14.166:9000

View File

@@ -1,159 +1,160 @@
@import '@assets/base.less'; @import '@assets/base.less';
.app-main { .app-main {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-radius: 4px; border-radius: 4px;
margin: 0 auto;
position: absolute;
min-width: 1439px;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #f3f4f6;
padding: 66px 16px 32px;
overflow: hidden;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently not supported by any browser */
.content-box {
width: 1439px;
margin: 0 auto; margin: 0 auto;
position: absolute; overflow: auto;
min-width: 1439px; }
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #f3f4f6;
padding: 66px 16px 32px;
overflow: hidden;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently not supported by any browser */
.content-box{
width: 1439px;
margin: 0 auto;
}
} }
.header-navigator { .header-navigator {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 50px; height: 50px;
min-width: 1439px; min-width: 1439px;
background-color: #fff; background-color: #fff;
border-radius: 4px; border-radius: 4px;
} }
.nav-title { .nav-title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
cursor: pointer; cursor: pointer;
width: 1407px; width: 1407px;
margin: 0 auto; margin: 0 auto;
line-height: 50px; line-height: 50px;
color: #1890ff; color: #1890ff;
font-size: 24px; font-size: 24px;
// font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", 微软雅黑, // font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", 微软雅黑,
// Arial, sans-serif; // Arial, sans-serif;
} }
.header-navigator .user { .header-navigator .user {
width: 36px; width: 36px;
height: 36px; height: 36px;
float: right; float: right;
/* background: #1890ff; */ /* background: #1890ff; */
border-radius: 50%; border-radius: 50%;
color: #fff; color: #fff;
margin-top: 7px; margin-top: 7px;
line-height: 36px; line-height: 36px;
text-align: center; text-align: center;
font-size: 16px; font-size: 16px;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08);
} }
.jump-box { .jump-box {
font-size: 14px; font-size: 14px;
margin-right: 20px; margin-right: 20px;
} }
.info-time-box { .info-time-box {
display: flex; display: flex;
} }
.time-box { .time-box {
margin-top: 8px; margin-top: 8px;
margin-right: 120px; margin-right: 120px;
} }
.head-navigator-box { .head-navigator-box {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 50px; height: 50px;
background-color: #fff; background-color: #fff;
border-radius: 4px; border-radius: 4px;
} }
.head-navigator { .head-navigator {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin: 0 auto; margin: 0 auto;
width: 1435px; width: 1435px;
} }
.head-navigator-left { .head-navigator-left {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.head-navigator-logo { .head-navigator-logo {
margin-right: 20px; margin-right: 20px;
// line-height: 50px; // line-height: 50px;
cursor: pointer; cursor: pointer;
color: #1890ff; color: #1890ff;
font-size: 24px; font-size: 24px;
// font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 微软雅黑, // font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, 微软雅黑,
// Arial, sans-serif; // Arial, sans-serif;
} }
.head-navigator-select-box { .head-navigator-select-box {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 500px; width: 500px;
} }
.head-navigator-menu-box { .head-navigator-menu-box {
display: flex; display: flex;
} }
.head-navigator-menu-box .ant-menu-horizontal { .head-navigator-menu-box .ant-menu-horizontal {
border-bottom: 0; border-bottom: 0;
} }
.ant-menu-horizontal > .ant-menu-item, .ant-menu-horizontal > .ant-menu-item,
.ant-menu-horizontal > .ant-menu-submenu { .ant-menu-horizontal > .ant-menu-submenu {
padding: 0px; padding: 0px;
margin: 0 12px; margin: 0 12px;
} }
.head-navigator-input-box { .head-navigator-input-box {
margin-right: 24px; margin-right: 24px;
} }
.head-navigator-input-box .ant-input { .head-navigator-input-box .ant-input {
border-radius: 16px; border-radius: 16px;
} }
.head-navigator-user-box { .head-navigator-user-box {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.head-navigator-bell { .head-navigator-bell {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-right: 24px; margin-right: 24px;
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
} }
.head-navigator-bell:hover { .head-navigator-bell:hover {
background-color: rgba(0, 10, 32, 0.03); background-color: rgba(0, 10, 32, 0.03);
} }
.head-navigator-user-img { .head-navigator-user-img {
width: 36px; width: 36px;
height: 36px; height: 36px;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08);
} }

View File

@@ -1,373 +1,353 @@
import React, { Fragment, useState, useEffect } from 'react'; import React, { Fragment, useEffect, useState } from 'react'
import { import {
RightOutlined, CaretDownOutlined,
UpOutlined, CaretUpOutlined,
DownOutlined, DownOutlined,
CaretDownOutlined, RightOutlined,
CaretUpOutlined, UpOutlined
} from '@ant-design/icons'; } from '@ant-design/icons'
import req from '@utils/request'; import req from '@utils/request'
import { Divider, Spin, Modal } from 'antd'; import { Divider, Modal, Spin } from 'antd'
import _ from 'lodash'; import _ from 'lodash'
import './index.less'; import { apiName } from './constant'
import { apiName } from './constant'; import './index.less'
import { imgObject } from '@constants'
/** /**
* 大分类中的背景图 * 大分类中的背景图颜色
*/ */
export const categoryBackImg = { const categoryBackColor = {
0: imgObject.backAllImg, 0: '#23b2ff',
1: imgObject.dataImg, 1: '#ea7d4d',
2: imgObject.javaImg, 2: '#e93532',
3: imgObject.npmImg, 3: '#343d71',
4: imgObject.parallelComputingImg, 4: '#dc4ad6',
5: imgObject.springbootImg, 5: '#72b633',
6: imgObject.sqlImg, 6: '#9047de',
7: imgObject.systemDesignImg, 7: '#dc4077'
8: imgObject.algorithmImg, }
};
const categoryShowCount = 4; const categoryShowCount = 4
const cacheMap = {} const cacheMap = {}
/** /**
* 上万后展示 W+ * 上万后展示 W+
* @param {*} total * @param {*} total
* @returns * @returns
*/ */
const formatTotal = (total) => { const formatTotal = total => {
if (total >= 10000) { if (total >= 10000) {
return Math.floor(total / 10000) + 'W+'; return Math.floor(total / 10000) + 'W+'
} }
return total; return total
} }
const CategoryList = ({ primaryCategoryId, categoryList, ...props }) => { const CategoryList = ({ primaryCategoryId, categoryList, ...props }) => {
const [secondCategoryList, setSecondCategoryList] = useState([]) const [secondCategoryList, setSecondCategoryList] = useState([])
const [currentActive, setCurrentActive] = useState(categoryList[0]) const [currentActive, setCurrentActive] = useState(categoryList[0])
const [isPutAway, setIsPutAway] = useState(true) const [isPutAway, setIsPutAway] = useState(true)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [currentLabelIndex, setCurrentLabelIndex] = useState([]) const [currentLabelIndex, setCurrentLabelIndex] = useState([])
const [openMoreFlag, setOpenMoreFlag] = useState(false) const [openMoreFlag, setOpenMoreFlag] = useState(false)
const getLabels = id => {
const getLabels = (id) => { return new Promise(resolve => {
return new Promise(resolve => { req({
req({ method: 'post',
method: 'post', url: apiName.queryLabelByCategoryId,
url: apiName.queryLabelByCategoryId, data: { categoryId: id }
data: { categoryId: id } }).then(res => {
}).then(res => { if (cacheMap[id]) {
if (cacheMap[id]) { resolve(cacheMap[id])
resolve(cacheMap[id])
} else {
cacheMap[id] = res.data
resolve(res.data)
}
})
})
}
// 获取大类下分类
const getCategoryByPrimary = () => {
req({
method: 'post',
url: apiName.queryCategoryByPrimary,
data: { categoryType: 2, parentId: currentActive.id }
}).then(async res => {
let list = res.data
for (let i = 0; i < list.length; i++) {
list[i].children = await getLabels(list[i].id)
if (i === 0 && list[i].children.length) {
list[i].children[0].active = true
}
}
setSecondCategoryList(_.cloneDeep(list))
setCurrentLabelIndex([0, 0])
props.onChangeLabel(_.get(list, [0, 'id']), _.get(list, [0, 'children', 0, 'id']))
})
}
useEffect(() => {
if (primaryCategoryId) {
getCategoryByPrimary()
}
}, [primaryCategoryId])
/**
* 切换一级分类
* @param {*} item
* @returns
*/
const onChangeCategory = (item) => () => {
if (currentActive.id === item.id) {
return;
}
setCurrentActive(item)
props.onChangeCategory(item);
};
/**
* 一级分类模块
* @returns
*/
const renderFirstContainer = () => {
return (
<div className="first-category-list">
{categoryList.slice(0, 7).map((categoryModuleItem, categoryModuleIndex) => {
return (
<div
className={`first-category-item ${categoryModuleItem.id === currentActive.id &&
'first-category-item-active'
}`}
key={`first_category_${categoryModuleItem.id}`}
style={{
backgroundImage: `url(${categoryBackImg[categoryModuleIndex]})`,
}}
onClick={onChangeCategory(categoryModuleItem)}>
<div className="first-category-item-title">
{categoryModuleItem.categoryName}
</div>
<div className="first-category-item-count">
{categoryModuleItem.count || 50}道题
</div>
</div>
);
})}
{categoryList.length > 7 && (
<div className="first-category-more" onClick={() => setOpenMoreFlag(true)}>
更多
<RightOutlined />
</div>
)}
</div>
);
};
/**
* 点击更多的分类项
* @param {*} id
* @returns
*/
const onChangeCategoryMore = (cur) => () => {
setOpenMoreFlag(false)
setCurrentActive(cur)
let list = [...categoryList].reduce((pre, item) => {
if (item.id !== cur.id) {
pre.push(item);
} else {
pre.unshift(item);
}
return pre;
}, []);
props.onChangeCategoryMore && props.onChangeCategoryMore(cur.id, list);
};
/**
* 更多分类
* @returns
*/
const renderMoreBox = () => {
return (
<div className="first-category-more-list">
{categoryList.slice(7).map((categoryModuleItem, categoryModuleIndex) => {
return (
<div
className="first-category-item"
key={`first_more_category_${categoryModuleItem.id}`}
style={{
backgroundImage: `url(${categoryBackImg[categoryModuleIndex]})`,
}}
onClick={onChangeCategoryMore(
categoryModuleItem
)}
>
<div className="first-category-item-title">
{categoryModuleItem.categoryName}
</div>
<div className="first-category-item-count">
{formatTotal(categoryModuleItem.subjectCount)}道题
</div>
</div>
);
})}
</div>
);
};
/**
* 选择标签-支持单选(多选)
* @param {*} categoryId 一级分类id
* @param {*} secondCategoryIndex 二级分类对象index
* @param {*} thirdCategoryIndex 三级标签index
* @param {*} active 三级标签当前的选中状态
* @returns
*/
const onChangeLabel = (secondCategoryIndex, thirdCategoryIndex, active) => () => {
const { isMultipleChoice } = props;
const list = _.cloneDeep(secondCategoryList)
if (isMultipleChoice) {
// 三级标签支持多选
_.set(list, [secondCategoryIndex, 'children', thirdCategoryIndex, 'active'], !active)
setSecondCategoryList(list)
} else { } else {
// 三级标签支持单选 cacheMap[id] = res.data
if (currentLabelIndex.length) { resolve(res.data)
_.set(list, [currentLabelIndex[0], 'children', currentLabelIndex[1], 'active'], false)
}
_.set(list, [secondCategoryIndex, 'children', thirdCategoryIndex, 'active'], !active)
setCurrentLabelIndex([secondCategoryIndex, thirdCategoryIndex])
setSecondCategoryList(list)
} }
props.onChangeLabel(_.get(list, [secondCategoryIndex, 'id']), _.get(list, [secondCategoryIndex, 'children', thirdCategoryIndex, 'id'])) })
}; })
}
/** // 获取大类下分类
* 展开/收起 const getCategoryByPrimary = () => {
* @param {*} secondCategoryIndex req({
* @returns method: 'post',
*/ url: apiName.queryCategoryByPrimary,
const onChangeOpenStatus = (secondCategoryIndex, isOpen) => () => { data: { categoryType: 2, parentId: currentActive.id }
const _list = _.cloneDeep(secondCategoryList) }).then(async res => {
_.set(_list, [secondCategoryIndex, 'isOpen'], !isOpen); let list = res.data
setSecondCategoryList(_list) for (let i = 0; i < list.length; i++) {
}; list[i].children = await getLabels(list[i].id)
if (i === 0 && list[i].children.length) {
list[i].children[0].active = true
}
}
setSecondCategoryList(_.cloneDeep(list))
setCurrentLabelIndex([0, 0])
props.onChangeLabel(_.get(list, [0, 'id']), _.get(list, [0, 'children', 0, 'id']))
})
}
/** useEffect(() => {
* 展开/收起 if (primaryCategoryId) {
*/ getCategoryByPrimary()
const onChangePutAway = () => { }
setIsPutAway(!isPutAway) }, [primaryCategoryId])
};
/** /**
* 二级分类模块 * 切换一级分类
* @returns * @param {*} item
*/ * @returns
const renderSecondContainer = () => { */
return ( const onChangeCategory = item => () => {
<Spin spinning={loading}> if (currentActive.id === item.id) {
<div className="second-category-list"> return
{secondCategoryList.map((secondCategoryItem, secondCategoryIndex) => { }
return ( setCurrentActive(item)
<div props.onChangeCategory(item)
style={{ }
display:
secondCategoryIndex >= categoryShowCount && isPutAway
? 'none'
: 'flex',
}}
className="second-category-item"
key={`second_category_${secondCategoryItem.id}`}>
<div className="second-category-item-title">
{secondCategoryItem.categoryName}
</div>
{secondCategoryItem?.children?.length > 0 && (
<div className="second-category-item-box">
<div
style={{
height: secondCategoryItem.isOpen ? 'auto' : 43,
}}
className="second-category-item-list"
id={`id_${secondCategoryIndex}`}>
{secondCategoryItem.children.map(
(thirdCategoryItem, thirdCategoryIndex) => {
return (
<div
className={`third-category-item ${thirdCategoryItem.active
? 'third-category-item-active'
: ''
}`}
key={`third_category_${thirdCategoryItem.id}`}
onClick={onChangeLabel(
secondCategoryIndex,
thirdCategoryIndex,
(thirdCategoryItem.active || false)
)}>
{thirdCategoryItem.labelName}
</div>
);
}
)}
</div>
{
secondCategoryItem.children.length > 5 ? <div
id={`second_id_${secondCategoryIndex}`}
className="second-category-item-status"
onClick={onChangeOpenStatus(
secondCategoryIndex,
(secondCategoryItem.isOpen || false)
)}>
<div className="second-category-item-type" style={{ fontSize: 12 }}>
{secondCategoryItem.isOpen ? '收起' : '展开'}
</div>
<div className="second-category-item-icon" style={{ fontSize: 12 }}>
{secondCategoryItem.isOpen ? (
<UpOutlined />
) : (
<DownOutlined />
)}
</div>
</div> : null
}
</div>
)}
</div>
);
})}
{secondCategoryList?.length >= categoryShowCount && (
<Divider
onClick={onChangePutAway}
dashed
style={{
marginTop: 10,
fontSize: 13,
cursor: 'pointer'
}}>
{isPutAway ? '展开' : '收起'}
{isPutAway ? (
<CaretDownOutlined style={{ marginLeft: 4 }} />
) : (
<CaretUpOutlined style={{ marginLeft: 4 }} />
)}
</Divider>
)}
</div>
</Spin>
);
};
/**
* 一级分类模块
* @returns
*/
const renderFirstContainer = () => {
return ( return (
<div className="category-box"> <div className='first-category-list'>
<Fragment>{categoryList?.length && renderFirstContainer()}</Fragment> {categoryList.slice(0, 7).map((categoryModuleItem, categoryModuleIndex) => {
<Fragment> return (
{secondCategoryList?.length > 0 && renderSecondContainer()} <div
</Fragment> className={`first-category-item ${
{/* {!this.props.isHideSec && ( categoryModuleItem.id === currentActive.id && 'first-category-item-active'
}`}
key={`first_category_${categoryModuleItem.id}`}
style={{
backgroundColor: `${categoryBackColor[categoryModuleIndex]}`
}}
onClick={onChangeCategory(categoryModuleItem)}
>
<div className='first-category-item-title'>{categoryModuleItem.categoryName}</div>
<div className='first-category-item-count'>{categoryModuleItem.count || 50}道题</div>
</div>
)
})}
{categoryList.length > 7 && (
<div className='first-category-more' onClick={() => setOpenMoreFlag(true)}>
更多
<RightOutlined />
</div>
)}
</div>
)
}
/**
* 点击更多的分类项
* @param {*} id
* @returns
*/
const onChangeCategoryMore = cur => () => {
setOpenMoreFlag(false)
setCurrentActive(cur)
let list = [...categoryList].reduce((pre, item) => {
if (item.id !== cur.id) {
pre.push(item)
} else {
pre.unshift(item)
}
return pre
}, [])
props.onChangeCategoryMore && props.onChangeCategoryMore(cur.id, list)
}
/**
* 更多分类
* @returns
*/
const renderMoreBox = () => {
return (
<div className='first-category-more-list'>
{categoryList.slice(7).map((categoryModuleItem, categoryModuleIndex) => {
return (
<div
className='first-category-item'
key={`first_more_category_${categoryModuleItem.id}`}
style={{
backgroundColor: `${categoryBackColor[categoryModuleIndex]}`
}}
onClick={onChangeCategoryMore(categoryModuleItem)}
>
<div className='first-category-item-title'>{categoryModuleItem.categoryName}</div>
<div className='first-category-item-count'>
{formatTotal(categoryModuleItem.subjectCount)}道题
</div>
</div>
)
})}
</div>
)
}
/**
* 选择标签-支持单选(多选)
* @param {*} categoryId 一级分类id
* @param {*} secondCategoryIndex 二级分类对象index
* @param {*} thirdCategoryIndex 三级标签index
* @param {*} active 三级标签当前的选中状态
* @returns
*/
const onChangeLabel = (secondCategoryIndex, thirdCategoryIndex, active) => () => {
const { isMultipleChoice } = props
const list = _.cloneDeep(secondCategoryList)
if (isMultipleChoice) {
// 三级标签支持多选
_.set(list, [secondCategoryIndex, 'children', thirdCategoryIndex, 'active'], !active)
setSecondCategoryList(list)
} else {
// 三级标签支持单选
if (currentLabelIndex.length) {
_.set(list, [currentLabelIndex[0], 'children', currentLabelIndex[1], 'active'], false)
}
_.set(list, [secondCategoryIndex, 'children', thirdCategoryIndex, 'active'], !active)
setCurrentLabelIndex([secondCategoryIndex, thirdCategoryIndex])
setSecondCategoryList(list)
}
props.onChangeLabel(
_.get(list, [secondCategoryIndex, 'id']),
_.get(list, [secondCategoryIndex, 'children', thirdCategoryIndex, 'id'])
)
}
/**
* 展开/收起
* @param {*} secondCategoryIndex
* @returns
*/
const onChangeOpenStatus = (secondCategoryIndex, isOpen) => () => {
const _list = _.cloneDeep(secondCategoryList)
_.set(_list, [secondCategoryIndex, 'isOpen'], !isOpen)
setSecondCategoryList(_list)
}
/**
* 展开/收起
*/
const onChangePutAway = () => {
setIsPutAway(!isPutAway)
}
/**
* 二级分类模块
* @returns
*/
const renderSecondContainer = () => {
return (
<Spin spinning={loading}>
<div className='second-category-list'>
{secondCategoryList.map((secondCategoryItem, secondCategoryIndex) => {
return (
<div
style={{
display: secondCategoryIndex >= categoryShowCount && isPutAway ? 'none' : 'flex'
}}
className='second-category-item'
key={`second_category_${secondCategoryItem.id}`}
>
<div className='second-category-item-title'>
{secondCategoryItem.categoryName}
</div>
{secondCategoryItem?.children?.length > 0 && (
<div className='second-category-item-box'>
<div
style={{
height: secondCategoryItem.isOpen ? 'auto' : 43
}}
className='second-category-item-list'
id={`id_${secondCategoryIndex}`}
>
{secondCategoryItem.children.map((thirdCategoryItem, thirdCategoryIndex) => {
return (
<div
className={`third-category-item ${
thirdCategoryItem.active ? 'third-category-item-active' : ''
}`}
key={`third_category_${thirdCategoryItem.id}`}
onClick={onChangeLabel(
secondCategoryIndex,
thirdCategoryIndex,
thirdCategoryItem.active || false
)}
>
{thirdCategoryItem.labelName}
</div>
)
})}
</div>
{secondCategoryItem.children.length > 5 ? (
<div
id={`second_id_${secondCategoryIndex}`}
className='second-category-item-status'
onClick={onChangeOpenStatus(
secondCategoryIndex,
secondCategoryItem.isOpen || false
)}
>
<div className='second-category-item-type' style={{ fontSize: 12 }}>
{secondCategoryItem.isOpen ? '收起' : '展开'}
</div>
<div className='second-category-item-icon' style={{ fontSize: 12 }}>
{secondCategoryItem.isOpen ? <UpOutlined /> : <DownOutlined />}
</div>
</div>
) : null}
</div>
)}
</div>
)
})}
{secondCategoryList?.length >= categoryShowCount && (
<Divider
onClick={onChangePutAway}
dashed
style={{
marginTop: 10,
fontSize: 13,
cursor: 'pointer'
}}
>
{isPutAway ? '展开' : '收起'}
{isPutAway ? (
<CaretDownOutlined style={{ marginLeft: 4 }} />
) : (
<CaretUpOutlined style={{ marginLeft: 4 }} />
)}
</Divider>
)}
</div>
</Spin>
)
}
return (
<div className='category-box'>
<Fragment>{categoryList?.length && renderFirstContainer()}</Fragment>
<Fragment>{secondCategoryList?.length > 0 && renderSecondContainer()}</Fragment>
{/* {!this.props.isHideSec && (
<Fragment> <Fragment>
{secondCategoryList?.length > 0 && this.renderSecondContainer()} {secondCategoryList?.length > 0 && this.renderSecondContainer()}
</Fragment> </Fragment>
)} */} )} */}
<Modal <Modal
open={openMoreFlag} open={openMoreFlag}
footer={null} footer={null}
closable={true} closable={true}
centered centered
onCancel={() => setOpenMoreFlag(false)}> onCancel={() => setOpenMoreFlag(false)}
{renderMoreBox()} >
</Modal> {renderMoreBox()}
</div> </Modal>
) </div>
)
} }
export default CategoryList export default CategoryList

View File

@@ -3,86 +3,60 @@ const host = import.meta.env.VITE_IMG_HOST
* 难度筛选 * 难度筛选
*/ */
export const filterDifficulty = [ export const filterDifficulty = [
{ {
id: 0, id: 0,
title: '全部', title: '全部'
}, },
{ {
id: 1, id: 1,
title: '初级', title: '初级'
}, },
{ {
id: 2, id: 2,
title: '中级', title: '中级'
}, },
{ {
id: 3, id: 3,
title: '高级', title: '高级'
}, },
{ {
id: 4, id: 4,
title: '资深', title: '资深'
}, },
{ {
id: 5, id: 5,
title: '专家', title: '专家'
}, }
]; ]
/** /**
* 难度等级 * 难度等级
*/ */
export const gradeObject = { export const gradeObject = {
1: { 1: {
color: 'rgba(60, 110, 238, 0.5)', color: 'rgba(60, 110, 238, 0.5)',
title: '初级', title: '初级'
}, },
2: { 2: {
color: 'rgba(60, 110, 238, 0.6)', color: 'rgba(60, 110, 238, 0.6)',
title: '中级', title: '中级'
}, },
3: { 3: {
color: 'rgba(60, 110, 238, 0.7)', color: 'rgba(60, 110, 238, 0.7)',
title: '高级', title: '高级'
}, },
4: { 4: {
color: 'rgba(60, 110, 238, 0.8)', color: 'rgba(60, 110, 238, 0.8)',
title: '资深', title: '资深'
}, },
5: { 5: {
color: 'rgba(60, 110, 238, 0.9)', color: 'rgba(60, 110, 238, 0.9)',
title: '专家', title: '专家'
}, }
}; }
export const imgObject = { export const imgObject = {
clickImg: clickImg:
host + '/jichi/icon/%E7%83%AD%E9%97%A8.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20231102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20231102T153146Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=e6b8cdb3231b1c3d7114212cb9278ecc17cf6d4ec0f759ea0200e04156d4c8b7', host +
ranking1Img: '/jichi/icon/%E7%83%AD%E9%97%A8.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20231102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20231102T153146Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=e6b8cdb3231b1c3d7114212cb9278ecc17cf6d4ec0f759ea0200e04156d4c8b7'
'https://img12.360buyimg.com/imagetools/jfs/t1/110906/3/22471/3750/6214a3bfE392596cf/122c9e4b30948682.png', }
ranking2Img:
'https://img13.360buyimg.com/imagetools/jfs/t1/211695/8/12987/4360/6214a3bfEd4679fde/4f3c55783bb9119c.png',
ranking3Img:
'https://img10.360buyimg.com/imagetools/jfs/t1/175261/19/28428/4566/6214a3bfE476e1b0f/ea59084c55001c06.png',
rankingImg:
'https://img11.360buyimg.com/imagetools/jfs/t1/167264/35/27633/603/6214a3bfEf8feff1d/8d833235e6bc468d.png',
timeline:
'https://img13.360buyimg.com/imagetools/jfs/t1/210387/35/7564/555/617f4fbbE0cb305c1/728913d21e650794.png',
backAllImg:
'https://img11.360buyimg.com/imagetools/jfs/t1/206213/24/13307/2603/617f4fc4E676d448d/622d5287fbf5a919.png',
dataImg:
'https://img12.360buyimg.com/imagetools/jfs/t1/207558/34/7606/3672/617f4fc4E1ca685fc/3953a92a6072fba4.png',
javaImg: 'https://img14.360buyimg.com/imagetools/jfs/t1/213752/24/2703/4803/617f4fc4E037da291/5f8050641d4d73d2.png',
npmImg: 'https://img11.360buyimg.com/imagetools/jfs/t1/200551/24/15367/3145/617f4fc4Ea153dc2e/b4bbf2de8807f42d.png',
parallelComputingImg:
'https://img14.360buyimg.com/imagetools/jfs/t1/207198/23/7638/3037/617f4fc4E0e20ab9d/40197a6c79c5a33f.png',
springbootImg:
'https://img13.360buyimg.com/imagetools/jfs/t1/171775/10/24915/4127/617f4fc4Eeb3d356e/cfbfe8d7c3155047.png',
sqlImg: 'https://img13.360buyimg.com/imagetools/jfs/t1/208027/11/7347/3074/617f4fc4Ef11e9495/1093903301db1d1d.png',
systemDesignImg:
'https://img12.360buyimg.com/imagetools/jfs/t1/206967/24/7622/3629/617f4fc4E60a188b3/cb659847c5d4232a.png',
algorithmImg:
'https://img14.360buyimg.com/imagetools/jfs/t1/215758/34/2633/4128/617f4fc4E5dcdab66/727be155858a06a5.png',
defaultImg:
'https://img13.360buyimg.com/imagetools/jfs/t1/155957/24/22934/2028/617a147cE8bcbb57a/7a4885e4ae99a895.png',
};

BIN
src/imgs/ranking1Img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
src/imgs/ranking2Img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
src/imgs/ranking3Img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/imgs/rankingImg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

View File

@@ -1,66 +1,71 @@
import axios from 'axios'; import { message } from 'antd'
import { message, Modal } from 'antd'; import axios from 'axios'
import { useLocation, useNavigate } from 'react-router-dom';
export const baseHttp = () => { export const baseHttp = () => {
const http = axios.create({ const http = axios.create({
baseURL: "/subject", baseURL: '/subject',
timeout: 5 * 60 * 1000, // request timeout timeout: 5 * 60 * 1000, // request timeout
withCredentials: true, // send cookies when cross-domain requests withCredentials: true, // send cookies when cross-domain requests
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8'
}, }
}); })
return http; return http
}; }
export default function request(config, url) { export default function request(config, url) {
// const navigate = useNavigate() console.log(config, url, 'config url')
// const navigate = useNavigate()
const userInfoStorage = localStorage.getItem('userInfo')
const userInfo = userInfoStorage ? JSON.parse(userInfoStorage) : {}
const baseURL = url || '/subject/subject'
// 1.创建axios的实例
const instance = axios.create({
baseURL,
timeout: 5 * 60 * 1000, // request timeout
withCredentials: true, // send cookies when cross-domain requests
headers: {
'Content-Type': 'application/json; charset=utf-8',
[userInfo.tokenName]: 'jichi ' + userInfo.tokenValue
}
})
const baseURL = url || '/subject'; // 2.axios的拦截器
// 1.创建axios的实例 // 2.1.请求拦截的作用
const instance = axios.create({ instance.interceptors.request.use(
baseURL, config => {
timeout: 5 * 60 * 1000, // request timeout return config
withCredentials: true, // send cookies when cross-domain requests },
headers: { err => {
'Content-Type': 'application/json; charset=utf-8', console.log(err)
}, }
}); )
// 2.axios的拦截 // 2.2.响应拦截
// 2.1.请求拦截的作用 instance.interceptors.response.use(
instance.interceptors.request.use( res => {
(config) => { let { code } = res.data
return config; if (code === 500) {
}, message.error(res.data.message)
(err) => { }
console.log(err); if (code === 401) {
} window.location.replace('/login')
); }
return res.data
},
err => {
let { status } = err?.response ?? {}
if (status === 401 || !status) {
message.info('页面异常')
window.location.replace('/login')
} else if (status === 500 || status === 503) {
message.error('服务器错误')
}
return Promise.reject(err)
}
)
// 2.2.响应拦截 // 3.发送真正的网络请求
instance.interceptors.response.use( return instance(config)
(res) => {
let { code } = res.data;
if (code === 500) {
message.error(res.data.message);
}
return res.data;
},
(err) => {
let { status } = err?.response ?? {};
if (status === 401 || !status) {
message.info('页面异常')
window.location.replace('/login')
} else if (status === 500 || status === 503) {
message.error('服务器错误');
}
return Promise.reject(err);
}
);
// 3.发送真正的网络请求
return instance(config);
} }

View File

@@ -1,35 +1,39 @@
.login-box{ .login-box {
width: 100%;
height: calc(100% - 100px);
background: url(../../imgs/login_bg.jpg) no-repeat 50%;
background-size: cover;
min-height: 600px;
position: relative;
.login-container-inner {
background: transparent;
position: absolute;
top: 50%;
left: 50%;
height: 534px;
transform: translate(-50%, -50%);
clear: both;
max-width: 1520px;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%; width: 100%;
height: calc(100% - 100px); .notes {
background: url(../../imgs/login_bg.jpg) no-repeat 50%; color: white;
background-size: cover; max-width: 400px;
min-height: 600px; line-height: 30px;
position: relative;
.login-container-inner{
background: transparent;
position: absolute;
top: 50%;
left: 50%;
height: 534px;
transform: translate(-50%,-50%);
clear: both;
max-width: 1520px;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.notes{
color: white;
max-width: 400px;
line-height: 30px;
}
.qrcode-box{
width: 400px;
height: 500px;
background: white;
border-radius: 10px;
text-align: center;
padding: 20px;
}
} }
} .qrcode-box {
width: 400px;
height: 500px;
background: white;
border-radius: 10px;
text-align: center;
padding: 20px;
.qrcode-desc {
padding: 20px 0;
line-height: 30px;
}
}
}
}

View File

@@ -1,45 +1,70 @@
import { useState } from 'react'
import { Input, Button, Space } from 'antd'
import LoginQrcode from '@imgs/login_qrcode.jpg' import LoginQrcode from '@imgs/login_qrcode.jpg'
import req from '@utils/request'; import req from '@utils/request'
import { Button, Input, Space, message } from 'antd'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import './index.less' import './index.less'
const loginApiName = '/user/doLogin' const loginApiName = '/user/doLogin'
const Login = () => { const Login = () => {
const [validCode, setValidCode] = useState('')
const navigate = useNavigate()
const [validCode, setValidCode] = useState('') const changeCode = e => {
const changeCode = e => { setValidCode(e.target.value)
setValidCode(e.target.value) }
}
const doLogin = () => { const doLogin = () => {
if (!validCode) return console.log(validCode)
req({ if (!validCode) return
method: 'get', req(
url: loginApiName, {
data: { validCode } method: 'get',
}) url: loginApiName,
} params: { validCode }
},
'/auth'
).then(res => {
if (res.success && res.data) {
message.success('登录成功')
localStorage.setItem('userInfo', JSON.stringify(res.data))
setTimeout(() => {
navigate('/question-bank')
}, 1000)
}
})
}
return (<div className="login-box"> return (
<div className='login-container-inner'> <div className='login-box'>
<div className='notes'>LeNet-5 <div className='login-container-inner'>
1998 11 LeNet-5 ·Gradient-Based Learning Applied to Document Recognition</div> <div className='notes'>
<div className='qrcode-box'> LeNet-5 1998 11 LeNet-5
<div className='qrcode-img'> ·Gradient-Based Learning Applied to Document
<img src={LoginQrcode} alt="" /> Recognition
</div>
<div className='qrcode-form'>
<Space>
<Input maxLength={3} placeholder='验证码' onChange={changeCode} value={validCode} />
<Button type='primary' ghost onClick={doLogin}></Button>
</Space>
</div>
</div>
</div> </div>
</div>) <div className='qrcode-box'>
<div className='qrcode-desc'>
<p></p>
<p></p>
</div>
<div className='qrcode-img'>
<img src={LoginQrcode} alt='' />
</div>
<div className='qrcode-form'>
<Space>
<Input maxLength={3} placeholder='验证码' onChange={changeCode} value={validCode} />
<Button type='primary' ghost onClick={doLogin}>
</Button>
</Space>
</div>
</div>
</div>
</div>
)
} }
export default Login export default Login

View File

@@ -1,20 +1,23 @@
import React from 'react' import ClickImg from '@/imgs/clickImg.png'
import { Popover, Spin } from 'antd' import Ranking1Img from '@/imgs/ranking1Img.png'
import Ranking2Img from '@/imgs/ranking2Img.png'
import Ranking3Img from '@/imgs/ranking3Img.png'
import RankingImg from '@/imgs/rankingImg.png'
import { debounce } from '@utils' import { debounce } from '@utils'
import { RankingTypeText, RankingTypeBtnText } from '../../constant' import { Popover, Spin, message } from 'antd'
import React from 'react'
import { RankingTypeBtnText, RankingTypeText } from '../../constant'
import './index.less' import './index.less'
import { message } from 'antd'
import { imgObject } from '@constants'
const rankingBackImg = { const rankingBackImg = {
0: imgObject.ranking1Img, 0: Ranking1Img,
1: imgObject.ranking2Img, 1: Ranking2Img,
2: imgObject.ranking3Img, 2: Ranking3Img
} }
export default function RankingBox(props) { export default function RankingBox(props) {
const { isLoading = false, currentActive, rankingType, contributionList } = props const { isLoading = false, currentActive, rankingType, contributionList } = props
const onChangeRanking = (index) => const onChangeRanking = index =>
debounce(() => { debounce(() => {
props.onHandleRanking && props.onHandleRanking(index) props.onHandleRanking && props.onHandleRanking(index)
}) })
@@ -36,17 +39,19 @@ export default function RankingBox(props) {
let rankingList = contributionList || [] let rankingList = contributionList || []
return ( return (
<div className="ranking-list-box"> <div className='ranking-list-box'>
<div className="ranking-list-header"> <div className='ranking-list-header'>
<div className="ranking-list-title">{RankingTypeText[rankingType]}</div> <div className='ranking-list-title'>{RankingTypeText[rankingType]}</div>
<div className="ranking-list-btns"> <div className='ranking-list-btns'>
{tabList.length > 0 && {tabList.length > 0 &&
tabList.map((item, index) => { tabList.map((item, index) => {
return ( return (
<div <div
key={`${rankingType}_${item.key}`} key={`${rankingType}_${item.key}`}
onClick={onChangeRanking(index)} onClick={onChangeRanking(index)}
className={`ranking-list-btn ${currentActive === index ? 'ranking-list-btn-active' : ''}`} className={`ranking-list-btn ${
currentActive === index ? 'ranking-list-btn-active' : ''
}`}
> >
{item.tab} {item.tab}
</div> </div>
@@ -55,56 +60,52 @@ export default function RankingBox(props) {
</div> </div>
</div> </div>
<Spin spinning={isLoading}> <Spin spinning={isLoading}>
<div className="ranking-list"> <div className='ranking-list'>
{rankingList?.length > 0 && {rankingList?.length > 0 &&
rankingList.map((item, index) => { rankingList.map((item, index) => {
return ( return (
<div className="ranking-item" key={item.id}> <div className='ranking-item' key={item.id}>
<div className="ranking-left"> <div className='ranking-left'>
<div <div
className="ranking-icon" className='ranking-icon'
style={{ style={{
backgroundImage: `url(${index <= 2 ? rankingBackImg[index] : imgObject.rankingImg})`, backgroundImage: `url(${index <= 2 ? rankingBackImg[index] : RankingImg})`
}} }}
> >
{index > 2 && index + 1} {index > 2 && index + 1}
</div> </div>
<div className="ranking-head-img"> <div className='ranking-head-img'>
<img src={item.headImg} className="ranking-head-icon" /> <img src={item.headImg} className='ranking-head-icon' />
</div> </div>
<Popover <Popover
title={ title={<div>{item.name}</div>}
<div>
{item.name}
</div>
}
content={ content={
<div className="tooltip-info"> <div className='tooltip-info'>
<div>{item.name}</div> <div>{item.name}</div>
{/* <div>{item.organizationFullName}</div> */} {/* <div>{item.organizationFullName}</div> */}
</div> </div>
} }
> >
<div className="ranking-info"> <div className='ranking-info'>
<div className="ranking-name">{item.name}</div> <div className='ranking-name'>{item.name}</div>
{/* <div className="ranking-department">{item.organizationName}</div> */} {/* <div className="ranking-department">{item.organizationName}</div> */}
</div> </div>
</Popover> </Popover>
</div> </div>
<div className="ranking-right">🔥 {item.count}</div> <div className='ranking-right'>🔥 {item.count}</div>
</div> </div>
) )
})} })}
</div> </div>
</Spin> </Spin>
<div className="ranking-btn-go" onClick={onJump}> <div className='ranking-btn-go' onClick={onJump}>
<div <div
className="ranking-btn-go-icon" className='ranking-btn-go-icon'
style={{ style={{
backgroundImage: `url(${imgObject.clickImg})`, backgroundImage: `url(${ClickImg})`
}} }}
></div> ></div>
<div className="ranking-btn-text">{RankingTypeBtnText[rankingType]}</div> <div className='ranking-btn-text'>{RankingTypeBtnText[rankingType]}</div>
</div> </div>
</div> </div>
) )

View File

@@ -1,10 +1,14 @@
import Head from '@/imgs/head.jpg' import Head from '@/imgs/head.jpg'
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons' import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'
import { Button, Card, Col, Form, Input, Radio, Row } from 'antd' import req from '@utils/request'
import { Button, Card, Col, Form, Input, Radio, Row, message } from 'antd'
import { memo, useState } from 'react' import { memo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import './index.less' import './index.less'
const { TextArea } = Input const { TextArea } = Input
const apiName = '/user/update'
const layout = { const layout = {
labelCol: { span: 4 }, labelCol: { span: 4 },
@@ -15,9 +19,43 @@ const UserInfo = () => {
const [form] = Form.useForm() const [form] = Form.useForm()
const [editFlag, setEditFlag] = useState(false) const [editFlag, setEditFlag] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const onFinish = values => { const onFinish = () => {
console.log(values) setLoading(true)
const userInfoStorage = localStorage.getItem('userInfo')
const { loginId = '' } = userInfoStorage ? JSON.parse(userInfoStorage) : {}
const values = form.getFieldsValue()
if (!Object.values(values).filter(Boolean).length) {
setLoading(false)
return
}
const params = {
userName: loginId,
...values
}
req(
{
method: 'post',
url: apiName,
data: { ...params }
},
'/auth'
)
.then(res => {
if (res.success) {
message.success('更新成功')
setTimeout(() => {
navigate('/question-bank')
}, 1000)
}
setLoading(false)
setEditFlag(false)
})
.catch(() => {
message.error('更新失败')
setLoading(false)
})
} }
const uploadButton = ( const uploadButton = (
@@ -50,10 +88,7 @@ const UserInfo = () => {
</Col> </Col>
<Col span={16}> <Col span={16}>
{editFlag ? ( {editFlag ? (
<Form.Item <Form.Item label='性&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;别' name='sex'>
label='性&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;别'
name='gender'
>
<Radio.Group> <Radio.Group>
<Radio value={1}></Radio> <Radio value={1}></Radio>
<Radio value={2}></Radio> <Radio value={2}></Radio>
@@ -65,6 +100,17 @@ const UserInfo = () => {
</Form.Item> </Form.Item>
)} )}
</Col> </Col>
<Col span={16}>
{editFlag ? (
<Form.Item label='手机号码' name='phone'>
<Input placeholder='请输入手机号码' />
</Form.Item>
) : (
<Form.Item label='手机号码'>
<></>
</Form.Item>
)}
</Col>
<Col span={16}> <Col span={16}>
{editFlag ? ( {editFlag ? (
<Form.Item <Form.Item
@@ -95,7 +141,12 @@ const UserInfo = () => {
<Form.Item wrapperCol={{ offset: 5 }}> <Form.Item wrapperCol={{ offset: 5 }}>
{editFlag ? ( {editFlag ? (
<> <>
<Button type='primary' style={{ marginRight: '20px' }} onClick={onFinish}> <Button
type='primary'
style={{ marginRight: '20px' }}
onClick={onFinish}
loading={loading}
>
</Button> </Button>
<Button onClick={() => setEditFlag(false)}></Button> <Button onClick={() => setEditFlag(false)}></Button>

View File

@@ -1,10 +1,10 @@
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { defineConfig, loadEnv } from 'vite'
const path = require('path') const path = require('path')
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default ({ mode }) => { export default ({ mode }) => {
const env = loadEnv(mode, process.cwd()); const env = loadEnv(mode, process.cwd())
return defineConfig({ return defineConfig({
resolve: { resolve: {
alias: { alias: {
@@ -14,17 +14,21 @@ export default ({ mode }) => {
'@utils': path.resolve(__dirname, 'src/utils'), '@utils': path.resolve(__dirname, 'src/utils'),
'@components': path.resolve(__dirname, 'src/components'), '@components': path.resolve(__dirname, 'src/components'),
'@imgs': path.resolve(__dirname, 'src/imgs'), '@imgs': path.resolve(__dirname, 'src/imgs'),
'@constants': path.resolve(__dirname, 'src/constants'), '@constants': path.resolve(__dirname, 'src/constants')
} }
}, },
plugins: [react()], plugins: [react()],
server: { server: {
proxy: { proxy: {
"/subject": { '/subject': {
target: env.VITE_API_HOST, target: env.VITE_API_HOST,
changeOrigin: true, changeOrigin: true
}, },
}, '/auth': {
target: env.VITE_API_HOST,
changeOrigin: true
}
}
} }
}) })
} }