feat: 增加搜索页

This commit is contained in:
秋水浮尘
2023-11-25 00:19:15 +08:00
parent 2f330b22cb
commit 3745d0521c
14 changed files with 796 additions and 525 deletions

View File

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

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<link rel="icon" href="/src/favicon.png" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"

View File

@@ -1,4 +1,3 @@
.left {
display: flex;
flex: 1;

BIN
src/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -30,6 +30,14 @@ const router = createBrowserRouter([
path: 'user-info',
// element: <UserInfo />
Component: lazy(() => import('@views/user-info'))
},
{
path: 'search-detail',
Component: lazy(() => import('@views/search-details'))
},
{
path: 'search-question',
Component: lazy(() => import('@views/search-question'))
}
]
}

View File

@@ -34,7 +34,7 @@ export default function request(config, url) {
res => {
let { code } = res.data
if (code === 500) {
message.error(res.data.message)
// message.error(res.data.message)
}
if (code === 401) {
window.location.replace('/login')

View File

@@ -83,14 +83,15 @@ const Header = () => {
}
}
const handleJump = value => {
navigate('/search-detail?t=' + value)
}
return (
<div className='head-navigator-box'>
<div className='head-navigator'>
<div className='head-navigator-left'>
<div
className='head-navigator-logo'
onClick={() => (window.location.href = '/question-bank')}
>
<div className='head-navigator-logo'>
<img src={Logo} style={{ height: 50 }} />
</div>
<TopMenu />
@@ -101,7 +102,11 @@ const Header = () => {
<div className='head-navigator-input-box'>
<Search
placeholder='请输入感兴趣的内容~'
onSearch={value => console.log(value)}
onSearch={value => {
if (value) {
handleJump(value)
}
}}
style={{ width: 300, borderRadius: '10px' }}
/>
</div>

View File

@@ -1,86 +1,96 @@
import React, { Fragment, Component } from 'react';
// import { withRouter } from 'react-router-dom';
import req from '@utils/request';
import RankingBox from '../ranking-box';
import { apiName, RankingType } from '../../constant';
import { mockRankingModuleList } from '../../mock';
import req from '@utils/request'
import { message } from 'antd'
import React, { Fragment, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { RankingType, apiName } from '../../constant'
import { mockRankingModuleList } from '../../mock'
import RankingBox from '../ranking-box'
class ContributionList extends Component {
constructor(props) {
super(props);
this.state = {
contributionList: mockRankingModuleList[1].rankingList || [],
contributeType: 0,
isLoading: false,
};
const ContributionList = props => {
const [contributionList, setContributionList] = useState(mockRankingModuleList[1].rankingList)
const [loading, setLoading] = useState(false)
const [contributeType, setContributeType] = useState(0)
const navigate = useNavigate()
/**
* 获得贡献榜
*/
const getContributeList = () => {
req({
method: 'post',
data: {},
url: apiName.getContributeList
})
.then(res => {
if (res.data && res.data.length > 0) {
this.setState({
contributionList: res.data,
isLoading: false
})
} else {
this.setState({
contributionList: [],
isLoading: false
})
}
})
.catch(err => console.log(err))
}
/**
* 切换排行榜
* @param {*} index
* @returns
*/
const onChangeRanking = index => {
setContributeType(index)
}
/**
* 去录题
*/
const onChangeJump = () => {
const userInfoStorage = localStorage.getItem('userInfo')
if (!userInfoStorage) {
return message.info('请登录')
}
const { loginId } = JSON.parse(userInfoStorage)
req(
{
method: 'get',
url: '/permission/getPermission',
params: {
userName: loginId
}
},
'/auth'
).then(res => {
if (res.success && res.data) {
if (res.data.includes('subject:add')) {
navigate('/upload-questions')
} else {
message.info('敬请期待')
}
} else {
message.info('敬请期待')
}
})
}
componentDidMount() {
// this.getContributeList();
}
/**
* 获得贡献榜
*/
getContributeList() {
req({
method: 'post',
data: {},
url: apiName.getContributeList,
})
.then((res) => {
if (res.data && res.data.length > 0) {
this.setState({
contributionList: res.data,
isLoading: false,
});
} else {
this.setState({
contributionList: [],
isLoading: false,
});
}
})
.catch((err) => console.log(err));
}
/**
* 切换排行榜
* @param {*} index
* @returns
*/
onChangeRanking = (index) => {
console.log(index, 'contribute index')
this.setState({
contributeType: index,
});
};
/**
* 去录题
*/
onChangeJump = () => {
this.props.history.push('/upload-questions');
};
render() {
const { contributionList, isLoading, contributeType } = this.state;
return (
<Fragment>
{contributionList?.length > 0 && (
<RankingBox
isLoading={isLoading}
contributionList={contributionList}
currentActive={contributeType}
rankingType={RankingType.contribution}
onHandleRanking={this.onChangeRanking}
onHandleJump={this.onChangeJump}
/>
)}
</Fragment>
);
}
return (
<Fragment>
{contributionList?.length > 0 && (
<RankingBox
isLoading={loading}
contributionList={contributionList}
currentActive={contributeType}
rankingType={RankingType.contribution}
onHandleRanking={onChangeRanking}
onHandleJump={onChangeJump}
/>
)}
</Fragment>
)
}
export default ContributionList;
export default ContributionList

View File

@@ -22,8 +22,12 @@ export default function RankingBox(props) {
props.onHandleRanking && props.onHandleRanking(index)
})
const onJump = debounce(() => {
if (props.onHandleJump) {
props.onHandleJump()
} else {
message.info('敬请期待')
}
// props.onHandleJump && props.onHandleJump()
message.info('敬请期待')
})
const tabList = [
{

View File

@@ -14,7 +14,6 @@ const QuestionBank = () => {
const [difficulty, setDiffculty] = useState('') //困难度(全部)
const [total, setTotal] = useState(0) // 总条数
const [pageIndex, setPageIndex] = useState(0)
const [secondCategoryId, setSecondCategoryId] = useState('')
const [selectedValue, setSelectedValue] = useState({
primaryId: '', // 大类ID

View File

@@ -0,0 +1,205 @@
import { ExclamationCircleOutlined } from '@ant-design/icons'
import { queryParse } from '@utils'
import { Card, Input, Pagination, Skeleton, message } from 'antd'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import './index.less'
const { Search } = Input
const mockList = [
{
id: 100,
subjectName: 'Redis支持哪几种数据类型',
subjectDifficult: 1,
subjectType: 4,
subjectScore: 1,
subjectParse: '解析什么',
subjectAnswer:
'<p><br></p><ol><li>String字符串</li><li>List列表</li><li>Hash字典</li><li>Set集合</li><li>Sorted Set有序集合</li></ol><p><br></p><p><strong>String</strong></p><p><br></p><p>String是简单的 key-value 键值对value 不仅可以是 String也可以是数字。String在redis内部存储默认就是一个字符串被redisObject所引用当遇到incr,decr等操作时会转成数值型进行计算此时redisObject的encoding字段为int。</p><p><br></p><p><strong>List</strong></p><p><br></p><p>Redis列表是简单的字符串列表可以类比到C++中的std::list简单的说就是一个链表或者说是一个队列。可以从头部或尾部向Redis列表添加元素。列表的最大长度为2^32 - 1也即每个列表支持超过40亿个元素。</p><p><br></p><p>Redis list的实现为一个双向链表即可以支持反向查找和遍历更方便操作不过带来了部分额外的内存开销Redis内部的很多实现包括发送缓冲队列等也都是用的这个数据结构。</p><p><br></p><p><strong>Hash</strong></p><p><br></p><p>Redis Hash对应Value内部实际就是一个HashMap实际这里会有2种不同实现这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储而不会采用真正的HashMap结构对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap。</p><p><br></p><p><strong>Set</strong></p><p><br></p><p>set 的内部实现是一个 value永远为null的HashMap实际就是通过计算hash的方式来快速排重的这也是set能提供判断一个成员是否在集合内的原因。</p><p><br></p><p><strong>Sorted Set</strong></p><p><br></p><p>Redis有序集合类似Redis集合不同的是增加了一个功能即集合是有序的。一个有序集合的每个成员带有分数用于进行排序。</p><p><br></p><p>Redis有序集合添加、删除和测试的时间复杂度均为O(1)(固定时间,无论里面包含的元素集合的数量)。列表的最大长度为2^32- 1元素(4294967295超过40亿每个元素的集合)。</p><p><br></p><p>Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序HashMap里放的是成员到score的映射而跳跃表里存放的是所有的成员排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。</p>',
labelName: ['Redis']
},
{
id: 101,
subjectName: 'Redis的高级数据类型有什么',
subjectDifficult: 2,
subjectType: 4,
subjectScore: 1,
subjectParse: '解析什么',
subjectAnswer:
'<p><br></p><p>bitmapbitmap是一种位数据类型常常用于统计大家比较知名的就是布隆过滤器。也可以统计一些大数据量的东西比如每天有多少优惠券被使用。</p><p><br></p><p>hyperloglog用于基数统计基数是数据集去重后元素个数运用了LogLog的算法。{1,3,5,7,5,7,8} &nbsp; 基数集:{1,3,5,7,8} &nbsp;基数:5</p><p><br></p><p>geo应用于地理位置计算主要是经纬度的计算常见的就是附近的人可以以当前人的坐标获取周围附近的成员可以计算经纬度计算地理位置</p>',
labelName: ['Redis']
},
{
id: 102,
subjectName: 'Redis的优点有什么',
subjectDifficult: 1,
subjectType: 4,
subjectScore: 1,
subjectParse: '解析什么',
subjectAnswer:
'<p><br></p><p>(1) 速度快因为数据存在内存中类似于HashMapHashMap的优势就是查找和操作的时间复杂度都是O(1)</p><p><br></p><p>(2) 支持丰富数据类型支持stringlistsetZsethash等</p><p><br></p><p>(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行</p><p><br></p><p>(4) 丰富的特性可用于缓存消息按key设置过期时间过期后将会自动删除</p>',
labelName: ['Redis']
},
{
id: 103,
subjectName: 'Redis相比Memcached有哪些优势',
subjectDifficult: 1,
subjectType: 4,
subjectScore: 1,
subjectParse: '解析什么',
subjectAnswer:
'<p><br></p><p>(1) Memcached所有的值均是简单的字符串redis作为其替代者支持更为丰富的数据类型</p><p><br></p><p>(2) Redis的速度比Memcached快很多</p><p><br></p><p>(3) Redis可以持久化其数据</p>',
labelName: ['Redis']
},
{
id: 106,
subjectName: 'redis过期策略都有哪些',
subjectDifficult: 1,
subjectType: 4,
subjectScore: 1,
subjectParse: '解析什么',
subjectAnswer:
'<p><br></p><p>noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错。</p><p><br></p><p>allkeys-lru当内存不足以容纳新写入数据时在键空间中移除最近最少使用的 key。</p><p><br></p><p>allkeys-random当内存不足以容纳新写入数据时在键空间中随机移除某个 key这个一般没人用吧为啥要随机肯定是把最近最少使用的 key 给干掉啊。</p><p><br></p><p>volatile-lru当内存不足以容纳新写入数据时在设置了过期时间的键空间中移除最近最少使用的 key。</p><p><br></p><p>volatile-random当内存不足以容纳新写入数据时在设置了过期时间的键空间中随机移除某个 key。</p><p><br></p><p>volatile-ttl当内存不足以容纳新写入数据时在设置了过期时间的键空间中有更早过期时间的 key 优先移除。</p>',
labelName: ['Redis']
}
]
const defaultValue = queryParse(location.search).t
const SearchDetails = () => {
const navigate = useNavigate()
const [isShowSkeleton, setIsShowSkeleton] = useState(true)
const [questionList, setQuestionList] = useState(mockList)
const [total, setTotal] = useState(0)
const [pageIndex, setPageIndex] = useState(0)
const [searchValue, setSearchValue] = useState(defaultValue)
const searchSubject = () => {
setIsShowSkeleton(false)
}
const handleJump = id => {
navigate('/brush-question/' + id)
}
const onChangePagination = () => {}
useEffect(() => {
searchSubject()
}, [searchValue])
return (
<div className='search-details-box'>
<div className='search-details-box-search'>
<div>
<Search
placeholder='请输入感兴趣的内容'
onSearch={value => {
if (value) {
// this.state.searchText = value
// this.pageIndex = 1
// this.searchSubject()
} else {
message.info('搜索词不能为空')
}
}}
enterButton
defaultValue={defaultValue}
/>
</div>
</div>
<Skeleton
title={{ width: '100%' }}
paragraph={{ rows: 20, width: new Array(20).fill('100%') }}
active
loading={isShowSkeleton}
>
<div className='search-details-box-content'>
<div className='search-details-box-content-card'>
<Card
style={{ width: '100%' }}
// tabList={this.tabList}
// onTabChange={key => {
// this.onTabChange(key, 'key')
// }}
>
<div className='search-details-box-content-main'>
{questionList?.length > 0 ? (
questionList.map((item, index) => {
if (item.subjectAnswer) {
return (
<div className='search-details-box-content-main-item'>
<div
className='search-details-box-content-main-item-question'
key={`search-details-question_${index}`}
onClick={() => handleJump(item.id)}
dangerouslySetInnerHTML={{
__html: item.subjectName
}}
></div>
<div
className='search-details-box-content-main-item-answer'
key={`search-details-answer_${index}`}
onClick={() => handleJump(item.id)}
dangerouslySetInnerHTML={{
__html: item.subjectAnswer + '...'
}}
></div>
</div>
)
} else {
return (
<div className='search-details-box-content-main-item'>
<div
className='search-details-box-content-main-item-question'
key={`search-details-question_${index}`}
onClick={() => handleJump(item.id)}
dangerouslySetInnerHTML={{
__html: item.subjectName
}}
></div>
<div
className='search-details-box-content-main-item-answer'
key={`search-details-answer_${index}`}
onClick={() => handleJump(item.id)}
dangerouslySetInnerHTML={{
__html: item.subjectAnswer
}}
></div>
</div>
)
}
})
) : (
<div className='search-null'>
<ExclamationCircleOutlined />
<div>很抱歉没有找到与你搜索相关的内容</div>
</div>
)}
</div>
</Card>
</div>
<div className='search-details-box-content-paging'>
{total > 20 && (
<Pagination
style={{
padding: '24px 0',
textAlign: 'center'
}}
showQuickJumper
current={pageIndex}
defaultCurrent={1}
total={total}
defaultPageSize={20}
onChange={onChangePagination}
/>
)}
</div>
</div>
</Skeleton>
</div>
)
}
export default SearchDetails

View File

@@ -0,0 +1,46 @@
.search-details-box {
width: 1439px;
margin: 0 auto;
background-color: #fff;
padding: 20px 50px;
border-radius: 5px;
overflow: auto;
.ant-card-body {
padding: 0 24px;
}
.search-details-box-search {
background-color: #f5f5f5;
padding: 20px 40px;
border-radius: 3px 3px 0 0;
font-size: 14px;
}
.search-details-box-content {
.search-details-box-content-main-item {
margin-top: 10px;
padding: 10px 0;
font-weight: 400;
border-bottom: 1px solid #e5e5e5;
.search-details-box-content-main-item-question {
color: #3c6eee;
cursor: pointer;
font-size: 18px;
margin-bottom: 5px;
}
.search-details-box-content-main-item-answer {
font-size: 13px;
cursor: pointer;
letter-spacing: 0;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
-webkit-line-clamp: 2; /*要显示的行数*/
-webkit-box-orient: vertical;
}
}
.search-null {
display: block;
text-align: center;
margin: 50px 0;
}
}
}

View File

@@ -1,438 +1,422 @@
import React, { Component, Fragment } from 'react';
import { Input, message, Tooltip, Select } from 'antd';
import _ from 'lodash';
import { debounce } from '@utils';
import { optionLetter } from '../../constant';
import KindEditor from '../kind-editor';
import './index.less';
const { TextArea } = Input;
const { Option } = Select;
const defalutLabel = '请使用富文本编辑器输入选项内容';
import { debounce } from '@utils'
import { Input, Select, Tooltip, message } from 'antd'
import _ from 'lodash'
import React, { Component, Fragment } from 'react'
import { optionLetter } from '../../constant'
import KindEditor from '../kind-editor'
import './index.less'
const { TextArea } = Input
const { Option } = Select
const defalutLabel = '请使用富文本编辑器输入选项内容'
// 判断题
const judgeList = [
{
label: '错误',
value: 0,
},
{
label: '正确',
value: 1,
},
];
const optionLetterLength = 7; // ABCD的长度
const showDeleteLength = 3; // 展示删除icon的最短长度
{
label: '错误',
value: 0
},
{
label: '正确',
value: 1
}
]
const optionLetterLength = 7 // ABCD的长度
const showDeleteLength = 3 // 展示删除icon的最短长度
export default class OptionInputBox extends Component {
constructor(props) {
super(props);
this.state = {
optionList: [
{
label: defalutLabel,
value: 1,
},
{
label: defalutLabel,
value: 2,
},
{
label: defalutLabel,
value: 3,
},
{
label: defalutLabel,
value: 4,
},
], // 选项列表
currentActiveList: [], // 当前选中的项
scoreValue: '', // 分数
subjectAnalysis: '', //试题解析
};
constructor(props) {
super(props)
this.state = {
optionList: [
{
label: defalutLabel,
value: 1
},
{
label: defalutLabel,
value: 2
},
{
label: defalutLabel,
value: 3
},
{
label: defalutLabel,
value: 4
}
], // 选项列表
currentActiveList: [], // 当前选中的项
scoreValue: '', // 分数
subjectAnalysis: '' //试题解析
}
}
kindEditor = KindEditor | null;
subjectAnswer = ''; // 选项内容
kindEditor = KindEditor | null
subjectAnswer = '' // 选项内容
/**
* 新增/删除
* @param {*} len
* @param {*} type add-新增 / del-删除
* @returns
*/
onChangeAddOption = (len, type) => () => {
let { optionList, currentActiveList } = this.state;
let list = [];
// 新增
if (type === 'add') {
if (len === optionLetterLength) {
return;
}
optionList.push({ label: defalutLabel, value: optionLetter[len].value });
} else {
// 删除
currentActiveList = [];
optionList.splice(len, 1);
// 重新初始化ABCD对应的id
list = optionList.map((item, index) => {
return {
label: item.label,
value: optionLetter[index].value,
};
});
/**
* 新增/删除
* @param {*} len
* @param {*} type add-新增 / del-删除
* @returns
*/
onChangeAddOption = (len, type) => () => {
let { optionList, currentActiveList } = this.state
let list = []
// 新增
if (type === 'add') {
if (len === optionLetterLength) {
return
}
optionList.push({ label: defalutLabel, value: optionLetter[len].value })
} else {
// 删除
currentActiveList = []
optionList.splice(len, 1)
// 重新初始化ABCD对应的id
list = optionList.map((item, index) => {
return {
label: item.label,
value: optionLetter[index].value
}
this.setState(
{
optionList: type === 'add' ? optionList : list,
currentActiveList,
},
() => {
this.handleChangeOption();
}
);
};
/**
* 确认/取消 编辑框
* @param {*} index
* @param {*} type submit/cancel
* @returns
*/
onChangeOptEditor = (index, type) => () => {
let { optionList } = this.state;
this.kindEditor && this.kindEditor.onClear();
if (type === 'submit') {
_.set(
optionList,
[index, 'label'],
!!this.subjectAnswer ? this.subjectAnswer : defalutLabel
);
}
_.set(optionList, [index, 'isShowEditor'], false);
this.subjectAnswer = '';
this.setState(
{
optionList,
},
() => {
this.handleChangeOption();
}
);
};
/**
* 展开 编辑项
* @param {*} index
* @returns
*/
onChangeShowEditor = (index) =>
debounce(() => {
let { optionList } = this.state;
if (optionList.filter((item) => item.isShowEditor).length > 0) {
return message.info('请先确认正在编辑的选项内容');
}
_.set(optionList, [index, 'isShowEditor'], true);
this.setState(
{
optionList,
},
() => {
this.kindEditor && this.kindEditor.onCashBack();
}
);
});
/**
* 富文本编辑器
* @param {*} e
*/
onChangeEditor = (index) => (e) => {
this.subjectAnswer = e;
};
/**
* 正确选项
* @param {*} value
*/
onChangeSelect = (value) => {
const { isMultiple } = this.props;
let str = value;
if (!isMultiple) {
// 单选,格式化成数组
str = [value];
}
this.setState(
{
currentActiveList: str,
},
() => {
this.handleChangeOption();
}
);
};
/**
* 本题分值
*/
onChangeScore = (e) => {
this.setState(
{
scoreValue: e.target.value.trim(),
},
() => {
this.handleChangeOption();
}
);
};
/**
* 试题解析
* @param {*} e
*/
onChangeSubjectAnalysis = (e) => {
this.setState(
{
subjectAnalysis: e.target.value.trim(),
},
() => {
this.handleChangeOption();
}
);
};
/**
* 清空
*/
handleClearOption = () => {
this.subjectAnswer = ''; // 选项内容
this.setState({
optionList: [
{
label: defalutLabel,
value: 1,
},
{
label: defalutLabel,
value: 2,
},
{
label: defalutLabel,
value: 3,
},
{
label: defalutLabel,
value: 4,
},
], // 选项列表
currentActiveList: [], // 当前选中的项
scoreValue: '', // 分数
subjectAnalysis: '', //试题解析
});
};
/**
* 向父组件传值
*/
handleChangeOption = () => {
let { currentActiveList, scoreValue, subjectAnalysis, optionList } = this.state;
const { isJudge } = this.props;
let activeList = [];
if (!isJudge) {
// 单选/多选
activeList = optionList.map((item) => {
let flag = 0;
if (currentActiveList.includes(item.value)) {
flag = 1;
}
return {
optionType: item.value,
optionContent: item.label,
isCorrect: flag,
};
});
} else {
// 判断
activeList = currentActiveList;
}
console.log('向父组件传值', activeList, scoreValue, subjectAnalysis);
// this.props.handleChangeOption(activeList, scoreValue, subjectAnalysis);
this.props.handleChangeOption(activeList, 1, subjectAnalysis);
};
render() {
const { subjectAnalysis } = this.state;
const { isJudge } = this.props;
return (
<Fragment>
{!isJudge && this.renderOption()}
{this.renderOptionBtn()}
<div className="option-input-container">
<div className="option-input-title">试题解析</div>
<TextArea
placeholder="试题解析(非必填 限500字"
value={subjectAnalysis}
style={{ height: 48, width: '100%' }}
maxLength={500}
autoSize={{ minRows: 3, maxRows: 4 }}
onChange={(e) => this.onChangeSubjectAnalysis(e)}
/>
</div>
</Fragment>
);
})
}
this.setState(
{
optionList: type === 'add' ? optionList : list,
currentActiveList
},
() => {
this.handleChangeOption()
}
)
}
/**
* 选项模块
* @returns
*/
renderOption = () => {
const { optionList } = this.state;
let listLen = optionList.length;
return (
<Fragment>
{optionList.length > 0 &&
optionList.map((item, index) => {
const isShowTip = item.label === defalutLabel;
return (
<div
className="option-input-container"
style={{ flexDirection: 'column' }}
key={`option_input_${index}`}>
<div className="option-input-main">
<div className="option-input-title-option">
{optionLetter[item.value - 1].label}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
<div
className="option-input-item"
key={`option_id_${item.value}`}>
<Tooltip
title="点击可输入该选项内容"
onClick={this.onChangeShowEditor(index)}>
<div
className="option-input-item-header"
style={
isShowTip
? {
color: 'rgba(51,51,51,0.3)',
}
: {
color: 'rgba(51,51,51,0.9)',
}
}
dangerouslySetInnerHTML={{
__html: item.label,
}}></div>
</Tooltip>
<div className="option-input-item-delete">
{listLen > showDeleteLength && (
<Tooltip
title="删除选项"
onClick={this.onChangeAddOption(index, 'del')}>
<img
className="option-input-item-delete-icon"
src="https://img14.360buyimg.com/imagetools/jfs/t1/212738/17/4123/399/618dd36bEd53475f5/38e899e92bbd5d5e.png"
/>
</Tooltip>
)}
</div>
</div>
</div>
{item.isShowEditor && (
<div
key={`option_editor_${index}`}
className="option-input-main"
style={{ marginTop: 19 }}>
<div className="option-input-editor">
<KindEditor
ref={(ref) => {
this.kindEditor = ref;
}}
bodyHeight={145}
borderRadius={12}
onChange={this.onChangeEditor(index)}
cashBackText={isShowTip ? '' : item.label}
/>
<div className="option-input-editor-btns">
<Tooltip title="取消后内容将不会更新到选项框内">
<div
className="option-input-editor-btn"
onClick={this.onChangeOptEditor(
index,
'cancel'
)}>
取消
</div>
</Tooltip>
<Tooltip title="确定后内容将会更新到选项框内">
<div
className="option-input-editor-btn option-input-editor-submit-btn"
onClick={this.onChangeOptEditor(
index,
'submit'
)}>
确定
</div>
</Tooltip>
</div>
</div>
</div>
)}
</div>
);
})}
</Fragment>
);
};
/**
* 确认/取消 编辑框
* @param {*} index
* @param {*} type submit/cancel
* @returns
*/
onChangeOptEditor = (index, type) => () => {
let { optionList } = this.state
this.kindEditor && this.kindEditor.onClear()
if (type === 'submit') {
_.set(optionList, [index, 'label'], !!this.subjectAnswer ? this.subjectAnswer : defalutLabel)
}
_.set(optionList, [index, 'isShowEditor'], false)
this.subjectAnswer = ''
this.setState(
{
optionList
},
() => {
this.handleChangeOption()
}
)
}
/**
* 选项模块-操作按钮
* @returns
*/
renderOptionBtn = () => {
const { optionList, scoreValue, currentActiveList } = this.state;
const { isMultiple, isJudge } = this.props;
let listLen = optionList.length;
return (
<div className="option-input-container">
<div className="option-input-title option-input-title-required">题目操作</div>
<div style={{ display: 'flex', width: '100%' }}>
{!isJudge && (
<div
className="option-input-option-btn"
onClick={this.onChangeAddOption(listLen, 'add')}>
添加选项
</div>
)}
<div className="option-input-option-btn option-input-option-input">
正确选项
<Select
mode={isMultiple && 'multiple'}
defaultActiveFirstOption={false}
value={currentActiveList}
placeholder="请选择"
style={{ minWidth: isMultiple ? '64px' : '68px', marginLeft: 4 }}
onChange={this.onChangeSelect}>
{isJudge
? judgeList.map((item, index) => {
return (
<Option
key={`option_select_${item.value}`}
value={item.value}>
{item.label}
</Option>
);
})
: optionList.map((item, index) => {
return (
<Option
key={`option_select_${item.value}`}
value={item.value}>
{optionLetter[index].label}
</Option>
);
})}
</Select>
/**
* 展开 编辑项
* @param {*} index
* @returns
*/
onChangeShowEditor = index =>
debounce(() => {
let { optionList } = this.state
if (optionList.filter(item => item.isShowEditor).length > 0) {
return message.info('请先确认正在编辑的选项内容')
}
_.set(optionList, [index, 'isShowEditor'], true)
this.setState(
{
optionList
},
() => {
this.kindEditor && this.kindEditor.onCashBack()
}
)
})
/**
* 富文本编辑器
* @param {*} e
*/
onChangeEditor = index => e => {
this.subjectAnswer = e
}
/**
* 正确选项
* @param {*} value
*/
onChangeSelect = value => {
const { isMultiple } = this.props
let str = value
if (!isMultiple) {
// 单选,格式化成数组
str = [value]
}
this.setState(
{
currentActiveList: str
},
() => {
this.handleChangeOption()
}
)
}
/**
* 本题分值
*/
onChangeScore = e => {
this.setState(
{
scoreValue: e.target.value.trim()
},
() => {
this.handleChangeOption()
}
)
}
/**
* 试题解析
* @param {*} e
*/
onChangeSubjectAnalysis = e => {
this.setState(
{
subjectAnalysis: e.target.value.trim()
},
() => {
this.handleChangeOption()
}
)
}
/**
* 清空
*/
handleClearOption = () => {
this.subjectAnswer = '' // 选项内容
this.setState({
optionList: [
{
label: defalutLabel,
value: 1
},
{
label: defalutLabel,
value: 2
},
{
label: defalutLabel,
value: 3
},
{
label: defalutLabel,
value: 4
}
], // 选项列表
currentActiveList: [], // 当前选中的项
scoreValue: '', // 分数
subjectAnalysis: '' //试题解析
})
}
/**
* 向父组件传值
*/
handleChangeOption = () => {
let { currentActiveList, scoreValue, subjectAnalysis, optionList } = this.state
const { isJudge } = this.props
let activeList = []
if (!isJudge) {
// 单选/多选
activeList = optionList.map(item => {
let flag = 0
if (currentActiveList.includes(item.value)) {
flag = 1
}
return {
optionType: item.value,
optionContent: item.label,
isCorrect: flag
}
})
} else {
// 判断
activeList = currentActiveList
}
console.log('向父组件传值', activeList, scoreValue, subjectAnalysis)
// this.props.handleChangeOption(activeList, scoreValue, subjectAnalysis);
this.props.handleChangeOption(activeList, 1, subjectAnalysis)
}
render() {
const { subjectAnalysis } = this.state
const { isJudge } = this.props
return (
<Fragment>
{!isJudge && this.renderOption()}
{this.renderOptionBtn()}
<div className='option-input-container'>
<div className='option-input-title'>试题解析</div>
<TextArea
placeholder='试题解析(非必填 限500字'
value={subjectAnalysis}
style={{ height: 48, width: '100%' }}
maxLength={500}
autoSize={{ minRows: 3, maxRows: 4 }}
onChange={e => this.onChangeSubjectAnalysis(e)}
/>
</div>
</Fragment>
)
}
/**
* 选项模块
* @returns
*/
renderOption = () => {
const { optionList } = this.state
let listLen = optionList.length
return (
<Fragment>
{optionList.length > 0 &&
optionList.map((item, index) => {
const isShowTip = item.label === defalutLabel
return (
<div
className='option-input-container'
style={{ flexDirection: 'column' }}
key={`option_input_${index}`}
>
<div className='option-input-main'>
<div className='option-input-title-option'>
{optionLetter[item.value - 1].label}&nbsp;&nbsp;&nbsp;&nbsp;
</div>
<div className='option-input-item' key={`option_id_${item.value}`}>
<Tooltip title='点击可输入该选项内容' onClick={this.onChangeShowEditor(index)}>
<div
className='option-input-item-header'
style={
isShowTip
? {
color: 'rgba(51,51,51,0.3)'
}
: {
color: 'rgba(51,51,51,0.9)'
}
}
dangerouslySetInnerHTML={{
__html: item.label
}}
></div>
</Tooltip>
<div className='option-input-item-delete'>
{listLen > showDeleteLength && (
<Tooltip title='删除选项' onClick={this.onChangeAddOption(index, 'del')}>
<img className='option-input-item-delete-icon' src='' />
</Tooltip>
)}
</div>
</div>
</div>
{item.isShowEditor && (
<div
key={`option_editor_${index}`}
className='option-input-main'
style={{ marginTop: 19 }}
>
<div className='option-input-editor'>
<KindEditor
ref={ref => {
this.kindEditor = ref
}}
bodyHeight={145}
borderRadius={12}
onChange={this.onChangeEditor(index)}
cashBackText={isShowTip ? '' : item.label}
/>
<div className='option-input-editor-btns'>
<Tooltip title='取消后内容将不会更新到选项框内'>
<div
className='option-input-editor-btn'
onClick={this.onChangeOptEditor(index, 'cancel')}
>
取消
</div>
</Tooltip>
<Tooltip title='确定后内容将会更新到选项框内'>
<div
className='option-input-editor-btn option-input-editor-submit-btn'
onClick={this.onChangeOptEditor(index, 'submit')}
>
确定
</div>
</Tooltip>
</div>
</div>
</div>
)}
</div>
)
})}
</Fragment>
)
}
/**
* 选项模块-操作按钮
* @returns
*/
renderOptionBtn = () => {
const { optionList, scoreValue, currentActiveList } = this.state
const { isMultiple, isJudge } = this.props
let listLen = optionList.length
return (
<div className='option-input-container'>
<div className='option-input-title option-input-title-required'>题目操作</div>
<div style={{ display: 'flex', width: '100%' }}>
{!isJudge && (
<div
className='option-input-option-btn'
onClick={this.onChangeAddOption(listLen, 'add')}
>
添加选项
</div>
);
};
)}
<div className='option-input-option-btn option-input-option-input'>
正确选项
<Select
mode={isMultiple && 'multiple'}
defaultActiveFirstOption={false}
value={currentActiveList}
placeholder='请选择'
style={{ minWidth: isMultiple ? '64px' : '68px', marginLeft: 4 }}
onChange={this.onChangeSelect}
>
{isJudge
? judgeList.map((item, index) => {
return (
<Option key={`option_select_${item.value}`} value={item.value}>
{item.label}
</Option>
)
})
: optionList.map((item, index) => {
return (
<Option key={`option_select_${item.value}`} value={item.value}>
{optionLetter[index].label}
</Option>
)
})}
</Select>
</div>
</div>
</div>
)
}
}

View File

@@ -35,6 +35,7 @@ interface UserInfo {
email?: string
sex?: string | number
introduce?: string
avatar?: string
}
const Sex: Record<string, any> = {
@@ -57,6 +58,7 @@ const UserInfo = () => {
const [editFlag, setEditFlag] = useState(false)
const [loading, setLoading] = useState(false)
const [userInfo, setUserInfo] = useState<UserInfo>({})
const [avatar, setAvatar] = useState()
const getUserInfo = async () => {
req(
@@ -85,7 +87,8 @@ const UserInfo = () => {
const onFinish = () => {
setLoading(true)
const values = form.getFieldsValue()
if (!Object.values(values).filter(Boolean).length) {
// return console.log(values)
if (!Object.values(values).filter(Boolean).length && !avatar) {
setLoading(false)
return
}
@@ -93,6 +96,9 @@ const UserInfo = () => {
userName: loginId,
...values
}
if (avatar) {
params.avatar = avatar
}
req(
{
method: 'post',
@@ -119,8 +125,11 @@ const UserInfo = () => {
})
}
const handleChange = info => {
console.log(info)
const handleChange = ({ file }) => {
// console.log(info)
if (file.status === 'done' && file.response.success && file.response.data) {
setAvatar(file.response.data)
}
}
const uploadButton = (
@@ -150,23 +159,24 @@ const UserInfo = () => {
satoken: 'jichi ' + tokenValue
}}
data={{
bucket: 'jichi',
bucket: 'user',
objectName: 'icon'
}}
// beforeUpload={beforeUpload}
onChange={handleChange}
>
{uploadButton}
{avatar ? (
<img src={avatar} style={{ height: '80px', width: '80px' }} />
) : (
uploadButton
)}
</Upload>
</Form.Item>
) : (
<Form.Item label='用户头像'>
<img className='user-info_header' src={Head} />
<img className='user-info_header' src={userInfo.avatar || Head} />
</Form.Item>
)}
{/* <Form.Item label='用户头像'>
<img className='user-info_header' src={Head} />
</Form.Item> */}
</Col>
<Col span={16}>
{editFlag ? (