使用vite

This commit is contained in:
2023-10-13 05:45:12 +08:00
parent 0865ef39f6
commit 2aadae6b85
11 changed files with 409 additions and 122 deletions

View File

@@ -1,327 +0,0 @@
import { List, ListItem, Avatar, Span, Dialog, Button, Input } from './weigets.js'
export default class ClientList {
constructor({ channels = {}, EventListeners = {}, name: username, onexit }) {
this.event = { onexit }
this.channels = channels
this.EventListeners = EventListeners
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const host = window.location.host
this.clientlist = []
this.element = List({
style: {
position: 'fixed',
top: '0',
right: '0',
display: 'flex',
flexDirection: 'wrap',
alignItems: 'center',
listStyle: 'none',
padding: '0 1rem',
}
})
document.body.appendChild(this.element)
// 连接 WebSocket
const linkStart = () => {
const websocket = new WebSocket(`${protocol}://${host}/webrtc/music?name=${username}`)
websocket.onmessage = async event => {
const data = JSON.parse(event.data)
const webrtc_init = async () => {
const webrtc = new RTCPeerConnection({
iceServers: [
{
urls: 'turn:satori.love:3478?transport=udp',
username: 'x-username',
credential: 'x-password'
},
{
urls: [
'stun:stun.1und1.de',
'stun:stun.callwithus.com',
'stun:stun.ekiga.net',
'stun:stun.fwdnet.net',
'stun:stun.fwdnet.net:3478',
'stun:stun.gmx.net',
'stun:stun.iptel.org',
'stun:stun.internetcalls.com',
'stun:stun.minisipserver.com',
'stun:stun.schlund.de',
'stun:stun.sipgate.net',
'stun:stun.sipgate.net:10000',
'stun:stun.softjoys.com',
'stun:stun.softjoys.com:3478',
'stun:stun.voip.aebc.com',
'stun:stun.voipbuster.com',
'stun:stun.voipstunt.com',
'stun:stun.voxgratia.org',
'stun:stun.wirlab.net',
'stun:stun.xten.com',
'stun:stunserver.org',
'stun:stun01.sipphone.com',
'stun:stun.zoiper.com'
]
}
],
iceCandidatePoolSize: 10, // 限制 ICE 候选者的数量
iceTransportPolicy: 'all', // 使用所有可用的候选者
bundlePolicy: 'balanced', // 每種類型的內容建立一個單獨的傳輸
})
webrtc.ondatachannel = ({ channel }) => {
console.debug(data.name, '建立', channel.label, '数据通道')
const client = this.clientlist.find(x => x.id === data.id)
const option = this.channels[channel.label]
channel.onopen = event => {
console.debug('对方打开', channel.label, '数据通道')
if (option && option.onopen) {
option.onopen(event, client)
}
}
channel.onmessage = event => {
//console.log('对方发送', channel.label, '数据消息')
if (option && option.onmessage) {
option.onmessage(event, client)
}
}
channel.onclose = event => {
console.debug('对方关闭', channel.label, '数据通道')
if (option && option.onclose) {
option.onclose(event, client)
}
}
channel.onerror = event => {
console.error(data.name, '通道', channel.label, '发生错误')
if (option && option.onerror) {
option.onerror(event, client)
}
}
}
webrtc.onicecandidate = event => {
if (event.candidate) {
websocket.send(JSON.stringify({
type: 'candidate',
id: data.id,
candidate: event.candidate
}))
}
}
webrtc.oniceconnectionstatechange = async event => {
if (webrtc.iceConnectionState === 'disconnected' || webrtc.iceConnectionState === 'failed') {
console.error(data.name, '需要添加新的 candidate')
// 添加新的 candidate
} else if (webrtc.iceConnectionState === 'connected' || webrtc.iceConnectionState === 'completed') {
console.debug(data.name, 'WebRTC 连接已经建立成功')
}
}
const channels = Object.entries(this.channels).map(([name, callback]) => {
const channel = webrtc.createDataChannel(name, { reliable: true })
return channel
})
return { webrtc, channels }
}
if (data.type === 'list') {
console.debug('取得在线对端列表:', data)
const { webrtc, channels } = await webrtc_init()
console.debug('发送给对方 offer')
const offer = await webrtc.createOffer()
await webrtc.setLocalDescription(offer)
this.clientlist.push({ id: data.id, name: data.name, webrtc, channels })
websocket.send(JSON.stringify({ type: 'offer', id: data.id, offer }))
// 传递正确的指针给元素, 以便其能够调取正确的头像
this.push(this.clientlist.find(client => client.id === data.id))
return
}
if (data.type === 'push') {
console.debug('新上线客户端:', data)
return
}
if (data.type === 'pull') {
console.debug('移除客户端:', data)
return this.exit(data)
}
if (data.type === 'offer') {
console.debug('收到对方 offer', data)
const { webrtc, channels } = await webrtc_init()
this.clientlist.push({ id: data.id, name: data.name, webrtc, channels })
// 传递正确的指针给元素, 以便其能够调取正确的头像
this.push(this.clientlist.find(client => client.id === data.id))
console.debug('发送给对方 answer')
await webrtc.setRemoteDescription(data.offer)
const answer = await webrtc.createAnswer()
await webrtc.setLocalDescription(answer)
websocket.send(JSON.stringify({ type: 'answer', id: data.id, answer }))
return
}
if (data.type === 'answer') {
console.debug('收到对方 answer', data)
const webrtc = this.clientlist.find(client => client.id === data.id).webrtc
await webrtc.setRemoteDescription(data.answer)
return
}
if (data.type === 'candidate') {
console.debug(data.name, '发来 candidate 候选通道')
const webrtc = this.clientlist.find(client => client.id === data.id).webrtc
await webrtc.addIceCandidate(data.candidate)
return
}
console.error('收到未知数据:', data)
}
websocket.onclose = async event => {
console.log('WebSocket 断线重连...')
await new Promise(resolve => setTimeout(resolve, 10000))
// this.websocket = linkStart()
// 调试模式: 直接刷新页面重载
window.location.reload()
}
return websocket
}
this.websocket = linkStart()
// 也插入自己的信息
const avatar = localStorage.getItem('avatar')
this.push({ id: 'self', name: username, avatar }, true)
}
getAvatar(id) { }
setAvatar(user) {
console.info('更新avatar', user)
document.getElementById(user.id).querySelector('img').src = user.avatar
const u = this.clientlist.find(client => client.id === user.id)
u.avatar = user.avatar
console.log(u, user)
//.avatar = user.avatar
}
exit(item) {
const client = this.clientlist.find(client => client.id === item.id)
if (!client) return console.error('目标用户本不存在')
this.clientlist = this.clientlist.filter(client => client.id !== item.id)
this.element.removeChild(document.getElementById(item.id))
this.event.onexit(client)
}
setChannel(name, option) {
this.channels[name] = option
}
// 新上线客户端
push(item, self = false) {
this.element.appendChild(ListItem({
id: item.id,
style: {
margin: '0 8px',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontSize: '12px',
borderRadius: '8px',
},
children: [
Avatar({
src: item.avatar ?? '/favicon.ico',
style: {
width: '32px',
height: '32px',
borderRadius: '50%',
cursor: 'pointer',
},
}),
Span({
textContent: item.name ?? item.id
})
],
onclick: event => document.body.appendChild(Dialog({
children: [
Avatar({
src: item.avatar ?? '/favicon.ico',
style: {
width: '240px',
height: '240px',
borderRadius: '8px',
margin: '0 auto',
display: 'block',
cursor: 'pointer',
},
onclick: event => {
console.log('点击头像', item)
if (!self) return console.log('只能修改自己的头像')
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = async event => {
const file = event.target.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = async event => {
const base64 = event.target.result
localStorage.setItem('avatar', base64)
window.location.reload() // 简单刷新页面
}
}
input.click()
}
}),
Input({
style: {
width: '100px',
border: '2px dotted #bbb',
borderRadius: '50%',
outline: 'none',
padding: '5px 0',
textAlign: 'center',
position: 'absolute',
bottom: '-2px',
background: 'rgba(255, 255, 255, 0.8)',
},
readOnly: !self, // 如果不是自己, 则不可编辑
value: item.name ?? item.id,
type: 'text',
placeholder: '请设置你的昵称',
onchange: event => {
localStorage.setItem('username', event.target.value)
window.location.reload() // 简单刷新页面
}
})
]
}))
}))
}
// 添加回调函数
on(name, callback) {
this.EventListeners[name] = callback
}
// 执行回调函数
_on(name, ...args) {
if (this.EventListeners[name]) {
this.EventListeners[name](...args)
}
}
// 通过指定通道发送数据(单播)
sendto(id, name, data) {
const client = this.clientlist.find(client => client.id === id)
if (!client) {
console.error('客户端不存在:', id)
return
}
if (!client.channels.find(ch => ch.label === name)) {
console.error('通道不存在:', name)
return
}
client.channels.filter(ch => ch.label === name).forEach(async ch => {
// 等待 datachannel 打开(临时解决方案)
while (ch.readyState !== 'open') {
await new Promise(resolve => setTimeout(resolve, 100))
}
ch.send(data)
})
}
// 通过指定通道发送数据(广播)
send(name, data) {
this.clientlist.forEach(client => {
client.channels.filter(ch => ch.label === name).forEach(async ch => {
// 等待 datachannel 打开(临时解决方案)
while (ch.readyState !== 'open') {
await new Promise(resolve => setTimeout(resolve, 100))
}
ch.send(data)
})
})
}
}

View File

@@ -1,108 +0,0 @@
// 使用示例:
// const db = new IndexedDB('myDatabase', 1, 'myStore')
// await db.open()
// await db.add({ id: 1, name: 'John' })
// const data = await db.get(1)
// console.log(data)
// await db.delete(1)
export default class IndexedDB {
constructor(databaseName, databaseVersion, storeName) {
this.databaseName = databaseName
this.databaseVersion = databaseVersion
this.storeName = storeName
this.db = null
}
open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.databaseName, this.databaseVersion)
request.onerror = (event) => {
reject(event.target.error)
}
request.onsuccess = (event) => {
this.db = event.target.result
resolve(this.db)
}
request.onupgradeneeded = (event) => {
const db = event.target.result
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' })
}
}
})
}
add(data) {
console.log('add', data)
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite')
const objectStore = transaction.objectStore(this.storeName)
// 判断是否已经存在
const request = objectStore.get(data.id)
request.onerror = (event) => {
reject(event.target.error)
}
request.onsuccess = (event) => {
if (event.target.result) return resolve(event.target.result)
const request = objectStore.add(data)
request.onerror = (event) => {
reject(event.target.error)
}
request.onsuccess = (event) => {
resolve(event.target.result)
}
}
})
}
get(id) {
console.log('get', id)
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly')
const objectStore = transaction.objectStore(this.storeName)
const request = objectStore.get(id)
request.onerror = (event) => {
reject(event.target.error)
}
request.onsuccess = (event) => {
resolve(event.target.result)
}
})
}
getAll() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly')
const objectStore = transaction.objectStore(this.storeName)
const request = objectStore.getAll()
request.onerror = (event) => {
reject(event.target.error)
}
request.onsuccess = (event) => {
resolve(event.target.result)
}
})
}
delete(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite')
const objectStore = transaction.objectStore(this.storeName)
const request = objectStore.delete(id)
request.onerror = (event) => {
reject(event.target.error)
}
request.onsuccess = (event) => {
resolve(event.target.result)
}
})
}
}

View File

@@ -1,225 +0,0 @@
import { Span, Button, List, ListItem, UploadMusic } from './weigets.js'
export default class MusicList {
constructor({ list = [], EventListeners = {}, onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }) {
this.event = { onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }
this.ul = List({ classList: ['music-list'] })
this.EventListeners = EventListeners
this.list = []
list.forEach(item => this.add(item)) // 列表逐一添加
document.body.appendChild(this.ul) // 元素加入页面
// 添加音乐播放器
this.audio = new Audio()
this.audio.addEventListener('ended', () => {
this.next()
})
//this.audio.addEventListener('timeupdate', () => {
// console.log(this.audio.currentTime)
//})
//this.audio.addEventListener('error', event => {
// console.error('音乐播放器错误:', event)
//})
// 本地添加音乐按钮
document.body.appendChild(UploadMusic({
style: { width: '20rem', height: '5rem', margin: '1rem 2rem' },
onchange: files => {
for (const file of files) {
const id = 'music' + Date.now()
const { name, size, type } = file
const reader = new FileReader()
reader.onload = async event => {
const arrayBuffer = event.target.result
this.add({ id, name, size, type, arrayBuffer }) // 添加到列表(默认并不存储)
this.like({ id, name, size, type, arrayBuffer }) // 本地缓存的必要条件是喜欢
}
reader.readAsArrayBuffer(file)
}
}
}))
// 写入 css 样式到 head
const style = document.createElement('style')
style.innerText = `
ul.music-list {
max-height: 70vh;
overflow-y: auto;
}
ul.music-list > li > span {
cursor: pointer;
}
ul.music-list > li.play > span {
color: #02be08;
}
ul.music-list > li.cache::marker {
color: #02be08;
font-size: 1em;
contentx: '⚡';
}
ul.music-list > li.disable {
color: #999999;
}
ul.music-list > li > button {
margin-left: 10px;
border: none;
border-radius: 1em;
cursor: pointer;
user-select: none;
font-size: .5rem;
padding: 0 .5rem;
color: #555555;
}
ul.music-list > li > button:hover {
background-color: #ccc;
}
`
document.head.appendChild(style)
}
add(item) {
// 如果ID已存在则不添加
if (this.list.find(i => i.id === item.id)) {
return
}
// 将字节转换为可读的单位
const bytesToSize = bytes => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
}
this.list.push(item)
this.ul.appendChild(ListItem({
id: item.id,
classList: item.arrayBuffer ? ['cache'] : [],
children: [
Span({
textContent: `${item.name} - ${bytesToSize(item.size)}`,
onclick: event => {
event.stopPropagation() // !如果使用async则此处不能阻止冒泡传递
const li = event.target.parentElement // ListItem
const ul = li.parentElement // List
const list = Array.from(ul.children) // ListItems
list.forEach(li => li.classList.remove('play'))
if (!this.audio.paused && this.playing === item) {
li.classList.remove('play')
this.stop(item)
} else {
li.classList.add('play')
this.play(item)
}
}
}),
Button({
textContent: item.save ? '移除' : '缓存',
onclick: event => {
event.stopPropagation() // !如果使用async则此处不能阻止冒泡传递
if (item.save) {
event.target.textContent = '缓存'
this.ul.querySelector(`#${item.id}`).classList.remove('cache')
this.unlike(item)
} else {
item.save = true
event.target.textContent = '移除'
this.ul.querySelector(`#${item.id}`).classList.add('cache')
this.like(item)
}
}
})
]
}))
this.event.onadd(item, this.list)
}
async remove(item) {
this.ul.querySelector(`#${item.id}`)?.remove()
if (!this.audio.paused) this.stop() // 停止播放
this.list = this.list.filter(i => i.id !== item.id)
this.event.onremove(item)
}
async load(item) {
await this.event.onload(item)
}
async play(item) {
if (!item.arrayBuffer) {
console.log('加载音乐类型:', item.type)
// 不支持流式加载wav和flac和m4a, 需要全部加载完毕才能播放
if (item.type === 'audio/wav' || item.type === 'audio/flac' || item.type === 'audio/x-m4a') {
await this.load(item)
this.audio.src = URL.createObjectURL(new Blob([item.arrayBuffer], { type: item.type }))
this.audio.play()
} else {
// 边加载边播放
const mediaSource = new MediaSource()
this.audio.src = URL.createObjectURL(mediaSource)
if (!item.arrayBufferChunks) item.arrayBufferChunks = []
mediaSource.addEventListener('sourceopen', async () => {
const sourceBuffer = mediaSource.addSourceBuffer(item.type)
const arrayBufferLoader = async (index = 0) => {
console.log('开始加载====================================')
// 按照数据长度计算出分片应有数量, 如果数量不到且没有停止加载则一直读取
const chunkNumber = Math.ceil(item.size / 1024 / 64) // 64KB每片
console.log({ index, chunkNumber, paused: this.audio.paused })
while (index < chunkNumber && !this.audio.paused) {
const 播放状态 = !this.audio.paused && this.playing === item
const 加载状态 = item.arrayBufferChunks.length < chunkNumber
const 结束时间 = sourceBuffer.buffered.length && sourceBuffer.buffered.end(0)
const 缓冲时间 = 结束时间 - this.audio.currentTime
if (!播放状态 && !加载状态) break // 播放停止且加载完毕则退出
if (this.audio.paused || this.playing !== item) break // 播放停止或已经切歌则退出
if (缓冲时间 > 60) { // 缓冲超过60秒则等待30秒
await new Promise(resolve => setTimeout(resolve, 30000))
continue
}
if (sourceBuffer.updating) { // sourceBuffer正在更新则等待更新结束
await new Promise(resolve => sourceBuffer.addEventListener('updateend', resolve))
continue
}
if (item.arrayBufferChunks.length <= index) { // 分片数量不足则等待
await new Promise(resolve => setTimeout(resolve, 200))
continue
}
console.log('播放器加载分片:', item.name, `${index + 1}/${chunkNumber}`)
const chunk = item.arrayBufferChunks[index] // 顺序取出一个arrayBuffer分片
sourceBuffer.appendBuffer(chunk) // 添加到sourceBuffer
index++
}
console.log('加载完毕====================================')
item.arrayBufferChunks = null // 加载完毕释放分片内存
}
this.event.onload(item)
this.audio.play()
arrayBufferLoader()
})
}
} else {
// 本地缓存直接播放
this.audio.src = URL.createObjectURL(new Blob([item.arrayBuffer], { type: item.type }))
this.audio.play()
}
this.playing = item
this.event.onplay(item)
}
async stop() {
if (this.audio.paused) {
return console.error('暂停播放:音乐播放器不是播放状态!')
}
this.audio.pause()
this.event.onstop(this.playing)
this.playing = null
}
async like(item) {
if (!item.arrayBuffer) {
await this.load(item)
}
this.event.onlike(item, this.list)
}
async unlike(item) {
this.event.onunlike(item, this.list)
}
async ban(item) {
this.event.onban(item)
}
next() { }
prev() { }
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,319 +0,0 @@
export function createElement({ innerText, innerHTML, textContent, readOnly, children = [], dataset, style, classList = [], ...attributes }, tagName = 'div') {
const element = document.createElement(tagName)
for (const key in attributes) {
if (key.startsWith('on')) element[key] = attributes[key] // 如果是事件则直接赋值
else element.setAttribute(key, attributes[key]) // 否则是属性则使用setAttribute
}
if (dataset) Object.assign(element.dataset, dataset)
if (style) Object.assign(element.style, style)
if (classList.length) element.classList.add(...classList)
if (textContent) element.textContent = textContent
if (innerText) element.innerText = innerText
if (innerHTML) element.innerHTML = innerHTML
if (readOnly) element.readOnly = readOnly
if (children) children.forEach(child => element.appendChild(child))
return element
}
export function List(options) {
return createElement(options, 'ul')
}
export function ListItem(options) {
return createElement(options, 'li')
}
export function Span(options) {
return createElement(options, 'span')
}
export function Button(options) {
return createElement(options, 'button')
}
export function Input(options) {
return createElement(options, 'input')
}
export function Avatar(options) {
const element = createElement(options, 'img')
element.onerror = () => element.src = '/favicon.ico'
return element
}
export function UploadMusic(options) {
let dragStats = null
const drop = createElement({
textContent: '点击或拖拽音乐到此处共享您的音乐',
style: {
width: '100%',
color: '#999',
lineHeight: '5rem',
textAlign: 'center',
position: 'absolute',
top: 0,
display: 'none'
}
})
return createElement({
...options,
style: {
width: '100%',
height: '10rem',
backdropFilter: 'blur(5px)',
backgroundColor: '#fcfcfc',
borderRadius: '1em',
border: '1px solid #f1f1f1',
position: 'relative',
userSelect: 'none',
cursor: 'pointer',
...options.style
},
onclick: event => {
// 临时创建一个input触发input的点击事件
const input = Input({
type: 'file',
multiple: true,
accept: 'audio/*',
style: {
display: 'none',
},
onchange: event => {
const files = Array.from(event.target.files)
if (files.length === 0) return
options.onchange(files)
}
})
input.click()
},
onmouseenter: event => {
drop.style.display = 'block'
},
onmouseleave: event => {
drop.style.display = 'none'
},
ondragover: event => {
console.log('dragover')
event.preventDefault()
event.stopPropagation()
if (dragStats) return
event.dataTransfer.dropEffect = 'copy'
drop.style.display = 'block'
},
ondragleave: event => {
event.preventDefault()
event.stopPropagation()
if (dragStats) return
clearTimeout(dragStats)
dragStats = setTimeout(() => {
drop.style.display = 'none'
dragStats = null
}, 1000)
},
ondrop: event => {
console.log('drop')
event.preventDefault()
event.stopPropagation()
const files = Array.from(event.dataTransfer.files)
// 检查必须是音乐文件, 移除其它文件
for (let i = 0; i < files.length; i++) {
if (!files[i].type.startsWith('audio/')) {
files.splice(i, 1)
i--
}
}
if (files.length === 0) return console.log('没有文件')
console.log('files', files)
options.onchange(files)
},
children: [
// 绘制一个云朵上传图标(别放弃...还有我呢!)
createElement({
style: {
width: '82px',
height: '30px',
background: '#e7f4fd',
background: '-webkit-linear-gradient(top,#e7f4fd 5%,#ceedfd 100%)',
borderRadius: '25px',
position: 'relative',
margin: '33px auto 5px'
},
children: [
createElement({
style: {
width: '45px',
height: '45px',
top: '-22px',
right: '12px',
borderRadius: '50px',
borderRadius: '25px',
background: '#e7f4fd',
position: 'absolute',
zIndex: '-1'
}
}),
createElement({
style: {
width: '25px',
height: '25px',
top: '-12px',
left: '12px',
borderRadius: '25px',
background: '#e7f4fd',
position: 'absolute',
zIndex: '-1'
}
}),
Span({
width: '87px',
position: 'absolute',
bottom: '-2px',
background: '#000',
zIndex: '-1',
boxShadow: '0 0 6px 2px rgba(0,0,0,0.1)',
borderRadius: '50%'
}),
createElement({
textContent: '♫',
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '3rem',
fontWeight: 'bold',
position: 'relative',
top: '-1.5rem',
color: '#e7f4fd',
textShadow: '0 1px 1px #fff'
//textShadow: '0 0 5px #000, 0 0 10px #000, 0 0 15px #000, 0 0 20px #000, 0 0 25px #000, 0 0 30px #000, 0 0 35px #000, 0 0 40px #000, 0 0 45px #000, 0 0 50px #000, 0 0 55px #000, 0 0 60px #000, 0 0 65px #000, 0 0 70px #000, 0 0 75px #000'
}
})
]
}),
drop
]
})
}
// 弹出窗口, 高斯模糊背景, 进入离开动画过渡
export function Dialog(options) {
const element = createElement({
tabIndex: 0,
style: {
position: 'fixed',
top: 0,
left: 0,
zIndex: 1000,
width: '100%',
height: '100%',
backdropFilter: 'blur(5px)',
duration: '0.5s',
transition: 'all 0.5s'
},
onclick: async event => {
// 判断必须是点击自身, 而不是子元素
if (event.target !== event.currentTarget) return
await event.target.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 100 }).finished
await event.target.remove()
},
onkeydown: async event => {
if (event.key !== 'Escape') return
await event.target.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 100 }).finished
await event.target.remove()
},
children: [
createElement({
...options,
style: {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#fff',
borderRadius: '150px',
boxShadow: '0 0 1em #ccc',
overflow: 'hidden',
display: 'flex',
justifyContent: 'center',
...options.style,
}
})
]
})
// 显示时自动聚焦
const observer = new MutationObserver(mutationsList => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
element.focus()
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 100 }).finished
return observer.disconnect()
}
}
})
observer.observe(document.body, { childList: true, subtree: true })
return element
}
// 深度代理, 用于监听数据的变化
export function useProxy(data, callback, path = []) {
if (Array.isArray(data)) {
const array = class extends Array {
constructor(args) {
super(args)
}
push(args) {
super.push(args)
console.log('push', args)
this.__notify()
}
pop(...args) {
const result = super.pop(...args)
console.log('pop')
this.__notify()
return result
}
shift(...args) {
const result = super.shift(...args)
this.__notify()
return result
}
unshift(...args) {
super.unshift(...args)
this.__notify()
}
splice(...args) {
super.splice(...args)
this.__notify()
}
deleteProperty(...args) {
console.log('deleteProperty', ...args)
super.deleteProperty(...args)
this.__notify()
}
__notify() {
if (callback) callback()
}
}
//console.log('为数组每项递归创建代理')
data.forEach((item, index) => {
data[index] = useProxy(item, callback, [...path, index])
})
return new array(...data)
}
if (typeof data === 'object' && data !== null) {
//console.log('为对象属性递归创建代理')
Object.keys(data).forEach(key => {
if (typeof data[key] === 'object' && data[key] !== null) {
data[key] = useProxy(data[key], callback, [...path, key])
}
})
return new Proxy(data, {
deleteProperty(target, key) {
console.log('deleteProperty', key)
return delete target[key]
}
})
}
console.log('为其它类型直接返回')
return data
}