📄 status.ts • 6455 bytes
/**
* UI 组件 - 状态显示
* Phase 5: 状态指示器
*/
/** 状态类型 */
export type StatusType = 'idle' | 'loading' | 'success' | 'error' | 'warning' | 'info'
/** 状态配置 */
export interface StatusOptions {
message?: string
icon?: string
color?: string
showTimestamp?: boolean
persist?: boolean // 是否持续显示(不自动清除)
}
/** ANSI 颜色 */
const ESC = '\x1b'
const RESET = `${ESC}[0m`
const CLEAR_LINE = `${ESC}[2K\r`
const MOVE_UP = `${ESC}[1A`
/** 状态图标 */
export const STATUS_ICONS: Record<StatusType, string> = {
idle: '○',
loading: '◐',
success: '●',
error: '✕',
warning: '⚠',
info: 'ℹ',
}
/** 状态颜色 */
export const STATUS_COLORS: Record<StatusType, string> = {
idle: '\x1b[38;5;240m',
loading: '\x1b[38;5;51m',
success: '\x1b[38;5;208m',
error: '\x1b[38;5;196m',
warning: '\x1b[38;5;220m',
info: '\x1b[38;5;51m',
}
/** 状态管理器 */
export class StatusIndicator {
private type: StatusType
private message: string
private startTime: number
private lines: number = 1
private frame: number = 0
private intervalId?: ReturnType<typeof setInterval>
constructor(type: StatusType = 'idle', message: string = '', options: StatusOptions = {}) {
this.type = type
this.message = message || this.getDefaultMessage(type)
this.startTime = Date.now()
if (options.icon) STATUS_ICONS[type] = options.icon
if (options.color) STATUS_COLORS[type] = options.color
}
/** 获取默认消息 */
private getDefaultMessage(type: StatusType): string {
const messages: Record<StatusType, string> = {
idle: '就绪',
loading: '加载中',
success: '完成',
error: '失败',
warning: '警告',
info: '提示',
}
return messages[type]
}
/** 更新状态 */
update(type: StatusType, message?: string): void {
this.type = type
if (message) this.message = message
this.render()
}
/** 开始动画 */
start(): void {
this.render()
if (this.type === 'loading') {
this.intervalId = setInterval(() => {
this.frame++
this.render()
}, 200)
}
}
/** 停止动画 */
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = undefined
}
}
/** 清除显示 */
clear(): void {
this.stop()
for (let i = 0; i < this.lines; i++) {
process.stdout.write(`${CLEAR_LINE}${MOVE_UP}`)
}
process.stdout.write(CLEAR_LINE)
}
/** 渲染状态 */
private render(): void {
const icon = this.getAnimatedIcon()
const color = STATUS_COLORS[this.type]
const timestamp = this.getTimestamp()
const line = ` ${color}${icon}${RESET} ${this.message}`
const suffix = timestamp ? ` ${ESC}[38;5;240m${timestamp}${RESET}` : ''
// 清除之前的行
if (this.lines > 1) {
for (let i = 0; i < this.lines - 1; i++) {
process.stdout.write(`${CLEAR_LINE}${MOVE_UP}`)
}
}
process.stdout.write(CLEAR_LINE)
process.stdout.write(line + suffix)
}
/** 获取动画图标 */
private getAnimatedIcon(): string {
const baseIcon = STATUS_ICONS[this.type]
if (this.type !== 'loading') {
return baseIcon
}
// 旋转动画
const frames = ['◐', '◓', '◑', '◒']
return frames[this.frame % frames.length]
}
/** 获取时间戳 */
private getTimestamp(): string {
const elapsed = Math.floor((Date.now() - this.startTime) / 1000)
if (elapsed < 1) return ''
if (elapsed < 60) {
return `${elapsed}s`
}
const minutes = Math.floor(elapsed / 60)
const seconds = elapsed % 60
return `${minutes}m ${seconds}s`
}
/** 成功状态 */
success(message?: string): void {
this.update('success', message || '操作成功')
setTimeout(() => this.clear(), 2000)
}
/** 失败状态 */
error(message?: string): void {
this.update('error', message || '操作失败')
}
/** 警告状态 */
warning(message?: string): void {
this.update('warning', message || '警告')
}
/** 信息状态 */
info(message?: string): void {
this.update('info', message || '提示')
}
}
/** 全局状态管理 */
class GlobalStatusManager {
private current?: StatusIndicator
set(type: StatusType, message?: string): StatusIndicator {
this.clear()
this.current = new StatusIndicator(type, message)
this.current.start()
return this.current
}
clear(): void {
if (this.current) {
this.current.clear()
this.current = undefined
}
}
success(message?: string): void {
if (this.current) {
this.current.success(message)
} else {
console.log(` ${STATUS_COLORS.success}●${RESET} ${message || '成功'}`)
}
}
error(message?: string): void {
if (this.current) {
this.current.error(message)
} else {
console.log(` ${STATUS_COLORS.error}✕${RESET} ${message || '失败'}`)
}
}
}
/** 全局实例 */
export const status = new GlobalStatusManager()
/** 创建状态指示器 */
export function createStatus(type: StatusType = 'idle', message?: string): StatusIndicator {
return new StatusIndicator(type, message)
}
/** Loading 动画 */
export async function withStatus<T>(
message: string,
operation: () => Promise<T>
): Promise<T> {
const indicator = createStatus('loading', message)
indicator.start()
try {
const result = await operation()
indicator.success('完成')
return result
} catch (error: any) {
indicator.error(error?.message || '失败')
throw error
}
}
/** 加载动画组 */
export class SpinnerGroup {
private spinners: Map<string, StatusIndicator> = new Map()
add(key: string, message: string): void {
const spinner = createStatus('loading', message)
spinner.start()
this.spinners.set(key, spinner)
}
complete(key: string, success: boolean = true): void {
const spinner = this.spinners.get(key)
if (spinner) {
spinner.stop()
if (success) {
console.log(` ${STATUS_COLORS.success}●${RESET} ${spinner['message']}`)
} else {
console.log(` ${STATUS_COLORS.error}✕${RESET} ${spinner['message']}`)
}
this.spinners.delete(key)
}
}
clear(): void {
this.spinners.forEach(s => s.clear())
this.spinners.clear()
}
}