国产+高潮+在线,国产 av 仑乱内谢,www国产亚洲精品久久,51国产偷自视频区视频,成人午夜精品网站在线观看

insertGetId 并發(fā) 返回重復(fù)id

windthesky

問(wèn)題描述

用的webman/database 模型

eventLoop使用 Workerman\Events\Fiber::class;
并發(fā)上傳,用insertGetId返回?cái)?shù)據(jù)庫(kù)id,會(huì)重復(fù),數(shù)據(jù)庫(kù)里面是正常自增字段

截圖
截圖
截圖

程序代碼或配置

/**
     * 上傳文件
     * @param Request $request
     * @return Response
     */
    public function upload_file(Request $request): Response
    {
        try {
            add_log('上傳文件');
            $user = $request->token_user;
            $file = $request->file('file');
            $type = $request->post('type');
            $位置 = $request->post('位置');
            if ($file && $type && $file->isValid()) {
                $ext = $file->getUploadExtension();
                if (in_array($ext, ['php', 'html', 'sql', 'json'])) {
                    return response('文件類型錯(cuò)誤', 503);
                }

                if (in_array($type, ['頭像', '頭圖', '詳情圖'])) {
                    if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
                        return response('非圖片格式', 503);
                    }
                    if ($file->getSize() > 1024 * 1024 * 10) {
                        return response('圖片大小不能超過(guò)10M', 503);
                    }
                } else if ($type === '附件') {
                    if (!in_array($ext, ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'zip', 'rar', 'jpg', 'jpeg', 'png', 'gif'])) {
                        return response('附件格式錯(cuò)誤', 503);
                    }
                    if ($file->getSize() > 1024 * 1024 * 100) {
                        return response('附件大小不能超過(guò)100M', 503);
                    }
                } else {
                    return response('參數(shù)錯(cuò)誤', 503);
                }
                $format      = $file->getExtension();
                $upload_name = $file->getUploadName();
                if (empty($upload_name)) return response('文件名錯(cuò)誤,請(qǐng)修改文件名', 503);
                $name = Tool::get_microtime();
                $dir  = public_path() . '/files/upload/' . date('Ym') . '/' . date('d') . '/';
                if (!is_dir($dir)) mkdir($dir, 0777, true);
                $file->move($dir . $name . '.' . $ext);
                $file_url = 'files/upload/' . date('Ym') . '/' . date('d') . '/' . $name . '.' . $ext;
                $path2    = public_path() . '/' . $file_url;

                switch ($type) {
                    case '頭像':
                        Image::load($path2)
                            ->optimize()
                            ->fit(Fit::Stretch, 100, 100)
                            ->quality(75)
                            ->save();
                        break;
                    case '詳情圖':
                        Image::load($path2)
                            ->optimize()
                            ->fit(Fit::Max, 800)
                            ->quality(75)
                            ->save();
                        break;
                    case '頭圖':
                        Image::load($path2)
                            ->optimize()
                            ->fit(Fit::Max, 500)
                            ->quality(75)
                            ->save();
                        break;
                    default:
                        break;
                }

                $imageInfo = getimagesize($path2);
                $fileSize  = filesize($path2);
                $sha1      = sha1_file($path2);

                $file_id = UploadFile::insertGetId([
                    '類型'     => $type,
                    '名稱'     => $upload_name,
                    '網(wǎng)址'     => $file_url,
                    '目錄'     => $path2,
                    '位置'     => $位置,
                    '文件大小' => $fileSize,
                    '文件格式' => $format,
                    '后綴'     => $ext,
                    '錄入人'   => $user['姓名'],
                    'sha1'     => $sha1,
                    'width'    => $imageInfo[0] ?? 0,
                    'height'   => $imageInfo[1] ?? 0,
                ]);

                return res_success([
                    'id'   => $file_id,
                    'name' => $upload_name,
                    'url'  => $file_url,
                    'size' => $fileSize,
                    'type' => $ext,
                ], '上傳成功');
            }
            return response('參數(shù)錯(cuò)誤', 503);
        } catch (Throwable $e) {
            write_log('exception', 'error', '上傳文件', $e);
            return response('上傳失敗', 503);
        }
    }

重現(xiàn)問(wèn)題的步驟

并發(fā)同時(shí)請(qǐng)求就會(huì)

操作系統(tǒng)環(huán)境及workerman/webman等具體版本

這里寫具體的系統(tǒng)環(huán)境相關(guān)信息
截圖

274 8 0
8個(gè)回答

tanhongbin

不會(huì)吧,你不用fiber 試試呢

  • windthesky 1天前

    'eventLoop' => ''也一樣

  • tanhongbin 1天前

    不可能,絕對(duì)不可能,你用的那個(gè)orm,lv的還是tp的?

  • windthesky 1天前

    webman/database 是 用laravel的illuminate/database

  • tanhongbin 1天前

    這個(gè)我測(cè)試過(guò) 真沒(méi)發(fā)現(xiàn)還有這個(gè)問(wèn)題呢,我也壓力測(cè)試過(guò),沒(méi)出現(xiàn)重復(fù)的

  • windthesky 1天前

    并發(fā)有小于10毫秒以下嗎?

  • tanhongbin 1天前

    webman內(nèi)網(wǎng)測(cè)試基本都是5ms以內(nèi)的,除非你的mysql太垃圾了

  • windthesky 1天前

    那就奇怪了,而且數(shù)據(jù)庫(kù)是正常的

  • tanhongbin 1天前

    你在linux測(cè)試的嘛,能不能在測(cè)試測(cè)試截個(gè)圖,多少并發(fā)能復(fù)現(xiàn)?

  • windthesky 1天前

    在window測(cè)試,就一個(gè)上傳,同時(shí)上傳幾個(gè)文件,就會(huì)發(fā)生

windthesky

截圖
截圖
截圖
截圖
截圖
多個(gè)上傳都是有重復(fù)的

  • 暫無(wú)評(píng)論
windthesky

截圖
數(shù)據(jù)庫(kù)配置

  • tanhongbin 1天前

    你開啟協(xié)程了嘛???

  • windthesky 1天前

    window不知道有沒(méi)有,但這個(gè)配置是默認(rèn)的,不管有沒(méi)有協(xié)程

  • tanhongbin 1天前

    如果開啟協(xié)程 或者 你使用swon 那個(gè)就會(huì)自動(dòng)變成協(xié)程,本地可以卸載然后evenloop => '' windows更不會(huì)并發(fā)處理就一個(gè)進(jìn)程 你想用多進(jìn)程必須linux代理多端口 才行,我測(cè)試沒(méi)問(wèn)題

windthesky

截圖
截圖
版本如截圖所示

  • 暫無(wú)評(píng)論
windthesky
<!--suppress ExceptionCaughtLocallyJS -->
<template>
  <div class="my-upload">
    <!-- 上傳區(qū)域 -->
    <div
      class="upload-area"
      :class="{
        'is-dragover': isDragover,
        'is-disabled': disabled,
        'has-files': fileList && fileList.length > 0 && !multiple,
      }"
      @click="handleClick"
      @drop="handleDrop"
      @dragover.prevent="handleDragover"
      @dragleave="handleDragleave"
    >
      <input
        ref="fileInputRef"
        type="file"
        :accept="accept"
        :multiple="multiple"
        :disabled="disabled"
        @change="handleFileChange"
        style="display: none"
      />

      <!-- 上傳圖標(biāo)和提示 -->
      <div class="upload-content" v-if="!fileList || !fileList.length || multiple">
        <div class="upload-icon">
          <el-icon size="48"><Upload /></el-icon>
        </div>
        <div class="upload-text">
          <p class="upload-title">{{ uploadText }}</p>
          <p class="upload-subtitle">{{ uploadSubText }}</p>
        </div>
      </div>

      <!-- 單文件預(yù)覽 -->
      <div class="single-file-preview" v-else-if="!multiple && fileList && fileList.length === 1">
        <div class="file-preview">
          <img
            v-if="
              isImageFile(fileList[0]) && !imageLoadErrors.get(fileList[0].uid || fileList[0].name)
            "
            :src="getFilePreviewUrl(fileList[0])"
            :alt="fileList[0].name"
            class="preview-image"
            @load="() => {}"
            @error="() => handleImageError(fileList[0])"
          />
          <div
            v-else-if="
              !isImageFile(fileList[0]) || imageLoadErrors.get(fileList[0].uid || fileList[0].name)
            "
            class="file-icon"
          >
            <el-icon size="48">
              <component :is="getFileIcon(fileList[0])" />
            </el-icon>
          </div>
        </div>
        <div class="file-info">
          <p class="file-name" :title="fileList[0].name">{{ fileList[0].name }}</p>
          <p class="file-size">{{ formatFileSize(fileList[0].size) }}</p>
        </div>
        <div class="file-actions">
          <el-button type="danger" size="small" :icon="Delete" circle @click.stop="removeFile(0)" />
        </div>
      </div>
    </div>

    <!-- 多文件列表 -->
    <div class="file-list" v-if="multiple && fileList && fileList.length > 0">
      <div class="file-item" v-for="(file, index) in fileList" :key="file.uid || index">
        <div class="file-preview">
          <img
            v-if="isImageFile(file) && !imageLoadErrors.get(file.uid || file.name)"
            :src="getFilePreviewUrl(file)"
            :alt="file.name"
            class="preview-thumbnail"
            @load="() => {}"
            @error="() => handleImageError(file)"
          />
          <div
            v-else-if="!isImageFile(file) || imageLoadErrors.get(file.uid || file.name)"
            class="file-icon"
          >
            <el-icon>
              <component :is="getFileIcon(file)" />
            </el-icon>
          </div>
        </div>

        <div class="file-info">
          <p class="file-name" :title="file.name">{{ file.name }}</p>
          <p class="file-size">{{ formatFileSize(file.size) }}</p>

          <!-- 上傳進(jìn)度 -->
          <div class="file-progress" v-if="file.status === 'uploading'">
            <el-progress
              :percentage="Math.max(0, Math.min(100, file.percentage || 0))"
              :stroke-width="4"
              :show-text="false"
            />
          </div>

          <!-- 狀態(tài)標(biāo)識(shí) -->
          <div class="file-status" v-else>
            <el-tag :type="getStatusType(file.status)" size="small">
              {{ getStatusText(file.status) }}
            </el-tag>
          </div>
        </div>

        <div class="file-actions">
          <el-button
            v-if="file.status === 'ready'"
            type="primary"
            size="small"
            :icon="Upload"
            circle
            :disabled="uploading"
            @click="uploadFile(file)"
          />
          <el-button type="danger" size="small" :icon="Delete" circle @click="removeFile(index)" />
        </div>
      </div>
    </div>

    <!-- 上傳控制按鈕 -->
    <div class="upload-controls" v-if="showControls && fileList && fileList.length > 0">
      <el-button
        type="primary"
        :loading="uploading || isChunkUploading"
        :disabled="!hasReadyFiles"
        @click="uploadAll"
      >
        {{ uploading || isChunkUploading ? '上傳中...' : '開始上傳' }}
      </el-button>
      <el-button v-if="!autoUpload" :disabled="uploading || isChunkUploading" @click="clearAll">
        清空列表
      </el-button>

      <!-- 分片上傳取消按鈕 -->
      <el-button
        v-if="isChunkUploading && currentChunkSession"
        type="danger"
        size="small"
        @click="cancelChunkUpload"
      >
        取消上傳
      </el-button>
    </div>

    <!-- 分片上傳進(jìn)度顯示 -->
    <div v-if="isChunkUploading && currentChunkSession" class="chunk-upload-progress mt-16">
      <div class="progress-header">
        <h4>大文件分片上傳進(jìn)度</h4>
        <span class="progress-text"
          >{{ Math.max(0, Math.min(100, chunkUploadProgress)).toFixed(1) }}%</span
        >
      </div>

      <el-progress
        :percentage="Math.max(0, Math.min(100, chunkUploadProgress))"
        :stroke-width="8"
        :text-inside="true"
        :format="() => `${Math.max(0, Math.min(100, chunkUploadProgress)).toFixed(1)}%`"
      />

      <div class="upload-stats mt-8">
        <div class="stat-item">
          <span class="stat-label">文件名稱:</span>
          <span class="stat-value">{{ currentChunkSession.fileName }}</span>
        </div>
        <div class="stat-item">
          <span class="stat-label">文件大?。?lt;/span>
          <span class="stat-value">{{ formatFileSize(currentChunkSession.fileSize) }}</span>
        </div>
        <div class="stat-item">
          <span class="stat-label">上傳速度:</span>
          <span class="stat-value">{{ formatUploadSpeed(uploadSpeed) }}</span>
        </div>
        <div class="stat-item">
          <span class="stat-label">剩余時(shí)間:</span>
          <span class="stat-value">{{ formatRemainingTime(remainingTime) }}</span>
        </div>
        <div class="stat-item">
          <span class="stat-label">狀態(tài):</span>
          <span class="stat-value status" :class="currentChunkSession.status">
            {{ getChunkUploadStatusText(currentChunkSession.status) }}
          </span>
        </div>
      </div>
    </div>

    <!-- 已上傳的文件列表 -->
    <div
      v-if="showUploadedList && uploadedFileList && uploadedFileList.length > 0"
      class="uploaded-files mt-16"
    >
      <h4>已上傳的文件</h4>
      <div class="uploaded-file-list">
        <div v-for="(file, index) in uploadedFileList" :key="file.id" class="uploaded-file-item">
          <div class="file-preview">
            <img
              v-if="isUploadedImageFile(file) && !imageLoadErrors.get(`uploaded_${file.id}`)"
              :src="getUploadedFilePreviewUrl(file)"
              :alt="file.name"
              class="preview-thumbnail"
              @error="() => handleUploadedImageError(file)"
            />
            <div
              v-else-if="!isUploadedImageFile(file) || imageLoadErrors.get(`uploaded_${file.id}`)"
              class="file-icon"
            >
              <el-icon size="24">
                <component :is="getUploadedFileIcon(file)" />
              </el-icon>
            </div>
          </div>

          <div class="file-info">
            <p
              class="file-name clickable"
              :title="file.name + ' (點(diǎn)擊下載)'"
              @click="downloadUploadedFile(file)"
            >
              {{ file.name }}
            </p>
            <p class="file-size">{{ formatFileSize(file.size) }}</p>
          </div>

          <div class="file-actions">
            <el-button
              type="primary"
              size="small"
              :icon="Download"
              circle
              @click="downloadUploadedFile(file)"
              title="下載"
            />
            <el-button
              type="danger"
              size="small"
              :icon="Delete"
              circle
              @click="removeUploadedFile(file, index)"
              title="刪除"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Delete,
  Document,
  Download,
  Files,
  FolderOpened,
  Picture,
  Upload,
} from '@element-plus/icons-vue'
import { baseURL2, chunkUploadConfig, uploadsUrl, deleteUploadFileUrl } from '@/config'

// 文件狀態(tài)類型
type FileStatus = 'ready' | 'uploading' | 'success' | 'error'

// 上傳響應(yīng)接口
interface UploadResponse {
  code?: number
  msg?: string
  data?: Record<string, unknown>
  url?: string
  [key: string]: unknown
}

// 文件對(duì)象接口
interface UploadFile {
  uid?: string
  name: string
  size: number
  type: string
  raw: File
  status: FileStatus
  percentage?: number
  url?: string
  response?: UploadResponse
}

// 已上傳的文件接口
interface UploadedFile {
  id: number | string
  name: string
  size: number
  type: string // 文件擴(kuò)展名
  url: string
}

// 分片上傳會(huì)話接口
interface ChunkUploadSession {
  sessionId: string
  fileId: number
  fileName: string
  fileSize: number
  fileHash: string
  chunkSize: number
  totalChunks: number
  uploadedChunks: number[]
  status: 'ready' | 'uploading' | 'merging' | 'completed' | 'failed' | 'cancelled'
}

// 分片信息接口
interface ChunkInfo {
  index: number
  start: number
  end: number
  blob: Blob
  hash?: string
  status: 'pending' | 'uploading' | 'completed' | 'failed'
  retryCount: number
}

// 組件屬性
interface Props {
  accept?: string
  multiple?: boolean
  disabled?: boolean
  maxSize?: number // MB
  maxCount?: number
  autoUpload?: boolean
  showControls?: boolean
  showUploadedList?: boolean // 是否顯示已上傳文件列表
  uploadText?: string
  uploadSubText?: string
  action?: string // 上傳地址
  headers?: Record<string, string> // 請(qǐng)求頭
  data?: Record<string, unknown> // 上傳數(shù)據(jù)
  beforeUpload?: (file: File) => boolean | Promise<boolean>
  onSuccess?: (response: UploadResponse, file: UploadFile, fileList: UploadFile[]) => void
  onError?: (error: Error, file: UploadFile, fileList: UploadFile[]) => void
  onProgress?: (event: ProgressEvent, file: UploadFile, fileList: UploadFile[]) => void
  onChange?: (file: UploadFile, fileList: UploadFile[]) => void
  onRemove?: (file: UploadFile, fileList: UploadFile[]) => void
  onUploadedFileRemove?: (file: UploadedFile) => void
}

const props = withDefaults(defineProps<Props>(), {
  accept: '.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar,.jpg,.jpeg,.png,.gif',
  multiple: true,
  disabled: false,
  maxSize: 1000, // 默認(rèn)100MB,匹配PHP接口附件類型限制
  maxCount: 10,
  autoUpload: false,
  showControls: true,
  showUploadedList: true, // 默認(rèn)顯示已上傳文件列表
  uploadText: '點(diǎn)擊或拖拽文件到此處上傳',
  uploadSubText: '支持PDF、Office文檔、圖片、壓縮包,單個(gè)文件不超過(guò)100MB',
  action: uploadsUrl, // 使用配置中的上傳地址作為默認(rèn)值
  headers: () => ({
    Authorization: 'Bearer ' + (localStorage.getItem('token') || ''),
    'X-Requested-With': 'XMLHttpRequest',
  }),
  data: () => ({
    type: '附件', // PHP接口要求的文件類型參數(shù)
  }),
})

// 使用 defineModel 進(jìn)行雙向綁定
const uploadedFileList = defineModel<UploadedFile[]>({ default: () => [] })

// 組件內(nèi)部管理的文件列表
const fileList = ref<UploadFile[]>([])

// 事件定義
const emit = defineEmits<{
  change: [file: UploadFile, fileList: UploadFile[]]
  success: [response: UploadResponse, file: UploadFile, fileList: UploadFile[]]
  error: [error: Error, file: UploadFile, fileList: UploadFile[]]
  progress: [event: ProgressEvent, file: UploadFile, fileList: UploadFile[]]
  remove: [file: UploadFile, fileList: UploadFile[]]
  'uploaded-file-remove': [file: UploadedFile]
}>()

// 響應(yīng)式數(shù)據(jù)
const fileInputRef = ref<HTMLInputElement>()
const isDragover = ref(false)
const uploading = ref(false)

// 分片上傳相關(guān)狀態(tài)
const isChunkUploading = ref(false)
const chunkUploadProgress = ref(0)
const currentChunkSession = ref<ChunkUploadSession | null>(null)
const uploadSpeed = ref(0)
const remainingTime = ref(0)

// 存儲(chǔ)已創(chuàng)建的Blob URLs用于清理
const createdBlobUrls = ref<Set<string>>(new Set())

// 跟蹤圖片加載失敗的文件
const imageLoadErrors = ref<Map<string, boolean>>(new Map())

// 處理圖片加載失敗
const handleImageError = (file: UploadFile) => {
  const key = file.uid || file.name
  imageLoadErrors.value.set(key, true)
}

// 處理已上傳文件的圖片加載失敗
const handleUploadedImageError = (file: UploadedFile) => {
  const key = `uploaded_${file.id}`
  imageLoadErrors.value.set(key, true)
}

// 確保uploadedFileList始終是數(shù)組
watch(
  uploadedFileList,
  (newVal) => {
    if (!Array.isArray(newVal)) {
      uploadedFileList.value = []
    }
  },
  { immediate: true },
)

// 計(jì)算屬性
const hasReadyFiles = computed(() => {
  if (!fileList.value || !Array.isArray(fileList.value)) {
    return false
  }
  return fileList.value.some((file) => file && file.status === 'ready')
})

// 判斷是否為圖片文件
const isImageFile = (file: UploadFile): boolean => {
  // 如果有 response.data.type,優(yōu)先使用后端返回的擴(kuò)展名
  const responseType = file.response?.data?.type
  if (responseType) {
    const ext = String(responseType).toLowerCase()
    // 使用后端返回的文件類型判斷
    return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)
  }

  // 對(duì)于未上傳的文件,使用MIME類型和文件名雙重判斷
  const mimeType = file.type.toLowerCase()
  const fileName = file.name.toLowerCase()

  // MIME類型判斷
  const isMimeImage = mimeType.startsWith('image/')

  // 文件擴(kuò)展名判斷
  const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']
  const isExtImage = imageExtensions.some((ext) => fileName.endsWith('.' + ext))

  // 圖片文件判斷完成
  return isMimeImage || isExtImage
}

// 判斷已上傳文件是否為圖片
const isUploadedImageFile = (file: UploadedFile): boolean => {
  // 防止空值錯(cuò)誤
  if (!file || !file.type) return false
  const ext = file.type.toLowerCase()
  return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)
}

// 獲取已上傳文件的圖標(biāo)
const getUploadedFileIcon = (file: UploadedFile) => {
  // 防止空值錯(cuò)誤
  if (!file || !file.type) return Document
  const ext = file.type.toLowerCase()

  // 圖片類型
  if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)) {
    return Picture
  }

  // 壓縮包類型
  if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
    return FolderOpened
  }

  // PDF文檔
  if (ext === 'pdf') {
    return Files
  }

  // 其他都是文檔類型
  return Document
}

// 獲取已上傳文件的預(yù)覽URL
const getUploadedFilePreviewUrl = (file: UploadedFile): string => {
  // 防止空值錯(cuò)誤
  if (!file || !file.url) return ''
  return baseURL2 + '/' + file.url.replace(/^\/+/g, '')
}

// 下載已上傳的文件
const downloadUploadedFile = (file: UploadedFile) => {
  // 防止空值錯(cuò)誤
  if (!file || !file.url) {
    ElMessage.error('文件下載地址不存在')
    return
  }

  const downloadUrl = baseURL2 + '/' + file.url.replace(/^\/+/g, '')
  const link = document.createElement('a')
  link.href = downloadUrl
  link.download = file.name || 'download'
  link.target = '_blank'
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
  ElMessage.success(`正在下載: ${file.name || '文件'}`)
}

// 生成唯一ID
const generateUID = (): string => {
  return (
    Date.now().toString(36) +
    Math.random().toString(36).substring(2) +
    Math.random().toString(36).substring(2)
  )
}

// 計(jì)算文件SHA1哈希
const calculateFileHash = async (file: File): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = async () => {
      try {
        const arrayBuffer = reader.result as ArrayBuffer
        const hashBuffer = await crypto.subtle.digest('SHA-1', arrayBuffer)
        const hashArray = Array.from(new Uint8Array(hashBuffer))
        const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
        resolve(hashHex)
      } catch (_error) {
        reject(_error)
      }
    }
    reader.onerror = reject
    reader.readAsArrayBuffer(file)
  })
}

// 判斷是否使用分片上傳
const shouldUseChunkUpload = (file: File): boolean => {
  return file.size > chunkUploadConfig.largeFileThreshold
}

// 切分文件
const createChunks = (file: File): ChunkInfo[] => {
  const chunks: ChunkInfo[] = []
  const chunkSize = chunkUploadConfig.chunkSize
  const totalChunks = Math.ceil(file.size / chunkSize)

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize
    const end = Math.min(start + chunkSize, file.size)
    const blob = file.slice(start, end)

    chunks.push({
      index: i,
      start,
      end,
      blob,
      status: 'pending',
      retryCount: 0,
    })
  }

  return chunks
}

// 格式化上傳速度
const formatUploadSpeed = (bytesPerSecond: number): string => {
  if (bytesPerSecond === 0) return '0 B/s'
  const k = 1024
  const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']
  const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k))
  return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}

// 格式化剩余時(shí)間
const formatRemainingTime = (seconds: number): string => {
  if (seconds === 0 || !isFinite(seconds)) return '未知'

  const hours = Math.floor(seconds / 3600)
  const minutes = Math.floor((seconds % 3600) / 60)
  const secs = Math.floor(seconds % 60)

  if (hours > 0) {
    return `${hours}小時(shí)${minutes}分鐘`
  } else if (minutes > 0) {
    return `${minutes}分鐘${secs}秒`
  } else {
    return `${secs}秒`
  }
}
// 檢查斷線續(xù)傳
const checkResumeUpload = async (fileHash: string): Promise<ChunkUploadSession | null> => {
  try {
    const response = await fetch(chunkUploadConfig.resumeUploadCheckUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...props.headers,
      },
      body: JSON.stringify({ fileHash }),
    })

    if (!response.ok) {
      throw new Error(`HTTP錯(cuò)誤: ${response.status} ${response.statusText}`)
    }

    const result = await response.json()
    // 斷線續(xù)傳檢查結(jié)果

    if (result.code === 200 && result.data.resumable) {
      return {
        sessionId: result.data.sessionId,
        fileId: result.data.fileId,
        fileName: '',
        fileSize: 0,
        fileHash,
        chunkSize: result.data.chunkSize,
        totalChunks: result.data.totalChunks,
        uploadedChunks: result.data.uploadedChunks,
        status: 'uploading',
      }
    }

    return null
  } catch {
    // 檢查斷線續(xù)傳失敗
    return null
  }
}

// 初始化分片上傳會(huì)話
const initChunkUpload = async (file: File): Promise<ChunkUploadSession | null> => {
  try {
    // 開始初始化分片上傳

    // 計(jì)算文件哈希
    const fileHash = await calculateFileHash(file)
    // 文件哈希計(jì)算完成

    // 檢查斷線續(xù)傳
    const resumeSession = await checkResumeUpload(fileHash)
    if (resumeSession) {
      // 檢測(cè)到斷線續(xù)傳
      resumeSession.fileName = file.name
      resumeSession.fileSize = file.size
      return resumeSession
    }

    // 初始化新的上傳會(huì)話
    const totalChunks = Math.ceil(file.size / chunkUploadConfig.chunkSize)

    const requestData = {
      fileName: file.name,
      fileSize: file.size,
      fileHash,
      fileType: props.data.type || '附件',
      chunkSize: chunkUploadConfig.chunkSize,
      totalChunks,
      位置: props.data.位置 || '',
    }

    // 初始化請(qǐng)求數(shù)據(jù)準(zhǔn)備完成

    const response = await fetch(chunkUploadConfig.initChunkUploadUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...props.headers,
      },
      body: JSON.stringify(requestData),
    })

    if (!response.ok) {
      throw new Error(`HTTP錯(cuò)誤: ${response.status} ${response.statusText}`)
    }

    const result = await response.json()
    // 初始化響應(yīng)收到

    if (result.code === 200) {
      return {
        sessionId: result.data.sessionId,
        fileId: result.data.fileId,
        fileName: file.name,
        fileSize: file.size,
        fileHash,
        chunkSize: result.data.chunkSize,
        totalChunks: result.data.totalChunks,
        uploadedChunks: result.data.uploadedChunks || [],
        status: 'ready',
      }
    } else {
      throw new Error(result.message || '初始化失敗')
    }
  } catch {
    // 初始化分片上傳失敗
    return null
  }
}

// 上傳單個(gè)分片
const uploadSingleChunk = async (
  chunk: ChunkInfo,
  session: ChunkUploadSession,
): Promise<boolean> => {
  try {
    const formData = new FormData()
    formData.append('sessionId', session.sessionId)
    formData.append('chunkIndex', chunk.index.toString())
    formData.append('chunk', chunk.blob)

    // 可選:計(jì)算分片哈希
    if (chunk.hash) {
      formData.append('chunkHash', chunk.hash)
    }

    // 上傳分片

    const response = await fetch(chunkUploadConfig.uploadChunkUrl, {
      method: 'POST',
      headers: props.headers,
      body: formData,
    })

    if (!response.ok) {
      throw new Error(`HTTP錯(cuò)誤: ${response.status} ${response.statusText}`)
    }

    const result = await response.json()

    if (result.code === 200) {
      chunk.status = 'completed'
      // 分片上傳成功
      return true
    } else {
      throw new Error(result.message || '分片上傳失敗')
    }
  } catch {
    // 分片上傳失敗
    chunk.status = 'failed'
    chunk.retryCount++
    return false
  }
}

// 合并分片
const mergeChunks = async (session: ChunkUploadSession): Promise<UploadedFile | null> => {
  try {
    // 開始合并分片

    const response = await fetch(chunkUploadConfig.mergeChunksUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...props.headers,
      },
      body: JSON.stringify({ sessionId: session.sessionId }),
    })

    if (!response.ok) {
      throw new Error(`HTTP錯(cuò)誤: ${response.status} ${response.statusText}`)
    }

    const result = await response.json()
    // 合并響應(yīng)收到

    if (result.code === 200) {
      return {
        id: result.data.id,
        name: result.data.name,
        size: result.data.size,
        type: result.data.type,
        url: result.data.url,
      }
    } else {
      throw new Error(result.message || '合并失敗')
    }
  } catch {
    // 合并分片失敗
    return null
  }
}

// 分片上傳主函數(shù)
const performChunkUpload = async (file: File): Promise<void> => {
  try {
    isChunkUploading.value = true
    chunkUploadProgress.value = 0

    // 初始化上傳會(huì)話
    const session = await initChunkUpload(file)
    if (!session) {
      throw new Error('初始化分片上傳失敗')
    }

    currentChunkSession.value = session
    session.status = 'uploading'

    // 創(chuàng)建分片
    const chunks = createChunks(file)
    // 文件切分完成

    // 標(biāo)記已上傳的分片
    for (const uploadedIndex of session.uploadedChunks) {
      if (chunks[uploadedIndex]) {
        chunks[uploadedIndex].status = 'completed'
      }
    }

    // 統(tǒng)計(jì)需要上傳的分片
    const pendingChunks = chunks.filter((chunk) => chunk.status === 'pending')
    // 需要上傳分片統(tǒng)計(jì)

    if (pendingChunks.length === 0) {
      // 所有分片都已上傳,直接合并
      // 所有分片已上傳,開始合并
      const uploadedFile = await mergeChunks(session)
      if (uploadedFile) {
        uploadedFileList.value.push(uploadedFile)
        ElMessage.success(`文件 "${file.name}" 上傳成功`)
      } else {
        throw new Error('文件合并失敗')
      }
      return
    }

    // 并發(fā)上傳分片
    const startTime = Date.now()
    const uploadedBytes = session.uploadedChunks.length * session.chunkSize

    await uploadChunksWithConcurrency(pendingChunks, session, (progress) => {
      const currentTime = Date.now()
      const elapsedTime = (currentTime - startTime) / 1000
      const currentUploadedBytes = uploadedBytes + progress.completed * session.chunkSize

      // 計(jì)算上傳速度
      if (elapsedTime > 0) {
        uploadSpeed.value = currentUploadedBytes / elapsedTime
        const remainingBytes = file.size - currentUploadedBytes
        // 避免除零錯(cuò)誤
        if (uploadSpeed.value > 0) {
          remainingTime.value = remainingBytes / uploadSpeed.value
        }
      }

      // 更新進(jìn)度(確保不超過(guò)100%)
      chunkUploadProgress.value = Math.min((currentUploadedBytes / file.size) * 100, 100)

      // 上傳進(jìn)度更新
    })

    // 合并分片
    // 所有分片上傳完成,開始合并
    session.status = 'merging'

    const uploadedFile = await mergeChunks(session)
    if (uploadedFile) {
      uploadedFileList.value.push(uploadedFile)
      session.status = 'completed'
      ElMessage.success(`文件 "${file.name}" 上傳成功`)

      // 延遲移除文件
      setTimeout(() => {
        const fileIndex = fileList.value.findIndex((f) => f.name === file.name)
        if (fileIndex !== -1) {
          fileList.value.splice(fileIndex, 1)
        }
      }, 1500)
    } else {
      throw new Error('文件合并失敗')
    }
  } catch (error) {
    // 分片上傳失敗
    ElMessage.error(`分片上傳失敗: ${error instanceof Error ? error.message : String(error)}`)
    if (currentChunkSession.value) {
      currentChunkSession.value.status = 'failed'
    }
  } finally {
    isChunkUploading.value = false
    chunkUploadProgress.value = 0
    uploadSpeed.value = 0
    remainingTime.value = 0
    currentChunkSession.value = null
  }
}

// 并發(fā)上傳分片
const uploadChunksWithConcurrency = async (
  chunks: ChunkInfo[],
  session: ChunkUploadSession,
  onProgress: (progress: { completed: number; total: number }) => void,
): Promise<void> => {
  const maxConcurrent = chunkUploadConfig.maxConcurrent
  const semaphore = new Array(maxConcurrent).fill(null)
  const totalPendingChunks = chunks.length // 保存初始待上傳分片數(shù)
  let completed = 0

  const uploadNext = async (): Promise<void> => {
    while (chunks.length > 0) {
      const chunk = chunks.shift()
      if (!chunk) break

      chunk.status = 'uploading'

      // 重試邏輯
      let success = false
      for (let retry = 0; retry <= chunkUploadConfig.retryLimit; retry++) {
        if (retry > 0) {
          // 分片重試
          await new Promise((resolve) => setTimeout(resolve, 1000 * retry))
        }

        success = await uploadSingleChunk(chunk, session)
        if (success) break
      }

      if (!success) {
        throw new Error(`分片 ${chunk.index} 上傳失敗,已重試 ${chunkUploadConfig.retryLimit} 次`)
      }

      completed++
      onProgress({ completed, total: totalPendingChunks })
    }
  }

  // 啟動(dòng)并發(fā)上傳
  await Promise.all(semaphore.map(() => uploadNext()))
}

// 取消分片上傳
const cancelChunkUpload = async (): Promise<void> => {
  if (!currentChunkSession.value) return

  try {
    const response = await fetch(chunkUploadConfig.cancelUploadUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...props.headers,
      },
      body: JSON.stringify({ sessionId: currentChunkSession.value.sessionId }),
    })

    if (!response.ok) {
      throw new Error(`HTTP錯(cuò)誤: ${response.status} ${response.statusText}`)
    }

    const result = await response.json()
    if (result.code === 200) {
      // 上傳已取消
      ElMessage.info('上傳已取消')
    } else {
      // 取消上傳響應(yīng)異常
    }
  } catch {
    // 取消上傳失敗
  } finally {
    isChunkUploading.value = false
    currentChunkSession.value = null
    chunkUploadProgress.value = 0
  }
}

// 移除已上傳的文件
const removeUploadedFile = async (file: UploadedFile, index: number) => {
  if (
    !uploadedFileList.value ||
    !Array.isArray(uploadedFileList.value) ||
    index < 0 ||
    index >= uploadedFileList.value.length
  ) {
    return
  }

  // 二次確認(rèn)刪除
  try {
    await ElMessageBox.confirm(`確定要?jiǎng)h除已上傳的文件 "${file.name}" 嗎?`, '刪除確認(rèn)', {
      confirmButtonText: '確定',
      cancelButtonText: '取消',
      type: 'warning',
    })

    // 調(diào)用刪除接口
    try {
      const response = await fetch(deleteUploadFileUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...props.headers,
        },
        body: JSON.stringify({
          fileId: file.id,
        }),
      })

      if (!response.ok) {
        throw new Error(`HTTP錯(cuò)誤: ${response.status} ${response.statusText}`)
      }

      const result = await response.json()

      if (result.code !== 200) {
        throw new Error(result.msg || '刪除失敗')
      }

      // 接口調(diào)用成功,從列表中移除文件
      uploadedFileList.value.splice(index, 1)
      emit('uploaded-file-remove', file)
      props.onUploadedFileRemove?.(file)

      ElMessage.success('文件刪除成功')
    } catch (error) {
      console.error('刪除文件失敗:', error)
      ElMessage.error(error instanceof Error ? error.message : '刪除文件失敗,請(qǐng)稍后重試')
    }
  } catch {
    // 用戶取消刪除,不做任何操作
  }
}

const getFileIcon = (file: UploadFile) => {
  // 如果有 response.data.type,優(yōu)先使用后端返回的擴(kuò)展名
  const responseType = file.response?.data?.type
  const ext = (responseType ? String(responseType) : file.type.split('/')[1] || '').toLowerCase()

  // 圖片類型
  if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)) {
    return Picture
  }

  // 壓縮包類型
  if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
    return FolderOpened
  }

  // PDF文檔
  if (ext === 'pdf') {
    return Files
  }

  // 其他都是文檔類型
  return Document
}

// 獲取文件預(yù)覽URL
const getFilePreviewUrl = (file: UploadFile): string => {
  if (file.url) {
    // 對(duì)于已上傳的文件,使用baseURL2 + 相對(duì)路徑構(gòu)成完整的預(yù)覽地址
    // 去除url開頭的斜杠避免重復(fù)
    return baseURL2 + '/' + file.url.replace(/^\/+/g, '')
  }
  // 對(duì)于未上傳的文件,使用Blob URL進(jìn)行預(yù)覽
  try {
    const blobUrl = URL.createObjectURL(file.raw)
    // 記錄已創(chuàng)建的Blob URL用于后續(xù)清理
    createdBlobUrls.value.add(blobUrl)
    return blobUrl
  } catch (error) {
    console.error('創(chuàng)建預(yù)覽URL失敗:', error)
    return ''
  }
}

// 格式化文件大小
const formatFileSize = (bytes: number): string => {
  if (bytes === 0) return '0 B'
  const k = 1024
  const sizes = ['B', 'KB', 'MB', 'GB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}

// 獲取狀態(tài)類型
const getStatusType = (status: FileStatus) => {
  const statusMap = {
    ready: 'info' as const,
    uploading: 'warning' as const,
    success: 'success' as const,
    error: 'danger' as const,
  }
  return statusMap[status] || ('info' as const)
}

// 獲取狀態(tài)文本
const getStatusText = (status: FileStatus): string => {
  const statusMap = {
    ready: '等待上傳',
    uploading: '上傳中',
    success: '上傳成功',
    error: '上傳失敗',
  }
  return statusMap[status] || '未知狀態(tài)'
}

// 驗(yàn)證文件
const validateFile = (file: File): boolean => {
  // 開始驗(yàn)證文件

  // 檢查文件大小
  if (file.size > props.maxSize * 1024 * 1024) {
    ElMessage.error(`文件大小不能超過(guò) ${props.maxSize}MB`)
    return false
  }

  // 檢查文件數(shù)量
  const currentFileList = fileList.value || []
  if (!props.multiple && currentFileList.length >= 1) {
    ElMessage.error('只能上傳一個(gè)文件')
    return false
  }

  if (props.multiple && currentFileList.length >= props.maxCount) {
    ElMessage.error(`最多只能上傳 ${props.maxCount} 個(gè)文件`)
    return false
  }

  // 獲取文件擴(kuò)展名進(jìn)行驗(yàn)證
  const fileName = file.name.toLowerCase()
  const allowedExtensions = [
    'pdf',
    'doc',
    'docx',
    'xls',
    'xlsx',
    'zip',
    'rar',
    'jpg',
    'jpeg',
    'png',
    'gif',
  ]

  const hasValidExtension = allowedExtensions.some((ext) => fileName.endsWith('.' + ext))

  // 如果擴(kuò)展名驗(yàn)證失敗,再嘗試MIME類型驗(yàn)證
  if (!hasValidExtension) {
    const allowedTypes = [
      'application/pdf', // PDF
      'application/msword', // DOC
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // DOCX
      'application/vnd.ms-excel', // XLS
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // XLSX
      'application/zip', // ZIP
      'application/x-rar-compressed', // RAR
      'application/x-rar', // RAR alternative
      'image/jpeg', // JPG/JPEG
      'image/png', // PNG
      'image/gif', // GIF
    ]

    if (!allowedTypes.includes(file.type)) {
      // 文件類型驗(yàn)證失敗
      ElMessage.error('附件格式錯(cuò)誤,請(qǐng)上傳PDF、DOC、DOCX、XLS、XLSX、ZIP、RAR或圖片文件')
      return false
    }
  }

  // 文件驗(yàn)證通過(guò)
  return true
}

// 添加文件到列表
const addFile = (file: File): UploadFile | null => {
  // 準(zhǔn)備添加文件

  if (!validateFile(file)) {
    // 文件驗(yàn)證失敗,不添加到列表
    return null
  }

  const uploadFile: UploadFile = {
    uid: generateUID(),
    name: file.name,
    size: file.size,
    type: file.type,
    raw: file,
    status: 'ready',
    percentage: 0,
  }

  // 創(chuàng)建上傳文件對(duì)象

  if (!props.multiple) {
    fileList.value = [uploadFile]
    // 單文件模式,替換文件列表
  } else {
    if (!fileList.value) {
      fileList.value = []
    }
    fileList.value.push(uploadFile)
    // 多文件模式,添加到文件列表
  }

  emit('change', uploadFile, fileList.value)

  // 選擇上傳方式:分片或普通上傳
  if (shouldUseChunkUpload(file)) {
    // 使用分片上傳
    if (props.autoUpload) {
      nextTick(() => {
        performChunkUpload(file)
      })
    }
  } else {
    // 使用普通上傳
    if (props.autoUpload) {
      // 自動(dòng)上傳模式,開始上傳文件
      nextTick(() => {
        if (uploadFile && uploadFile.uid) {
          uploadSingleFile(uploadFile)
        }
      })
    } else {
      // 手動(dòng)上傳模式,文件已添加到列表等待用戶操作
    }
  }

  return uploadFile
}

// 點(diǎn)擊上傳區(qū)域
const handleClick = () => {
  if (props.disabled) return
  if (!props.multiple && fileList.value.length > 0) return

  fileInputRef.value?.click()
}

// 文件輸入改變
const handleFileChange = (event: Event) => {
  const target = event.target as HTMLInputElement
  const files = target.files

  if (files) {
    Array.from(files).forEach((file) => {
      addFile(file)
    })
  }

  // 清空input值,允許重復(fù)選擇同一文件
  target.value = ''
}

// 拖拽相關(guān)事件
const handleDragover = (event: DragEvent) => {
  if (props.disabled) return
  event.preventDefault()
  isDragover.value = true
}

const handleDragleave = () => {
  isDragover.value = false
}

const handleDrop = (event: DragEvent) => {
  if (props.disabled) return
  event.preventDefault()
  isDragover.value = false

  const files = event.dataTransfer?.files
  if (files) {
    Array.from(files).forEach((file) => {
      addFile(file)
    })
  }
}

// 上傳單個(gè)文件
const uploadSingleFile = async (file: UploadFile) => {
  // 防止重復(fù)上傳:檢查文件狀態(tài)
  if (file.status === 'uploading' || file.status === 'success') {
    // 文件正在上傳中或已上傳成功,跳過(guò)重復(fù)上傳
    return
  }

  if (props.beforeUpload) {
    try {
      const result = await props.beforeUpload(file.raw)
      if (!result) return
    } catch {
      // beforeUpload error
      return
    }
  }

  const formData = new FormData()
  formData.append('file', file.raw)

  // 添加額外數(shù)據(jù)
  Object.keys(props.data).forEach((key) => {
    const value = props.data[key]
    if (value !== undefined && value !== null) {
      formData.append(key, String(value))
    }
  })

  file.status = 'uploading'
  file.percentage = 0

  try {
    const xhr = new XMLHttpRequest()

    // 上傳進(jìn)度
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        // 確保百分比在有效范圍內(nèi)
        file.percentage = Math.max(0, Math.min(100, Math.round((event.loaded * 100) / event.total)))
        emit('progress', event, file, fileList.value)
        props.onProgress?.(event, file, fileList.value)
      }
    }

    // 上傳完成
    xhr.onload = () => {
      if (xhr.status === 200) {
        try {
          const response = JSON.parse(xhr.responseText)
          // 上傳響應(yīng)數(shù)據(jù)

          // 檢查響應(yīng)的成功狀態(tài)
          if (response.code === 200 || response.code === '200') {
            file.status = 'success'
            file.percentage = 100
            file.response = response

            emit('success', response, file, fileList.value)
            props.onSuccess?.(response, file, fileList.value)

            // 顯示成功提示
            ElMessage.success(response.msg || `文件 "${file.name}" 上傳成功`)

            // 如果有返回?cái)?shù)據(jù),自動(dòng)添加到已上傳列表
            if (response.data && response.data.id) {
              const uploadedFile: UploadedFile = {
                id: response.data.id,
                name: response.data.name || file.name,
                size: response.data.size || file.size,
                type: response.data.type || file.type.split('/')[1] || '',
                url: response.data.url || '',
              }
              // 防止重復(fù)添加
              const exists = uploadedFileList.value.some((f) => f.id === uploadedFile.id)
              if (!exists) {
                uploadedFileList.value.push(uploadedFile)
                // 添加已上傳文件
              }
            }

            // 上傳成功后延遲一下移除文件,讓用戶能看到成功狀態(tài)
            setTimeout(() => {
              if (fileList.value && Array.isArray(fileList.value)) {
                const fileIndex = fileList.value.findIndex((f) => f.uid === file.uid)
                if (fileIndex !== -1) {
                  fileList.value.splice(fileIndex, 1)
                }
              }
            }, 1500) // 1.5秒后移除
          } else {
            // 后端返回了錯(cuò)誤狀態(tài)
            handleUploadError(new Error(response.msg || '上傳失敗'), file)
          }
        } catch {
          // 解析上傳響應(yīng)失敗
          handleUploadError(new Error('解析響應(yīng)失敗:' + xhr.responseText), file)
        }
      } else {
        handleUploadError(new Error(`上傳失敗,狀態(tài)碼:${xhr.status}`), file)
      }
    }

    // 上傳錯(cuò)誤
    xhr.onerror = () => {
      handleUploadError(new Error('網(wǎng)絡(luò)錯(cuò)誤'), file)
    }

    // 必須先調(diào)用open()方法,然后再設(shè)置請(qǐng)求頭
    xhr.open('POST', props.action, true)

    // 設(shè)置請(qǐng)求頭
    Object.keys(props.headers).forEach((key) => {
      // 避免設(shè)置無(wú)效的請(qǐng)求頭
      if (key && props.headers[key]) {
        xhr.setRequestHeader(key, props.headers[key])
      }
    })

    xhr.send(formData)
  } catch (error) {
    handleUploadError(error as Error, file)
  }
}

// 處理上傳錯(cuò)誤
const handleUploadError = (error: Error, file: UploadFile) => {
  file.status = 'error'
  file.percentage = 0

  emit('error', error, file, fileList.value)
  props.onError?.(error, file, fileList.value)

  ElMessage.error(`文件 ${file.name} 上傳失?。?{error.message}`)
}

// 上傳指定文件
const uploadFile = (file: UploadFile) => {
  // 防止重復(fù)上傳:檢查文件狀態(tài)
  if (file.status === 'uploading' || file.status === 'success') {
    console.warn('文件正在上傳中或已上傳成功,跳過(guò)重復(fù)上傳:', file.name)
    return
  }

  // 選擇上傳方式:分片或普通上傳
  if (shouldUseChunkUpload(file.raw)) {
    // 使用分片上傳
    performChunkUpload(file.raw)
  } else {
    // 使用普通上傳
    uploadSingleFile(file)
  }
}

// 上傳所有文件
const uploadAll = () => {
  if (!fileList.value || !Array.isArray(fileList.value)) {
    ElMessage.warning('沒(méi)有可上傳的文件')
    return
  }
  const readyFiles = fileList.value.filter((file) => file.status === 'ready')
  if (readyFiles.length === 0) {
    ElMessage.warning('沒(méi)有可上傳的文件')
    return
  }

  // 防止重復(fù)上傳:檢查是否已經(jīng)在上傳中
  if (uploading.value || isChunkUploading.value) {
    console.warn('文件正在上傳中,跳過(guò)重復(fù)上傳')
    return
  }

  uploading.value = true

  // 分類文件:分片上傳和普通上傳
  const chunkFiles: File[] = []
  const normalFiles: UploadFile[] = []

  readyFiles.forEach((file) => {
    if (shouldUseChunkUpload(file.raw)) {
      chunkFiles.push(file.raw)
    } else {
      normalFiles.push(file)
    }
  })

  console.log(
    `分類結(jié)果: ${chunkFiles.length} 個(gè)大文件使用分片上傳,${normalFiles.length} 個(gè)小文件使用普通上傳`,
  )

  // 并發(fā)執(zhí)行上傳
  const uploadPromises: Promise<void>[] = []

  // 添加普通上傳任務(wù)
  normalFiles.forEach((file) => {
    uploadPromises.push(uploadSingleFile(file))
  })

  // 添加分片上傳任務(wù)(一次只上傳一個(gè)大文件)
  const uploadChunkFiles = async () => {
    for (const file of chunkFiles) {
      await performChunkUpload(file)
    }
  }

  if (chunkFiles.length > 0) {
    uploadPromises.push(uploadChunkFiles())
  }

  Promise.all(uploadPromises).finally(() => {
    uploading.value = false

    // 批量上傳完成后,延遲一下清空所有成功的文件
    setTimeout(() => {
      if (fileList.value && Array.isArray(fileList.value)) {
        const successFiles = fileList.value.filter((file) => file.status === 'success')
        if (successFiles.length > 0) {
          // 移除所有成功的文件
          fileList.value = fileList.value.filter((file) => file.status !== 'success')
        }
      }
    }, 2000) // 2秒后清空成功的文件
  })
}

// 移除文件時(shí)清理對(duì)應(yīng)的Blob URL
const removeFile = (index: number) => {
  if (
    !fileList.value ||
    !Array.isArray(fileList.value) ||
    index < 0 ||
    index >= fileList.value.length
  ) {
    return
  }
  const file = fileList.value[index]

  // 二次確認(rèn)刪除
  ElMessageBox.confirm(`確定要?jiǎng)h除文件 "${file.name}" 嗎?`, '刪除確認(rèn)', {
    confirmButtonText: '確定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      // 如果文件有Blob URL,清理它
      const previewUrl = getFilePreviewUrl(file)
      if (previewUrl.startsWith('blob:') && createdBlobUrls.value.has(previewUrl)) {
        try {
          URL.revokeObjectURL(previewUrl)
          createdBlobUrls.value.delete(previewUrl)
        } catch (error) {
          console.warn('清理文件預(yù)覽URL失敗:', error)
        }
      }

      // 清理圖片加載錯(cuò)誤狀態(tài)
      const key = file.uid || file.name
      imageLoadErrors.value.delete(key)

      fileList.value.splice(index, 1)

      emit('remove', file, fileList.value)
      props.onRemove?.(file, fileList.value)

      ElMessage.success('文件刪除成功')
    })
    .catch(() => {
      // 用戶取消刪除,不做任何操作
    })
}

// 獲取分片上傳狀態(tài)文本
const getChunkUploadStatusText = (status: string): string => {
  const statusMap: Record<string, string> = {
    ready: '準(zhǔn)備中',
    uploading: '上傳中',
    merging: '合并中',
    completed: '已完成',
    failed: '上傳失敗',
    cancelled: '已取消',
  }
  return statusMap[status] || '未知狀態(tài)'
}

// 清空所有文件
const clearAll = () => {
  // 清理所有Blob URLs
  if (fileList.value && Array.isArray(fileList.value)) {
    fileList.value.forEach((file) => {
      if (file && file.url && file.url.startsWith('blob:')) {
        try {
          URL.revokeObjectURL(file.url)
        } catch (error) {
          console.warn('清理Blob URL失敗:', error)
        }
      }
    })
  }

  // 清空?qǐng)D片加載錯(cuò)誤狀態(tài)
  imageLoadErrors.value.clear()

  fileList.value = []
}

// 清空成功的文件
const clearSuccessFiles = () => {
  if (fileList.value && Array.isArray(fileList.value)) {
    // 先清理成功文件的Blob URLs
    const successFiles = fileList.value.filter((file) => file && file.status === 'success')
    successFiles.forEach((file) => {
      if (file.url && file.url.startsWith('blob:')) {
        try {
          URL.revokeObjectURL(file.url)
        } catch (error) {
          console.warn('清理Blob URL失敗:', error)
        }
      }
    })

    // 然后過(guò)濾掉成功的文件
    fileList.value = fileList.value.filter((file) => file && file.status !== 'success')
  }
}

// 組件銷毀時(shí)的清理邏輯
onBeforeUnmount(async () => {
  // MyUpload組件即將銷毀,開始清理資源...

  // 如果有正在進(jìn)行的分片上傳,先取消它
  if (isChunkUploading.value && currentChunkSession.value) {
    // 檢測(cè)到正在進(jìn)行的分片上傳,正在取消...
    try {
      await cancelChunkUpload()
      // 分片上傳已成功取消
    } catch (error) {
      console.error('取消分片上傳時(shí)出錯(cuò):', error)
    }
  }

  // 清理所有創(chuàng)建的Blob URLs
  createdBlobUrls.value.forEach((blobUrl) => {
    try {
      URL.revokeObjectURL(blobUrl)
    } catch (error) {
      console.warn('清理Blob URL失敗:', error)
    }
  })
  createdBlobUrls.value.clear()

  // 清理文件預(yù)覽的Blob URLs(兼容舊的清理方式)
  if (fileList.value && Array.isArray(fileList.value)) {
    fileList.value.forEach((file) => {
      if (file.url && file.url.startsWith('blob:')) {
        try {
          URL.revokeObjectURL(file.url)
          // 已清理Blob URL
        } catch (error) {
          console.warn('清理Blob URL失敗:', error)
        }
      }
    })
  }

  // 重置所有狀態(tài)
  isChunkUploading.value = false
  chunkUploadProgress.value = 0
  currentChunkSession.value = null
  uploadSpeed.value = 0
  remainingTime.value = 0
  uploading.value = false

  // MyUpload組件資源清理完成
})

// 暴露方法
defineExpose({
  uploadAll,
  clearAll,
  clearSuccessFiles,
  addFile,
  cancelChunkUpload,
  performChunkUpload,
  hasReadyFiles,
  fileList,
})
</script>

<style scoped lang="scss">
@import '@/assets/styles/variables.scss';

.my-upload {
  width: 100%;

  .upload-area {
    position: relative;
    border: 2px dashed $border-color-base;
    border-radius: 8px;
    background-color: $color-white;
    transition: all 0.3s ease;
    cursor: pointer;
    overflow: hidden;

    &:hover {
      border-color: $primary-color;
      background-color: rgba($primary-color, 0.02);
    }

    &.is-dragover {
      border-color: $primary-color;
      background-color: rgba($primary-color, 0.05);
      transform: scale(1.02);
    }

    &.is-disabled {
      cursor: not-allowed;
      opacity: 0.6;

      &:hover {
        border-color: $border-color-base;
        background-color: $color-white;
      }
    }

    &.has-files {
      cursor: default;

      &:hover {
        border-color: $border-color-base;
        background-color: $color-white;
      }
    }

    .upload-content {
      padding: 40px 20px;
      text-align: center;

      .upload-icon {
        color: $text-secondary;
        margin-bottom: 16px;
        transition: color 0.3s ease;
      }

      .upload-text {
        .upload-title {
          margin: 0 0 8px 0;
          font-size: 16px;
          font-weight: 500;
          color: $text-primary;
        }

        .upload-subtitle {
          margin: 0;
          font-size: 14px;
          color: $text-secondary;
        }
      }
    }

    .single-file-preview {
      display: flex;
      align-items: center;
      padding: 20px;
      gap: 16px;

      .file-preview {
        flex-shrink: 0;
        width: 80px;
        height: 80px;
        border-radius: 6px;
        overflow: hidden;
        background-color: $background-color-base;
        display: flex;
        align-items: center;
        justify-content: center;
        border: 1px solid $border-color-light;

        .preview-image {
          width: 100%;
          height: 100%;
          object-fit: cover;
          display: block;
          border-radius: 4px;

          &[src=''],
          &:not([src]) {
            display: none;
          }
        }

        .file-icon {
          color: $text-secondary;
        }
      }

      .file-info {
        flex: 1;
        min-width: 0;

        .file-name {
          margin: 0 0 8px 0;
          font-size: 16px;
          font-weight: 500;
          color: $text-primary;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }

        .file-size {
          margin: 0;
          font-size: 14px;
          color: $text-secondary;
        }
      }

      .file-actions {
        flex-shrink: 0;
      }
    }
  }

  .file-list {
    margin-top: 16px;
    border: 1px solid $border-color-light;
    border-radius: 6px;
    overflow: hidden;

    .file-item {
      display: flex;
      align-items: center;
      padding: 16px;
      gap: 16px;
      border-bottom: 1px solid $border-color-light;
      background-color: $color-white;
      transition: background-color 0.3s ease;

      &:last-child {
        border-bottom: none;
      }

      &:hover {
        background-color: $background-color-base;
      }

      .file-preview {
        flex-shrink: 0;
        width: 48px;
        height: 48px;
        border-radius: 4px;
        overflow: hidden;
        background-color: $background-color-base;
        display: flex;
        align-items: center;
        justify-content: center;

        .preview-thumbnail {
          width: 100%;
          height: 100%;
          object-fit: cover;
          display: block;
          border-radius: 4px;

          &[src=''],
          &:not([src]) {
            display: none;
          }
        }

        .file-icon {
          color: $text-secondary;
          font-size: 24px;
        }
      }

      .file-info {
        flex: 1;
        min-width: 0;

        .file-name {
          margin: 0 0 4px 0;
          font-size: 14px;
          font-weight: 500;
          color: $text-primary;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }

        .file-size {
          margin: 0 0 8px 0;
          font-size: 12px;
          color: $text-secondary;
        }

        .file-progress {
          width: 200px;
        }

        .file-status {
          margin-top: 4px;
        }
      }

      .file-actions {
        flex-shrink: 0;
        display: flex;
        gap: 8px;
      }
    }
  }

  .upload-controls {
    margin-top: 16px;
    text-align: center;

    .el-button {
      margin: 0 8px;
    }
  }

  // 分片上傳進(jìn)度樣式
  .chunk-upload-progress {
    background: $color-white;
    border: 1px solid $border-color-light;
    border-radius: 8px;
    padding: 16px;
    margin-top: 16px;

    .progress-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 12px;

      h4 {
        margin: 0;
        font-size: 16px;
        font-weight: 600;
        color: $text-primary;

        &::before {
          content: '??';
          margin-right: 8px;
        }
      }

      .progress-text {
        font-size: 18px;
        font-weight: 600;
        color: $primary-color;
      }
    }

    .upload-stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 8px;
      margin-top: 12px;

      .stat-item {
        display: flex;
        align-items: center;

        .stat-label {
          font-size: 13px;
          color: $text-secondary;
          margin-right: 8px;
          min-width: 70px;
        }

        .stat-value {
          font-size: 13px;
          color: $text-primary;
          font-weight: 500;

          &.status {
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 12px;

            &.ready {
              background: #e1f3ff;
              color: #0066cc;
            }

            &.uploading {
              background: #fff7e6;
              color: #d46b08;
            }

            &.merging {
              background: #f6ffed;
              color: #389e0d;
            }

            &.completed {
              background: #f6ffed;
              color: #52c41a;
            }

            &.failed {
              background: #fff2f0;
              color: #a8071a;
            }

            &.cancelled {
              background: #f5f5f5;
              color: #666;
            }
          }
        }
      }
    }
  }

  .uploaded-files {
    margin-top: 20px;

    h4 {
      margin: 0 0 12px 0;
      font-size: 16px;
      font-weight: 600;
      color: $text-primary;

      &::before {
        content: '';
        display: inline-block;
        width: 4px;
        height: 16px;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        margin-right: 8px;
        border-radius: 2px;
      }
    }
  }

  .uploaded-file-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .uploaded-file-item {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    background: $color-white;
    border: 1px solid $border-color-light;
    border-radius: 8px;
    transition: all 0.3s ease;
    gap: 12px;

    &:hover {
      background: $background-color-base;
      border-color: $primary-color;
      transform: translateY(-1px);
      box-shadow: 0 2px 8px rgba($primary-color, 0.15);
    }

    .file-preview {
      flex-shrink: 0;
      width: 48px;
      height: 48px;
      border-radius: 4px;
      overflow: hidden;
      background-color: $background-color-base;
      display: flex;
      align-items: center;
      justify-content: center;

      .preview-thumbnail {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }

      .file-icon {
        color: $primary-color;
      }
    }

    .file-info {
      flex: 1;
      min-width: 0;

      .file-name {
        margin: 0 0 4px 0;
        font-size: 14px;
        font-weight: 500;
        color: $text-primary;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;

        &.clickable {
          cursor: pointer;
          color: $primary-color;
          transition: all 0.3s ease;

          &:hover {
            color: darken($primary-color, 10%);
            text-decoration: underline;
          }

          &:active {
            color: darken($primary-color, 20%);
          }
        }
      }

      .file-size {
        margin: 0;
        font-size: 12px;
        color: $text-secondary;
      }
    }

    .file-actions {
      display: flex;
      gap: 6px;
      flex-shrink: 0;
    }
  }
}

// 響應(yīng)式設(shè)計(jì)
@media (max-width: 768px) {
  .my-upload {
    .upload-area {
      .upload-content {
        padding: 30px 16px;
      }

      .single-file-preview {
        padding: 16px;

        .file-preview {
          width: 60px;
          height: 60px;
        }
      }
    }

    .file-list {
      .file-item {
        padding: 12px;

        .file-info {
          .file-progress {
            width: 150px;
          }
        }

        .file-actions {
          flex-direction: column;
          gap: 4px;
        }
      }
    }
  }
}
</style>

這是上傳組件MyUpload.vue

  • 暫無(wú)評(píng)論
windthesky
<MyUpload
            ref="uploadRef"
            v-model="form.附件網(wǎng)址"
            :data="{
              type: '附件',
              位置: '車輛信息',
            }"
          />

上傳組件使用

  • 暫無(wú)評(píng)論
hub_rb

linux上并發(fā)設(shè)置都沒(méi)問(wèn)題建議你看下mysql配置
或者在每個(gè)請(qǐng)求結(jié)束也就是response上吧lastId寫入log文件 也有可能是opcache緩存的問(wèn)題

  • tanhongbin 1天前

    這個(gè)明顯不符合mysql的邏輯 它就不可能返回兩個(gè)相同的id 主鍵自增的情況下

  • hub_rb 1天前

    這個(gè)也確實(shí) 畢竟入庫(kù)數(shù)據(jù)沒(méi)問(wèn)題 基本應(yīng)該就是業(yè)務(wù)代碼層面的問(wèn)題了

windthesky

測(cè)試了,跟長(zhǎng)id有關(guān)系,1000000000000001不會(huì),10000000000000001會(huì)重復(fù)

截圖
截圖

  • lsmir2 19小時(shí)前

    數(shù)據(jù)庫(kù)表結(jié)構(gòu),id字段類型.

  • windthesky 13小時(shí)前

    bigint本身數(shù)據(jù)庫(kù)是沒(méi)問(wèn)題的,是獲取的id有問(wèn)題

??