taro3.x: 微聊简单实现
时间显示处理方法:
// 格式化时间段 const toTimeSolt = (h: number) => { let bt = ''; if (0 <= h && h <= 3) bt = '凌晨'; if (4 <= h && h <= 8) bt = '早上'; if (9 <= h && h <= 11) bt = '上午'; if (12 == h) bt = '中午'; if (13 <= h && h <= 17) bt = '下午'; if (18 <= h && h <= 23) bt = '晚上'; return bt; } // 格式星期 const toWeek = (w: number) => { let week = ''; switch (w) { case 0: week = '星期日'; break; case 1: week = '星期一'; break; case 2: week = '星期二'; break; case 3: week = '星期三'; break; case 4: week = '星期四'; break; case 5: week = '星期五'; break; case 6: week = '星期六'; break; } return week; } export const formatChatListTime = (timestamp: string, type: number = 1) => { let oldtime = new Date(Number(timestamp) * 1000); let date = new Date(); let today = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); //今天凌晨 let yestday = new Date(today - 24 * 3600 * 1000).getTime(); // let beforeYestday = new Date(today - 24 * 3600 * 1000 * 2).getTime(); let beforeWeek = new Date(today - 24 * 3600 * 1000 * 7).getTime(); let Y = oldtime.getFullYear(); //年份 let M = oldtime.getMonth() + 1; //月份 let d = oldtime.getDate(); //日 // let h = oldtime.getHours() % 12 == 0 ? 12 : oldtime.getHours() % 12; //12小时 let H = oldtime.getHours(); //24小时 let m = oldtime.getMinutes(); //分 let w = toWeek(oldtime.getUTCDay()); //星期 let timesolt = toTimeSolt(oldtime.getHours()); //时间段 let timeStr = ''; //当天 if (oldtime.getTime() > yestday) { timeStr = H + ':' + m; } //昨天 if (oldtime.getTime() < today && yestday <= oldtime.getTime()) { timeStr = '昨天 ' + (type == 1 ? H + ':' + m : ''); } // 一周内 if (oldtime.getTime() < yestday && beforeWeek <= oldtime.getTime()) { timeStr = w + (type == 1 ? ' ' + H + ':' + m : ''); } // 一周前 if (oldtime.getTime() < beforeWeek) { timeStr = type == 1 ? Y + '年' + M + '月' + d + '日 ' + timesolt + ' ' + H + ':' + m : Y + '/' + M + '/' + d; } // 比当前时间晚 if (oldtime.getTime() > date.getTime()) { timeStr = '手动修改'; } return timeStr; }
主文件index.tsx:
import React, { useEffect, useRef, useState } from 'react'
import Taro, { getCurrentInstance } from '@tarojs/taro'
import { View, Text, ScrollView, Input, Image } from "@tarojs/components"
import classnames from 'classnames'
import api from '@services/api'
import app from '@services/request'
import NavBar from '@components/navbar'
import useNavData from '@hooks/useNavData'
import { PRICE_TYPE } from '@constants/house'
import { formatChatListTime } from '@utils/index'
import { getTotalPage, INIT_PAGE, IPage } from '@utils/page'
import './index.scss'
interface IParam {
currentPage: number
}
const INIT_PARAM = { currentPage: 1 }
const MESSAGE_TYPE = {
text: '1',
image: '2',
image_text: '3'
}
const ChatRoom = () => {
const PAGE_LIMIT: number = 10
const router: any = getCurrentInstance().router
const fromUserId: string = router?.params.fromUserId
const user: any = JSON.parse(router?.params.user) || {}
const toUser: any = JSON.parse(router?.params.toUser) || {}
const { contentHeight } = useNavData()
const [param, setParam] = useState<IParam>(INIT_PARAM)
const [page, setPage] = useState<IPage>(INIT_PAGE)
const [chatData, setChatData] = useState<any[]>([])
const [bottom, setBottom] = useState<number>(0)
const [toView, setToView] = useState<string>('')
const [isPhoto, setIsPhoto] = useState<boolean>(false)
const [inputData, setInputData] = useState<any>({ value: '', send: false })
const ref = useRef<string>('')
useEffect(() => {
fetchChatData()
}, [param])
const fetchChatData = () => {
app.request({
url: app.testApiUrl(api.getChatData),
data: {
page: param.currentPage,
limit: PAGE_LIMIT,
from_user_id: fromUserId,
}
}, { loading: false }).then((result: any) => {
if (param.currentPage === INIT_PARAM.currentPage) {
setChatData(result.data)
} else {
setChatData([...result.data, ...chatData])
}
setToView(`toView_${result.data[result.data.length - 1].id}`)
setPage({
...page,
totalCount: result.pagination.totalCount,
totalPage: getTotalPage(PAGE_LIMIT, result.pagination.totalCount)
})
})
}
const handleScrollToUpper = () => {
if (page.totalPage > param.currentPage) {
setParam({
currentPage: param.currentPage + 1
})
}
}
const handleInputFocus = (e: any) => {
setIsPhoto(false)
setBottom(e.detail.height)
setToView(`toView_${chatData[chatData.length - 1].id}`)
}
const handleInputChange = (e: any) => {
const value = e.detail.value
setInputData({
value,
send: !!value
})
}
const handlePhotoClick = (type: any) => {
Taro.chooseImage({
count: 9,
sourceType: [type],
success: (res: any) => {
app.uploadFile(res).then((result: any) => {
sendMessage(MESSAGE_TYPE.image, result)
})
}
})
}
const sendMessage = (type: string, content: any) => {
app.request({
url: app.apiUrl(api.postChatSend),
method: 'POST',
data: {
to_user_id: toUser.id,
message_type: type,
content
}
}).then(() => {
setIsPhoto(false)
setInputData({ value: '', send: false })
fetchChatData()
})
}
const renderContentByType = (chatItem: any, isMine: boolean = false) => {
const content = {
'1': <Text className={classnames('text', isMine && 'text-primary')}>{chatItem.content}</Text>,
'2': <Image className="image" src={chatItem.content} mode="widthFix" />,
'3': (
<View className="content">
<View className="content-image">
<Image src={chatItem.content.image_path} mode="aspectFit" />
<View className="tag">新房</View>
</View>
<View className="content-title">{chatItem.content.title}</View>
<View className="content-text">{chatItem.content.price}{PRICE_TYPE[chatItem.content.price_type]}</View>
</View>
)
}
return content[chatItem.message_type]
}
const renderTime = (time: string) => {
if (ref.current !== time) {
ref.current = time
return (
<View className="item-tip">
<Text className="text">{formatChatListTime(time)}</Text>
</View>
)
}
}
const renderChatList = () => {
return chatData.map((item: any, index: number) => (
<View key={index} id={`toView_${item.id}`}>
{
item.to_user_id !== toUser.id ?
(
<View className="msg-item">
{renderTime(item.time)}
<View className="item-content">
<View className="photo">
<Image src={toUser.avatar} />
</View>
<View className="message">
{renderContentByType(item)}
</View>
</View>
</View>
) :
(
<View className="msg-item">
{renderTime(item.time)}
<View className="item-content item-content-reverse">
<View className="photo">
<Image src={user.avatar} />
</View>
<View className="message">
{renderContentByType(item, true)}
</View>
</View>
</View>
)
}
</View>
))
}
return (
<View className="chat-room">
<NavBar title={toUser.nickname} back={true}></NavBar>
<View className="chat-room-content">
<ScrollView
scrollY
className="msg-box"
style={{ maxHeight: contentHeight - 52 }}
upperThreshold={40}
onScrollToUpper={handleScrollToUpper}
onClick={() => setIsPhoto(false)}
scrollIntoView={toView}
scrollWithAnimation={false}
>
{renderChatList()}
</ScrollView>
<View className="send-box" style={{ bottom }}>
<View className="send-content">
<Input
adjustPosition={false}
onFocus={handleInputFocus}
onBlur={() => setBottom(0)}
value={inputData.value}
onInput={handleInputChange}
/>
{
inputData.send ?
<View
className="btn btn-primary"
onClick={() => sendMessage(MESSAGE_TYPE.text, inputData.value)}
>
<Text className="text">发送</Text>
</View> :
<View className="icon-btn" onClick={() => setIsPhoto(true)}>
<Text className="iconfont iconadd"></Text>
</View>
}
</View>
{
isPhoto &&
<View className="photo-content">
<View className="photo-item photo-album" onClick={() => handlePhotoClick('album')}>
<View className="iconfont iconphoto"></View>
<View className="text">照片</View>
</View>
<View className="photo-item photograph" onClick={() => handlePhotoClick('camera')}>
<View className="iconfont iconphotograph"></View>
<View className="text">拍照</View>
</View>
</View>
}
</View>
</View>
</View>
)
}
export default ChatRoom
解决问题有:
1. 默认滚动位置在做底部,根据列表id的不同时设置scrollIntoView实现。
2.预览或真机时,自定义导航栏会往上顶,设置adjustPosition={false}当输入键盘出来时,输入框不自动调整位置
样式:
.chat-room { position: relative; width: 100%; height: 100vh; background-color: $bg2-color; &-content { .msg-box { width: 100%; .msg-item { padding: 0 30px 30px; .item-content { display: flex; align-items: center; &.item-content-reverse { flex-direction: row-reverse; } .photo { width: 80px; height: 80px; background-color: $bg-color; border-radius: 50%; overflow: hidden; Image { width: 100%; height: 100%; } } .message { margin: 0 20px; .text { padding: 15px 30px; border-radius: $border-radius-10; background-color: $white; } .text-primary { background-color: $primary-color; color: $white; } .image { max-width: 300px; } .content { width: 400px; padding: 20px; background-color: $white; border-radius: $border-radius-base; .content-image { position: relative; width: 100%; height: 200px; background-color: $bg-color; Image { width: 100%; height: 100%; } .tag { position: absolute; top: 20px; left: 20px; background-color: $white; padding: 4px 12px; font-size: $font-26; border-radius: $border-radius-base; } } .content-title { margin-top: 10px; font-size: $font-30; } .content-text { margin-top: 10px; font-size: $font-26; color: $desc-color; } } } } .item-tip { text-align: center; margin-bottom: 30px; .text { padding: 4px 10px; font-size: $font-26; border-radius: $border-radius-10; background: rgba($color: $black, $alpha: 0.2); color: $white; } } } } .send-box { position: absolute; bottom: 0; width: 100%; font-size: $font-basic; padding: 20px 0; background-color: $bg-color; z-index: 99; box-shadow: 0 -1px 6px 0 rgba(0, 0, 0, 0.08); .send-content { display: flex; align-items: center; justify-content: space-between; padding: 0 30px; Input { flex: 1; height: 60px; line-height: 60px; border: $border; border-radius: $border-radius-10; margin-right: 20px; padding: 0 20px; background-color: $white; } .btn { width: 100px; height: 60px; } .icon-btn { font-size: 40px; color: $text-color; } } .photo-content { display: flex; margin: 30px; .photo-item { text-align: center; margin-right: 30px; font-size: $font-basic; .iconfont { border: $border; border-radius: $border-radius-10; padding: 20px; font-size: 50px; margin-bottom: 8px; color: $text-color; } } } } } }

浙公网安备 33010602011771号