mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 13:05:09 +00:00
feat: 新版webui
This commit is contained in:
120
napcat.webui/src/utils/music.ts
Normal file
120
napcat.webui/src/utils/music.ts
Normal 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
|
||||
}
|
||||
11
napcat.webui/src/utils/object.ts
Normal file
11
napcat.webui/src/utils/object.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 深拷贝
|
||||
* @param obj 需要拷贝的对象
|
||||
* @returns 拷贝后的对象
|
||||
*/
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
// 我们这里只只用于配置项,所以直接使用字符串就行
|
||||
const newObj = JSON.parse(JSON.stringify(obj))
|
||||
|
||||
return newObj
|
||||
}
|
||||
248
napcat.webui/src/utils/onebot.ts
Normal file
248
napcat.webui/src/utils/onebot.ts
Normal 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
|
||||
}
|
||||
13
napcat.webui/src/utils/qq.ts
Normal file
13
napcat.webui/src/utils/qq.ts
Normal 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
|
||||
}
|
||||
60
napcat.webui/src/utils/request.ts
Normal file
60
napcat.webui/src/utils/request.ts
Normal 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
|
||||
})
|
||||
0
napcat.webui/src/utils/suggestions.ts
Normal file
0
napcat.webui/src/utils/suggestions.ts
Normal file
97
napcat.webui/src/utils/terminal.ts
Normal file
97
napcat.webui/src/utils/terminal.ts
Normal 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
|
||||
}
|
||||
52
napcat.webui/src/utils/time.ts
Normal file
52
napcat.webui/src/utils/time.ts
Normal 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)}`
|
||||
}
|
||||
45
napcat.webui/src/utils/url.ts
Normal file
45
napcat.webui/src/utils/url.ts
Normal 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)
|
||||
}
|
||||
263
napcat.webui/src/utils/zod.ts
Normal file
263
napcat.webui/src/utils/zod.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user