📄 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}`)
  }
}