# ImageFilesField 图片文件上传组件

一个全面的图片上传和管理组件，提供拖放上传功能、图片预览、缩略图生成和文件管理功能。适用于处理图片库、头像上传以及带视觉预览的文档附件。

## 安装

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

## 导入

```typescript
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
import type { ImageFile, RemoveConfirm } from "@ticatec/uniface-element/ImageFilesField";
import type { FileUpload } from "@ticatec/uniface-element";
```

## 基本用法

```svelte
<script>
  import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
  
  let files = [];
  
  // 简单上传函数
  const uploadImage = (file, progressUpdate, onUploaded, errorHandler) => {
    // 模拟上传进度
    let progress = 0;
    const interval = setInterval(() => {
      progress += 10;
      progressUpdate(Math.floor(file.size / 100 * progress));
      
      if (progress >= 100) {
        clearInterval(interval);
        // 调用 onUploaded 返回上传的文件URL和缩略图URL
        onUploaded(`/uploads/${file.name}`, `/thumbnails/${file.name}`);
      }
    }, 100);
    
    return {
      cancel: async () => {
        clearInterval(interval);
        return true;
      }
    };
  };
</script>

<ImageFilesField 
  bind:files={files}
  uploadFile={uploadImage}
  maxFiles={5}
/>
```

## 属性

| 属性 | 类型 | 默认值 | 描述 |
|------|------|---------|-------------|
| `files` | `Array<ImageFile>` | `[]` | 已上传的图片文件数组 |
| `uploadFile` | `FileUpload` | 必需 | 上传文件处理函数 |
| `maxFiles` | `number` | `1` | 允许的最大文件数量 |
| `variant` | `"" \| "plain" \| "outlined" \| "filled"` | `""` | 视觉变体 |
| `disabled` | `boolean` | `false` | 组件是否禁用 |
| `readonly` | `boolean` | `false` | 组件是否只读 |
| `displayMode` | `DisplayMode` | `DisplayMode.Edit` | 显示模式（编辑或查看） |
| `removeFileConfirm` | `RemoveConfirm \| null` | `null` | 文件移除确认函数 |
| `style` | `string` | `""` | 附加 CSS 样式 |
| `text` | `string \| null` | `null` | 文件的文本表示（自动生成） |

## 类型

### ImageFile

```typescript
interface ImageFile {
  name: string;        // 文件名称
  uri: string;         // 完整尺寸图片URL
  thumbnail?: string;  // 缩略图URL（可选）
}
```

### RemoveConfirm

```typescript
type RemoveConfirm = (file: ImageFile) => Promise<boolean>;
```

### FileUpload

```typescript
type FileUpload = (
  file: File,
  progressUpdate: ProgressUpdate,
  onUploaded: OnUploaded,
  errorHandler: ErrorHandler
) => UploadProgress;

type ProgressUpdate = (bytesUploaded: number) => void;
type OnUploaded = (url: string, thumbnail?: string) => void;
type ErrorHandler = (error: Error) => void;

interface UploadProgress {
  cancel: () => Promise<boolean>;
}
```

## 示例

### 基本图片上传与预览

```svelte
<script>
  import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
  import FormField from "@ticatec/uniface-element/FormField";
  
  let profileImages = [];
  
  const uploadProfileImage = (file, progressUpdate, onUploaded, errorHandler) => {
    const formData = new FormData();
    formData.append('image', file);
    
    const xhr = new XMLHttpRequest();
    
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        progressUpdate(e.loaded);
      }
    });
    
    xhr.onload = () => {
      if (xhr.status === 200) {
        const response = JSON.parse(xhr.responseText);
        onUploaded(response.url, response.thumbnail);
      } else {
        errorHandler(new Error(`上传失败: ${xhr.statusText}`));
      }
    };
    
    xhr.onerror = () => {
      errorHandler(new Error('上传过程中发生网络错误'));
    };
    
    xhr.open('POST', '/api/upload/profile-image');
    xhr.send(formData);
    
    return {
      cancel: async () => {
        xhr.abort();
        return true;
      }
    };
  };
  
  const confirmRemoveImage = async (file) => {
    return confirm(`确定要移除 ${file.name} 吗？`);
  };
</script>

<div class="profile-section">
  <FormField label="个人头像">
    <ImageFilesField 
      bind:files={profileImages}
      uploadFile={uploadProfileImage}
      removeFileConfirm={confirmRemoveImage}
      maxFiles={3}
      variant="outlined"
    />
  </FormField>
  
  <div class="image-info">
    <p>已上传 {profileImages.length} 张图片</p>
    {#each profileImages as image}
      <p>• {image.name}</p>
    {/each}
  </div>
</div>

<style>
  .profile-section {
    max-width: 600px;
    margin: 20px;
    padding: 20px;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
  }
  
  .image-info {
    margin-top: 16px;
    padding: 12px;
    background: #f8fafc;
    border-radius: 6px;
  }
</style>
```

### 产品图片库管理

```svelte
<script>
  import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
  import FormField from "@ticatec/uniface-element/FormField";
  
  let productImages = [
    {
      name: "product-main.jpg",
      uri: "/images/products/product-main.jpg",
      thumbnail: "/images/products/thumbs/product-main.jpg"
    },
    {
      name: "product-side.jpg", 
      uri: "/images/products/product-side.jpg",
      thumbnail: "/images/products/thumbs/product-side.jpg"
    }
  ];
  
  let isUploading = false;
  let uploadProgress = {};
  
  const uploadProductImage = (file, progressUpdate, onUploaded, errorHandler) => {
    isUploading = true;
    const fileId = `${file.name}-${Date.now()}`;
    uploadProgress[fileId] = 0;
    
    // 模拟图片处理和上传
    const totalSteps = 100;
    let currentStep = 0;
    
    const interval = setInterval(() => {
      currentStep += Math.random() * 5;
      
      if (currentStep >= totalSteps) {
        currentStep = totalSteps;
        clearInterval(interval);
        
        // 模拟成功上传
        const baseUrl = '/api/products/images';
        const imageUrl = `${baseUrl}/${fileId}.jpg`;
        const thumbnailUrl = `${baseUrl}/thumbs/${fileId}.jpg`;
        
        onUploaded(imageUrl, thumbnailUrl);
        delete uploadProgress[fileId];
        isUploading = false;
      }
      
      const bytesUploaded = Math.floor(file.size * (currentStep / totalSteps));
      progressUpdate(bytesUploaded);
      uploadProgress[fileId] = currentStep;
      uploadProgress = { ...uploadProgress };
    }, 50);
    
    return {
      cancel: async () => {
        clearInterval(interval);
        delete uploadProgress[fileId];
        uploadProgress = { ...uploadProgress };
        isUploading = false;
        return true;
      }
    };
  };
  
  const confirmRemoveProductImage = async (file) => {
    const result = await new Promise((resolve) => {
      const modal = document.createElement('div');
      modal.innerHTML = `
        <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
          <div style="background: white; padding: 24px; border-radius: 8px; max-width: 400px;">
            <h3>移除图片</h3>
            <p>确定要移除 <strong>${file.name}</strong> 吗？</p>
            <p>此操作无法撤销。</p>
            <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
              <button id="cancel">取消</button>
              <button id="confirm" style="background: #dc2626; color: white;">移除</button>
            </div>
          </div>
        </div>
      `;
      
      document.body.appendChild(modal);
      
      modal.querySelector('#cancel').onclick = () => {
        document.body.removeChild(modal);
        resolve(false);
      };
      
      modal.querySelector('#confirm').onclick = () => {
        document.body.removeChild(modal);
        resolve(true);
      };
    });
    
    return result;
  };
  
  $: totalImages = productImages.length;
  $: canAddMore = totalImages < 10;
</script>

<div class="product-gallery">
  <h2>产品图片库</h2>
  
  <FormField label="产品图片（最多10张）" error={!canAddMore ? "最多允许10张图片" : ""}>
    <ImageFilesField 
      bind:files={productImages}
      uploadFile={uploadProductImage}
      removeFileConfirm={confirmRemoveProductImage}
      maxFiles={10}
      variant="filled"
      disabled={!canAddMore}
    />
  </FormField>
  
  <div class="gallery-stats">
    <div class="stat-item">
      <span class="stat-label">图片总数:</span>
      <span class="stat-value">{totalImages}/10</span>
    </div>
    
    {#if isUploading}
      <div class="upload-status">
        <span>正在上传图片...</span>
        <div class="progress-indicators">
          {#each Object.entries(uploadProgress) as [fileId, progress]}
            <div class="progress-bar">
              <div class="progress-fill" style="width: {progress}%"></div>
            </div>
          {/each}
        </div>
      </div>
    {/if}
  </div>
  
  <div class="image-list">
    <h3>当前图片</h3>
    {#each productImages as image, index}
      <div class="image-item">
        <img src={image.thumbnail || image.uri} alt={image.name} width="50" height="50" />
        <div class="image-details">
          <span class="image-name">{image.name}</span>
          <span class="image-position">位置 {index + 1}</span>
        </div>
      </div>
    {/each}
  </div>
</div>

<style>
  .product-gallery {
    max-width: 800px;
    margin: 20px;
    padding: 24px;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
  }
  
  .gallery-stats {
    margin: 16px 0;
    padding: 12px;
    background: #f8fafc;
    border-radius: 6px;
  }
  
  .stat-item {
    display: flex;
    justify-content: space-between;
    margin-bottom: 8px;
  }
  
  .stat-label {
    font-weight: 500;
  }
  
  .stat-value {
    color: #3b82f6;
    font-weight: 600;
  }
  
  .upload-status {
    margin-top: 12px;
  }
  
  .progress-indicators {
    margin-top: 8px;
  }
  
  .progress-bar {
    height: 4px;
    background: #e5e7eb;
    border-radius: 2px;
    overflow: hidden;
    margin: 4px 0;
  }
  
  .progress-fill {
    height: 100%;
    background: #3b82f6;
    transition: width 0.3s ease;
  }
  
  .image-list {
    margin-top: 20px;
  }
  
  .image-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px;
    border-bottom: 1px solid #f3f4f6;
  }
  
  .image-item img {
    border-radius: 4px;
    object-fit: cover;
  }
  
  .image-details {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  
  .image-name {
    font-weight: 500;
  }
  
  .image-position {
    font-size: 0.875rem;
    color: #6b7280;
  }
</style>
```

### 头像上传与裁剪

```svelte
<script>
  import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
  import FormField from "@ticatec/uniface-element/FormField";
  import { DisplayMode } from "@ticatec/uniface-element";
  
  let userAvatar = [];
  let displayMode = DisplayMode.Edit;
  
  const uploadAvatar = (file, progressUpdate, onUploaded, errorHandler) => {
    // 验证文件类型
    if (!file.type.startsWith('image/')) {
      errorHandler(new Error('请选择图片文件'));
      return { cancel: async () => true };
    }
    
    // 验证文件大小（最大5MB）
    if (file.size > 5 * 1024 * 1024) {
      errorHandler(new Error('图片必须小于5MB'));
      return { cancel: async () => true };
    }
    
    // 创建图片用于验证
    const img = new Image();
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    return new Promise((resolve) => {
      img.onload = () => {
        // 设置画布大小为正方形缩略图
        const size = 200;
        canvas.width = size;
        canvas.height = size;
        
        // 计算裁剪尺寸（中心裁剪为正方形）
        const minDimension = Math.min(img.width, img.height);
        const x = (img.width - minDimension) / 2;
        const y = (img.height - minDimension) / 2;
        
        // 绘制裁剪并调整大小的图片
        ctx.drawImage(img, x, y, minDimension, minDimension, 0, 0, size, size);
        
        // 模拟上传进度
        let progress = 0;
        const interval = setInterval(() => {
          progress += 10;
          progressUpdate(Math.floor(file.size * (progress / 100)));
          
          if (progress >= 100) {
            clearInterval(interval);
            
            // 将画布转换为blob并创建URL
            canvas.toBlob((blob) => {
              const thumbnailUrl = URL.createObjectURL(blob);
              const fullUrl = URL.createObjectURL(file);
              
              onUploaded(fullUrl, thumbnailUrl);
            }, 'image/jpeg', 0.8);
          }
        }, 100);
        
        resolve({
          cancel: async () => {
            clearInterval(interval);
            return true;
          }
        });
      };
      
      img.onerror = () => {
        errorHandler(new Error('无效的图片文件'));
        resolve({ cancel: async () => true });
      };
      
      img.src = URL.createObjectURL(file);
    });
  };
  
  const confirmRemoveAvatar = async (file) => {
    return confirm('移除您的个人头像？');
  };
  
  const toggleMode = () => {
    displayMode = displayMode === DisplayMode.Edit ? DisplayMode.View : DisplayMode.Edit;
  };
  
  $: hasAvatar = userAvatar.length > 0;
</script>

<div class="avatar-section">
  <div class="avatar-header">
    <h2>个人头像</h2>
    <button on:click={toggleMode} class="mode-toggle">
      {displayMode === DisplayMode.Edit ? '预览模式' : '编辑模式'}
    </button>
  </div>
  
  <div class="avatar-container">
    <FormField label="上传头像">
      <ImageFilesField 
        bind:files={userAvatar}
        uploadFile={uploadAvatar}
        removeFileConfirm={confirmRemoveAvatar}
        maxFiles={1}
        variant="outlined"
        {displayMode}
        style="min-height: 200px; display: flex; align-items: center; justify-content: center;"
      />
    </FormField>
    
    {#if !hasAvatar && displayMode === DisplayMode.Edit}
      <div class="avatar-placeholder">
        <div class="placeholder-icon">👤</div>
        <p>点击上传您的个人头像</p>
        <p class="placeholder-hint">推荐：正方形图片，最大5MB</p>
      </div>
    {/if}
  </div>
  
  {#if hasAvatar}
    <div class="avatar-info">
      <h3>当前头像</h3>
      <div class="avatar-details">
        <img 
          src={userAvatar[0].thumbnail || userAvatar[0].uri} 
          alt="头像预览"
          class="avatar-preview"
        />
        <div class="avatar-metadata">
          <p><strong>文件:</strong> {userAvatar[0].name}</p>
          <p><strong>状态:</strong> ✅ 上传成功</p>
        </div>
      </div>
    </div>
  {/if}
</div>

<style>
  .avatar-section {
    max-width: 500px;
    margin: 20px;
    padding: 24px;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
  }
  
  .avatar-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
  }
  
  .mode-toggle {
    padding: 6px 12px;
    background: #f3f4f6;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    cursor: pointer;
  }
  
  .avatar-container {
    position: relative;
  }
  
  .avatar-placeholder {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    pointer-events: none;
  }
  
  .placeholder-icon {
    font-size: 48px;
    margin-bottom: 12px;
  }
  
  .placeholder-hint {
    font-size: 0.875rem;
    color: #6b7280;
  }
  
  .avatar-info {
    margin-top: 20px;
    padding: 16px;
    background: #f8fafc;
    border-radius: 8px;
  }
  
  .avatar-details {
    display: flex;
    align-items: center;
    gap: 16px;
  }
  
  .avatar-preview {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    object-fit: cover;
    border: 3px solid white;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }
  
  .avatar-metadata p {
    margin: 4px 0;
  }
</style>
```

### 文档附件与缩略图

```svelte
<script>
  import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
  import FormField from "@ticatec/uniface-element/FormField";
  
  let documentImages = [];
  let uploadErrors = [];
  
  const uploadDocument = (file, progressUpdate, onUploaded, errorHandler) => {
    // 清除之前的错误
    uploadErrors = [];
    
    // 验证文件类型（本例仅限图片）
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!allowedTypes.includes(file.type)) {
      const error = new Error(`不支持的文件类型: ${file.type}。请上传JPEG、PNG、GIF或WebP图片。`);
      errorHandler(error);
      uploadErrors = [...uploadErrors, error.message];
      return { cancel: async () => true };
    }
    
    // 模拟文档处理（OCR、缩略图生成等）
    let progress = 0;
    const stages = [
      '正在上传文件...',
      '正在生成缩略图...',
      '正在处理文档...',
      '正在保存到数据库...',
      '完成！'
    ];
    let currentStage = 0;
    
    const interval = setInterval(() => {
      progress += Math.random() * 15;
      
      if (progress > (currentStage + 1) * 20 && currentStage < stages.length - 1) {
        currentStage++;
        console.log(stages[currentStage]);
      }
      
      if (progress >= 100) {
        progress = 100;
        clearInterval(interval);
        
        // 模拟成功上传
        const documentId = Date.now().toString();
        const documentUrl = `/api/documents/${documentId}`;
        const thumbnailUrl = `/api/documents/${documentId}/thumbnail`;
        
        onUploaded(documentUrl, thumbnailUrl);
      }
      
      progressUpdate(Math.floor(file.size * (progress / 100)));
    }, 200);
    
    return {
      cancel: async () => {
        clearInterval(interval);
        console.log('上传已取消');
        return true;
      }
    };
  };
  
  const confirmRemoveDocument = async (file) => {
    const userConfirmed = confirm(`移除文档 "${file.name}"？\n\n这将永久删除文档及其缩略图。`);
    
    if (userConfirmed) {
      // 模拟API调用以删除文档
      console.log('正在删除文档:', file.name);
      // 这里可以添加实际删除逻辑
    }
    
    return userConfirmed;
  };
  
  $: documentCount = documentImages.length;
  $: hasErrors = uploadErrors.length > 0;
</script>

<div class="document-section">
  <h2>文档附件</h2>
  
  <FormField label="上传文档" error={hasErrors ? uploadErrors.join(', ') : ''}>
    <ImageFilesField 
      bind:files={documentImages}
      uploadFile={uploadDocument}
      removeFileConfirm={confirmRemoveDocument}
      maxFiles={20}
      variant="filled"
    />
  </FormField>
  
  <div class="document-summary">
    <div class="summary-stats">
      <div class="stat">
        <span class="stat-number">{documentCount}</span>
        <span class="stat-label">文档</span>
      </div>
      <div class="stat">
        <span class="stat-number">{20 - documentCount}</span>
        <span class="stat-label">剩余</span>
      </div>
    </div>
    
    {#if hasErrors}
      <div class="error-panel">
        <h4>上传错误</h4>
        {#each uploadErrors as error}
          <p class="error-message">❌ {error}</p>
        {/each}
      </div>
    {/if}
  </div>
  
  {#if documentCount > 0}
    <div class="document-list">
      <h3>已上传文档</h3>
      <div class="document-grid">
        {#each documentImages as doc, index}
          <div class="document-card">
            <div class="document-thumbnail">
              <img src={doc.thumbnail || doc.uri} alt={doc.name} />
              <div class="document-index">{index + 1}</div>
            </div>
            <div class="document-info">
              <p class="document-name" title={doc.name}>{doc.name}</p>
              <p class="document-status">✅ 已处理</p>
            </div>
          </div>
        {/each}
      </div>
    </div>
  {/if}
</div>

<style>
  .document-section {
    max-width: 800px;
    margin: 20px;
    padding: 24px;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
  }
  
  .document-summary {
    margin: 16px 0;
  }
  
  .summary-stats {
    display: flex;
    gap: 24px;
    margin-bottom: 16px;
  }
  
  .stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 12px;
    background: #f8fafc;
    border-radius: 8px;
    min-width: 80px;
  }
  
  .stat-number {
    font-size: 1.5rem;
    font-weight: 600;
    color: #3b82f6;
  }
  
  .stat-label {
    font-size: 0.875rem;
    color: #6b7280;
  }
  
  .error-panel {
    padding: 12px;
    background: #fef2f2;
    border: 1px solid #fecaca;
    border-radius: 6px;
  }
  
  .error-message {
    margin: 4px 0;
    color: #dc2626;
    font-size: 0.875rem;
  }
  
  .document-list {
    margin-top: 24px;
  }
  
  .document-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 16px;
    margin-top: 12px;
  }
  
  .document-card {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    overflow: hidden;
    background: white;
  }
  
  .document-thumbnail {
    position: relative;
    height: 120px;
    background: #f9fafb;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .document-thumbnail img {
    max-width: 100%;
    max-height: 100%;
    object-fit: cover;
  }
  
  .document-index {
    position: absolute;
    top: 8px;
    right: 8px;
    background: #3b82f6;
    color: white;
    border-radius: 50%;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    font-weight: 600;
  }
  
  .document-info {
    padding: 12px;
  }
  
  .document-name {
    margin: 0 0 4px 0;
    font-weight: 500;
    font-size: 0.875rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  
  .document-status {
    margin: 0;
    font-size: 0.75rem;
    color: #16a34a;
  }
</style>
```

### 自定义上传进度与错误处理

```svelte
<script>
  import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
  import FormField from "@ticatec/uniface-element/FormField";
  
  let galleryImages = [];
  let activeUploads = new Map();
  let uploadHistory = [];
  
  const uploadWithErrorHandling = (file, progressUpdate, onUploaded, errorHandler) => {
    const uploadId = Date.now() + Math.random();
    
    // 添加到活跃上传跟踪
    activeUploads.set(uploadId, {
      file: file.name,
      startTime: new Date(),
      progress: 0
    });
    activeUploads = new Map(activeUploads);
    
    // 模拟各种上传场景
    const scenarios = [
      { success: true, delay: 2000 },   // 2秒后成功
      { success: false, error: '网络超时', delay: 3000 },  // 网络错误
      { success: false, error: '服务器错误 500', delay: 1500 }, // 服务器错误
      { success: true, delay: 4000 },   // 慢速成功
    ];
    
    const scenario = scenarios[Math.floor(Math.random() * scenarios.length)];
    let progress = 0;
    let cancelled = false;
    
    const interval = setInterval(() => {
      if (cancelled) {
        clearInterval(interval);
        return;
      }
      
      progress += Math.random() * 20;
      
      // 更新跟踪
      if (activeUploads.has(uploadId)) {
        activeUploads.get(uploadId).progress = Math.min(progress, 95);
        activeUploads = new Map(activeUploads);
      }
      
      progressUpdate(Math.floor(file.size * (Math.min(progress, 95) / 100)));
      
      if (progress >= 100) {
        clearInterval(interval);
        
        setTimeout(() => {
          if (cancelled) return;
          
          // 从活跃上传中移除
          activeUploads.delete(uploadId);
          activeUploads = new Map(activeUploads);
          
          if (scenario.success) {
            // 成功上传
            const imageUrl = `/uploads/${uploadId}_${file.name}`;
            const thumbnailUrl = `/uploads/thumbs/${uploadId}_${file.name}`;
            
            onUploaded(imageUrl, thumbnailUrl);
            
            // 添加到历史记录
            uploadHistory = [{
              file: file.name,
              status: 'success',
              timestamp: new Date(),
              message: '上传成功完成'
            }, ...uploadHistory];
          } else {
            // 上传失败
            const error = new Error(scenario.error);
            errorHandler(error);
            
            // 添加到历史记录
            uploadHistory = [{
              file: file.name,
              status: 'error',
              timestamp: new Date(),
              message: scenario.error
            }, ...uploadHistory];
          }
        }, scenario.delay - (progress > 95 ? (progress - 95) * 50 : 0));
      }
    }, 100);
    
    return {
      cancel: async () => {
        cancelled = true;
        clearInterval(interval);
        
        // 从活跃上传中移除
        activeUploads.delete(uploadId);
        activeUploads = new Map(activeUploads);
        
        // 添加到历史记录
        uploadHistory = [{
          file: file.name,
          status: 'cancelled',
          timestamp: new Date(),
          message: '用户取消上传'
        }, ...uploadHistory];
        
        return true;
      }
    };
  };
  
  const confirmRemoveGalleryImage = async (file) => {
    const shouldRemove = confirm(`从图片库中移除 "${file.name}"？`);
    
    if (shouldRemove) {
      // 添加到历史记录
      uploadHistory = [{
        file: file.name,
        status: 'removed',
        timestamp: new Date(),
        message: '从图片库中移除图片'
      }, ...uploadHistory];
    }
    
    return shouldRemove;
  };
  
  const clearHistory = () => {
    uploadHistory = [];
  };
  
  const formatTime = (date) => {
    return date.toLocaleTimeString('zh-CN');
  };
  
  const getStatusIcon = (status) => {
    const icons = {
      success: '✅',
      error: '❌',
      cancelled: '⏹️',
      removed: '🗑️'
    };
    return icons[status] || '📄';
  };
  
  $: hasActiveUploads = activeUploads.size > 0;
  $: totalImages = galleryImages.length;
</script>

<div class="advanced-upload">
  <h2>高级图片上传</h2>
  
  <FormField label="图片库">
    <ImageFilesField 
      bind:files={galleryImages}
      uploadFile={uploadWithErrorHandling}
      removeFileConfirm={confirmRemoveGalleryImage}
      maxFiles={15}
      variant="outlined"
    />
  </FormField>
  
  {#if hasActiveUploads}
    <div class="active-uploads">
      <h3>活跃上传</h3>
      {#each Array.from(activeUploads.entries()) as [id, upload]}
        <div class="upload-item">
          <div class="upload-info">
            <span class="upload-filename">{upload.file}</span>
            <span class="upload-time">开始时间: {formatTime(upload.startTime)}</span>
          </div>
          <div class="upload-progress">
            <div class="progress-bar">
              <div class="progress-fill" style="width: {upload.progress}%"></div>
            </div>
            <span class="progress-text">{Math.round(upload.progress)}%</span>
          </div>
        </div>
      {/each}
    </div>
  {/if}
  
  <div class="gallery-stats">
    <div class="stat-row">
      <span>图片总数: {totalImages}/15</span>
      <span>活跃上传: {activeUploads.size}</span>
    </div>
  </div>
  
  {#if uploadHistory.length > 0}
    <div class="upload-history">
      <div class="history-header">
        <h3>上传历史</h3>
        <button on:click={clearHistory} class="clear-history">清除</button>
      </div>
      
      <div class="history-list">
        {#each uploadHistory.slice(0, 10) as entry}
          <div class="history-item" class:error={entry.status === 'error'}>
            <span class="history-icon">{getStatusIcon(entry.status)}</span>
            <div class="history-details">
              <span class="history-file">{entry.file}</span>
              <span class="history-message">{entry.message}</span>
            </div>
            <span class="history-time">{formatTime(entry.timestamp)}</span>
          </div>
        {/each}
      </div>
    </div>
  {/if}
</div>

<style>
  .advanced-upload {
    max-width: 800px;
    margin: 20px;
    padding: 24px;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
  }
  
  .active-uploads {
    margin: 20px 0;
    padding: 16px;
    background: #fef3c7;
    border-radius: 8px;
    border-left: 4px solid #f59e0b;
  }
  
  .upload-item {
    display: flex;
    align-items: center;
    gap: 16px;
    margin: 8px 0;
  }
  
  .upload-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  
  .upload-filename {
    font-weight: 500;
  }
  
  .upload-time {
    font-size: 0.75rem;
    color: #6b7280;
  }
  
  .upload-progress {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 120px;
  }
  
  .progress-bar {
    flex: 1;
    height: 6px;
    background: #e5e7eb;
    border-radius: 3px;
    overflow: hidden;
  }
  
  .progress-fill {
    height: 100%;
    background: #3b82f6;
    transition: width 0.3s ease;
  }
  
  .progress-text {
    font-size: 0.875rem;
    font-weight: 500;
    min-width: 40px;
  }
  
  .gallery-stats {
    margin: 16px 0;
    padding: 12px;
    background: #f8fafc;
    border-radius: 6px;
  }
  
  .stat-row {
    display: flex;
    justify-content: space-between;
    font-weight: 500;
  }
  
  .upload-history {
    margin-top: 24px;
  }
  
  .history-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
  }
  
  .clear-history {
    padding: 4px 8px;
    background: #f3f4f6;
    border: 1px solid #d1d5db;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.875rem;
  }
  
  .history-list {
    border: 1px solid #e5e7eb;
    border-radius: 6px;
    overflow: hidden;
  }
  
  .history-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px;
    border-bottom: 1px solid #f3f4f6;
    background: white;
  }
  
  .history-item:last-child {
    border-bottom: none;
  }
  
  .history-item.error {
    background: #fef2f2;
  }
  
  .history-icon {
    font-size: 1.25rem;
  }
  
  .history-details {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  
  .history-file {
    font-weight: 500;
    font-size: 0.875rem;
  }
  
  .history-message {
    font-size: 0.75rem;
    color: #6b7280;
  }
  
  .history-time {
    font-size: 0.75rem;
    color: #9ca3af;
    min-width: 80px;
    text-align: right;
  }
</style>
```

## 样式

ImageFilesField 组件可通过 CSS 自定义属性进行样式化：

```css
.uniface-images-field {
  --border-color: #e2e8f0;
  --background-color: #ffffff;
  --hover-background: #f9fafb;
  --upload-icon-color: #6b7280;
  --preview-overlay: rgba(0, 0, 0, 0.7);
}

/* 变体特定样式 */
.uniface-common-editor.outlined {
  border: 2px dashed var(--border-color);
  border-radius: 8px;
  padding: 20px;
}

.uniface-common-editor.filled {
  background-color: #f8fafc;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 20px;
}

/* 上传区域样式 */
.editor-action-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  border: 2px dashed #d1d5db;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
}

.editor-action-icon:hover {
  border-color: #3b82f6;
  background-color: #f0f9ff;
}
```

## 可访问性

ImageFilesField 组件包括以下可访问性功能：

- 支持键盘导航
- 屏幕阅读器的 ARIA 标签
- 交互元素的焦点管理
- 图片的替代文本支持
- 上传进度的屏幕阅读器提示

## 最佳实践

1. **文件验证**：在上传前始终验证文件类型和大小
2. **错误处理**：提供清晰的错误消息和重试机制
3. **进度反馈**：显示上传进度并允许取消
4. **缩略图生成**：生成适当的缩略图以提高性能
5. **安全性**：在客户端和服务器端验证文件
6. **存储**：实现适当的文件存储和清理策略
7. **性能**：对于大型图片库考虑懒加载

## 许可证

MIT