📄 dialog.ts • 7045 bytes
/**
* UI 组件 - 确认对话框
* Phase 5: 用户交互
*/
/** 选择选项 */
export interface Choice<T = string> {
value: T
label: string
description?: string
}
/** 确认选项配置 */
export interface ConfirmOptions<T = string> {
message: string
choices: Choice<T>[]
defaultValue?: T
allowCancel?: boolean
cancelValue?: T
cancelLabel?: string
}
/** 确认结果 */
export interface ConfirmResult<T = string> {
selected: T
cancelled: boolean
index: number
}
/** ANSI 颜色 */
const ESC = '\x1b'
const RESET = `${ESC}[0m`
const BOLD = `${ESC}[1m`
const DIM = `${ESC}[2m`
const YELLOW = `${ESC}[38;5;220m`
const GREEN = `${ESC}[38;5;208m`
const CYAN = `${ESC}[38;5;51m`
const RED = `${ESC}[38;5;196m`
const GRAY = `${ESC}[38;5;240m`
/**
* 显示确认对话框
*/
export async function confirm<T = string>(
options: ConfirmOptions<T>
): Promise<ConfirmResult<T>> {
const {
message,
choices,
defaultValue,
allowCancel = true,
cancelValue,
cancelLabel = '取消'
} = options
// 显示消息
console.log('')
console.log(` ${BOLD}${YELLOW}? ${message}${RESET}`)
console.log('')
// 显示选项
const allChoices: Choice<T>[] = [...choices]
if (allowCancel) {
allChoices.push({
value: cancelValue as T,
label: cancelLabel,
description: '取消当前操作',
})
}
allChoices.forEach((choice, index) => {
const key = index + 1
const isDefault = choice.value === defaultValue
const defaultMark = isDefault ? ` ${GREEN}[默认]${RESET}` : ''
console.log(` ${CYAN}${key}${RESET}. ${choice.label}${defaultMark}`)
if (choice.description) {
console.log(` ${DIM}${choice.description}${RESET}`)
}
})
console.log('')
// 读取用户输入
const readline = await import('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
const prompt = allowCancel
? ` 请选择 (1-${allChoices.length}) 或按回车取消: `
: ` 请选择 (1-${allChoices.length}): `
rl.question(prompt, (answer) => {
rl.close()
const trimmed = answer.trim()
// 空输入处理
if (trimmed === '') {
if (defaultValue !== undefined) {
const defaultIndex = allChoices.findIndex(c => c.value === defaultValue)
resolve({
selected: defaultValue,
cancelled: false,
index: defaultIndex >= 0 ? defaultIndex : 0,
})
} else if (allowCancel) {
resolve({
selected: cancelValue as T,
cancelled: true,
index: allChoices.length - 1,
})
} else {
// 默认选择第一个
resolve({
selected: allChoices[0].value,
cancelled: false,
index: 0,
})
}
return
}
// 数字选择
const num = parseInt(trimmed, 10)
if (!isNaN(num) && num >= 1 && num <= allChoices.length) {
resolve({
selected: allChoices[num - 1].value,
cancelled: num === allChoices.length && allowCancel,
index: num - 1,
})
return
}
// 文本匹配
const matchedIndex = allChoices.findIndex(c =>
c.label.toLowerCase().includes(trimmed.toLowerCase())
)
if (matchedIndex >= 0) {
resolve({
selected: allChoices[matchedIndex].value,
cancelled: matchedIndex === allChoices.length - 1 && allowCancel,
index: matchedIndex,
})
return
}
// 无效输入,重新提示
console.log(` ${RED}无效选择,请重新输入${RESET}`)
resolve(confirm(options) as Promise<ConfirmResult<T>>)
})
})
}
/**
* 快速确认(是/否)
*/
export async function askYesNo(message: string, defaultYes: boolean = false): Promise<boolean> {
const result = await confirm({
message,
choices: [
{ value: true, label: '是', description: '确认执行' },
{ value: false, label: '否', description: '取消操作' },
],
defaultValue: defaultYes,
allowCancel: true,
cancelValue: false,
})
return result.selected
}
/**
* 显示确认列表
*/
export async function selectFromList<T = string>(
message: string,
items: Choice<T>[],
allowMultiple: boolean = false
): Promise<T | T[]> {
if (!allowMultiple) {
const result = await confirm({
message,
choices: items,
allowCancel: true,
cancelValue: undefined as unknown as T,
})
if (result.cancelled) {
return undefined as unknown as T
}
return result.selected
}
// 多选模式
console.log('')
console.log(` ${BOLD}${YELLOW}? ${message}${RESET}`)
console.log('')
console.log(` ${DIM}输入数字用逗号分隔选择多项,如: 1,3,5${RESET}`)
console.log('')
items.forEach((item, index) => {
console.log(` ${CYAN}${index + 1}${RESET}. ${item.label}`)
if (item.description) {
console.log(` ${DIM}${item.description}${RESET}`)
}
})
console.log('')
const readline = await import('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
rl.question(' 请选择: ', (answer) => {
rl.close()
const trimmed = answer.trim()
if (!trimmed) {
resolve([] as unknown as T[])
return
}
const indices = trimmed.split(',')
.map(s => parseInt(s.trim(), 10) - 1)
.filter(n => !isNaN(n) && n >= 0 && n < items.length)
const selected = indices.map(i => items[i].value)
resolve(selected as unknown as T)
})
})
}
/**
* 显示提示信息
*/
export function showNotice(message: string, type: 'info' | 'warning' | 'error' | 'success' = 'info'): void {
const colors = {
info: CYAN,
warning: YELLOW,
error: RED,
success: GREEN,
}
const icons = {
info: 'ℹ️',
warning: '⚠️',
error: '❌',
success: '✅',
}
console.log(` ${colors[type]}${icons[type]} ${message}${RESET}`)
}
/**
* 显示分隔线
*/
export function showDivider(char: string = '─', length: number = 50): void {
console.log(` ${GRAY}${char.repeat(length)}${RESET}`)
}
/**
* 显示标题
*/
export function showTitle(text: string, level: 1 | 2 | 3 = 2): void {
const decorations: Record<number, { prefix: string; suffix: string; color: string }> = {
1: { prefix: '═══', suffix: '═══', color: YELLOW },
2: { prefix: '───', suffix: '───', color: CYAN },
3: { prefix: '', suffix: '', color: GRAY },
}
const d = decorations[level]
if (level === 3) {
console.log(` ${BOLD}${d.color}${text}${RESET}`)
} else {
console.log(` ${d.color}${d.prefix} ${BOLD}${text}${RESET} ${d.suffix}${RESET}`)
}
}