UpLoader.vue 9.66 KB
<!--
 * 上传器
  -->
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMessage } from 'src/common/hooks';

import { getObjectURL } from './utils';
var XLSX = require('xlsx');

const { t } = useI18n();
const { warn } = useMessage();

interface IDetailProps {
  bgColor?: string; // 背景色
  multiple?: boolean; // 多选
  accept?: string; // 上传类型 'image/*'
  dark?: boolean; // 启用深色背景
  noShadow?: boolean; // 阴影
}
const props = withDefaults(defineProps<IDetailProps>(), {
  bgColor: '#96C0CE',
  multiple: false,
  accept: '',
  dark: false,
  noShadow: false,
});

const emit = defineEmits<{
  (e: 'onOk', value: any): void;
}>();

const darkDeep = '#222831';
const darkLight = '#393E46';

const imageTypeList = ['png', 'jpg', 'gif', 'svg'];
const excelTypeList = ['xlsx', 'xls'];
const otherTypeList = ['pdf'];

const viewModel = ref(false);
const fileType = ref('');
const viewDialogTitle = ref('');

const state = reactive({
  fileList: [] as any[],
  viewTableHtml: null as any,
});

const canPreview = computed(() => (type: any) => {
  const viewTypeList = imageTypeList.concat(excelTypeList, otherTypeList);
  return viewTypeList.includes(type);
});

const totalSize = computed(() => {
  const total = state.fileList.reduce((a, b) => a + b.size, 0);

  let unit: string; // 单位
  let size: number | string = 0;

  if (total < 1000) {
    unit = 'B';
    size = total.toFixed(3);
  } else {
    const KB_size = total * 0.0009765625;
    if (KB_size < 1000) {
      unit = 'KB';
      size = KB_size.toFixed(3);
    } else {
      const MB_size = KB_size * 0.0009765625;
      if (MB_size < 1000) {
        unit = 'MB';
        size = MB_size.toFixed(2);
      } else {
        const GB_size = MB_size * 0.0009765625;
        unit = 'GB';
        size = GB_size.toFixed(2);
      }
    }
  }

  return size + unit;
});

function selectFile() {
  let _input: HTMLInputElement = document.createElement('input'); // 创建一个隐藏的input
  _input.setAttribute('type', 'file');
  if (props.multiple) {
    _input.setAttribute('multiple', true); // 多选
  }
  // _input.setAttribute('accept', 'image/*'); // 上传类型
  if (props.accept) {
    _input.setAttribute('accept', props.accept);
  }
  _input.onchange = function (e: any) {
    let arr = [] as any[];
    for (let i = 0; i < e.target.files.length; i++) {
      const _file = e.target.files[i];
      const typeArr = _file.name.split('.');

      const type = typeArr.length > 1 ? typeArr[typeArr.length - 1] : '';

      let unit: string; // 单位
      let size: number | string = 0;
      if (_file.size < 1000) {
        unit = 'B';
        size = _file.size.toFixed(3);
      } else {
        const KB_size = _file.size * 0.0009765625;
        if (KB_size < 1000) {
          unit = 'KB';
          size = KB_size.toFixed(3);
        } else {
          const MB_size = KB_size * 0.0009765625;
          if (MB_size < 1000) {
            unit = 'MB';
            size = MB_size.toFixed(2);
          } else {
            const GB_size = MB_size * 0.0009765625;
            if (GB_size <= 2) {
              unit = 'GB';
              size = GB_size.toFixed(2);
            } else {
              const msg = '文件过大,请选择不超过2GB的文件';
              warn(msg);
              console.warn(msg);
              return;
            }
          }
        }
      }

      arr.push({
        file: _file,
        name: _file.name,
        sizeFormat: size + unit,
        fileType: type,
        size: _file.size,
      });
    }
    nextTick(() => {
      state.fileList = state.fileList.concat(arr);
    });
  };
  _input.click();
}

function removeItem(index: number) {
  state.fileList.splice(index, 1);
}

function removeAll() {
  state.fileList = [];
}

function Ok() {
  let list = [];
  for (const item of state.fileList) {
    list.push(item);
  }
  emit('onOk', list);
}

// 预览
function onView(data: any) {
  if (excelTypeList.includes(data.fileType)) {
    fileType.value = data.fileType;
    viewDialogTitle.value = data.name;
    viewModel.value = true;
    void viewXlsx(data);
  } else {
    const url = getObjectURL(data.file);
    window.open(url);
    window.URL.revokeObjectURL(url);
  }
}

async function viewXlsx(data: any) {
  const file = data.file;
  const _data = await file.arrayBuffer();

  const wb = XLSX.read(_data);
  const ws = wb.Sheets[wb.SheetNames[0]];
  const ele = XLSX.utils.sheet_to_html(ws, { id: 'tabeller' });
  state.viewTableHtml = ele;
}

function closeViewDialog() {
  viewModel.value = false;
  fileType.value = '';
  state.viewTableHtml = null;
}
</script>
<template>
  <div
    class="com-uploader"
    :class="{ 'shadow-4': !props.noShadow }"
    :style="{
      borderColor: props.dark ? darkDeep : props.bgColor,
      backgroundColor: props.dark ? darkLight : '#ffffff',
    }"
  >
    <div
      class="uploader-title"
      :style="{ backgroundColor: props.dark ? darkDeep : props.bgColor }"
    >
      <div class="icon">
        <q-btn
          v-if="state.fileList.length > 0"
          style="width: 28px"
          class="q-mr-xs"
          dense
          size="12px"
          label="OK"
          :title="t('Confirm upload')"
          :style="{
            backgroundColor: props.dark ? props.bgColor : '#fff',
            color: props.dark ? darkLight : props.bgColor,
          }"
          @click="Ok"
        />
        <q-btn
          v-if="state.fileList.length > 0"
          dense
          flat
          icon="bi-trash"
          size="12px"
          :title="t('clear all')"
          :style="{ color: props.dark ? props.bgColor : '#fff' }"
          @click="removeAll"
        />
        <span
          v-if="state.fileList.length > 0"
          :style="{
            color: props.dark ? props.bgColor : '#fff',
          }"
          class="total-file-size"
          >{{ totalSize }}</span
        >
      </div>
      <div class="btn">
        <q-btn
          square
          dense
          push
          icon="add"
          size="12px"
          :title="t('Select file')"
          :label="t('Select file')"
          :style="{
            backgroundColor: props.dark ? props.bgColor : '#fff',
            color: props.dark ? darkLight : props.bgColor,
          }"
          @click="selectFile"
        />
      </div>
    </div>
    <div class="uploader-content">
      <div class="uploader-main-content">
        <div
          class="file-item"
          :style="{ borderColor: props.bgColor }"
          v-for="(item, index) in state.fileList"
          :key="index"
        >
          <div
            class="name"
            :title="item.name + ' ' + item.sizeFormat"
            :style="{ color: props.bgColor }"
          >
            <span class="file-name">{{ item.name }}</span>
            <span class="file-size">&nbsp;&nbsp;{{ item.sizeFormat }}</span>
          </div>
          <div class="btns">
            <div>
              <q-btn
                v-if="canPreview(item.fileType)"
                flat
                dense
                size="12px"
                :title="t('preview')"
                icon="bi-eye"
                :style="{ color: props.bgColor }"
                @click="onView(item)"
              />
            </div>
            <div>
              <q-btn
                flat
                dense
                size="12px"
                :title="t('remove from list')"
                icon="bi-x-circle"
                :style="{ color: props.bgColor }"
                @click="removeItem(index)"
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <q-dialog v-model="viewModel" persistent>
    <q-card
      style="min-height: 600px; min-width: 600px; max-width: 1400px"
      class="column no-wrap"
    >
      <q-card-section class="row items-center q-pb-none">
        <div class="text-h6">{{ viewDialogTitle }}</div>
        <q-space />
        <q-btn icon="close" flat round dense @click="closeViewDialog" />
      </q-card-section>

      <q-card-section class="q-pt-none overflow-auto" style="flex: 1">
        <div v-if="excelTypeList.includes(fileType)">
          <div v-html="state.viewTableHtml" class="table-view"></div>
        </div>
      </q-card-section>
    </q-card>
  </q-dialog>
</template>

<style lang="scss" scoped>
// 上传器
.com-uploader {
  width: 100%;
  min-width: 400px;
  height: 100%;
  min-height: 200px;
  border-radius: 4px;
  box-sizing: border-box;
  border: 1px solid;
  display: flex;
  flex-flow: column nowrap;
  // box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px,
  //   rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
  .total-file-size {
    display: inline-block;
    font-size: 12px;
  }
  .uploader-title {
    height: 46px;
    padding: 8px;
    display: flex;
    flex-flow: row nowrap;
    justify-content: space-between;
    align-items: center;
  }
  .uploader-content {
    flex: 1;
    overflow: auto;
  }
  .uploader-main-content {
    display: grid;
    grid-template-columns: 100%;
    row-gap: 4px;
    margin: 4px;
    .file-item {
      height: 40px;
      box-sizing: border-box;
      border-bottom: 2px solid;

      display: flex;
      flex-flow: row nowrap;
      justify-content: space-between;
      align-items: center;

      .name {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        .file-name {
          font-weight: bold;
        }
        .file-size {
          font-size: 12px;
        }
      }
      .btns {
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        column-gap: 2px;
        > div {
          width: 28px;
        }
      }
    }
  }
  // .dialog-box {
  //   // min-width: 600px;
  //   min-height: 600px;
  // }
}
</style>