gaoluyang
2026-06-24 712aa51536236d43e87273e4ce45ac5691dffad8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<script lang="ts" setup>
import type { CSSProperties } from 'vue';
 
import type { CropperAvatarProps } from './typing';
 
import { computed, ref, unref, watch, watchEffect } from 'vue';
 
import { useVbenModal } from '..\..\packages\effects\common-ui\src';
import { IconifyIcon } from '..\..\packages\icons\src';
import { $t } from '..\..\packages\locales\src';
 
import { Button, message } from 'ant-design-vue';
 
import cropperModal from './cropper-modal.vue';
 
defineOptions({ name: 'CropperAvatar' });
 
const props = withDefaults(defineProps<CropperAvatarProps>(), {
  width: 200,
  value: '',
  showBtn: true,
  btnProps: () => ({}),
  btnText: '',
  uploadApi: () => Promise.resolve(),
  size: 5,
});
 
const emit = defineEmits(['update:value', 'change']);
 
const sourceValue = ref(props.value || '');
const [CropperModal, modalApi] = useVbenModal({
  connectedComponent: cropperModal,
});
 
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
 
const getIconWidth = computed(
  () => `${Number.parseInt(`${props.width}`.replace(/px/, '')) / 2}px`,
);
 
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
 
const getImageWrapperStyle = computed(
  (): CSSProperties => ({ height: unref(getWidth), width: unref(getWidth) }),
);
 
watchEffect(() => {
  sourceValue.value = props.value || '';
});
 
watch(
  () => sourceValue.value,
  (v: string) => {
    emit('update:value', v);
  },
);
 
function handleUploadSuccess({ data, source }: any) {
  sourceValue.value = source;
  emit('change', { data, source });
  message.success($t('ui.cropper.uploadSuccess'));
}
 
const closeModal = () => modalApi.close();
const openModal = () => modalApi.open();
 
defineExpose({
  closeModal,
  openModal,
});
</script>
 
<template>
  <!-- 头像容器 -->
  <div class="inline-block text-center" :style="getStyle">
    <!-- 图片包装器 -->
    <div
      class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-card"
      :style="getImageWrapperStyle"
      @click="openModal"
    >
      <!-- 遮罩层 -->
      <div
        class="duration-400 absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-40 opacity-0 transition-opacity group-hover:opacity-100"
        :style="getImageWrapperStyle"
      >
        <IconifyIcon
          icon="lucide:cloud-upload"
          class="m-auto text-gray-400"
          :style="{
            ...getImageWrapperStyle,
            width: getIconWidth,
            height: getIconWidth,
            lineHeight: getIconWidth,
          }"
        />
      </div>
      <!-- 头像图片 -->
      <img
        v-if="sourceValue"
        :src="sourceValue"
        alt="avatar"
        class="h-full w-full object-cover"
      />
    </div>
    <!-- 上传按钮 -->
    <Button
      v-if="showBtn"
      class="mx-auto mt-2"
      @click="openModal"
      v-bind="btnProps"
    >
      {{ btnText ? btnText : $t('ui.cropper.selectImage') }}
    </Button>
 
    <CropperModal
      :size="size"
      :src="sourceValue"
      :upload-api="uploadApi"
      @upload-success="handleUploadSuccess"
    />
  </div>
</template>