chore: starter
This commit is contained in:
29
packages/utils/package.json
Normal file
29
packages/utils/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@cslab-dcs/utils",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./src/*.ts",
|
||||
"import": "./src/*.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"mitt": "^3.0.1",
|
||||
"ofetch": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
123
packages/utils/src/date.ts
Normal file
123
packages/utils/src/date.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { ConfigType, Dayjs, ManipulateType, OpUnitType } from 'dayjs'
|
||||
/**
|
||||
* 日期时间工具
|
||||
* 基于 dayjs 封装
|
||||
*/
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
// 扩展插件
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
// 设置中文
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
/**
|
||||
* 默认日期格式
|
||||
*/
|
||||
export const DATE_FORMAT = 'YYYY-MM-DD'
|
||||
export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
|
||||
export const TIME_FORMAT = 'HH:mm:ss'
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
export function formatDate(date?: ConfigType, format = DATE_FORMAT): string {
|
||||
return dayjs(date).format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
export function formatDateTime(date?: ConfigType, format = DATETIME_FORMAT): string {
|
||||
return dayjs(date).format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
export function formatTime(date?: ConfigType, format = TIME_FORMAT): string {
|
||||
return dayjs(date).format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间(如:3分钟前)
|
||||
*/
|
||||
export function fromNow(date: ConfigType): string {
|
||||
return dayjs(date).fromNow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到现在的相对时间
|
||||
*/
|
||||
export function toNow(date: ConfigType): string {
|
||||
return dayjs(date).toNow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期加减
|
||||
*/
|
||||
export function addTime(date: ConfigType, value: number, unit: ManipulateType): Dayjs {
|
||||
return dayjs(date).add(value, unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期减
|
||||
*/
|
||||
export function subtractTime(date: ConfigType, value: number, unit: ManipulateType): Dayjs {
|
||||
return dayjs(date).subtract(value, unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个日期之间的差值
|
||||
*/
|
||||
export function diff(date1: ConfigType, date2: ConfigType, unit: OpUnitType = 'day'): number {
|
||||
return dayjs(date1).diff(dayjs(date2), unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断日期是否在某个日期之前
|
||||
*/
|
||||
export function isBefore(date1: ConfigType, date2: ConfigType): boolean {
|
||||
return dayjs(date1).isBefore(dayjs(date2))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断日期是否在某个日期之后
|
||||
*/
|
||||
export function isAfter(date1: ConfigType, date2: ConfigType): boolean {
|
||||
return dayjs(date1).isAfter(dayjs(date2))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个日期是否相同
|
||||
*/
|
||||
export function isSame(date1: ConfigType, date2: ConfigType, unit: OpUnitType = 'day'): boolean {
|
||||
return dayjs(date1).isSame(dayjs(date2), unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳
|
||||
*/
|
||||
export function now(): number {
|
||||
return dayjs().valueOf()
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期
|
||||
*/
|
||||
export function parseDate(date: ConfigType): Dayjs {
|
||||
return dayjs(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 dayjs 实例供高级用法
|
||||
*/
|
||||
export { dayjs }
|
||||
84
packages/utils/src/event.ts
Normal file
84
packages/utils/src/event.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Emitter, EventType, Handler } from 'mitt'
|
||||
/**
|
||||
* 事件总线
|
||||
* 基于 mitt 封装
|
||||
*/
|
||||
import mitt from 'mitt'
|
||||
|
||||
// 定义事件类型
|
||||
export interface AppEvents {
|
||||
// 通用事件
|
||||
'app:ready': void
|
||||
'app:error': Error
|
||||
|
||||
// 主题切换
|
||||
'theme:change': 'light' | 'dark'
|
||||
|
||||
// 用户事件
|
||||
'user:login': { userId: string, token: string }
|
||||
'user:logout': void
|
||||
|
||||
// 文件事件
|
||||
'file:open': { path: string }
|
||||
'file:save': { path: string, content: string }
|
||||
'file:close': { path: string }
|
||||
|
||||
// 编辑器事件
|
||||
'editor:change': { content: string }
|
||||
'editor:save': void
|
||||
|
||||
// 允许自定义事件
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// 创建事件总线实例
|
||||
const emitter: Emitter<AppEvents> = mitt<AppEvents>()
|
||||
|
||||
/**
|
||||
* 发送事件
|
||||
*/
|
||||
export function emit<K extends keyof AppEvents>(type: K, event: AppEvents[K]): void {
|
||||
emitter.emit(type, event)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听事件
|
||||
*/
|
||||
export function on<K extends keyof AppEvents>(type: K, handler: Handler<AppEvents[K]>): void {
|
||||
emitter.on(type, handler as Handler<AppEvents[keyof AppEvents]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消监听事件
|
||||
*/
|
||||
export function off<K extends keyof AppEvents>(type: K, handler?: Handler<AppEvents[K]>): void {
|
||||
emitter.off(type, handler as Handler<AppEvents[keyof AppEvents]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听所有事件
|
||||
*/
|
||||
export function onAll(handler: (type: EventType, event: unknown) => void): void {
|
||||
emitter.on('*', handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有事件监听
|
||||
*/
|
||||
export function clear(): void {
|
||||
emitter.all.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件总线实例
|
||||
*/
|
||||
export const eventBus = {
|
||||
emit,
|
||||
on,
|
||||
off,
|
||||
onAll,
|
||||
clear,
|
||||
emitter,
|
||||
}
|
||||
|
||||
export default eventBus
|
||||
195
packages/utils/src/helpers.ts
Normal file
195
packages/utils/src/helpers.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 通用辅助函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 延迟执行
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
*/
|
||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
fn: T,
|
||||
delay: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
return function (this: unknown, ...args: Parameters<T>) {
|
||||
if (timer)
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
*/
|
||||
export function throttle<T extends (...args: unknown[]) => unknown>(
|
||||
fn: T,
|
||||
delay: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let lastTime = 0
|
||||
|
||||
return function (this: unknown, ...args: Parameters<T>) {
|
||||
const now = Date.now()
|
||||
if (now - lastTime >= delay) {
|
||||
lastTime = now
|
||||
fn.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝
|
||||
*/
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as T
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => deepClone(item)) as T
|
||||
}
|
||||
|
||||
if (obj instanceof Object) {
|
||||
const copy = {} as T
|
||||
Object.keys(obj).forEach((key) => {
|
||||
;(copy as Record<string, unknown>)[key] = deepClone((obj as Record<string, unknown>)[key])
|
||||
})
|
||||
return copy
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
export function generateId(prefix = ''): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const randomStr = Math.random().toString(36).substring(2, 10)
|
||||
return `${prefix}${timestamp}${randomStr}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID
|
||||
*/
|
||||
export function generateUUID(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为空值(null、undefined、空字符串、空数组、空对象)
|
||||
*/
|
||||
export function isEmpty(value: unknown): boolean {
|
||||
if (value === null || value === undefined)
|
||||
return true
|
||||
if (typeof value === 'string' && value.trim() === '')
|
||||
return true
|
||||
if (Array.isArray(value) && value.length === 0)
|
||||
return true
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析 JSON
|
||||
*/
|
||||
export function safeJsonParse<T>(str: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(str) as T
|
||||
}
|
||||
catch {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0)
|
||||
return '0 B'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
const k = 1024
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${units[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转短横线
|
||||
*/
|
||||
export function camelToKebab(str: string): string {
|
||||
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 短横线转驼峰
|
||||
*/
|
||||
export function kebabToCamel(str: string): string {
|
||||
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
// 降级方案
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const result = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
export function downloadFile(content: string | Blob, filename: string, mimeType = 'text/plain'): void {
|
||||
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
101
packages/utils/src/http.ts
Normal file
101
packages/utils/src/http.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { FetchOptions } from 'ofetch'
|
||||
/**
|
||||
* HTTP 请求工具
|
||||
* 基于 ofetch 封装
|
||||
*/
|
||||
import { ofetch } from 'ofetch'
|
||||
|
||||
export interface RequestConfig extends FetchOptions {
|
||||
baseURL?: string
|
||||
showError?: boolean
|
||||
}
|
||||
|
||||
const defaultConfig: RequestConfig = {
|
||||
baseURL: '',
|
||||
timeout: 30000,
|
||||
showError: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求实例
|
||||
*/
|
||||
export function createRequest(config: RequestConfig = {}) {
|
||||
const mergedConfig = { ...defaultConfig, ...config }
|
||||
|
||||
const request = ofetch.create({
|
||||
baseURL: mergedConfig.baseURL,
|
||||
timeout: mergedConfig.timeout,
|
||||
onRequest({ options }) {
|
||||
// 请求拦截器 - 添加通用 headers
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
onRequestError({ error }) {
|
||||
// 请求错误处理
|
||||
console.error('[Request Error]', error)
|
||||
},
|
||||
onResponse({ response }) {
|
||||
// 响应拦截器
|
||||
return response._data
|
||||
},
|
||||
onResponseError({ response }) {
|
||||
// 响应错误处理
|
||||
const status = response.status
|
||||
const message = response._data?.message || `HTTP Error: ${status}`
|
||||
console.error('[Response Error]', message)
|
||||
throw new Error(message)
|
||||
},
|
||||
})
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认请求实例
|
||||
*/
|
||||
export const http = createRequest()
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
export async function get<T>(url: string, params?: Record<string, unknown>, config?: RequestConfig): Promise<T> {
|
||||
return http<T>(url, {
|
||||
method: 'GET',
|
||||
query: params,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
export async function post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
|
||||
return http<T>(url, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
export async function put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
|
||||
return http<T>(url, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
*/
|
||||
export async function del<T>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return http<T>(url, {
|
||||
method: 'DELETE',
|
||||
...config,
|
||||
})
|
||||
}
|
||||
10
packages/utils/src/index.ts
Normal file
10
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @cslab-dcs/utils
|
||||
* 工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './date'
|
||||
export * from './event'
|
||||
export * from './helpers'
|
||||
export * from './http'
|
||||
export * from './storage'
|
||||
136
packages/utils/src/storage.ts
Normal file
136
packages/utils/src/storage.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 本地存储工具
|
||||
* 抽象 localStorage 操作
|
||||
*/
|
||||
|
||||
const PREFIX = 'cslab-dcs:'
|
||||
|
||||
/**
|
||||
* 存储选项
|
||||
*/
|
||||
export interface StorageOptions {
|
||||
prefix?: string
|
||||
expire?: number // 过期时间(毫秒)
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储数据结构
|
||||
*/
|
||||
interface StorageData<T> {
|
||||
value: T
|
||||
expire?: number
|
||||
createTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存储
|
||||
*/
|
||||
export function setStorage<T>(key: string, value: T, options: StorageOptions = {}): void {
|
||||
const { prefix = PREFIX, expire } = options
|
||||
|
||||
const data: StorageData<T> = {
|
||||
value,
|
||||
createTime: Date.now(),
|
||||
}
|
||||
|
||||
if (expire) {
|
||||
data.expire = Date.now() + expire
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(prefix + key, JSON.stringify(data))
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[Storage] setStorage error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储
|
||||
*/
|
||||
export function getStorage<T>(key: string, options: StorageOptions = {}): T | null {
|
||||
const { prefix = PREFIX } = options
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(prefix + key)
|
||||
if (!raw)
|
||||
return null
|
||||
|
||||
const data: StorageData<T> = JSON.parse(raw)
|
||||
|
||||
// 检查是否过期
|
||||
if (data.expire && data.expire < Date.now()) {
|
||||
removeStorage(key, options)
|
||||
return null
|
||||
}
|
||||
|
||||
return data.value
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[Storage] getStorage error:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除存储
|
||||
*/
|
||||
export function removeStorage(key: string, options: StorageOptions = {}): void {
|
||||
const { prefix = PREFIX } = options
|
||||
localStorage.removeItem(prefix + key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有存储
|
||||
*/
|
||||
export function clearStorage(options: StorageOptions = {}): void {
|
||||
const { prefix = PREFIX } = options
|
||||
|
||||
const keys = Object.keys(localStorage).filter(k => k.startsWith(prefix))
|
||||
keys.forEach(key => localStorage.removeItem(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有存储键
|
||||
*/
|
||||
export function getStorageKeys(options: StorageOptions = {}): string[] {
|
||||
const { prefix = PREFIX } = options
|
||||
|
||||
return Object.keys(localStorage)
|
||||
.filter(k => k.startsWith(prefix))
|
||||
.map(k => k.replace(prefix, ''))
|
||||
}
|
||||
|
||||
/**
|
||||
* Session 存储
|
||||
*/
|
||||
export const sessionStorage = {
|
||||
set<T>(key: string, value: T): void {
|
||||
try {
|
||||
window.sessionStorage.setItem(PREFIX + key, JSON.stringify(value))
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[SessionStorage] set error:', e)
|
||||
}
|
||||
},
|
||||
|
||||
get<T>(key: string): T | null {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(PREFIX + key)
|
||||
return raw ? JSON.parse(raw) : null
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[SessionStorage] get error:', e)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
remove(key: string): void {
|
||||
window.sessionStorage.removeItem(PREFIX + key)
|
||||
},
|
||||
|
||||
clear(): void {
|
||||
const keys = Object.keys(window.sessionStorage).filter(k => k.startsWith(PREFIX))
|
||||
keys.forEach(key => window.sessionStorage.removeItem(key))
|
||||
},
|
||||
}
|
||||
10
packages/utils/tsconfig.json
Normal file
10
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user