<template>
  <el-upload
    v-if="isRender && uploadUrl"
    ref="upload"
    v-bind="$attrs"
    class="es-upload"
    :portrait="portrait"
    :list-type="listType"
    :wait="1000"
    :height="boxHeight"
    :multiple="portrait ? false : multiple"
    :action="uploadUrl"
    :http-request="chunkHttpRequest"
    :handles="chunkHandles"
    :show-file-list="showList"
    :file-list="lists"
    :result-file="resultFile"
    :disabled="isDisabled || isReadonly"
    :readonly="isReadonly"
    :class="[
      {
        'es-avatar-uploader': portrait && !$slots.default,
        'es-upload-readonly': isReadonly && !downloads
      },
      'es-upload-' + listType
    ]"
    :previewAdjunct="host + previewAdjunct"
    :headers="headers"
    :data="datas"
    :show-info="showList ? showInfo : undefined"
    :auto-upload="autoUpload"
    :response="true"
    :operate="operate"
    :accept="accepts"
    :drag-sort="isSort"
    :preview="preview"
    :exclude="excludes"
    :download="isDownload"
    :deleted="isRemove"
    :fileIcons="_icons"
    :picture="_picture"
    :before-upload="handleBeforeUpload"
    :before-remove="handleBeforeRemove"
    :on-preview="handlePreview"
    :on-downloads="handleDownloads"
    :on-download="handleDownload"
    :on-sort="handleSort"
    :on-success="handleSuccess"
    :on-change="handleChange"
    :on-remove="handleRemove"
    :on-error="handleError"
  >
    <template v-if="prepend">
      <template v-if="Array.isArray(prepend)">
        <el-button
          v-for="(item, index) in prepend"
          v-bind="{ ...item, size: btnSize }"
          :key="index"
          @click.stop="handleClick(item)"
        >
          {{ item.label || item.text }}
        </el-button>
      </template>
      <el-button
        v-else
        v-bind="{ ...prepend, size: btnSize }"
        @click.stop="handleClick(prepend)"
      >
        {{ prepend.label || prepend.text }}
      </el-button>
    </template>
    <slot>
      <expand-dom v-if="render" :render="render"></expand-dom>
      <template v-else>
        <template v-if="portrait">
          <img
            v-if="profile"
            :src="profile"
            class="avatar"
            :style="{ width: width, height: height }"
          />
          <i
            v-else
            class="avatar-uploader-icon"
            :class="{ 'el-icon-plus': !isReadonly, 'is-disabled': isDisabled }"
            :style="{ width: width, height: height, lineHeight: height }"
          ></i>
        </template>
        <template v-else>
          <template v-if="!isReadonly">
            <i
              class="el-icon-plus es-uploader-icon"
              :class="{ 'is-disabled': isDisabled }"
              v-if="selectType === 'icon-plus'"
            ></i>
            <el-button
              v-else
              class="es-upload-button"
              :class="icon"
              :type="selectType"
              :size="btnSize"
              :disabled="isDisabled"
              >{{ text ? text : autoUpload ? '点击上传' : '选择文件' }}
            </el-button>
            <el-button
              class="es-upload-button"
              v-if="!autoUpload"
              :type="uploadType"
              :size="btnSize"
              :disabled="isDisabled"
              @click.stop="handleUpload"
            >
              上传文件
            </el-button>
          </template>
          <el-button
            v-if="showFileList && isDownloads"
            slot="handle"
            class="es-upload-button es-upload-downloads"
            :size="btnSize"
            @click.stop="handleDownloads"
          >
            批量下载
          </el-button>
        </template>
      </template>
    </slot>
    <template v-if="append">
      <template v-if="Array.isArray(append)">
        <el-button
          v-for="(item, index) in append"
          v-bind="{ ...item, size: btnSize }"
          :key="index"
          @click.stop="handleClick(item)"
        >
          {{ item.label || item.text }}
        </el-button>
      </template>
      <el-button
        v-else
        v-bind="{ ...append, size: btnSize }"
        @click.stop="handleClick(append)"
      >
        {{ append.label || append.text }}
      </el-button>
    </template>
    <template slot="tip">
      <slot name="tip">
        <div v-if="tips" class="el-upload__tip">{{ tips }}</div>
      </slot>
    </template>
    <el-image-viewer
      slot="dialog"
      v-if="previewScale && showImg"
      :z-index="9999"
      :initial-index="0"
      :url-list="[imgUrl]"
      :on-close="closeViewer"
    ></el-image-viewer>
    <es-dialog v-else :title="title" :visible.sync="showImg" slot="dialog">
      <div
        v-loading="imgChange"
        element-loading-background="rgba(0, 0, 0, 0.8)"
        class="es-upload-dialog"
      >
        <img v-show="!imgChange" ref="showImg" :src="imgUrl" :style="styles" />
      </div>
    </es-dialog>
    <es-dialog :title="title" :visible.sync="showVideo" slot="dialog">
      <es-player
        autoplay
        type="video/mp4"
        :is-pause="!showVideo"
        :source="source"
      ></es-player>
    </es-dialog>
  </el-upload>
</template>
<script>
import {
  chunkCheckFile,
  chunkMergeChunk,
  chunkUploadChunk,
  delAdjunct,
  downloadByAdjunctId,
  getAdjunctFileInfos,
  getAdjunctProperties,
  getDochubBucket,
  previewAdjunct,
  previewAdjunct2,
  previewAdjunctOffice,
  uploadDownloads,
  uploadOnlyOne,
  uploads,
  uploadSort
} from 'eoss-ui/src/config/api.js';
import { debounce } from 'throttle-debounce';
import util from 'eoss-ui/src/utils/util.js';
import picture from './picture.js';
import store from 'eoss-ui/src/utils/store';
import { Base64 } from 'js-base64';
import SparkMD5 from 'spark-md5';

export default {
  name: 'EsUpload',
  components: {
    expandDom: {
      functional: true,
      props: {
        render: [Function, String]
      },
      render: (h, ctx) => {
        if (typeof ctx.props.render == 'string') {
          try {
            return util.toFunction(ctx.props.render)(h, params);
          } catch (error) {
            throw error;
          }
        }
        return ctx.props.render(h);
      }
    }
  },
  inheritAttrs: false,
  inject: {
    elForm: {
      default: ''
    },
    elFormItem: {
      default: ''
    }
  },
  props: {
    prepend: [Array, Object],
    append: [Array, Object],
    render: Function,
    template: String,
    value: [String, Array, Object],
    name: String,
    method: {
      type: String,
      default: 'get'
    },
    text: String,
    icon: {
      type: Boolean,
      default: true
    },
    headers: Object,
    listType: {
      type: String,
      default: 'table' // text,table,picture,picture-card
    },
    operate: {
      type: Boolean,
      default: true
    },
    accept: String,
    // 附件code
    code: String,
    // 业务id
    ownId: String,
    //附件文档id
    documentId: String,
    requiredOwnId: {
      type: Boolean,
      default: true
    },
    //
    downloads: {
      type: Boolean,
      default: false
    },
    // 附件扩展编码
    extendCode: String,
    // 是否添加文件md5值
    fileMd5: {
      type: Boolean,
      default: false
    },
    // 额外的参数
    data: {
      type: Object,
      default() {
        return {};
      }
    },
    param: {
      type: Object,
      default() {
        return {};
      }
    },
    // 单文件大小
    size: Number,
    totalSize: Number,
    // 是否头像模式上传
    portrait: {
      type: Boolean,
      default: false
    },
    photo: [String, Boolean],
    //头像按钮框宽度
    width: String,
    //头像按钮框高度度
    height: String,
    // 自定义的文字提示
    tips: {
      type: String,
      default: ''
    },
    dragSort: {
      type: Boolean,
      default: false
    },
    // 上传成功后绑定到file的内容
    showInfo: {
      type: Array,
      default() {
        return [
          { label: '上传时间', field: 'uploadTime', width: 170 },
          { label: '大小', field: 'fileSize', width: 100 },
          { label: '上传人', field: 'userName', width: 100 }
        ];
      }
    },
    // 上传地址
    action: String,
    // 自动上传
    autoUpload: {
      type: Boolean,
      default: true
    },
    // 是否支持多选文件
    multiple: {
      type: Boolean,
      default: true
    },
    // 显示文件列表
    showFileList: {
      type: Boolean,
      default: true
    },
    // 上传的文件列表
    fileList: [Array, String],
    requestFiles: {
      type: Boolean,
      default: true
    },
    filesParam: {
      type: Object,
      default() {
        return {};
      }
    },
    state: {
      type: Number,
      default: 1
    },
    selectType: {
      type: String,
      default: 'primary'
    },
    uploadType: {
      type: String,
      default: 'success'
    },
    host: {
      type: String,
      default() {
        return util.getStorage('host') ? util.getStorage('host') : '';
      }
    },
    message: Object,
    btnSize: {
      type: String,
      default: 'medium'
    },
    // 只读
    readonly: {
      type: Boolean,
      default: false
    },
    disabled: Boolean,
    preview: { type: [Boolean, String], default: true },
    exclude: String,
    download: { type: [Boolean, String], default: true },
    deleted: { type: [Boolean, String], default: true },
    remove: { type: [Boolean, String], default: true },
    beforeUpload: Function,
    onPreview: Function,
    // 文件列表排序
    onSort: Function,
    beforeRemove: Function,
    onDownloads: Function,
    onDownload: Function,
    onSort: Function,
    onSuccess: Function,
    onChange: Function,
    onRemove: Function,
    onError: Function,
    dangerouslyUseHTMLString: Boolean,
    fileCount: Number,
    listHeight: String,
    display: Boolean,
    resultFile: {
      type: Boolean,
      default: true
    },
    properties: {
      type: String,
      default() {
        return getAdjunctProperties;
      }
    },
    fileIcons: {
      type: Object,
      default() {
        return {};
      }
    },
    picture: {
      type: Object,
      default() {
        return {};
      }
    },
    previewScale: {
      type: Boolean,
      default: true
    },
    useCaseCode: String
  },
  data() {
    return {
      uploadUrl: this.action,
      lists: [],
      image: '',
      fileAccept: '',
      fileSize: 0,
      fileTotalSize: 0,
      filesTotalSize: 0,
      showImg: false,
      imgChange: false,
      title: '',
      imgUrl: '',
      styles: {},
      showVideo: false,
      videoChange: false,
      source: '',
      excludeNames: undefined,
      boxHeight: this.listHeight,
      previewAdjunct: previewAdjunct,
      kkfileview: null,
      dochubConfig: {},
      // 文档中台桶配置下发的分片上传配置（baseBucketConfig），null 表示未下发 → 回退组件 props
      bucketChunkConfig: null
    };
  },
  computed: {
    isRender() {
      let useCaseCodes = util.getStorage('useCaseCodes');
      if (useCaseCodes && this.useCaseCode) {
        return useCaseCodes.indexOf(this.useCaseCode) > -1;
      }
      return true;
    },
    _icons() {
      return {
        ppt: 'es-icon-ppt',
        png: 'es-icon-tupian',
        gif: 'es-icon-tupian',
        jpg: 'es-icon-tupian',
        jpeg: 'es-icon-tupian',
        xls: 'es-icon-Excel',
        xlsx: 'es-icon-Excel',
        docx: 'es-icon-word',
        doc: 'es-icon-word',
        rar: 'es-icon-yasuobao',
        zip: 'es-icon-yasuobao',
        txt: 'es-icon-txt',
        ipa: 'es-icon-ios',
        apk: 'es-icon-android',
        mp4: 'es-icon-kebofangwenjian',
        avi: 'es-icon-kebofangwenjian',
        mp3: 'es-icon-shengyinwenjian',
        wma: 'es-icon-shengyinwenjian',
        pdf: 'es-icon-pdf',
        ...this.fileIcons
      };
    },
    _picture() {
      return {
        ...picture,
        ...this.picture
      };
    },
    show() {
      if ((this.photo && typeof this.photo === 'string') || this.value) {
        return false;
      }
      return this.showFileList;
    },
    excludes() {
      return this.exclude ? this.exclude : this.excludeNames;
    },
    accepts() {
      return this.accept
        ? this.accept
        : this.fileAccept
        ? this.fileAccept
        : this.portrait
        ? 'image/*'
        : undefined;
    },
    _size() {
      return this.size ? this.size : this.fileSize ? this.fileSize : 0;
    },
    _totalSize() {
      return this.totalSize
        ? this.totalSize
        : this.fileTotalSize
        ? this.fileTotalSize
        : 0;
    },
    getAdjunctFileInfos() {
      return typeof this.fileList === 'string'
        ? this.fileList
        : getAdjunctFileInfos;
    },
    showList() {
      return this.portrait ? false : this.showFileList;
    },
    isDownloads() {
      return this.downloads;
    },
    isDownload() {
      if (this.download) {
        return true;
      }
      return false;
    },
    isDisabled() {
      return this.disabled || (this.elForm || {}).disabled;
    },
    isReadonly() {
      return this.readonly || (this.elForm || {}).readonly;
    },
    isRemove() {
      if (this.remove == false || this.deleted == false) {
        return false;
      }
      return true;
    },
    isSort() {
      // return this.dragSort && !this.isDisabled && this.isReadonly;
      return this.dragSort && !this.isDisabled;
    },
    datas() {
      if (this.data) {
        return util.extend({}, this.data, this.params);
      }
      return this.params;
    },
    params() {
      let params = { userId: util.getStorage('userId'), ...this.param };
      if (this.code !== undefined) {
        params.code = this.code;
        params.bucketCode = this.code;
      }
      if (this.ownId !== undefined) {
        params.ownId = this.ownId;
        params.businessId = this.ownId;
      }
      return params;
    },
    showMessage() {
      if (this.message !== undefined) {
        return util.extend({}, { success: true, error: true }, this.message);
      }
      return { success: true, error: true };
    },
    profile() {
      const previewAdjunct =
        this.dochubConfig.downloadDocumentUrl || this.previewAdjunct;
      if (this.image) {
        if (typeof this.image === 'string') {
          return this.image;
        }
        return `${this.host}${previewAdjunct}?documentId=${
          this.image.adjunctId
        }&adjunctId=${this.image.adjunctId}&_tt=${new Date().getTime()}`;
      }
      if (this.value) {
        if (typeof this.value === 'string') {
          if (this.value.indexOf('data:image/') === 0) {
            return this.value;
          } else if (this.value.indexOf(previewAdjunct) > -1) {
            let val =
              this.host +
              util.jointUrl({
                url: this.value,
                reg: '/main2'
              });
            return val;
          } else if (this.value.indexOf('/') === -1) {
            return `${this.host}${previewAdjunct}?documentId=${
              this.value
            }&adjunctId=${this.value}&_tt=${new Date().getTime()}`;
          }
          return this.value;
        } else if (Array.isArray(this.value)) {
          let adjunctId = this.value[0].response
            ? this.value[0].response.adjunctId
            : this.value[0].adjunctId;
          return `${
            this.host
          }${previewAdjunct}?documentId=${adjunctId}&adjunctId=${adjunctId}&_tt=${new Date().getTime()}`;
        } else {
          let adjunctId = this.value.response
            ? this.value.response.adjunctId
            : this.value.adjunctId;
          return `${
            this.host
          }${previewAdjunct}?documentId=${adjunctId}&adjunctId=${adjunctId}&_tt=${new Date().getTime()}`;
        }
      }
      if (this.photo && typeof this.photo === 'string') {
        if (this.photo.indexOf('data:image/') === 0) {
          return this.photo;
        } else if (this.photo.indexOf(previewAdjunct) > -1) {
          let val =
            this.host +
            util.jointUrl({
              url: this.value,
              reg: '/main2'
            });
          return val;
        } else if (this.photo.indexOf('/') === -1) {
          return `${this.host}${previewAdjunct}?documentId=${
            this.photo
          }&adjunctId=${this.photo}&_tt=${new Date().getTime()}`;
        }
      }
      return this.photo;
    },
    // 是否走分片上传：完全由后台桶配置（/v2/chunk/config）的开关决定，且具备桶编码(code)与业务id(ownId)
    isChunk() {
      const cfg = this.bucketChunkConfig;
      return !!(cfg && cfg.enableChunkUpload) && !!this.code && !!this.ownId;
    },
    // 生效的分片大小（字节）：桶配置 > 默认 5MB
    _chunkSize() {
      const cfg = this.bucketChunkConfig;
      return (
        (cfg && Number(cfg.chunkSize) > 0 && Number(cfg.chunkSize)) ||
        5 * 1024 * 1024
      );
    },
    // 生效的分片并发数：桶配置 > 默认 3
    _chunkConcurrent() {
      const cfg = this.bucketChunkConfig;
      return (
        (cfg &&
          Number(cfg.chunkConcurrent) > 0 &&
          Number(cfg.chunkConcurrent)) ||
        3
      );
    },
    // 分片三接口地址：优先 dochubConfig 下发，否则回退 api.js 常量（与本组件 dochubConfig.x || 常量 的写法一致）
    chunkUrls() {
      const cfg = this.dochubConfig || {};
      return {
        check: cfg.chunkCheckUrl || chunkCheckFile,
        upload: cfg.chunkUploadUrl || chunkUploadChunk,
        merge: cfg.chunkMergeUrl || chunkMergeChunk
      };
    },
    // 传给 el-upload 的自定义上传实现；非分片模式返回 undefined 以走默认上传
    chunkHttpRequest() {
      return this.isChunk ? this.chunkUpload : undefined;
    },
    // 操作列按钮：非分片模式回退用户经 $attrs 传入的 handles（或 undefined → el-upload 默认），
    // 分片模式注入"暂停/继续"并保留预览/下载/删除（emit 复用既有事件链）。仅分片模式改变渲染，不影响存量业务。
    chunkHandles() {
      if (!this.isChunk) return this.$attrs.handles;
      const done = (f) => f.status === 'success' || f.status == 0;
      return [
        {
          icon: 'el-icon-video-pause',
          title: '暂停',
          rules: (f) => this.isChunkUploading(f) && !f.chunkPaused,
          event: (f) => this.pauseUpload(f)
        },
        {
          icon: 'el-icon-video-play',
          title: '继续',
          rules: (f) => this.isChunkUploading(f) && !!f.chunkPaused,
          event: (f) => this.resumeUpload(f)
        },
        {
          icon: 'el-icon-document-copy',
          title: '预览',
          rules: (f) => !!this.preview && done(f),
          emit: 'preview'
        },
        {
          icon: 'el-icon-download',
          title: '下载',
          rules: (f) => this.isDownload && done(f),
          emit: 'download'
        },
        {
          icon: 'el-icon-delete',
          title: '删除',
          rules: (f) => this.isRemove,
          emit: 'remove'
        }
      ];
    }
  },
  watch: {
    fileList: {
      immediate: true,
      deep: true,
      handler(val) {
        if (Array.isArray(val) && val.length > 0) {
          this.lists = val;
          let filesTotalSize = 0;
          val.forEach((item) => {
            if (
              Object.prototype.hasOwnProperty.call(item, 'fileSize') &&
              item.fileSize
            ) {
              filesTotalSize += parseFloat(item.fileSize, 10);
            } else {
              filesTotalSize += item.size
                ? Math.round((item.size / 1024) * 10) / 10
                : 0;
            }
          });
          this.filesTotalSize = filesTotalSize;
          this.$emit('input', val);
        }
      }
    },
    value: {
      immediate: true,
      deep: true,
      handler(val) {
        if (val && typeof val === 'object' && this.showFileList) {
          if (Array.isArray(val)) {
            this.lists = val;
          } else {
            this.lists = [val];
          }
        }
      }
    },
    params: {
      deep: true,
      handler() {
        this.getFiles();
      }
    },
    lists: {
      deep: true,
      handler(val) {
        this.$emit('update:fileCount', val.length);
        this.$emit('getFilesAmount', val.length);
      }
    },
    display(val) {
      val && this.getHeight();
    }
  },
  beforeCreate() {
    this.getFiles = debounce(500, () => {
      this.getFileLists();
    });
    this.getAdjunctPropertie = debounce(500, () => {
      this.getAdjunctProperties();
    });
  },
  created() {
    // 分片上传的运行态（按 file.uid 索引，非响应式，仅用于流程控制）
    this._chunkState = {};
    const dochubConfig = sessionStorage.getItem('dochubConfig');
    if (dochubConfig) {
      this.dochubConfig = JSON.parse(dochubConfig);
    }
    this.getAdjunctPropertie();
  },
  mounted() {
    this.$nextTick(() => {
      this.getHeight();
    });
  },
  methods: {
    getHeight() {
      if (this.height === 'auto') {
        let pt =
          parseInt(util.getStyle(this.$el.parentNode, 'padding-top'), 10) || 0;
        let pb =
          parseInt(util.getStyle(this.$el.parentNode, 'padding-bottom'), 10) ||
          0;
        let btn = this.$refs.upload
          ? this.$refs.upload.$children[0].$el.offsetHeight || 0
          : 0;
        this.boxHeight =
          this.$el.parentNode.offsetHeight - pt - pb - btn + 'px';
      }
    },
    //根据code获取附件参数配置
    getAdjunctProperties() {
      if (this.code !== undefined) {
        let config = store.get(this.code);
        if (config) {
          this.fileAccept = config.accept;
          this.fileSize = config.size;
          this.fileTotalSize = config.totalSize;
          if (config.dochubConfig) {
            this.dochubConfig = config.dochubConfig;
            if (config.bucketChunkConfig !== undefined) {
              this.bucketChunkConfig = config.bucketChunkConfig;
            } else {
              this.getBucketChunkConfig();
            }
          }

          let url =
            this.portrait || this.documentId
              ? this.dochubConfig.reuploadDocumentUrl || uploadOnlyOne
              : this.dochubConfig.uploadDocumentUrl || uploads;
          this.uploadUrl = url.indexOf(this.host) > -1 ? url : this.host + url;
          this.requestFiles && this.getFiles();
        } else {
          util
            .ajax({
              method: this.method,
              url: this.properties,
              data: { code: this.code, ...this.param },
              params: { code: this.code, ...this.param }
            })
            .then((res) => {
              if (res.rCode === 0) {
                if (res.results) {
                  this.excludeNames = res.results.excludeName;
                  if (res.results.fileTypeExtName) {
                    let fileTypeExtName =
                      res.results.fileTypeExtName.split(';');
                    this.fileAccept = fileTypeExtName
                      .filter((item) => {
                        return item;
                      })
                      .join(',');
                  }
                  if (res.results.dochubConfig) {
                    this.dochubConfig = res.results.dochubConfig;
                    sessionStorage.setItem(
                      'dochubConfig',
                      JSON.stringify(res.results.dochubConfig)
                    );
                    // dochubConfig 有数据 → 走文档中台，再拉取该桶的桶配置（分片上传开关/参数）
                    this.getBucketChunkConfig();
                  }
                  let url = this.portrait
                    ? this.dochubConfig.reuploadDocumentUrl || uploadOnlyOne
                    : this.dochubConfig.uploadDocumentUrl || uploads;
                  this.uploadUrl =
                    url.indexOf(this.host) > -1 ? url : this.host + url;
                  if (res.results.kkViewRootPath) {
                    this.kkfileview = res.results.kkViewRootPath;
                  }
                  this.fileSize = res.results.limitFileSize
                    ? res.results.limitFileSize
                    : 0;
                  this.fileTotalSize = res.results.limitTotalSize;
                  store.set(this.code, {
                    accept: this.fileAccept,
                    size: this.fileSize,
                    totalSize: this.fileTotalSize,
                    dochubConfig: this.dochubConfig
                  });
                }
                this.requestFiles && this.getFiles();
              }
            })
            .catch((err) => {
              if (err.message && err.message !== 'canceled') {
                this.$message.error(err.message);
              }
            });
        }
      }
    },
    // 根据桶编码调中台获取桶配置接口（/v2/getBucket），取 baseBucketConfig 中的分片上传开关与参数。
    getBucketChunkConfig() {
      if (!this.code) return;
      let url = getDochubBucket;
      util
        .ajax({
          method: this.method,
          url: url,
          data: { bucketCode: this.code },
          params: { bucketCode: this.code }
        })
        .then((res) => {
          if (res.rCode === 0 && res.results && res.results.baseBucketConfig) {
            const cfg = res.results.baseBucketConfig;
            this.bucketChunkConfig = {
              enableChunkUpload: cfg.enableChunkUpload,
              chunkSize: cfg.chunkSize,
              chunkConcurrent: cfg.chunkConcurrent
            };
          } else {
            this.bucketChunkConfig = null;
          }
          // 回写本地缓存，避免同 code 的其他实例重复请求
          let cached = store.get(this.code);
          if (cached) {
            store.set(this.code, {
              ...cached,
              bucketChunkConfig: this.bucketChunkConfig
            });
          }
        })
        .catch(() => {
          // 桶配置获取失败不阻断上传，维持 props 行为
          this.bucketChunkConfig = null;
        });
    },
    getFileLists() {
      if (
        !this.show ||
        (this.fileList &&
          Array.isArray(this.fileList) &&
          this.fileList.length) ||
        (this.requiredOwnId &&
          !Object.prototype.hasOwnProperty.call(this.params, 'ownId'))
      ) {
        return false;
      }
      let url =
        typeof this.fileList === 'string'
          ? this.fileList
          : this.dochubConfig.getDocumentList
          ? this.dochubConfig.getDocumentList
          : getAdjunctFileInfos;
      util
        .ajax({
          method: this.method,
          url: url,
          data: this.params,
          params: this.params,
          format: false
        })
        .then((res) => {
          if (res.rCode === 0) {
            if (this.portrait === true) {
              this.image = JSON.parse(JSON.stringify(res.results))[0];
            } else {
              this.lists = JSON.parse(JSON.stringify(res.results));
              let filesTotalSize = 0;
              this.lists.forEach((item) => {
                if (
                  Object.prototype.hasOwnProperty.call(item, 'fileSize') &&
                  item.fileSize
                ) {
                  filesTotalSize += parseFloat(item.fileSize, 10);
                } else {
                  filesTotalSize += item.size
                    ? Math.round((item.size / 1024) * 10) / 10
                    : 0;
                }
              });
              this.filesTotalSize = filesTotalSize;
              if (this.lists.length) {
                this.$emit('input', this.lists);
              }
            }
          } else {
            let msg = res.msg || '系统错误，请联系管理员！';
            this.$message.error(msg);
          }
        })
        .catch((err) => {
          if (err.message && err.message !== 'canceled') {
            this.$message.error(err.message);
          }
        });
    },
    handleUpload() {
      this.$refs.upload.submit();
    },
    openKkfileview(file) {
      let host = util.win.location.origin;
      if (this.host) {
        if (util.startWith(this.host, 'http')) {
          host = this.host;
        } else {
          host += this.host;
        }
      }
      let url = encodeURIComponent(
        Base64.encode(
          host +
            (typeof this.download === 'string'
              ? this.download
              : downloadByAdjunctId) +
            `?adjunctId=${
              file.adjunctId || file.response.adjunctId
            }&fullfilename=${file.originalName}`
        )
      );
      util.win.open(`${this.kkfileview}?url=${url}`);
    },
    handlePreview(res) {
      if (this.preview) {
        if (this.onPreview) {
          this.onPreview(res);
        } else {
          let file = res.response ? res.response : res;
          let url =
            typeof this.preview === 'string'
              ? this.preview
              : typeof this.preview === 'function'
              ? this.preview(file)
              : null;
          if (res.status === 'success' || res.status == 0) {
            let suffix = file.suffix;
            suffix = suffix.toLowerCase();
            if (this.dochubConfig.previewDocumentUrl) {
              if (
                suffix.includes('jpeg') ||
                suffix.includes('jpg') ||
                suffix.includes('gif') ||
                suffix.includes('png')
              ) {
                this.imgUrl =
                  res.url && util.startWith(url, ['http', '/', true])
                    ? res.url
                    : this.host +
                      (url
                        ? url
                        : `${this.dochubConfig.downloadDocumentUrl}?documentId=${file.adjunctId}`);
                this.title = file.originalName;
                this.showImg = true;
                this.$nextTick(() => {
                  this.loadImage();
                });
              } else if (suffix.includes('mp4')) {
                this.source = {
                  src:
                    this.host +
                    (url
                      ? url
                      : `${this.dochubConfig.downloadDocumentUrl}?documentId=${file.adjunctId}`)
                };
                this.title = file.originalName;
                this.showVideo = true;
              } else {
                util.win.open(
                  this.host +
                    (url
                      ? url
                      : `${this.dochubConfig.previewDocumentUrl}?documentId=${file.adjunctId}&action=附件预览`)
                );
              }
            } else {
              if (
                suffix.includes('doc') ||
                suffix.includes('docx') ||
                suffix.includes('xls') ||
                suffix.includes('xlsx') ||
                suffix.includes('ppt')
              ) {
                if (this.kkfileview) {
                  this.openKkfileview(file);
                } else {
                  util.win.open(
                    this.host +
                      (url ? url : previewAdjunctOffice) +
                      '?cmd=view&bucketName=' +
                      file.absolutePath +
                      '&fileId=' +
                      file.adjunctId +
                      '&fileName=' +
                      file.newName
                  );
                }
              } else if (suffix.includes('pdf')) {
                if (this.kkfileview) {
                  this.openKkfileview(file);
                } else {
                  util.win.open(
                    this.host +
                      (url ? url : previewAdjunct2) +
                      '/' +
                      file.originalName +
                      '?adjunctId=' +
                      file.adjunctId
                  );
                }
              } else if (
                suffix.includes('jpeg') ||
                suffix.includes('jpg') ||
                suffix.includes('gif') ||
                suffix.includes('png')
              ) {
                this.imgUrl =
                  res.url && util.startWith(url, ['http', '/', true])
                    ? res.url
                    : this.host +
                      (url ? url : this.previewAdjunct) +
                      '?adjunctId=' +
                      file.adjunctId;
                this.title = file.originalName;
                this.showImg = true;
                this.$nextTick(() => {
                  this.loadImage();
                });
              } else if (suffix.includes('mp4')) {
                this.source = {
                  src:
                    this.host +
                    (url ? url : this.previewAdjunct) +
                    '?adjunctId=' +
                    file.adjunctId
                };
                this.title = file.originalName;
                this.showVideo = true;
              } else {
                if (this.kkfileview) {
                  this.openKkfileview(file);
                } else {
                  util.win.open(
                    this.host +
                      (url ? url : this.previewAdjunct) +
                      '?adjunctId=' +
                      file.adjunctId
                  );
                }
              }
            }
          } else {
            util.win.open(URL.createObjectURL(file.raw));
          }
        }
      }
    },
    closeViewer() {
      this.showImg = false;
    },
    loadImage() {
      this.$refs.showImg &&
        (this.$refs.showImg.onload = () => {
          this.imgChange = false;
          let w = this.$refs.showImg.naturalWidth;
          let h = this.$refs.showImg.naturalHeight;
          let pw = this.$refs.showImg.parentNode.offsetWidth;
          let ph = this.$refs.showImg.parentNode.offsetHeight;
          if (w / h > pw / ph) {
            this.styles = { 'max-width': '100%' };
          } else {
            this.styles = { 'max-height': '100%' };
          }
          if (w < pw) {
            this.styles.width = w + 'px';
          }
          if (h < ph) {
            this.styles.height = h + 'px';
          }
        });
    },
    isLt(file) {
      let flag = false;
      let flieArr = file.name.split('.');
      let suffix = '.' + flieArr[flieArr.length - 1].toLowerCase();
      if (this.accepts) {
        let accepts = this.accepts.toLowerCase().split(',');
        flag = accepts.includes(suffix);
        if (!flag) {
          this.$message.error(`文件类型错误，请上传${this.accepts}类型文件!`);
          return flag;
        }
      }
      let size = this._size;
      let text = '单个附件';
      let se = Math.round((file.size / 1024) * 10) / 10;
      if (this._size === 0 && this._totalSize === 0) {
        flag = true;
      } else if (this._size > 0 && this._totalSize === 0) {
        flag = se < this._size;
        size = this._size;
        text = '单个附件';
      } else if (this._size === 0 && this._totalSize > 0) {
        flag = this.filesTotalSize + se < this._totalSize;
        size = this._totalSize;
        text = '附件总';
      } else {
        flag = se < this._size && this.filesTotalSize + se < this._totalSize;
        if (se < this._size) {
          size = this._size;
          text = '单个附件';
        } else {
          size = this._totalSize;
          text = '附件总';
        }
      }
      if (!flag) {
        this.$message.error(`上传${text}大小不能超过${size}KB!`);
        return flag;
      }
      return flag;
    },
    handleBeforeUpload(file) {
      const flag = this.isLt(file);
      if (this.portrait) {
        if (flag && this.beforeUpload) {
          return this.beforeUpload(file);
        }
        return flag;
      }
      if (this.beforeUpload) {
        if (flag) {
          return this.beforeUpload(file);
        }
        return flag;
      }
      return flag;
    },
    handleBeforeRemove(file, fileList) {
      if (file && (file.status === 'success' || file.status == 0)) {
        return this.$confirm('确定删除文件吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        })
          .then(() => {
            if (this.beforeRemove) {
              return this.beforeRemove(file, fileList);
            } else if (file.status === 'success' || file.status == 0) {
              let userName =
                file.userName ||
                (file.response && file.response.userName) ||
                util.getStorage('userName');
              // eslint-disable-next-line no-undef
              return new Promise((resolve, reject) => {
                let url =
                  typeof this.deleted === 'string'
                    ? this.deleted
                    : this.remove === 'string'
                    ? this.remove
                    : this.dochubConfig.deleteDocumentUrl || delAdjunct;
                util
                  .ajax({
                    method: this.method,
                    url: url,
                    data: {
                      userName: userName,
                      documentId: file.adjunctId || file.response.adjunctId,
                      id: file.adjunctId || file.response.adjunctId
                    },
                    params: {
                      userName: userName,
                      documentId: file.adjunctId || file.response.adjunctId,
                      id: file.adjunctId || file.response.adjunctId
                    }
                  })
                  .then((res) => {
                    if (res.rCode === 0) {
                      let se =
                        this.filesTotalSize -
                        Math.round((file.size / 1024) * 10) / 10;
                      this.filesTotalSize = se;
                      this.$message.success(res.msg);
                      resolve();
                    } else {
                      let msg = res.msg || '系统错误，请联系管理员！';
                      this.$message.error(msg);
                      reject();
                    }
                  })
                  .catch((err) => {
                    if (err.message && err.message !== 'canceled') {
                      this.$message.error(err.message);
                    }
                  });
              });
            }
            return true;
          })
          .catch(() => {
            return flag;
          });
      }
    },
    handleDownloads() {
      let url =
        typeof this.downloads === 'string'
          ? this.downloads
          : this.dochubConfig.compressDownloadDocumentUrl || uploadDownloads;
      util.win.open(
        this.host +
          url +
          '?ownId=' +
          this.ownId +
          '&code=' +
          this.code +
          (this.extendCode ? '&extendCode=' + this.extendCode : '') +
          '&businessId=' +
          this.ownId +
          '&fileName=文档.zip' +
          '&bucketCode=' +
          this.code
      );
    },
    handleDownload(file) {
      if (file.status === 'success' || file.status == 0) {
        let url =
          typeof this.download === 'string'
            ? this.download
            : this.dochubConfig.downloadDocumentUrl || downloadByAdjunctId;
        util.win.open(
          this.host +
            url +
            '?adjunctId=' +
            (file.adjunctId || file.response.adjunctId) +
            '&documentId=' +
            (file.adjunctId || file.response.adjunctId)
        );
      } else {
        this.aLinkDownload(file);
      }
    },
    handleClick(res) {
      if (res.event && typeof res.event === 'function') {
        res.event(res);
      } else {
        this.$emit('click', res);
      }
    },
    aLinkDownload(file) {
      var a = document.createElement('a');
      let event = new MouseEvent('click');
      a.href = URL.createObjectURL(file.raw);
      a.download = file.name;
      a.dispatchEvent(event);
    },
    handleSort(files) {
      if (this.isSort) {
        if (this.onSort) {
          this.onSort(files);
        } else {
          if (
            !this.ownId ||
            !this.code ||
            (this.fileList && this.fileList.length > 0)
          )
            return;
          let ids = files.map((item) => {
            if (item.status === 'success' || item.status == 0) {
              return item.adjunctId || item.response.adjunctId;
            }
          });
          ids = ids.join(',');
          util
            .ajax({
              method: this.method,
              url: this.dochubConfig.sortDocumentsUrl || uploadSort,
              data: {
                ids: ids,
                documentIds: ids,
                ownId: this.ownId,
                code: this.code,
                bucketCode: this.code,
                businessId: this.ownId
              },
              params: {
                ids: ids,
                documentIds: ids,
                ownId: this.ownId,
                code: this.code,
                bucketCode: this.code,
                businessId: this.ownId
              }
            })
            .then((res) => {
              if (res.rCode === 0) {
                this.$message.success(res.msg);
              } else {
                let msg = res.msg || '系统错误，请联系管理员！';
                this.$message.error(msg);
              }
            })
            .catch((err) => {
              if (err.message && err.message !== 'canceled') {
                this.$message.error(err.message);
              }
            });
        }
      }
    },
    handleSuccess(response, file, fileList) {
      if (this.portrait) {
        this.image = URL.createObjectURL(file.raw);
      }
      if (this.showMessage.success) {
        if (response.rCode) {
          if (this.dangerouslyUseHTMLString) {
            this.$alert(
              response.msg ? response.msg : '上传失败：请联系管理员',
              '提示',
              {
                dangerouslyUseHTMLString: true,
                setHeight: response.msg
              }
            ).catch();
          } else {
            this.$message({
              type: 'error',
              message: response.msg || '成功！'
            });
          }
        } else {
          this.$message.success(response.msg);
        }
      }
      let se =
        this.filesTotalSize +
        (this.resultFile ? Math.round((file.size / 1024) * 10) / 10 : 0);
      this.filesTotalSize = se;
      this.onSuccess &&
        this.onSuccess(response, file, fileList, this.dochubConfig);
      this.$emit('success', response, file, fileList, this.dochubConfig);
      if (this.resultFile) {
        if (this.portrait) {
          this.$emit('input', file);
        } else {
          this.$emit('input', fileList);
        }
        if (this.elForm) {
          this.elForm.clearValidate(this.name);
        }
      }
    },
    handleChange(file, fileList) {
      this.$emit('change', fileList);
      this.onChange && this.onChange(file, fileList);
    },
    handleRemove(file, fileList) {
      this.cleanupChunk(file);
      this.$emit('input', fileList.length ? fileList : '');
      this.$emit('remove', file, fileList);
      this.$emit('change', fileList);
      this.onRemove && this.onRemove(file, fileList);
    },
    handleError(err, file, fileList) {
      if (this.showMessage.error) {
        if (this.dangerouslyUseHTMLString) {
          this.$alert(err.msg ? err.msg : '上传失败：请联系管理员', '提示', {
            dangerouslyUseHTMLString: true,
            setHeight: err.msg
          }).catch();
        } else {
          this.$message({
            type: 'error',
            message: '上传失败：' + (err.msg ? err.msg : '请联系管理员')
          });
        }
      }
      this.$emit('error', err, file, fileList);
      this.onError && this.onError(err, file, fileList);
    },
    // el-upload 的自定义上传实现：建立可暂停的运行态并返回受控 Promise。
    // 暂停时不 resolve/reject（promise 维持 pending，文件保持"上传中"而非失败），继续时再次进入流程。
    chunkUpload(options) {
      const file = options.file;
      const uid = file.uid;
      const state = {
        paused: false,
        controller: null,
        fileMd5: '',
        totalChunks: Math.max(1, Math.ceil(file.size / this._chunkSize)),
        options: options
      };
      this._chunkState[uid] = state;
      return new Promise((resolve, reject) => {
        state.resolve = resolve;
        state.reject = reject;
        this.runChunkFlow(uid);
      });
    },
    // 分片主流程（可被"继续"重新进入）：MD5(缓存) → 秒传/断点续传检测 → 并发上传缺失分片 → 合并入库。
    runChunkFlow(uid) {
      const state = this._chunkState[uid];
      if (!state) return;
      const options = state.options;
      const file = options.file;
      const onProgress = options.onProgress || function () {};
      const baseParams = () => ({
        ...this.param,
        fileMd5: state.fileMd5,
        fileName: file.name,
        fileSize: file.size,
        totalChunks: state.totalChunks,
        bucketCode: this.code,
        businessId: this.ownId
      });
      const isAbort = (e) =>
        !!e &&
        (e.__aborted === true ||
          e.name === 'CanceledError' ||
          e.name === 'AbortError' ||
          (typeof e.message === 'string' && /cancel|abort/i.test(e.message)));

      Promise.resolve()
        .then(() => state.fileMd5 || this.computeFileMd5(file, onProgress))
        .then((md5) => {
          state.fileMd5 = md5;
          if (state.paused) return null;
          return util.ajax({
            method: 'post',
            url: this.chunkUrls.check,
            data: baseParams(),
            params: baseParams(),
            headers: options.headers
          });
        })
        .then((checkRes) => {
          if (!checkRes) return; // 暂停于 MD5/检测阶段，维持 pending
          if (checkRes.rCode !== 0) return Promise.reject(checkRes);
          const info = checkRes.results || {};
          // 秒传命中：该业务桶下已存在同 MD5 文件，直接视为成功
          if (info.instant) {
            onProgress({ percent: 100 });
            this.normalizeChunkDoc(info, file.name);
            this.finishChunk(uid, {
              rCode: 0,
              msg: checkRes.msg || '秒传成功',
              results: info
            });
            // 秒传文件已存在于服务端，刷新列表以展示其完整信息（含可下载/预览的 id）
            this.requestFiles && this.getFiles();
            return;
          }
          // 断点续传：跳过服务端已存在的分片
          const uploaded = new Set(info.uploadedChunks || []);
          const pending = [];
          for (let i = 0; i < state.totalChunks; i++) {
            if (!uploaded.has(i)) pending.push(i);
          }
          let finished = state.totalChunks - pending.length;
          // 上传阶段占 5~95%（MD5 校验已占 0~5%），保证进度单调不回退
          onProgress({
            percent: Math.min(
              95,
              5 + Math.round((finished / state.totalChunks) * 90)
            )
          });
          state.controller =
            typeof AbortController !== 'undefined'
              ? new AbortController()
              : null;
          const signal = state.controller ? state.controller.signal : undefined;
          return this.runChunkQueue(pending, this._chunkConcurrent, (index) => {
            if (state.paused) return Promise.reject({ __aborted: true });
            return this.uploadOneChunk(
              file,
              index,
              state.totalChunks,
              state.fileMd5,
              options.headers,
              signal
            ).then(() => {
              finished++;
              onProgress({
                percent: Math.min(
                  95,
                  5 + Math.round((finished / state.totalChunks) * 90)
                )
              });
            });
          }).then(() => {
            if (state.paused) return; // 暂停于上传阶段，维持 pending
            return util
              .ajax({
                method: 'post',
                url: this.chunkUrls.merge,
                data: baseParams(),
                params: baseParams(),
                headers: options.headers
              })
              .then((mergeRes) => {
                if (mergeRes.rCode !== 0) return Promise.reject(mergeRes);
                onProgress({ percent: 100 });
                this.normalizeChunkDoc(mergeRes.results, file.name);
                this.finishChunk(uid, mergeRes);
                // 刷新列表：让刚入库的文件以与其他行一致的结构展示（时间/大小/上传人等）
                this.requestFiles && this.getFiles();
              });
          });
        })
        .catch((e) => {
          // 暂停导致的中止：保持 pending 等待"继续"；其余才算失败
          if (state.paused || isAbort(e)) return;
          this.failChunk(uid, e);
        });
    },
    // 暂停：置暂停标记并中止在传分片；promise 维持 pending，文件保持"上传中"
    pauseUpload(file) {
      const state = file && this._chunkState[file.uid];
      if (!state) return;
      state.paused = true;
      if (state.controller) {
        try {
          state.controller.abort();
        } catch (e) {
          /* ignore */
        }
      }
      this.$set(file, 'chunkPaused', true);
      this.$emit('chunk-pause', file);
    },
    // 继续：清暂停标记，重新进入流程（checkFile 会返回已传分片，自动续传）
    resumeUpload(file) {
      const state = file && this._chunkState[file.uid];
      if (!state || !state.paused) return; // 仅暂停态可续传，防止重复触发并发流程
      state.paused = false;
      this.$set(file, 'chunkPaused', false);
      this.$emit('chunk-resume', file);
      this.runChunkFlow(file.uid);
    },
    // 该文件是否处于分片上传运行态（用于操作列按钮显隐）
    isChunkUploading(file) {
      return !!(file && this._chunkState && this._chunkState[file.uid]);
    },
    // 归一化合并响应：dochub Document 用 docTagId 标识、且无 suffix 字段，
    // 而组件下载读 adjunctId、预览读 suffix/originalName，这里统一映射补齐，避免预览取 suffix 报错。
    normalizeChunkDoc(results, fileName) {
      if (!results || typeof results !== 'object') return results;
      const id =
        results.adjunctId ||
        results.docTagId ||
        results.documentId ||
        results.id;
      if (id) {
        if (!results.adjunctId) results.adjunctId = id;
        if (!results.documentId) results.documentId = id;
      }
      // 文件名：优先用上传时的原始名，其次后端 documentFile.docName
      const name =
        fileName ||
        (results.documentFile && results.documentFile.docName) ||
        results.originalName ||
        results.name ||
        '';
      if (!results.originalName && name) results.originalName = name;
      // 后缀：预览按后缀判断类型，缺省从文件名推导；始终保证为字符串
      if (results.suffix === undefined || results.suffix === null) {
        results.suffix =
          name && name.lastIndexOf('.') > -1
            ? name.slice(name.lastIndexOf('.') + 1)
            : '';
      }
      // 展示字段：dochub 的元数据在 documentFile 下且字段名不同，映射成 showInfo 默认读取的扁平字段
      const df = results.documentFile;
      if (df && typeof df === 'object') {
        if (!results.uploadTime) {
          results.uploadTime = df.createTime || df.lastModifyTime || '';
        }
        if (!results.userName) {
          results.userName = df.createUserName || df.lastModifyUserName || '';
        }
        if (!results.fileSize && (df.docSize || df.docSize === 0)) {
          results.fileSize = this.formatFileSize(df.docSize);
        }
      }
      return results;
    },
    // 字节数格式化为可读大小（与列表展示风格一致：1 位小数、无空格）
    formatFileSize(bytes) {
      const n = Number(bytes);
      if (!n || n <= 0) return '0B';
      if (n >= 1073741824) return (n / 1073741824).toFixed(1) + 'GB';
      if (n >= 1048576) return (n / 1048576).toFixed(1) + 'MB';
      if (n >= 1024) return (n / 1024).toFixed(1) + 'KB';
      return n + 'B';
    },
    // 完成：兑现 promise 并清理运行态
    finishChunk(uid, response) {
      const state = this._chunkState[uid];
      if (!state) return;
      state.resolve && state.resolve(response);
      delete this._chunkState[uid];
    },
    // 失败：拒绝 promise 并清理运行态
    failChunk(uid, err) {
      const state = this._chunkState[uid];
      if (!state) return;
      state.reject && state.reject(err);
      delete this._chunkState[uid];
    },
    // 移除文件时清理分片运行态（中止在传分片，孤立 pending promise 不再触发回调）
    cleanupChunk(file) {
      const uid = file && file.uid;
      const state = uid && this._chunkState && this._chunkState[uid];
      if (!state) return;
      state.paused = true;
      state.resolve = null;
      state.reject = null;
      if (state.controller) {
        try {
          state.controller.abort();
        } catch (e) {
          /* ignore */
        }
      }
      delete this._chunkState[uid];
    },
    // 上传单个分片（multipart/form-data）；signal 用于暂停时中止
    uploadOneChunk(file, index, totalChunks, fileMd5, headers, signal) {
      const start = index * this._chunkSize;
      const end = Math.min(start + this._chunkSize, file.size);
      const formData = new FormData();
      formData.append('file', file.slice(start, end), file.name);
      formData.append('fileMd5', fileMd5);
      formData.append('fileName', file.name);
      formData.append('fileSize', file.size);
      formData.append('chunkIndex', index);
      formData.append('totalChunks', totalChunks);
      formData.append('bucketCode', this.code);
      formData.append('businessId', this.ownId);
      Object.keys(this.param || {}).forEach((key) => {
        formData.append(key, this.param[key]);
      });
      return util
        .ajax({
          method: 'post',
          url: this.chunkUrls.upload,
          data: formData,
          format: false,
          headers: headers,
          signal: signal
        })
        .then((res) => {
          if (res.rCode !== 0) return Promise.reject(res);
          return res;
        });
    },
    // 分块读取文件并增量计算 MD5（避免大文件一次性读入内存）
    computeFileMd5(file, onProgress) {
      return new Promise((resolve, reject) => {
        const size = this._chunkSize;
        const chunks = Math.max(1, Math.ceil(file.size / size));
        const spark = new SparkMD5.ArrayBuffer();
        const reader = new FileReader();
        let current = 0;
        const loadNext = () => {
          const start = current * size;
          reader.readAsArrayBuffer(
            file.slice(start, Math.min(start + size, file.size))
          );
        };
        reader.onload = (e) => {
          spark.append(e.target.result);
          current++;
          // 校验阶段占进度 0~5%，剩余留给上传与合并
          onProgress &&
            onProgress({ percent: Math.round((current / chunks) * 5) });
          if (current < chunks) {
            loadNext();
          } else {
            // 取标准 32 位 MD5 的中间 16 位，与后端 hutool digestHex16（即 doc_md5）口径一致，
            // 否则秒传按 md5 比对永远不命中
            resolve(spark.end().substring(8, 24));
          }
        };
        reader.onerror = () => reject({ msg: '文件读取失败，无法计算MD5' });
        loadNext();
      });
    },
    // 限制并发的分片任务队列
    runChunkQueue(indexes, concurrency, worker) {
      return new Promise((resolve, reject) => {
        if (!indexes.length) return resolve();
        let cursor = 0;
        let active = 0;
        let failed = false;
        const schedule = () => {
          if (failed) return;
          if (cursor >= indexes.length && active === 0) return resolve();
          while (active < concurrency && cursor < indexes.length) {
            const index = indexes[cursor++];
            active++;
            worker(index)
              .then(() => {
                active--;
                schedule();
              })
              .catch((err) => {
                failed = true;
                reject(err);
              });
          }
        };
        schedule();
      });
    }
  }
};
</script>
