351 lines
15 KiB
JavaScript
351 lines
15 KiB
JavaScript
import { get, set, del, update, createStore, values } from 'idb-keyval'
|
|
import { Span, Button, List, ListItem, TextArea, createElement, Input } from './weigets.js'
|
|
|
|
export default class Chat {
|
|
constructor({ name, EventListeners = {}, onsend, onexit }) {
|
|
this.event = { onsend, onexit }
|
|
this.store = createStore(`db-chat-${name}`, `store-chat-${name}`)
|
|
this.ul = List({ classList: ['chat-list'] })
|
|
this.EventListeners = EventListeners
|
|
this.element = createElement({
|
|
style: {
|
|
position: 'fixed', bottom: 0, left: '50%',
|
|
maxWidth: '64rem', width: '100%',
|
|
transform: 'translate(-50%, 0)',
|
|
display: 'block',
|
|
transition: 'all .15s',
|
|
},
|
|
children: [
|
|
this.ul,
|
|
createElement({
|
|
style: {
|
|
display: 'flex',
|
|
margin: '1rem 2rem',
|
|
},
|
|
children: [
|
|
Input({
|
|
type: 'text',
|
|
// 随机显示一句话
|
|
placeholder: [
|
|
'输入聊天内容',
|
|
'ctrl+左键: 左边显示',
|
|
'ctrl+右键: 右边显示',
|
|
'ctrl+下键: 居中显示',
|
|
'ctrl+L: 清屏',
|
|
].sort(() => Math.random() - 0.5)[0],
|
|
style: {
|
|
flex: 1,
|
|
height: '3rem',
|
|
padding: '0 1rem',
|
|
boxSizing: 'border-box',
|
|
boxShadow: '0 0 1rem #eee',
|
|
border: 'none',
|
|
outline: 'none',
|
|
},
|
|
onkeydown: event => {
|
|
event.stopPropagation()
|
|
const text = event.target.value.trim()
|
|
if (text && event.key === 'Enter') {
|
|
this.发送消息(text)
|
|
event.target.value = ''
|
|
}
|
|
if (!text && event.key === 'Enter') {
|
|
event.target.parentNode.parentNode.style.display = 'none'
|
|
}
|
|
if (event.ctrlKey && event.key === 'ArrowLeft') {
|
|
event.target.parentNode.parentNode.style.left = '0'
|
|
event.target.parentNode.parentNode.style.right = '50%'
|
|
}
|
|
if (event.ctrlKey && event.key === 'ArrowRight') {
|
|
event.target.parentNode.parentNode.style.left = '50%'
|
|
event.target.parentNode.parentNode.style.right = '0'
|
|
}
|
|
if (event.ctrlKey && event.key === 'ArrowDown') {
|
|
event.target.parentNode.parentNode.style.left = '50%'
|
|
event.target.parentNode.parentNode.style.right = '50%'
|
|
}
|
|
if (event.ctrlKey && event.key === 'l') {
|
|
this.ul.innerHTML = '' // 清理屏幕
|
|
// 获取当前时间戳
|
|
const timestamp = new Date().getTime()
|
|
// 清理本地存储
|
|
values(this.store).then(list => list.forEach(item => {
|
|
const t = new Date(item.time).getTime()
|
|
if (t < timestamp) del(item.id, this.store)
|
|
}))
|
|
console.log('清屏', `store-chat-${name}`, timestamp)
|
|
localStorage.setItem(`store-chat-${name}`, timestamp)
|
|
}
|
|
}
|
|
}),
|
|
Button({
|
|
textContent: '发送(Enter)',
|
|
onclick: event => {
|
|
const text = event.target.previousSibling.value.trim()
|
|
if (text) {
|
|
this.发送消息(text)
|
|
event.target.previousSibling.value = ''
|
|
}
|
|
},
|
|
style: {
|
|
padding: '.5rem 1rem',
|
|
boxSizing: 'border-box',
|
|
boxShadow: '0 0 1rem #eee',
|
|
borderRadius: '1rem',
|
|
}
|
|
}),
|
|
]
|
|
})
|
|
]
|
|
})
|
|
document.body.appendChild(this.element)
|
|
document.head.appendChild(createElement({
|
|
innerText: `
|
|
ul.chat-list::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
ul.chat-list {
|
|
scrollbar-width: none;
|
|
}
|
|
ul.chat-list {
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
}
|
|
ul.chat-list > li > span {
|
|
cursor: pointer;
|
|
}
|
|
ul.chat-list > li.play > span {
|
|
color: #02be08;
|
|
}
|
|
ul.chat-list > li.cache::marker {
|
|
color: #02be08;
|
|
font-size: 1em;
|
|
contentx: '⚡';
|
|
}
|
|
ul.chat-list > li.disable {
|
|
color: #888;
|
|
}`
|
|
}, 'style'))
|
|
this.从本地载入消息()
|
|
this.挂载全局快捷键()
|
|
}
|
|
async 挂载全局快捷键() {
|
|
document.addEventListener('keydown', event => {
|
|
if (event.key === 'Enter' || event.key === '/') {
|
|
this.element.style.display = 'block'
|
|
this.element.querySelector('input').focus()
|
|
}
|
|
})
|
|
}
|
|
async 从本地载入消息() {
|
|
const data = await values(this.store)
|
|
data.map(item => ({ timestamp: new Date(item.time).getTime(), ...item }))
|
|
.filter(item => !item.ban)
|
|
.sort((a, b) => a.timestamp - b.timestamp).forEach(item => {
|
|
this.添加元素(item)
|
|
})
|
|
}
|
|
async 筛选指定范围的消息({ start, end }) {
|
|
const data = await values(this.store)
|
|
return data.map(item => ({ timestamp: new Date(item.time).getTime(), ...item })).filter(item => {
|
|
const timestamp = new Date(item.time).getTime()
|
|
return timestamp >= start && timestamp <= end && !item.ban
|
|
})
|
|
}
|
|
// 检查本地不存在的id,存储消息
|
|
async 合并消息列表(list) {
|
|
const data = await values(this.store)
|
|
const ids = data.map(item => item.id)
|
|
list.filter(item => !ids.includes(item.id)).forEach(item => {
|
|
this.添加元素(item)
|
|
this.存储消息(item)
|
|
})
|
|
}
|
|
// 收到应答(对方确认消息已被接收)
|
|
answer(data) {
|
|
const { name, text, time, type } = data
|
|
const item = this.add({ name, text, time, type })
|
|
item.classList.add('disable')
|
|
}
|
|
// 添加一条消息
|
|
add({ name, text, time, type }) {
|
|
// 如果和上一条消息是同一人, 且时间间隔小于1小时, 则向上合并
|
|
// && new Date(time).getTime() - new Date(this.last.time).getTime() < 1000 * 60 * 60
|
|
console.log('添加一条消息', this.last, name, text, time, type)
|
|
if (this.last && this.last.name === name) {
|
|
this.last.item.appendChild(Span({ textContent: text }))
|
|
this.last = { name, text, time, type, item: this.last.item }
|
|
return this.last.item
|
|
}
|
|
|
|
const item = ListItem({
|
|
classList: [type],
|
|
children: [
|
|
Span({ textContent: `${name} ${time} ${text}` })
|
|
]
|
|
})
|
|
this.ul.appendChild(item)
|
|
this.ul.scrollTop = this.ul.scrollHeight
|
|
|
|
// 记录到上一条消息
|
|
this.last = { name, text, time, type, item }
|
|
return item
|
|
}
|
|
send(text) {
|
|
if (this.event.onsend) {
|
|
this.event.onsend(text)
|
|
}
|
|
}
|
|
添加元素(data) {
|
|
// 人类可读的时间: 今天,昨天, 空字符串
|
|
function convertTimestampToReadableTime(timestamp) {
|
|
const date = new Date(timestamp);
|
|
const today = new Date();
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(today.getDate() - 1);
|
|
if (date.toDateString() === today.toDateString()) {
|
|
return '今天';
|
|
} else if (date.toDateString() === yesterday.toDateString()) {
|
|
return '昨天';
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
// 如果上一条日期不等于这一条日期,则添加日期(只计算日期,不计算时间)
|
|
if (!this.last || this.last.time.split(' ')[0] !== data.time.split(' ')[0]) {
|
|
this.ul.appendChild(ListItem({
|
|
style: { listStyle: 'none', maxWidth: '24rem', textAlign: 'center' },
|
|
children: [
|
|
Span({
|
|
style: {
|
|
display: 'inline-block',
|
|
padding: '.1rem .5rem',
|
|
marginTop: '2rem',
|
|
boxSizing: 'border-box',
|
|
boxShadow: '0 0 1rem #eee',
|
|
borderRadius: '1rem',
|
|
fontSize: '12px',
|
|
},
|
|
textContent: `${data.time.split(' ')[0]} ${convertTimestampToReadableTime(new Date(data.time).getTime())}`
|
|
})
|
|
]
|
|
}))
|
|
}
|
|
|
|
// 如果和上一条消息是同一人, 且时间间隔小于1小时, 则向上合并
|
|
// && new Date(time).getTime() - new Date(this.last.time).getTime() < 1000 * 60 * 60
|
|
console.log('添加一条消息', this.last, data)
|
|
if (this.last && this.last.name === data.name) {
|
|
this.last.item.querySelector('ul').appendChild(ListItem({ textContent: data.text }))
|
|
this.last = { ...data, item: this.last.item }
|
|
return this.last.item
|
|
}
|
|
// 将时间转换为人类可读的格式: 如果是今天,则显示时间,如果是昨天,则显示昨天,如果是今年,则显示月日,如果是去年,则显示年月日
|
|
const redate = (str) => {
|
|
const date = new Date(str)
|
|
const now = new Date()
|
|
const year = date.getFullYear()
|
|
const month = date.getMonth() + 1
|
|
const day = date.getDate()
|
|
const hour = date.getHours()
|
|
const minute = date.getMinutes()
|
|
const nowYear = now.getFullYear()
|
|
const nowMonth = now.getMonth() + 1
|
|
const nowDay = now.getDate()
|
|
if (year === nowYear && month === nowMonth && day === nowDay) {
|
|
return `${hour}:${minute}`
|
|
}
|
|
if (year === nowYear && month === nowMonth && day === nowDay - 1) {
|
|
return `昨天 ${hour}:${minute}`
|
|
}
|
|
if (year === nowYear) {
|
|
return `${month}月${day}日 ${hour}:${minute}`
|
|
}
|
|
return `${year}年${month}月${day}日 ${hour}:${minute}`
|
|
}
|
|
const scroll = this.ul.scrollHeight === this.ul.clientHeight + this.ul.scrollTop
|
|
this.ul.appendChild(ListItem({
|
|
style: {
|
|
margin: '1rem',
|
|
padding: '.5rem 1rem',
|
|
boxSizing: 'border-box',
|
|
boxShadow: '0 0 1rem #eee',
|
|
maxWidth: '24rem',
|
|
borderRadius: '1rem',
|
|
listStyle: 'none',
|
|
},
|
|
children: [
|
|
createElement({
|
|
style: {
|
|
display: 'flex',
|
|
alignItems: 'end',
|
|
justifyContent: 'space-between',
|
|
gap: '.25rem',
|
|
},
|
|
children: [
|
|
Span({ textContent: `${data.name}` }),
|
|
Span({
|
|
style: {
|
|
color: '#888',
|
|
fontSize: '12px',
|
|
},
|
|
textContent: `${redate(data.time)}`
|
|
}),
|
|
Button({
|
|
style: {
|
|
boxSizing: 'border-box',
|
|
boxShadow: '0 0 1rem #eee',
|
|
borderRadius: '1rem',
|
|
fontSize: '12px',
|
|
color: '#555',
|
|
marginLeft: 'auto',
|
|
whiteSpace: 'nowrap',
|
|
},
|
|
textContent: '移除',
|
|
title: '加入屏蔽列表不再被渲染',
|
|
onclick: event => {
|
|
event.target.parentNode.remove()
|
|
update(data.id, item => ({ ban: true, ...item }), this.store)
|
|
}
|
|
})
|
|
]
|
|
}),
|
|
List({
|
|
style: {
|
|
listStyle: 'disc',
|
|
},
|
|
children: [
|
|
ListItem({ textContent: `${data.text}` }),
|
|
]
|
|
}),
|
|
]
|
|
}))
|
|
if (scroll) {
|
|
console.log('滚动到底部')
|
|
this.ul.scrollTop = this.ul.scrollHeight
|
|
}
|
|
// 记录到上一条消息
|
|
this.last = { ...data, item: this.ul.lastChild }
|
|
}
|
|
async 存储消息(data) {
|
|
// 检查id是否已经存在
|
|
const item = await get(data.id, this.store)
|
|
if (item) return;
|
|
await set(data.id, data, this.store)
|
|
}
|
|
发送消息(text) {
|
|
const name = localStorage.getItem('username')
|
|
const id = window.crypto.randomUUID()
|
|
const time = new Date().toLocaleString()
|
|
const type = 'text'
|
|
const data = { id, name, text, time, type }
|
|
this.添加元素(data)
|
|
this.存储消息(data)
|
|
this.send(data)
|
|
}
|
|
收到消息(data) {
|
|
console.log('收到消息', data)
|
|
this.添加元素(data)
|
|
this.存储消息(data)
|
|
}
|
|
} |