index.vue 8.64 KB
<script setup lang="ts">
  import usePagination from '@/hooks/pagination';
  import useMaterialApi from '@/http/material';
  import { createVNode, onMounted, ref, toRef } from 'vue';
  import useLoading from '@/hooks/loading';
  import { AnyObject } from '@/types/global';
  import {
    createModalVNode,
    createFormVNode,
    createFormItemVNode,
    createInputNumberFormItemVNode,
    createInputUploadFormItemVNode,
    createSelectionFormItemVNode,
  } from '@/utils/createVNode';
  import { FileItem, Message, RequestOption, Upload } from '@arco-design/web-vue';
  import { promiseToBoolean } from '@/utils';
  import { pick, set } from 'lodash';
  import useOss from '@/hooks/oss';

  const { loading, setLoading } = useLoading();
  const { pagination, setPage, setPageSize, setTotal } = usePagination({ pageSize: 30 });
  const filter = ref<AnyObject>({});
  const list = ref<AnyObject[]>([]);

  const sortOption = [
    { label: '创建时间', value: 'id' },
    { label: '权重值', value: 'weight' },
  ];

  const onQuery = async (params?: AnyObject) => {
    setLoading(true);
    const { current: page, pageSize } = pagination.value;
    useMaterialApi
      .get({ ...filter.value, page, pageSize, ...params })
      .then(({ data, meta }) => {
        list.value = data as AnyObject[];
        setPage(meta.current);
        setPageSize(meta.limit);
        setTotal(meta.total);
      })
      .finally(() => setLoading(false));
  };

  const onPageChange = (current: number) => {
    onQuery({ page: current });
  };

  const onSearch = () => onQuery({ page: 1 });

  const onReset = () => {
    filter.value = { type: '', sortBy: 'id', sortType: 'desc' };
    onSearch();
  };

  const onPageSizeChange = (current: number) => {
    onQuery({ page: 1, pageSize: current });
  };

  onMounted(() => onReset());

  const onCreate = () => {
    const formRef = ref();
    const formValue = ref<{ type: number | string; fileList: FileItem[] }>({ type: '', fileList: [] });
    const formRule = {
      type: [{ required: true, message: '请选择分类' }],
      fileList: [{ required: true, type: 'array', message: '请选择上传的素材' }],
    };

    createModalVNode(
      () =>
        createFormVNode({ ref: formRef, model: formValue, rules: formRule, autoLabelWidth: true }, [
          createSelectionFormItemVNode(toRef(formValue.value, 'type'), useMaterialApi.typeOption, { label: '分类', field: 'type' }, {}),
          createFormItemVNode({ hideLabel: false, label: '素材', field: 'fileList', rowClass: 'mb-0' }, [
            createVNode(Upload, {
              'model-value:fileList': formValue.value.fileList,
              'onUpdate:fileList': (value: FileItem[]) => (formValue.value.fileList = value),
              'multiple': true,
              'accept': 'image/*',
              'listType': 'picture-card',
              'responseUrlKey': 'url',
              'imagePreview': true,
              'imageLoading': 'lazy',
              'onBeforeUpload': (file: File & { width: number; height: number }) => {
                const imgObj = new Image();
                imgObj.src = URL.createObjectURL(file);
                imgObj.onload = () => {
                  file.width = imgObj.width as number;
                  file.height = imgObj.height as number;
                };
                return Promise.resolve(file);
              },
              'customRequest': (option: RequestOption) => {
                const { fileItem, onProgress, onSuccess, onError } = option;
                if (fileItem.file) {
                  fileItem.status = 'uploading';
                  useOss()
                    .upload(fileItem.file, 'image', onProgress)
                    .then(({ url }) => {
                      fileItem.percent = 100;
                      fileItem.url = url || '';
                      fileItem.status = 'done';
                      onSuccess({ name: fileItem.name, url: fileItem.url });
                    })
                    .catch((e) => {
                      fileItem.status = 'error';
                      onError(e.response);
                    });
                }
              },
            }),
          ]),
        ]),
      {
        title: '上传素材',
        width: 540,
        onOk: () => onSearch(),
        onBeforeOk: async () => {
          if (await formRef.value?.validate()) {
            return false;
          }
          if (formValue.value.fileList.filter((item) => item.status === 'uploading')?.length !== 0) {
            Message.warning({ id: 'create-msg', content: '请待素材上传完成' });
            return false;
          }

          const data: { type: number; url: string | undefined; expand: Partial<File> }[] = [];
          formValue.value.fileList
            ?.filter((item) => item.status === 'done')
            ?.forEach((item) => {
              data.push({
                type: formValue.value.type as number,
                url: item.url,
                expand: pick(item.file, ['size', 'type', 'width', 'height']),
              });
            });

          return promiseToBoolean(useMaterialApi.create({ data }));
        },
      }
    );
  };

  const onUpdate = (row: any) => {
    const formValue = ref({ ...row });
    const type = toRef(formValue.value, 'type', '');
    const weight = toRef(formValue.value, 'weight', 0);
    const url = toRef(formValue.value, 'url', 0);
    const expand = toRef(formValue.value, 'expand', undefined);

    createModalVNode(
      () =>
        createFormVNode({ model: formValue }, [
          createSelectionFormItemVNode(type, useMaterialApi.typeOption, { label: '分类' }),
          createInputNumberFormItemVNode(weight, { label: '权重' }),
          createInputUploadFormItemVNode(
            url,
            { label: '链接', rowClass: 'mb-0' },
            { accept: 'image/*', onSuccess: async (file: any) => set(expand, 'value', pick(file, ['width', 'height', 'size', 'type'])) }
          ),
        ]),
      {
        title: '编辑素材信息',
        onBeforeOk: () => promiseToBoolean(useMaterialApi.update(row.id, formValue.value)),
        onOk: () => onSearch(),
      }
    );
  };

  const onDelete = (row: any) => {
    createModalVNode('是否确认删除此素材', {
      title: '删除操作',
      onBeforeOk: () => promiseToBoolean(useMaterialApi.destroy(row.id)),
      onOk: () => onSearch(),
    });
  };
</script>

<template>
  <page-view has-card has-bread>
    <filter-search :loading="loading" inline :model="filter" @search="onSearch" @reset="onReset">
      <filter-search-item label="类型">
        <a-select v-model="filter.type" placeholder="请选择" :options="useMaterialApi.typeOption" allow-clear />
      </filter-search-item>
      <filter-search-item label="排序">
        <a-select v-model="filter.sortBy" placeholder="请选择" :options="sortOption" />
      </filter-search-item>
    </filter-search>
    <a-space v-permission="['system-material-create']" fill>
      <icon-button style="margin-bottom: 16px" type="primary" icon="plus" label="新增" @click="onCreate" />
    </a-space>

    <a-image-preview-group :closable="false" :default-scale="0.7" :actions-layout="['fullScreen', 'zoomIn', 'zoomOut']">
      <a-space size="small" wrap>
        <template v-for="item in list" :key="item.id">
          <a-image
            :src="item.url"
            :width="269"
            :height="140"
            :show-loader="true"
            fit="cover"
            :title="`${item.expand.width}*${item.expand.height}`"
          >
            <template #extra>
              <div class="actions">
                <span v-permission="['system-material-edit']" class="action" @click="onUpdate(item)"><icon-edit /></span>
                <span v-permission="['system-material-delete']" class="action" @click="onDelete(item)"><icon-delete /></span>
              </div>
            </template>
          </a-image>
        </template>
      </a-space>
    </a-image-preview-group>
    <div style="display: flex; justify-content: end; margin-top: 10px">
      <a-pagination
        :current="pagination.current"
        :page-size="pagination.pageSize"
        :total="pagination.total"
        :page-size-options="pagination.pageSizeOptions as number[]"
        :show-page-size="pagination.showPageSize as boolean"
        :show-total="pagination.showTotal as boolean"
        @change="onPageChange"
        @page-size-change="onPageSizeChange"
      />
    </div>
  </page-view>
</template>

<style scoped lang="less">
  .actions {
    display: flex;
    align-items: center;
  }

  .action {
    padding: 5px 4px;
    font-size: 14px;
    margin-left: 12px;
    border-radius: 2px;
    line-height: 1;
    cursor: pointer;
  }

  .action:first-child {
    margin-left: 0;
  }
</style>