Skip to content

Commit

Permalink
feat: 头像裁剪组件 (#2570)
Browse files Browse the repository at this point in the history
* feat: 头像裁剪组件

* docs: update config.json

* feat: 增加bottom插槽,控制工具栏位置,抛出工具栏相关方法

* feat: 中间的文字提示可以通过props更改

* feat: 命名规范更改

* docs: update

* feat: 头像裁剪组件-taro版本

* feat: 冲突

* feat: 优化文档结构

* feat: 针对web绘制使用设备像素比提升图像质量

* feat: 适配支付宝小程序canvas 2d,要求基础库版本2.7.0或更高

* feat: vue版本优化获取元素宽高的方式

* test: update test

* test: update test avatarcropper toouch

* Revert "test: update test avatarcropper toouch"

This reverts commit 81f0843.

* test: update test avatarcropper toouch

* test: touch x

* test: update touch

* test: update test image

* test: update input set value

* Revert "test: update test image"

This reverts commit ef51d8e.
  • Loading branch information
yi-boide authored Oct 18, 2023
1 parent 90976e9 commit 359a355
Show file tree
Hide file tree
Showing 17 changed files with 1,953 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"unplugin-vue-markdown": "^0.24.3",
"vite": "^4.4.11",
"vitest": "^0.34.6",
"vitest-canvas-mock": "^0.3.3",
"vue": "^3.3.4",
"vue-tsc": "1.8.15"
},
Expand Down
7 changes: 7 additions & 0 deletions packages/nutui-taro-demo/project.private.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
"condition": {
"miniprogram": {
"list": [
{
"name": "AvatarCropper",
"pathName": "business/pages/avatarcropper/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "exhibition/pages/imagepreview/index",
"pathName": "exhibition/pages/imagepreview/index",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: 'AvatarCropper'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<div class="demo barrage-demo" :class="{ web: env === 'WEB' }">
<Header v-if="env === 'WEB'" />
<h2>基础用法</h2>
<nut-cell>
<nut-avatar-cropper @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
</nut-avatar-cropper>
</nut-cell>
<h2>裁剪区域toolbar插槽</h2>
<nut-cell>
<nut-avatar-cropper ref="avatarCropperRef" toolbar-position="top" edit-text="修改" @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
<template #toolbar>
<div class="toolbar">
<nut-button type="primary" @click="avatarCropperRef.cancel()">取消</nut-button>
<nut-button type="primary" @click="avatarCropperRef.reset()">重置</nut-button>
<nut-button type="primary" @click="avatarCropperRef.rotate()">旋转</nut-button>
<nut-button type="primary" @click="avatarCropperRef.confirm()">确认</nut-button>
</div>
</template>
</nut-avatar-cropper>
</nut-cell>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import Taro from '@tarojs/taro';
import Header from '../../../components/header.vue';
const env = Taro.getEnv();
const imageUrl = ref(
'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
);
const avatarCropperRef = ref();
const cutImage = (url: string) => {
imageUrl.value = url;
};
</script>

<style>
.toolbar {
display: flex;
justify-content: space-between;
}
</style>
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,17 @@
"type": "component",
"author": "ailululu",
"taro": true
},
{
"version": "1.0.0",
"name": "AvatarCropper",
"type": "component",
"tarodoc": true,
"show": true,
"cName": "头像裁剪",
"desc": "仿微信头像裁剪功能",
"taro": true,
"author": "Marvin"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`layout default slot 1`] = `
"<div class=\\"nut-avatar-cropper\\" data-edit-text=\\"编辑\\"><img src=\\"https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png\\"><input type=\\"file\\" accept=\\"image/*\\" class=\\"nut-avatar-cropper__input\\"></div>
<div class=\\"nut-cropper-popup\\" style=\\"display: none;\\"><canvas class=\\"nut-cropper-popup__canvas\\"></canvas>
<div class=\\"nut-cropper-popup__highlight\\">
<div class=\\"highlight\\" style=\\"width: 0px; height: 0px;\\"></div>
</div>
<div class=\\"nut-cropper-popup__toolbar bottom\\">
<div class=\\"flex-sb\\">
<div class=\\"nut-cropper-popup__toolbar-item\\">
<view class=\\"nut-button nut-button--danger nut-button--normal nut-button--round\\">
<view class=\\"nut-button__wrap\\">
<!--v-if-->
<!--v-if-->
<view class=\\"\\">取消</view>
</view>
</view>
</div>
<div class=\\"nut-cropper-popup__toolbar-item\\"><svg class=\\"nut-icon nut-icon-refresh2\\" style=\\"color: rgb(255, 255, 255);\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 1024 1024\\" role=\\"presentation\\">
<path d=\\"M771.938 315.077h199.55L958.358 469.99 758.81 364.964c-13.128-7.877-18.38-23.63-10.502-36.759 2.625-7.877 13.128-13.128 23.63-13.128zm-535.63 393.846H44.636L57.764 554.01l191.672 105.026c13.128 7.877 18.38 23.63 10.502 36.759-5.25 7.877-15.753 13.128-23.63 13.128zM509.374 1024C257.313 1024 44.636 845.456 5.251 596.02 0 575.016 15.754 556.637 36.76 551.386c21.005-2.626 42.01 10.502 44.636 31.507 34.133 210.052 215.302 362.339 427.98 362.339 191.671 0 362.338-128.657 417.476-312.452 5.252-21.005 28.882-34.133 49.887-26.256 21.006 5.251 34.134 28.882 26.257 49.887C937.354 871.713 735.179 1024 509.375 1024zm467.364-551.385c-18.379 0-36.759-13.128-39.384-34.133C903.22 231.056 722.05 78.77 509.374 78.77c-191.671 0-362.338 128.657-414.85 312.452-5.252 21.005-28.883 34.133-49.888 26.256-21.005-5.251-34.133-28.882-26.257-49.887C81.395 152.287 283.57 0 509.374 0c252.062 0 464.739 178.544 504.123 427.98 2.626 21.005-10.502 42.01-31.507 44.635h-5.252z\\" fill=\\"currentColor\\" fill-opacity=\\"0.9\\"></path>
</svg></div>
<div class=\\"nut-cropper-popup__toolbar-item\\"><svg class=\\"nut-icon nut-icon-retweet\\" style=\\"color: rgb(255, 255, 255);\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 1024 1024\\" role=\\"presentation\\">
<path d=\\"M136 552h63.6c4.4 0 8-3.6 8-8V288.7h528.6v72.6c0 1.9.6 3.7 1.8 5.2 2.9 3.6 8.1 4.3 11.7 1.4L893 255.4c4.3-5 3.6-10.3 0-13.2L749.7 129.8c-1.5-1.2-3.3-1.8-5.2-1.8-4.6 0-8.4 3.8-8.4 8.4V209H199.7c-39.5 0-71.7 32.2-71.7 71.8V544c0 4.4 3.6 8 8 8zm752-80h-63.6c-4.4 0-8 3.6-8 8v255.3H287.8v-72.6c0-1.9-.6-3.7-1.8-5.2-2.9-3.6-8.1-4.3-11.7-1.4L131 768.6c-4.3 5-3.6 10.3 0 13.2l143.3 112.4c1.5 1.2 3.3 1.8 5.2 1.8 4.6 0 8.4-3.8 8.4-8.4V815h536.6c39.5 0 71.7-32.2 71.7-71.8V480c-.2-4.4-3.8-8-8.2-8z\\" fill=\\"currentColor\\" fill-opacity=\\"0.9\\"></path>
</svg></div>
<div class=\\"nut-cropper-popup__toolbar-item\\">
<view class=\\"nut-button nut-button--success nut-button--normal nut-button--round\\">
<view class=\\"nut-button__wrap\\">
<!--v-if-->
<!--v-if-->
<view class=\\"\\">确定</view>
</view>
</view>
</div>
</div>
</div>
</div>"
`;
83 changes: 83 additions & 0 deletions src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'vitest-canvas-mock';
import { mount } from '@vue/test-utils';
import AvatarCropper from '../index.vue';
import { sleep, trigger, triggerDrag } from '@/packages/utils/unit';
import { h } from 'vue';

const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', {
type: 'image/jpg'
});

test('layout default slot', () => {
const wrapper = mount(AvatarCropper, {
slots: {
default: h('img', {
src: 'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
})
}
});

expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find('.nut-avatar-cropper').html()).toContain(
'<img src="https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png">'
);
});

test('should render base cutAvatar and type', async () => {
const wrapper = mount(AvatarCropper);
const up_load = wrapper.find('.nut-avatar-cropper');
expect(up_load.exists()).toBe(true);
const up_load1 = wrapper.find('.nut-avatar-cropper__input');
expect(up_load1.attributes().type).toBe('file');
});

test('AvatarCropper: Select the image to open the crop window', async () => {
const wrapper = mount(AvatarCropper);
const input: any = wrapper.find<HTMLInputElement>('.nut-avatar-cropper__input');
expect(input.exists()).toBe(true);
const smallFile = new File([new ArrayBuffer(100)], 'small.jpg');
Object.defineProperty(input.element, 'files', {
get: vi.fn().mockReturnValue([mockFile, smallFile])
});
expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;');
await input.trigger('change');
await sleep();
expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', '');
const canvas = wrapper.find('.nut-cropper-popup__canvas');
expect(canvas.exists()).toBe(true);

const track = wrapper.find('.nut-cropper-popup__highlight');

trigger(track, 'touchstart', 0, 0, { x: 0, y: 0 });
trigger(track, 'touchmove', 20, 20, { x: 40, y: 60 });
trigger(track, 'touchend', 20, 100, { x: 40, y: 60 });

triggerDrag(track, 50, 60);
const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item');
expect(toolbar.length).toBe(4);

const cancel = toolbar[0];
cancel.trigger('click');
expect(wrapper.emitted('cancel')).toBeTruthy();
expect(input.element.value).toBe('');
await sleep();
expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;');

const reset = toolbar[1];
reset.trigger('click');
expect(wrapper.vm.angle).toBe(0);

const rotate = toolbar[2];
rotate.trigger('click');
expect(wrapper.vm.angle).toBe(90);
triggerDrag(track, 1000, 2000);
rotate.trigger('click');
expect(wrapper.vm.angle).toBe(180);
rotate.trigger('click');
rotate.trigger('click');
expect(wrapper.vm.angle).toBe(0);

const confirm = toolbar[3];
confirm.trigger('click');
expect(wrapper.emitted('confirm')).toBeTruthy();
});
46 changes: 46 additions & 0 deletions src/packages/__VUE/avatarcropper/demo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template>
<div class="demo full">
<h2>基础用法</h2>
<nut-cell>
<nut-avatar-cropper @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
</nut-avatar-cropper>
</nut-cell>
<h2>裁剪区域toolbar插槽</h2>
<nut-cell>
<nut-avatar-cropper ref="avatarCropperRef" toolbar-position="top" edit-text="修改" @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
<template #toolbar>
<div class="toolbar">
<nut-button type="primary" @click="avatarCropperRef.cancel()">取消</nut-button>
<nut-button type="primary" @click="avatarCropperRef.reset()">重置</nut-button>
<nut-button type="primary" @click="avatarCropperRef.rotate()">旋转</nut-button>
<nut-button type="primary" @click="avatarCropperRef.confirm()">确认</nut-button>
</div>
</template>
</nut-avatar-cropper>
</nut-cell>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
const imageUrl = ref(
'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
);
const avatarCropperRef = ref();
const cutImage = (url: string) => {
imageUrl.value = url;
};
</script>

<style>
.toolbar {
display: flex;
justify-content: space-between;
}
</style>
Loading

0 comments on commit 359a355

Please sign in to comment.