Pārlūkot izejas kodu

feat: add CropperAvatar component

Vben 3 gadi atpakaļ
vecāks
revīzija
8e410fc640

+ 7 - 0
CHANGELOG.zh_CN.md

@@ -1,3 +1,10 @@
+## Wip
+
+### ✨ Features
+
+- `Cropper` 头像裁剪新增圆形裁剪功能
+- 新增头像上传组件
+
 ## 2.4.2(2021-06-10)
 
 ### ✨ Refactor

+ 1 - 1
package.json

@@ -37,7 +37,7 @@
     "@logicflow/extension": "^0.4.13",
     "@vueuse/core": "^5.0.2",
     "@zxcvbn-ts/core": "^0.3.0",
-    "ant-design-vue": "2.1.2",
+    "ant-design-vue": "2.1.6",
     "axios": "^0.21.1",
     "codemirror": "^5.61.1",
     "cropperjs": "^1.5.11",

+ 3 - 4
src/components/Application/src/search/AppSearchModal.vue

@@ -4,7 +4,7 @@
       <div :class="getClass" @click.stop v-if="visible">
         <div :class="`${prefixCls}-content`" v-click-outside="handleClose">
           <div :class="`${prefixCls}-input__wrapper`">
-            <Input
+            <a-input
               :class="`${prefixCls}-input`"
               :placeholder="t('common.searchText')"
               ref="inputRef"
@@ -14,7 +14,7 @@
               <template #prefix>
                 <SearchOutlined />
               </template>
-            </Input>
+            </a-input>
             <span :class="`${prefixCls}-cancel`" @click="handleClose">
               {{ t('common.cancelText') }}
             </span>
@@ -59,7 +59,6 @@
 <script lang="ts">
   import { defineComponent, computed, unref, ref, watch, nextTick } from 'vue';
   import { SearchOutlined } from '@ant-design/icons-vue';
-  import { Input } from 'ant-design-vue';
   import AppSearchFooter from './AppSearchFooter.vue';
   import Icon from '/@/components/Icon';
   import clickOutside from '/@/directives/clickOutside';
@@ -75,7 +74,7 @@
 
   export default defineComponent({
     name: 'AppSearchModal',
-    components: { Icon, SearchOutlined, AppSearchFooter, Input },
+    components: { Icon, SearchOutlined, AppSearchFooter },
     directives: {
       clickOutside,
     },

+ 3 - 4
src/components/CountDown/src/CountdownInput.vue

@@ -1,13 +1,12 @@
 <template>
-  <AInput v-bind="$attrs" :class="prefixCls" :size="size" :value="state">
+  <a-input v-bind="$attrs" :class="prefixCls" :size="size" :value="state">
     <template #addonAfter>
       <CountButton :size="size" :count="count" :value="state" :beforeStartFunc="sendCodeApi" />
     </template>
-  </AInput>
+  </a-input>
 </template>
 <script lang="ts">
   import { defineComponent, PropType } from 'vue';
-  import { Input } from 'ant-design-vue';
   import CountButton from './CountButton.vue';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { useRuleFormItem } from '/@/hooks/component/useFormItem';
@@ -24,7 +23,7 @@
 
   export default defineComponent({
     name: 'CountDownInput',
-    components: { [Input.name]: Input, CountButton },
+    components: { CountButton },
     inheritAttrs: false,
     props,
     setup(props) {

+ 6 - 3
src/components/Cropper/index.ts

@@ -1,4 +1,7 @@
-import type Cropper from 'cropperjs';
+import { withInstall } from '/@/utils';
+import cropperImage from './src/Cropper.vue';
+import avatarCropper from './src/CropperAvatar.vue';
 
-export type { Cropper };
-export { default as CropperImage } from './src/Cropper.vue';
+export * from './src/typing';
+export const CropperImage = withInstall(cropperImage);
+export const CropperAvatar = withInstall(avatarCropper);

+ 0 - 15
src/components/Cropper/src/AvatarCropper.vue

@@ -1,15 +0,0 @@
-<template>
-  <div :class="$attrs.class" :style="$attrs.style"> </div>
-</template>
-<script lang="ts">
-  // TODO
-  import { defineComponent } from 'vue';
-
-  export default defineComponent({
-    name: 'AvatarCropper',
-    props: {},
-    setup() {
-      return {};
-    },
-  });
-</script>

+ 258 - 0
src/components/Cropper/src/CopperModal.vue

@@ -0,0 +1,258 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    @register="register"
+    :title="t('component.cropper.modalTitle')"
+    width="800px"
+    :canFullscreen="false"
+    @ok="handleOk"
+    :okText="t('component.cropper.okText')"
+  >
+    <div :class="prefixCls">
+      <div :class="`${prefixCls}-left`">
+        <div :class="`${prefixCls}-cropper`">
+          <CropperImage
+            v-if="src"
+            :src="src"
+            height="300px"
+            :circled="circled"
+            @cropend="handleCropend"
+            @ready="handleReady"
+          />
+        </div>
+
+        <div :class="`${prefixCls}-toolbar`">
+          <Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
+            <a-button size="small" preIcon="ant-design:upload-outlined" type="primary" />
+          </Upload>
+          <Space>
+            <a-button
+              type="primary"
+              preIcon="ant-design:reload-outlined"
+              size="small"
+              @click="handlerToolbar('reset')"
+            />
+            <a-button
+              type="primary"
+              preIcon="ant-design:rotate-left-outlined"
+              size="small"
+              @click="handlerToolbar('rotate', -45)"
+            />
+            <a-button
+              type="primary"
+              preIcon="ant-design:rotate-right-outlined"
+              size="small"
+              @click="handlerToolbar('rotate', 45)"
+            />
+            <a-button
+              type="primary"
+              preIcon="vaadin:arrows-long-h"
+              size="small"
+              @click="handlerToolbar('scaleX')"
+            />
+            <a-button
+              type="primary"
+              preIcon="vaadin:arrows-long-v"
+              size="small"
+              @click="handlerToolbar('scaleY')"
+            />
+            <a-button
+              type="primary"
+              preIcon="ant-design:zoom-in-outlined"
+              size="small"
+              @click="handlerToolbar('zoom', 0.1)"
+            />
+            <a-button
+              type="primary"
+              preIcon="ant-design:zoom-out-outlined"
+              size="small"
+              @click="handlerToolbar('zoom', -0.1)"
+            />
+          </Space>
+        </div>
+      </div>
+      <div :class="`${prefixCls}-right`">
+        <div :class="`${prefixCls}-preview`">
+          <img :src="previewSource" v-if="previewSource" />
+        </div>
+        <template v-if="previewSource">
+          <div :class="`${prefixCls}-group`">
+            <Avatar :src="previewSource" size="large" />
+            <Avatar :src="previewSource" :size="48" />
+            <Avatar :src="previewSource" :size="64" />
+            <Avatar :src="previewSource" :size="80" />
+          </div>
+        </template>
+      </div>
+    </div>
+  </BasicModal>
+</template>
+<script lang="ts">
+  import type { CropendResult, Cropper } from './typing';
+
+  import { defineComponent, ref } from 'vue';
+  import CropperImage from './Cropper.vue';
+  import { Space, Upload, Avatar } from 'ant-design-vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { dataURLtoBlob } from '/@/utils/file/base64Conver';
+  import { isFunction } from '/@/utils/is';
+  import { useI18n } from '/@/hooks/web/useI18n';
+
+  const props = {
+    circled: { type: Boolean, default: true },
+    uploadApi: {
+      type: Function as PropType<({ file: Blob, name: stirng, filename: string }) => Promise<any>>,
+    },
+  };
+
+  export default defineComponent({
+    name: 'CropperAvatar',
+    components: { BasicModal, Space, CropperImage, Upload, Avatar },
+    props,
+    emits: ['uploadSuccess', 'register'],
+    setup(props, { emit }) {
+      let filename = '';
+      const src = ref('');
+      const previewSource = ref('');
+      const cropper = ref<Cropper>();
+      let scaleX = 1;
+      let scaleY = 1;
+
+      const { prefixCls } = useDesign('cropper-am');
+      const [register, { closeModal, setModalProps }] = useModalInner();
+      const { t } = useI18n();
+
+      // Block upload
+      function handleBeforeUpload(file: File) {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        src.value = '';
+        previewSource.value = '';
+        reader.onload = function (e) {
+          src.value = (e.target?.result as string) ?? '';
+          filename = file.name;
+        };
+        return false;
+      }
+
+      function handleCropend({ imgBase64 }: CropendResult) {
+        previewSource.value = imgBase64;
+      }
+
+      function handleReady(cropperInstance: Cropper) {
+        cropper.value = cropperInstance;
+      }
+
+      function handlerToolbar(event: string, arg?: number) {
+        if (event === 'scaleX') {
+          scaleX = arg = scaleX === -1 ? 1 : -1;
+        }
+        if (event === 'scaleY') {
+          scaleY = arg = scaleY === -1 ? 1 : -1;
+        }
+        cropper?.value?.[event]?.(arg);
+      }
+
+      async function handleOk() {
+        const uploadApi = props.uploadApi;
+        if (uploadApi && isFunction(uploadApi)) {
+          const blob = dataURLtoBlob(previewSource.value);
+          try {
+            setModalProps({ confirmLoading: true });
+            const result = await uploadApi({ name: 'file', file: blob, filename });
+            emit('uploadSuccess', { source: previewSource.value, data: result.data });
+            closeModal();
+          } finally {
+            setModalProps({ confirmLoading: false });
+          }
+        }
+      }
+
+      return {
+        t,
+        prefixCls,
+        src,
+        register,
+        previewSource,
+        handleBeforeUpload,
+        handleCropend,
+        handleReady,
+        handlerToolbar,
+        handleOk,
+      };
+    },
+  });
+</script>
+
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-cropper-am';
+
+  .@{prefix-cls} {
+    display: flex;
+
+    &-left,
+    &-right {
+      height: 340px;
+    }
+
+    &-left {
+      width: 55%;
+    }
+
+    &-right {
+      width: 45%;
+    }
+
+    &-cropper {
+      height: 300px;
+      background: #eee;
+      background-image: linear-gradient(
+          45deg,
+          rgba(0, 0, 0, 0.25) 25%,
+          transparent 0,
+          transparent 75%,
+          rgba(0, 0, 0, 0.25) 0
+        ),
+        linear-gradient(
+          45deg,
+          rgba(0, 0, 0, 0.25) 25%,
+          transparent 0,
+          transparent 75%,
+          rgba(0, 0, 0, 0.25) 0
+        );
+      background-position: 0 0, 12px 12px;
+      background-size: 24px 24px;
+    }
+
+    &-toolbar {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 10px;
+    }
+
+    &-preview {
+      width: 220px;
+      height: 220px;
+      margin: 0 auto;
+      overflow: hidden;
+      border: 1px solid @border-color-base;
+      border-radius: 50%;
+
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    &-group {
+      display: flex;
+      padding-top: 8px;
+      margin-top: 8px;
+      border-top: 1px solid @border-color-base;
+      justify-content: space-around;
+      align-items: center;
+    }
+  }
+</style>

+ 102 - 44
src/components/Cropper/src/Cropper.vue

@@ -1,5 +1,5 @@
 <template>
-  <div :class="$attrs.class" :style="getWrapperStyle">
+  <div :class="getClass" :style="getWrapperStyle">
     <img
       v-show="isReady"
       ref="imgElRef"
@@ -12,16 +12,16 @@
 </template>
 <script lang="ts">
   import type { CSSProperties } from 'vue';
-
   import { defineComponent, onMounted, ref, unref, computed } from 'vue';
-
   import Cropper from 'cropperjs';
   import 'cropperjs/dist/cropper.css';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useThrottleFn } from '@vueuse/shared';
 
   type Options = Cropper.Options;
 
   const defaultOptions: Options = {
-    aspectRatio: 16 / 9,
+    aspectRatio: 1,
     zoomable: true,
     zoomOnTouch: true,
     zoomOnWheel: true,
@@ -43,40 +43,33 @@
     rotatable: true,
   };
 
-  export default defineComponent({
-    name: 'CropperImage',
-    props: {
-      src: {
-        type: String,
-        required: true,
-      },
-      alt: {
-        type: String,
-      },
-      height: {
-        type: [String, Number],
-        default: '360px',
-      },
-      crossorigin: {
-        type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
-        default: undefined,
-      },
-      imageStyle: {
-        type: Object as PropType<CSSProperties>,
-        default: () => ({}),
-      },
-      options: {
-        type: Object as PropType<Options>,
-        default: () => ({}),
-      },
+  const props = {
+    src: { type: String, required: true },
+    alt: { type: String },
+    circled: { type: Boolean, default: false },
+
+    realTimePreview: { type: Boolean, default: true },
+    height: { type: [String, Number], default: '360px' },
+    crossorigin: {
+      type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
+      default: undefined,
     },
-    emits: ['cropperedInfo'],
-    setup(props, ctx) {
-      const imgElRef = ref<ElRef<HTMLImageElement>>(null);
-      const cropper: any = ref<Nullable<Cropper>>(null);
+    imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
+    options: { type: Object as PropType<Options>, default: () => ({}) },
+  };
 
+  export default defineComponent({
+    name: 'CropperImage',
+    props,
+    emits: ['cropend', 'ready', 'cropendError'],
+    setup(props, { attrs, emit }) {
+      const imgElRef = ref<ElRef<HTMLImageElement>>();
+      const cropper = ref<Nullable<Cropper>>();
       const isReady = ref(false);
 
+      const { prefixCls } = useDesign('cropper-image');
+      const throttleRealTimeCroppered = useThrottleFn(realTimeCroppered, 80);
+
       const getImageStyle = computed((): CSSProperties => {
         return {
           height: props.height,
@@ -85,11 +78,22 @@
         };
       });
 
+      const getClass = computed(() => {
+        return [
+          prefixCls,
+          attrs.class,
+          {
+            [`${prefixCls}--circled`]: props.circled,
+          },
+        ];
+      });
+
       const getWrapperStyle = computed((): CSSProperties => {
-        const { height } = props;
-        return { height: `${height}`.replace(/px/, '') + 'px' };
+        return { height: `${props.height}`.replace(/px/, '') + 'px' };
       });
 
+      onMounted(init);
+
       async function init() {
         const imgEl = unref(imgElRef);
         if (!imgEl) {
@@ -99,29 +103,83 @@
           ...defaultOptions,
           ready: () => {
             isReady.value = true;
+            realTimeCroppered();
+            emit('ready', cropper.value);
+          },
+          crop() {
+            throttleRealTimeCroppered();
+          },
+          zoom() {
+            throttleRealTimeCroppered();
+          },
+          cropmove() {
+            throttleRealTimeCroppered();
           },
           ...props.options,
         });
       }
 
+      // Real-time display preview
+      function realTimeCroppered() {
+        props.realTimePreview && croppered();
+      }
+
       // event: return base64 and width and height information after cropping
-      const croppered = (): void => {
+      function croppered() {
+        if (!cropper.value) {
+          return;
+        }
         let imgInfo = cropper.value.getData();
-        cropper.value.getCroppedCanvas().toBlob((blob) => {
+        const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas();
+        canvas.toBlob((blob) => {
+          if (!blob) {
+            return;
+          }
           let fileReader: FileReader = new FileReader();
+          fileReader.readAsDataURL(blob);
           fileReader.onloadend = (e) => {
-            ctx.emit('cropperedInfo', {
+            emit('cropend', {
               imgBase64: e.target?.result ?? '',
               imgInfo,
             });
           };
-          fileReader.readAsDataURL(blob);
-        }, 'image/jpeg');
-      };
+          fileReader.onerror = () => {
+            emit('cropendError');
+          };
+        }, 'image/png');
+      }
 
-      onMounted(init);
+      // Get a circular picture canvas
+      function getRoundedCanvas() {
+        const sourceCanvas = cropper.value!.getCroppedCanvas();
+        const canvas = document.createElement('canvas');
+        const context = canvas.getContext('2d')!;
+        const width = sourceCanvas.width;
+        const height = sourceCanvas.height;
+        canvas.width = width;
+        canvas.height = height;
+        context.imageSmoothingEnabled = true;
+        context.drawImage(sourceCanvas, 0, 0, width, height);
+        context.globalCompositeOperation = 'destination-in';
+        context.beginPath();
+        context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
+        context.fill();
+        return canvas;
+      }
 
-      return { imgElRef, getWrapperStyle, getImageStyle, isReady, croppered };
+      return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered };
     },
   });
 </script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-cropper-image';
+
+  .@{prefix-cls} {
+    &--circled {
+      .cropper-view-box,
+      .cropper-face {
+        border-radius: 50%;
+      }
+    }
+  }
+</style>

+ 90 - 0
src/components/Cropper/src/CropperAvatar.vue

@@ -0,0 +1,90 @@
+<template>
+  <div :class="getClass" :style="getStyle">
+    <div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal">
+      <img :src="sourceValue" v-if="sourceValue" alt="avatar" />
+    </div>
+    <a-button :class="`${prefixCls}-upload-btn`" @click="openModal">
+      {{ t('component.cropper.selectImage') }}
+    </a-button>
+    <CopperModal @register="register" @uploadSuccess="handleUploadSuccess" :uploadApi="uploadApi" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, computed, CSSProperties, unref, ref } from 'vue';
+  import CopperModal from './CopperModal.vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useModal } from '/@/components/Modal';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { useI18n } from '/@/hooks/web/useI18n';
+
+  const props = {
+    src: { type: String, required: true },
+    width: { type: [String, Number], default: '200px' },
+    uploadApi: { type: Function as PropType<({ file: Blob, name: stirng }) => Promise<void>> },
+  };
+
+  export default defineComponent({
+    name: 'CropperAvatar',
+    components: { CopperModal },
+    props,
+    setup(props) {
+      const sourceValue = ref('');
+      const { prefixCls } = useDesign('cropper-avatar');
+      const [register, { openModal }] = useModal();
+      const { createMessage } = useMessage();
+      const { t } = useI18n();
+
+      const getClass = computed(() => [prefixCls]);
+
+      const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px');
+
+      const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
+
+      const getImageWrapperStyle = computed(
+        (): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) })
+      );
+
+      function handleUploadSuccess({ source }) {
+        sourceValue.value = source;
+        createMessage.success(t('component.cropper.uploadSuccess'));
+      }
+
+      return {
+        t,
+        prefixCls,
+        register,
+        openModal,
+        sourceValue,
+        getClass,
+        getImageWrapperStyle,
+        getStyle,
+        handleUploadSuccess,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  @prefix-cls: ~'@{namespace}-cropper-avatar';
+
+  .@{prefix-cls} {
+    display: inline-block;
+    text-align: center;
+
+    &-image-wrapper {
+      overflow: hidden;
+      cursor: pointer;
+      background: @component-background;
+      border: 1px solid @border-color-base;
+      border-radius: 50%;
+
+      img {
+        width: 100%;
+      }
+    }
+
+    &-upload-btn {
+      margin: 10px auto;
+    }
+  }
+</style>

+ 8 - 0
src/components/Cropper/src/typing.ts

@@ -0,0 +1,8 @@
+import type Cropper from 'cropperjs';
+
+export interface CropendResult {
+  imgBase64: string;
+  imgInfo: Cropper.Data;
+}
+
+export type { Cropper };

+ 6 - 0
src/locales/lang/en/component.ts

@@ -8,6 +8,12 @@ export default {
     normalText: 'Get SMS code',
     sendText: 'Reacquire in {0}s',
   },
+  cropper: {
+    selectImage: 'Select Image',
+    uploadSuccess: 'Uploaded success!',
+    modalTitle: 'Avatar upload',
+    okText: 'Confirm and upload',
+  },
   drawer: {
     loadingText: 'Loading...',
     cancelText: 'Close',

+ 6 - 0
src/locales/lang/zh_CN/component.ts

@@ -8,6 +8,12 @@ export default {
     normalText: '获取验证码',
     sendText: '{0}秒后重新获取',
   },
+  cropper: {
+    selectImage: '选择图片',
+    uploadSuccess: '上传成功',
+    modalTitle: '头像上传',
+    okText: '确认并上传',
+  },
   drawer: {
     loadingText: '加载中...',
     cancelText: '关闭',

+ 4 - 3
src/router/menus/modules/demo/comp.ts

@@ -6,6 +6,7 @@ const menu: MenuModule = {
   menu: {
     name: t('routes.demo.comp.comp'),
     path: '/comp',
+    tag: { dot: true },
     children: [
       {
         path: 'basic',
@@ -124,6 +125,9 @@ const menu: MenuModule = {
       {
         path: 'cropper',
         name: t('routes.demo.comp.cropperImage'),
+        tag: {
+          content: 'new',
+        },
       },
       {
         path: 'countTo',
@@ -192,9 +196,6 @@ const menu: MenuModule = {
           {
             path: 'json',
             name: t('routes.demo.editor.jsonEditor'),
-            tag: {
-              content: 'new',
-            },
           },
           {
             path: 'markdown',

+ 0 - 6
src/router/menus/modules/demo/feat.ts

@@ -6,9 +6,6 @@ const menu: MenuModule = {
   menu: {
     name: t('routes.demo.feat.feat'),
     path: '/feat',
-    tag: {
-      dot: true,
-    },
     children: [
       {
         path: 'icon',
@@ -21,9 +18,6 @@ const menu: MenuModule = {
       {
         name: t('routes.demo.feat.sessionTimeout'),
         path: 'session-timeout',
-        tag: {
-          content: 'new',
-        },
       },
       {
         path: 'tabs',

+ 50 - 26
src/views/demo/comp/cropper/index.vue

@@ -1,52 +1,76 @@
 <template>
-  <PageWrapper title="图片裁剪示例" contentBackground>
-    <div class="container">
-      <div class="cropper-container">
-        <CropperImage
-          ref="refCropper"
-          src="https://fengyuanchen.github.io/cropperjs/images/picture.jpg"
-          @cropperedInfo="cropperedInfo"
-          style="width: 40vw"
-        />
+  <PageWrapper title="图片裁剪示例" content="需要开启测试接口服务才能进行上传测试!">
+    <CollapseContainer title="头像裁剪">
+      <CropperAvatar :src="cropperImg" :uploadApi="uploadApi" />
+    </CollapseContainer>
+
+    <CollapseContainer title="矩形裁剪" class="my-4">
+      <div class="container p-4">
+        <div class="cropper-container mr-10">
+          <CropperImage ref="refCropper" :src="img" @cropend="handleCropend" style="width: 40vw" />
+        </div>
+        <img :src="cropperImg" class="croppered" v-if="cropperImg" />
+      </div>
+      <p v-if="cropperImg">裁剪后图片信息:{{ info }}</p>
+    </CollapseContainer>
+
+    <CollapseContainer title="圆形裁剪">
+      <div class="container p-4">
+        <div class="cropper-container mr-10">
+          <CropperImage
+            ref="refCropper"
+            :src="img"
+            @cropend="handleCircleCropend"
+            style="width: 40vw"
+            circled
+          />
+        </div>
+        <img :src="circleImg" class="croppered" v-if="circleImg" />
       </div>
-      <a-button type="primary" @click="onCropper"> 裁剪 </a-button>
-      <img :src="cropperImg" class="croppered" v-if="cropperImg" />
-    </div>
-    <p v-if="cropperImg">裁剪后图片信息:{{ info }}</p>
+      <p v-if="circleImg">裁剪后图片信息:{{ circleInfo }}</p>
+    </CollapseContainer>
   </PageWrapper>
 </template>
 <script lang="ts">
-  import { defineComponent, ref, unref } from 'vue';
+  import { defineComponent, ref } from 'vue';
   import { PageWrapper } from '/@/components/Page';
-  import { CropperImage } from '/@/components/Cropper';
+  import { CollapseContainer } from '/@/components/Container/index';
+  import { CropperImage, CropperAvatar } from '/@/components/Cropper';
+  import { uploadApi } from '/@/api/sys/upload';
   import img from '/@/assets/images/header.jpg';
-  import { templateRef } from '@vueuse/core';
 
   export default defineComponent({
     components: {
       PageWrapper,
       CropperImage,
+      CollapseContainer,
+      CropperAvatar,
     },
     setup() {
-      let info = ref('');
-      let cropperImg = ref('');
-      const refCropper = templateRef<HTMLElement | null>('refCropper', null);
+      const info = ref('');
+      const cropperImg = ref('');
+      const circleInfo = ref('');
+      const circleImg = ref('');
 
-      const onCropper = (): void => {
-        unref(refCropper).croppered();
-      };
-
-      function cropperedInfo({ imgBase64, imgInfo }) {
+      function handleCropend({ imgBase64, imgInfo }) {
         info.value = imgInfo;
         cropperImg.value = imgBase64;
       }
 
+      function handleCircleCropend({ imgBase64, imgInfo }) {
+        circleInfo.value = imgInfo;
+        circleImg.value = imgBase64;
+      }
+
       return {
         img,
         info,
+        circleInfo,
         cropperImg,
-        onCropper,
-        cropperedInfo,
+        circleImg,
+        handleCropend,
+        handleCircleCropend,
+        uploadApi,
       };
     },
   });

+ 0 - 1
src/views/demo/comp/upload/index.vue

@@ -21,7 +21,6 @@
   import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
   import { PageWrapper } from '/@/components/Page';
   import { Alert } from 'ant-design-vue';
-
   import { uploadApi } from '/@/api/sys/upload';
 
   const schemas: FormSchema[] = [

+ 1 - 2
src/views/demo/feat/copy/index.vue

@@ -14,11 +14,10 @@
   import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
   import { useMessage } from '/@/hooks/web/useMessage';
   import { PageWrapper } from '/@/components/Page';
-  import { Input } from 'ant-design-vue';
 
   export default defineComponent({
     name: 'Copy',
-    components: { CollapseContainer, PageWrapper, [Input.name]: Input },
+    components: { CollapseContainer, PageWrapper },
     setup() {
       const valueRef = ref('');
       const { createMessage } = useMessage();

+ 0 - 2
test/server/service/FileService.ts

@@ -10,8 +10,6 @@ export default class UserService {
     let fileReader, fileResource, writeStream;
 
     const fileFunc = function (file) {
-      console.log(file);
-
       fileReader = fs.createReadStream(file.path);
       fileResource = filePath + `/${file.name}`;
       console.log(fileResource);

+ 5 - 10
yarn.lock

@@ -2188,10 +2188,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
-ant-design-vue@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.1.2.tgz#2065d7e63199c0c584919458af57b6a0b597f677"
-  integrity sha512-gDG0wauGVt4LE63behrJaIcq4BB+dgs+dpj9jz17IgKr2MPYSEeKetU/x9Kk8d58cGonz4Ulncg7fBZJ7EljsQ==
+ant-design-vue@2.1.6:
+  version "2.1.6"
+  resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.1.6.tgz#c51cdc858e1b1b8b569f5435eb487f53a3f1745e"
+  integrity sha512-qICxb6Y4f7QuSuh/jbLhZA9SkUBnP9xYfy/E6yD7+1fg04aAzmRK8oLv8ETuGTrROVdSVeic9v/NS2BXEuuARg==
   dependencies:
     "@ant-design-vue/use" "^0.0.1-0"
     "@ant-design/icons-vue" "^6.0.0"
@@ -2201,7 +2201,7 @@ ant-design-vue@2.1.2:
     async-validator "^3.3.0"
     dom-align "^1.10.4"
     dom-scroll-into-view "^2.0.0"
-    is-mobile "^2.2.1"
+    lodash "^4.17.21"
     lodash-es "^4.17.15"
     moment "^2.27.0"
     omit.js "^2.0.0"
@@ -6079,11 +6079,6 @@ is-jpg@^2.0.0:
   resolved "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz#2e1997fa6e9166eaac0242daae443403e4ef1d97"
   integrity sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc=
 
-is-mobile@^2.2.1:
-  version "2.2.2"
-  resolved "https://registry.npmjs.org/is-mobile/-/is-mobile-2.2.2.tgz#f6c9c5d50ee01254ce05e739bdd835f1ed4e9954"
-  integrity sha512-wW/SXnYJkTjs++tVK5b6kVITZpAZPtUrt9SF80vvxGiF/Oywal+COk1jlRkiVq15RFNEQKQY31TkV24/1T5cVg==
-
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"