feat: 圈子

This commit is contained in:
秋水浮尘
2024-07-19 00:10:20 +08:00
parent b6a5ca7bef
commit 2dd818762e
12 changed files with 464 additions and 71 deletions

View File

@@ -63,4 +63,4 @@
"typescript": "^4.6.3",
"vite": "^4.5.0"
}
}
}

View File

@@ -31,7 +31,7 @@ export const userInfoSlice = createSlice({
}
}
})
// 导出加减的方法
// 导出方法
export const { saveUserInfo } = userInfoSlice.actions
// 默认导出

View File

@@ -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) : {}

View 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;
}
}
}
}
}

View 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 />
&nbsp;
</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 />
&nbsp;
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
</div>
)
}
export default CommentList

View File

@@ -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 {

View File

@@ -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>
)
}

View 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
})
}

View File

@@ -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

View File

@@ -48,6 +48,10 @@ export default ({ mode }) => {
'/practice': {
target: env.VITE_API_HOST,
changeOrigin: true
},
'/circle': {
target: env.VITE_API_HOST,
changeOrigin: true
}
}
}