# PopupHint 弹出提示

一个全局提示框系统，当用户悬停在元素上时提供上下文提示和信息，具有自动定位和时间控制功能。

## 功能特性

- **全局访问**: 通过 `window.Hint` 在整个应用中使用
- **鼠标跟踪**: 自动在鼠标光标附近定位提示
- **延迟显示**: 500毫秒延迟显示以防止闪烁
- **自动隐藏**: 当鼠标离开元素时自动隐藏
- **门户渲染**: 在文档主体中渲染提示以确保正确的z-index层级
- **响应式定位**: 自动调整位置以保持在视口内
- **事件管理**: 正确清理事件监听器
- **轻量级**: 通过高效的事件处理实现最小性能影响

## 安装

```bash
npm install @ticatec/uniface-element
```

## 设置

首先，在你的根布局或应用组件中添加 PopupHint 组件：

```svelte
<script>
  import PopupHint from '@ticatec/uniface-element/PopupHint';
</script>

<!-- 应用内容 -->
<main>
  <!-- 你的组件 -->
</main>

<!-- PopupHint - 全局实例 -->
<PopupHint />
```

## 基本用法

一旦 PopupHint 被挂载，你就可以在应用的任何地方显示提示：

```javascript
// 悬停在元素上时显示提示
function handleMouseEnter(event) {
  window.Hint.show(
    event.target,           // 目标元素
    '这是一个有用的提示！', // 提示文本
    event.clientX,          // 鼠标X坐标
    event.clientY           // 鼠标Y坐标
  );
}
```

## API 参考

### 全局方法

#### `window.Hint.show(element, text, x, y)`

在指定坐标附近显示提示框。

**参数:**
- `element` (Element): 触发提示的目标元素
- `text` (string): 要显示的提示文本
- `x` (number): 鼠标X坐标
- `y` (number): 鼠标Y坐标

**行为:**
- 添加自动 `mouseleave` 事件监听器来隐藏提示
- 500毫秒延迟显示以防止意外触发
- 自动在光标附近定位提示并带有偏移

### 类型

```typescript
export type ShowHint = (element: Element, text: string, x: number, y: number) => void;

export default interface IHint {
  show: ShowHint;
}
```

## 示例

### 基本按钮提示

```svelte
<script>
  function showHint(text) {
    return function(event) {
      window.Hint.show(event.target, text, event.clientX, event.clientY);
    };
  }
</script>

<div class="button-group">
  <button on:mouseenter={showHint('保存当前工作')}>
    保存
  </button>
  
  <button on:mouseenter={showHint('取消所有更改')}>
    取消
  </button>
  
  <button on:mouseenter={showHint('导出数据为Excel格式')}>
    导出
  </button>
</div>
```

### 图标工具提示

```svelte
<script>
  import Icon from '@ticatec/uniface-element/Icon';
  
  const iconHints = {
    edit: '编辑此项目',
    delete: '删除此项目（无法撤销）',
    share: '与他人分享',
    bookmark: '添加到书签',
    download: '下载文件'
  };
  
  function handleIconHover(iconType) {
    return function(event) {
      const hint = iconHints[iconType];
      if (hint) {
        window.Hint.show(event.target, hint, event.clientX, event.clientY);
      }
    };
  }
</script>

<div class="toolbar">
  <Icon 
    name="icon_google_edit" 
    on:mouseenter={handleIconHover('edit')}
  />
  <Icon 
    name="icon_google_delete" 
    on:mouseenter={handleIconHover('delete')}
  />
  <Icon 
    name="icon_google_share" 
    on:mouseenter={handleIconHover('share')}
  />
  <Icon 
    name="icon_google_bookmark" 
    on:mouseenter={handleIconHover('bookmark')}
  />
  <Icon 
    name="icon_google_download" 
    on:mouseenter={handleIconHover('download')}
  />
</div>

<style>
  .toolbar {
    display: flex;
    gap: 8px;
    padding: 12px;
  }
</style>
```

### 表单字段提示

```svelte
<script>
  import FormField from '@ticatec/uniface-element/FormField';
  import TextEditor from '@ticatec/uniface-element/TextEditor';
  
  let formData = {
    username: '',
    email: '',
    password: ''
  };
  
  const fieldHints = {
    username: '用户名必须是3-20个字符，仅限字母和数字',
    email: '我们将使用此邮箱进行账户恢复和通知',
    password: '密码必须至少8个字符，包含大写、小写字母和数字'
  };
  
  function showFieldHint(fieldName) {
    return function(event) {
      window.Hint.show(event.target, fieldHints[fieldName], event.clientX, event.clientY);
    };
  }
</script>

<form class="registration-form">
  <FormField label="用户名">
    <TextEditor 
      variant="outlined"
      bind:value={formData.username}
      placeholder="输入用户名"
      on:mouseenter={showFieldHint('username')}
    />
  </FormField>
  
  <FormField label="邮箱">
    <TextEditor 
      variant="outlined"
      bind:value={formData.email}
      placeholder="输入邮箱地址"
      on:mouseenter={showFieldHint('email')}
    />
  </FormField>
  
  <FormField label="密码">
    <TextEditor 
      variant="outlined"
      type="password"
      bind:value={formData.password}
      placeholder="输入密码"
      on:mouseenter={showFieldHint('password')}
    />
  </FormField>
</form>
```

### 数据表格行提示

```svelte
<script>
  let users = [
    { 
      id: 1, 
      name: '张三', 
      email: 'zhangsan@example.com', 
      status: 'active',
      lastLogin: '2024-01-15 14:30:00',
      role: '管理员'
    },
    { 
      id: 2, 
      name: '李四', 
      email: 'lisi@example.com', 
      status: 'inactive',
      lastLogin: '2024-01-10 09:15:00',
      role: '用户'
    }
  ];
  
  function showUserHint(user) {
    return function(event) {
      const hint = `
        最后登录: ${user.lastLogin}
        角色: ${user.role}
        状态: ${user.status === 'active' ? '活跃' : '非活跃'}
        ID: ${user.id}
      `;
      window.Hint.show(event.target, hint, event.clientX, event.clientY);
    };
  }
  
  function showStatusHint(status) {
    return function(event) {
      const hints = {
        active: '用户当前活跃，可以访问系统',
        inactive: '用户账户已被停用',
        pending: '用户注册等待批准',
        suspended: '用户账户已被暂时挂起'
      };
      
      window.Hint.show(event.target, hints[status] || '未知状态', event.clientX, event.clientY);
    };
  }
</script>

<table class="user-table">
  <thead>
    <tr>
      <th>姓名</th>
      <th>邮箱</th>
      <th>状态</th>
    </tr>
  </thead>
  <tbody>
    {#each users as user}
      <tr on:mouseenter={showUserHint(user)}>
        <td>{user.name}</td>
        <td>{user.email}</td>
        <td>
          <span 
            class="status status-{user.status}"
            on:mouseenter={showStatusHint(user.status)}
          >
            {user.status === 'active' ? '活跃' : '非活跃'}
          </span>
        </td>
      </tr>
    {/each}
  </tbody>
</table>

<style>
  .user-table {
    width: 100%;
    border-collapse: collapse;
  }
  
  .user-table th,
  .user-table td {
    padding: 12px;
    text-align: left;
    border-bottom: 1px solid #ddd;
  }
  
  .user-table tr:hover {
    background-color: #f5f5f5;
  }
  
  .status {
    padding: 4px 8px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
  }
  
  .status-active {
    background: #d4edda;
    color: #155724;
  }
  
  .status-inactive {
    background: #f8d7da;
    color: #721c24;
  }
</style>
```

### 图表和图形提示

```svelte
<script>
  let chartData = [
    { month: '1月', sales: 12000, growth: '+15%' },
    { month: '2月', sales: 13500, growth: '+12.5%' },
    { month: '3月', sales: 11200, growth: '-17%' },
    { month: '4月', sales: 14800, growth: '+32%' },
    { month: '5月', sales: 16200, growth: '+9.5%' }
  ];
  
  function showChartHint(dataPoint) {
    return function(event) {
      const hint = `
        月份: ${dataPoint.month}
        销售额: ¥${dataPoint.sales.toLocaleString()}
        增长: ${dataPoint.growth}
      `;
      window.Hint.show(event.target, hint, event.clientX, event.clientY);
    };
  }
</script>

<div class="chart-container">
  <h3>销售图表</h3>
  <div class="chart">
    {#each chartData as data, i}
      <div 
        class="chart-bar"
        style="height: {(data.sales / 20000) * 200}px;"
        on:mouseenter={showChartHint(data)}
      >
        <div class="bar-label">{data.month}</div>
      </div>
    {/each}
  </div>
</div>

<style>
  .chart-container {
    padding: 20px;
  }
  
  .chart {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    height: 250px;
    border-bottom: 2px solid #333;
    padding: 20px 0;
  }
  
  .chart-bar {
    background: linear-gradient(to top, #007bff, #66b3ff);
    width: 60px;
    border-radius: 4px 4px 0 0;
    cursor: pointer;
    transition: transform 0.2s;
    position: relative;
  }
  
  .chart-bar:hover {
    transform: scale(1.05);
  }
  
  .bar-label {
    position: absolute;
    bottom: -25px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 12px;
    font-weight: 600;
  }
</style>
```

### 图片画廊元数据

```svelte
<script>
  let images = [
    {
      id: 1,
      src: 'https://picsum.photos/300/200?random=1',
      title: '夕阳风景',
      description: '山峦上的美丽夕阳',
      camera: '佳能 EOS R5',
      settings: 'f/8, 1/250s, ISO 100',
      location: '科罗拉多州落基山脉'
    },
    {
      id: 2,
      src: 'https://picsum.photos/300/200?random=2',
      title: '城市建筑',
      description: '现代建筑设计',
      camera: '索尼 A7R IV',
      settings: 'f/11, 1/125s, ISO 200',
      location: '西雅图市中心'
    }
  ];
  
  function showImageHint(image) {
    return function(event) {
      const hint = `
        ${image.title}
        
        ${image.description}
        
        相机: ${image.camera}
        设置: ${image.settings}
        位置: ${image.location}
      `;
      window.Hint.show(event.target, hint, event.clientX, event.clientY);
    };
  }
</script>

<div class="gallery">
  {#each images as image}
    <div 
      class="image-card"
      on:mouseenter={showImageHint(image)}
    >
      <img src={image.src} alt={image.title} />
      <div class="image-overlay">
        <h4>{image.title}</h4>
      </div>
    </div>
  {/each}
</div>

<style>
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
    padding: 20px;
  }
  
  .image-card {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
    cursor: pointer;
    transition: transform 0.2s;
  }
  
  .image-card:hover {
    transform: translateY(-4px);
  }
  
  .image-card img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
  
  .image-overlay {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    background: linear-gradient(transparent, rgba(0,0,0,0.7));
    color: white;
    padding: 20px;
  }
  
  .image-overlay h4 {
    margin: 0;
    font-size: 16px;
  }
</style>
```

### 进度指示器状态

```svelte
<script>
  let tasks = [
    { 
      id: 1, 
      name: '数据库迁移', 
      progress: 75, 
      status: '进行中',
      eta: '剩余2小时',
      details: '正在迁移用户数据表（4个中已完成3个）'
    },
    { 
      id: 2, 
      name: 'API测试', 
      progress: 100, 
      status: '已完成',
      eta: '已完成',
      details: '所有端点测试成功，覆盖率100%'
    },
    { 
      id: 3, 
      name: 'UI更新', 
      progress: 45, 
      status: '进行中',
      eta: '剩余4小时',
      details: '正在更新12个组件（12个中已完成5个）'
    }
  ];
  
  function showProgressHint(task) {
    return function(event) {
      const hint = `
        任务: ${task.name}
        状态: ${task.status}
        进度: ${task.progress}%
        预计完成: ${task.eta}
        
        详情: ${task.details}
      `;
      window.Hint.show(event.target, hint, event.clientX, event.clientY);
    };
  }
</script>

<div class="progress-dashboard">
  <h3>任务进度</h3>
  {#each tasks as task}
    <div class="progress-item">
      <div class="progress-info">
        <span class="task-name">{task.name}</span>
        <span class="progress-percent">{task.progress}%</span>
      </div>
      <div 
        class="progress-bar"
        on:mouseenter={showProgressHint(task)}
      >
        <div 
          class="progress-fill"
          style="width: {task.progress}%"
        ></div>
      </div>
    </div>
  {/each}
</div>

<style>
  .progress-dashboard {
    max-width: 500px;
    padding: 20px;
  }
  
  .progress-item {
    margin-bottom: 16px;
  }
  
  .progress-info {
    display: flex;
    justify-content: space-between;
    margin-bottom: 8px;
  }
  
  .task-name {
    font-weight: 600;
  }
  
  .progress-percent {
    color: #666;
  }
  
  .progress-bar {
    height: 8px;
    background: #e0e0e0;
    border-radius: 4px;
    overflow: hidden;
    cursor: pointer;
  }
  
  .progress-fill {
    height: 100%;
    background: linear-gradient(90deg, #007bff, #28a745);
    transition: width 0.3s ease;
  }
</style>
```

### 动态内容条件提示

```svelte
<script>
  let user = {
    name: '张三',
    email: 'zhangsan@example.com',
    role: '管理员',
    lastActive: '2024-01-15T10:30:00Z',
    permissions: ['读取', '写入', '删除', '管理']
  };
  
  function showUserBadgeHint(event) {
    const now = new Date();
    const lastActive = new Date(user.lastActive);
    const hoursAgo = Math.floor((now - lastActive) / (1000 * 60 * 60));
    
    let activityStatus;
    if (hoursAgo < 1) {
      activityStatus = '当前在线';
    } else if (hoursAgo < 24) {
      activityStatus = `${hoursAgo}小时前在线`;
    } else {
      const daysAgo = Math.floor(hoursAgo / 24);
      activityStatus = `${daysAgo}天前在线`;
    }
    
    const hint = `
      ${user.name} (${user.role.toUpperCase()})
      ${user.email}
      
      ${activityStatus}
      
      权限: ${user.permissions.join('、')}
    `;
    
    window.Hint.show(event.target, hint, event.clientX, event.clientY);
  }
  
  function showPermissionHint(permission) {
    return function(event) {
      const permissionDescriptions = {
        '读取': '可以查看所有内容和数据',
        '写入': '可以创建和编辑内容',
        '删除': '可以删除内容和数据',
        '管理': '拥有所有功能的完全管理权限'
      };
      
      const hint = permissionDescriptions[permission] || '未知权限';
      window.Hint.show(event.target, hint, event.clientX, event.clientY);
    };
  }
</script>

<div class="user-profile">
  <div 
    class="user-badge"
    on:mouseenter={showUserBadgeHint}
  >
    <div class="avatar">
      {user.name.charAt(0)}
    </div>
    <div class="user-info">
      <div class="user-name">{user.name}</div>
      <div class="user-role">{user.role}</div>
    </div>
  </div>
  
  <div class="permissions">
    <h4>权限</h4>
    <div class="permission-badges">
      {#each user.permissions as permission}
        <span 
          class="permission-badge"
          on:mouseenter={showPermissionHint(permission)}
        >
          {permission}
        </span>
      {/each}
    </div>
  </div>
</div>

<style>
  .user-profile {
    max-width: 300px;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
  
  .user-badge {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
    padding: 8px;
    border-radius: 6px;
    transition: background-color 0.2s;
  }
  
  .user-badge:hover {
    background-color: #f5f5f5;
  }
  
  .avatar {
    width: 40px;
    height: 40px;
    background: #007bff;
    color: white;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
  }
  
  .user-name {
    font-weight: 600;
  }
  
  .user-role {
    color: #666;
    font-size: 14px;
  }
  
  .permissions {
    margin-top: 20px;
  }
  
  .permissions h4 {
    margin: 0 0 12px 0;
  }
  
  .permission-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }
  
  .permission-badge {
    background: #e3f2fd;
    color: #1976d2;
    padding: 4px 8px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
  }
  
  .permission-badge:hover {
    background: #bbdefb;
  }
</style>
```

## 最佳实践

1. **有意义的内容**: 在提示中提供真正有用的信息
2. **适当的时机**: 500毫秒延迟防止意外触发
3. **简洁的文本**: 保持提示文本简短但信息丰富
4. **一致的定位**: 使用鼠标坐标进行自然定位
5. **性能**: 避免在提示生成中进行复杂计算
6. **无障碍性**: 考虑键盘用户和屏幕阅读器
7. **内容层次**: 将提示用于次要信息，而非主要操作

## 样式

PopupHint 组件可以通过CSS进行样式设置：

```css
.uniface-popup-hint {
  position: fixed;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 13px;
  line-height: 1.4;
  z-index: 10000;
  pointer-events: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  max-width: 300px;
  word-wrap: break-word;
}

.uniface-popup-hint::before {
  content: '';
  position: absolute;
  top: -4px;
  left: 8px;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 4px solid rgba(0, 0, 0, 0.8);
}
```

## 无障碍功能

- 提示是补充信息，不是关键UI元素
- 不干扰键盘导航
- 内容是增强理解的描述性文本
- 适当使用时与屏幕阅读器兼容
- 不捕获焦点或阻止交互

## 性能考虑

- 使用事件委托实现高效的内存管理
- 自动清理事件监听器防止内存泄漏
- 门户渲染确保正确的层级而不重新计算布局
- 最小的DOM操作实现流畅性能

## 相关组件

- [Tooltip](../tooltip/README.md) - 用于更结构化的工具提示内容
- [MessageBox](../message-box/README.md) - 用于模态信息对话框
- [Toast](../toast/README.md) - 用于通知消息
- [Dialog](../dialog/README.md) - 用于复杂的交互内容