chore: starter

This commit is contained in:
2026-01-29 18:44:04 +08:00
commit 9cda358a2e
91 changed files with 16946 additions and 0 deletions

123
packages/utils/src/date.ts Normal file
View 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 }

View 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

View 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
View 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,
})
}

View File

@@ -0,0 +1,10 @@
/**
* @cslab-dcs/utils
* 工具函数统一导出
*/
export * from './date'
export * from './event'
export * from './helpers'
export * from './http'
export * from './storage'

View 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))
},
}