feat: 新版webui

This commit is contained in:
bietiaop
2025-01-24 21:13:44 +08:00
parent afc9c7ed8d
commit 31c0c1f4bc
201 changed files with 18454 additions and 3422 deletions

View File

@@ -0,0 +1,120 @@
import { PlayMode } from '@/const/enum'
import type {
FinalMusic,
Music163ListResponse,
Music163URLResponse
} from '@/types/music'
import { request } from './request'
/**
* 获取网易云音乐歌单
* @param id 歌单id
* @returns 歌单信息
*/
export const get163MusicList = async (id: string) => {
const res = await request.get<Music163ListResponse>(
`https://wavesgame.top/playlist/track/all?id=${id}`
)
if (res?.data?.code !== 200) {
throw new Error('获取歌曲列表失败')
}
return res.data
}
/**
* 获取歌曲地址
* @param ids 歌曲id
* @returns 歌曲地址
*/
export const getSongsURL = async (ids: number[]) => {
const _ids = ids.reduce((prev, cur, index) => {
const groupIndex = Math.floor(index / 10)
if (!prev[groupIndex]) {
prev[groupIndex] = []
}
prev[groupIndex].push(cur)
return prev
}, [] as number[][])
const res = await Promise.all(
_ids.map(async (id) => {
const res = await request.get<Music163URLResponse>(
`https://wavesgame.top/song/url?id=${id.join(',')}`
)
if (res?.data?.code !== 200) {
throw new Error('获取歌曲地址失败')
}
return res.data.data
})
)
const result = res.reduce((prev, cur) => {
return prev.concat(...cur)
}, [])
return result
}
/**
* 获取网易云音乐歌单歌曲
* @param id 歌单id
* @returns 歌曲信息
*/
export const get163MusicListSongs = async (id: string) => {
const listRes = await get163MusicList(id)
const songs = listRes.songs.map((song) => song.id)
const songsRes = await getSongsURL(songs)
const finalMusic: FinalMusic[] = []
for (let i = 0; i < listRes.songs.length; i++) {
const song = listRes.songs[i]
const music = songsRes.find((s) => s.id === song.id)
const songURL = music?.url
if (songURL) {
finalMusic.push({
id: song.id,
url: songURL,
title: song.name,
artist: song.ar.map((p) => p.name).join('/'),
cover: song.al.picUrl
})
}
}
return finalMusic
}
/**
* 获取随机音乐
* @param ids 歌曲id
* @param currentId 当前音乐id
* @returns 随机音乐id
*/
export const getRandomMusic = (ids: number[], currentId: number): number => {
const randomIndex = Math.floor(Math.random() * ids.length)
const randomId = ids[randomIndex]
if (randomId === currentId) {
return getRandomMusic(ids, currentId)
}
return randomId
}
/**
* 获取下一首音乐id
* @param ids 歌曲id
* @param currentId 当前音乐ID
* @param mode 播放模式
*/
export const getNextMusic = (
musics: FinalMusic[],
currentId: number,
mode: PlayMode
): number => {
const ids = musics.map((music) => music.id)
if (mode === PlayMode.Loop) {
const currentIndex = ids.findIndex((id) => id === currentId)
const nextIndex = currentIndex + 1
return ids[nextIndex] || ids[0]
}
if (mode === PlayMode.Random) {
return getRandomMusic(ids, currentId)
}
return currentId
}

View File

@@ -0,0 +1,11 @@
/**
* 深拷贝
* @param obj 需要拷贝的对象
* @returns 拷贝后的对象
*/
export const deepClone = <T>(obj: T): T => {
// 我们这里只只用于配置项,所以直接使用字符串就行
const newObj = JSON.parse(JSON.stringify(obj))
return newObj
}

View File

@@ -0,0 +1,248 @@
import type { Op } from 'quill'
import type { EmojiValue } from '@/components/chat_input/formats/emoji_blot'
import type { ImageValue } from '@/components/chat_input/formats/image_blot'
import type { ReplyBlockValue } from '@/components/chat_input/formats/reply_blot'
import {
type AllOB11WsResponse,
type OB11AllEvent,
type OB11GroupMessage,
type OB11Message,
type OB11Notice,
OB11NoticeType,
type OB11PrivateMessage,
type OB11Segment,
type OneBot11Lifecycle,
type RequestResponse
} from '../types/onebot'
/**
* 获取事件名称
* @param event 事件类型
* @returns 事件名称
* @description 用于获取事件的名称
*/
export const getEventName = (event: OB11AllEvent['post_type']): string => {
switch (event) {
case 'message':
return '消息'
case 'notice':
return '通知'
case 'request':
return '请求'
case 'meta_event':
return '元事件'
case 'message_sent':
return '消息上报'
default:
return '未知'
}
}
/**
* 获取生命周期事件名称
* @param event 生命周期事件类型
* @returns 生命周期事件名称
* @description 用于获取生命周期事件的名称
*/
export const getLifecycleName = (
event: OneBot11Lifecycle['sub_type']
): string => {
switch (event) {
case 'enable':
return '启用'
case 'disable':
return '停用'
case 'connect':
return '连接'
default:
return '未知'
}
}
/**
* 获取生命周期事件Chip颜色
* @param event 生命周期事件类型
* @returns 生命周期事件颜色
* @description 用于获取生命周期事件的颜色
*/
export const getLifecycleColor = (
event: OneBot11Lifecycle['sub_type']
): 'success' | 'danger' | 'default' => {
switch (event) {
case 'enable':
return 'success'
case 'disable':
return 'danger'
case 'connect':
return 'success'
default:
return 'default'
}
}
/**
* 判断 OneBot WS 返回值是否为事件
* @param data OneBot WS 返回值
* @returns 是否为事件
* @description 用于判断 OneBot WS 返回值是否为事件
*/
export const isOB11Event = (data: AllOB11WsResponse): data is OB11AllEvent => {
return 'post_type' in data
}
/**
* 判断 OneBot WS 返回值是否为请求响应
* @param data OneBot WS 返回值
* @returns 是否为请求响应
* @description 用于判断 OneBot WS 返回值是否为请求响应
*/
export const isOB11RequestResponse = (
data: AllOB11WsResponse
): data is RequestResponse => {
return 'status' in data && 'retcode' in data
}
/**
* 获取请求响应状态文本
* @param status 请求响应状态
* @returns 请求响应状态文本
* @description 用于获取请求响应状态的文本
*/
export const getResponseStatusText = (
status: RequestResponse['status']
): string => {
switch (status) {
case 'ok':
return '成功'
case 'failed':
return '失败'
case 'async':
return '异步'
default:
return '未知'
}
}
/**
* 获取请求响应状态颜色
* @param status 请求响应状态
* @returns 请求响应状态颜色
* @description 用于获取请求响应状态的颜色
*/
export const getResponseStatusColor = (
status: RequestResponse['status']
): 'success' | 'danger' | 'warning' | 'default' => {
switch (status) {
case 'ok':
return 'success'
case 'failed':
return 'danger'
case 'async':
return 'warning'
default:
return 'default'
}
}
/**
* 获取通知类型名称
* @param type 通知类型
* @returns 通知类型名称
* @description 用于获取通知类型的名称
*/
export const getNoticeTypeName = (type: OB11Notice['notice_type']): string => {
switch (type) {
case OB11NoticeType.GroupUpload:
return '群文件上传'
case OB11NoticeType.GroupAdmin:
return '群管理员变动'
case OB11NoticeType.GroupDecrease:
return '群成员减少'
case OB11NoticeType.GroupIncrease:
return '群成员增加'
case OB11NoticeType.GroupBan:
return '群禁言'
case OB11NoticeType.FriendAdd:
return '好友添加'
case OB11NoticeType.GroupRecall:
return '群消息撤回'
case OB11NoticeType.FriendRecall:
return '好友消息撤回'
case OB11NoticeType.Notify:
return '通知'
case OB11NoticeType.GroupMsgEmojiLike:
return '群消息表情回应'
case OB11NoticeType.GroupEssence:
return '群消息精华'
case OB11NoticeType.GroupCard:
return '群名片更新'
default:
return '未知'
}
}
/**
* 判断 OneBot 消息是否为私聊消息
* @param data OneBot 消息
* @returns 是否为私聊消息
* @description 用于判断 OneBot 消息是否为私聊消息
*/
export const isOB11PrivateMessage = (
data: OB11Message
): data is OB11PrivateMessage => {
return data.message_type === 'private'
}
/**
* 判断 OneBot 消息是否为群消息
* @param data OneBot 消息
* @returns 是否为群消息
* @description 用于判断 OneBot 消息是否为群消息
*/
export const isOB11GroupMessage = (
data: OB11Message
): data is OB11GroupMessage => {
return data.message_type === 'group'
}
/**
* 将 Quill Delta 转换为 OneBot 消息
* @param op Quill Delta
* @returns OneBot 消息
* @description 用于将 Quill Delta 转换为 OneBot 消息
*/
export const quillToMessage = (op: Op) => {
let message: OB11Segment = {
type: 'text',
data: {
text: op.insert as string
}
}
if (typeof op.insert !== 'string') {
if (op.insert?.image) {
message = {
type: 'image',
data: {
file: (op.insert.image as ImageValue).src
}
}
} else if (op.insert?.emoji) {
message = {
type: 'face',
data: {
id: (op.insert.emoji as EmojiValue).id
}
}
} else if (op.insert?.reply) {
message = {
type: 'reply',
data: {
id: (op.insert.reply as ReplyBlockValue).messageId
}
}
}
}
return message
}

View File

@@ -0,0 +1,13 @@
import type { QQItem } from '@/components/quick_login'
/**
* 判断 QQ 快速登录列表项是否为 QQ 信息
* @param data QQ 快速登录列表项
* @returns 是否为 QQ 信息
* @description 用于判定 QQ 快速登录列表项是否为 QQ 信息
*/
export const isQQQuickNewItem = (
data?: QQItem | LoginListItem | null
): data is LoginListItem => {
return !!data && 'nickName' in data && 'faceUrl' in data
}

View File

@@ -0,0 +1,60 @@
import axios from 'axios'
import key from '@/const/key'
export const serverRequest = axios.create({
timeout: 5000
})
export const request = axios.create({
timeout: 10000
})
export const requestServerWithFetch = async (
url: string,
options: RequestInit
) => {
const token = localStorage.getItem(key.token)
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${JSON.parse(token)}`
}
}
const baseURL = '/api'
const response = await fetch(baseURL + url, options)
return response
}
serverRequest.interceptors.request.use((config) => {
const baseURL = '/api'
config.baseURL = baseURL
const token = localStorage.getItem(key.token)
if (token) {
config.headers['Authorization'] = `Bearer ${JSON.parse(token)}`
}
return config
})
serverRequest.interceptors.response.use((response) => {
if (response.data.code !== 0) {
if (response.data.message === 'Unauthorized') {
const token = localStorage.getItem(key.token)
if (token && JSON.parse(token)) {
localStorage.removeItem(key.token)
window.location.reload()
}
}
throw new Error(response.data.message)
}
return response
})

View File

View File

@@ -0,0 +1,97 @@
import { LogLevel } from '@/const/enum'
export const gradientText = (
text: string,
startColor: [number, number, number] = [255, 0, 0],
endColor: [number, number, number] = [0, 0, 255],
bold: boolean = false,
italic: boolean = false,
underline: boolean = false
) => {
const steps = text.length
const colorStep = startColor.map(
(start, index) => (endColor[index] - start) / steps
)
let coloredText = ''
for (let i = 0; i < steps; i++) {
const color = startColor.map((start, index) =>
Math.round(start + colorStep[index] * i)
)
coloredText += `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${text[i]}`
}
// 添加样式
if (bold) {
coloredText = `\x1b[1m${coloredText}`
}
if (italic) {
coloredText = `\x1b[3m${coloredText}`
}
if (underline) {
coloredText = `\x1b[4m${coloredText}`
}
return coloredText + '\x1b[0m' // 重置颜色和样式
}
export const logColor = {
[LogLevel.DEBUG]: 'green',
[LogLevel.INFO]: 'black',
[LogLevel.WARN]: 'yellow',
[LogLevel.ERROR]: 'red',
[LogLevel.FATAL]: 'red'
} as const
export const colorizeLogLevel = (content: string) => {
const logLevel = content.match(/\[[a-zA-Z]+\]/) || []
let _content = content
const level =
(logLevel?.[0]?.replace('[', '').replace(']', '') as LogLevel) ??
LogLevel.INFO
const color = logColor[level]
switch (color) {
case 'green':
_content = `\x1b[32m${_content}\x1b[0m`
break
case 'black':
_content = `\x1b[30m${_content}\x1b[0m`
break
case 'yellow':
_content = `\x1b[33m${_content}\x1b[0m`
break
case 'red':
_content = `\x1b[31m${_content}\x1b[0m`
break
default:
_content = `\x1b[30m${_content}\x1b[0m`
}
return {
content: _content,
level
}
}
export const colorizeLogLevelWithTag = (content: string, level: LogLevel) => {
let _content = content
switch (level) {
case LogLevel.DEBUG:
_content = `\x1b[32m[DEBUG] ${content}\x1b[0m`
break
case LogLevel.INFO:
_content = `\x1b[30m[INFO] ${content}\x1b[0m`
break
case LogLevel.WARN:
_content = `\x1b[33m[WARN] ${content}\x1b[0m`
break
case LogLevel.ERROR:
_content = `\x1b[31m[ERROR] ${content}\x1b[0m`
break
case LogLevel.FATAL:
_content = `\x1b[31m[FATAL] ${content}\x1b[0m`
break
default:
_content = `\x1b[30m${content}\x1b[0m`
}
return _content
}

View File

@@ -0,0 +1,52 @@
/**
* 休眠函数
* @param ms 休眠时间
* @returns Promise<void>
*/
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))
/**
* 获取距离当前时间的发布时间
* @param time 发布时间
* @returns string
* @example getReleaseTime("2021-10-10T10:10:10Z") => "1天前"
*/
export const getReleaseTime = (time: string) => {
const releaseTime = new Date(time).getTime()
const nowTime = new Date().getTime()
const diffTime = nowTime - releaseTime
const diffDays = Math.floor(diffTime / (24 * 3600 * 1000))
const diffHours = Math.floor((diffTime % (24 * 3600 * 1000)) / (3600 * 1000))
const diffMinutes = Math.floor((diffTime % (3600 * 1000)) / (60 * 1000))
const diffSeconds = Math.floor((diffTime % (60 * 1000)) / 1000)
if (diffDays > 0) {
return `${diffDays}天前`
} else if (diffHours > 0) {
return `${diffHours}小时前`
} else if (diffMinutes > 0) {
return `${diffMinutes}分钟前`
} else {
return `${diffSeconds}秒前`
}
}
const formatNumber = (n: number) => n.toString().padStart(2, '0')
/**
* 将时间戳转换为日期字符串
* @param timestamp 时间戳
* @returns string
* @example timestampToDateString(163383301) => "2021-10-10 10:10:10"
*/
export const timestampToDateString = (timestamp: number) => {
timestamp = timestamp * 1000
const date = new Date(timestamp)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return `${year}-${formatNumber(month)}-${formatNumber(day)} ${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`
}

View File

@@ -0,0 +1,45 @@
import { AxiosResponse } from 'axios'
/**
* 打开链接
* @param url 链接地址
* @param newTab 是否在新标签页打开
*/
export function openUrl(url: string, newTab = true) {
if (newTab) {
window.open(url)
} else {
window.location.href = url
}
}
/**
* 将Axios的响应转换为标准的HTTP报文
* @param response Axios响应
* @returns 标准的HTTP报文
*/
export function parseAxiosResponse(
response: AxiosResponse,
http_version: string = 'HTTP/1.1'
) {
if (!response?.status) {
return 'No response'
}
const statusLine = `${http_version} ${response.status} ${response.statusText}`
const headers = Object.entries(response.headers)
.map(([key, value]) => `${key}: ${value}`)
.join('\r\n')
const body = response.data
? `\r\n\r\n${typeof response.data === 'string' ? JSON.stringify(JSON.parse(response.data), null, 2) : JSON.stringify(response.data, null, 2)}`
: ''
return `${statusLine}\r\n${headers}${body}`
}
/**
* 判断是否为URI
* @param uri URI
* @returns 是否为URI
*/
export const isURI = (uri: string) => {
return /^(http|https|file):\/\/.*/.test(uri)
}

View File

@@ -0,0 +1,263 @@
import {
ZodArray,
ZodBigInt,
ZodBoolean,
ZodDate,
ZodDefault,
ZodEffects,
ZodEnum,
ZodLazy,
ZodLiteral,
ZodNull,
ZodNullable,
ZodNumber,
ZodObject,
ZodOptional,
ZodRecord,
ZodSchema,
ZodString,
ZodTuple,
ZodTypeAny,
ZodUnion,
ZodUnknown
} from 'zod'
export type LiteralValue = string | number | boolean | null
export type ParsedSchema = {
name?: string
type: string | string[]
optional: boolean
value?: LiteralValue
enum?: LiteralValue[]
children?: ParsedSchema[]
description?: string
}
export function parse(
schema: ZodTypeAny,
name?: string,
isRoot = true
): ParsedSchema | ParsedSchema[] {
const optional = schema.isOptional ? schema.isOptional() : false
const description = schema.description
if (schema instanceof ZodString) {
return { name, type: 'string', optional, description }
}
if (schema instanceof ZodNumber) {
return { name, type: 'number', optional, description }
}
if (schema instanceof ZodBoolean) {
return { name, type: 'boolean', optional, description }
}
if (schema instanceof ZodBigInt) {
return { name, type: 'bigint', optional, description }
}
if (schema instanceof ZodDate) {
return { name, type: 'date', optional, description }
}
if (schema instanceof ZodUnknown) {
return { name, type: 'unknown', optional, description }
}
if (schema instanceof ZodLiteral) {
return {
name,
type: 'value',
optional,
value: schema._def.value as LiteralValue,
description
}
}
if (schema instanceof ZodEnum) {
const data = {
name,
type: 'enum',
optional,
enum: schema._def.values as LiteralValue[],
description
}
return data
}
if (schema instanceof ZodUnion) {
const options = schema._def.options
const parsedOptions = options.map(
(option: ZodTypeAny) => parse(option, undefined, false) as ParsedSchema
)
const basicTypes = [
'string',
'number',
'boolean',
'bigint',
'date',
'unknown',
'value',
'enum'
]
const optionTypes: (string | string[])[] = parsedOptions.map(
(option: ParsedSchema) => option.type
)
const isAllBasicTypes = optionTypes.every((type) =>
basicTypes.includes(Array.isArray(type) ? type[0] : type)
)
if (isAllBasicTypes) {
const types = [
...new Set(
optionTypes.flatMap((type) => (Array.isArray(type) ? type[0] : type))
)
]
return { name, type: types, optional, description }
} else {
return {
name,
type: 'union',
optional,
children: parsedOptions,
description
}
}
}
if (schema instanceof ZodObject) {
const shape = schema._def.shape()
const children = Object.keys(shape).map((key) =>
parse(shape[key], key, false)
) as ParsedSchema[]
if (isRoot) {
return children
} else {
return { name, type: 'object', optional, children, description }
}
}
if (schema instanceof ZodArray) {
const childSchema = parse(
schema._def.type,
undefined,
false
) as ParsedSchema
return {
name,
type: 'array',
optional,
children: Array.isArray(childSchema) ? childSchema : [childSchema],
description
}
}
if (schema instanceof ZodNullable || schema instanceof ZodDefault) {
return parse(schema._def.innerType, name)
}
if (schema instanceof ZodOptional) {
const data = parse(schema._def.innerType, name)
if (Array.isArray(data)) {
data.forEach((item) => {
item.optional = true
item.description = description
})
} else {
data.optional = true
data.description = description
}
return data
}
if (schema instanceof ZodRecord) {
const valueType = parse(schema._def.valueType) as ParsedSchema
return {
name,
type: 'record',
optional,
children: [valueType],
description
}
}
if (schema instanceof ZodTuple) {
const items: ParsedSchema[] = schema._def.items.map((item: ZodTypeAny) =>
parse(item)
)
return { name, type: 'tuple', optional, children: items, description }
}
if (schema instanceof ZodNull) {
return { name, type: 'null', optional, description }
}
if (schema instanceof ZodLazy) {
return parse(schema._def.getter(), name)
}
if (schema instanceof ZodEffects) {
return parse(schema._def.schema, name)
}
return { name, type: 'unknown', optional, description }
}
const generateDefault = (schema: ZodSchema): unknown => {
if (schema instanceof ZodObject) {
const obj: Record<string, unknown> = {}
for (const key in schema.shape) {
obj[key] = generateDefault(schema.shape[key])
}
return obj
}
if (schema instanceof ZodString) {
return 'textValue'
}
if (schema instanceof ZodNumber) {
return 0
}
if (schema instanceof ZodBoolean) {
return false
}
if (schema instanceof ZodArray) {
return []
}
if (schema instanceof ZodUnion) {
return generateDefault(schema._def.options[0])
}
if (schema instanceof ZodEnum) {
return schema._def.values[0]
}
if (schema instanceof ZodLiteral) {
return schema._def.value
}
if (schema instanceof ZodNullable) {
return null
}
if (schema instanceof ZodOptional) {
return generateDefault(schema._def.innerType)
}
if (schema instanceof ZodRecord) {
return {}
}
if (schema instanceof ZodTuple) {
return schema._def.items.map((item: ZodTypeAny) => generateDefault(item))
}
if (schema instanceof ZodNull) {
return null
}
if (schema instanceof ZodLazy) {
return generateDefault(schema._def.getter())
}
if (schema instanceof ZodEffects) {
return generateDefault(schema._def.schema)
}
return null
}
export const generateDefaultJson = (schema: ZodSchema) => {
return JSON.stringify(generateDefault(schema), null, 2)
}