feat: 圈子
This commit is contained in:
@@ -63,4 +63,4 @@
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^4.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ export const userInfoSlice = createSlice({
|
||||
}
|
||||
}
|
||||
})
|
||||
// 导出加减的方法
|
||||
// 导出方法
|
||||
export const { saveUserInfo } = userInfoSlice.actions
|
||||
|
||||
// 默认导出
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { message } from 'antd'
|
||||
import axios from 'axios'
|
||||
|
||||
export default function request(config, url) {
|
||||
export default function request(config, url = '') {
|
||||
// const navigate = useNavigate()
|
||||
const userInfoStorage = localStorage.getItem('userInfo')
|
||||
const userInfo = userInfoStorage ? JSON.parse(userInfoStorage) : {}
|
||||
|
0
src/views/chicken-circle/comps/base-item/index.less
Normal file
0
src/views/chicken-circle/comps/base-item/index.less
Normal file
0
src/views/chicken-circle/comps/base-item/index.tsx
Normal file
0
src/views/chicken-circle/comps/base-item/index.tsx
Normal file
60
src/views/chicken-circle/comps/comment-list/index.less
Normal file
60
src/views/chicken-circle/comps/comment-list/index.less
Normal file
@@ -0,0 +1,60 @@
|
||||
.comment-list-box{
|
||||
padding: 20px 0;
|
||||
position: relative;
|
||||
.flex{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.align-top{
|
||||
align-items: flex-start;
|
||||
}
|
||||
.avatar{
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.top-arrow{
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: 25%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: white;
|
||||
border-top: 1px solid rgba(228, 230, 235, 0.5);
|
||||
border-right: 1px solid rgba(228, 230, 235, 0.5);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.comment-number{
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.comment-wrapper{
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
.text-area-outer-box{
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
flex: 1;
|
||||
.comment-bottom{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.comment-list-wrapper{
|
||||
.comment-list-item{
|
||||
margin-top: 30px;
|
||||
.ope-btn-group{
|
||||
gap: 16px;
|
||||
color: gray;
|
||||
.reply-btn{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
src/views/chicken-circle/comps/comment-list/index.tsx
Normal file
114
src/views/chicken-circle/comps/comment-list/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Button, Input } from 'antd'
|
||||
import { useState, useEffect, FC } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { commentSave, getCommentList } from '../../service'
|
||||
import './index.less'
|
||||
import { CommentOutlined, FileImageOutlined, SmileOutlined } from '@ant-design/icons'
|
||||
|
||||
const CommentList: FC<any> = props => {
|
||||
const { userInfo } = useSelector(store => store.userInfo)
|
||||
const { momentId } = props
|
||||
const [replyList, setReplyList] = useState([])
|
||||
const [comment, setComment] = useState('')
|
||||
const getList = async () => {
|
||||
const res = await getCommentList({ id: momentId })
|
||||
if (res.success && res.data) {
|
||||
setReplyList(res.data)
|
||||
} else {
|
||||
setReplyList([])
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [])
|
||||
|
||||
const changeComment = e => {
|
||||
setComment(e.target.value)
|
||||
}
|
||||
|
||||
const saveComment = () => {
|
||||
const params = {
|
||||
momentId,
|
||||
replyType: 2,
|
||||
content: comment,
|
||||
targetId: 12
|
||||
}
|
||||
commentSave(params).then(() => {
|
||||
getList()
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className='comment-list-box'>
|
||||
<div className='top-arrow'></div>
|
||||
<div className='comment-number'>评论 {replyList.length}</div>
|
||||
<div className='comment-wrapper'>
|
||||
<img src={userInfo?.avatar} className='avatar' />
|
||||
<div className='text-area-outer-box'>
|
||||
<div className='text-area-box'>
|
||||
<Input.TextArea
|
||||
onChange={changeComment}
|
||||
placeholder='和平发言'
|
||||
style={{ border: 'none', paddingLeft: 0 }}
|
||||
/>
|
||||
</div>
|
||||
<div className='comment-bottom'>
|
||||
<div className='icon-box flex'>
|
||||
<div style={{ marginRight: 20 }}>
|
||||
<SmileOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<FileImageOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div className='submit-btn-box flex'>
|
||||
<div className='text-num-box'>1/1000</div>
|
||||
<Button onClick={saveComment} type='primary'>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='comment-list-wrapper'>
|
||||
{replyList.map((item: Record<string, any>) => {
|
||||
return (
|
||||
<div key={item.id} className='comment-list-item flex align-top'>
|
||||
<img src={item.avatar} className='avatar' />
|
||||
<div>
|
||||
<div>{item.userName}</div>
|
||||
<div style={{ margin: '10px 0' }}>{item.content}</div>
|
||||
<div className='ope-btn-group flex'>
|
||||
<div>12小时前</div>
|
||||
<div className='reply-btn'>
|
||||
<CommentOutlined />
|
||||
评论
|
||||
</div>
|
||||
</div>
|
||||
{item.children?.length &&
|
||||
item.children.map(child => {
|
||||
return (
|
||||
<div key={child.id} className='comment-list-item flex align-top'>
|
||||
<img src={child.avatar} className='avatar' />
|
||||
<div>
|
||||
<div>{child.userName}</div>
|
||||
<div style={{ margin: '10px 0' }}>{child.content}</div>
|
||||
<div className='ope-btn-group flex'>
|
||||
<div>12小时前</div>
|
||||
<div className='reply-btn'>
|
||||
<CommentOutlined />
|
||||
评论
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CommentList
|
@@ -51,6 +51,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-list{
|
||||
margin-left: 48px;
|
||||
margin-top: 20px;
|
||||
width: 390px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
img{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.card-footer{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border-top: 1px solid rgba(228,230,235,0.5);
|
||||
border-bottom: 1px solid rgba(228,230,235,0.5);
|
||||
margin-top: 20px;
|
||||
.footer-item{
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pop-content {
|
||||
|
@@ -1,22 +1,36 @@
|
||||
import headImg from '@/imgs/head.jpg'
|
||||
import javaImg from '@/imgs/java.jpeg'
|
||||
import jobImg from '@/imgs/job.jpg'
|
||||
import {
|
||||
FileImageOutlined,
|
||||
MessageOutlined,
|
||||
PlusOutlined,
|
||||
ShareAltOutlined,
|
||||
SmileOutlined
|
||||
SmileOutlined,
|
||||
MessageTwoTone
|
||||
} from '@ant-design/icons'
|
||||
import { Avatar, Button, Card, Input, Popover, Tabs } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { Avatar, Button, Card, Input, Popover, Tabs, message, Upload, Image } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { fetchCircleList, saveMoment, getMoments } from './service'
|
||||
import CommentList from './comps/comment-list'
|
||||
import './index.less'
|
||||
|
||||
const { Meta } = Card
|
||||
|
||||
const { TextArea } = Input
|
||||
const Circle = () => {
|
||||
const userInfoStorage = localStorage.getItem('userInfo')
|
||||
const { tokenValue = '' } = userInfoStorage ? JSON.parse(userInfoStorage) : {}
|
||||
|
||||
const [hasFocus, setHasFocus] = useState(false)
|
||||
const [comment, setComment] = useState('')
|
||||
const [circleList, setCircleList] = useState([])
|
||||
const [currentSelectCircle, setCurrentSelectCircle] = useState(null)
|
||||
const [openFlag, setOpenFlag] = useState(false)
|
||||
const [imgList, setImgList] = useState([])
|
||||
const [momentList, setMomentList] = useState([])
|
||||
const [currentReplyCommentId, setCurrentReplyCommentId] = useState(undefined)
|
||||
const [previewList, setPreviewList] = useState({
|
||||
list: [],
|
||||
index: 0
|
||||
})
|
||||
|
||||
const toggleFocus = (flag: boolean) => {
|
||||
setHasFocus(!flag)
|
||||
@@ -26,31 +40,99 @@ const Circle = () => {
|
||||
setComment(e.target.value)
|
||||
}
|
||||
|
||||
const getCircleList = async () => {
|
||||
const res = await fetchCircleList()
|
||||
if (res.success && res.data?.length > 0) {
|
||||
setCircleList(res.data)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getCircleList()
|
||||
getMomentList()
|
||||
}, [])
|
||||
|
||||
const changeCircle = selectItem => {
|
||||
setCurrentSelectCircle(selectItem)
|
||||
setOpenFlag(false)
|
||||
}
|
||||
|
||||
const renderTab = () => {
|
||||
return circleList.map(item => {
|
||||
return {
|
||||
label: item.circleName,
|
||||
key: item.id,
|
||||
children: (
|
||||
<div className='pop-content'>
|
||||
{item.children.map(child => {
|
||||
return (
|
||||
<div
|
||||
className='pop-content-item'
|
||||
key={child.id}
|
||||
onClick={() => changeCircle(child)}
|
||||
>
|
||||
<img src={child.icon} className='item-img' />
|
||||
<span className='item-name'>{child.circleName}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderPopContent = () => {
|
||||
return (
|
||||
<Tabs
|
||||
tabPosition='left'
|
||||
size='small'
|
||||
items={[
|
||||
{
|
||||
label: '推荐圈子',
|
||||
key: '1',
|
||||
children: (
|
||||
<div className='pop-content'>
|
||||
<div className='pop-content-item'>
|
||||
<img src={javaImg} className='item-img' />
|
||||
<span className='item-name'>JAVA圈子</span>
|
||||
</div>
|
||||
<div className='pop-content-item'>
|
||||
<img src={jobImg} className='item-img' />
|
||||
<span className='item-name'>求职圈子</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
return <Tabs tabPosition='left' size='small' items={renderTab()} />
|
||||
}
|
||||
|
||||
const getMomentList = async () => {
|
||||
const res = await getMoments({
|
||||
pageInfo: {
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
})
|
||||
setMomentList(res.data.result)
|
||||
}
|
||||
|
||||
const publishMoment = async () => {
|
||||
const params: any = {
|
||||
circleId: currentSelectCircle?.id,
|
||||
content: comment
|
||||
}
|
||||
if (imgList.length) {
|
||||
params.picUrlList = imgList.map(item => item.response.data)
|
||||
}
|
||||
const res = await saveMoment(params)
|
||||
if (res.success) {
|
||||
getMomentList()
|
||||
return message.success('发布成功')
|
||||
}
|
||||
return message.error('有点繁忙呢,要不再试试~~~')
|
||||
}
|
||||
const uploadButton = (
|
||||
<button style={{ border: 0, background: 'none' }} type='button'>
|
||||
<PlusOutlined />
|
||||
</button>
|
||||
)
|
||||
const handleChange = ({ fileList }) => {
|
||||
setImgList(fileList)
|
||||
}
|
||||
|
||||
const showReply = (item: any) => {
|
||||
if (item.id !== currentReplyCommentId) {
|
||||
setCurrentReplyCommentId(item.id)
|
||||
} else {
|
||||
setCurrentReplyCommentId(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = (picList, index) => {
|
||||
setPreviewList({
|
||||
list: picList,
|
||||
index
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -73,8 +155,33 @@ const Circle = () => {
|
||||
onFocus={() => toggleFocus(false)}
|
||||
onBlur={() => toggleFocus(true)}
|
||||
/>
|
||||
<Popover placement='bottomLeft' trigger='click' content={renderPopContent}>
|
||||
<div className='choose-circle'>选择圈子 {'>'}</div>
|
||||
<Upload
|
||||
name='uploadFile'
|
||||
action='/oss/upload'
|
||||
listType='picture-card'
|
||||
fileList={imgList}
|
||||
withCredentials
|
||||
headers={{
|
||||
satoken: 'jichi ' + tokenValue
|
||||
}}
|
||||
data={{
|
||||
bucket: 'user',
|
||||
objectName: 'icon'
|
||||
}}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{imgList.length >= 8 || imgList.length === 0 ? null : uploadButton}
|
||||
</Upload>
|
||||
<Popover
|
||||
placement='bottomLeft'
|
||||
trigger='click'
|
||||
open={openFlag}
|
||||
onOpenChange={open => setOpenFlag(open)}
|
||||
content={renderPopContent}
|
||||
>
|
||||
<div className='choose-circle'>
|
||||
{currentSelectCircle?.circleName || '选择圈子'} {'>'}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className='publish-options'>
|
||||
@@ -83,37 +190,73 @@ const Circle = () => {
|
||||
<SmileOutlined />
|
||||
<span style={{ marginLeft: '8px' }}>表情</span>
|
||||
</div>
|
||||
<div>
|
||||
<FileImageOutlined />
|
||||
<span style={{ marginLeft: '8px' }}>图片</span>
|
||||
</div>
|
||||
<Upload
|
||||
name='uploadFile'
|
||||
className='avatar-uploader'
|
||||
accept='image/*'
|
||||
showUploadList={false}
|
||||
withCredentials
|
||||
action='/oss/upload'
|
||||
headers={{
|
||||
satoken: 'jichi ' + tokenValue
|
||||
}}
|
||||
data={{
|
||||
bucket: 'user',
|
||||
objectName: 'icon'
|
||||
}}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<div>
|
||||
<FileImageOutlined />
|
||||
<span style={{ marginLeft: '8px' }}>图片</span>
|
||||
</div>
|
||||
</Upload>
|
||||
</div>
|
||||
<div className='right-box'>
|
||||
<Button type='primary' disabled={!comment.length}>
|
||||
<Button type='primary' disabled={!comment.length} onClick={publishMoment}>
|
||||
发布
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
style={{ marginTop: '10px' }}
|
||||
actions={[
|
||||
<div>
|
||||
<ShareAltOutlined />
|
||||
<span style={{ marginLeft: 8 }}>分享</span>
|
||||
</div>,
|
||||
<div>
|
||||
<MessageOutlined />
|
||||
<span style={{ marginLeft: 8 }}>2</span>
|
||||
</div>
|
||||
]}
|
||||
>
|
||||
<Meta
|
||||
avatar={<Avatar src={headImg} />}
|
||||
title='鸡翅小弟'
|
||||
description='每天练习,两年半定有所成。'
|
||||
/>
|
||||
</Card>
|
||||
{momentList.map((item: any) => {
|
||||
return (
|
||||
<Card style={{ marginTop: '10px' }} bodyStyle={{ paddingBottom: 0 }} key={item.id}>
|
||||
<Meta
|
||||
avatar={<Avatar src={item.userAvatar} />}
|
||||
title={item.userName}
|
||||
description={item.content}
|
||||
/>
|
||||
{item.picUrlList?.length && (
|
||||
<Image.PreviewGroup items={previewList.list}>
|
||||
<div className='img-list'>
|
||||
{item.picUrlList.map((t: string) => (
|
||||
<Image key={t} width={110} src={t} />
|
||||
))}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
)}
|
||||
<div className='card-footer'>
|
||||
<a key='share' className='footer-item'>
|
||||
<ShareAltOutlined />
|
||||
<span style={{ marginLeft: 8 }}>分享</span>
|
||||
</a>
|
||||
<a key='comment' className='footer-item' onClick={() => showReply(item)}>
|
||||
{currentReplyCommentId === item.id ? <MessageTwoTone /> : <MessageOutlined />}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
color: item.id === currentReplyCommentId ? '#1e80ff' : ''
|
||||
}}
|
||||
>
|
||||
{item.replyCount}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{currentReplyCommentId === item.id && <CommentList momentId={item.id} />}
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
60
src/views/chicken-circle/service.ts
Normal file
60
src/views/chicken-circle/service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import req from '@utils/request'
|
||||
|
||||
export const RequestUrl = {
|
||||
CircleList: '/circle/share/circle/list',
|
||||
MomentSave: '/circle/share/moment/save',
|
||||
GetMoments: '/circle/share/moment/getMoments',
|
||||
CommentSave: '/circle/share/comment/save',
|
||||
CommentList: '/circle/share/comment/list'
|
||||
}
|
||||
|
||||
const baseService = ({ method = 'get', url = '', params = {} }) => {
|
||||
const reqParam = {
|
||||
method,
|
||||
url
|
||||
}
|
||||
if (method === 'get') {
|
||||
reqParam.params = params
|
||||
}
|
||||
if (method === 'post') {
|
||||
reqParam.data = params
|
||||
}
|
||||
return req(reqParam, '/circle')
|
||||
}
|
||||
|
||||
export const fetchCircleList = () => {
|
||||
return baseService({
|
||||
url: RequestUrl.CircleList
|
||||
})
|
||||
}
|
||||
|
||||
export const saveMoment = params => {
|
||||
return baseService({
|
||||
method: 'post',
|
||||
url: RequestUrl.MomentSave,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const commentSave = params => {
|
||||
return baseService({
|
||||
method: 'post',
|
||||
url: RequestUrl.CommentSave,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getCommentList = params => {
|
||||
return baseService({
|
||||
method: 'post',
|
||||
url: RequestUrl.CommentList,
|
||||
params
|
||||
})
|
||||
}
|
||||
export const getMoments = params => {
|
||||
return baseService({
|
||||
method: 'post',
|
||||
url: RequestUrl.GetMoments,
|
||||
params
|
||||
})
|
||||
}
|
@@ -18,18 +18,6 @@ const layout = {
|
||||
wrapperCol: { span: 10, offset: 1 }
|
||||
}
|
||||
|
||||
const beforeUpload = (file: RcFile) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
if (!isJpgOrPng) {
|
||||
message.error('You can only upload JPG/PNG file!')
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
if (!isLt2M) {
|
||||
message.error('Image must smaller than 2MB!')
|
||||
}
|
||||
return isJpgOrPng && isLt2M
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
nickName?: string
|
||||
phone?: string
|
||||
|
@@ -48,6 +48,10 @@ export default ({ mode }) => {
|
||||
'/practice': {
|
||||
target: env.VITE_API_HOST,
|
||||
changeOrigin: true
|
||||
},
|
||||
'/circle': {
|
||||
target: env.VITE_API_HOST,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user